[Fast Start] Support fp4 (#55465)
Signed-off-by: Isotr0py <Isotr0py@outlook.com> Co-authored-by: Isotr0py <Isotr0py@outlook.com>
siyu committed
Sep 10, 2026 at 21:22 UTC
73fb19151f4ff461196d7fb5d82e9cb4d57234f6
14 files changed
+232
-106
tests/model_executor/model_loader/test_weight_cache.py
+85
-32
@@ -7,34 +7,33 @@ warm restarts (weights mapped from the daemon via CUDA IPC) must both serve
7
identical outputs.
8
"""
9
10
+import os
11
import shutil
12
import subprocess
13
import sys
14
import tempfile
15
import threading
16
import time
17
+from dataclasses import dataclass, field
18
+from typing import Any
19
20
import pytest
21
22
from vllm import SamplingParams
23
+from vllm.assets.image import ImageAsset
24
+from vllm.model_executor.model_loader.weight_cache.protocol import get_socket_path
25
from vllm.platforms import current_platform
26
22
-MODEL = "Qwen/Qwen3.5-0.8B"
23
-PROMPTS = [
24
- "Hello, my name is",
25
- "The capital of France is",
26
-]
27
DAEMON_TIMEOUT_S = 600
28
29
30
class WeightCacheDaemon:
31
"""Context manager running the real weight cache daemon as a subprocess."""
32
33
- _READY_MARKER = "Weight cache daemon READY"
34
-
35
- def __init__(self, model: str, tp_size: int):
33
+ def __init__(self, model: str, tp_size: int, extra_args: list[str] | None = None):
34
# Short base path: Unix socket paths are limited to ~107 characters.
35
self.socket_dir = tempfile.mkdtemp(prefix="vllm_ipc_")
36
+ self.tp_size = tp_size
37
self._cmd = [
38
sys.executable,
39
"-m",
@@ -46,6 +45,7 @@ class WeightCacheDaemon:
45
"--weight-cache-socket-dir",
46
self.socket_dir,
47
"--enforce-eager",
48
+ *(extra_args or []),
49
]
50
self._proc: subprocess.Popen | None = None
51
self._lines: list[str] = []
@@ -75,6 +75,13 @@ class WeightCacheDaemon:
75
76
def _wait_ready(self, timeout_s: float) -> None:
77
assert self._proc is not None
78
+ # Each rank binds its socket only once the model is fully cached.
79
+ # Poll for the socket files rather than a log line: model loading can
80
+ # pull in JIT compilers that swap the process's stderr and swallow
81
+ # everything logged afterwards, making log-based readiness flaky.
82
+ expected = [
83
+ get_socket_path(gpu_id, self.socket_dir) for gpu_id in range(self.tp_size)
84
+ ]
85
deadline = time.monotonic() + timeout_s
86
while time.monotonic() < deadline:
87
if self._proc.poll() is not None:
@@ -82,7 +89,7 @@ class WeightCacheDaemon:
89
f"Weight cache daemon exited with {self._proc.returncode}:\n"
90
f"{self._logs()}"
91
)
85
- if self._READY_MARKER in self._logs():
92
+ if all(os.path.exists(path) for path in expected):
93
return
94
time.sleep(1.0)
95
raise TimeoutError(
@@ -100,24 +107,76 @@ class WeightCacheDaemon:
107
shutil.rmtree(self.socket_dir, ignore_errors=True)
108
109
103
-def generate(vllm_runner, socket_dir: str | None, fallback: bool, **kwargs):
110
+@dataclass
111
+class ModelCase:
112
+ model: str
113
+ prompts: list[str]
114
+ images: list | None = None
115
+ llm_kwargs: dict[str, Any] = field(default_factory=dict)
116
+ daemon_args: list[str] = field(default_factory=list)
117
+
118
+
119
+def generate(
120
+ vllm_runner,
121
+ case: ModelCase,
122
+ socket_dir: str | None,
123
+ fallback: bool,
124
+):
125
extra_config = (
126
{} if socket_dir is None else {"socket_dir": socket_dir, "fallback": fallback}
127
)
128
with vllm_runner(
108
- MODEL,
129
+ case.model,
130
load_format="auto" if socket_dir is None else "ipc_cache",
131
model_loader_extra_config=extra_config,
132
# Greedy outputs are compared across engine restarts; cap the batch
133
# size so scheduling cannot change the results.
134
max_num_seqs=1,
114
- **kwargs,
135
+ **case.llm_kwargs,
136
) as llm:
137
sampling_params = SamplingParams(temperature=0, max_tokens=16, ignore_eos=True)
117
- return llm.generate(PROMPTS, sampling_params)
138
+ return llm.generate(case.prompts, sampling_params, images=case.images)
139
+
140
+
141
+# Both hybrid-recurrent models need chunked prefill for their mamba-style
142
+# cache mode.
143
+QWEN_CASE = ModelCase(
144
+ model="Qwen/Qwen3.5-0.8B",
145
+ prompts=[
146
+ "Hello, my name is",
147
+ "The capital of France is",
148
+ ],
149
+ llm_kwargs=dict(
150
+ gpu_memory_utilization=0.3,
151
+ enforce_eager=True,
152
+ enable_chunked_prefill=True,
153
+ ),
154
+)
155
+
156
+# Kimi K3 adds the mxfp4-pack MoE path (post-load kernel setup runs in
157
+# pre-processed mode) and a vision tower, exercised with an image request.
158
+K3_CASE = ModelCase(
159
+ model="tiny-random/kimi-k3",
160
+ prompts=[
161
+ "The capital of France is",
162
+ "def quicksort(arr):",
163
+ "<|kimi_image_placeholder|>What is shown in the image?",
164
+ ],
165
+ images=[None, None, ImageAsset("stop_sign").pil_image],
166
+ llm_kwargs=dict(
167
+ gpu_memory_utilization=0.3,
168
+ enforce_eager=True,
169
+ trust_remote_code=True,
170
+ enable_chunked_prefill=True,
171
+ # The image placeholder expands to >1k tokens for the stop_sign asset.
172
+ max_model_len=4096,
173
+ ),
174
+ daemon_args=["--trust-remote-code"],
175
+)
176
177
120
-def test_ipc_cache_cold_start_and_warm_restart(vllm_runner):
178
+@pytest.mark.parametrize("case", [QWEN_CASE, K3_CASE], ids=["qwen3.5", "kimi-k3"])
179
+def test_ipc_cache_cold_start_and_warm_restart(vllm_runner, case: ModelCase):
180
"""Cold start falls back to disk; warm restarts load weights via CUDA IPC.
181
182
All runs must produce outputs identical to a default-loader baseline. The
@@ -126,32 +185,26 @@ def test_ipc_cache_cold_start_and_warm_restart(vllm_runner):
185
"""
186
if not current_platform.is_cuda_alike():
187
pytest.skip("Weight cache IPC sharing requires CUDA or ROCm")
129
- llm_kwargs = dict(
130
- gpu_memory_utilization=0.3,
131
- enforce_eager=True,
132
- # Qwen3.5 is a hybrid mamba model; its mamba cache mode requires
133
- # chunked prefill.
134
- enable_chunked_prefill=True,
135
- )
188
+ if case is K3_CASE and not current_platform.is_device_capability_family(100):
189
+ pytest.skip("Kimi K3 IPC weight cache requires an SM100 MXFP4 backend")
190
191
# Baseline: plain disk loading with the default loader.
138
- baseline_outputs = generate(vllm_runner, None, fallback=True, **llm_kwargs)
192
+ baseline_outputs = generate(
193
+ vllm_runner,
194
+ case,
195
+ None,
196
+ fallback=True,
197
+ )
198
assert all(text for _, texts in baseline_outputs for text in texts)
199
200
# Cold start: no daemon is serving, so the loader falls back to disk.
201
with tempfile.TemporaryDirectory(prefix="vllm_ipc_empty_") as empty_socket_dir:
143
- cold_outputs = generate(
144
- vllm_runner, empty_socket_dir, fallback=True, **llm_kwargs
145
- )
202
+ cold_outputs = generate(vllm_runner, case, empty_socket_dir, fallback=True)
203
147
- with WeightCacheDaemon(MODEL, tp_size=1) as daemon:
148
- warm_outputs = generate(
149
- vllm_runner, daemon.socket_dir, fallback=False, **llm_kwargs
150
- )
204
+ with WeightCacheDaemon(case.model, tp_size=1, extra_args=case.daemon_args) as d:
205
+ warm_outputs = generate(vllm_runner, case, d.socket_dir, fallback=False)
206
# Warm restart: a second engine lifetime against the same daemon.
152
- restart_outputs = generate(
153
- vllm_runner, daemon.socket_dir, fallback=False, **llm_kwargs
154
- )
207
+ restart_outputs = generate(vllm_runner, case, d.socket_dir, fallback=False)
208
209
assert cold_outputs == baseline_outputs
210
assert warm_outputs == baseline_outputs
tests/models/kimi_k3/test_latent_moe_tail.py
+1
@@ -84,6 +84,7 @@ def test_deferred_finalize_enabled_before_moe_kernel_setup(
84
SimpleNamespace(
85
is_cuda=lambda: True,
86
is_device_capability_family=lambda capability: capability == 100,
87
+ current_device=lambda: torch.device("cuda"),
88
),
89
)
90
monkeypatch.setattr(
vllm/model_executor/kernels/linear/nvfp4/cutlass.py
+3
-5
@@ -9,6 +9,7 @@ from vllm._custom_ops import (
9
)
10
from vllm.model_executor.layers.quantization.utils.nvfp4_utils import (
11
cutlass_fp4_supported,
12
+ nvfp4_weight_padding_bytes,
13
pad_nvfp4_weight_for_cutlass,
14
slice_nvfp4_output,
15
swizzle_blockscale,
@@ -36,11 +37,8 @@ class CutlassNvFp4LinearKernel(NvFp4LinearKernel):
37
layer.weight_scale = torch.nn.Parameter(
38
swizzle_blockscale(layer.weight_scale.data), requires_grad=False
39
)
39
- padded_weight, weights_padding_cols = pad_nvfp4_weight_for_cutlass(
40
- layer.weight.data
41
- )
40
+ padded_weight, _ = pad_nvfp4_weight_for_cutlass(layer.weight.data)
41
layer.weight = torch.nn.Parameter(padded_weight, requires_grad=False)
43
- layer.weights_padding_cols = weights_padding_cols
42
43
def apply_weights(
44
self,
@@ -51,7 +49,7 @@ class CutlassNvFp4LinearKernel(NvFp4LinearKernel):
49
output_size = layer.output_size_per_partition
50
output_dtype = x.dtype
51
output_shape = [*x.shape[:-1], output_size]
54
- weights_padding_bytes = getattr(layer, "weights_padding_cols", 0)
52
+ weights_padding_bytes = nvfp4_weight_padding_bytes(layer)
53
54
x_fp4, x_blockscale = scaled_fp4_quant(
55
x,
vllm/model_executor/kernels/linear/nvfp4/flashinfer.py
+9
-20
@@ -9,6 +9,7 @@ from vllm.model_executor.layers.fusion.quant_activation import (
9
as_quantized_activation,
10
)
11
from vllm.model_executor.layers.quantization.utils.nvfp4_utils import (
12
+ nvfp4_weight_padding_bytes,
13
pad_nvfp4_activation_for_cutlass,
14
pad_nvfp4_weight_for_cutlass,
15
slice_nvfp4_output,
@@ -130,11 +131,8 @@ class FlashInferCuteDslNvFp4LinearKernel(NvFp4LinearKernel):
131
layer.weight_scale = torch.nn.Parameter(
132
swizzle_blockscale(layer.weight_scale.data), requires_grad=False
133
)
133
- padded_weight, weights_padding_cols = pad_nvfp4_weight_for_cutlass(
134
- layer.weight.data
135
- )
134
+ padded_weight, _ = pad_nvfp4_weight_for_cutlass(layer.weight.data)
135
layer.weight = torch.nn.Parameter(padded_weight, requires_grad=False)
137
- layer.weights_padding_cols = weights_padding_cols
136
137
def apply_weights(
138
self,
@@ -154,7 +152,7 @@ class FlashInferCuteDslNvFp4LinearKernel(NvFp4LinearKernel):
152
)
153
154
x_fp4 = pad_nvfp4_activation_for_cutlass(
157
- x_fp4, getattr(layer, "weights_padding_cols", 0)
155
+ x_fp4, nvfp4_weight_padding_bytes(layer)
156
)
157
158
out = flashinfer_scaled_fp4_mm(
@@ -206,11 +204,8 @@ class FlashInferCutlassNvFp4LinearKernel(NvFp4LinearKernel):
204
layer.weight_scale = torch.nn.Parameter(
205
swizzle_blockscale(layer.weight_scale.data), requires_grad=False
206
)
209
- padded_weight, weights_padding_cols = pad_nvfp4_weight_for_cutlass(
210
- layer.weight.data
211
- )
207
+ padded_weight, _ = pad_nvfp4_weight_for_cutlass(layer.weight.data)
208
layer.weight = torch.nn.Parameter(padded_weight, requires_grad=False)
213
- layer.weights_padding_cols = weights_padding_cols
209
210
def apply_weights(
211
self,
@@ -219,7 +214,7 @@ class FlashInferCutlassNvFp4LinearKernel(NvFp4LinearKernel):
214
bias: torch.Tensor | None = None,
215
) -> torch.Tensor:
216
output_size = layer.output_size_per_partition
222
- weights_padding_bytes = getattr(layer, "weights_padding_cols", 0)
217
+ weights_padding_bytes = nvfp4_weight_padding_bytes(layer)
218
219
qa = as_quantized_activation(x, self.input_quant_key())
220
if qa is not None:
@@ -343,11 +338,8 @@ class FlashInferCudnnNvFp4LinearKernel(NvFp4LinearKernel):
338
layer.weight_scale = torch.nn.Parameter(
339
swizzle_blockscale(layer.weight_scale.data), requires_grad=False
340
)
346
- padded_weight, weights_padding_cols = pad_nvfp4_weight_for_cutlass(
347
- layer.weight.data
348
- )
341
+ padded_weight, _ = pad_nvfp4_weight_for_cutlass(layer.weight.data)
342
layer.weight = torch.nn.Parameter(padded_weight, requires_grad=False)
350
- layer.weights_padding_cols = weights_padding_cols
343
344
def apply_weights(
345
self,
@@ -358,7 +350,7 @@ class FlashInferCudnnNvFp4LinearKernel(NvFp4LinearKernel):
350
output_size = layer.output_size_per_partition
351
output_dtype = x.dtype
352
output_shape = [*x.shape[:-1], output_size]
361
- weights_padding_bytes = getattr(layer, "weights_padding_cols", 0)
353
+ weights_padding_bytes = nvfp4_weight_padding_bytes(layer)
354
355
x_fp4, x_blockscale = scaled_fp4_quant(
356
x,
@@ -408,11 +400,8 @@ class FlashInferB12xNvFp4LinearKernel(NvFp4LinearKernel):
400
layer.weight_scale = torch.nn.Parameter(
401
swizzle_blockscale(layer.weight_scale.data), requires_grad=False
402
)
411
- padded_weight, weights_padding_cols = pad_nvfp4_weight_for_cutlass(
412
- layer.weight.data
413
- )
403
+ padded_weight, _ = pad_nvfp4_weight_for_cutlass(layer.weight.data)
404
layer.weight = torch.nn.Parameter(padded_weight, requires_grad=False)
415
- layer.weights_padding_cols = weights_padding_cols
405
406
def apply_weights(
407
self,
@@ -432,7 +421,7 @@ class FlashInferB12xNvFp4LinearKernel(NvFp4LinearKernel):
421
)
422
423
x_fp4 = pad_nvfp4_activation_for_cutlass(
435
- x_fp4, getattr(layer, "weights_padding_cols", 0)
424
+ x_fp4, nvfp4_weight_padding_bytes(layer)
425
)
426
427
out = flashinfer_scaled_fp4_mm(
vllm/model_executor/layers/fused_moe/experts/trtllm_nvfp4_moe.py
+4
-2
@@ -30,6 +30,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
30
kNvfp4Dynamic,
31
kNvfp4Static,
32
)
33
+from vllm.model_executor.utils import is_weights_pre_processed
34
from vllm.platforms import current_platform
35
from vllm.utils.flashinfer import has_flashinfer_trtllm_fused_moe
36
@@ -149,8 +150,9 @@ class TrtLlmNvFp4ExpertsBase:
150
return self.quant_config.g1_alphas * self.quant_config.a2_gscale
151
152
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
152
- layer.w13_weight_scale_2.data.mul_(layer.w13_input_scale)
153
- layer.w2_weight_scale_2.data.mul_(layer.w2_input_scale)
153
+ if not is_weights_pre_processed():
154
+ layer.w13_weight_scale_2.data.mul_(layer.w13_input_scale)
155
+ layer.w2_weight_scale_2.data.mul_(layer.w2_input_scale)
156
# Recompute g1_scale_c since g1_alphas was just fused in-place.
157
# Register as a layer parameter so EPLB rearranges it alongside
158
# other expert weights.
vllm/model_executor/layers/quantization/modelopt.py
+39
-2
@@ -40,6 +40,7 @@ from vllm.model_executor.layers.fused_moe.oracle.mxfp8 import (
40
select_mxfp8_moe_backend,
41
)
42
from vllm.model_executor.layers.fused_moe.oracle.nvfp4 import (
43
+ NvFp4MoeBackend,
44
convert_to_nvfp4_moe_kernel_format,
45
is_global_sf_supported_for_nvfp4_backend,
46
make_nvfp4_moe_kernel,
@@ -100,7 +101,11 @@ from vllm.model_executor.parameter import (
101
ModelWeightParameter,
102
PerTensorScaleParameter,
103
)
103
-from vllm.model_executor.utils import replace_parameter, set_weight_attrs
104
+from vllm.model_executor.utils import (
105
+ is_weights_pre_processed,
106
+ replace_parameter,
107
+ set_weight_attrs,
108
+)
109
from vllm.utils.math_utils import cdiv
110
111
if TYPE_CHECKING:
@@ -819,6 +824,8 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
824
quant_config: NVFP4 Quant Config
825
"""
826
827
+ supports_pre_processed_weights = True
828
+
829
def __init__(
830
self,
831
quant_config: ModelOptNvFp4Config,
@@ -969,6 +976,15 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
976
"""
977
Convert NVFP4 MoE weights into kernel format and setup the kernel.
978
"""
979
+ if is_weights_pre_processed():
980
+ if self.nvfp4_backend != NvFp4MoeBackend.FLASHINFER_TRTLLM:
981
+ raise RuntimeError(
982
+ "pre-processed weights require FLASHINFER_TRTLLM backend, "
983
+ f"moe backend, got {self.nvfp4_backend}"
984
+ )
985
+ self._restore_padded_moe_dims(layer)
986
+ self._build_moe_kernel(layer)
987
+ return
988
989
# Use a single gscale for w13.
990
if self.moe.is_act_and_mul and not torch.allclose(
@@ -1013,7 +1029,10 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1029
replace_parameter(layer, "w2_weight_scale_2", w2_scale_2)
1030
replace_parameter(layer, "w2_input_scale", a2_scale)
1031
1016
- # Setup modular kernel.
1032
+ self._build_moe_kernel(layer)
1033
+
1034
+ def _build_moe_kernel(self, layer: RoutedExperts) -> None:
1035
+ """Build the modular MoE kernel from the (already in-format) weights."""
1036
self.moe_quant_config = self.get_fused_moe_quant_config(layer)
1037
assert self.experts_cls is not None
1038
self.moe_kernel = make_nvfp4_moe_kernel(
@@ -1025,6 +1044,16 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1044
)
1045
self.moe_kernel.fused_experts.process_weights_after_loading(layer)
1046
1047
+ def _restore_padded_moe_dims(self, layer: RoutedExperts) -> None:
1048
+ """Recover the padded ``moe_config`` dims from the exported weights."""
1049
+ mc = layer.moe_config
1050
+ padded_hidden = layer.w2_weight.shape[1]
1051
+ if padded_hidden != mc.hidden_dim:
1052
+ if mc.hidden_dim_unpadded is None:
1053
+ mc.hidden_dim_unpadded = mc.hidden_dim
1054
+ mc.hidden_dim = padded_hidden
1055
+ mc.intermediate_size_per_partition = layer.w2_weight.shape[2] * 2
1056
+
1057
def get_fused_moe_quant_config(self, layer: RoutedExperts) -> FusedMoEQuantConfig:
1058
return make_nvfp4_moe_quant_config(
1059
backend=self.nvfp4_backend,
@@ -2438,6 +2467,12 @@ class ModelOptLinearMethod(LinearMethodBase):
2467
# NVFP4 methods.
2468
self.kernel: Any = None
2469
2470
+ @property
2471
+ def supports_pre_processed_weights(self) -> bool: # type: ignore[override]
2472
+ # TODO(Isotr0py): support fp8/mxfp8 ModelOpt kernels transpose/repack.
2473
+ w = self.spec.weight
2474
+ return isinstance(w, QuantKey) and w.dtype == FP4_DTYPE
2475
+
2476
def create_weights(
2477
self,
2478
layer,
@@ -2476,6 +2511,8 @@ class ModelOptLinearMethod(LinearMethodBase):
2511
expose_input_quant_key(layer, self.kernel)
2512
2513
def process_weights_after_loading(self, layer) -> None:
2514
+ if is_weights_pre_processed():
2515
+ return
2516
self.fmt.pre_process(layer)
2517
self.wkey.process(layer, WEIGHT)
2518
if self.akey:
vllm/model_executor/layers/quantization/mxfp4.py
+23
-6
@@ -36,7 +36,11 @@ from vllm.model_executor.layers.quantization.base_config import (
36
QuantizeMethodBase,
37
)
38
from vllm.model_executor.layers.quantization.utils.quant_utils import is_layer_skipped
39
-from vllm.model_executor.utils import replace_parameter, set_weight_attrs
39
+from vllm.model_executor.utils import (
40
+ is_weights_pre_processed,
41
+ replace_parameter,
42
+ set_weight_attrs,
43
+)
44
from vllm.platforms import current_platform
45
46
logger = init_logger(__name__)
@@ -474,6 +478,8 @@ class GptOssMxfp4MoEMethod(FusedMoEMethodBase):
478
class Mxfp4MoEMethod(FusedMoEMethodBase):
479
"""MXFP4 MoE quantization method."""
480
481
+ supports_pre_processed_weights = True
482
+
483
def __init__(self, moe: FusedMoEConfig):
484
super().__init__(moe)
485
@@ -760,9 +766,11 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
766
replace_parameter(layer, "w2_bias", w2_bias)
767
768
# Build quant config
763
- self.moe_quant_config = self.get_fused_moe_quant_config(layer)
769
+ self._build_moe_kernel(layer)
770
765
- # Build kernel (modular or monolithic)
771
+ def _build_moe_kernel(self, layer: RoutedExperts) -> None:
772
+ """Build the modular MoE kernel from the (already in-format) weights."""
773
+ self.moe_quant_config = self.get_fused_moe_quant_config(layer)
774
if self.moe_quant_config is not None and self.experts_cls is not None:
775
self.moe_kernel = make_mxfp4_moe_kernel(
776
moe_quant_config=self.moe_quant_config,
@@ -774,6 +782,18 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
782
self.moe_kernel.fused_experts.process_weights_after_loading(layer)
783
784
def process_weights_after_loading(self, layer):
785
+ if self.mxfp4_backend == Mxfp4MoeBackend.NONE:
786
+ return
787
+
788
+ if is_weights_pre_processed():
789
+ if self.mxfp4_backend != Mxfp4MoeBackend.FLASHINFER_TRTLLM_MXFP4_MXFP8:
790
+ raise RuntimeError(
791
+ "pre-processed weights require FLASHINFER_TRTLLM_MXFP4_MXFP8 "
792
+ f"moe backend, got {self.mxfp4_backend}"
793
+ )
794
+ self._build_moe_kernel(layer)
795
+ return
796
+
797
w13 = layer.w13_weight
798
w2 = layer.w2_weight
799
w13_scale = layer.w13_weight_scale
@@ -781,9 +801,6 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
801
w13_bias = getattr(layer, "w13_bias", None)
802
w2_bias = getattr(layer, "w2_bias", None)
803
784
- if self.mxfp4_backend == Mxfp4MoeBackend.NONE:
785
- return
786
-
804
self._setup_kernel(layer, w13, w2, w13_scale, w2_scale, w13_bias, w2_bias)
805
806
def get_fused_moe_quant_config(
vllm/model_executor/layers/quantization/utils/nvfp4_utils.py
+8
@@ -98,6 +98,14 @@ def pad_nvfp4_weight_for_cutlass(
98
return weight, weights_padding_bytes
99
100
101
+def nvfp4_weight_padding_bytes(layer: torch.nn.Module) -> int:
102
+ """
103
+ Compute K-dim padding (packed bytes) of an NVFP4 weight,
104
+ Aligned with `pad_nvfp4_weight_for_cutlass` above.
105
+ """
106
+ return layer.weight.shape[1] - layer.input_size_per_partition // 2
107
+
108
+
109
def pad_nvfp4_activation_for_cutlass(
110
x_fp4: torch.Tensor,
111
weights_padding_bytes: int,
vllm/model_executor/model_loader/weight_cache/daemon.py
+14
-33
@@ -17,8 +17,8 @@ Engines then load from the daemons with:
17
vllm serve /path/to/model --tensor-parallel-size 4 \\
18
--load-format ipc_cache
19
20
-Only tensor parallelism is supported; pipeline, data, and expert parallelism
21
-are rejected at launch.
20
+Only tensor and expert parallelism are supported; pipeline and data
21
+parallelism are rejected at launch.
22
"""
23
24
import contextlib
@@ -54,23 +54,7 @@ from vllm.model_executor.model_loader.weight_cache.protocol import (
54
)
55
from vllm.platforms import current_platform
56
57
-logger = init_logger(__name__)
58
-
59
-
60
-def _report_ready(message: str) -> None:
61
- """Write a readiness message straight to the original stderr descriptor.
62
-
63
- Loading the model pulls in FlashInfer's CuTeDSL JIT compiler, which swaps
64
- ``sys.stdout``/``sys.stderr`` for in-memory buffers while compiling kernels
65
- across worker threads. That save/restore races on the interpreter-global
66
- streams and can leave them (and vLLM's logging handler, which caches the
67
- original stream object) detached, silently swallowing everything logged
68
- afterwards -- including the daemon's readiness announcement. Since operators
69
- rely on that line to know the daemon is serving, write it directly to file
70
- descriptor 2 so it bypasses the logging machinery entirely.
71
- """
72
- with contextlib.suppress(OSError):
73
- os.write(2, (message + "\n").encode())
57
+logger = init_logger("vllm.model_executor.model_loader.weight_cache.daemon")
58
59
60
def export_entries(
@@ -192,9 +176,6 @@ class WeightCacheDaemon:
176
logger.info(
177
"Weight cache daemon rank %d serving on %s", self.tp_rank, socket_path
178
)
195
- print(
196
- f"Weight cache daemon rank {self.tp_rank} ready: serving on {socket_path}"
197
- )
179
if ready_callback is not None:
180
ready_callback()
181
try:
@@ -277,6 +258,12 @@ class WeightCacheDaemon:
258
"gpu_uuid": self._gpu_uuid(),
259
},
260
)
261
+ logger.info_once(
262
+ "Weight cache daemon rank %d sent %d tensors (+%d aliases) to engine",
263
+ self.tp_rank,
264
+ len(self.entries),
265
+ len(self.aliases),
266
+ )
267
268
def _handle_release(self, conn: socket.socket) -> None:
269
self.entries.clear()
@@ -308,17 +295,16 @@ def _run_daemon(
295
296
297
def _reject_unsupported_parallelism(parallel_config: ParallelConfig) -> None:
311
- """Reject every parallelism mode other than tensor parallelism."""
298
+ """Reject parallelism modes other than tensor/expert parallelism."""
299
unsupported = {
300
"pipeline parallelism": parallel_config.pipeline_parallel_size > 1,
301
"data parallelism": parallel_config.data_parallel_size > 1,
315
- "expert parallelism": parallel_config.enable_expert_parallel,
302
}
303
for name, enabled in unsupported.items():
304
if enabled:
305
raise ValueError(
320
- f"The weight cache daemon only supports tensor parallelism; "
321
- f"{name} is not supported"
306
+ f"The weight cache daemon only supports tensor and expert "
307
+ f"parallelism; {name} is not supported"
308
)
309
310
@@ -397,16 +383,11 @@ def main() -> None:
383
for proc in procs:
384
proc.join()
385
sys.exit(max((p.exitcode or 0) for p in procs))
400
- logger.info_once(
401
- "===== Weight cache daemon READY: all %d ranks serving in %s =====",
386
+ logger.info(
387
+ "Weight cache daemon ready: all %d ranks serving in %s",
388
tp_size,
389
args.weight_cache_socket_dir or "the default socket dir",
390
)
405
- _report_ready(
406
- f"===== Weight cache daemon READY: all {tp_size} rank(s) serving in "
407
- f"{args.weight_cache_socket_dir or 'the default socket dir'} ====="
408
- )
409
-
391
for proc in procs:
392
proc.join()
393
sys.exit(max(proc.exitcode or 0 for proc in procs))
vllm/model_executor/model_loader/weight_cache/protocol.py
+7
@@ -117,6 +117,13 @@ def _fp8_round_trips_via_ipc(quant_config: Any) -> bool:
117
IPC_QUANT_ALLOWLIST: dict[str | None, Any] = {
118
None: lambda _quant_config: True, # unquantized
119
"fp8": _fp8_round_trips_via_ipc,
120
+ "modelopt_fp4": lambda _quant_config: True,
121
+ # Kimi-K3 routed experts (mxfp4-pack): the MegaMoE experts keep their packed
122
+ # weights and scales as plain parameters and defer the DeepGEMM transform to
123
+ # forward(), so the exported tensors are complete. Any other mxfp4 method
124
+ # that repacks or drops parameters still fails closed at the engine's
125
+ # supports_pre_processed_weights guard.
126
+ "mxfp4": lambda _quant_config: True,
127
}
128
129
vllm/models/kimi_k3/nvidia/latent_moe_runner.py
+1
-1
@@ -132,7 +132,7 @@ class LatentMoERunner(MoERunner):
132
else 0
133
),
134
dtype=norm.weight.dtype,
135
- device=norm.weight.device,
135
+ device=current_platform.current_device(),
136
rms_eps=norm.variance_epsilon,
137
)
138
self._k3_latent_moe_tail_op = op
vllm/models/kimi_k3/nvidia/low_latency_gemm.py
+2
@@ -711,6 +711,8 @@ def _run_plan(
711
return shape_dynamic_skinny_gemm(x, weight, config, None)
712
if not hasattr(torch.ops._C, "dsv3_fused_a_gemm"):
713
return None
714
+ if x.shape[0] != 1 and not _is_packed_row_major(x):
715
+ return None
716
output = torch.empty((x.shape[0], weight.shape[0]), dtype=x.dtype, device=x.device)
717
ops.dsv3_fused_a_gemm(output, x, weight.t(), enable_pdl=True)
718
return output
vllm/models/kimi_k3/nvidia/model.py
+29
-5
@@ -116,7 +116,7 @@ from vllm.transformers_utils.configs.kimi_k3 import KimiK3Config
116
from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
117
from vllm.utils.math_utils import cdiv
118
from vllm.utils.multi_stream_utils import maybe_execute_in_parallel
119
-from vllm.utils.torch_utils import aux_stream
119
+from vllm.utils.torch_utils import aux_stream, is_meta_module
120
from vllm.v1.worker.ubatching import dbo_current_ubatch_id
121
122
from ..common.mm_preprocess import (
@@ -366,6 +366,10 @@ class KimiK3MegaMoEExperts(DeepseekV4MegaMoEExperts):
366
self.activation = activation
367
self.activation_beta = activation_beta
368
self.activation_linear_beta = activation_linear_beta
369
+ self.register_buffer("_mega_l1_packed", None, persistent=False)
370
+ self.register_buffer("_mega_l1_scale", None, persistent=False)
371
+ self.register_buffer("_mega_l2_packed", None, persistent=False)
372
+ self.register_buffer("_mega_l2_scale", None, persistent=False)
373
374
def synchronize_first_launch(self) -> None:
375
ep_group = get_ep_group()
@@ -381,6 +385,14 @@ class KimiK3MegaMoEExperts(DeepseekV4MegaMoEExperts):
385
if self._transformed_l1_weights is not None:
386
return
387
388
+ # Weight cache IPC engine: the daemon exported the transformed
389
+ # buffers; reuse them zero-copy and drop the raw packed params.
390
+ if self._mega_l1_packed is not None:
391
+ self._transformed_l1_weights = (self._mega_l1_packed, self._mega_l1_scale)
392
+ self._transformed_l2_weights = (self._mega_l2_packed, self._mega_l2_scale)
393
+ self._drop_raw_mega_weights()
394
+ return
395
+
396
self._check_runtime_supported()
397
from vllm.utils.deep_gemm import _import_deep_gemm
398
@@ -406,6 +418,15 @@ class KimiK3MegaMoEExperts(DeepseekV4MegaMoEExperts):
418
activation=self.activation,
419
)
420
)
421
+ l1_packed, l1_scale = self._transformed_l1_weights
422
+ l2_packed, l2_scale = self._transformed_l2_weights
423
+ self.register_buffer("_mega_l1_packed", l1_packed, persistent=False)
424
+ self.register_buffer("_mega_l1_scale", l1_scale, persistent=False)
425
+ self.register_buffer("_mega_l2_packed", l2_packed, persistent=False)
426
+ self.register_buffer("_mega_l2_scale", l2_scale, persistent=False)
427
+ self._drop_raw_mega_weights()
428
+
429
+ def _drop_raw_mega_weights(self) -> None:
430
self.w13_weight = None
431
self.w13_weight_scale = None
432
self.w2_weight = None
@@ -1793,7 +1814,9 @@ class KimiK3ForConditionalGeneration(
1814
quant_config=self._maybe_ignore_quant_config(quant_config),
1815
prefix=maybe_prefix(prefix, "vision_tower"),
1816
)
1796
- if self._maybe_ignore_quant_config(quant_config) is not None:
1817
+ if is_meta_module(self.vision_tower):
1818
+ pass
1819
+ elif self._maybe_ignore_quant_config(quant_config) is not None:
1820
self.vision_tower = self.vision_tower.to(device=self.device)
1821
else:
1822
self.vision_tower = self.vision_tower.to(
@@ -1833,9 +1856,10 @@ class KimiK3ForConditionalGeneration(
1856
quant_config=self._maybe_ignore_quant_config(quant_config),
1857
prefix=maybe_prefix(prefix, "mm_projector"),
1858
)
1836
- self.mm_projector = self.mm_projector.to(
1837
- device=self.device, dtype=model_config.dtype
1838
- )
1859
+ if not is_meta_module(self.mm_projector):
1860
+ self.mm_projector = self.mm_projector.to(
1861
+ device=self.device, dtype=model_config.dtype
1862
+ )
1863
1864
self.quant_config = quant_config
1865
with self._mark_language_model(vllm_config):
vllm/utils/torch_utils.py
+7
@@ -88,6 +88,13 @@ def kv_cache_uses_per_token_head_scales(kv_cache_dtype: str) -> bool:
88
return kv_cache_dtype.endswith("per_token_head")
89
90
91
+def is_meta_module(module: torch.nn.Module) -> bool:
92
+ """Return True if module contains any meta parameters or buffers."""
93
+ return any(p.is_meta for p in module.parameters()) or any(
94
+ b.is_meta for b in module.buffers()
95
+ )
96
+
97
+
98
def is_strictly_contiguous(t: torch.Tensor) -> bool:
99
"""
100
Check if tensor is contiguous AND has no degenerate strides.