想象你是一座繁忙集装箱码头的调度员。货船载满了标准集装箱——这些箱子就是矩阵中的数据。客户的需求很简单:把两堆货(矩阵 A 和矩阵 B)按规则相乘,再叠到已有的货堆(矩阵 C)上,得到新货堆(矩阵 D)。码头上有一台威力惊人的龙门吊——它就是 Tensor Core。普通吊车一次只能吊一个箱子(普通 CUDA 核心一次做一个乘加),而龙门吊一次能吊起一整片集装箱,在空中完成“翻转、对位、叠放”的组合动作,这就是 mma 指令。 但要让这台龙门吊干活,你不能只说“把货搬过去”,必须写一份精确到每一步的吊装作业单:用几号吊具、吊哪一片、放在哪个作业区、集装箱朝哪个方向码放。这份作业单,就是 PTX 指令。CUDA C++ 像物流系统只关心“从哪到哪”,而 PTX 必须关心“吊机的每个具体动作”——它是连接高级语言与 GPU 硬件的中间指令层。本文将带你以码头作业为线索,从 PTX 指令语法、矩阵片段寄存器、内存布局到性能调优,完整掌握 Tensor Core 的底层编程。

岗位关联

掌握 PTX 与 mma 指令编程,意味着你能在最底层控制 GPU 的算力引擎,写出比高级库更快、更省显存的算子,这是高性能计算与 AI 推理引擎工程师的核心竞争力。

目录
  1. 可视化演示
  2. PTX指令概览
  3. mma指令语法
  4. 矩阵片段寄存器
  5. 内存布局约束
  6. 数据类型精度
  7. 性能调优技巧
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

双模式展示PTX编程:MMA指令执行流程(Shm→ldm加载→frag寄存器→mma.sync→输出)和代际对比(V100 FP16/A100 BF16/H100 FP8算力演进)。

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

MMA指令执行流程 Shm:共享内存中的矩阵分块(tile) ldm:ldmatrix指令加载到寄存器fragment mma:mma.sync执行矩阵乘加(D=A×B+C) flow: ldmatrix A,B → mma.sync al,m,n,k → store D warp内线程协同加载fragment并执行MMA运算。

PTX指令概览

码头调度室里有两套文档:一套是物流公司给的“运输总单”(CUDA C++ 源码),只写从哪个仓库取货、送到哪个泊位;另一套是龙门吊司机实际执行的“吊装作业单”(PTX 指令),精确到吊具型号、起吊角度、放箱位置。总单经过一层翻译,变成作业单,再经过一层翻译,变成司机手上的操作按钮——这就是 GPU 的两级编译:CUDA C++ 先编译成 PTX,再由驱动编译成 SASS 机器码。

PTX 的全称是 Parallel Thread Execution(并行线程执行),它是 NVIDIA 定义的虚拟指令集。它的巧妙之处在于:作业单不绑定具体某台吊机型号,老吊机和新吊机都能读懂同一份作业单,只是实际动作快慢不同。这保证了 PTX 编写的代码能跨越多代 GPU 运行,同时也让程序员能够触及硬件能力的“最后一公里”。

在 GPU 港口里,最小的“作业班组”是 warp,一个 warp 恰好有 32 名工人(线程)。龙门吊的每一次起吊,都需要这 32 名工人同时握住吊具、齐声喊号子一起发力。因此 mma 指令是 warp 级指令——它要求一个 warp 的 32 个线程全部参与、协同完成。在 CUDA 内核中,你可以用 asm volatile 嵌入 PTX 指令,这就是“内联 PTX”,相当于在总单中直接粘贴一份吊装作业单。

回到码头:如果你想让龙门吊高效工作,就先要学会读懂作业单的格式。PTX 是一个庞大的指令集,而我们最关心的那一页,写着 mma——矩阵乘加指令。它就是龙门吊最核心的“组合动作”手册。

warp_size=25=32\text{warp\_size} = 2^5 = 32
一个 warp 包含 32 个线程,这是 GPU 调度与 mma 指令执行的基本单位。
  1. warp_size表示一个 warp 包含的线程数量。你可以把它理解为龙门吊班组的人数,这个人数由硬件固定,程序员无法改变。
    warp_size\text{warp\_size}
  2. 2^5为什么恰好是 32?因为 GPU 硬件按二进制设计,用 5 个比特就能给 32 名工人分别编号(0~31),这是硬件寻址的自然结果。
    252^5
  3. = 32结论值。所有 warp 级指令(包括 mma)都隐含“32 个线程同时行动”这一前提,忘记这一点,后续的寄存器分配与内存布局都无从谈起。
    =32= 32

mma指令语法

龙门吊最厉害的一招叫“组合吊装”:吊具同时抓起一整片集装箱(矩阵 A 的某个分块),在空中与另一片集装箱(矩阵 B 的分块)对位交错,落到已有的货堆(矩阵 C)上,瞬间完成“乘加叠放”。整个过程一条指令搞定,这就是 mma(matrix multiply-accumulate)。普通 CUDA 核心要一条条指令地做“乘一次、加一次”,而 Tensor Core 把整个分块的乘加打包成了一个硬件动作。

在 PTX 中,一次组合吊装写成这样的作业单:

逐个词拆开看:mma 是操作码,sync 表示班组内需要同步,aligned 表示所有地址与数据类型必须对齐;m16n8k8 是本次吊装的货物规格——M 维度 16、N 维度 8、K 维度 8,意味着 A 是 16×8 的货片,B 是 8×8 的货片,C 和 D 都是 16×8 的货堆。随后的 .row、.col 分别是 A、B 矩阵的“码放朝向”(行主序或列主序),最后的 .f32.f16.f16.f32 依次声明 D、A、B、C 四种货物的材质(数据类型)。

看到这里你可能会问:作业单上的 A、B、C、D 究竟存放在哪里?龙门吊的吊具上挂着四个作业区,每个区由 32 名工人分别掌管一部分货物——这就是矩阵片段寄存器,下一节我们走进这四个作业区。

D16×8=A16×8×B8×8+C16×8\mathbf{D}_{16 \times 8} = \mathbf{A}_{16 \times 8} \times \mathbf{B}_{8 \times 8} + \mathbf{C}_{16 \times 8}
mma 指令的数学语义:输出矩阵 D 等于输入矩阵 A 与 B 相乘后,再加上累加矩阵 C。
  1. D = A×B+C这是整个指令的灵魂。Tensor Core 不是简单地算乘法,而是在一次硬件操作里完成“乘起”和“叠加”两件事。龙门吊一次落箱,既完成了新货的搬运,也完成了与旧货堆的合并。
    D=A×B+C\mathbf{D} = \mathbf{A} \times \mathbf{B} + \mathbf{C}
  2. m16n8k8 形状指令名中的 m/n/k 分别对应 M、N、K 三个维度。M 是 A 的行数,N 是 B 的列数,K 是 A 的列数(同时也是 B 的行数)。这些形状是硬件固定的,不能随意指定。
    M=16,N=8,K=8M = 16, \quad N = 8, \quad K = 8
  3. A 与 B 的维度约束A 必须能左乘 B,即 A 的列数等于 B 的行数。这里 A 是 16×8,B 是 8×8,中间共享的 K=8 像两片货物互相咬合的卡槽。
    AM×K×BK×N\mathbf{A}_{M \times K} \times \mathbf{B}_{K \times N}
  4. C 与 D 同维度累加矩阵 C 与输出矩阵 D 的形状完全相同,都是 M×N。这保证了“叠放”操作天然合法,无需额外形状转换。
    CM×N,DM×N\mathbf{C}_{M \times N}, \quad \mathbf{D}_{M \times N}
text1 行
mma.sync.aligned.m16n8k8.row.col.f32.f16.f16.f32 {d}, {a}, {b}, {c};

矩阵片段寄存器

龙门吊的吊具不是一个大铁板,而是由 32 名工人手持的托盘拼成的。每名工人手里只攥着几件货物(几个数),这些货物就存放在工人胸前的工具口袋(寄存器)里。整个吊具的 A 区、B 区、C 区、D 区,分别对应 mma 指令的四个矩阵片段(fragment)。没有集中的大仓库,所有数据都分散在每名工人的口袋中。

以 m16n8k8 的 A 片段为例:A 矩阵共有 16×8=128 个元素,分给 32 名工人,每人恰好拿 4 个元素。如果元素是 fp16 半精度(16 位),那么每 2 个元素可以塞进一个 32 位寄存器,所以每名工人口袋里是 2 个寄存器。同理,B 片段 8×8=64 个元素分给 32 人,每人 2 个元素(1 个寄存器);C 和 D 片段是 fp32 累加结果,16×8=128 个 32 位数分下去,每人 4 个寄存器。

这些片段不是随意分配的,而是严格按 lane ID(工人在班组中的编号)映射。第 0 号工人拿 A 矩阵第一行的前几个元素,第 1 号工人拿紧接着的几个……硬件工程师在文档里画好了完整的“分货表”。正因如此,同一个寄存器在不同 mma 指令中可以被不同片段复用——上一轮还是 A 片段的寄存器,下一轮经过 ldmatrix 重新装载,就变成了新的 B 片段。这就是寄存器复用:工具口袋不变,装的东西可以换。理解了四个作业区,下一步要搞清楚货物从大仓库(全局内存)运到作业区途中的码放规则。

Rthread=M×K32R_{\text{thread}} = \frac{M \times K}{32}
每线程持有的 A 片段元素数等于矩阵总元素数除以 warp 线程数(32)。
  1. M×K这是 A 矩阵的总元素个数,对应龙门吊作业区 A 里的全部货物数量。m16n8k8 时就是 16×8=128 件货。
    M×KM \times K
  2. ÷32把总货物平均分给 32 名工人。这是“warp 级协作”的数学体现——没有谁单独拿一整片,而是人人有份。
    ÷32\div 32
  3. R_thread每名工人最终手中的货物件数。m16n8k8 时结果是 4,即每线程持有 4 个 A 元素(打包成 2 个寄存器)。
    Rthread=4R_{\text{thread}} = 4

内存布局约束

集装箱在货场上码放是有规章的:箱子必须全部朝一个方向,吊具才能一次抓牢;如果朝向混乱,龙门吊一抓就散。同样,Tensor Core 要求矩阵以特定的“主序”排列——行主序(row-major)或列主序(col-major)。mma 指令中的 .row 和 .col 修饰符,就是告诉硬件“这批货是横着码还是竖着码”。行主序的相邻元素在内存中横向挨着,列主序则纵向挨着。

货物从港口外的大仓库(全局内存)运到龙门吊旁边的中转堆场(共享内存)之后,还需要一辆专用的“集装箱转运车”把它们整齐装到工人的口袋里,这辆车就是 ldmatrix。一条 ldmatrix.sync.aligned.m8n8.x4 指令,可以同时从共享内存抓取 4 个 8×8 的矩阵块,精准分配到 32 名工人的寄存器中——这个动作比普通 ld.shared 逐元素装载快得多,因为它完全摸清了龙门吊的分货表。

但转运车对货物摆放有苛刻要求:每个 8×8 矩阵块的起始地址必须 16 字节对齐。除此之外,32 名工人同时去共享内存取货时,还可能撞上同一个“装卸通道”(bank),导致取货排队。共享内存有 32 个 bank,每个 bank 每次服务一个 4 字节单元;如果多名工人同时访问同一个 bank,就会被串行处理,这就是 bank 冲突。解决方式是使用 swizzle 模式:码放货物时故意按异或规则打乱位置,让工人的访问均匀分散到不同 bank。

回到码头:货物码放规则解决了,接下来要考虑选什么材质的集装箱。不同的货物需要不同的箱子,而 Tensor Core 对不同“材质”的运算速度与精度也大不相同。

bank=(addr4) mod 32\text{bank} = \left( \frac{\text{addr}}{4} \right) \bmod 32
共享内存地址到 bank 的映射规则:每 4 字节一个单元,对 32 取模得到 bank 编号。
  1. addr ÷ 4addr 是共享内存的字节地址,硬件按 4 字节为一个基本单元分配 access。先把字节地址换算成单元编号。
    addr4\frac{\text{addr}}{4}
  2. mod 32对 32 取模。因为共享内存恰好有 32 个 bank,单元编号对 32 求余,就得到它所属的通道号。两名工人若算出相同 bank,就会冲突。
     mod 32\bmod 32
  3. 映射结果把 bank 视作“装卸通道”。swizzle 的本质,就是通过地址变换让原本映射到同一通道的访问散开,使 32 条通道被均匀使用。
    bank∈{0,1,…,31}\text{bank} \in \{0, 1, \dots, 31\}

数据类型精度

码头上备有四种集装箱:标准钢箱(FP16)、重型加固箱(TF32)、轻量铝箱(BF16)和速装箱(INT8)。选用哪种箱子,直接影响龙门吊的单次吊装重量(吞吐量)和货物安全(精度)。FP16 半精度是 Tensor Core 最经典的搭档:只有 16 位,尾数 10 位,数值范围有限,但两个 FP16 能拼进一个 32 位寄存器,计算吞吐翻倍,是深度学习训练和推理的主力材质。

TF32 是专门为“既要精度又要速度”准备的折中方案:它保留了 FP32 的 8 位指数(数值范围与 FP32 相同),但把尾数砍到 10 位。相当于重型箱——外面尺寸和 FP32 一样大,但内壁加厚,装贵重货物不会溢出。BF16 则反过来,指数 8 位保范围、尾数仅 7 位,适合对数值范围敏感但对尾数精度不敏感的场合。INT8 则是把货物压缩打包,配合 FP32 累加器,常用于推理阶段的量化加速。

如何从数学上理解 TF32 的截断?FP32 有 23 位尾数,TF32 只保留高 10 位,等价于把尾数的小数部分“四舍五入”到 10 位精度——或者说,把原数放大 2 的 13 次方倍、截掉小数部分、再缩小回来。

选好了集装箱材质,最后一步是优化整个码头的作业节奏。再快的龙门吊,如果工人和转运车总是互相等待,吞吐量也上不去——这就是性能调优的用武之地。

tf32(x)=trunc(x×213)×2−13\text{tf32}(x) = \text{trunc}\left(x \times 2^{13}\right) \times 2^{-13}
TF32 转换的数学本质:通过缩放、截断、还原三步,把 FP32 的 23 位尾数压缩到 10 位。
  1. x × 2^13把 x 放大 8192 倍。这样原本尾数的低 13 位就被“顶”到了整数部分,为下一步截断做准备。放大倍数的选择由尾数位数差决定:23-10=13。
    x×213x \times 2^{13}
  2. trunc截断操作,一刀砍掉小数部分(即低 13 位尾数)。这相当于把货物倒进一个只有 10 位精度的模具,多余的边角料直接削掉。
    trunc(⋅)\text{trunc}(\cdot)
  3. × 2^-13再把结果缩小 8192 倍,还原到原来的数量级。缩放和截断合在一起,就是“只保留 10 位有效尾数”的数学表达。
    ×2−13\times 2^{-13}

性能调优技巧

码头运营的核心目标是“吊机不停”——龙门吊每秒钟都应该在干活,而不是等货。最有效的办法是双缓冲:准备两块中转堆场,龙门吊正在计算堆场 A 里的货物时,转运车已经往堆场 B 卸下了下一批货;等 A 算完,立刻切换到 B,转运车再回头给 A 补货。计算与搬运像两条传送带一样重叠进行,这就是软件流水线。

在 PTX 层面,这需要精细控制每条 ldmatrix 和 mma 的发射顺序。更深一层的约束来自寄存器压力:每名工人的工具口袋数量是有限的(每个线程最多 255 个寄存器)。mma 的四个片段本就要占用不少寄存器,如果为了流水线多缓存几批货,寄存器就会超支,导致内核无法启动或占用率暴跌。占用率(occupancy)决定了一个 SM 上能同时驻留多少个 warp——驻留越多,当一个 warp 等待内存时,另一个 warp 就能顶上干活,延迟就被“藏”了起来。

至此,我们从读懂作业单、掌握组合动作、分配四区货物、规范码放规则、选择集装箱材质,到最终优化码头调度,完整走了一遍 Tensor Core 的 PTX 编程流程。龙门吊依然在轰鸣,但现在的你,已经能亲手写出那份让它发挥极限效率的吊装作业单了。

occupancy=active_warpsmax_warps\text{occupancy} = \frac{\text{active\_warps}}{\text{max\_warps}}
占用率是活跃 warp 数与硬件最大 warp 数的比值,它衡量着 GPU 有多大能力用“人多”来掩盖“等待”。
  1. active_warps当前同时驻留在 SM 上的 warp 数量。它受寄存器、共享内存等资源限制,不是想开多少就开多少。
    active_warps\text{active\_warps}
  2. max_warps硬件允许的 warp 数上限,由 SM 设计决定。这是码头工位的总数,满载时能容纳的班组数。
    max_warps\text{max\_warps}
  3. 比值含义占用率越高,意味着越多的班组同时待命,即使某组在等货,其他组也能继续吊装,从而隐藏访存延迟。但占用率并非越高越好——过高的并行可能挤占寄存器,反而限制每个 warp 的流水线深度,需要在实践中权衡。
    active_warpsmax_warps\frac{\text{active\_warps}}{\text{max\_warps}}

AI 应用场景

AI推理引擎中的PTX算子优化

阶段:模型推理部署与性能优化

解决的问题:深度学习框架默认的卷积与矩阵乘法算子,往往为了通用性牺牲了硬件利用率。在生产环境中,同一个模型要在不同型号的 GPU 上达到极致吞吐,必须针对 Tensor Core 的 mma 指令手工编写或调优 kernel。PTX 层编程让工程师能精确控制矩阵分块形状、内存布局与数据类型,把算子效率从框架默认的 60% 提升到 90% 以上。

具体案例:某视频理解模型的推理服务延迟超标。工程师将核心 GEMM 替换为基于 mma.sync.aligned.m16n8k16 的手写 PTX kernel,并用 ldmatrix.x4 批量装载共享内存分块,配合双缓冲流水线,最终在 A100 上实现了 1.8 倍加速,且精度无损。这就像把龙门吊的每一次起吊都重新设计了吊具与路径,让每秒钟的吊装次数大幅上升。

大模型混合精度训练策略

阶段:模型训练与精度管理

解决的问题:大模型训练动辄需要数百张 GPU,FP32 全精度训练显存开销大、速度慢;无脑使用 FP16 又可能出现梯度下溢或溢出。理解 TF32 与 FP16 的位宽结构后,工程师可以为不同层选择不同的 Tensor Core 精度——例如用 TF32 代替 FP32 做前向传播,用 FP16 配合 FP32 累加器做反向传播,在保持收敛质量的同时显著提升训练吞吐。

具体案例:某 LLM 微调任务中,基线用 FP32 训练一个 epoch 需要 12 小时。团队开启 torch.backends.cuda.matmul.allow_tf32 = True 后,矩阵乘法自动切换到 TF32 Tensor Core 路径,训练时间缩短到 8 小时,验证集困惑度几乎不变。这是理解“8 位指数保范围、10 位尾数够用”精度权衡后做出的合理选择。

科学计算中的自定义矩阵内核

阶段:HPC 数值计算与 kernel 开发

解决的问题:许多科学计算(如气象模拟、分子动力学)需要自定义的数据类型和矩阵形状,标准库的 GEMM 无法直接覆盖。通过 PTX 内联汇编,研究人员可以直接组合 mma 指令与自定义的 ldmatrix 装载流程,为特定物理模型打造专属的矩阵引擎。

具体案例:某量子化学团队需要频繁计算 16×8 尺寸的小矩阵相乘,标准 cuBLAS 因分块开销过大而效率低下。他们用内联 PTX 编写了专用的 m16n8k8 kernel,将预热开销与寄存器占用降到最低,最终在 GPU 集群上获得了 2.3 倍的端到端加速。

Python 演示

python48 行
import numpy as np  def to_fp16(x):    """模拟FP16在Tensor Core中的输入:转半精度后转回fp32参与累加。"""    return x.astype(np.float16).astype(np.float32)  def to_tf32(x):    """模拟TF32截断:保留fp32的8位指数,尾数从23位截到10位。"""    bits = np.ascontiguousarray(x, dtype=np.float32).view(np.uint32)    bits = bits & np.uint32(0xFFFFE000)    return bits.view(np.float32)  def to_fp32(x):    """FP32基准:不做任何精度截断。"""    return x.astype(np.float32)  def mma_accumulate(A, B, C, cast_fn):    """模拟mma指令语义 D = A x B + C,输入先按指定精度舍入。"""    Aq = cast_fn(A)    Bq = cast_fn(B)    return Aq @ Bq + C  def run_demo():    np.random.seed(42)    M, K, N = 128, 64, 64    A = np.random.randn(M, K).astype(np.float32)    B = np.random.randn(K, N).astype(np.float32)    C = np.random.randn(M, N).astype(np.float32)     ref = A @ B + C    schemes = {        "fp16 (Tensor Core)": to_fp16,        "tf32  (Tensor Core)": to_tf32,        "fp32  (CUDA Core)": to_fp32,    }    for name, cast in schemes.items():        out = mma_accumulate(A, B, C, cast)        err = np.abs(out - ref).mean()        print(f"{name:22s} MAE = {err:.6e}")  if __name__ == "__main__":    run_demo()
目的:在 CPU 上用 NumPy 模拟 Tensor Core 三种典型精度(FP16、TF32、FP32)下的矩阵乘加行为,直观对比不同“集装箱材质”带来的数值误差,帮助你理解为何训练中精度选择如此重要。
  1. def to_fp16(x)把 FP32 张量先降为 FP16 再升回 FP32,模拟 Tensor Core 以 FP16 读取输入、以 FP32 累加的过程。
  2. def to_tf32(x)通过位运算把 FP32 的尾数低 13 位清零,等价于 TF32 的 10 位尾数截断。
  3. bits & np.uint32(0xFFFFE000)0xFFFFE000 的二进制低 13 位全为 0,按位与即丢弃低 13 位尾数。
  4. def mma_accumulate(A, B, C, cast_fn)用 Aq @ Bq + C 一句话复现 mma 指令的数学语义 D = A×B+C。
  5. schemes = { ... }用字典把三种精度策略映射到对应的舍入函数,便于统一循环对比。
  6. err = np.abs(out - ref).mean()计算平均绝对误差(MAE),量化每种精度相对 FP32 基准的偏差。

常见误区

误区我以为 PTX 就是 GPU 最终执行的机器码,写完 PTX 就能直接控制硬件电路,所以调优 PTX 等同于调优 SASS。

正解PTX 是虚拟指令集,属于中间表示层。它还要经过驱动中的编译器(ptxas)翻译成 SASS,才能真正被 GPU 执行。就像吊装作业单是写给人看的规范文件,司机还得把它转化为手上的具体操作。同一条 PTX 在不同代 GPU 上会生成不同的 SASS,这正是 PTX 跨代兼容的原因。

为何常见:CUDA 编译报错信息里常同时出现 PTX 和 SASS 相关文件,加上 nvcc 默认输出 .ptx 中间文件,容易让人误以为 PTX 就是最终可执行代码。

误区我以为写好一条 `mma.sync.aligned.m16n8k8...` 指令后,当前线程就会自己完成整个 16×8 的矩阵乘加。

正解mma 是 warp 级指令,必须由整个 warp 的 32 个线程协同执行。每个线程只持有矩阵片段的一部分(例如 A 片段每线程仅 4 个元素),所有线程同时发出一条相同的 mma 指令,硬件才把碎片拼成完整的矩阵运算。好比龙门吊的 32 名工人必须同时发力,缺一个人吊具就会倾斜。

为何常见:普通 CUDA 指令如 fma.rn.f32 是单线程语义,先入为主地以为所有指令都如此,忽略了 mma 属于 warp 级矩阵指令这个特殊类别。

误区既然 mma 能做矩阵乘加,那我是不是可以用它算任意大小(比如 100×100)的矩阵乘法,只要写好 M、N、K 就行。

正解Tensor Core 只支持硬件预设的少数形状组合,如 m16n8k8、m16n8k16、m16n8k32 等。更大的矩阵必须拆成多个这种形状的分块(tile),循环调用 mma 指令。就像龙门吊的吊具规格是固定的,一片只能吊 16×8 的货,大货堆必须分批吊装。

为何常见:矩阵乘法在数学上对形状没有限制,把数学直觉直接搬到了硬件指令上,忽略了 Tensor Core 是定制化电路而非通用计算单元。

误区我以为 A、B、C、D 四个片段是放在共享内存里的大数组,mma 指令直接读写共享内存。

正解fragment 是寄存器片段的缩写,四个矩阵片段最终都分配在 warp 各线程的寄存器文件中。共享内存只是数据从全局内存到寄存器之间的“中转堆场”,真正的搬运动作由 ldmatrix 完成并写入寄存器。理解这一点对控制寄存器压力、提升占用率至关重要。

为何常见:共享内存名字里有“共享”二字,且编程中确实要大量操作共享内存,容易让人误以为它就是矩阵数据的最终存放地。

误区FP16 不过是比 FP32 数字精度差一些,反正神经网络对误差不敏感,所有层都可以无脑用 FP16 跑 Tensor Core。

正解FP16 的指数只有 5 位,数值范围远小于 FP32。梯度、中间激活很容易超出 ±65504 的范围导致溢出,而小数值又会下溢为 0。TF32 正是为了在保留 FP32 范围的同时获得 Tensor Core 加速而设计的。选择数据类型要看数值范围与尾数精度的双重约束,而不是笼统地“低精度换速度”。

为何常见:深度学习中“混合精度训练”的概念很流行,很多人只记住了“FP16 更快”,却忽略了混合精度的核心是“为不同数据分配合适的精度”。

面试题

Q1. PTX 在 GPU 编程中的角色是什么?

单选题 · easy

  • A.最终执行的硬件机器码
  • B.介于 CUDA 与 SASS 间的中间指令层← 正确答案
  • C.负责显存分配的运行时库
  • D.一种用于调试 CUDA 的图形界面工具

答案:B。PTX 是虚拟指令集,位于 CUDA C++ 与 SASS 之间,由驱动编译为最终机器码。它像“吊装作业单”——规范而跨代通用,但还不是司机手上的具体操作。A 是 SASS 的角色,C 是 CUDA 运行时 API 的职责,D 与 PTX 无关。

Q2. 一个 warp 包含多少个线程?

单选题 · easy

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

答案:C。GPU 硬件规定一个 warp 有 32 个线程,这是调度与 warp 级指令(如 mma)的基本单位。32 来自二进制的 5 比特寻址(2^5=32)。

Q3. mma.sync.aligned.m16n8k8.row.col.f32.f16.f16.f32 中 .row 和 .col 分别描述哪个矩阵的布局?

单选题 · mid

  • A.分别是 C 和 D 的布局
  • B.分别是 A 和 B 的布局← 正确答案
  • C.分别是 D 和 C 的布局
  • D.分别是 B 和 A 的布局

答案:B。mma 指令中布局修饰符依次作用于 A 和 B:.row 表示 A 行主序,.col 表示 B 列主序。C 和 D 默认行主序。就像码放货物时先规定 A 区横放、B 区竖放。

Q4. m16n8k8 指令中,A 矩阵的维度是多少?

单选题 · mid

  • A.16×8← 正确答案
  • B.8×8
  • C.16×16
  • D.8×16

答案:A。m16n8k8 表示 M=16、N=8、K=8。A 矩阵形状是 M×K = 16×8,B 是 K×N = 8×8,C/D 是 M×N = 16×8。

Q5. 关于矩阵片段(fragment)的说法,正确的有哪些?

多选题 · mid

  • A.A/B/C/D 四个片段都存储在共享内存中
  • B.每个线程持有片段的一部分元素← 正确答案
  • C.mma 由整个 warp 协作完成← 正确答案
  • D.片段数量与每线程寄存器数无关

答案:BC。fragment 存储在寄存器而非共享内存,A 错;warp 的 32 个线程平分矩阵元素,B 对;mma 是 warp 级指令,C 对;每线程元素数=总元素÷32,直接决定寄存器占用,D 错。这就像 32 名工人每人攥几件货,而不是货全堆在公共堆场。

Q6. ldmatrix 指令的主要作用是什么?

单选题 · easy

  • A.从全局内存直接加载标量
  • B.从共享内存高效装载 8×8 矩阵块到寄存器← 正确答案
  • C.释放共享内存空间
  • D.把寄存器数据写回全局内存

答案:B。ldmatrix 是 Tensor Core 编程专用的“转运车”,一次可装载多个 8×8 矩阵块到寄存器片段,避免逐元素加载的低效。A 是普通 ld 指令的职责,C/D 是 stmatrix 等指令的职责。

Q7. TF32 相比 FP16 的核心优势是什么?

单选题 · mid

  • A.计算速度更快
  • B.保留与 FP32 相同的指数范围← 正确答案
  • C.尾数精度更高到 23 位
  • D.不需要 Tensor Core 支持

答案:B。TF32 有 8 位指数,与 FP32 相同,数值范围大;FP16 只有 5 位指数,易溢出。TF32 尾数仍是 10 位,速度通常不高于 FP16。它就像“尺寸和 FP32 一样大、内壁特殊加固”的重型箱。

Q8. 共享内存地址映射中,若地址 addr=68(字节),则对应的 bank 编号是?

单选题 · mid

  • A.bank 0
  • B.bank 1← 正确答案
  • C.bank 16
  • D.bank 17

答案:B。bank = (addr/4) mod 32 = (68/4) mod 32 = 17 mod 32 = 17。因此地址 68 落在 bank 17。若多个线程同时访问同一 bank,将产生冲突而串行化。

Q9. 为什么 swizzle 模式能减少共享内存 bank 冲突?

单选题 · hard

  • A.它增加了共享内存容量
  • B.它改变数据存储地址使同一行的访问散落到不同 bank← 正确答案
  • C.它把矩阵缩小到单个 bank 内
  • D.它禁止多个线程同时访问共享内存

答案:B。swizzle 通过 XOR 地址变换,让原本连续映射到同一 bank 的数据分散到不同 bank。swizzle 不改变容量、不缩小矩阵,也不禁止并发访问,只是重新排布了“货位”,使 32 条装卸通道被均匀使用。

Q10. 关于双缓冲与软件流水线,下列说法正确的有哪些?

多选题 · hard

  • A.双缓冲需要两份共享内存空间← 正确答案
  • B.软件流水线让数据拷贝与矩阵计算重叠← 正确答案
  • C.双缓冲可以提高共享内存总容量
  • D.流水线设计可能增加寄存器压力← 正确答案

答案:ABD。双缓冲用两块缓冲区交替装载,A 对;核心思想是搬运与计算重叠,B 对;共享内存容量由硬件决定,双缓冲只是分区使用而非扩容,C 错;为了提前缓存数据,往往需要更多寄存器,可能降低占用率,D 对。这就像准备两个中转堆场让吊机不停歇,但需要更多工人手袋装货。