stride、contiguous 是什么?view 和 reshape 有什么区别?
compile time 怎么按 shape 合并维度?

英文问法What are strides, and what does contiguous mean? · What's the difference between view and reshape? · How would you optimize a kernel at compile time given tensor shapes?

考过NVIDIA 框架岗 2023 onsite 第 4 轮(compiler 方向:先问 torch 的 stride 和 contiguous tensor,再问如何在 compile time 根据 tensor shape 做优化,答案是合并 contiguous 的维度,知乎)

先弄清楚:每个概念是什么

Tensor = storage + metadata
数据只是一条一维的 storage,shape、stride、offset 是怎么读它的 metadata。
Stride
第 \(i\) 维 index 加 1,在 storage 里跳过几个元素。例:shape [2, 3, 4] 的 stride 是 (12, 4, 1),x[1, 2, 3] 在第 \(12 + 8 + 3 = 23\) 个。
Contiguous
按逻辑顺序(最后一维变得最快)在内存里一个挨一个,即 stride 正好是后面各维 size 的乘积。
View op
只改 metadata、不搬数据。transpose / permute 交换 stride,slicing 改 offset 和 stride,expand 把 stride 设成 0,结果通常不再 contiguous。
view vs reshape vs contiguous()
view 从不拷贝,新 shape 和原 stride 不兼容就报错。reshape 能 view 就 view,不能就拷贝。contiguous() 已经连续就返回自己,否则按逻辑顺序拷一份。

看图:storage 和 stride,合并维度

a:shape [2, 3],stride (3, 1)012345行 0行 1storage012345行 +1:跳 3列 +1:跳 1a.T:shape [3, 2],stride (1, 3),没有拷贝031425storage012345列 +1:跳 3行 +1:跳 1按 a.T 一行一行读:0 → 3 → 1 → 4 → 2 → 5在 storage 里来回跳 → non-contiguousa.T.contiguous() 或 a.T.reshape(6):按逻辑顺序拷一份storage031425新的 storage,和 a 不再共享内存a.T.view(6) 不拷贝:要 1 = 3 × 2,不成立 → 报错
图 1:a = arange(6).reshape(2, 3)。左边是逻辑形状,右边是一维 storage。a 和 a.T 共用一条 storage(橙色),弧线是某一维 index 加 1 时跳的格数。按 a.T 的行读要来回跳,所以不是 contiguous,contiguous() 或 reshape 会拷出新的 storage(蓝色)。
x = arange(24).reshape(2, 3, 4):shape [2, 3, 4],stride (12, 4, 1)01234567891011121314151617181920212223x[0]x[1]12 = 4 × 3,4 = 1 × 4,三维都能合并 → [24],stride 1,一层循环走完x[:, :, :2]:shape [2, 3, 2],stride (12, 4, 1)+4+4+4+4+401234567891011121314151617181920212223x[0]x[1]维 0、1:12 = 4 × 3 → 合并,跨过 x[0]、x[1] 的边界也是 +4维 1、2:4 ≠ 1 × 2 → 不能合并(每 2 个之后空 2 格)结果 [6, 2],stride (4, 1):外层 6 次、每次跳 4,内层连续 2 个
图 2:24 个元素的 storage,蓝色格是用到的元素。x 用到全部 24 个,三维合并成一层循环。x[:, :, :2] 每 4 个只用前 2 个,每一对的开头都相隔 +4,所以维 0、1 能合并。每一对之后空 2 格,维 2 合不进去,结果是 [6, 2]、stride (4, 1)。

30 秒回答

A tensor is a flat storage plus metadata: offset, shape and stride, where stride[i] is how many elements you skip when index i grows by one. It's contiguous when each stride equals the product of the later sizes, i.e. row-major. Transpose, permute, slicing and expand only rewrite metadata, so they're free but usually make the tensor non-contiguous. view never copies, so it fails when the new shape isn't compatible with the strides; reshape returns a view when it can and copies otherwise. At compile time, adjacent dims i and i+1 can be merged when stride[i] equals stride[i+1] times size[i+1]; a contiguous [2, 3, 4] elementwise kernel becomes one loop over 24 elements.

推导 / 关键公式

\[ \text{addr} = \text{offset} + \textstyle\sum_k i_k\,\text{stride}_k \] \[ \text{contiguous:}\quad \text{stride}_i = \textstyle\prod_{k>i} \text{size}_k \]

第 \(i\)、\(i+1\) 维能当成一维走(view 能跨这两维、compile time 能合并)的条件:

\[ \text{stride}_i = \text{stride}_{i+1}\times \text{size}_{i+1} \]

合并后 size \(= \text{size}_i\,\text{size}_{i+1}\),stride \(= \text{stride}_{i+1}\)。

例子

x = arange(24).reshape(2, 3, 4):

tensorshapestridecontiguous合并后
x[2, 3, 4](12, 4, 1)是[24]
x.transpose(1, 2)[2, 4, 3](12, 1, 4)否不能合并
x[:, :, :2][2, 3, 2](12, 4, 1)否[6, 2],stride (4, 1)
[3, 1] expand 成 [3, 4][3, 4](1, 0)否–

view 为什么报错:图 1 的 a.T 是 [3, 2]、stride (1, 3),a.T.view(6) 要 \(1 = 3\times 2\),不成立。

手撕代码:按 stride 合并维度

def merge_dims(shape, stride):
    shp, std = [], []
    for n, s in zip(shape, stride):
        if n == 1:
            continue                    # size 1 不影响地址
        if shp and std[-1] == s * n:    # 外层走 1 步 = 内层走完 1 圈
            shp[-1] *= n
            std[-1] = s
        else:
            shp.append(n)
            std.append(s)
    return shp, std

merge_dims([2, 3, 4], [12, 4, 1])       # ([24], [1])
merge_dims([2, 3, 2], [12, 4, 1])       # ([6, 2], [4, 1])

合并后 index 计算变简单、循环层数变少,更容易 vectorize。

必答点

追问

kernel 为什么关心 contiguous?

连续时相邻 thread 读相邻地址(coalescing),也能用 float4 这类 vectorized load。不连续就要逐维算 index,或者先拷一份 contiguous。

多个 operand 的 elementwise op 怎么合并维度?

每个 operand 都满足条件才合并,size 1 的维度总能合并。PyTorch 的 TensorIterator coalesce_dimensions() 就是这条规则。

expand 和 repeat 有什么区别?

expand 把 stride 设为 0,不分配内存,多个元素指向同一地址,要 in-place 写得先 clone。repeat 真的拷贝数据。

能依赖 reshape 返回的是 view 还是 copy 吗?

不能,文档明确说不要依赖。要独立副本就 clone(),要保证共享内存就用 view。

来源PyTorch 2.14 docs:Tensor.view、torch.reshape、Tensor.stride、Tensor.contiguous、Tensor Views、Tensor.expand、Tensor.repeat · NumPy 2.5 docs:ndarray.strides、numpy.reshape · PyTorch TensorIterator.cpp coalesce_dimensions()