Skip to content

fix: break attention state ownership cycles - #1640

Merged
hiworldwzj merged 3 commits into
ModelTC:mainfrom
sufubao:fix-attention-state-lifetime
Oct 18, 2026
Merged

hiworldwzj merged 3 commits into
ModelTC:mainfrom
sufubao:fix-attention-state-lifetime

Conversation

@sufubao

@sufubao sufubao commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

InferStateInfo 持有 attention state,而 attention state 再强引用 InferStateInfo,会形成循环引用。一次 forward 结束后,临时状态及其 CUDA Tensor 需要等待 Python 循环 GC 才能释放;这不一定是永久泄漏,但会延迟显存复用、抬高峰值,并可能触发运行时 OOM。

本 PR 同时修正 FlashInfer decode wrapper 的执行模式判断:decode overlap 中,只要任一 microbatch 超出 graph 上限,整体就会回退 eager。原先逐个 microbatch 判断 graph 是否可运行,会使较短 batch 跳过 wrapper 创建,随后在 eager 执行时访问空 wrapper。

实现

  • Prefill/Decode state 共用 BaseAttState,各后端工厂显式设置 _infer_state_weak_ref=weakref.ref(infer_state)。通过只读 infer_state property 获取原对象;未绑定时返回 None,owner 已释放时抛出 ReferenceError。不再接受原来的 infer_state= 构造参数或直接属性赋值,仓库内调用与测试已适配。
  • InferStateInfo.init_att_state() 完成主、辅 attention state 初始化后统一调用 check_att_states(),拒绝 state 第一层实例属性直接保存任何 InferStateInfo。检查不递归遍历容器和闭包。
  • 将原 is_cuda_graph 改为 is_cuda_graph_capture,增加 is_cuda_graph_replay。单 batch 与 overlap 均在初始化 attention state 前,根据实际执行分支设置标志。FlashInfer FP、FP8、MLA 共用该判断:
Decode 执行方式 capture replay 创建 wrapper
eager,包括 graph 超限回退 False False 是
capture True False 是,随捕获状态缓存
replay 的临时输入状态 False True 否,使用图缓存中的 wrapper

两个标志同时为 True 时明确报错。现有 prefill/decode CUDA Graph 缓存继续强持有捕获用 InferStateInfo;decode 回放复制 Tensor metadata,并保留捕获 state 的弱引用绑定。

回归检查

  • 新增弱引用生命周期、主辅 state 强引用检查、graph metadata 复制/plan 刷新,以及 FlashInfer FP/FP8/MLA wrapper 初始化测试。
  • 补充单 batch 和 overlap 的 capture/replay/eager 分支断言,覆盖任一 microbatch 超出 graph 长度限制时的整体回退。
  • 适配现有 Neo、GDN、量化 attention、MTP/speculative 等测试的 state 构造和 owner 保活。
  • 当前提交的核心及 graph 调度、布局、autotune、prefill 状态、Neo CPU 回归合计 167 passed,3 项 Neo GPU 测试跳过。该轮禁用了 CUDA;Neo 的外部 attention 调用使用 mock,不代表 Neo 扩展内核精度验证。
  • 38 个变更 Python 文件的 Black、Flake8 及完整 PR 的 git diff --check 通过;GitHub Pre-commit 检查通过。

真实模型验证

环境:NVIDIA H200、BF16、TP1。

Graph 精度与执行路径

矩阵使用 page_size=1、FlashInfer FP;hybrid 线性层使用 Triton GDN。Qwen3-4B、Qwen3.5-0.8B 分别测试 decode graph 开/关 × prefill graph 开/关四种组合,共记录 392 次真实 forward 输出。与同模型全 eager 路径比较,所有 logits 满足 rtol=0.01, atol=0.05,greedy top1 全部一致;最大绝对差为 0.0625。

覆盖首次捕获、重复命中、batch 1→2 / 3→4 padding、batch 5 超限回退、KV 长度 160 超限回退、prefill token 数超限回退,以及分块前缀 KV 复用。测试配置为 decode 最大 batch 4、最大长度 128,prefill 最大处理 token 数 32。Hybrid 模型按生产流程,使用 HOLD request 预热并预捕获配置桶后验证真实请求。

显存问题复现

Qwen3.5-27B,4096-token prefill,开启 prompt logprobs,两个 graph 均关闭,默认 Python GC,不限制进程分配器预算。两组各启动新进程,并一致关闭新增的强引用检查以构造对照;强引用组仅额外恢复 attention → InferStateInfo 的强引用,数值计算路径相同。该检查在生产代码中保持开启。

所有权 完成轮数 CUDA OOM 活跃 CUDA Tensor 峰值
强引用对照 40 第 41 次尝试 134.99 GiB
当前弱引用 150 无 93.26 GiB

强引用组结束后手动 GC 释放约 45.49 GiB,并清除滞留 owner;弱引用组每轮结束后的滞留 owner 为 0,结束后 GC 未额外释放 GPU Tensor 显存。正常迭代中不手动 GC。具体 OOM 轮数受默认 GC 触发节奏影响,不是服务请求阈值。

真实 HTTP

四个全新 Qwen3.5-27B 服务:强/弱引用 × decode graph 开/关;HTTP 验证中 prefill graph 关闭,其兼容性由上面的模型矩阵验证。统一配置为 256 请求槽位、65536 KV tokens、动态缓存和 chunked prefill 开启。强引用由实验探针注入,只豁免该实验属性的防护检查,其他初始化检查仍执行。

每组包含 32 个 decode-heavy 请求(并发 8,32 输入 / 256 输出)和 64 个长输入请求(串行,4096 输入 / 1 输出,返回 prompt logprobs)。共 384/384 个正式请求成功,无 HTTP OOM。

Decode graph Decode 非缓存存活状态峰值,强 / 弱引用 长输入活跃 CUDA Tensor 峰值,强 / 弱引用 长输入 NVML 采样峰值,强 / 弱引用
关闭 13 / 0 110.09 / 96.34 GiB 119.64 / 102.57 GiB
开启 7 / 0 110.59 / 96.38 GiB 119.22 / 102.63 GiB

开启 graph 时,强/弱引用两组均保留正常的 8 个缓存状态,已从非缓存计数中排除。强弱引用对照及弱引用 graph 开关对照中,每对 96 个请求的生成文本均一致;decode 选中 token 的 logprob 最大差约 4.67e-5,长输入的生成及 prompt logprobs 对照差为 0。

活跃 Tensor 与 NVML 使用不同统计口径,NVML 包含 allocator 缓存和 CUDA context,差值不能直接视为泄漏量。探针不在 HTTP 请求路径手动 GC 或同步 CUDA,但 Python 记账可能影响 GC 时机;结果限定于上述配置。

验证边界

  • 未完成量化、MTP、TP 多卡、真实模型 overlap 或 Neo 扩展计算的完整端到端精度验证;对应单元测试不等同于模型验证。
  • 人为跳过生产预热、直接用真实 hybrid 请求冷捕获 decode graph 时,已有路径会重复推进共享 SSM 并产生精度偏差。强/弱引用冷捕获轨迹逐元素相同,正常生产预热路径通过;此问题不由本 PR 引入,未在此修复。
  • 独立 Hugging Face eager 对照的 10 次输出 top1 一致,但严格全词表 logits allclose 未通过,最大绝对差 0.4375;不将其计为精度通过。Qwen3-4B 的强/弱引用 eager 对照 49 次输出逐元素相同,最大差为 0。
  • 回归测试纳入本 PR;大模型实验脚本及原始测量数据未提交。

sufubao and others added 3 commits October 17, 2026 21:57
Use an explicit weak reference and read-only infer_state property shared by attention states. Check initialized attention metadata for direct InferStateInfo references to prevent ownership cycles.

Track decode graph capture and replay separately so eager overlap fallback always initializes FlashInfer wrappers. Add lifetime, graph-copy, wrapper initialization and scheduling regressions, and adapt existing tests to weak ownership.
@hiworldwzj
hiworldwzj merged commit 70f3927 into ModelTC:main Oct 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants