3. Tensor与显存:存储格式、连续性与生命周期

聊到显存优化,Tensor 是绕不开的核心对象。说白了,你训练模型时,显存里装的就是 Tensor。我刚开始做深度学习时,总觉得 Tensor 就是个多维数组,后来踩了不少坑才明白——它的存储方式、内存布局、生命周期,每一个细节都直接影响显存占用。

3.1 Tensor 的存储格式

Tensor 在底层由两部分组成:元数据(形状、步长、数据类型等)和实际数据(存储在连续内存块中)。元数据很小,真正吃显存的是数据部分。

举个例子,一个形状为 (3, 224, 224) 的 float32 Tensor:

import torch
x = torch.randn(3, 224, 224)
print(x.element_size())  # 4 bytes (float32)
print(x.numel())         # 3*224*224 = 150528
print(x.storage().nbytes())  # 150528 * 4 = 602112 bytes

嗯,这里要注意:storage() 才是数据真正存放的地方。Tensor 本身只是一个“视图”,它通过 stride(步长)来索引 storage 中的数据。

核心概念:Tensor 的显存占用 = 元素个数 × 每个元素字节数。但实际中,由于对齐和碎片,可能会略高一些。

我在项目中遇到过一个问题:两个 Tensor 形状相同,但显存占用却不一样。后来发现,其中一个 Tensor 是另一个的切片,它们共享了同一个 storage。这引出了下一个话题——连续与非连续张量。

3.2 连续与非连续张量

连续张量,指的是数据在内存中按行优先顺序紧密排列。非连续张量则相反,数据在内存中可能“跳着”存放。

为什么会这样?你想想看,当你对 Tensor 做转置、切片、部分索引时,PyTorch 并不会真的复制数据,而是创建一个新的“视图”,修改 stride 和 offset。这样省了显存,但带来了非连续的问题。

import torch
x = torch.randn(3, 4)
y = x.T  # 转置,非连续
print(x.is_contiguous())  # True
print(y.is_contiguous())  # False

非连续张量有什么影响?

  • 运算效率低:很多底层操作(如矩阵乘法)要求输入是连续的,非连续时会触发隐式 contiguous() 调用,复制数据。
  • 显存可能翻倍:如果你不小心调用了 .contiguous(),PyTorch 会分配一块新内存,把数据拷贝过去。原本共享的 storage 就变成了两份。

避坑指南:我曾经在写一个数据预处理 pipeline 时,反复对 Tensor 做切片和转置,最后调用了 .contiguous(),结果显存直接翻倍。排查了半天才发现是这里的问题。建议:尽量在数据加载阶段就保证连续性,避免在训练循环中频繁触发隐式拷贝。

如何判断一个 Tensor 是否连续?用 is_contiguous()。如何强制连续?用 contiguous()。但记住,contiguous() 只在非连续时才复制,连续时返回自身。

3.3 张量生命周期与显存回收

Tensor 的生命周期,说白了就是它从创建到被销毁的过程。Python 的垃圾回收机制(引用计数 + 循环检测)负责回收不再使用的 Tensor,释放显存。

但这里有个坑:Python 的垃圾回收不是即时的。即使你删除了一个 Tensor 的引用,显存也不会立刻释放,而是等到下一次 GC 循环或显存压力触发时才会回收。

import torch
import gc

x = torch.randn(1000, 1000)  # 占用约 4MB
del x  # 删除引用
gc.collect()  # 手动触发垃圾回收

我个人习惯在关键节点手动调用 gc.collect() 和 torch.cuda.empty_cache()。但要注意,empty_cache() 只是清空缓存,不会释放正在使用的显存。

小技巧:在训练循环中,如果你创建了大量临时 Tensor(比如中间特征图),可以用 with torch.no_grad(): 包裹,避免构建计算图,减少显存占用。同时,尽量复用 Tensor 而不是反复创建新对象。

还有一个容易被忽略的点:计算图中的中间变量。当你执行 loss.backward() 时,PyTorch 会保留所有中间激活值用于反向传播。这些中间变量直到反向传播完成后才会被释放。所以,如果你的网络很深,显存占用会非常大。

我记得有一次训练一个 50 层的 ResNet,显存直接爆了。后来用 checkpoint 技术(梯度检查点),以时间换空间,才把模型塞进 8GB 显存里。

3.4 知识体系总览

下面这张图总结了 Tensor 与显存的核心逻辑:

Tensor 与显存 存储格式 • 元数据 (shape, stride, dtype) • 实际数据 (storage) • 显存占用 = numel × element_size 连续与非连续 • 连续:数据紧密排列 • 非连续:视图共享 storage • contiguous() 可能触发拷贝 生命周期与回收 • 引用计数 + GC 回收 • 手动 gc.collect() • 计算图中间变量 核心原则:理解存储结构 → 避免非连续拷贝 → 及时回收临时 Tensor 显存优化 = 减少冗余 + 提高复用 + 控制生命周期

这张图把三个核心知识点串在了一起。你从存储格式入手,理解 Tensor 到底怎么占显存;然后关注连续性,避免不必要的拷贝;最后管理好生命周期,及时释放不再需要的资源。这三步走下来,显存优化就有了基础。

一句话总结:Tensor 的显存管理,本质上就是搞清楚“数据存在哪、怎么存、什么时候该删”。

想要更多资料,联系可可