A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 5, 21:00 UTC
[QeRL] Fix online quantized reloading (#38442)
Signed-off-by: Kyle Sayers <kylesayrs@gmail.com>
Kyle Sayers committed
Mar 29, 2026 at 16:56 UTC
d28d86e8a34bf2617be294c235d6e6ef3321917b
9 files changed
+104
-62
.buildkite/test-amd.yaml
+3
-3
@@ -812,7 +812,7 @@ steps:
812
commands:
813
- apt-get update && apt-get install -y curl libsodium23
814
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
815
- - pytest -v -s model_executor
815
+ - pytest -v -s model_executor -m '(not slow_test)'
816
- pytest -v -s entrypoints/openai/completion/test_tensorizer_entrypoint.py
817
818
@@ -1242,7 +1242,7 @@ steps:
1242
- vllm/platforms/rocm.py
1243
commands:
1244
- TARGET_TEST_SUITE=L4 pytest basic_correctness/ -v -s -m 'distributed(num_gpus=2)'
1245
- - CUDA_VISIBLE_DEVICES=0,1 pytest -v -s model_executor/model_loader/test_sharded_state_loader.py
1245
+ - CUDA_VISIBLE_DEVICES=0,1 pytest -v -s model_executor/model_loader/test_sharded_state_loader.py -m '(not slow_test)'
1246
- pytest models/test_transformers.py -v -s -m 'distributed(num_gpus=2)'
1247
- pytest models/language -v -s -m 'distributed(num_gpus=2)'
1248
- pytest models/multimodal -v -s -m 'distributed(num_gpus=2)' --ignore models/multimodal/generation/test_whisper.py
@@ -2501,7 +2501,7 @@ steps:
2501
- tests/models/
2502
commands:
2503
- TARGET_TEST_SUITE=L4 pytest basic_correctness/ -v -s -m 'distributed(num_gpus=2)'
2504
- - CUDA_VISIBLE_DEVICES=0,1 pytest -v -s model_executor/model_loader/test_sharded_state_loader.py
2504
+ - CUDA_VISIBLE_DEVICES=0,1 pytest -v -s model_executor/model_loader/test_sharded_state_loader.py -m '(not slow_test)'
2505
- pytest models/test_transformers.py -v -s -m 'distributed(num_gpus=2)'
2506
- pytest models/language -v -s -m 'distributed(num_gpus=2)'
2507
- pytest models/multimodal -v -s -m 'distributed(num_gpus=2)' --ignore models/multimodal/generation/test_whisper.py
.buildkite/test_areas/model_executor.yaml
+1
-1
@@ -13,5 +13,5 @@ steps:
13
commands:
14
- apt-get update && apt-get install -y curl libsodium23
15
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
16
- - pytest -v -s model_executor
16
+ - pytest -v -s model_executor -m '(not slow_test)'
17
- pytest -v -s entrypoints/openai/completion/test_tensorizer_entrypoint.py
.buildkite/test_areas/models_distributed.yaml
+1
-1
@@ -14,7 +14,7 @@ steps:
14
- tests/models/
15
commands:
16
- TARGET_TEST_SUITE=L4 pytest basic_correctness/ -v -s -m 'distributed(num_gpus=2)'
17
- - CUDA_VISIBLE_DEVICES=0,1 pytest -v -s model_executor/model_loader/test_sharded_state_loader.py
17
+ - CUDA_VISIBLE_DEVICES=0,1 pytest -v -s model_executor/model_loader/test_sharded_state_loader.py -m '(not slow_test)'
18
# Avoid importing model tests that cause CUDA reinitialization error
19
- pytest models/test_transformers.py -v -s -m 'distributed(num_gpus=2)'
20
- pytest models/language -v -s -m 'distributed(num_gpus=2)'
tests/model_executor/model_loader/test_reload.py
+45
-20
@@ -38,7 +38,10 @@ def test_move_metatensors():
38
39
def test_reload_lifecycle():
40
layer = torch.nn.Linear(2, 3)
41
- info = LayerReloadingInfo(restore_metadata=capture_layer_to_meta(layer))
41
+ info = LayerReloadingInfo(
42
+ restore_metadata=capture_layer_to_meta(layer),
43
+ restore_device=torch.device("cpu"),
44
+ )
45
46
restore_layer_on_meta(layer, info)
47
for name, tensor in get_layer_tensors(layer).items():
@@ -48,7 +51,7 @@ def test_reload_lifecycle():
51
assert tensor.__class__ == meta_tensor.__class__
52
assert tensor.__dict__ == meta_tensor.__dict__
53
51
- materialize_layer(layer)
54
+ materialize_layer(layer, info)
55
for name, tensor in get_layer_tensors(layer).items():
56
materialized_tensor = getattr(layer, name)
57
assert tensor.dtype == materialized_tensor.dtype
@@ -60,7 +63,10 @@ def test_reload_lifecycle():
63
def test_model_cleanup(dist_init, default_vllm_config):
64
layer = QKVParallelLinear(2, 3, 4)
65
assert layer.weight.weight_loader.__self__ is layer
63
- info = LayerReloadingInfo(restore_metadata=capture_layer_to_meta(layer))
66
+ info = LayerReloadingInfo(
67
+ restore_metadata=capture_layer_to_meta(layer),
68
+ restore_device=torch.device("cpu"),
69
+ )
70
71
mock_info_dict: WeakKeyDictionary[torch.nn.Module, LayerReloadingInfo] = (
72
WeakKeyDictionary()
@@ -90,39 +96,46 @@ def test_get_numel_loaded():
96
assert ret == "value"
97
98
93
-@pytest.mark.parametrize("tp_size", [2])
99
+@pytest.mark.parametrize(
100
+ "tp_size", [pytest.param(1), pytest.param(2, marks=[pytest.mark.slow_test])]
101
+)
102
@pytest.mark.parametrize(
103
"base_model,mul_model,add_model",
104
[
97
- (
105
+ pytest.param(
106
"Qwen/Qwen3-0.6B",
107
"inference-optimization/Qwen3-0.6B-debug-multiply",
108
"inference-optimization/Qwen3-0.6B-debug-add",
109
+ marks=[pytest.mark.slow_test],
110
),
102
- (
111
+ pytest.param(
112
"inference-optimization/Qwen3-0.6B-FP8_BLOCK",
113
"inference-optimization/Qwen3-0.6B-debug-multiply-FP8_BLOCK",
114
"inference-optimization/Qwen3-0.6B-debug-add-FP8_BLOCK",
115
+ marks=[pytest.mark.slow_test],
116
),
107
- (
117
+ pytest.param(
118
"inference-optimization/Qwen3-0.6B-W4A16-G128",
119
"inference-optimization/Qwen3-0.6B-debug-multiply-W4A16-G128",
120
"inference-optimization/Qwen3-0.6B-debug-add-W4A16-G128",
121
+ marks=[pytest.mark.slow_test],
122
),
112
- (
123
+ pytest.param(
124
"inference-optimization/DeepSeek-V3-debug-empty",
125
"inference-optimization/DeepSeek-V3-debug-multiply",
126
"inference-optimization/DeepSeek-V3-debug-add",
127
+ marks=[pytest.mark.slow_test],
128
),
117
- (
129
+ pytest.param(
130
"inference-optimization/DeepSeek-V3-debug-empty-FP8_DYNAMIC",
131
"inference-optimization/DeepSeek-V3-debug-multiply-FP8_DYNAMIC",
132
"inference-optimization/DeepSeek-V3-debug-add-FP8_DYNAMIC",
133
),
122
- (
134
+ pytest.param(
135
"inference-optimization/DeepSeek-V3-debug-empty-NVFP4A16",
136
"inference-optimization/DeepSeek-V3-debug-multiply-NVFP4A16",
137
"inference-optimization/DeepSeek-V3-debug-add-NVFP4A16",
138
+ marks=[pytest.mark.slow_test],
139
),
140
],
141
)
@@ -138,6 +151,8 @@ def test_reload_weights(base_model, mul_model, add_model, tp_size, vllm_runner):
151
tensor_parallel_size=tp_size,
152
enable_expert_parallel=(tp_size > 1 and "DeepSeek" in base_model),
153
enable_prefix_caching=False,
154
+ max_model_len=16,
155
+ max_num_seqs=1,
156
) as llm:
157
llm.collective_rpc("reload_weights", kwargs={"weights_path": mul_model})
158
mul_perp = llm.generate_prompt_perplexity(["3 4 = 12"], mask=["3 4 ="])[0]
@@ -150,34 +165,42 @@ def test_reload_weights(base_model, mul_model, add_model, tp_size, vllm_runner):
165
assert add_perp < mul_perp
166
167
153
-@pytest.mark.parametrize("tp_size", [2])
168
+@pytest.mark.parametrize(
169
+ "tp_size", [pytest.param(1), pytest.param(2, marks=[pytest.mark.slow_test])]
170
+)
171
@pytest.mark.parametrize(
172
"base_model,mul_model,add_model,quantization",
173
[
157
- (
174
+ pytest.param(
175
"Qwen/Qwen3-0.6B",
176
"inference-optimization/Qwen3-0.6B-debug-multiply",
177
"inference-optimization/Qwen3-0.6B-debug-add",
178
"fp8",
179
),
163
- (
180
+ pytest.param(
181
"inference-optimization/DeepSeek-V3-debug-empty",
182
"inference-optimization/DeepSeek-V3-debug-multiply",
183
"inference-optimization/DeepSeek-V3-debug-add",
184
"fp8",
185
+ marks=[pytest.mark.slow_test],
186
),
169
- (
187
+ pytest.param(
188
"Qwen/Qwen3-0.6B",
189
"inference-optimization/Qwen3-0.6B-debug-multiply",
190
"inference-optimization/Qwen3-0.6B-debug-add",
191
"mxfp8",
192
+ marks=[pytest.mark.slow_test],
193
+ ),
194
+ pytest.param(
195
+ "inference-optimization/DeepSeek-V3-debug-empty",
196
+ "inference-optimization/DeepSeek-V3-debug-multiply",
197
+ "inference-optimization/DeepSeek-V3-debug-add",
198
+ "mxfp8",
199
+ marks=[
200
+ pytest.mark.slow_test,
201
+ pytest.mark.xfail(reason="mxfp4 & mla is not supported yet"),
202
+ ],
203
),
175
- # ( TODO: support mxfp4 & mla
176
- # "inference-optimization/DeepSeek-V3-debug-empty",
177
- # "inference-optimization/DeepSeek-V3-debug-multiply",
178
- # "inference-optimization/DeepSeek-V3-debug-add",
179
- # "mxfp8",
180
- # ),
204
],
205
)
206
def test_online_quantize_reload(
@@ -195,6 +218,8 @@ def test_online_quantize_reload(
218
tensor_parallel_size=tp_size,
219
enable_expert_parallel=(tp_size > 1 and "DeepSeek" in base_model),
220
enable_prefix_caching=False,
221
+ max_model_len=16,
222
+ max_num_seqs=1,
223
) as llm:
224
llm.collective_rpc("reload_weights", kwargs={"weights_path": mul_model})
225
mul_perp = llm.generate_prompt_perplexity(["3 4 = 12"], mask=["3 4 ="])[0]
vllm/model_executor/layers/quantization/fp8.py
+5
-2
@@ -1006,14 +1006,17 @@ class Fp8OnlineMoEMethod(Fp8MoEMethod):
1006
initialize_online_processing(layer)
1007
1008
def process_weights_after_loading(self, layer: Module) -> None:
1009
+ # TODO(@ksayers): inplace fp8 quant kernel, initialize scales with ones
1010
if getattr(layer, "_already_called_process_weights_after_loading", False):
1011
return
1012
1013
fp8_dtype = current_platform.fp8_dtype()
1014
w13 = torch.empty_like(layer.w13_weight, dtype=fp8_dtype)
1015
w2 = torch.empty_like(layer.w2_weight, dtype=fp8_dtype)
1015
- w13_scale = torch.ones(layer.num_experts, dtype=torch.float32)
1016
- w2_scale = torch.ones(layer.num_experts, dtype=torch.float32)
1016
+ w13_scale = torch.ones(
1017
+ layer.num_experts, device=w13.device, dtype=torch.float32
1018
+ )
1019
+ w2_scale = torch.ones(layer.num_experts, device=w2.device, dtype=torch.float32)
1020
layer.w13_input_scale = None
1021
layer.w2_input_scale = None
1022
vllm/model_executor/model_loader/reload/layerwise.py
+15
-3
@@ -49,7 +49,10 @@ def get_layerwise_info(layer: torch.nn.Module) -> LayerReloadingInfo:
49
information existed, a new entry is constructed
50
"""
51
if layer not in LAYERWISE_INFO:
52
- LAYERWISE_INFO[layer] = LayerReloadingInfo()
52
+ LAYERWISE_INFO[layer] = LayerReloadingInfo(
53
+ restore_metadata=({}, {}),
54
+ restore_device=torch.get_default_device(),
55
+ )
56
57
return LAYERWISE_INFO[layer]
58
@@ -64,6 +67,7 @@ def record_metadata_for_reloading(model: torch.nn.Module):
67
for layer in model.modules():
68
info = get_layerwise_info(layer)
69
info.restore_metadata = capture_layer_to_meta(layer)
70
+ info.restore_device = torch.get_default_device()
71
72
73
@torch.no_grad()
@@ -99,10 +103,18 @@ def initialize_layerwise_reload(model: torch.nn.Module):
103
# Restore layer parameters/buffers onto meta device
104
restore_layer_on_meta(layer, info)
105
106
+ # Wrap weight loaders to buffer loading
107
initialize_online_processing(layer)
108
109
110
def initialize_online_processing(layer: torch.nn.Module):
111
+ """
112
+ Wrap a layer's weight loaders with online processing loaders.
113
+ Called by either `initialize_layerwise_reload` or an online quantization scheme,
114
+ prevents double wrapping in the case of online quantization + reloading
115
+
116
+ :param layer: layer whose parameter weight loaders will be wrapped
117
+ """
118
info = get_layerwise_info(layer)
119
120
# Track loading progress to determine when to process/copy
@@ -211,7 +223,7 @@ def finalize_layerwise_processing(model: torch.nn.Module, model_config: ModelCon
223
elif info.load_numel <= 0:
224
# first load but received no weights. This happens on dummy load
225
if info.kernel_tensors is None:
214
- materialize_layer(layer)
226
+ materialize_layer(layer, info)
227
228
# reloading: place kernel tensors back as a fallback
229
else:
@@ -244,7 +256,7 @@ def _layerwise_process(layer: torch.nn.Module, info: LayerReloadingInfo):
256
4. Copies processed values back to original tensor storage
257
"""
258
# Materialize layer tensors onto device
247
- materialize_layer(layer)
259
+ materialize_layer(layer, info)
260
261
# Reset online quantization flag so process_weights_after_loading
262
# will run again during reload
vllm/model_executor/model_loader/reload/meta.py
+5
-4
@@ -94,14 +94,15 @@ def restore_layer_on_meta(layer: torch.nn.Module, info: LayerReloadingInfo):
94
layer.register_buffer(name, buffer)
95
96
97
-def materialize_layer(layer: torch.nn.Module) -> None:
97
+def materialize_layer(layer: torch.nn.Module, info: LayerReloadingInfo):
98
"""Materialize all meta tensors in a layer to actual tensors."""
99
if layer.__class__.__name__ in SKIP_MODULES:
100
return
101
102
- for name, tensor in get_layer_tensors(layer).items():
103
- if name not in SKIP_TENSORS:
104
- setattr(layer, name, materialize_meta_tensor(tensor))
102
+ with info.restore_device:
103
+ for name, tensor in get_layer_tensors(layer).items():
104
+ if name not in SKIP_TENSORS:
105
+ setattr(layer, name, materialize_meta_tensor(tensor))
106
107
108
class CopyCounter(TorchDispatchMode):
vllm/model_executor/model_loader/reload/types.py
+12
-7
@@ -13,21 +13,26 @@ LayerTensors = tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]
13
14
@dataclass
15
class LayerReloadingInfo:
16
- # model format (meta), populated by `record_metadata_for_reloading`
17
- restore_metadata: LayerTensors = field(default_factory=lambda: ({}, {}))
16
+ # model format metadata, recorded by `record_metadata_for_reloading`
17
+ restore_metadata: LayerTensors
18
19
- # kernel format (device), used to copy into when reloading only
20
- kernel_tensors: LayerTensors | None = None
19
+ # device to materialize layers with, recorded by `record_metadata_for_reloading`
20
+ restore_device: torch.device
21
22
- # track how many restored elements are ready for loading
22
+ # track how many elements are ready for loading, used by `online_process_loader`
23
load_numel: int = 0
24
load_numel_total: int | None = None
25
26
- # stores arguments and tensors ready for loading
26
+ # used by `online_process_loader` to buffer args and tensors until ready to load
27
loaded_weights: list[tuple[str, BoundArguments]] = field(default_factory=list)
28
29
+ # kernel formatted tensors, copied into by `_layerwise_process` when reloading
30
+ kernel_tensors: LayerTensors | None = None
31
+
32
def reset(self):
30
- self.__init__(restore_metadata=self.restore_metadata) # type: ignore[misc]
33
+ self.__init__( # type: ignore[misc]
34
+ restore_metadata=self.restore_metadata, restore_device=self.restore_device
35
+ )
36
37
def can_load(self) -> bool:
38
return self.load_numel_total is not None
vllm/v1/worker/gpu_model_runner.py
+17
-21
@@ -4943,28 +4943,24 @@ class GPUModelRunner(
4943
4944
# begin loading weights
4945
logger.info_once("Reloading weights inplace...", scope="local")
4946
- load_device = (
4947
- self.vllm_config.load_config.device or self.vllm_config.device_config.device
4948
- )
4949
- with torch.device(load_device):
4950
- if is_checkpoint_format:
4951
- # load weights from checkpoint/ original model format
4952
- initialize_layerwise_reload(model)
4953
- loaded_weights = model.load_weights(weights_iterator)
4954
- finalize_layerwise_reload(model, self.model_config)
4946
+ if is_checkpoint_format:
4947
+ # load weights from checkpoint/ original model format
4948
+ initialize_layerwise_reload(model)
4949
+ loaded_weights = model.load_weights(weights_iterator)
4950
+ finalize_layerwise_reload(model, self.model_config)
4951
4956
- else:
4957
- # load weights from kernel format
4958
- logger.warning_once(
4959
- "Reloading with `is_checkpoint_format=True` requires that "
4960
- "weights be in kernel format and already sharded",
4961
- scope="local",
4962
- )
4963
- loaded_weights = set()
4964
- for name, loaded_weight in weights_iterator:
4965
- param = model.get_parameter(name) # TODO: buffers?
4966
- param.copy_(loaded_weight)
4967
- loaded_weights.add(name)
4952
+ else:
4953
+ # load weights from kernel format
4954
+ logger.warning_once(
4955
+ "Reloading with `is_checkpoint_format=True` requires that "
4956
+ "weights be in kernel format and already sharded",
4957
+ scope="local",
4958
+ )
4959
+ loaded_weights = set()
4960
+ for name, loaded_weight in weights_iterator:
4961
+ param = model.get_parameter(name) # TODO: buffers?
4962
+ param.copy_(loaded_weight)
4963
+ loaded_weights.add(name)
4964
4965
# logging and validation
4966
counter_after_reloading = time.perf_counter()