@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 47e605092b
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 5, 12:15 UTC

[Gemma4] Enable Fast Prefill Optimization (#38879)

Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>

Lucas Wilkinson committed Apr 6, 2026 at 11:19 UTC 47e605092b7fce3d64264b34250b1a286f344633
1 file changed +369 -47
vllm/model_executor/models/gemma4.py
+369 -47
@@ -19,6 +19,7 @@
19 """Gemma 4 model implementation for vLLM."""
20
21 from collections.abc import Iterable
22 +from dataclasses import replace
23 from itertools import islice
24
25 import regex as re
@@ -32,6 +33,7 @@ from vllm.distributed import (
33 get_tensor_model_parallel_rank,
34 get_tensor_model_parallel_world_size,
35 )
36 +from vllm.forward_context import get_forward_context
37 from vllm.logger import init_logger
38 from vllm.model_executor.layers.activation import GeluAndMul
39 from vllm.model_executor.layers.attention import Attention
@@ -56,6 +58,7 @@ from vllm.model_executor.model_loader.weight_utils import (
58 maybe_remap_kv_scale_name,
59 )
60 from vllm.sequence import IntermediateTensors
61 +from vllm.v1.attention.backends.utils import KVSharingFastPrefillMetadata
62
63 from .interfaces import MixtureOfExperts, SupportsLoRA, SupportsPP
64 from .utils import (
@@ -636,7 +639,205 @@ class Gemma4DecoderLayer(nn.Module):
639 return hidden_states, None
640
641
639 -@support_torch_compile
642 +def _run_decoder_layers(
643 + decoder_layers: list[Gemma4DecoderLayer],
644 + layer_idx_start: int,
645 + positions: torch.Tensor,
646 + hidden_states: torch.Tensor,
647 + per_layer_inputs: torch.Tensor | None = None,
648 + **kwargs,
649 +) -> torch.Tensor:
650 + """Run a slice of decoder layers with PLE extraction."""
651 + residual = None
652 + for idx, layer in enumerate(decoder_layers):
653 + layer_idx = idx + layer_idx_start
654 + layer_per_input = (
655 + per_layer_inputs[:, layer_idx, :] if per_layer_inputs is not None else None
656 + )
657 + hidden_states, residual = layer(
658 + positions,
659 + hidden_states,
660 + residual,
661 + per_layer_input=layer_per_input,
662 + **kwargs,
663 + )
664 + return hidden_states
665 +
666 +
667 +@support_torch_compile(
668 + enable_if=lambda vllm_config: vllm_config.cache_config.kv_sharing_fast_prefill
669 +)
670 +class Gemma4SelfDecoderLayers(nn.Module):
671 + """Compiled wrapper: embedding + non-KV-shared layers (YOCO first half).
672 +
673 + Owns the embedding and PLE modules so they are inside the compiled
674 + graph. Gemma4Model delegates embedding methods here.
675 + """
676 +
677 + def __init__(
678 + self,
679 + *,
680 + vllm_config: VllmConfig,
681 + prefix: str = "",
682 + decoder_layers: list[Gemma4DecoderLayer],
683 + layer_idx_start: int,
684 + embed_tokens: VocabParallelEmbedding,
685 + normalizer: torch.Tensor,
686 + embed_tokens_per_layer: VocabParallelEmbedding | None,
687 + embed_scale_per_layer: torch.Tensor | None,
688 + per_layer_model_projection: ColumnParallelLinear | None,
689 + per_layer_projection_norm: RMSNorm | None,
690 + per_layer_input_scale: torch.Tensor | None,
691 + per_layer_projection_scale: torch.Tensor | None,
692 + ):
693 + super().__init__()
694 + self.decoder_layers = decoder_layers
695 + self.layer_idx_start = layer_idx_start
696 +
697 + config = _get_text_config(vllm_config.model_config.hf_config)
698 + self.config = config
699 + self.hidden_size_per_layer_input = getattr(
700 + config, "hidden_size_per_layer_input", 0
701 + )
702 + self.vocab_size_per_layer_input = getattr(
703 + config, "vocab_size_per_layer_input", config.vocab_size
704 + )
705 +
706 + # Shared references to modules owned by Gemma4Model — must be
707 + # inside this nn.Module so torch.compile captures them.
708 + self.embed_tokens = embed_tokens
709 + self.normalizer = normalizer
710 + self.embed_tokens_per_layer = embed_tokens_per_layer
711 + self.embed_scale_per_layer = embed_scale_per_layer
712 + self.per_layer_model_projection = per_layer_model_projection
713 + self.per_layer_projection_norm = per_layer_projection_norm
714 + self.per_layer_input_scale = per_layer_input_scale
715 + self.per_layer_projection_scale = per_layer_projection_scale
716 +
717 + def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
718 + return self.embed_tokens(input_ids) * self.normalizer
719 +
720 + def get_per_layer_inputs(self, input_ids: torch.Tensor) -> torch.Tensor | None:
721 + """Get per-layer embeddings from embed_tokens_per_layer.
722 +
723 + Returns:
724 + Per-layer embeddings (num_tokens, num_layers,
725 + hidden_size_per_layer_input)
726 + """
727 + if self.embed_tokens_per_layer is None:
728 + return None
729 + per_layer_inputs_mask = torch.logical_and(
730 + input_ids >= 0,
731 + input_ids < self.vocab_size_per_layer_input,
732 + )
733 + per_layer_inputs_tokens = torch.where(
734 + per_layer_inputs_mask, input_ids, torch.zeros_like(input_ids)
735 + )
736 + per_layer_embeds = self.embed_tokens_per_layer(per_layer_inputs_tokens)
737 + per_layer_embeds = per_layer_embeds * self.embed_scale_per_layer
738 + return per_layer_embeds.reshape(
739 + *input_ids.shape,
740 + self.config.num_hidden_layers,
741 + self.hidden_size_per_layer_input,
742 + )
743 +
744 + def project_per_layer_inputs(
745 + self,
746 + inputs_embeds: torch.Tensor,
747 + per_layer_inputs: torch.Tensor | None,
748 + ) -> torch.Tensor | None:
749 + """Project inputs_embeds and combine with per_layer_inputs.
750 +
751 + Steps:
752 + 1. Project inputs_embeds: hidden_size → total_ple_dim
753 + 2. Scale by hidden_size^{-0.5}
754 + 3. Reshape to (num_tokens, num_layers, per_layer_dim)
755 + 4. Normalize with per_layer_projection_norm
756 + 5. Combine: (projection + per_layer_inputs) * 1/sqrt(2)
757 + """
758 + if self.per_layer_model_projection is None:
759 + return None
760 + per_layer_projection = self.per_layer_model_projection(inputs_embeds)
761 + per_layer_projection = per_layer_projection * self.per_layer_projection_scale
762 + per_layer_projection = per_layer_projection.reshape(
763 + *inputs_embeds.shape[:-1],
764 + self.config.num_hidden_layers,
765 + self.hidden_size_per_layer_input,
766 + )
767 + per_layer_projection = self.per_layer_projection_norm(per_layer_projection)
768 + if per_layer_inputs is None:
769 + return per_layer_projection
770 + return (per_layer_projection + per_layer_inputs) * self.per_layer_input_scale
771 +
772 + def forward(
773 + self,
774 + input_ids: torch.Tensor | None,
775 + positions: torch.Tensor,
776 + inputs_embeds: torch.Tensor | None = None,
777 + per_layer_inputs: torch.Tensor | None = None,
778 + **kwargs,
779 + ) -> tuple[torch.Tensor, torch.Tensor | None]:
780 + if inputs_embeds is not None:
781 + hidden_states = inputs_embeds
782 + per_layer_inputs = self.project_per_layer_inputs(
783 + hidden_states, per_layer_inputs
784 + )
785 + else:
786 + hidden_states = self.embed_input_ids(input_ids)
787 + per_layer_embeds = self.get_per_layer_inputs(input_ids)
788 + per_layer_inputs = self.project_per_layer_inputs(
789 + hidden_states, per_layer_embeds
790 + )
791 +
792 + hidden_states = _run_decoder_layers(
793 + self.decoder_layers,
794 + self.layer_idx_start,
795 + positions,
796 + hidden_states,
797 + per_layer_inputs,
798 + **kwargs,
799 + )
800 + return hidden_states, per_layer_inputs
801 +
802 +
803 +@support_torch_compile(
804 + enable_if=lambda vllm_config: vllm_config.cache_config.kv_sharing_fast_prefill
805 +)
806 +class Gemma4CrossDecoderLayers(nn.Module):
807 + """Cross-decoder layers (YOCO second half, KV-shared)."""
808 +
809 + def __init__(
810 + self,
811 + *,
812 + vllm_config: VllmConfig,
813 + prefix: str = "",
814 + decoder_layers: list[Gemma4DecoderLayer],
815 + layer_idx_start: int,
816 + ):
817 + super().__init__()
818 + self.decoder_layers = decoder_layers
819 + self.layer_idx_start = layer_idx_start
820 +
821 + def forward(
822 + self,
823 + positions: torch.Tensor,
824 + hidden_states: torch.Tensor,
825 + per_layer_inputs: torch.Tensor | None = None,
826 + **kwargs,
827 + ) -> torch.Tensor:
828 + return _run_decoder_layers(
829 + self.decoder_layers,
830 + self.layer_idx_start,
831 + positions,
832 + hidden_states,
833 + per_layer_inputs,
834 + **kwargs,
835 + )
836 +
837 +
838 +@support_torch_compile(
839 + enable_if=lambda vllm_config: not vllm_config.cache_config.kv_sharing_fast_prefill
840 +)
841 class Gemma4Model(nn.Module):
842 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
843 super().__init__()
@@ -740,6 +941,75 @@ class Gemma4Model(nn.Module):
941 torch.tensor(config.hidden_size**0.5),
942 persistent=False,
943 )
944 +
945 + # --- You Only Cache Once (YOCO) split for fast prefill ---
946 + first_kv_shared_layer_idx = config.num_hidden_layers - getattr(
947 + config, "num_kv_shared_layers", 0
948 + )
949 +
950 + from vllm.compilation.backends import set_model_tag
951 +
952 + # Layers 0..(K-1) are self-decoder layers in YOCO
953 + with set_model_tag("self_decoder"):
954 + self.self_decoder = Gemma4SelfDecoderLayers(
955 + vllm_config=vllm_config,
956 + prefix=f"{prefix}.self_decoder",
957 + decoder_layers=self.layers[:first_kv_shared_layer_idx],
958 + layer_idx_start=0,
959 + embed_tokens=self.embed_tokens,
960 + normalizer=self.normalizer,
961 + embed_tokens_per_layer=getattr(self, "embed_tokens_per_layer", None),
962 + embed_scale_per_layer=getattr(self, "embed_scale_per_layer", None),
963 + per_layer_model_projection=getattr(
964 + self, "per_layer_model_projection", None
965 + ),
966 + per_layer_projection_norm=getattr(
967 + self, "per_layer_projection_norm", None
968 + ),
969 + per_layer_input_scale=getattr(self, "per_layer_input_scale", None),
970 + per_layer_projection_scale=getattr(
971 + self, "per_layer_projection_scale", None
972 + ),
973 + )
974 + # Layers K..(N-1) are cross-decoder layers in YOCO
975 + with set_model_tag("cross_decoder"):
976 + self.cross_decoder = Gemma4CrossDecoderLayers(
977 + vllm_config=vllm_config,
978 + prefix=f"{prefix}.cross_decoder",
979 + decoder_layers=self.layers[first_kv_shared_layer_idx:],
980 + layer_idx_start=first_kv_shared_layer_idx,
981 + )
982 +
983 + self.fast_prefill_enabled = cache_config.kv_sharing_fast_prefill
984 +
985 + if self.fast_prefill_enabled:
986 + # Allocate static buffers for CUDAGraph
987 + max_num_tokens = vllm_config.scheduler_config.max_num_batched_tokens
988 + device = next(self.parameters()).device
989 + self.positions = torch.zeros(
990 + max_num_tokens, dtype=torch.int64, device=device
991 + )
992 + self.hidden_states = torch.zeros(
993 + (max_num_tokens, config.hidden_size),
994 + dtype=self.embed_tokens.weight.dtype,
995 + device=device,
996 + )
997 + if (
998 + self.hidden_size_per_layer_input
999 + and self.hidden_size_per_layer_input > 0
1000 + ):
1001 + self.per_layer_inputs = torch.zeros(
1002 + (
1003 + max_num_tokens,
1004 + config.num_hidden_layers,
1005 + self.hidden_size_per_layer_input,
1006 + ),
1007 + dtype=self.embed_tokens.weight.dtype,
1008 + device=device,
1009 + )
1010 + else:
1011 + self.per_layer_inputs = None
1012 +
1013 # Custom factory that includes per_layer_inputs for PLE-enabled PP.
1014 # per_layer_inputs has shape (batch, num_layers, per_layer_dim),
1015 # which differs from the standard (batch, hidden_size) shape,
@@ -776,47 +1046,22 @@ class Gemma4Model(nn.Module):
1046 self.make_empty_intermediate_tensors = _make_empty_intermediate_tensors
1047
1048 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
779 - return self.embed_tokens(input_ids) * self.normalizer
1049 + return self.self_decoder.embed_input_ids(input_ids)
1050
781 - def get_per_layer_inputs(self, input_ids: torch.Tensor) -> torch.Tensor:
1051 + def get_per_layer_inputs(self, input_ids: torch.Tensor) -> torch.Tensor | None:
1052 """Get per-layer embeddings from embed_tokens_per_layer.
1053
1054 Returns:
1055 Per-layer embeddings (num_tokens, num_layers,
1056 hidden_size_per_layer_input)
1057 """
788 - if self.embed_tokens_per_layer is None:
789 - return None
790 -
791 - # Handle out-of-vocab tokens for PLE (vocab_size_per_layer_input may
792 - # be smaller than the main vocab_size).
793 - per_layer_inputs_mask = torch.logical_and(
794 - input_ids >= 0,
795 - input_ids < self.vocab_size_per_layer_input,
796 - )
797 - per_layer_inputs_tokens = torch.where(
798 - per_layer_inputs_mask, input_ids, torch.zeros_like(input_ids)
799 - )
800 -
801 - # Get packed per-layer embeddings: (num_tokens, total_ple_dim)
802 - per_layer_embeds = self.embed_tokens_per_layer(per_layer_inputs_tokens)
803 -
804 - # Apply embed_scale (sqrt of per-layer hidden dim)
805 - per_layer_embeds = per_layer_embeds * self.embed_scale_per_layer
806 -
807 - # Reshape to (num_tokens, num_layers, hidden_size_per_layer_input)
808 - per_layer_embeds = per_layer_embeds.reshape(
809 - *input_ids.shape,
810 - self.config.num_hidden_layers,
811 - self.hidden_size_per_layer_input,
812 - )
813 - return per_layer_embeds
1058 + return self.self_decoder.get_per_layer_inputs(input_ids)
1059
1060 def project_per_layer_inputs(
1061 self,
1062 inputs_embeds: torch.Tensor,
1063 per_layer_inputs: torch.Tensor | None,
819 - ) -> torch.Tensor:
1064 + ) -> torch.Tensor | None:
1065 """Project inputs_embeds and combine with per_layer_inputs.
1066
1067 Steps:
@@ -826,29 +1071,94 @@ class Gemma4Model(nn.Module):
1071 4. Normalize with per_layer_projection_norm
1072 5. Combine: (projection + per_layer_inputs) * 1/sqrt(2)
1073 """
829 - if self.per_layer_model_projection is None:
830 - return None
1074 + return self.self_decoder.project_per_layer_inputs(
1075 + inputs_embeds, per_layer_inputs
1076 + )
1077
832 - # Project from hidden_size to total_ple_dim
833 - # Scaled projection: output = linear(input, weight) * scale
834 - per_layer_projection = self.per_layer_model_projection(inputs_embeds)
835 - per_layer_projection = per_layer_projection * self.per_layer_projection_scale
1078 + def fast_prefill_forward(
1079 + self,
1080 + input_ids: torch.Tensor | None,
1081 + positions: torch.Tensor,
1082 + inputs_embeds: torch.Tensor | None = None,
1083 + per_layer_inputs: torch.Tensor | None = None,
1084 + **kwargs,
1085 + ) -> torch.Tensor:
1086 + logits_indices_padded, num_logits_indices = None, None
1087 + attn_metadata = get_forward_context().attn_metadata
1088
837 - # Reshape to (num_tokens, num_layers, hidden_size_per_layer_input)
838 - per_layer_projection = per_layer_projection.reshape(
839 - *inputs_embeds.shape[:-1],
840 - self.config.num_hidden_layers,
841 - self.hidden_size_per_layer_input,
1089 + if attn_metadata is not None:
1090 + assert isinstance(attn_metadata, dict)
1091 + layer_attn_metadata = attn_metadata[
1092 + self.layers[-1].self_attn.attn.layer_name
1093 + ]
1094 + if isinstance(layer_attn_metadata, KVSharingFastPrefillMetadata):
1095 + logits_indices_padded = layer_attn_metadata.logits_indices_padded
1096 + num_logits_indices = layer_attn_metadata.num_logits_indices
1097 +
1098 + batch_size = positions.size(0)
1099 + self.positions[:batch_size].copy_(positions)
1100 + self_decoder_hidden_states, per_layer_inputs = self.self_decoder(
1101 + input_ids=input_ids,
1102 + positions=self.positions[:batch_size],
1103 + inputs_embeds=inputs_embeds,
1104 + per_layer_inputs=per_layer_inputs,
1105 + **kwargs,
1106 )
1107
844 - # Normalize
845 - per_layer_projection = self.per_layer_projection_norm(per_layer_projection)
1108 + if logits_indices_padded is None:
1109 + logits_indices_padded = torch.arange(
1110 + batch_size,
1111 + dtype=positions.dtype,
1112 + device=positions.device,
1113 + )
1114
847 - if per_layer_inputs is None:
848 - return per_layer_projection
1115 + # NOTE: Keep .clone() until fix in
1116 + # https://github.com/vllm-project/vllm/pull/22282
1117 + hidden_states = self_decoder_hidden_states.clone()
1118
850 - # Combine: (projection + per_layer_inputs) * scale
851 - return (per_layer_projection + per_layer_inputs) * self.per_layer_input_scale
1119 + num_padded = logits_indices_padded.size(0)
1120 + self.positions[:num_padded].copy_(positions[logits_indices_padded])
1121 + self.hidden_states[:num_padded].copy_(
1122 + self_decoder_hidden_states[logits_indices_padded]
1123 + )
1124 + if self.per_layer_inputs is not None and per_layer_inputs is not None:
1125 + self.per_layer_inputs[:num_padded].copy_(
1126 + per_layer_inputs[logits_indices_padded]
1127 + )
1128 +
1129 + # Update batch_descriptor so the cross-decoder's piecewise
1130 + # CUDAGraphWrapper dispatches to the correct (reduced) batch size.
1131 + forward_context = get_forward_context()
1132 + orig_batch_desc = forward_context.batch_descriptor
1133 + if orig_batch_desc is not None:
1134 + forward_context.batch_descriptor = replace(
1135 + orig_batch_desc, num_tokens=num_padded
1136 + )
1137 +
1138 + cross_per_layer = (
1139 + self.per_layer_inputs[:num_padded]
1140 + if self.per_layer_inputs is not None
1141 + else None
1142 + )
1143 + cross_hidden_states = self.cross_decoder(
1144 + self.positions[:num_padded],
1145 + self.hidden_states[:num_padded],
1146 + cross_per_layer,
1147 + **kwargs,
1148 + )
1149 +
1150 + # Restore the original batch_descriptor
1151 + forward_context.batch_descriptor = orig_batch_desc
1152 +
1153 + if num_logits_indices is not None:
1154 + assert num_logits_indices > 0
1155 + hidden_states[logits_indices_padded[:num_logits_indices]] = (
1156 + cross_hidden_states[:num_logits_indices]
1157 + )
1158 + else:
1159 + hidden_states = cross_hidden_states
1160 +
1161 + return hidden_states
1162
1163 def forward(
1164 self,
@@ -859,6 +1169,18 @@ class Gemma4Model(nn.Module):
1169 per_layer_inputs: torch.Tensor | None = None,
1170 **kwargs,
1171 ) -> torch.Tensor | IntermediateTensors:
1172 + if self.fast_prefill_enabled:
1173 + hidden_states = self.fast_prefill_forward(
1174 + input_ids,
1175 + positions,
1176 + inputs_embeds,
1177 + per_layer_inputs,
1178 + **kwargs,
1179 + )
1180 + hidden_states = self.norm(hidden_states)
1181 + return hidden_states
1182 +
1183 + # Normal (non-fast-prefill) path with PP support
1184 if get_pp_group().is_first_rank:
1185 if inputs_embeds is not None:
1186 hidden_states = inputs_embeds