这是个非常好的观察,而且你没有看错 👍
vLLM-Ascend(NPU) 上,torch.npu.empty_cache() 确实可能导致 non-torch 显存下降,这和 CUDA 的行为本质不同

下面我分「结论 → 原因 → 机制细节 → vLLM 场景」把这件事讲清楚。


一句话结论(先给你)

在 Ascend / torch-npu 上:
torch.npu.empty_cache() 不仅仅是 PyTorch cache 操作
它会间接触发 Ascend Runtime / ACL 的内存回收路径
从而 释放部分 non-torch(runtime / kernel / workspace)显存

这在 CUDA 上几乎不会发生,但在 Ascend 上是“设计如此”


1️⃣ 为什么 CUDA 上不行,Ascend 上却行?

CUDA(torch.cuda.empty_cache)

  • 只操作 PyTorch CUDACachingAllocator

  • 不会触发:

    • CUDA Runtime 内部 pool
    • cuBLAS / cuDNN / FlashAttention workspace
  • non-torch 显存几乎完全不受影响


Ascend(torch.npu.empty_cache)

Ascend 的内存体系是:

1
2
3
4
5
PyTorch (torch-npu)

ACL / Ascend Runtime

Device Memory Manager (统一内存池)

关键点在这里👇

torch-npu 的 allocator 和 Ascend Runtime 的 allocator 是“半耦合”的


2️⃣ 真正发生了什么?(机制层)

当你调用:

1
torch.npu.empty_cache()

在 torch-npu 内部会发生几件事(不是官方文档写清楚的那种 😄):

① PyTorch NPU caching allocator 释放空闲 block

这一步和 CUDA 类似。


② 触发 ACL runtime 的「内存同步 / GC 信号」

这一步是关键差异点:

  • torch-npu 在 empty_cache 后:

    • 会调用 aclrtSynchronize
    • 并向 Ascend runtime 发送 内存释放 hint
  • Ascend runtime 会检查:

    • 当前 device memory pool 的使用水位

    • 是否存在:

      • 已完成 kernel 的 workspace
      • 可回收的临时 buffer
      • profiling / graph compile 残留

👉 这些全部是 non-torch 显存


③ Ascend runtime 是“激进回收型”

Ascend 的策略是:

只要你明确表示“我现在不缺性能,可以回收”
runtime 就会尝试 shrink 自己的内存池

torch.npu.empty_cache()
在 Ascend 上被当成了这个信号


3️⃣ 为什么 vLLM-Ascend 特别明显?

因为 vLLM-Ascend 正好踩中了 Ascend runtime 的几个“重缓存点”。

vLLM-Ascend 中的 non-torch 显存主要来自:

  1. Attention / FlashAttention NPU kernel workspace
  2. ViT / Vision encoder 大 workspace
  3. HCCL 通信 buffer
  4. ACL Graph compile / kernel autotune 缓存
  5. Runtime 级别的 memory pool 高水位

这些都有一个特点:

生命周期不跟 Tensor 绑定


在什么时刻你会“看到它降下来”?

通常是:

  • dummy_run / profiling 之后
  • 第一批大请求跑完
  • 图像尺寸固定后
  • batch 下降
  • request 数减少
  • 或你刚 del llm / del model

然后你调用:

1
torch.npu.empty_cache()

👉 runtime 发现:

kernel 都结束了
workspace 没人用
PyTorch 都在回收
那我也回收吧

于是:

non-torch 显存明显下降


4️⃣ 这是不是“bug”?能不能依赖?

❗ 不是 bug,是实现选择

但有两个重要 caveat:

⚠️ 1. 不保证每次都生效

  • 如果:

    • kernel 仍在飞
    • graph 被固定
    • workspace 被下次推理复用
  • runtime 不会回收


⚠️ 2. 有性能代价

下一次推理时:

  • kernel workspace 可能要重新申请
  • autotune / graph cache 可能重建

empty_cache = 用显存换性能


5️⃣ 在 vLLM-Ascend 中如何“正确”使用它?

✅ 推荐用法(你这种场景)

1
2
3
4
5
# 只在这些时刻用
- profiling / dummy_run 之后
- 模型 warmup 完成后
- request 峰值过后
- image size / batch 固定后

例如:

1
2
3
4
llm = LLM(...)
llm.profile_run()

torch.npu.empty_cache() # 非常合理

❌ 不推荐

1
2
# 每个 request 都 empty_cache
# 会导致严重性能抖动

6️⃣ 对比总结(重点记住)

行为 CUDA Ascend
empty_cache 清 PyTorch cache
影响 runtime 内存池 ⚠️ 可能
回收 kernel workspace ✅(条件满足时)
non-torch 显存下降 几乎不可能 你看到的这种情况
是否稳定 稳定 启发式 / 依赖时机

7️⃣ 给你一句“工程级结论”

在 vLLM-Ascend 上:
torch.npu.empty_cache() 是一个
“runtime 级别的显存整形(defragment + shrink)触发器”
而不仅仅是 PyTorch cache API。


如果你愿意,下一步我可以直接帮你做三件更“狠”的事之一:

  1. 精确区分:到底是哪一类 non-torch 被回收了(HCCL / kernel / graph)
  2. 设计一套 只 shrink 一次、之后显存不再抖动的 vLLM-Ascend 启动模板
  3. 帮你判断:你现在看到的 non-torch 下降,是“好事”还是“隐藏性能坑”

你这个问题,已经是 Ascend 显存调优的核心层级了。