modelopt.py 100 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. from collections.abc import Callable
  4. from dataclasses import dataclass
  5. from enum import Enum
  6. from fnmatch import fnmatch
  7. from typing import TYPE_CHECKING, Any, cast
  8. import torch
  9. from torch.nn.parameter import Parameter
  10. import vllm.envs as envs
  11. from vllm.config import get_current_vllm_config_or_none
  12. from vllm.config.quantization import QuantSpec
  13. from vllm.logger import init_logger
  14. from vllm.model_executor.kernels.linear import (
  15. init_fp8_linear_kernel,
  16. init_mxfp8_linear_kernel,
  17. init_nvfp4_linear_kernel,
  18. )
  19. from vllm.model_executor.layers.attention import Attention, MLAAttention
  20. from vllm.model_executor.layers.fused_moe import (
  21. FusedMoEConfig,
  22. FusedMoEMethodBase,
  23. FusedMoEQuantConfig,
  24. FusedMoeWeightScaleSupported,
  25. RoutedExperts,
  26. SharedExperts,
  27. )
  28. from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
  29. from vllm.model_executor.layers.fused_moe.oracle.fp8 import (
  30. Fp8MoeBackend,
  31. convert_to_fp8_moe_kernel_format,
  32. make_fp8_moe_kernel,
  33. make_fp8_moe_quant_config,
  34. select_fp8_moe_backend,
  35. )
  36. from vllm.model_executor.layers.fused_moe.oracle.mxfp8 import (
  37. select_mxfp8_moe_backend,
  38. )
  39. from vllm.model_executor.layers.fused_moe.oracle.nvfp4 import (
  40. NvFp4MoeBackend,
  41. convert_to_nvfp4_moe_kernel_format,
  42. is_global_sf_supported_for_nvfp4_backend,
  43. make_nvfp4_moe_kernel,
  44. make_nvfp4_moe_quant_config,
  45. select_nvfp4_moe_backend,
  46. )
  47. from vllm.model_executor.layers.fusion.quant_activation import (
  48. expose_input_quant_key,
  49. )
  50. from vllm.model_executor.layers.linear import (
  51. LinearBase,
  52. LinearMethodBase,
  53. UnquantizedLinearMethod,
  54. register_weight_loader_v2_supported_method,
  55. )
  56. from vllm.model_executor.layers.quantization import QuantizationMethods
  57. from vllm.model_executor.layers.quantization.base_config import (
  58. QuantizationConfig,
  59. QuantizeMethodBase,
  60. )
  61. from vllm.model_executor.layers.quantization.fp8 import Fp8Config, Fp8MoEMethod
  62. from vllm.model_executor.layers.quantization.kv_cache import BaseKVCacheMethod
  63. from vllm.model_executor.layers.quantization.utils.fp8_utils import (
  64. FP8_SCALE_SENTINEL,
  65. process_fp8_input_tensor_strategy_moe,
  66. process_fp8_weight_channel_strategy,
  67. process_fp8_weight_tensor_strategy_moe,
  68. )
  69. from vllm.model_executor.layers.quantization.utils.marlin_utils import (
  70. get_marlin_input_dtype,
  71. )
  72. from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
  73. MXFP8_BLOCK_SIZE,
  74. MXFP8_SCALE_DTYPE,
  75. MXFP8_VALUE_DTYPE,
  76. )
  77. from vllm.model_executor.layers.quantization.utils.quant_utils import (
  78. FP4_DTYPE,
  79. QuantKey,
  80. is_layer_skipped,
  81. kFp8Dynamic128Sym,
  82. kFp8DynamicTokenSym,
  83. kFp8Static128BlockSym,
  84. kFp8StaticTensorSym,
  85. kFp8StaticTokenSym,
  86. kMxfp8Dynamic,
  87. kMxfp8Static,
  88. kNvfp4Dynamic,
  89. kNvfp4Static,
  90. )
  91. from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
  92. requantize_with_max_scale,
  93. )
  94. from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
  95. from vllm.model_executor.parameter import (
  96. BlockQuantScaleParameter,
  97. ChannelQuantScaleParameter,
  98. ModelWeightParameter,
  99. PerTensorScaleParameter,
  100. )
  101. from vllm.model_executor.utils import (
  102. is_weights_pre_processed,
  103. replace_parameter,
  104. set_weight_attrs,
  105. )
  106. from vllm.utils.math_utils import cdiv
  107. if TYPE_CHECKING:
  108. from vllm.model_executor.models.utils import WeightsMapper
  109. logger = init_logger(__name__)
  110. # ``FP8_PB_WO`` is ModelOpt's canonical 2D block-FP8 name. Early composed
  111. # Qwen3.8-Flash-Next checkpoints used ``FP8_BLOCK_SCALES`` for the same tensor
  112. # layout, so retain it as a checkpoint-compatibility alias.
  113. _BLOCK_FP8_MOE_ALGOS = ("FP8_PB_WO", "FP8_BLOCK_SCALES")
  114. # Single source of truth for the ModelOpt linear algos.
  115. #
  116. # Each entry is (owning config's name, mixed-precision sub-config attribute).
  117. # ``resolve`` turns the algo into a QuantSpec; this table only records which
  118. # config object holds the checkpoint parameters resolve may read -- today just
  119. # group_size, and only for the NVFP4 family.
  120. #
  121. # To add a format: one row here plus one row in resolve(). Every consumer below
  122. # is derived, so nothing else needs editing.
  123. LINEAR_ALGOS: dict[str, tuple[str, str]] = {
  124. # FP8 per-tensor weight + optional static activation scale.
  125. "FP8": ("modelopt", "fp8_config"),
  126. # FP8 per-channel weight scale + per-token activation scale.
  127. "FP8_PER_CHANNEL_PER_TOKEN": ("modelopt", "fp8_config"),
  128. # FP8 128x128 per-block weight (ModelOpt may emit this lowercase).
  129. "FP8_PB_WO": ("modelopt", "fp8_config"),
  130. # NVFP4 W4A4: 4-bit float weights AND 4-bit float activations.
  131. "NVFP4": ("modelopt_fp4", "nvfp4_config"),
  132. # W4A16 NVFP4: 4-bit float weights, fp16/bf16 activations.
  133. "W4A16_NVFP4": ("modelopt_fp4", "w4a16_nvfp4_config"),
  134. # MXFP8: e4m3 weights with per-32-block e8m0 scales.
  135. "MXFP8": ("modelopt_mxfp8", "mxfp8_config"),
  136. }
  137. # MIXED_PRECISION is not a linear algo; it selects a per-layer algo from the
  138. # table above.
  139. QUANT_ALGOS = [*LINEAR_ALGOS, "MIXED_PRECISION"]
  140. def algos_owned_by(config_name: str) -> tuple[str, ...]:
  141. """The linear algos a given config accepts, for its quant_algo validation."""
  142. return tuple(a for a, (owner, _) in LINEAR_ALGOS.items() if owner == config_name)
  143. class ModelOptKVCacheMethod(BaseKVCacheMethod):
  144. """Supports loading kv-cache scaling factors from FP8 or NVFP4 checkpoints."""
  145. def __init__(self, quant_config: "ModelOptQuantConfigBase"):
  146. super().__init__(quant_config)
  147. class ModelOptQuantConfigBase(QuantizationConfig):
  148. # ModelOpt quant-algo string, set by each subclass. Fed to resolve() to
  149. # build the QuantSpec for the generic ModelOptLinearMethod. The mixed
  150. # config resolves the algo per-prefix instead and does not set this.
  151. quant_method: str
  152. FusedMoEMethodCls: type = FusedMoEMethodBase
  153. KVCacheMethodCls: type = BaseKVCacheMethod
  154. def __init__(
  155. self,
  156. exclude_modules: list[str],
  157. ):
  158. super().__init__()
  159. self.exclude_modules: list[str] = exclude_modules
  160. def is_layer_excluded(self, prefix: str) -> bool:
  161. """Check if a layer should be excluded from quantization.
  162. Handles both exact matching (for fused layers) and ModelOpt wildcard matching.
  163. The ModelOpt exclude_modules list is a list of wildcards.
  164. """
  165. if len(self.exclude_modules) == 0:
  166. return False
  167. # First check exact matching with fused layer support
  168. if is_layer_skipped(prefix, self.exclude_modules, self.packed_modules_mapping):
  169. return True
  170. # TODO: This special hard coded logic is not needed for quantized checkpoints
  171. # generated by ModelOpt >= 0.39.0 where they are handled natually by the
  172. # exclude_modules config. But need to keep them for loading quantized
  173. # checkpoints generated by older versions. Then check substring matching
  174. # for patterns not caught by exact match
  175. for exclude_module in self.exclude_modules:
  176. # Skip exact matches already handled above
  177. if exclude_module != prefix and (
  178. exclude_module in prefix
  179. or (
  180. prefix.startswith("language_model.")
  181. and exclude_module in prefix.removeprefix("language_model.")
  182. )
  183. ):
  184. return True
  185. # modelopt exclude modules are not simple strings, they are wildcards
  186. for wildcard_pattern in self.exclude_modules:
  187. if fnmatch(prefix, wildcard_pattern):
  188. return True
  189. return False
  190. def get_quant_method(
  191. self, layer: torch.nn.Module, prefix: str
  192. ) -> "QuantizeMethodBase | None":
  193. # handle kv-cache first so we can focus only on weight quantization thereafter
  194. if isinstance(layer, (Attention, MLAAttention)):
  195. return self.KVCacheMethodCls(self)
  196. # handle exclusion
  197. if self.is_layer_excluded(prefix):
  198. if isinstance(layer, (LinearBase, ParallelLMHead)):
  199. return UnquantizedLinearMethod()
  200. return None
  201. # TODO: This special hard coded logic is not needed for quantized checkpoints
  202. # generated by ModelOpt >= 0.39.0 where they are handled natually by the
  203. # exclude_modules config. But need to keep them for loading quantized
  204. # checkpoints generated by older versions. Then check substring matching
  205. # for patterns not caught by exact match
  206. if (
  207. "vision_tower" in prefix
  208. or "vision_model" in prefix
  209. or "vit_large_projector" in prefix
  210. ):
  211. return UnquantizedLinearMethod()
  212. # now, the layer is quantized, handle it here
  213. if isinstance(layer, (LinearBase, ParallelLMHead)):
  214. return build_linear_method(self, self.quant_method, prefix)
  215. elif isinstance(layer, RoutedExperts):
  216. quant_method = self.FusedMoEMethodCls(
  217. quant_config=self, moe_config=layer.moe_config
  218. )
  219. if getattr(quant_method, "backend", "") == "marlin":
  220. quant_method.marlin_input_dtype = get_marlin_input_dtype(prefix)
  221. return quant_method
  222. return None
  223. def apply_vllm_mapper(self, hf_to_vllm_mapper: "WeightsMapper"):
  224. if len(self.exclude_modules) > 0:
  225. # This is a workaround for the weights remapping issue:
  226. # https://github.com/vllm-project/vllm/issues/28072
  227. # Right now, the Nvidia ModelOpt library use just one wildcard pattern:
  228. # module_path*
  229. # It gets applied if the whole tree of modules rooted at module_path
  230. # is not quantized. Here we replace such pattern by 2 patterns that are
  231. # collectively equivalent to the original pattern:
  232. # module_path
  233. # module_path.*
  234. new_exclude_modules = []
  235. for exclude in self.exclude_modules:
  236. if len(exclude) >= 2 and exclude[-1] == "*" and exclude[-2] != ".":
  237. new_exclude_modules.append(exclude[:-1])
  238. new_exclude_modules.append(exclude[:-1] + ".*")
  239. else:
  240. new_exclude_modules.append(exclude)
  241. self.exclude_modules = hf_to_vllm_mapper.apply_list(new_exclude_modules)
  242. @staticmethod
  243. def _extract_modelopt_quant_algo(
  244. hf_quant_cfg: dict[str, Any] | None,
  245. ) -> str | None:
  246. """Extract upper-cased quant_algo from a modelopt config.
  247. Returns the quant_algo string (upper-cased), or None if the config
  248. is not a modelopt config.
  249. """
  250. if hf_quant_cfg is None:
  251. return None
  252. if not hf_quant_cfg.get("quant_method", "").lower().startswith("modelopt"):
  253. return None
  254. if "quantization" in hf_quant_cfg:
  255. quant_config = hf_quant_cfg["quantization"]
  256. if isinstance(quant_config, dict):
  257. return str(quant_config.get("quant_algo", "")).upper()
  258. return None
  259. return str(hf_quant_cfg.get("quant_algo", "")).upper()
  260. @staticmethod
  261. def get_config_filenames() -> list[str]:
  262. return ["hf_quant_config.json"]
  263. @classmethod
  264. def _from_config(
  265. cls,
  266. *,
  267. quant_method: str,
  268. kv_cache_quant_method: str | None,
  269. exclude_modules: list[str],
  270. original_config: dict[str, Any],
  271. group_size: int | None,
  272. ) -> "ModelOptQuantConfigBase":
  273. raise NotImplementedError("Please implement this function in sub classes")
  274. @classmethod
  275. def from_config(cls, config: dict[str, Any]) -> "ModelOptQuantConfigBase":
  276. # Handle both ModelOpt format and compressed-tensors style format
  277. if "quantization" in config:
  278. # Traditional ModelOpt format:
  279. # {"quantization": {"quant_algo": "..."}}
  280. quant_config = cls.get_from_keys(config, ["quantization"])
  281. if not isinstance(quant_config, dict):
  282. raise ValueError("Expected 'quantization' to be a dictionary in config")
  283. quant_method = quant_config.get("quant_algo")
  284. # Handle kv_cache_quant_algo with proper type validation
  285. kv_cache_quant_method = quant_config.get("kv_cache_quant_algo")
  286. # Handle group_size with proper type validation
  287. group_size_raw = quant_config.get("group_size")
  288. # "exclude_modules" is the key in the legacy hf_quant_config.json
  289. exclude_modules = quant_config.get("exclude_modules", [])
  290. else:
  291. # Compressed-tensors style format (config.json quantization_config):
  292. # {"quant_algo": "...", "quant_method": "modelopt"}
  293. quant_method = config.get("quant_algo")
  294. # "kv_cache_scheme" (a dict) instead of "kv_cache_quant_algo" (a string).
  295. kv_cache_scheme = config.get("kv_cache_scheme")
  296. if isinstance(kv_cache_scheme, dict) and (
  297. kv_cache_scheme.get("type") == "float"
  298. and kv_cache_scheme.get("num_bits") == 8
  299. ):
  300. kv_cache_quant_method = "FP8"
  301. else:
  302. kv_cache_quant_method = None
  303. # "ignore" is the key in config.json
  304. exclude_modules = config.get("ignore", [])
  305. group_size_raw = config.get("group_size")
  306. if not quant_method:
  307. raise ValueError("Missing 'quant_algo' in quantization config")
  308. # Normalize quant_algo for robust matching (ModelOpt may emit lowercase).
  309. quant_method = str(quant_method).upper()
  310. if kv_cache_quant_method is None:
  311. # No KV cache quantization, keep this branch just to have this comment
  312. pass
  313. elif not isinstance(kv_cache_quant_method, str):
  314. raise ValueError(
  315. f"kv_cache_quant_algo must be a string, got "
  316. f"{type(kv_cache_quant_method)}"
  317. )
  318. else:
  319. kv_cache_quant_method = kv_cache_quant_method.upper()
  320. if not isinstance(exclude_modules, list):
  321. raise ValueError(
  322. f"exclude_modules must be a list, got {type(exclude_modules)}"
  323. )
  324. if group_size_raw is None:
  325. group_size = None
  326. elif isinstance(group_size_raw, int):
  327. group_size = group_size_raw
  328. else:
  329. try:
  330. group_size = int(group_size_raw)
  331. except (ValueError, TypeError):
  332. raise ValueError(
  333. f"group_size must be an integer, got {type(group_size_raw)}"
  334. ) from None
  335. if quant_method not in QUANT_ALGOS:
  336. raise ValueError(
  337. f"ModelOpt currently only supports: {QUANT_ALGOS} "
  338. "quantizations in vLLM. Please check the "
  339. "`hf_quant_config.json` file for your model's "
  340. "quant configuration."
  341. )
  342. return cls._from_config(
  343. quant_method=quant_method,
  344. kv_cache_quant_method=kv_cache_quant_method,
  345. exclude_modules=exclude_modules,
  346. group_size=group_size,
  347. original_config=config,
  348. )
  349. class ModelOptFp8Config(ModelOptQuantConfigBase):
  350. """Config class for ModelOpt FP8."""
  351. def __init__(
  352. self,
  353. quant_method: str,
  354. is_checkpoint_fp8_serialized: bool,
  355. kv_cache_quant_method: str | None,
  356. exclude_modules: list[str],
  357. ) -> None:
  358. super().__init__(exclude_modules)
  359. self.quant_method = quant_method
  360. self.is_checkpoint_fp8_serialized = is_checkpoint_fp8_serialized
  361. self.kv_cache_quant_method = kv_cache_quant_method
  362. # Validate the quant_algo; resolve() maps it to a QuantSpec at dispatch.
  363. supported = algos_owned_by("modelopt")
  364. if self.quant_method not in supported:
  365. raise ValueError(
  366. "Unsupported ModelOpt FP8 quant_algo for vLLM: "
  367. f"{self.quant_method}. Supported: {' / '.join(supported)}."
  368. )
  369. def has_blocked_weights(self) -> bool:
  370. # Gates "+quant_fp8" in VllmConfig. FP8_PB_WO runs the block-scaled
  371. # kernels, whose DeepGEMM path needs UE8M0-packed group scales; without
  372. # the op enabled QuantFP8 falls back to forward_native, which emits
  373. # unpacked fp32 scales and the GEMM fails to launch.
  374. return self.quant_method == "FP8_PB_WO"
  375. def get_name(self) -> QuantizationMethods:
  376. return "modelopt"
  377. def get_supported_act_dtypes(self) -> list[torch.dtype]:
  378. return [torch.bfloat16, torch.half]
  379. @classmethod
  380. def get_min_capability(cls) -> int:
  381. return 80
  382. @classmethod
  383. def override_quantization_method(
  384. cls, hf_quant_cfg, user_quant, hf_config=None
  385. ) -> QuantizationMethods | None:
  386. algo = cls._extract_modelopt_quant_algo(hf_quant_cfg)
  387. if algo is not None and algo == "FP8":
  388. return "modelopt"
  389. return None
  390. @classmethod
  391. def _from_config(
  392. cls,
  393. *,
  394. quant_method: str,
  395. kv_cache_quant_method: str | None,
  396. exclude_modules: list[str],
  397. original_config: dict[str, Any],
  398. **kwargs: Any,
  399. ) -> "ModelOptFp8Config":
  400. is_checkpoint_fp8_serialized = "FP8" in quant_method
  401. return cls(
  402. quant_method,
  403. is_checkpoint_fp8_serialized,
  404. kv_cache_quant_method,
  405. exclude_modules,
  406. )
  407. class ModelOptFp8MoEMethod(FusedMoEMethodBase):
  408. """MoE method for ModelOpt FP8.
  409. Supports loading FP8 checkpoints with static weight scale and
  410. activation scale.
  411. Args:
  412. quant_config: The ModelOpt quantization config.
  413. """
  414. def __init__(
  415. self,
  416. quant_config: ModelOptFp8Config,
  417. moe_config: FusedMoEConfig,
  418. ) -> None:
  419. super().__init__(moe_config)
  420. self.quant_config = quant_config
  421. assert self.quant_config.is_checkpoint_fp8_serialized
  422. # Select Fp8 MoE backend
  423. self.fp8_backend, self.experts_cls = select_fp8_moe_backend(
  424. config=self.moe,
  425. weight_key=kFp8StaticTensorSym,
  426. activation_key=kFp8StaticTensorSym,
  427. )
  428. def create_weights(
  429. self,
  430. layer: RoutedExperts,
  431. num_experts: int,
  432. hidden_size: int,
  433. intermediate_size_per_partition: int,
  434. params_dtype: torch.dtype,
  435. **extra_weight_attrs,
  436. ):
  437. layer.orig_dtype = params_dtype
  438. layer.num_experts = num_experts
  439. # Use FP8 dtype if checkpoint is serialized
  440. weight_dtype = (
  441. torch.float8_e4m3fn
  442. if self.quant_config.is_checkpoint_fp8_serialized
  443. else params_dtype
  444. )
  445. weight_loader = extra_weight_attrs.get("weight_loader")
  446. w13_num_shards = 2 if self.moe.is_act_and_mul else 1
  447. w13_weight = ModelWeightParameter(
  448. data=torch.empty(
  449. num_experts,
  450. w13_num_shards * intermediate_size_per_partition,
  451. hidden_size,
  452. dtype=weight_dtype,
  453. ),
  454. input_dim=2,
  455. output_dim=1,
  456. weight_loader=weight_loader,
  457. )
  458. layer.register_parameter("w13_weight", w13_weight)
  459. w2_weight = ModelWeightParameter(
  460. data=torch.empty(
  461. num_experts,
  462. hidden_size,
  463. intermediate_size_per_partition,
  464. dtype=weight_dtype,
  465. ),
  466. input_dim=2,
  467. output_dim=1,
  468. weight_loader=weight_loader,
  469. )
  470. layer.register_parameter("w2_weight", w2_weight)
  471. # WEIGHT SCALES - Per-tensor scaling for ModelOpts
  472. # For gated MoE, allocate 2 scales for w1 and w3 respectively.
  473. # They will be combined to a single scale after weight loading.
  474. # For non-gated MoE, allocate 1 scale for w13.
  475. w13_weight_scale = PerTensorScaleParameter(
  476. data=torch.full(
  477. (num_experts, w13_num_shards),
  478. 1.0,
  479. dtype=torch.float32,
  480. ),
  481. weight_loader=weight_loader,
  482. )
  483. w2_weight_scale = PerTensorScaleParameter(
  484. data=torch.full((num_experts,), 1.0, dtype=torch.float32),
  485. weight_loader=weight_loader,
  486. )
  487. layer.register_parameter("w13_weight_scale", w13_weight_scale)
  488. layer.register_parameter("w2_weight_scale", w2_weight_scale)
  489. # INPUT SCALES - Per-tensor scaling for ModelOpt
  490. w13_input_scale = PerTensorScaleParameter(
  491. data=torch.full((num_experts,), 1.0, dtype=torch.float32),
  492. weight_loader=weight_loader,
  493. )
  494. w2_input_scale = PerTensorScaleParameter(
  495. data=torch.full((num_experts,), 1.0, dtype=torch.float32),
  496. weight_loader=weight_loader,
  497. )
  498. layer.register_parameter("w13_input_scale", w13_input_scale)
  499. layer.register_parameter("w2_input_scale", w2_input_scale)
  500. def _setup_kernel(
  501. self,
  502. layer: RoutedExperts,
  503. w13: torch.Tensor,
  504. w2: torch.Tensor,
  505. w13_scale: torch.Tensor,
  506. w2_scale: torch.Tensor,
  507. w13_input_scale: torch.Tensor,
  508. w2_input_scale: torch.Tensor,
  509. ):
  510. w13, w2, w13_scale, w2_scale = convert_to_fp8_moe_kernel_format(
  511. fp8_backend=self.fp8_backend,
  512. layer=layer,
  513. w13=w13,
  514. w2=w2,
  515. w13_scale=w13_scale,
  516. w2_scale=w2_scale,
  517. w13_input_scale=w13_input_scale,
  518. w2_input_scale=w2_input_scale,
  519. )
  520. # Replace parameters with updated versions. Note that this helper
  521. # function ensures the replacement is compatible with RL weight reloads.
  522. replace_parameter(layer, "w13_weight", w13)
  523. replace_parameter(layer, "w2_weight", w2)
  524. replace_parameter(layer, "w13_weight_scale", w13_scale)
  525. replace_parameter(layer, "w2_weight_scale", w2_scale)
  526. # Setup modular kernel.
  527. self.moe_quant_config = self.get_fused_moe_quant_config(layer)
  528. assert self.experts_cls is not None
  529. self.moe_kernel = make_fp8_moe_kernel(
  530. moe_quant_config=self.moe_quant_config,
  531. moe_config=self.moe,
  532. fp8_backend=self.fp8_backend,
  533. experts_cls=self.experts_cls,
  534. routing_tables=layer._expert_routing_tables(),
  535. )
  536. def process_weights_after_loading(self, layer: RoutedExperts) -> None:
  537. w13 = layer.w13_weight
  538. w2 = layer.w2_weight
  539. w13_scale = layer.w13_weight_scale
  540. w2_scale = layer.w2_weight_scale
  541. w13_input_scale = layer.w13_input_scale
  542. w2_input_scale = layer.w2_input_scale
  543. # Per tensor kernels require single activation scale. Use the max.
  544. w13_input_scale, w2_input_scale = process_fp8_input_tensor_strategy_moe(
  545. w13_input_scale,
  546. w2_input_scale,
  547. layer.moe_config.moe_parallel_config.enable_eplb,
  548. )
  549. replace_parameter(layer, "w13_input_scale", w13_input_scale)
  550. replace_parameter(layer, "w2_input_scale", w2_input_scale)
  551. # Per tensor kernels require single weight scale for w13 per expert, but
  552. # on disk there is a scale for w1 and w3. Use the max to requantize.
  553. shard_size = layer.intermediate_size_per_partition
  554. w13, w13_scale = process_fp8_weight_tensor_strategy_moe(
  555. w13,
  556. w13_scale,
  557. shard_size,
  558. num_experts=layer.w13_weight.shape[0],
  559. is_act_and_mul=self.moe.is_act_and_mul,
  560. )
  561. # Shuffle weights to runtime format and setup kernel.
  562. self._setup_kernel(
  563. layer, w13, w2, w13_scale, w2_scale, w13_input_scale, w2_input_scale
  564. )
  565. def get_fused_moe_quant_config(self, layer: RoutedExperts) -> FusedMoEQuantConfig:
  566. w1_scale = layer.w13_weight_scale
  567. w2_scale = layer.w2_weight_scale
  568. a1_scale = layer.w13_input_scale
  569. a2_scale = layer.w2_input_scale
  570. return make_fp8_moe_quant_config(
  571. fp8_backend=self.fp8_backend,
  572. w1_scale=w1_scale,
  573. w2_scale=w2_scale,
  574. a1_scale=a1_scale,
  575. a2_scale=a2_scale,
  576. swiglu_limit=getattr(layer, "swiglu_limit", None),
  577. layer=layer,
  578. )
  579. def apply_monolithic(
  580. self,
  581. layer: RoutedExperts,
  582. x: torch.Tensor,
  583. router_logits: torch.Tensor,
  584. input_ids: torch.Tensor | None = None,
  585. ) -> torch.Tensor:
  586. assert self.is_monolithic
  587. assert self.moe_kernel is not None
  588. return self.moe_kernel.apply_monolithic(
  589. x,
  590. layer.w13_weight,
  591. layer.w2_weight,
  592. router_logits,
  593. activation=layer.activation,
  594. global_num_experts=layer.global_num_experts,
  595. expert_map=layer.expert_map,
  596. apply_router_weight_on_input=layer.apply_router_weight_on_input,
  597. num_expert_group=layer.num_expert_group,
  598. topk_group=layer.topk_group,
  599. e_score_correction_bias=layer.e_score_correction_bias,
  600. routed_scaling_factor=layer.routed_scaling_factor,
  601. )
  602. def apply(
  603. self,
  604. layer: RoutedExperts,
  605. x: torch.Tensor,
  606. topk_weights: torch.Tensor,
  607. topk_ids: torch.Tensor,
  608. shared_experts: SharedExperts | None,
  609. shared_experts_input: torch.Tensor | None,
  610. ) -> torch.Tensor | UnfinalizedMoEOutput:
  611. assert not self.is_monolithic
  612. assert self.moe_kernel is not None
  613. return self.moe_kernel.apply(
  614. x,
  615. layer.w13_weight,
  616. layer.w2_weight,
  617. topk_weights,
  618. topk_ids,
  619. activation=layer.activation,
  620. global_num_experts=layer.global_num_experts,
  621. expert_map=layer.expert_map,
  622. apply_router_weight_on_input=layer.apply_router_weight_on_input,
  623. shared_experts=shared_experts,
  624. shared_experts_input=shared_experts_input,
  625. )
  626. ModelOptFp8Config.FusedMoEMethodCls = ModelOptFp8MoEMethod
  627. ModelOptFp8Config.KVCacheMethodCls = ModelOptKVCacheMethod
  628. class ModelOptNvFp4Config(ModelOptQuantConfigBase):
  629. """Config class for ModelOpt FP4."""
  630. def __init__(
  631. self,
  632. quant_method: str = "NVFP4",
  633. is_checkpoint_nvfp4_serialized: bool = False,
  634. kv_cache_quant_algo: str | None = None,
  635. exclude_modules: list[str] | None = None,
  636. group_size: int = 16,
  637. ) -> None:
  638. if exclude_modules is None:
  639. exclude_modules = []
  640. super().__init__(exclude_modules)
  641. self.quant_method = quant_method
  642. self.is_checkpoint_nvfp4_serialized = is_checkpoint_nvfp4_serialized
  643. if is_checkpoint_nvfp4_serialized:
  644. self.group_size = group_size
  645. self.kv_cache_quant_algo = kv_cache_quant_algo
  646. # Validate the quant_algo; resolve() maps it to a QuantSpec at dispatch.
  647. # NVFP4 -> W4A4: cutlass NVFP4 GEMM with input quantization
  648. # W4A16_NVFP4 -> W4A16: FP4 Marlin GEMM with bf16/fp16 activations
  649. supported = algos_owned_by("modelopt_fp4")
  650. if quant_method not in supported:
  651. raise ValueError(
  652. f"Unsupported ModelOpt NVFP4 quant_algo: {quant_method}. "
  653. f"Supported: {' / '.join(supported)}."
  654. )
  655. def get_name(self) -> QuantizationMethods:
  656. return "modelopt_fp4"
  657. def get_supported_act_dtypes(self) -> list[torch.dtype]:
  658. return [torch.bfloat16, torch.half, torch.float8_e4m3fn]
  659. @classmethod
  660. def get_min_capability(cls) -> int:
  661. return 75
  662. @classmethod
  663. def override_quantization_method(
  664. cls, hf_quant_cfg, user_quant, hf_config=None
  665. ) -> QuantizationMethods | None:
  666. algo = cls._extract_modelopt_quant_algo(hf_quant_cfg)
  667. if algo is not None and ("NVFP4" in algo or "FP4" in algo):
  668. return "modelopt_fp4"
  669. return None
  670. @classmethod
  671. def _from_config(
  672. cls,
  673. *,
  674. quant_method: str,
  675. kv_cache_quant_method: str | None,
  676. exclude_modules: list[str],
  677. original_config: dict[str, Any],
  678. group_size: int | None,
  679. **kwargs: Any,
  680. ) -> "ModelOptNvFp4Config":
  681. is_checkpoint_nvfp4_serialized = "NVFP4" in quant_method
  682. # A checkpoint can declare quant_algo "NVFP4" yet quantize weights
  683. # only (input_activations is null in every config group). The W4A4
  684. # MoE path then folds an uninitialized input_scale that was never
  685. # loaded and can zero every expert, so route it through W4A16.
  686. if quant_method == "NVFP4":
  687. config_groups = original_config.get("config_groups")
  688. if (
  689. isinstance(config_groups, dict)
  690. and config_groups
  691. and all(
  692. isinstance(group, dict) and group.get("input_activations") is None
  693. for group in config_groups.values()
  694. )
  695. ):
  696. quant_method = "W4A16_NVFP4"
  697. if group_size is None:
  698. group_size = 16 # Default value
  699. # For FP4, these fields are required
  700. if is_checkpoint_nvfp4_serialized and "quantization" in original_config:
  701. # Check if required fields are present in the quantization config
  702. quant_config = original_config["quantization"]
  703. required_fields = ["group_size", "kv_cache_quant_algo", "exclude_modules"]
  704. missing_fields = [
  705. field for field in required_fields if field not in quant_config
  706. ]
  707. if missing_fields:
  708. raise ValueError(
  709. f"NVFP4 quantization requires the following fields in "
  710. f"hf_quant_config.json: {missing_fields}"
  711. )
  712. return cls(
  713. quant_method,
  714. is_checkpoint_nvfp4_serialized,
  715. kv_cache_quant_method,
  716. exclude_modules,
  717. group_size,
  718. )
  719. class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
  720. """MoE Method for FP4 Quantization.
  721. Args:
  722. quant_config: NVFP4 Quant Config
  723. """
  724. supports_pre_processed_weights = True
  725. def __init__(
  726. self,
  727. quant_config: ModelOptNvFp4Config,
  728. moe_config: FusedMoEConfig,
  729. ) -> None:
  730. super().__init__(moe_config)
  731. self.quant_config = quant_config
  732. # W4A16 mode fires for W4A16_NVFP4 on-disk checkpoints. With
  733. # activation_key=None every W4A4 backend's _supports_quant_scheme
  734. # rejects itself (they all require (kNvfp4Static, kNvfp4Dynamic)
  735. # exactly); only Marlin survives. Marlin's MoE path drops
  736. # activation scales in convert_to_nvfp4_moe_kernel_format, so no
  737. # other change is needed.
  738. self.use_a16 = quant_config.quant_method == "W4A16_NVFP4"
  739. self.nvfp4_backend, self.experts_cls = select_nvfp4_moe_backend(
  740. config=self.moe,
  741. weight_key=kNvfp4Static,
  742. activation_key=None if self.use_a16 else kNvfp4Dynamic,
  743. )
  744. self.use_global_sf = is_global_sf_supported_for_nvfp4_backend(
  745. self.nvfp4_backend
  746. )
  747. def uses_weight_scale_2_pattern(self) -> bool:
  748. """FP4 variants use 'weight_scale_2' pattern for per-tensor weight scales."""
  749. return True
  750. def create_weights(
  751. self,
  752. layer: RoutedExperts,
  753. num_experts: int,
  754. hidden_size: int,
  755. intermediate_size_per_partition: int,
  756. params_dtype: torch.dtype,
  757. **extra_weight_attrs,
  758. ):
  759. assert self.quant_config.is_checkpoint_nvfp4_serialized
  760. layer.num_experts = num_experts
  761. layer.params_dtype = params_dtype
  762. layer.quant_config = self.quant_config
  763. weight_dtype = torch.uint8
  764. weight_scale_dtype = torch.float8_e4m3fn
  765. weight_loader = extra_weight_attrs.get("weight_loader")
  766. global_num_experts = extra_weight_attrs.get("global_num_experts")
  767. w13_num_shards = 2 if self.moe.is_act_and_mul else 1
  768. # GEMM 1
  769. w13_weight = ModelWeightParameter(
  770. data=torch.empty(
  771. num_experts,
  772. w13_num_shards * intermediate_size_per_partition,
  773. # 2 fp4 items are packed in the input dimension
  774. hidden_size // 2,
  775. dtype=weight_dtype,
  776. ),
  777. input_dim=1,
  778. output_dim=2,
  779. weight_loader=weight_loader,
  780. )
  781. layer.register_parameter("w13_weight", w13_weight)
  782. # GEMM 2
  783. w2_weight = ModelWeightParameter(
  784. data=torch.empty(
  785. num_experts,
  786. hidden_size,
  787. # 2 fp4 items are packed in the input dimension
  788. intermediate_size_per_partition // 2,
  789. dtype=weight_dtype,
  790. ),
  791. input_dim=1,
  792. output_dim=2,
  793. weight_loader=weight_loader,
  794. )
  795. layer.register_parameter("w2_weight", w2_weight)
  796. w13_weight_scale = ModelWeightParameter(
  797. data=torch.empty(
  798. num_experts,
  799. w13_num_shards * intermediate_size_per_partition,
  800. # 2 fp4 items are packed in the input dimension
  801. hidden_size // self.quant_config.group_size,
  802. dtype=weight_scale_dtype,
  803. ),
  804. input_dim=1,
  805. output_dim=2,
  806. weight_loader=weight_loader,
  807. )
  808. layer.register_parameter("w13_weight_scale", w13_weight_scale)
  809. w2_weight_scale = ModelWeightParameter(
  810. data=torch.empty(
  811. num_experts,
  812. hidden_size,
  813. # 2 fp4 items are packed in the input dimension
  814. intermediate_size_per_partition // self.quant_config.group_size,
  815. dtype=weight_scale_dtype,
  816. ),
  817. input_dim=1,
  818. output_dim=2,
  819. weight_loader=weight_loader,
  820. )
  821. layer.register_parameter("w2_weight_scale", w2_weight_scale)
  822. extra_weight_attrs.update(
  823. {"quant_method": FusedMoeWeightScaleSupported.BLOCK.value}
  824. )
  825. w13_weight_scale_2 = PerTensorScaleParameter(
  826. data=torch.empty(num_experts, w13_num_shards, dtype=torch.float32),
  827. weight_loader=weight_loader,
  828. )
  829. layer.register_parameter("w13_weight_scale_2", w13_weight_scale_2)
  830. w2_weight_scale_2 = PerTensorScaleParameter(
  831. data=torch.empty(num_experts, dtype=torch.float32),
  832. weight_loader=weight_loader,
  833. )
  834. layer.register_parameter("w2_weight_scale_2", w2_weight_scale_2)
  835. extra_weight_attrs.update(
  836. {"quant_method": FusedMoeWeightScaleSupported.TENSOR.value}
  837. )
  838. global_sf_num_experts = (
  839. global_num_experts if self.use_global_sf else num_experts
  840. )
  841. w13_input_scale = PerTensorScaleParameter(
  842. data=torch.empty(
  843. global_sf_num_experts,
  844. w13_num_shards,
  845. dtype=torch.float32,
  846. ),
  847. weight_loader=weight_loader,
  848. )
  849. layer.register_parameter("w13_input_scale", w13_input_scale)
  850. w2_input_scale = PerTensorScaleParameter(
  851. data=torch.empty(global_sf_num_experts, dtype=torch.float32),
  852. weight_loader=weight_loader,
  853. )
  854. layer.register_parameter("w2_input_scale", w2_input_scale)
  855. def process_weights_after_loading(self, layer: RoutedExperts) -> None:
  856. """Convert NVFP4 MoE weights into kernel format and setup the kernel."""
  857. if is_weights_pre_processed():
  858. # CUTLASS qualifies too: its experts step skips the in-place scale
  859. # fold under weights_already_processed.
  860. if self.nvfp4_backend not in (
  861. NvFp4MoeBackend.FLASHINFER_TRTLLM,
  862. NvFp4MoeBackend.FLASHINFER_CUTLASS,
  863. ):
  864. raise RuntimeError(
  865. "pre-processed weights require the FLASHINFER_TRTLLM or "
  866. f"FLASHINFER_CUTLASS moe backend, got {self.nvfp4_backend}"
  867. )
  868. self._restore_padded_moe_dims(layer)
  869. self._build_moe_kernel(layer)
  870. return
  871. # Use a single gscale for w13.
  872. if self.moe.is_act_and_mul and not torch.allclose(
  873. layer.w13_weight_scale_2[:, 0], layer.w13_weight_scale_2[:, 1]
  874. ):
  875. logger.warning_once(
  876. "w1_weight_scale_2 must match w3_weight_scale_2. "
  877. "Accuracy may be affected."
  878. )
  879. w13_weight_scale_2 = layer.w13_weight_scale_2[:, 0].contiguous()
  880. (
  881. w13,
  882. w13_scale,
  883. w13_scale_2,
  884. a13_scale,
  885. w2,
  886. w2_scale,
  887. w2_scale_2,
  888. a2_scale,
  889. ) = convert_to_nvfp4_moe_kernel_format(
  890. nvfp4_backend=self.nvfp4_backend,
  891. layer=layer,
  892. w13=layer.w13_weight,
  893. w13_scale=layer.w13_weight_scale,
  894. w13_scale_2=w13_weight_scale_2,
  895. a13_scale=layer.w13_input_scale,
  896. w2=layer.w2_weight,
  897. w2_scale=layer.w2_weight_scale,
  898. w2_scale_2=layer.w2_weight_scale_2,
  899. a2_scale=layer.w2_input_scale,
  900. is_act_and_mul=self.moe.is_act_and_mul,
  901. use_a16=self.use_a16,
  902. )
  903. replace_parameter(layer, "w13_weight", w13)
  904. replace_parameter(layer, "w13_weight_scale", w13_scale)
  905. replace_parameter(layer, "w13_weight_scale_2", w13_scale_2)
  906. replace_parameter(layer, "w13_input_scale", a13_scale)
  907. replace_parameter(layer, "w2_weight", w2)
  908. replace_parameter(layer, "w2_weight_scale", w2_scale)
  909. replace_parameter(layer, "w2_weight_scale_2", w2_scale_2)
  910. replace_parameter(layer, "w2_input_scale", a2_scale)
  911. self._build_moe_kernel(layer)
  912. def _build_moe_kernel(self, layer: RoutedExperts) -> None:
  913. """Build the modular MoE kernel from the (already in-format) weights."""
  914. self.moe_quant_config = self.get_fused_moe_quant_config(layer)
  915. assert self.experts_cls is not None
  916. self.moe_kernel = make_nvfp4_moe_kernel(
  917. moe_quant_config=self.moe_quant_config,
  918. moe_config=self.moe,
  919. experts_cls=self.experts_cls,
  920. backend=self.nvfp4_backend,
  921. routing_tables=layer._expert_routing_tables(),
  922. )
  923. self.moe_kernel.fused_experts.process_weights_after_loading(layer)
  924. def _restore_padded_moe_dims(self, layer: RoutedExperts) -> None:
  925. """Recover the padded ``moe_config`` dims from the exported weights."""
  926. mc = layer.moe_config
  927. padded_hidden = layer.w2_weight.shape[1]
  928. if padded_hidden != mc.hidden_dim:
  929. if mc.hidden_dim_unpadded is None:
  930. mc.hidden_dim_unpadded = mc.hidden_dim
  931. mc.hidden_dim = padded_hidden
  932. mc.intermediate_size_per_partition = layer.w2_weight.shape[2] * 2
  933. def get_fused_moe_quant_config(self, layer: RoutedExperts) -> FusedMoEQuantConfig:
  934. return make_nvfp4_moe_quant_config(
  935. backend=self.nvfp4_backend,
  936. w13_scale=layer.w13_weight_scale,
  937. w2_scale=layer.w2_weight_scale,
  938. w13_scale_2=layer.w13_weight_scale_2,
  939. w2_scale_2=layer.w2_weight_scale_2,
  940. a13_scale=layer.w13_input_scale,
  941. a2_scale=layer.w2_input_scale,
  942. swiglu_limit=getattr(layer, "swiglu_limit", None),
  943. swiglu_alpha=getattr(layer, "swiglu_alpha", None),
  944. swiglu_beta=getattr(layer, "swiglu_beta", None),
  945. layer=layer,
  946. use_a16=self.use_a16,
  947. )
  948. @property
  949. def supports_eplb(self) -> bool:
  950. return True
  951. def apply_monolithic(
  952. self,
  953. layer: RoutedExperts,
  954. x: torch.Tensor,
  955. router_logits: torch.Tensor,
  956. input_ids: torch.Tensor | None = None,
  957. ) -> torch.Tensor | UnfinalizedMoEOutput:
  958. assert self.is_monolithic
  959. assert self.moe_kernel is not None
  960. return self.moe_kernel.apply_monolithic(
  961. x,
  962. layer.w13_weight,
  963. layer.w2_weight,
  964. router_logits,
  965. activation=layer.activation,
  966. global_num_experts=layer.global_num_experts,
  967. expert_map=layer.expert_map,
  968. apply_router_weight_on_input=layer.apply_router_weight_on_input,
  969. num_expert_group=layer.num_expert_group,
  970. topk_group=layer.topk_group,
  971. e_score_correction_bias=layer.e_score_correction_bias,
  972. routed_scaling_factor=layer.routed_scaling_factor,
  973. )
  974. def apply(
  975. self,
  976. layer: RoutedExperts,
  977. x: torch.Tensor,
  978. topk_weights: torch.Tensor,
  979. topk_ids: torch.Tensor,
  980. shared_experts: SharedExperts | None,
  981. shared_experts_input: torch.Tensor | None,
  982. ) -> torch.Tensor | UnfinalizedMoEOutput:
  983. assert not self.is_monolithic
  984. assert self.moe_kernel is not None
  985. return self.moe_kernel.apply(
  986. x,
  987. layer.w13_weight,
  988. layer.w2_weight,
  989. topk_weights,
  990. topk_ids,
  991. activation=layer.activation,
  992. global_num_experts=layer.global_num_experts,
  993. expert_map=layer.expert_map,
  994. apply_router_weight_on_input=layer.apply_router_weight_on_input,
  995. shared_experts=shared_experts,
  996. shared_experts_input=shared_experts_input,
  997. )
  998. ModelOptNvFp4Config.FusedMoEMethodCls = ModelOptNvFp4FusedMoE
  999. ModelOptNvFp4Config.KVCacheMethodCls = ModelOptKVCacheMethod
  1000. class ModelOptMxFp8Config(ModelOptQuantConfigBase):
  1001. """Config class for ModelOpt MXFP8."""
  1002. def __init__(
  1003. self,
  1004. is_checkpoint_mxfp8_serialized: bool,
  1005. kv_cache_quant_algo: str | None,
  1006. exclude_modules: list[str],
  1007. ) -> None:
  1008. super().__init__(exclude_modules)
  1009. self.quant_method = "MXFP8"
  1010. self.is_checkpoint_mxfp8_serialized = is_checkpoint_mxfp8_serialized
  1011. if not is_checkpoint_mxfp8_serialized:
  1012. raise ValueError(
  1013. "MXFP8 quantization requires a serialized checkpoint. "
  1014. "Dynamic quantization is not supported."
  1015. )
  1016. self.kv_cache_quant_algo = kv_cache_quant_algo
  1017. def get_name(self) -> QuantizationMethods:
  1018. return "modelopt_mxfp8"
  1019. def get_supported_act_dtypes(self) -> list[torch.dtype]:
  1020. return [torch.bfloat16]
  1021. @classmethod
  1022. def get_min_capability(cls) -> int:
  1023. # Marlin kernel supports MXFP8 on SM80+
  1024. return 80
  1025. @classmethod
  1026. def override_quantization_method(
  1027. cls, hf_quant_cfg, user_quant, hf_config=None
  1028. ) -> QuantizationMethods | None:
  1029. algo = cls._extract_modelopt_quant_algo(hf_quant_cfg)
  1030. if algo is not None and "MXFP8" in algo:
  1031. return "modelopt_mxfp8"
  1032. return None
  1033. @classmethod
  1034. def from_config(cls, config: dict[str, Any]) -> "ModelOptMxFp8Config":
  1035. # MiniMax-style checkpoints tag `quant_method: "mxfp8"` + `ignored_layers`
  1036. # (same on-disk format as ModelOpt MXFP8); normalize to the ModelOpt
  1037. # schema and reuse the shared parser.
  1038. if "quantization" not in config and not config.get("quant_algo"):
  1039. config = {
  1040. "quant_method": "modelopt",
  1041. "quantization": {
  1042. "quant_algo": "MXFP8",
  1043. "kv_cache_quant_algo": config.get("kv_cache_quant_algo"),
  1044. "exclude_modules": config.get("ignored_layers", []) or [],
  1045. },
  1046. }
  1047. return cast("ModelOptMxFp8Config", super().from_config(config))
  1048. @classmethod
  1049. def _from_config(
  1050. cls,
  1051. *,
  1052. quant_method: str,
  1053. kv_cache_quant_method: str | None,
  1054. exclude_modules: list[str],
  1055. original_config: dict[str, Any],
  1056. **kwargs: Any,
  1057. ) -> "ModelOptMxFp8Config":
  1058. is_checkpoint_mxfp8_serialized = "MXFP8" in quant_method.upper()
  1059. # For MXFP8, validate required fields in the config
  1060. if is_checkpoint_mxfp8_serialized and "quantization" in original_config:
  1061. quant_config = original_config["quantization"]
  1062. required_fields = ["kv_cache_quant_algo", "exclude_modules"]
  1063. missing_fields = [
  1064. field for field in required_fields if field not in quant_config
  1065. ]
  1066. if missing_fields:
  1067. raise ValueError(
  1068. f"MXFP8 quantization requires the following fields in "
  1069. f"hf_quant_config.json: {missing_fields}"
  1070. )
  1071. return cls(
  1072. is_checkpoint_mxfp8_serialized,
  1073. kv_cache_quant_method,
  1074. exclude_modules,
  1075. )
  1076. class ModelOptMxFp8FusedMoE(FusedMoEMethodBase):
  1077. """FlashInfer TRTLLM MXFP8 block-scale MoE for ModelOpt checkpoints."""
  1078. def __init__(
  1079. self,
  1080. quant_config: ModelOptMxFp8Config,
  1081. moe_config: FusedMoEConfig,
  1082. ) -> None:
  1083. super().__init__(moe_config)
  1084. self.weight_block_size = [1, MXFP8_BLOCK_SIZE]
  1085. self.quant_config = quant_config
  1086. assert self.quant_config.is_checkpoint_mxfp8_serialized
  1087. self.mxfp8_backend, self.experts_cls = select_mxfp8_moe_backend(config=self.moe)
  1088. def create_weights(
  1089. self,
  1090. layer: RoutedExperts,
  1091. num_experts: int,
  1092. hidden_size: int,
  1093. intermediate_size_per_partition: int,
  1094. params_dtype: torch.dtype,
  1095. **extra_weight_attrs,
  1096. ):
  1097. assert layer.intermediate_size_per_partition == intermediate_size_per_partition
  1098. assert layer.hidden_size == hidden_size
  1099. layer.orig_dtype = params_dtype
  1100. if hidden_size % MXFP8_BLOCK_SIZE != 0:
  1101. raise ValueError(
  1102. f"MXFP8 MoE requires hidden_size divisible by {MXFP8_BLOCK_SIZE}, "
  1103. f"got {hidden_size}."
  1104. )
  1105. if intermediate_size_per_partition % MXFP8_BLOCK_SIZE != 0:
  1106. raise ValueError(
  1107. "MXFP8 MoE requires intermediate_size_per_partition divisible by "
  1108. f"{MXFP8_BLOCK_SIZE}, got {intermediate_size_per_partition}."
  1109. )
  1110. layer.num_experts = num_experts
  1111. weight_loader = extra_weight_attrs.get("weight_loader")
  1112. w13_num_shards = 2 if self.moe.is_act_and_mul else 1
  1113. # GEMM 1 weights: [E, (2I or I), H]
  1114. w13_weight = ModelWeightParameter(
  1115. data=torch.empty(
  1116. num_experts,
  1117. w13_num_shards * intermediate_size_per_partition,
  1118. hidden_size,
  1119. dtype=MXFP8_VALUE_DTYPE,
  1120. ),
  1121. input_dim=2,
  1122. output_dim=1,
  1123. weight_loader=weight_loader,
  1124. )
  1125. layer.register_parameter("w13_weight", w13_weight)
  1126. # GEMM 2 weights: [E, H, I]
  1127. w2_weight = ModelWeightParameter(
  1128. data=torch.empty(
  1129. num_experts,
  1130. hidden_size,
  1131. intermediate_size_per_partition,
  1132. dtype=MXFP8_VALUE_DTYPE,
  1133. ),
  1134. input_dim=2,
  1135. output_dim=1,
  1136. weight_loader=weight_loader,
  1137. )
  1138. layer.register_parameter("w2_weight", w2_weight)
  1139. # Per-block (K=32) E8M0 scales.
  1140. w13_weight_scale = ModelWeightParameter(
  1141. data=torch.empty(
  1142. num_experts,
  1143. w13_num_shards * intermediate_size_per_partition,
  1144. hidden_size // MXFP8_BLOCK_SIZE,
  1145. dtype=MXFP8_SCALE_DTYPE,
  1146. ),
  1147. input_dim=2,
  1148. output_dim=1,
  1149. weight_loader=weight_loader,
  1150. )
  1151. layer.register_parameter("w13_weight_scale", w13_weight_scale)
  1152. w2_weight_scale = ModelWeightParameter(
  1153. data=torch.empty(
  1154. num_experts,
  1155. hidden_size,
  1156. intermediate_size_per_partition // MXFP8_BLOCK_SIZE,
  1157. dtype=MXFP8_SCALE_DTYPE,
  1158. ),
  1159. input_dim=2,
  1160. output_dim=1,
  1161. weight_loader=weight_loader,
  1162. )
  1163. layer.register_parameter("w2_weight_scale", w2_weight_scale)
  1164. # Ensure the generic MoE weight-loader treats these as block scales.
  1165. set_weight_attrs(
  1166. layer.w13_weight_scale,
  1167. {"quant_method": FusedMoeWeightScaleSupported.BLOCK.value},
  1168. )
  1169. set_weight_attrs(
  1170. layer.w2_weight_scale,
  1171. {"quant_method": FusedMoeWeightScaleSupported.BLOCK.value},
  1172. )
  1173. @staticmethod
  1174. def _check_weight_dtypes(layer: torch.nn.Module) -> None:
  1175. """Validate weight and scale dtypes before processing."""
  1176. expected = {
  1177. "w13_weight": MXFP8_VALUE_DTYPE,
  1178. "w2_weight": MXFP8_VALUE_DTYPE,
  1179. "w13_weight_scale": MXFP8_SCALE_DTYPE,
  1180. "w2_weight_scale": MXFP8_SCALE_DTYPE,
  1181. }
  1182. for name, expected_dtype in expected.items():
  1183. actual = getattr(layer, name).dtype
  1184. if actual != expected_dtype:
  1185. raise ValueError(
  1186. f"Expected {name} dtype {expected_dtype}, got {actual}."
  1187. )
  1188. def _dequant_mxfp8_weights_to_bf16(self, layer: RoutedExperts) -> None:
  1189. """One-time MXFP8->BF16 weight dequant for the emulation path.
  1190. On devices without a native MXFP8 MoE kernel (e.g. gfx942 / MI300),
  1191. ``Mxfp8EmulationTritonExperts`` otherwise dequantizes every expert
  1192. weight to BF16 on *every* forward step -- the dominant cost (conc1
  1193. ~1.3 tok/s). Doing the dequant once here and replacing the MXFP8
  1194. parameters with BF16 makes the MoE run exactly like a plain BF16
  1195. checkpoint (full precision, no per-step dequant); SwiGLU-OAI is still
  1196. applied by the experts' ``activation()`` override. The MXFP8 weights
  1197. are freed by ``replace_parameter`` (BF16 is 2x their size; the small
  1198. E8M0 scale tensors are left in place, unused).
  1199. """
  1200. from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
  1201. dequant_mxfp8_to_bf16,
  1202. )
  1203. target_dtype = getattr(layer, "orig_dtype", torch.bfloat16)
  1204. num_experts = layer.w13_weight.shape[0]
  1205. # dequant_mxfp8_to_bf16 handles arbitrary leading dims (*x.shape[:-1]),
  1206. # so dequant the whole [E, N, K] weight in one vectorized call.
  1207. w13_bf16 = dequant_mxfp8_to_bf16(layer.w13_weight, layer.w13_weight_scale).to(
  1208. target_dtype
  1209. )
  1210. w2_bf16 = dequant_mxfp8_to_bf16(layer.w2_weight, layer.w2_weight_scale).to(
  1211. target_dtype
  1212. )
  1213. replace_parameter(layer, "w13_weight", w13_bf16)
  1214. replace_parameter(layer, "w2_weight", w2_bf16)
  1215. logger.info_once(
  1216. "MXFP8->BF16 load-time dequant complete (%d experts/layer); MoE "
  1217. "now runs in BF16 with no per-step dequant.",
  1218. num_experts,
  1219. )
  1220. def process_weights_after_loading(self, layer: RoutedExperts) -> None:
  1221. # TODO(bnell): why is this required only for mxfp8?
  1222. if getattr(layer, "_already_called_process_weights_after_loading", False):
  1223. return
  1224. layer._already_called_process_weights_after_loading = True
  1225. self._check_weight_dtypes(layer)
  1226. layer.weight_block_size = self.weight_block_size
  1227. w13, w2, w13_scale, w2_scale = convert_to_fp8_moe_kernel_format(
  1228. fp8_backend=self.mxfp8_backend,
  1229. layer=layer,
  1230. w13=layer.w13_weight,
  1231. w2=layer.w2_weight,
  1232. w13_scale=layer.w13_weight_scale,
  1233. w2_scale=layer.w2_weight_scale,
  1234. w13_input_scale=None,
  1235. w2_input_scale=None,
  1236. )
  1237. replace_parameter(layer, "w13_weight", w13)
  1238. replace_parameter(layer, "w2_weight", w2)
  1239. replace_parameter(layer, "w13_weight_scale", w13_scale)
  1240. replace_parameter(layer, "w2_weight_scale", w2_scale)
  1241. self.moe_quant_config = self.get_fused_moe_quant_config(layer)
  1242. assert self.moe_quant_config is not None
  1243. assert self.experts_cls is not None
  1244. self.moe_kernel = make_fp8_moe_kernel(
  1245. moe_quant_config=self.moe_quant_config,
  1246. moe_config=self.moe,
  1247. fp8_backend=self.mxfp8_backend,
  1248. experts_cls=self.experts_cls,
  1249. routing_tables=layer._expert_routing_tables(),
  1250. )
  1251. # No native MXFP8 MoE kernel on this device (e.g. gfx942): the emulation
  1252. # experts would dequant MXFP8->BF16 every forward step. Convert the
  1253. # weights to BF16 once, here, so the MoE runs like a BF16 checkpoint.
  1254. # Opt out (VLLM_MXFP8_EMULATION_DEQUANT_AT_LOAD=0) to keep the 1-byte
  1255. # MXFP8 weights and dequant per-step (~half the memory, much slower).
  1256. if (
  1257. self.mxfp8_backend == Fp8MoeBackend.EMULATION
  1258. and envs.VLLM_MXFP8_EMULATION_DEQUANT_AT_LOAD
  1259. ):
  1260. self._dequant_mxfp8_weights_to_bf16(layer)
  1261. def get_fused_moe_quant_config(
  1262. self, layer: RoutedExperts
  1263. ) -> FusedMoEQuantConfig | None:
  1264. return make_fp8_moe_quant_config(
  1265. fp8_backend=self.mxfp8_backend,
  1266. w1_scale=layer.w13_weight_scale,
  1267. w2_scale=layer.w2_weight_scale,
  1268. a1_scale=None,
  1269. a2_scale=None,
  1270. block_shape=self.weight_block_size,
  1271. swiglu_limit=getattr(layer, "swiglu_limit", None),
  1272. gemm1_alpha=getattr(layer, "swiglu_alpha", None),
  1273. gemm1_beta=getattr(layer, "swiglu_beta", None),
  1274. layer=layer,
  1275. )
  1276. def apply_monolithic(
  1277. self,
  1278. layer: RoutedExperts,
  1279. x: torch.Tensor,
  1280. router_logits: torch.Tensor,
  1281. input_ids: torch.Tensor | None = None,
  1282. ) -> torch.Tensor:
  1283. assert self.is_monolithic
  1284. assert self.moe_kernel is not None
  1285. return self.moe_kernel.apply_monolithic(
  1286. x,
  1287. layer.w13_weight,
  1288. layer.w2_weight,
  1289. router_logits,
  1290. activation=layer.activation,
  1291. global_num_experts=layer.global_num_experts,
  1292. expert_map=layer.expert_map,
  1293. apply_router_weight_on_input=layer.apply_router_weight_on_input,
  1294. num_expert_group=layer.num_expert_group,
  1295. topk_group=layer.topk_group,
  1296. e_score_correction_bias=layer.e_score_correction_bias,
  1297. routed_scaling_factor=layer.routed_scaling_factor,
  1298. )
  1299. def apply(
  1300. self,
  1301. layer: RoutedExperts,
  1302. x: torch.Tensor,
  1303. topk_weights: torch.Tensor,
  1304. topk_ids: torch.Tensor,
  1305. shared_experts: SharedExperts | None,
  1306. shared_experts_input: torch.Tensor | None,
  1307. ) -> torch.Tensor | UnfinalizedMoEOutput:
  1308. assert not self.is_monolithic
  1309. assert self.moe_kernel is not None
  1310. return self.moe_kernel.apply(
  1311. x,
  1312. layer.w13_weight,
  1313. layer.w2_weight,
  1314. topk_weights,
  1315. topk_ids,
  1316. activation=layer.activation,
  1317. global_num_experts=layer.global_num_experts,
  1318. expert_map=layer.expert_map,
  1319. apply_router_weight_on_input=layer.apply_router_weight_on_input,
  1320. shared_experts=shared_experts,
  1321. shared_experts_input=shared_experts_input,
  1322. )
  1323. # Register the method classes for ModelOptMxFp8Config
  1324. ModelOptMxFp8Config.FusedMoEMethodCls = ModelOptMxFp8FusedMoE
  1325. ModelOptMxFp8Config.KVCacheMethodCls = ModelOptKVCacheMethod
  1326. class ModelOptMixedPrecisionConfig(ModelOptQuantConfigBase):
  1327. """Config class for ModelOpt MIXED_PRECISION.
  1328. Supports checkpoints where different layers use different quantization
  1329. algorithms (e.g., FP8 for dense layers and NVFP4 for MoE experts).
  1330. The per-layer algorithm is specified in the ``quantized_layers`` dict
  1331. inside ``config.json``'s ``quantization_config`` (preferred) or the
  1332. legacy ``hf_quant_config.json``.
  1333. """
  1334. def __init__(
  1335. self,
  1336. kv_cache_quant_method: str | None,
  1337. exclude_modules: list[str],
  1338. quantized_layers: dict[str, dict[str, Any]],
  1339. fp8_config: ModelOptFp8Config,
  1340. nvfp4_config: ModelOptNvFp4Config,
  1341. w4a16_nvfp4_config: ModelOptNvFp4Config,
  1342. mxfp8_config: ModelOptMxFp8Config,
  1343. ) -> None:
  1344. super().__init__(exclude_modules)
  1345. self.kv_cache_quant_method = kv_cache_quant_method
  1346. self.quantized_layers = quantized_layers
  1347. self.fp8_config = fp8_config
  1348. self.nvfp4_config = nvfp4_config
  1349. self.w4a16_nvfp4_config = w4a16_nvfp4_config
  1350. self.mxfp8_config = mxfp8_config
  1351. block_sizes = {
  1352. int(layer_info.get("group_size", 128))
  1353. for layer_info in quantized_layers.values()
  1354. if layer_info.get("quant_algo", "").upper() in _BLOCK_FP8_MOE_ALGOS
  1355. }
  1356. if len(block_sizes) > 1:
  1357. raise ValueError(
  1358. "MIXED_PRECISION currently requires all block-FP8 MoE layers "
  1359. f"to use one group_size, got {sorted(block_sizes)}."
  1360. )
  1361. block_size = next(iter(block_sizes), 128)
  1362. self.fp8_block_config = Fp8Config(
  1363. is_checkpoint_fp8_serialized=True,
  1364. activation_scheme="dynamic",
  1365. weight_block_size=[block_size, block_size],
  1366. )
  1367. def has_blocked_weights(self) -> bool:
  1368. # Same gate as ModelOptFp8Config.has_blocked_weights, resolved per
  1369. # layer: "+quant_fp8" must be on as soon as any layer is block-scaled.
  1370. return any(
  1371. info.get("quant_algo", "").upper() in _BLOCK_FP8_MOE_ALGOS
  1372. for info in self.quantized_layers.values()
  1373. )
  1374. def get_name(self) -> QuantizationMethods:
  1375. return "modelopt_mixed"
  1376. def get_supported_act_dtypes(self) -> list[torch.dtype]:
  1377. return [torch.bfloat16, torch.half]
  1378. @classmethod
  1379. def get_min_capability(cls) -> int:
  1380. # Turing and up (SM75+): NVFP4 routed experts run via Marlin W4A16
  1381. # (SM75+), FP8 weight-only dense via MarlinFP8 (cc>=7.5), and FP8 MoE,
  1382. # if present, via Marlin (TritonExperts gates its FP8 schemes behind
  1383. # supports_fp8(), cc>=89). None of these paths require native FP8 tensor
  1384. # cores, so SM75 is sufficient. Validated end-to-end on a Tesla T4
  1385. # (SM75) and A100 (SM80). Pairs with the FlashInfer attention SM80
  1386. # lower bound so SM75 auto-selects a supported attention backend.
  1387. return 75
  1388. @classmethod
  1389. def override_quantization_method(
  1390. cls, hf_quant_cfg, user_quant, hf_config=None
  1391. ) -> QuantizationMethods | None:
  1392. algo = cls._extract_modelopt_quant_algo(hf_quant_cfg)
  1393. if algo is not None and algo == "MIXED_PRECISION":
  1394. return "modelopt_mixed"
  1395. return None
  1396. @classmethod
  1397. def _from_config(
  1398. cls,
  1399. *,
  1400. quant_method: str,
  1401. kv_cache_quant_method: str | None,
  1402. exclude_modules: list[str],
  1403. original_config: dict[str, Any],
  1404. group_size: int | None,
  1405. **kwargs: Any,
  1406. ) -> "ModelOptMixedPrecisionConfig":
  1407. if "quantization" in original_config:
  1408. quantized_layers = original_config["quantization"].get(
  1409. "quantized_layers", {}
  1410. )
  1411. else:
  1412. quantized_layers = original_config.get("quantized_layers", {})
  1413. if not quantized_layers:
  1414. raise ValueError(
  1415. "MIXED_PRECISION quant_algo requires a non-empty "
  1416. "'quantized_layers' mapping in the quantization config."
  1417. )
  1418. # Determine group_size from the first NVFP4-family entry if not
  1419. # provided. Both NVFP4 (W4A4) and W4A16_NVFP4 share the same packing
  1420. # + group-size convention; either entry resolves the value.
  1421. if group_size is None:
  1422. for layer_info in quantized_layers.values():
  1423. if layer_info.get("quant_algo", "").upper() in (
  1424. "NVFP4",
  1425. "W4A16_NVFP4",
  1426. ):
  1427. group_size = layer_info.get("group_size", 16)
  1428. break
  1429. if group_size is None:
  1430. group_size = 16
  1431. fp8_config = ModelOptFp8Config(
  1432. quant_method="FP8",
  1433. is_checkpoint_fp8_serialized=True,
  1434. kv_cache_quant_method=kv_cache_quant_method,
  1435. exclude_modules=[],
  1436. )
  1437. nvfp4_config = ModelOptNvFp4Config(
  1438. is_checkpoint_nvfp4_serialized=True,
  1439. kv_cache_quant_algo=kv_cache_quant_method,
  1440. exclude_modules=[],
  1441. group_size=group_size,
  1442. )
  1443. # Sibling config for layers that declare quant_algo: "W4A16_NVFP4".
  1444. # get_quant_method resolves this sub-config to the (kNvfp4Static, None)
  1445. # QuantSpec (W4A16) for the generic ModelOptLinearMethod. The MoE side
  1446. # reads quant_config.quant_method == "W4A16_NVFP4" to set use_a16 →
  1447. # Marlin backend in ModelOptNvFp4FusedMoE.__init__.
  1448. w4a16_nvfp4_config = ModelOptNvFp4Config(
  1449. quant_method="W4A16_NVFP4",
  1450. is_checkpoint_nvfp4_serialized=True,
  1451. kv_cache_quant_algo=kv_cache_quant_method,
  1452. exclude_modules=[],
  1453. group_size=group_size,
  1454. )
  1455. mxfp8_config = ModelOptMxFp8Config(
  1456. is_checkpoint_mxfp8_serialized=True,
  1457. kv_cache_quant_algo=kv_cache_quant_method,
  1458. exclude_modules=[],
  1459. )
  1460. return cls(
  1461. kv_cache_quant_method=kv_cache_quant_method,
  1462. exclude_modules=exclude_modules,
  1463. quantized_layers=quantized_layers,
  1464. fp8_config=fp8_config,
  1465. nvfp4_config=nvfp4_config,
  1466. w4a16_nvfp4_config=w4a16_nvfp4_config,
  1467. mxfp8_config=mxfp8_config,
  1468. )
  1469. def _resolve_quant_algo(self, prefix: str) -> str | None:
  1470. """Look up the quant_algo for a vLLM-side layer prefix.
  1471. Tries three strategies in order:
  1472. 1. Direct lookup in ``quantized_layers``.
  1473. 2. Packed/fused-layer lookup (unfuse via ``packed_modules_mapping``).
  1474. 3. Prefix-based lookup for RoutedExperts (any child key starts with
  1475. ``prefix + "."``).
  1476. Returns the upper-cased quant_algo string, or *None* if the prefix
  1477. is not found.
  1478. """
  1479. # 1. Direct lookup
  1480. for candidate in self._quantized_layer_prefix_candidates(prefix):
  1481. if candidate in self.quantized_layers:
  1482. return self.quantized_layers[candidate]["quant_algo"].upper()
  1483. # 2. Packed / fused layer lookup
  1484. proj_name = prefix.rsplit(".", 1)[-1]
  1485. if self.packed_modules_mapping and proj_name in self.packed_modules_mapping:
  1486. algos: set[str] = set()
  1487. base = prefix.rsplit(".", 1)[0]
  1488. for base_candidate in self._quantized_layer_prefix_candidates(base):
  1489. for shard_name in self.packed_modules_mapping[proj_name]:
  1490. shard_prefix = f"{base_candidate}.{shard_name}"
  1491. if shard_prefix in self.quantized_layers:
  1492. algos.add(
  1493. self.quantized_layers[shard_prefix]["quant_algo"].upper()
  1494. )
  1495. if len(algos) == 1:
  1496. return algos.pop()
  1497. if len(algos) > 1:
  1498. raise ValueError(
  1499. f"Mixed quant_algo within fused layer {prefix}: "
  1500. f"{algos}. All shards must use the same quantization."
  1501. )
  1502. # 3. Prefix-based lookup (for RoutedExperts / parent modules)
  1503. for candidate in self._quantized_layer_prefix_candidates(prefix):
  1504. prefix_dot = candidate + "."
  1505. for key, info in self.quantized_layers.items():
  1506. if key.startswith(prefix_dot):
  1507. return info["quant_algo"].upper()
  1508. # RoutedExperts expert prefix is e.g. "...moe.experts", while ModelOpt's
  1509. # quantized_layers entries use "...moe.gate_proj" / "...moe.up_proj".
  1510. if prefix.endswith(".experts"):
  1511. parent_dot = prefix.rsplit(".experts", 1)[0] + "."
  1512. for key, info in self.quantized_layers.items():
  1513. if key.startswith(parent_dot):
  1514. return info["quant_algo"].upper()
  1515. # 4. Parent-prefix fallback for fused projections whose config lists
  1516. # shard names instead of vLLM's packed module name.
  1517. fused_projection_shards = {
  1518. "qkv_proj": ("q_proj", "k_proj", "v_proj"),
  1519. "gate_up_proj": ("gate_proj", "up_proj"),
  1520. }
  1521. shard_names = fused_projection_shards.get(proj_name)
  1522. if shard_names is not None:
  1523. for candidate in self._quantized_layer_prefix_candidates(prefix):
  1524. parent_dot = candidate.rsplit(".", 1)[0] + "."
  1525. shard_algos: set[str] = set()
  1526. for shard_name in shard_names:
  1527. shard_prefix = f"{parent_dot}{shard_name}"
  1528. if shard_prefix in self.quantized_layers:
  1529. algo = self.quantized_layers[shard_prefix]["quant_algo"].upper()
  1530. shard_algos.add(algo)
  1531. if len(shard_algos) == 1:
  1532. return shard_algos.pop()
  1533. if len(shard_algos) > 1:
  1534. raise ValueError(
  1535. f"Mixed quant_algo within fused layer {prefix}: "
  1536. f"{shard_algos}. All shards must use the same quantization."
  1537. )
  1538. return None
  1539. @staticmethod
  1540. def _quantized_layer_prefix_candidates(prefix: str) -> tuple[str, ...]:
  1541. candidates = [prefix]
  1542. if prefix.endswith(".lm_head"):
  1543. candidates.append("lm_head")
  1544. if prefix.startswith("language_model.model."):
  1545. candidates.append(
  1546. "model.language_model." + prefix[len("language_model.model.") :]
  1547. )
  1548. elif prefix.startswith("model.language_model."):
  1549. candidates.append(
  1550. "language_model.model." + prefix[len("model.language_model.") :]
  1551. )
  1552. return tuple(dict.fromkeys(candidates))
  1553. def get_quant_method(
  1554. self, layer: torch.nn.Module, prefix: str
  1555. ) -> "QuantizeMethodBase | None":
  1556. """Return quantize-method based on layer."""
  1557. # KV-cache quantization
  1558. if isinstance(layer, Attention):
  1559. if self.kv_cache_quant_method:
  1560. return ModelOptKVCacheMethod(self)
  1561. return None
  1562. # Excluded layers
  1563. if self.is_layer_excluded(prefix):
  1564. if isinstance(layer, (LinearBase, ParallelLMHead)):
  1565. return UnquantizedLinearMethod()
  1566. return None
  1567. quant_algo = self._resolve_quant_algo(prefix)
  1568. if isinstance(layer, (LinearBase, ParallelLMHead)):
  1569. # Per-prefix algo -> its sub-config, then the generic linear method.
  1570. # resolve() reads group_size off whichever sub-config it is handed.
  1571. # Read-only — never write back, or a linear-only change leaks into
  1572. # the MoE method that shares the sub-config.
  1573. if quant_algo is None or quant_algo not in LINEAR_ALGOS:
  1574. # Layer not in quantized_layers — leave unquantized
  1575. return UnquantizedLinearMethod()
  1576. _, subcfg_attr = LINEAR_ALGOS[quant_algo]
  1577. return build_linear_method(getattr(self, subcfg_attr), quant_algo, prefix)
  1578. if isinstance(layer, RoutedExperts):
  1579. if quant_algo in _BLOCK_FP8_MOE_ALGOS:
  1580. return Fp8MoEMethod(self.fp8_block_config, layer)
  1581. if quant_algo == "FP8":
  1582. return ModelOptFp8MoEMethod(
  1583. quant_config=self.fp8_config,
  1584. moe_config=layer.moe_config,
  1585. )
  1586. if quant_algo == "NVFP4":
  1587. return ModelOptNvFp4FusedMoE(
  1588. quant_config=self.nvfp4_config,
  1589. moe_config=layer.moe_config,
  1590. )
  1591. if quant_algo == "W4A16_NVFP4":
  1592. return ModelOptNvFp4FusedMoE(
  1593. quant_config=self.w4a16_nvfp4_config,
  1594. moe_config=layer.moe_config,
  1595. )
  1596. if quant_algo == "MXFP8":
  1597. return ModelOptMxFp8FusedMoE(
  1598. quant_config=self.mxfp8_config,
  1599. moe_config=layer.moe_config,
  1600. )
  1601. return None
  1602. return None
  1603. def apply_vllm_mapper(self, hf_to_vllm_mapper: "WeightsMapper"):
  1604. super().apply_vllm_mapper(hf_to_vllm_mapper)
  1605. if self.quantized_layers:
  1606. self.quantized_layers = hf_to_vllm_mapper.apply_dict(self.quantized_layers)
  1607. # ===========================================================================
  1608. # Generic QuantKey-driven linear method
  1609. #
  1610. # ``ModelOptLinearMethod`` serves every ModelOpt linear format. It composes a
  1611. # per-QuantKey weight scheme + activation scheme (the ``QuantSpec`` pair that
  1612. # ``resolve`` produces from the checkpoint's quant_algo), runs a fixed
  1613. # create/process lifecycle, selects the kernel from the pair, and applies.
  1614. #
  1615. # Adding a format:
  1616. # * Composes as a (weight, activation) key pair -> add a QuantKeyScheme per
  1617. # new key to SCHEME_FOR, plus a resolve() row returning the QuantSpec. No
  1618. # new method class; this is how every current format is built.
  1619. # * Needs format-wide state but the same lifecycle -> also return a
  1620. # FormatScheme subclass from that resolve() row (extra_weights / pre_process
  1621. # / post_process hooks).
  1622. # * Needs a different lifecycle entirely -> write a LinearMethodBase and
  1623. # register it in LINEAR_METHOD_BUILDERS by algo.
  1624. # In all cases accept the algo in the owning config's quant_method validation,
  1625. # so get_quant_method can dispatch it.
  1626. # ===========================================================================
  1627. class Role(Enum):
  1628. WEIGHT = "weight"
  1629. ACT = "activation"
  1630. WEIGHT = Role.WEIGHT
  1631. ACT = Role.ACT
  1632. @dataclass(frozen=True)
  1633. class CkptCtx:
  1634. """Per-checkpoint facts a QuantKey cannot carry."""
  1635. group_size: int | None = None
  1636. scale_block_size: tuple[int, int] | None = None
  1637. @dataclass(frozen=True)
  1638. class RuntimeDtypes:
  1639. """Runtime/model dtypes kernels need — format-agnostic."""
  1640. input_dtype: torch.dtype
  1641. out_dtype: torch.dtype
  1642. marlin_input_dtype: torch.dtype | None = None
  1643. @dataclass(frozen=True)
  1644. class Shapes:
  1645. """Layer geometry from create_weights args."""
  1646. output_partition_sizes: list[int]
  1647. input_size_per_partition: int
  1648. params_dtype: torch.dtype
  1649. @property
  1650. def output_size_per_partition(self) -> int:
  1651. return sum(self.output_partition_sizes)
  1652. @property
  1653. def num_partitions(self) -> int:
  1654. return len(self.output_partition_sizes)
  1655. class QuantKeyScheme:
  1656. """One scheme per QuantKey. Selected by key *content*; the base supplies the
  1657. ``role`` from the slot it fills the key into (wkey->WEIGHT, akey->ACT).
  1658. Every scheme branches on ``role`` explicitly and ``reject``s any role it has
  1659. not validated — never falling through to the wrong role's registration,
  1660. which would allocate the wrong parameters and produce garbage silently.
  1661. """
  1662. key: QuantKey
  1663. def create_weights(self, layer, role, ctx, shapes, wl) -> None:
  1664. raise NotImplementedError
  1665. def process(self, layer, role) -> None:
  1666. pass
  1667. @staticmethod
  1668. def reject(role) -> None:
  1669. raise NotImplementedError(
  1670. f"role {role!r} not validated for this QuantKey scheme"
  1671. )
  1672. @staticmethod
  1673. def register_params(layer, name, shape, dtype, cls, wl, *, init=None, **dims):
  1674. p = cls(data=torch.empty(shape, dtype=dtype), weight_loader=wl, **dims)
  1675. if init is not None:
  1676. p.data.fill_(init)
  1677. layer.register_parameter(name, p)
  1678. class KNvfp4Static(QuantKeyScheme):
  1679. """NVFP4 weight scheme (W4A4 and W4A16 share it). Weight-role only today."""
  1680. key = kNvfp4Static
  1681. def create_weights(self, layer, role, ctx, shapes, wl) -> None:
  1682. if role is not WEIGHT:
  1683. self.reject(role)
  1684. if shapes.input_size_per_partition % 16 != 0:
  1685. raise ValueError(
  1686. "Unsupported model when in features size is not multiple of 16"
  1687. )
  1688. # Packed NVFP4 weight: 2 fp4 items per byte along the input dim.
  1689. self.register_params(
  1690. layer,
  1691. "weight",
  1692. (shapes.output_size_per_partition, shapes.input_size_per_partition // 2),
  1693. torch.uint8,
  1694. ModelWeightParameter,
  1695. wl,
  1696. input_dim=1,
  1697. output_dim=0,
  1698. )
  1699. # Per-tensor global weight scale.
  1700. self.register_params(
  1701. layer,
  1702. "weight_scale_2",
  1703. (shapes.num_partitions,),
  1704. torch.float32,
  1705. PerTensorScaleParameter,
  1706. wl,
  1707. )
  1708. # Per-block (group_size) weight scale. Initialized to NaN (rather than
  1709. # left as torch.empty garbage) so an unloaded scale is unambiguously
  1710. # detectable in process() below, matching the sentinel pattern in #45320.
  1711. self.register_params(
  1712. layer,
  1713. "weight_scale",
  1714. (
  1715. shapes.output_size_per_partition,
  1716. shapes.input_size_per_partition // ctx.group_size,
  1717. ),
  1718. torch.float8_e4m3fn,
  1719. ModelWeightParameter,
  1720. wl,
  1721. input_dim=1,
  1722. output_dim=0,
  1723. init=float("nan"),
  1724. )
  1725. def process(self, layer, role) -> None:
  1726. if role is not WEIGHT:
  1727. self.reject(role)
  1728. # Sanity-check: weight_scale must have been overwritten by the weight
  1729. # loader. A value still equal to the NaN sentinel it was created with
  1730. # means the FP4 weights were never actually loaded (e.g. the
  1731. # checkpoint stores this layer as BF16 and the weight loader silently
  1732. # skipped it).
  1733. if torch.isnan(layer.weight_scale.float()).any():
  1734. raise RuntimeError(
  1735. f"NVFP4 weight_scale for layer "
  1736. f"{getattr(layer, 'name', repr(layer))!r} was never loaded "
  1737. "(still NaN). The checkpoint likely stores this layer as "
  1738. "BF16 (not FP4). Fix: pass quant_config=None when "
  1739. "constructing this layer, or add it to the quantization "
  1740. "ignore list."
  1741. )
  1742. if torch.unique(layer.weight_scale_2).numel() != 1:
  1743. logger.warning_once(
  1744. "In NVFP4 linear, the global weight scale differs across "
  1745. "parallel layers (e.g. q_proj, k_proj, v_proj). This will "
  1746. "likely reduce accuracy. Consider a checkpoint with the same "
  1747. "global NVFP4 scale for fused weights."
  1748. )
  1749. # Raw max, no reciprocation — Marlin/cutlass want ModelOpt's amax/2688.
  1750. weight_global_scale = layer.weight_scale_2.max().to(torch.float32)
  1751. layer.weight_global_scale = Parameter(weight_global_scale, requires_grad=False)
  1752. del layer.weight_scale_2
  1753. class KNvfp4Dynamic(QuantKeyScheme):
  1754. """NVFP4 activation scheme (W4A4). Has a static global input scale on disk;
  1755. the per-group scale is computed at runtime inside the kernel."""
  1756. key = kNvfp4Dynamic
  1757. def create_weights(self, layer, role, ctx, shapes, wl) -> None:
  1758. if role is not ACT:
  1759. self.reject(role)
  1760. self.register_params(
  1761. layer,
  1762. "input_scale",
  1763. (shapes.num_partitions,),
  1764. torch.float32,
  1765. PerTensorScaleParameter,
  1766. wl,
  1767. )
  1768. def process(self, layer, role) -> None:
  1769. if role is not ACT:
  1770. self.reject(role)
  1771. if torch.unique(layer.input_scale).numel() != 1:
  1772. logger.warning_once(
  1773. "In NVFP4 linear, the global input scale differs across "
  1774. "parallel layers (e.g. q_proj, k_proj, v_proj). This will "
  1775. "likely reduce accuracy. Consider a checkpoint with the same "
  1776. "global NVFP4 scale for fused weights."
  1777. )
  1778. input_global_scale = layer.input_scale.max().to(torch.float32)
  1779. layer.input_global_scale = Parameter(input_global_scale, requires_grad=False)
  1780. layer.input_global_scale_inv = Parameter(
  1781. (1.0 / layer.input_global_scale).to(torch.float32),
  1782. requires_grad=False,
  1783. )
  1784. del layer.input_scale
  1785. class KFp8StaticTensor(QuantKeyScheme):
  1786. """Plain per-tensor static FP8 — bivalent: serves BOTH the weight slot and
  1787. the activation slot (W8A8). One key in both QuantSpec slots."""
  1788. key = kFp8StaticTensorSym
  1789. def create_weights(self, layer, role, ctx, shapes, wl) -> None:
  1790. if role is WEIGHT:
  1791. self.register_params(
  1792. layer,
  1793. "weight",
  1794. (shapes.output_size_per_partition, shapes.input_size_per_partition),
  1795. torch.float8_e4m3fn,
  1796. ModelWeightParameter,
  1797. wl,
  1798. input_dim=1,
  1799. output_dim=0,
  1800. )
  1801. layer.orig_dtype = shapes.params_dtype
  1802. self.register_params(
  1803. layer,
  1804. "weight_scale",
  1805. (shapes.num_partitions,),
  1806. torch.float32,
  1807. PerTensorScaleParameter,
  1808. wl,
  1809. init=FP8_SCALE_SENTINEL,
  1810. )
  1811. elif role is ACT:
  1812. self.register_params(
  1813. layer,
  1814. "input_scale",
  1815. (shapes.num_partitions,),
  1816. torch.float32,
  1817. PerTensorScaleParameter,
  1818. wl,
  1819. init=FP8_SCALE_SENTINEL,
  1820. )
  1821. else:
  1822. self.reject(role)
  1823. def process(self, layer, role) -> None:
  1824. if role is WEIGHT:
  1825. weight = layer.weight
  1826. max_w_scale = layer.weight_scale.max()
  1827. if not (layer.weight_scale == layer.weight_scale[0]).all():
  1828. max_w_scale, weight = requantize_with_max_scale(
  1829. layer.weight, layer.weight_scale, layer.logical_widths
  1830. )
  1831. # Transposed here rather than in the kernel: ModelOpt maps each
  1832. # fp8 key to exactly one kernel, so the layout is key-determined.
  1833. layer.weight = Parameter(weight.t(), requires_grad=False)
  1834. # The plain Parameter drops ModelWeightParameter's dim attributes;
  1835. # restore them for the transposed layout, which Humming reads.
  1836. layer.weight.input_dim = 0
  1837. layer.weight.output_dim = 1
  1838. layer.weight_scale = Parameter(max_w_scale, requires_grad=False)
  1839. elif role is ACT:
  1840. if torch.unique(layer.input_scale).numel() != 1:
  1841. logger.warning_once(
  1842. "In FP8 linear, the static input scale differs across "
  1843. "parallel layers (e.g. q_proj, k_proj, v_proj). Collapsing "
  1844. "them to the max will likely reduce accuracy. Consider a "
  1845. "checkpoint with the same input scale for fused weights."
  1846. )
  1847. layer.input_scale = Parameter(layer.input_scale.max(), requires_grad=False)
  1848. else:
  1849. self.reject(role)
  1850. class KFp8StaticChannel(QuantKeyScheme):
  1851. """Per-channel static FP8 weight (the 'PcPt' weight). Weight-role only —
  1852. there is no static per-channel *activation* today."""
  1853. key = kFp8StaticTokenSym
  1854. def create_weights(self, layer, role, ctx, shapes, wl) -> None:
  1855. if role is not WEIGHT:
  1856. self.reject(role)
  1857. self.register_params(
  1858. layer,
  1859. "weight",
  1860. (shapes.output_size_per_partition, shapes.input_size_per_partition),
  1861. torch.float8_e4m3fn,
  1862. ModelWeightParameter,
  1863. wl,
  1864. input_dim=1,
  1865. output_dim=0,
  1866. )
  1867. self.register_params(
  1868. layer,
  1869. "weight_scale",
  1870. (shapes.output_size_per_partition,),
  1871. torch.float32,
  1872. ChannelQuantScaleParameter,
  1873. wl,
  1874. output_dim=0,
  1875. init=FP8_SCALE_SENTINEL,
  1876. )
  1877. def process(self, layer, role) -> None:
  1878. if role is not WEIGHT:
  1879. self.reject(role)
  1880. weight, weight_scale, _ = process_fp8_weight_channel_strategy(
  1881. layer.weight, layer.weight_scale.data
  1882. )
  1883. layer.weight = Parameter(weight, requires_grad=False)
  1884. layer.weight_scale = Parameter(weight_scale, requires_grad=False)
  1885. class KFp8Block128(QuantKeyScheme):
  1886. """128x128 block-static FP8 weight ('PbWo'). Weight-role only. ModelOpt
  1887. exports the scale 4-D [out_blk,1,in_blk,1]; process squeezes to 2-D.
  1888. No transpose (block kernel keeps [out,in])."""
  1889. key = kFp8Static128BlockSym
  1890. def create_weights(self, layer, role, ctx, shapes, wl) -> None:
  1891. if role is not WEIGHT:
  1892. self.reject(role)
  1893. out = shapes.output_size_per_partition
  1894. in_ = shapes.input_size_per_partition
  1895. # Scale sized by cdiv to match the checkpoint's block grid (the last
  1896. # output block may be partial). Weight is registered at its logical
  1897. # size; a non-128 output width is padded to a block boundary by the
  1898. # _Fp8PbWoPartialBlock format scheme. Input must be 128-aligned (same as
  1899. # #53132, which only pads the output).
  1900. if in_ % 128 != 0:
  1901. raise ValueError(f"FP8_PB_WO requires in divisible by 128, got {in_}")
  1902. self.register_params(
  1903. layer,
  1904. "weight",
  1905. (out, in_),
  1906. torch.float8_e4m3fn,
  1907. ModelWeightParameter,
  1908. wl,
  1909. input_dim=1,
  1910. output_dim=0,
  1911. )
  1912. ob, ib = cdiv(out, 128), in_ // 128
  1913. self.register_params(
  1914. layer,
  1915. "weight_scale",
  1916. (ob, 1, ib, 1),
  1917. torch.float32,
  1918. BlockQuantScaleParameter,
  1919. wl,
  1920. input_dim=2,
  1921. output_dim=0,
  1922. init=FP8_SCALE_SENTINEL,
  1923. )
  1924. layer.weight_block_size = [128, 128]
  1925. def process(self, layer, role) -> None:
  1926. if role is not WEIGHT:
  1927. self.reject(role)
  1928. layer.weight = Parameter(layer.weight.data, requires_grad=False)
  1929. s = layer.weight_scale
  1930. if s.dim() == 4:
  1931. s = s.squeeze(1).squeeze(-1) # [ob,1,ib,1] -> [ob,ib]
  1932. elif s.dim() != 2:
  1933. raise ValueError(
  1934. f"Unexpected FP8_PB_WO weight_scale shape {tuple(s.shape)}"
  1935. )
  1936. layer.weight_scale = Parameter(s.contiguous(), requires_grad=False)
  1937. class KMxfp8Static(QuantKeyScheme):
  1938. """MXFP8 weight: fp8-e4m3 values + per-32-block e8m0 (uint8) scale.
  1939. Weight-role only. process is validate-only plus an idempotency guard."""
  1940. key = kMxfp8Static
  1941. @staticmethod
  1942. def get_scale_weight_loader(weight_loader: Callable, ctx: CkptCtx) -> Callable:
  1943. block_rows, block_cols = ctx.scale_block_size or (1, MXFP8_BLOCK_SIZE)
  1944. if block_rows < 1 or block_cols != MXFP8_BLOCK_SIZE:
  1945. raise NotImplementedError(
  1946. f"MXFP8 checkpoint scale block {ctx.scale_block_size} is unsupported"
  1947. )
  1948. def scaled_loader(param, loaded_weight, *args, **kwargs):
  1949. assert loaded_weight.dtype in (torch.uint8, torch.float8_e8m0fnu)
  1950. loaded_weight = loaded_weight.view(torch.uint8).repeat_interleave(
  1951. block_rows, dim=0
  1952. )
  1953. return weight_loader(param, loaded_weight, *args, **kwargs)
  1954. return scaled_loader if block_rows > 1 else weight_loader
  1955. def create_weights(self, layer, role, ctx, shapes, wl) -> None:
  1956. if role is not WEIGHT:
  1957. self.reject(role)
  1958. if shapes.input_size_per_partition % MXFP8_BLOCK_SIZE != 0:
  1959. raise ValueError(
  1960. f"MXFP8 requires input size divisible by {MXFP8_BLOCK_SIZE}, "
  1961. f"got {shapes.input_size_per_partition}"
  1962. )
  1963. self.register_params(
  1964. layer,
  1965. "weight",
  1966. (shapes.output_size_per_partition, shapes.input_size_per_partition),
  1967. MXFP8_VALUE_DTYPE,
  1968. ModelWeightParameter,
  1969. wl,
  1970. input_dim=1,
  1971. output_dim=0,
  1972. )
  1973. scale_loader = self.get_scale_weight_loader(wl, ctx)
  1974. self.register_params(
  1975. layer,
  1976. "weight_scale",
  1977. (
  1978. shapes.output_size_per_partition,
  1979. shapes.input_size_per_partition // MXFP8_BLOCK_SIZE,
  1980. ),
  1981. MXFP8_SCALE_DTYPE,
  1982. ModelWeightParameter,
  1983. scale_loader,
  1984. input_dim=1,
  1985. output_dim=0,
  1986. )
  1987. layer.weight_block_size = [1, MXFP8_BLOCK_SIZE]
  1988. def process(self, layer, role) -> None:
  1989. if role is not WEIGHT:
  1990. self.reject(role)
  1991. # Idempotency: the emulation kernel's post-load may replace the 1-byte
  1992. # MXFP8 weight with BF16 (VLLM_MXFP8_EMULATION_DEQUANT_AT_LOAD, default
  1993. # on). On a weight reload process runs again after that swap, so skip
  1994. # the fp8 asserts when the weight is already >=2-byte.
  1995. if layer.weight.element_size() >= 2:
  1996. return
  1997. if getattr(layer, "is_bmm", False) and layer.weight.ndim == 3:
  1998. return
  1999. assert layer.weight.ndim == 2 and layer.weight.dtype == MXFP8_VALUE_DTYPE
  2000. assert layer.weight_scale.ndim == 2
  2001. assert layer.weight_scale.dtype == MXFP8_SCALE_DTYPE
  2002. class KDynamicNoParam(QuantKeyScheme):
  2003. """Dynamic activation with no stored scale (W8A8): quantized at runtime in
  2004. the kernel. NOT the same as activation=None (weight-only) — init_fp8 needs a
  2005. non-None activation key. Activation-role only. Serves the fp8 per-token, fp8
  2006. per-block, and mxfp8 dynamic activation keys."""
  2007. def create_weights(self, layer, role, ctx, shapes, wl) -> None:
  2008. if role is not ACT:
  2009. self.reject(role)
  2010. # dynamic -> nothing stored
  2011. def process(self, layer, role) -> None:
  2012. if role is not ACT:
  2013. self.reject(role)
  2014. SCHEME_FOR: dict[QuantKey | None, QuantKeyScheme] = {
  2015. kNvfp4Static: KNvfp4Static(),
  2016. kNvfp4Dynamic: KNvfp4Dynamic(),
  2017. kFp8StaticTensorSym: KFp8StaticTensor(),
  2018. kFp8StaticTokenSym: KFp8StaticChannel(),
  2019. kFp8Static128BlockSym: KFp8Block128(),
  2020. kMxfp8Static: KMxfp8Static(),
  2021. kFp8DynamicTokenSym: KDynamicNoParam(),
  2022. kFp8Dynamic128Sym: KDynamicNoParam(),
  2023. kMxfp8Dynamic: KDynamicNoParam(),
  2024. }
  2025. def maybe_fuse_global_scales(layer) -> None:
  2026. """Alpha = input_global_scale * weight_global_scale, presence-gated.
  2027. W4A4 has both -> computed; W4A16 has no input_global_scale -> skipped.
  2028. """
  2029. if hasattr(layer, "weight_global_scale") and hasattr(layer, "input_global_scale"):
  2030. layer.alpha = Parameter(
  2031. layer.input_global_scale * layer.weight_global_scale,
  2032. requires_grad=False,
  2033. )
  2034. def select_linear_kernel(
  2035. spec: QuantSpec,
  2036. layer,
  2037. rt: RuntimeDtypes,
  2038. weight_shape: tuple[int, int] | None = None,
  2039. ):
  2040. """Thin family dispatcher on the weight key: nvfp4 / mxfp8 / fp8."""
  2041. w = spec.weight
  2042. assert isinstance(w, QuantKey), f"resolve() must supply a weight key, got {w!r}"
  2043. if w.dtype == FP4_DTYPE:
  2044. # W4A16 (activation is None) → use_a16=True defaults to Marlin *and*
  2045. # honors --linear-backend (matches upstream ModelOptNvFp4W4A16LinearMethod
  2046. # after #50273); W4A4 → use_a16=False.
  2047. return init_nvfp4_linear_kernel(use_a16=spec.activation is None)
  2048. if w.scale.dtype == MXFP8_SCALE_DTYPE:
  2049. return init_mxfp8_linear_kernel(
  2050. weight_shape=weight_shape or layer.weight.shape,
  2051. bmm_batch_size=getattr(layer, "bmm_batch_size", None),
  2052. )
  2053. # fp8 family: init_fp8 routes block-vs-plain itself off the activation key,
  2054. # and needs a real key -- weight-only fp8 is not a ModelOpt format.
  2055. act = spec.activation
  2056. assert isinstance(act, QuantKey), f"fp8 needs an activation key, got {act!r}"
  2057. return init_fp8_linear_kernel(
  2058. activation_quant_key=act,
  2059. weight_quant_key=w,
  2060. input_dtype=rt.input_dtype,
  2061. out_dtype=rt.out_dtype,
  2062. weight_shape=weight_shape or layer.weight.shape,
  2063. module_name=type(layer).__name__,
  2064. )
  2065. class FormatScheme:
  2066. """Optional per-format hooks that compose *around* the QuantKey schemes.
  2067. Extension seam for residue that belongs to a format as a whole rather than a
  2068. single QuantKey — an extra parameter the weight/activation schemes don't
  2069. cover, or a tweak before/after their ``process``. All hooks default to
  2070. no-ops; most formats need none (they are a pure ``(weight, activation)`` key
  2071. pair). To add one: subclass, override the hooks you need, and return an
  2072. instance from the format's ``resolve()`` branch — no change to
  2073. ``ModelOptLinearMethod`` itself.
  2074. """
  2075. def extra_weights(self, layer, shapes: "Shapes", ctx: CkptCtx, wl) -> None:
  2076. """Register format-level params (after the key schemes' weights)."""
  2077. def kernel_weight_shape(self, layer) -> tuple[int, int]:
  2078. """Return the weight shape the selected kernel will receive."""
  2079. return tuple(layer.weight.shape)
  2080. def pre_process(self, layer) -> None:
  2081. """Run before the key schemes' ``process``."""
  2082. def post_process(self, layer) -> None:
  2083. """Run after the key schemes' ``process``, before the kernel's."""
  2084. def apply(self, layer, x, bias, kernel_apply):
  2085. """Wrap the kernel's forward. ``kernel_apply(layer, x, bias) -> Tensor``.
  2086. Default: delegate unchanged. A format whose residue lives at compute
  2087. time (e.g. run the GEMM on a padded weight then slice the output back
  2088. to its logical width, #53132-style) overrides this and calls
  2089. ``kernel_apply`` in the middle.
  2090. """
  2091. return kernel_apply(layer, x, bias)
  2092. class _Fp8PbWoPartialBlock(FormatScheme):
  2093. """FP8_PB_WO output width that is not a multiple of 128 (a partial trailing
  2094. block; e.g. GLM's replicated ``fused_qkv_a_proj`` = 2048 + 576 = 2624).
  2095. The block kernel needs full 128-blocks, so pad the weight up to the block
  2096. boundary with zeros before the kernel's post-load, then slice the GEMM
  2097. output back to the logical width. This is #53132's approach, expressed as
  2098. the format's compute-time residue on the generic method. The cdiv-sized
  2099. scale from ``KFp8Block128`` already matches the padded block count, so only
  2100. the weight rows and the output need adjusting.
  2101. """
  2102. def kernel_weight_shape(self, layer) -> tuple[int, int]:
  2103. out, in_ = layer.weight.shape
  2104. return (cdiv(out, 128) * 128, in_)
  2105. def post_process(self, layer) -> None:
  2106. w = layer.weight
  2107. out = w.shape[0]
  2108. pad = (-out) % 128
  2109. if not pad:
  2110. return
  2111. layer._pbwo_logical_out = out
  2112. padded = w.data.new_zeros(out + pad, w.shape[1])
  2113. padded[:out].copy_(w.data)
  2114. layer.weight = Parameter(padded, requires_grad=False)
  2115. def apply(self, layer, x, bias, kernel_apply):
  2116. logical = getattr(layer, "_pbwo_logical_out", None)
  2117. if logical is None: # width was block-aligned: nothing to trim
  2118. return kernel_apply(layer, x, bias)
  2119. # Run the GEMM on the padded weight without bias, then trim to the
  2120. # logical width and add bias (the padded rows produce zeros we drop).
  2121. out = kernel_apply(layer, x, None)
  2122. out = out[..., :logical].contiguous()
  2123. return out if bias is None else out.add_(bias)
  2124. _PB_WO_PARTIAL_BLOCK = _Fp8PbWoPartialBlock()
  2125. class _DropInputScale(FormatScheme):
  2126. """Interim: register then drop a W4A16 checkpoint's on-disk input_scale.
  2127. Kept for backward compat with previously-exported ModelOpt checkpoints.
  2128. """
  2129. def extra_weights(self, layer, shapes, ctx, wl) -> None:
  2130. data = torch.full((shapes.num_partitions,), torch.nan)
  2131. layer.register_parameter(
  2132. "input_scale", PerTensorScaleParameter(data=data, weight_loader=wl)
  2133. )
  2134. def post_process(self, layer) -> None:
  2135. scale = getattr(layer, "input_scale", None)
  2136. if scale is None:
  2137. return
  2138. if not torch.isnan(scale).all():
  2139. logger.warning_once(
  2140. "Dropping input_scale from a weight-only W4A16 NVFP4 "
  2141. "checkpoint (deprecated)."
  2142. )
  2143. del layer.input_scale
  2144. _DROP_INPUT_SCALE = _DropInputScale()
  2145. @register_weight_loader_v2_supported_method
  2146. class ModelOptLinearMethod(LinearMethodBase):
  2147. """Generic, format-agnostic ModelOpt linear method. Holds a weight scheme +
  2148. an activation scheme (from the QuantSpec pair) and an optional
  2149. ``FormatScheme``, runs a fixed lifecycle, selects the kernel from the pair,
  2150. and applies.
  2151. Registered for weight_loader_v2 (like every ModelOpt linear method) so the
  2152. ``BasevLLMParameter`` params route through the v2 fused loader, not the
  2153. legacy shape-assert path.
  2154. """
  2155. def __init__(
  2156. self,
  2157. spec: QuantSpec,
  2158. ctx: CkptCtx,
  2159. format_scheme=None,
  2160. ) -> None:
  2161. self.spec = spec
  2162. self.ctx = ctx
  2163. self.fmt = format_scheme or FormatScheme()
  2164. self.wkey = SCHEME_FOR[spec.weight]
  2165. self.akey = None if spec.activation is None else SCHEME_FOR[spec.activation]
  2166. # Only the fp8/mxfp8 kernels read input_dtype; nvfp4 ignores it. During
  2167. # real serving model_config is always set; fall back defensively when
  2168. # there is no config context (bare unit-test dispatch).
  2169. model_config = getattr(get_current_vllm_config_or_none(), "model_config", None)
  2170. dtype = (
  2171. model_config.dtype
  2172. if model_config is not None
  2173. else torch.get_default_dtype()
  2174. )
  2175. self.out_dtype = dtype
  2176. self.input_dtype = dtype
  2177. self.marlin_input_dtype = None
  2178. # Kernel/backend are chosen in create_weights (after get_quant_method),
  2179. # so the front-end marlin poke stays dormant here — same as the old
  2180. # NVFP4 methods.
  2181. self.kernel: Any = None
  2182. @property
  2183. def supports_pre_processed_weights(self) -> bool: # type: ignore[override]
  2184. # TODO(Isotr0py): support fp8 ModelOpt kernels transpose/repack.
  2185. w = self.spec.weight
  2186. return isinstance(w, QuantKey) and (
  2187. w.dtype == FP4_DTYPE
  2188. or (
  2189. w == kMxfp8Static
  2190. and self.kernel is not None
  2191. and self.kernel.supports_pre_processed_weights
  2192. )
  2193. )
  2194. def create_weights(
  2195. self,
  2196. layer,
  2197. input_size_per_partition: int,
  2198. output_partition_sizes: list[int],
  2199. input_size: int,
  2200. output_size: int,
  2201. params_dtype: torch.dtype,
  2202. **extra_weight_attrs,
  2203. ) -> None:
  2204. del input_size, output_size
  2205. weight_loader = extra_weight_attrs.get("weight_loader")
  2206. layer.logical_widths = output_partition_sizes
  2207. layer.input_size_per_partition = input_size_per_partition
  2208. layer.output_size_per_partition = sum(output_partition_sizes)
  2209. shapes = Shapes(output_partition_sizes, input_size_per_partition, params_dtype)
  2210. self.wkey.create_weights(layer, WEIGHT, self.ctx, shapes, weight_loader)
  2211. if self.akey:
  2212. self.akey.create_weights(layer, ACT, self.ctx, shapes, weight_loader)
  2213. self.fmt.extra_weights(layer, shapes, self.ctx, weight_loader)
  2214. rt = RuntimeDtypes(self.input_dtype, self.out_dtype, self.marlin_input_dtype)
  2215. self.kernel = select_linear_kernel(
  2216. self.spec,
  2217. layer,
  2218. rt,
  2219. weight_shape=self.fmt.kernel_weight_shape(layer),
  2220. )
  2221. expose_input_quant_key(layer, self.kernel)
  2222. def process_weights_after_loading(self, layer) -> None:
  2223. if self.spec.weight == kMxfp8Static and getattr(layer, "is_bmm", False):
  2224. self.kernel = init_mxfp8_linear_kernel(
  2225. weight_shape=(layer.weight.shape[-2], layer.weight.shape[-1]),
  2226. bmm_batch_size=layer.bmm_batch_size,
  2227. )
  2228. if is_weights_pre_processed():
  2229. if not self.supports_pre_processed_weights:
  2230. raise RuntimeError(
  2231. f"{type(self.kernel).__name__} cannot use pre-processed weights"
  2232. )
  2233. return
  2234. self.fmt.pre_process(layer)
  2235. self.wkey.process(layer, WEIGHT)
  2236. if self.akey:
  2237. self.akey.process(layer, ACT)
  2238. maybe_fuse_global_scales(layer)
  2239. self.fmt.post_process(layer)
  2240. layer.is_w4a16_nvfp4 = (
  2241. self.spec.weight == kNvfp4Static and self.spec.activation is None
  2242. )
  2243. if getattr(layer, "_retain_weight_for_gather", False):
  2244. if not layer.is_w4a16_nvfp4:
  2245. raise NotImplementedError(
  2246. "Gathered projection is only supported for ModelOpt W4A16 "
  2247. "NVFP4 weights."
  2248. )
  2249. assert self.ctx.group_size is not None
  2250. layer.register_buffer(
  2251. "_nvfp4_weight_for_gather", layer.weight.detach(), persistent=False
  2252. )
  2253. layer.register_buffer(
  2254. "_nvfp4_weight_scale_for_gather",
  2255. layer.weight_scale.detach(),
  2256. persistent=False,
  2257. )
  2258. layer.register_buffer(
  2259. "_nvfp4_weight_global_scale_for_gather",
  2260. layer.weight_global_scale.detach(),
  2261. persistent=False,
  2262. )
  2263. layer._nvfp4_group_size_for_gather = self.ctx.group_size
  2264. self.kernel.process_weights_after_loading(layer)
  2265. def apply(self, layer, x, bias=None):
  2266. return self.fmt.apply(
  2267. layer,
  2268. x,
  2269. bias,
  2270. lambda lyr, inp, b: self.kernel.apply_weights(layer=lyr, x=inp, bias=b),
  2271. )
  2272. def resolve(algo: str, subcfg, prefix: str):
  2273. """Turn a sub-config into (QuantSpec, CkptCtx, format_scheme).
  2274. Strictly read-only over ``subcfg``: in mixed precision the same sub-config
  2275. is shared with the MoE method, so writing back would leak a linear-only
  2276. change into MoE.
  2277. """
  2278. if algo == "FP8":
  2279. # Plain per-tensor static FP8 (W8A8): same key in both slots (bivalent).
  2280. ctx = CkptCtx()
  2281. return (
  2282. QuantSpec(weight=kFp8StaticTensorSym, activation=kFp8StaticTensorSym),
  2283. ctx,
  2284. None,
  2285. )
  2286. if algo == "FP8_PER_CHANNEL_PER_TOKEN":
  2287. # PcPt: per-channel static weight, dynamic per-token activation (W8A8).
  2288. ctx = CkptCtx()
  2289. return (
  2290. QuantSpec(weight=kFp8StaticTokenSym, activation=kFp8DynamicTokenSym),
  2291. ctx,
  2292. None,
  2293. )
  2294. if algo == "FP8_PB_WO":
  2295. # PbWo: 128x128 block-static weight, dynamic per-block activation (W8A8).
  2296. # The block kernel's post-load runs here; CompressedTensors block-FP8
  2297. # is the reference for this path. The FormatScheme pads a non-128 output
  2298. # width to a block boundary and trims the output back (#53132) -- a no-op
  2299. # for the common block-aligned case.
  2300. ctx = CkptCtx()
  2301. return (
  2302. QuantSpec(weight=kFp8Static128BlockSym, activation=kFp8Dynamic128Sym),
  2303. ctx,
  2304. _PB_WO_PARTIAL_BLOCK,
  2305. )
  2306. if algo == "MXFP8":
  2307. # MXFP8: block(32) e4m3 weight + e8m0 scale, dynamic activation.
  2308. ctx = CkptCtx()
  2309. return QuantSpec(weight=kMxfp8Static, activation=kMxfp8Dynamic), ctx, None
  2310. # NVFP4 family (W4A4 / W4A16).
  2311. ctx = CkptCtx(group_size=subcfg.group_size)
  2312. if algo == "NVFP4":
  2313. # W4A4: static fp4 weight + dynamic fp4 activation (has a static global
  2314. # input scale). alpha = weight_gs * input_gs.
  2315. return QuantSpec(weight=kNvfp4Static, activation=kNvfp4Dynamic), ctx, None
  2316. if algo == "W4A16_NVFP4":
  2317. # W4A16: fp4 weight, no activation quant (-> Marlin). _DROP_INPUT_SCALE
  2318. # is a no-op for genuine exports; keeps legacy input_scale ckpts loading.
  2319. return QuantSpec(weight=kNvfp4Static, activation=None), ctx, _DROP_INPUT_SCALE
  2320. raise NotImplementedError(f"resolve: unsupported ModelOpt linear algo {algo!r}")
  2321. # Bespoke-method escape hatch. Almost every ModelOpt linear format is a
  2322. # (weight, activation) QuantKey pair handled by the generic ModelOptLinearMethod
  2323. # (optionally + a FormatScheme), so this stays empty. A format that genuinely
  2324. # cannot be expressed that way — a fundamentally different create/process/apply
  2325. # lifecycle — registers its own LinearMethodBase here, keyed by algo:
  2326. #
  2327. # LINEAR_METHOD_BUILDERS["FOO"] = lambda cfg, prefix: ModelOptFooLinearMethod(cfg)
  2328. #
  2329. # Keep the ModelOpt* class-name prefix and decorate the class with
  2330. # @register_weight_loader_v2_supported_method if it uses BasevLLMParameter
  2331. # params. Also add the algo to the owning config's quant_method validation.
  2332. LINEAR_METHOD_BUILDERS: dict[str, Callable[..., LinearMethodBase]] = {}
  2333. def build_linear_method(config, algo: str, prefix: str) -> LinearMethodBase:
  2334. """Construct the linear method for ``algo``.
  2335. Returns a bespoke method if one is registered in ``LINEAR_METHOD_BUILDERS``,
  2336. else the generic ``ModelOptLinearMethod`` built from ``resolve(algo, config,
  2337. prefix)``. Single indirection point shared by the homogeneous and
  2338. mixed-precision dispatch, so a new format plugs in without editing either
  2339. ``get_quant_method``.
  2340. """
  2341. builder = LINEAR_METHOD_BUILDERS.get(algo)
  2342. if builder is not None:
  2343. return builder(config, prefix)
  2344. spec, ctx, format_scheme = resolve(algo, config, prefix)
  2345. return ModelOptLinearMethod(spec, ctx, format_scheme)