patch_4p67.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. #!/usr/bin/env python3
  2. """Teach the glm5next model classes their fused projection names.
  3. local-inference-lab/GLM-5.3-Flash-NVFP4-4p67 is the first checkpoint of this
  4. family that QUANTIZES the individual attention projections (per-shard
  5. MXFP8 entries `self_attn.q_proj` … in hf_quant_config). vLLM fuses those
  6. shards into single modules (`in_proj_qkvbfg_a`, `fused_qkv_a_proj`,
  7. `wk_weights_proj`), and the ModelOpt MIXED_PRECISION config resolves a
  8. fused module's quant_algo through its shards ONLY via the model class's
  9. `packed_modules_mapping` (handed to the quant config by
  10. model_executor/model_loader/utils.py). Without it every fused linear comes
  11. up UnquantizedLinearMethod and weight loading dies with
  12. KeyError: 'layers.0.self_attn.in_proj_qkvbfg_a.weight_scale'
  13. Anchors self-assert; idempotent.
  14. """
  15. import py_compile
  16. import sys
  17. B = "/usr/local/lib/python3.12/dist-packages/vllm/models/glm5next/nvidia"
  18. MAPPING = '''
  19. # NVFP4-4p67: quantized per-shard attention/shared-expert projections
  20. # are fused by vLLM; expose the shard names so the ModelOpt
  21. # MIXED_PRECISION config can resolve the fused module's quant_algo
  22. # (see model_loader/utils.py packed_modules_mapping pass-through).
  23. packed_modules_mapping = {
  24. "gate_up_proj": ["gate_proj", "up_proj"],
  25. "in_proj_qkvbfg_a": ["q_proj", "k_proj", "v_proj",
  26. "b_proj", "f_a_proj", "g_a_proj"],
  27. "fused_qkv_a_proj": ["q_a_proj", "kv_a_proj_with_mqa"],
  28. "wk_weights_proj": ["wk", "weights_proj"],
  29. }
  30. '''
  31. # CRITICAL: it must live on Glm5NextForConditionalGeneration (the registered
  32. # class). configure_quant_config reads BOTH hf_to_vllm_mapper and
  33. # packed_modules_mapping off that class; Glm4v's inherited mapper rewrites
  34. # the config's quantized_layers keys into vLLM spelling, and a class-level
  35. # mapping on the inner CausalLM would SHADOW the inherited Glm4v mapper's
  36. # companion config path. MTP is constructed as a separate root model, so it
  37. # needs its own copy.
  38. MAPPING_TARGETS = [
  39. (
  40. f"{B}/model.py",
  41. "class Glm5NextForCausalLM(\n"
  42. " nn.Module, HasInnerState, SupportsPP, MixtureOfExperts, IsHybrid\n"
  43. "):\n",
  44. ),
  45. (
  46. f"{B}/model.py",
  47. "class Glm5NextForConditionalGeneration(\n"
  48. " Glm4vForConditionalGeneration, HasInnerState, IsHybrid\n"
  49. "):\n",
  50. ),
  51. (
  52. f"{B}/mtp.py",
  53. "class Glm5NextMTP(nn.Module, DeepseekV2MixtureOfExperts):\n",
  54. ),
  55. ]
  56. for path, anchor in MAPPING_TARGETS:
  57. src = open(path).read()
  58. # count INSERTED BLOCKS (marker appears once per patch), not the plain
  59. # string (which also appears in comments and inherited bases).
  60. want = sum(1 for p, _ in MAPPING_TARGETS if p == path)
  61. if src.count('"wk_weights_proj": ["wk", "weights_proj"],') >= want:
  62. print(f"already patched: {path}")
  63. continue
  64. n = src.count(anchor)
  65. assert n == 1, f"anchor {n}x in {path} — day-0 image drifted, review me"
  66. open(path, "w").write(src.replace(anchor, anchor + MAPPING, 1))
  67. py_compile.compile(path, doraise=True)
  68. print(f"patched: {path}")
  69. # KDA strip gate. day-0 KDA construction FORCIBLY unquantizes its
  70. # projections ("BF16 even in fp8 checkpoints") by nulling
  71. # vllm_config.quant_config around super().__init__ — which is exactly the
  72. # 4p67 assumption-break: this checkpoint stores MXFP8 scales for
  73. # self_attn.{q,k,v,b,f_a,g_a}_proj, so the fused linear comes up BF16 and
  74. # the loader dies routing those scales. Make the strip conditional on the
  75. # checkpoint genuinely lacking KDA shard scales.
  76. KDA_STRIP_OLD = ''' saved_quant_config = vllm_config.quant_config
  77. vllm_config.quant_config = None
  78. super().__init__(config, vllm_config, prefix)
  79. vllm_config.quant_config = saved_quant_config
  80. '''
  81. KDA_STRIP_NEW = ''' saved_quant_config = vllm_config.quant_config
  82. _qc = vllm_config.quant_config
  83. _kda_quantized = _qc is not None and any(
  84. ("%s.%s" % (prefix, _s)) in getattr(_qc, "quantized_layers", {})
  85. for _s in (
  86. "q_proj", "k_proj", "v_proj",
  87. "b_proj", "f_a_proj", "g_a_proj",
  88. )
  89. )
  90. if not _kda_quantized:
  91. # day-0 checkpoints keep the KDA projections BF16 (no scales
  92. # stored for them); NVFP4-4p67 stores MXFP8 -> keep quant.
  93. vllm_config.quant_config = None
  94. super().__init__(config, vllm_config, prefix)
  95. vllm_config.quant_config = saved_quant_config
  96. '''
  97. kda_path = f"{B}/kda.py"
  98. src = open(kda_path).read()
  99. if "_kda_quantized" in src:
  100. print(f"already patched: {kda_path}")
  101. else:
  102. n = src.count(KDA_STRIP_OLD)
  103. assert n == 1, f"KDA strip anchor {n}x in {kda_path} — review me"
  104. open(kda_path, "w").write(src.replace(KDA_STRIP_OLD, KDA_STRIP_NEW, 1))
  105. py_compile.compile(kda_path, doraise=True)
  106. print(f"patched: {kda_path}")
  107. # (2) Indexer wk_weights_proj: attention.py hardcodes quant_config=None
  108. # (legacy path dequantizes FP8 wk to BF16 on load). Gate it the same way as
  109. # KDA: if the checkpoint declares MXFP8 for indexer.wk / indexer.weights_proj
  110. # (NVFP4-4p67), build the fused linear QUANTIZED and let the stacked loader
  111. # route weights+scales.
  112. IDX_OLD = ''' self.wk_weights_proj = MergedColumnParallelLinear(
  113. hidden_size,
  114. [self.head_dim, self.n_head],
  115. bias=False,
  116. quant_config=None,
  117. '''
  118. IDX_NEW = ''' _idx_quantized = quant_config is not None and any(
  119. ("%s.%s" % (prefix, _s)) in getattr(quant_config, "quantized_layers", {})
  120. for _s in ("wk", "weights_proj")
  121. )
  122. self.wk_weights_proj = MergedColumnParallelLinear(
  123. hidden_size,
  124. [self.head_dim, self.n_head],
  125. bias=False,
  126. quant_config=(quant_config if _idx_quantized else None),
  127. '''
  128. apath = f"{B}/attention.py"
  129. src = open(apath).read()
  130. if "_idx_quantized" in src:
  131. print(f"already patched: {apath}")
  132. else:
  133. n = src.count(IDX_OLD)
  134. assert n == 1, f"indexer anchor {n}x in {apath} — review me"
  135. open(apath, "w").write(src.replace(IDX_OLD, IDX_NEW, 1))
  136. py_compile.compile(apath, doraise=True)
  137. print(f"patched: {apath}")
  138. # (3) Loader shims: legacy FP8 dequant helpers must not eat MXFP8 tensors
  139. # (4p67 names scales `weight_scale` — the wk shim's is_weight test would
  140. # swallow the fp8 weight and then drop the scale). Skip the shims when the
  141. # target fused param is quantized (has a weight_scale param).
  142. model_path = f"{B}/model.py"
  143. src = open(model_path).read()
  144. WK_OLD = '''def _try_load_fp8_indexer_wk(name, tensor, buf, params_dict, loaded_params):
  145. if "indexer.wk." not in name or "wk_weights" in name:
  146. return False
  147. '''
  148. WK_NEW = '''def _try_load_fp8_indexer_wk(name, tensor, buf, params_dict, loaded_params):
  149. if "indexer.wk." not in name or "wk_weights" in name:
  150. return False
  151. if (f"{name.rsplit('.wk.', 1)[0]}.wk_weights_proj.weight_scale"
  152. in params_dict):
  153. # MXFP8 checkpoint: fused param is quantized, stacked loader owns it.
  154. return False
  155. '''
  156. ATTN_OLD = ''' target_w = f"{layer_prefix}.{target_base}.weight"
  157. target_s = f"{layer_prefix}.{target_base}.weight_scale_inv"
  158. '''
  159. ATTN_NEW = ''' target_w = f"{layer_prefix}.{target_base}.weight"
  160. target_s = f"{layer_prefix}.{target_base}.weight_scale_inv"
  161. if f"{layer_prefix}.{target_base}.weight_scale" in params_dict:
  162. # MXFP8 checkpoint: param is quantized, stacked loader owns it.
  163. return False
  164. '''
  165. if "stacked loader owns it" in src:
  166. print(f"already patched: {model_path} (wk shim)")
  167. else:
  168. n = src.count(WK_OLD)
  169. assert n == 1, f"wk-shim anchor {n}x in {model_path} — review me"
  170. src = src.replace(WK_OLD, WK_NEW, 1)
  171. open(model_path, "w").write(src)
  172. py_compile.compile(model_path, doraise=True)
  173. print(f"patched: {model_path} (wk shim)")
  174. if "MXFP8 checkpoint: param is quantized" in src:
  175. print(f"already patched: {model_path} (attn shim)")
  176. else:
  177. src = open(model_path).read()
  178. n = src.count(ATTN_OLD)
  179. assert n == 1, f"attn-shim anchor {n}x in {model_path} — review me"
  180. src = src.replace(ATTN_OLD, ATTN_NEW, 1)
  181. open(model_path, "w").write(src)
  182. py_compile.compile(model_path, doraise=True)
  183. print(f"patched: {model_path} (attn shim)")
  184. def helper(name, uses):
  185. h = (
  186. "def _glm5_q_layers(quant_config):\n"
  187. " # quantized_layers keys are HF-spelled at module-construction\n"
  188. " # time and vLLM-spelled after apply_vllm_mapper; test both.\n"
  189. " return set(getattr(quant_config, 'quantized_layers', {}) or {})\n"
  190. "\n"
  191. "\n"
  192. "def _glm5_quantized(quant_config, prefix, shards):\n"
  193. " layers = _glm5_q_layers(quant_config)\n"
  194. " if not layers:\n"
  195. " return False\n"
  196. " tail = prefix\n"
  197. " for lead in (\n"
  198. " 'language_model.model.', 'model.language_model.',\n"
  199. " 'model.', 'language_model.',\n"
  200. " ):\n"
  201. " if tail.startswith(lead):\n"
  202. " tail = tail[len(lead):]\n"
  203. " break\n"
  204. " return any(\n"
  205. " k.endswith(f'.{tail}.{s}') or k == f'{tail}.{s}'\n"
  206. " for s in shards\n"
  207. " for k in layers\n"
  208. " )\n\n\n"
  209. )
  210. # place right before the first class that needs it
  211. anchor = uses
  212. return h, anchor
  213. for path, defs_marker, insert_before in [
  214. (f"{B}/kda.py", "_glm5_quantized", "class Glm5NextLinearAttention"),
  215. (f"{B}/attention.py", "_glm5_quantized", "class Indexer(nn.Module):"),
  216. ]:
  217. src = open(path).read()
  218. if defs_marker in src:
  219. print(f"helper already in {path}")
  220. continue
  221. idx = src.index(insert_before)
  222. helper_src, _ = helper(path, insert_before)
  223. src = src[:idx] + helper_src + src[idx:]
  224. open(path, "w").write(src)
  225. py_compile.compile(path, doraise=True)
  226. print(f"helper added: {path}")
  227. # rewrite the two constructor gates to use the both-spellings helper
  228. GATES = [
  229. (
  230. f"{B}/kda.py",
  231. ''' _kda_quantized = _qc is not None and any(
  232. ("%s.%s" % (prefix, _s)) in getattr(_qc, "quantized_layers", {})
  233. for _s in (
  234. "q_proj", "k_proj", "v_proj",
  235. "b_proj", "f_a_proj", "g_a_proj",
  236. )
  237. )
  238. ''',
  239. " _kda_quantized = _glm5_quantized(\n"
  240. " _qc, prefix,\n"
  241. ' ("q_proj", "k_proj", "v_proj",\n'
  242. ' "b_proj", "f_a_proj", "g_a_proj"),\n'
  243. " )\n",
  244. ),
  245. (
  246. f"{B}/attention.py",
  247. ''' _idx_quantized = quant_config is not None and any(
  248. ("%s.%s" % (prefix, _s)) in getattr(quant_config, "quantized_layers", {})
  249. for _s in ("wk", "weights_proj")
  250. )
  251. ''',
  252. " _idx_quantized = _glm5_quantized(\n"
  253. ' quant_config, prefix, ("wk", "weights_proj")\n'
  254. " )\n"
  255. " import logging as _lg\n"
  256. ' _lg.getLogger("vllm").warning(\n'
  257. ' "4p67 IDX GATE prefix=%s q=%s qc=%s n=%s",\n'
  258. ' prefix, _idx_quantized,\n'
  259. " type(quant_config).__name__,\n"
  260. ' len(getattr(quant_config, "quantized_layers", {}) or {}),\n'
  261. " )\n",
  262. ),
  263. ]
  264. for path, old, new in GATES:
  265. src = open(path).read()
  266. if "_glm5_quantized(" in src.split("def _glm5_quantized", 1)[-1]:
  267. print(f"gate already rewritten: {path}")
  268. continue
  269. n = src.count(old)
  270. assert n == 1, f"gate anchor {n}x in {path} — review me"
  271. open(path, "w").write(src.replace(old, new, 1))
  272. py_compile.compile(path, doraise=True)
  273. print(f"gate rewritten: {path}")
  274. # (5) DecoderLayer hardcodes quant_config=None for MLA attention ("BF16 in
  275. # checkpoint" — again broken by 4p67). Gate it; helper lives in kda.py
  276. # (always importable; model.py already imports from it? if not, guard).
  277. MLA_OLD = " quant_config=None, # MLA projections are BF16 in checkpoint\n"
  278. MLA_NEW = """ quant_config=(
  279. quant_config
  280. if _glm5_quantized(
  281. quant_config,
  282. f"{prefix}.self_attn",
  283. ("q_a_proj", "kv_a_proj_with_mqa"),
  284. )
  285. else None
  286. ), # 4p67: quantized when checkpoint stores MLA scales
  287. """
  288. mla_path = f"{B}/model.py"
  289. src = open(mla_path).read()
  290. if "4p67: quantized when checkpoint stores MLA scales" in src:
  291. print(f"already patched: {mla_path} (mla gate)")
  292. else:
  293. n = src.count(MLA_OLD)
  294. assert n == 1, f"mla-gate anchor {n}x in {mla_path} — review me"
  295. src = src.replace(MLA_OLD, MLA_NEW, 1)
  296. if "from .kda import" in src:
  297. import re as _re
  298. src = _re.sub(
  299. r"from \.kda import (\w+)",
  300. r"from .kda import \1, _glm5_quantized",
  301. src,
  302. count=1,
  303. )
  304. else:
  305. raise AssertionError(
  306. f"no 'from .kda import' in {mla_path} — add import explicitly"
  307. )
  308. assert "_glm5_quantized" in "\n".join(src.splitlines()[:120]), "import missing"
  309. open(mla_path, "w").write(src)
  310. py_compile.compile(mla_path, doraise=True)
  311. print(f"patched: {mla_path} (mla gate)")
  312. print("patch_4p67 OK")