Prhub

#36180 [npu] Combine NPU test fixes from #35472 and #34516

原始 PR 作者 pllimax 合并时间 2026-08-25 09:00 文件变更 7 提交数 4 评论 8 代码增减 +92 / -28

执行摘要

合并 NPU 测试修复:进程组收割与挂起检测

PR body 明确动机:Improve reliability of Ascend NPU benchmark and accuracy tests by detecting hung benchmark processes earlier and cleaning up orphaned process groups。此前 benchmark 进程一旦挂起会一直占住 stdout,CI 只能等到全局超时;而 deep-ep / HCCL 的 worker 进程在父进程退出后会被 reparent,遗留为孤儿进程,污染同一 runner 上的后续测试。作者把 #35472、#34516 两个修复整合到主线,消除重复实现并统一进程组清理逻辑。

值得 NPU 测试维护者精读 run_bench_serving 的看门狗实现:把 'stdout 空转超时 + 排空宽限 + 进程组 SIGKILL' 组合起来管理长跑子进程,是一个可复用的测试进程治理模式。一般开发者可略读,只需关注 DEEPEP_HCCL_BUFFSIZE 这类环境变量约定是否与本仓库 NPU 部署文档一致。

讨论亮点

cherryblo 的 review 集中在 test_npu_performance_utils.py 的代码可读性:两处要求 add comment(新增的 watchdog 常量与 L504 附近逻辑)、建议把 poll() 分支与 reader_done 分支合并为单个 if 语句、质疑 last_activity 为何用 list 包装、并指出某个条件判断不再需要。作者在后续提交(763a7e1c)中做了实质性简化:移除绝对超时上限 BENCHMARK_SERVING_TIMEOUT、删除 _collect_process_tree / _kill_recorded_pids 的孤儿进程快照记录、把 last_activity 从 list 改为标量,与评论方向一致;但 poll 分支与 reader_done 分支是否最终合并没有明确记录。整体是风格与可读性层面的交锋,没有出现正确性或设计原则的分歧。

实现拆解

  1. 新增 kill_process_group 进程组收割工具(python/sglang/test/ascend/e2e/test_npu_multi_node_utils.py):基于 start_new_session 进程模型,用 os.killpg(process.pid, signal.SIGKILL) 收割整个进程组,分场景处理 ProcessLookupError、PermissionError 与 OSError,作为 NPU 测试套件的统一清理入口,供 perf 与 accuracy 两侧复用。
  2. 重写 run_bench_serving 的看门狗(python/sglang/test/ascend/e2e/test_npu_performance_utils.py):benchmark 子进程改为 start_new_session=True 启动;新增常量 BENCHMARK_STDOUT_DRAIN_GRACE(10 秒)、BENCHMARK_STDOUT_IDLE_TIMEOUT(300 秒)、BENCHMARK_WATCHDOG_POLL_INTERVAL(30 秒);守护线程 _kill_on_timeout 覆盖三种僵死场景(直接子进程退出但 stdout 被孙进程持有、stdout EOF 但子进程不退出、长时间无输出疑似挂起),统一走 kill_process_group 强杀;读线程逐行消费 stdout 并刷新 last_activity 标量供 watchdog 判断。
  3. accuracy 清理统一(python/sglang/test/ascend/e2e/test_npu_accuracy_utils.py):删除本地重复的 _kill_evalscope_session 与 signal 导入,在 run_evalscope 的正常、异常与超时路径统一调用 kill_process_group,避免两套收割逻辑漂移。
  4. 测试配置与类名修正:qwen3_vl_8b_thinking 与 qwen3_vl_30b_a3b_thinking 两个 MMMU 测试把 HCCL_BUFFSIZE 改名为 DEEPEP_HCCL_BUFFSIZE(对齐 deep-ep 的新环境变量约定),类名从 TestQwen3 改为带模型语义的 TestQwen3_VL_8B_Thinking_MMMU / TestQwen3_VL_30B_A3B_Thinking_MMMU,并把 8B 的 nightly est_time 从 6500 提升到 12000;kimi_k2_6 性能测试增加 --max-total-tokens 32256 参数。
  5. CI workflow 调整(.github/workflows/pr-test-npu.yml):base-c-test-acc 四个 stage 的 runner 标签从 linux-aarch64-a3-- 切换为 linux-aarch64-a3-800t-,与性能 job 使用同一批 800t 节点。
文件 模块 状态 重要度
python/sglang/test/ascend/e2e/test_npu_performance_utils.py 性能看门狗 modified 5.65
python/sglang/test/ascend/e2e/test_npu_multi_node_utils.py 进程回收 modified 4.89
python/sglang/test/ascend/e2e/test_npu_accuracy_utils.py 精度测试 modified 4.94
test/registered/npu/accuracy/qwen3_vl_8b_thinking/test_npu_qwen3_vl_8b_thinking_1p_mmmu.py MMMU 精度测试 modified 4.34
test/registered/npu/accuracy/qwen3_vl_30b_a3b_thinking/test_npu_qwen3_vl_30b_a3b_thinking_1p_mmmu.py MMMU 精度测试 modified 4.22
.github/workflows/pr-test-npu.yml CI 配置 modified 3.33
test/registered/npu/performance/kimi_k2_6/test_npu_kimi_k2_6_w4a8_8p_in3k5_out1k5_20ms.py 性能基准 modified 2.85

关键符号

kill_process_group _kill_on_timeout run_bench_serving run_evalscope _kill_evalscope_session

关键源码片段

python/sglang/test/ascend/e2e/test_npu_performance_utils.py test-coverage

核心变更:run_bench_serving 引入进程组级 watchdog,新增三个超时常量并覆盖三种僵死场景,是本 PR 最具技术含量的文件。

# 用独立 session 启动 benchmark,保证 process.pid == pgid,
# 后续无论谁持有 stdout,都能按进程组整体收割。
process = subprocess.Popen(
    cmd_args,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    bufsize=1,
    env=env,
    start_new_session=True,
)# reader_done 表示 stdout 已读到 EOF;last_activity 记录最后一条输出时间。
reader_done = threading.Event()
last_activity = time.time()
​
​
def _kill_on_timeout():
    while True:
        # 场景 1:直接子进程已退出,但 stdout 仍被子孙进程持有。
        # 等待 DRAIN_GRACE 秒让 reader 排空剩余输出,超时则强杀全部。
        if process.poll() is not None:
            if not reader_done.wait(timeout=BENCHMARK_STDOUT_DRAIN_GRACE):
                logger.error(
                    f"Benchmark stdout still open after process exit, "
                    f"killing process group {process.pid}"
                )
                kill_process_group(process)
                return
        # 场景 2:stdout 已 EOF,但直接子进程迟迟不退出。
        if reader_done.is_set():
            try:
                process.wait(timeout=BENCHMARK_STDOUT_DRAIN_GRACE)
            except subprocess.TimeoutExpired:
                logger.error(
                    f"Benchmark {process.pid} still alive after stdout EOF, "
                    f"killing process group {process.pid}"
                )
                kill_process_group(process)
                return
        # 场景 3:进程还在运行但长时间无输出,判定为挂起。
        idle = time.time() - last_activity
        if idle > BENCHMARK_STDOUT_IDLE_TIMEOUT:
            logger.error(
                f"Benchmark produced no output for {idle:.0f}s "
                f"(> {BENCHMARK_STDOUT_IDLE_TIMEOUT}s), "
                f"killing process group {process.pid}"
            )
            kill_process_group(process)
            return
        time.sleep(BENCHMARK_WATCHDOG_POLL_INTERVAL)
​
​
# daemon 线程保证主测试线程结束时 watchdog 不阻塞退出。
watchdog = threading.Thread(target=_kill_on_timeout, daemon=True)
watchdog.start()try:
    # 读线程逐行消费 stdout 并刷新 last_activity,供 watchdog 判断空转。
    with open(result_file, "a", encoding="utf-8") as f:
        for line in process.stdout:
            last_activity = time.time()
            if line.strip():
                print(line, end="")
            f.write(line)
            # ... 解析指标行 ...
finally:
    reader_done.set()
    process.wait()
python/sglang/test/ascend/e2e/test_npu_multi_node_utils.py test-coverage

新增 kill_process_group 公共工具,是 perf 与 accuracy 两侧收割进程组的统一入口。

def kill_process_group(process):
    """SIGKILL 收割整个进程组。    process 是用 start_new_session=True 启动的 Popen,其 pid 即进程组 id;
    调用方必须保证这一点,否则 killpg 会误杀无关进程。
    """
    if process is None:
        return
    try:
        os.killpg(process.pid, signal.SIGKILL)
        logger.info(f"killed process group pgid={process.pid}")
    except ProcessLookupError:
        # 进程组已自然退出,属于正常情况。
        logger.info(f"process group pgid={process.pid} already gone")
    except PermissionError:
        # 无权限时只告警,避免掩盖真实问题。
        logger.warning(f"no permission to kill process group pgid={process.pid}")
    except OSError as e:
        # 兜底捕获,保证清理路径不会二次抛出异常。
        logger.warning(f"failed to kill process group pgid={process.pid}: {e}")

评论区精华

watchdog 新增逻辑需要注释说明 style

cherryblo 在 BENCHMARK_SERVING_TIMEOUT 常量和 watchdog 逻辑附近两次要求 add comment,认为多分支的看门狗逻辑(排空宽限、空转超时、轮询间隔)缺少解释,读者难以理解各常量用途。

结论:作者保留并补充注释;后续提交直接移除了绝对超时上限 BENCHMARK_SERVING_TIMEOUT,进一步减少需要解释的配置。 · 已解决

stdout 处理分支应合并为单个 if 语句 设计

cherryblo 建议 Merge into a single if-statement:poll() 非 None 与 reader_done.is_set() 两个分支在逻辑上存在重叠,嵌套读起来绕。

结论:看门狗在提交 2 中被简化(去掉绝对超时上限、last_activity 改标量),但 poll 分支与 reader_done 分支是否最终合并没有明确记录。 · 未确认

last_activity 为何用 list 包装 question

cherryblo 提问 why use list?,指向线程间共享时间戳的同步方式;早期版本用 list 包装标量以规避子线程写入 int 的可见性问题。

结论:提交 2 将 last_activity 改为标量,移除了 list 包装,简化共享方式。 · 已解决

移除不再需要的超时判断条件 设计

cherryblo 指出 Remove this condition; this check is no longer required.,该条件对应早期版本的绝对超时上限 BENCHMARK_SERVING_TIMEOUT。

结论:提交 2 移除了 BENCHMARK_SERVING_TIMEOUT 绝对上限,以 idle 超时 + 排空宽限替代,与建议一致。 · 已解决

风险与影响

  1. 误杀风险(test_npu_performance_utils.py):watchdog 在 300 秒无输出即 SIGKILL 整个进程组。benchmark 在长 prefill、模型权重下载或大数据集准备阶段可能长时间无 stdout,存在误杀正常任务的可能;SIGKILL 不留恢复窗口,排障时只能靠日志。
  2. 进程组收割的边界风险(test_npu_multi_node_utils.py):kill_process_group 依赖 start_new_session 保证 pid == pgid,若未来有人复用该函数但未用 start_new_session 启动进程,os.killpg 会因进程组不存在或误杀同组进程而静默失败/误伤。
  3. DEEPEP_HCCL_BUFFSIZE 改名兼容性:旧变量 HCCL_BUFFSIZE 在底层 deep-ep 版本上是否仍被读取未在 PR 中验证,若新变量名不被 CANN 版本识别,通信 buffer 会回退默认值,可能影响多卡大模型 benchmark 与精度表现。
  4. CI runner 标签切换(pr-test-npu.yml):linux-aarch64-a3-800t-* 依赖 runner 池容量与标签准确性,若资源不足或标签未注册,accuracy job 会一直排队阻塞合并。
  5. 影响范围受限:全部为测试与 CI 配置变更,不触碰 srt 运行时代码,无推理路径回归风险。

影响范围限于 Ascend NPU 的 CI 与注册测试体系:benchmark/accuracy 测试不再因进程挂起或孤儿进程拖死 runner,nightly accuracy 与性能回归的稳定性提升;Qwen3-VL thinking 两个 MMMU 测试的命名与配置更贴近模型语义,便于 CI 结果归因。对使用 NPU CI 的维护者和模型验证工程师有直接收益,对运行时用户无感知。影响程度中等偏低。

SIGKILL 误杀进程组风险 300 秒无输出判定挂起可能误杀长任务 DEEPEP_HCCL_BUFFSIZE 改名兼容性待验证 CI runner 标签依赖 a3-800t 资源池 纯测试变更,无运行时影响

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论