想象一支出色的仪仗队正在操场训练。队长一声令下“向左转”,32名士兵必须同一瞬间整齐转向——不是各自为战,而是像一个人那样行动。这32人的小队,就是GPU里的一个"Warp"。GPU里有上万个线程,但它们并不是自由的个体,而是被分成一个个32人的小队,每队统一行动、同步执行同一条指令——这就是GPU的锁步魔法。 但协同也有麻烦:队长喊“受伤的出列休息,其他人继续训练”,小队得分两拨依次行动,时间翻倍,这叫分支发散。再比如领装备:32人排队到相邻窗口一次领完是合并访存;分散到天南海北就得到处跑,效率暴跌。为了让小队永不停歇,总教官会在一个小队等装备时立刻调另一队补上——延迟就这样被藏了起来。 这篇文章就从这支仪仗队出发,带你搞懂Warp是什么、线程怎么分组、锁步如何执行、发散怎样应对、访存怎么合并、调度器如何隐藏延迟,最后落到实际优化上。

岗位关联

学懂Warp调度,你就掌握了GPU性能优化的第一性原理——无论做大模型推理、训练加速还是图像渲染,都绕不开“32线程如何协同”这道基本功。

目录
  1. 可视化演示
  2. Warp基本概念
  3. 线程组织分配
  4. 锁步执行细节
  5. 分支发散问题
  6. 发散应对策略
  7. 合并访存优化
  8. 调度与延迟隐藏
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

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

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

Warp基本概念

操场边,仪仗队总教官拿起喇叭喊“向右看——齐!”,32名士兵瞬间整齐甩头。这不是巧合,而是这支队伍的铁律:所有人只服从一条命令,在同一拍完成同一个动作。在GPU这座“数字阅兵场”里,同样的铁律也在上演,主角就是被称作 Warp 的32人小队。

GPU芯片里跑着成千上万个“微型士兵”——线程。硬件并不一个一个地指挥它们,而是每32个线程编成一支队伍,整队调度。这支队伍就是 Warp,它是GPU硬件调度的最小单位:调度器说“执行这条指令”,32个线程就一起执行,一个不能少,一个也不多。

这种“一个教官、32个士兵、同一步伐”的模型,在计算机体系结构里叫 SIMT(Single Instruction, Multiple Threads,单指令多线程)。它和 CPU 的 SIMD 不同:SIMT 的每个线程都有自己的寄存器,只是硬件强制它们在同一个节拍执行同一条指令。每个线程都能用自己的编号算出归哪支队伍管:

回到操场,士兵看自己的编号就能算出属于第几排;GPU线程拿到 thread_id,除以32就知道归哪支 Warp 管。把“队伍”这个概念刻进脑子,后面所有的优化招式都好懂了。

warp_id=⌊thread_id32⌋\displaystyle \text{warp\_id} = \left\lfloor \frac{\text{thread\_id}}{32} \right\rfloor
warp_id 就是“队号”:拿线程编号除以32,向下取整,得到它是第几支队伍。
  1. thread_id(线程编号)核函数中每个线程都有一个全局唯一编号,相当于花名册上的序号。
    thread_id\displaystyle \text{thread\_id}
  2. 除以32(整除)32人编成一队,除法得到的商就是队伍编号,向下取整保证不超编。
    ⌊thread_id32⌋\displaystyle \left\lfloor \frac{\text{thread\_id}}{32} \right\rfloor
  3. warp_id(队号)GPU调度只认队号,同一队的32个线程同进同退。
    warp_id\displaystyle \text{warp\_id}

线程组织分配

总教官拿到128人的新兵名单,第一件事就是喊:“1到32号,第一队!33到64号,第二队!”——编号决定归属,归队顺序一丝不乱。GPU在启动一个核函数时,同样按线程编号把线程分成一支支 Warp,这个过程叫 Warp 划分。

在 CUDA 里,每个线程有一个全局编号 thread_id,它由线程块(Block)编号和块内线程编号组合而成。对硬件来说,它不需要理解 Block 的语义,只需要把连续编号的线程每32个分成一队:编号除以32的商是 warp_id(队号),余数是 lane_id(队内位置)。

这里有个容易被忽视的细节:Block 的线程数最好设计成32的整数倍。如果 Block 有48个线程,硬件照样分成两支队——第二支队只有16个士兵,剩下16个名额空着不上场,白白浪费资源。线程对齐(alignment)对GPU来说,就像把新兵名单排好队再发车,排面不齐就有空位。

把编号规则想成编队口令就简单了:先说你属于哪一排(warp_id),再数数你在排里的第几个(lane_id)。GPU硬件调度只认这个,Block 边界在它眼里只是“口令里的换行符”。

thread_id=warp_id×32+lane_id\displaystyle \text{thread\_id} = \text{warp\_id} \times 32 + \text{lane\_id}
反过来用:给出队号和队内位置,乘以32再加偏移,就能还原线程的全局编号。
  1. warp_id(队伍编号)线程属于第几支 Warp,由 thread_id 除以32的商决定。
    warp_id\displaystyle \text{warp\_id}
  2. 乘以32(队伍起点)每支队伍占32个编号,队号乘以32得到队内0号线程的全局编号。
    warp_id×32\displaystyle \text{warp\_id} \times 32
  3. 加上lane_id(队内位置)再加上0到31的队内偏移,就得到该线程的全局编号。
    warp_id×32+lane_id\displaystyle \text{warp\_id} \times 32 + \text{lane\_id}

锁步执行细节

仪仗队最震撼的瞬间,是上百人踢正步时“咔”的一声齐响。那声音来自铁律:所有人必须同一拍落地。GPU 的 Warp 也是如此——不是“尽量同步”,而是硬件结构的必然:一条指令从取指到执行,整支队伍只有一个节拍。

具体来说,Warp 的指令流水线这样工作:调度器选好一支 Warp,把它的下一条指令取出、解码,然后一次性广播给32个线程的执行单元。指令取指和解码只做一次,执行单元却在32条“线”上并行——这就是“单指令多线程”在硬件层面的实现方式。

因此,衡量 GPU 执行效率的核心指标是 IPC(Instructions Per Cycle,每周期指令数)。每个时钟周期,调度器能让多少条指令完成发射,直接决定吞吐量。锁步还带来一个隐藏福利:同一 Warp 内天然同步,不需要像 CPU 多线程那样加锁对齐。

就像仪仗队的“齐步走”让整排人同时起步,锁步执行让线程之间的协作几乎零成本。但记住:队伍越“齐”,一旦有人想单独行动,麻烦就来了——这正是下一节要讲的分支发散。

IPC=已完成指令数时钟周期数\displaystyle \text{IPC} = \frac{\text{已完成指令数}}{\text{时钟周期数}}
IPC 告诉我们每个时钟周期完成几条指令,是调度器吞吐能力的直观标尺。
  1. 已完成指令数(分子)队伍完成一次完整动作,比如“踢腿”或“转向”,就算一条指令执行完毕。
    已完成指令数\displaystyle \text{已完成指令数}
  2. 时钟周期数(分母)教官挥旗的节拍次数,也就是 GPU 的心跳。
    时钟周期数\displaystyle \text{时钟周期数}
  3. IPC(每周期指令数)一个心跳能完成几次动作,衡量调度器吞吐能力的核心指标。
    已完成指令数时钟周期数\displaystyle \frac{\text{已完成指令数}}{\text{时钟周期数}}

分支发散问题

教官忽然喊道:“左脚有伤的士兵向左转,其他人向右转!”32个人的队伍愣住了——仪仗队的铁律是全体一个动作,现在却出现两种指令。硬件只能这样处理:先让左脚有伤的士兵完成向左转,其他人立正等;再让其他人向右转,伤员们立正等。本来一拍能做完的事,现在要两拍。

这就是 warp divergence(分支发散)。当 Warp 内不同线程在 if-else 处走了不同分支,硬件无法同时执行两条路径,只能把两条路径串行化:先执行条件为真的那部分线程(其余线程被掩蔽,处于空闲状态),再执行条件为假的部分。总耗时从“一条路径”变成“两条路径之和”。

发散的代价和两支路的规模有关。如果一支路只有1个线程、另一支路有31个,硬件照样得为这1个线程单独跑一遍完整路径,浪费31个线程的资源。所以发散最怕的不是“存在分支”,而是“一个 Warp 里两拨人势均力敌还交错排列”。

说白了,发散就是“队伍内有人想走不同方向”。训练有素的仪仗队绝不会让伤员站在队里——要么提前调走,要么统一动作。GPU 程序也一样,想办法让同队的人做同一件事,就是下一节的应对策略。

Twarp=Tpath_A+Tpath_B\displaystyle T_{\text{warp}} = T_{\text{path\_A}} + T_{\text{path\_B}}
发散让耗时从“一条路”变成“两条路相加”,这是分支发散最直接的代价公式。
  1. T_path_A(分支A耗时)条件成立的线程先执行路径 A,其余线程被掩蔽等待。
    Tpath_A\displaystyle T_{\text{path\_A}}
  2. T_path_B(分支B耗时)路径 A 走完后,条件不成立的线程再执行路径 B。
    Tpath_B\displaystyle T_{\text{path\_B}}
  3. 相加(+)两路串行,总时间等于两者之和,与单分支相比近乎翻倍。
    Tpath_A+Tpath_B\displaystyle T_{\text{path\_A}} + T_{\text{path\_B}}

发散应对策略

聪明的教官会重新点兵:所有左脚有伤的士兵集中到第一队,健康的集中到第二队。第一队统一向左转,第二队统一向右转,每队都无发散。这就是应对发散的第一招:数据重排。

程序员手里对应的做法,是把走同一分支的数据聚集到同一个 Warp 里——典型例子是光线追踪中,把相同材质的光线重新打包。更进一步的招数是改写分支本身:把 if-else 换成整队统一的计算,或者用谓词执行(predication)——让所有线程都执行两个路径的指令,用掩码决定谁把结果写回。谓词执行消除了分支跳转,代价是所有线程都多做一遍无用功。

还有一个思路是让分支尽量“傻”一点:把大的 if-else 拆成多个对整队一致的判断,或用循环展开消除内部小分支。判断该用哪招,可以量化收益——利用率就是“真正干活的线程数”占整队的比例:

仪仗队里不该有“开小差”的士兵,硬件里“干等”的线程却照样占着资源。把利用率这个数字刻在心里,写 kernel 时自然就会问自己:这32个士兵,此刻有几个真在干活?

利用率=有效线程数32×100%\displaystyle \text{利用率} = \frac{\text{有效线程数}}{32} \times 100\%
利用率反映一队32人里真正干活的比例,是衡量发散损失的核心指标。
  1. 有效线程数(分子)真正在干活、没有干等的线程数量。
    有效线程数\displaystyle \text{有效线程数}
  2. 32(分母)一队满编32人,这是硬件固定的大小。
    32\displaystyle 32
  3. 乘以100%(占比)算出硬件算力被利用的比例,其余部分全在空转。
    有效线程数32×100%\displaystyle \frac{\text{有效线程数}}{32} \times 100\%

合并访存优化

训练结束,32名士兵排队领装备。如果装备窗口一字排开,士兵按编号顺序站到 1~32 号窗口,管理员一次就能发完。但要是士兵各自跑向操场不同角落的窗口,管理员就得开着车来回跑32趟。GPU 访存就是这一幕。

GPU 的内存系统按“扇区/缓存行”搬运数据,一次事务最划算的是把一整块连续数据搬回来。如果 Warp 内相邻线程访问相邻地址——线程 i 访问 base + i×元素大小——硬件就能把32个请求合并成一次事务,这叫 coalesced access(合并访存)。反之,如果线程跳着访问(存在间隔 stride),一个请求要跨好几块数据,事务次数暴涨。

事务次数可以用一个简单的公式估算:32个线程访问的首尾跨度,除以一次事务能搬运的字节数,向上取整。跨度越大、事务越多、等效带宽越低。这也是为什么数据布局(SoA vs AoS)如此重要——字段连续排布,线程才能连续读。

合并访存的精髓就是“让相邻士兵去相邻窗口”。GPU 程序员写循环时多问一句“线程 i 读的地址是不是连续的”,往往就能拿到数量级的性能提升——这比任何花哨技巧都管用。

事务次数=⌈地址跨度扇区大小⌉\displaystyle \text{事务次数} = \left\lceil \frac{\text{地址跨度}}{\text{扇区大小}} \right\rceil
事务次数等于“地址跨度能装下几个扇区”:合并访存只要1次,跳着访问要翻好几倍。
  1. 地址跨度(分子)32个线程第一个与最后一个地址之间的距离,跨度=(N-1)×stride+1。
    地址跨度\displaystyle \text{地址跨度}
  2. 扇区大小(分母)GPU 一次搬运的连续字节数,比如32字节。
    扇区大小\displaystyle \text{扇区大小}
  3. 向上取整只要跨过一个扇区边界就得再来一趟,向上取整得到最少事务次数。
    ⌈地址跨度扇区大小⌉\displaystyle \left\lceil \frac{\text{地址跨度}}{\text{扇区大小}} \right\rceil

调度与延迟隐藏

仪仗队训练场上从来不止一支队伍。第一队去仓库领装备了(等待装备的时间可能几十拍),教官绝不会让操场空着——立刻喊第二队上来练正步。等第一队领完回来,第二队又该歇了。这种“你歇我练、我歇你练”的接力,就是调度与延迟隐藏。

GPU 里每个流处理器簇(SM)有多个 Warp 调度器,每个调度器管理一组 Warp。当某个 Warp 的指令需要等待全局内存返回(几百个周期)时,调度器马上切换到一个“就绪”的 Warp 发射指令。这种上下文切换代价极低,因为每个 Warp 的寄存器都独立保存,不像 CPU 线程切换要保存一大堆状态。

要让延迟被完全隐藏,前提是活跃的 Warp 数量足够多,多到“一个在等、总有另一个能上”。这个前提用占用率(occupancy)刻画——活跃 Warp 数占硬件最大容量的比例。但占用率并非越高越好:寄存器占用过大、缓存争抢加剧时,高占用率反而拖慢性能。

调度器的本质是“别让执行单元闲着”。对工程师来说,调 Block 大小、压寄存器用量,本质上都是在优化占用率,让操场上有足够多的队伍轮换。GPU 性能调优的那些经典参数,追根溯源都落在这支“仪仗队轮换”的接力赛上。

占用率=活跃Warp数最大Warp数×100%\displaystyle \text{占用率} = \frac{\text{活跃Warp数}}{\text{最大Warp数}} \times 100\%
占用率描述操场上有多少队伍在轮换,太低时访存延迟就藏不住了。
  1. 活跃Warp数(分子)操场上有任务在身的队伍数量,它们是调度器可切换的对象。
    活跃Warp数\displaystyle \text{活跃Warp数}
  2. 最大Warp数(分母)操场容纳上限,由硬件资源和每线程占用资源(如寄存器)共同决定。
    最大Warp数\displaystyle \text{最大Warp数}
  3. 乘以100%(占比)占用率太低时,访存等待的空档没人补位,延迟就露出来了。
    活跃Warp数最大Warp数×100%\displaystyle \frac{\text{活跃Warp数}}{\text{最大Warp数}} \times 100\%

AI 应用场景

大模型训练中的矩阵乘法 Kernel 优化

阶段:模型训练(前向/反向传播)

解决的问题:Transformer 的核心是矩阵乘法(GEMM),老式写法让每个线程按行跳着读数据,访存完全不合并,A100 的带宽利用率常常不到20%。用 Warp 思维重排:让32个线程连续读同一行,再用共享内存分块搬运,把全局访存全部变成合并访问。

具体案例:某团队用分块策略重写 SGEMM 后,在 A100 上从约2 TFLOPS 提升到19 TFLOPS(理论峰值约19.5),差距根源就是“每个 Warp 一次事务取回32个连续浮点数”。

光线追踪渲染中的发散管理

阶段:推理/渲染仿真

解决的问题:光线追踪里每条光线命中不同物体、走不同着色分支,Warp 发散让 GPU 效率暴跌。借鉴“仪仗队重新编队”的思路,把同材质、同状态的光线重新打包进同一 Warp,让每队动作统一。

具体案例:渲染器中引入光线重排序(ray reordering)后,复杂场景帧渲染时间降低约30%,相当于“每支队伍做适合自己的统一动作”。

推荐系统 Embedding 查询优化

阶段:推理(特征查找)

解决的问题:Embedding 查表是典型的随机访存,同一 Warp 内32个用户请求的 Embedding ID 各不相同,地址散落。按 ID 排序后批量查表,让相邻线程访问相邻地址,事务次数从几十次降到几次。

具体案例:某推荐服务把 Embedding 查询按 ID 排序合并后,单次请求延迟从80μs降到30μs。

Python 演示

python48 行
import math WARP_SIZE = 32  def count_warps(n_threads):    """N 个线程按 32 人一队,不满 32 也算一整队"""    return math.ceil(n_threads / WARP_SIZE)  def divergence_cycles(decisions, cycles_per_path=10):    """估算发散代价:某 Warp 内同时存在两种决策就要多走一遍"""    extra = 0    for w in range(count_warps(len(decisions))):        part = decisions[w * WARP_SIZE:(w + 1) * WARP_SIZE]        if part and 0 < sum(part) < len(part):            extra += cycles_per_path    return extra  def memory_transactions(n_threads, stride, sector_bytes=32):    """估算访存事务:首尾跨度跨过几个 32 字节扇区"""    span = (n_threads - 1) * stride + 1    return math.ceil(span / sector_bytes)  def simulate(n_threads, stride, divergent):    """divergent: none 无发散 / half 相邻线程决策不同"""    warps = count_warps(n_threads)    if divergent == "none":        decisions = [1] * n_threads    else:        decisions = [i % 2 for i in range(n_threads)]     div = divergence_cycles(decisions)    trans = memory_transactions(n_threads, stride)    total = warps * 10 + trans * 20 + div    print(f"线程{n_threads:5d} | Warp{warps:3d} | 步长{stride:3d} | "          f"事务{trans:3d} | 发散开销{div:3d} | 总代价{total}")    return total  if __name__ == "__main__":    print("Warp 调度代价对比实验(总代价越小越快):")    simulate(1024, 1, "none")    # 理想:合并访存 + 无发散    simulate(1024, 1, "half")    # 同一 Warp 内一半走 A 一半走 B    simulate(1024, 8, "none")    # 步长 8 的跳跃访存    simulate(1024, 8, "half")    # 发散 + 非合并访存,最差情况
目的:用 Python 模拟 GPU 的 Warp 分组、分支发散和合并访存,直观对比不同写法的性能代价。
  1. WARP_SIZE = 32每队固定32人,这是整个 GPU 调度世界的铁律
  2. math.ceil(n_threads / WARP_SIZE)不满32的线程也算一队,解释了为什么 Block 大小要对齐到32的倍数
  3. 0 < sum(part) < len(part)判断队内是否同时有两种决策,只要有,整队就要串行多走一遍
  4. div += cycles_per_path每个发散的 Warp 都要额外付出一个分支路径的时间
  5. (n_threads - 1) * stride + 1首尾跨度公式:连续访问跨度小,跳着访问跨度大
  6. simulate(1024, 8, "half")步长8加发散是最坏组合,总代价遥遥领先

常见误区

误区我以为只要把线程数调大,GPU 程序就会线性变快,所以一遇到性能问题就先加线程。

正解线程多但都在干等,等于仪仗队人多却在原地踏步。如果线程增多反而带来更多发散或非合并访存,性能可能更差。优化优先看 Warp 里的“有效线程占比”和访存是否合并,而不是盲目堆线程数。

为何常见:CPU 多线程的直觉被搬到了 GPU 上,忽略了 GPU 以 Warp 为单位协同执行的特殊性。

误区我以为 GPU 线程像 CPU 超线程一样各自独立取指、乱序执行,互不干涉。

正解同一 Warp 的32个线程是锁步的——硬件一次取指、全队执行。所谓“独立”只是编程模型给你的幻觉,底层执行的自由度远没有 CPU 线程大。

为何常见:CUDA 编程接口刻意隐藏了锁步细节,让开发者按“线程独立”写代码,反而掩盖了硬件真相。

误区我以为只要写了分支语句,GPU 就一定会付出性能代价,于是到处避免 if。

正解只有当 Warp 内一部分线程走 A、另一部分走 B 时才会发散;如果整队32个线程都走同一个分支,硬件照样一次执行完毕,零额外代价。

为何常见:把“分支”和“分歧”混为一谈,真正致命的是分歧,而不是分支本身。

误区我以为把占用率调到100%就是最优,于是拼命缩小寄存器用量、调大 Block。

正解占用率太高可能引发寄存器溢出(spill)到本地内存,或让缓存被各 Warp 争抢到抖动,反而拖慢速度。占用率要结合具体 kernel 的访存与计算比例来权衡。

为何常见:占用率是个好看的指标,但性能是资源权衡的结果,单一指标最大化往往适得其反。

误区我以为只要数据量小、就几百个字节,访存方式随便写都无所谓。

正解性能瓶颈在事务次数而不在数据总量。哪怕32个线程只取4字节,若地址分散在几个不同扇区,依然要多次事务,延迟翻倍。

为何常见:直觉上“数据少=快”,忽略了硬件按扇区搬运的固定成本。

面试题

Q1. GPU 中一个 Warp 包含多少个线程?

单选题 · easy

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

答案:C。Warp 固定包含32个线程,这是 GPU 硬件调度器的设计铁律。其余选项都不符合 SIMT 的硬件约定,记住“32人仪仗队”这个画面就不会错。

Q2. 线程全局编号 tid=100 属于哪一支 Warp(编号从0开始)?

单选题 · easy

  • A.第1支Warp
  • B.第2支Warp
  • C.第3支Warp
  • D.第4支Warp← 正确答案

答案:D。warp_id = ⌊100/32⌋ = 3,即第4支 Warp。这就像花名册上100号士兵被编入第4小队,队号从0开始数。

Q3. SIMT 模型的核心含义是?

单选题 · easy

  • A.单条指令多线程同步执行← 正确答案
  • B.多条指令同线程乱序执行
  • C.每个线程独立取指独立执行
  • D.指令与数据分离存储两处

答案:A。SIMT(单指令多线程)指整队32个线程由硬件一次取指、同步执行同一条指令,只是各自处理不同数据。C 描述的 CPU 式独立取指恰恰是 GPU 不做的。

Q4. 当 Warp 内出现分支发散时,硬件会如何处理?

单选题 · mid

  • A.同时执行两个分支的所有线程
  • B.只执行人数较多的那个分支
  • C.依次串行执行两个分支路径← 正确答案
  • D.自动合并两条分支为一条

答案:C。锁步执行决定了同一时刻只能执行一条路径,硬件先让一支路执行、另一支路掩蔽等待,再反过来。A 需要双发射能力,硬件没有;B 会丢结果;D 是程序员优化方向而非硬件自动行为。

Q5. 下列哪些做法能有效降低 warp divergence 的影响?

多选题 · mid

  • A.把走同一分支的数据归并到同一Warp← 正确答案
  • B.使用谓词执行让分支内路径合并← 正确答案
  • C.把线程块数字调大两倍
  • D.让分支条件对整队32线程一致← 正确答案

答案:ABD。A 数据重排、B 谓词执行、D 让分支边界对齐 Warp 都是经典手段。C 增加 block 数量只改变调度规模,不改变一个 Warp 内分支分裂的局面。

Q6. 合并访存(coalesced access)的最关键要求是?

单选题 · mid

  • A.所有线程访问同一个内存地址
  • B.相邻线程访问连续递增的地址← 正确答案
  • C.每个线程使用独立的内存指针
  • D.数据必须放在共享内存之中

答案:B。相邻 lane 地址连续,硬件才能把32个请求合并成一次内存事务。A 虽能合并但毫无吞吐意义;C 指针间接访存容易打散地址;D 与合并访存无直接关系。

Q7. 32个线程以 stride=4 字节间隔访问内存(每个事务32字节),大约需要几次内存事务?

单选题 · hard

  • A.大约需要1次事务
  • B.大约需要4次事务← 正确答案
  • C.大约需要8次事务
  • D.大约需要16次事务

答案:B。跨度=(32-1)×4+1=125字节,一个事务搬32字节,向上取整⌈125/32⌉=4次。连续访问时仅1次,差距一目了然。

Q8. 关于占用率与延迟隐藏,下列说法正确的是?

多选题 · hard

  • A.低占用率时内存延迟更难被隐藏← 正确答案
  • B.占用率越高性能一定越强
  • C.更多活跃Warp可轮流掩盖访存等待← 正确答案
  • D.寄存器用量会限制最大Warp容纳数← 正确答案

答案:ACD。活跃 Warp 越多,调度器越容易在访存等待时切换其他 Warp,从而隐藏延迟(A、C)。但占用率过高可能寄存器溢出或缓存争抢,性能反而下降(B错)。D 是资源约束的本质。

Q9. 下列哪种数据布局最有利于 GPU 合并访存?

单选题 · easy

  • A.结构体数组 AoS 布局
  • B.数组结构体 SoA 布局← 正确答案
  • C.链表式随机内存布局
  • D.哈希表散列存储布局

答案:B。SoA(数组结构体)让同一字段连续排列,相邻线程读同一字段时地址连续,触发合并。AoS 字段交错,读一个字段要跨大步长,容易打散事务。

Q10. 关于 Warp 锁步执行,以下描述正确的有?

多选题 · mid

  • A.同一 Warp 在同一时刻执行同一条指令← 正确答案
  • B.同 Warp 内线程共享各自的寄存器文件
  • C.Warp 是 GPU 硬件调度的最小单位← 正确答案
  • D.一条指令只需取指和解码一次← 正确答案

答案:ACD。锁步即同指令同步执行(A),硬件取指一次供全队使用(D),调度器以 Warp 为单位发射指令(C)。B 错误——每个线程有自己的寄存器槽,互不共享。