01 · 为什么 Tensor 变小,峰值却没降
推理和训练反复产生中间 Tensor、工作区与通信缓冲。若每次都新分配,既有分配成本,也可能让多个已不用的工作区继续预留。另一方面,转置、reshape 和内核布局转换可能产生额外副本。规划的目标是让真正必须同时存在的数据占空间,并减少不必要搬运。
但不能只按代码行判断“用完”。训练反向、异步复制、通信以及其他 view 可能仍在读取原存储;这些消费者结束前,原空间不能写入新内容。容量优化与执行依赖是同一个账本的两面。
依据:PyTorch 2.14 · CUDA缓存分配器与内存统计 ↗ · PyTorch 2.14 · CUDA stream、等待与内存生命周期 ↗
02 · 生命周期是最后一个读者结束,而非函数返回
A 产生 → 本地算子读 A → 通信或复制继续读 A
↓ 完成事件
所有读者结束 → A 的存储可复用为 C
# CPU提交读A的任务之后返回,不代表A可以被覆盖。为每个 storage 记录“从何时产生,到最后一次访问结束”。两个对象只有在生命周期不重叠、容量和对齐满足、且没有隐藏别名时,才可以共用一段空间。跨 stream 可通过事件建立依赖,但同步可能延长关键路径;应同时比较多缓冲和等待复用两种方案。
record_stream 等分配器接口帮助框架跟踪侧 stream 使用;它不替代数据依赖等待,更不允许用户就地覆盖仍被另一个任务读取的数据。
03 · A、B、C 三个对象的 14 MiB 与 10 MiB
自设离散时刻:A=4 MiB,生命周期[0,3);B=6 MiB,[1,4);C=4 MiB,[3,5)。左端包含、右端不包含,所以确认 A 在时刻3完成全部访问后,C可以复用A的4 MiB空间。
| 对象 | 生命周期 | 独立预留 | 安全复用方案 |
|---|---|---|---|
| A | [0,3) | 4 MiB | 槽0:4 MiB |
| B | [1,4) | 6 MiB | 槽1:6 MiB |
| C | [3,5) | 4 MiB | 时刻3后使用槽0 |
若把三个工作区分别保留供后续重用,合计预留14 MiB;合并A/C只需10 MiB。这不是说任何动态分配器都会达到14 MiB,也不是说实际整模型峰值只有10 MiB;此例排除了参数、对齐、碎片和其他对象。
C能否在时刻3覆盖A的槽?
固定槽教学例:A 4 MiB、B 6 MiB、C 4 MiB;时刻为任意教学单位,区间左闭右开,完成点已建立依赖。把 A 的复制访问延长到时刻5,A/B/C在[3,4)同时存活,立即复用就会覆盖尚未读取完的数据。可以增加第三个槽,或延迟C;前者占14 MiB,后者可能变慢。
04 · 从固定工作区到图内存规划
- 采集对象大小、对齐、别名关系和全部消费者的结束点。
- 在给定执行顺序下找不重叠对象,给它们分配同一可容纳的区域。
- 保留图重放需要的地址与长期输出;并发请求之间使用独立槽或严格依赖。
- 动态 shape 变化时重新验证容量,使用分档工作区或可控回退。
张量元素大小和真实分配大小不同,内核还可能需要 scratch workspace。静态图更容易知道生命周期;动态分支、外部算子和跨图别名会使计划更保守。排程变化后必须重新核对,不能沿用旧内存计划而忽略新增重叠。
can_reuse = (last_reader_end(A) <= first_write(C)
and slot_capacity >= bytes(C)
and alignment_ok(C)
and no_live_alias(A))
# 在已确定的执行依赖下判断;真实系统通常用事件/图依赖。05 · 转置可能只改索引,也可能复制内容
用连续存储[0,1,2,3,4,5]表示一个2×3矩阵:两行是[0,1,2]和[3,4,5]。行/列步距 stride=(3,1),坐标(i,j)读 storage[3i+j]。转置后 shape=(3,2)、stride=(1,3),坐标(i,j)读 storage[i+3j],无需搬动六个元素。
| 表示 | shape / stride | 逐行读取结果 |
|---|---|---|
| 原矩阵 | (2,3) / (3,1) | [0,1,2];[3,4,5] |
| 转置 view | (3,2) / (1,3) | [0,3];[1,4];[2,5] |
| 转置后的连续副本 | (3,2) / (2,1) | 内容相同,但新 storage=[0,3,1,4,2,5] |
view 共享存储,修改转置 view 的(1,0)会改变原矩阵(0,1)。如果下一个算子要求连续布局,就可能 materialize 一份副本;更快内核的收益要抵扣转换成本。reshape 也不保证总是零复制,取决于原布局。
06 · 少复制不等于永远用 view
若连续副本生成一次后被许多算子复用,转换可能值得;如果每层来回转换,读写与临时峰值可能很大。应优先让生产者直接生成消费者可用布局,或选择兼容 stride 的内核,但不假定任意 stride 都能高效执行。
一个小切片 view 还可能让很大的底层 storage 一直存活。例如只保留大激活的一小段,元素计数很少,原分配仍有引用;拷贝这小段虽增加一次搬运,却可能释放原存储。判断依据是最后引用和总成本,不是“view一定省内存”。
推理的 KV 分页与缓存格式也约束 Attention 布局;训练的保存激活、原地操作和版本计数限制复用。不要为省副本覆盖反向需要的数值。
依据:PyTorch 2.14 · Tensor view、stride与连续布局 ↗ · PyTorch 2.14 · save_on_cpu与saved_tensors_hooks ↗ · vLLM · Paged Attention 内核设计 ↗
07 · allocated、reserved 与真正能复用的空间
PyTorch CUDA 缓存分配器会保留未用块,方便未来快速分配。memory_allocated 观察 Tensor 占用,memory_reserved 观察分配器管理的总量;后者包括尚未交还驱动的空闲块。因此 nvidia-smi 占用较大不一定表示所有空间里都有活跃 Tensor。
empty_cache 释放未用的缓存块,不会释放仍被 Tensor 引用的内容,也不是每步优化按钮。盲目每步调用可能破坏后续复用。查看内存快照和分配事件,才能区分长期活跃数据、池预留、碎片及工作区。
把规划方案的“10 MiB工作区”与整设备的reserved直接比,会混入其他分配;应该在相同shape、并发和热身状态下比较分配次数、峰值allocated/reserved以及关键路径。
08 · 用 CPU 先验证别名和复用安全
下方脚本给A/B/C做一个固定槽规划,检查所有同时存活对象都没有地址区间重叠。再让A的访问延长,确认旧计划被拒绝。用一维列表实现矩阵与转置view,验证共享写入和独立副本的差别;不依赖PyTorch,也不模拟设备分配性能。
import torch
x = torch.arange(6).reshape(2, 3)
t = x.transpose(0, 1)
assert t.stride() == (1, 3)
c = t.contiguous()
assert c.stride() == (2, 1)
t[1, 0] = 99
assert x[0, 1].item() == 99
assert c[1, 0].item() == 1实际框架优先使用其已有分配器与编译内存规划,不要在Python层随意改底层storage或写危险as_strided。加入手工工作区时,要把传输、通信和图重放的所有访问纳入所有权管理。
依据:PyTorch 2.14 · Tensor view、stride与连续布局 ↗ · PyTorch 2.14 · torch.compile参数、模式与后端 ↗ · PyTorch 2.14 · CUDA Graphs与使用约束 ↗
09 · 验证容量、时间与并发正确性
| 目标 | 检查方式 |
|---|---|
| 减少分配 | 热身后记录分配次数与分配相关等待,比较相同shape和并发。 |
| 减少搬运 | 查 transpose/contiguous/copy 的实际内核及总字节。 |
| 降低峰值 | 记录临时工作区、KV/激活、allocated/reserved与设备总占用。 |
| 正确性 | 对照输出/梯度;覆盖动态shape、并发请求、侧stream、通信和图重放。 |
缓冲复用可能只减少峰值,不明显加速;布局改变可能加快核心kernel,却增加转换时间。最终应看端到端吞吐、TTFT/TPOT或训练step,而不只看分配器日志。
10 · 平台对照与练习
GPU与NPU都需要正确的生命周期、对齐和消费者布局;具体分配池、stream/队列、图编译和内核工作区接口不同。NVIDIA阅读入口是PyTorch CUDA与图执行文档;昇腾可沿TorchAir编译及CANN异步访问约束核对,不把同名接口当成同样语义。
依据:Ascend · TorchAir官方仓库与NPU后端入口 ↗ · CANN Community 9.1.0 · aclrtMemcpyAsync与Host内存约束 ↗
- A最后一次设备算子读在3结束,通信读在5结束,哪个时刻才是安全复用点?
- 为什么转置view没有复制,下一步MatMul仍可能变慢?
- 若只能保留10 MiB工作区,A延长到5时,你会延迟C还是改算法?怎样评估等待代价?
流程位置与关联方法
这个方法可能出现在多个位置;计算线包含模型计算与算子实现,执行线包含运行时、内存和通信。
资料与核查范围
原理依据以原始论文和官方文档为主;算例与时间线是教学推导。文档核查日期:2026-10-10。引用 main / latest 文档时,实际接口以安装版本为准。
- PyTorch 2.14 · CUDA缓存分配器与内存统计 ↗
- PyTorch 2.14 · Tensor view、stride与连续布局 ↗
- PyTorch 2.14 · CUDA stream、等待与内存生命周期 ↗
- PyTorch 2.14 · torch.compile参数、模式与后端 ↗
- PyTorch 2.14 · save_on_cpu与saved_tensors_hooks ↗
- vLLM · Paged Attention 内核设计 ↗
- PyTorch 2.14 · CUDA Graphs与使用约束 ↗
- Ascend · TorchAir官方仓库与NPU后端入口 ↗
- CANN Community 9.1.0 · aclrtMemcpyAsync与Host内存约束 ↗
代码为接入片段或教学伪代码;本页没有声称完成 NVIDIA / 昇腾硬件实测。