全章的路线:从一次调用出发,由近到远,最后回到用户等待的完整请求。
加速器执行中的提交、搬移与同步
调用一次 torch.matmul,CPU 先跑框架代码:读形状和类型、挑实现、准备地址参数,再经运行时和驱动把任务交给 GPU。先分清三个经常混用的词:
CPU 提交、GPU 执行,两者构成一条流水。大矩阵算得久,CPU 来得及准备下一项;小算子转瞬结束,GPU 就停下来等 CPU。所以加速有两个方向:缩短 kernel,或者减少加速器等主机的时间。
数据在哪:H2D、D2H、D2D
主机内存(CPU)
显存(GPU)
普通内存的数据要先被 CPU 复制进锁页缓冲,复制引擎才能搬,等于多走一遍。让 CPU 直接在可复用的锁页缓冲里准备输入,能省掉这次中转和反复分配。
例 5-1 · 把 64 MiB 输入切几份传
PCIe Gen5 x16 · 64 GB/s 单向stream 与 event:谁先谁后
stream 是一串按顺序执行的 GPU 任务:H2D 和读它的 kernel 放进同一个 stream,计算自然排在复制之后。复制和计算放在两个 stream 时,用 event 连起来:复制流在 H2D 后记录事件,计算流等它。这个等待发生在 GPU 的队列里,CPU 照样往下提交。
图 5-2 · 两条流、两个槽、两种事件
批 2 要写回槽 A,必须等“批 0 已用完”。“已传完”和“已用完”是同一块缓冲上的两个不同时刻。
程序停住不动?先分“慢”和“卡死”
每次等待都指向一个具体事件,事件永远不被记录,等待就永远不结束。被等的一侧仍有产出,说明只是慢,回到时间线逐段计时;完全不动,才是在等一个不会来的事件。
三种常见的“等不到”
① 漏记“缓冲已用完”,复制引擎一直不能写下一批;② A 流等 B 流的事件,B 的这项任务又排在 A 未完成的工作后面,循环等待;③ 多卡集合通信里有一张卡少发起一次调用。给等待加超时,超时后打印各流未完成的任务。
例 5-2 · kernel 快 4 倍,整次调用只快 1.75 倍
三种计时位置测到三种耗时:在异步调用前后读 CPU 时钟,只量到提交(3 μs);用 CUDA event 夹住 kernel,量到加速器执行;等结果回来再停表,才是调用者真实等待的时间。
首次编译被多少次调用摊薄
编译 100 ms · 每次执行 1 ms一个 kernel 在 SM 上怎么跑
图 5-3 里那段 20 μs 的 kernel 能不能缩短,要看 SM 里发生了什么。固定一个线程块:输出块 128×128、沿 K 每步 64;A 块、W 块各 16 KiB,FP32 累加器 64 KiB,都放共享内存,工作集共 96 KiB,256 个线程(8 个 warp)。硬件取 H100 的一个 SM。
warp
32 个线程一起发射同一条指令,各算各的数据。一条加载指令因此一次给出 32 个地址。
驻留与占用率
线程块要同时分到线程槽、寄存器、共享内存才能上 SM。每项资源能放几块取最小值;占用率 = 驻留 warp ÷ 64。
延迟隐藏
Little 定律:维持带宽需要的在途字节 = 带宽 × 延迟。在途请求都由驻留的 warp 发出,驻留越少,在途越少。
SM 占用率计算器
H100 · 2048 线程 · 64K 寄存器 · 228 KB 共享内存 · 3.35 TB/s ÷ 132 SM矩阵指令的粒度
一条 MMA 是 16×8×16,做 4096 次浮点运算。128×128×64 的块需要 8×16×4 = 512 条,8 个 warp 每个发 64 条。累加器放在共享内存时,每个 K 步前后都要在寄存器和共享内存之间搬一次,放进寄存器就省了这笔搬移。
Hopper 的 warpgroup 指令
4 个相邻 warp 联合发出,直接从共享内存取操作数,异步执行:发出后可以去做别的事。FlashAttention-3 就靠它让矩阵乘与 Softmax 重叠。
图 5-5 · warp specialization:生产者搬、消费者算
每个槽配一对栅栏(barrier):“满”由异步复制完成触发,消费者等它;“空”由计算结束触发,下一次复制等它。按 H100 一个 SM 的份额:搬一个 32 KiB 的 K 块 1.29 μs,算它 0.28 μs。复制比计算慢 4.6 倍,因为这个块每搬 1 byte 只做 64 次运算,而 H100 的矩阵峰值与带宽之比约 295 FLOP/byte。
对应到昇腾:同样五步,换了一套部件
| 机制 | NVIDIA GPU | 昇腾 |
|---|---|---|
| 发射与分工 | warp 是 32 线程的发射单位,生产者/消费者 warp 在同一 SM 内分工 | AIC、AIV 是独立的核:矩阵在 AIC,向量在 AIV,各有指令流 |
| 驻留与容量 | 寄存器、共享内存、线程槽决定每 SM 驻留块数 | L0、L1 与统一缓冲的容量决定每个核一次容纳的块 |
| 搬移与同步 | 异步复制到达栅栏,warp 等栅栏 | MTE / NDDMA 搬移,BufferID 标识缓冲并组织交接 |
异步批量复制(TMA)按张量描述符整块搬,不能在行间插 padding,所以改用 swizzle 置换块内地址来分散 bank,见 5.2.3。
单个算子的分块、布局与实际访存
把投影写成 C = A·W,M=1024,K=4096,N=12288,A、W、C 分别 8、96、24 MiB。算 C[i,j] 要 A 的第 i 行和 W 的第 j 列;接着算右边的 C[i,j+1],A 还是同一行。这行 A 若还在片上,就能直接复用;若已被换走,就要重新搬。
图 5-6 · 同一行 A 被相邻输出反复使用
蓝色是正在用的 A 行,橙色是 W 列,绿色是刚算出的输出。
分块就是让搬来的数据多用几次。固定一块 m×n 的输出,沿 K 依次搬入 A 块(m×k)和 W 块(k×n),一直累加到同一份 FP32 部分和里,最后才写回。A 块每个元素服务 n 列输出,W 块每个元素服务 m 行输出。
片上需求 S = 2mk (A块) + 2kn (W块) + 4mn (累加器) bytes
接口读写 V = 2MK·(N/n) + 2KN·(M/m) + 2MN
分块计算器 · 例 5-3 与图 5-8、5-9
接口 = 工作缓冲 ↔ 下一级存储少读一半数据,为什么可能更慢
128×128 vs 64×64大块只能驻留 1 份,小块能驻留 4 份。驻留越少,在途的访存请求越少,实际拿到的带宽越低(5.1 的 Little 定律)。若两者都受带宽限制:T₁₂₈/T₆₄ = (1560/3096) × (B₆₄/B₁₂₈)。
数据布局:让 32 个请求落到 32 个 bank
共享内存分 32 个 bank,每轮每个 bank 给一个 32-bit 字,bank 号 = 字地址 mod 32。一个 warp 的 32 个 lane 同时读 32×32 FP32 块的一列,行跨度 32 时全部撞进同一个 bank,要排 32 轮。
图 5-10 · bank 冲突可视化
32 lane · 32 bank沿哪个维度切:并行任务与部分和
RMSNorm 对每行先求平方和(一次归约)再缩放。1024 行时一组一行就有 1024 个任务;decode 只有 1 行,同样的分法只产生 1 个任务,GPU 大面积空闲。把一行拆成几段,各自求局部平方和,再合并,再重读输入做归一化。
RMSNorm 拆分 · 图 5-11
[rows, 4096] BF16切 M 或 N,各执行者得到不同位置的完整结果,需要时拼起来;切 K,各执行者得到同一输出的部分和,必须相加。5.2.2 的输出块就是同时切 M、N,再在一个线程块内沿 K 累加,不跨任何边界。到第 6 章,这三种切法会变成多卡并行。
拆分还改变了数值
浮点加法不满足结合律。在 FP32 里算 1 + 2⁻²⁴ + 2⁻²⁴:
先加前两项
先加后两项
在浏览器里复现图 5-13
随机生成一行 4096 个 BF16 元素(每项平方约 0.5),用 FP32 按不同段数求平方和,和双精度参考值比较。书中那一行的结果是:1 段差 −175.2 ULP,8 段只差 −9.2 ULP。
如果用原子加把局部和累到同一地址,到达顺序由调度决定,同一输入两次运行结果可能不同。先写缓冲、再由一个执行者按固定顺序相加,结果就确定,代价是多一次写读,并且要等最慢的一段。更麻烦的是,kernel 往往随 batch 大小改变段数,同一行输入在不同 batch 下会算出不同的输出(第 10.5.3 节的 logprob 不一致正源于此)。
相邻算子的融合、缓冲与流水执行
FFN 里 G = XWg、U = XWu,然后 Z = SiLU(G) ⊙ U,再交给 down 投影。G、U、Z 都是 [1024, 12288],BF16 各 24 MiB。SiLU 和 ⊙ 都是逐元素运算:Z[i,j] 只依赖 G[i,j]、U[i,j],读进来就能直接算出最终结果。
融合省掉的是中间结果的一次写回和一次重读,两步运算本身一步也没少。每少保存一个 24 MiB 的中间量,就少 48 MiB 读写。
例 5-4 与图 5-15 · 激活链加量化,三种融合程度
实测激活链 14.2 → 7.7 μs理论
只看前两步:120 → 72 MiB,时间比 0.6,约 1.7×
实测
RTX PRO 6000:14.2 → 7.7 μs,约 1.8×
峰值显存
读写少了 96 MiB,峰值只从 72 → 60 MiB。容量和访问量是两回事,同一块空间可以被反复读写。
双缓冲:搬下一块的同时算这一块
布局转换也有同样的取舍:另建一份 24 MiB 的重排结果要多 48 MiB 读写,不如让前一个算子直接按后一个需要的布局写出。缓冲则决定两个阶段能否重叠。槽 A 的“搬完了”和“用完了”是两个时刻,只有最后一次读结束,才能覆盖它。
流水模拟器 · 例 5-5 与图 5-17
Tpipe = tm + (n−1)·max(tm, tc) + tc。书中的例子里搬移是慢的一方,双缓冲只能把 6.28 μs 降到 5.44 μs(−13%),计算单元只有两成时间在干活。试试把槽数加到 3,再加快计算:时间不变。要更快只能少搬字节(更大的输出块、L2 共享)或换带宽更高的卡。
背压(backpressure)
访问顺序不同的两个阶段之间要排队:三个 16 KiB 排队块加 32 KiB 重排空间共 80 KiB。RTX PRO 6000 单块最多 99 KB,单独驻留放得下;但一个 SM 要驻留两块时每块只能分到 49 KiB,扣掉重排只剩一个排队槽,搬移器写第二块前必须等计算释放。下游来不及处理,上游就被迫暂停。
FlashAttention:Softmax 也能逐块算
逐元素融合到 Softmax 这里卡住了:分母是整行指数和,只读到前一块时,既不知道最终分母,也不知道后面会不会出现更大的分数。标准做法 S = QKᵀ/√d、P = softmax(S)、O = PV,中间矩阵随序列长度平方增长。
单头,L = 8192,d = 128
Q K V O 各 2 MiB · S P 各 256 MiB (FP32)要丢掉已经处理过的分数,只需保留三个量:最大分数 m,以 m 为基准的指数和 ℓ,同样权重下的值之和 u。最终输出 = u / ℓ。
在线 Softmax 单步演示 · 例 5-6、练习 5-3
每块一个元素| 块 | s | V | m′ | r = em−m′ | ℓ′ = rℓ + p | u′ = ru + pV | u′/ℓ′ |
|---|
最大值变大时,旧的 ℓ 和 u 必须同乘 r,分子分母一起缩小,相对权重才不变。旧分数用完即丢,缓冲留给下一块。
K/V 块该切多大 · 图 5-20
99 KB 共享内存 = 101,376 bytes2ad (Q) + 2bd (K/V 槽) + 4ab (分数/概率) + 4ad (输出累加器) + 12a (逐行状态) ≤ 101376
b 越小,留给 Q 的空间越大,整份 K/V 被重扫的次数越少;但每块只有几行,在线状态更新次数暴涨,矩阵乘的一个维度也太小。从 64 缩到 1,少读 144 MiB,更新次数却变成 41 倍。
实测时间
2.6 ms → 80 μs
数学后端 vs FlashAttention 后端,约 33×(因果注意力,8192 token,RTX PRO 6000)
新增显存峰值
848 → 22 MiB
计量输出与临时工作区,Q K V 预先驻留
之后的版本
FA-2 改进线程间分工、减少非矩阵运算;FA-3 用异步执行让搬移、矩阵乘、Softmax 相互重叠。
编译器:表达、变换与选择
前两节是手算的。实际模型有各种形状、各种算子组合、各种硬件,编译器做的是三件事:把运算写成循环与数组访问,按依赖和数值规则排除非法变换,结合容量与实测比较成本。
候选有多少
只算相邻边界的融合/分开用循环变换表达分块与融合
以 Y = SiLU(AW) 为例。split 把 i 拆成块号 io 和块内坐标 ii(i = 64·io + ii);reorder 把块循环提到外层;把 K 按 32 拆成 128 个归约块;最后把 SiLU 挪到每个输出块的 ko 循环之后(Halide/TVM 叫 compute_at)。
for i in range(M):
for j in range(N):
acc = 0.0
for k in range(K):
acc += A[i,k] * W[k,j]
C[i,j] = round_to_bf16(acc)
for i in range(M): # 再读一遍 C
for j in range(N):
Y[i,j] = silu(C[i,j])for io in range(ceil_div(M, BM)): for jo in range(ceil_div(N, BN)): acc = zeros_fp32(BM, BN) # 16 KiB for ko in range(ceil_div(K, BK)): a = load_tile(A, io, ko) # 4 KiB w = load_tile(W, ko, jo) # 4 KiB matmul_accumulate(acc, a, w) c = round_to_bf16(acc) store_tile(Y, io, jo, silu(c))
缩进层级就是保存期限。计算位置还决定复用还是重算:放在外层,先算完整结果供大家读;放进消费者的分块循环,只算当前需要的部分,多个块要用同一份时就会重复计算。
哪些变换不合法
激活不能挪进归约
分块 Softmax 要留统计量
两块各一个值 1 和 3,块内归一化后还是 1 和 3。只凭这两个输出,分不清整行应该给它们 1:2 还是 2:1 的权重。所以 FlashAttention 必须保留 m、ℓ、u。
舍入位置是运算定义
原程序先舍入到 BF16 再激活,融合后的程序也必须保留这次 round_to_bf16。中间值可以从寄存器直接传,舍入不能省。数据放在哪和数值格式是两项独立选择。
例 5-7 · 整行 FP8 scale 必须先读完整行
E4M3FN · 最大值 448AKG:多面体编译
AKG 把规则循环表示成三类信息:要执行哪些迭代、每次读写哪些数组元素、哪些读写必须保持先后。循环边界和下标能用线性约束描述(“多面体”由此得名),编译器据此推导依赖、调整顺序,并由数组访问反推每个 tile 需要哪块数据(64×64×32 的 tile 需要 A 的 64×32、W 的 32×64)。外层安排算子顺序,内层安排块的连续计算和局部缓存。手工做的“拆循环—定读取区域—留累加结果—归约后激活”,在这里成了一组联动的编译步骤。
例 5-8 · 融合反而多读 40%
A [4096,4096] FP16 → FP8 · W [4096,1536] FP8单独量化:读 32 MiB 原输入,写 16 MiB FP8,每个输出列块再读 16 MiB FP8。融合进矩阵乘:先读 32 MiB 求每行 scale,每个列块都重读 32 MiB FP16 原输入、就地量化。权重与输出两种方案相同,各 204 MiB。
成本估计与实测选择
| 步骤 | 矩阵算例要填什么 | 排除或比较的依据 |
|---|---|---|
| 1 固定目标 | 输入形状、精度、误差、调用频数 | 同一项数学工作、同一负载 |
| 2 列出候选 | tile、循环次序、缓冲数、融合范围 | 编译器与硬件是否支持 |
| 3 检查可行性 | 共享内存、寄存器、线程、对齐、依赖 | 编译资源报告、正确性检查 |
| 4 估计时间 | 计算量、实际读写、启动、布局转换 | 各资源服务时间与关键路径 |
| 5 实测并选择 | 代表形状的时间、波动、完整算子链 | 最小化负载总成本,保留相近候选 |
实测往往推翻直觉:配套的矩阵转置程序在 RTX PRO 6000 上,16×16 块 7.9 μs,32×32 块 2.9 μs,2.7×。TVM 自动调优或 Agent 改 kernel,评价对象都是编译后的程序,因为编译器可能把源码里分开的算子融合成一个 kernel。
例 5-9 · 选哪个实现取决于调用比例
旧:A、B 都 10 μs · 新:A 5 μs、B 20 μs运行时:提交、重放与动态形状
编译器选好程序后,运行时每一步还要准备输入、挑程序、提交任务、管理缓冲。kernel 代码一字不改,换一种提交方式或复用已有的准备结果,总时间也会变。
例 5-10 · GPU 越快,越是在等 CPU
真实案例:vLLM v0.6.0(2024 年 9 月)
单张 H100 · Llama 3 8BAPI 服务器和引擎在同一个 Python 进程里争 GIL,一步里 GPU 只忙 38%。v0.6.0 把 API 服务器挪到独立进程、引入多步调度、把输出处理改成异步。没动一个 kernel、没换硬件,吞吐到 v0.5.3 的 2.7×(70B 为 1.8×)。
2.7× 是三项改动合计,在请求同时到达、--num-scheduler-steps 10 下测得,不能全部归于 GIL;多步调度在低负载下可能拉长首 token 等待。
CUDA Graph:重放提交序列
先把一串 kernel 及其依赖录成图,之后主机每次只发一次 graph launch。GPU 还是逐个执行图里的节点。图重放减少的是主机提交次数,减少 kernel 数量靠的是融合。
图 5-27 ~ 5-31 · 3 次 FFN,四种方式
每次 FFN 6 个 kernel,融合激活链后 5 个例 5-11 · 输入复制会吃掉图重放的收益
普通 40 μs = 等提交 20 + 计算 20 · 图 = 准备 5 + 复制 + 计算 20图按录制时的地址 G 读输入。新输入在别处(地址 X)时,必须先复制到 G,复制要读一遍、写一遍。
同样的思路用在准备阶段:FlashInfer 的 plan / run
plan 根据本步各请求长度和 KV 存放位置选 kernel、划分任务、安排工作区,并把元数据拷到 GPU;run 照计划算注意力。同一步 36 层的长度和 KV 布局都相同,只 plan 一次:36 层注意力从 2.0 / 2.4 ms 降到 0.72 / 0.75 ms,H2D 复制从 144 次降到 4 次,kernel 数不变。反过来,请求长度变了还沿用旧计划,结果就错了。
动态形状:通用、分桶,还是特化
例 5-12 · 重复多少组,特化才值得
每组 10 次 FFN:8×256 行 + 1536 + 2048| 策略 | 准备 P | 每组执行 T | 做法 |
|---|---|---|---|
| 通用 | 100 ms | 17.0 ms | 按实际 5632 行执行,MFU 20% |
| 分桶 512 / 2048 | 400 ms | 12.4 ms | 补齐到 8192 行(多算 45%),MFU 40% |
| 逐形状特化 | 900 ms | 6.8 ms | 三个形状各编一份,MFU 50% |
自动调优多久回本
Persistent kernel:按数据块交接
后一个 kernel 通常要等前一个全部算完。如果它只需要前一个的一块结果,就把等待条件缩小到数据块:kernel 常驻 GPU,从队列取任务,用事件确认数据就绪(Mirage Persistent Kernel 即此类)。例子:512 个 token 的 up 投影接 SiLU,每 64 个 token 一块,共 8 块。
图 5-36 / 5-37 · 粗粒度 vs 按块流水
每块投影 12.8 μs、激活 1.76 μs,每次 launch 5 μs;按块调度每个任务另加 0.7 μs。省下的约 11 μs:少一次 launch,七块激活藏进投影;任务开销又加回 6.3 μs。两阶段越接近,按块重叠省得越多;一边远长于另一边时,基本只剩少一次 launch。
从局部优化到完整请求
局部加速比必须放回整条请求里看。串行阶段按 Amdahl 定律相加;并行分支要沿依赖图找最长路径;共享资源还会让一个分支的优化拖慢另一个。
Amdahl 定律
S = 1 / ((1−f) + f/s)图 5-38 · 关键路径会切换
准备 10 → A ∥ B → 收尾 10容量的节省 ≠ 读取的节省
DeepSeek V4.1 让 38 个全局注意力层共享 4 份全局 KV:8K 下全局 KV 驻留从 27.455 降到 6.953 MiB(约四分之一),逐层逻辑读取却只从 12.556 降到 11.375 MiB(约 −9%)。共享状态不必每层各存一份,但用到它的每层仍要读它。
综合案例:一次 Qwen3-8B 请求
RTX PRO 6000,输入 7239 token,输出 32 token,并发 1,BF16、eager。36 层 × 32 步 = 1152 次 SwiGLU 激活调用:prefill 36 次(每次 7239 行),decode 1116 次(每次 1 行)。替换成一个新调度的 kernel(block256 / 4 warps)。
从 kernel 加速比到用户等待
vLLM 0.23 · 11 对交错计时| 阶段 | 形状 × 次数 | 原 kernel | 新 kernel |
|---|---|---|---|
| Prefill | 7239 行 × 36 | 11.22 ms | 11.24 ms |
| Decode | 1 行 × 1116 | 2.37 ms | 0.92 ms(2.6×) |
| 激活合计 | 13.6 ms | 12.2 ms |
11 对里 9 对变快、2 对变慢,约占请求 0.16%。decode 的改进发生在首输出之后,所以首 token 时间仍由 prefill 决定;输出越长,单行激活重复越多,累计节省越多。方法总结:先统计各形状的调用次数,再算每次省多少,最后按依赖关系判断这些节省能否缩短请求。
四个容易下错的结论
图重放把 3 次 FFN 的主机提交从 18 降到 3,GPU 仍执行 18 个 kernel;融合才把 kernel 减到 15。主机提交和加速器计算要分开看。
K/V 块从 64 行缩到 1 行,少读 144 MiB,状态更新却变成约 41 倍。读取、状态更新与矩阵处理率一起决定成本。
搬 1.29 μs、算 0.28 μs 时,两个槽已让搬移器满负荷,第三个槽没用,加快计算也没用。只能少搬字节或提高带宽。
只跑 70 组时,分桶 1.27 s,特化 1.38 s。特化每组更快,但省下的时间还没抵消额外的准备开销。
三个反复用到的判断
一、复用需要空间
更大的 tile 让输入多用几次,也要更多累加存储;保留低位宽中间结果让后续少读,也要中间存储。在 SM 上,这份空间还决定驻留几个线程块、有多少在途请求能隐藏延迟。
二、依赖决定顺序
逐元素运算拿到输入就能完成,归约要保存部分和或在线统计。编译器按依赖移动计算位置,运行时用事件确认数据就绪、缓冲可释放。融合和流水都建在依赖之上。
三、总时间取决于执行顺序
带宽决定一批数据传多久,较慢阶段决定流水节拍,复用次数决定准备开销能否回本,关键路径决定局部改进能否缩短请求。
下一章把这些关系推广到多张加速器:一片结果何时就绪、谁要读、保存多久,将决定通信何时开始,以及两端要多少缓冲。
动手验证
实验 5-1 · 矩阵块大小如何改变局部存储与读取量
把例 5-3 的输出块改成 64×128(k=32),求局部存储、A/W 读取次数与总访问量;64×128 的有效带宽至少要达到 64×64 的多少才更快?(上面的分块计算器可以直接选 m=64、n=128。)
实验 5-2 · 激活融合能减少多少访存与显存(核心)
画出三步独立与完全融合时各张量的分配、使用、释放顺序,推导 156 / 60 MiB 与峰值显存。若融合每次多 5 μs 固定开销、带宽 1792 GB/s,输入至少多少行时融合才划算?
实验 5-3 · 分块归约与预取
在例 5-6 里加入分数 ln4、值 5 的元素(在线 Softmax 演示里点“练习 5-3”)。再从 99 KB 中划出一个 b×d 的 BF16 预取槽(b=64),求新的最大 Q 行数、扫描次数与访问量。
实验 5-4 · 融合改变的是结果还是成本
把 SiLU 移进 ko 循环、删掉中间 BF16 舍入、把量化移进每个输出列块循环,分别说明改变了数学运算、数值舍入还是执行成本,并各给一个能区分前后的输入。
实验 5-5 · 分块循环与临时存储
写出 i-k-j 的分块循环,标出输入缓存和累加器的创建、最后使用与释放位置;对照转置程序两种块大小,比较访问连续性、同步与临时数据量。
实验 5-6 · 调用比例与调优收益
p 取 1/2、2/3、9/10 计算两种实现的平均时间。若按形状选实现每次多 1 μs,何时优于始终用旧实现?
实验 5-7 · 特化要重复多少次
把小形状调用从 8 次改为 4 次,求三种方案两两相等时的重复次数;再假设两个分桶程序已缓存,重新求各方案最优的区间。
实验 5-8 · 图重放与融合各减少什么(核心)
统计四种方式下的主机提交次数和 kernel 数;把例 5-11 换到 RTX 4090(1008 GB/s)求临界输入大小(上面的复制计算器可以直接切换);再让上游直接写图缓冲,比较 2 MiB 与 16 MiB 时的完成时间。
实验 5-9 · kernel 加速如何改变请求耗时(核心)
用 11 对请求记录计算配对差的中位数,解释它和“两组中位数相减”的区别;decode 从 31 步增至 127 步时,激活总共能省多少?在图 5-38 中,A 缩到多少后请求时间不再下降?