stride、contiguous 是什么?view 和 reshape 有什么区别?
compile time 怎么按 shape 合并维度?
先弄清楚:每个概念是什么
- 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,合并维度
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.
推导 / 关键公式
第 \(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):
| tensor | shape | stride | contiguous | 合并后 |
|---|---|---|---|---|
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。
必答点
- tensor = storage + (offset, shape, stride);contiguous ⇔ stride 是后面各维 size 之积
- transpose、permute、slicing、expand 只改 metadata,不拷贝,通常变 non-contiguous
- view 不拷贝、要求 stride 兼容;reshape 能 view 就 view,否则拷贝
- 合并条件 \(\text{stride}_i = \text{stride}_{i+1}\,\text{size}_{i+1}\);contiguous 的 [2, 3, 4] → [24]
- NumPy
.strides单位是 bytes(float32 的 [2, 3, 4] 是 (48, 16, 4)),PyTorch.stride()是元素个数
追问
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()