| 机制 | 数据类型 | 何时记录 | 何时启用 | 谁消费 | |
|---|---|---|---|---|---|
| Stat 统计 | DeviceStats | 每次 alloc/free/split/merge | 永远开(O(1) 计数) | memory_stats() / memory_allocated() 等 Python API |
\ |
| Trace 历史 | TraceEntry | 同上 + 段级事件 | 默认关,需要 _record_memory_history() 打开 |
memory_snapshot() / _dump_snapshot() |
|
| Profiler 上报 | reportMemoryUsageToProfiler | 同上 | 默认关,只有 profiler profile_memory=True 时才有效果 |
torch.profiler / Kineto |
DeviceStats
torch.cuda.memory_stats()返回的
对应到torch c这一层是_cuda_memoryStats,THCPModule_memoryStats,getDeviceStats
统计信息包含下面的列表:
```C++
struct DeviceStats {
StatArray allocation; // 用户层 malloc 次数
StatArray segment; // 物理段(cudaMalloc/cuMemCreate)次数
StatArray active; // active 块的数量(含 split 出来的)
StatArray inactive_split; // split 出来但未使用的块数
StatArray allocated_bytes; // 用户视角的字节数(= block->size 之和)
StatArray reserved_bytes; // 从设备/驱动 reserve 的字节(pool 总和)
StatArray active_bytes; // 活跃块的字节
StatArray inactive_split_bytes;
StatArray requested_bytes; // 用户请求的原始字节(未对齐前)
int64_t num_alloc_retries = 0; // cudaMalloc 失败触发 cache flush 的次数
int64_t num_ooms = 0;
Stat oversize_allocations;
Stat oversize_segments;
int64_t num_sync_all_streams = 0;
int64_t num_device_alloc = 0;
int64_t num_device_free = 0;
...
};
<pre><code class="line-numbers">其中StatArray是区分了small pool, large pool的数组,每个数组的元素包含current/peak/allocated/freed。统计峰值,以及统计累计值
比较关键的点就是这个num\_alloc\_retries,有的话就说明有性能退化了。
<br>
代码中的for\_each\_selected\_stat\_type就是维护这个统计值的
# Trace
精细化的trace,记录每一次内存相关的操作
```C++
struct TraceEntry {
enum Action {
ALLOC, // 用户 malloc
FREE_REQUESTED, // 用户 free 进来
FREE_COMPLETED, // 真正 coalesce 回池(可能比 FREE_REQUESTED 晚,因 record_stream 等 event)
SEGMENT_ALLOC, // cudaMalloc/cuMemCreate
SEGMENT_FREE, // cudaFree
SEGMENT_MAP, // cuMemMap(expandable)
SEGMENT_UNMAP, // cuMemUnmap(expandable)
SNAPSHOT, // 快照标记(用于和 snapshot 对齐)
OOM // addr_ 字段在这里被借用,存的是 cudaMemGetInfo 报的 free bytes
};
...
Action action_;
c10::DeviceIndex device_;
size_t addr_;
std::shared_ptr<GatheredContext> context_; // 调用栈(Python 栈,可选)
void* stream_{};
size_t size_;
MempoolId_t mempool_;
trace_time_ time_{};
std::string compile_context_; // 当前 torch.compile 阶段名("FX trace", "Inductor lowering"...)
std::string user_metadata_; // 用户通过 _record_user_metadata 注的字符串
};
- 包括具体的操作,device,stream,分配的地址,大小等信息
-
区分了FREE_REQUESTED/FREE_COMPLETED,用来观察record stream导致的延迟回收
-
context可以认为是python的traceback,用来记录分配的栈信息
-
time,分配的时间
在DeviceCachingAllocator中维护了一个Ringbuffer。ring buffer的大小通过_record_memory_history(max_entries=...)来决定。
- Ring buffer会维护起点和终点,所以输出的时候trace是按照时间顺序输出的
代码中的record_trace就是生成trace entry,并记录到ring buffer中。同时也会调用若干个callback,对应出现内存操作时候的动作(比如做一些记录)
- AI说这里的CUPTI的内存事件就是从这里抓取的,这块我没有细追
Python层
torch.cuda._record_memory_history,调用到_cuda_record_memory_history
torch.cuda._snapshot/_dump_snapshot,调用到_cuda_memorySnapshot
_cuda_record_memory_history对应到Allocator的recordHistory,这里会把record_history这个开关打开,后续的内存操作就会被记录到上面的ring buffer中。
_cuda_memorySnapshot对应到Allocator的snapshot
- 这里会把ring buffer的trace返回出去
-
以及所有分配的segement的元信息,比如size,block的占用情况等
snapshot本身也会记录一个snapshot的事件,这样方便多次snapshot做diff,做对齐
Profiler
- 核心接口就是reportMemoryUsageToProfiler
- 会拿一个thread local的debug info,里面会有一个MemoryReportingInfoBase的指针,用这个来做report
-
记录ptr, alloc_size, device。如果是分配alloc_size就是正数,free就是负数
-
用户层的语义,不涉及到caching allocator内部的行为(不感知segment之类的信息)
- torch.profiler.profile(profile_memory=True)的时候,这个thread local的debug info就会被塞一个report进去。
- 没开profiler的时候,指针为空,直接返回
MemoryViz
Memory viz的脚本在torch/cuda/_memory_viz.py中
要求输入的schema:
{
"segments": [...], # 物理段列表,每段含 blocks
"device_traces": [[...]], # 每个 device 一条 trace 事件流(alloc/free_*/segment_*/...)
"external_annotations": [...],
}
Record memory history + dump snapshot出来的东西就是这个格式,所以可以直接用。
但是平常使用的torch profiler也可以用memory snapshot,是走的另一条路。
在_profile_to_snapshot会把 profiler 的 tensor 生命周期事件,伪装成 caching allocator 的 trace 格式。profile记录的是每一个tensor的分配情况。相对来说粗粒度一些。
走trace的方法更细粒度一些,可以看到详细的事件,用来debug内存碎片。
运维观测
注意点:
- allocation/active的区别
- 统计口径都是block,(对齐后的),用户申请的是requested_bytes
-
调用free时,allocation减。真正变成free时,active减。所以主要是record stream影响这里的行为
count类
| 字段 | 含义 | 何时关注 | 异常信号 |
|---|---|---|---|
allocation |
客户端调用 allocator 的"次数",即上层 tensor 申请了多少次 | 想知道分配频度、是不是有特别频繁的小申请 | allocated - freed 持续上涨:上层有持续未释放的 tensor,疑似泄漏 |
segment |
调用底层(如 cudaMalloc/cuMemMap)拿到的段数量 |
看 allocator 向 OS/驱动要了多少块裸内存 | current 越大说明从驱动占走的物理段越多;peak 高但当前低,说明用过一阵又还了 |
active |
处于"活跃"状态的块(已分配,或者被 recordStream 标记为还在被某个 stream 使用) |
看真正"还没死透"的块。和 allocation 的差体现 record_stream 推迟释放的影响 |
active.current 远大于 allocation.current:说明大量块被 stream 持有等待事件,可能是 recordStream 用法不当或事件没回收 |
inactive_split |
不活跃、但因为之前被切过、无法整体归还给驱动的小碎块 | 怀疑碎片化时第一眼看这个 | inactive_split.current 很大、inactive_split_bytes 也很大、但 active_bytes 不大 → 严重碎片化,常伴随 OOM |
SUM 类指标(字节数)
| 字段 | 含义 | 何时关注 | 异常信号 |
|---|---|---|---|
allocated_bytes |
上层视角"分配出去"的字节数(活跃块的总字节,等同 torch.cuda.memory_allocated) |
看模型/训练实际占了多少显存 | peak 比预期高很多 → 有意外的大临时 tensor |
reserved_bytes |
allocator 向驱动保留的总字节(缓存中的 + 已分配的,等同 torch.cuda.memory_reserved) |
看 allocator 总共"占了" GPU 多少 | reserved 远大于 allocated:缓存非常多空闲块,可能是峰值过后释放但没归还;超过显存阈值可考虑 empty_cache 或调整 max_split_size |\ |
active_bytes |
active 块对应的字节数 |
配合 active 看是否被 stream 拖住释放 |
active_bytes - allocated_bytes 大:很多块上层已 free 但还在 stream 持有 |
inactive_split_bytes |
inactive_split 块对应的字节数;这部分就是"碎片" |
排查 OOM 时核心指标 | 占 reserved_bytes 比例很高(比如 >30%)→ 内存碎片严重,是潜在 OOM 触发点 |
requested_bytes |
上层"请求"的字节数(未做对齐/向上取整前的真实大小) | 估算对齐开销 | allocated_bytes - requested_bytes 占比大:很多 padding 浪费,工作负载尺寸分布不规则 |
reserved = active + inactive_split + 一些未切分空闲块 *allocated ≤ active ≤ reserved**,三者拉开越大说明缓存/碎片越多。*
全局counter,统计关键事件
| 字段 | 含义 | 异常信号 |
|---|---|---|
| num_alloc_retries | 调 cudaMalloc 失败、触发 flush 缓存后重试的次数 | 任何 >0 都值得警惕:说明已经在显存边界挣扎,缓存被反复清;增长快意味着抖动严重 |
| num_ooms | flush 后还失败、真正抛 OOM 的次数 | >0 即正式 OOM,配合 inactive_split_bytes / reserved_bytes 判断是"真的不够"还是"碎片化" |
| oversize_allocations | 大于 max_split_size、走"不可切分"路径的活跃分配数(Stat,有 current/peak/allocated/freed) | current 大说明工作负载里有很多巨大 tensor,且这些块释放后不会拆分被复用,回收效率差 |
| oversize_segments | 上述 oversize 块对应的底层 segment 数 | 多但很少复用 → 调大 max_split_size 或 expandable_segments 可能更优 |
| num_sync_all_streams | 调用 synchronize_and_free_events() 的总次数;这一步会强制等所有 stream 完成以回收被 recordStream 持有的块 | 频繁触发说明缓存压力大、频繁需要兜底回收,性能可能受拖累 |
| num_device_alloc / num_device_free | 直接向驱动 malloc/free 的次数(含 expandable segments 的 map/unmap) | 两者差值大:常驻显存;两者都大且持续上涨:缓存效率低,频繁向 OS 借/还 |
| num_oom_rejections | 被 OOM 抢占策略拒掉的分配次数(避免触发真 OOM 的预防性拒绝) | >0 说明触发了预防机制;如果用户层没看到对应失败,往往伴随 fallback 路径变慢 |
| max_split_size | 当前 allocator 配置上的"块最大可切分尺寸" | 不是动态量,只是配置回显;用来对照前面那些 oversize / inactive_split 指标 |
文章评论