main
py 1,331 lines 54.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from collections.abc import Callable, Iterable
5 from enum import Enum
6 from typing import TYPE_CHECKING, Any, Literal, cast, overload
7
8 import torch
9
10 from vllm.distributed.eplb.eplb_state import EplbState
11 from vllm.logger import init_logger
12 from vllm.model_executor.custom_op import PluggableLayer
13 from vllm.model_executor.layers.fused_moe.config import (
14 FusedMoEConfig,
15 )
16 from vllm.model_executor.layers.fused_moe.expert_map_manager import (
17 ExpertMapManager,
18 )
19 from vllm.model_executor.layers.fused_moe.fused_moe_method_base import (
20 FusedMoEMethodBase,
21 )
22 from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
23 from vllm.model_executor.layers.fused_moe.unquantized_fused_moe_method import (
24 UnquantizedFusedMoEMethod,
25 )
26 from vllm.model_executor.layers.quantization.base_config import (
27 QuantizationConfig,
28 resolve_quant_method,
29 )
30 from vllm.utils.math_utils import cdiv
31
32 if TYPE_CHECKING:
33 from vllm.model_executor.layers.fused_moe.runner.shared_experts import SharedExperts
34
35
36 logger = init_logger(__name__)
37
38
39 def _index_expert_mapping(
40 mapping: list[tuple[str, str, int, str]],
41 ) -> dict[str, list[tuple[str, str, int, str]]]:
42 """Index by logical checkpoint ID, preserving entry order and EPLB replicas.
43
44 An empty index keeps the full scan for unsupported mapping names.
45 """
46 mapping_by_expert: dict[str, list[tuple[str, str, int, str]]] = {}
47 for entry in mapping:
48 prefix, _, suffix = entry[1].partition(".")
49 expert_key, sep, _ = suffix.partition(".")
50 if (
51 prefix != "experts"
52 or not expert_key
53 or (expert_key.isdecimal() and not sep)
54 ):
55 return {}
56 if expert_key.isdecimal():
57 mapping_by_expert.setdefault(expert_key, []).append(entry)
58 return mapping_by_expert
59
60
61 class FusedMoeWeightScaleSupported(Enum):
62 TENSOR = "tensor"
63 CHANNEL = "channel"
64 GROUP = "group"
65 BLOCK = "block"
66
67
68 @PluggableLayer.register("routed_experts")
69 class RoutedExperts(PluggableLayer):
70 """Container for routed expert weights and execution logic.
71
72 This module owns the expert weight parameters (w13_weight, w2_weight, scales, etc.)
73 and handles:
74 - Loading checkpoint weights into parameters
75 - Executing routed experts via quant_method.apply()
76 """
77
78 def __init__(
79 self,
80 layer_name: str,
81 params_dtype: torch.dtype,
82 moe_config: FusedMoEConfig,
83 quant_config: QuantizationConfig | None,
84 expert_map_manager: ExpertMapManager,
85 ckpt_gate_proj_name: str = "gate_proj",
86 ckpt_down_proj_name: str = "down_proj",
87 ckpt_up_proj_name: str = "up_proj",
88 is_fused_checkpoint_transposed: bool = False,
89 #
90 # Extra params that are needed by quant_methods, pass along for now
91 # Prefer getting these from other sources, e.g. moe_config or
92 # router object
93 #
94 renormalize: bool = True,
95 use_grouped_topk: bool = False,
96 num_expert_group: int | None = None,
97 topk_group: int | None = None,
98 custom_routing_function: Callable | None = None,
99 scoring_func: str = "softmax",
100 routed_scaling_factor: float = 1.0,
101 swiglu_limit: float | None = None,
102 swiglu_alpha: float | None = None,
103 swiglu_beta: float | None = None,
104 e_score_correction_bias: torch.Tensor | None = None,
105 apply_router_weight_on_input: bool = False,
106 ):
107 super().__init__()
108 self.layer_name = layer_name
109 self.moe_config = moe_config
110 self.quant_config = quant_config
111 self.ckpt_gate_proj_name = ckpt_gate_proj_name
112 self.ckpt_down_proj_name = ckpt_down_proj_name
113 self.ckpt_up_proj_name = ckpt_up_proj_name
114 self.is_fused_checkpoint_transposed = is_fused_checkpoint_transposed
115 self.expert_map_manager = expert_map_manager
116 self.hidden_size = moe_config.hidden_dim
117 self.global_num_experts = moe_config.num_experts
118 self.local_num_experts = moe_config.num_local_experts
119 self.params_dtype = params_dtype
120
121 # Register buffers for state_dict compatibility
122 self.update_expert_map_info()
123
124 self.rocm_aiter_fmoe_enabled = moe_config.rocm_aiter_fmoe_enabled
125
126 # It would be good to eventually codify these in FusedMoEConfig
127 # or some other config.
128 self.top_k = self.moe_config.experts_per_token
129 self.activation = self.moe_config.activation
130 self.renormalize = renormalize
131 self.use_grouped_topk = use_grouped_topk
132 self.num_expert_group = num_expert_group
133 self.topk_group = topk_group
134 self.custom_routing_function = custom_routing_function
135 self.scoring_func = scoring_func
136 self.routed_scaling_factor = routed_scaling_factor
137 self.swiglu_limit = swiglu_limit
138 self.swiglu_alpha = swiglu_alpha
139 self.swiglu_beta = swiglu_beta
140 self.e_score_correction_bias = e_score_correction_bias
141 self.apply_router_weight_on_input = apply_router_weight_on_input
142 # End random parameters
143 self._loaded_expert_biases: set[str] = set()
144
145 self.quant_method = self._get_quant_method(
146 self.layer_name,
147 self.quant_config,
148 self.moe_config,
149 )
150
151 # Round up hidden size and update moe_config.
152 # TODO: move roundup to _get_quant_method?
153 self.hidden_size, self.intermediate_size_per_partition = (
154 self.quant_method.maybe_roundup_sizes(
155 self.hidden_size,
156 self.moe_config.intermediate_size_per_partition,
157 self.moe_config.in_dtype,
158 self.moe_config.moe_parallel_config,
159 )
160 )
161 self.moe_config.hidden_dim = self.hidden_size
162 self.moe_config.intermediate_size_per_partition = (
163 self.intermediate_size_per_partition
164 )
165
166 if (
167 self.moe_config.moe_parallel_config.enable_eplb
168 and not self.quant_method.supports_eplb
169 ):
170 # TODO: Add support for additional quantization methods.
171 # The implementation for other quantization methods does not
172 # contain essential differences, but the current quant API
173 # design causes duplicated work when extending to new
174 # quantization methods, so I'm leaving it for now.
175 # If you plan to add support for more quantization methods,
176 # please refer to the implementation in `Fp8MoEMethod`.
177 raise NotImplementedError(
178 f"EPLB is not supported {self.quant_method.__class__.__name__}."
179 )
180
181 moe_quant_params: dict[str, Any] = {
182 "num_experts": moe_config.num_local_experts,
183 "hidden_size": self.hidden_size,
184 "unpadded_hidden_size": self.moe_config.hidden_dim_unpadded,
185 "intermediate_size_per_partition": (
186 self.moe_config.intermediate_size_per_partition
187 ),
188 "params_dtype": params_dtype,
189 "weight_loader": self.weight_loader,
190 "global_num_experts": moe_config.num_experts,
191 }
192
193 self.quant_method.create_weights(layer=self, **moe_quant_params)
194
195 self.lora_base_layer_prefix = ""
196
197 # TODO(bnell): Temporary hack. Get rid of this.
198 def _replace_quant_method(self, quant_method: FusedMoEMethodBase):
199 self.quant_method = quant_method
200
201 # TODO(bnell): Hack for elastic_ep. Get rid of this
202 def _set_moe_config(self, new_moe_config: FusedMoEConfig):
203 self.moe_config = new_moe_config
204 self.global_num_experts = new_moe_config.num_experts
205 # local experts?
206
207 def _get_quant_method(
208 self,
209 prefix: str,
210 quant_config: QuantizationConfig | None,
211 moe_config: FusedMoEConfig,
212 ) -> FusedMoEMethodBase:
213 """Helper method to ensure quant_method is never None and
214 of the proper type.
215 """
216 quant_method = None
217 if quant_config is not None:
218 quant_method = resolve_quant_method(quant_config, self, prefix)
219 if quant_method is None:
220 quant_method = UnquantizedFusedMoEMethod(moe_config)
221 assert isinstance(quant_method, FusedMoEMethodBase)
222 return quant_method
223
224 def _ensure_moe_quant_config_init(self):
225 if self.quant_method.moe_quant_config is None:
226 # Note: the moe_quant_config can't be constructed until after
227 # weight loading post processing.
228 self.quant_method.moe_quant_config = (
229 self.quant_method.get_fused_moe_quant_config(self)
230 )
231
232 @property
233 def use_ep(self) -> bool:
234 return self.moe_config.moe_parallel_config.use_ep
235
236 @property
237 def expert_map(self) -> torch.Tensor | None:
238 # AITER fused-MoE kernels consume the 0/1 expert_mask; every other
239 # backend consumes the canonical -1/local-slot map. Ask the active
240 # experts kernel which it wants (only AITER sets consumes_expert_mask)
241 # rather than keying on the global VLLM_ROCM_USE_AITER switch, which
242 # does not reflect the per-layer kernel actually selected.
243 moe_kernel = getattr(self.quant_method, "moe_kernel", None)
244 if moe_kernel is not None and moe_kernel.fused_experts.consumes_expert_mask:
245 return self.expert_mask
246 return self._expert_map
247
248 def update_expert_map_info(self):
249 # Update local attributes from ExpertMapManager
250 self.local_num_experts = self.expert_map_manager.local_num_experts
251 self.expert_placement_strategy = self.expert_map_manager.placement_strategy
252 self.register_buffer(
253 "_expert_map", self.expert_map_manager.expert_map, persistent=False
254 )
255 self.register_buffer(
256 "expert_mask", self.expert_map_manager.expert_mask, persistent=False
257 )
258
259 # Get routing tables from ExpertMapManager
260 routing_tables = self.expert_map_manager.routing_tables
261 if routing_tables is not None:
262 # Register routing tables as buffers for this layer
263 global_to_physical, physical_to_global, local_global = routing_tables
264 self.register_buffer(
265 "expert_global_to_physical", global_to_physical, persistent=False
266 )
267 self.register_buffer(
268 "expert_physical_to_global", physical_to_global, persistent=False
269 )
270 self.register_buffer(
271 "expert_local_to_global", local_global, persistent=False
272 )
273
274 def _expert_routing_tables(
275 self,
276 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None:
277 # Return cached routing tables if already registered as buffers
278 if hasattr(self, "expert_global_to_physical"):
279 return cast(
280 tuple[torch.Tensor, torch.Tensor, torch.Tensor],
281 (
282 self.expert_global_to_physical,
283 self.expert_physical_to_global,
284 self.expert_local_to_global,
285 ),
286 )
287 return None
288
289 def update_expert_map(self):
290 # Update ExpertMapManager with new EP configuration
291 # The moe_parallel_config (including ep_size and ep_rank)
292 # should already be updated.
293 # Note: ExpertMapManager.update() recalculates expert maps and
294 # reinitializes routing tables internally.
295 self.expert_map_manager.update(
296 self.moe_config.moe_parallel_config,
297 global_num_experts=self.global_num_experts,
298 )
299
300 # Update local attributes from ExpertMapManager
301 self.update_expert_map_info()
302
303 def _map_global_expert_id_to_local_expert_id(self, expert_id: int) -> int:
304 """Map global expert ID to local expert ID."""
305 return self.expert_map_manager.map_global_to_local(expert_id)
306
307 #
308 # Weight Loading Methods
309 #
310
311 @staticmethod
312 def _to_scalar(loaded_weight: torch.Tensor) -> torch.Tensor:
313 # Per-tensor scales arrive 0-D or as shape-(1,) (llm-compressor NVFP4);
314 # reduce to a 0-D scalar. numel > 1 raises instead of broadcasting.
315 return loaded_weight.reshape(())
316
317 def _load_per_tensor_weight_scale(
318 self,
319 shard_id: str,
320 param: torch.nn.Parameter,
321 loaded_weight: torch.Tensor,
322 expert_id: int,
323 ):
324 param_data = param.data
325 # for per tensor weight quantization
326 if shard_id in ("w1", "w3"):
327 # We have to keep the weight scales of w1 and w3 because
328 # we need to re-quantize w1/w3 weights after weight loading.
329 idx = 0 if shard_id == "w1" else 1
330 param_data[expert_id][idx] = self._to_scalar(loaded_weight)
331 # If we are in the row parallel case (down_proj)
332 elif shard_id == "w2":
333 param_data[expert_id] = self._to_scalar(loaded_weight)
334
335 def _load_combined_w13_weight_scale(
336 self,
337 shard_dim: int,
338 loaded_weight: torch.Tensor,
339 param: torch.Tensor,
340 tp_rank: int,
341 ):
342 """Load w13 weight scales assuming that w1 weight scales and w3 weight
343 scales are stored in the same loaded_weight tensor.
344 """
345 shard_size = param.shape[shard_dim]
346 loaded_weight = loaded_weight.narrow(
347 shard_dim, shard_size * tp_rank, shard_size
348 )
349 param.copy_(loaded_weight)
350
351 def _load_model_weight_or_group_weight_scale(
352 self,
353 shard_dim: int,
354 expert_data: torch.Tensor,
355 shard_id: str,
356 loaded_weight: torch.Tensor,
357 tp_rank: int,
358 is_scale: bool = False,
359 ):
360 """Load grouped weight scales for group quantization or model weights
361
362 Args:
363 shard_dim: dimension to shard
364 expert_data: parameter for a particular expert
365 shard_id: either w1, w2, or w3
366 loaded_weight: checkpoint weight to load into the param
367 tp_rank: tensor parallel rank
368 is_scale: whether padding should use unit scales instead of zero weights.
369
370 """
371 padded_tp = self.moe_config.tp_shard_with_padding
372 if padded_tp:
373 destination = expert_data
374 if shard_id in ("w1", "w3") and self.moe_config.is_act_and_mul:
375 half = destination.shape[shard_dim] // 2
376 destination = destination.narrow(
377 shard_dim, 0 if shard_id == "w1" else half, half
378 )
379 shard_size = destination.shape[shard_dim]
380 expected_size = (
381 self.moe_config.intermediate_size
382 * shard_size
383 // self.moe_config.intermediate_size_per_partition
384 )
385 if loaded_weight.shape[shard_dim] != expected_size:
386 raise ValueError(
387 "Block-aligned TP loading expects an unsharded checkpoint "
388 f"projection of size {expected_size}, got "
389 f"{loaded_weight.shape[shard_dim]}."
390 )
391 start = min(tp_rank * shard_size, expected_size)
392 size = min(shard_size, expected_size - start)
393 loaded_weight = loaded_weight.narrow(shard_dim, start, size)
394 destination.fill_(1 if is_scale else 0)
395
396 if shard_id == "w2":
397 self._load_w2(
398 shard_dim=shard_dim,
399 loaded_weight=loaded_weight,
400 expert_data=expert_data,
401 tp_rank=tp_rank,
402 load_full=padded_tp,
403 )
404 elif shard_id in ("w1", "w3"):
405 self._load_w13(
406 shard_id=shard_id,
407 shard_dim=shard_dim,
408 loaded_weight=loaded_weight,
409 expert_data=expert_data,
410 tp_rank=tp_rank,
411 load_full=padded_tp,
412 )
413
414 def _load_per_channel_weight_scale(
415 self,
416 expert_data: torch.Tensor,
417 shard_dim: int,
418 shard_id: str,
419 loaded_weight: torch.Tensor,
420 tp_rank: int,
421 ):
422 # for per channel weight quantization
423 if shard_id == "w2":
424 hidden_dim = self._get_hidden_dim(shard_dim, expert_data.ndim)
425 expert_data = self._narrow_expert_data_for_padding(
426 expert_data,
427 loaded_weight,
428 hidden_dim=hidden_dim,
429 shard_dim=shard_dim,
430 )
431 expert_data.copy_(loaded_weight)
432 elif shard_id in ("w1", "w3"):
433 self._load_w13(
434 shard_id=shard_id,
435 shard_dim=shard_dim,
436 loaded_weight=loaded_weight,
437 expert_data=expert_data,
438 tp_rank=tp_rank,
439 )
440
441 @staticmethod
442 def _get_hidden_dim(shard_dim: int, ndim: int) -> int:
443 """Compute the hidden dimension index from the shard (intermediate)
444 dimension and tensor rank.
445
446 For 2D weight tensors the two data dims are (0, 1). For 3D tensors
447 with an expert dimension at dim 0, they are (1, 2). ``shard_dim``
448 occupies one of these; the hidden dimension is the other.
449 For 1D tensors (e.g. per-channel scales) returns 0.
450 """
451 if ndim < 2:
452 return 0
453 dim_a = ndim - 2
454 dim_b = ndim - 1
455 if shard_dim == dim_a:
456 return dim_b
457 if shard_dim == dim_b:
458 return dim_a
459 raise ValueError(
460 f"shard_dim={shard_dim} is not a valid data dimension "
461 f"for a {ndim}D tensor (expected {dim_a} or {dim_b})"
462 )
463
464 @staticmethod
465 def _orient_fused_weight(
466 fused_weight: torch.Tensor,
467 is_fused_checkpoint_transposed: bool,
468 ) -> torch.Tensor:
469 """Normalise a fused expert tensor to the vLLM weight layout."""
470 if is_fused_checkpoint_transposed:
471 return fused_weight.transpose(-1, -2)
472 return fused_weight
473
474 @staticmethod
475 def _narrow_expert_data_for_padding(
476 expert_data: torch.Tensor,
477 loaded_weight: torch.Tensor,
478 hidden_dim: int,
479 shard_dim: int | None = None,
480 ) -> torch.Tensor:
481 """Narrow expert_data to match loaded_weight for padded dimensions.
482
483 When backends (e.g., DeepEP) round up hidden_size, weight parameters
484 are larger than checkpoint weights. Narrow the padded hidden dimension
485 before copying. Similarly, when padding occurs on the shard
486 (intermediate) dimension (e.g. for MXFP4 GEMM), narrow that dimension
487 as well.
488
489 Args:
490 expert_data: The (possibly padded) parameter tensor to narrow.
491 loaded_weight: The checkpoint weight tensor with original size.
492 hidden_dim: The dimension index corresponding to hidden_size.
493 Must be non-negative.
494 shard_dim: The dimension index corresponding to the shard
495 (intermediate) dimension. Defaults to `None`.
496
497 """
498 dims = (hidden_dim,) if shard_dim is None else (hidden_dim, shard_dim)
499 if loaded_weight.ndim > 0:
500 for dim in dims:
501 if (
502 0 <= dim < expert_data.ndim
503 and dim < loaded_weight.ndim
504 and expert_data.shape[dim] > loaded_weight.shape[dim]
505 ):
506 expert_data = expert_data.narrow(dim, 0, loaded_weight.shape[dim])
507 return expert_data
508
509 def _load_w13(
510 self,
511 expert_data: torch.Tensor,
512 shard_dim: int,
513 shard_id: str,
514 loaded_weight: torch.Tensor,
515 tp_rank: int,
516 load_full: bool = False,
517 ):
518 # Index the loaded weight for tp sharding.
519 # gate_up_proj: "MergedColumnParallel", so tp sharding on output_dim
520 if self.moe_config.is_act_and_mul:
521 shard_size = expert_data.shape[shard_dim] // 2
522 else:
523 shard_size = expert_data.shape[shard_dim]
524 # Only narrow if the loaded_weight is not a scalar (0-dim tensor)
525 # and we're not loading the full weight
526 if not load_full and loaded_weight.ndim > 0:
527 # When the parameter has been padded (e.g. MXFP4 rounding up
528 # intermediate_size_per_partition), shard_size is the padded
529 # size. Compute the offset into the checkpoint weight using
530 # the *unpadded* per-rank size so that every TP rank lands at
531 # the correct slice.
532 tp_size = self.moe_config.moe_parallel_config.tp_size
533 loaded_per_rank = loaded_weight.shape[shard_dim] // tp_size
534 start_offset = loaded_per_rank * tp_rank
535 available = loaded_weight.shape[shard_dim] - start_offset
536 if available <= 0:
537 # If there is no available weight to load for this TP rank
538 # (can happen on last TP rank with padding), we can skip
539 # loading and return early
540 return
541 narrow_size = min(loaded_per_rank, available)
542 loaded_weight = loaded_weight.narrow(shard_dim, start_offset, narrow_size)
543 # Narrow parameter and load.
544 # w1, gate_proj: Load into first logical weight of w13.
545 if shard_id == "w1":
546 expert_data = expert_data.narrow(shard_dim, 0, shard_size)
547 # w3, up_proj: Load into second logical weight of w13.
548 else:
549 assert shard_id == "w3"
550 expert_data = expert_data.narrow(shard_dim, shard_size, shard_size)
551 hidden_dim = self._get_hidden_dim(shard_dim, expert_data.ndim)
552 expert_data = self._narrow_expert_data_for_padding(
553 expert_data,
554 loaded_weight,
555 hidden_dim=hidden_dim,
556 shard_dim=shard_dim,
557 )
558 expert_data.copy_(loaded_weight)
559
560 def _load_w2(
561 self,
562 expert_data: torch.Tensor,
563 shard_dim: int,
564 loaded_weight: torch.Tensor,
565 tp_rank: int,
566 load_full: bool = False,
567 ):
568 # Index the loaded weight for tp sharding.
569 # down_proj: "RowParallel" so tp sharding on input_dim
570 # Padded TP weights have already been sliced by the grouped loader.
571 if not load_full and loaded_weight.ndim > 0:
572 # Same padding fix as _load_w13: use unpadded per-rank size.
573 tp_size = self.moe_config.moe_parallel_config.tp_size
574 loaded_per_rank = loaded_weight.shape[shard_dim] // tp_size
575 start_offset = loaded_per_rank * tp_rank
576 available = loaded_weight.shape[shard_dim] - start_offset
577 if available <= 0:
578 # If there is no available weight to load for this TP rank
579 # (can happen on last TP rank with padding), we can skip
580 # loading and return early
581 return
582 narrow_size = min(loaded_per_rank, available)
583 loaded_weight = loaded_weight.narrow(shard_dim, start_offset, narrow_size)
584 # w2, down_proj: Load into only logical weight of w2.
585 hidden_dim = self._get_hidden_dim(shard_dim, expert_data.ndim)
586 expert_data = self._narrow_expert_data_for_padding(
587 expert_data,
588 loaded_weight,
589 hidden_dim=hidden_dim,
590 shard_dim=shard_dim,
591 )
592 if (
593 loaded_weight.device.type == "cpu"
594 and expert_data.device.type == "cuda"
595 and not expert_data.is_contiguous()
596 and expert_data.ndim == 2
597 ):
598 # A strided CPU-to-CUDA copy can allocate a full-shard CUDA
599 # temporary. Make the TP slice contiguous on CPU and copy rows
600 # in chunks to limit the temporary used for the padded view.
601 loaded_weight = loaded_weight.contiguous()
602 num_chunks = max(cdiv(loaded_weight.nbytes, 1 << 20), 1)
603 for dst, src in zip(
604 expert_data.chunk(num_chunks, dim=0),
605 loaded_weight.chunk(num_chunks, dim=0),
606 ):
607 dst.copy_(src)
608 return
609 expert_data.copy_(loaded_weight)
610
611 def _load_single_value(
612 self, param: torch.nn.Parameter, loaded_weight: torch.Tensor, expert_id: int
613 ):
614 param_data = param.data
615
616 # Used for both scalar input_scale and the size-2 `weight_shape`
617 # param (compressed-tensors). Assign directly so both shapes load;
618 # _to_scalar's reshape(()) would reject the size-2 weight_shape.
619 param_data[expert_id] = loaded_weight
620
621 @overload
622 def weight_loader(
623 self,
624 param: torch.nn.Parameter,
625 loaded_weight: torch.Tensor,
626 weight_name: str,
627 shard_id: str,
628 expert_id: int,
629 return_success: Literal[False],
630 ) -> None: ...
631
632 @overload
633 def weight_loader(
634 self,
635 param: torch.nn.Parameter,
636 loaded_weight: torch.Tensor,
637 weight_name: str,
638 shard_id: str,
639 expert_id: int,
640 return_success: Literal[True],
641 ) -> bool: ...
642
643 def weight_loader(
644 self,
645 param: torch.nn.Parameter,
646 loaded_weight: torch.Tensor,
647 weight_name: str,
648 shard_id: str,
649 expert_id: int,
650 return_success: bool = False,
651 ) -> bool | None:
652 quant_config_name = self.quant_config and self.quant_config.get_name()
653 if quant_config_name == "gpt_oss_mxfp4":
654 # (FIXME) for gpt-oss all experts are combined
655 if "bias" in weight_name:
656 dim1 = loaded_weight.shape[1]
657 param.data[:, :dim1].copy_(loaded_weight)
658 else:
659 dim1 = loaded_weight.shape[1]
660 dim2 = loaded_weight.shape[2]
661 param.data[:, :dim1, :dim2].copy_(loaded_weight)
662 return True if return_success else None
663
664 quant_method_name = self.quant_method.__class__.__name__
665 global_expert_id = expert_id
666 expert_id = self._map_global_expert_id_to_local_expert_id(global_expert_id)
667
668 use_global_sf = (
669 getattr(self.quant_method, "use_global_sf", False)
670 and "input_scale" in weight_name
671 )
672
673 if expert_id == -1 and not use_global_sf:
674 # Failed to load this param since it's not local to this rank
675 return False if return_success else None
676 # Hereafter, `expert_id` is local physical id
677
678 # is_transposed: if the dim to shard the weight
679 # should be flipped. Required by GPTQ/AWQ (K-first format).
680 # should be whatever dimension intermediate_size_per_partition is
681 is_transposed = getattr(param, "is_transposed", False)
682
683 # compressed-tensors checkpoints with packed weights are stored flipped
684 # TODO (mgoin): check self.quant_method.quant_config.quant_format
685 # against known CompressionFormat enum values that have this quality
686 if (
687 quant_method_name
688 in (
689 "CompressedTensorsWNA16MoEMethod",
690 "CompressedTensorsW4A16FlydslMoEMethod",
691 )
692 and is_transposed
693 ):
694 # Transpose on the parameter's device: a CPU transpose of the
695 # mmap view reads the file column-wise, which is very slow on
696 # NFS. The contiguous .to() reads it sequentially.
697 loaded_weight = loaded_weight.to(param.device).t().contiguous()
698
699 if shard_id not in ("w1", "w2", "w3"):
700 raise ValueError(f"shard_id must be ['w1','w2','w3'] but got {shard_id}.")
701
702 # Fetch the dim to shard the parameter/loaded weight
703 # based on the shard id. This will be whatever
704 # dimension intermediate_size_per_partition is used.
705 SHARD_ID_TO_SHARDED_DIM = {"w1": 0, "w2": 1, "w3": 0}
706 shard_dim = SHARD_ID_TO_SHARDED_DIM[shard_id]
707 if is_transposed:
708 shard_dim = int(not shard_dim)
709
710 full_load = len(loaded_weight.shape) == 3
711 if full_load:
712 shard_dim += 1
713
714 expert_data = param.data if full_load else param.data[expert_id]
715
716 if "bias" in weight_name:
717 self._loaded_expert_biases.add(weight_name.rsplit(".", 1)[-1])
718 if shard_id == "w2":
719 expert_data = self._narrow_expert_data_for_padding(
720 expert_data,
721 loaded_weight,
722 hidden_dim=0,
723 )
724 expert_data.copy_(loaded_weight)
725 else:
726 self._load_w13(
727 shard_id=shard_id,
728 shard_dim=0,
729 loaded_weight=loaded_weight,
730 expert_data=expert_data,
731 tp_rank=self.moe_config.tp_rank,
732 )
733 return True if return_success else None
734
735 # Case input scale: input_scale loading is only supported for fp8
736 if "input_scale" in weight_name:
737 # this is needed for compressed-tensors only
738 loaded_weight = loaded_weight.to(param.data.device)
739
740 # ModelOpt NVFP4 stores w13 input scales as two logical shards.
741 # The generic assignment below would broadcast w1/w3 into the
742 # whole expert row, so the second shard would overwrite the first.
743 if (
744 "ModelOpt" in quant_method_name
745 and param.data.ndim == 2
746 and shard_id in ("w1", "w3")
747 ):
748 scale_expert_id = global_expert_id if use_global_sf else expert_id
749 scale_shard_id = 0 if shard_id == "w1" else 1
750 param.data[scale_expert_id][scale_shard_id] = self._to_scalar(
751 loaded_weight
752 )
753 return True if return_success else None
754
755 if (
756 "compressed" in quant_method_name.lower()
757 and param.data[expert_id] != 1
758 and (param.data[expert_id] - loaded_weight).abs() > 1e-5
759 ):
760 raise ValueError(
761 "input_scales of w1 and w3 of a layer "
762 f"must be equal. But got {param.data[expert_id]} "
763 f"vs. {loaded_weight}"
764 )
765
766 self._load_single_value(
767 param=param,
768 loaded_weight=loaded_weight,
769 expert_id=global_expert_id if use_global_sf else expert_id,
770 )
771 return True if return_success else None
772
773 # TODO @dsikka: ModelOpt should follow the proper MoE loading pattern
774 if "ModelOpt" in quant_method_name:
775 # Determine per-tensor weight scale patterns based on variant
776 # Use the dedicated method instead of brittle string matching
777 uses_weight_scale_2 = self.quant_method.uses_weight_scale_2_pattern()
778 quant_method = getattr(param, "quant_method", None)
779
780 # Call _load_per_tensor_weight_scale() to load per-tensor (scalar)
781 # weights scales.
782 # Input scales are always per-tensor.
783 # Weight scales: FP4 uses "weight_scale_2" and FP8 uses
784 # "weight_scale" for per-tensor scales.
785 # NOTE: ModelOpt MXFP8 MoE uses block scales in weight_scale
786 # tensors (quant_method=BLOCK), so those must not be treated
787 # as per-tensor scalars here.
788 is_block_weight_scale = (
789 "weight_scale" in weight_name
790 and quant_method == FusedMoeWeightScaleSupported.BLOCK.value
791 )
792 is_per_tensor = (
793 "weight_scale_2" in weight_name
794 if uses_weight_scale_2
795 else "weight_scale" in weight_name
796 ) or "input_scale" in weight_name
797 is_per_tensor = is_per_tensor and not is_block_weight_scale
798 if is_per_tensor:
799 self._load_per_tensor_weight_scale(
800 shard_id=shard_id,
801 param=param,
802 loaded_weight=loaded_weight,
803 expert_id=expert_id,
804 )
805 return True if return_success else None
806
807 # If the weight is w13_weight_scale and w13_weight_scales are
808 # combined into single loaded_weight, call
809 # _load_combined_w13_weight_scale() to load it.
810 # This is checked by comparing the hidden_out dims of the
811 # loaded_weight and the param.
812 if "w13_weight_scale" in weight_name:
813 loaded_weight_hidden_out = loaded_weight.shape[-2]
814 param_hidden_out = param.data.shape[-2] * self.moe_config.tp_size
815 if loaded_weight_hidden_out == param_hidden_out:
816 self._load_combined_w13_weight_scale(
817 shard_dim=shard_dim,
818 loaded_weight=loaded_weight,
819 param=expert_data,
820 tp_rank=self.moe_config.tp_rank,
821 )
822 return True if return_success else None
823
824 # For other weights, call _load_model_weight_or_group_weight_scale()
825 # to load it.
826 if "weight" in weight_name:
827 self._load_model_weight_or_group_weight_scale(
828 shard_id=shard_id,
829 shard_dim=shard_dim,
830 loaded_weight=loaded_weight,
831 expert_data=expert_data,
832 tp_rank=self.moe_config.tp_rank,
833 )
834 return True if return_success else None
835
836 # Case weight scales, zero_points and offset, weight/input global scales
837 if "scale" in weight_name or "zero" in weight_name or "offset" in weight_name:
838 # load the weight scales and zp based on the quantization scheme
839 # supported weight scales/zp can be found in
840 # FusedMoeWeightScaleSupported
841 # TODO @dsikka: once hardened, refactor to use vLLM Parameters
842 # specific to each case
843 quant_method = getattr(param, "quant_method", None)
844 if quant_method == FusedMoeWeightScaleSupported.CHANNEL.value:
845 self._load_per_channel_weight_scale(
846 shard_id=shard_id,
847 shard_dim=shard_dim,
848 loaded_weight=loaded_weight,
849 expert_data=expert_data,
850 tp_rank=self.moe_config.tp_rank,
851 )
852 elif quant_method in [
853 FusedMoeWeightScaleSupported.GROUP.value,
854 FusedMoeWeightScaleSupported.BLOCK.value,
855 ]:
856 scale_refine = getattr(self.quant_method, "weight_scale_refine", None)
857 if (
858 quant_method == FusedMoeWeightScaleSupported.BLOCK.value
859 and scale_refine is not None
860 ):
861 # FP8 block scales are stored per (block_n, block_k) tile
862 # of the unsharded weight, while the TP-sharded parameters
863 # use a refined block grid (see Fp8MoEMethod). Upsample the
864 # scales to the refined grid (lossless: the refined block
865 # divides the checkpoint block) so per-rank slicing stays
866 # exact. Dim -2 is the weight's N dim, dim -1 is K.
867 loaded_weight = loaded_weight.repeat_interleave(
868 scale_refine[0], dim=-2
869 ).repeat_interleave(scale_refine[1], dim=-1)
870 self._load_model_weight_or_group_weight_scale(
871 shard_id=shard_id,
872 shard_dim=shard_dim,
873 loaded_weight=loaded_weight,
874 expert_data=expert_data,
875 tp_rank=self.moe_config.tp_rank,
876 is_scale=True,
877 )
878 elif quant_method == FusedMoeWeightScaleSupported.TENSOR.value:
879 self._load_per_tensor_weight_scale(
880 shard_id=shard_id,
881 param=param,
882 loaded_weight=loaded_weight,
883 expert_id=expert_id,
884 )
885 else:
886 WEIGHT_SCALE_SUPPORTED = [e.value for e in FusedMoeWeightScaleSupported]
887 raise ValueError(
888 f"quant method must be one of {WEIGHT_SCALE_SUPPORTED}"
889 )
890 return True if return_success else None
891
892 # Case weight_shape
893 if "weight_shape" in weight_name:
894 # only required by compressed-tensors
895 self._load_single_value(
896 param=param, loaded_weight=loaded_weight, expert_id=expert_id
897 )
898 return True if return_success else None
899
900 # Case model weights
901 if "weight" in weight_name:
902 self._load_model_weight_or_group_weight_scale(
903 shard_id=shard_id,
904 shard_dim=shard_dim,
905 loaded_weight=loaded_weight,
906 expert_data=expert_data,
907 tp_rank=self.moe_config.tp_rank,
908 )
909 return True if return_success else None
910
911 return False if return_success else None
912
913 def load_weights(
914 self, weights: Iterable[tuple[str, torch.Tensor]]
915 ) -> Iterable[str]:
916 expert_mapping = self.get_expert_mapping(include_fused=True)
917 mapping_by_expert = _index_expert_mapping(expert_mapping)
918
919 for expert_name, loaded_weight in weights:
920 qual_name = f"{self.layer_name}.{expert_name}"
921 is_fused = loaded_weight.dim() == 3
922 # Fused tensors and ambiguous names keep the full mapping.
923 candidates = expert_mapping
924 if not is_fused and qual_name.count("experts.") == 1:
925 expert_key = qual_name.partition("experts.")[2].partition(".")[0]
926 candidates = mapping_by_expert.get(expert_key, expert_mapping)
927
928 matched = False
929 for param_name, weight_name, expert_id, shard_id in candidates:
930 if weight_name not in qual_name:
931 if matched and is_fused:
932 # Fused tensors use the first contiguous run of matches.
933 break
934 continue
935 matched = True
936 is_per_expert_fused_w13 = (
937 not is_fused
938 and shard_id in {"w1", "w3"}
939 and any(
940 f".{fused_name}." in qual_name
941 for fused_name in ("gate_up_proj", "w13")
942 )
943 )
944 weight_name = qual_name.replace(weight_name, param_name)
945 param_name = weight_name.removeprefix(f"{self.layer_name}.")
946 param = getattr(self, param_name, None)
947 if param is None:
948 if param_name.endswith(("w13_bias", "w2_bias")):
949 continue
950 raise AttributeError(
951 f"Layer {self.layer_name} has no parameter {param_name!r} "
952 f"for checkpoint weight {qual_name!r}"
953 )
954 if is_fused:
955 quant_method = getattr(param, "quant_method", None)
956 # Block scales share the weight's two-dimensional layout.
957 # Other quantization metadata can use independent layouts.
958 uses_weight_layout = (
959 "scale" not in weight_name
960 or quant_method == FusedMoeWeightScaleSupported.BLOCK.value
961 )
962 # w1 and w3 share one fused tensor; use a local copy so the
963 # transpose below doesn't mutate loaded_weight across
964 # iterations (else w3 is transposed twice and wrongly chunked)
965 fused_weight = self._orient_fused_weight(
966 loaded_weight,
967 self.is_fused_checkpoint_transposed and uses_weight_layout,
968 )
969 if shard_id in {"w1", "w3"}:
970 # Repurpose expert_id for deconcatenating w1 and w3
971 experts_shard = fused_weight.chunk(2, dim=1)[expert_id]
972 else:
973 experts_shard = fused_weight
974 start = 0
975 elif is_per_expert_fused_w13:
976 shard_index = 0 if shard_id == "w1" else 1
977 experts_shard = loaded_weight.chunk(2, dim=0)[
978 shard_index
979 ].unsqueeze(0)
980 start = expert_id
981 else:
982 # loaded_weight is a single expert weight, so we add a dummy expert
983 # dimension to unify the loading logic with the fused case
984 experts_shard = loaded_weight.unsqueeze(0)
985 start = expert_id
986
987 # Unified loading logic for fused and non-fused experts
988 loaded_experts = experts_shard.unbind()
989 for expert_id, loaded_expert in enumerate(loaded_experts, start=start):
990 success = param.weight_loader(
991 param=param,
992 loaded_weight=loaded_expert,
993 weight_name=weight_name,
994 shard_id=shard_id,
995 expert_id=expert_id,
996 return_success=True,
997 )
998 if success:
999 logger.debug(
1000 "Loaded expert %d of shard %s into %s for layer %s",
1001 expert_id,
1002 shard_id,
1003 param_name,
1004 self.layer_name,
1005 )
1006 yield param_name
1007
1008 def get_expert_mapping(
1009 self,
1010 ckpt_gate_proj_name: str | None = None,
1011 ckpt_down_proj_name: str | None = None,
1012 ckpt_up_proj_name: str | None = None,
1013 include_fused: bool = False,
1014 ) -> list[tuple[str, str, int, str]]:
1015 moe_config = self.moe_config
1016 num_fused_shared_experts = self.expert_map_manager.num_fused_shared_experts
1017 num_redundant_experts = moe_config.num_experts - moe_config.num_logical_experts
1018 return self.build_expert_params_mapping(
1019 ckpt_gate_proj_name or self.ckpt_gate_proj_name,
1020 ckpt_down_proj_name or self.ckpt_down_proj_name,
1021 ckpt_up_proj_name or self.ckpt_up_proj_name,
1022 num_experts=moe_config.num_logical_experts + num_fused_shared_experts,
1023 num_redundant_experts=num_redundant_experts,
1024 routed_experts_prefix="",
1025 lora_base_layer_prefix=self.lora_base_layer_prefix,
1026 include_fused=include_fused,
1027 )
1028
1029 @staticmethod
1030 def make_expert_params_mapping(
1031 model: torch.nn.Module,
1032 ckpt_gate_proj_name: str,
1033 ckpt_down_proj_name: str,
1034 ckpt_up_proj_name: str,
1035 num_experts: int,
1036 num_redundant_experts: int = 0,
1037 routed_experts_prefix: str = "routed_experts",
1038 ) -> list[tuple[str, str, int, str]]:
1039 """Build the expert mapping, detecting the LoRA `base_layer.` prefix by
1040 scanning `model`'s parameters.
1041
1042 Legacy entry point for models that still hand-roll `load_weights`; the
1043 `RoutedExperts` weight loader uses `get_expert_mapping` /
1044 `build_expert_params_mapping` instead (which take the prefix directly).
1045 See `build_expert_params_mapping` for the returned tuple format.
1046 """
1047 has_base_layer = any(".base_layer." in n for n, _ in model.named_parameters())
1048 prefix = "base_layer." if has_base_layer else ""
1049 # These loaders index ``params_dict[full_name]``, so both sides get it.
1050 return RoutedExperts.build_expert_params_mapping(
1051 ckpt_gate_proj_name,
1052 ckpt_down_proj_name,
1053 ckpt_up_proj_name,
1054 num_experts,
1055 num_redundant_experts,
1056 routed_experts_prefix,
1057 lora_base_layer_prefix=prefix,
1058 lora_base_layer_prefix_on_param_name=prefix,
1059 )
1060
1061 @staticmethod
1062 def build_expert_params_mapping(
1063 ckpt_gate_proj_name: str,
1064 ckpt_down_proj_name: str,
1065 ckpt_up_proj_name: str,
1066 num_experts: int,
1067 num_redundant_experts: int = 0,
1068 routed_experts_prefix: str = "routed_experts",
1069 lora_base_layer_prefix: str = "",
1070 lora_base_layer_prefix_on_param_name: str = "",
1071 include_fused: bool = False,
1072 ) -> list[tuple[str, str, int, str]]:
1073 """Create expert parameter mapping for weight loading with redundant experts.
1074
1075 This mapping handles the physical-to-logical expert ID conversion needed
1076 when loading weights with EPLB redundant experts.
1077
1078 Args:
1079 ckpt_gate_proj_name: Name of gate projection in checkpoint
1080 ckpt_down_proj_name: Name of down projection in checkpoint
1081 ckpt_up_proj_name: Name of up projection in checkpoint
1082 num_experts: Number of logical (non-redundant) experts
1083 num_redundant_experts: Number of redundant experts
1084 lora_base_layer_prefix: LoRA ``base_layer.`` prefix for the
1085 ``weight_name`` (checkpoint) side
1086 lora_base_layer_prefix_on_param_name: same, for the ``param_name``
1087 side. Independent because ``get_expert_mapping`` resolves
1088 ``param_name`` via ``getattr`` against this layer's bare
1089 ``w13_weight``/``w2_weight`` (no prefix), while
1090 ``make_expert_params_mapping`` indexes the model-wide
1091 ``params_dict`` (prefix included).
1092 include_fused: Prepend the fused pre-fused-checkpoint entries
1093 routed_experts_prefix: Prefix of the routed experts submodule
1094
1095 Returns:
1096 List of tuples (param_name, weight_name, expert_id, shard_id)
1097 where:
1098 - param_name: Parameter name in the layer
1099 - weight_name: Weight name in checkpoint
1100 - expert_id: Physical expert ID
1101 - shard_id: Shard identifier (w1, w2, w3)
1102
1103 """
1104 num_physical_experts = num_experts + num_redundant_experts
1105
1106 # In the returned mapping:
1107 # - `expert_id` is the physical expert id
1108 # - `weight_name` contains the weight name of the logical expert
1109 # So that we should map the expert id to logical in `weight_name`
1110 physical_to_logical_map = (
1111 EplbState.build_initial_global_physical_to_logical_map(
1112 num_experts, num_redundant_experts
1113 )
1114 )
1115
1116 if routed_experts_prefix != "":
1117 routed_experts_prefix = f"{routed_experts_prefix}."
1118
1119 w13 = (
1120 f"experts.{lora_base_layer_prefix_on_param_name}{routed_experts_prefix}w13_"
1121 )
1122 w2 = f"experts.{lora_base_layer_prefix_on_param_name}{routed_experts_prefix}w2_"
1123
1124 fused_mapping = []
1125 if include_fused:
1126 gate_up = None
1127 if ckpt_gate_proj_name == "gate_proj" and ckpt_up_proj_name == "up_proj":
1128 gate_up = "gate_up_proj"
1129 elif ckpt_gate_proj_name == "w1" and ckpt_up_proj_name == "w3":
1130 gate_up = "w13"
1131 else:
1132 logger.warning(
1133 "Unexpected gate/up projection names: %s, %s. "
1134 "Fused gate/up mapping will be skipped.",
1135 ckpt_gate_proj_name,
1136 ckpt_up_proj_name,
1137 )
1138 if gate_up is not None:
1139 fused_mapping = [
1140 # (param_name, weight_name, expert_id, shard_id)
1141 (f"{w13}weight", f"experts.{gate_up}", 0, "w1"),
1142 (f"{w13}weight", f"experts.{gate_up}", 1, "w3"),
1143 (f"{w2}weight", f"experts.{ckpt_down_proj_name}", 0, "w2"),
1144 ]
1145 fused_mapping.extend(
1146 (
1147 w13,
1148 (
1149 f"experts.{physical_to_logical_map[expert_id]}."
1150 f"{gate_up}.{lora_base_layer_prefix}"
1151 ),
1152 expert_id,
1153 shard_id,
1154 )
1155 for expert_id in range(num_physical_experts)
1156 for shard_id in ("w1", "w3")
1157 )
1158
1159 per_expert_mapping = [
1160 # (param_name, weight_name, expert_id, shard_id)
1161 (
1162 w13 if weight_name in [ckpt_gate_proj_name, ckpt_up_proj_name] else w2,
1163 f"experts.{physical_to_logical_map[expert_id]}.{weight_name}.{lora_base_layer_prefix}",
1164 expert_id,
1165 shard_id,
1166 )
1167 for expert_id in range(num_physical_experts)
1168 for shard_id, weight_name in [
1169 ("w1", ckpt_gate_proj_name),
1170 ("w2", ckpt_down_proj_name),
1171 ("w3", ckpt_up_proj_name),
1172 ]
1173 ]
1174
1175 return fused_mapping + per_expert_mapping
1176
1177 def get_expert_weights(self) -> Iterable[torch.Tensor]:
1178 def _maybe_make_contiguous(
1179 name: str, p: torch.nn.Parameter
1180 ) -> torch.nn.Parameter:
1181 """In some cases, the last 2 dimensions (the non-expert dimensions)
1182 of the weight scale tensor are transposed. This function
1183 transforms the tensor (view update) so the tensor is contiguous().
1184 Example: A non-contiguous scale tensor,
1185 `x` of shape (E, 32, 16) and stride (512, 1, 32) is transformed to
1186 `x_` of shape (E, 16, 32) and stride (512, 32, 1).
1187 Note that we specifically use torch.transpose() so `x_` refers
1188 to the same underlying memory. The tensors `x` and `x_`, pointing
1189 to the same underlying memory make this transformation safe in the
1190 context of EPLB. i.e. It is the same memory and just the view
1191 is different.
1192 Note: This function handles the "weight_scale" tensors specifically.
1193 This could however be generalized to handle similar tensors.
1194 """
1195 if p.ndim != 3:
1196 return p
1197 if p.is_contiguous():
1198 # Already contiguous. do nothing.
1199 return p
1200 # p is non-contiguous. We only handle the case where the last 2
1201 # dimensions of the scales tensor is transposed. We can handle
1202 # other cases when they become relevant.
1203 is_transposed_12 = p.stride(1) == 1 and p.stride(2) != 1
1204 if "weight_scale" not in name or not is_transposed_12:
1205 # do nothing.
1206 return p
1207
1208 # Do not update the layer parameter as the layer's MoE operations would
1209 # expect the parameter's tensor to the same shape / stride. Instead,
1210 # make a new torch.nn.Parameter that is used just in the context of
1211 # EPLB.
1212 return torch.nn.Parameter(
1213 torch.transpose(p.data, 1, 2), requires_grad=False
1214 )
1215
1216 weights = list(self.named_parameters())
1217 weights = [(name, _maybe_make_contiguous(name, p)) for name, p in weights]
1218
1219 # `w13_input_scale` and `w2_input_scale` are global per-tensor
1220 # activation scales shared across all experts (e.g. NVFP4).
1221 # They are broadcast views (stride 0) from .expand() and are
1222 # not actual expert weights, so exclude them from EPLB.
1223 NON_EXPERT_WEIGHTS = {
1224 "e_score_correction_bias",
1225 "w13_input_scale",
1226 "w2_input_scale",
1227 "hash_indices_table",
1228 }
1229
1230 # Parameters of non-expert submodules that live inside runner (RoutedExperts).
1231 # These must be excluded from EPLB weight rearrangement.
1232 NON_EXPERT_PREFIXES = ()
1233
1234 assert all(
1235 weight.is_contiguous()
1236 for name, weight in weights
1237 if not name.startswith(NON_EXPERT_PREFIXES)
1238 and name not in NON_EXPERT_WEIGHTS
1239 )
1240
1241 return [
1242 weight.view(self.local_num_experts, -1)
1243 for name, weight in weights
1244 if name not in NON_EXPERT_WEIGHTS
1245 and weight.shape != torch.Size([])
1246 and not name.startswith(NON_EXPERT_PREFIXES)
1247 ]
1248
1249 #
1250 # Execution
1251 #
1252
1253 def forward_modular(
1254 self,
1255 x: torch.Tensor,
1256 topk_weights: torch.Tensor,
1257 topk_ids: torch.Tensor,
1258 shared_experts: "SharedExperts | None" = None,
1259 shared_experts_input: torch.Tensor | None = None,
1260 ) -> torch.Tensor | UnfinalizedMoEOutput:
1261 """Execute routed experts using the quantization method's apply function.
1262
1263 This is called by the runner after router selection (for modular kernels)
1264 quant_method.apply() which accesses the weights on this RoutedExperts
1265 instance.
1266
1267 Args:
1268 x: Input tensor after any transforms
1269 topk_weights: Routing weights from router (for modular kernels)
1270 topk_ids: Selected expert IDs from router (for modular kernels)
1271 shared_experts: The shared experts (if any)
1272 shared_experts_input: Input for shared experts (if any)
1273
1274 Returns:
1275 Finalized routed states or a deferred-finalize output.
1276
1277 """
1278 assert not self.quant_method.is_monolithic
1279
1280 # Modular kernels use pre-computed routing
1281 return self.quant_method.apply(
1282 layer=self,
1283 x=x,
1284 topk_weights=topk_weights,
1285 topk_ids=topk_ids,
1286 shared_experts=shared_experts,
1287 shared_experts_input=shared_experts_input,
1288 )
1289
1290 def forward_monolithic(
1291 self,
1292 x: torch.Tensor,
1293 router_logits: torch.Tensor | None = None,
1294 input_ids: torch.Tensor | None = None,
1295 ) -> torch.Tensor | UnfinalizedMoEOutput:
1296 """Execute routed experts using the quantization method's apply function.
1297
1298 This is called by the runner after router selection (for modular kernels)
1299 or with router logits (for monolithic kernels). It delegates to
1300 quant_method.apply() which accesses the weights on this RoutedExperts
1301 instance.
1302
1303 Args:
1304 x: Input tensor after any transforms
1305 router_logits: Router logits (for monolithic kernels)
1306 input_ids: input ids for DeepSeek V4
1307
1308 Returns:
1309 Finalized routed states or a deferred-finalize output.
1310
1311 """
1312 assert self.quant_method.is_monolithic
1313
1314 # Monolithic kernels handle routing internally
1315 return self.quant_method.apply_monolithic(
1316 layer=self,
1317 x=x,
1318 router_logits=router_logits,
1319 input_ids=input_ids,
1320 )
1321
1322 def forward(
1323 self,
1324 *args,
1325 **kwargs,
1326 ) -> torch.Tensor:
1327 raise AssertionError("Call forward_modular or forward_monolithic instead.")
1328
1329
1330 # Mark the RoutedExperts weight_loader as supporting MoE-specific parameters
1331 RoutedExperts.weight_loader.supports_moe_loading = True # type: ignore[attr-defined]