想象一场盛大的阅兵式。指挥官站在高台上,喊一声「正步走」,台下成千上万名士兵同时迈出步伐、同时挥舞手臂——没有人需要逐个叮嘱,一条口令就驱动了千军万马。这就是 GPU 的灵魂:SIMT(Single Instruction, Multiple Threads,单指令多线程)。在 GPU 的世界里,「指挥官」是 warp 调度器,「口令」是一条机器指令,「士兵」是线程,而每 32 个士兵排成的一排,叫做一个 warp(线程束)。遇到路口时,如果方阵里有人要左转、有人要右转,指挥官只能先带一批人走完,再折回来带另一批——这个场景就是 GPU 里著名的「分支分歧」。这篇文章将用阅兵方阵作为向导,带你从零看懂 SIMT 执行模型,理解 GPU 为什么能靠「一条口令」高效并行,也理解它在什么情况下会「卡壳」。

岗位关联

掌握 SIMT 是理解 GPU 架构的基石。无论你做深度学习训练、推理引擎优化还是 CUDA 高性能编程,只有看懂 warp 调度与分支分歧,才能写出真正跑得快的 GPU 代码。

目录
  1. 可视化演示
  2. 1. SIMT核心思想:一条口令,千军万马
  3. 2. SIMT与SIMD对比:队长与舞者
  4. 3. Warp线程束:方阵的最小分队
  5. 4. Warp调度机制:指挥官的时间管理艺术
  6. 5. 分支分歧:方阵在路口走散了
  7. 6. 性能优化实践:让方阵永远整齐
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示梯度下降算法在损失曲面上的优化轨迹,通过滑块调节学习率,观察不同学习率下收敛速度和稳定性变化。

本节配有交互动效(进入视口后自动加载,约 3KB)。

梯度下降优化 沿损失函数负梯度方向更新参数 学习率过大可能导致震荡或发散 θ ← θ - η · ∇L(θ) 调节滑块改变学习率观察收敛行为

1. SIMT核心思想:一条口令,千军万马

阅兵式上,指挥官绝不会走到每个士兵面前说「你迈左脚」「你迈右脚」。他只需要喊一声口令,整支方阵就会整齐划一。GPU 的 SIMT 执行模型正是如此:硬件不关心单个线程,而是把执行单元编成方阵,一条指令同时广播给一大群线程。

CPU 是另一种风格——它更像一支特警小队:每个成员有独立的任务书,有的处理图像,有的处理网络包,各走各的。GPU 则相反,它假设你的任务是「对海量数据做同样的事」,比如把 100 万张图片统一缩放。这时方阵式管理远比小队式管理高效:没有逐个下发任务的通信开销,硬件只需不断喊口令。

落实到技术上,CUDA 程序里你写出的一个内核函数(kernel),编译后会变成一段指令序列。GPU 从这段指令中取出一条,送给当前正在执行它的所有线程。每个线程虽然执行同一条指令,但它们各自操作自己的数据——这正是「单指令多线程」的含义:指令只有一条流,数据却像阅兵方阵里每个士兵脚下的点位一样,各不相同。

回到阅兵场:指挥官的心中没有「单个士兵」这个概念,他眼里只有一排排方阵。GPU 调度器的心中也只有 warp,没有单个线程——记住这一点,SIMT 的世界就打开了一半。

Nwarp=⌈Nthread32⌉\displaystyle N_{\text{warp}} = \left\lceil \frac{N_{\text{thread}}}{32} \right\rceil
线程总数除以 32 向上取整,就得到需要多少个 warp 来装下所有线程。这是衡量 GPU 工作量的第一把尺子。
  1. 线程总数 $N_{\text{thread}}$你创建的全部 CPU 端逻辑线程数量。比如写 CUDA 时启动了 1000 个线程,这个数就是 1000。
    Nthread\displaystyle N_{\text{thread}}
  2. 每束 32 人GPU 硬件规定 32 个线程组成一个 warp,这是不可修改的编队规模。
    32\displaystyle 32
  3. 向上取整 $\lceil \cdot \rceil$线程数不一定正好是 32 的倍数,余下的零头也必须占一个 warp,所以向上取整得到所需 warp 数。
    ⌈Nthread32⌉\displaystyle \left\lceil \frac{N_{\text{thread}}}{32} \right\rceil

2. SIMT与SIMD对比:队长与舞者

如果把并行世界比作舞台,SIMD 像一支整齐划一的舞蹈队:舞蹈老师喊「向左挥手」,一整排舞者同时把手臂甩向左边,老师眼里看到的是「一排人」这个整体。而 SIMT 方阵不同,指挥官喊「正步走」后,每个士兵走向自己前方不同的点位——指令相同,站位处处不同。

SIMD(单指令多数据)在 CPU 中非常常见,比如 AVX 指令一次可操作 8 个浮点数。它的特点是:向量宽度对程序员是可见的,你要知道一次能塞进去几个数,还得手动把数据排成向量。SIMT 则把这种并行彻底抽象成了「线程」:你写的代码像串行,但硬件悄悄把每 32 个线程编成一组,一次指令处理 32 份数据。程序员不需要感知 warp 宽度,硬件宽度被完美隐藏。

两者最关键的差异在于分支处理。SIMD 处理分支非常笨拙,往往要先把数据遮罩再分别计算;SIMT 则灵活得多——遇到分支时,warp 可以屏蔽一部分线程,依次执行各个分支路径。正是这种「可屏蔽」的能力,让 GPU 能运行逻辑复杂的图形渲染和 AI 算子,而不只是纯向量计算。

舞蹈队(SIMD)动作统一却僵硬,方阵(SIMT)看似整齐却暗含灵活。GPU 最终选择 SIMT,正是因为它能在「统一指挥」和「各自发挥」之间找到平衡点。

ISIMT=⌈Ndata32⌉\displaystyle I_{\text{SIMT}} = \left\lceil \frac{N_{\text{data}}}{32} \right\rceil
SIMT 处理 N 个数据需要约 N/32 条指令周期,因为每条指令能让 32 个线程同时工作。对比之下,SIMD 需要的指令数是 N/向量宽度,而向量宽度必须由程序员写死在代码里。
  1. 数据总量 $N_{\text{data}}$待处理的元素个数,比如 10000 个待归一化的浮点数。
    Ndata\displaystyle N_{\text{data}}
  2. 每轮 32 个数SIMT 下一条指令发射给一个 warp,32 个线程各处理一个数据,所以一轮并行处理 32 个数。
    32\displaystyle 32
  3. 向上取整得到指令轮数数据不是 32 的整数倍时,最后一轮只有部分线程工作,但仍占一个完整指令周期。
    ⌈Ndata32⌉\displaystyle \left\lceil \frac{N_{\text{data}}}{32} \right\rceil

3. Warp线程束:方阵的最小分队

阅兵场上,指挥官不会对着几千人的大部队逐一下令,他面向的是「排」——排长接收口令,全排同时动作。一排便是一个 warp,GPU 中最小的执行编组。

一个 warp 固定包含 32 个线程。为什么偏偏是 32?这是硬件架构的折中选择:太小则调度开销占比高,太大则分支分歧的损失过重。32 这个数字并不神秘,但你必须记住它,因为 CUDA 编程中几乎所有性能玄机都与它有关。比如块大小(block size)取 128、256 这样的 32 倍数,就是为了确保每个块能完整地切分成整数个 warp。

warp 内每个线程拥有独立的寄存器文件,也就是说,每个士兵虽然站在同一排,但他有自己的装备和行囊。这带来的结果有两面:好处是线程之间数据互不干扰,坏处是寄存器总量有限,warp 越多每个线程能用的寄存器就越少,二者此消彼长。另外,同一个 warp 内的线程共享同一条指令流,因此它们天然处于「隐式同步」状态——谁也不能比同伴多走一步。

方阵的整齐是有代价的:为了保持队形,所有人都得等最慢的人。GPU 解决这个问题的办法不是让方阵变得松散,而是准备更多方阵轮流上场——这正是下一节调度机制的由来。

Twarp=max⁡i=132(Ti)\displaystyle T_{\text{warp}} = \max_{i=1}^{32}(T_i)
一个 warp 执行一条指令的耗时,等于最慢的那个线程的耗时。就像方阵前进的速度取决于走得最慢的士兵。
  1. 遍历每个线程 $i$从 1 数到 32,代表 warp 内的 32 位士兵。
    i=1,2,…,32\displaystyle i=1,2,\dots,32
  2. 单个线程耗时 $T_i$每份数据不同,线程完成指令的时间可能不一样,比如有的数据访存快、有的访存慢。
    Ti\displaystyle T_i
  3. 取最大值 $\max$warp 必须等所有线程都完成才能执行下一条指令,所以整体耗时由最慢者决定。
    max⁡i=132(Ti)\displaystyle \max_{i=1}^{32}(T_i)

4. Warp调度机制:指挥官的时间管理艺术

阅兵时,指挥官绝不会让整支队伍傻站在原地等某个方阵整理队形。他会这样安排:第一排走完一步,趁着他们调整步伐的空隙,马上给第二排下口令;第二排走完,又轮到第三排……GPU 的 warp 调度器就是这个时间管理大师。

在 GPU 的流式多处理器(SM,Streaming Multiprocessor)内部,有多个 warp 调度器。每个调度器每个时钟周期可以选择一个就绪的 warp,发射一条指令。当一个 warp 遇到访存操作——内存延迟动辄几百个周期——它就会进入等待状态。此时调度器立刻切换到另一个不依赖内存结果、已经就绪的 warp 继续发射指令。这个「不等闲、不空转」的技巧,叫延迟隐藏。

与 CPU 线程切换动辄数十个周期的上下文保存恢复开销不同,warp 切换几乎零成本。为什么?因为每个 warp 的线程寄存器都常驻在物理寄存器堆中,切换只是调度器把发射指针从 A warp 挪到 B warp,没有任何数据要搬进搬出。所以,GPU 上「更多 warp」通常意味着「更高吞吐」——warp 越多,调度器越容易找到就绪的 warp 来填充流水线。

指挥官的高明不在于喊得多响,而在于永远不让人闲着。GPU 吞吐的秘密也正在于此:用大量 warp 轮转来填满每一个计算周期。

Nhide=⌈LlatencyTissue⌉\displaystyle N_{\text{hide}} = \left\lceil \frac{L_{\text{latency}}}{T_{\text{issue}}} \right\rceil
要完全隐藏内存延迟,至少需要「延迟周期数 ÷ 发射间隔」个 warp。这是估算 GPU 需要多少并行度才能跑满的理论依据。
  1. 延迟周期数 $L_{\text{latency}}$内存访问需要等待的时钟周期,例如 600。
    Llatency\displaystyle L_{\text{latency}}
  2. 发射间隔 $T_{\text{issue}}$调度器发出一条指令所花的时间,理想情况下每周期可发射一条,即间隔为 1。
    Tissue\displaystyle T_{\text{issue}}
  3. 两者相除后向上取整在等待内存的 600 个周期里,调度器最多还能发射 600 条其他 warp 的指令,所以至少需要 600 个 warp 在轮转,流水线才不会断档。
    ⌈LlatencyTissue⌉\displaystyle \left\lceil \frac{L_{\text{latency}}}{T_{\text{issue}}} \right\rceil

5. 分支分歧:方阵在路口走散了

阅兵方阵行进到一个分岔路口:24 个士兵要向左走,8 个士兵要向右走。指挥官手里只有一条口令,他不可能同时喊出「左转」和「右转」。于是他先带 24 人向左走一段,让他们原地待命;再折回路口,带 8 人向右走完。整个过程,道路上一半的时间是空的。在 GPU 术语里,这种「同一 warp 内线程走向不同分支」的现象,叫做分支分歧(divergence)。

当 warp 内条件判断出现不同结果时,硬件并不是并行执行两个分支,而是把线程按分支方向分组,逐个执行。执行 THEN 分支时,走 ELSE 的线程被「屏蔽」——它们不参与计算,但也不释放执行槽位;执行完 THEN,调度器再折回来发射 ELSE 分支的指令,轮到 THEN 组的线程被屏蔽。整个过程就是串行化:两个分支本来可以共享的周期,现在要各付一遍学费。

分支分歧的代价有多大?如果一半线程走 THEN 需要 10 个周期,另一半走 ELSE 也需要 10 个周期,总耗时为 20 个周期,而理想情况是 10 个周期——整整损失一半效率。任何形式的「同 warp 不同路」都会造成类似浪费,路径越长惩罚越重。所以 GPU 编程的第一戒律是:让同一个 warp 内的线程尽量走上同一条路。

方阵走散了不可怕,可怕的是指挥官不知道散在哪。学会在代码中「看见」分歧,是 CUDA 开发者从入门到进阶的分水岭。

E=Nactive32×100%\displaystyle E = \frac{N_{\text{active}}}{32} \times 100\%
效率等于当前分支中活跃线程数除以 32。一半线程被屏蔽时,执行效率只有 50%,余下周期全部浪费。
  1. 活跃线程数 $N_{\text{active}}$当前分支路径上真正干活的线程数,比如走 THEN 的 24 个。
    Nactive\displaystyle N_{\text{active}}
  2. warp 总线程数 32硬件始终按完整的 32 线程 warp 分配执行资源。
    32\displaystyle 32
  3. 相除并化为百分比活跃线程占比越低,被屏蔽的线程越多,吞吐浪费越严重。
    Nactive32×100%\displaystyle \frac{N_{\text{active}}}{32} \times 100\%

6. 性能优化实践:让方阵永远整齐

阅兵总指挥的最高境界,是在出发前就把目的地相同的人编进同一排,让方阵永远不需要在路口分裂。GPU 编程的优化思路与此惊人地一致。

第一条策略是「条件统一化」。把分支条件尽量提到循环外层,让整个 warp 的线程判断结果一致。例如与其在循环内写 if (i % 32 == 0),不如让第 0 号线程提前处理特殊情况,其余线程走统一路径。第二条策略是「数据重排」。很多算法中,数据是否满足条件与索引相关,比如稀疏矩阵的非零元素分布不均。若能把相同性质的数据搬运到连续的地址空间,让同一 warp 内的线程面对相同判断,分歧自然消失。

第三条策略是「合理配置线程块」。CUDA 中线程块(block)大小应取 32 的整数倍,比如 128、256,避免块尾出现不完整的 warp 浪费槽位。同时要关注占用率——同驻在 SM 上的 warp 数量与硬件上限的比值。占用率越高,调度器越容易隐藏延迟。高的占用率意味着要小心寄存器使用量:每个线程用的寄存器越多,同驻线程数就越少。

阅兵式上,最优秀的方阵不是步伐最华丽的,而是从头到尾没有一次散乱。GPU 程序亦如此:真正的高性能代码,往往不是算得最快的,而是让每个 warp 都走得最齐的。

O=Nactive warpsNmax warps\displaystyle O = \frac{N_{\text{active warps}}}{N_{\text{max warps}}}
占用率是活跃 warp 数与硬件可容纳的最大 warp 数之比。优化目标是让这个比值尽量靠近 100%。
  1. 活跃 warp 数 $N_{\text{active warps}}$当前同时驻留在 SM 上、可被调度执行的 warp 数量。
    Nactive warps\displaystyle N_{\text{active warps}}
  2. 最大 warp 数 $N_{\text{max warps}}$由 SM 的寄存器数量、线程槽位等硬件资源决定的上限。
    Nmax warps\displaystyle N_{\text{max warps}}
  3. 两者相除得到资源利用率的百分比。比值低说明硬件大量空闲,延迟无法被有效隐藏。
    Nactive warpsNmax warps\displaystyle \frac{N_{\text{active warps}}}{N_{\text{max warps}}}

AI 应用场景

深度学习矩阵乘法加速

阶段:模型训练与推理中的核心算子优化

解决的问题:矩阵乘法是深度学习中最频繁的计算,传统 CPU 串行循环或手写 SIMD 都难以压榨硬件吞吐。GPU 利用 SIMT 模型,把矩阵分块交给大量线程,每个 warp 内的线程执行完全相同的乘加指令、处理不同数据,天然无分歧,吞吐拉满。

具体案例:cuBLAS 和 cuDNN 中的 GEMM(通用矩阵乘)内核,将 128×128 的矩阵分块映射到线程块,每个线程负责多个输出元素,warp 内按固定步长取数,内存访问合并成连续事务,实现接近硬件峰值算力的性能。

推荐系统稀疏特征处理

阶段:特征工程与模型推理

解决的问题:推荐系统特征高度稀疏,不同样本命中不同特征槽位,导致 if 分支频出。若同一 warp 的特征分布差异大,分支分歧将严重拖慢推理。利用 SIMT 的分支特性,将样本按特征命中数重排分组,使同一 warp 处理分布相似的样本。

具体案例:某广告推荐引擎将用户特征向量按长度排序、分批送入 GPU,每批内部特征分布均匀,GPU 推理延迟降低 40%,同时通过调整 block size 为 256,保证 warp 完整。

Python 演示

python37 行
class SimtWarp:    """简化版SIMT warp模拟器:模拟32个线程执行同一条分支指令的开销"""     def __init__(self, data):        self.data = list(data)  # 32个元素,代表warp内32个线程的数据        self.cycles = 0         # 总执行周期        self.divergence_count = 0  # 分支分歧次数     def run_branch(self, pred, then_op, else_op):        """执行一条带分支的指令,模拟硬件对分歧的处理"""        masks = [pred(x) for x in self.data]        if all(masks) or not any(masks):            # 无分歧:所有线程方向一致,只需1个周期            self.cycles += 1            for i in range(32):                self.data[i] = then_op(self.data[i]) if masks[i] else else_op(self.data[i])        else:            # 有分歧:两个分支串行执行,共需2个周期            self.cycles += 2            self.divergence_count += 1            for i in range(32):                self.data[i] = then_op(self.data[i]) if masks[i] else else_op(self.data[i]) def main():    aligned = [i + 1 for i in range(32)]     # 全是正数:warp无分支分歧    mixed = [i - 15 for i in range(32)]      # 正负各半:warp发生分支分歧     w1 = SimtWarp(aligned)    w1.run_branch(lambda v: v > 0, lambda v: v * 2, lambda v: v - 1)    print("整齐方阵: 数据=", w1.data[0], "... 周期=", w1.cycles, " 分歧数=", w1.divergence_count)     w2 = SimtWarp(mixed)    w2.run_branch(lambda v: v > 0, lambda v: v * 2, lambda v: v - 1)    print("分歧方阵: 数据=", w2.data[0], "... 周期=", w2.cycles, " 分歧数=", w2.divergence_count) if __name__ == "__main__":    main()
目的:用 32 个数字代表一个 warp,演示同一分支指令在无分歧和有分歧两种情况下的执行周期差异,帮你直观感受分支分歧的代价。
  1. class SimtWarp:定义一个简化版 warp 模拟器,用 32 个元素的列表模拟 32 个线程的数据。
  2. self.divergence_count = 0统计分歧次数,后缀 _count 避免与将来可能的方法名冲突。
  3. run_branch(self, pred, then_op, else_op)模拟一条带条件分支的指令执行过程。
  4. masks = [pred(x) for x in self.data]先算出每个线程的条件结果,相当于硬件判断哪些线程走哪条路。
  5. if all(masks) or not any(masks):所有线程方向完全一致时,硬件只需执行一次,周期加 1。
  6. self.cycles += 2有分歧时硬件先执行 THEN 再执行 ELSE,总线周期翻倍。
  7. self.data[i] = then_op(...) if masks[i] else else_op(...)每个线程按自己的方向计算结果,互不干扰。
  8. aligned = [i + 1 for i in range(32)]构造全是正数的数据,模拟一个整齐划一的方阵。
  9. mixed = [i - 15 for i in range(32)]构造一半正数一半负数的数据,模拟一个走到路口的方阵。
  10. if __name__ == "__main__":程序入口,让脚本可直接运行。

常见误区

误区既然都是「单指令、多数据并行」,SIMT 和 SIMD 应该只是叫法不同,原理上没区别吧?我写代码时不用特意区分它们。

正解SIMD 的向量宽度是硬件暴露给程序员的(比如 AVX 一次处理 8 个浮点数),程序员要手动排数据;SIMT 则把并行抽象成大量线程,warp 宽度 32 对程序员透明,并且每个线程拥有独立寄存器,遇到分支还能屏蔽部分线程。SIMT 的灵活性和可编程性远高于 SIMD,这也是 GPU 选择 SIMT 的原因。

为何常见:两者都强调「Single Instruction」,名字太像,初学者很容易把「数据并行」和「线程并行」混为一谈。

误区写 CUDA 时我创建了几百个线程,GPU 当然是一个一个地调度它们,跟 CPU 多线程差不多。

正解GPU 硬件调度与执行的基本单位是 warp(32 个线程),不是单个线程。调度器每次发射一条指令给整个 warp。如果线程块大小不是 32 的倍数,最后一个 warp 就会有空闲槽位,白白浪费执行资源。所以 CUDA 最佳实践中 block 大小一律取 32 的倍数。

为何常见:CPU 按线程调度的概念先入为主,很容易把单线程粒度的思维惯性迁移到 GPU 上。

误区线程越多切换越频繁,GPU 线程太多肯定会有调度开销,所以精简线程数量能提升性能。

正解GPU 恰恰相反——线程是隐藏延迟的资源。只要不超出寄存器和内存上限,活跃 warp 越多,调度器越容易找到就绪 warp 来填充流水线,吞吐越高。真正的性能瓶颈往往不是线程太多,而是占用率太低导致延迟无处隐藏。

为何常见:CPU 上下文切换开销大的经验深入人心,被错误地套用到了 warp 切换几乎零成本的 GPU 上。

误区GPU 有几千个核心,warp 发生分支时,几个分支当然可以同时在闲置的核心上各跑各的,互不影响。

正解一个 warp 只有一条指令流。遇到分歧时,硬件把同一 warp 的线程分成多组,依次执行各分支路径,执行 THEN 时屏蔽 ELSE 线程,执行 ELSE 时反向屏蔽。分支路径是串行的,不是并行的。唯一的例外是不同 warp 之间可以各自执行不同指令,但同一个 warp 内不可能真正并发。

为何常见:直觉上「核多=并发多」,人们容易把「多核并行」误解为「多分支并行」,忽略了 warp 内指令流唯一的约束。

误区既然 warp 大小会影响性能和分歧损失,那我是不是可以把 warp 改成 16 或 64,让程序更灵活?

正解warp 大小 32 是 GPU 硬件架构的固定参数,由指令发射端口宽度、寄存器文件划分等物理设计决定,软件无法修改。程序员只能通过调整线程块大小、分支结构等软性手段,去「适配」这一硬件规则,而不是反过来改变它。

为何常见:软件可配置参数太多,容易让人误以为硬件组织方式也能像线程数一样自由设定。

面试题

Q1. 在 NVIDIA GPU 的 SIMT 执行模型中,一个 warp 包含多少个线程?

单选题 · easy

  • A.8 个线程
  • B.16 个线程
  • C.32 个线程← 正确答案
  • D.64 个线程

答案:C。warp 是 GPU 调度的基本单位,硬件固定将 32 个线程编为一组。8、16、64 都是干扰项,mGPU 上不存在这些 warp 规格。

Q2. 下列哪项是 SIMT 与 SIMD 最本质的区别?

单选题 · easy

  • A.SIMT 用于 GPU,SIMD 用于 CPU,二者互不相干
  • B.SIMT 是线程级并行且宽度透明,SIMD 是数据级并行且宽度可见← 正确答案
  • C.SIMT 比 SIMD 快十倍以上
  • D.SIMT 不需要处理分支,SIMD 需要处理分支

答案:B。SIMT 把并行抽象为线程,warp 宽度对程序员透明;SIMD 的向量宽度由程序员显式管理。二者应用领域有重叠,SIMT 也并非更快,更不是不处理分支。

Q3. 有一个 warp 内发生分支分歧,16 个线程执行 THEN 分支,16 个执行 ELSE 分支,各需 10 个周期。总执行周期是多少?

单选题 · easy

  • A.10 个周期
  • B.20 个周期← 正确答案
  • C.40 个周期
  • D.5 个周期

答案:B。同一 warp 的指令流只有一条,THEN 与 ELSE 必须串行执行,总周期为 10+10=20。如果全部线程走同一分支则只需 10 个周期,这正是分歧的代价。

Q4. 关于 warp 调度与延迟隐藏,下列说法正确的是?

单选题 · mid

  • A.warp 切换需要保存和恢复大量寄存器状态
  • B.访存等待期间调度器切换到就绪 warp 以隐藏延迟← 正确答案
  • C.只有单线程执行完才能切换下一个 warp
  • D.调度器每周期必须执行完所有 warp 才发射指令

答案:B。GPU 的线程寄存器常驻物理寄存器堆,warp 切换接近零成本,无需保存恢复状态(A 错)。访存阻塞时切换到其他就绪 warp 正是延迟隐藏的核心机制(B 对)。调度器按周期独立选择就绪 warp 发射指令,无需等待其他 warp 完成(C、D 错)。

Q5. 下列哪些措施可以有效减少分支分歧带来的性能损失?

多选题 · mid

  • A.将条件分支移到 warp 边界之外← 正确答案
  • B.用条件表达式替换 if 语句
  • C.重新组织数据使同分支数据集中在同一 warp← 正确答案
  • D.增大线程数量

答案:AC。A 让整个 warp 统一判断方向,能从根源消除分歧;C 通过数据重排使同方向线程聚在同一个 warp;B 的条件表达式本质上仍是分支判断,无法消除分歧;D 增大线程数量可能增加无分歧 warp 的占比,但不能直接减少已有 warp 的分歧,不是有效措施。

Q6. 某 GPU 调度器每周期可发射 1 条指令,内存延迟为 600 个周期。理论上至少需要多少个 warp 才能完全隐藏访存延迟?

单选题 · mid

  • A.32 个
  • B.64 个
  • C.600 个← 正确答案
  • D.600/32 个

答案:C。隐藏延迟需要让调度器在等待期间始终有就绪 warp 可发射。延迟 600 周期意味着至少 600 次发射机会需要被填充,因此至少需要约 600 个轮转中的 warp。32 和 64 远不足以填满延迟窗口。

Q7. 关于 SIMT 执行模型中的线程束,以下说法正确的是?

多选题 · mid

  • A.warp 是 GPU 执行与调度的基本单位← 正确答案
  • B.warp 内线程天然隐式同步← 正确答案
  • C.调度器按周期轮转发射指令到不同 warp← 正确答案
  • D.单个 warp 可以同时执行多条不同指令

答案:ABC。warp 是调度的最小单位(A 对);warp 内线程共享指令流,天然同步(B 对);调度器在一个周期内选择就绪 warp 发射指令,靠轮转隐藏延迟(C 对)。单个 warp 同一时刻只能执行一条指令,不同指令并行发生在 warp 之间(D 错)。

Q8. 已知有 10000 个线程,每个 SM 最多同驻 2048 个线程,那么每个 SM 需要多少轮才能处理完所有线程?

单选题 · hard

  • A.5 轮← 正确答案
  • B.10 轮
  • C.16 轮
  • D.32 轮

答案:A。10000 ÷ 2048 ≈ 4.88,向上取整为 5 轮。这里 2048 是线程槽位上限而非 warp 数,注意与「warp 是 32 线程」并不冲突,2048 线程恰好对应 64 个同驻 warp。

Q9. 一个 warp 内 32 个线程执行 if 分支,其中 24 个线程走 THEN(需 10 周期),8 个走 ELSE(需 5 周期)。该 warp 执行此分支的周期开销为多少?

单选题 · hard

  • A.10 个周期← 正确答案
  • B.5 个周期
  • C.15 个周期
  • D.7.5 个周期
  • E.矩阵乘天然无分歧,SIMT 可高效并行
  • F.分支分歧无法避免,只能接受性能损失
  • G.SIMT 只适合训练阶段的算子
  • H.warp 大小可通过编译器动态调大

答案:A。矩阵乘的每个输出计算路径一致,几乎没有分支,天然契合 SIMT 的「统一口令」模式(A 对)。分歧可通过数据重排与条件外提等策略缓解(B 错)。推理算子同样运行在 GPU 上,大量使用 SIMT(C 错)。warp 大小由硬件固定为 32,编译器无法改变(D 错)。