More than code

More Than Code
The efficiency of your iteration of reading, practicing and thinking decides your understanding of the world.
  1. 首页
  2. 未分类
  3. 正文

Torch Allocator 2 profile

2026年5月28日 33点热度 0人点赞 0条评论
机制 数据类型 何时记录 何时启用 谁消费
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 指标
标签: 暂无
最后更新:2026年5月28日

sheep

think again

点赞
< 上一篇
下一篇 >

文章评论

取消回复

COPYRIGHT © 2021 heavensheep.xyz. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS