@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / ff08e51940
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 14, 22:35 UTC

[NVIDIA] Fix Llama4 Scout FP4 functionality issues (#21499)

Signed-off-by: Po-Han Huang <pohanh@nvidia.com>

Po-Han Huang (NVIDIA) committed Jul 30, 2025 at 22:33 UTC ff08e51940a77d2dd14a6c512bec4613d060b4fa
3 files changed +219 -70
vllm/model_executor/layers/fused_moe/layer.py
+14 -1
@@ -874,6 +874,14 @@ class FusedMoE(torch.nn.Module):
874 elif shard_id == "w2":
875 param_data[expert_id] = loaded_weight
876
877 + def _load_w13_weight_scale(self, shard_dim: int,
878 + loaded_weight: torch.Tensor,
879 + param: torch.Tensor, tp_rank: int):
880 + shard_size = param.shape[shard_dim]
881 + loaded_weight = loaded_weight.narrow(shard_dim, shard_size * tp_rank,
882 + shard_size)
883 + param.copy_(loaded_weight)
884 +
885 def _load_model_weight_or_group_weight_scale(self,
886 shard_dim: int,
887 expert_data: torch.Tensor,
@@ -1123,7 +1131,12 @@ class FusedMoE(torch.nn.Module):
1131 "weight_scale_2" in weight_name if uses_weight_scale_2 else
1132 "weight_scale" in weight_name) or "input_scale" in weight_name
1133
1126 - if per_tensor_conditions:
1134 + if "w13_weight_scale" in weight_name:
1135 + self._load_w13_weight_scale(shard_dim=shard_dim,
1136 + loaded_weight=loaded_weight,
1137 + param=param,
1138 + tp_rank=self.tp_rank)
1139 + elif per_tensor_conditions:
1140 self._load_per_tensor_weight_scale(
1141 shard_id=shard_id,
1142 param=param,
vllm/model_executor/layers/quantization/modelopt.py
-2
@@ -778,8 +778,6 @@ class ModelOptNvFp4LinearMethod(LinearMethodBase):
778 # Swizzle the weight blockscale.
779 # contracting dimension is input dimension
780 # block_size = 16;
781 - assert (layer.weight_scale.shape[1] % 16 == 0), (
782 - "Expected weight_scale.dim(1) to be divisible by 16")
781 assert (layer.weight_scale.dtype == torch.float8_e4m3fn), (
782 "Weight Block scale must be represented as FP8-E4M3")
783 swizzled_weight_scale = swizzle_blockscale(layer.weight_scale)
vllm/model_executor/models/llama4.py
+205 -67
@@ -342,34 +342,94 @@ class Llama4Model(LlamaModel):
342 expert_params_mapping: list[tuple[str, str, int, str]],
343 fused: bool = True,
344 ) -> bool:
345 + """
346 + Load MoE expert weights.
347 +
348 + Args:
349 + name: The name of the weight to load.
350 + loaded_weight: The weight to load.
351 + params_dict: The dictionary of module parameters.
352 + loaded_params: The set of already loaded parameters.
353 + expert_params_mapping: The mapping of expert parameters. Must be
354 + generated by FusedMoE.make_expert_params_mapping().
355 + fused: Whether the expert weights are fused into a single weight
356 + tensor or are separate weight tensors for each expert.
357 + When fused is True, loaded_weight should have shape of:
358 + [num_experts, hidden_in, hidden_out] for gate/up/down proj and
359 + [hidden_out, hidden_in] for the others like router.
360 + When fused is False, loaded_weight should have shape of:
361 + [hidden_out, hidden_in].
362 +
363 + Returns:
364 + True if loaded_weight is one of MoE weights and the MoE expert
365 + weights are loaded successfully, False otherwise.
366 + """
367 +
368 + # Whether the MoE expert weights are loaded successfully.
369 expert_param_loaded = False
346 - if "experts.gate_up_proj" in name:
347 - loaded_weight = loaded_weight.chunk(2, dim=-1)
370 +
371 + # If fused is True, the loaded weight is in the layout of:
372 + # [num_experts, hidden_in, hidden_out], so we must transpose the last
373 + # two dimensions to match the expected layout of the parameters.
374 + if fused and loaded_weight.ndim == 3:
375 + loaded_weight = loaded_weight.transpose(-1, -2)
376 +
377 + # If the gate_proj and up_proj weights are fused into a single
378 + # weight tensor, we need to split the weight tensor into a tuple
379 + # of two weight tensors along the hidden_out dimension.
380 + if "experts.gate_up_proj" in name:
381 + loaded_weight = loaded_weight.chunk(2, dim=-2)
382 +
383 + # Iterate over all the expert parameters and load the weights if we find
384 + # a match in weight name.
385 for (param_name, weight_name, expert_id,
386 shard_id) in expert_params_mapping:
387 +
388 + # Get a view of the loaded_weight to avoid modifying the original
389 + # one across iterations.
390 new_loaded_weight = loaded_weight
391 +
392 + # If expert weights are fused into a single weight tensor, remove
393 + # the expert index from the expected weight name.
394 if fused:
395 + # The string between e_str and proj_str is the expert index.
396 e_str, _, proj_str, _ = weight_name.split('.')
397 weight_name = f"{e_str}.{proj_str}"
398 param_name = f"{param_name}weight"
399 +
400 + # Skip if the current weight is not one of the MoE weights.
401 if weight_name not in name:
402 continue
403 +
404 + # Replace the weight name with the parameter name.
405 full_param_name = name.replace(weight_name, param_name)
358 - # Skip layers on other devices.
406 +
407 + # Skip if the current weight corresponds to a parameter that
408 + # does not exist on the current PP (pipeline parallel) rank.
409 if is_pp_missing_parameter(name, self):
410 continue
411 +
412 + # Skip if the current weight is for the bias.
413 if ((name.endswith(".bias") or name.endswith("_bias"))
414 and name not in params_dict):
415 continue
416 +
417 param = params_dict[full_param_name]
418 weight_loader = param.weight_loader
419 +
420 if fused:
421 + # If the parameter is for w13 together, the corresponding weight
422 + # will be a tuple, so we must select the correct weight
423 + # depending on the shard id, which is either "w1" or "w3".
424 if "w13" in full_param_name:
425 + assert shard_id in ["w1", "w3"]
426 shard_idx = 0 if shard_id == "w1" else 1
427 new_loaded_weight = new_loaded_weight[shard_idx]
370 - new_loaded_weight = new_loaded_weight.transpose(-1, -2)
428 +
429 + # If EP (expert parallel) is enabled, update expert_id to the
430 + # starting expert index for the current EP rank and extract the
431 + # corresponding expert weights.
432 layer_idx = extract_layer_index(name)
372 - # EP mapping
433 expert_map = self.layers[
434 layer_idx].feed_forward.experts.expert_map
435 if expert_map is not None:
@@ -382,6 +442,9 @@ class Llama4Model(LlamaModel):
442 else:
443 # TODO: add EP support for non fused weights
444 pass
445 +
446 + # Load the weight into the module parameter with corresponding
447 + # shard id and expert id.
448 weight_loader(param,
449 new_loaded_weight,
450 full_param_name,
@@ -390,10 +453,13 @@ class Llama4Model(LlamaModel):
453
454 loaded_params.add(full_param_name)
455 expert_param_loaded = True
456 +
457 return expert_param_loaded
458
459 def load_weights(self, weights: Iterable[tuple[str,
460 torch.Tensor]]) -> set[str]:
461 + # Name mapping from the parameter name to the shard name and
462 + # corresponding shard id.
463 stacked_params_mapping = [
464 # (param_name, shard_name, shard_id)
465 (".qkv_proj", ".q_proj", "q"),
@@ -402,26 +468,43 @@ class Llama4Model(LlamaModel):
468 (".gate_up_proj", ".gate_proj", 0),
469 (".gate_up_proj", ".up_proj", 1),
470 ]
471 + # Indicate whether the expert weights are fused into a single weight
472 + # tensor.
473 fused_experts_params = False
474 + # Expert parameter mapping for the case where the expert weights are
475 + # not fused into a single weight tensor.
476 expert_params_mapping = FusedMoE.make_expert_params_mapping(
477 ckpt_gate_proj_name="gate_proj",
478 ckpt_down_proj_name="down_proj",
479 ckpt_up_proj_name="up_proj",
480 num_experts=self.num_experts)
481 + # Expert parameter mapping for the case where the expert weights are
482 + # fused into a single weight tensor.
483 expert_params_mapping_fused = FusedMoE.make_expert_params_mapping(
484 ckpt_gate_proj_name="gate_up_proj",
485 ckpt_down_proj_name="down_proj",
486 ckpt_up_proj_name="gate_up_proj",
487 num_experts=1)
488 + # All the module parameters.
489 params_dict = dict(self.named_parameters())
490 + # The module parameters that have been loaded.
491 loaded_params: set[str] = set()
492 +
493 + # Iterate over all the weights and load them into module parameters.
494 for name, loaded_weight in weights:
495 +
496 + # If the name contains "experts.gate_up_proj" or "experts.down_proj"
497 + # without the expert indices, it means the expert weights are fused
498 + # into a single weight tensor across all experts.
499 if "experts.gate_up_proj" in name or "experts.down_proj" in name:
500 fused_experts_params = True
501 expert_params_mapping = expert_params_mapping_fused
502 +
503 + # If kv cache quantization scales exist and the weight name
504 + # corresponds to one of the kv cache quantization scales, load
505 + # them.
506 if (self.quant_config is not None and
507 (scale_name := self.quant_config.get_cache_scale(name))):
424 - # Loading kv cache quantization scales
508 param = params_dict[scale_name]
509 weight_loader = getattr(param, "weight_loader",
510 default_weight_loader)
@@ -430,84 +513,119 @@ class Llama4Model(LlamaModel):
513 weight_loader(param, loaded_weight)
514 loaded_params.add(scale_name)
515 continue
516 +
517 + # Iterate over stacked_params_mapping to check if the current weight
518 + # is one of the stacked parameters. If so, load the weight with the
519 + # corresponding shard id. Note that MoE weights are handled
520 + # separately in the else block.
521 for param_name, weight_name, shard_id in stacked_params_mapping:
522 + # Skip if the current weight is not one of the stacked
523 + # parameters or if the current weight is a MoE weight.
524 if weight_name not in name or "experts" in name:
525 continue
436 - # This check is for ModelOpt ckpts with kv cache quant enabled
526 +
527 + # For ModelOpt checkpoints, we need to rename the self_attn
528 + # weight/weight_scale names except for kv cache scales.
529 if not (name.endswith(
530 (".k_scale", ".v_scale")) and "self_attn" in name):
531 name = name.replace(weight_name, param_name)
532 +
533 + # Skip if the current weight corresponds to a parameter that
534 + # does not exist on the current PP (pipeline parallel) rank.
535 if is_pp_missing_parameter(name, self):
536 continue
442 - if name.endswith("scale") and "expert" not in name:
443 - # Remapping the name of FP8 kv-scale.
537 +
538 + # Remap kv cache scale names for ModelOpt checkpoints.
539 + # TODO: ModelOpt should implement get_cache_scale() such that
540 + # kv cache scale name remapping can be done there.
541 + if name.endswith("scale"):
542 name = maybe_remap_kv_scale_name(name, params_dict)
543 if name is None:
544 continue
545 +
546 + # Load the weight into the module parameter with corresponding
547 + # shard id and exit the for loop and the else block.
548 param = params_dict[name]
549 weight_loader = getattr(param, "weight_loader",
550 default_weight_loader)
551 +
552 if weight_loader == default_weight_loader:
553 weight_loader(param, loaded_weight)
554 else:
555 weight_loader(param, loaded_weight, shard_id)
556 +
557 loaded_params.add(name)
558 break
559 +
560 + # Handle normal (non-stacked) weights and MoE weights.
561 else:
457 - moe_loaded = self.load_moe_expert_weights(
458 - name,
459 - loaded_weight,
460 - params_dict,
461 - loaded_params,
462 - expert_params_mapping,
463 - fused=fused_experts_params)
464 -
465 - if not moe_loaded:
466 - if is_pp_missing_parameter(name, self):
467 - continue
562 + # First, try to load MoE weights using load_moe_expert_weights.
563 + # If successful, move on to next loaded weight.
564 + if self.load_moe_expert_weights(name,
565 + loaded_weight,
566 + params_dict,
567 + loaded_params,
568 + expert_params_mapping,
569 + fused=fused_experts_params):
570 + continue
571
469 - # Handle flat expert scale parameters that
470 - # don't match per-expert patterns
471 - if ("experts." in name and ("w13_input_scale" in name
472 - or "w13_weight_scale" in name
473 - or "w2_input_scale" in name
474 - or "w2_weight_scale" in name)):
475 - # These are flat expert scales that apply to all experts
476 - param = params_dict[name]
477 - weight_loader = getattr(param, "weight_loader",
478 - default_weight_loader)
479 -
480 - # Check for MoE-specific loading support via
481 - # attribute instead of expensive runtime reflection
482 - supports_moe = getattr(weight_loader,
483 - 'supports_moe_loading', False)
484 -
485 - if supports_moe:
486 - # This is a MoE weight loader
487 - if "w13_" in name:
488 - shard_id = "w1"
489 - elif "w2_" in name:
490 - shard_id = "w2"
491 - else:
492 - shard_id = "w1"
493 -
494 - weight_loader(param,
495 - loaded_weight,
496 - name,
497 - shard_id=shard_id,
498 - expert_id=0)
499 - else:
500 - # Regular weight loader (handles both
501 - # param.weight_loader and default_weight_loader)
502 - weight_loader(param, loaded_weight)
503 - loaded_params.add(name)
504 - continue
572 + # Skip if the current weight corresponds to a parameter that
573 + # does not exist on the current PP (pipeline parallel) rank.
574 + if is_pp_missing_parameter(name, self):
575 + continue
576 +
577 + # Handle flat expert scale parameters that don't match
578 + # per-expert patterns, i.e. one weight scale tensor for all
579 + # experts.
580 + scale_names = [
581 + "w13_input_scale", "w13_weight_scale", "w2_input_scale",
582 + "w2_weight_scale"
583 + ]
584 + if ("experts." in name and any(scale_name in name
585 + for scale_name in scale_names)):
586
587 param = params_dict[name]
588 weight_loader = getattr(param, "weight_loader",
589 default_weight_loader)
509 - weight_loader(param, loaded_weight)
590 +
591 + # If weight loader supports special moe loading, use it to
592 + # avoid expensive runtime reflection
593 + if getattr(weight_loader, 'supports_moe_loading', False):
594 + # Map the weight name to the corresponding shard id.
595 + shard_id = "w2" if "w2_" in name else "w1"
596 +
597 + # Transpose if weight scales are FP8 block scales with
598 + # three dimensions:
599 + # [num_experts, hidden_in, hidden_out].
600 + if name.endswith("weight_scale") \
601 + and loaded_weight.dtype == torch.float8_e4m3fn \
602 + and loaded_weight.ndim == 3:
603 + loaded_weight = loaded_weight.transpose(-1, -2)
604 +
605 + # Load the weight into the module parameter with
606 + # corresponding shard id and expert id.
607 + weight_loader(param,
608 + loaded_weight,
609 + name,
610 + shard_id=shard_id,
611 + expert_id=0)
612 +
613 + else:
614 + # Regular weight loader (handles both
615 + # param.weight_loader and default_weight_loader)
616 + weight_loader(param, loaded_weight)
617 +
618 loaded_params.add(name)
619 + continue
620 +
621 + # Handle normal (non-stacked, non-MoE) weights.
622 + param = params_dict[name]
623 + weight_loader = getattr(param, "weight_loader",
624 + default_weight_loader)
625 + weight_loader(param, loaded_weight)
626 + loaded_params.add(name)
627 +
628 + # Finally, return the set of loaded parameters.
629 return loaded_params
630
631
@@ -560,23 +678,43 @@ class Llama4ForCausalLM(LlamaForCausalLM):
678 loaded_weight: torch.Tensor,
679 ) -> tuple[str, torch.Tensor]:
680
563 - def permute(w: torch.Tensor, n_heads: int):
681 + # Helper function to permute the weight's channels
682 + def permute(w: torch.Tensor, n_heads: int, is_weight_scale: bool):
683 +
684 + # Calculate the expected shape of the weight.
685 + # Do not rely on w's shape, as it may be in another layout.
686 attn_in = self.config.head_dim * n_heads
687 attn_out = self.config.hidden_size
688
689 + # If the weight is FP4 packed as uint8, we need to divide attn_out
690 + # by 2.
691 + if w.dtype == torch.uint8 and w.shape[1] * 2 == attn_out:
692 + attn_out = attn_out // 2
693 +
694 + # If the weight is a weight scale, we need to divide attn_out by
695 + # block size, which is currently 16.
696 + elif w.dtype == torch.float8_e4m3fn and is_weight_scale \
697 + and w.shape[1] * 16 == attn_out:
698 + attn_out = attn_out // 16
699 +
700 return w.view(n_heads, attn_in // n_heads // 2, 2,
701 attn_out).transpose(1, 2).reshape(attn_in, attn_out)
702
703 modules = name.split(".")
704
572 - # rotary embeds should be sliced
573 - if ("wk" in modules or "k_proj" in modules) \
574 - and modules[-1] == "weight":
575 - loaded_weight = permute(loaded_weight,
576 - self.config.num_key_value_heads)
577 - elif ("wq" in modules or "q_proj" in modules) \
578 - and modules[-1] == "weight":
579 - loaded_weight = permute(loaded_weight,
580 - self.config.num_attention_heads)
705 + # Permute Q/K weights and weight block scales for rotary embedding
706 + is_weight = modules[-1] == "weight"
707 + is_nvfp4_weight_scale = (modules[-1] == "weight_scale" and
708 + loaded_weight.dtype == torch.float8_e4m3fn)
709 +
710 + if is_weight or is_nvfp4_weight_scale:
711 + if ("wk" in modules or "k_proj" in modules):
712 + loaded_weight = permute(loaded_weight,
713 + self.config.num_key_value_heads,
714 + is_nvfp4_weight_scale)
715 + elif ("wq" in modules or "q_proj" in modules):
716 + loaded_weight = permute(loaded_weight,
717 + self.config.num_attention_heads,
718 + is_nvfp4_weight_scale)
719
720 return name, loaded_weight