flowchart TD
subgraph Stride2D["二维张量行优先 Stride 映射"]
Coord["二维坐标 (row, col)"] --> Formula["一维物理下标 = row * stride_row + col * stride_col"]
Formula --> Mem["连续跨步读取二维矩阵块"]
end
第 4 课:二维 Grid、Stride 与广播
参考答案版:包含完整代码实现与问答题深度参考解析。建议先独立完成练习题后再展开核对。
本课目标与完成标准
学完后你应能:实现矩阵按列广播 bias,区分逻辑边界与物理 stride。
通过必须同时满足:
- 独立补齐本课唯一的代码填空题,并通过给定检查;
- 三个问答题均说明因果链,而不是只报术语;
- 能指出至少一个正确性边界和一个性能取舍;
- 总分不低于 8/10,且没有一票否决级概念错误。
前置关系
- 课程前置:Python、PyTorch 张量、CUDA 基本线程/内存概念
- 本课在路线中的作用:广播不复制 bias;每行 program 使用相同列下标读取 b,并按 x stride 找输入。
核心心智模型
1. 它是什么,解决什么问题
在深度学习的前向与反向计算中,线性层(Linear Layer)的输出通常需要加上偏置向量:,其中 ,。 根据 NumPy/PyTorch 广播语义,长度为 的偏置向量 需要沿行维度( 维度)复制并与每一行相加。如果显式将 复制成 的稠密矩阵,将浪费大量全局显存并引入无谓的内存搬运。 本课解决的核心工程问题是:如何在 Triton 中通过将 Program 映射到二维逻辑网格(或一维 Program 处理一整行),结合张量的物理跨步(Stride)代数,实现零内存开销的隐式广播(Implicit In-Register Broadcasting),并深刻辨析“逻辑维度边界(Logical Shape)”与“物理内存步长(Physical Stride)”的本质解耦。
2. 它如何工作
二维 Stride 寻址与广播的数据通路如图 4-1 与图 4-2 所示:
图 4-1:Triton 二维 Block 与行优先 Stride 计算代数
flowchart LR
Bias["1D 偏置 (BLOCK_N,)"] --> Expand["切片扩展维度 [None, :] 变成 (1, BLOCK_N)"]
Expand --> Broad["沿行方向自动广播与 (BLOCK_M, BLOCK_N) 矩阵相加"]
图 4-2:Triton 广播机制(Broadcasting)执行流
网格与行映射:主机端发射 个 Program 块:
grid = (M,)。每个 Program 的row = tl.program_id(0)精准负责矩阵的一整行。列向量与步长寻址: 通过
cols = tl.arange(0, BLOCK)生成列索引向量,计算输入矩阵的物理偏移量: 其中 为行步长(跨行跳跃的元素个数), 为列步长(通常为 1)。这使得内核天然兼容任意内存布局(如连续、转置或带 Padding 的对齐内存)。偏置向量的零拷贝广播: 偏置向量 只依赖于列索引:
b + cols。所有 个并发执行的 Program 块在相同的逻辑列位置上读取相同的偏置数据 。底层 L2 Cache 会对偏置张量进行高命中率广播缓存,彻底省去显式张量扩充。受保护的计算与写回: 使用掩码
mask = cols < N抑制超出逻辑列数 的无效访存,直接在寄存器中执行xv + bv,随后写回连续输出矩阵:out + row * N + cols。
3. 正确性条件与常见误区
逻辑列宽 vs 物理跨步(混淆 与 ): 常见致命错误是将谓词掩码写为
mask = cols < sxm。在张量存在行对齐填充(Row Padding)或属于大矩阵切片时,物理行步长 往往严格大于逻辑有效列数 (例如 ,但为了 128-bit 对齐,底层分配了 )。若使用cols < sxm,内核将误把 Padding 区域的未初始化垃圾数据读入并写回输出,破坏张量边界。块大小为 2 的幂次约束(Power of Two): Triton 的
tl.arange(0, BLOCK)要求BLOCK必须是 2 的幂次。若列数 ,必须将BLOCK向上对齐到triton.next_power_of_2(17) = 32,并通过mask = cols < N屏蔽后 15 个 lane。输出张量的连续性假设: 若写回公式写为
out + row * N + cols,则隐式假设了输出矩阵out必须是行优先且完全紧凑连续的(out.stride(0) == N, out.stride(1) == 1)。若输出也是非连续切片,必须传入输出自身的步长。
4. 性能与工程取舍
一维 Program 一整行 vs 二维分块(Tiling): 当列宽 较小(如 或 4096)时,一维 Program 处理一整行具有极其简洁的代码结构,无跨 Block 协调开销; 但当 极大(如 LLM 词表维度 )时,单个 Program 分配如此巨大的
BLOCK会瞬间耗尽 SM 寄存器配额,导致编译失败或严重的寄存器溢出(Register Spill)。此时必须将网格重构为二维分块grid = (M, cdiv(N, BLOCK_N))。Cache 广播效应: 由于 个 Program 几乎并发发射并读取同一段偏置地址 ,偏置向量会被迅速固化在 SM 的 L1/L2 Cache 中,访存带宽损耗趋近于零。
具体演示
以 为例:
BLOCK = triton.next_power_of_2(17) = 32。- 网格发射 3 个 Program:
pid=0, 1, 2。 - 每个 Program 中:
cols = [0, 1, ..., 16, 17, ..., 31]mask = [True x 17, False x 15]row 0加载x[0, 0:17]与b[0:17]row 1加载x[1, 0:17]与b[0:17]row 2加载x[2, 0:17]与b[0:17]
- 的 17 个浮点数在片上被 3 个 Program 高速共享复用,写回时严格保护后 15 个无效位不污染显存。
请在阅读后先合上这一节,用自己的语言复述“输入状态 → 中间状态 → 输出状态”,再做练习。
实践任务:唯一代码填空题
补齐 bias 地址。
规则:只能修改 TODO/______ 所在位置;不要删除断言或放宽误差。代码注释说明了每个边界条件。
Code
import torch
import triton
import triton.language as tl
@triton.jit
def bias_kernel(x, b, out, M: tl.constexpr, N: tl.constexpr,
sxm: tl.constexpr, sxn: tl.constexpr, BLOCK: tl.constexpr):
row = tl.program_id(0)
cols = tl.arange(0, BLOCK)
mask = cols < N
xoffs = row * sxm + cols * sxn
xv = tl.load(x + xoffs, mask=mask)
bv = tl.load(b + ______, mask=mask) # TODO: bias 只依赖列
tl.store(out + row * N + cols, xv + bv, mask=mask)
def add_bias(x, b):
assert x.ndim == 2 and b.ndim == 1 and x.shape[1] == b.numel()
M, N = x.shape
out = torch.empty((M, N), device=x.device, dtype=x.dtype)
block = triton.next_power_of_2(N)
bias_kernel[(M,)](x, b, out, M, N, x.stride(0), x.stride(1), BLOCK=block)
return out
for shape in ((3, 17), (4, 128)):
x = torch.randn(shape, device="cuda"); b = torch.randn(shape[1], device="cuda")
torch.testing.assert_close(add_bias(x, b), x + b)检查方法
在 CUDA/Triton 环境运行本单元格;断言覆盖规则尺寸和非规则尾块。首次 JIT 不计入性能。
提交时请给出:补齐后的代码、实际运行输出(环境不可用时注明“仅静态审查”)以及对失败用例的解释。
Q1
不要背定义:请从输入、状态变化和输出三个阶段解释“二维 Grid、Stride 与广播”的工作机制。
你的答案:
Q2
把 mask 写成 cols < sxm 为什么对 padding stride 错?
你的答案:
Q3
N 很大时怎样扩展为二维 grid 并保持输出地址正确?
你的答案:
评分与通过规则
- 代码 4 分:正常输入 2 分,边界输入 1 分,解释实现 1 分;
- Q1~Q3 各 2 分;
- 一票否决:结果碰巧正确但核心因果链错误、删除边界检查、把未运行结果说成实测。
需要提示时按四级机制请求:概念区域 → 具体方向 → 关键局部 → 完整答案。