执行摘要
- 一句话:统一 Qwen3.5 LoRA 前向路径
- 推荐动作:建议重点关注 LoRA 场景下的回归测试,特别是 TP>1 的配置。设计决策 '将复杂性转移到 LoRA 层' 值得在其他类似模型中复用。
功能与动机
PR body: 'There're 2 forwarding code path for Qwen3.5 after #36976. This PR unifies them by adapting the LoRA layer implementation.' 目的是消除因 LoRA 启用与否导致的分叉逻辑,降低维护复杂度。
实现拆解
- 移除模型中的 LoRA 条件分支:在
Qwen3_5DecoderLayer 和 GatedDeltaNetAttention 中去掉 create_in_proj_qkvz 参数,始终创建融合的 in_proj_qkvz 投影。Qwen3_5Model 移除 enable_lora 标志。同时修改 load_weights,统一从 in_proj_qkvz 张量中加载并映射到 in_proj_qkv(Q、K、V)和 in_proj_z。
- 引入
expand_packed_lora 方法:在 MergedColumnParallelLinearWithLoRA 类中新增该方法,当 set_lora 接收的 lora_b 数量与 n_slices 不匹配时(例如将 in_proj_qkv(覆盖3个slice)和 in_proj_z(覆盖1个slice)作为2个适配器传递给4-slice层),自动根据 output_sizes 拆分并复制 lora_a,使每 slice 都有对应的适配器。
- 更新
create_dummy_lora 的 packed_modules_mapping 处理:在 ModelManager.create_dummy_lora 中,通过 getattr(module, 'n_slices', ...) 获取真实的 slice 数,当与 packed_modules_mapping 中的替代名数量不一致时,使用 slice_i 作为命名,确保 dummy LoRA 权重创建正确。
- 移除
Qwen3_5ForCausalLMBase 和 Qwen3_5ForConditionalGeneration 中的 packed_modules_mapping 运行时修改:不再根据 LoRA 启用与否动态调整映射,保持映射一致。
关键文件:
vllm/model_executor/models/qwen3_5.py(模块 模型定义;类别 source;类型 data-contract;符号 update_packed_mapping): 核心模型文件,移除 LoRA 条件分支,统一 load_weights 映射路径。
vllm/lora/layers/column_parallel_linear.py(模块 LoRA层;类别 source;类型 core-logic;符号 expand_packed_lora): 新增 expand_packed_lora 方法,实现打包适配器展开的核心逻辑。
vllm/model_executor/layers/mamba/gdn_linear_attn.py(模块 注意力层;类别 source;类型 data-contract): 移除 create_in_proj_qkvz 参数,始终使用融合投影,简化前向路径。
vllm/lora/model_manager.py(模块 LoRA管理;类别 source;类型 data-contract): 适配新架构,通过 n_slices 属性修正 create_dummy_lora 的映射逻辑。
关键符号:expand_packed_lora
关键源码片段
vllm/model_executor/models/qwen3_5.py
核心模型文件,移除 LoRA 条件分支,统一 load_weights 映射路径。
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
# Unified stacked params mapping - no dynamic branching based on LoRA
stacked_params_mapping = [
# GDN: in_proj_qkvz -> in_proj_qkv (Q,K,V) and in_proj_z
('in_proj_qkvz', 'in_proj_qkv', (0, 1, 2)),
('in_proj_qkvz', 'in_proj_z', 3),
# self attention
('qkv_proj', 'q_proj', 'q'),
('qkv_proj', 'k_proj', 'k'),
('qkv_proj', 'v_proj', 'v'),
('in_proj_ba', 'in_proj_b', 0),
('in_proj_ba', 'in_proj_a', 1),
]
# Previously, when self.enable_lora was True, extra mappings for
# in_proj_qkv and in_proj_z were added; now they are fixed.
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
for name, loaded_weight in weights:
# ... expert weights processing omitted for brevity ...
param = params_dict[name]
weight_loader = param.weight_loader
# Always pass shard_id (previously omitted for in_proj_z in LoRA case)
weight_loader(param, loaded_weight, shard_id)
loaded_params.add(name)
return loaded_params
vllm/lora/layers/column_parallel_linear.py
新增 expand_packed_lora 方法,实现打包适配器展开的核心逻辑。
def expand_packed_lora(
self,
lora_a: list[torch.Tensor],
lora_b: list[torch.Tensor],
) -> tuple[list[torch.Tensor], list[torch.Tensor]]:
'''
Expand packed adapter groups when they don't match n_slices.
E.g. in_proj_qkv (covers Q+K+V) + in_proj_z
'''
expanded_a: list[torch.Tensor] = []
expanded_b: list[torch.Tensor] = []
start_idx = 0
for a_i, b_i in zip(lora_a, lora_b):
# Determine which output slices this b_i covers.
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}.'
)
# Split b_i into per-slice tensors and replicate a_i for each.
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
评论区精华
Reviewer gemini-code-assist[bot] 指出 model_manager.py 中的 'HACK' 注释值得担忧,建议替换为详细解释或更健壮的方案。作者未在评论中回应,但该 PR 已被批准合并。未解决的疑虑是:该 HACK 是否会在未来维护中引入问题。
- model_manager.py 中的 HACK 注释 (design): 作者未回应,PR 仍被合并。HACK 未解决。
风险与影响
- 风险:主要风险是回归:统一路径后,未提供测试文件变更(仅描述了测试计划),可能遗漏 LoRA 启用场景下的行为差异。特别是
expand_packed_lora 对 output_sizes 的依赖需要确保对齐。另外,model_manager.py 中的 HACK 代码可能在其他打包模块上表现异常。
- 影响:影响 Qwen3.5 和 Qwen3.5-MoE 模型用户,尤其是使用 LoRA 微调的部署。变更后模型代码路径单一,逻辑更简洁,但需要验证 LoRA 功能正确。系统其他部分不受影响。
- 风险标记:核心路径变更, 缺少测试覆盖, HACK代码, 依赖 output_sizes 对齐
关联脉络
- PR #36976 [Feature] Qwen3.5 LoRA support (introduces two forwarding paths): 该 PR 引入了因 LoRA 分裂的两条前向路径,本 PR 在其基础上进行统一。
参与讨论