想象你是一家超大规模“并行工厂”的车间主任。GPU 就是这座工厂,里面没有上百人的生产线,而是上万个工位同时开工。作为主任,你手里有一份超大订单,比如生产 10 万件 T 恤。你不会把订单直接砸给一个工人,而是把它分成大项目(Grid)、中项目(Block)、再分到一个个工人(Thread)手里。工人也不是单独行动的——32 个工人必须同乘一辆“班车”(Warp)进出工位,班车停,所有人同步开工;中途遇到岔路(if/else),班车只能先送一批人去剪裁、再送另一批人去熨烫,谁先谁后指令说了算。小组内部有一张公共工作台(共享内存),组员们把半成品放上去互相传递;组长吹一声哨(Barrier),所有人先放下手里的活,等组员都准备好了再一起进入下一道工序。工厂的“上座率”(Occupancy)则决定了工位是否被充分利用——班车明明能坐 32 人,却因为工具箱(寄存器)塞得太满只能坐 16 人,那另一半工位就白白空着。这篇内容,就是带着你把这个工厂的每个层级、每束调度、每个资源约束都走一遍。
掌握 GPU 线程模型,是写出高性能 CUDA 内核的必修课。无论是深度学习算子优化、图像处理还是科学计算,它决定你能否把 GPU 的上万核心真正“喂饱”。
可视化演示
展示CUDA三大核心抽象:Thread-Block-Grid三级层次结构、32线程Warp的SIMT调度机制、以及不同Block大小下共享内存与寄存器的占用率变化。交互切换不同视角。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Thread-Block-Grid三级层次 Grid包含多个Block,Block包含多个Thread 中心Block展开显示4×4=16个Thread total = gridDim × blockDim GPU通过三级层次组织并行计算资源
线程层级结构
回到那座并行工厂。车间主任接了一个生产 10 万件 T 恤的大单,他不会把订单直接丢给某个工人,而是先把单子拆成“大项目”(Grid),每个大项目派给若干个“生产小组”(Block),小组里再分到一个个“工人”(Thread)。这一层套一层的嵌套,就是 CUDA 线程模型最核心的组织结构:一个 Grid 由多个 Block 组成,一个 Block 由多个 Thread 组成。
用 CUDA 的话说,Grid 和 Block 都可以是一维、二维或三维的索引空间。当我们启动内核时,写出 gridDim.x 表示网格在 x 方向上有几个 Block,blockDim.x 表示每个 Block 在 x 方向上有几个 Thread。这种嵌套关系决定了任务如何被逐级分解:
这个嵌套结构就像俄罗斯套娃——打开大项目是小组,打开小组是工人。理解了这层嵌套,后面的共享内存、同步、调度才有安放的位置。
- gridDim.x网格在 x 维上包含的块数。就像订单里分出了几组生产小组,这个数告诉 GPU 总共有多少个 Block 等着被调度。
- blockDim.x每个块在 x 维上包含的线程数。一个生产小组里分配了几名工人,决定了每个 Block 能并行处理多少份数据。
- 两者相乘把“组数 × 每组人数”得到所有能同时参与生产的工人总数,也就是整个内核启动后一共会产生多少个线程。
SIMT执行模型
工厂里有一条规定:同乘一辆班车的 32 个工人,必须同步开工、同步收工、做一模一样的动作。这就是 GPU 的 SIMT(单指令多线程)执行模型。CPU 像一位博学的教授,一次只专心讲一章;GPU 更像教官,一声口令“向左转!”,几百个工位上的工人齐刷刷同时转身——一条指令驱动成百上千个线程同步执行。
不过,工人会遇到岔路。假设指令是“如果布料是红色的就裁剪,否则熨烫”,一个 warp 里有 16 个红色布料工人、16 个非红色布料工人,教官怎么办?他只能先喊“裁剪的去做”,16 人动;再喊“熨烫的去做”,另外 16 人动。两条路径被串行执行,时间翻倍。这就是 SIMT 里著名的“分支发散”(divergence)问题:
理解了 SIMT,你就明白了 GPU 高性能的秘诀和陷阱:它强在“同一动作复制万份”,弱在“各做各的”。写内核时,让线程尽量走同一条路,班车就能一趟跑完。
- t_if走 if 分支的那部分线程执行所需的时间。教官先带 16 个工人完成“裁剪”动作,耗时 t_if。
- t_else走 else 分支的那部分线程执行所需的时间。完成后教官再带另外 16 个工人做“熨烫”,耗时 t_else。
- 相加由于同一时刻 warp 只能执行一条指令,两个分支无法并行,只能先做完一支再做另一支,总耗时是二者之和。这就是为什么写 CUDA 代码时要尽量避免 warp 内的线程分道扬镳。
Thread执行单元
每个工人(Thread)是工厂里最小的执行单元。他有一个只属于自己的私人工具箱(寄存器),里面放着他正在处理的数据;他的工位上有独立的操作通道,可以自己取布料、自己放成品。但注意:工人的私人工具箱非常小——一个线程通常只有几十个 32 位寄存器可用,放不了多少东西。如果你让一个线程背上几千个私人物品,GPU 就不得不把多余的“行李”寄存到远处的仓库(本地内存),速度瞬间掉下来。
在 CUDA 中,每个线程有唯一的 threadIdx(在块内的编号)和 blockIdx(块在网格中的编号)。工人干活时,通过这两个编号知道自己是谁、该处理哪块布料。线程的私有属性还包括它消耗的寄存器数量,这个数字直接影响后面要讲的“上座率”:
线程是“最小但独立”的工人:有自己的工具、自己的编号、自己的活。但一个人干不成大项目,工人必须加入小组才能协作——这就引出 Block。
- R_thread每个线程运行内核时申请的寄存器个数,由编译器根据代码复杂度决定。工人开工前要领取自己的工具数量。
- blockDim.x块内线程数。一个小组里有几名工人,每个人都要领一套工具。
- 相乘整个块的寄存器需求。GPU 每个 SM(流式多处理器)的寄存器总量是固定的——相当于车间每个工位区的工具柜总数固定,小组领走的工具多了,能同时开工的小组就少了。
Block线程块
工人不是孤军奋战的,他们以“生产小组”(Block)为单位坐在同一个工位区。小组内部有一张公共工作台(共享内存),所有组员都能把半成品放上去,也能拿别人放下的料。这张工作台比私人工具箱大得多,而且就在工位旁边,大家拿取极快——这正是 Block 内线程通信高效的根本原因。
但公共工作台有个规矩:组员不能各拿各的乱操作。假设 32 个工人都在往工作台上放零件,有人还没放完,有人就去拿了,那拿到的可能是残缺品。所以组长要吹哨(__syncthreads() 同步栅栏):所有人必须把手里的零件都放上工作台、举手示意,组长确认全员到齐后吹哨,大家才能开始取料。这个机制保证了共享数据的一致性:
Block 是“协作的单元”:共享内存让组员高效配合,同步栅栏让大家配合得不出错。但一个小组最多 1024 人,一个订单需要上千个小组共同完成——这些小组由 Grid 统一编排。
- S_thread每个线程在工作台上占用的空间,比如它往共享内存数组里写一个 float 就占 4 字节。工人放零件占的工作台面积。
- blockDim.x块内线程数。组里多少人,每人占一块地方,总面积自然按人数累加。
- 相乘得到整个块的共享内存申请量。SM 的工作台总面积有限,一个组占多了,能同时驻留的组就少了。
Grid网格
Grid(网格)是整个工厂的“总订单”。它把所有 Block 组织在一起,形成一个完整的任务空间。车间主任(硬件调度器)拿到 Grid 后,会把里面的 Block 一个个分配给不同的工位区(SM)执行。关键点来了:Block 与 Block 之间是彼此独立的——不同小组不需要知道对方在干什么,也不共享工作台,它们可以按任意顺序开工、完工,互不干扰。
这种“块间独立”是 GPU 能大规模并行的前提:如果所有小组都要互相通信,那整个 GPU 都会卡在等待上。正因独立,GPU 才能像流水线一样,哪个工位区空了就往里塞一个 Block。那么一个线程在全局唯一的编号如何计算?答案是双重编号:
Grid 是“全局的调度蓝图”:它定义了任务总量,而块间独立保证了 GPU 可以自由地动态调度。现在问题来了:调度的最小单位是什么?答案是 Warp。
- blockIdx.x当前线程所在 Block 在整个 Grid 中的编号。相当于第几组工人,从 0 数起。
- blockDim.x每块线程数。每个小组的人数相同,前面几组一共占了多少个工人,用组号乘人数得到。
- threadIdx.x线程在块内的编号。自己在这组里的第几个位置。
- 相加组的偏移加上组内偏移,得到的 tid 就是这位工人在整个工厂里的工号。访问数组时,常用 tid 直接定位
data[tid]。
Warp调度机制
接下来看工厂最精妙的环节——班车调度。GPU 在硬件层面并不是一个一个地调度线程,而是以 32 个线程为一束打包调度,这一束就是 Warp。32 名工人必须同乘一辆班车进出工位区,班车停靠时,32 人同时开始执行同一条指令;下一时刻,整车人同时执行下一条指令。硬件调度器就是那个排班员,它手里有很多班车,当一辆班车因等待数据而停下时,它立刻调度另一辆班车上工,用“车海战术”掩盖延迟——这就是 GPU 高吞吐的秘诀。
一个线程在 Warp 里的身份由它在线程块内的编号决定:前 32 个线程属于 Warp 0,第 33 到 64 个属于 Warp 1,以此类推。具体位置这样计算:
Warp 是 GPU 的真正执行单位:同一辆班车内的 32 人永远步调一致。这也意味着,如果你的 Block 线程数是 66,那么最后一个 Warp 只坐了 2 个人,剩下 30 个座位空着——资源被白白浪费。所以写代码时,Block 尺寸尽量凑成 32 的整数倍。
- threadIdx.x ÷ 32整除运算。块内线程号每 32 个归入同一个 Warp,就像每 32 名工人坐同一辆班车,整除结果就是班车编号。
- threadIdx.x mod 32取余运算。确定线程在这辆班车上坐的是第几个座位(0 到 31 号),这个座位号称为 lane。
占用率因素
工厂老板最关心的指标是“上座率”——Occupancy(占用率)。它衡量的是:每个工位区(SM)最多能同时容纳多少个 Warp,而当前你的内核实际用上了多少个。上座率 100% 意味着班车全部满座出发,GPU 的每个角落都在干活;上座率只有 50% 意味着有一半的班车停在车库里,工位空置。公式如下:
影响上座率的有三样“硬资源”:寄存器配额太大,每人领的工具太多,工具柜装不下那么多人;共享内存申请太多,工作台被占满;线程数超过 SM 上限,工位不够。想提高上座率,就要学会“精打细算”——少用寄存器、少占共享内存,让更多 Warp 挤上车。
- N_active_warp你的内核实际能在 SM 上同时存活的 Warp 数。受寄存器、共享内存等资源约束,不是想开几辆班车就开几辆。
- N_max_warp硬件允许的 Warp 上限。不同计算能力的 GPU 有不同的上限,比如常见的 64。
- 比值两者相除得到占用率。它不是性能的绝对保证,但占用率太低时,GPU 一定“吃不饱”,延迟无法被掩盖。
硬件参数组织线程
既然上座率这么重要,那具体怎么配置 Grid 和 Block 的大小?答案藏在硬件的规格书里。GPU 的计算能力(Compute Capability)参数表写明了各种上限:每个 Block 最多 1024 个线程、每个 SM 最多 2048 个线程、Grid 每维最大 2³¹-1、共享内存每 SM 通常 48KB 到 228KB。这些数字就像工厂的建筑图纸:车间能放多少台机器、每台机器能站几个人,都是设计死的。
三维 Block 的维度也有限制,也就是说一个 Block 的 x、y、z 三个方向的线程数相乘不能超过总上限:
有了这些数字,配置策略就清晰了:先根据数据规模定 Grid 总线程数;再选 Block 大小为 128 或 256(32 的倍数,资源利用率高);最后用 cudaOccupancyMaxPotentialBlockSize 这类 API 帮你算出理论最优值。工厂的每一寸工位都被充满,GPU 才真正物尽其用。
- blockDim.x / y / z块在三个方向上的线程数。就像小组在车间里的长、宽、高三维排布,三个方向人数相乘等于小组总人数。
- ≤ 1024硬件规定的单块人数上限。超过这个数内核直接启动失败。
AI 应用场景
深度学习矩阵乘法的 Kernel 设计
阶段:模型训练与推理的算子优化层
解决的问题:矩阵乘法是神经网络最核心的算子。传统写法让每个线程独立从全局内存取数,访存延迟高、重复读多。利用线程模型重新编排后,把一个大矩阵切成多个子矩阵 Tile,每个 Block 负责一个 Tile,Block 内线程通过共享内存协作复用数据,性能可提升数倍甚至数十倍。
具体案例:在优化 LLM 训练中的 GEMM(通用矩阵乘法)时,工程师将 4096×4096 的矩阵划分为 16×16 的 Block 网格,每个 Block 内 256 个线程(8×8 排列),每个线程处理 4×4 的微块。计算时先把 Tile 载入共享内存,再用 __syncthreads() 同步后重复利用,访存次数从上千次降到几十次。
图像卷积的共享内存加速
阶段:CV 预处理与推理部署
解决的问题:卷积操作需要反复读取相邻像素,直接访问全局内存带宽不够。利用 Block 内共享内存,让每个 Block 负责输出图像的一个小块,把该块及其边界像素一次性载入共享内存,然后所有线程从共享内存取数做卷积。
具体案例:在 YOLO 推理的图像预处理中,把 1920×1080 的图像分成 15×8 个 Block,每块 32×32 线程,共享内存里放 34×34 的带 halo 数据块。这样每个像素只从全局内存读一次,卷积计算全程走共享内存,整体推理延迟降低约 40%。
大模型推理中 FlashAttention 的 Warp 协作
阶段:LLM 推理的注意力算子优化
解决的问题:Attention 计算中 QK^T 矩阵的每一行与一个 Warp 对应,Warp 内的 32 个线程分别负责不同的头维和序列分块,通过 Warp Shuffle 指令(同一 Warp 内线程交换数据)避免把中间矩阵写回全局内存,从而省下整个注意力矩阵的显存读写。
具体案例:在部署 70B 参数模型时,推理引擎用 FlashAttention-2 把 Q 矩阵按照 Warp 划分:一个 Warp 负责 64 行的输出,32 个线程通过 shuffle 在寄存器间传递部分和,最终只需一次全局写回。显存占用降低 75%,长序列生成速度提升 2 倍以上。
Python 演示
import math class CUDAThread: """模拟单个CUDA线程,持有全局编号与寄存器占用""" def __init__(self, global_id, regs): self.id = global_id self.registers = regs class BlockSim: """模拟一个线程块:包含多个Thread,拥有共享内存""" def __init__(self, block_id, block_dim, shared_bytes): self.block_id = block_id self.block_dim = block_dim self.shared_bytes = shared_bytes self.thread_list = [] for i in range(block_dim): gid = block_id * block_dim + i self.thread_list.append(CUDAThread(gid, regs=8)) class GridSim: """模拟整个Grid:按Warp粒度调度线程执行""" def __init__(self, grid_dim, block_dim, sm_max_warps=64): self.grid_dim = grid_dim self.block_dim = block_dim self.block_list = [] for b in range(grid_dim): self.block_list.append(BlockSim(b, block_dim, shared_bytes=1024)) total_threads = grid_dim * block_dim self.warp_count = math.ceil(total_threads / 32) self.max_warps = sm_max_warps def run(self): """按Warp推进:每次让32个线程同步执行一条指令""" executed = 0 ticks = 0 while executed < self.grid_dim * self.block_dim: executed += 32 ticks += 1 return ticks def occupancy(self): """计算占用率 = 活跃Warp数 / SM最大Warp数""" active = min(self.warp_count, self.max_warps) return active / self.max_warps if __name__ == "__main__": gpu = GridSim(grid_dim=16, block_dim=256) total = gpu.grid_dim * gpu.block_dim ticks = gpu.run() print("总线程数:", total) print("总Warp数:", gpu.warp_count) print("模拟指令轮次:", ticks) print("占用率: %.1f%%" % (gpu.occupancy() * 100))
gid = block_id * block_dim + i每个线程的全局编号:块编号乘以块大小再加上块内偏移,对应正文里的 tid 映射公式。self.thread_list.append(...)把线程装进 Block 的列表中,模拟 Block 内包含 Thread 的层级结构。math.ceil(total_threads / 32)线程总数除以 32 向上取整得到 Warp 数:不足 32 人的尾巴也得算一辆班车,这就是“虚位 Warp”的来源。while executed < totalSIMT 主循环:每轮只推进 32 个线程,模拟一个 Warp 整束同步执行一条指令。active = min(self.warp_count, self.max_warps)占用率计算:活跃 Warp 数不能超过 SM 硬件上限,这个比值就是上座率。
常见误区
误区很多人以为 GPU 有上万核心,那就把 Grid 和 Block 开到最大,线程总数越多 GPU 越忙,性能自然越高。
正解线程多只是“名义上的并行度”,硬件真正执行的最小单位是 Warp。如果每个线程寄存器占用过高、共享内存申请过大,SM 能同时驻留的 Warp 反而变少,占用率下降。就像工厂订单写得再大,工位区只放得下两组人,多出来的订单只能排队。性能瓶颈往往不是线程不够,而是资源装不下。
为何常见:因为 CPU 编程习惯里“任务越多越忙”的直觉惯性,忽略了 GPU 的物理资源墙。
误区初学者常把 GPU 线程想象成 32 个独立的 CPU 核心,以为它们可以一个跑加法、一个跑乘法,互不干涉。
正解SIMT 模型下,一个 Warp 在同一时钟周期只能执行同一条指令。如果代码里有 if/else 分支,GPU 会先让满足条件的线程执行 if 分支、屏蔽其余线程,再反过来执行 else 分支——相当于班车分两趟跑,总耗时翻倍。写代码时应尽量让 Warp 内线程走同一路径,或通过重新组织数据避免分歧。
为何常见:因为“多线程”这个词容易让人联想到 CPU 的多核并行,忽略了 GPU 是“单指令广播”的架构。
误区有人以为共享内存和全局内存一样,写完立刻别人就能读到最新值,所以直接写共享内存数组再让别的线程读,结果拿到脏数据。
正解共享内存没有自动的一致性保证。一个线程写、另一个线程读,必须用 __syncthreads() 在中间做栅栏同步——所有线程到达这个点后才能继续。就像工作台上放零件,必须等所有人都放完、组长吹哨,大家才能取料。遗漏同步会导致数据竞争,而且这类 bug 时隐时现,极难排查。
为何常见:因为常规并发编程里“可见性”是语言运行时保证的,而 CUDA 把同步的职责完全交给了程序员。
误区既然 Block 内通信方便,那干脆把 Block 设成 1024 线程,能多协作就多协作。
正解Block 过大有两个问题:一是每个 SM 能容纳的 Block 数量有限,大块会让调度不够灵活,资源碎片化;二是共享内存和寄存器按块分配,块越大每个线程分到的份额越小,甚至导致内核启动失败。业界常用 128 或 256 的 Block,既能跑满 Warp,又给资源留有余地。工厂里不是组越大越好,而是要让每个工位区都能塞进合适数量的小组。
为何常见:因为“通信方便”听起来诱人,忽略了硬件对 Block 驻留数量的限制。
面试题
Q1. 在 CUDA 线程层次结构中,GPU 调度的最小执行单元是?
- A.Thread 线程是执行的最小单元← 正确答案
- B.Block 块是执行的最小单元
- C.Grid 网格是执行的最小单元
- D.Kernel 内核是执行的最小单元
答案:A。Thread 是真正执行计算的最小单元,Block 和 Grid 只是逻辑组织容器,内核则是整个程序。就像工厂里真正干活的是工人,小组和订单只是把人组织起来的方式。
Q2. 一个 Warp 中包含多少个线程?
- A.8 个线程为一束共同调度
- B.16 个线程为一束共同调度
- C.32 个线程为一束共同调度← 正确答案
- D.64 个线程为一束共同调度
答案:C。Warp 是硬件定义的调度单位,固定为 32 个线程。32 人同乘一辆班车,班车停大家一起停。其他数字都不是 CUDA 的 Warp 规格。
Q3. Block 内线程之间最常用的高效通信方式是?
- A.通过全局内存传递中间数据
- B.通过共享内存协作交换数据← 正确答案
- C.通过网络接口远程交换数据
- D.通过文件系统落盘后再读取
答案:B。共享内存在 SM 内部,延迟远低于全局内存,是 Block 内协作的首选。相当于小组的工作台在工位边上,比跨车间传递快得多。全局内存虽可用但慢,网络和文件系统根本不在 GPU 编程的考虑范围。
Q4. 全局线程编号计算式 tid = blockIdx.x * blockDim.x + threadIdx.x 中,blockDim.x 的含义是?
- A.网格中块在 x 方向的个数
- B.块中线程在 x 方向的数量← 正确答案
- C.设备上流多处理器的总数
- D.线程在 x 方向上的步长间隙
答案:B。blockDim 描述的是“块的大小”,即每个块里有多少线程。blockIdx 是第几块,threadIdx 是块内第几个。三者相乘相加得到全局编号,就像“组号 × 每组人数 + 组内序号”算出总工号。
Q5. SIMT 模型中,若一个 Warp 内一半线程走 if 分支、一半走 else 分支,执行结果是?
- A.两条分支同时并行执行互不干扰
- B.两条分支串行执行总耗时翻倍← 正确答案
- C.只执行人数多的那个分支路径
- D.整个内核抛出异常终止运行
答案:B。SIMT 规定同一 Warp 同时刻只能执行一条指令,所以 GPU 会先执行 if 路径、屏蔽另一半线程,再执行 else 路径。班车只能先送一批人再去接另一批,无法同时去两个地方。
Q6. 下列哪些因素会影响 GPU 的 Occupancy(占用率)?
- A.每个线程使用的寄存器数量← 正确答案
- B.每个块申请的共享内存大小← 正确答案
- C.SM 最大可容纳的线程总数← 正确答案
- D.主机端硬盘的连续读写速度
答案:ABC。占用率由 SM 内三大资源决定:寄存器总量、共享内存总量、线程驻留上限。硬盘读写与 GPU 内核执行无关,属于主机端 I/O,不参与 SM 资源配额。上座率能坐多少人,取决于工人工具箱、工作台和工位三个硬限制。
Q7. 现代 CUDA GPU 中,一个线程块最多允许有多少个线程?
- A.最多允许 256 个线程
- B.最多允许 512 个线程
- C.最多允许 1024 个线程← 正确答案
- D.最多允许 2048 个线程
答案:C。硬件规定 blockDim 三个维度乘积不超过 1024。这是工厂设计图纸上的工位区上限,超过则内核启动失败。256 和 512 是常用配置但不是上限,2048 超出了硬件约束。
Q8. 以下哪些情况会导致 Warp 执行效率明显下降?
- A.Warp 内出现 if/else 分支并且两组都执行← 正确答案
- B.内存访问时相邻线程读取相邻地址
- C.线程等待共享内存数据产生束内停滞← 正确答案
- D.Block 内线程数不是 32 的整数倍← 正确答案
答案:ACD。分支发散让 Warp 串行执行多条路径;等待共享内存会停住整辆班车;线程数不是 32 的倍数会造成最后一个 Warp 虚位。而相邻线程读相邻地址正是合并访问,是最高效的内存模式,属于加速而非降效。
Q9. 每个线程寄存器用量增大时,对 Occupancy 的影响是?
- A.寄存器多能提升运行速度,占用率变高
- B.每线程寄存器越多同时驻留的线程越少← 正确答案
- C.寄存器数量与占用率之间完全无关
- D.共享内存变大就能抵消寄存器的影响
答案:B。SM 的寄存器文件总量固定(如 65536 个),每个线程分到的寄存器越多,能同时容纳的线程总数就越少,占用率随之下降。工具箱占地方太多,工位区自然站不下那么多人。共享内存和寄存器是两套独立资源,无法互相抵消。
Q10. 关于 Grid 与 Block 的关系,下列说法正确的是?
- A.Block 间共享内存可直接同步协作
- B.Block 间执行顺序不确定且相互独立← 正确答案
- C.Grid 的维度决定了任务的总规模← 正确答案
- D.Block 内协作依赖共享内存加同步← 正确答案
答案:BCD。共享内存只存在于 Block 内部,Block 之间无法直接用它通信,A 错。GPU 不保证 Block 的执行顺序,它们相互独立,这让调度器能动态填充空闲 SM,B 对。Grid 维度 × Block 维度 = 总线程数,C 对。Block 内线程用共享内存交换数据时必须搭配 __syncthreads() 同步,D 对。