执行摘要
- 一句话:共享 VLM engine,精简 H100/H200 CI 测试矩阵
- 推荐动作:值得负责 CI 与长测试套件的工程师精读。重点看三个设计决策:一是
_start_engine + tearDownClass 的组合如何用类级共享把 engine 启动成本摊薄,同时借 CustomTestCase 保证异常路径下的清理;二是 stage="extra-a" 的 label-gated 拆分方式与共享 fixture 常量的配套;三是“保持矩阵对角、skip 替代删除”的覆盖维护思路。普通功能开发者可略读,仅需知道 per-commit 测试结构有了变化。
功能与动机
PR body 明确说明了目标与边界:"Trims per-commit wall time on the 1-gpu-h100, 4-gpu-h100 and 8-gpu-h200 pools by sharing server/engine launches, keeping matrix diagonals, and shrinking oversized eval sample counts. No accuracy threshold is changed." per-commit 测试在这些主力池上耗时过长,VLM 测试每个用例都单独启动 Engine、Qwen3-Omni 等 30B 级模型服务启动代价很高,而部分 GSM8K 评估跑数千题只为一个二值结论。作者选择“共享启动 + 分层门控 + 压缩样本”来削减成本,而不是删覆盖或放宽准确率。
实现拆解
实现按 5 步拆解:
-
VLM 输入格式测试:每类共享一个 engine(核心改动)。test/registered/vlm/test_vlm_input_format.py 中把 Engine 创建从实例级 setUp 上移到类级 setUpClass 的新方法 _start_engine,清理从 tearDown 改为 tearDownClass;测试基类从 unittest.IsolatedAsyncioTestCase 换成 CustomTestCase,利用其“setUpClass 抛异常也执行 tearDownClass”的语义做引擎兜底回收。三个用例 test_accepts_image、test_accepts_precomputed_embeddings、test_accepts_processor_output 从 async_generate 改为同步 generate,原因是 tokenizer manager 会把 handle_loop 钉死在第一个请求所在的 loop 上,逐测试开 loop 会困死后续请求。est_time 从 747s 降到 300s,是本 PR 单项收益最大的改动。
-
昂贵用例迁入 label-gated extra 阶段。新增 test/registered/sessions/test_streaming_session_swa_extra.py 与 test/registered/vlm/test_vision_openai_server_extra.py,均注册到 stage="extra-a",只有打 run-ci-extra 标签才执行;per-commit 侧同步瘦身:test_streaming_session_swa.py 移除 TestStreamingSessionSWARetractMixedChunk(est_time 519→390),test_vision_openai_server_a.py 移除 TestQwen3OmniServer(780→560)并为 --context-length 300 补校准注释。支撑改动是 python/sglang/test/server_fixtures/streaming_session_fixture.py 新增共享常量 SWA_MODEL、SWA_COMMON_ARGS,让 base 与 extra 两侧从同一处导入参数。
-
压缩评估样本量。test/registered/models_e2e/test_minimax_m25_basic.py 的 GSM8K num_questions/parallel 从 1400 降到 200;test/registered/models_e2e/test_mimo_v2_flash.py 删除 gsm8k_num_questions = 1319 等覆盖改吃 kit 默认值(est_time 350→200)。所有准确率阈值原样保留,样本量下降约 7 倍会直接放大门控统计方差。
-
保留矩阵对角,skip 替代删除。test/registered/models_e2e/test_qwen3_next_models_mtp.py 中 TestQwen3NextMTPV2 加 @unittest.skip 并标注 manual-only,覆盖映射为:topk=1 由 TestQwen3NextMTPLazyV2 覆盖、extra_buffer 策略由 TestQwen3NextMTPTopk 覆盖,该组合属对角线冗余。提交 d8c27367 曾尝试恢复 dsv32 tp/dp 测试,后因 #29686 要求删除对应文件而放弃。
-
目录整理与 est_time 重标定。一批 server 端模型测试从 test/registered/models/ 迁到 test/registered/models_e2e/(test_inkling.py、test_generation_models.py、test_vlm_models.py 等),其中 test_inkling.py est_time 600→250;test_breakable_cuda_graph.py、test_post_capture_kv_sizing.py、test_lora_overlap_loading.py 等仅做 est_time 重标定。
关键文件:
test/registered/vlm/test_vlm_input_format.py(模块 多模态输入;类别 test;类型 test-coverage;符号 setUp, _start_engine, tearDownClass, test_accepts_image): 核心改造:engine 从 per-test 改为 per-class 共享、async 改 sync、基类切换为 CustomTestCase,est_time 从 747s 降到 300s,是本 PR 收益最大的单项,也是后续共享 engine 模式的范本。
test/registered/vlm/test_vision_openai_server_extra.py(模块 视觉服务;类别 test;类型 test-coverage;符号 TestQwen3OmniServer): 新增 label-gated extra 文件,承载被移出 per-commit 的 TestQwen3OmniServer,展示了 stage="extra-a" 的注册模式与 mixin 隔离写法。
test/registered/sessions/test_streaming_session_swa_extra.py(模块 流式会话;类别 test;类型 test-coverage;符号 TestStreamingSessionSWARetractMixedChunk): 新增 label-gated extra 文件,SWA mixed-chunk retract 变体从 per-commit 迁入,与 base 文件共享 fixture 常量。
python/sglang/test/server_fixtures/streaming_session_fixture.py(模块 会话夹具;类别 test;类型 test-coverage;符号 SWA_MODEL, SWA_COMMON_ARGS): SWA_MODEL 与 SWA_COMMON_ARGS 上提为共享常量,供 per-commit 与 extra 两侧导入,是测试文件拆分的配套支撑,避免参数重复定义漂移。
test/registered/vlm/test_vision_openai_server_a.py(模块 视觉服务;类别 test;类型 test-coverage;符号 TestQwen2VLContextLengthServer): 删除 TestQwen3OmniServer、重标 est_time(780→560),并为 context-length 300 校准补注释;提交历史显示此处曾尝试换 llava-0.5b 又因 warmup 越界回退。
test/registered/models_e2e/test_qwen3_next_models_mtp.py(模块 MTP 测试;类别 test;类型 test-coverage;符号 TestQwen3NextMTPV2): 用 @unittest.skip 而非删除来保留矩阵对角条目 TestQwen3NextMTPV2,并写明覆盖映射与 manual-only 定位,est_time 430→290。
test/registered/models_e2e/test_minimax_m25_basic.py(模块 MiniMax 测试;类别 test;类型 test-coverage;符号 test_a_gsm8k): GSM8K 样本量从 1400 降到 200(parallel 同步下调),是阈值不变前提下统计风险最集中的样本削减。
test/registered/sessions/test_streaming_session_swa.py(模块 流式会话;类别 test;类型 test-coverage;符号 TestStreamingSessionSWA, TestStreamingSessionSWARetractLargePage, TestStreamingSessionSWAAbortLeakRepro): per-commit 文件瘦身:移除 mixed-chunk 变体、改从 fixture 导入共享常量,est_time 519→390。
关键符号:_start_engine, tearDownClass, setUpClass, test_accepts_image, test_accepts_precomputed_embeddings, test_accepts_processor_output, TestStreamingSessionSWARetractMixedChunk, TestQwen3OmniServer
关键源码片段
test/registered/vlm/test_vlm_input_format.py
核心改造:engine 从 per-test 改为 per-class 共享、async 改 sync、基类切换为 CustomTestCase,est_time 从 747s 降到 300s,是本 PR 收益最大的单项,也是后续共享 engine 模式的范本。
class VLMInputTestBase:
model_path = None
chat_template = None
processor = None
visual = None # 可调用对象,用于预计算 embedding
engine = None # 类级共享:每个测试类只启动一个 engine
@classmethod
def setUpClass(cls):
assert cls.model_path is not None, "Set model_path in subclass"
assert cls.chat_template is not None, "Set chat_template in subclass"
cls.image_urls = [IMAGE_MAN_IRONING_URL, IMAGE_SGL_LOGO_URL]
if _is_cuda:
cls.device = torch.device("cuda")
elif _is_xpu:
cls.device = torch.device("xpu")
else:
cls.device = torch.device("cpu")
cls.main_image = []
for image_url in cls.image_urls:
response = requests.get(image_url)
cls.main_image.append(Image.open(BytesIO(response.content)))
cls.processor = AutoProcessor.from_pretrained(
cls.model_path, trust_remote_code=True, use_fast=True
)
_fix_added_tokens_encoding(cls.processor.tokenizer)
cls._init_visual()
cls._start_engine() # engine 启动从每个测试一次改为每类一次
@classmethod
def _start_engine(cls):
# 一个类只启动一个 engine,所有测试顺序读它。这里刻意用同步
# generate 而非 async:tokenizer manager 会把 handle_loop 钉死在
# 第一个请求所在的 loop 上,若每个测试各自开 loop 就会困死。
cls.engine = Engine(
model_path=cls.model_path,
chat_template=cls.chat_template,
device=cls.device.type,
mem_fraction_static=0.8,
enable_multimodal=True,
disable_cuda_graph=True,
trust_remote_code=True,
)
@classmethod
def tearDownClass(cls):
# CustomTestCase 保证即使 setUpClass 抛异常也会走 tearDownClass,
# 避免 engine 泄漏在 CI 机器上长期驻留。
if cls.engine is not None:
cls.engine.shutdown()
cls.engine = None
def test_accepts_image(self):
req = self.get_completion_request()
conv = generate_chat_conv(req, template_name=self.chat_template)
text = conv.get_prompt()
output = self.engine.generate(
prompt=text,
image_data=self.main_image,
sampling_params=dict(temperature=0.0, max_new_tokens=512),
)
self.verify_response(output)
test/registered/vlm/test_vision_openai_server_extra.py
新增 label-gated extra 文件,承载被移出 per-commit 的 TestQwen3OmniServer,展示了 stage="extra-a" 的注册模式与 mixin 隔离写法。
"""Label-gated vision/omni server launches too expensive for the per-commit
budget; the per-commit set lives in test_vision_openai_server_a.py."""
import unittest
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.vlm_utils import OmniOpenAITestMixin
# 注册到 extra-a 阶段:只有显式打 run-ci-extra 标签的 PR 才执行,
# 不再占用 per-commit 预算。
register_cuda_ci(est_time=180, stage="extra-a", runner_config="1-gpu-large")
class TestQwen3OmniServer(OmniOpenAITestMixin):
model = "Qwen/Qwen3-Omni-30B-A3B-Instruct"
extra_args = [ # H100 显存紧张的 workaround
"--mem-fraction-static=0.90",
"--disable-cuda-graph",
"--disable-fast-image-processor",
"--grammar-backend=none",
]
# 删除 mixin,避免它被当作独立测试类被收集执行。
del OmniOpenAITestMixin
if __name__ == "__main__":
unittest.main()
python/sglang/test/server_fixtures/streaming_session_fixture.py
SWA_MODEL 与 SWA_COMMON_ARGS 上提为共享常量,供 per-commit 与 extra 两侧导入,是测试文件拆分的配套支撑,避免参数重复定义漂移。
# 共享的 SWA 测试常量:per-commit 与 extra 两个 streaming-session 测试文件
# 都从这里导入,避免相同参数在多个文件里重复定义导致漂移。
SWA_MODEL = "openai/gpt-oss-20b"
# gpt-oss-20b 的通用启动参数,与 TestSessionLatency/TestSWARadixCacheKL 对齐。
SWA_COMMON_ARGS = [
"--mem-fraction-static",
"0.70",
"--cuda-graph-backend-prefill=disabled",
]
ABORT_REPRO_CONTEXT_LEN = 512
ABORT_REPRO_PAGE_SIZE = 256
ABORT_REPRO_GEN_LEN = 4
评论区精华
本 PR 没有任何 reviewer 评论或 review thread,作者自审自合并;真正的“评审”发生在 14 个 commit 与 bot 重跑里。值得注意的设计取舍:
- 提交 4e905f67 "revert context-length launch to qwen2-vl; llava-0.5b overruns warmup":上下文长度用例曾想换更便宜的 llava-0.5b 压时间,实测 warmup 越界后回退到 qwen2-vl,并在代码里留下“必须高于 warmup 图展开长度、低于测试图展开长度”的校准注释。
- 提交 b38998f "restore the qwen3-next lazy extra buffer matrix; #31991 enabled the alloc-fail legs on purpose":作者明确 #31991 是有意启用 alloc-fail 测试腿,裁剪矩阵时恢复并保留其意图。
- 提交 81d967e "keep MTPV2 and GLM-4.1V as skipped classes instead of deleting them":skip 而非删除,保留本地手动回归能力。
-
CI bot 的重跑名单覆盖了本 PR 改动的全部 11 个测试文件(含 vlm input format、vision openai server a/extra、streaming session swa/extra、breakable cuda graph、post capture kv sizing、inkling、qwen3 next mtp、mimo v2 flash、minimax m25 basic),全部通过。
-
无实质 review 讨论,CI 重跑覆盖全部改动文件 (other): 8 个 1-gpu-h100 与 1 个 4-gpu-h100 重跑全部通过,确认裁剪后的测试行为稳定;设计权衡主要由 14 个 commit 承载而非评审对话。
风险与影响
-
风险:技术风险集中在以下几点:
-
测试隔离性下降:test_vlm_input_format.py 改为类级共享 engine 后,若某个用例污染 engine 状态(异常中断、残留状态),会级联影响同类后续用例;目前依赖“所有测试只读”的假设,缺少显式状态校验。
- 样本量缩水的统计风险:
test_minimax_m25_basic.py 的 GSM8K 从 1400 题降到 200 题而阈值不变,二项分布置信区间显著变宽,稳定过阈的模型可能偶发抖动失败,边缘回归被掩盖的概率上升;TestQwen3NextMTPV2 从 CI 移除后该组合只能靠相邻两条腿间接覆盖。
- extra 门控的覆盖依赖:Qwen3-Omni 服务与 SWA mixed-chunk retract 只跑
run-ci-extra,若流程漏打该标签,这些配置的回归会延迟到手动验证才暴露。
- 同步化依赖实现细节:async→sync 的正确性建立在“tokenizer manager 把 handle_loop 钉死在首个请求 loop”这一实现行为上,未来若重构事件循环,测试可能集体困死。
- 目录迁移的引用失效风险:多个文件从
test/registered/models/ 迁到 models_e2e/,若外部脚本或 CI 配置仍引用旧路径会静默漏跑。
-
影响:影响范围:
-
对贡献者:1-gpu-h100、4-gpu-h100、8-gpu-h200 三个 per-commit 主池墙钟时间显著下降,反馈循环加快,是全仓库贡献者都能感知的改进。
- 对覆盖策略:形成“per-commit 轻覆盖 + 对角线”与“extra 重覆盖”的两层结构,回归检测质量开始依赖
run-ci-extra 标签纪律。
- 对团队规范:引入了“共享 engine 需配合同步 generate”“skip 而非删除”“共享常量入 fixture”等 CI 测试编写约定,后续新增长测试应遵循同一模式。
- 无产品运行时影响:所有改动均为测试代码与 CI 注册信息,不涉及模型推理路径。
- 风险标记:共享 engine 降低测试隔离性, GSM8K 样本量缩减约 7 倍, per-commit 覆盖依赖 extra 标签纪律, async 转 sync 依赖事件循环钉死假设, 测试目录迁移可能使旧路径引用失效
关联脉络
- PR #33929 [misc] Remove break-graph debug log; reclaim pid-less /dev/shm leaks in CI: 同属 per-commit CI 稳定与开销治理:清理无主 /dev/shm 泄漏与删除调试日志,与本次墙钟裁剪共同改善 CI 超时与资源雪崩问题。
- PR #33904 [CI] Refresh the CPU HF cache base only on main-ref runs: 同属 CI 预算优化线(缓存刷新条件收紧),与本次启动矩阵裁剪是同一治理方向的相邻改动。
参与讨论