[Bugfix][Ascend NPU][Diffusion] Load over-wide unquantized fallback weights straight into host memory under DLOFix/mxfp online offload after quant (#7009)

Signed-off-by: brandneway <gyuan4892@gmail.com>
This commit is contained in:
ooooooye authored and GitHub committed 2026-09-18 07:36:48 +00:00
1 parent 5d3e6dc1b4
commit 823af92347
7 files changed
+561 -43

No files matched your search

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Unit tests for Int8 quantization config."""
import pytest
@@ -12,7 +12,7 @@ from vllm_omni.platforms import current_omni_platform
from vllm_omni.quantization import build_quant_config
from vllm_omni.quantization.factory import SUPPORTED_QUANTIZATION_METHODS
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion]
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
npu_available = pytest.mark.skipif(not current_omni_platform.is_npu(), reason="NPU platform not available.")
@@ -248,6 +248,124 @@ class TestNPUQuantMatmulShapeFallback:
assert layer.quant_method is method
class TestHostFallbackLoading:
"""Over-wide fallback layers load straight into host memory when the loader
will offload the model back to CPU after online quantization (DLO).
Without the context the eager path is preserved: the fallback weight is a
real tensor created at construction time.
"""
@pytest.fixture(autouse=True)
def _mock_tp(self, mocker):
# Same stand-in as TestNPUQuantMatmulShapeFallback: the eager fallback
# delegates to UnquantizedLinearMethod, which reads the TP group.
mock_group = mocker.Mock()
mock_group.rank_in_group = 0
mocker.patch("vllm.model_executor.layers.linear.get_tensor_model_parallel_world_size", return_value=1)
mocker.patch("vllm.model_executor.layers.linear.get_tensor_model_parallel_rank", return_value=0)
mocker.patch("vllm.distributed.parallel_state.get_tp_group", return_value=mock_group)
@pytest.fixture
def quant_config(self):
from vllm_omni.quantization.int8_config import DiffusionInt8Config
return DiffusionInt8Config(is_checkpoint_int8_serialized=False, activation_scheme="dynamic")
def _make_over_wide_layer(self, quant_config, weight_loader):
from vllm_omni.quantization.int8_config import (
NPU_QUANT_MATMUL_MAX_OUT_FEATURES,
NPUInt8OnlineLinearMethod,
)
out_features = NPU_QUANT_MATMUL_MAX_OUT_FEATURES + 1
method = NPUInt8OnlineLinearMethod(quant_config)
layer = Module()
layer.quant_method = method
method.create_weights(
layer,
input_size_per_partition=8,
output_partition_sizes=[out_features],
input_size=8,
output_size=out_features,
params_dtype=torch.bfloat16,
weight_loader=weight_loader,
)
return layer, out_features
@staticmethod
def _loaded_values(out_features):
return torch.arange(out_features * 8, dtype=torch.float32).reshape(out_features, 8).to(torch.bfloat16)
def test_fallback_without_context_keeps_eager_weights(self, quant_config):
layer, _ = self._make_over_wide_layer(quant_config, lambda param, loaded_weight, *a, **k: None)
assert type(layer.quant_method) is UnquantizedLinearMethod
assert layer.weight.device.type != "meta"
def test_fallback_under_context_defers_weight_to_meta(self, quant_config):
from vllm_omni.quantization.int8_config import (
UnquantizedHostLinearMethod,
load_unquantizable_fallback_on_cpu,
)
with load_unquantizable_fallback_on_cpu():
layer, _ = self._make_over_wide_layer(quant_config, lambda param, loaded_weight, *a, **k: None)
assert isinstance(layer.quant_method, UnquantizedHostLinearMethod)
assert layer.quant_method.uses_meta_device
assert layer.weight.device.type == "meta"
def test_fallback_weight_loads_straight_to_cpu(self, quant_config):
from vllm_omni.quantization.int8_config import load_unquantizable_fallback_on_cpu
def copy_loader(param, loaded_weight, *args, **kwargs):
param.data.copy_(loaded_weight)
with load_unquantizable_fallback_on_cpu():
layer, out_features = self._make_over_wide_layer(quant_config, copy_loader)
loaded = self._loaded_values(out_features)
layer.weight.weight_loader(layer.weight, loaded)
assert layer.weight.device.type == "cpu"
assert torch.equal(layer.weight, loaded)
# Marks the post-load sweep to skip the layer instead of bouncing the
# host tensor to the accelerator and back for a no-op process call.
assert layer._already_called_process_weights_after_loading
def test_chunked_load_marks_completion_only_when_full(self, quant_config):
from vllm_omni.quantization.int8_config import load_unquantizable_fallback_on_cpu
state = {"offset": 0}
def half_loader(param, loaded_weight, *args, **kwargs):
start = state["offset"]
param.data[start : start + loaded_weight.shape[0]].copy_(loaded_weight)
state["offset"] += loaded_weight.shape[0]
with load_unquantizable_fallback_on_cpu():
layer, out_features = self._make_over_wide_layer(quant_config, half_loader)
loaded = self._loaded_values(out_features)
halves = loaded.chunk(2, dim=0)
layer.weight.weight_loader(layer.weight, halves[0])
assert not getattr(layer, "_already_called_process_weights_after_loading", False)
layer.weight.weight_loader(layer.weight, halves[1])
assert layer._already_called_process_weights_after_loading
assert torch.equal(layer.weight, loaded)
def test_context_does_not_leak(self, quant_config):
from vllm_omni.quantization.int8_config import load_unquantizable_fallback_on_cpu
with load_unquantizable_fallback_on_cpu():
pass
layer, _ = self._make_over_wide_layer(quant_config, lambda param, loaded_weight, *a, **k: None)
assert type(layer.quant_method) is UnquantizedLinearMethod
class TestOffloadAfterQuant:
def test_only_methods_advertising_the_capability_are_asked(self):
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Tests for MXFP8 quantization config and linear method dispatch.
Coverage:
@@ -9,11 +9,13 @@ Coverage:
- Weight / scale shape-transform arithmetic from process_weights_after_loading (CPU)
- build_quant_config integration
- MXFP8_QUANT_CONFIG structure as the auto-detection contract
- Offload-after-quant capability of the shared online-method mixin (CPU)
"""
import pytest
import torch
from pytest_mock import MockerFixture
from torch.nn import Module
from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
from vllm_omni.platforms import current_omni_platform
@@ -335,3 +337,99 @@ def test_supported_methods_include_mxfp8():
from vllm_omni.quantization import SUPPORTED_QUANTIZATION_METHODS
assert "mxfp8" in SUPPORTED_QUANTIZATION_METHODS
# ---------------------------------------------------------------------------
# Offload-after-quant capability (CPU, no NPU ops)
# ---------------------------------------------------------------------------
class TestOffloadAfterQuant:
"""Online MXFP methods advertise ``supports_offload_after_quant`` so the
diffusion loader can stream each layer back to host memory right after it
is quantized, instead of accumulating the whole model on the accelerator
until the final ``model.to("cpu")``.
The capability lives on ``_LazyWeightMixin``, which is shared by all
online MXFP8/MXFP4 methods, so pinning it here covers mxfp4 too.
"""
@pytest.fixture(autouse=True)
def _mock_tp(self, mocker: MockerFixture):
# create_weights resolves the TP group while computing per-partition
# shapes; these tests never initialize torch.distributed.
mock_group = mocker.Mock()
mock_group.rank_in_group = 0
mocker.patch("vllm.model_executor.layers.linear.get_tensor_model_parallel_world_size", return_value=1)
mocker.patch("vllm.model_executor.layers.linear.get_tensor_model_parallel_rank", return_value=0)
mocker.patch("vllm.distributed.parallel_state.get_tp_group", return_value=mock_group)
def _make_method(self):
from vllm_omni.quantization.mxfp8_config import (
DiffusionMXFP8Config,
NPUMxfp8OnlineLinearMethod,
)
return NPUMxfp8OnlineLinearMethod(DiffusionMXFP8Config(is_checkpoint_mxfp8_serialized=False))
def test_only_methods_advertising_the_capability_are_asked(self):
from vllm_omni.diffusion.model_loader.diffusers_loader import (
DiffusersPipelineLoader,
)
class MetaDeviceOnlyMethod:
"""Stands in for upstream online FP8: lazy, but no offload hook."""
uses_meta_device = True
model = torch.nn.Sequential(Module(), Module())
lazy_mxfp8 = self._make_method()
model[0].quant_method = lazy_mxfp8
model[1].quant_method = MetaDeviceOnlyMethod()
marked = DiffusersPipelineLoader._request_offload_after_quant(model)
assert marked == 1
assert lazy_mxfp8._offload_after_quant
def _load_layer_in_one_chunk(self, method, layer):
method.create_weights(
layer,
input_size_per_partition=8,
output_partition_sizes=[4],
input_size=8,
output_size=4,
params_dtype=torch.bfloat16,
weight_loader=lambda param, loaded_weight, *args, **kwargs: param.copy_(loaded_weight),
)
full_weight = torch.randn(4, 8, dtype=torch.bfloat16)
layer.weight.weight_loader(layer.weight, full_weight)
def test_finished_layer_returns_to_cpu_when_enabled(self, mocker: MockerFixture):
method = self._make_method()
method.enable_offload_after_quant()
mocker.patch.object(method, "process_weights_after_loading")
layer = Module()
moved_to = []
layer.to = lambda device, *args, **kwargs: moved_to.append(device) # type: ignore[method-assign]
self._load_layer_in_one_chunk(method, layer)
method.process_weights_after_loading.assert_called_once_with(layer)
assert layer._already_called_process_weights_after_loading
assert moved_to == ["cpu"]
def test_finished_layer_stays_put_by_default(self, mocker: MockerFixture):
method = self._make_method()
mocker.patch.object(method, "process_weights_after_loading")
layer = Module()
moved_to = []
layer.to = lambda device, *args, **kwargs: moved_to.append(device) # type: ignore[method-assign]
self._load_layer_in_one_chunk(method, layer)
method.process_weights_after_loading.assert_called_once_with(layer)
assert layer._already_called_process_weights_after_loading
assert moved_to == []