4.11. 流水线
高级同步原语中介绍的流水线是一种对工作进行分阶段处理并协调多缓冲区生产者-消费者模式的机制,通常用于使计算与异步数据复制重叠。
本节详细介绍如何主要通过cuda::pipeline API 使用流水线(并在适用时提供指向原语的参考)。
4.11.1. 初始化
可以在不同的线程作用域创建cuda::pipeline。对于cuda::thread_scope_thread以外的作用域,需要一个cuda::pipeline_shared_state<scope, count>对象来协调参与的线程。此状态封装了有限的资源,使流水线能够处理最多count个并发阶段。
// Create a pipeline at thread scope
constexpr auto scope = cuda::thread_scope_thread;
cuda::pipeline<scope> pipeline = cuda::make_pipeline();
// Create a pipeline at block scope
constexpr auto scope = cuda::thread_scope_block;
constexpr auto stages_count = 2;
__shared__ cuda::pipeline_shared_state<scope, stages_count> shared_state;
auto pipeline = cuda::make_pipeline(group, &shared_state);
流水线可以是统一式的,也可以是分区式的。在统一流水线中,所有参与线程既是生产者,也是消费者。在分区流水线中,每个参与线程要么是生产者,要么是消费者,其角色在流水线对象的生存期内不能改变。线程局部流水线不能分区。要创建分区流水线,我们需要向cuda::make_pipeline()提供生产者的数量或线程的角色。
// Create a partitioned pipeline at block scope where only thread 0 is a producer
constexpr auto scope = cuda::thread_scope_block;
constexpr auto stages_count = 2;
__shared__ cuda::pipeline_shared_state<scope, stages_count> shared_state;
auto thread_role = (group.thread_rank() == 0) ? cuda::pipeline_role::producer : cuda::pipeline_role::consumer;
auto pipeline = cuda::make_pipeline(group, &shared_state, thread_role);
为支持分区,共享的cuda::pipeline会引入额外开销,包括为每个阶段使用一组共享内存屏障进行同步。即使流水线是统一式的、原本可以改用__syncthreads(),也会使用这些屏障。因此,应尽可能优先使用线程局部流水线,以避免这些开销。
4.11.2. 提交工作
向流水线阶段提交工作涉及以下步骤:
由一组生产者线程使用
pipeline.producer_acquire(),集体获取流水线的头部。向流水线头部提交异步操作,例如
memcpy_async。使用
pipeline.producer_commit(),集体提交(推进)流水线头部。
如果所有资源均在使用中,pipeline.producer_acquire()会阻塞生产者线程,直到消费者线程释放下一个流水线阶段的资源。
4.11.3. 消费工作
从先前已提交的阶段中消费工作涉及以下步骤:
由一组消费者线程集体等待该阶段完成,例如使用
pipeline.consumer_wait()等待尾部(最早的)阶段。使用
pipeline.consumer_release(),集体释放该阶段。
使用cuda::pipeline<cuda:thread_scope_thread>时,还可以使用cuda::pipeline_consumer_wait_prior<N>()友元函数,等待除最后 N 个阶段以外的所有阶段完成,这类似于原语 API 中的__pipeline_wait_prior(N)。
4.11.4. 线程束纠缠
同一线程束中的 CUDA 线程共享流水线机制。这种共享会导致线程束内已提交操作的序列相互纠缠,在某些情况下可能影响性能。
提交。提交操作会被合并,使得对于所有调用提交操作的汇聚线程,流水线的序列号仅递增一次,且这些线程提交的操作会被合为一个批次。如果线程束完全汇聚,序列号递增 1,所有已提交的操作都将作为同一批次放入流水线的同一阶段;如果线程束完全分歧,序列号递增 32,所有已提交的操作都将分散到不同阶段。
设PB为线程束共享流水线的实际操作序列。
PB = {BP0, BP1, BP2, …, BPL}设TB为某个线程感知到的操作序列,仿佛序列号仅因该线程调用提交操作而递增。
TB = {BT0, BT1, BT2, …, BTL}
pipeline::producer_commit()的返回值来自该线程感知到的批次序列。
线程感知序列中的索引始终对应于实际线程束共享序列中相等或更大的索引。仅当所有提交操作均由完全汇聚的线程调用时,这两个序列才相等。
BTn ≡ BPm,其中n <= m
例如,当线程束完全分歧时:
线程束共享流水线的实际序列将为:
PB = {0, 1, 2, 3, ..., 31}(PL=31)。此线程束中每个线程感知到的序列将为:
线程 0:
TB = {0}(TL=0)线程 1:
TB = {0}(TL=0)…线程 31:
TB = {0}(TL=0)
等待。CUDA 线程调用pipeline::consumer_wait()或pipeline_consumer_wait_prior<N>(),以等待感知序列TB中的批次完成。请注意,pipeline::consumer_wait()等价于pipeline_consumer_wait_prior<N>(),其中N = PL。
等待先前批次的变体至少会等待实际序列中截至并包括PL-N的批次。由于TL <= PL,等待截至并包括PL-N的批次也包含了等待批次TL-N。因此,当TL < PL时,线程会无意中等待额外的、更新的批次。在上述线程束完全分歧的极端示例中,每个线程都可能等待全部 32 个批次。
注意
建议由汇聚的线程调用提交操作,使线程感知到的批次序列与实际序列保持一致,从而避免过度等待。
当这些操作之前的代码使线程发生分歧时,应先通过__syncwarp使线程束重新汇聚,再调用提交操作。
4.11.5. 提前退出
当参与流水线的线程必须提前退出时,该线程必须在退出之前使用cuda::pipeline::quit()显式停止参与。其余参与线程可以正常继续执行后续操作。
4.11.6. 跟踪异步内存操作
以下示例演示如何通过异步内存复制,集体将数据从全局内存复制到共享内存,并使用流水线跟踪复制操作。每个线程使用自己的流水线独立提交内存复制,然后等待复制完成并消费数据。有关异步数据复制的更多详细信息,请参阅第 3.2.5 节。
#include <cuda/pipeline>
__global__ void example_kernel(const float *in)
{
constexpr int block_size = 128;
__shared__ __align__(sizeof(float)) float buffer[4 * block_size];
// Create a unified pipeline per thread
cuda::pipeline<cuda::thread_scope_thread> pipeline = cuda::make_pipeline();
// First stage of memory copies
pipeline.producer_acquire();
// Every thread fetches one element of the first block
cuda::memcpy_async(buffer, in, sizeof(float), pipeline);
pipeline.producer_commit();
// Second stage of memory copies
pipeline.producer_acquire();
// Every thread fetches one element of the second and third block
cuda::memcpy_async(buffer + block_size, in + block_size, sizeof(float), pipeline);
cuda::memcpy_async(buffer + 2 * block_size, in + 2 * block_size, sizeof(float), pipeline);
pipeline.producer_commit();
// Third stage of memory copies
pipeline.producer_acquire();
// Every thread fetches one element of the last block
cuda::memcpy_async(buffer + 3 * block_size, in + 3 * block_size, sizeof(float), pipeline);
pipeline.producer_commit();
// Wait for the oldest stage (waits for first stage)
pipeline.consumer_wait();
pipeline.consumer_release();
// __syncthreads();
// Use data from the first stage
// Wait for the oldest stage (waits for second stage)
pipeline.consumer_wait();
pipeline.consumer_release();
// __syncthreads();
// Use data from the second stage
// Wait for the oldest stage (waits for third stage)
pipeline.consumer_wait();
pipeline.consumer_release();
// __syncthreads();
// Use data from the third stage
}
|
#include <cuda_pipeline.h>
__global__ void example_kernel(const float *in)
{
constexpr int block_size = 128;
__shared__ __align__(sizeof(float)) float buffer[4 * block_size];
// First batch of memory copies
// Every thread fetches one element of the first block
__pipeline_memcpy_async(buffer, in, sizeof(float));
__pipeline_commit();
// Second batch of memory copies
// Every thread fetches one element of the second and third block
__pipeline_memcpy_async(buffer + block_size, in + block_size, sizeof(float));
__pipeline_memcpy_async(buffer + 2 * block_size, in + 2 * block_size, sizeof(float));
__pipeline_commit();
// Third batch of memory copies
// Every thread fetches one element of the last block
__pipeline_memcpy_async(buffer + 3 * block_size, in + 3 * block_size, sizeof(float));
__pipeline_commit();
// Wait for all except the last two batches of memory copies (waits for first batch)
__pipeline_wait_prior(2);
// __syncthreads();
// Use data from the first batch
// Wait for all except the last batch of memory copies (waits for second batch)
__pipeline_wait_prior(1);
// __syncthreads();
// Use data from the second batch
// Wait for all batches of memory copies (waits for third batch)
__pipeline_wait_prior(0);
// __syncthreads();
// Use data from the last batch
}
|
4.11.7. 使用流水线的生产者-消费者模式
在第 4.10.7 节中,我们展示了如何在空间上划分线程块,以使用异步屏障实现生产者-消费者模式。借助cuda::pipeline,可以使用单个分区流水线来简化这一过程,为每个数据缓冲区配置一个阶段,而不是为每个缓冲区配置两个异步屏障。
#include <cuda/pipeline>
#include <cooperative_groups.h>
#pragma nv_diag_suppress static_var_with_dynamic_init
using pipeline = cuda::pipeline<cuda::thread_scope_block>;
__device__ void produce(pipeline &pipe, int num_stages, int stage, int num_batches, int batch, float *buffer, int buffer_len, float *in, int N)
{
if (batch < num_batches)
{
pipe.producer_acquire();
/* copy data from in(batch) to buffer(stage) using asynchronous memory copies */
pipe.producer_commit();
}
}
__device__ void consume(pipeline &pipe, int num_stages, int stage, int num_batches, int batch, float *buffer, int buffer_len, float *out, int N)
{
pipe.consumer_wait();
/* consume buffer(stage) and update out(batch) */
pipe.consumer_release();
}
__global__ void producer_consumer_pattern(float *in, float *out, int N, int buffer_len)
{
auto block = cooperative_groups::this_thread_block();
/* Shared memory buffer declared below is of size 2 * buffer_len
so that we can alternatively work between two buffers.
buffer_0 = buffer and buffer_1 = buffer + buffer_len */
__shared__ extern float buffer[];
const int num_batches = N / buffer_len;
// Create a partitioned pipeline with 2 stages where half the threads are producers and the other half are consumers.
constexpr auto scope = cuda::thread_scope_block;
constexpr int num_stages = 2;
cuda::std::size_t producer_count = block.size() / 2;
__shared__ cuda::pipeline_shared_state<scope, num_stages> shared_state;
pipeline pipe = cuda::make_pipeline(block, &shared_state, producer_count);
// Fill the pipeline
if (block.thread_rank() < producer_count)
{
for (int s = 0; s < num_stages; ++s)
{
produce(pipe, num_stages, s, num_batches, s, buffer, buffer_len, in, N);
}
}
// Process the batches
int stage = 0;
for (size_t b = 0; b < num_batches; ++b)
{
if (block.thread_rank() < producer_count)
{
// Prefetch the next batch
produce(pipe, num_stages, stage, num_batches, b + num_stages, buffer, buffer_len, in, N);
}
else
{
// Consume the oldest batch
consume(pipe, num_stages, stage, num_batches, b, buffer, buffer_len, out, N);
}
stage = (stage + 1) % num_stages;
}
}
|
在此示例中,我们使用线程块中一半的线程作为生产者,另一半作为消费者。第一步,我们需要创建一个cuda::pipeline对象。由于我们希望部分线程为生产者,部分线程为消费者,因此需要使用带有cuda::thread_scope_block的分区流水线。分区流水线需要一个cuda::pipeline_shared_state来协调参与线程。我们在线程块作用域内为一个 2 阶段流水线初始化状态,然后调用cuda::make_pipeline()。接下来,生产者线程通过提交从in到buffer的异步复制来填满流水线。此时所有数据复制都在进行中。最后,在主循环中,我们遍历所有数据批次,并根据线程是生产者还是消费者,或者为未来的批次提交另一次异步复制,或者消费当前批次。