Skip to content

07-13 上午:高性能计算高级话题

最后更新于·约 6991 字

一个模型算子从框架表达走到设备执行,会经过运行时、编译器、库、内存和硬件调度。GPU 的价值在于能同时推进大量结构相似的工作;依赖链、随机访问和频繁 CPU 往返仍会限制它。

HPC 应用

HPC 把原本无法在可接受时间内完成的计算,变成可以支持决策或研究的结果。课程给出的例子都指向同一个目的:把可用的算力转化为更及时的判断。

  • 天气预报:在离散网格上反复求解大气方程。日常判断下不下小雨,预测偏一点影响有限;台风路径、登陆时间和强度预测偏一点,后面的转移、停工、救援安排可能完全不同。高性能计算提供的是更高时空分辨率、更频繁的同化与更及时的预报。
  • 分子动力学与材料/药物模拟:过去需要从海量候选中逐一做实验;算力足够时,可以先在模型中模拟原子或分子的相互作用,筛出少数值得做湿实验的候选。仿真不能取代实验,但能把实验资源集中到更有希望的地方。
  • 量子系统与大规模模型:量子系统模拟、大模型训练和推理都把问题规模推到了单机难以承担的程度。

一个 HPC 系统通常同时包含计算芯片、内存和存储、节点内外互连、运行时与通信库、编译器和框架,还需考虑供电和散热。高端 GPU 的功耗已达数百瓦甚至接近千瓦;把多块卡装进一个节点后,供电、散热和机房设计都会成为系统约束。性能由这些环节共同决定,芯片只是其中一环。

应用、系统与硬件

同一个 AI 任务可以从不同层面优化。算法层决定模型结构、训练目标和数据;框架层把模型表达为算子图;运行时负责调度、内存和通信;编译器与库把算子变成具体 kernel;最后才由 GPU、CPU、内存和网络执行。任何一层成为瓶颈,其他层做得再好也无法把端到端时间降下来。

训练时,参数、梯度和激活常分散在各处。部分状态在 GPU 高带宽内存中参与前向和反向,优化器状态或不常用数据可能在主机内存中;每一次移动都要花时间。于是显存够不够、带宽够不够、一个 kernel 算得快不快相互牵连,需要一起看。

CPU 的低延迟设计

通用 CPU 的首要目标是低延迟1地执行复杂、分支多、数据访问难以预测的程序。它需要强大的控制逻辑:取指、译码、分支预测、乱序执行、缓存层次、寄存器重命名等。一个核心能把单条指令流跑得很快,但这些控制部件占用了大量晶体管和功耗。

流水线是理解 CPU 的第一步。把一条指令分成取指、译码、执行、访存、写回等阶段后,不同指令可同时处在不同阶段。这样提升的是吞吐2,不是让单条指令瞬间完成。若后续指令依赖前一条的结果,或分支预测错误,流水线就要等待或清空。

CPU 会继续挖掘指令级并行。超标量处理器可在一个周期发射多条独立指令;乱序执行会在不改变程序可见结果的前提下,先执行准备好的指令;多级 cache 尽量避免主存访问把核心停住。它们非常适合控制密集、依赖复杂、工作量不规则的代码,但实现成本也高。

GPU 采取了另一种取舍。把晶体管优先用于大量更简单的计算单元,让大量相同结构的线程同时工作。当一批线程在等内存时,硬件切换到另一批就绪线程。它偏向吞吐量,而非单线程延迟。

存储层次与性能

算术单元快,程序仍可能因为取不到数据而跑不快。离计算单元越近的存储越小、越快、单位容量越贵;越远则容量越大、延迟越高。按 GPU 侧粗略分层的存储如下。

层级 访问 谁可见 典型用途
寄存器 最快 单个线程 局部标量、累加器
shared memory 快,程序管理 同一 block 显式复用的小块数据、线程协作
L1/L2 cache 次快 硬件缓存 缓存 global 访问
显存 HBM/GDDR 较慢 整个 device 大数组、输入输出
主机内存 更慢 CPU/进程 不在 device 上的数据
SSD 最慢 持久 冷数据、检查点

寄存器最快但只属于一个线程;shared memory 由同一 thread block 的线程显式协作使用;global memory 对整个 GPU 可见,容量大但访问代价高。优化矩阵乘法时把输入块放进 shared memory,正是为了让同一块数据被许多线程复用,减少对 global memory 的重复读取。

课件把 Flip-Flops、SRAM、DRAM 与 SSD 放在同一张图里:越快的存储越贵、容量越小,从片内寄存器一路到持久磁盘形成明显层级。

图:寄存器/缓存(SRAM)在片内快而贵,DRAM 慢而大,SSD 更大但更慢;性能优化的本质是把数据停在更近、更快的层级。

主机内存和 GPU 显存之间通常经 PCIe3 或更高速的互连传输。一次频繁的 CPU-GPU 往返很容易吞掉 kernel 加速带来的收益,所以常见做法是把数据一次搬到 device,连续运行多个 kernel,最后再搬回结果;能否这样做取决于算法的依赖关系和显存容量。

训练大模型时,显存够不够要算的远不止一份参数。以 GPT-3 规模为例,参数、梯度、优化器状态(Adam 的两组动量)和激活值会在显存里同时占份额,加上为反向传播保留的中间张量,预算很快被填满。

课件给出 GPT-3 规模训练显存的拆账:参数、梯度、Adam 状态与激活各占一份,并对比开启激活重计算前后的峰值占用。

图:显存里同时住着参数、梯度和优化器状态;激活重计算用少量额外前向计算的代价,换取激活峰值的大幅下降。

激活重计算(activation checkpoint)的思路是不保存每一层前向的激活,用时再临时重算一次。它让训练更省显存、能装下更大 batch 或更大模型,代价是反向阶段多做一遍前向,通常有少量计算开销可接受。这是显存受限时用计算换空间的典型例子,与微基准里牺牲带宽换复用的思路一脉相承。

CPU 与 GPU 的取舍

一块 GPU 一般比一块 CPU 更适合规则的大规模数值计算、图像处理和矩阵乘法;串行控制逻辑、复杂分支、任务调度、操作系统与 I/O 管理则更适合 CPU。两种硬件的侧重可以这样对照:

课件对比 CPU 与 GPU 的核心构成:CPU 用少量复杂核心、大 cache 与强控制;GPU 用大量简单核心换吞吐。

图:一侧几个复杂核、一侧几百上千个简单核——这是 CPU 低延迟与 GPU 高吞吐取舍在物理上的直接体现。

CPU GPU
偏好 控制密集、依赖复杂、不规则 规则、大批量、相同指令的吞吐
单线程延迟
大规模并行吞吐 受核心数限制 依赖大量并发线程
典型职责 控制、提交任务、系统调度 执行计算密集 kernel

实际产品的规格把这种取舍落到具体数字上。课件给出 CPU、GPU 与 FPGA 的一页参数对比:不同型号在核心数、频率、FP32/FP16 能力、内存带宽与典型功耗上差异明显,FPGA 则可通过可重构逻辑按需映射电路。

课件用一张表对比 CPU、GPU 与 FPGA 的规格:核心规模、算力、带宽、功耗各有侧重。

图:同一列指标在不同硬件上的取舍一目了然——GPU 用高并行远超 CPU 的矩阵吞吐,FPGA 换来灵活性但浮点峰值通常较低。

选择硬件先看瓶颈在哪种资源:访存受限的工作,带宽更高的设备更有利;计算受限的规则矩阵运算,低精度矩阵单元多的设备更有利;需要频繁改动数据通路、又没有成熟等价实现时,才值得考虑 FPGA。

思考题

CPU 和 GPU 的控制逻辑、延迟设计、线程设计有什么差别?为什么它们适合不同工作负载?

答案

CPU 每个核有较强的控制、缓存和乱序能力,目标是让单线程或少量线程尽快完成,适合分支复杂、依赖强、延迟敏感的任务。GPU 用大量简单线程和宽向量单元换吞吐,通过成千上万可切换线程隐藏延迟,适合数据并行、计算规则、访存模式可预测的任务。

GPU 的高吞吐设计

GPU 由许多个 Streaming Multiprocessor(SM4)组成。一个 SM 内有大量标量运算单元、寄存器文件、load/store 单元、共享内存和专门的矩阵计算单元。不同架构的具体数量会变,但思路稳定:同时保留许多可运行线程,用大量并行算术去覆盖存储访问的等待。

GPU 顶层结构中可看到主机接口、调度控制、HBM 和多个计算单元;数据路径本身是设计重点。

图:计算单元不是孤立的峰值数字;host 接口、全局调度和 HBM 决定数据如何到达各个 SM。

在现代 AI GPU 上,普通 CUDA core 可做常规标量/向量运算,Tensor Core5 则面向小矩阵乘加,例如 \(D=A\times B+C\)。深度学习中的 GEMM、卷积和 attention 经过分块后,能大量调用这种矩阵单元,因此低精度矩阵吞吐远高于一般 CPU 的单核算力。

一张 SM 内部功能单元示意图:整数/浮点单元、寄存器、load-store 单元与 Tensor Core 并列存在。

图:同一 SM 内的 warp 共享执行管线、寄存器文件和片上存储,资源用量会限制同时驻留的工作。 课件的 H100 Block Diagram:SM、Tensor Core、HBM、NVLink 与片上互连的关系一目了然,展示一代旗舰 GPU 的完整构成。 图:从旗舰卡看,GPU 是一台包含大量 SM、专用矩阵单元、多层存储与高速互连的完整系统,不是孤立的一堆核心。

低精度不是把数值随便截短。FP32 有较大的指数和尾数范围;FP16、BF16、FP8 等格式以更少位数换取更低内存流量和更高矩阵吞吐。训练通常会在速度与数值稳定性之间混合使用多种精度,例如低精度前向/反向、较高精度累积或主权重。格式选择应由误差容忍度、硬件指令和实际测量共同决定。

思考题

GPU 有很多 ALU,为什么一个内存延迟高的 kernel 仍然跑不满?

答案

ALU 只有在有足够可执行指令时才能忙。若线程并行度不足、寄存器或共享内存限制 active warps,内存等待期间没有其他 warp 可切换,计算单元就会空闲。此外不合并访存、分支分歧和依赖链也会降低有效指令供给。

SIMD、SPMD、SIMT 与 warp

这几个缩写都牵扯同时做很多相近事情,但含义并不相同。

SIMD:一条指令同时算很多个数

单周期 CPU 的 ALU6 一次通常只算一对数。a[i] + b[i] 一个时钟做一次。SIMD7(Single Instruction, Multiple Data)是给 ALU 配上一组宽寄存器,让一条加法指令同时作用在多个数据元素上。比如一个 256 位宽度的向量寄存器可以装 8 个 32 位 float,一条 add 就同时把 8 对数相加。对数组来说,这就是 8 倍的算术吞吐机会——前提是这些元素要能装进同一个寄存器、且循环能连续访问。

这就是为什么把循环写得规则、连续很重要。SIMD 一次吃一串相邻元素,跳跃访问会让它一条指令只能用到部分 lane。GPU 的底层也是这个思路,只是规模大得多、且用大量线程来组织,于是就有了 SIMT8 与 warp9 这些进一步的抽象。

课件用 SISD、SIMD 等分类说明并行硬件模型:GPU 底层往往是一台 SIMD 引擎,指令管线像 SIMD 管线一样广播到多个数据单元。

图:SISD/SIMD/MIMD 是经典并行分类;理解 GPU 从 SIMD 引擎出发,再叠加 SIMT 的线程语义。

课件强调 GPU 本质是 SIMD 引擎:一条指令在同一时刻广播给很多数据单元,构成底层吞吐。

图:GPU 的指令管线按 SIMD 方式运行;大量相同指令并行推进,是它能做高吞吐的前提。

  • SIMD(single instruction, multiple data)指一条向量指令同时处理多个数据元素,常见于 CPU 向量扩展。
  • SPMD(single program, multiple data)指许多逻辑线程运行同一段程序、处理不同数据;CUDA kernel 在编程模型上是 SPMD。
  • SIMT(single instruction, multiple threads)是 NVIDIA GPU 的硬件执行抽象。硬件把线程组成固定宽度的 warp,并让 warp 中活跃线程大体锁步执行同一条指令流。

CUDA 中线程按 threadblockgrid 组织。一个 kernel launch 启动一个 grid;grid 中有多个 block;block 中有多个 thread。同一 block 的线程可以借助 shared memory 协作并同步,不同 block 通常要独立完成,因而可被调度到不同 SM。block 数量通常远大于 SM 数量,硬件会在一个 block 完成后继续调度下一个。

例如二维矩阵中,一个线程可用

int i = blockIdx.x * blockDim.x + threadIdx.x;
int j = blockIdx.y * blockDim.y + threadIdx.y;
if (i < N && j < N) {
    C[i + j * N] = A[i + j * N] + B[i + j * N];
}

确定自己负责的元素。blockIdxthreadIdx 是 CUDA 提供的内建索引,不属于普通变量。边界判断不能省,因为为了方便常把 grid 向上取整,最边缘 block 的一部分线程会落在有效矩阵外。

warp 是调度和执行时更关键的粒度。通常一个 warp 有 32 个线程;同一 warp 遇到 if/else 的不同分支时,硬件往往要分别执行两条路径,并屏蔽不属于当前路径的线程。这叫分支发散(divergence)。发散并非语法错误,但会降低该 warp 的有效并行度;按数据布局和任务划分让同一 warp 内线程走相同分支,是 GPU 编程的常见考虑。

思考题

一个 warp 内的两个分支走向不同路径时,硬件通常如何执行?这会带来什么性能影响?

答案

常见做法是 SIMT 执行分支的不同路径时先屏蔽不参与的线程,分别执行,再汇合。功能上结果正确,但两个路径都要执行时,这个 warp 的执行时间近似相加,部分线程每条指令阶段都在等待。若分支条件与线程编号高度相关,应尽量让同一 warp 的线程走同一分支。

GPU 的延迟隐藏

GPU 核心并不靠把单个标量指令的延迟压到极低来获得吞吐。一个 SM 同时驻留许多 warp;某个 warp 等待 global memory 时,warp scheduler 立刻挑选另一个已就绪 warp 发射指令。要有足够多的可驻留 warp,称为 occupancy;它受每个 block 的线程数、寄存器使用量、shared memory 用量和硬件上限共同约束。occupancy 高只说明有更多可切换工作,若算术单元或 DRAM 带宽已饱和,继续提高它未必更快。

两种隐藏内存延迟的策略:CPU 用 cache,GPU 用替换线程

CPU 靠多级 cache 让大多数访问命中,尽量少去 DRAM。但 cache 成本高、面积大,还有个更根本的问题——当真的 miss 时,CPU 通常只有一个(或几个)线程可以等,等待期间执行单元空转。

GPU 将 cache 做小一点,转而同时堆大量线程。当一个 warp 因为访存要等待时,调度器几乎立刻换上一个已经就绪的 warp,让计算单元继续忙。这样单个访存的延迟并没有消失,但因为总有别的工作可做,吞吐被撑满了。它不需要为每一条访存都命中投入巨大 cache,代价是需要足够多的并行线程——这就是 occupancy(驻留 warp 数)为什么重要。

HPC 里一个常用说法是,程序员负责提供足够的并行度,硬件负责用换线程的方式把延迟藏起来。你写 CUDA 时只要把循环拆给很多线程、让每个线程有独立的工作,剩下的等待由大量并行来吞掉。

一个很生活化的比喻是:GPU 的调度颗粒度从来没有落到一个线程个人,而是一条 warp,就像大学里很多活动是按班级、按寝室整体安排的,而不是逐个同学单独处理。写 CUDA 时你只需要感觉到每个线程大概在干什么,调度和优化的粒度交给硬件以 warp 为单位处理。这也是为什么编 CUDA 常常比手写 CPU 的 SIMD intrinsic 轻松——你不必记一堆指令,只需把循环拆到每个 thread。

线程束中的控制流也有成本。warp 的 32 个线程遇到不同 if 分支时,硬件通常分路径依次执行,再以 mask 禁用暂不活跃的线程;这就是 divergence。边界检查造成的少量分歧通常可接受,按随机类别把同一 warp 分到完全不同的大分支则会明显损失吞吐。重排数据、按类别分桶或把分支变成掩码运算,需要与重排开销一并测量。

思考题

CPU 用 cache 隐藏访存延迟,GPU 用大量线程切换隐藏延迟。这两种策略对程序设计各有什么要求?

答案

CPU 更强调局部性,让数据留在 cache,减少等待;程序应连续访问、提高复用。GPU 则需要暴露足够多的独立工作,当某些 warp 等内存时,其他 warp 可以继续执行;程序要有足够并行度、合理资源占用和可预测的访存模式。两者并不对立,GPU 也有 cache,但设计重心不同。

地址、布局与设备传输

CPU 端数组指针不能被普通 GPU kernel 直接解引用。通常要分配 device memory,再把输入从 host 拷贝到 device,启动 kernel,把结果拷回;统一虚拟地址让指针形式看起来相近,数据是否已迁移、能否被该设备直接访问仍是运行时问题。离散 GPU 上 PCIe/NVLink10 的传输比 device DRAM 访问慢得多,反复在每个小 kernel 前后复制数据会淹没计算收益。

课件画出 CPU 与 GPU 经 PCI Bus 互连的关系:CPU 控制并把任务/数据交给 GPU,GPU 计算后再返回,系统里两者通过主机内存交换。

图:CPU 与 GPU 之间隔着 PCIe/NVLink 等互连;跨这条链路的拷贝比片内访问慢,是决定端到端性能的重要因素。

因此异构程序应把连续的 GPU 工作围在同一段设备驻留期内。输入一次上传,若干 kernel 在 device 上消费和产生中间张量,最后下载需要的输出。逐元素操作融合为一个 kernel、量化权重留在显存并与反量化 GEMM 融合,都会减少往返和中间全局内存流量。数据传输能和计算重叠的前提是独立 buffer、异步 copy、非默认 stream 以及足够的硬件 copy engine;不满足依赖时强行异步只会读到未完成的数据。

AI 系统中数据在 host 与 device 之间移动的完整路径。

图:输入从 CPU 端上传,kernel 在 GPU 侧消费并产生中间张量,结果再回落主机;传输路径与计算路径共同决定端到端时间。

思考题

GPU 显存带宽很高,为什么每步都把小数组从 CPU 复制到 GPU 仍可能很慢?

答案

host-device 拷贝走 PCIe 或 NVLink,并伴随 API、驱动和队列开销。小数组的传输量小,固定延迟占比高;若每次拷贝后只做很少计算,传输和启动开销会淹没计算。应尽量把数据留在 GPU 并合并传输。

CPU 与 GPU 的分工边界

设备选择由数据规模和依赖决定,而不是由语言决定。一个只有几百个元素的 kernel 可能启动开销大于计算;一个需要按 token 逐步依赖的 decode 阶段也难像巨大 prefill 矩阵那样装满 GPU。性能模型必须把 host 预处理、传输、kernel、同步都计入端到端时间。

CPU 与 GPU 在不同工作负载下的取舍:前者偏向低延迟、控制与缓存,后者偏向大规模吞吐。

图:同样的计算任务落在 CPU 或 GPU 上,瓶颈来源不同;选择设备要同时看数据规模、分支复杂度与传输开销。

思考题

一个任务只有 1000 个元素,放到 GPU 上一定更快吗?

答案

不一定。GPU 需要 launch kernel、传输数据并组织足够多的线程。数据量小时,这些固定开销可能比 CPU 直接循环更高。GPU 适合大批量、规则、可并行的计算,小任务是否迁移要实测端到端时间。

线程层次与硬件资源

GPU 程序中的 grid 是一次 kernel 启动的全部线程块,block 是能共享 shared memory 并执行 block 内 barrier 的协作单位,thread 才是一个具体的索引计算实例。block 会被整体分派给一个 SM,block 内线程再按硬件固定宽度组成 warp;一个 block 不能跨 SM,因此需要跨 block 协作的算法不能依赖普通的 block barrier。grid 可以有远多于硬件同时容纳的 block,运行时会在一个 block 完成后把后续 block 调度上来。

资源限制可用一个小例子理解。若 SM 最多驻留 2048 个线程、每个 block 256 线程,线程数上限允许 8 个 block;但若每个 block 申请大量 shared memory,或每线程使用很多寄存器,实际可能只能驻留 2 个 block。编译器生成的寄存器数、launch 的 block 大小和动态 shared memory 大小一起决定这个上限。调 block size 时要保持 warp 的整数倍、观察寄存器 spill、测量吞吐,不存在所有 kernel 通用的 256 最快。

算术强度与 GPU Roofline

把计算峰值和访存带宽放到同一张图上,就得到 GPU Roofline11。同一块 GPU 同时有极高的计算峰值和有限的 HBM12 带宽。

逐元素 y=f(x) 每加载/存储若干字节只做少量运算,常在带宽屋顶下;GEMM 则让一块 A、B 元素参与许多次乘加,算术强度13随 tile 复用提升,才有机会受 tensor core 峰值限制。把两个逐元素 kernel 融合,会少一次把中间张量写到/读回 HBM,因此即使总 FLOP 不变也可能更快。

这也是判断优化方向的约束:被 HBM 限制的 kernel 优先减少全局字节、合并访问和提高复用;被计算限制的 GEMM 才更关心 MMA tile、指令吞吐和流水。若 Nsight 显示两种资源都远未用满,原因可能是问题太小、warp 太少、频繁同步或 host 端出现空洞,而不是某条内层指令还不够快。

一个最基本的异构程序有四步:在主机端分配内存;分配 device memory 并把输入复制过去;启动 kernel;把结果复制回来并释放资源。kernel 函数用 __global__ 标记,从 CPU 端通过 <<<grid, block>>> 发射。主机代码与设备代码处在不同地址空间中,指针和数据的位置必须分清。

GPU 一启动就一定更快这种说法经常是错的。如果问题很小,kernel launch 和数据传输的固定开销可能比实际计算还大;如果每个线程只做几次运算却读写很多 global memory,也常常是带宽受限;如果线程数不够,SM 无法被填满。GPU 的高吞吐需要足够多的独立工作、规则的并行划分和合理的数据复用。

看到一个计算任务时,可以判断它是否有大量彼此独立、执行路径相近的元素操作,再看数据能否以高局部性留在 GPU 侧,最后才决定如何把循环改写成 kernel。线程块、合并访存、shared memory 和矩阵 tile 都在解决这条数据路径上的具体问题;硬件不会替串行依赖、杂乱分支和频繁搬运自动制造并行度。

把问题画成数据流

为每个输入、输出和中间张量标出所在设备、字节数、生产者与消费者。若某个中间结果只被下一个 GPU 算子使用,回传 host 通常没有意义;若下一步依赖前一步的标量决定分支,就应把这个同步点明确放进关键路径。

思考题

为什么提高占用率不一定提高 kernel 性能?

答案

占用率表示 SM 上可驻留 warp 数相对上限的比例。它有助于隐藏延迟,但如果瓶颈是内存带宽、指令吞吐或同步,继续提高占用率只会增加竞争。寄存器和共享内存用量也可能为了高占用率被迫降低,导致 tile 变小或溢出。应结合 Roofline、内存事务数和指令比例判断。


  1. 延迟。一个请求或一次操作从发出到完成的时间。 

  2. 吞吐量。单位时间完成的任务量或数据量。 

  3. PCIe,CPU 与 GPU 等设备间常用的主机总线接口。 

  4. streaming multiprocessor,GPU 内承担大量线程调度和计算的模块。 

  5. Tensor Core,NVIDIA GPU 中加速矩阵乘加的专用单元。 

  6. arithmetic logic unit,算术逻辑单元,执行整数和逻辑运算的部件。 

  7. single instruction multiple data,一条指令处理多个数据元素。 

  8. single instruction multiple threads,单指令多线程,GPU 线程组的执行方式。 

  9. warp,GPU 最小线程调度单位,通常由若干线程同时执行同一条指令。 

  10. Roofline 模型。结合计算峰值和内存带宽,估计程序能达到的性能上限。 

  11. high bandwidth memory,高带宽显存,靠近 GPU 的堆叠存储器件。 

  12. 算术强度,计算量与访存量的比值。 

有用的话请给我个 star => Stars 本站总浏览