Prhub

#36235 [CI] Stop the resolution ratchets re-parsing the whole package

原始 PR 作者 alexnails 合并时间 2026-08-25 07:55 文件变更 1 提交数 2 评论 2 代码增减 +11 / -3

执行摘要

预过滤并延迟解析,让棘轮测试提速 2.4 倍

CI shard 7 因 test_resolution_is_reproducible.py 耗时 359s(占该 shard 41%)而超时 15 分钟,成为瓶颈。PR body 明确指出三个扫描测试 test_every_fork_of_a_record_has_a_resolved_onetest_only_the_gate_runs_the_pipelinetest_the_gate_is_reached_from_the_launcher_and_from_publish 做了大量丢弃的工作,其中 ast.get_source_segment 被循环调用 28,817 次,每次都会重新切分整个文件,导致 O(n²) 开销。

值得精读,尽管它只是一个测试优化 PR,但体现了很好的性能分析方法论:精准定位热点(量化耗时分布)、通过最小改动消除 O(n²) 开销、用自校验确保正确性。对于 CI 维护者,可作为后续优化其他慢测试的参考模板。对于普通开发者,可以学习 get_source_segment 的性能陷阱和预过滤模式的通用价值。

讨论亮点

该 PR 的 review 讨论较少,仅有 CI 机器人和作者的交互:

  1. 作者发起 rerun: alexnails 请求 /rerun-test test_resolution_is_reproducible.py
  2. CI 机器人拒绝 rerun: github-actions[bot] 指出 PR 已与 main 分叉,需要 rebase 并重新推送,未执行 rerun。这反映了该 PR 在 main 更新后 CI 未及时验证,可能影响后续合并。

没有其他实质性设计讨论。作者在 PR body 中详细记录了性能分析和方法论,但未在评论区展开。

实现拆解

该 PR 仅修改一个测试文件 test/registered/unit/server_args/test_resolution_is_reproducible.py,通过三处优化提升三个扫描测试的性能:

  1. test_every_fork_of_a_record_has_a_resolved_one 预过滤 + 滞后解析(+11/-3)
    - 在 ast.parse(source) 前增加 if "Process" not in source: continue,因为该测试只关注包含 Process 字面量的文件,预过滤可跳过无关文件,避免不必要的 AST 解析。
    - 将 body = ast.get_source_segment(source, node) 从循环开头移到 if not forks: continue 之后,因为 body 只在存在 fork 时才被使用,此举避免了对 28,817 个函数定义都调用昂贵的 get_source_segment(该函数每次都会重新切分整个文件)。

  2. test_only_the_gate_runs_the_pipeline 预过滤(+3/-1)
    - 在 ast.parse(path.read_text()) 前读取 source 并检查 if "_run_resolution_pipeline" not in source: continue,与 1 相同的逻辑,只解析包含目标符号的文件。

  3. test_the_gate_is_reached_from_the_launcher_and_from_publish 预过滤(+3/-1)
    - 在 ast.parse 前检查 if "resolve_once" not in source: continue

三处预过滤的字符串匹配条件与 AST 匹配逻辑完全一致,因此不会漏报任何应被检查的文件。PR body 也明确说明,两个 test_only_the_gate_runs_the_pipelinetest_the_gate_is_reached_from_the_launcher_and_from_publish 测试本身会断言精确的调用方列表,因此预过滤的效果等同于自校验。

此外,PR body 还揭示了两个遗留问题,但并未在本 PR 中解决:_RestoresProcessState 覆写 _callTestMethod 导致部分测试缺少 CI 标记和重试机制;test_no_bare_replace_of_a_record_outside_the_helpertest_every_program_that_builds_a_record_resolves_it 因无法找到足够有选择性的预过滤键而放弃优化。

文件 模块 状态 重要度
test/registered/unit/server_args/test_resolution_is_reproducible.py 测试 modified 4.95

关键符号

test_every_fork_of_a_record_has_a_resolved_one test_only_the_gate_runs_the_pipeline test_the_gate_is_reached_from_the_launcher_and_from_publish

关键源码片段

test/registered/unit/server_args/test_resolution_is_reproducible.py test-coverage

该文件是唯一改动文件,包含三个性能优化点,是本次变更的核心。

# test/registered/unit/server_args/test_resolution_is_reproducible.py
# 此测试遍历整个 sglang 包,查找包含 Process fork 调用的函数,并断言它们
# 在 fork 前已解析记录。优化前,它会对每个函数调用 ast.get_source_segment,
# 该函数每次都会重新切分文件,导致 O(n²) 开销。本 PR 移除了对无关文件的
# AST 解析,并延迟 body 的获取。def test_every_fork_of_a_record_has_a_resolved_one(self):
    import ast
​
    package_root = pathlib.Path(next(iter(sglang.__path__))).resolve()
    offenders, examined = [], 0
    for path in sorted(package_root.rglob("*.py")):
        rel = path.relative_to(package_root).as_posix()
        if rel.startswith("test/") or "/test/" in rel:
            continue
        # The diffusion runtime has its own record with no gate.
        if rel.startswith("multimodal_gen/"):
            continue
        try:
            source = path.read_text(encoding="utf-8-sig")
            # 添加预过滤:如果文件不包含 'Process',则无需解析 AST,因为
            # 后续匹配依赖该名称。
            if "Process" not in source:
                continue
            tree = ast.parse(source)
        except (SyntaxError, UnicodeDecodeError):
            continue
        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            forks = [
                call
                for call in ast.walk(node)
                if isinstance(call, ast.Call)
                and (
                    (
                        isinstance(call.func, ast.Attribute)
                        and call.func.attr == "Process"
                    )
                    or (
                        isinstance(call.func, ast.Name)
                        and call.func.id == "Process"
                    )
                )
                # get_source_segment 在这里仍然被调用,但只有在 forks 非空时才
                # 调用,且次数受限。
                and "server_args" in (ast.get_source_segment(source, call) or "")
            ]
            if not forks:
                continue
            # 将 body 获取移到 guard 之后,避免对不包含 fork 的函数
            # 重复切分整个文件。
            body = ast.get_source_segment(source, node) or ""
            examined += 1
            # `spawn` starts a fresh interpreter, so the child may probe.
            if 'get_context("spawn")' in body or "'spawn'" in body:
                continue
            if "resolve_once(" in body or "publish(" in body:
                continue
            if rel in self._AFTER_LAUNCHER_RESOLVE:
                continue
            offenders.append(f"{rel}:{forks[0].lineno} {node.name}")
    self.assertGreater(
        examined, 5, f"only {examined} fork sites found; the scan broke"
    )
    self.assertEqual(
        offenders,
        [],
        "these fork a child that will resolve the record, without resolving "
        "it first -- the child cannot initialize CUDA if this process "
        f"already has:\n  " + "\n  ".join(offenders),
    )

评论区精华

CI rerun 请求被拒绝 other

alexnails 请求 /rerun-test test_resolution_is_reproducible.py,CI 机器人回复 PR 已与 main 分叉,需要 rebase 后重新推送。

结论:未执行 rerun,需 rebase 和重新推送。 · 已解决

风险与影响

风险极低,因为改动仅涉及测试文件内部实现,不改变任何断言逻辑。预过滤条件与 AST 匹配条件完全一致,理论不会漏报。作者还强调了两个测试 test_only_the_gate_runs_the_pipelinetest_the_gate_is_reached_from_the_launcher_and_from_publish 本身是精确断言,可自校验预过滤的正确性。唯一潜在风险是预过滤字符串与 AST 匹配未来不同步(例如引入 _run_resolution_pipeline 的新变体),但此类变化会同时失败于现有断言。此外,PR body 指出两个未能预过滤的测试,但明确不处理,不会引入风险。

影响范围限于 CI 性能test_resolution_is_reproducible.py 在 CI shard 7 上的耗时从 359s 降至约 149s(本地归一化),缓解 shard 超时问题。对用户无直接影响,对开发者的主要受益是 CI 稳定性提升和等待时间缩短。改动可维护性良好,仅涉及测试内部优化,不影响 SGLang 运行时代码。

仅测试文件变更 预过滤条件与 AST 匹配一致 CI 未覆盖(rerun 被拒)

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论