执行摘要
- 一句话:LoRA 测试聚焦回归契约,方法数削减 61%
- 推荐动作:值得精读,推荐对象是对测试治理、回归守护和 LoRA 特性感兴趣的同学。重点看三点:一是
test_chunked_sgmv_cuda_graph.py 如何用「捕获时 1 段、重放时 4 段」的状态切换构造 CUDA graph 动态 segment 回归,技巧可复用于其他 CUDA graph 类测试;二是 test_lora_manager_tied_lm_head.py 用 patch.object + patch.dict(sys.modules) 在 CPU 上复现 GPU/模型加载路径的回归,极大降低成本;三是 PR body 的 admission 决策表是测试裁剪的范本,可迁移到其他测试套件。若你负责 LoRA 相关改动,建议后续改动时先跑这些契约验证。
功能与动机
PR body 明确指出:Apply .claude/rules/unit-test-admission.md to the LoRA tests so each retained case names a concrete future regression。背景是 #34070 的 broad cleanup 把一批有价值的回归测试与冗余覆盖一起删掉了,本 PR 在保留 admission 规则的前提下恢复「只聚焦契约束」的覆盖,并修剪无法证明具体回归的弱 happy path(例如 test_lora_radix_cache.py 的旧断言不证明 radix 命中)。
实现拆解
- 建立 admission 基线:对照
#34070 之前的 LoRA suite,逐一给每个文件标注 before/after/保留理由/CI 归属,作为本次裁剪的决策表。
- 恢复并重写 CUDA graph 回归(
test/registered/kernels/ops/gemm/test_chunked_sgmv_cuda_graph.py,16 方法 → 5 契约):保留并重构 #28371 的四类 CUDA graph 回归——shrink/expand 重放必须使用当前全部 segment、prepare_lora_batch 必须中和静态尾部 segment、以及 MLA 吸收后 KV 分支的 step_a_q_fwd/step_b_q_fwd/step_a_v_fwd/step_b_v_fwd 重放回归;删除了在其他文件已有覆盖的通用 kernel sweep。
- 驱逐策略测试 CPU 化:删除
test/registered/lora/test_lora_eviction_policy.py(201 行,原注册 CUDA/AMD/XPU/base-c),新建 test/registered/unit/lora/test_eviction_policy.py(5 个契约,注册 base-a-test-cpu,1s),保留 LRU-vs-FIFO 语义、候选过滤、remove 记账、非法策略拒绝、#14795 base-model-last 回归。
- OpenAI 兼容测试精简为负向契约(
test/registered/lora/test_lora_openai_compatible.py,11 方法 → 2 方法):只保留「未知 adapter 必须被拒绝而非静默落到 base model」和「LoRA 禁用时请求必须报错并提示 --enable-lora」两个负向路由契约;移除了失去 LoRA 仍保持绿色、无法识别具体回归的 happy path,并删除 AMD 注册与外部 adapter 下载依赖。
- 新增/迁移其余聚焦契约:
test_lora_manager_tied_lm_head.py 用 mock/patch 在 CPU 上直接构造 #18634 tied-lm_head 去重回归,替换原 1 个重型 GPU E2E 测试;test_lm_head_pruning.py 在 CPU 上守护 LogitsProcessor._get_pruned_states 与 get_lm_head_pruned_lens 的逐请求行数一致;test_io_struct.py 新增 embedding 请求批拆分后 lora_path/lora_id 存活与数量校验两个契约;test_embedding_lora_support.py 重建为「base vs LoRA 非平凡 + HF parity oracle」的单点 E2E 回归(nightly)。
- CI/配置配套:
test_lora_radix_cache.py 保持已删除状态;CUDA 图回归注册到 base-b-kernel-unit(1-gpu-large),embedding oracle 与 OpenAI 负向契约保留 nightly CUDA 注册,其余纯 Python 覆盖统一收敛到 CPU base-a。
关键文件:
test/registered/kernels/ops/gemm/test_chunked_sgmv_cuda_graph.py(模块 CUDA 图;类别 test;类型 test-coverage;符号 _make_batch_info, _set_segment_state, _capture, test_shrink_replay_uses_all_current_segments): 本次变更中权重最高的回归测试:把 #28371 的 CUDA graph 动态 LoRA segment 回归精炼为 5 个聚焦契约,覆盖 shrink/expand 重放、prepare_lora_batch 尾部中和、MLA 吸收后 KV 分支重放,是 LoRA 在 CUDA graph 路径上的核心守护。
test/registered/lora/test_embedding_lora_support.py(模块 嵌入支持;类别 test;类型 test-coverage;符号 TestEmbeddingLoRAParity, _hf_embeddings, _sglang_embeddings, extract_embeddings): 重建为端到端 embedding LoRA 的独立 HF parity oracle:先验证 LoRA 对 embedding 有非平凡影响(base vs LoRA 不相等),再与 Hugging Face peft 输出做相似度对比,替代原先无效的镜像断言。
test/registered/lora/test_lora_eviction_policy.py(模块 驱逐策略;类别 test;类型 deletion;符号 TestLoRAEvictionPolicy, _test_eviction_policy, test_lru_basic, test_lru_with_reuse): 旧的 12 方法策略测试整体删除,迁移并收敛为 test/registered/unit/lora/test_eviction_policy.py 的 5 个 CPU 契约,同时移除 CUDA/AMD/XPU/base-c 四处注册。
test/registered/unit/lora/test_eviction_policy.py(模块 驱逐策略;类别 test;类型 test-coverage;符号 TestLoRAEvictionPolicy, _make_policy, test_reuse_changes_lru_but_not_fifo, test_selection_skips_older_noncandidates): 驱逐策略测试的新家:纯 Python 覆盖迁移到 CPU base-a(1 秒),保留 LRU/FIFO 语义差异、候选过滤、remove 记账、非法策略拒绝和 #14795 base-model-last 回归,是测试资源优化最典型的文件。
test/registered/lora/test_lora_openai_compatible.py(模块 API 路由;类别 test;类型 test-coverage;符号 setup_class, TestLoRAOpenAICompatible, test_unknown_model_adapter_is_rejected, TestLoRADisabledError): 从 11 个 happy path 精简约到 2 个负向路由契约,移除 AMD 注册与真实 adapter 下载依赖,避免“LoRA 被静默丢弃时仍保持绿色”的无效覆盖。
test/registered/unit/lora/test_lora_manager_tied_lm_head.py(模块 LoRA 管理器;类别 test;类型 test-coverage;符号 _TiedEmbedding, _ParallelLMHead, TestTiedLMHeadLoRA, test_tied_head_gets_independent_wrapper_with_shared_base_weight): 用 mock/patch 在 CPU 上直接复现 #18634 tied lm_head 去重故障,替换原 1 个需要 GPU 与模型下载的重型 E2E 测试,是“重型测试 CPU 化”的代表作。
test/registered/unit/lora/test_lm_head_pruning.py(模块 头部裁剪;类别 test;类型 test-coverage;符号 TestLMHeadPruning, test_lora_segments_match_pruned_rows_per_request): 新增契约守护 LoRA lm_head 分段与 logits state 裁剪的逐请求行数一致,防止“总数相等但路由错位”被测试放过的场景。
test/registered/unit/managers/test_io_struct.py(模块 请求结构;类别 test;类型 test-coverage;符号 test_lora_identity_survives_batch_split, test_lora_path_count_must_match_embedding_batch): 补充 embedding 批量请求的 LoRA 结构契约:批拆分后 lora_path/lora_id 必须随子请求存活,且 lora_path 数量与 batch 不匹配时必须报错而非静默路由到 base。
关键符号:test_shrink_replay_uses_all_current_segments, test_expand_replay_uses_all_current_segments, test_prepare_batch_neutralizes_static_tail_segments, test_absorbed_kv_b_replay_uses_all_current_segments, test_hf_sglang_embedding_similarity, test_unknown_model_adapter_is_rejected, test_lora_disabled_error, test_tied_head_gets_independent_wrapper_with_shared_base_weight, test_reuse_changes_lru_but_not_fifo, test_selection_skips_older_noncandidates, test_base_model_is_evicted_only_as_last_resort, test_remove_excludes_adapter_from_future_selection, test_unknown_policy_is_rejected, test_lora_segments_match_pruned_rows_per_request, test_lora_identity_survives_batch_split, test_lora_path_count_must_match_embedding_batch, _set_segment_state, _capture
关键源码片段
test/registered/kernels/ops/gemm/test_chunked_sgmv_cuda_graph.py
本次变更中权重最高的回归测试:把 #28371 的 CUDA graph 动态 LoRA segment 回归精炼为 5 个聚焦契约,覆盖 shrink/expand 重放、prepare_lora_batch 尾部中和、MLA 吸收后 KV 分支重放,是 LoRA 在 CUDA graph 路径上的核心守护。
"""CUDA-graph 回归测试的核心构造:用「活跃/非活跃」两种 segment 状态切换来验证
CUDA graph 重放时能正确使用当前 batch 的全部 LoRA segment(对应 #28371)。"""
def _set_segment_state(batch_info, *, active):
"""在 LoRABatchInfo 中切换 LoRA segment 状态。
active=True 时构造 4 个 adapter segment(rank = 8),
active=False 时退化为单 segment(rank = 0),
用于模拟「捕获时为 1 段、重放时为 4 段」的动态变化。"""
if active:
lora_ranks = [MAX_RANK] * NUM_LORAS
weight_indices = [1, 2, 3, 4]
seg_indptr = [0, 2, 4, 6, BS]
else:
lora_ranks = [0] * NUM_LORAS
weight_indices = [0]
seg_indptr = [0, BS]
num_segments = len(weight_indices)
batch_info.lora_ranks.copy_(torch.tensor(lora_ranks, dtype=torch.int32, device="cuda"))
batch_info.weight_indices.zero_()
# 只填充前 num_segments 个有效槽位,其余保持为 0
batch_info.weight_indices[:num_segments].copy_(
torch.tensor(weight_indices, dtype=torch.int32, device="cuda")
)
batch_info.seg_indptr.fill_(seg_indptr[-1])
batch_info.seg_indptr[: num_segments + 1].copy_(
torch.tensor(seg_indptr, dtype=torch.int32, device="cuda")
)
batch_info.num_segments = num_segments
def _capture(call):
"""在独立 warmup stream 上预热 3 次后捕获 CUDA graph,返回 (graph, output)。
预热必不可少:kernel 首次运行会触发 autotune,直接捕获可能把调优路径固化进图。"""
warmup_stream = torch.cuda.Stream()
warmup_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(warmup_stream):
for _ in range(3):
call()
torch.cuda.current_stream().wait_stream(warmup_stream)
torch.cuda.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
output = call()
return graph, output
def test_shrink_replay_uses_all_current_segments():
"""一个「单 segment 捕获」的图,在重放 4 个 adapter 后必须与 eager shrink 一致。"""
_chunked_lora_shrink_kernel._clear_cache()
batch_info = _make_batch_info()
inputs = torch.randn(BS, 64, dtype=torch.float16, device="cuda")
weights = torch.randn(NUM_LORAS, MAX_RANK, 64, dtype=torch.float16, device="cuda")
# 先以 4-segment 的 eager 结果作为基准
_set_segment_state(batch_info, active=True)
expected = chunked_sgmv_lora_shrink_forward(
inputs, weights, batch_info, num_slices=1
).clone()
# 以 1-segment 状态捕获图:重放时必须重新读取 weight_indices / seg_indptr
_set_segment_state(batch_info, active=False)
graph, captured_output = _capture(
lambda: chunked_sgmv_lora_shrink_forward(inputs, weights, batch_info, num_slices=1)
)
# 恢复 4-segment 状态并重放,输出必须与 eager 基准一致
_set_segment_state(batch_info, active=True)
captured_output.zero_()
graph.replay()
torch.cuda.synchronize()
torch.testing.assert_close(
captured_output[:, :MAX_RANK],
expected[:, :MAX_RANK],
rtol=RTOL,
atol=ATOL,
)
test/registered/unit/lora/test_lora_manager_tied_lm_head.py
用 mock/patch 在 CPU 上直接复现 #18634 tied lm_head 去重故障,替换原 1 个需要 GPU 与模型下载的重型 E2E 测试,是“重型测试 CPU 化”的代表作。
"""回归 #18634:LoRA 包装与输入输出 embedding 共享对象的 lm_head 时的去重行为。
整个测试在 CPU 上运行(base-a,1 秒),通过 patch 替代 GPU 与模型下载。"""
class TestTiedLMHeadLoRA(CustomTestCase):
def test_tied_head_gets_independent_wrapper_with_shared_base_weight(self):
"""仅挂载 lm_head 的 LoRA 必须能在 tied 权重下存活:
lm_head 被换成独立包装器,但包装器内部仍与 embed_tokens 共享参数字面量。"""
model = torch.nn.Module()
tied_embedding = _TiedEmbedding()
model.embed_tokens = tied_embedding
model.lm_head = tied_embedding # 模拟 tied input/output embedding
manager = LoRAManager.__new__(LoRAManager)
manager.base_model = model
manager.base_hf_config = SimpleNamespace(num_hidden_layers=0)
manager.target_modules = {"lm_head"}
wrapped_lm_head = object()
# guard:避免真实 import 拉入大量 CUDA 依赖,仅做模块级替换
inkling_module = types.ModuleType("sglang.srt.models.inkling_common.dense_mlp")
inkling_module.InklingBatchDenseMLP = type("InklingBatchDenseMLP", (), {})
with (
patch.object(lora_manager_module, "ParallelLMHead", _ParallelLMHead),
patch.object(manager, "set_lora_module", return_value=wrapped_lm_head) as set_lora_module,
patch.dict(sys.modules, {"sglang.srt.models.inkling_common.dense_mlp": inkling_module}),
):
manager.init_lora_modules()
# lm_head 不再与 embed_tokens 是同一个对象,但两者共享底层 weight
self.assertIsNot(model.lm_head, model.embed_tokens)
self.assertIs(model.lm_head.weight, model.embed_tokens.weight)
self.assertIs(manager.lm_head_module, wrapped_lm_head)
set_lora_module.assert_called_once_with("lm_head", model.lm_head)
评论区精华
该 PR 没有产生实质 review 评论(review_comments_count = 0),合并前仅有一次 /rerun-test 触发 CI 重跑,两个 issue 评论都是命令与结果的自动化回执。真正值得关注的设计讨论全部沉淀在 PR body 的逐测试 admission 决策表里:例如对 test_lora_openai_compatible.py 明确写出「happy paths that stayed green when LoRA was dropped are removed」,即承认这批测试在 LoRA 被静默丢弃时仍然通过、属于无效守护;对 test_lora_radix_cache.py 则判定「The old assertion did not prove a radix-cache hit and did not identify a distinct future regression」。这些决策体现了「测试必须能指认具体回归」的取舍原则,是本次变更的方法论核心。
风险与影响
- 风险:
- 大面积删除既有覆盖:28 个方法被移除(包含 OpenAI 兼容的全部 happy path、LRU/FIFO 的多数组合场景、AMD/XPU 的注册)。虽然每个删除都给出了 admission 理由,但若这些 happy path 曾隐式守护过某些非预期行为,保护会丢失;特别是
test_lora_eviction_policy.py 删除后,驱逐策略在 CUDA/AMD/XPU 平台上的行为不再有直接覆盖。
- 新增 CUDA graph 测试的 flaky 风险:
test_chunked_sgmv_cuda_graph.py 涉及独立 warmup stream、CUDA graph 捕获与重放、_clear_cache() 与 torch.cuda.synchronize(),在 1-gpu-large 上运行,stream 时序敏感,存在偶发失败可能(PR 内已有一次 /rerun-test)。
- 外部依赖:
test_embedding_lora_support.py 需要从 Hugging Face 下载 meta-llama/Llama-2-7b-hf 与 LoRA adapter,且要动态加载 peft/transformers,对网络与显存要求高(nightly 150s),存在外部源不可用或版本漂移导致误报的风险。
- 覆盖转移而非新增:
test_lora_radix_cache.py 保持删除,意味着「LoRA 与 radix cache 命中」这一组合路径在当前测试体系中没有任何守卫,后续若该交互回归将无法被 CI 捕捉。
- 无任何源码/配置改动,不影响线上推理路径,回归风险仅限测试面。
- 影响:对用户:零运行时影响,模型输出、API 行为、性能均不变。对CI 系统:净减少约 28 个测试方法与 4 类跨平台注册(AMD、XPU、base-c、nightly CUDA 中的部分项),驱逐策略与 tied-lm-head 从 GPU/模型下载依赖降为 CPU 1 秒测试,CI 资源消耗显著下降。对团队:这是一次测试治理范式的落地实践——以 .claude/rules/unit-test-admission.md 为标尺,确立了「每个用例必须对应一个可指认的回归」的评审标准,对后续新测试有直接示范作用。对LoRA 功能线:回归保护更集中但覆盖范围变窄,未来修改 eviction policy、OpenAI 路由、CUDA graph 段逻辑时需要依赖这批聚焦契约。影响程度:中等,属于工程效率与质量保障层面的改进。
- 风险标记:大面积删除既有测试覆盖, 新增 CUDA 图测试 flaky 风险, 外部模型下载依赖, 跨平台注册移除
关联脉络
- PR #34070 (标题未在材料中提供): PR body 明确引用该 PR:其 broad cleanup 删除了大量冗余覆盖的同时误删了若干有价值的 LoRA 回归测试,本 PR 正是在 admission 规则下恢复其中值得保留的聚焦契约。
参与讨论