GPU 执行模型 1:CPU 与 GPU 前端

基础概念

Host and Device 概念

硬件队列的概念

硬件队列的概念需要单独进行强调。

软件概念

stream

stream 是软件层面的抽象。CUDA runtime 和 UMD(User Mode Driver)将其暴露给程序员用来表达并发性(concurrency)和依赖关系。

event

如果说 Stream (流) 是装载 GPU 任务的传送带,那么 Event (事件) 就是你插在传送带上的“旗帜”。

整体执行流程

第零阶段:Context

当你的程序启动并调用 CUDA 时,驱动已经在 GPU 上为你创建了一个 Context(上下文)。

这个 Context 是资源的沙箱,

第一阶段:Host 端

在这个阶段,CPU 上的用户态驱动(UMD)像是一个编译器和调度员,负责把高层的并发意图转化为底层的物理队列流。

第二阶段:GPU 硬件前端

在这个阶段,指令跨越 PCIe 总线抵达 GPU 硅片。

NSYS 讨论

Kernel Launch 和 Kernel Execution 对应关系

CPU 上执行的每一个 launch API 调用都和一个 GPU kernel execution 对应。

两者通过 correlationId 关联,可以在 nsys 中观察到这个关联。这个 ID 实际上就是在第一阶段第 3 步,UMD 往 PushBuffer 里写指令时打上的一个序列号标签。


另外值得注意的是,nsys 里 nvtx range 不一致的问题。即:一个 process 的 CUDA HW 和 Thread 对同一个 NVTX range 显示的时间范围不一样。

Thread 的 range 显示的是 CPU 上执行 range_push 和 range_pop 的时刻。由于异步执行,如果 range_pop 之前 CPU 没有和 GPU 同步(即等待 GPU),那么 range_pop 时刻之后,range_pop 之前的 launch 的 kernel 可能还在 GPU 上跑。

CUDA HW 的 range 显示的是 thread 的 range 中 launch 的 kernel 在 GPU 上执行的时刻。CUDA HW range 的开始时刻,是 CPU 在 thread range 里 launch 的第一个 GPU 操作(kernel 或 memcpy),在 GPU 上开始执行的时刻。CUDA HW range 的结束时刻,是 CPU 在 thread range 里 launch 的最后一个 GPU 操作,在 GPU 上结束执行的时刻。

Thread range 和 CUDA HW range 通过 correlationId 进行关联。

需要注意的是,两个 kernel 在 GPU 上执行时,中间会有空隙。如果空隙比较大,是因为 PushBuffer 经常空,GPU 拿不到新任务,只能空闲。CUDA HW 的 range 开始时刻减去结束时刻的时间值,包括空隙。

PushBuffer 满

Nsys 在 CUDA Overhead 那一行报的 command buffer full,实际上就是第一阶段第 3 步 UMD 写 PushBuffer 的时候,PushBuffer 满了。

这时候,CPU 会阻塞在第 3 步进行等待,等 GPU HIU 去取 PushBuffer 的内容。PushBuffer 腾出空间以后,CPU 上的 UMD 才能写入新的内容,然后返回。

PushBuffer 空

以下几种情况可能导致 PushBuffer 经常是空的:

导致的结果就是,kernel 执行之间会出现 gap,GPU 利用率不高。

CUDA Graph

CUDA Graph 的物理本质:把成百上千次“写 PushBuffer + 敲门铃”的 CPU 动作,压缩成“一次写 + 一次敲门铃”,然后把剩下的分发工作全部甩锅给 GPU 硬件自己去干。

以前的流模式 (Stream Model) 有多笨

在没有 Graph 的时候,假设你有 100 个互相有依赖的小 Kernel(比如 A 算完算 B 和 C,B 和 C 算完算 D)。

哪怕你用了 Stream 和 Event 实现了完美的并发设计,在底层依然是:

CUDA Graph 怎么运作

CUDA Graph 将执行分成了两个截然不同的阶段:捕获/实例化 (Capture & Instantiate) 和 发射 (Launch)。

CUDA Graph 解决的问题