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

[Model][Quantization] Add GGUF support for MiniMax-M2.1 (#36965)

Signed-off-by: kangletian <Letian.Kang@amd.com>

PikaPikachu committed Mar 30, 2026 at 14:24 UTC 63babd17f1b110e267e1ad801a9b9d4ccf5bbe7d
5 files changed +137 -10
vllm/config/model.py
+1
@@ -948,6 +948,7 @@ class ModelConfig:
948 # imports during override detection (e.g., MXFP4 imports Triton)
949 "mxfp4",
950 "cpu_awq",
951 + "gguf",
952 ]
953 quantization_methods = [
954 q for q in supported_quantization if q not in overrides
vllm/model_executor/layers/quantization/gguf.py
+14 -1
@@ -3,7 +3,10 @@
3
4 from collections.abc import Mapping
5 from types import MappingProxyType
6 -from typing import Any
6 +from typing import TYPE_CHECKING, Any
7 +
8 +if TYPE_CHECKING:
9 + from vllm.model_executor.layers.quantization import QuantizationMethods
10
11 import gguf
12 import torch
@@ -79,6 +82,16 @@ class GGUFConfig(QuantizationConfig):
82 def from_config(cls, config: dict[str, Any]) -> "GGUFConfig":
83 return cls()
84
85 + @classmethod
86 + def override_quantization_method(
87 + cls, hf_quant_cfg: dict[str, Any], user_quant: str | None
88 + ) -> "QuantizationMethods | None":
89 + # When user explicitly specifies --quantization gguf, override
90 + # whatever quantization method is in the HF model config (e.g. fp8).
91 + if user_quant == "gguf":
92 + return "gguf"
93 + return None
94 +
95 def get_quant_method(
96 self, layer: torch.nn.Module, prefix: str
97 ) -> "QuantizeMethodBase | None":
vllm/model_executor/model_loader/gguf_loader.py
+74 -7
@@ -24,6 +24,7 @@ from vllm.model_executor.model_loader.weight_utils import (
24 get_gguf_extra_tensor_names,
25 get_gguf_weight_type_map,
26 gguf_quant_weights_iterator,
27 + gguf_quant_weights_iterator_multi,
28 )
29 from vllm.transformers_utils.gguf_utils import detect_gguf_multimodal
30 from vllm.utils.torch_utils import set_default_torch_dtype
@@ -74,6 +75,31 @@ class GGUFModelLoader(BaseModelLoader):
75 "or <repo_id>:<quant_type>)"
76 )
77
78 + @staticmethod
79 + def _get_all_gguf_files(model_path: str) -> list[str]:
80 + """Discover all GGUF shard files from a single shard path.
81 +
82 + Supports variable-width shard indices by dynamically detecting
83 + the padding from the original filename.
84 + E.g. ``*-00001-of-00005.gguf`` → all 5 shards,
85 + ``*-01-of-15.gguf`` → all 15 shards.
86 + """
87 + match = re.search(r"-(\d+)-of-(\d+)\.gguf$", model_path)
88 + if not match:
89 + return [model_path]
90 + total = int(match.group(2))
91 + num_digits = len(match.group(1))
92 + prefix = model_path[: match.start(1)]
93 + suffix = model_path[match.end(2) :]
94 + files = []
95 + for i in range(1, total + 1):
96 + shard_path = f"{prefix}{i:0{num_digits}d}-of-{total:0{num_digits}d}{suffix}"
97 + if os.path.isfile(shard_path):
98 + files.append(shard_path)
99 + if files:
100 + logger.info("Discovered %d GGUF shard files", len(files))
101 + return files if files else [model_path]
102 +
103 def _get_gguf_weights_map(self, model_config: ModelConfig):
104 """
105 GGUF uses this naming convention for their tensors from HF checkpoint:
@@ -145,6 +171,29 @@ class GGUFModelLoader(BaseModelLoader):
171 r"\.mlp\.experts\.[0-9]+\.(gate|up|down)_proj\.weight"
172 )
173 )
174 + if model_type == "minimax_m2":
175 + model_type = "minimax-m2"
176 + # GGUF layer map assumes merged expert weights
177 + # map them manually like deepseek2
178 + for idx in range(config.num_hidden_layers):
179 + gguf_to_hf_name_map[f"blk.{idx}.exp_probs_b.bias"] = (
180 + f"model.layers.{idx}.block_sparse_moe.e_score_correction_bias"
181 + )
182 + gguf_to_hf_name_map[f"blk.{idx}.ffn_down_exps.weight"] = (
183 + f"model.layers.{idx}.block_sparse_moe.experts.0.w2.weight"
184 + )
185 + gguf_to_hf_name_map[f"blk.{idx}.ffn_gate_exps.weight"] = (
186 + f"model.layers.{idx}.block_sparse_moe.experts.0.w1.weight"
187 + )
188 + gguf_to_hf_name_map[f"blk.{idx}.ffn_up_exps.weight"] = (
189 + f"model.layers.{idx}.block_sparse_moe.experts.0.w3.weight"
190 + )
191 + sideload_params.append(
192 + re.compile(
193 + f"model\\.layers\\.{idx}"
194 + r"\.block_sparse_moe\.experts\.(gate_up_proj|down_proj)"
195 + )
196 + )
197
198 arch = None
199 for key, value in gguf.MODEL_ARCH_NAMES.items():
@@ -190,6 +239,13 @@ class GGUFModelLoader(BaseModelLoader):
239 revert_hf_rename(name): tensor for name, tensor in state_dict.items()
240 }
241
242 + if model_type == "minimax-m2" and not hf_checkpoint_map:
243 + # Reverse HF convention: mlp -> block_sparse_moe
244 + state_dict = {
245 + name.replace(".mlp.", ".block_sparse_moe."): tensor
246 + for name, tensor in state_dict.items()
247 + }
248 +
249 def find_hf_name_in_tensor_map(hf_name: str) -> str | None:
250 """
251 Map HuggingFace parameter name to GGUF tensor name.
@@ -277,9 +333,10 @@ class GGUFModelLoader(BaseModelLoader):
333 model_name_or_path: str,
334 gguf_to_hf_name_map: dict[str, str],
335 ) -> dict[str, str]:
280 - weight_type_map = get_gguf_weight_type_map(
281 - model_name_or_path, gguf_to_hf_name_map
282 - )
336 + gguf_files = self._get_all_gguf_files(model_name_or_path)
337 + weight_type_map = {}
338 + for f in gguf_files:
339 + weight_type_map.update(get_gguf_weight_type_map(f, gguf_to_hf_name_map))
340 is_multimodal = hasattr(model_config.hf_config, "vision_config")
341 if is_multimodal:
342 mmproj_file = detect_gguf_multimodal(model_name_or_path)
@@ -321,7 +378,15 @@ class GGUFModelLoader(BaseModelLoader):
378 )
379 yield from gguf_quant_weights_iterator(mmproj_file, gguf_to_hf_name_map)
380
324 - yield from gguf_quant_weights_iterator(model_name_or_path, gguf_to_hf_name_map)
381 + gguf_files = self._get_all_gguf_files(model_name_or_path)
382 + if len(gguf_files) > 1:
383 + yield from gguf_quant_weights_iterator_multi(
384 + gguf_files, gguf_to_hf_name_map
385 + )
386 + else:
387 + yield from gguf_quant_weights_iterator(
388 + model_name_or_path, gguf_to_hf_name_map
389 + )
390
391 def download_model(self, model_config: ModelConfig) -> None:
392 self._prepare_weights(model_config)
@@ -340,9 +405,11 @@ class GGUFModelLoader(BaseModelLoader):
405 local_model_path = self._prepare_weights(model_config)
406 gguf_weights_map = self._get_gguf_weights_map(model_config)
407 # we can only know if tie word embeddings after mapping weights
343 - if "lm_head.weight" in get_gguf_extra_tensor_names(
344 - local_model_path, gguf_weights_map
345 - ):
408 + gguf_files = self._get_all_gguf_files(local_model_path)
409 + all_extra_names = []
410 + for f in gguf_files:
411 + all_extra_names.extend(get_gguf_extra_tensor_names(f, gguf_weights_map))
412 + if "lm_head.weight" in all_extra_names:
413 model_config.hf_config.update({"tie_word_embeddings": True})
414
415 weight_type_map = self._get_gguf_weight_type(
vllm/model_executor/model_loader/weight_utils.py
+43
@@ -1222,6 +1222,49 @@ def gguf_quant_weights_iterator(
1222 yield name, param
1223
1224
1225 +def gguf_quant_weights_iterator_multi(
1226 + gguf_files: list[str], gguf_to_hf_name_map: dict[str, str]
1227 +) -> Generator[tuple[str, torch.Tensor], None, None]:
1228 + """
1229 + Iterate over the quant weights across multiple GGUF shard files
1230 + and convert them to torch tensors.
1231 +
1232 + Like gguf_quant_weights_iterator, we yield all weight types first
1233 + before yielding any weights data to avoid issues with packed layers
1234 + that have different quant types.
1235 + """
1236 + readers = [gguf.GGUFReader(f) for f in gguf_files]
1237 +
1238 + # First pass: yield all weight types across all shards
1239 + for reader in readers:
1240 + for tensor in reader.tensors:
1241 + if tensor.name in gguf_to_hf_name_map:
1242 + weight_type = tensor.tensor_type
1243 + name = gguf_to_hf_name_map[tensor.name]
1244 + if weight_type.name not in ("F32", "BF16", "F16"):
1245 + weight_type_name = name.replace("weight", "qweight_type")
1246 + weight_type = torch.tensor(weight_type)
1247 + yield weight_type_name, weight_type
1248 +
1249 + # Second pass: yield all weight data across all shards
1250 + for reader in readers:
1251 + for tensor in reader.tensors:
1252 + if tensor.name in gguf_to_hf_name_map:
1253 + weight = tensor.data
1254 + weight_type = tensor.tensor_type
1255 + name = gguf_to_hf_name_map[tensor.name]
1256 + if weight_type.name not in ("F32", "BF16", "F16"):
1257 + name = name.replace("weight", "qweight")
1258 + if weight_type.name == "BF16" and tensor.data.dtype == np.uint8:
1259 + weight = weight.view(np.uint16)
1260 + if reader.byte_order == "S":
1261 + weight = weight.byteswap()
1262 + param = torch.tensor(weight).view(torch.bfloat16)
1263 + else:
1264 + param = torch.tensor(weight)
1265 + yield name, param
1266 +
1267 +
1268 def convert_pyslice_to_tensor(x: Any) -> torch.Tensor:
1269 """convert PySafeSlice object from safetensors to torch.Tensor
1270
vllm/model_executor/models/minimax_m2.py
+5 -2
@@ -331,7 +331,7 @@ class MiniMaxM2Model(nn.Module):
331 self.embed_tokens = VocabParallelEmbedding(
332 config.vocab_size,
333 config.hidden_size,
334 - quant_config=None,
334 + quant_config=quant_config,
335 prefix=f"{prefix}.embed_tokens",
336 )
337 else:
@@ -518,7 +518,10 @@ class MiniMaxM2ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
518 )
519 if get_pp_group().is_last_rank:
520 self.lm_head = ParallelLMHead(
521 - config.vocab_size, config.hidden_size, quant_config=None
521 + config.vocab_size,
522 + config.hidden_size,
523 + quant_config=quant_config,
524 + prefix=maybe_prefix(prefix, "lm_head"),
525 )
526 else:
527 self.lm_head = PPMissingLayer()