连续指多维张量的逻辑元素在底层一维 Storage 里紧密、按序排列。很多底层算子与内存操作只认连续张量;非连续张量靠跨步寻址仍能读写,但访存低效,部分 API 直接报错。

底层算子性能

这是连续性最核心的价值。PyTorch 的 CPU / CUDA 内核大量针对连续内存做向量化、SIMD 与 GPU 合并访存优化:连续张量的元素在内存中顺序排列,CPU 可以预取,GPU 能 coalesced 合并访存,带宽吃满;非连续张量(如转置得到的 b)靠 跨步寻址访问,地址跳跃,无法合并访存,缓存命中率骤降,速度明显变慢。

非连续不是不能读——跨步寻址可以正常读写——只是内存访问低效。

强制要求连续的 API

.view() 只能作用在连续张量上,非连续时直接抛错;reshape 会自动判断,不连续时先做 contiguous 拷贝:

a = torch.arange(6).reshape(2,3)
b = a.t()
b.view(6)  # RuntimeError: view size is not compatible with input tensor's size and stride

.view() 不拷贝数据,只修改 shape / stride,依赖连续内存;非连续的布局无法简单重解释 shape。此外 torch.frombuffer()、部分原生 C++ 扩展与自定义 CUDA kernel 同样要求输入连续。

零拷贝与 NumPy 互操作

tensor.numpy() 与张量零拷贝共享内存的前提之一是连续;非连续张量转 numpy 时内部会自动拷贝,不再共享 Storage:

a = torch.arange(6).reshape(2,3)
b = a.t()
arr = b.numpy() # 这里会拷贝,不再和a共享Storage!

切片同理:连续张量的切片(a[:, :2])多能维持视图零拷贝,非连续张量的切片更容易触发隐式拷贝。

隐式拷贝的内存开销

很多函数遇到非连续张量时,会在内部静默调用 .contiguous() 拷贝一份新内存:代码里没写 .contiguous(),运行时却自动分配新 Storage、复制数据,凭空多出内存与时间开销,是最容易踩的性能坑之一。

C 连续与 Fortran 连续

PyTorch 默认 C 连续(行优先):最后一维的相邻元素在内存中相邻。.t() 得到的转置张量是 Fortran 连续(列优先),而 is_contiguous() 只判断 C 连续,所以返回 False:

b.is_contiguous()         # False(C连续?否)
b.is_f_contiguous()       # True(Fortran连续?是)

F 连续同样是内存紧密排布,只是顺序为列优先;部分算子也能利用 F 连续做优化。

何时手动调用 .contiguous()

  • 调用 .view() 之前
  • 传给自定义 C++ / CUDA 算子
  • 转 numpy 且想保持零拷贝
  • 张量运算异常慢时,排查是否非连续

连续与非连续对照

特性连续张量非连续张量(转置、切片)
数据存储逻辑元素在底层一维内存紧密排列内存跳跃访问,stride 不规则
.view()可用报错
访存速度快,SIMD / GPU 合并访存慢,跳跃访问
视图零拷贝可用可用(但很多操作会触发隐式拷贝)
.numpy()零拷贝共享自动拷贝

误区澄清

「非连续张量不能读写」是误解:跨步寻址可以正常读写(对转置后的 b 直接索引赋值即可验证),代价只是访存变慢、部分 API 不支持。