Prhub

#39423 [NPU][Bugfix] Disable pinned memory to fix DeepSeek-V2 DP-attention hang

原始 PR 作者 iridiumine 合并时间 2026-09-15 14:59 文件变更 2 提交数 2 评论 2 代码增减 +7 / -6

执行摘要

NPU 禁用 pinned memory,修复 DP 挂死

PR body 明确说明根因:#36472 让 NPU 机器解析到新的 NPUSRTPlatform,而该类「Unlike the base default (is_pin_memory_available=False), the new class returns True, silently switching hot paths such as the next-token-logits copies and the xgrammar bitmask allocation to pinned memory with non_blocking H2D copies」。由于 torch_npu 的 pinned-memory + async-copy 语义尚未针对 CANN 验证,non_blocking H2D 依赖的流序保证在 NPU 上并不成立,在 compute / copy / HCCL 三流并发的 DP attention 场景下 DeepSeek-V2 会间歇挂死;作者给出两处独立复现(#36472 作者在常规 DP attention 推理中观察到挂死,以及本 PR 在 Ascend 910C 上用 TestDPRetract 压力复现),并强调单步收益仅限 KB 量级 decode buffer,因此在语义验证完成前直接关闭是安全选择。

值得精读(约 5 分钟)。NPU 平台维护者必读:这是一个典型的「平台新类覆盖基类安全默认值导致死锁」案例,7 行改动换来稳定性。关注三个设计决策:(1)为什么选择无条件 return False 而不是保留 device 分支或加环境变量开关——答案是把默认值收敛到与 SRTPlatform 一致,避免参数组合分叉;(2)如何用两处独立复现(#36472 作者观察 + Ascend 910C TestDPRetract 压测)为「未验证的异步语义」背书;(3)重开路径的条件是否应该固化成代码注释或跟踪 issue,避免 remove comments 之后只剩一行无解释的 return False。

讨论亮点

本 PR 没有任何 review 评论(review_comments_count=0),讨论信息主要来自 PR body 与两条 issue 评论。PR body 承担了完整的技术论证:一是解释 #36472 的新类覆盖了基类安全默认值,二是给出两处独立复现作为证据,三是明确「先验证 CANN 流序语义再重开」的重开路径。评论区只有 CI 动作:sglang-npu-bot 发出 /tag-and-rerun-ci,作者 iridiumine 随后发出 /rerun-failed-ci;PR 状态区显示 Extra 流水线为失败态,但没有人对该失败项做技术判定。值得注意的是第二个 commit 只做了一件事——删除源码侧注释,最终代码里 is_pin_memory_available 只剩一行 return False,解释被转移到测试文件与 PR body。

实现拆解

  1. 定位回归入口并收敛返回值:修改 python/sglang/srt/platforms/npu.py 中 NPUDeviceMixin.is_pin_memory_available。原实现是「device 非空且字符串等于 cpu 时返回 False,否则返回 True」,与 SRTPlatform 基类默认的 False 相反;现直接 return False(+1/-3),删除 device 分支判断,调用方无论传 npu 还是 cpu 都拿到同一个结果。
  2. 影响传播路径:该方法是平台接口层唯一的「是否允许 pinned buffer」开关,所有通过它判断的热路径都会回退到 pageable memory + 同步 H2D,PR body 点名的两条是 next-token logits 拷贝与 xgrammar bitmask 分配;这也意味着 DP attention 下 copy 流不再需要与 HCCL / compute 流做额外的顺序保证,挂死条件被消除。
  3. 测试同步对齐:test/registered/unit/platforms/test_platform_interface.py 的 test_pin_memory_available_for_npu_targets 把前三条断言由 assertTrue 改为 assertFalse,并补充 3 行注释说明「Pinned memory stays disabled on NPU」,覆盖 None、"npu"、torch.device("npu", 0)、"cpu"、torch.device("cpu") 五种入参。
  4. 提交演进:commit 9933d13 完成主改动,随后 commit cc1a62f(message 为 remove comments)删除源码侧新增注释,最终解释性文字只保留在测试文件与 PR body 中。
  5. 配套范围:没有配置项、文档或部署脚本改动,不走环境变量开关,属于直接改默认行为;PR body 建议后续「Re-enable only after the pinned-memory async-copy semantics are validated against CANN (optionally behind a debug env var first)」。
文件 角色 变更要点
python/sglang/srt/platforms/npu.py 源码核心 NPUDeviceMixin.is_pin_memory_available 无条件返回 False,回到 #36472 之前语义
test/registered/unit/platforms/test_platform_interface.py 测试配套 断言全部改为 assertFalse,加注释固化「NPU 不启用 pinned memory」的约定
文件 模块 状态 重要度
python/sglang/srt/platforms/npu.py NPU 平台 modified 5.75
test/registered/unit/platforms/test_platform_interface.py 平台单测 modified 4.07

关键符号

NPUDeviceMixin.is_pin_memory_available TestPlatformInterface.test_pin_memory_available_for_npu_targets

关键源码片段

python/sglang/srt/platforms/npu.py core-logic

唯一的源码改动点:NPUDeviceMixin.is_pin_memory_available 从「CPU 返回 False、其余返回 True」改为无条件 False,直接决定 NPU 上 next-token logits 拷贝与 xgrammar bitmask 分配是否使用 pinned memory + non_blocking H2D,是 DP attention 挂死的开关。

class NPUDeviceMixin:
    """NPU 设备能力 mixin:NPUSRTPlatform 通过它把 torch_npu 接入 SRTPlatform。"""
​
    # 其余设备管理方法(get_device / set_device / get_device_name /
    # empty_cache / synchronize / get_available_memory 等)保持原样,此处省略。
​
    def is_pin_memory_available(self, device=None) -> bool:
        # 本 PR 的修复点:无条件返回 False,等价于回退 PR#36472 之前的行为。
        # 旧实现为「device 是 "cpu" 时返回 False,否则(含 npu)返回 True」,
        # 会让 next-token logits 拷贝与 xgrammar bitmask 分配走上
        # pinned memory + non_blocking H2D。
        # torch_npu 的 pinned + async copy 语义尚未针对 CANN 验证,
        # non_blocking H2D 依赖的流序保证在 NPU 上不成立;DP attention 下
        # compute / copy / HCCL 多流并发时,DeepSeek-V2 会间歇性挂死。
        # device 参数被有意忽略:调用方传 "npu" 还是 "cpu" 都拿到 False,
        # 避免不同入参下行为分叉而再次隐式开启 pinned memory。
        # 重开条件:先在 CANN 上验证 pinned memory 的异步拷贝流序语义,
        # 可考虑先用 debug 环境变量做灰度。
        return False
​
​
class NPUSRTPlatform(NPUDeviceMixin, SRTPlatform):
    """默认的 in-tree NPU SRT 平台实现。"""
​
    def get_default_attention_backend(self) -> str:
        return "ascend"
​
    def get_dispatch_key_name(self) -> str:
        return "npu"
​
    def supports_fp8(self) -> bool:
        # NPU 量化后端位于 hardware_backend/npu/quantization
        return True
​
    def support_cuda_graph(self) -> bool:
        # 由 hardware_backend/npu/graph_runner 中的 NPUGraphRunner 提供
        return True
​
    def support_piecewise_cuda_graph(self) -> bool:
        return False
test/registered/unit/platforms/test_platform_interface.py test-coverage

把「NPU 上 pinned memory 可用」的断言反向固定为不可用,并加注释解释原因;它是此次行为回退唯一的 in-tree 自动化守护。

def test_pin_memory_available_for_npu_targets(self):
    # Pinned memory 在 NPU 上保持关闭:torch_npu 的 pinned memory +
    # non_blocking H2D 路径未针对 CANN 验证(见
    # NPUSRTPlatform.is_pin_memory_available)。
    base = NPUSRTPlatform()
    # 断言由 assertTrue 收紧为 assertFalse:默认无参、显式 npu 设备、显式 cpu
    # 设备三种入参都必须为 False,防止 device 参数差异导致行为分叉,
    # 再次隐式打开 pinned memory 而复发 DP attention 挂死。
    self.assertFalse(base.is_pin_memory_available())
    self.assertFalse(base.is_pin_memory_available(device="npu"))
    self.assertFalse(base.is_pin_memory_available(device=torch.device("npu", 0)))
    self.assertFalse(base.is_pin_memory_available(device="cpu"))
    self.assertFalse(base.is_pin_memory_available(device=torch.device("cpu")))

评论区精华

是否应直接关闭 NPU 端 pinned memory(回退 #36472 的默认值) 设计

PR body 指出 #36472 引入的 NPUSRTPlatform 与基类默认相反地返回 True,导致 next-token logits 拷贝、xgrammar bitmask 分配改走 pinned memory + non_blocking H2D;而 torch_npu 的 pinned + async copy 语义未对 CANN 验证,DP attention 下多流并发时 DeepSeek-V2 间歇挂死,并给出两处独立复现(#36472 作者观察 + Ascend 910C TestDPRetract 压测)。

结论:选择保守回退:is_pin_memory_available 无条件返回 False,理由是单步收益仅 KB 量级 decode buffer,不值得承担未验证的流序风险;重开需先验证 CANN 异步拷贝语义,可先用 debug env var 灰度。无 reviewer 提出反对意见。 · 已采纳并合并

单元测试断言与新的默认行为对齐 测试

test_pin_memory_available_for_npu_targets 由 assertTrue 改为 assertFalse,覆盖 None / "npu" / torch.device("npu", 0) / "cpu" / torch.device("cpu") 五种入参,并新增注释说明关闭原因;与此同时第二个 commit `remove comments` 删掉了源码侧的解释性注释。

结论:测试与新行为一致,成为该回退唯一的 in-tree 自动化守护;但解释性文字只留在测试文件,源码侧已无注释。 · 已解决

CI 状态与重跑流程 other

评论记录只有 CI 动作:sglang-npu-bot 发出 /tag-and-rerun-ci,作者 iridiumine 随后发出 /rerun-failed-ci;PR 状态区显示 Extra 流水线为失败态,Base 与 AMD ROCm 仍处于进行中。

结论:没有 reviewer 对失败项做技术判断,合入依据以人工压测 + 单测为主,最终由 sglang-npu-bot 合并。 · 已合并

风险与影响

  1. 性能回退(NPU 专属):is_pin_memory_available=False 会让 next-token logits 拷贝与 xgrammar bitmask 分配退回 pageable memory + 同步 H2D,这两条都在每步 decode 热路径上。PR body 声称收益只有 KB 量级,但 Checklist 中未勾选 speed benchmark,实际损失未量化。
  2. 缺少端到端回归测试:真正触发挂死的场景是 DP attention + 小 KV 预算 + radix retraction 压力(SGLANG_TEST_RETRACT=1 对应的 TestDPRetract),这一复现没有进入 in-tree registered CI;单测只锁住 is_pin_memory_available 返回 False 这一布尔值,若未来有人重开 pinned memory,挂死风险不会有自动化拦截。
  3. CI 证据偏弱:PR body 的 CI States 显示 Extra 流水线失败,review 记录为空,合并由 sglang-npu-bot 完成,说明合入依据主要是人工压测而非绿灯 CI。
  4. 知识留存风险:源码侧注释被 remove comments commit 删除,python/sglang/srt/platforms/npu.py 里看不到为何关闭;重开条件(CANN 验证 + 流序保证)只存在于 PR body,未落到代码注释、环境变量或跟踪 issue 中。
  5. 潜在调用方分支切换:任何在 NPU 上依赖 is_pin_memory_available()==True 而走 pinned 分支的调用方(拷贝、buffer 分配)现在都会走另一条路径,需要确认这些分支在 pageable 语义下同样正确。
  6. 回归面受控:改动仅限 NPUDeviceMixin,CUDA / ROCm / XPU 平台不经过该实现,跨平台回归风险低。

用户侧:Ascend 910C 等 NPU 部署在 DeepSeek-V2 + DP attention(尤其叠加 retraction / 小 KV 预算 / 受限解码)场景下不再间歇挂死,可用性显著提升;代价是 NPU decode 路径失去 H2D 拷贝与计算的 overlap,理论上有小幅时延上升,但未见量化数据。系统侧:无 API、无配置、无 schema、无部署脚本变更,属于默认行为回退,部署方零改动即可受益。团队侧:补上了 #36472 in-tree NPU 平台收敛的一个缺口,并给出一个通用教训——新平台类最危险的改动是覆盖基类的安全默认值;后续若要为 NPU 重新启用 pinned memory,需要先做 CANN 异步拷贝流序验证、再以 debug env var 灰度,并配套端到端挂死回归测试。

核心路径行为回退 缺少端到端挂死回归测试 NPU decode 热路径性能未量化 CI 证据偏弱(Extra 失败) 源码侧缺少重开条件注释

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论