@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / f91fb2fcf3
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 2, 06:00 UTC

[Bugfix] Convert Gemma4-MM ViT linear layers to vllm native impl (#43798)

Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn> Co-authored-by: ZiTian Zhao <zitian.zhao@tencentmusic.com> Co-authored-by: B-201 <Joy25810@foxmail.com>

Isotr0py committed Jun 2, 2026 at 12:41 UTC f91fb2fcf3f100d46baf4db2e5d86fa5596b276b
4 files changed +111 -11
vllm/model_executor/layers/quantization/bitsandbytes.py
+8 -1
@@ -1,6 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 +from functools import cached_property
5 from typing import Any, Union
6
7 import torch
@@ -168,6 +169,12 @@ class BitsAndBytesConfig(QuantizationConfig):
169 return None
170
171
172 +class BitsAndBytesWeightParameter(torch.nn.Parameter):
173 + @cached_property
174 + def dtype(self) -> torch.dtype:
175 + return torch.get_default_dtype()
176 +
177 +
178 def is_layer_skipped_bnb(prefix: str, llm_int8_skip_modules: list[str]):
179 # Split the prefix into its dot-separated components
180 components = prefix.split(".")
@@ -246,7 +253,7 @@ class BitsAndBytesLinearMethod(LinearMethodBase):
253 "The input size is not aligned with the quantized weight shape."
254 )
255
249 - qweight = torch.nn.Parameter(
256 + qweight = BitsAndBytesWeightParameter(
257 torch.empty(total_size // quant_ratio, 1, dtype=torch.uint8),
258 requires_grad=False,
259 )
vllm/model_executor/model_loader/bitsandbytes_loader.py
+23
@@ -744,6 +744,29 @@ class BitsAndBytesModelLoader(BaseModelLoader):
744 stacked_quant_state_dict[quant_param_name][shard_index] = quant_state_dict[
745 non_stacked_param_name
746 ]
747 +
748 + # repeat k_proj for v_proj for k_eq_v models (e.g. Gemma4)
749 + config = getattr(model, "config", None)
750 + if config is not None:
751 + text_config = config.get_text_config()
752 + if getattr(text_config, "attention_k_eq_v", False):
753 + shard_packed = {
754 + name
755 + for name, subs in self.modules_mapping.packed_mapping.items()
756 + if len(subs) == 3
757 + }
758 + for param_name, shards in stacked_quant_state_dict.items():
759 + is_target = (
760 + isinstance(shards, dict)
761 + and len(shards) == 2
762 + and any(
763 + param_name.endswith(f"{p}.weight") for p in shard_packed
764 + )
765 + )
766 + if is_target:
767 + assert 1 in shards and 2 not in shards
768 + shards[2] = shards[1]
769 +
770 return stacked_quant_state_dict
771
772 def _bind_quant_states_to_params(
vllm/model_executor/models/gemma4_mm.py
+51 -10
@@ -16,7 +16,7 @@ reason about temporal order.
16
17 import math
18 from collections.abc import Iterable, Mapping, Sequence
19 -from typing import Annotated, Any, Literal
19 +from typing import TYPE_CHECKING, Annotated, Any, Literal
20
21 import numpy as np
22 import torch
@@ -41,6 +41,7 @@ from vllm.model_executor.layers.layernorm import RMSNorm
41 from vllm.model_executor.layers.linear import ReplicatedLinear
42 from vllm.model_executor.models.gemma4 import Gemma4ForCausalLM
43 from vllm.model_executor.models.module_mapping import MultiModelKeys
44 +from vllm.model_executor.models.transformers.utils import recursive_replace_linear
45 from vllm.multimodal import MULTIMODAL_REGISTRY
46 from vllm.multimodal.inputs import (
47 MultiModalFieldConfig,
@@ -71,6 +72,7 @@ from .interfaces import (
72 SupportsLoRA,
73 SupportsMultiModal,
74 SupportsPP,
75 + SupportsQuant,
76 )
77 from .utils import (
78 AutoWeightsLoader,
@@ -79,6 +81,9 @@ from .utils import (
81 maybe_prefix,
82 )
83
84 +if TYPE_CHECKING:
85 + from vllm.model_executor.layers.quantization import QuantizationConfig
86 +
87 logger = init_logger(__name__)
88
89 # Video constants — match transformers Gemma4VideoProcessor defaults.
@@ -872,6 +877,9 @@ class Gemma4MultimodalEmbedder(nn.Module):
877 self,
878 multimodal_config: Gemma4VisionConfig | Gemma4AudioConfig,
879 text_config: Gemma4TextConfig,
880 + *,
881 + quant_config: "QuantizationConfig | None" = None,
882 + prefix: str = "",
883 ):
884 super().__init__()
885
@@ -895,6 +903,8 @@ class Gemma4MultimodalEmbedder(nn.Module):
903 embedding_dim,
904 self.text_hidden_size,
905 bias=False,
906 + quant_config=quant_config,
907 + prefix=maybe_prefix(prefix, "embedding_projection"),
908 )
909
910 def forward(self, inputs_embeds: torch.Tensor) -> torch.Tensor:
@@ -917,6 +927,7 @@ class Gemma4MultimodalEmbedder(nn.Module):
927 class Gemma4ForConditionalGeneration(
928 nn.Module,
929 SupportsMultiModal,
930 + SupportsQuant,
931 SupportsPP,
932 SupportsLoRA,
933 SupportsEagle3,
@@ -936,11 +947,14 @@ class Gemma4ForConditionalGeneration(
947 # Maps checkpoint prefixes to vLLM module paths.
948 hf_to_vllm_mapper = WeightsMapper(
949 orig_to_new_prefix={
950 + # vision tower
951 + "model.vision_tower": "vision_tower",
952 + "model.embed_vision": "embed_vision",
953 + # audio tower
954 + "model.audio_tower.": "audio_tower.",
955 "model.embed_audio.": "embed_audio.",
940 - "model.embed_vision.": "embed_vision.",
956 + # backbone
957 "model.language_model.": "language_model.model.",
942 - "model.vision_tower.": "vision_tower.",
943 - "model.audio_tower.": "audio_tower.",
958 "lm_head.": "language_model.lm_head.",
959 "model": "language_model.model",
960 }
@@ -959,7 +973,15 @@ class Gemma4ForConditionalGeneration(
973 with self._mark_tower_model(vllm_config, {"image", "video"}):
974 self.vision_tower = AutoModel.from_config(config=config.vision_config)
975 self.embed_vision = Gemma4MultimodalEmbedder(
962 - config.vision_config, config.text_config
976 + config.vision_config,
977 + config.text_config,
978 + quant_config=quant_config,
979 + prefix=maybe_prefix(prefix, "embed_vision"),
980 + )
981 + recursive_replace_linear(
982 + self.vision_tower,
983 + quant_config,
984 + prefix=maybe_prefix(prefix, "vision_tower"),
985 )
986
987 # ---- Audio tower (variants with audio_config) ----
@@ -972,7 +994,15 @@ class Gemma4ForConditionalGeneration(
994 # position embeddings, softcap, gradient_clipping).
995 self.audio_tower.post_init()
996 self.embed_audio = Gemma4MultimodalEmbedder(
975 - config.audio_config, config.text_config
997 + config.audio_config,
998 + config.text_config,
999 + quant_config=quant_config,
1000 + prefix=maybe_prefix(prefix, "embed_audio"),
1001 + )
1002 + recursive_replace_linear(
1003 + self.audio_tower,
1004 + quant_config,
1005 + prefix=maybe_prefix(prefix, "audio_tower"),
1006 )
1007 else:
1008 self.audio_tower = None
@@ -1153,6 +1183,7 @@ class Gemma4ForConditionalGeneration(
1183 vt = self.vision_tower
1184 vision_cfg = self.config.vision_config
1185 pooling_k2 = vision_cfg.pooling_kernel_size**2
1186 + target_dtype = self.language_model.model.embed_tokens.weight.dtype
1187
1188 # Concurrent requests with different image resolutions may
1189 # arrive as a list of per-image tensors, while same-resolution
@@ -1193,7 +1224,11 @@ class Gemma4ForConditionalGeneration(
1224 )
1225 pad_tensor = (pp_tensor == -1).all(dim=-1)
1226
1196 - inputs_embeds = vt.patch_embedder(pv_tensor, pp_tensor, pad_tensor)
1227 + inputs_embeds = vt.patch_embedder(
1228 + pv_tensor,
1229 + pp_tensor,
1230 + pad_tensor,
1231 + ).to(target_dtype)
1232 encoder_outputs = vt.encoder(
1233 inputs_embeds=inputs_embeds,
1234 attention_mask=~pad_tensor,
@@ -1230,7 +1265,9 @@ class Gemma4ForConditionalGeneration(
1265 all_valid_states[orig_idx] = valid_states
1266 valid_lens[orig_idx] = valid_states.shape[0]
1267
1233 - target_dtype = self.embed_vision.embedding_projection.weight.dtype
1268 + # Use embed_tokens dtype as compute dtype; embedding_projection.weight
1269 + # may be uint8 under BnB 4-bit, which would corrupt the cast.
1270 + target_dtype = self.language_model.model.embed_tokens.weight.dtype
1271
1272 # Project all images in a single batched call.
1273 flat_valid_states = torch.cat(all_valid_states, dim=0).to(target_dtype)
@@ -1273,7 +1310,7 @@ class Gemma4ForConditionalGeneration(
1310 vt = self.vision_tower
1311 vision_cfg = self.config.vision_config
1312 pooling_k2 = vision_cfg.pooling_kernel_size**2
1276 - target_dtype = self.embed_vision.embedding_projection.weight.dtype
1313 + target_dtype = self.language_model.model.embed_tokens.weight.dtype
1314
1315 if isinstance(frame_counts, torch.Tensor):
1316 fc_list = frame_counts.tolist()
@@ -1301,7 +1338,11 @@ class Gemma4ForConditionalGeneration(
1338 pp_chunk = pixel_position_ids[i : i + max_batch_size]
1339 pad_chunk = padding_positions[i : i + max_batch_size]
1340
1304 - inputs_embeds = vt.patch_embedder(pv_chunk, pp_chunk, pad_chunk)
1341 + inputs_embeds = vt.patch_embedder(
1342 + pv_chunk,
1343 + pp_chunk,
1344 + pad_chunk,
1345 + ).to(target_dtype)
1346 encoder_outputs = vt.encoder(
1347 inputs_embeds=inputs_embeds,
1348 attention_mask=~pad_chunk,
vllm/model_executor/models/transformers/utils.py
+29
@@ -32,6 +32,7 @@ from vllm.model_executor.layers.linear import (
32 ReplicatedLinear,
33 RowParallelLinear,
34 )
35 +from vllm.model_executor.models.utils import maybe_prefix
36 from vllm.transformers_utils.config import is_rope_parameters_nested
37
38 if TYPE_CHECKING:
@@ -227,6 +228,34 @@ def replace_rms_norm_class(rms_norm: nn.Module, hidden_size: int) -> RMSNorm:
228 return RMSNorm(**kwargs)
229
230
231 +def recursive_replace_linear(
232 + model: nn.Module,
233 + quant_config: "QuantizationConfig | None",
234 + prefix: str = "",
235 +):
236 + """Recursively replace linear modules in the model as needed."""
237 +
238 + def _recursive_replace(module: nn.Module, prefix: str):
239 + for child_name, child_module in module.named_children():
240 + new_module = child_module
241 + qual_name = maybe_prefix(prefix, child_name)
242 + # Replace modules as needed
243 + if isinstance(child_module, nn.Linear):
244 + style = "replicate"
245 + new_module = replace_linear_class(
246 + child_module,
247 + style,
248 + quant_config,
249 + prefix=qual_name,
250 + )
251 + else:
252 + _recursive_replace(child_module, prefix=qual_name)
253 + if new_module is not child_module:
254 + setattr(module, child_name, new_module)
255 +
256 + _recursive_replace(model, prefix=prefix)
257 +
258 +
259 def log_replacement(name: str, old_module: nn.Module, new_module: nn.Module):
260 logger.debug("%s: %s -> %s", name, old_module, new_module)
261