From b016f461be57949f4d0749ec04caaee1c16c5b60 Mon Sep 17 00:00:00 2001 From: Swigler <124839156+Swigler@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:30:21 +0300 Subject: [PATCH] convert : fix LoRA conversion crash for Qwen3.5 V-head reorder (#28324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * convert: fix LoRA conversion crash for Qwen3.5 V-head reorder _reorder_v_heads does reshape+permute+reshape to reorder V heads from grouped to tiled order. LoraTorchTensor.reshape() cannot split its row dimension (A matrix), so converting Qwen3.5 LoRA adapters that target out_proj crashes with NotImplementedError. Fix: detect LoRA tensors and apply the equivalent index permutation directly — column reorder (dim=last) permutes A's columns, row reorder (dim=0) permutes B's rows. This is mathematically identical: (B @ A)[:, perm] == B @ A[:, perm] (B @ A)[perm, :] == B[perm, :] @ A Verified: both paths produce exactly zero diff against the full-tensor reorder on random (rank=32, 4096×4096) matrices. Fixes #21125 Signed-off-by: Radu Swigler * convert: add ty: ignore for hasattr-guarded LoRA call Assisted-By: Claude Opus 4.6 * fix comment * nowrap --------- Signed-off-by: Radu Swigler Co-authored-by: Radu Swigler Co-authored-by: Sigbjørn Skjæret Assisted-by: Claude Opus 4.6 --- conversion/qwen.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/conversion/qwen.py b/conversion/qwen.py index 6b87ff25e0..95a41fb3ad 100644 --- a/conversion/qwen.py +++ b/conversion/qwen.py @@ -469,6 +469,21 @@ class _LinearAttentionVReorderBase(Qwen3NextModel): shape = list(tensor.shape) if dim < 0: dim += len(shape) + + # LoRA tensors (W ≈ B @ A) cannot reshape their row dimension. + # Instead, build a permutation index and apply it to A (column reorder) or B (row reorder) directly. + if hasattr(tensor, 'get_lora_A_B'): + n = shape[dim] + idx = torch.arange(n).reshape(num_k_heads, num_v_per_k, head_dim) + idx = idx.permute(1, 0, 2).contiguous().reshape(n) + lora_A, lora_B = tensor.get_lora_A_B() # ty: ignore[call-non-callable] + if dim == len(shape) - 1: + return type(tensor)(lora_A[:, idx], lora_B) + elif dim == 0: + return type(tensor)(lora_A, lora_B[idx]) + else: + raise NotImplementedError(f"_reorder_v_heads on dim={dim} not supported for LoRA tensors") + new_shape = shape[:dim] + [num_k_heads, num_v_per_k, head_dim] + shape[dim + 1:] tensor = tensor.reshape(*new_shape) perm = list(range(len(new_shape)))