ITADN

[Bug]: LoRA set_lora on merged QKV/gate_up raises IndexError when lora_a is a single tensor

#51409OpenECMGit 创建于 11 天前
E
ECMGitcommented
### Your current environment Reproduced on the **official upstream nightly image**, `vllm/vllm-openai:nightly-aarch64`. <details> <summary>Output of python3 -m vllm.collect_env</summary> ``` ============================== System Info ============================== OS : Ubuntu 22.04.5 LTS (aarch64) GCC version : (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0 Libc version : glibc-2.35 ============================== PyTorch Info ============================== PyTorch version : 2.13.0+cu130 Is debug build : False CUDA used to build PyTorch : 13.0 ============================== Python Environment ============================== Python version : 3.12.13 (main, Mar 4 2026, 09:23:07) [GCC 11.4.0] (64-bit runtime) Python platform : Linux-6.17.0-1014-nvidia-64k-aarch64-with-glibc2.35 ============================== CUDA / GPU Info ============================== Is CUDA available : True CUDA runtime version : 13.0.88 GPU models and configuration : GPU 0: NVIDIA GB300 GPU 1: NVIDIA GB300 GPU 2: NVIDIA GB300 GPU 3: NVIDIA GB300 Nvidia driver version : 580.159.04 ============================== Versions of relevant libraries ============================== [pip3] torch==2.13.0+cu130 [pip3] torchaudio==2.11.0+cu130 [pip3] torchvision==0.28.0+cu130 [pip3] transformers==5.14.1 [pip3] triton==3.7.1 [pip3] nvidia-nccl-cu13==2.30.7 ============================== vLLM Info ============================== vLLM Version : 0.26.1rc1.dev457+gc810e5ee9 (git sha: c810e5ee9) vLLM Build Flags: CUDA Archs: 8.0 8.7 8.9 9.0 10.0 11.0 12.0; ROCm: Disabled; XPU: Disabled ============================== Environment Variables ============================== VLLM_IMAGE_TAG=vllm/vllm-openai:nightly-c810e5ee9976ad86b81d1277b53e76d0ee639414 VLLM_BUILD_COMMIT=c810e5ee9976ad86b81d1277b53e76d0ee639414 VLLM_BUILD_URL=https://buildkite.com/vllm/release-v2/builds/4838 VLLM_USAGE_SOURCE=production-docker-image CUDA_VERSION=13.0.2 TORCH_CUDA_ARCH_LIST=8.0 8.7 8.9 9.0 10.0 11.0 12.0 ``` </details> The affected code is unchanged on `main` as of `448344c0e2`. ### 🐛 Describe the bug `MergedColumnParallelLinearWithLoRA.set_lora()` declares ```python lora_a: torch.Tensor | list[torch.Tensor] ``` but only normalizes `lora_b`. A single 2-D `lora_a` of shape `[rank, input_size]` is passed through unexpanded, so `lora_a[i]` yields a 1-D **row** instead of the i-th slice's matrix. This raises `IndexError` on any packed layer — fused QKV (3 slices) or gate_up (2 slices). `vllm/lora/layers/column_parallel_linear.py`: ```python def set_lora(self, index, lora_a: torch.Tensor | list[torch.Tensor], # a single tensor IS a declared input lora_b: torch.Tensor | list[torch.Tensor]): self.reset_lora(index) if isinstance(lora_b, list) and len(lora_b) != self.n_slices: # only lora_b is normalized lora_a, lora_b = self.expand_packed_lora(lora_a, lora_b) # nothing expands a single-tensor lora_a into a per-slice list if self.tp_size > 1: lora_a = self.slice_lora_a(lora_a) for i in range(self.n_slices): if (lora_a_i := lora_a[i]) is not None: self.lora_a_stacked[i][index, 0, : lora_a_i.shape[0], : lora_a_i.shape[1]] ``` Confirmed directly against the shipped nightly binary: ``` $ python3 -c "import inspect; from vllm.lora.layers.column_parallel_linear import \ MergedColumnParallelLinearWithLoRA as M; \ print('NORMALIZES_LORA_A:', 'isinstance(lora_a, torch.Tensor)' in inspect.getsource(M.set_lora))" NORMALIZES_LORA_A: False ``` It breaks at four distinct sites: | Combination | Function | Error | |---|---|---| | fused QKV, `fully_sharded_loras=True` | `MergedQKVParallelLinearWithShardedLoRA.slice_lora_a` | `too many indices for tensor of dimension 1` | | gate_up, `fully_sharded_loras=True` | `MergedColumnParallelLinearWithShardedLoRA.slice_lora_a` | `too many indices for tensor of dimension 1` | | fused QKV, `fully_sharded_loras=False` | `set_lora` copy loop | `tuple index out of range` | | gate_up, `fully_sharded_loras=False` | `set_lora` copy loop | `tuple index out of range` | The two `slice_lora_a` sites sit behind `if self.tp_size > 1`. The two copy-loop sites do **not** — those reproduce at TP=1 as well. ### Why a single tensor is a legitimate input It is a declared input, not caller misuse. Only a *fused* source produces one A — e.g. Megatron, which stores QKV as a single linear. An ordinary PEFT checkpoint stores q/k/v separately and the loader assembles a list, which is why this is not seen in normal serving. Such weights reach `set_lora` when an RL trainer pushes them **in-memory** (`update_weights_from_ipc -> add_lora -> set_lora`), bypassing the loader entirely. This is how we hit it: RL post-training (verl + vLLM + Megatron) with LoRA and TP>1 raises `IndexError` on the first weight sync, the rollout worker dies, and the training run aborts. vLLM already treats `lora_a` as shared across slices elsewhere — `expand_packed_lora()` does `expanded_a.append(a_i)`, with the comment *"Split b_i into per-slice tensors and replicate a_i for each"*. And `MergedColumnParallelLinearVariableSliceWithLoRA.set_lora()` in the same file already contains exactly the missing normalization, under the comment *"Override to handle single tensor weights"*. Only the bare-tensor entry point on the base class was missed. ### Scope Adapters loaded **from disk** cannot reach this. I tested it: `worker_manager._load_adapter()` builds `expected_lora_modules` by expanding packed modules into their constituents (`q_proj`/`k_proj`/`v_proj`), so a checkpoint targeting the fused name (`qkv_proj`) is rejected by `check_unexpected_modules()` with a clean `ValueError` before reaching `set_lora`. The in-memory weight-push path is the only route. ### Reproduce 2 GPUs, no model and no kernel — finishes in seconds: ```python # builds QKVParallelLinear / MergedColumnParallelLinear + the LoRA wrapper, # then calls set_lora(0, lora_a, lora_b) with lora_a as ONE 2-D tensor torchrun --nproc_per_node=2 repro.py ``` Running a 2x2x2 matrix — {fused QKV, gate_up} x {`fully_sharded_loras` True, False} x {`lora_a` single tensor, per-slice list} — on the nightly image above: ``` vllm 0.26.1rc1.dev457+gc810e5ee9 tp=2 [FAIL] qkv fully_sharded=True lora_a=single-tensor File ".../vllm/lora/layers/column_parallel_linear.py", line 317, in set_lora File ".../vllm/lora/layers/column_parallel_linear.py", line 639, in slice_lora_a IndexError: too many indices for tensor of dimension 1 [FAIL] qkv fully_sharded=False lora_a=single-tensor File ".../vllm/lora/layers/column_parallel_linear.py", line 323, in set_lora IndexError: tuple index out of range [FAIL] gate_up fully_sharded=True lora_a=single-tensor File ".../vllm/lora/layers/column_parallel_linear.py", line 559, in slice_lora_a IndexError: too many indices for tensor of dimension 1 [FAIL] gate_up fully_sharded=False lora_a=single-tensor File ".../vllm/lora/layers/column_parallel_linear.py", line 323, in set_lora IndexError: tuple index out of range [PASS] qkv fully_sharded=True lora_a=list [PASS] qkv fully_sharded=False lora_a=list [PASS] gate_up fully_sharded=True lora_a=list [PASS] gate_up fully_sharded=False lora_a=list 4 combination(s) failed ``` 4/4 single-tensor combinations fail on every run; the 4 per-slice-list controls pass, confirming only the single-tensor entry point is affected. ### No existing test covers it `tests/lora/test_layers.py` is the only file in the whole `tests/` tree with a direct `set_lora()` call, and its call sites miss the combination: - `populate_loras()` exercises the merged classes, but feeds them via `PackedLoRALayerWeights.pack()`, which always builds a **list** `lora_a`. - The one site that passes a bare tensor targets `MergedColumnParallelLinearVariableSliceWithLoRA` — the sibling class that already normalizes. ### Related but distinct PR #37019 generalizes the same `slice_lora_a` methods from hardcoded 2/3 subloras to arbitrary N, driven by Qwen3.5 GDN 4-way fusion. I checked its diff: it does not touch the base class's `set_lora`, so this gap survives it. ### Fix PR to follow — replicate the shared `lora_a` to one entry per group, counted by `len(lora_b)` rather than `n_slices` so `expand_packed_lora()`'s zip against `lora_b`'s groups keeps working. ### Before submitting a new issue... - [x] Make sure you already searched for relevant issues, and asked the core devs or community in the [Discussion Forum](https://discuss.vllm.ai/c/help/) first, before raising a new issue.
2 条评论