Repository navigation
[Bugfix][Ascend NPU][Diffusion] Load over-wide unquantized fallback weights straight into host memory under DLOFix/mxfp online offload after quant - #7009
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
This PR appears to belong to: docs/design/module/quantization.md. Module owners: @david6666666 @Isotr0py @lishunyang12 @brandneway, please review your own changes and leave a short self-review comment describing what you checked. PRs without author self-review may not be assigned a reviewer. Please take a look when you have a chance. If you would like an automated review, mention @vllm-omni-review-bot in a comment. |
| # through the accelerator (~24 GiB startup peak on MiniMax H3). | ||
| from vllm_omni.quantization.int8_config import load_unquantizable_fallback_on_cpu | ||
|
|
||
| fallback_ctx = ( |
There was a problem hiding this comment.
[P2] HSDP still bypasses this path. offload_after_quant is set before the branch, but _load_model_with_hsdp() still constructs and loads online-quantized layers on the accelerator via self.load_weights(model), so an over-wide fallback only moves to CPU at the final model.to("cpu"). Please either apply the host-fallback/offload handling there or explicitly reject/document CPU offload + HSDP.
There was a problem hiding this comment.
Addressed in 8d543c0. The HSDP path now applies the same host-fallback handling: _load_model_with_hsdp takes offload_after_quant and wraps both _init_from_load_format and self.load_weights(model) in load_unquantizable_fallback_on_cpu() when it is set. As you noted, the HSDP branch initializes on the accelerator whenever a quant config is present (hsdp_defer_to_cpu=False), so without this the over-wide fallback weights round-tripped through the device before apply_hsdp_to_model — now they load straight into host memory and sharding distributes them like any other parameter.
Covered by test_hsdp_enters_host_fallback_context_only_when_offloading_after_quant (parametrized on/off) in tests/diffusion/model_loader/test_diffusers_loader.py.
| # through the accelerator (~24 GiB startup peak on MiniMax H3). | ||
| from vllm_omni.quantization.int8_config import load_unquantizable_fallback_on_cpu | ||
|
|
||
| fallback_ctx = ( |
There was a problem hiding this comment.
Could you add a test through load_model() that checks the fallback stays on CPU through the post-load sweep? The current tests call create_weights() directly or only check the allowlist, so they don't protect the startup-memory bound this change relies on.
There was a problem hiding this comment.
Added in 8d543c0: test_dlo_load_model_keeps_host_fallback_on_cpu_through_post_load_sweep drives the full load_model() with a DLO + online-quant config and a real over-wide (NPU_QUANT_MATMUL_MAX_OUT_FEATURES + 1) layer built through NPUInt8OnlineLinearMethod.create_weights. It asserts:
- construction ran inside
load_unquantizable_fallback_on_cpu()(the wiring this PR adds), - the layer's
quant_methodwas swapped toUnquantizedHostLinearMethod, - after the real
_process_weights_after_loadingpass and the finalmodel.to("cpu")sweep, the weight is still the same CPU tensor with the loaded values intact, and_already_called_process_weights_after_loadingis set (so the sweep skips it rather than bouncing the host tensor to the accelerator and back).
Also fixed the pre-commit failures (pytest Hardware mark on test_int8_config.py, SPDX headers, ruff format, three mypy ignores on the mixin process_weights_after_loading calls) and rebased onto main resolving the diffusers_loader.py conflict — the restructured AllGather validation on main now carries the "host-loaded unquantized fallback layers are also allowed" wording. All commits are signed off; DCO is green.
2a7fc4b to
8d543c0
Compare
Omni ReviewBot triage noteAutomated triage of commit
These are automated triage suggestions only — the final decision belongs to the maintainers. |
Dong1017
left a comment
There was a problem hiding this comment.
Fix direction is right; no code-level blocker. I verified the mechanism on a real Ascend 910B1 (vllm 0.28.0 + torch_npu 2.10, head 8d543c0):
- Unit tests: 59 passed, 3 skipped (NPU-only cases) — checkbox 1 confirmed.
- A/B probe on the exact MiniMax-H3
adaln_proj.linearshape (out=96768): base materializes the fallback weight on the NPU at construction; head keeps it on CPU for its whole life, NPU peak 0 bytes, completion flag set, and the new log line appears verbatim. 50 × 0.496 GiB extrapolates to the claimed ~24 GiB/rank — checkbox 2's mechanism is confirmed, but please still attach the 8-cardtorch_npumemory-snapshot run declared in the Test Plan and fill the Test Result checkboxes.
Non-blocking:
docs/user_guide/diffusion/offloader/distributed_layerwise_offload.md:359still says other online quantization methods require rank-local transfer; this PR widens the validated set — please update the bullet (see inline).- Title has the branch name leaked in: "...under DLOFix/mxfp online offload after quant".
| Fp8PerTensorOnlineLinearMethod, | ||
| Int8OnlineLinearMethod, | ||
| NPUInt8OnlineLinearMethod, | ||
| UnquantizedHostLinearMethod, |
There was a problem hiding this comment.
Docs consistency: this allowlist (and the error at line 630) contradicts the Limitations bullet at docs/user_guide/diffusion/offloader/distributed_layerwise_offload.md:359 ("Other online quantization methods require rank-local transfer..."). Please update it to include host-loaded unquantized fallback.
| assert lazy_mxfp8._offload_after_quant | ||
|
|
||
| def _load_layer_in_one_chunk(self, method, layer): | ||
| method.create_weights( |
There was a problem hiding this comment.
Mock the TP group before creating weights; both new offload tests fail without it.
There was a problem hiding this comment.
Fixed in 1367cb4: TestOffloadAfterQuant now has an autouse _mock_tp fixture patching get_tensor_model_parallel_world_size / get_tensor_model_parallel_rank / get_tp_group (same stand-in as TestHostFallbackLoading in test_int8_config.py), so both create_weights-driven offload tests run without initializing torch.distributed. Good catch — the GitHub build job doesn't run the test files, which is exactly how this slipped through.
| # Materialize on host just-in-time: checkpoint chunks are CPU | ||
| # tensors, so loading stays CPU->CPU with no accelerator round trip. | ||
| if layer.weight.device.type == "meta": | ||
| weight = ModelWeightParameter( |
There was a problem hiding this comment.
Share the lazy weight materialization with LazyWeightMixin instead of copying it.
There was a problem hiding this comment.
Done in 1367cb4. UnquantizedHostLinearMethod is now (LazyWeightMixin, UnquantizedLinearMethod) and the duplicated patched_weight_loader is gone. The mixin grew a _lazy_load_device() hook (default torch.get_default_device()) that the host fallback overrides to torch.device("cpu"), so the materialization target — the only real difference — is a one-method override. The other two deltas are declared explicitly:
supports_offload_after_quant = False: the weight never visits the accelerator, so there is nothing to hand back to host (the mixin'slayer.to("cpu")on completion stays inert because_request_offload_after_quantnever opts this method in);process_weights_after_loadingis an explicit no-op the mixin's completion path calls right before flagging the layer — and the flag is what keeps the loader's post-load sweep from bouncing the host tensor to the accelerator and back.
All four TestHostFallbackLoading tests pass unchanged against the shared implementation, which also pins that the behavioral contract (meta deferral, chunked completion, CPU materialization) is identical.
…ight after quantization
The diffusion loader's _request_offload_after_quant only asks quant
methods that advertise supports_offload_after_quant. Only int8's
LazyWeightMixin did; the MXFP _LazyWeightMixin (shared by all online
mxfp8/mxfp4 methods) did not, so with CPU offload + online mxfp8 the
whole quantized transformer accumulated on the accelerator until the
final model.to("cpu") — a ~43 GiB load-time peak on MiniMax H3.
Mirror the int8 three-piece contract on _LazyWeightMixin:
- supports_offload_after_quant = True capability declaration
- enable_offload_after_quant() per-layer opt-in
- layer.to("cpu") right after process_weights_after_loading once the
patched weight loader sees the layer fully loaded
This caps the load-time device footprint at roughly one in-flight
layer plus non-quantized components instead of the full model.
Covers mxfp4 as well since all online MXFP methods share the mixin.
Adds CPU tests pinning the advertise/enable contract and the
return-to-CPU behavior of a fully loaded layer.
Signed-off-by: brandneway <gyuan4892@gmail.com>
…eights straight into host memory under DLO online quant
Ascend's npu_quant_matmul rejects weights whose output dimension exceeds
65535, so on MiniMax-H3 all 50 adaln_proj.linears (96768 out_features)
stay unquantized. Under --enable-distributed-layerwise-offload + online
quantization those bf16 weights (~24 GiB per rank) materialized on the
accelerator at construction and only moved to host at the final
model.to("cpu") — the dominant load-time device peak.
- Add UnquantizedHostLinearMethod: mirrors LazyWeightMixin's meta +
just-in-time materialize pattern but materializes on CPU and skips
quantization, marking the layer fully processed so the post-load
sweep never bounces the host tensor to the accelerator and back.
- Gate it on a load_unquantizable_fallback_on_cpu() context the
diffusion loader enters while constructing a model that returns to
host after online quantization — wired into both the ordinary path
and the HSDP path (which initializes on the accelerator whenever a
quant config is set).
- Allow the method in the DLO+AllGather online-quant allowlist: its
runtime layout is a plain contiguous bf16 weight, identical to the
ordinary unquantized path DLO already shards.
- Tests: meta-deferral/CPU-materialization/chunked-completion units,
the allowlist entry, an end-to-end load_model() check that the
fallback stays on CPU through the post-load sweep, and an HSDP
context-wiring check.
Signed-off-by: brandneway <gyuan4892@gmail.com>
…WeightMixin Review follow-up: UnquantizedHostLinearMethod copied the mixin's meta + just-in-time patched_weight_loader wholesale. It now derives from LazyWeightMixin and only overrides what differs: - a new _lazy_load_device() hook on the mixin (default: torch.get_default_device()) lets the host fallback materialize on CPU instead, so loading stays CPU->CPU with no accelerator round trip; - supports_offload_after_quant stays False — the weight never visits the accelerator, so there is nothing to return to host; - process_weights_after_loading is an explicit no-op; the mixin calls it on completion right before flagging the layer, and the flag is what keeps the loader's post-load sweep from bouncing the host tensor to the accelerator and back. Also mock the TP group in the mxfp8 offload-after-quant tests (create_weights resolves per-partition shapes through it and these tests never initialize torch.distributed), and re-apply the HSDP host-fallback context onto main's new broadcast-loading structure there: the context now spans the whole init/load region, which is safe because broadcast loading excludes online quantization. Signed-off-by: brandneway <gyuan4892@gmail.com>
8d543c0 to
1367cb4
Compare
| "per-tensor FP8, INT8, and MXFP8 linears; unsupported " | ||
| f"online methods: {', '.join(sorted(unsupported_methods))}. " | ||
| "per-tensor FP8, INT8, and MXFP8 linears " | ||
| "(host-loaded unquantized fallback layers are also allowed); " |
There was a problem hiding this comment.
Since the allowlist now admits host-loaded fallback layers, the Limitations section in docs/user_guide/diffusion/offloader/distributed_layerwise_offload.md (~L359, "Other online quantization methods require rank-local transfer") is out of sync — same point as the earlier docs thread. Can we fold that doc update into this PR?
There was a problem hiding this comment.
Folded into this PR in 90b939c — the Limitations bullet in distributed_layerwise_offload.md now reads: per-tensor online FP8, INT8, and MXFP8 linears run with either DiT transfer path via the ordinary loader, over-wide NPU-unquantizable fallback layers (output dimension past 65535) load straight into host memory under offload-after-quant with a plain contiguous bf16 layout and are equally allowed under allgather, and everything else still requires rank-local transfer until validated. This also picks up the INT8/MXFP8 entries from #6573 that the same sentence had missed.
…e offload guide Review follow-up: the Limitations section still said only per-tensor online FP8 can run with either DiT transfer path, while the loader allowlist now covers INT8 and MXFP8 (vllm-project#6573) and the over-wide NPU-unquantizable fallback layers this PR loads straight into host memory (plain contiguous bf16 — the layout DLO already shards). Rewrite the bullet to match: FP8/INT8/MXFP8 online linears run under allgather via the ordinary loader; host-loaded unquantized fallback layers are equally allowed; everything else still requires rank-local transfer until validated. Signed-off-by: brandneway <gyuan4892@gmail.com>
…eights straight into host memory under DLOFix/mxfp online offload after quant (vllm-project#7009) Signed-off-by: brandneway <gyuan4892@gmail.com> Signed-off-by: Matthieu Laneuville <matthieu.laneuville@surf.nl>
…eights straight into host memory under DLOFix/mxfp online offload after quant (vllm-project#7009) Signed-off-by: brandneway <gyuan4892@gmail.com>
Purpose
Background. Ascend's
npu_quant_matmul(QuantBatchMatmulV3) rejects anyweight whose output dimension exceeds 65535, and it fails at the first forward
rather than at load time, so
_fell_back_to_unquantized_npukeeps such layersunquantized. On MiniMax-H3 every one of the 50 DiT blocks has an
adaln_proj.linearwithout_features = 18 * 5376 = 96768, so all 50 fallback. With
--enable-distributed-layerwise-offload+ online INT8(
--diffusion-quantization-config '{"transformer":{"method":"int8"}}'), thesebf16 weights (496 MiB each, ~24.2 GiB per rank in total) were materialized on
the accelerator at construction time and stayed resident until the final
model.to("cpu"). Together with the TP8-sharded text encoder (~6.9 GiB perrank), memory-snapshot replay shows the single-rank startup peak at
~31.9 GiB — over the 32 GB card limit — even though under DLO none of these
weights are device-resident at inference time. (The genuinely quantized layers
already stream back to CPU per layer via
enable_offload_after_quant; thefallback layers were the residual peak.)
Fix. When the loader will offload the whole model back to host memory
after online quantization (the offload-after-quant path), over-wide fallback
weights now never visit the accelerator during startup:
UnquantizedHostLinearMethod: mirrorsLazyWeightMixin's meta +just-in-time materialization pattern, but materializes on CPU and skips
quantization entirely. Checkpoint chunks are CPU tensors, so loading stays
CPU→CPU; the weights only reach the accelerator through the offload
backend's runtime prefetch, where DLO's shared bf16 buffer is already sized
for them.
load_unquantizable_fallback_on_cpu()context (ContextVar-scoped) isentered by
DiffusersPipelineLoader.load_modelaround model constructiononly when offload-after-quant is active; otherwise the eager path is
byte-for-byte unchanged, so non-DLO deployments are unaffected.
_already_called_process_weights_after_loadingso the post-load sweep skipsthem instead of bouncing each ~496 MiB host tensor to the accelerator and
back for a no-op
process_weights_after_loading.UnquantizedHostLinearMethodis added to the DLO+AllGather online-methodallowlist: its runtime layout is a plain contiguous bf16 weight, identical
to the ordinary unquantized path DLO already shards.
This also removes two wasted full-size copies for these weights (H2D during
loading + D2H at the final
model.to("cpu")).Test Plan
tests/diffusion/quantization/test_int8_config.py::TestHostFallbackLoading(eager path unchanged without the context; meta deferral under the
context; CPU-direct load with value equality; chunked load marks
completion only when full; context does not leak)
tests/diffusion/model_loader/test_diffusers_loader.py::test_dlo_allgather_allows_unquantized_host_fallbackrecord the startup memory peak via
torch_npumemory snapshot.vLLM Version: <fill in>
vLLM-Omni Commit: <fill in>
Test Result
pytest tests/diffusion/quantization/test_int8_config.py tests/diffusion/model_loader/test_diffusers_loader.py)MiniMax-H3 DLO + INT8 launch, with the new log line
"Loading over-wide unquantized fallback weights straight into host memory"