@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 648edcf729
Fork 0

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] Compose online quantization with quantized reloading (#38032)

Signed-off-by: Kyle Sayers <kylesayrs@gmail.com>

Kyle Sayers committed Mar 27, 2026 at 16:22 UTC 648edcf7291147fd3e76e9c525e3c5256e4725ee
10 files changed +184 -260
tests/model_executor/model_loader/test_reload.py
+57
@@ -148,3 +148,60 @@ def test_reload_weights(base_model, mul_model, add_model, tp_size, vllm_runner):
148 mul_perp = llm.generate_prompt_perplexity(["3 4 = 12"], mask=["3 4 ="])[0]
149 add_perp = llm.generate_prompt_perplexity(["3 4 = 7"], mask=["3 4 ="])[0]
150 assert add_perp < mul_perp
151 +
152 +
153 +@pytest.mark.parametrize("tp_size", [2])
154 +@pytest.mark.parametrize(
155 + "base_model,mul_model,add_model,quantization",
156 + [
157 + (
158 + "Qwen/Qwen3-0.6B",
159 + "inference-optimization/Qwen3-0.6B-debug-multiply",
160 + "inference-optimization/Qwen3-0.6B-debug-add",
161 + "fp8",
162 + ),
163 + (
164 + "inference-optimization/DeepSeek-V3-debug-empty",
165 + "inference-optimization/DeepSeek-V3-debug-multiply",
166 + "inference-optimization/DeepSeek-V3-debug-add",
167 + "fp8",
168 + ),
169 + (
170 + "Qwen/Qwen3-0.6B",
171 + "inference-optimization/Qwen3-0.6B-debug-multiply",
172 + "inference-optimization/Qwen3-0.6B-debug-add",
173 + "mxfp8",
174 + ),
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 + # ),
181 + ],
182 +)
183 +def test_online_quantize_reload(
184 + base_model, mul_model, add_model, quantization, tp_size, vllm_runner
185 +):
186 + if cuda_device_count_stateless() < tp_size:
187 + pytest.skip(reason="Not enough CUDA devices")
188 +
189 + if quantization == "fp8" and not current_platform.supports_fp8():
190 + pytest.skip(reason="Requires FP8 support")
191 +
192 + with vllm_runner(
193 + model_name=base_model,
194 + quantization=quantization,
195 + tensor_parallel_size=tp_size,
196 + enable_expert_parallel=(tp_size > 1 and "DeepSeek" in base_model),
197 + enable_prefix_caching=False,
198 + ) as llm:
199 + llm.collective_rpc("reload_weights", kwargs={"weights_path": mul_model})
200 + mul_perp = llm.generate_prompt_perplexity(["3 4 = 12"], mask=["3 4 ="])[0]
201 + add_perp = llm.generate_prompt_perplexity(["3 4 = 7"], mask=["3 4 ="])[0]
202 + assert mul_perp < add_perp
203 +
204 + llm.collective_rpc("reload_weights", kwargs={"weights_path": add_model})
205 + mul_perp = llm.generate_prompt_perplexity(["3 4 = 12"], mask=["3 4 ="])[0]
206 + add_perp = llm.generate_prompt_perplexity(["3 4 = 7"], mask=["3 4 ="])[0]
207 + assert add_perp < mul_perp
vllm/model_executor/layers/quantization/fp8.py
+27 -202
@@ -73,7 +73,9 @@ from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
73 cutlass_fp8_supported,
74 normalize_e4m3fn_to_e4m3fnuz,
75 )
76 -from vllm.model_executor.model_loader.weight_utils import initialize_single_dummy_weight
76 +from vllm.model_executor.model_loader.reload.layerwise import (
77 + initialize_online_processing,
78 +)
79 from vllm.model_executor.parameter import (
80 BlockQuantScaleParameter,
81 ModelWeightParameter,
@@ -496,8 +498,8 @@ class Fp8LinearMethod(LinearMethodBase):
498
499
500 class Fp8OnlineLinearMethod(Fp8LinearMethod):
499 - """Online version of Fp8LinearMethod, loads the fp16/bf16 checkpoint
500 - and quantized the weights during loading."""
501 + """Online version of Fp8LinearMethod which loads a full precision checkpoint
502 + and quantizes weights during loading."""
503
504 uses_meta_device: bool = True
505
@@ -519,84 +521,25 @@ class Fp8OnlineLinearMethod(Fp8LinearMethod):
521 layer.orig_dtype = params_dtype
522 layer.weight_block_size = None
523
522 - # WEIGHT
523 - def patched_weight_loader(param, loaded_weight, *args, **kwargs):
524 - # track how many elements we have updated
525 - if not hasattr(layer, "_loaded_numel"):
526 - layer._loaded_numel = 0
527 -
528 - # when the first `loaded_weight` is about to be
529 - # loaded to `param`, materialize `param` just-in-time
530 - weight = ModelWeightParameter(
531 - data=torch.empty_like(layer.weight, device=layer._load_device),
532 - input_dim=1,
533 - output_dim=0,
534 - weight_loader=patched_weight_loader,
535 - )
536 - _copy_missing_attrs(layer.weight, weight)
537 - layer.register_parameter("weight", weight)
538 - del layer._load_device
539 -
540 - # refresh the reference to `param` to reflect just-in-time
541 - # materialization
542 - param = layer.weight
543 -
544 - # load the current weight chunk
545 - copy_numel_counter = CopyNumelCounter()
546 - with copy_numel_counter:
547 - res = weight_loader(param, loaded_weight, *args, **kwargs) # type: ignore[misc]
548 - layer._loaded_numel += copy_numel_counter.copied_numel
549 -
550 - # if we have loaded all of the elements, call
551 - # process_weights_after_loading
552 - target_loaded_numel = layer.weight.numel()
553 - if layer._loaded_numel == target_loaded_numel:
554 - self.process_weights_after_loading(layer)
555 -
556 - # Prevent the usual `process_weights_after_loading` call from doing
557 - # anything
558 - layer._already_called_process_weights_after_loading = True
559 -
560 - # Note that we keep `layer._loaded_numel` around just in case
561 - # there is logic added to vllm in the future which calls a
562 - # weight loader twice - we do not want to re-initialize in
563 - # that case.
564 -
565 - return res
566 -
524 weight = ModelWeightParameter(
525 data=torch.empty(
526 output_size_per_partition,
527 input_size_per_partition,
571 - # materialized just-in-time in `patched_weight_loader`
572 - device="meta",
528 + device="meta", # materialized and processed during loading
529 dtype=params_dtype,
530 ),
531 input_dim=1,
532 output_dim=0,
577 - weight_loader=patched_weight_loader,
533 + weight_loader=weight_loader,
534 )
579 - # stash the correct device for `patched_weight_loader`
580 - layer._load_device = torch.get_default_device()
535 layer.register_parameter("weight", weight)
536
537 + initialize_online_processing(layer)
538 +
539 def process_weights_after_loading(self, layer: Module) -> None:
540 if getattr(layer, "_already_called_process_weights_after_loading", False):
541 return
542
587 - # deferred initialization of randomly initialized weights for the
588 - # `--load_format dummy` feature
589 - if layer.weight.device == torch.device("meta"):
590 - weight = ModelWeightParameter(
591 - data=torch.empty_like(layer.weight, device=layer._load_device),
592 - input_dim=1,
593 - output_dim=0,
594 - weight_loader=layer.weight.weight_loader,
595 - )
596 - _copy_missing_attrs(layer.weight, weight)
597 - layer.register_parameter("weight", weight)
598 - initialize_single_dummy_weight(layer.weight)
599 -
543 # TODO(future): support block_quant in online quant path
544 assert not self.block_quant
545
@@ -845,9 +788,6 @@ class Fp8MoEMethod(FusedMoEMethodBase):
788 )
789
790 def process_weights_after_loading(self, layer: Module) -> None:
848 - if getattr(layer, "_already_called_process_weights_after_loading", False):
849 - return
850 -
791 # Allow for accessing weights and scales in standard way.
792 w13 = layer.w13_weight
793 w2 = layer.w2_weight
@@ -892,9 +832,6 @@ class Fp8MoEMethod(FusedMoEMethodBase):
832 layer, w13, w2, w13_scale, w2_scale, w13_input_scale, w2_input_scale
833 )
834
895 - # Prevent duplicate processing (e.g., during weight reload)
896 - layer._already_called_process_weights_after_loading = True
897 -
835 def maybe_make_prepare_finalize(
836 self,
837 routing_tables: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
@@ -1013,86 +950,12 @@ class Fp8OnlineMoEMethod(Fp8MoEMethod):
950 layer.orig_dtype = params_dtype
951 layer.weight_block_size = None
952
1016 - # We are doing online quantization, patch the weight loaded
1017 - # to call `process_weights_after_loading` in a streaming fashion
1018 - # as soon as the last weight chunk is loaded.
1019 - weight_loader = extra_weight_attrs["weight_loader"]
1020 - # create a new holder to prevent modifying behavior of any other
1021 - # objects which might depend on the old one
1022 - new_extra_weight_attrs = extra_weight_attrs
1023 -
1024 - def patched_weight_loader(param, loaded_weight, *args, **kwargs):
1025 - # add a counter to track how many elements we have updated
1026 - if not hasattr(layer, "_loaded_numel"):
1027 - layer._loaded_numel = 0
1028 -
1029 - # save the ids of original w13 and w2 so that we can
1030 - # distinguish which one `param` should map to further
1031 - # down in this file
1032 - layer._w13_weight_orig_id = id(layer.w13_weight)
1033 - layer._w2_weight_orig_id = id(layer.w2_weight)
1034 -
1035 - # when the first `loaded_weight` is about to be
1036 - # loaded to `param`, materialize `param` just-in-time
1037 -
1038 - w13_weight = torch.nn.Parameter(
1039 - torch.empty_like(layer.w13_weight, device=layer._load_device),
1040 - requires_grad=False,
1041 - )
1042 - set_weight_attrs(w13_weight, extra_weight_attrs)
1043 - _copy_missing_attrs(layer.w13_weight, w13_weight)
1044 - layer.register_parameter("w13_weight", w13_weight)
1045 -
1046 - w2_weight = torch.nn.Parameter(
1047 - torch.empty_like(layer.w2_weight, device=layer._load_device),
1048 - requires_grad=False,
1049 - )
1050 - set_weight_attrs(w2_weight, extra_weight_attrs)
1051 - _copy_missing_attrs(layer.w2_weight, w2_weight)
1052 - layer.register_parameter("w2_weight", w2_weight)
1053 - del layer._load_device
1054 -
1055 - # refresh the reference to `param` to reflect just-in-time
1056 - # materialization
1057 - if id(param) == layer._w13_weight_orig_id:
1058 - param = layer.w13_weight
1059 - elif id(param) == layer._w2_weight_orig_id:
1060 - param = layer.w2_weight
1061 -
1062 - # load the current weight chunk
1063 - copy_numel_counter = CopyNumelCounter()
1064 - with copy_numel_counter:
1065 - res = weight_loader(param, loaded_weight, *args, **kwargs) # type: ignore[misc]
1066 - layer._loaded_numel += copy_numel_counter.copied_numel
1067 -
1068 - # if we have loaded all of the elements, call
1069 - # process_weights_after_loading
1070 - target_loaded_numel = layer.w13_weight.numel() + layer.w2_weight.numel()
1071 - if layer._loaded_numel == target_loaded_numel:
1072 - self.process_weights_after_loading(layer)
1073 -
1074 - # Prevent the usual `process_weights_after_loading` call
1075 - # from doing anything
1076 - layer._already_called_process_weights_after_loading = True
1077 -
1078 - # Note that we keep `layer._loaded_numel`,
1079 - # `layer._w13_weight_orig_id` and `layer._w2_weight_orig_id`
1080 - # around because if EP is on, weight loaders for non-local
1081 - # experts will run but not actually copy any elements, and we
1082 - # need to not re-initialize in that case.
1083 -
1084 - return res
1085 -
1086 - new_extra_weight_attrs["weight_loader"] = patched_weight_loader
1087 - extra_weight_attrs = new_extra_weight_attrs
1088 -
953 # WEIGHTS
954 w13_weight = torch.nn.Parameter(
955 torch.empty(
956 num_experts,
957 2 * intermediate_size_per_partition,
958 hidden_size,
1095 - # materialized just-in-time in `patched_weight_loader`
959 device="meta",
960 dtype=params_dtype,
961 ),
@@ -1106,91 +969,53 @@ class Fp8OnlineMoEMethod(Fp8MoEMethod):
969 num_experts,
970 hidden_size,
971 intermediate_size_per_partition,
1109 - # materialized just-in-time in `patched_weight_loader`
1110 - device="meta",
972 + device="meta", # materialized and processed during loading
973 dtype=params_dtype,
974 ),
975 requires_grad=False,
976 )
977 layer.register_parameter("w2_weight", w2_weight)
978 set_weight_attrs(w2_weight, extra_weight_attrs)
1117 - # stash the correct device for `patched_weight_loader`
1118 - layer._load_device = torch.get_default_device()
979
980 # BIASES (for models like GPT-OSS that have biased MoE)
981 if self.moe.has_bias:
1122 - # Use the original weight_loader (not patched) for biases
1123 - orig_extra_weight_attrs = dict(extra_weight_attrs)
1124 - orig_extra_weight_attrs["weight_loader"] = weight_loader
982 w13_bias = torch.nn.Parameter(
983 torch.zeros(
984 num_experts,
985 2 * intermediate_size_per_partition,
986 + device="meta", # materialized and processed during loading
987 dtype=layer.orig_dtype,
988 ),
989 requires_grad=False,
990 )
991 layer.register_parameter("w13_bias", w13_bias)
1134 - set_weight_attrs(w13_bias, orig_extra_weight_attrs)
992 + set_weight_attrs(w13_bias, extra_weight_attrs)
993 +
994 w2_bias = torch.nn.Parameter(
1136 - torch.zeros(num_experts, hidden_size, dtype=layer.orig_dtype),
995 + torch.zeros(
996 + num_experts,
997 + hidden_size,
998 + device="meta", # materialized and processed during loading
999 + dtype=layer.orig_dtype,
1000 + ),
1001 requires_grad=False,
1002 )
1003 layer.register_parameter("w2_bias", w2_bias)
1140 - set_weight_attrs(w2_bias, orig_extra_weight_attrs)
1141 -
1142 - # WEIGHT_SCALES
1143 - # Allocate 2 scales for w1 and w3 respectively.
1144 - # They will be combined to a single scale after weight loading.
1145 - w13_weight_scale = torch.nn.Parameter(
1146 - torch.ones(num_experts, dtype=torch.float32), requires_grad=False
1147 - )
1148 - w2_weight_scale = torch.nn.Parameter(
1149 - torch.ones(num_experts, dtype=torch.float32), requires_grad=False
1150 - )
1151 - layer.register_parameter("w13_weight_scale", w13_weight_scale)
1152 - layer.register_parameter("w2_weight_scale", w2_weight_scale)
1153 - set_weight_attrs(w13_weight_scale, extra_weight_attrs)
1154 - set_weight_attrs(w2_weight_scale, extra_weight_attrs)
1004 + set_weight_attrs(w2_bias, extra_weight_attrs)
1005
1156 - layer.w13_input_scale = None
1157 - layer.w2_input_scale = None
1006 + initialize_online_processing(layer)
1007
1008 def process_weights_after_loading(self, layer: Module) -> None:
1009 if getattr(layer, "_already_called_process_weights_after_loading", False):
1010 return
1011
1163 - # deferred initialization of randomly initialized weights for the
1164 - # `--load_format dummy` feature
1165 - if layer.w13_weight.device == torch.device("meta"):
1166 - w13_weight = torch.nn.Parameter(
1167 - torch.empty_like(layer.w13_weight, device=layer._load_device),
1168 - requires_grad=False,
1169 - )
1170 - set_weight_attrs(
1171 - w13_weight, {"weight_loader": layer.w13_weight.weight_loader}
1172 - )
1173 - _copy_missing_attrs(layer.w13_weight, w13_weight)
1174 - layer.register_parameter("w13_weight", w13_weight)
1175 - initialize_single_dummy_weight(layer.w13_weight)
1176 - if layer.w2_weight.device == torch.device("meta"):
1177 - w2_weight = torch.nn.Parameter(
1178 - torch.empty_like(layer.w2_weight, device=layer._load_device),
1179 - requires_grad=False,
1180 - )
1181 - set_weight_attrs(
1182 - w2_weight, {"weight_loader": layer.w2_weight.weight_loader}
1183 - )
1184 - _copy_missing_attrs(layer.w2_weight, w2_weight)
1185 - layer.register_parameter("w2_weight", w2_weight)
1186 - initialize_single_dummy_weight(layer.w2_weight)
1187 -
1188 - # If checkpoint is fp16, quantize in place.
1012 fp8_dtype = current_platform.fp8_dtype()
1013 w13 = torch.empty_like(layer.w13_weight, dtype=fp8_dtype)
1014 w2 = torch.empty_like(layer.w2_weight, dtype=fp8_dtype)
1192 - w13_scale = layer.w13_weight_scale
1193 - w2_scale = layer.w2_weight_scale
1015 + w13_scale = torch.ones(layer.num_experts, dtype=torch.float32)
1016 + w2_scale = torch.ones(layer.num_experts, dtype=torch.float32)
1017 + layer.w13_input_scale = None
1018 + layer.w2_input_scale = None
1019
1020 for expert in range(layer.local_num_experts):
1021 w13[expert, :, :], w13_scale[expert] = ops.scaled_fp8_quant(
@@ -1207,8 +1032,8 @@ class Fp8OnlineMoEMethod(Fp8MoEMethod):
1032 w2,
1033 w13_scale,
1034 w2_scale,
1210 - layer.w13_input_scale,
1211 - layer.w2_input_scale,
1035 + w13_input_scale=layer.w13_input_scale,
1036 + w2_input_scale=layer.w2_input_scale,
1037 )
1038
1039 # Prevent duplicate processing (e.g., during weight reload)
vllm/model_executor/layers/quantization/mxfp8.py
+2
@@ -337,6 +337,8 @@ class Mxfp8OnlineMoEMethod(Fp8OnlineMoEMethod):
337 w2 = torch.empty_like(layer.w2_weight, dtype=fp8_dtype)
338 w13_scale = layer.w13_weight_scale
339 w2_scale = layer.w2_weight_scale
340 + layer.w13_input_scale = None
341 + layer.w2_input_scale = None
342
343 w13, w13_scale = self._quantize_mxfp8_moe_weight(layer.w13_weight)
344 w2, w2_scale = self._quantize_mxfp8_moe_weight(layer.w2_weight)
vllm/model_executor/model_loader/base_loader.py
+19 -7
@@ -9,6 +9,7 @@ import vllm.envs as envs
9 from vllm.config import ModelConfig, VllmConfig
10 from vllm.config.load import LoadConfig
11 from vllm.logger import init_logger
12 +from vllm.model_executor.model_loader.reload import finalize_layerwise_processing
13 from vllm.model_executor.model_loader.utils import (
14 initialize_model,
15 process_weights_after_loading,
@@ -49,16 +50,13 @@ class BaseModelLoader(ABC):
50 device_config.device if load_config.device is None else load_config.device
51 )
52 target_device = torch.device(load_device)
52 - with set_default_torch_dtype(model_config.dtype):
53 - with target_device:
54 - model = initialize_model(
55 - vllm_config=vllm_config, model_config=model_config, prefix=prefix
56 - )
57 -
53 + with set_default_torch_dtype(model_config.dtype), target_device:
54 + model = initialize_model(
55 + vllm_config=vllm_config, model_config=model_config, prefix=prefix
56 + )
57 log_model_inspection(model)
58
59 logger.debug("Loading weights on %s ...", load_device)
61 - # Quantization does not happen in `load_weights` but after it
60 self.load_weights(model, model_config)
61
62 # Log peak GPU memory after loading weights. This is needed
@@ -71,6 +69,11 @@ class BaseModelLoader(ABC):
69 scope="local",
70 )
71
72 + # Process weights into kernel format. Note that when using online
73 + # quantization, weights are (typically) quantized as they are loaded.
74 + if _has_online_quant(model):
75 + finalize_layerwise_processing(model, model_config)
76 +
77 process_weights_after_loading(model, model_config, target_device)
78
79 return model.eval()
@@ -84,3 +87,12 @@ def log_model_inspection(model: nn.Module) -> None:
87 from vllm.model_inspection import format_model_inspection
88
89 logger.info("vLLM model structure:\n%s", format_model_inspection(model))
90 +
91 +
92 +def _has_online_quant(model: nn.Module):
93 + for module in model.modules():
94 + quant_method = getattr(module, "quant_method", None)
95 + if getattr(quant_method, "uses_meta_device", False):
96 + return True
97 +
98 + return False
vllm/model_executor/model_loader/dummy_loader.py
+9
@@ -1,10 +1,13 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import torch
4 import torch.nn as nn
5
6 from vllm.config import ModelConfig
7 from vllm.config.load import LoadConfig
8 from vllm.model_executor.model_loader.base_loader import BaseModelLoader
9 +from vllm.model_executor.model_loader.reload.meta import materialize_meta_tensor
10 +from vllm.model_executor.model_loader.reload.utils import get_layer_tensors
11 from vllm.model_executor.model_loader.weight_utils import initialize_dummy_weights
12
13
@@ -23,6 +26,12 @@ class DummyModelLoader(BaseModelLoader):
26 pass # Nothing to download
27
28 def load_weights(self, model: nn.Module, model_config: ModelConfig) -> None:
29 + # materialize meta tensors as part of online quantization lifecycle
30 + for layer in model.modules():
31 + for name, param in get_layer_tensors(layer).items():
32 + if param.device == torch.device("meta"):
33 + setattr(layer, name, materialize_meta_tensor(param))
34 +
35 # NOTE(woosuk): For accurate performance evaluation, we assign
36 # random values to the weights.
37 initialize_dummy_weights(model, model_config)
vllm/model_executor/model_loader/reload/__init__.py
+2
@@ -21,12 +21,14 @@ Limitations:
21 __all__ = [
22 "record_metadata_for_reloading",
23 "initialize_layerwise_reload",
24 + "finalize_layerwise_processing",
25 "finalize_layerwise_reload",
26 "set_torchao_reload_attrs",
27 "support_quantized_model_reload_from_hp_weights",
28 ]
29
30 from .layerwise import (
31 + finalize_layerwise_processing,
32 finalize_layerwise_reload,
33 initialize_layerwise_reload,
34 record_metadata_for_reloading,
vllm/model_executor/model_loader/reload/layerwise.py
+61 -29
@@ -28,6 +28,7 @@ __all__ = [
28 "get_layerwise_info",
29 "record_metadata_for_reloading",
30 "initialize_layerwise_reload",
31 + "finalize_layerwise_processing",
32 "finalize_layerwise_reload",
33 ]
34
@@ -89,7 +90,7 @@ def initialize_layerwise_reload(model: torch.nn.Module):
90 info = get_layerwise_info(layer)
91
92 # Skip if the layer has already been initialized
92 - if info.can_process():
93 + if info.can_load():
94 continue
95
96 # Save current tensors for later copying
@@ -98,15 +99,21 @@ def initialize_layerwise_reload(model: torch.nn.Module):
99 # Restore layer parameters/buffers onto meta device
100 restore_layer_on_meta(layer, info)
101
101 - # Track loading progress to determine when to process/copy
102 - info.load_numel = 0
103 - info.load_numel_total = get_layer_size(layer)
102 + initialize_online_processing(layer)
103
105 - # Wrap each parameter's weight loader
106 - # Note that nested wrapping will occur for shared tensors
107 - for name, tensor in get_layer_tensors(layer).items():
108 - if _get_weight_loader(tensor).__name__ != "online_process_loader":
109 - tensor.weight_loader = make_online_process_loader(layer, name)
104 +
105 +def initialize_online_processing(layer: torch.nn.Module):
106 + info = get_layerwise_info(layer)
107 +
108 + # Track loading progress to determine when to process/copy
109 + info.load_numel = 0
110 + info.load_numel_total = get_layer_size(layer)
111 +
112 + # Wrap each parameter's weight loader
113 + # Note that nested wrapping will occur for shared tensors
114 + for name, tensor in get_layer_tensors(layer).items():
115 + if _get_weight_loader(tensor).__name__ != "online_process_loader":
116 + tensor.weight_loader = make_online_process_loader(layer, name)
117
118
119 def make_online_process_loader(layer: torch.nn.Module, param_name: str) -> Callable:
@@ -118,7 +125,7 @@ def make_online_process_loader(layer: torch.nn.Module, param_name: str) -> Calla
125
126 @wraps(original_loader, assigned=("__doc__", "__annotations__"))
127 def online_process_loader(*args, **kwargs):
121 - if not info.can_process():
128 + if not info.can_load():
129 # Unfortunately, some qconfigs are set up to load the same weight
130 # multiple times. For example, CT_WNA16 loads `weight_shape` for
131 # each of the qkv partitions. This results in layers loading extra
@@ -140,7 +147,7 @@ def make_online_process_loader(layer: torch.nn.Module, param_name: str) -> Calla
147 bound_args = loader_signature.bind(*args, **kwargs)
148 bound_args.apply_defaults()
149
143 - # Cache loaded weights, track loading progress
150 + # Buffer loaded weights, track loading progress
151 info.loaded_weights.append((param_name, bound_args))
152 num_loaded, ret = get_numel_loaded(original_loader, bound_args)
153 info.load_numel += num_loaded
@@ -163,19 +170,26 @@ def make_online_process_loader(layer: torch.nn.Module, param_name: str) -> Calla
170 return online_process_loader
171
172
166 -def finalize_layerwise_reload(model: torch.nn.Module, model_config: ModelConfig):
173 +def finalize_layerwise_processing(model: torch.nn.Module, model_config: ModelConfig):
174 """
168 - Remove the outermost layer of weight loading wrappers.
175 + Apply processing to any layers which were not layerwise processed during loading.
176 + This includes attention layers and layers which have weight elements which are not
177 + loaded (due to padding).
178
179 This function should be applied after `initialize_layerwise_reload` is applied
180 unwrap the layerwise weight loaders.
181
173 - Also processes Attention/MLA layers, which must be processed after all other layers
182 + :param model: model to finalize processing for
183 + :param model_config: config needed for applying processing to attention layers
184 """
175 - model._do_torchao_reload = model._original_do_torchao_reload
185 + if hasattr(model, "_original_do_torchao_reload"):
186 + model._do_torchao_reload = model._original_do_torchao_reload
187
188 for layer in model.modules():
189 info = get_layerwise_info(layer)
190 + if not info.can_load():
191 + info.reset()
192 + continue
193
194 # Attention/MLA layers are processed after all other layers
195 if isinstance(layer, (Attention, MLAAttention)):
@@ -184,17 +198,29 @@ def finalize_layerwise_reload(model: torch.nn.Module, model_config: ModelConfig)
198 "Layerwise reloading of Q/K/V scale weights is not implemented yet"
199 )
200
201 + elif info.kernel_tensors is None:
202 + raise NotImplementedError(
203 + "Layerwise loading of Q/K/V scale weights is not implemented yet"
204 + )
205 +
206 else:
207 _place_kernel_tensors(layer, info)
208 layer.process_weights_after_loading(model_config.dtype)
209
191 - # No weights were loaded, place kernel tensors back
192 - elif info.can_process() and info.load_numel <= 0:
193 - _place_kernel_tensors(layer, info)
210 + # No weights were loaded
211 + elif info.load_numel <= 0:
212 + # first load but received no weights. This happens on dummy load
213 + if info.kernel_tensors is None:
214 + materialize_layer(layer)
215 +
216 + # reloading: place kernel tensors back as a fallback
217 + else:
218 + logger.warning("%s: Failed to load weights", layer.__class__.__name__)
219 + _place_kernel_tensors(layer, info)
220
221 # Process non-attention layers which did not load all elements. This can happen
222 # if the created weight has extra padding elements which are not loaded
197 - # Having too many of these delayed layers can lead to execess memory usage
223 + # Having too many of these delayed layers can lead to excess memory usage
224 # see Limitations(4)
225 elif info.load_numel > 0 and info.load_numel < info.load_numel_total: # type: ignore[operator]
226 logger.debug("%s: Delayed processing", layer.__class__.__name__)
@@ -203,20 +229,24 @@ def finalize_layerwise_reload(model: torch.nn.Module, model_config: ModelConfig)
229 info.reset()
230
231
232 +def finalize_layerwise_reload(*args, **kwargs):
233 + finalize_layerwise_processing(*args, **kwargs)
234 +
235 +
236 def _layerwise_process(layer: torch.nn.Module, info: LayerReloadingInfo):
237 """
208 - Finalize layer loading after all weights have been cached.
238 + Finalize layer loading after all weights have been buffered.
239
240 This function:
241 1. Materializes the layer onto the target device
212 - 2. Loads all cached weights
242 + 2. Loads all buffered weights
243 3. Runs quantization processing if applicable
244 4. Copies processed values back to original tensor storage
245 """
246 # Materialize layer tensors onto device
247 materialize_layer(layer)
248
219 - # Reset FP8 online quantization flag so process_weights_after_loading
249 + # Reset online quantization flag so process_weights_after_loading
250 # will run again during reload
251 if hasattr(layer, "_already_called_process_weights_after_loading"):
252 delattr(layer, "_already_called_process_weights_after_loading")
@@ -225,7 +255,7 @@ def _layerwise_process(layer: torch.nn.Module, info: LayerReloadingInfo):
255 for param in get_layer_tensors(layer).values():
256 param.weight_loader = _get_original_loader(param)
257
228 - # Load all cached weights into materialized layer (using original loaders)
258 + # Load all buffered weights into materialized layer (using original loaders)
259 for name, args in info.loaded_weights:
260 param = getattr(layer, name)
261 args.arguments["param"] = param
@@ -239,13 +269,14 @@ def _layerwise_process(layer: torch.nn.Module, info: LayerReloadingInfo):
269
270 # Copy processed values into original tensor storage (preserves cudagraph refs)
271 # this code is a no-op if not reloading (because kernel tensors is empty)
242 - parameters, buffers = info.kernel_tensors
243 - for name, param in parameters.items():
244 - param.data.copy_(getattr(layer, name))
245 - for name, buffer in buffers.items():
246 - buffer.data.copy_(getattr(layer, name))
272 + if info.kernel_tensors is not None:
273 + parameters, buffers = info.kernel_tensors
274 + for name, param in parameters.items():
275 + param.data.copy_(getattr(layer, name))
276 + for name, buffer in buffers.items():
277 + buffer.data.copy_(getattr(layer, name))
278
248 - _place_kernel_tensors(layer, info)
279 + _place_kernel_tensors(layer, info)
280
281 info.reset()
282 logger.debug("%s: Processed", layer.__class__.__name__)
@@ -268,6 +299,7 @@ def _place_kernel_tensors(layer: torch.nn.Module, info: LayerReloadingInfo):
299 for name in get_layer_tensors(layer):
300 delattr(layer, name)
301
302 + assert info.kernel_tensors is not None
303 parameters, buffers = info.kernel_tensors
304 for name, param in parameters.items():
305 layer.register_parameter(name, param)
vllm/model_executor/model_loader/reload/meta.py
+3 -4
@@ -104,7 +104,7 @@ def materialize_layer(layer: torch.nn.Module) -> None:
104 setattr(layer, name, materialize_meta_tensor(tensor))
105
106
107 -class MetaCopyCounter(TorchDispatchMode):
107 +class CopyCounter(TorchDispatchMode):
108 """
109 Tracks total number of elements modified with `copy_`.
110
@@ -122,7 +122,7 @@ class MetaCopyCounter(TorchDispatchMode):
122 if kwargs is None:
123 kwargs = {}
124
125 - if func is torch.ops.aten.copy_.default and args[0].device.type == "meta":
125 + if func is torch.ops.aten.copy_.default:
126 assert args[0].numel() == args[1].numel()
127 self.copied_numel += args[0].numel()
128
@@ -140,7 +140,6 @@ def get_numel_loaded(
140 :return: number of elements loaded by the weight loader, the return value of the
141 weight loader
142 """
143 - assert args.arguments["param"].device.type == "meta"
144 - with MetaCopyCounter() as counter:
143 + with CopyCounter() as counter:
144 return_value = weight_loader(*args.args, **args.kwargs)
145 return counter.copied_numel, return_value
vllm/model_executor/model_loader/reload/types.py
+3 -3
@@ -16,8 +16,8 @@ class LayerReloadingInfo:
16 # model format (meta), populated by `record_metadata_for_reloading`
17 restore_metadata: LayerTensors = field(default_factory=lambda: ({}, {}))
18
19 - # kernel format (device)
20 - kernel_tensors: LayerTensors = field(default_factory=lambda: ({}, {}))
19 + # kernel format (device), used to copy into when reloading only
20 + kernel_tensors: LayerTensors | None = None
21
22 # track how many restored elements are ready for loading
23 load_numel: int = 0
@@ -29,5 +29,5 @@ class LayerReloadingInfo:
29 def reset(self):
30 self.__init__(restore_metadata=self.restore_metadata) # type: ignore[misc]
31
32 - def can_process(self) -> bool:
32 + def can_load(self) -> bool:
33 return self.load_numel_total is not None
vllm/model_executor/model_loader/weight_utils.py
+1 -15
@@ -1323,25 +1323,11 @@ def initialize_dummy_weights(
1323 is fixed, the random values generated by this function only depends on
1324 the parameter's number of elements and its data type.
1325 """
1326 -
1327 - # Check if any module uses online quantization with meta device weights.
1328 - # If so, we'll skip initializing params on meta device since they'll be
1329 - # handled in `process_weights_after_loading`.
1330 - def uses_meta_device(module: torch.nn.Module) -> bool:
1331 - quant_method = getattr(module, "quant_method", None)
1332 - return getattr(quant_method, "uses_meta_device", False)
1333 -
1334 - has_online_quant = any(uses_meta_device(m) for m in model.modules())
1335 -
1326 for param in model.state_dict().values():
1337 - if has_online_quant and param.device == torch.device("meta"):
1338 - # For online quantization, weights are created on meta device and
1339 - # dummy weight init will happen in `process_weights_after_loading`.
1340 - continue
1341 -
1327 initialize_single_dummy_weight(param, low, high, seed)
1328
1329
1330 +@torch.no_grad()
1331 def initialize_single_dummy_weight(
1332 param: torch.Tensor,
1333 low: float = -1e-3,