Prhub

#47640 [Bugfix][LoRA] Guard None group members in expand_packed_lora (partial LoRA on Qwen3.5/3.6 GatedDeltaNet)

原始 PR 作者 eilamc14 合并时间 2026-08-19 21:24 文件变更 1 提交数 10 评论 14 代码增减 +25 / -6

执行摘要

修复 expand_packed_lora 对 None 组员的崩溃

该 PR 解决 Issue #47639 报告的崩溃:当 LoRA 适配器只针对 GatedDeltaNet 打包投影组的部分成员(如 in_proj_qkv 而不含 in_proj_z)时,expand_packed_lora 解引用 b_i.shape[0] 会在 None 上抛出 AttributeError。PR body 明确写道:'This is a regression introduced by #37912, which unified the GDN input projections into a fused packed layer and added expand_packed_lora — but the new helper didn't carry over the None-tolerance that its sibling slice_lora_b and the set_lora stacking loop already have.' 在 v0.20.0 中,部分组通过 create_in_proj_qkvz 构建为独立模块,可以正常工作;从 v0.21.0 起崩溃并持续到 v0.24.0 及当前 main。

值得精读,因为它是理解 vLLM LoRA 打包层 None 容错模式的精简案例,展示了如何在融合层中通过占位符保持切片对齐。关注点:expand_packed_loraslice_lora_bset_lora 的容错一致性;以及缺少自动化测试的隐患。对于 LoRA 开发者,这个模式可以复用到其他打包投影组。

讨论亮点

PR 本身没有实质性的 review 评论,维护者 linitra24 和 jeejeelee 直接批准。讨论主要来自 Issue #47639 评论:

  • 作者多次更新版本未修复情况:> "Update : not fixed in vllm v0.25.0 , pr still relevant"、> "still not fixed in vllm0.27.1"。
  • 作者主动移除回归测试:> "I removed the test from the pr, may not be necessary, only 1 file (though a very important one) change."
  • CI 失败后作者判断"i think unrelated?",经 /ci retry 与再次 /ci run 后 Buildkite CI #84597 通过。

实现拆解

  1. 定位问题:在 vllm/lora/layers/column_parallel_linear.pyMergedColumnParallelLinearWithLoRA.expand_packed_lora 中,遍历 lora_a/lora_b 时对每个 b_i 直接调用 b_i.shape[0],当 b_iNone(未适配的组员)时抛出 AttributeError
  2. 放宽类型签名:将 lora_alora_b 及返回值的类型从 list[torch.Tensor] 改为 list[torch.Tensor | None],与同文件中的 slice_lora_bset_loraNone 容错保持一致。
  3. 增加 None 分支:在循环开头判断 b_i is None 时,推断其覆盖的切片数为 self.n_slices - start_idx(即剩余全部切片),为这些切片追加 None 占位符,然后 start_idx 累加并 continue。这保证了后续组的位置对齐,未适配切片保留基础权重。
  4. 注释明确布局假设:代码注释说明该推断依赖 in_proj_qkvz 组唯一的布局——多切片成员 in_proj_qkv 在前、可选成员 in_proj_z 在后,因此 None 成员恰好覆盖剩余切片。
  5. 测试与验证:作者最初在 PR 中包含 tests/lora/test_expand_packed_lora.py 回归测试,但在提交 ca0f6ddf 中移除,使 PR 仅含代码改动;作者在 1×H200、vLLM 0.24.0、torch 2.11.0+cu130 上用真实 Qwen3.6-27B LoRA 适配器(目标 q_proj,k_proj,v_proj,in_proj_qkv)验证了修复前后行为。最终 PR 未包含自动化测试。
文件 模块 状态 重要度
vllm/lora/layers/column_parallel_linear.py LoRA 层 modified 6.28

关键符号

expand_packed_lora

关键源码片段

vllm/lora/layers/column_parallel_linear.py core-logic

唯一变更文件,`expand_packed_lora` 是崩溃的直接位置;修复通过新增 `None` 分支恢复部分 LoRA 组支持。

# 整理自 vllm/lora/layers/column_parallel_linear.py 的 expand_packed_lora
def expand_packed_lora(
    self,
    lora_a: list[torch.Tensor | None],
    lora_b: list[torch.Tensor | None],
) -> tuple[list[torch.Tensor | None], list[torch.Tensor | None]]:
    """
    Expand packed adapter groups when they don't match n_slices.
    E.g. in_proj_qkv (covers Q+K+V) + in_proj_z.    A None group member means that member was not adapted; the slice(s)
    it covers are emitted as None placeholders so subsequent groups stay
    aligned and those slices are left at base weights.
    """
    expanded_a: list[torch.Tensor | None] = []
    expanded_b: list[torch.Tensor | None] = []
    start_idx = 0
    for a_i, b_i in zip(lora_a, lora_b):
        if b_i is None:
            # 未适配的组员:张量缺失导致无法读取行数,
            # 因此把覆盖范围推断为剩余的全部切片。
            # 该推断对唯一走此路径的融合 GDN in_proj_qkvz 组是精确的,
            # 因为多切片成员 in_proj_qkv(Q+K+V)在前,
            # 可选成员 in_proj_z 在后。
            covered = self.n_slices - start_idx
            for _ in range(covered):
                expanded_a.append(None)
                expanded_b.append(None)
            start_idx += covered
            continue
        # 确定这个 b_i 覆盖哪些输出切片。
        b_rows, cu_rows, covered = b_i.shape[0], 0, 0
        for i in range(start_idx, self.n_slices):
            cu_rows += self.output_sizes[i]
            if cu_rows == b_rows:
                covered = i - start_idx + 1
                break
        else:
            raise ValueError(
                f"Cannot determine how to split lora_b with {b_rows} rows "
                f"into {self.n_slices} slices with output sizes "
                f"{self.output_sizes} starting from index {start_idx}."
            )
        # 将 b_i 切成逐切片张量,并为每个切片复制 a_i。
        start = 0
        for j in range(covered):
            size = self.output_sizes[start_idx + j]
            expanded_b.append(b_i[start : start + size, :])
            expanded_a.append(a_i)
            start += size
        start_idx += covered
    return expanded_a, expanded_b

评论区精华

回归测试移除 测试

提交 ca0f6ddf 移除了 tests/lora/test_expand_packed_lora.py,作者认为测试“可能不必要”,希望 PR 保持精简。

结论:测试被移除,PR 仅含代码改动;维护者批准时未要求补充测试。 · 已解决

Bug 在多个版本未修复 question

作者在 Issue #47639 评论中多次更新:"not fixed in vllm v0.25.0"、"not fixed in vllm 0.26.0"、"still not fixed in vllm0.27.1",持续 @ 维护者。

结论:维护者最终批准并合并,修复随后续版本发布。 · 已解决

CI 失败与重试 other

Buildkite CI #84349 首次失败,作者认为与改动无关,执行 /ci retry 与 /ci run 后 CI #84597 通过。

结论:重试后通过,未发现与改动相关的 CI 问题。 · 已解决

风险与影响

技术风险集中在以下三点:

  1. 布局顺序假设covered = self.n_slices - start_idx 的推断依赖 in_proj_qkvz 组内 in_proj_qkv 在前、in_proj_z 在后的固定顺序。若未来新增其他打包组或调整成员顺序,且存在 None 成员,该推断可能错误地覆盖切片。代码注释已说明这一假设,但缺少运行时校验。
  2. 缺少自动化测试:作者移除了回归测试,CI 无法覆盖部分 LoRA 加载场景,未来改动容易再次引入同类回归。手动验证仅在特定硬件与模型组合(H200、Qwen3.6-27B)上完成。
  3. None 路径行为未变:正常路径的逻辑与错误提示保持不变,但类型签名放宽后,调用方传入 None 的行为从崩溃变为静默跳过,可能掩盖调用方构造 lora_b 时的其他问题。
    总体回归风险较低,因为改动仅新增分支,不影响原有非 None 流程。

影响范围集中在 LoRA 与 Qwen3.5/3.6 GatedDeltaNet 的组合场景:此前加载仅适配 in_proj_qkv(不含 in_proj_z)的适配器会直接崩溃,修复后恢复 v0.20.0 的部分组支持。对系统性能无影响(仅多一次 b_i is None 判断)。对团队而言,这是对 #37912 重构的兼容性修补,维护者需注意其依赖的布局假设,并考虑未来补充回归测试。

缺少自动化测试覆盖 依赖打包组布局顺序假设

关联 Issue

#47639 [Bug]: Partial LoRA on Qwen3.5/Qwen3.6 GatedDeltaNet (in_proj_qkv without in_proj_z) crashes in expand_packed_lora — regression from #37912

完整报告

参与讨论