mlp_ref.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. #!/usr/bin/env python3
  2. """Offline MLP/MoE reference for rows dumped by glm53_step_tap (DEEP mode).
  3. For each `file:pos` spec the tool takes the row's `L<L>.mlp.in0` (the MLP
  4. input, replicated) and `L<L>.mlp.out0` (the MLP output after the TP
  5. all-reduce) from the vector dump, recomputes the layer from the nvidia NVFP4
  6. checkpoint two ways, and reports how far the dumped output is from each:
  7. ref16 dequantised fp32 weights, fp32 activations, no activation quant
  8. (what a W4A16 path such as marlin approximates)
  9. ref4 same, but the activations are quantised to NVFP4 with the layer's
  10. static input_scale before each GEMM, exactly like the W4A4 cutlass
  11. path (scaled_fp4_quant + dequant)
  12. Layers < first_k_dense_replace (0..2) are dense Glm5NextMLP; the others are
  13. MoE: sigmoid scores, noaux_tc top-8 on scores + e_score_correction_bias,
  14. weights renormalised and scaled by routed_scaling_factor, plus the BF16
  15. shared expert. Between every pair of specs it also prints the input
  16. difference, the dumped output difference, and what ref16/ref4 make of the
  17. same two inputs, which separates legitimate sensitivity from a wrong path.
  18. python3 mlp_ref.py --ckpt /ckpt --layer 0 /hit/vec-rank0-step3257.pt:92 /hit/vec-rank0-step3283.pt:92
  19. """
  20. import argparse
  21. import json
  22. import os
  23. import torch
  24. from safetensors import safe_open
  25. from vllm import _custom_ops as ops
  26. from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import (
  27. dequantize_to_dtype,
  28. )
  29. dev = torch.device("cuda")
  30. PREFIX = "model.language_model.layers."
  31. class Ckpt:
  32. def __init__(self, d: str):
  33. self.d = d
  34. cfg = json.load(open(os.path.join(d, "config.json")))
  35. self.cfg = cfg.get("text_config", cfg)
  36. self.map = json.load(open(os.path.join(d, "model.safetensors.index.json")))["weight_map"]
  37. self._open = {}
  38. def get(self, name: str) -> torch.Tensor:
  39. shard = self.map[name]
  40. f = self._open.get(shard)
  41. if f is None:
  42. f = safe_open(os.path.join(self.d, shard), "pt", device="cuda")
  43. self._open[shard] = f
  44. return f.get_tensor(name)
  45. def has(self, name: str) -> bool:
  46. return name in self.map
  47. def fp4_linear(self, base: str):
  48. """Dequantised fp32 weight [out, in], and (weight_scale_2, input_scale)."""
  49. w = self.get(base + ".weight")
  50. s = self.get(base + ".weight_scale")
  51. g = self.get(base + ".weight_scale_2").reshape(()).float()
  52. a = self.get(base + ".input_scale").reshape(()).float()
  53. W = dequantize_to_dtype(w[None], s[None], g[None], torch.float32, 16, swizzle=False)[0]
  54. return W, float(g), float(a)
  55. def quant_act(x: torch.Tensor, input_scale: float) -> torch.Tensor:
  56. """NVFP4-quantise a [M, K] activation with a static global scale, then dequantise (fp32)."""
  57. q, sf = ops.scaled_fp4_quant(
  58. x.to(torch.bfloat16), torch.tensor(1.0 / input_scale, device=dev), is_sf_swizzled_layout=True
  59. )
  60. return dequantize_to_dtype(q, sf, torch.tensor(input_scale, device=dev), torch.float32, 16, swizzle=True)
  61. def swiglu(gate: torch.Tensor, up: torch.Tensor, limit: float) -> torch.Tensor:
  62. return torch.nn.functional.silu(gate.clamp(max=limit)) * up.clamp(-limit, limit)
  63. def mlp3(x: torch.Tensor, Wg, Wu, Wd, a_gu: float | None, a_d: float | None, limit: float) -> torch.Tensor:
  64. """gate/up/down with optional activation quant (a_* = input_scale, None = no quant)."""
  65. xq = quant_act(x, a_gu) if a_gu else x
  66. h = swiglu(xq @ Wg.T, xq @ Wu.T, limit)
  67. hq = quant_act(h, a_d) if a_d else h
  68. return hq @ Wd.T
  69. def dense_refs(ck: Ckpt, L: int, X: torch.Tensor, limit: float):
  70. base = f"{PREFIX}{L}.mlp."
  71. Wg, gg, ag = ck.fp4_linear(base + "gate_proj")
  72. Wu, gu, au = ck.fp4_linear(base + "up_proj")
  73. Wd, gd, ad = ck.fp4_linear(base + "down_proj")
  74. a_gu = max(ag, au) # vLLM's merged gate_up takes the max shard input_scale
  75. print(f"dense L{L}: input_scale gate {ag:.5g} up {au:.5g} (merged {a_gu:.5g}) down {ad:.5g}; "
  76. f"weight_scale_2 {gg:.4g}/{gu:.4g}/{gd:.4g}")
  77. r16 = mlp3(X, Wg, Wu, Wd, None, None, limit)
  78. r4 = mlp3(X, Wg, Wu, Wd, a_gu, ad, limit)
  79. return r16, r4, {}
  80. def moe_refs(ck: Ckpt, L: int, X: torch.Tensor, limit: float):
  81. cfg = ck.cfg
  82. E, K8, rsf = int(cfg["n_routed_experts"]), int(cfg["num_experts_per_tok"]), float(cfg["routed_scaling_factor"])
  83. base = f"{PREFIX}{L}.mlp."
  84. gw = ck.get(base + "gate.weight").float() # [E, hidden]
  85. bias = ck.get(base + "gate.e_score_correction_bias").float() if ck.has(base + "gate.e_score_correction_bias") else torch.zeros(E, device=dev)
  86. logits = X @ gw.T
  87. scores = torch.sigmoid(logits)
  88. choice = scores + bias
  89. top = torch.topk(choice, K8 + 1, dim=-1)
  90. ids = top.indices[:, :K8]
  91. margin = (top.values[:, K8 - 1] - top.values[:, K8]).tolist()
  92. w = torch.gather(scores, 1, ids)
  93. if cfg.get("norm_topk_prob", True):
  94. w = w / w.sum(-1, keepdim=True)
  95. w = w * rsf
  96. # global activation scales as the cutlass path uses them: max over experts
  97. a13 = max(float(ck.get(f"{base}experts.{e}.{p}.input_scale").reshape(())) for e in range(E) for p in ("gate_proj", "up_proj"))
  98. a2 = max(float(ck.get(f"{base}experts.{e}.down_proj.input_scale").reshape(())) for e in range(E))
  99. print(f"moe L{L}: global input_scale w13 {a13:.5g} w2 {a2:.5g}; routed_scaling {rsf}; renorm {cfg.get('norm_topk_prob')}")
  100. # shared expert: BF16 (in the quantisation ignore list)
  101. sg = ck.get(base + "shared_experts.gate_proj.weight").float()
  102. su = ck.get(base + "shared_experts.up_proj.weight").float()
  103. sd = ck.get(base + "shared_experts.down_proj.weight").float()
  104. shared = mlp3(X, sg, su, sd, None, None, limit)
  105. r16 = shared.clone()
  106. r4 = shared.clone()
  107. cache = {}
  108. for r in range(X.shape[0]):
  109. for j in range(K8):
  110. e = int(ids[r, j])
  111. if e not in cache:
  112. Wg, _, _ = ck.fp4_linear(f"{base}experts.{e}.gate_proj")
  113. Wu, _, _ = ck.fp4_linear(f"{base}experts.{e}.up_proj")
  114. Wd, _, _ = ck.fp4_linear(f"{base}experts.{e}.down_proj")
  115. cache[e] = (Wg, Wu, Wd)
  116. Wg, Wu, Wd = cache[e]
  117. xr = X[r : r + 1]
  118. r16[r] += w[r, j] * mlp3(xr, Wg, Wu, Wd, None, None, limit)[0]
  119. r4[r] += w[r, j] * mlp3(xr, Wg, Wu, Wd, a13, a2, limit)[0]
  120. info = {"ids": ids.tolist(), "w": [[round(v, 4) for v in row] for row in w.tolist()], "margin_8_vs_9": margin}
  121. return r16, r4, info
  122. def rel(a: torch.Tensor, b: torch.Tensor) -> str:
  123. l2 = float((a - b).norm() / (b.norm() + 1e-12))
  124. mx = float((a - b).abs().max() / (b.abs().max() + 1e-12))
  125. return f"l2 {l2:.4f} max {mx:.4f}"
  126. def main() -> int:
  127. ap = argparse.ArgumentParser()
  128. ap.add_argument("--ckpt", required=True)
  129. ap.add_argument("--layer", type=int, required=True)
  130. ap.add_argument("specs", nargs="+", help="vec.pt:pos")
  131. args = ap.parse_args()
  132. ck = Ckpt(args.ckpt)
  133. L = args.layer
  134. limit = float(ck.cfg.get("swiglu_limit") or 1e30)
  135. dense = L < int(ck.cfg.get("first_k_dense_replace", 0))
  136. names, X, Y = [], [], []
  137. for spec in args.specs:
  138. path, _, pos = spec.rpartition(":")
  139. b = torch.load(path, map_location="cpu")
  140. rows = [i for i, r in enumerate(b["rows"]) if r["pos"] == int(pos)]
  141. if not rows:
  142. print(f"{spec}: no row at pos {pos}")
  143. return 1
  144. i = rows[-1]
  145. names.append(f"{os.path.basename(path)}:{pos}")
  146. X.append(b["cps"][f"L{L}.mlp.in0"][i].float())
  147. Y.append(b["cps"][f"L{L}.mlp.out0"][i].float())
  148. X = torch.stack(X).to(dev)
  149. Y = torch.stack(Y).to(dev)
  150. with torch.no_grad():
  151. r16, r4, info = (dense_refs if dense else moe_refs)(ck, L, X, limit)
  152. print(f"\nlayer L{L} ({'dense' if dense else 'moe'}), swiglu_limit {limit}")
  153. for i, n in enumerate(names):
  154. extra = ""
  155. if info:
  156. extra = f" experts {info['ids'][i]} w {info['w'][i]} margin8/9 {info['margin_8_vs_9'][i]:.4f}"
  157. print(f"{n:34s} |in|max {float(X[i].abs().max()):.3f} |out|max {float(Y[i].abs().max()):.3f}{extra}")
  158. print(f" dumped vs ref16: {rel(Y[i], r16[i])} dumped vs ref4: {rel(Y[i], r4[i])} ref4 vs ref16 (quant loss): {rel(r4[i], r16[i])}")
  159. print("\npairs (a vs b):")
  160. for i in range(len(names)):
  161. for j in range(i + 1, len(names)):
  162. same = ""
  163. if info and info["ids"][i] != info["ids"][j]:
  164. same = f" EXPERT SETS DIFFER: {sorted(set(info['ids'][i]) ^ set(info['ids'][j]))}"
  165. print(f"{names[i]} vs {names[j]}{same}")
  166. print(f" input {rel(X[i], X[j])} | dumped out {rel(Y[i], Y[j])} | ref16 out {rel(r16[i], r16[j])} | ref4 out {rel(r4[i], r4[j])}")
  167. return 0
  168. if __name__ == "__main__":
  169. raise SystemExit(main())