Skip to content

Commit 093ce8c

Browse files
KINGFIOXKurumi5210
andauthored
[Feature] Support NZ static buffers for prefetch offload (vllm-project#11945)
### What this PR does / why we need it? This PR supports Ascend NZ-format static buffers for prefetch CPU offload. - Marks parameters whose quantized weights are transformed to `ACL_FORMAT_FRACTAL_NZ` during `process_weights_after_loading`, after `maybe_trans_nz` has run. - Casts the corresponding prefetch `StaticBufferPool` buffers to `ACL_FORMAT_FRACTAL_NZ` during `post_init`, so offloaded weights are copied back into buffers with the expected Ascend storage format. - Refactors the Ascend prefetch offloader to reuse upstream vLLM `PrefetchOffloader` behavior while installing Ascend-specific module offloaders and keeping CUDA stream/event aliases mapped to NPU APIs after model runner initialization. In my setup, this makes it possible to run prefill for GLM-5.1-W8A8 with context `max-model-len 202752`, `max-num-batched-tokens 128`, `block-size 128` and `gpu-memory-utilization 0.92` on a single-node 16-card a3, although it is still very slow. I use prefetch CPU offload for selected MLP weights with: ```bash --offload-backend prefetch --offload-group-size 4 --offload-num-in-group 2 --offload-prefetch-step 2 --offload-params mlp ``` ### Previous works [vllm-project#10251](vllm-project#10251) added the initial Ascend prefetch offloader by porting the upstream vLLM implementation and replacing CUDA stream/event APIs with NPU equivalents. However, that implementation still reused the normal-layout `StaticBufferPool` flow and did not preserve Ascend NZ weight format for static buffers. This matters because vLLM Ascend has enabled NZ conversion for quantized weights by default since [vllm-project#4878](vllm-project#4878), included from `v0.13.0rc1`: `VLLM_ASCEND_ENABLE_NZ=1` means quantized weights are transformed to NZ by `maybe_trans_nz` during `process_weights_after_loading`. When prefetch offload copies those NZ weights through normal-layout static buffers, the offloaded weights may be restored with the wrong layout. I tested the vllm-project#10251 behavior on Ascend A3 and reproduced accuracy issues from this mismatch. - vLLM version: v0.24.0 - vLLM main: vllm-project/vllm@85c09e9 --------- Signed-off-by: KINGFIOX <me@kingfiox.work> Co-authored-by: kurumi5210 <Jaychou1620@Gmail.com>
1 parent 9dcbeaa commit 093ce8c

7 files changed

Lines changed: 299 additions & 294 deletions

File tree

.github/workflows/scripts/test_config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@
268268
- vllm_ascend/worker/model_runner_v1.py
269269
- vllm_ascend/compilation/acl_graph.py
270270
tests:
271+
- tests/ut/model_executor
271272
- tests/e2e/pull_request/one_card/test_cpu_weight_offload.py
272273

273274
# Model Loader
@@ -717,7 +718,7 @@ estimated_times:
717718
tests/e2e/pull_request/one_card/test_camem.py: 140
718719
tests/e2e/pull_request/one_card/test_completion_with_prompt_embeds.py: 200
719720
tests/e2e/pull_request/one_card/test_cpu_offloading.py: 30
720-
tests/e2e/pull_request/one_card/test_cpu_weight_offload.py: 830
721+
tests/e2e/pull_request/one_card/test_cpu_weight_offload.py: 1400
721722
tests/e2e/pull_request/one_card/test_guided_decoding.py: 610
722723
tests/e2e/pull_request/one_card/test_minicpm.py: 330
723724
tests/e2e/pull_request/one_card/test_multi_instance.py: 140

tests/e2e/pull_request/one_card/test_cpu_weight_offload.py

Lines changed: 78 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -6,63 +6,109 @@
66
#
77
"""End-to-end tests for CPU weight offloading on Ascend NPU.
88
9-
Covers both the prefetch backend (NPUPrefetchOffloader) and the UVA
10-
backend (functional_call fallback path, since UVA is not available on
11-
NPU hardware). Tests verify that offloading produces the same outputs
9+
Covers both the prefetch backend (AscendPrefetchOffloader).
10+
Tests verify that offloading produces the same outputs
1211
as the baseline (no offloading).
1312
"""
1413

15-
import os
16-
1714
import pytest
1815

19-
from tests.e2e.conftest import wait_until_npu_memory_free
16+
from tests.e2e.conftest import VllmRunner, wait_until_npu_memory_free
17+
from tests.e2e.pull_request import utils as e2e_utils
2018
from tests.e2e.pull_request.utils import PROMPTS_SHORT, compare_logprobs
2119

2220
MODEL = "Qwen/Qwen3-0.6B"
2321

22+
_OFFLOAD_KEYS = {
23+
"offload_backend",
24+
"offload_group_size",
25+
"offload_num_in_group",
26+
"offload_prefetch_step",
27+
"offload_params",
28+
"cpu_offload_gb",
29+
}
30+
31+
32+
def _compare_offload_logprobs(
33+
runner_kwargs: dict,
34+
prompts: list[str],
35+
atol: float = 0.0689,
36+
decode_atol: float | None = None,
37+
) -> None:
38+
"""Compare prefetch/offload run against a no-offload eager baseline.
39+
40+
Unlike ``compare_logprobs``, this keeps ``additional_config`` (e.g.
41+
``weight_nz_mode``) on both sides and strips offload-related kwargs from
42+
the baseline so accuracy of the offloader itself is exercised.
43+
"""
44+
if decode_atol is None:
45+
decode_atol = 2 * atol
46+
47+
baseline_kwargs = {k: v for k, v in runner_kwargs.items() if k not in _OFFLOAD_KEYS}
48+
baseline_kwargs.pop("cudagraph_capture_sizes", None)
49+
baseline_kwargs["enforce_eager"] = True
50+
51+
# baseline(eager, no offload)
52+
with VllmRunner(**baseline_kwargs) as runner:
53+
baseline_outputs = runner.model.generate(
54+
prompts=prompts,
55+
sampling_params=e2e_utils._LOGPROB_SAMPLING_PARAMS,
56+
)
57+
58+
# enabled offload
59+
with VllmRunner(**runner_kwargs) as runner:
60+
offload_outputs = runner.model.generate(
61+
prompts=prompts,
62+
sampling_params=e2e_utils._LOGPROB_SAMPLING_PARAMS,
63+
)
64+
65+
for prompt_idx, (base_out, offload_out) in enumerate(zip(baseline_outputs, offload_outputs)):
66+
base_seq = base_out.outputs[0]
67+
offload_seq = offload_out.outputs[0]
68+
69+
assert base_seq.logprobs is not None and offload_seq.logprobs is not None, (
70+
f"logprobs not returned for prompt {prompt_idx}"
71+
)
72+
assert len(base_seq.token_ids) == len(offload_seq.token_ids) == 3, (
73+
f"Expected 3 tokens for prompt {prompt_idx}, "
74+
f"got baseline={len(base_seq.token_ids)}, offload={len(offload_seq.token_ids)}"
75+
)
76+
77+
e2e_utils._check_prefill_token(base_seq, offload_seq, prompt_idx, atol)
78+
for token_idx in range(1, 3):
79+
e2e_utils._check_decode_token(base_seq, offload_seq, token_idx, prompt_idx, decode_atol)
80+
2481

2582
# -------------------- Prefetch backend tests --------------------
2683

2784

85+
@pytest.mark.parametrize("enforce_eager", [True, False], ids=["eager", "graph"])
86+
@pytest.mark.parametrize("nz_mode", [0, 1], ids=["ND", "NZ"])
2887
@wait_until_npu_memory_free()
29-
def test_prefetch_offload_eager():
30-
"""Test prefetch CPU offloading in eager mode.
88+
def test_prefetch_offload_accuracy(enforce_eager, nz_mode):
89+
"""Test prefetch CPU offloading across eager/graph × ND/NZ.
3190
3291
Compares outputs between:
33-
1. Baseline (eager, no offloading)
92+
1. Baseline (eager, no offloading, same weight_nz_mode)
3493
2. Prefetch offloading (group_size=4, num_in_group=1)
35-
with enforce_eager=True (no ACL graph capture)
36-
"""
37-
runner_kwargs = {
38-
"model_name": MODEL,
39-
"max_model_len": 512,
40-
"enforce_eager": True,
41-
"offload_backend": "prefetch",
42-
"offload_group_size": 4,
43-
"offload_num_in_group": 1,
44-
}
45-
compare_logprobs(runner_kwargs=runner_kwargs, prompts=PROMPTS_SHORT)
46-
4794
48-
@wait_until_npu_memory_free()
49-
def test_prefetch_offload_aclgraph():
50-
"""Test prefetch CPU offloading with ACL graph capture.
51-
52-
Compares outputs between:
53-
1. Baseline (eager, no offloading)
54-
2. Prefetch offloading (group_size=4, num_in_group=1)
55-
with ACL graph capture enabled (default, non-eager)
95+
NZ uses weight_nz_mode=2 so BF16 weights are converted to FRACTAL_NZ
96+
(mode 1 only enables NZ for quantized weights).
5697
"""
57-
runner_kwargs = {
98+
runner_kwargs: dict = {
5899
"model_name": MODEL,
59100
"max_model_len": 512,
60-
"cudagraph_capture_sizes": [1, 2, 4, 8],
61101
"offload_backend": "prefetch",
62102
"offload_group_size": 4,
63103
"offload_num_in_group": 1,
104+
"additional_config": {"weight_nz_mode": nz_mode},
64105
}
65-
compare_logprobs(runner_kwargs=runner_kwargs, prompts=PROMPTS_SHORT)
106+
if enforce_eager:
107+
runner_kwargs["enforce_eager"] = True
108+
else:
109+
runner_kwargs["cudagraph_capture_sizes"] = [1, 2, 4, 8]
110+
111+
_compare_offload_logprobs(runner_kwargs=runner_kwargs, prompts=PROMPTS_SHORT)
66112

67113

68114
@wait_until_npu_memory_free()
@@ -83,48 +129,3 @@ def test_prefetch_offload_selective_params():
83129
"offload_params": {"gate_up_proj", "down_proj"},
84130
}
85131
compare_logprobs(runner_kwargs=runner_kwargs, prompts=PROMPTS_SHORT)
86-
87-
88-
# -------------------- UVA backend tests --------------------
89-
# UVA (Unified Virtual Addressing) is not available on Ascend NPU, so
90-
# the UVA offloader falls back to the functional_call path that moves
91-
# weights to device on-demand. Tests below mirror the upstream
92-
# test_cpu_offload.py parametrization but only exercise the non-UVA
93-
# (functional_call) path with enforce_eager, as NPU does not support
94-
# UVA zero-copy.
95-
96-
97-
@pytest.mark.parametrize("disable_pin_memory", [False, True])
98-
@wait_until_npu_memory_free()
99-
def test_uva_offload_functional_call(disable_pin_memory):
100-
"""Test UVA offloader's functional_call fallback on NPU.
101-
102-
With UVA disabled (forced by env var), the UVA offloader falls back
103-
to moving weights to device inside a functional_call wrapper.
104-
enforce_eager is required because this fallback is incompatible
105-
with graph capture.
106-
107-
Parametrized over pin_memory to cover both pinned and unpinned
108-
CPU storage paths.
109-
"""
110-
old_uva = os.environ.get("VLLM_WEIGHT_OFFLOADING_DISABLE_UVA")
111-
old_pin = os.environ.get("VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY")
112-
try:
113-
os.environ["VLLM_WEIGHT_OFFLOADING_DISABLE_UVA"] = "1"
114-
os.environ["VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY"] = str(int(disable_pin_memory))
115-
runner_kwargs = {
116-
"model_name": MODEL,
117-
"max_model_len": 512,
118-
"enforce_eager": True,
119-
"cpu_offload_gb": 1,
120-
}
121-
compare_logprobs(runner_kwargs=runner_kwargs, prompts=PROMPTS_SHORT)
122-
finally:
123-
if old_uva is None:
124-
os.environ.pop("VLLM_WEIGHT_OFFLOADING_DISABLE_UVA", None)
125-
else:
126-
os.environ["VLLM_WEIGHT_OFFLOADING_DISABLE_UVA"] = old_uva
127-
if old_pin is None:
128-
os.environ.pop("VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY", None)
129-
else:
130-
os.environ["VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY"] = old_pin
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from types import SimpleNamespace
2+
3+
from vllm.model_executor.offloader.base import NoopOffloader
4+
5+
from vllm_ascend.model_executor.offloader.base import create_offloader
6+
from vllm_ascend.model_executor.offloader.prefetch import _is_using_nz_weight
7+
8+
9+
def test_create_offloader_without_config_returns_noop():
10+
offloader = create_offloader(None)
11+
12+
assert isinstance(offloader, NoopOffloader)
13+
14+
15+
def test_create_offloader_for_non_prefetch_backend_returns_noop():
16+
offload_config = SimpleNamespace(
17+
offload_backend=None,
18+
prefetch=None,
19+
)
20+
21+
offloader = create_offloader(offload_config)
22+
23+
assert isinstance(offloader, NoopOffloader)
24+
25+
26+
def test_is_using_nz_weight_handles_invalid_npu_format(monkeypatch):
27+
param = SimpleNamespace(
28+
data=SimpleNamespace(device=SimpleNamespace(type="npu")),
29+
)
30+
31+
monkeypatch.setattr(
32+
"vllm_ascend.model_executor.offloader.prefetch.torch_npu.get_npu_format",
33+
lambda _: object(),
34+
raising=False,
35+
)
36+
37+
assert not _is_using_nz_weight(param)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Ascend-specific model parameter offloading."""
2+
3+
from vllm_ascend.model_executor.offloader.base import create_offloader
4+
from vllm_ascend.model_executor.offloader.prefetch import AscendPrefetchOffloader
5+
6+
__all__ = [
7+
"AscendPrefetchOffloader",
8+
"create_offloader",
9+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from typing import TYPE_CHECKING
2+
3+
from vllm.model_executor.offloader.base import BaseOffloader, NoopOffloader
4+
5+
from vllm_ascend.model_executor.offloader.prefetch import AscendPrefetchOffloader
6+
7+
if TYPE_CHECKING:
8+
from vllm.config import OffloadConfig
9+
10+
11+
def create_offloader(offload_config: "OffloadConfig | None") -> BaseOffloader:
12+
"""Create an Ascend-aware offloader while preserving vLLM defaults."""
13+
14+
if offload_config is None:
15+
return NoopOffloader()
16+
17+
backend = offload_config.offload_backend
18+
prefetch = offload_config.prefetch
19+
20+
if backend == "prefetch":
21+
return AscendPrefetchOffloader(
22+
group_size=prefetch.offload_group_size,
23+
num_in_group=prefetch.offload_num_in_group,
24+
prefetch_step=prefetch.offload_prefetch_step,
25+
offload_params=prefetch.offload_params,
26+
mode="cpu",
27+
)
28+
return NoopOffloader()

0 commit comments

Comments
 (0)