[Bugfix][LoRA] Guard None group members in expand_packed_lora (partial LoRA on Qwen3.5/3.6 GatedDeltaNet) (#47640)
Signed-off-by: Eilam C <eilamc14@gmail.com> Co-authored-by: Jee Jee Li <pandaleefree@gmail.com>
Eilam committed
Aug 19, 2026 at 16:24 UTC
c2e7242ab7287cc9e2bdbea5901c560fc2d17bff
1 file changed
+25
-6
vllm/lora/layers/column_parallel_linear.py
+25
-6
@@ -265,17 +265,36 @@ class MergedColumnParallelLinearWithLoRA(ColumnParallelLinearWithLoRA):
265
266
def expand_packed_lora(
267
self,
268
- lora_a: list[torch.Tensor],
269
- lora_b: list[torch.Tensor],
270
- ) -> tuple[list[torch.Tensor], list[torch.Tensor]]:
268
+ lora_a: list[torch.Tensor | None],
269
+ lora_b: list[torch.Tensor | None],
270
+ ) -> tuple[list[torch.Tensor | None], list[torch.Tensor | None]]:
271
"""
272
Expand packed adapter groups when they don't match n_slices.
273
- E.g. in_proj_qkv (covers Q+K+V) + in_proj_z
273
+ E.g. in_proj_qkv (covers Q+K+V) + in_proj_z.
274
+
275
+ A None group member means that member was not adapted; the slice(s)
276
+ it covers are emitted as None placeholders so subsequent groups stay
277
+ aligned and those slices are left at base weights. This matches the
278
+ None-tolerance already present in slice_lora_b() and the set_lora()
279
+ stacking loop.
280
"""
275
- expanded_a: list[torch.Tensor] = []
276
- expanded_b: list[torch.Tensor] = []
281
+ expanded_a: list[torch.Tensor | None] = []
282
+ expanded_b: list[torch.Tensor | None] = []
283
start_idx = 0
284
for a_i, b_i in zip(lora_a, lora_b):
285
+ if b_i is None:
286
+ # Unadapted group member: its row count is unknown (the tensor
287
+ # is missing), so infer its coverage as the remaining slices.
288
+ # This is exact for the only layout that reaches this path,
289
+ # the fused GDN in_proj_qkvz group, whose sole multi-slice
290
+ # member (in_proj_qkv, Q+K+V) leads and whose only optional
291
+ # member (in_proj_z) trails.
292
+ covered = self.n_slices - start_idx
293
+ for _ in range(covered):
294
+ expanded_a.append(None)
295
+ expanded_b.append(None)
296
+ start_idx += covered
297
+ continue
298
# Determine which output slices this b_i covers.
299
b_rows, cu_rows, covered = b_i.shape[0], 0, 0
300
for i in range(start_idx, self.n_slices):