Skip to content

[Offloader] Skip per-tensor scale parameters from CPU offload - #43453

Open
zemf4you wants to merge 1 commit into
vllm-project:mainfrom
zemf4you:offloader-skip-scales
Open

zemf4you wants to merge 1 commit into
vllm-project:mainfrom
zemf4you:offloader-skip-scales

Conversation

@zemf4you

@zemf4you zemf4you commented May 22, 2026

Copy link
Copy Markdown

Summary

UVAOffloader._maybe_offload_to_cpu reassigns .data on a Parameter to move it to CPU. In non-UVA fallback mode (any platform where is_pin_memory_available() returns False — WSL being the common case today), the offloader replaces the module's forward with a wrapper that, on each call, gathers module.state_dict() and reapplies it via functional_call.

The swap only updates attributes on the module itself. Python references held outside the module — most notably the per-tensor scale Parameters cached by FusedMoEQuantConfig at process_weights_after_loading() time — still resolve to the same Parameter object whose .data is now on CPU. Marlin's GEMM (and any kernel that does TORCH_CHECK(x.is_cuda())) then crashes with:

RuntimeError: b_scales is not on GPU

This is Crash 2 from #37883 and reproduces with any NVFP4 / FP8 / GPTQ MoE model + --cpu-offload-gb on a non-UVA host.

In genuine UVA mode the cached reference resolves to a UVA-mapped CUDA view (.is_cuda == True), so the same code path works there.

Fix

Two-part contract: a Parameter marker in the offloader plus opt-in from the quant config that holds external references.

1. Offloader (vllm/model_executor/offloader/uva.py). New _VLLM_SKIP_OFFLOAD_ATTR = "_vllm_skip_offload" constant. The non-UVA fallback path of _maybe_offload_to_cpu honours the marker:

if not self.uva_offloading and getattr(p, _VLLM_SKIP_OFFLOAD_ATTR, False):
    continue

The name follows vLLM's existing _vllm_* Parameter-marker convention (_vllm_is_uva_offloaded is set elsewhere in this same module; _vllm_patched / _vllm_fxgraph_dumps_patched live in patch_utils). The check is gated on not self.uva_offloading so UVA-capable hosts still offload scales (no memory regression).

2. NVFP4 MoE quant config (compressed_tensors_moe_w4a4_nvfp4.py). After process_weights_after_loading produces the kernel-format w13_weight_scale and w2_weight_scale Parameters that get cached in self.moe_quant_config = self.get_fused_moe_quant_config(layer), set the marker:

layer.w13_weight_scale._vllm_skip_offload = True
layer.w2_weight_scale._vllm_skip_offload = True

These are the two Parameters that FusedMoEQuantConfig.make(...) keeps a reference to (as w13_scale / w2_scale); the other cached scales (w13_scale_2, w2_scale_2, a13_scale, a2_scale) are plain Tensor attributes, which the offloader does not traverse.

No naming heuristic. Opt-in is explicit. Other quant schemes that hit the same cached-reference issue should set the marker at the analogous site in their own process_weights_after_loading.

Per-tensor scales are typically <0.1% of weight bytes, so excluding them from offload has negligible memory impact.

Test plan

pytest tests/quantization/test_uva_scale_skip.py -v

16 cases covering:

  • marker constant (name + _vllm_ prefix convention)
  • marker semantics (no marker / True / False / scale-looking names without marker are NOT auto-protected — regression guard against reintroducing a substring fallback)
  • offloader integration in non-UVA mode (marked scales stay on GPU, unmarked module gets full offload, user --cpu-offload-params filter does not override the marker, marker protects arbitrary parameter names)
  • UVA-mode gating (marked scales ARE offloaded in genuine UVA mode — marker is non-UVA-only)
  • NVFP4 producer-side check (source grep confirming the quant config sets the marker on the two cached scale Parameters)

Neighbouring regressions: tests/quantization/test_turboquant.py (119/119 passed), tests/kernels/core/test_uva.py (skipped on hosts without UVA hw, same as before this PR).

End-to-end repro

Originally discovered while trying to serve RedHatAI/Qwen3.6-35B-A3B-NVFP4 (35B-A3B MoE in NVFP4, compressed-tensors quant) with --kv-cache-dtype turboquant_4bit_nc on a single RTX 3090 (24 GB, SM 8.6) under WSL2. The model is ~17 GB of weights, the practical CUDA-accessible VRAM on this configuration is ~15 GB (WDDM holds the rest), so --cpu-offload-gb is mandatory. With non-UVA fallback active (WSL), process_weights_after_loading runs successfully but the first profile_run forward crashes inside Marlin's NVFP4 MoE GEMM with b_scales is not on GPU.

With this PR applied (no other workarounds — no --cpu-offload-params filter, no env overrides), the model loads, profile_run completes, and the OpenAI HTTP server answers POST /v1/completions correctly:

prompt:  "7+5="                                  -> "12"
prompt:  "The capital of France is"              -> " Paris."
prompt:  "Once upon a time, there was"           -> " a young adventurer named Alex..."

Related (not duplicate)

@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Once the PR is approved and ready to go, your PR reviewer(s) can run CI to test the changes comprehensively before merging.

To run CI, PR reviewers can either: Add ready label to the PR or enable auto-merge.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to skip the offloading of scale and zero-point parameters to the CPU when the UVAOffloader is operating in non-UVA fallback mode. This change prevents issues where cached parameter references in quantization configurations (such as Marlin MoE) become desynchronized, leading to kernel errors. The implementation includes an explicit _vllm_skip_offload marker and a name-based heuristic fallback to identify these parameters. A new test suite, tests/quantization/test_uva_scale_skip.py, has been added to verify the skipping logic across various scenarios. I have no feedback to provide as there were no review comments to assess.

@zemf4you
zemf4you force-pushed the offloader-skip-scales branch 2 times, most recently from 83250cc to dd3787b Compare May 22, 2026 23:46
Closes the runtime failure described in vllm-project#37883 Crash 2 ("b_scales is
not on GPU") for NVFP4 MoE.

Root cause
----------
UVAOffloader._maybe_offload_to_cpu replaces .data on a Parameter when
moving it to CPU. In non-UVA fallback mode (WSL or any platform where
pinned memory is not available) the forward wrapper later swaps the
module's attributes via functional_call(module.state_dict()). External
Python references to the Parameter -- e.g. FusedMoEQuantConfig.w1_scale
cached by the NVFP4 MoE quant config at process_weights_after_loading
time -- still resolve to the same Parameter object whose .data now lives
on CPU. Marlin's GEMM (and any kernel that does TORCH_CHECK(x.is_cuda()))
then crashes.

In genuine UVA mode the cached reference resolves to a UVA-mapped CUDA
view, so the bug does not appear there.

Fix
---
Two-part contract:

1. vllm/model_executor/offloader/uva.py adds an opt-in marker
   `_vllm_skip_offload`, following the existing `_vllm_*` Parameter
   attribute convention (`_vllm_is_uva_offloaded` is already used in
   this same module; `_vllm_patched` etc. live in patch_utils). The
   non-UVA fallback path of `_maybe_offload_to_cpu` honours the marker
   and leaves marked Parameters on their original device.

   The check is gated on `not self.uva_offloading`, so UVA-capable
   hosts still offload scales (no functional or memory regression).

2. compressed_tensors_moe_w4a4_nvfp4.py sets the marker on the two
   Parameters that FusedMoEQuantConfig.make(...) caches by reference
   (`layer.w13_weight_scale` and `layer.w2_weight_scale`) immediately
   after process_weights_after_loading produces them. The other cached
   scales are plain Tensor attributes (not Parameters), so the offloader
   does not traverse them and they need no marker.

Opt-in is explicit; there is no name-based heuristic. Other quant
schemes that hit the same cached-reference issue should set the marker
at the analogous site in their own process_weights_after_loading.

Per-tensor scales are typically <0.1% of weight bytes, so excluding
them from offload has negligible memory impact.

Tests
-----
tests/quantization/test_uva_scale_skip.py (16 cases):
- marker constant (name + `_vllm_` convention)
- marker semantics (no marker / True / False; scale-looking names
  without marker are NOT auto-protected -- regression guard against
  reintroducing a substring fallback)
- offloader integration in non-UVA mode (marked scales stay on GPU,
  unmarked module gets full offload, user --cpu-offload-params filter
  does not override marker, marker protects arbitrary names)
- UVA mode gating (marked scales ARE offloaded in genuine UVA mode --
  the marker is non-UVA-only)
- NVFP4 producer side (source grep confirming the quant config sets the
  marker on the two cached scale Parameters)

Signed-off-by: zemf4you <radimir@zemf4you.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant