prepare.py 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557
  1. """Local NVFP4/BF16 weight preparation for the CuTeDSL W4A16 path."""
  2. from __future__ import annotations
  3. import math
  4. from dataclasses import dataclass
  5. import torch
  6. from b12x.moe.fused.w4a16.host import (
  7. W4A16PackedBuffers,
  8. make_w4a16_packed_buffers as _make_w4a16_packed_buffers,
  9. unswizzle_expert_scales,
  10. validate_nf3_moe_inputs,
  11. validate_w4a16_packed_inputs,
  12. )
  13. _PACKED_TILE_SIZE = 16
  14. _PACKED_TILE_N_SIZE = 64
  15. _PACK_FACTOR_4BIT = 8
  16. _MODEL_OPT_W13_LAYOUTS = {"w13", "w31"}
  17. _SOURCE_FORMATS = {
  18. "modelopt_nvfp4": "modelopt_nvfp4",
  19. "fp4_e8m0_k32": "fp4_e8m0_k32",
  20. "compressed_tensors": "compressed_tensors",
  21. }
  22. _E8M0_K32_BF16_MAX_SCALE_BYTE = 247
  23. _E8M0_LOGICAL_TAIL_SCALE_N_ALIGNMENT = 64
  24. # Canonical W13 layout names are "w13"/"w31"; accept the physical FC1-half
  25. # spellings as aliases. Logical checkpoint order "w13" arrives up/gate and
  26. # needs a swap before the kernel's SwiGLU; "w31" is already kernel-native
  27. # gate/up order.
  28. _W13_LAYOUTS = {
  29. "w13": "w13",
  30. "w31": "w31",
  31. "up_gate": "w13",
  32. "gate_up": "w31",
  33. }
  34. _MODEL_OPT_NVFP4_FORMATS = {"modelopt_nvfp4"}
  35. # NF3 ("nf3_2p1") 3-bit codebook. Must match kernel._NF3_CODEBOOK. The scale
  36. # convention (see NF3_KERNEL_MODSPEC.md): weights decode to the FULL-precision
  37. # bf16 codebook value, the K/32 scale byte encodes t_s * 2**-4 (e4m3-style,
  38. # E4=0 arm), and the per-tensor global_scale carries the matching 2**116.
  39. _NF3_CODEBOOK = (-1.0, -0.6047, -0.3563, -0.1275, 0.1275, 0.3563, 0.6047, 1.0)
  40. _NF3_SCALE_GLOBAL = 2.0**116
  41. _NF3_SCALE_FLOOR = 2.0 ** -10 # f16(t_s*2^-4) must stay f16-NORMAL (>=2^-14) -> t_s >= 2^-10; real early-layer scales dip below the old 2^-7
  42. @dataclass(frozen=True)
  43. class W4A16PackedWeights:
  44. w13: torch.Tensor
  45. w13_scale: torch.Tensor
  46. w13_global_scale: torch.Tensor
  47. w2: torch.Tensor
  48. w2_scale: torch.Tensor
  49. w2_global_scale: torch.Tensor
  50. workspace: torch.Tensor
  51. hidden_size: int
  52. intermediate_size: int
  53. num_experts: int
  54. is_gated: bool
  55. params_dtype: torch.dtype
  56. source_format: str = "modelopt_nvfp4"
  57. w13_layout: str = "w13"
  58. weight_layout: str = "packed"
  59. scale_format: str = "e4m3_k16"
  60. @dataclass(frozen=True)
  61. class W4A16ModelOptWeights:
  62. w13: torch.Tensor
  63. w13_scale: torch.Tensor
  64. w13_global_scale: torch.Tensor
  65. w2: torch.Tensor
  66. w2_scale: torch.Tensor
  67. w2_global_scale: torch.Tensor
  68. workspace: torch.Tensor
  69. hidden_size: int
  70. intermediate_size: int
  71. num_experts: int
  72. is_gated: bool
  73. params_dtype: torch.dtype
  74. source_format: str = "modelopt_nvfp4"
  75. weight_layout: str = "modelopt"
  76. scale_format: str = "e4m3_k16"
  77. micro_w13_scale: torch.Tensor | None = None
  78. micro_w13_global_scale: torch.Tensor | None = None
  79. micro_w2_scale: torch.Tensor | None = None
  80. micro_w2_global_scale: torch.Tensor | None = None
  81. # Physical order of the two fused FC1 halves in source W13. "w13" (logical,
  82. # == "up_gate" physical) needs a row rotation before W4A16 SwiGLU; "w31"
  83. # (== "gate_up") is already in the kernel-native order.
  84. w13_layout: str = "w13"
  85. @dataclass(frozen=True)
  86. class PreparedNF3MoeWeights:
  87. """Runtime NF3 ("nf3_2p1") hybrid-expert W4A16 weights.
  88. Mirrors W4A16PackedWeights so run_w4a16_moe consumes it identically. The
  89. packed planes are int32 (3 words per 32-code unit); the K/32 scales are
  90. e4m3-style bytes viewed uint8; the global scales carry the 2**116 the NF3
  91. scale convention defers out of the codebook. fc1_tile_n / fc2_tile_n record
  92. the CTA N-tile the flat-span packing was built for -- the kernel MUST be
  93. compiled/launched with the SAME tile_n (read them back from the compiled
  94. W4A16FusedMoeCompileResult).
  95. """
  96. w13: torch.Tensor
  97. w13_scale: torch.Tensor
  98. w13_global_scale: torch.Tensor
  99. w2: torch.Tensor
  100. w2_scale: torch.Tensor
  101. w2_global_scale: torch.Tensor
  102. workspace: torch.Tensor
  103. hidden_size: int
  104. intermediate_size: int
  105. num_experts: int
  106. is_gated: bool
  107. params_dtype: torch.dtype
  108. fc1_tile_n: int
  109. fc2_tile_n: int
  110. source_format: str = "nf3_2p1"
  111. w13_layout: str = "packed"
  112. weight_layout: str = "nf3_2p1"
  113. scale_format: str = "e4m3_k32"
  114. def _make_workspace(
  115. device: torch.device, *, max_blocks_per_sm: int = 4
  116. ) -> torch.Tensor:
  117. props = torch.cuda.get_device_properties(device)
  118. sms = int(props.multi_processor_count)
  119. return torch.zeros(
  120. (sms * int(max_blocks_per_sm) + 2,),
  121. dtype=torch.int32,
  122. device=device,
  123. )
  124. def _scale_perms() -> tuple[list[int], list[int]]:
  125. scale_perm: list[int] = []
  126. for i in range(8):
  127. scale_perm.extend([i + 8 * j for j in range(8)])
  128. scale_perm_single: list[int] = []
  129. for i in range(4):
  130. scale_perm_single.extend([2 * i + j for j in [0, 1, 8, 9, 16, 17, 24, 25]])
  131. return scale_perm, scale_perm_single
  132. def _e8m0_logical_tail_scale_n(size_n: int) -> int:
  133. return (
  134. (int(size_n) + _E8M0_LOGICAL_TAIL_SCALE_N_ALIGNMENT - 1)
  135. // _E8M0_LOGICAL_TAIL_SCALE_N_ALIGNMENT
  136. ) * _E8M0_LOGICAL_TAIL_SCALE_N_ALIGNMENT
  137. def _permute_packed_scales(
  138. scales: torch.Tensor,
  139. *,
  140. size_k: int,
  141. size_n: int,
  142. group_size: int,
  143. output_size_n: int | None = None,
  144. ) -> torch.Tensor:
  145. scale_perm, scale_perm_single = _scale_perms()
  146. if group_size < size_k and group_size != -1:
  147. block = len(scale_perm)
  148. if output_size_n is not None or int(size_n) % block != 0:
  149. padded_n = (
  150. ((int(size_n) + block - 1) // block) * block
  151. if output_size_n is None
  152. else int(output_size_n)
  153. )
  154. if padded_n < int(size_n) or padded_n % block != 0:
  155. raise ValueError(
  156. f"output_size_n must be a multiple of {block} and >= size_n"
  157. )
  158. padded = scales.new_zeros((int(scales.shape[0]), padded_n))
  159. padded[:, : int(size_n)] = scales
  160. scales = padded.reshape((int(scales.shape[0]), -1, block))
  161. scales = scales[:, :, scale_perm].reshape((int(scales.shape[0]), padded_n))
  162. if output_size_n is None:
  163. scales = scales[:, : int(size_n)]
  164. return scales.contiguous()
  165. scales = scales.reshape((-1, block))[:, scale_perm]
  166. else:
  167. block = len(scale_perm_single)
  168. if output_size_n is not None or int(size_n) % block != 0:
  169. padded_n = (
  170. ((int(size_n) + block - 1) // block) * block
  171. if output_size_n is None
  172. else int(output_size_n)
  173. )
  174. if padded_n < int(size_n) or padded_n % block != 0:
  175. raise ValueError(
  176. f"output_size_n must be a multiple of {block} and >= size_n"
  177. )
  178. padded = scales.new_zeros((int(scales.shape[0]), padded_n))
  179. padded[:, : int(size_n)] = scales
  180. scales = padded.reshape((int(scales.shape[0]), -1, block))
  181. scales = scales[:, :, scale_perm_single].reshape(
  182. (int(scales.shape[0]), padded_n)
  183. )
  184. if output_size_n is None:
  185. scales = scales[:, : int(size_n)]
  186. return scales.contiguous()
  187. scales = scales.reshape((-1, block))[:, scale_perm_single]
  188. return scales.reshape((-1, size_n)).contiguous()
  189. def _nvfp4_compute_scale_factor(
  190. packed_scales: torch.Tensor,
  191. a_dtype: torch.dtype,
  192. ) -> float:
  193. if a_dtype == torch.float16:
  194. return 1.0
  195. max_scalar = 0.0
  196. for expert in range(int(packed_scales.shape[0])):
  197. ws_float = packed_scales[expert].float() * (2**7)
  198. nonzero_mask = ws_float > 0
  199. if bool(nonzero_mask.any().item()):
  200. max_scalar = max(max_scalar, float(ws_float[nonzero_mask].max().item()))
  201. if max_scalar > 0.0 and max_scalar < 448 * (2**7):
  202. return float(2 ** math.floor(math.log2((448 * (2**7)) / max_scalar)))
  203. return 1.0
  204. def _process_nvfp4_packed_scales(
  205. packed_scales: torch.Tensor,
  206. *,
  207. scale_factor: float,
  208. ) -> torch.Tensor:
  209. packed_scales = packed_scales.to(torch.float16)
  210. packed_scales = packed_scales.view(-1, 4)[:, [0, 2, 1, 3]].view(
  211. packed_scales.size(0),
  212. -1,
  213. )
  214. if scale_factor > 1.0:
  215. packed_scales = (packed_scales.float() * scale_factor).to(torch.float16)
  216. packed_scales = packed_scales * (2**7)
  217. packed_scales[packed_scales < 2] = 0
  218. packed_scales = packed_scales.view(torch.int16) << 1
  219. packed_scales = packed_scales.view(torch.float8_e4m3fn)
  220. return packed_scales[:, 1::2].contiguous()
  221. def _process_nvfp4_packed_global_scale(
  222. global_scale: torch.Tensor,
  223. *,
  224. a_dtype: torch.dtype,
  225. ) -> torch.Tensor:
  226. if a_dtype == torch.float16:
  227. target_exponent = 5
  228. elif a_dtype == torch.bfloat16:
  229. target_exponent = 8
  230. else:
  231. raise TypeError(f"unsupported W4A16 activation dtype {a_dtype}")
  232. fp4_exponent = 2
  233. exponent_bias = 2 ** (target_exponent - 1) - 2 ** (fp4_exponent - 1)
  234. return global_scale * (2.0 ** (exponent_bias - 7))
  235. def _process_nvfp4_micro_global_scale_from_packed(
  236. packed_global_scale: torch.Tensor,
  237. *,
  238. a_dtype: torch.dtype,
  239. ) -> torch.Tensor:
  240. if a_dtype == torch.float16:
  241. target_exponent = 5
  242. elif a_dtype == torch.bfloat16:
  243. target_exponent = 8
  244. else:
  245. raise TypeError(f"unsupported W4A16 activation dtype {a_dtype}")
  246. fp4_exponent = 2
  247. exponent_bias = 2 ** (target_exponent - 1) - 2 ** (fp4_exponent - 1)
  248. return (packed_global_scale * (2.0 ** (-exponent_bias))).to(torch.float32).contiguous()
  249. def _normalize_source_format(source_format: str) -> str:
  250. if source_format.lower() == "mxfp4_native":
  251. raise ValueError(
  252. "source_format='mxfp4_native' has been removed; use "
  253. "source_format='fp4_e8m0_k32' for E8M0 K/32 scales, or add "
  254. "a real MXFP4 source contract"
  255. )
  256. try:
  257. return _SOURCE_FORMATS[source_format.lower()]
  258. except KeyError as exc:
  259. raise ValueError(
  260. "source_format must be one of 'modelopt_nvfp4', "
  261. "'fp4_e8m0_k32', or 'compressed_tensors', "
  262. f"got {source_format!r}"
  263. ) from exc
  264. def _normalize_w13_layout(w13_layout: str) -> str:
  265. try:
  266. return _W13_LAYOUTS[w13_layout.lower()]
  267. except KeyError as exc:
  268. raise ValueError(
  269. "w13_layout must be one of 'w13'/'w31' (or the 'up_gate'/'gate_up' "
  270. f"aliases), got {w13_layout!r}"
  271. ) from exc
  272. def _source_global_scale(
  273. global_scale: torch.Tensor, *, source_format: str
  274. ) -> torch.Tensor:
  275. if source_format == "compressed_tensors":
  276. return (1.0 / global_scale).to(torch.float32).contiguous()
  277. return global_scale.contiguous()
  278. def _validate_e8m0_k32_scales(
  279. scales: torch.Tensor,
  280. *,
  281. rows: int,
  282. cols: int,
  283. name: str,
  284. allow_k_tail: bool = False,
  285. ) -> torch.Tensor:
  286. """Validate source E8M0 K/32 scale tensor shape and dtype."""
  287. if scales.ndim != 3:
  288. raise ValueError(
  289. f"{name} must be [E, N, ceil(K/32)], got {tuple(scales.shape)}"
  290. )
  291. if allow_k_tail:
  292. if int(cols) % 8 != 0:
  293. raise ValueError(
  294. f"{name} compact E8M0 K-tail requires K divisible by 8, got "
  295. f"{int(cols)}"
  296. )
  297. expected_cols = (int(cols) + 31) // 32
  298. elif int(cols) % 32 != 0:
  299. raise ValueError(f"{name} requires K divisible by 32, got {int(cols)}")
  300. else:
  301. expected_cols = int(cols) // 32
  302. if tuple(scales.shape[1:]) != (int(rows), expected_cols):
  303. raise ValueError(
  304. f"{name} must have shape [E, {int(rows)}, {expected_cols}] for "
  305. f"E8M0 K/32 scales, got {tuple(scales.shape)}"
  306. )
  307. e8m0_dtype = getattr(torch, "float8_e8m0fnu", None)
  308. if scales.dtype == torch.uint8:
  309. return scales.view(e8m0_dtype) if e8m0_dtype is not None else scales
  310. if e8m0_dtype is not None and scales.dtype == e8m0_dtype:
  311. return scales
  312. raise TypeError(f"{name} must be torch.uint8 or torch.float8_e8m0fnu")
  313. def _pack_e8m0_k32_scales(
  314. scales: torch.Tensor,
  315. *,
  316. size_k: int,
  317. size_n: int,
  318. row_rotation: int | None = None,
  319. reuse_input_storage: bool = False,
  320. allow_k_tail: bool = False,
  321. padded_size_n: int | None = None,
  322. ) -> torch.Tensor:
  323. if allow_k_tail:
  324. if int(size_k) % 8 != 0:
  325. raise ValueError(
  326. f"compact E8M0 K-tail requires K divisible by 8, got {size_k}"
  327. )
  328. scale_cols = (int(size_k) + 31) // 32
  329. elif int(size_k) % 32 != 0:
  330. raise ValueError(f"E8M0 K/32 scales require K divisible by 32, got {size_k}")
  331. else:
  332. scale_cols = int(size_k) // 32
  333. if tuple(scales.shape[1:]) != (int(size_n), scale_cols):
  334. raise ValueError(
  335. f"expected E8M0 scale shape [E, {int(size_n)}, {scale_cols}], "
  336. f"got {tuple(scales.shape)}"
  337. )
  338. output_size_n = int(size_n) if padded_size_n is None else int(padded_size_n)
  339. if output_size_n < int(size_n):
  340. raise ValueError(
  341. f"padded_size_n must be >= size_n, got {output_size_n} < {int(size_n)}"
  342. )
  343. source = scales.view(torch.uint8)
  344. if reuse_input_storage:
  345. if output_size_n != int(size_n):
  346. raise ValueError("reuse_input_storage requires unpadded E8M0 scales")
  347. if allow_k_tail:
  348. raise ValueError(
  349. "reuse_input_storage is not supported for compact E8M0 K-tail scales"
  350. )
  351. if not source.is_contiguous():
  352. raise ValueError("reuse_input_storage requires contiguous E8M0 scales")
  353. source.clamp_(max=_E8M0_K32_BF16_MAX_SCALE_BYTE)
  354. packed = source.reshape(
  355. int(source.shape[0]),
  356. scale_cols,
  357. output_size_n,
  358. )
  359. else:
  360. source = source.clamp(max=_E8M0_K32_BF16_MAX_SCALE_BYTE)
  361. packed = torch.empty(
  362. (int(source.shape[0]), scale_cols, output_size_n),
  363. dtype=torch.uint8,
  364. device=scales.device,
  365. )
  366. for expert in range(int(source.shape[0])):
  367. expert_source = source[expert]
  368. if row_rotation is not None:
  369. expert_source = torch.cat(
  370. [expert_source[row_rotation:], expert_source[:row_rotation]],
  371. dim=0,
  372. )
  373. expert_packed = _permute_packed_scales(
  374. expert_source.T.contiguous(),
  375. size_k=size_k,
  376. size_n=size_n,
  377. group_size=32,
  378. output_size_n=output_size_n,
  379. )
  380. expert_packed = (
  381. expert_packed.view(-1, 4)[:, [0, 2, 1, 3]]
  382. .reshape_as(expert_packed)
  383. .contiguous()
  384. )
  385. packed[expert].copy_(expert_packed)
  386. return packed.view(scales.dtype) if scales.dtype != torch.uint8 else packed
  387. def _repack_4bit_no_perm(
  388. qweight_i32: torch.Tensor,
  389. *,
  390. size_k: int,
  391. size_n: int,
  392. out: torch.Tensor | None = None,
  393. flat_scratch: torch.Tensor | None = None,
  394. gather_scratch: torch.Tensor | None = None,
  395. ) -> torch.Tensor:
  396. """Pack 4-bit weights into the W4A16 A16 kernel layout."""
  397. if qweight_i32.dtype != torch.int32:
  398. raise TypeError("qweight_i32 must be torch.int32")
  399. if tuple(qweight_i32.shape) != (size_k // _PACK_FACTOR_4BIT, size_n):
  400. raise ValueError(
  401. f"expected qweight shape {(size_k // _PACK_FACTOR_4BIT, size_n)}, "
  402. f"got {tuple(qweight_i32.shape)}"
  403. )
  404. if size_k % _PACKED_TILE_SIZE != 0 or size_n % _PACKED_TILE_N_SIZE != 0:
  405. raise ValueError(
  406. f"W4A16 repack requires K,N multiples of 16,64; got {size_k},{size_n}"
  407. )
  408. k_tiles = size_k // _PACKED_TILE_SIZE
  409. n_tiles = size_n // _PACKED_TILE_N_SIZE
  410. packed_shape = (k_tiles, n_tiles, 128)
  411. if out is not None and (
  412. out.dtype != torch.int32 or tuple(out.shape) != packed_shape
  413. ):
  414. raise ValueError(
  415. f"out must be int32 with shape {packed_shape}, got "
  416. f"{out.dtype} {tuple(out.shape)}"
  417. )
  418. if flat_scratch is not None and (
  419. flat_scratch.dtype != torch.int32
  420. or tuple(flat_scratch.shape) != packed_shape
  421. ):
  422. raise ValueError(
  423. f"flat_scratch must be int32 with shape {packed_shape}, got "
  424. f"{flat_scratch.dtype} {tuple(flat_scratch.shape)}"
  425. )
  426. if gather_scratch is not None and (
  427. gather_scratch.dtype != torch.int32
  428. or tuple(gather_scratch.shape) != packed_shape
  429. ):
  430. raise ValueError(
  431. f"gather_scratch must be int32 with shape {packed_shape}, got "
  432. f"{gather_scratch.dtype} {tuple(gather_scratch.shape)}"
  433. )
  434. tiles = qweight_i32.view(
  435. k_tiles,
  436. 2,
  437. n_tiles,
  438. _PACKED_TILE_N_SIZE,
  439. )
  440. if flat_scratch is None:
  441. flat = tiles.permute(0, 2, 1, 3).reshape(
  442. k_tiles,
  443. n_tiles,
  444. 2 * _PACKED_TILE_N_SIZE,
  445. )
  446. else:
  447. flat_scratch.view(k_tiles, n_tiles, 2, _PACKED_TILE_N_SIZE).copy_(
  448. tiles.permute(0, 2, 1, 3)
  449. )
  450. flat = flat_scratch
  451. device = qweight_i32.device
  452. out_pos = torch.arange(128, device=device, dtype=torch.long)
  453. th_id = out_pos // 4
  454. warp_id = out_pos % 4
  455. tc_col = th_id // 4
  456. tc_row = (th_id % 4) * 2
  457. offsets = torch.tensor([0, 1, 8, 9], device=device, dtype=torch.long)
  458. pack_idx = torch.tensor([0, 2, 4, 6, 1, 3, 5, 7], device=device, dtype=torch.long)
  459. elem = tc_row[:, None] + offsets[None, :]
  460. row = elem // _PACK_FACTOR_4BIT
  461. pos = elem % _PACK_FACTOR_4BIT
  462. col1 = (warp_id * 16 + tc_col)[:, None].expand(-1, 4)
  463. col2 = col1 + 8
  464. source_index = torch.cat(
  465. [row * _PACKED_TILE_N_SIZE + col1, row * _PACKED_TILE_N_SIZE + col2],
  466. dim=1,
  467. )[:, pack_idx]
  468. source_shift = torch.cat([pos, pos], dim=1)[:, pack_idx] * 4
  469. result = (
  470. torch.empty(packed_shape, device=device, dtype=torch.int32)
  471. if out is None
  472. else out
  473. )
  474. result.zero_()
  475. for slot in range(8):
  476. gather_index = source_index[:, slot].view(1, 1, 128).expand(
  477. k_tiles,
  478. n_tiles,
  479. 128,
  480. )
  481. shift = source_shift[:, slot].view(1, 1, 128)
  482. if gather_scratch is None:
  483. gathered = flat.gather(2, gather_index)
  484. nibble = (gathered >> shift) & 0xF
  485. result |= nibble << (slot * 4)
  486. else:
  487. torch.gather(flat, 2, gather_index, out=gather_scratch)
  488. torch.bitwise_right_shift(gather_scratch, shift, out=gather_scratch)
  489. torch.bitwise_and(gather_scratch, 0xF, out=gather_scratch)
  490. if slot:
  491. torch.bitwise_left_shift(
  492. gather_scratch,
  493. slot * 4,
  494. out=gather_scratch,
  495. )
  496. torch.bitwise_or(result, gather_scratch, out=result)
  497. return result.reshape(k_tiles, n_tiles * 128).contiguous()
  498. def _repack_weight(
  499. weight: torch.Tensor,
  500. *,
  501. size_k: int,
  502. size_n: int,
  503. row_rotation: int | None = None,
  504. reuse_input_storage: bool = False,
  505. ) -> torch.Tensor:
  506. num_experts = int(weight.shape[0])
  507. if tuple(weight.shape[1:]) != (size_n, size_k // 2):
  508. raise ValueError(
  509. f"expected packed weight shape {(num_experts, size_n, size_k // 2)}, "
  510. f"got {tuple(weight.shape)}"
  511. )
  512. if size_k % _PACKED_TILE_SIZE != 0 or size_n % _PACKED_TILE_N_SIZE != 0:
  513. raise ValueError(
  514. f"W4A16 repack requires K,N multiples of 16,64; got {size_k},{size_n}"
  515. )
  516. packed_shape = (
  517. num_experts,
  518. size_k // _PACKED_TILE_SIZE,
  519. (size_n // _PACKED_TILE_N_SIZE) * 128,
  520. )
  521. if reuse_input_storage:
  522. if not weight.is_contiguous():
  523. raise ValueError("reuse_input_storage requires contiguous packed weights")
  524. packed = weight.view(torch.int32).reshape(packed_shape)
  525. else:
  526. packed = torch.empty(packed_shape, device=weight.device, dtype=torch.int32)
  527. k_tiles = size_k // _PACKED_TILE_SIZE
  528. n_tiles = size_n // _PACKED_TILE_N_SIZE
  529. qweight_scratch = torch.empty(
  530. (size_k // _PACK_FACTOR_4BIT, size_n),
  531. device=weight.device,
  532. dtype=torch.int32,
  533. )
  534. flat_scratch = torch.empty(
  535. (k_tiles, n_tiles, 128),
  536. device=weight.device,
  537. dtype=torch.int32,
  538. )
  539. gather_scratch = torch.empty_like(flat_scratch)
  540. for expert in range(num_experts):
  541. expert_weight = weight[expert].view(torch.int32)
  542. if row_rotation is not None:
  543. rotated_rows = int(size_n) - int(row_rotation)
  544. qweight_scratch[:, :rotated_rows].copy_(expert_weight[row_rotation:].T)
  545. qweight_scratch[:, rotated_rows:].copy_(expert_weight[:row_rotation].T)
  546. else:
  547. qweight_scratch.copy_(expert_weight.T)
  548. _repack_4bit_no_perm(
  549. qweight_scratch,
  550. size_k=size_k,
  551. size_n=size_n,
  552. out=packed[expert].view(k_tiles, n_tiles, 128),
  553. flat_scratch=flat_scratch,
  554. gather_scratch=gather_scratch,
  555. )
  556. return packed
  557. def _permute_nvfp4_scales(
  558. scales: torch.Tensor,
  559. global_scales: torch.Tensor,
  560. *,
  561. size_k: int,
  562. size_n: int,
  563. a_dtype: torch.dtype,
  564. row_rotation: int | None = None,
  565. ) -> tuple[torch.Tensor, torch.Tensor]:
  566. combined_scale_factor = _nvfp4_compute_scale_factor(scales, a_dtype)
  567. packed_scales: torch.Tensor | None = None
  568. for expert in range(scales.shape[0]):
  569. expert_source = scales[expert].to(a_dtype)
  570. if row_rotation is not None:
  571. expert_source = torch.cat(
  572. [expert_source[row_rotation:], expert_source[:row_rotation]],
  573. dim=0,
  574. )
  575. expert_scales = _permute_packed_scales(
  576. expert_source.T,
  577. size_k=size_k,
  578. size_n=size_n,
  579. group_size=16,
  580. )
  581. expert_packed = _process_nvfp4_packed_scales(
  582. expert_scales,
  583. scale_factor=combined_scale_factor,
  584. )
  585. if packed_scales is None:
  586. packed_scales = torch.empty(
  587. (int(scales.shape[0]), *expert_packed.shape),
  588. dtype=expert_packed.dtype,
  589. device=expert_packed.device,
  590. )
  591. packed_scales[expert].copy_(expert_packed)
  592. if packed_scales is None:
  593. packed_scales = torch.empty(
  594. (0, size_k // _PACKED_TILE_SIZE, size_n // 2),
  595. dtype=torch.float8_e4m3fn,
  596. device=scales.device,
  597. )
  598. packed_global = _process_nvfp4_packed_global_scale(
  599. global_scales,
  600. a_dtype=a_dtype,
  601. ).to(torch.float32)
  602. packed_global = packed_global / combined_scale_factor
  603. return packed_scales, packed_global.contiguous()
  604. # ---------------------------------------------------------------------------
  605. # NF3 ("nf3_2p1") packing.
  606. #
  607. # The kernel's B operand is a stream of 32-code "units" (2 N-columns x 16 K),
  608. # each a 12-byte (lo0, lo1, hi) triple. The GEMM reads unit index
  609. # unit = cur_group_id * (tile_n // 2) + (tid % (tile_n // 2))
  610. # from the pipe's B SMEM region (see kernel._load_b_scale_registers,
  611. # NF3-MAPPING-V1) which -- verified algebraically -- equals the stock staged
  612. # unit index; cur_group_id is the K16 row within the CTA-K tile, (tid %
  613. # (tile_n // 2)) is the N-pair. The flat-span staging copies the packer's
  614. # global bytes verbatim, so the packer must place, at global unit
  615. # I = nt*(K/16)*(tile_n/2) + R*(tile_n/2) + p (n_tile-major)
  616. # the codes for CTA N-tile nt, K16-row-global R, N-pair p. The 4 jj words in
  617. # the triple correspond to the stock 4-u32 group; per jj the 8 codes decode
  618. # (packed_dequant_nf3x8_to_bfloat2x4) to fragments
  619. # frag[0,0]=(cb[c0],cb[c1]) frag[0,1]=(cb[c2],cb[c3])
  620. # frag[1,0]=(cb[c4],cb[c5]) frag[1,1]=(cb[c6],cb[c7])
  621. # which must equal the stock fragment identity for this thread/jj:
  622. # frag[0,0]=(C1@K0, C1@K0+1) frag[0,1]=(C1@K0+8, C1@K0+9)
  623. # frag[1,0]=(C2@K0, C2@K0+1) frag[1,1]=(C2@K0+8, C2@K0+9)
  624. # where, from _repack_4bit_no_perm composed with the read mapping,
  625. # wru = p + (tile_n/2)*nt ; n_tile_64 = wru // 32 ; th_id = wru % 32
  626. # tc_col = th_id // 4 ; tc_row = (th_id % 4) * 2
  627. # C1 = n_tile_64*64 + jj*16 + tc_col ; C2 = C1 + 8 ; K0 = R*16 + tc_row
  628. # => code order [c0..c7] =
  629. # [ (C1,K0),(C1,K0+1),(C1,K0+8),(C1,K0+9),
  630. # (C2,K0),(C2,K0+1),(C2,K0+8),(C2,K0+9) ].
  631. # This whole chain is asserted by _nf3_pack_selftest against an independent
  632. # torch simulation of the kernel read+dequant.
  633. # ---------------------------------------------------------------------------
  634. def _nf3_check_shapes(size_k: int, size_n: int, tile_n: int) -> None:
  635. if int(size_k) % 32 != 0:
  636. raise ValueError(f"NF3 requires K % 32 == 0, got {size_k}")
  637. if int(size_n) % 64 != 0:
  638. raise ValueError(f"NF3 requires N % 64 == 0, got {size_n}")
  639. if int(tile_n) % 16 != 0 or int(tile_n) < 64:
  640. raise ValueError(f"NF3 tile_n must be a multiple of 16 and >= 64, got {tile_n}")
  641. if int(size_n) % int(tile_n) != 0:
  642. raise ValueError(f"NF3 requires N ({size_n}) divisible by tile_n ({tile_n})")
  643. def _nf3_code_gather_index(
  644. size_k: int, size_n: int, tile_n: int, device: torch.device
  645. ) -> torch.Tensor:
  646. """[units, 4, 8] flat indices into a contiguous [N, K] code tensor.
  647. Entry [I, jj, v] is the (C * size_k + K) index of the v-th code (NF3 order)
  648. of triple word jj at global unit I. Built from the exact _repack /
  649. read-mapping math documented above so the packer and the kernel agree.
  650. """
  651. _nf3_check_shapes(size_k, size_n, tile_n)
  652. npairs = int(tile_n) // 2
  653. k16 = int(size_k) // 16
  654. units = k16 * (int(size_n) // 2)
  655. ntile_units = k16 * npairs
  656. idx_i = torch.arange(units, device=device, dtype=torch.long)
  657. nt = idx_i // ntile_units
  658. within = idx_i % ntile_units
  659. R = within // npairs
  660. p = within % npairs
  661. wru = p + npairs * nt
  662. n_tile_64 = wru // 32
  663. th_id = wru % 32
  664. tc_col = th_id // 4
  665. tc_row = (th_id % 4) * 2
  666. jj = torch.arange(4, device=device, dtype=torch.long)
  667. col1 = jj[None, :] * 16 + tc_col[:, None] # [units, 4]
  668. col2 = col1 + 8
  669. C1 = n_tile_64[:, None] * 64 + col1 # [units, 4]
  670. C2 = n_tile_64[:, None] * 64 + col2
  671. K0 = (R * 16 + tc_row)[:, None] # [units, 1]
  672. k_off = torch.tensor([0, 1, 8, 9], device=device, dtype=torch.long)
  673. idx = torch.empty((units, 4, 8), device=device, dtype=torch.long)
  674. for t in range(4):
  675. idx[:, :, t] = C1 * int(size_k) + (K0 + k_off[t])
  676. idx[:, :, 4 + t] = C2 * int(size_k) + (K0 + k_off[t])
  677. return idx
  678. def _nf3_pack_codes(
  679. codes_nk: torch.Tensor, *, size_k: int, size_n: int, tile_n: int
  680. ) -> torch.Tensor:
  681. """Pack one expert's [N, K] codes (0..7) into int32 [units*3] NF3 planes."""
  682. if tuple(codes_nk.shape) != (int(size_n), int(size_k)):
  683. raise ValueError(
  684. f"expected codes shape {(int(size_n), int(size_k))}, "
  685. f"got {tuple(codes_nk.shape)}"
  686. )
  687. device = codes_nk.device
  688. flat = codes_nk.reshape(-1).to(torch.int64)
  689. if bool((flat < 0).any()) or bool((flat > 7).any()):
  690. raise ValueError("NF3 codes must be integers in [0, 7]")
  691. idx = _nf3_code_gather_index(size_k, size_n, tile_n, device) # [units,4,8]
  692. g = flat[idx.reshape(-1)].reshape(idx.shape) # [units,4,8]
  693. sh_lo = (torch.arange(4, device=device, dtype=torch.int64) * 2).view(1, 1, 4)
  694. sh_hi = torch.arange(4, device=device, dtype=torch.int64).view(1, 1, 4)
  695. la = ((g[:, :, 0:4] & 3) << sh_lo).sum(-1) # [units,4]
  696. lb = ((g[:, :, 4:8] & 3) << sh_lo).sum(-1)
  697. lo16 = la | (lb << 8)
  698. ha = (((g[:, :, 0:4] >> 2) & 1) << sh_hi).sum(-1)
  699. hb = (((g[:, :, 4:8] >> 2) & 1) << sh_hi).sum(-1)
  700. hi8 = ha | (hb << 4)
  701. lo0 = lo16[:, 0] | (lo16[:, 1] << 16)
  702. lo1 = lo16[:, 2] | (lo16[:, 3] << 16)
  703. hi = hi8[:, 0] | (hi8[:, 1] << 8) | (hi8[:, 2] << 16) | (hi8[:, 3] << 24)
  704. words = torch.stack([lo0, lo1, hi], dim=1).reshape(-1) # int64 in [0, 2**32)
  705. words = torch.where(words >= 2**31, words - 2**32, words)
  706. return words.to(torch.int32).contiguous()
  707. def _process_nf3_packed_scales(packed_scales: torch.Tensor) -> torch.Tensor:
  708. """[rows, N] float t_s -> [rows, N] uint8 e4m3-style byte = highbyte(f16(t_s*2**-4)<<1)."""
  709. packed_scales = packed_scales.to(torch.float16)
  710. packed_scales = packed_scales.view(-1, 4)[:, [0, 2, 1, 3]].view(
  711. packed_scales.size(0),
  712. -1,
  713. )
  714. packed_scales = (packed_scales.float() * (2.0**-4)).to(torch.float16)
  715. packed_scales = packed_scales.view(torch.int16) << 1
  716. packed_scales = packed_scales.view(torch.uint8)
  717. return packed_scales[:, 1::2].contiguous()
  718. def _nf3_pack_scales(
  719. t_s: torch.Tensor, *, size_k: int, size_n: int
  720. ) -> torch.Tensor:
  721. """Pack one expert's [N, K//32] scales into uint8 [K//32, N] (permuted)."""
  722. if tuple(t_s.shape) != (int(size_n), int(size_k) // 32):
  723. raise ValueError(
  724. f"expected scale shape {(int(size_n), int(size_k) // 32)}, "
  725. f"got {tuple(t_s.shape)}"
  726. )
  727. t_s = t_s.to(torch.float32)
  728. # zero scales (whole group exactly zero in the checkpoint) MUST stay zero:
  729. # stored byte 0 decodes to exact +0.0. Flooring them to _NF3_SCALE_FLOOR
  730. # injects noise into all-zero groups (real early-layer experts have many)
  731. # -> compounding early-layer error -> prompt-dependent degeneration.
  732. zero_mask = t_s == 0
  733. t_s = t_s.clamp(min=_NF3_SCALE_FLOOR)
  734. t_s[zero_mask] = 0.0
  735. if bool((t_s >= 32.0).any()):
  736. raise ValueError("NF3 scales must be < 32 for the e4m3 K/32 encoding")
  737. permuted = _permute_packed_scales(
  738. t_s.T.contiguous(),
  739. size_k=size_k,
  740. size_n=size_n,
  741. group_size=32,
  742. ) # [K//32, N] float
  743. return _process_nf3_packed_scales(permuted)
  744. def _nf3_pack_code_experts(
  745. codes: torch.Tensor, *, size_k: int, size_n: int, tile_n: int
  746. ) -> torch.Tensor:
  747. packed = [
  748. _nf3_pack_codes(codes[e], size_k=size_k, size_n=size_n, tile_n=tile_n)
  749. for e in range(int(codes.shape[0]))
  750. ]
  751. return torch.stack(packed, dim=0).contiguous()
  752. def _nf3_pack_scale_experts(
  753. t_s: torch.Tensor, *, size_k: int, size_n: int
  754. ) -> torch.Tensor:
  755. packed = [
  756. _nf3_pack_scales(t_s[e], size_k=size_k, size_n=size_n)
  757. for e in range(int(t_s.shape[0]))
  758. ]
  759. return torch.stack(packed, dim=0).contiguous()
  760. def _prepare_w4a16_packed_weights(
  761. w13_fp4: torch.Tensor,
  762. w13_blockscale: torch.Tensor,
  763. w13_global_scale: torch.Tensor,
  764. w2_fp4: torch.Tensor,
  765. w2_blockscale: torch.Tensor,
  766. w2_global_scale: torch.Tensor,
  767. *,
  768. activation: str,
  769. params_dtype: torch.dtype = torch.bfloat16,
  770. source_format: str,
  771. w13_layout: str = "w13",
  772. reuse_input_storage: bool = False,
  773. ) -> W4A16PackedWeights:
  774. source_format = _normalize_source_format(source_format)
  775. w13_layout = _normalize_w13_layout(w13_layout)
  776. shape = validate_w4a16_packed_inputs(
  777. w13_fp4,
  778. w13_global_scale,
  779. w2_fp4,
  780. w2_global_scale,
  781. activation=activation,
  782. )
  783. num_experts = shape.num_experts
  784. hidden_size = shape.hidden_size
  785. intermediate_size = shape.intermediate_size
  786. w13_rows = shape.w13_rows
  787. is_gated = shape.is_gated
  788. w13 = w13_fp4
  789. w13_scale = unswizzle_expert_scales(
  790. w13_blockscale,
  791. rows=w13_rows,
  792. cols=hidden_size,
  793. )
  794. w13_row_rotation = None
  795. if is_gated and w13_layout == "w13":
  796. # In-place: the half-swap is folded into the repack via row_rotation;
  797. # never materialize a second copy of the weights/scales.
  798. w13_row_rotation = intermediate_size
  799. w2_scale = unswizzle_expert_scales(
  800. w2_blockscale,
  801. rows=hidden_size,
  802. cols=intermediate_size,
  803. )
  804. packed_w13 = _repack_weight(
  805. w13 if reuse_input_storage else w13.contiguous(),
  806. size_k=hidden_size,
  807. size_n=w13_rows,
  808. row_rotation=w13_row_rotation,
  809. reuse_input_storage=reuse_input_storage,
  810. )
  811. packed_w2 = _repack_weight(
  812. w2_fp4 if reuse_input_storage else w2_fp4.contiguous(),
  813. size_k=intermediate_size,
  814. size_n=hidden_size,
  815. reuse_input_storage=reuse_input_storage,
  816. )
  817. native_w13_global_scale = _source_global_scale(
  818. w13_global_scale,
  819. source_format=source_format,
  820. )
  821. native_w2_global_scale = _source_global_scale(
  822. w2_global_scale,
  823. source_format=source_format,
  824. )
  825. packed_w13_scale, packed_w13_global_scale = _permute_nvfp4_scales(
  826. w13_scale,
  827. native_w13_global_scale,
  828. size_k=hidden_size,
  829. size_n=w13_rows,
  830. a_dtype=params_dtype,
  831. row_rotation=w13_row_rotation,
  832. )
  833. packed_w2_scale, packed_w2_global_scale = _permute_nvfp4_scales(
  834. w2_scale,
  835. native_w2_global_scale,
  836. size_k=intermediate_size,
  837. size_n=hidden_size,
  838. a_dtype=params_dtype,
  839. )
  840. return W4A16PackedWeights(
  841. w13=packed_w13,
  842. w13_scale=packed_w13_scale,
  843. w13_global_scale=packed_w13_global_scale,
  844. w2=packed_w2,
  845. w2_scale=packed_w2_scale,
  846. w2_global_scale=packed_w2_global_scale,
  847. workspace=_make_workspace(w13_fp4.device, max_blocks_per_sm=4),
  848. hidden_size=hidden_size,
  849. intermediate_size=intermediate_size,
  850. num_experts=num_experts,
  851. is_gated=is_gated,
  852. params_dtype=params_dtype,
  853. source_format=source_format,
  854. w13_layout=w13_layout,
  855. scale_format="e4m3_k16",
  856. )
  857. def prepare_w4a16_modelopt_nvfp4_weights(
  858. w13_fp4: torch.Tensor,
  859. w13_blockscale: torch.Tensor,
  860. w13_global_scale: torch.Tensor,
  861. w2_fp4: torch.Tensor,
  862. w2_blockscale: torch.Tensor,
  863. w2_global_scale: torch.Tensor,
  864. *,
  865. activation: str,
  866. params_dtype: torch.dtype = torch.bfloat16,
  867. w13_layout: str = "w13",
  868. reuse_input_storage: bool = False,
  869. ) -> W4A16PackedWeights:
  870. """Prepare ModelOpt NVFP4 tensors into the W4A16 packed runtime layout.
  871. The per-block scales are the normal NVFP4 K/16 scale grid in b12x swizzled
  872. storage. The global scales are raw ModelOpt weight global scales; activation
  873. input scales are not folded into W4A16 weight preparation. For gated
  874. activations, ``w13_layout`` describes whether fused W13 rows arrive in
  875. checkpoint/logical W13 order or already swapped W31 order.
  876. """
  877. return _prepare_w4a16_packed_weights(
  878. w13_fp4,
  879. w13_blockscale,
  880. w13_global_scale,
  881. w2_fp4,
  882. w2_blockscale,
  883. w2_global_scale,
  884. activation=activation,
  885. params_dtype=params_dtype,
  886. source_format="modelopt_nvfp4",
  887. w13_layout=w13_layout,
  888. reuse_input_storage=reuse_input_storage,
  889. )
  890. def prepare_w4a16_modelopt_native_weights(
  891. w13_fp4: torch.Tensor,
  892. w13_blockscale: torch.Tensor,
  893. w13_global_scale: torch.Tensor,
  894. w2_fp4: torch.Tensor,
  895. w2_blockscale: torch.Tensor,
  896. w2_global_scale: torch.Tensor,
  897. *,
  898. activation: str,
  899. params_dtype: torch.dtype = torch.bfloat16,
  900. source_format: str = "modelopt_nvfp4",
  901. w13_layout: str = "w13",
  902. ) -> W4A16ModelOptWeights:
  903. """Prepare W4A16 metadata while keeping ModelOpt FP4 weights native.
  904. This is the memory-safe path for GLM serving that needs A4 prefill and A16
  905. decode in the same process. It keeps the checkpoint FP4 tensors resident
  906. instead of materializing a second full W4A16 packed copy.
  907. """
  908. source_format = _normalize_source_format(source_format)
  909. if source_format not in _MODEL_OPT_NVFP4_FORMATS:
  910. raise ValueError(
  911. "native W4A16 ModelOpt weights require source_format "
  912. "'modelopt_nvfp4'"
  913. )
  914. w13_layout = _normalize_w13_layout(w13_layout)
  915. shape = validate_w4a16_packed_inputs(
  916. w13_fp4,
  917. w13_global_scale,
  918. w2_fp4,
  919. w2_global_scale,
  920. activation=activation,
  921. )
  922. num_experts = shape.num_experts
  923. hidden_size = shape.hidden_size
  924. intermediate_size = shape.intermediate_size
  925. w13_rows = shape.w13_rows
  926. is_gated = shape.is_gated
  927. w13_scale = unswizzle_expert_scales(
  928. w13_blockscale,
  929. rows=w13_rows,
  930. cols=hidden_size,
  931. )
  932. w2_scale = unswizzle_expert_scales(
  933. w2_blockscale,
  934. rows=hidden_size,
  935. cols=intermediate_size,
  936. )
  937. native_w13_global_scale = _source_global_scale(
  938. w13_global_scale,
  939. source_format=source_format,
  940. )
  941. native_w2_global_scale = _source_global_scale(
  942. w2_global_scale,
  943. source_format=source_format,
  944. )
  945. # The W4A16 activation consumes FC1 output in gate/up logical order.
  946. # Checkpoint-native ModelOpt GLM tensors are up/gate, while vLLM/FI can
  947. # hand over gate/up tensors after its own W13 reorder. Keep that physical
  948. # order explicit so source_format never implies a layout transformation.
  949. w13_row_rotation = (
  950. intermediate_size if is_gated and w13_layout == "w13" else None
  951. )
  952. packed_w13_scale, packed_w13_global_scale = _permute_nvfp4_scales(
  953. w13_scale,
  954. native_w13_global_scale,
  955. size_k=hidden_size,
  956. size_n=w13_rows,
  957. a_dtype=params_dtype,
  958. row_rotation=w13_row_rotation,
  959. )
  960. packed_w2_scale, packed_w2_global_scale = _permute_nvfp4_scales(
  961. w2_scale,
  962. native_w2_global_scale,
  963. size_k=intermediate_size,
  964. size_n=hidden_size,
  965. a_dtype=params_dtype,
  966. )
  967. return W4A16ModelOptWeights(
  968. w13=w13_fp4,
  969. w13_scale=packed_w13_scale,
  970. w13_global_scale=packed_w13_global_scale,
  971. w2=w2_fp4,
  972. w2_scale=packed_w2_scale,
  973. w2_global_scale=packed_w2_global_scale,
  974. workspace=_make_workspace(w13_fp4.device, max_blocks_per_sm=4),
  975. hidden_size=hidden_size,
  976. intermediate_size=intermediate_size,
  977. num_experts=num_experts,
  978. is_gated=is_gated,
  979. params_dtype=params_dtype,
  980. source_format=source_format,
  981. micro_w13_scale=packed_w13_scale,
  982. micro_w13_global_scale=_process_nvfp4_micro_global_scale_from_packed(
  983. packed_w13_global_scale,
  984. a_dtype=params_dtype,
  985. ),
  986. micro_w2_scale=packed_w2_scale,
  987. micro_w2_global_scale=_process_nvfp4_micro_global_scale_from_packed(
  988. packed_w2_global_scale,
  989. a_dtype=params_dtype,
  990. ),
  991. w13_layout=w13_layout,
  992. )
  993. def prepare_w4a16_e8m0_native_weights(
  994. w13_fp4: torch.Tensor,
  995. w13_e8m0_scale: torch.Tensor,
  996. w13_global_scale: torch.Tensor,
  997. w2_fp4: torch.Tensor,
  998. w2_e8m0_scale: torch.Tensor,
  999. w2_global_scale: torch.Tensor,
  1000. *,
  1001. activation: str,
  1002. params_dtype: torch.dtype = torch.bfloat16,
  1003. w13_layout: str = "w13",
  1004. ) -> W4A16ModelOptWeights:
  1005. """Prepare native MXFP4 (E8M0 K/32) weights for the W4A16 path.
  1006. Keeps the FP4 weights resident as a single copy (``weight_layout="modelopt"``)
  1007. and carries two small scale forms so one object serves both kernels:
  1008. ``w13_scale``/``w2_scale`` are the packed E8M0 grid the main W4A16 GEMM reads
  1009. at med/large M, and ``micro_*`` are packed E8M0 grids in the native row order
  1010. that the small-M micro decode kernel reads. ``run_w4a16_moe`` routes small M
  1011. to micro and the rest to the main W4A16 kernel automatically.
  1012. """
  1013. w13_layout = _normalize_w13_layout(w13_layout)
  1014. shape = validate_w4a16_packed_inputs(
  1015. w13_fp4,
  1016. w13_global_scale,
  1017. w2_fp4,
  1018. w2_global_scale,
  1019. activation=activation,
  1020. )
  1021. num_experts = shape.num_experts
  1022. hidden_size = shape.hidden_size
  1023. intermediate_size = shape.intermediate_size
  1024. w13_rows = shape.w13_rows
  1025. is_gated = shape.is_gated
  1026. allow_w2_k_tail = intermediate_size % 32 != 0
  1027. padded_w13_scale_n = (
  1028. _e8m0_logical_tail_scale_n(w13_rows) if allow_w2_k_tail else w13_rows
  1029. )
  1030. w13_scale = _validate_e8m0_k32_scales(
  1031. w13_e8m0_scale,
  1032. rows=w13_rows,
  1033. cols=hidden_size,
  1034. name="w13_e8m0_scale",
  1035. )
  1036. w2_scale = _validate_e8m0_k32_scales(
  1037. w2_e8m0_scale,
  1038. rows=hidden_size,
  1039. cols=intermediate_size,
  1040. name="w2_e8m0_scale",
  1041. allow_k_tail=allow_w2_k_tail,
  1042. )
  1043. # Main-GEMM (med/large M) packed E8M0 scales. The "w13" (up_gate) layout
  1044. # needs the FC1 half-swap folded into the scale grid; the kernel applies the
  1045. # matching source_n_rotation to the native weights. micro reads the un-rotated
  1046. # grid and handles the layout itself (w13_gate_first).
  1047. w13_row_rotation = intermediate_size if (is_gated and w13_layout == "w13") else None
  1048. packed_w13_scale = _pack_e8m0_k32_scales(
  1049. w13_scale,
  1050. size_k=hidden_size,
  1051. size_n=w13_rows,
  1052. row_rotation=w13_row_rotation,
  1053. padded_size_n=padded_w13_scale_n,
  1054. )
  1055. micro_w13_scale = (
  1056. packed_w13_scale
  1057. if w13_row_rotation is None
  1058. else _pack_e8m0_k32_scales(
  1059. w13_scale,
  1060. size_k=hidden_size,
  1061. size_n=w13_rows,
  1062. padded_size_n=padded_w13_scale_n,
  1063. )
  1064. )
  1065. packed_w2_scale = _pack_e8m0_k32_scales(
  1066. w2_scale,
  1067. size_k=intermediate_size,
  1068. size_n=hidden_size,
  1069. allow_k_tail=allow_w2_k_tail,
  1070. )
  1071. # Storage-compatible single grid: micro reads the SAME packed _pack_e8m0_k32
  1072. # scales the main GEMM reads (no separate K/16 micro grid).
  1073. w13_global = w13_global_scale.contiguous()
  1074. w2_global = w2_global_scale.contiguous()
  1075. return W4A16ModelOptWeights(
  1076. w13=w13_fp4,
  1077. w13_scale=packed_w13_scale,
  1078. w13_global_scale=w13_global,
  1079. w2=w2_fp4,
  1080. w2_scale=packed_w2_scale,
  1081. w2_global_scale=w2_global,
  1082. workspace=_make_workspace(w13_fp4.device, max_blocks_per_sm=4),
  1083. hidden_size=hidden_size,
  1084. intermediate_size=intermediate_size,
  1085. num_experts=num_experts,
  1086. is_gated=is_gated,
  1087. params_dtype=params_dtype,
  1088. source_format="fp4_e8m0_k32",
  1089. scale_format="e8m0_k32",
  1090. micro_w13_scale=micro_w13_scale,
  1091. micro_w13_global_scale=w13_global,
  1092. micro_w2_scale=packed_w2_scale,
  1093. micro_w2_global_scale=w2_global,
  1094. w13_layout=w13_layout,
  1095. )
  1096. def prepare_w4a16_compressed_tensors_weights(
  1097. w13_fp4: torch.Tensor,
  1098. w13_blockscale: torch.Tensor,
  1099. w13_global_scale: torch.Tensor,
  1100. w2_fp4: torch.Tensor,
  1101. w2_blockscale: torch.Tensor,
  1102. w2_global_scale: torch.Tensor,
  1103. *,
  1104. activation: str,
  1105. params_dtype: torch.dtype = torch.bfloat16,
  1106. w13_layout: str = "w13",
  1107. reuse_input_storage: bool = False,
  1108. ) -> W4A16PackedWeights:
  1109. """Prepare CompressedTensors NVFP4 tensors into the W4A16 packed runtime layout.
  1110. The per-block scales are the normal NVFP4 K/16 scale grid in b12x swizzled
  1111. storage. The CT global scales are stored inverted relative to the ModelOpt
  1112. weight global scale convention, so they are inverted before packing.
  1113. """
  1114. return _prepare_w4a16_packed_weights(
  1115. w13_fp4,
  1116. w13_blockscale,
  1117. w13_global_scale,
  1118. w2_fp4,
  1119. w2_blockscale,
  1120. w2_global_scale,
  1121. activation=activation,
  1122. params_dtype=params_dtype,
  1123. source_format="compressed_tensors",
  1124. w13_layout=w13_layout,
  1125. reuse_input_storage=reuse_input_storage,
  1126. )
  1127. def prepare_w4a16_fp4_e8m0_k32_weights(
  1128. w13_fp4: torch.Tensor,
  1129. w13_e8m0_scale: torch.Tensor,
  1130. w13_global_scale: torch.Tensor,
  1131. w2_fp4: torch.Tensor,
  1132. w2_e8m0_scale: torch.Tensor,
  1133. w2_global_scale: torch.Tensor,
  1134. *,
  1135. activation: str,
  1136. params_dtype: torch.dtype = torch.bfloat16,
  1137. w13_layout: str = "w13",
  1138. reuse_input_storage: bool = False,
  1139. ) -> W4A16PackedWeights:
  1140. """Prepare FP4 weights with E8M0 K/32 scales for W4A16.
  1141. The per-block source scales are [E, N, K/32] E8M0 bytes. They are only
  1142. saturated to the BF16 kernel's supported byte range and rearranged for
  1143. kernel access; they are not expanded to K/16 or folded into global scales.
  1144. """
  1145. w13_layout = _normalize_w13_layout(w13_layout)
  1146. shape = validate_w4a16_packed_inputs(
  1147. w13_fp4,
  1148. w13_global_scale,
  1149. w2_fp4,
  1150. w2_global_scale,
  1151. activation=activation,
  1152. )
  1153. num_experts = shape.num_experts
  1154. hidden_size = shape.hidden_size
  1155. intermediate_size = shape.intermediate_size
  1156. w13_rows = shape.w13_rows
  1157. is_gated = shape.is_gated
  1158. w13 = w13_fp4
  1159. w13_scale = _validate_e8m0_k32_scales(
  1160. w13_e8m0_scale,
  1161. rows=w13_rows,
  1162. cols=hidden_size,
  1163. name="w13_e8m0_scale",
  1164. )
  1165. w13_row_rotation = None
  1166. if is_gated and w13_layout != "w31":
  1167. w13_row_rotation = intermediate_size
  1168. w2_scale = _validate_e8m0_k32_scales(
  1169. w2_e8m0_scale,
  1170. rows=hidden_size,
  1171. cols=intermediate_size,
  1172. name="w2_e8m0_scale",
  1173. )
  1174. packed_w13 = _repack_weight(
  1175. w13 if reuse_input_storage else w13.contiguous(),
  1176. size_k=hidden_size,
  1177. size_n=w13_rows,
  1178. row_rotation=w13_row_rotation,
  1179. reuse_input_storage=reuse_input_storage,
  1180. )
  1181. packed_w2 = _repack_weight(
  1182. w2_fp4 if reuse_input_storage else w2_fp4.contiguous(),
  1183. size_k=intermediate_size,
  1184. size_n=hidden_size,
  1185. reuse_input_storage=reuse_input_storage,
  1186. )
  1187. packed_w13_scale = _pack_e8m0_k32_scales(
  1188. w13_scale,
  1189. size_k=hidden_size,
  1190. size_n=w13_rows,
  1191. row_rotation=w13_row_rotation,
  1192. reuse_input_storage=reuse_input_storage,
  1193. )
  1194. packed_w2_scale = _pack_e8m0_k32_scales(
  1195. w2_scale,
  1196. size_k=intermediate_size,
  1197. size_n=hidden_size,
  1198. reuse_input_storage=reuse_input_storage,
  1199. )
  1200. return W4A16PackedWeights(
  1201. w13=packed_w13,
  1202. w13_scale=packed_w13_scale,
  1203. w13_global_scale=w13_global_scale.contiguous(),
  1204. w2=packed_w2,
  1205. w2_scale=packed_w2_scale,
  1206. w2_global_scale=w2_global_scale.contiguous(),
  1207. workspace=_make_workspace(w13_fp4.device, max_blocks_per_sm=4),
  1208. hidden_size=hidden_size,
  1209. intermediate_size=intermediate_size,
  1210. num_experts=num_experts,
  1211. is_gated=is_gated,
  1212. params_dtype=params_dtype,
  1213. source_format="fp4_e8m0_k32",
  1214. w13_layout=w13_layout,
  1215. scale_format="e8m0_k32",
  1216. )
  1217. def prepare_w4a16_packed_weights(
  1218. *args,
  1219. source_format: str = "modelopt_nvfp4",
  1220. w13_layout: str = "w13",
  1221. **kwargs,
  1222. ) -> W4A16PackedWeights:
  1223. source_format = _normalize_source_format(source_format)
  1224. w13_layout = _normalize_w13_layout(w13_layout)
  1225. if source_format == "modelopt_nvfp4":
  1226. return prepare_w4a16_modelopt_nvfp4_weights(
  1227. *args, w13_layout=w13_layout, **kwargs
  1228. )
  1229. if source_format == "compressed_tensors":
  1230. return prepare_w4a16_compressed_tensors_weights(
  1231. *args, w13_layout=w13_layout, **kwargs
  1232. )
  1233. if source_format == "fp4_e8m0_k32":
  1234. return prepare_w4a16_fp4_e8m0_k32_weights(
  1235. *args, w13_layout=w13_layout, **kwargs
  1236. )
  1237. raise AssertionError(f"unhandled W4A16 source_format {source_format!r}")
  1238. def make_w4a16_packed_buffers(
  1239. prepared: W4A16PackedWeights | W4A16ModelOptWeights,
  1240. *,
  1241. m: int,
  1242. topk: int,
  1243. dtype: torch.dtype,
  1244. device: torch.device,
  1245. route_num_experts: int | None = None,
  1246. ) -> W4A16PackedBuffers:
  1247. return _make_w4a16_packed_buffers(
  1248. prepared,
  1249. m=m,
  1250. topk=topk,
  1251. dtype=dtype,
  1252. device=device,
  1253. route_num_experts=route_num_experts,
  1254. )
  1255. def prepare_nf3_moe_weights(
  1256. w13_codes: torch.Tensor,
  1257. w13_scale: torch.Tensor,
  1258. w2_codes: torch.Tensor,
  1259. w2_scale: torch.Tensor,
  1260. *,
  1261. activation: str,
  1262. fc1_tile_n: int,
  1263. fc2_tile_n: int,
  1264. params_dtype: torch.dtype = torch.bfloat16,
  1265. ) -> PreparedNF3MoeWeights:
  1266. """Pack NF3 codes/scales into the runtime "nf3_2p1" W4A16 layout.
  1267. Inputs are per-expert integer codes (0..7) in kernel-native output order
  1268. (gate/up already resolved for FC1) and per-group float scales ``t_s``:
  1269. w13_codes [E, 2*I, hidden] w13_scale [E, 2*I, hidden//32]
  1270. w2_codes [E, hidden, I] w2_scale [E, hidden, I//32]
  1271. ``fc1_tile_n`` / ``fc2_tile_n`` MUST equal the CTA N-tiles the kernel will
  1272. use (read them from the compiled W4A16FusedMoeCompileResult) -- the
  1273. flat-span layout is tile_n specific.
  1274. """
  1275. if params_dtype != torch.bfloat16:
  1276. raise ValueError("nf3_2p1 W4A16 weights are bf16-only for v1")
  1277. shape = validate_nf3_moe_inputs(
  1278. w13_codes,
  1279. w13_scale,
  1280. w2_codes,
  1281. w2_scale,
  1282. activation=activation,
  1283. )
  1284. device = w13_codes.device
  1285. packed_w13 = _nf3_pack_code_experts(
  1286. w13_codes, size_k=shape.hidden_size, size_n=shape.w13_rows, tile_n=fc1_tile_n
  1287. )
  1288. packed_w2 = _nf3_pack_code_experts(
  1289. w2_codes,
  1290. size_k=shape.intermediate_size,
  1291. size_n=shape.hidden_size,
  1292. tile_n=fc2_tile_n,
  1293. )
  1294. packed_w13_scale = _nf3_pack_scale_experts(
  1295. w13_scale, size_k=shape.hidden_size, size_n=shape.w13_rows
  1296. )
  1297. packed_w2_scale = _nf3_pack_scale_experts(
  1298. w2_scale, size_k=shape.intermediate_size, size_n=shape.hidden_size
  1299. )
  1300. global_scale = torch.full(
  1301. (shape.num_experts,), _NF3_SCALE_GLOBAL, dtype=torch.float32, device=device
  1302. )
  1303. return PreparedNF3MoeWeights(
  1304. w13=packed_w13,
  1305. w13_scale=packed_w13_scale,
  1306. w13_global_scale=global_scale,
  1307. w2=packed_w2,
  1308. w2_scale=packed_w2_scale,
  1309. w2_global_scale=global_scale.clone(),
  1310. workspace=_make_workspace(device, max_blocks_per_sm=4),
  1311. hidden_size=shape.hidden_size,
  1312. intermediate_size=shape.intermediate_size,
  1313. num_experts=shape.num_experts,
  1314. is_gated=shape.is_gated,
  1315. params_dtype=params_dtype,
  1316. fc1_tile_n=int(fc1_tile_n),
  1317. fc2_tile_n=int(fc2_tile_n),
  1318. )
  1319. def _nf3_pack_selftest() -> None:
  1320. """Prove the packer matches the kernel's read+dequant chain (torch/CPU).
  1321. Packs random codes, then INDEPENDENTLY simulates the kernel's flat-span
  1322. staging + register read (unit = cur_group_id*(tile_n//2) + tid%(tile_n//2))
  1323. + packed_dequant_nf3x8 fragment placement in pure torch, and asserts the
  1324. reconstructed dequantized matrix equals codebook[codes] at every (N, K).
  1325. """
  1326. codebook = _NF3_CODEBOOK
  1327. def simulate(packed, size_k, size_n, tile_n, tile_k):
  1328. cta_threads = tile_n * tile_k // 64
  1329. b_sh_stride = tile_n // 2
  1330. cta_k_blocks = tile_k // 16
  1331. b_sh_stage = b_sh_stride * cta_k_blocks
  1332. b_sh_wr_iters = b_sh_stage // cta_threads
  1333. tb_n_warps = tile_n // 64
  1334. b_chunks = b_sh_stage * 12 // 16
  1335. wr_iters_nf3 = (b_chunks + cta_threads - 1) // cta_threads
  1336. ntile_stride = (size_k // 16) * (tile_n // 2)
  1337. n_tiles = size_n // tile_n
  1338. k_tiles = size_k // tile_k
  1339. packed = packed.tolist()
  1340. w = [[None] * size_n for _ in range(size_k)]
  1341. def u32(x):
  1342. return x & 0xFFFFFFFF
  1343. for nt in range(n_tiles):
  1344. for tile_idx in range(k_tiles):
  1345. span_base_unit = (
  1346. nt * ntile_stride + tile_idx * cta_k_blocks * b_sh_stride
  1347. )
  1348. smem = {}
  1349. for i in range(wr_iters_nf3):
  1350. for tid in range(cta_threads):
  1351. c = i * cta_threads + tid
  1352. if c >= b_chunks:
  1353. continue
  1354. for word in range(4):
  1355. smem[c * 4 + word] = u32(
  1356. packed[span_base_unit * 3 + c * 4 + word]
  1357. )
  1358. for tid in range(cta_threads):
  1359. warp_row = (tid // 32) // tb_n_warps
  1360. for kk in range(b_sh_wr_iters):
  1361. cur = b_sh_wr_iters * warp_row + kk
  1362. unit = cur * b_sh_stride + (tid % b_sh_stride)
  1363. lo0 = smem[unit * 3 + 0]
  1364. lo1 = smem[unit * 3 + 1]
  1365. hi = smem[unit * 3 + 2]
  1366. R = tile_idx * cta_k_blocks + cur
  1367. p = tid % b_sh_stride
  1368. for jj in range(4):
  1369. lo_w = lo0 if (jj // 2) == 0 else lo1
  1370. lo16 = (lo_w >> (16 * (jj % 2))) & 0xFFFF
  1371. hi8 = (hi >> (8 * jj)) & 0xFF
  1372. la = lo16 & 0xFF
  1373. lb = (lo16 >> 8) & 0xFF
  1374. ha = hi8 & 0xF
  1375. hb = (hi8 >> 4) & 0xF
  1376. def code(byte2, nib1, j):
  1377. return ((byte2 >> (2 * j)) & 3) | (
  1378. ((nib1 >> j) & 1) << 2
  1379. )
  1380. cds = [code(la, ha, j) for j in range(4)] + [
  1381. code(lb, hb, j) for j in range(4)
  1382. ]
  1383. wru = p + (tile_n // 2) * nt
  1384. n64 = wru // 32
  1385. th = wru % 32
  1386. tc_col = th // 4
  1387. tc_row = (th % 4) * 2
  1388. col1 = jj * 16 + tc_col
  1389. c1 = n64 * 64 + col1
  1390. c2 = c1 + 8
  1391. k0 = R * 16 + tc_row
  1392. w[k0 + 0][c1] = codebook[cds[0]]
  1393. w[k0 + 1][c1] = codebook[cds[1]]
  1394. w[k0 + 8][c1] = codebook[cds[2]]
  1395. w[k0 + 9][c1] = codebook[cds[3]]
  1396. w[k0 + 0][c2] = codebook[cds[4]]
  1397. w[k0 + 1][c2] = codebook[cds[5]]
  1398. w[k0 + 8][c2] = codebook[cds[6]]
  1399. w[k0 + 9][c2] = codebook[cds[7]]
  1400. return w
  1401. torch.manual_seed(0)
  1402. cases = [
  1403. (256, 128, 128, 128),
  1404. (256, 128, 128, 64),
  1405. (64, 256, 256, 64),
  1406. (512, 256, 256, 64),
  1407. ]
  1408. for size_k, size_n, tile_n, tile_k in cases:
  1409. codes = torch.randint(0, 8, (size_n, size_k), dtype=torch.int64)
  1410. packed = _nf3_pack_codes(
  1411. codes, size_k=size_k, size_n=size_n, tile_n=tile_n
  1412. )
  1413. w = simulate(packed, size_k, size_n, tile_n, tile_k)
  1414. for k in range(size_k):
  1415. for n in range(size_n):
  1416. expect = _NF3_CODEBOOK[int(codes[n, k])]
  1417. got = w[k][n]
  1418. assert got is not None, f"unfilled ({k},{n}) for {(size_k, size_n, tile_n, tile_k)}"
  1419. assert abs(got - expect) < 1e-9, (
  1420. f"mismatch at ({k},{n}) for {(size_k, size_n, tile_n, tile_k)}: "
  1421. f"{got} != {expect}"
  1422. )
  1423. print("nf3 pack self-test PASSED")
  1424. __all__ = [
  1425. "PreparedNF3MoeWeights",
  1426. "W4A16PackedBuffers",
  1427. "W4A16ModelOptWeights",
  1428. "W4A16PackedWeights",
  1429. "make_w4a16_packed_buffers",
  1430. "prepare_nf3_moe_weights",
  1431. "prepare_w4a16_compressed_tensors_weights",
  1432. "prepare_w4a16_e8m0_native_weights",
  1433. "prepare_w4a16_fp4_e8m0_k32_weights",
  1434. "prepare_w4a16_modelopt_native_weights",
  1435. "prepare_w4a16_modelopt_nvfp4_weights",
  1436. "prepare_w4a16_packed_weights",
  1437. ]