Summary
Enabling --enable-lora on an AutoRound int4 model (INC ark_linear path) on XPU hits two independent bugs in v0.24.0. With the two small patches below, LoRA serving works end-to-end on 2× Arc Pro B70 (Battlemage), TP=2: clean boot, 12/12 guided-JSON validity, runtime adapter loading via /v1/load_lora_adapter, and a zero-weight (lora_B = 0) test adapter produces outputs bit-identical to the base model at temperature 0. I'm happy to send PR(s) for both.
_get_lora_device doesn't know the INC/AutoRound ark_linear layout → --enable-lora crash-loops at startup for any INC WNA16 (AutoRound int4) checkpoint.
- Unconditional
tensor_model_parallel_all_gather in _mcp_apply → after fixing (1), non-sharded (default) LoRA at TP>1 crashes during profiling with a rank-dimension shape mismatch. From code reading this one is not XPU-specific (the gather lives in the platform-agnostic layer code), but I have only verified it on XPU.
Related context: #40601 (INC scheme refactor that owns inc_wna16_linear.py), #39778 (W4A16 / auto_round_kernel support), #41663 (oneCCL stable-fallback env used on this machine).
Environment
| Component |
Version |
| GPUs |
2× Intel Arc Pro B70 (Battlemage G31), TP=2 |
| CPU |
AMD EPYC 7413 |
| OS / kernel |
Ubuntu 26.04 LTS, 7.0.0-27-generic, xe driver |
| PyTorch |
2.12.0+xpu (XPU runtime 20250302, oneAPI 2025.3.3, Level Zero driver 26.18.38308.1) |
| vLLM |
0.24.0+xpu, source build from upstream Dockerfile.xpu at v0.24.0 (ee0da84), unmodified |
| Model |
Intel/Qwen3.6-27B-int4-AutoRound (W4A16 group_size=128, hybrid GDN+attention VLM arch, served text-only) |
| Relevant flags |
--tensor-parallel-size 2 --enforce-eager --max-model-len 32768 --enable-lora --max-lora-rank 64 --max-loras 2 |
oneCCL env from #41663 applied (CCL_ENABLE_SYCL_KERNELS=0, CCL_ATL_TRANSPORT=ofi, CCL_ZE_IPC_EXCHANGE=sockets, CCL_TOPO_FABRIC_VERTEX_CONNECTION_CHECK=0, SYCL_UR_USE_LEVEL_ZERO_V2=0). The vision-tower "no matching PunicaWrapper … will be ignored" warnings are unrelated/benign.
Bug 1 — _get_lora_device: unsupported INC/AutoRound ark_linear base layer
Symptom: with --enable-lora, every worker dies while wrapping layers at load_model, container crash-loops:
File ".../vllm/lora/layers/base_linear.py", line 80, in __init__
self.device = _get_lora_device(self.base_layer)
File ".../vllm/lora/layers/utils.py", line 70, in _get_lora_device
raise ValueError(f"Unsupported base layer: {base_layer}")
ValueError: Unsupported base layer: MergedColumnParallelLinear(
in_features=5120, output_features=8192, bias=False, tp_size=2, gather_output=False
(ark_linear): QuantLinear(in_features=5120, out_features=8192, bias=True, w_bit=4, group_size=128)
)
Root cause: INCWNA16LinearScheme.process_weights_after_loading (vllm/model_executor/layers/quantization/inc/schemes/inc_wna16_linear.py) repacks the quantized tensors into a layer.ark_linear submodule and then deletes layer.qweight / layer.qzeros / layer.scales from the parallel layer. _get_lora_device probes weight / weight_packed / qweight / w2_* — none of which exist anymore.
Fix (verified): add an ark_linear branch. QuantLinear.post_init() keeps qweight (repacked in place), so the device lookup is stable:
--- a/vllm/lora/layers/utils.py
+++ b/vllm/lora/layers/utils.py
@@ def _get_lora_device(base_layer: nn.Module) -> torch.device:
# GPTQ/AWQ
elif hasattr(base_layer, "qweight"):
return base_layer.qweight.device
+ # AutoRound ark QuantLinear (INC WNA16 scheme): quantized tensors are
+ # repacked into the `ark_linear` submodule and the originals are deleted
+ # from the parallel layer after weight loading.
+ elif hasattr(base_layer, "ark_linear"):
+ return base_layer.ark_linear.qweight.device
# MoE layer
With this patch, TP=1 boots and serves correctly with --enable-lora.
Bug 2 — unconditional all_gather in _mcp_apply breaks non-sharded LoRA at TP>1
Symptom: with Bug 1 patched, TP=2 with default (non-fully-sharded) LoRA crashes during determine_available_memory → profile_run (dummy LoRA run), first on a merged column layer:
File ".../vllm/lora/layers/column_parallel_linear.py", line 344, in apply
return _mcp_apply(x, bias, self)
File ".../vllm/lora/layers/column_parallel_linear.py", line 66, in _mcp_apply
lora_output = layer.punica_wrapper.add_expand(...)
File ".../vllm/lora/punica_wrapper/punica_xpu.py", line 109, in _apply_expand
bgmv_expand_slice(
File ".../vllm/lora/ops/xpu_ops/lora_ops.py", line 79, in bgmv_expand_slice
torch.ops._xpu_C.bgmv_expand_slice(
RuntimeError: inputs.size(1) must match lora_b_weights.size(-1)
Root cause: in _mcp_apply (vllm/lora/layers/column_parallel_linear.py, line 64 at v0.24.0):
local_lora_rank = layer.lora_a_stacked[0].shape[2]
buffer_shape = (layer.n_slices, x.shape[0], local_lora_rank)
...
buffers = tensor_model_parallel_all_gather(buffers) # gathers along the LAST dim (rank)
The gather is only shape-consistent with --fully-sharded-loras, where lora_a is sharded along the rank dim (r/tp per rank → gather reconstitutes r, matching lora_b_weights.size(-1) == r). In the default non-sharded config, each rank already holds the full rank r, so the gather produces r * tp and the expand asserts (128 != 64 with max_lora_rank=64, TP=2). At TP=1 the gather is a no-op, which is why this only shows at TP>1. It also costs one collective per wrapped layer per step even when it happens to be benign.
Fix (verified):
--- a/vllm/lora/layers/column_parallel_linear.py
+++ b/vllm/lora/layers/column_parallel_linear.py
@@ def _mcp_apply(x, bias, layer):
- buffers = tensor_model_parallel_all_gather(buffers)
+ # Only the fully-sharded path shards the LoRA rank dim across TP ranks;
+ # gathering in the non-sharded path breaks shapes (rank*tp vs rank) and
+ # costs one collective per wrapped layer per step.
+ if layer.lora_config.fully_sharded_loras:
+ buffers = tensor_model_parallel_all_gather(buffers)
--fully-sharded-loras also works as a workaround without any patch for Bug 2 (it makes the existing code path self-consistent), but see the performance note below.
Verification & measurements (TP=2, full production config)
- Boot: clean startup with
--enable-lora in both modes (non-sharded + patch, and fully-sharded).
- Output integrity: 12/12 guided-JSON requests (llguidance backend,
json_schema mode) valid, no mojibake/garbage tokens.
- Adapter math: rank-8 adapter with
lora_B = 0 (exact no-op) on the 16 full-attention layers, loaded at runtime via POST /v1/load_lora_adapter (VLLM_ALLOW_RUNTIME_LORA_UPDATING=True): outputs at temperature 0 are bit-identical to the base model through the sliced TP=2 path.
- Throughput (engine-total, identical workload replayed against each configuration):
| Configuration |
Engine throughput |
vs LoRA off |
--enable-lora off (baseline) |
~12.0 tok/s |
— |
| non-sharded + Bug-2 patch |
~10.5 tok/s |
≈ −12% |
--fully-sharded-loras (no patch) |
~5–6.6 tok/s |
≈ 2× slower |
The fully-sharded slowdown is expected on this platform: it adds a rank-dim collective per wrapped layer per step, and with the #41663 oneCCL fallback env those collectives run on the host-staging path. Also, enabling LoRA reduced the available KV cache pool from 16.61 GiB to ~14.8 GiB.
Repro
- Serve
Intel/Qwen3.6-27B-int4-AutoRound on XPU with --enable-lora → Bug 1 (any TP).
- Apply the
_get_lora_device patch, serve with TP≥2, default LoRA sharding → Bug 2 during startup profiling (no adapter needs to be loaded; the dummy-LoRA profile run triggers it).
I can open a PR with both fixes (or split them) — let me know which shape you'd prefer.
Summary
Enabling
--enable-loraon an AutoRound int4 model (INCark_linearpath) on XPU hits two independent bugs in v0.24.0. With the two small patches below, LoRA serving works end-to-end on 2× Arc Pro B70 (Battlemage), TP=2: clean boot, 12/12 guided-JSON validity, runtime adapter loading via/v1/load_lora_adapter, and a zero-weight (lora_B = 0) test adapter produces outputs bit-identical to the base model at temperature 0. I'm happy to send PR(s) for both._get_lora_devicedoesn't know the INC/AutoRoundark_linearlayout →--enable-loracrash-loops at startup for any INC WNA16 (AutoRound int4) checkpoint.tensor_model_parallel_all_gatherin_mcp_apply→ after fixing (1), non-sharded (default) LoRA at TP>1 crashes during profiling with a rank-dimension shape mismatch. From code reading this one is not XPU-specific (the gather lives in the platform-agnostic layer code), but I have only verified it on XPU.Related context: #40601 (INC scheme refactor that owns
inc_wna16_linear.py), #39778 (W4A16 / auto_round_kernel support), #41663 (oneCCL stable-fallback env used on this machine).Environment
7.0.0-27-generic, xe driverDockerfile.xpuatv0.24.0(ee0da84), unmodifiedIntel/Qwen3.6-27B-int4-AutoRound(W4A16 group_size=128, hybrid GDN+attention VLM arch, served text-only)--tensor-parallel-size 2 --enforce-eager --max-model-len 32768 --enable-lora --max-lora-rank 64 --max-loras 2oneCCL env from #41663 applied (
CCL_ENABLE_SYCL_KERNELS=0,CCL_ATL_TRANSPORT=ofi,CCL_ZE_IPC_EXCHANGE=sockets,CCL_TOPO_FABRIC_VERTEX_CONNECTION_CHECK=0,SYCL_UR_USE_LEVEL_ZERO_V2=0). The vision-tower "no matching PunicaWrapper … will be ignored" warnings are unrelated/benign.Bug 1 —
_get_lora_device: unsupported INC/AutoRoundark_linearbase layerSymptom: with
--enable-lora, every worker dies while wrapping layers atload_model, container crash-loops:Root cause:
INCWNA16LinearScheme.process_weights_after_loading(vllm/model_executor/layers/quantization/inc/schemes/inc_wna16_linear.py) repacks the quantized tensors into alayer.ark_linearsubmodule and then deleteslayer.qweight/layer.qzeros/layer.scalesfrom the parallel layer._get_lora_deviceprobesweight/weight_packed/qweight/w2_*— none of which exist anymore.Fix (verified): add an
ark_linearbranch.QuantLinear.post_init()keepsqweight(repacked in place), so the device lookup is stable:With this patch, TP=1 boots and serves correctly with
--enable-lora.Bug 2 — unconditional
all_gatherin_mcp_applybreaks non-sharded LoRA at TP>1Symptom: with Bug 1 patched, TP=2 with default (non-fully-sharded) LoRA crashes during
determine_available_memory→profile_run(dummy LoRA run), first on a merged column layer:Root cause: in
_mcp_apply(vllm/lora/layers/column_parallel_linear.py, line 64 at v0.24.0):The gather is only shape-consistent with
--fully-sharded-loras, wherelora_ais sharded along the rank dim (r/tpper rank → gather reconstitutesr, matchinglora_b_weights.size(-1) == r). In the default non-sharded config, each rank already holds the full rankr, so the gather producesr * tpand the expand asserts (128 != 64withmax_lora_rank=64, TP=2). At TP=1 the gather is a no-op, which is why this only shows at TP>1. It also costs one collective per wrapped layer per step even when it happens to be benign.Fix (verified):
--fully-sharded-lorasalso works as a workaround without any patch for Bug 2 (it makes the existing code path self-consistent), but see the performance note below.Verification & measurements (TP=2, full production config)
--enable-lorain both modes (non-sharded + patch, and fully-sharded).json_schemamode) valid, no mojibake/garbage tokens.lora_B = 0(exact no-op) on the 16 full-attention layers, loaded at runtime viaPOST /v1/load_lora_adapter(VLLM_ALLOW_RUNTIME_LORA_UPDATING=True): outputs at temperature 0 are bit-identical to the base model through the sliced TP=2 path.--enable-loraoff (baseline)--fully-sharded-loras(no patch)The fully-sharded slowdown is expected on this platform: it adds a rank-dim collective per wrapped layer per step, and with the #41663 oneCCL fallback env those collectives run on the host-staging path. Also, enabling LoRA reduced the available KV cache pool from 16.61 GiB to ~14.8 GiB.
Repro
Intel/Qwen3.6-27B-int4-AutoRoundon XPU with--enable-lora→ Bug 1 (any TP)._get_lora_devicepatch, serve with TP≥2, default LoRA sharding → Bug 2 during startup profiling (no adapter needs to be loaded; the dummy-LoRA profile run triggers it).I can open a PR with both fixes (or split them) — let me know which shape you'd prefer.