glm53-flash_SM121.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. #!/usr/bin/env python3
  2. """SM121 kernel patches for GLM-5.3-Flash-NVFP4 on GB10.
  3. This Spark recipe runs this once while building glm53-flash-sm121:v8
  4. (GB10 / SM121). The serving tag on top is MM + Ray TP2 with the OpenAI API
  5. on :8888. Stock glm53-flash-arm64-cu130 only lists SM120 packed MLA on
  6. capability 12; our checkpoint is NoPE (pe_dim=0), so this image selects
  7. SM90 sparse-MLA + FA2 instead. We did not author FA2 MLA or FlashInfer
  8. 0.6.18 — we only gate what is already in the tree onto GB10.
  9. Modes:
  10. sm90 default. Seven steps the Dockerfile runs with no args.
  11. sm120 unused packed-cache path (fp8_ds_mla pad / topk 2048 / skip
  12. warmup). Kept so it is not a second file. Do not bake.
  13. python3 glm53-flash_SM121.py
  14. python3 glm53-flash_SM121.py sm90
  15. python3 glm53-flash_SM121.py sm120
  16. python3 glm53-flash_SM121.py --legacy-sm120
  17. Unknown args are refused. Each replacement must match stock vLLM / FI 0.6.18
  18. exactly once or we abort. Already-applied trees are skipped.
  19. """
  20. from __future__ import annotations
  21. import sys
  22. from pathlib import Path
  23. VLLM = Path("/usr/local/lib/python3.12/dist-packages/vllm")
  24. FI = Path("/usr/local/lib/python3.12/dist-packages/flashinfer")
  25. LOG = "[mia-sm121]"
  26. USAGE = (
  27. "usage: python3 glm53-flash_SM121.py [sm90|sm120|--legacy-sm120]\n"
  28. " sm90 (default): seven GB10 steps baked by Dockerfile\n"
  29. " sm120 / --legacy-sm120: unused packed-cache path (do not bake)"
  30. )
  31. # ---------------------------------------------------------------------------
  32. # CLI (parse first so the bake helpers can sit in FI-then-vLLM source order)
  33. # ---------------------------------------------------------------------------
  34. def parse_mode(argv: list[str]) -> str:
  35. if not argv:
  36. return "sm90"
  37. if len(argv) != 1:
  38. raise SystemExit("refuse extra args\n" + USAGE)
  39. arg = argv[0]
  40. if arg == "sm90":
  41. return "sm90"
  42. if arg in ("sm120", "--legacy-sm120"):
  43. return "sm120"
  44. raise SystemExit("unknown mode %r\n%s" % (arg, USAGE))
  45. def apply_once(path: Path, old: str, new: str, label: str) -> str:
  46. text = path.read_text()
  47. n_old = text.count(old)
  48. n_new = text.count(new)
  49. if n_old == 0 and n_new == 1:
  50. return "skipped"
  51. if n_old != 1:
  52. raise SystemExit(
  53. "%s refuse %s (old=%d new=%d); stock tree changed"
  54. % (LOG, label, n_old, n_new)
  55. )
  56. path.write_text(text.replace(old, new, 1))
  57. return "applied"
  58. def announce(title: str, results: list[str]) -> None:
  59. if all(r == "skipped" for r in results):
  60. print("%s skip %s (already in this image)" % (LOG, title))
  61. return
  62. print("%s %s (%s)" % (LOG, title, " ".join(results)))
  63. # ---------------------------------------------------------------------------
  64. # FlashInfer 0.6.18 — files exist in this image after the Dockerfile pip.
  65. # Defined first (unlike a v1-then-v8 changelog). apply_sm90 still runs them
  66. # after the vLLM tree so a rebuild matches the same end state.
  67. # ---------------------------------------------------------------------------
  68. def bake_fa2_fp8_cta_tile() -> None:
  69. old = " constexpr uint32_t EFF_CTA_TILE_KV = std::is_same_v<DTypeKV, __nv_fp8_e4m3> ? 32 : CTA_TILE_KV;\n"
  70. new = " constexpr uint32_t EFF_CTA_TILE_KV = std::is_same_v<DTypeKV, __nv_fp8_e4m3> ? (CTA_TILE_KV < 32u ? CTA_TILE_KV : 32u) : CTA_TILE_KV;\n"
  71. r = apply_once(
  72. FI / "data/include/flashinfer/attention/mla.cuh",
  73. old,
  74. new,
  75. "mla.cuh fp8 tile",
  76. )
  77. announce("FA2 fp8 CTA tile cap for GB10 ~101KB smem", [r])
  78. def bake_fa2_fp8_sm12_gate() -> None:
  79. old = " major, minor = get_compute_capability(self.device)\n if major != 9:\n"
  80. new = " major, minor = get_compute_capability(self.device)\n if major not in (9, 12):\n"
  81. r = apply_once(FI / "mla/_core.py", old, new, "_core.py fp8 SM12 gate")
  82. announce("FA2 fp8 gate major in (9, 12)", [r])
  83. # ---------------------------------------------------------------------------
  84. # vLLM tree — SM90 NoPE + FA2 on capability 12, PDL off, indexer/kpool
  85. # ---------------------------------------------------------------------------
  86. def bake_sm90_on_capability_12() -> None:
  87. old = """ elif device_capability.major == 12:
  88. return [
  89. AttentionBackendEnum.TRITON_MLA,
  90. AttentionBackendEnum.FLASHINFER_MLA_SPARSE_SM120,
  91. ]"""
  92. new = """ elif device_capability.major == 12:
  93. return [
  94. AttentionBackendEnum.TRITON_MLA,
  95. AttentionBackendEnum.FLASHINFER_MLA_SPARSE_SM90,
  96. AttentionBackendEnum.FLASHINFER_MLA_SPARSE_SM120,
  97. ]"""
  98. r = apply_once(VLLM / "platforms/cuda.py", old, new, "cuda.py SM90 list")
  99. announce("cuda.py lists SM90 on capability 12", [r])
  100. def bake_sm90_wrapper_for_gb10() -> None:
  101. path = VLLM / "v1/attention/backends/mla/flashinfer_mla_sparse_sm90.py"
  102. cap_old = (
  103. " def supports_compute_capability(cls, capability: DeviceCapability) -> bool:\n"
  104. " return capability.major == 9\n"
  105. )
  106. cap_new = (
  107. " def supports_compute_capability(cls, capability: DeviceCapability) -> bool:\n"
  108. " return capability.major in (9, 12)\n"
  109. )
  110. fa_old = ' backend="fa3",\n'
  111. fa_new = (
  112. ' backend=("fa3" if torch.cuda.get_device_capability()[0] == 9 else "fa2"),\n'
  113. )
  114. gate_old = """ if not has_flashinfer_sm90_nope_mla():
  115. return (
  116. "FLASHINFER_MLA_SPARSE_SM90 requires FlashInfer with SM90 "
  117. "MLA support (ckv_scale_arr in "
  118. "BatchMLAPagedAttentionWrapper.run, FlashInfer >= 0.6.18)"
  119. )"""
  120. gate_new = """ if kv_cache_dtype in ("fp8", "fp8_e4m3") and not has_flashinfer_sm90_nope_mla():
  121. return (
  122. "FLASHINFER_MLA_SPARSE_SM90 fp8 KV requires FlashInfer with "
  123. "SM90 MLA support (ckv_scale_arr in "
  124. "BatchMLAPagedAttentionWrapper.run, FlashInfer >= 0.6.18)"
  125. )"""
  126. results = [
  127. apply_once(path, cap_old, cap_new, "sm90 capability gate"),
  128. apply_once(path, fa_old, fa_new, "sm90 FA2 wrapper"),
  129. apply_once(path, gate_old, gate_new, "sm90 fp8 version gate"),
  130. ]
  131. announce("sm90 cap (9,12) + FA2 off-Hopper + fp8 gate", results)
  132. def bake_pdl_off_sm12() -> None:
  133. old = """ @classmethod
  134. def is_arch_support_pdl(cls) -> bool:
  135. try:
  136. device = torch.cuda.current_device()
  137. major, _ = torch.cuda.get_device_capability(device)
  138. except Exception:
  139. return False
  140. return major >= 9
  141. """
  142. new = """ @classmethod
  143. def is_arch_support_pdl(cls) -> bool:
  144. try:
  145. device = torch.cuda.current_device()
  146. major, _ = torch.cuda.get_device_capability(device)
  147. except Exception:
  148. return False
  149. # PDL lowering is unvalidated on SM12x (GB10) and races on KDA
  150. # state kernels there; keep it to Hopper/Blackwell-datacenter.
  151. return major in (9, 10)
  152. """
  153. r = apply_once(VLLM / "platforms/cuda.py", old, new, "cuda.py PDL")
  154. announce("PDL gated off on SM12x (KDA race on this kit)", [r])
  155. def bake_indexer_topk_neg1() -> None:
  156. path = VLLM / "model_executor/layers/sparse_attn_indexer_kpool.py"
  157. prefill_old = (
  158. " pool_topk = torch.empty(\n"
  159. " (num_rows, select_k), dtype=torch.int32, device=logits.device\n"
  160. " )\n"
  161. )
  162. prefill_new = (
  163. " pool_topk = torch.full(\n"
  164. " (num_rows, select_k), -1, dtype=torch.int32, device=logits.device\n"
  165. " )\n"
  166. )
  167. decode_old = (
  168. " pool_topk = torch.empty(\n"
  169. " (num_rows, select_k), dtype=torch.int32, device=logits.device\n"
  170. " )\n"
  171. )
  172. decode_new = (
  173. " pool_topk = torch.full(\n"
  174. " (num_rows, select_k), -1, dtype=torch.int32, device=logits.device\n"
  175. " )\n"
  176. )
  177. results = [
  178. apply_once(path, prefill_old, prefill_new, "indexer prefill -1"),
  179. apply_once(path, decode_old, decode_new, "indexer decode -1"),
  180. ]
  181. announce("indexer top-k -1 init (prefill + decode)", results)
  182. def bake_kpool_pid_clamp() -> None:
  183. old = " hist_out = tl.where(pid >= 0, hist_val, -1)\n"
  184. new = " hist_out = tl.where((pid >= 0) & (pid < pool_len), hist_val, -1)\n"
  185. r = apply_once(
  186. VLLM / "models/glm5next/nvidia/ops/kpool_compress.py",
  187. old,
  188. new,
  189. "kpool pid clamp",
  190. )
  191. announce("kpool pid clamp to pool_len", [r])
  192. def apply_sm90() -> None:
  193. bake_sm90_on_capability_12()
  194. bake_sm90_wrapper_for_gb10()
  195. bake_pdl_off_sm12()
  196. bake_indexer_topk_neg1()
  197. bake_kpool_pid_clamp()
  198. bake_fa2_fp8_cta_tile()
  199. bake_fa2_fp8_sm12_gate()
  200. # ---------------------------------------------------------------------------
  201. # sm120 — unused packed-cache path (python3 glm53-flash_SM121.py sm120)
  202. # Marker-based _once, not apply_once. Not invoked by the Dockerfile.
  203. # ---------------------------------------------------------------------------
  204. MARKER = "GLM53_NOPE_FP8_DS_MLA_PAD"
  205. TOPK_MARKER = "GLM53_SM120_TOPK2048"
  206. ROOT: Path | None = None
  207. def _once(path: Path, needle: str, replacement: str, marker: str) -> str:
  208. text = path.read_text()
  209. if marker in text:
  210. return "already"
  211. if needle not in text:
  212. raise SystemExit(f"{path}: expected snippet not found")
  213. path.write_text(text.replace(needle, replacement, 1))
  214. return "patched"
  215. def packed_cache_write() -> str:
  216. assert ROOT is not None
  217. path = ROOT / "v1/attention/backend.py"
  218. needle = """ from vllm import _custom_ops as ops
  219. ops.concat_and_cache_mla(
  220. kv_c_normed,
  221. k_pe.squeeze(1),
  222. kv_cache,
  223. slot_mapping.flatten(),
  224. kv_cache_dtype=kv_cache_dtype,
  225. scale=k_scale,
  226. )
  227. """
  228. replacement = f""" from vllm import _custom_ops as ops
  229. # {MARKER}: fp8_ds_mla pages always store 64-dim RoPE. NoPE models
  230. # (qk_rope_head_dim=0) pad zeros so concat_and_cache_mla accepts them.
  231. pe = k_pe.squeeze(1)
  232. if kv_cache_dtype == "fp8_ds_mla" and pe.shape[-1] == 0:
  233. pe = pe.new_zeros(*pe.shape[:-1], 64)
  234. ops.concat_and_cache_mla(
  235. kv_c_normed,
  236. pe,
  237. kv_cache,
  238. slot_mapping.flatten(),
  239. kv_cache_dtype=kv_cache_dtype,
  240. scale=k_scale,
  241. )
  242. """
  243. return _once(path, needle, replacement, MARKER)
  244. def packed_sm120_decode() -> str:
  245. assert ROOT is not None
  246. path = ROOT / "v1/attention/backends/mla/flashinfer_mla_sparse_sm120.py"
  247. needle = """ if isinstance(q, tuple):
  248. q = torch.cat(q, dim=-1)
  249. num_actual_toks = q.shape[0]
  250. """
  251. replacement = f""" if isinstance(q, tuple):
  252. q = torch.cat(q, dim=-1)
  253. # {MARKER}: match the 656 B fp8_ds_mla page (ckv + 64-dim kpe).
  254. rope_dim = self.qk_rope_head_dim
  255. if rope_dim == 0:
  256. q = torch.nn.functional.pad(q, (0, 64))
  257. rope_dim = 64
  258. num_actual_toks = q.shape[0]
  259. """
  260. status = _once(path, needle, replacement, MARKER)
  261. if status == "already":
  262. return status
  263. needle2 = """ qk_nope_head_dim=self.qk_nope_head_dim,
  264. kv_lora_rank=self.kv_lora_rank,
  265. qk_rope_head_dim=self.qk_rope_head_dim,
  266. """
  267. replacement2 = """ qk_nope_head_dim=self.qk_nope_head_dim,
  268. kv_lora_rank=self.kv_lora_rank,
  269. qk_rope_head_dim=rope_dim,
  270. """
  271. text = path.read_text()
  272. if needle2 not in text:
  273. raise SystemExit(f"{path}: flashinfer rope-dim call site not found")
  274. path.write_text(text.replace(needle2, replacement2, 1))
  275. return "patched"
  276. def packed_sm120_topk() -> str:
  277. """Unused packed path: SM120 helper still wants topk=2048."""
  278. assert ROOT is not None
  279. path = ROOT / "v1/attention/backends/mla/flashinfer_mla_sparse_sm120.py"
  280. needle = """ output = q.new_empty(
  281. (num_actual_toks, self.num_heads, self.kv_lora_rank),
  282. dtype=q.dtype,
  283. )
  284. """
  285. replacement = f""" # {TOPK_MARKER}: FlashInfer SM120 v32/GLM wants block_tables (N, 1, 2048).
  286. if topk_indices_physical.shape[-1] != 2048:
  287. if topk_indices_physical.shape[-1] > 2048:
  288. topk_indices_physical = topk_indices_physical[..., :2048].contiguous()
  289. else:
  290. pad = 2048 - topk_indices_physical.shape[-1]
  291. topk_indices_physical = torch.nn.functional.pad(
  292. topk_indices_physical, (0, pad), value=-1
  293. )
  294. output = q.new_empty(
  295. (num_actual_toks, self.num_heads, self.kv_lora_rank),
  296. dtype=q.dtype,
  297. )
  298. """
  299. status = _once(path, needle, replacement, TOPK_MARKER)
  300. if status == "already":
  301. return status
  302. text = path.read_text()
  303. text = text.replace(
  304. "max_seq_len=attn_metadata.topk_tokens,",
  305. "max_seq_len=2048,",
  306. 1,
  307. )
  308. text = text.replace(
  309. "sparse_mla_top_k=attn_metadata.topk_tokens,",
  310. "sparse_mla_top_k=2048,",
  311. 1,
  312. )
  313. path.write_text(text)
  314. return "patched"
  315. def packed_skip_fi_warmup() -> str:
  316. """Unused packed path: fused_moe gemm1 autotune OOMs rank 0 on GB10."""
  317. assert ROOT is not None
  318. path = ROOT / "model_executor/warmup/kernel_warmup.py"
  319. needle = """ flashinfer_sparse_mla_decode_autotune_warmup(worker)
  320. deepseek_v4_sparse_mla_attention_warmup(worker)
  321. """
  322. marker = "GLM53_SKIP_FI_SPARSE_WARMUP"
  323. replacement = f""" # {marker}: skip FlashInfer SM120 sparse-MLA autotune (kills rank 0 on GB10).
  324. deepseek_v4_sparse_mla_attention_warmup(worker)
  325. """
  326. return _once(path, needle, replacement, marker)
  327. def packed_skip_fi_autotune() -> str:
  328. assert ROOT is not None
  329. path = ROOT / "model_executor/warmup/kernel_warmup.py"
  330. marker = "GLM53_SKIP_FI_AUTOTUNE"
  331. needle = ''' from flashinfer.autotuner import AutoTuner, set_autotune_process_group
  332. '''
  333. replacement = f''' # {marker}: fused_moe gemm1/gemm2 autotune kills rank 0 on GB10.
  334. logger.info_once("Skipping FlashInfer autotune on SM121")
  335. return
  336. from flashinfer.autotuner import AutoTuner, set_autotune_process_group
  337. '''
  338. return _once(path, needle, replacement, marker)
  339. def apply_sm120() -> None:
  340. global ROOT
  341. import vllm
  342. ROOT = Path(vllm.__file__).resolve().parent
  343. cache = packed_cache_write()
  344. decode = packed_sm120_decode()
  345. topk = packed_sm120_topk()
  346. warmup = packed_skip_fi_warmup()
  347. autotune = packed_skip_fi_autotune()
  348. print(
  349. "%s packed-path (not baked) cache_write=%s sm120_decode=%s "
  350. "sm120_topk=%s skip_fi_warmup=%s skip_fi_autotune=%s"
  351. % (LOG, cache, decode, topk, warmup, autotune)
  352. )
  353. def main(argv: list[str] | None = None) -> None:
  354. mode = parse_mode(sys.argv[1:] if argv is None else argv)
  355. if mode == "sm90":
  356. apply_sm90()
  357. return
  358. apply_sm120()
  359. if __name__ == "__main__":
  360. main()