/ AI Infra
[Infra-11] Stride:从 Tensor 视图到 Kernel、KV Cache 与通信布局
从地址公式出发理解 stride:它如何支撑零拷贝 view、约束 Kernel 寻址与合并访存,并进一步影响 Attention、Paged KV Cache、量化、通信打包、torch.compile 和线上正确性。
很多人第一次接触 stride,是在 transpose() 之后调用 view(),然后遇到一句:
view size is not compatible with input tensor's size and stride于是 stride 很容易被理解成一个 PyTorch API 细节,或者一串只有写自定义算子时才需要关心的数字。
在 AI Infra 中,这个认识远远不够。Stride 描述的是逻辑索引如何映射到存储地址。它向上决定一次 permute 能否只修改 metadata,向下决定 Kernel 读哪个地址、相邻线程能否合并访存;再往系统侧延伸,它还会影响 KV Cache 的页内布局、通信前是否需要 pack、offload 后能否正确恢复数据,以及编译器是否为同一个 shape 重新生成图。
如果 shape 是 Tensor 对外展示的逻辑结构,那么 stride 就是贯穿框架、Kernel 与存储系统的数据布局协议。
1. 先从一条地址公式理解 stride#
对普通的 strided Tensor,可以把它概念化为:
Tensor = storage + storage_offset + shape + stride + dtype + device其中:
storage是底层的一维存储;storage_offset是 Tensor 第一个逻辑元素相对 storage 起点的偏移;shape[k]表示第k维有多少个逻辑元素;stride[k]表示第k维索引增加 1 时,要在 storage 中跨过多少个元素;dtype决定每个存储元素占多少字节。
对逻辑索引 ,元素偏移是:
如果 base 指向 storage 起点,那么字节地址是:
例如一个连续的 Tensor:
x = torch.arange(24).reshape(2, 3, 4)
print(x.shape) # torch.Size([2, 3, 4])
print(x.stride()) # (12, 4, 1)访问 x[b, i, j] 时,元素偏移就是:
b * 12 + i * 4 + j这里有一个经常跨语言、跨设备踩坑的细节:PyTorch 的 stride 以元素为单位,不一定以字节为单位。如果一个 BF16 Tensor 的 stride(0) == 1024,对应的字节跨度是 1024 * 2 = 2048。而某些 C/C++、DMA 或通信接口接收的 pitch/stride 却可能是字节数。接口边界上必须明确单位。
1.1 Shape 相同,不代表布局相同#
下面两个 Tensor 的 shape 和数值完全相同,stride 却不同:
x = torch.arange(6).reshape(2, 3)
y = x.t().contiguous().t()
print(x.shape, x.stride()) # (2, 3), (3, 1)
print(y.shape, y.stride()) # (2, 3), (1, 2)
print(torch.equal(x, y)) # True如果下游只检查:
assert x.shape == y.shape它只验证了逻辑接口,没有验证地址解释是否一致。这正是许多自定义 Kernel 和 KV Cache 问题的根源。
1.2 Stride 也不是所有物理布局的完整描述#
“stride 是物理布局描述”适用于规则的 strided view,但它不是任意布局的万能编码。
例如 4-bit 权重可能把两个逻辑元素打包进一个 byte,scale 可能按 group 存放,硬件专用 GEMM 还可能采用 tile、swizzle 或 interleave。此时除了普通 stride,还要明确:
pack factor
group size
scale stride
tile layout
swizzle rule
alignment因此更精确的说法是:stride 是规则多维 Tensor 最核心的地址映射参数,也是更复杂 layout contract 的基础组成部分。
2. 为什么 transpose 可以是零拷贝#
考虑:
x = torch.arange(24).reshape(2, 3, 4)
y = x.transpose(1, 2)
print(x.shape, x.stride()) # (2, 3, 4), (12, 4, 1)
print(y.shape, y.stride()) # (2, 4, 3), (12, 1, 4)transpose 不需要移动 24 个元素。它只是交换 shape 和 stride 中对应维度的解释,让 y 继续共享 x 的 storage。
这类 view 操作的价值,是把布局变换从“搬运数据”降成“修改 metadata”:
storage 不变
data copy = 0
shape / stride / offset 改变推理路径中经常出现:
[B, S, hidden]
-> [B, S, num_heads, head_dim]
-> [B, num_heads, S, head_dim]
-> packed token layout
-> paged KV layout如果每一步都物化为新的连续 Tensor,代价不仅是一个 API 调用,还包括 D2D copy、临时 buffer、额外 HBM 流量和新的同步依赖。
2.1 view、reshape、permute 与 contiguous#
这几个 API 可以这样区分:
| 操作 | 主要行为 | 是否可能复制 |
|---|---|---|
transpose / permute | 改变维度与 stride 的解释 | 通常不复制 |
view | 在当前 stride 可兼容时重解释 shape | 不复制;不兼容时报错 |
reshape | 尽量返回 view,否则创建副本 | 可能复制 |
contiguous | 按当前逻辑顺序生成 canonical layout | 非连续时会复制 |
expand | 用 stride 0 表示广播维 | 不复制 |
PyTorch 对 view() 的兼容条件有明确约束;而 reshape() 是否复制属于实现结果,性能敏感代码不应凭感觉判断。可以比较 storage 指针,或在 profiler 中检查 clone、copy_、contiguous。
2.2 Stride 0:读起来方便,写起来危险#
x = torch.randn(1, 128)
y = x.expand(32, 128)
print(y.stride()) # (0, 1)y[b, :] 的不同 batch 行实际都指向同一段 storage。作为只读输入,这是一种高效广播;作为输出或 in-place 写入目标,多个逻辑位置可能映射到同一个物理地址。
所以“支持 arbitrary-stride input”与“支持 arbitrary-stride output”不是同一份承诺。Kernel 的输出合同通常还要禁止 overlapping storage。
3. 到了 Kernel 层,stride 就是地址生成代码#
一个二维 Kernel 最朴素的寻址方式是:
offset = row * stride_row + col * stride_col;
value = input[offset];而只支持 row-major 连续输入的 Kernel 可能直接写成:
offset = row * num_cols + col;后者隐含了:
stride_row == num_cols
stride_col == 1两种设计都合理,关键是不能让接口和实现说两套话。
3.1 Canonical-layout Kernel#
只支持少数固定布局的优点是:
- 地址表达式更简单;
- stride、tile size 等信息可以编译期固化;
- 寄存器和整数运算更少;
- 更容易使用向量 load/store、TMA 等硬件路径;
- 编译变体与测试空间更可控。
代价是调用方必须满足布局约束,不满足时需要拒绝、dispatch 到其他 Kernel,或先做 layout conversion。
3.2 Stride-aware Kernel#
真正按运行时 stride 寻址,可以直接消费 transpose、padded slice 或跨层 view,避免一次完整 copy。但通用性不是免费的:动态地址计算、更多寄存器、难以证明的对齐条件,以及更复杂的 fast-path dispatch 都可能降低性能。
实践中更常见的结构是三级路径:
if is_canonical_layout(x):
return fast_kernel(x)
if x.stride(-1) == 1:
return outer_stride_kernel(x)
return generic_or_contiguous_fallback(x)Triton 的 make_block_ptr 也要求显式给出 base、shape、strides、offsets、block_shape 和 order。这说明即使使用更高级的块级抽象,编译器仍然需要完整的地址映射信息。
3.3 最危险的是“半通用”#
下面这种写法尤其危险:
offset =
page_id * stride_page +
token_id * num_heads * head_dim +
head_id * head_dim +
dim_id;page_id 使用了真实 stride,后面三维却偷偷假设连续。它可能在 canonical 输入上通过所有测试,在 padded 或跨层 view 上静默读错。
正确选择只有两个:
// 方案一:所有维度都使用真实 stride
offset =
page_id * stride_page +
token_id * stride_token +
head_id * stride_head +
dim_id * stride_dim;或者:
// 方案二:明确验证 canonical layout
TORCH_CHECK(stride_dim == 1);
TORCH_CHECK(stride_head == head_dim);
TORCH_CHECK(stride_token == num_heads * head_dim);一半接收 stride、一半硬编码连续假设,是 stride Bug 最常见也最隐蔽的形态。
4. Stride 为什么会直接影响 GPU 性能#
“能正确寻址”只解决 correctness;地址在一个 warp 内如何分布,才决定访存效率。
假设 lane t 访问:
address(t) = base + t * stride_lane * element_size当 stride_lane == 1 时,相邻 lane 通常访问相邻元素,硬件更容易把请求合并成少量内存事务。当 stride_lane == 128 时,同一 warp 的地址会分散到更多 segment,造成 over-fetch、cache line 利用率下降和更多内存事务。
这里真正重要的不是“Tensor 最后一维必须连续”这句口诀,而是:
Kernel 中映射到相邻线程的那个逻辑维度,应该尽量对应小 stride,最常见是 stride 1。
Attention、RMSNorm、Softmax 经常让 lane 沿 head_dim 或 hidden dimension 工作,所以它们常要求最后一维连续。FlashInfer 的 TRT-LLM decode 接口就明确要求 head_dim 维 stride 为 1,同时允许 head 与 page 维使用任意 stride;这是一个很典型的“内层严格、外层通用”的 layout contract。
4.1 Stride 还影响向量化和对齐#
即使 stride(-1) == 1,也不代表一定能安全使用 16-byte vector load。还要检查:
storage_offset 对齐
base address 对齐
行首地址对齐
元素数量是否满足向量宽度
边界是否需要 mask因此 Kernel 合同通常不应只写:
input: [B, S, H]而应该写成:
Input:
shape: [B, S, H]
dtype: fp16 / bf16
required:
stride_H == 1
row address aligned to 16 bytes
supported:
arbitrary stride_B
arbitrary stride_S
unsupported:
overlapping storage
stride_H != 15. 在常见推理算子中怎么看 stride#
5.1 GEMM:leading dimension 本质上就是 stride#
对:
逻辑 shape 可能是:
X: [M, K]
W: [N, K]
Y: [M, N]但 GEMM 库还需要 layout、transpose flag、leading dimension 与 batch stride。权重究竟是 row-major、column-major、转置 view、TP shard,还是量化后的 packed tile,仅看 [N, K] 无法判断。
因此看到转置权重时,不应该立刻在每次 forward 中调用:
w = w.transpose(0, 1).contiguous()更合理的顺序是:
- 让 GEMM 通过 transpose/layout 参数直接消费;
- 若硬件 Kernel 需要专用布局,在模型加载阶段转换一次;
- 如果权重长期固定,直接以 backend 偏好的格式保存;
- 最后才考虑在热路径中反复 materialize。
5.2 QKV、RoPE、RMSNorm 与 Softmax#
QKV 常见逻辑布局包括:
BSHD = [batch, seq, head, dim]
BHSD = [batch, head, seq, dim]
THD = [total_tokens, head, dim]若它们都是普通连续布局:
BSHD stride = [S*H*D, H*D, D, 1]
BHSD stride = [H*S*D, S*D, D, 1]二者的 dim 都连续,但 token 与 head 的局部性不同。prefill 需要处理大量 query token,decode 需要扫描历史 KV;同一个布局不一定同时对两条路径最优。
RoPE 成对读取 hidden 元素,RMSNorm 和 Softmax 沿归约维计算 max/sum/square sum。若归约维 stride 不是 1,Kernel 可能需要 gather 式访问,或先做一次转换。是否值得支持通用 stride,应比较:
只使用一次的大 Tensor,直接 strided 访问可能更省;被多次复用的权重或 cache,预先转换往往更划算。
5.3 量化:逻辑 stride 与物理 packing 必须分开#
逻辑权重是 [N, K],物理存储可能是:
packed_weight: [N, K / pack_factor]
scales: [N, K / group_size]
zero_points: [...]此时“第 k 个权重”不再等于“从 base 跨过 k * sizeof(dtype)”。Kernel 需要同时理解逻辑索引、打包单元、group 和 scale 布局。把普通 Tensor 的 stride 公式直接套到 W4、FP4 或 swizzled scale 上,往往会得到合法地址中的错误数据。
6. Paged KV Cache:page table 与 stride 各管一层#
以 NHD Paged KV Cache 为例:
[num_pages, page_size, num_kv_heads, head_dim]连续情况下:
stride_dim = 1
stride_head = head_dim
stride_token = num_kv_heads * head_dim
stride_page = page_size * num_kv_heads * head_dim访问请求中第 t 个历史 token 时,系统先通过 page table 得到物理页:
logical token t
-> logical_page, token_in_page
-> physical_page = page_table[request, logical_page]然后再用 stride 定位页内元素:
offset =
physical_page * stride_page
+ token_in_page * stride_token
+ kv_head * stride_head
+ dim * stride_dim所以二者解决的是不同问题:
Page table 负责“逻辑页映射到哪个物理页”,stride 负责“物理页内的 token、head 与 dim 怎样排布”。
FlashInfer 同时支持 NHD:
[page, token, head, dim]与 HND:
[page, head, token, dim]它们拥有相同的逻辑信息,却让 token stride 与 head stride 交换。选哪一个要看 Attention Kernel 的线程映射、prefill/decode 访问模式与后端支持,而不是只看名字。
6.1 Layer-major 与 block-major 的系统取舍#
把 layer 维加入 KV Cache 后,常见选择是:
layer-major: [num_layers, num_blocks, ...]
block-major: [num_blocks, num_layers, ...]Layer-major 容易给每层 Attention 提供连续 slice;block-major 则把同一个 block 的多层 KV 放得更近,更利于 PD 分离、offload 或 connector 按 block 跨层搬运。
这不是单个 Kernel 能独立决定的布局。它同时连接:
KV allocator
-> cache write kernel
-> attention backend
-> prefix cache
-> offload / KV connector
-> RDMA or storage backend2026 年 vLLM 的一个 RFC 就在讨论这种张力:block-major 能让 connector 聚合跨层 block,却可能让 per-layer view 变成 non-contiguous;替代方案是使用一个 layer-major 大 allocation,让每层保持连续,同时让层间地址维持固定 stride,便于 cudaMemcpy2D 或 RDMA 注册利用。
7. 通信系统为什么也必须理解 stride#
NCCL、RDMA 和常见 DMA 接口最喜欢的输入是:
base address + num_bytes但一个真实 KV view 可能是从以下 Tensor 切出来的:
[num_blocks, num_layers, page_size, num_heads, head_dim]当取某一层时,逻辑元素是完整的,物理地址却可能分散在每个 block 中。系统通常有三种选择。
7.1 Pack,再发送,再 unpack#
strided source
-> pack kernel
-> contiguous communication buffer
-> NCCL / RDMA
-> unpack
-> destination layout优点是协议简单,代价是至少增加一次读写往返、临时 buffer 和同步点。
7.2 让通信层接收布局描述#
通信组件接收:
base
shape
stride or pitch
block size
offset
layout enum然后使用 scatter-gather、2D copy 或专用 pack Kernel。它能消除部分中间 materialization,但协议、注册和边界检查更复杂。
7.3 从 allocator 开始统一布局#
更彻底的方式,是在分配阶段就选择 Attention、KV Connector、offload 和 prefix cache 都能接受的 canonical layout。这样优化的不只是一次 copy,而是模块之间的整个数据平面。
MoE 也有同样问题。All-to-All 希望每个目标 rank 对应一段连续 buffer,grouped GEMM 希望同一 Expert 的 token 连续。于是 token permutation、send buffer 布局和 Expert 输入 stride,本质上是同一个端到端布局问题。
8. Stride Bug 为什么经常不崩溃,只输出乱码#
越界访问容易被 sanitizer 或运行时捕获;stride 错误更危险,因为错误地址可能仍然落在已分配的合法 storage 内。
典型场景是:
生产者:按真实 stride 写入
消费者:按 canonical stride 读取block 0 的 base 恰好相同,所以测试正常;从 block 1 开始,消费者按更短的逻辑 block size 递增,而生产者按带 padding 或跨组件的真实 stride(0) 递增。程序不越界、不报错,Attention 却读到了另一个合法区域。
vLLM issue #47783 记录了一个非常典型的案例:cache-store 遵循 packed KV view 的 stride(0),某条 FlashInfer sparse decode 路径却没有消费这个 block stride。写入和读取只在 block 0 内一致,后续 block 读错位置,最终表现为退化文本与评测精度接近归零。
这类 Bug 常在以下变化后出现:
- 开启 KV offload 或 prefix cache;
- 从普通 prefill 切到 chunked prefill;
- 更换 Attention backend;
- 合并多层或多组件 KV allocation;
- 从 NHD 切换到 HND;
- 加入 padding、对齐或量化 scale;
- 只恢复了 shape,没有恢复 offset 与 stride。
8.1 .contiguous() 修好结果,不等于修好系统#
如果加上一句:
x = x.contiguous()输出恢复正常,只能证明下游不支持当前布局。接下来仍需回答:
- 这个 copy 是初始化一次,还是每层、每请求、每个 decode token 一次?
- Tensor 多大,产生多少额外 HBM 流量?
- 是否增加峰值显存和 allocator 压力?
- 是否引入新的 stream 同步?
- 是否破坏 CUDA Graph 的地址稳定性?
- 能否由 Kernel 支持常见 outer stride,或在更早阶段一次性转换?
.contiguous() 是合理的边界适配工具,但不应该成为热路径中的默认“消毒剂”。
9. Torch.compile 与 Graph 也会观察布局#
同一个逻辑 shape 可能出现不同 stride:
shape = (32, 128, 4096)
stride = (524288, 4096, 1)
shape = (32, 128, 4096)
stride = (524288, 1, 128)对 eager Python 代码而言,它们可能都能正确运行;对编译系统而言,它们可能需要不同的地址生成和 Kernel。torch.compile 的 Tensor guard 可以包含 stride,布局变化可能触发 guard failure 和 recompile。
所以当服务出现:
recompile
graph break
cache miss
latency 抖动除了检查 shape、dtype 与动态 batch,还应该检查 stride 是否在不同请求路径中漂移。
CUDA Graph 或其他设备 Graph 还需要关注地址稳定性。一个隐式 reshape copy 或 contiguous fallback 可能产生新 allocation,让原本可 capture 的稳定地址路径失效。
10. 一套可复用的 stride 调试方法#
排查前先把“Tensor 看起来是什么”升级成“Tensor 实际如何寻址”。可以准备统一的描述函数:
from __future__ import annotations
import torch
def describe_tensor(name: str, x: torch.Tensor) -> None:
element_size = x.element_size()
storage_ptr = x.untyped_storage().data_ptr()
print(f"{name}:")
print(f" shape = {tuple(x.shape)}")
print(f" stride(elements) = {tuple(x.stride())}")
print(
" stride(bytes) = "
f"{tuple(s * element_size for s in x.stride())}"
)
print(f" storage_offset = {x.storage_offset()}")
print(f" dtype = {x.dtype}")
print(f" device = {x.device}")
print(f" element_size = {element_size} bytes")
print(f" contiguous = {x.is_contiguous()}")
print(f" storage_ptr = {hex(storage_ptr)}")
print(f" first_element_ptr= {hex(x.data_ptr())}")然后按生产者—消费者链路记录:
谁分配?
谁创建 view?
谁写入?
谁读取?
谁传输或 offload?
谁要求 canonical layout?真正要验证的是:
而不是两端恰好拥有相同 shape。
10.1 不要只测 torch.randn(shape)#
直接创建的随机 Tensor 几乎总是标准连续布局。自定义算子至少应覆盖:
| 布局 | 构造方式 | 主要覆盖点 |
|---|---|---|
| Contiguous | torch.randn(...) | canonical fast path |
| Transpose | x.transpose(...) | 维度互换 |
| Permute | x.permute(...) | 一般非连续布局 |
| Narrow | x[:, 1:-1] | 非零 offset |
| Padded slice | 从更大 Tensor 切片 | outer stride 大于逻辑行宽 |
| Layer view | cache[:, layer] | 跨层 KV stride |
| Broadcast | x.expand(...) | stride 0 与重叠输入 |
| Canonical copy | x.contiguous() | 对照基线 |
Paged KV Cache 还必须覆盖 block_id > 0。只测 block 0,会漏掉最典型的 block-stride 错误。
10.2 输出错误时,找第一个发生差异的位置#
不要只看最终生成文本。按数据流逐层放置数值探针:
Q / K / V
-> KV write
-> 按 page table read back
-> attention output
-> norm output
-> logits对 KV Cache,最有效的测试之一是写入一个可识别模式,再用消费者的真实寻址路径读回:
expected = k[token_idx, head_idx, :].clone()
write_to_paged_cache(...)
actual = read_back_from_cache(...)
torch.testing.assert_close(actual, expected)同时扫描 page、token、head、dim、offset 与不同 layout,不要只做完整模型的端到端回归。
10.3 正确之后,再查隐藏 copy#
一次 100 微秒 copy:
模型加载时一次:通常可以摊销
每个请求一次:需要评估
每层一次:可能明显
每个 decode token、每层一次:通常不可接受Profiler 中重点搜索:
contiguous
clone
copy_
layout conversion
pack / unpack
stream synchronization正确性基线与性能基线必须同时建立,否则很容易用一份隐藏的全量 copy 换来“看起来已经修好”。
11. 设计新算子时,怎样写 Layout Contract#
每个自定义算子都应该明确四件事:
1. 必须满足什么布局?
2. 哪些 outer stride 可以变化?
3. 不满足时是报错、dispatch,还是 materialize?
4. fast path 与 fallback 分别由哪些测试覆盖?一个简单的 Python 边界可以是:
def my_rms_norm(x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
if x.ndim != 3:
raise ValueError(f"expected 3D input, got shape={tuple(x.shape)}")
if x.stride(-1) != 1:
raise ValueError(
"hidden dimension must be contiguous: "
f"shape={tuple(x.shape)}, stride={tuple(x.stride())}"
)
if not weight.is_contiguous():
weight = weight.contiguous()
return torch.ops.my_backend.rms_norm(x, weight)这段代码不一定是最终最快实现,但它把约束从隐含假设变成了可观测合同。随后可以为常见的 outer-stride layout 增加专用路径,而不是让错误输入进入 Kernel 后静默产生乱码。
12. 怎样真正“利用”stride#
理解 stride 的目标不是把所有 Tensor 强制变成 contiguous,而是让布局变化发生在收益最高、代价最低的位置。
12.1 让热访问维度对应 stride 1#
先从 Kernel 的线程映射出发:
RMSNorm / Softmax: lane 沿 hidden dimension
Attention: lane 沿 head_dim
Token scan: lane 沿 token dimension
Grouped GEMM: 同一 Expert 的 token 与 hidden tile再确定 canonical layout,而不是先选一个“看起来顺眼”的 shape 顺序。
12.2 用 view 消除只改解释的 copy#
当多个连续维满足兼容关系时,合并或拆分维度可以只改 metadata。若一次 permute 破坏了后续 view 条件,可以尝试调整算子与 reshape 的顺序,而不是默认走:
permute -> contiguous -> view很多算子融合的收益,本质就是让中间布局不再落盘。
12.3 把一次性转换移出热路径#
权重长期只读、重复复用,适合:
checkpoint layout
-> load-time pack / transpose / shard reorder
-> backend canonical layout
-> repeated inferenceKV Cache 动态读写,无法用同样方式完全摊销,所以更需要在 allocator、Attention 与 connector 之间共同设计布局。
12.4 为常见布局专门化,而不是追求无限通用#
完全 arbitrary-stride 的单一 Kernel 往往不是最佳工程答案。更实用的是覆盖少数高频布局:
if canonical_nhd:
return nhd_fast_kernel(...)
if canonical_hnd:
return hnd_fast_kernel(...)
if last_dim_contiguous:
return general_outer_stride_kernel(...)
return explicit_fallback(...)这样既保留常见 view 的零拷贝能力,也能让关键参数在编译期 specialization。
13. 一份用于代码审查的 stride 清单#
最后把最常用的问题压缩成一份 checklist。
正确性#
Tensor 的 shape、stride、storage_offset 分别是什么?
stride 的单位是元素还是字节?
生产者与消费者的完整地址公式是否一致?
是否存在 padding、切片、跨层 view 或 stride 0?
输出是否可能写入 overlapping storage?
Paged Cache 是否测试了 block 1 及之后的位置?兼容性#
Kernel 支持哪些 canonical layout?
哪些维度必须 stride 1?
是否支持 arbitrary outer stride?
不支持时会明确报错,还是静默 fallback?
backend 切换时是否重新验证 layout contract?性能#
相邻线程对应的逻辑维度是什么?
访问是否合并,是否满足向量化与对齐?
是否新增 contiguous、pack 或 unpack?
转换是在初始化阶段,还是 decode 热路径?
同一 shape 的多种 stride 是否导致 recompile?
新的 allocation 是否影响 Graph capture?14. 总结#
Stride 不只是 transpose() 后打印出来的一组数字。它连接了 AI Infra 中几乎所有数据移动与地址生成问题:
逻辑索引
-> Tensor view
-> Kernel 地址计算
-> warp 内存访问
-> Attention / GEMM 布局
-> Paged KV Cache
-> pack / communication / offload
-> compile guard 与 Graph真正成熟的布局设计,不是让所有 Tensor 都 contiguous,而是做到:
- 热路径使用与线程映射匹配的 canonical layout;
- 只改变逻辑解释时,尽量使用零拷贝 view;
- Kernel 明确声明并验证 stride、offset、对齐与 overlap 合同;
- 跨模块传递完整布局信息,而不只传 shape;
- 只在必要边界做可解释、可测量的布局转换;
- 为 NHD、HND、last-dim-contiguous 等常见布局提供专用路径;
- 用非连续、padded、非零 offset 与 block 1+ 的测试守住正确性。
可以用一句话记住本文:
Shape 定义 Tensor“看起来是什么”,stride 定义系统“应该去哪里读写它”。
延伸阅读#
- PyTorch Tensor Views
- PyTorch
torch.Tensor.view - PyTorch
torch.Tensor.expand - PyTorch
torch.compileTroubleshooting - Triton
make_block_ptr - NVIDIA CUDA Programming Guide: Coalesced Global Memory Access
- FlashInfer: Append Paged KV Cache
- FlashInfer: TRT-LLM Batch Decode with KV Cache
- vLLM #47783: Packed KV Layout Stride Mismatch
- vLLM #45997: Constant-stride Layer-major KV Cache Layout