mla_ref.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. #!/usr/bin/env python3
  2. """Offline decode-step reference for the MLA layers in a glm53_step_tap dump.
  3. Two halves, per MLA layer and tapped row:
  4. up-projection `mla.out` (the SM90 kernel's latent output, [H, kv_lora_rank])
  5. times the checkpoint's W_UV (v half of kv_b_proj, per head)
  6. against the dumped `mla.attn_out`. Needs only today's dumps.
  7. kernel softmax(scale * q_nope . K_j) V_j over the latent rows the
  8. kernel attended (K_j = V_j = fp8 cache row * k_scale), against
  9. the dumped `mla.out`. Needs `mla.q_nope`, `mla.k_scale` and
  10. `mla.kv_rows` from the extended tap (extra dict in the blob).
  11. python3 mla_ref.py --ckpt /ckpt /hit/vec-rank0-step3257.pt:92
  12. """
  13. import argparse
  14. import json
  15. import os
  16. import re
  17. import torch
  18. from safetensors import safe_open
  19. from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import (
  20. dequantize_to_dtype,
  21. )
  22. dev = torch.device("cuda")
  23. PREFIX = "model.language_model.layers."
  24. class Ckpt:
  25. def __init__(self, d):
  26. self.d = d
  27. cfg = json.load(open(os.path.join(d, "config.json")))
  28. self.cfg = cfg.get("text_config", cfg)
  29. self.map = json.load(open(os.path.join(d, "model.safetensors.index.json")))["weight_map"]
  30. self._open = {}
  31. def get(self, name):
  32. f = self._open.get(self.map[name])
  33. if f is None:
  34. f = safe_open(os.path.join(self.d, self.map[name]), "pt", device="cuda")
  35. self._open[self.map[name]] = f
  36. return f.get_tensor(name)
  37. def has(self, name):
  38. return name in self.map
  39. def linear(self, base):
  40. """fp32 [out, in] whether the layer is NVFP4 or BF16 in the checkpoint."""
  41. if self.has(base + ".weight_scale"):
  42. w = self.get(base + ".weight")
  43. s = self.get(base + ".weight_scale")
  44. g = self.get(base + ".weight_scale_2").reshape(()).float()
  45. return dequantize_to_dtype(w[None], s[None], g[None], torch.float32, 16, swizzle=False)[0], "nvfp4"
  46. return self.get(base + ".weight").float(), "bf16"
  47. def rel(a, b):
  48. a, b = a.float().reshape(-1), b.float().reshape(-1)
  49. return f"l2 {float((a - b).norm() / (b.norm() + 1e-12)):.2e} max {float((a - b).abs().max() / (b.abs().max() + 1e-12)):.2e}"
  50. def main():
  51. ap = argparse.ArgumentParser()
  52. ap.add_argument("--ckpt", required=True)
  53. ap.add_argument("--layers", default="")
  54. ap.add_argument("--kv-vs", default="", help="another dump: compare the rows this step READ (extra kv_rows) "
  55. "against the cache_row that dump WROTE at the same positions (e.g. the prefill replay)")
  56. ap.add_argument("specs", nargs="+")
  57. args = ap.parse_args()
  58. ck = Ckpt(args.ckpt)
  59. cfg = ck.cfg
  60. H_all, nope, vd, lora = (int(cfg[k]) for k in ("num_attention_heads", "qk_nope_head_dim", "v_head_dim", "kv_lora_rank"))
  61. rope = int(cfg.get("qk_rope_head_dim") or 0)
  62. scale = (nope + rope) ** -0.5
  63. for spec in args.specs:
  64. path, _, pos = spec.rpartition(":")
  65. m = re.search(r"rank(-?\d+)", os.path.basename(path))
  66. rank = int(m.group(1)) if m else 0
  67. b = torch.load(path, map_location="cpu")
  68. rows = [i for i, r in enumerate(b["rows"]) if r["pos"] == int(pos)]
  69. if not rows:
  70. print(f"{spec}: no row at pos {pos}")
  71. continue
  72. i = rows[-1]
  73. cps, extra = b["cps"], b.get("extra", {})
  74. layers = [int(x) for x in args.layers.split(",") if x] or sorted(
  75. int(k[1:].split(".")[0]) for k in cps if k.endswith(".mla.out")
  76. )
  77. print(f"\n== {os.path.basename(path)} pos {pos} rank {rank} ==")
  78. print(f"{'layer':6s} {'kv_b':6s} {'up-proj: attn_out vs ref':28s} {'kernel: out vs ref':28s} rows |out|max")
  79. for L in layers:
  80. out = cps[f"L{L}.mla.out"][i].to(dev).float()
  81. H = out.numel() // lora
  82. out = out.view(H, lora)
  83. attn = cps[f"L{L}.mla.attn_out"][i].to(dev).float().view(H, vd)
  84. W, kind = ck.linear(f"{PREFIX}{L}.self_attn.kv_b_proj")
  85. W = W.view(H_all, nope + vd, lora)[rank * H : (rank + 1) * H]
  86. W_UV = W[:, nope:, :] # [H, vd, lora]
  87. attn_ref = torch.einsum("hl,hvl->hv", out, W_UV)
  88. up = rel(attn, attn_ref)
  89. kern = "(needs q_nope/kv_rows from the extended tap)"
  90. nrows = "-"
  91. qn_key = f"L{L}.mla.q_nope"
  92. kv_key = f"L{L}.mla.kv_rows"
  93. if qn_key in cps and kv_key in extra:
  94. q_nope = cps[qn_key][i].to(dev).float().view(H, lora)
  95. kv = extra[kv_key][i].to(dev)
  96. ks = float(extra.get(f"L{L}.mla.k_scale", 1.0))
  97. if kv.dtype == torch.uint8:
  98. kv = kv.view(torch.float8_e4m3fn)
  99. latent = kv.float() * ks # [N, lora]
  100. logits = torch.einsum("hl,nl->hn", q_nope, latent) * scale
  101. p = torch.softmax(logits, dim=-1)
  102. out_ref = p @ latent
  103. kern = rel(out, out_ref)
  104. nrows = str(latent.shape[0])
  105. # fp8 KV write side: the cache row read back this step vs the bf16 latent written
  106. wr = ""
  107. cr_key, kvc_key = f"L{L}.mla.cache_row", f"L{L}.mla.kvc"
  108. if cr_key in cps and kvc_key in cps:
  109. cr = cps[cr_key][i].to(dev)
  110. kvc = cps[kvc_key][i].to(dev).float()
  111. if cr.dtype == torch.uint8:
  112. cr = cr.view(torch.float8_e4m3fn)
  113. crf = cr.float()
  114. ks = float(extra.get(f"L{L}.mla.k_scale", 1.0))
  115. fit = float((crf * kvc).sum() / ((crf * crf).sum() + 1e-12)) # least-squares scale
  116. wr = f"kv write: {rel(crf * ks, kvc)} (k_scale {ks:g}, ls-fit {fit:.4g})"
  117. print(f"L{L:<5d} {kind:6s} {up:28s} {kern:28s} {nrows:5s} {float(out.abs().max()):.3f} {wr}")
  118. # rows this step read vs rows another run wrote at the same positions
  119. if args.kv_vs and kv_key in extra:
  120. other = torch.load(args.kv_vs, map_location="cpu")
  121. ocr = other["cps"].get(cr_key)
  122. topk = cps[f"L{L}.mla.topk"][i]
  123. kv = extra[kv_key][i]
  124. if kv.dtype == torch.uint8:
  125. kv = kv.view(torch.float8_e4m3fn)
  126. kvf = kv.float()
  127. worst, bad = 0.0, []
  128. for j in range(kvf.shape[0]):
  129. p = int(topk[j])
  130. orow = [k for k, r in enumerate(other["rows"]) if r["pos"] == p]
  131. if not orow or ocr is None:
  132. continue
  133. o = ocr[orow[-1]]
  134. o = (o.view(torch.float8_e4m3fn) if o.dtype == torch.uint8 else o).float()
  135. d = float((kvf[j] - o).norm() / (o.norm() + 1e-12))
  136. worst = max(worst, d)
  137. if d > 0.5:
  138. bad.append((p, round(d, 3)))
  139. print(f" kv rows read vs {os.path.basename(args.kv_vs)} rows written: worst rel {worst:.3f}; rows > 0.5: {bad[:10]}")
  140. if __name__ == "__main__":
  141. main()