Prhub

#33654 [CI] Move CPU-only unit tests to the CPU suite and trim dead 5090 registrations

原始 PR 作者 hnyls2002 合并时间 2026-08-05 16:43 文件变更 30 提交数 5 评论 3 代码增减 +142 / -164

执行摘要

CPU 单测迁至 CPU 池并压缩 5090 队列

PR 标题和正文明确指出这是一次“registration-level trims on the 1-gpu-5090 pool (~675s per commit off base-b, no test logic touched)”。核心动机有三:

1) 大量纯 CPU 单元测试占用了稀缺的 1-gpu-5090 队列资源;
2) 模块级 pytestmark.skip 对分片不可见,文件仍计入预算并产生 import 开销,还阻塞 owner 在本地直接运行测试;
3) VLM 性能测试的硬阈值放在队列竞争最激烈的池子里容易误报,更适合 label 门控。正文还明确排除了两个不能迁移的文件,说明作者对真实设备依赖做了逐一核实。

值得 CI 维护者和测试贡献者精读。重点关注三处:一是注册级 disabled= 取代 pytestmark.skip 的分片治理思路;二是 run_bench_serving_multi 让多个 benchmark 共享 server 的聚合测试模式;三是“CUDA 屏蔽后跑通 + 保留一个设备路径注册(如 AMD)”的 CPU-only 判定模板。该 PR 不涉及产品逻辑,对业务研发可略读。

讨论亮点

本 PR 没有任何 review 评论(review_comments_count 为 0),关键设计决策均由作者在 PR body 中给出论证:

  • pytest skip 与注册级 disabled 的取舍:作者明确指出 pytest-level skip 对分片不可见,文件仍在消耗预算并产生 import 开销,同时阻塞 owner 在稳定化阶段直接运行测试,因此注册标志是 canonical CI gate。
  • VLM perf 缩减依据:作者称 medians are stable at that sample size and the thresholds are loose ceilings,因此只降 prompt 数而不放松断言;同时也说明“两个 bench 共用一个 server”是主要耗时来源。
  • 排除迁移的判定:对未迁移文件分别给出了具体理由(memory_allocated 设备依赖、nixl 仅 CUDA runner 安装),说明并非一刀切迁移。

实现拆解

1. 识别并迁移 CPU-only 单元测试

以“CUDA 屏蔽后仍能跑通”作为判定标准,将 19 个文件从 base-b / 1-gpu-small 迁移到 base-a-test-cpu:

  • 典型文件包括 test/registered/unit/models/test_llava.py、test/registered/unit/managers/test_profile_merger_http_api.py、test/registered/unit/managers/test_prefill_adder.py、test/registered/unit/models/test_deepseek_mla_dispatch.py 等;
  • 原本指向休眠 base-c-test-cpu(无派发 workflow)的注册统一改指 base-a-test-cpu;
  • 已存在 live CPU 注册的文件(如 test_eagle_worker_v2_topk1_fastpath.py、test_vit_pos_embed_interpolate.py)删除重复的 CUDA 注册;
  • 同步移除这些文件的 AMD 1-gpu 注册,但 test_vit_pos_embed_interpolate.py 因在有 GPU 时真实走设备路径而保留 AMD 注册。

2. 目录归位到 unit 镜像树

把位于功能目录下的 CPU 迁移文件搬到 test/registered/unit/ 对应子目录,符合单测放置约定:

  • disaggregation/test_specv2_kvcache_offloading.py → unit/disaggregation/;
  • vlm/test_evs.py → unit/multimodal/;
  • prefill_only/test_embed_overrides.py → unit/managers/;
  • models/test_vit_pos_embed_interpolate.py → unit/models/。

3. LoRA skip 语义替换

8 个 unit/lora 文件将模块级 pytestmark = pytest.mark.skip(...) 替换为注册级 register_cuda_ci(..., disabled="reason"),例如 test_experimental_sgl_marlin_multi_prefill.py、test_inkling_linearized_lora_unit.py、test_inkling_moe_lora_overlap_unit.py。这样分片时能感知禁用状态,不再占用预算,同时移除 pytestmark 行后 owner 可临时去掉 disabled 在本地稳定测试。

4. VLM 性能测试压缩

test/registered/perf/test_vlm_perf_5090.py 将 test_vlm_offline_throughput 与 test_vlm_online_latency 两个用例合并为 test_vlm_perf:

  • 通过 run_bench_serving_multi 让离线与在线两个 benchmark 共享一个 server;
  • online 阶段从 250 个 prompt 降到 50 个(1 req/s),保留全部四个阈值断言;
  • 新增 _local_tokenizer_path() 优先使用本地模型快照,避免 bench 客户端在 CI 中长时间阻塞于 HF Hub API;
  • 注册从 base-b 移到 extra-a,est_time 从 406 降到 180。

5. 边界排除与验证

未被迁移的两类文件有明确依据:test_radix_cache_unit.py 的 test_memory_allocated 读取 torch.get_device_module().memory_allocated() 依赖设备模块;test_hicache_nixl_storage.py 依赖只在 CUDA runner 上安装的 nixl。PR 经过 /tag-and-rerun-ci 与 /rerun-test test_vlm_perf_5090.py 验证,重跑在 1-gpu-5090 上通过。

本 PR 不涉及产品源码、配置或 schema 变更,全部为测试注册与文件位置调整。

文件 模块 状态 重要度
test/registered/perf/test_vlm_perf_5090.py 性能测试 modified 7.16
test/registered/unit/lora/test_experimental_sgl_marlin_multi_prefill.py LoRA 测试 modified 4.7
test/registered/unit/models/test_vit_pos_embed_interpolate.py 位置嵌入 renamed 5.18
test/registered/unit/disaggregation/test_specv2_kvcache_offloading.py KV 缓存 renamed 4.67
test/registered/unit/multimodal/test_evs.py 多模态 renamed 4.88

关键符号

test_vlm_perf _local_tokenizer_path test_vlm_offline_throughput test_vlm_online_latency

关键源码片段

test/registered/perf/test_vlm_perf_5090.py test-coverage

VLM 性能测试从 base-b 移至 extra-a,并通过 run_bench_serving_multi 将离线 / 在线两个阶段合并到同一个 server,预估耗时从 406 秒降至 180 秒;是本次改动最实质的测试工程变更。

# test/registered/perf/test_vlm_perf_5090.py
# 将两个独立的性能测试合并为共享 server 的流程:离线吞吐与在线延迟
# 复用同一个 SGLang 实例,避免重复拉起引擎的固定开销;
# 注册从 base-b 移到 extra-a,预估耗时从 406 秒压到 180 秒。import os
import unittestfrom sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import (
    DEFAULT_SMALL_VLM_MODEL_NAME_FOR_TEST,
    DEFAULT_URL_FOR_TEST,
    CustomTestCase,
    auto_config_device,
    get_benchmark_args,
    is_in_ci,
    run_bench_serving_multi,
    write_github_step_summary,
)register_cuda_ci(est_time=180, stage="extra-a", runner_config="1-gpu-small")
register_amd_ci(est_time=300, suite="stage-b-test-1-gpu-small-amd")
​
​
def _local_tokenizer_path():
    # 优先使用本地快照中的 tokenizer,避免 bench 客户端在 CI 中触发
    # HF Hub API 调用(可能阻塞数分钟),找不到时回退到 None
    try:
        from sglang.srt.utils import find_local_repo_dir
​
        local_dir = find_local_repo_dir(
            DEFAULT_SMALL_VLM_MODEL_NAME_FOR_TEST, revision=None
        )
        if local_dir and os.path.isdir(local_dir):
            return local_dir
    except Exception:
        pass
    return None
​
​
class TestVLMPerf5090(CustomTestCase):
    def test_vlm_perf(self):
        # 构建统一的 benchmark 参数字典,离线与在线两个场景共用一组配置
        common = dict(
            base_url=DEFAULT_URL_FOR_TEST,
            dataset_name="mmmu",
            dataset_path="",
            tokenizer=_local_tokenizer_path(),
            random_input_len=4096,
            random_output_len=2048,
            seed=0,
            device=auto_config_device(),
        )
        offline = get_benchmark_args(
            num_prompts=200, request_rate=float("inf"), **common
        )
        # 在线阶段从 250 个 prompt 降到 50 个(1 req/s),约 1 分钟跑完;
        # 中位数在该样本量下稳定,且断言使用的都是宽松上限,不会误报
        online = get_benchmark_args(num_prompts=50, request_rate=1, **common)
​
        (_, res_offline), (_, res_online) = run_bench_serving_multi(
            DEFAULT_SMALL_VLM_MODEL_NAME_FOR_TEST,
            DEFAULT_URL_FOR_TEST,
            other_server_args=["--mem-fraction-static", "0.7"],
            benchmark_args=[offline, online],
        )
​
        if is_in_ci():
            write_github_step_summary(
                f"### test_vlm_perf (5090)\n"
                f"Output throughput: {res_offline['output_throughput']:.2f} token/s\n"
                f"median_e2e_latency_ms: {res_online['median_e2e_latency_ms']:.2f} ms\n"
            )
            # 四个关键阈值断言全部保留,只合并了执行路径
            self.assertGreater(res_offline["output_throughput"], 2000)
            self.assertLess(res_online["median_e2e_latency_ms"], 16500)
            self.assertLess(res_online["median_ttft_ms"], 150)
            self.assertLess(res_online["median_itl_ms"], 8)
​
​
if __name__ == "__main__":
    unittest.main()
test/registered/unit/lora/test_experimental_sgl_marlin_multi_prefill.py configuration

作为 8 个 LoRA 测试的代表,展示从模块级 pytestmark.skip 到注册级 disabled= 的规范转换,是本次分片预算治理的核心模式。

# test/registered/unit/lora/test_experimental_sgl_marlin_multi_prefill.py
# 原实现使用模块级 pytestmark = pytest.mark.skip 跳过 CI,
# 该方式对分片不可见:文件仍计入分片预算并产生一次 import 开销;
# 改为注册级 disabled 后,调度器能看到这是禁用项,owner 也可在本地直接运行。
from pathlib import Pathfrom sglang.test.ci.ci_register import register_cuda_ciregister_cuda_ci(
    est_time=45,
    stage="base-b",
    runner_config="1-gpu-small",
    disabled="fused MoE LoRA-add kernel needs more opt-in shared memory than the ",
)
# 背景:该 fused kernel 的共享内存占用超过小 GPU CI runner 的 opt-in 上限
# (L4 上约 99 KiB,rank=128 时会 OOM);选择在 CI 禁用而不是裁剪生产 kernel。
test/registered/unit/models/test_vit_pos_embed_interpolate.py rename-or-move

从 test/registered/models 迁到 unit/models,移除重复的 CUDA 注册并保留 AMD 设备路径,体现 CPU-only 判定的典型边界。

# test/registered/unit/models/test_vit_pos_embed_interpolate.py
# 纯 stub 级位精确单测:无模型权重、无分布式初始化,
# 在 CPU 上即可完成插值算术的 bit-exact 验证,因此迁到 CPU 队列;
# 原有 base-a-test-cpu 注册已生效,故删除重复的 CUDA 注册。
import torchfrom sglang.test.ci.ci_register import register_amd_ci, register_cpu_ci
from sglang.test.test_utils import CustomTestCaseregister_cpu_ci(est_time=20, suite="base-a-test-cpu")
# AMD 注册保留:当 GPU 存在时该测试仍然走设备路径,覆盖 ROCm 行为
register_amd_ci(est_time=20, stage="stage-a", runner_config="1-gpu-small-amd")NUM_POS = 2304 # Qwen3-VL 的位置编码数,对应 48x48 网格
HIDDEN = 64 # 小 hidden dim 保持单测快速
MERGE = 2# 覆盖单图 / 大上采样 / 混合 batch / 视频 / 视频 + 图片 / 多重复等场景
GRID_CASES = {
    "single": [[1, 16, 16]],
    "single_large": [[1, 64, 98]], # h, w 可超过网格边长(上采样)
    "multi_mixed": [[1, 16, 24], [1, 32, 12], [1, 8, 40]],
    "video": [[4, 16, 20]],
    "video_plus_image": [[3, 12, 16], [1, 20, 28], [2, 8, 8]],
    "many": [[1, 24, 24]] * 8,
}
​
​
def _devices():
    # CPU 上必须跑;CUDA 可用时追加 GPU 以覆盖设备路径
    devs = [torch.device("cpu")]
    if torch.cuda.is_available():
        devs.append(torch.device("cuda"))
    return devs

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 测试覆盖转移风险:CPU-only 判定基于“屏蔽 CUDA 后跑通”,可能掩盖真实 GPU 路径上的回归。test_vit_pos_embed_interpolate.py 保留 AMD 注册作为设备路径兜底,但 NVIDIA/CUDA 侧的注册被移除,后续 CUDA 特有行为失去回归覆盖。
  2. 注册语义变更风险:8 个 LoRA 文件从 pytestmark.skip 改为 registration 的 disabled= 参数,行为依赖 sglang.test.ci.ci_register 的实现;若 disabled 的收集语义与 pytest skip 不一致,可能出现“看似禁用实则仍被分片”或相反的情况,需要观察首个全量 CI 结果。
  3. 基准采样量缩减:VLM perf 的在线阶段从 250 个 prompt 减到 50 个,虽作者称中位数稳定,但未来模型或 runtime 变化导致方差增大时,可能更容易出现误报或漏报。
  4. 文件迁移的历史追踪成本:30 个文件中多个发生 rename/move,git blame 与跨版本追溯会暂时变弱。
  5. CPU 套件承载能力:迁移后 base-a-test-cpu 的测试增多,若某些测试隐式依赖 GPU 相关环境变量或路径,可能在托管 runner 上出现偶发失败。

对普通用户无影响;对 CI 与开发流程影响显著:

  • CI 吞吐:每个 commit 在 base-b 上约节省 675 秒 GPU 队列时间,1-gpu-5090 池竞争明显缓解,真实 GPU 测试的排队等待减少;
  • 分片预算可观测性:LoRA 测试因改为注册级 disabled,分片预算不再被无效文件吞噬,CI 状态展示更准确;
  • 开发者体验:LoRA 相关 owner 可在本地直接运行这些测试文件来做稳定性修复;VLM perf 迁到 extra-a 后可通过 label 精准触发;
  • 团队规范:unit/ 镜像树成为纯单测的规范位置,后续新增单测放置更清晰,也为更大规模的“仅 CPU 回归层”铺路。
测试覆盖转移风险 基准采样缩减 注册语义变更 无 review 讨论

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论