详细解析(按幻灯片顺序、带公式格式说明)

1. 总体议程(Agenda — 目标)

  • 理解 Taro 出现的动机(为什么要设计新的系统)
  • 学会使用 Taro 的 C++ 编程模型(API/用法示例)
  • 深入 Taro 的协程感知(coroutine-aware)调度算法(核心技术)
  • 在微基准和真实应用上评估 Taro 的表现
  • 总结与结论

2. 什么是 Task Graph-based Programming System(TGPS)

概念:TGPS 把程序的函数调用和它们之间的依赖关系,以自顶向下的方式封装为一个 任务图(task graph)。任务图节点代表任务(task),边表示先后/数据依赖(precedes)。
视觉化(示意):

Code
 A
 B
 C
 D

如果 A 是 B、C 的前驱,而 B、C 又是 D 的前驱,图形上就是:

   A
  / \
 B   C
  \ /
   D

核心思想:把并行度和依赖显式化,让调度器(scheduler)根据这个图来并行执行任务,而不是隐式依赖于线程同步与锁。

3. 任务图的构造(示例代码解释)

幻灯片里给出的伪代码(C++ 风格)示例:

Scheduler sched;
task_a = sched.emplace([](){ /* Code block A */ });
task_b = sched.emplace([](){ /* Code block B */ });
task_c = sched.emplace([](){ /* Code block C */ });
task_d = sched.emplace([](){ /* Code block D */ });
task_a.precede(task_b);
task_a.precede(task_c);
task_b.precede(task_d);
task_c.precede(task_d);
sched.schedule();
sched.wait();

逐行说明:

  • Scheduler sched;:创建一个调度器对象。
  • sched.emplace(...):在调度器中插入一个任务,返回任务句柄(task_a 等)。
  • task_x.precede(task_y);:表示 task_x 必须先于 task_y 完成(建立依赖边)。
  • sched.schedule();:向运行时提交任务图以供执行(开始调度)。
  • sched.wait();:等待整个任务图执行完成(阻塞调用线程直到完成)。
    好处
  1. 写法直观,易表达任务图。
  2. 能实现“非规则”的并行(irregular parallelism),适合不规则依赖图。

4. 任务图执行顺序示例(不同可能的拓扑调度)

给定同一个依赖关系,调度器可以产生不同的实际执行顺序,例如:

  • 可能的执行序列: A -> B -> C -> D
  • 或:B -> A -> D -> C(取决于任务是否就绪、调度策略、优先级等)
    重要点:调度器要保证任何终态都满足依赖约束(即产生一个拓扑序列)。

5. 为什么需要改进:异构计算上的挑战(CPU/GPU 混合)

在异构系统(CPU + GPU)上,现有的 TGPS 面临关键问题:任务的“原子执行”(atomic execution per task)导致 CPU 被动等待 GPU 完成,从而浪费资源。

  • 假设一台机器只有 1 个 CPU(或有限的 CPU 线程)和 1 个 GPU。
  • 某些任务(例如 B)包含 GPU 调用:CPU 发起 GPU 工作后继续等待,直到 GPU 完成(CPU 线程被阻塞)。
  • 如果调度器把每个任务当作“原子不可拆分”,那么当一个 CPU 线程被 GPU 阻塞时,它不能去运行其他就绪任务,系统并不会充分利用 CPU。
    时序示意(简化):
时间 →
CPU: A (run) → B (attach GPU, then wait) → idle while GPU runs
GPU:    idle  → B (running on GPU) → ...

这会带来两类损失:

  1. CPU 空闲时间(而实际上有其他就绪任务可做)。
  2. 更差的吞吐或更长的总完成时间。

6. 理论上的并行度度量(引入经典并行算法量)

为分析并行系统性能,常用两个基准量:

  • 工作量(work):全部任务在单处理器上执行所需时间,记为 T1T_1T1
  • 临界路径长度(critical path / span):任务图上最长的依赖链所需时间,记为 T∞T_\inftyT
    调度在 ppp 个处理器上至少需要的时间有下界(Brent 定理):
    Tp≥max⁡(T1p,;T∞) T_p \ge \max\left(\frac{T_1}{p},; T_\infty\right) Tpmax(pT1,;T)
    解释:并行加速受限于总工作量分配和最短不可并行化路径。
    在异构(CPU+GPU)场景,T1T_1T1T∞T_\inftyT 的估计需要把 CPU/GPU 的不同执行成本考虑进来,且阻塞带来的延迟会放大 T∞T_\inftyT 的实际影响。

7. 现有 TGPS 在异构系统中的典型调度问题(更细致)

  • 把 CPU 到 GPU 的调用当作任务内部的同步点(atomic)。结果就是当任务内部发生 GPU 调用时,CPU 线程被阻塞直到 GPU 完成。
  • 阻塞导致调度器无法把该 CPU 线程分配去执行其他就绪任务。
  • 这在多任务密集、GPU 调用延迟大的场景尤其糟糕。
    因此我们需要一个能 在任务内部进行更细粒度的“挂起/恢复” 的调度方法,从而在 CPU 被用于等待时仍能继续做其他工作 — 这就是 Taro 的动机所在。

8. Taro — 协程感知(coroutine-aware)的调度算法:核心思路

Taro 的关键创新在于 把任务体内部的异步点(例如发起 GPU 工作)暴露给调度器,以便可以挂起该任务的执行上下文(coroutine),释放 CPU 去执行其他任务,然后在 GPU 完成时恢复该协程。要点包括:

  1. 任务以协程(coroutine)形式编写或包装
    • 一个任务可以在内部 awaityield,在等待异步事件(比如 GPU 完成)期间将控制权返回给调度器,而不是阻塞线程。
  2. 调度器管理协程就绪/等待队列
    • 有两个或更多状态集合,例如 ready(可运行协程队列)与 suspended(等待 I/O/GPU 的协程)。
    • 一旦 GPU 事件完成,相关协程被放回 ready 队列,等待调度执行其后续代码。
  3. 避免阻塞的工作窃取(work-stealing)与协程迁移
    • 调度器仍然可以做工作窃取(work-stealing)以实现负载均衡,但要意识到协程的挂起/恢复边界。
    • 协程的上下文切换代价通常比线程小,适合用于细粒度异步。
  4. 把任务原子性拆分成“阶段”
    • 每个任务可以分成若干“阶段”(phase):在一个阶段里发起异步操作,在下一个阶段里处理结果。调度器只在阶段边界切换协程。
      示意伪代码(概念):
task_b = sched.emplace(coroutine([]{
    // 阶段 1:CPU 工作
    do_cpu_work();
    // 发起 GPU 工作并挂起协程,等待 GPU 完成
    co_await gpu_launch_and_wait();
    // 阶段 2:GPU 完成后的 CPU 工作
    process_gpu_results();
}));

在这里 co_await gpu_launch_and_wait(); 并不是把线程阻塞,而是把协程挂起并让出 CPU。

9. 调度器需要处理的机制细节(实现要点)

实现协程感知调度器会涉及这些机制:

  • 协程状态机:每个任务协程需要一个小的状态机来保存局部变量与恢复点(这正是 C++ 协程或其他协程库提供的)。
  • 事件触发回调:当 GPU 操作完成时,GPU 驱动或运行时需要触发回调,把对应的协程移回 ready 队列。
  • 挂起时资源管理:不能把临时资源泄露,且要保留协程需要的 context(例如指针、缓冲区)。
  • 优先级与亲和性(affinity):某些任务可能强依赖数据位于 GPU 内存或某个 NUMA 节点,调度器需要考虑亲和性以减少迁移成本。
  • 阻塞/非阻塞混合策略:当协程不能被挂起(或代价太高)时,调度器仍需支持传统阻塞回退路径。

10. 性能/复杂度的数学直觉(和阻塞的影响)

把“等待时间”明确分解到调度模型中。若任务包含一次 GPU 等待,其总执行时间可以视为:
Ttask=Tcpu-before+Tgpu+Tcpu-after T_{\text{task}} = T_{\text{cpu-before}} + T_{\text{gpu}} + T_{\text{cpu-after}} Ttask=Tcpu-before+Tgpu+Tcpu-after
传统阻塞模型中,T_cpu-before 执行完后,CPU 线程会被堵塞 TgpuT_{gpu}Tgpu 时间;但协程模型可以把这段 TgpuT_{gpu}Tgpu 视为 非占用 CPU 的等待期,从而让其他任务利用这段时间。这会降低系统整体的 TpT_pTp,特别是当 TgpuT_{gpu}Tgpu 相对较大且任务数足够多时。
因此协程感知调度器力求让有效 CPU 工作时间接近 T1T_1T1,并尽量把阻塞引起的空闲时间最小化,从而接近期望的下界 max⁡(T1/p,T∞)\max(T_1/p, T_\infty)max(T1/p,T)

11. 在微基准与真实应用上的评估(如何做实验)

评估 Taro 时通常做两个层面的实验:

  1. 微基准(microbenchmarks)
    • 控制变量(任务粒度、GPU 调用延迟、依赖密度、任务图形态)。
    • 对比基线:传统原子任务 TGPS vs Taro(协程感知)。
    • 度量:吞吐(throughput)、平均任务延迟、CPU 利用率、总完成时间(makespan)。
  2. 真实应用(real-world)
    • 把一个真实工作负载(例如混合 CPU/GPU 的图像处理流水线、科学计算或机器学习前处理)移植到 Taro。
    • 度量端到端运行时间、阶段性延迟、资源利用(如 GPU/CPU 利用率)。
      衡量指标常用:
  • 总完成时间(makespan)
  • 加速比(speedup)
  • CPU 空闲时间比率
  • GPU 利用率

12. 结论(总结 Taro 的价值)

  • 问题识别:现有 TGPS 在异构系统上容易因为把任务当作“原子”而引起不必要的 CPU 阻塞,从而浪费资源。
  • 解决方案:Taro 通过把任务包装为协程、并让调度器在任务内部的异步点进行挂起/恢复,从而实现协程感知调度,减少阻塞带来的资源浪费。
  • 收益:在许多任务混合的异构场景下,Taro 能提高 CPU 利用率、降低总完成时间、提升吞吐,尤其当 GPU 调用延迟不可忽视时效果显著。
  • 实现难点:需要合理管理协程上下文、事件回调、亲和性和负载均衡等细节,且要权衡协程切换开销与阻塞代价。

13. 拓展:把幻灯片代码做更“化”的逐行注释

把上面示例的关键片段做注释版本:

// 创建调度器
Scheduler sched;
// 在调度器中创建四个任务(返回任务句柄)
task_a = sched.emplace([](){ /* 代码块 A */ });
task_b = sched.emplace([](){ /* 代码块 B */ });
task_c = sched.emplace([](){ /* 代码块 C */ });
task_d = sched.emplace([](){ /* 代码块 D */ });
// 建立依赖关系:A 是 B、C 的前驱,B、C 都是 D 的前驱
task_a.precede(task_b);
task_a.precede(task_c);
task_b.precede(task_d);
task_c.precede(task_d);
// 提交并执行任务图,等待完成
sched.schedule();
sched.wait();

如果 task_b 内含 GPU 调用,Taro 推荐的写法是把 task_b 改写为协程式:

task_b = sched.emplace(coroutine([]{
    // CPU 前置工作
    do_cpu_part();
    // 发起 GPU 并挂起协程(非阻塞线程)
    co_await launch_gpu_and_suspend();
    // GPU 完成后的 CPU 后置工作
    finish_cpu_part();
}));

14. 补充:关键术语对照(便于阅读)

  • Task / 任务
  • Task graph / 任务图(又称任务依赖图)
  • Scheduler / 调度器
  • Coroutine / 协程(可挂起的函数/执行体)
  • Precede / 先行(建立依赖边)
  • Atomic task / 原子任务(不可中断的任务执行)
  • Work-stealing / 工作窃取(负载均衡策略)
  • Critical path / 临界路径(又称 span)
  • Makespan / 总完成时间(全部任务完成的时间)

如何解决这个挑战?答案:C++ 协程!

在处理 CPU + GPU 的异构任务图(TGPS)时,任务存在同步问题:
CPU 任务和 GPU 任务不能同时执行,因为 传统 TGPS 一次只能执行一个任务(atomic execution per task)
因此:

  • CPU 发出 GPU kernel 后,会阻塞等待 GPU
  • CPU 不能继续执行其他任务
  • GPU 空的时候 CPU 忙;CPU 忙的时候 GPU 空 → 资源没有得到正确重叠(overlap)
    这就是挑战。

协程如何解决这个异构调度问题?

协程(Coroutine)是一种:
可以自己挂起(suspend),并在未来由调用者恢复(resume)的函数。
普通函数 → 不能中断
协程 → 可以随时停下,并在之后继续执行
换句话说:

普通函数 = “一次性全部执行完的代码”
协程 = “可分段执行的代码”

为什么协程有用?生活类比理解

你下班回家想做两件事:

  1. 烧水
  2. 洗澡
    如果你按正常逻辑:
  • 开火烧水
  • 站在那里等水烧开(CPU 阻塞)
  • 水开了
  • 再去洗澡
    整个过程非常慢。
    但如果用协程思想:
  • 开火烧水(开始 GPU 工作)
  • 挂起任务co_await
  • 趁烧水过程去洗澡(重叠 CPU 工作)
  • 洗完后水也刚好烧好
    → CPU 任务和 GPU 任务有效重叠!
    这就是协程对于异构计算的意义:

协程可以让 CPU 和 GPU 同时工作,不再让 CPU 阻塞等待 GPU。

C++ 协程的核心:suspend(挂起)与 resume(恢复)

协程:

  • 能暂停自己(co_await something
  • 暂停后 CPU 可以执行别的任务
  • 当等待的事件完成后(例如 GPU kernel 完成)
  • 再恢复执行

但——C++ 协程不容易用

协程概念简单,但 C++ 的协程机制非常复杂,需要:

  • Coroutine
  • Promise
  • Awaitable
  • coroutine_handle
  • compiler transformation rules
  • await_suspend/await_resume
  • initial_suspend / final_suspend
    因此:

实现一个简单的协程 → 需要写大量繁琐的样板代码(boilerplate)

真正的 C++ 协程代码实例

你给出的示例展示了实际 C++ 协程是多么复杂:

▼ GPU 工作协程(简化后逻辑)

Coro gpu_work() {
    cudaStream_t stream;
    cudaStreamCreate(stream);
    gpu_matmul<<<8, 256, 0, stream>>>(matA, matB);
    while(cudaStreamQuery(stream) != cudaSuccess) {
        co_await std::suspend_always{};  // GPU 未完成 → 挂起协程
    }
    cudaStreamDestroy(stream);
}

其含义:

  1. Launch GPU kernel
  2. GPU busy
  3. CPU 不阻塞
    暂停(suspend)协程
  4. 每次调度器检查 GPU 状态
    → GPU 完成后恢复协程继续执行

▼ Promise(协程框架核心)

struct promise_type {
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    Coro get_return_object();
    void return_void() {}
    void unhandled_exception() {}
};

Promise 决定:

  • 协程开始时是否暂停
  • 结束时是否暂停
  • 如何处理异常

▼ 编译器生成的核心逻辑

co_await X
=
compiler生成:
if (!X.await_ready()) {
    X.await_suspend(handle);
    // 协程挂起
}
X.await_resume();

复杂,但这就是 C++ 协程的底层机制。

为什么不能直接用协程做 TGPS?

因为:

  • 代码太复杂
  • 每个任务都要写 Promise / Awaitable
  • scheduler 也要自己写
  • 难以读、难以维护
    你需要的是:

一个简单易用的 TGPS 框架,内部自动使用协程,但用户不需要知道协程细节。

解决方案:Taro

Taro 的核心思想:
用协程实现 CPU/GPU 的异步重叠
但完全隐藏复杂性
用户只需要写普通的 TGPS 任务
Taro 自动使用 coroutine-aware scheduler 进行调度
实现 CPU/GPU 重叠执行(真正异构调度)
也就是说:

Taro = 用协程实现的一套异构任务图系统(TGPS),对用户完全透明。

最终总结

◾ 异构任务图的挑战

CPU 需要等待 GPU → 无法多任务并行 → GPU/CPU 闲置

◾ 协程的能力

可以暂停、恢复 → CPU 不再阻塞 → 任务可交错执行 → 实现 GPU/CPU overlap

◾ C++ 协程的问题

写法太复杂,需实现 promise/awaitable/handle 等大量细节

◾ Taro 的关键价值

把 C++ 协程隐藏起来,提供易用的 TGPS API,实现异构调度。

2. 背景:同步 vs 异步(Synchronous / Asynchronous)

示例:烧水。

同步(Synchronous)

turn on stove
wait    <--- 卡住 CPU
turn off stove

CPU 在等待,不做别的事 ⇒ CPU 闲置

异步(Asynchronous)

方法:

  • Polling:轮询
  • Callback:回调
turn on stove
do something else
callback: water ready
turn off stove

CPU 可以去干别的事 ⇒ multitasking。

3. 为什么要 C++ Coroutine?

因为 coroutine 可以:

  • 自动挂起(suspend)与恢复(resume)
  • 等待 GPU 完成 时 CPU 不阻塞
  • 允许多个 CPU/GPU 任务重叠执行
    GPU = 热水壶
    CPU = 你
    Coroutine = “做事 → 等待 → 做事”

4. Taro 的核心理念

Taro 统一异构调度:

  • CPU
  • GPU(CUDA)
  • 自定义加速器(TPU/FPGA/自定义算子)
    通过 coroutine 自动支持:
  • 挂起
  • 恢复
  • 依赖调度
  • 多任务重叠执行

5. Taro 的基本使用方式(Example 1)

#include <taro/taro.hpp>
#include <taro/scheduler/cuda.hpp>
taro::Taro taro{NUM_THREADS};
auto cuda = taro.cuda_scheduler(NUM_STREAMS);

你有:

  • 多个 CPU threads
  • 多个 CUDA streams
    Taro 会负责绑定、调度、依赖、恢复 coroutine。

6. Example 1 —— 四个任务 (A, B, C, D)

图示:

 A
 ├──► B
 └──► C
 B ──► D
 C ──► D

Task A(同步 GPU 调用)

auto task_a = taro.emplace([&]() {
    cuda.wait([&](cudaStream_t stream){
        kernel_a1<<<...>>>();
    }); // synchronize 等 GPU 完成
});

cuda.wait() 是同步的:

  • GPU kernel 启动
  • CPU 等待该 stream 完成
  • CPU 不能做别的任务

Task B(使用 coroutine 的 GPU 异步多任务)

auto task_b = taro.emplace([&]() -> taro::Coro {
    cpu_work_b1();
    co_await cuda.suspend_callback([&](cudaStream_t stream){
        kernel_b1<<<...>>>();
        kernel_b2<<<...>>>();
    }); // suspend
});

co_await 会做什么?

Coroutine 变换(编译器展开)

当遇到

co_await something

编译器执行:

  1. 执行 something.await_ready()
  2. 如果不 ready → await_suspend()
    ⇒ coroutine 被挂起
  3. 由 Taro 调度器在未来时间点 resume
    效果:
  • GPU execute kernel
  • CPU 去执行 别的任务
  • GPU 完成后 → resume coroutine → 接着执行

Task C(使用 polling 方式 suspend)

auto task_c = taro.emplace([&]() -> taro::Coro {
    co_await cuda.suspend_polling([&](cudaStream_t stream){
        kernel_c1<<<...>>>();
    });
    cpu_work_c1();
});

含义:

  • 让 GPU 执行 kernel
  • coroutine 挂起
  • Taro 定期 polling CUDA stream
  • 当 stream 完成 → resume coroutine
  • 执行 cpu_work_c1()

▶ 为什么不用 cudaStreamSynchronize()

同步版本:

kernel_c1<<<...>>>();
cudaStreamSynchronize(stream);
cpu_work_c1();

CPU 会 阻塞等待
Taro coroutine版本:CPU 不会阻塞,不会空转。

Task D(同步 GPU 等待)

auto task_d = taro.emplace([&]() {
    cuda.wait([&](stream){
        kernel_d1<<<...>>>();
    });
});

任务依赖图

task_a.precede(task_b);
task_a.precede(task_c);
task_b.precede(task_d);
task_c.precede(task_d);

7. Example 2 —— 混合更多 coroutine + 同步

Example 2 展示:

  • A:GPU suspend + multitask
  • B:GPU suspend + CPU 后处理
  • C:CPU 前处理 + GPU 同步
  • D:GPU suspend + CPU 后处理
    展示 Taro 统一管理不同类型的 GPU/CPU 调用方式。

8. Taro 的可扩展性(Custom Accelerator)

你可以接入:

  • 自定义硬件(TPU/FPGA/自研芯片)
  • 第三方 accelerator
    只需写:
#include <taro/scheduler/custom.hpp>
auto custom = taro.custom_scheduler(...);
co_await custom.suspend_callback([&](...){
    // 调用你的自定义加速器
});

Taro 的使命:

抽象底层异构细节 + 自动进行 coroutine 调度 + 任务依赖管理

9. 用一句话总结 Taro

Taro 是一个异构任务图调度器,它用 C++ Coroutine 将 CPU/GPU 的同步/异步统一抽象,使得多任务可以自动重叠执行,从而显著减少等待、提高硬件利用率。

Taro 框架:完整解析(含数学排版)

Taro 是一个 CPU + GPU 协同任务调度框架,基于 C++20 协程 (coroutines),支持复杂任务图、异步 GPU 调度、自动挂起/恢复任务,并在多种工作负载上取得显著加速效果。
其核心思想是:

程序员写同步/异步任务逻辑,Taro 自动完成跨 CPU/GPU 的调度、挂起、恢复、抢占和负载均衡。

1⃣ 为什么需要 Taro?(Motivation)

传统 CPU+GPU 混合任务图面临 三个关键痛点

痛点 描述
1. 管理 GPU 异步事件很复杂 GPU kernel 调度是异步的,需要用 CUDA callback、polling、events 等管理。
2. CPU 与 GPU 的并发利用率低 当 CPU 线程在等待 GPU 回调时会阻塞,导致 CPU 空闲浪费。
3. 现有框架缺乏对协程的深度支持 大部分任务图库不支持挂起/恢复任务的语义。
Taro 的目标是:

目标三件套

  • Simplicity(简单):用户只需写代码,不需要手动处理回调和同步。
  • Efficiency(高效):最大化 CPU/GPU 使用率,避免线程阻塞。
  • Extendibility(可扩展):支持更多加速器、更多异步模式、更多任务图模式。

2⃣ Taro 编程模型(C++20 协程 + GPU 调度)

用户主要使用两个机制:

✦ 同步任务

适合 CPU-only 工作:

auto task = taro.emplace([&](){
    cpu_work();
});

✦ 异步任务(使用协程)

适合 CPU + GPU 混合,例如:

auto task = taro.emplace([&]() -> taro::Coro {
    cpu_work_a1();
    co_await cuda.suspend_callback([&](cudaStream_t stream){
        kernel_a1<<<...>>>();
        kernel_a2<<<...>>>();
    });
    cpu_work_a2();
});

关键点:

  • co_await cuda.suspend_callback(...)
    挂起协程、释放 CPU 线程、让出执行权
  • GPU 完成后触发 callback
    → Taro 调度器 唤醒协程(按同一 worker 优先恢复)
  • 协程恢复后继续执行

3⃣ Taro 调度器:协程感知的调度算法

这是 Taro 的核心创新点。

每个 Worker 线程有两个队列


队列 作用
LPQ (Low-priority Queue) 存放“新任务”
HPQ (High-priority Queue) 存放“挂起后等待恢复的任务”
恢复任务优先级高,因为:

让任务回到原来的 worker最能减少竞争与缓存失效。

核心调度动作


动作 描述
Offload 任务向 GPU 推送 kernel
Suspend 挂起协程,放到 HPQ
Callback GPU 完成后触发回调 →
恢复任务
Steal 空闲 worker 从别的 worker 的 LPQ/HPQ 偷任务
Poll 通过 polling 查询 GPU 状态
Wait/Block CPU 等待其它任务完成时阻塞
Enqueue 任务被恢复后重新进入 HPQ

▶ 样例调度流程(关键步骤)

现在以示例代码为例,有任务 A、B、C、D 与一个 GPU stream、两个 CPU worker。
整体流程如下(简化版):

  1. Worker 1 执行 A
  2. A 启动 GPU kernel → Suspend → 放入 Worker 1 的 HPQ
  3. Worker 1 进入睡眠
  4. Worker 2 执行其他任务(C / D)
  5. GPU 完成 A → callback → A 被放回 Worker 1 的 HPQ
  6. Worker 1 被唤醒 → 但若被 C block,则无法执行 A
  7. Worker 2 此时可能 steal Asteal B/D
  8. 剩余任务按 HPQ → LPQ 的优先级执行完毕
    你贴出的图完整展示了 多步抢占、恢复、等待、回调、polling 的精细流程。

4⃣ 数学:任务执行时间模型

Taro 的实验包含两类任务:

1. 数据驱动任务(小而多)

一个典型的数据驱动协程:

while(k++ < 300) {
    // CPU work
    co_await cuda.suspend_polling(...);
}

每次循环:
T=Tcpu+Tgpu+Tsuspend/resume T = T_{\text{cpu}} + T_{\text{gpu}} + T_{\text{suspend/resume}} T=Tcpu+Tgpu+Tsuspend/resume
数据驱动任务特点:

  • GPU kernel 非常短
  • GPU callback 频繁
  • CPU 与 GPU 交替频繁
    因此线程阻塞的代价非常大。
    Taro 在此场景能达到:
    18× 加速比 18\times \text{ 加速比} 18× 加速比

2. 固定运行时任务(长 GPU kernel)

cpu_busy_loop(ctime);
co_await cuda.suspend_callback(... large GPU kernel ...)

此时:
Tgpu≫Tsuspend/resume T_{\text{gpu}} \gg T_{\text{suspend/resume}} TgpuTsuspend/resume
GPU kernel 足够长,调度开销不敏感。
Taro 与 Fiber 均达到最优。

5⃣ 实验结果总结

多种微基准测试:

  • 线性链
  • Divide & Conquer
  • MapReduce
  • Wavefront
  • Random DAG
    Taro 在大多数 workload 比 Taskflow / Boost fiber 明显更快。

数据驱动任务

Taro 达到:
18.3× 18.3\times 18.3×
相较 Fiber 与 Taskflow。
原因:

  • Taro 的调度模型专门优化了频繁 Suspend/Resume 的场景
  • 使用 C++20 atomic::wait/notify 实现极低开销线程休眠

固定耗时任务(GPU 大核)

Taro ≈ Fiber (最优)
比 Taskflow 快明显。

大规模电路仿真(实际 workload)

与 RTLflow(state-of-the-art)比较:

  • Taro 达到:
    1.7× 1.7\times 1.7×
    加速比
  • 内存占用可比
    这是论文中意义最大的结果,证明 Taro 并不仅仅是 microbenchmark 优化,而是能在真实工程场景中大幅加速。

6⃣ 结论 + 展望

已经做到:

  • 简洁的 C++ 编程模型
  • 对 GPU 回调、polling 的完整封装
  • 协程感知的任务调度器
  • 几乎在所有基准上取得优异性能
  • 对复杂电路模拟 workload 显著提速

Future work

  • 更多文档
  • 更多加速器(例如 TPU、NPU…)
  • 更多计算模式(Pipeline、IO async)
  • 对称协程传递(symmetric coroutine transfer)
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐