cuda编程笔记(40)--Pipelines(流水线)

流水线是在"高级同步原语"中引入的一种机制,用于阶段性地管理工作,并协调多缓冲区(Multi-buffer)的生产者-消费者模式。它通常被用来实现计算与异步数据拷贝的重叠(Overlap) 。 本节详细介绍了主要通过 cuda::pipeline API 使用流水线的方法。

初始化 (Initialization)

cuda::pipeline 可以在不同的线程作用域(Scope)下创建。

  • 对于非线程私有(thread_scope_thread)的作用域,需要一个 cuda::pipeline_shared_state<scope, count> 对象来协调参与的线程。

  • 这种"共享状态"封装了有限的资源,允许流水线同时处理最多 count 个并发阶段(Stages)。

流水线分为统一型(Unified)分区型(Partitioned)

  • 统一型流水线:所有参与线程既是生产者又是消费者。

  • 分区型流水线:每个线程的角色在流水线生命周期内固定,要么是生产者,要么是消费者。

  • 注意 :线程私有流水线不能分区。创建分区流水线时,需向 cuda::make_pipeline() 提供生产者数量或当前线程的角色。

为了支持分区,共享类型的 cuda::pipeline 会产生额外开销(例如每阶段使用一组共享内存屏障进行同步)。即使在可以使用 __syncthreads() 的统一型流水线中,这些开销依然存在。因此,在可能的情况下,优先使用线程私有流水线以规避开销。

如果说 cuda::barrier 是一个灵活的"信号灯",那么 cuda::pipeline 就是一整套自动化的生产线管理系统

  1. Stage(阶段)的概念 : 在高性能计算中,我们希望在 计算当前批次数据 (N) 的同时,硬件已经在异步搬运 下一批次 (N+1) 甚至 下下批次 (N+2) 的数据。

    1. 双缓冲 (Double Buffering)stages_count = 2。一个阶段在被消费者计算,另一个阶段在被生产者填充。

    2. 三缓冲 (Triple Buffering)stages_count = 3。这种设置能更激进地掩盖长距离访存延迟,防止计算单元因为等数据而停顿(Stall)。

  2. 角色分配(Unified vs Partitioned)

    • 统一型(Unified):相当于"全员搬运,全员加工"。大家一起搬一段,搬完一起算。

    • 分区型(Partitioned) :这就是我们前面展示的 Warp 特化。明确谁是专门跑腿的(Producer),谁是专门干活的(Consumer)。

  3. Shared State(共享状态) : 这是 Pipeline 的核心"账本",通常存在 __shared__ 内存里。它记录了当前有多少个 Stage 正在搬运,多少个已经就绪。

线程私有流水线 (单线程异步)

cpp 复制代码
// 作用域仅限于当前线程
constexpr auto scope = cuda::thread_scope_thread;
// 创建流水线实例,无需共享状态
cuda::pipeline<scope> pipeline = cuda::make_pipeline();

统一型流水线 (全块参与)

适用于整个 Thread Block 一起搬运,一起计算。

cpp 复制代码
constexpr auto scope = cuda::thread_scope_block;
constexpr auto stages_count = 2; // 双缓冲

// 在共享内存中定义流水线的"账本"
__shared__ cuda::pipeline_shared_state<scope, stages_count> shared_state;

auto group = cooperative_groups::this_thread_block();
// 所有线程都参与,且既搬运也计算
auto pipeline = cuda::make_pipeline(group, &shared_state);

分区型流水线 (Warp 特化专用)

这就是实现生产者-消费者模式最优雅的代码写法。

cpp 复制代码
constexpr auto scope = cuda::thread_scope_block;
constexpr auto stages_count = 2;

__shared__ cuda::pipeline_shared_state<scope, stages_count> shared_state;
auto group = cooperative_groups::this_thread_block();

// 角色划分:0号线程是生产者,其他是消费者
// 在实际项目中,通常是 thread_rank() < 32 为生产者
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);

你可能会问:"上一节刚才手写 4 个屏障不是也能实现双缓冲吗?"

  1. 管理复杂度 :当 Stage 数量增加到 3 个甚至更多(多缓冲)时,手写 Barrier 的状态转移逻辑会极其复杂且易错。Pipeline 封装了这些 acquire/release 逻辑。

  2. 资源抽象pipeline_shared_state 自动处理了底层硬件资源的分配。

  3. 语义清晰 :代码中直接体现 producerconsumer 角色,可读性极强,且编译器能针对特定的角色进行指令优化。

pipeline_shared_state

cpp 复制代码
template <thread_scope _Scope, uint8_t _Stages_count>
class pipeline_shared_state;

指定作用域和并发阶段

pipeline_role

cpp 复制代码
enum class pipeline_role
{
  producer,
  consumer
};

角色分为生产者和消费者

make_pipeline

它类似于工厂函数,创建pipeline对象。

cpp 复制代码
  friend _LIBCUDACXX_INLINE_VISIBILITY inline pipeline<thread_scope_thread> make_pipeline();
  
  template <class _Group, thread_scope _Pipeline_scope, uint8_t _Pipeline_stages_count>
  friend _LIBCUDACXX_INLINE_VISIBILITY pipeline<_Pipeline_scope>
  make_pipeline(const _Group& __group, pipeline_shared_state<_Pipeline_scope, _Pipeline_stages_count>* __shared_state);

  template <class _Group, thread_scope _Pipeline_scope, uint8_t _Pipeline_stages_count>
  friend _LIBCUDACXX_INLINE_VISIBILITY pipeline<_Pipeline_scope>
  make_pipeline(const _Group& __group,
                pipeline_shared_state<_Pipeline_scope, _Pipeline_stages_count>* __shared_state,
                size_t __producer_count);

  template <class _Group, thread_scope _Pipeline_scope, uint8_t _Pipeline_stages_count>
  friend _LIBCUDACXX_INLINE_VISIBILITY pipeline<_Pipeline_scope>
  make_pipeline(const _Group& __group,
                pipeline_shared_state<_Pipeline_scope, _Pipeline_stages_count>* __shared_state,
                pipeline_role __role);

有四种make_pipeline的创建方式。和上面介绍的基本一致。

就是多了一个可以传递size_t __producer_count的版本。

Pipeline 会按照线程在当前 Group(通常是 Thread Block)中的 Rank(排名) 自动划分:

  • Rank < producer_count :自动被赋予 producer 角色。

  • Rank >= producer_count :自动被赋予 consumer 角色。

如果传递的是pipeline_role,系统会自动统计不同身份角色的个数。

提交任务 (Submitting Work)

将任务提交到流水线阶段(Stage)涉及以下步骤:

  1. 获取阶段(Acquire) :由一组生产者线程通过调用 pipeline.producer_acquire() 集合性地获取流水线的"头部"(Head)。

  2. 提交操作(Submit) :向流水线头部提交异步操作(例如使用 memcpy_async 搬运数据)。

  3. 提交阶段(Commit) :通过调用 pipeline.producer_commit() 集合性地提交(并推进)流水线头部。

阻塞逻辑 :如果流水线的所有资源(即所有 Stage)都在使用中,pipeline.producer_acquire() 会阻塞生产者线程,直到消费者线程释放下一个流水线阶段的资源。

消耗任务 (Consuming Work)

从之前已提交(Committed)的阶段中消耗任务涉及以下步骤:

  1. 等待完成(Wait) :由一组消费者线程通过调用 pipeline.consumer_wait() 集合性地等待阶段完成。该操作通常等待的是流水线的"尾部"(Tail),即最早提交但尚未处理的阶段。

  2. 释放阶段(Release) :通过调用 pipeline.consumer_release() 集合性地释放该阶段。

对于线程私有作用域的流水线(cuda::pipeline<cuda::thread_scope_thread>),还可以使用 cuda::pipeline_consumer_wait_prior<N>() 友元函数。它允许线程等待除最后 N 个阶段以外的所有阶段完成,这与原语 API 中的 __pipeline_wait_prior(N) 功能类似。

Warp 缠绕 (Warp Entanglement)

在 GPU 硬件层面上,异步拷贝并不是"发出去就不管了"。硬件必须有一个地方记录:"当前有几批货正在路上?每一批分别运到了哪里?"

这个记录数据的地方叫做 硬件追踪槽位(Hardware Tracking Slots)

  • 物理实现 :这些槽位是实打实的硬件寄存器(寄存器资源极其昂贵)。当你定义 stages_count = 2 时,硬件就会分配相应数量的槽位来追踪这两批数据的状态。

  • 状态更新 :状态确实是在更新(比如从"传输中"变为"已完成"),但槽位编号是固定的。

  • 资源占满 :如果槽位只有 2 个,而你发出了第 3 个提交指令(commit),硬件就没地方记录这第 3 批货的进度了。为了保证数据安全,硬件只能强制让第 3 个提交指令挂起(Stall) ,直到前两个槽位中的一个被 release

为什么 producer_commit 会在全员到齐前就消耗 Stage?

这是最容易产生误解的地方。我们要区分 "软件层面的集体契约""硬件层面的指令发射"

  • 软件层面的契约(Collective)cuda::pipeline 的 API 要求 producer_count 个人到齐。这是一种逻辑保障,确保 64 个人都干完活了再通知消费者。

  • 硬件层面的行为(Instruction Issue) : 硬件(GPU)是按照 Warp 为单位执行指令的。它并不真正理解你在软件里设定的"64 人契约"。

    • 只要有一个线程发出了 cp.async.commit_group 指令,硬件就会立刻反应:"有人提交了一批任务,我得给它分配一个 追踪槽位(Stage)。"

这就是问题的根源: 如果 Warp 内发生了分歧,32 个线程分 32 次发射了 commit 指令,即使你的逻辑目标是"大家一起搬一波数据",但硬件会诚实地为每一次指令发射分配一个独立的追踪槽位

假设你设定了 stages_count = 2,而你的 Warp 发生了分歧:

  1. 线程 0 发射 commit :硬件占用 槽位 0 (Stage 0)。

  2. 线程 1 发射 commit :硬件占用 槽位 1 (Stage 1)。

  3. 线程 2 发射 commit硬件发现没有空闲槽位了!

    • 此时,线程 2 会在硬件层面上被阻塞(Stall)

    • 死锁循环开启

      • 线程 2 没法完成 commit,导致整个 Warp 无法完成软件设定的"64 人签到"。

      • 因为 64 人没签完,consumer_wait() 永远不会放行。

      • 消费者不放行,就不会执行 consumer_release()

      • 不执行 release,硬件槽位 0 和 1 就永远不会释放。

      • 槽位不释放,线程 2 就永远卡在 commit 那里。

提前退出 (Early Exit)

当参与流水线的线程必须提前退出时,该线程在退出前必须使用 cuda::pipeline::quit() 显式地退出参与状态。这样,剩下的参与线程才能正常进行后续的流水线操作。

正如我们之前讨论的,cuda::pipeline 是一个 集体性(Collective) 的原语。这意味着:

  1. 计数器依赖 :流水线内部维护了参与线程的总数。每一次 producer_acquireconsumer_wait 都期望收到全员的"信号"。

  2. 死锁风险 :如果一个线程在循环中途直接 return 或跳出,流水线会认为这个线程"还在路上",从而导致其他线程在下一次同步点(如 commitwait)时永久等待这个失踪的线程。

  3. 动态调整 :调用 quit() 相当于正式向流水线递交"辞职信"。流水线接收到后,会自动将"预期的参与线程总数"减去对应的数量,从而让剩下的线程能够继续完成剩余的 Stage。

追踪异步内存操作

以下示例演示了如何使用流水线结合异步内存拷贝,将数据从全局内存集体拷贝到共享内存,并使用流水线追踪拷贝操作。在这个例子中,每个线程使用自己的流水线独立提交内存拷贝,然后等待拷贝完成并消耗数据。

代码连续提交了三个阶段(Stage 1, Stage 2, Stage 3)。

  • Stage 1: 搬运第 1 个 block。

  • Stage 2: 搬运第 2 和第 3 个 block(演示了一个 Stage 可以包含多次拷贝)。

  • Stage 3: 搬运第 4 个 block。

cpp 复制代码
#include <cuda/pipeline>

__global__ void example_kernel(const float *in) {
    constexpr int block_size = 128;
    // 共享内存总大小为 4 个 block
    __shared__ __align__(sizeof(float)) float buffer[4 * block_size];

    // 创建线程私有流水线
    // 每个线程都有一个独立的"账本"来记录自己的搬运进度
    cuda::pipeline<cuda::thread_scope_thread> pipeline = cuda::make_pipeline();

    // --- 第一阶段:提交 ---
    pipeline.producer_acquire();
    // 每个线程负责搬运自己索引位置的一个 float
    cuda::memcpy_async(buffer + threadIdx.x, in + threadIdx.x, sizeof(float), pipeline);
    pipeline.producer_commit(); // 此时第一批货"上路了"

    // --- 第二阶段:提交 ---
    pipeline.producer_acquire();
    // 一个 Stage 可以包含多个异步请求
    cuda::memcpy_async(buffer + block_size + threadIdx.x, in + block_size + threadIdx.x, sizeof(float), pipeline);
    cuda::memcpy_async(buffer + 2 * block_size + threadIdx.x, in + 2 * block_size + threadIdx.x, sizeof(float), pipeline);
    pipeline.producer_commit();

    // --- 第三阶段:提交 ---
    pipeline.producer_acquire();
    cuda::memcpy_async(buffer + 3 * block_size + threadIdx.x, in + 3 * block_size + threadIdx.x, sizeof(float), pipeline);
    pipeline.producer_commit();

    // --- 消耗阶段 ---
    
    // 等待最老的阶段 (即第一阶段)
    pipeline.consumer_wait(); 
    // 此时 buffer[0...127] 的数据已经搬完了
    // 注意:如果后面要跨线程访问 buffer,这里仍需 __syncthreads()
    pipeline.consumer_release();

    // 等待第二阶段
    pipeline.consumer_wait();
    // 此时 buffer[128...383] (即第2、3个 block) 数据就绪
    pipeline.consumer_release();

    // 等待第三阶段
    pipeline.consumer_wait();
    // 此时 buffer[384...511] 数据就绪
    pipeline.consumer_release();
}
  • 如果 :你只需要每个线程处理自己搬运的那个数据 (比如 my_val = buffer[threadIdx.x]),那么不需要 __syncthreads()。因为 consumer_wait() 已经保证了当前线程发起的异步拷贝已完成。

  • 但是 :如果你需要处理别人搬运的数据 (这通常才是使用共享内存的目的),那么在 consumer_wait() 之后必须__syncthreads()。因为 pipeline 只能保证"我搬完了",不能保证"我隔壁线程也搬完了"。

API介绍

四个操作共同构成了一个闭环的资源流转系统 。要理解它们,最核心的直觉是:流水线(Pipeline)是一组有限的"车厢"(Stages),生产者装货,消费者取货并退还车厢。

我们可以把这四个操作拆解为两个阶段:获取/提交(生产者)等待/释放(消费者)

线程块分区的情况

producer_acquireproducer_commit

  • 谁来操作? 只有被指定为 Producer 角色的线程。

  • 必须同时吗? 是的。 * 数量必须对齐 :必须有整整 producer_count 个线程调用了这两个方法,流水线才会推进到下一个逻辑阶段。

    • Warp 必须收敛:正如我们之前讨论的,如果 Warp 内部 32 个线程分先后调用,会因为消耗多个硬件槽位(Stage)而导致死锁。

consumer_waitconsumer_release

  • 谁来操作? 只有被指定为 Consumer 角色的线程。

  • 必须同时吗? 是的。

    • 必须所有消费者线程都调用了 wait,它们才会一起被唤醒。

    • 必须所有消费者都调用了 release,对应的硬件槽位(Stage)才会被标记为空闲,从而允许生产者进行下一次 acquire

核心逻辑 :在分区模式下,这四个操作就像是角色内部的屏障(Barrier)。生产者们必须集体行动,消费者们也必须集体行动。

producer_acquire() ------ 申请资源

  • 动作:生产者向系统申请下一个可用的物理阶段(Stage)。

  • 什么时候会阻塞? 当流水线所有的 stages_count 都被占满且没被释放时。

    • 例子 :你初始化了 2 个 Stage,且已经 commit 了两次,但消费者还没来得及 release 任何一个。此时第三次调用 acquire 就会立即阻塞
  • 什么时候放行? 当消费者调用 consumer_release() 归还了一个旧Stage时,阻塞的生产者会被唤醒。

producer_commit() ------ 提交任务

  • 动作:生产者告诉系统:"这个Stage我装好了(异步指令发完了),你可以推给消费者了。"

  • 注意 :这个操作永远不会阻塞(除非发生 Warp 纠缠导致的资源瞬间耗尽),它只是一个状态标记。

consumer_wait() ------ 等待数据

  • 动作:消费者查看流水线中最老的那节Stage(Tail),确认数据到哪了。

  • 什么时候会阻塞? 两种情况:

    1. 生产者根本还没 commit

    2. 生产者虽然 commit 了,但后台的异步拷贝还没搬完。

  • 什么时候放行? 当该阶段对应的所有异步内存操作全部完成时,阻塞的消费者会放行。

consumer_release() ------ 释放资源

  • 动作:消费者告诉系统:"货取完了,这节车厢现在是空的,可以回收到仓库了。"

  • 重要性 :它不阻塞自己,但它是解开生产者阻塞的唯一钥匙

操作 目标 阻塞点 放行条件
producer_acquire 空位 流水线满员(所有 Stage 都在排队) 消费者执行了 consumer_release
producer_commit 标记 通常不阻塞 N/A
consumer_wait 数据 生产者没干完,或数据还在路上 后台 异步拷贝引擎 完成搬运
consumer_release 归还 不阻塞 N/A

线程私有的情况

同时,线程私有的情况还有些不一样,因为没有显式的Stage设置,所以阻塞上限在硬件限制

如果你在 make_pipeline 时没传 groupshared_state,那么这个流水线就是你一个人的

  • 必须同时吗? 完全不需要。

  • 每个线程拥有自己独立的物理槽位(Stage)计数器。

  • 线程 A 可以已经 commit 了 3 次,而线程 B 才刚刚开始第一次 acquire。它们互不干扰,也不会互相等待。

维度 线程块共享 (scope_block) 线程私有 (scope_thread)
阻塞原因 人为设定的缓冲区满了(比如你只设了 2 级缓冲)。 硬件处理能力到顶了达到了流水线深度上限
阻塞后果 全家停步 :只要有一个 Warp 卡在 acquire,整个 Block 进度受阻。 个体停步:线程 A 搬得太猛卡住了,线程 B 只要还有槽位,就能继续搬。
死锁风险 :通常是因为生产者和消费者在同步点步调不一致。 :因为没有跨线程竞争,除非你逻辑写死,否则很难死锁。

cuda::memcpy_async

cpp 复制代码
template <typename _Group, thread_scope _Scope>
_LIBCUDACXX_INLINE_VISIBILITY async_contract_fulfillment memcpy_async(
  _Group const& __group, void* __destination, void const* __source, std::size_t __size, pipeline<_Scope>& __pipeline);

使用流水线实现生产者-消费者模式

在上一节中,我们展示了如何通过空间划分线程块,并使用异步屏障来实现生产者-消费者模式。利用 cuda::pipeline,这一过程可以得到极大简化:通过一个分区流水线(Partitioned Pipeline)即可管理,每个数据缓冲区对应一个流水线阶段(Stage),而不再需要为每个缓冲区维护两个异步屏障。

在这个示例中,我们使用线程块中一半的线程作为生产者,另一半作为消费者。

  1. 第一步 :创建一个 cuda::pipeline 对象。由于我们需要区分角色,必须使用 cuda::thread_scope_block 作用域的分区流水线。

  2. 初始化 :分区流水线需要 cuda::pipeline_shared_state 来协调。我们在线程块作用域内初始化一个 2 阶段(Stage)的状态,并调用 cuda::make_pipeline()

  3. 预填充(Fill):生产者线程首先提交异步拷贝,填充流水线的所有阶段。此时所有数据拷贝都在进行中(In-flight)。

  4. 主循环:遍历所有数据块。根据线程角色,生产者提交未来批次的异步拷贝,消费者则消耗当前批次。

cpp 复制代码
#include <cuda/pipeline>
#include <cooperative_groups.h>
#include <cuda_runtime.h>
#include <iostream>
#include <vector>

using pipeline_t = cuda::pipeline<cuda::thread_scope_block>;

// ==========================================
// 生产者逻辑
// ==========================================
__device__ void produce(pipeline_t &pipe, int stage, int batch, int num_batches, 
                       float *buffer, int buffer_len, float *in) {
    if (batch < num_batches) {
        // 1. 申请空间:如果 Stage 0/1 都满了,这里会阻塞
        pipe.producer_acquire();

        // 2. 异步搬运:将数据从 Global 搬到对应的 Shared Memory 槽位
        float* src = in + batch * buffer_len;
        float* dst = buffer + stage * buffer_len;
        
        // 每个生产者线程负责搬运一部分(步长为生产者线程数)
        int producer_thread_count = blockDim.x / 2;
        int tid = threadIdx.x;
        for (int i = tid; i < buffer_len; i += producer_thread_count) {
            cuda::memcpy_async(dst + i, src + i, sizeof(float), pipe);
        }

        // 3. 提交:标记该 Stage 已经下单完毕
        pipe.producer_commit();
    }
}

// ==========================================
// 消费者逻辑
// ==========================================
__device__ void consume(pipeline_t &pipe, int stage, int batch, 
                       float *buffer, int buffer_len, float *out) {
    // 1. 等待:直到该阶段的数据完全搬运到 Shared Memory
    pipe.consumer_wait();

    // 2. 干活:处理数据并写回 Global
    float* src = buffer + stage * buffer_len;
    float* dst = out + batch * buffer_len;
    
    int consumer_thread_count = blockDim.x - (blockDim.x / 2);
    int tid = threadIdx.x - (blockDim.x / 2);
    for (int i = tid; i < buffer_len; i += consumer_thread_count) {
        dst[i] = src[i] * 2.0f; // 业务逻辑
    }

    // 3. 释放:归还 Stage,让生产者可以再次 acquire
    pipe.consumer_release();
}

// ==========================================
// 主 Kernel
// ==========================================
__global__ void producer_consumer_pattern(float *in, float *out, int N, int buffer_len) {
    auto block = cooperative_groups::this_thread_block();
    
    // 外部动态共享内存
    extern __shared__ float buffer[];

    const int num_batches = N / buffer_len;
    constexpr int num_stages = 2; // 双缓冲

    // 初始化分区流水线
    size_t producer_count = block.size() / 2;
    __shared__ cuda::pipeline_shared_state<cuda::thread_scope_block, num_stages> shared_state;
    
    // 注意:所有线程都必须执行 make_pipeline,角色根据 thread_rank 分配
    pipeline_t pipe = cuda::make_pipeline(block, &shared_state, producer_count);

    // 1. 预热(Fill the pipeline):让生产者先把前 2 个缓冲区填满
    if (block.thread_rank() < producer_count) {
        for (int s = 0; s < num_stages; ++s) {
            produce(pipe, s, s, num_batches, buffer, buffer_len, in);
        }
    }

    // 2. 主流水线循环
    int stage = 0;
    for (int b = 0; b < num_batches; ++b) {
        if (block.thread_rank() < producer_count) {
            // 生产者:始终尝试"预取"未来第 N+2 个批次的数据
            produce(pipe, stage, b + num_stages, num_batches, buffer, buffer_len, in);
        } else {
            // 消费者:消耗当前第 b 个批次
            consume(pipe, stage, b, buffer, buffer_len, out);
        }
        
        // 关键:两个角色必须同步切换 Stage 指针
        stage = (stage + 1) % num_stages;
    }
}

// ==========================================
// Main 函数
// ==========================================
int main() {
    const int N = 1024 * 10;
    const int buffer_len = 1024;
    size_t size = N * sizeof(float);

    float *h_in = new float[N], *h_out = new float[N];
    for(int i=0; i<N; ++i) h_in[i] = i;

    float *d_in, *d_out;
    cudaMalloc(&d_in, size);
    cudaMalloc(&d_out, size);
    cudaMemcpy(d_in, h_in, size, cudaMemcpyHostToDevice);

    // 启动参数:128 线程(64 产 / 64 消),动态共享内存大小为 2 个 buffer
    size_t shared_mem = 2 * buffer_len * sizeof(float);
    producer_consumer_pattern<<<1, 128, shared_mem>>>(d_in, d_out, N, buffer_len);

    cudaMemcpy(h_out, d_out, size, cudaMemcpyDeviceToHost);
    std::cout << "Result [0]: " << h_out[0] << ", [1023]: " << h_out[1023] << std::endl;

    cudaFree(d_in); cudaFree(d_out);
    delete[] h_in; delete[] h_out;
    return 0;
}

在进入主循环前,生产者先行动。

  • 生产者 连续调用两次 produce

    • Batch 0 搬到 Stage 0

    • Batch 1 搬到 Stage 1

  • 消费者 :在 if 逻辑外,原地等待,不参与预热。

  • 结果:此时硬件的异步拷贝引擎已经忙起来了,Global 的前两个批次数据正在飞往 Shared Memory。

循环第一轮 (b=0, stage=0)

  • 消费者 :调用 consume 处理 Batch 0(位于 Stage 0)。它会执行 consumer_wait(),如果异步拷贝没完,它就等;完了就开工计算。

  • 生产者 :调用 produce 预取 Batch 2(位于 Stage 0)。

    • 注意 :此时 Stage 0 正在被消费者使用。所以生产者的 producer_acquire() 会阻塞,直到消费者处理完 Batch 0 并执行 consumer_release()
  • 同步点 :当消费者放手 Stage 0,生产者立刻填入 Batch 2

循环第二轮 (b=1, stage=1)

  • 消费者 :处理 Batch 1(位于 Stage 1)。

  • 生产者 :预取 Batch 3(覆盖到 Stage 1)。

  • 逻辑同上:消费者用旧的,生产者塞新的。

相关推荐
w2018002 小时前
段永平投资问答录pdf完整版
笔记·pdf
我的征途是星辰大海。2 小时前
设计模式(学习笔记)(第一章)
笔记·学习·设计模式
弘毅 失败的 mian2 小时前
STM32 时钟详解
经验分享·笔记·stm32·单片机·嵌入式硬件·嵌入式
qeen872 小时前
【算法笔记】差分与经典例题解析
c语言·c++·笔记·学习·算法·差分
中屹指纹浏览器3 小时前
2026分布式多账号运营下指纹浏览器集群调度方案
经验分享·笔记
摇滚侠3 小时前
Java 零基础全套视频教程,面向对象(进阶),笔记 90-103
java·开发语言·笔记
23471021273 小时前
4.22 学习笔记
软件测试·笔记·python·学习
liuyunshengsir3 小时前
3 个由浅到深的 CUDA 编程完整示例
cuda
智者知已应修善业3 小时前
【100赫兹50分频得到2赫兹频率74HC14+74HC160】2023-6-26
驱动开发·经验分享·笔记·硬件架构·硬件工程