hc_ref.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. #!/usr/bin/env python3
  2. """Offline reference for the mHC ops in a DEEP glm53_step_tap dump.
  3. The dump holds every mHC op call's inputs (by keyword: x, residual,
  4. post_layer_mix, comb_res_mix) and outputs (residual, post, comb, layer input)
  5. per layer: `hc_pre` (layer 0), `hc_fused` (attn pre, fused with the previous
  6. layer's post), `hc_fused@1` (ffn pre) and `hc_post` (last layer). This replays
  7. each call with vLLM's own torch implementation (mhc_pre_torch / mhc_post_torch)
  8. on the checkpoint's hc_* parameters and reports the relative difference of
  9. every output. The CUDA op fuses the RMSNorm into the layer input, which the
  10. torch reference does not, so the norm is applied here before comparing out3.
  11. python3 hc_ref.py --ckpt /ckpt /hit/vec-rank0-step3257.pt:92 /hit/vec-rank0-step3283.pt:92
  12. """
  13. import argparse
  14. import json
  15. import os
  16. import torch
  17. from safetensors import safe_open
  18. import vllm.model_executor.kernels.mhc as mk
  19. dev = torch.device("cuda")
  20. PREFIX = "model.language_model.layers."
  21. class Ckpt:
  22. def __init__(self, d):
  23. self.d = d
  24. cfg = json.load(open(os.path.join(d, "config.json")))
  25. self.cfg = cfg.get("text_config", cfg)
  26. self.map = json.load(open(os.path.join(d, "model.safetensors.index.json")))["weight_map"]
  27. self._open = {}
  28. def get(self, name):
  29. f = self._open.get(self.map[name])
  30. if f is None:
  31. f = safe_open(os.path.join(self.d, self.map[name]), "pt", device="cuda")
  32. self._open[self.map[name]] = f
  33. return f.get_tensor(name)
  34. def rel(a, b):
  35. a, b = a.float().reshape(-1), b.float().reshape(-1)
  36. return float((a - b).norm() / (b.norm() + 1e-12))
  37. def rmsnorm(x, w, eps):
  38. xf = x.float()
  39. return (xf * torch.rsqrt(xf.pow(2).mean(-1, keepdim=True) + eps) * w.float()).to(x.dtype)
  40. def main():
  41. ap = argparse.ArgumentParser()
  42. ap.add_argument("--ckpt", required=True)
  43. ap.add_argument("--thr", type=float, default=1e-2)
  44. ap.add_argument("specs", nargs="+")
  45. args = ap.parse_args()
  46. ck = Ckpt(args.ckpt)
  47. cfg = ck.cfg
  48. n = int(cfg.get("mhc_num_residual_streams") or 4)
  49. hid = int(cfg["hidden_size"])
  50. rms_eps = float(cfg.get("rms_norm_eps", 1e-5))
  51. hc_eps = float(cfg.get("hc_eps") or 1e-6)
  52. post_mult = float(cfg.get("mhc_post_mult_value") or 2.0)
  53. sink = int(cfg.get("mhc_sinkhorn_iterations") or 20)
  54. for spec in args.specs:
  55. path, _, pos = spec.rpartition(":")
  56. b = torch.load(path, map_location="cpu")
  57. rows = [i for i, r in enumerate(b["rows"]) if r["pos"] == int(pos)]
  58. if not rows:
  59. print(f"{spec}: no row at pos {pos}")
  60. continue
  61. i = rows[-1]
  62. cps = b["cps"]
  63. g = lambda k: cps[k][i].to(dev)
  64. layers = sorted({int(k[1:].split(".")[0]) for k in cps if ".hc_" in k})
  65. print(f"\n== {os.path.basename(path)} pos {pos} (rows in dump: {len(b['rows'])}) ==")
  66. print(f"{'layer':6s} {'call':10s} {'residual':>10s} {'post':>10s} {'comb':>10s} {'x(normed)':>10s}")
  67. worst = 0.0
  68. for L in layers:
  69. P = f"{PREFIX}{L}."
  70. params = {
  71. "attn": (ck.get(P + "hc_attn_fn").float(), ck.get(P + "hc_attn_scale").float(), ck.get(P + "hc_attn_base").float(), ck.get(P + "input_layernorm.weight")),
  72. "ffn": (ck.get(P + "hc_ffn_fn").float(), ck.get(P + "hc_ffn_scale").float(), ck.get(P + "hc_ffn_base").float(), ck.get(P + "post_attention_layernorm.weight")),
  73. }
  74. for call, kind in (("hc_pre", "attn"), ("hc_fused", "attn"), ("hc_fused@1", "ffn")):
  75. key = f"L{L}.{call}.out0"
  76. if key not in cps:
  77. continue
  78. fn, sc, base, nw = params[kind]
  79. if call == "hc_pre":
  80. residual_cur = g(f"L{L}.hc_pre.in_residual").view(n, hid)
  81. res_out = None
  82. post_d, comb_d, x_d = g(key), g(f"L{L}.{call}.out1"), g(f"L{L}.{call}.out2")
  83. else:
  84. x = g(f"L{L}.{call}.in_x")
  85. residual = g(f"L{L}.{call}.in_residual").view(n, hid)
  86. post = g(f"L{L}.{call}.in_post_layer_mix").view(n, 1)
  87. comb = g(f"L{L}.{call}.in_comb_res_mix").view(n, n)
  88. residual_cur = mk.mhc_post_torch(x, residual, post, comb)
  89. res_out = g(key).view(n, hid)
  90. post_d, comb_d, x_d = g(f"L{L}.{call}.out1"), g(f"L{L}.{call}.out2"), g(f"L{L}.{call}.out3")
  91. post_r, comb_r, xin_r = mk.mhc_pre_torch(residual_cur, fn, sc, base, rms_eps, hc_eps, hc_eps, post_mult, sink)
  92. x_r = rmsnorm(xin_r.reshape(-1, hid), nw, rms_eps)
  93. e = [rel(res_out, residual_cur) if res_out is not None else float("nan"), rel(post_d, post_r), rel(comb_d, comb_r), rel(x_d, x_r)]
  94. worst = max(worst, *[v for v in e if v == v])
  95. flag = " <--" if any(v == v and v > args.thr for v in e) else ""
  96. print(f"L{L:<5d} {call:10s} {e[0]:10.2e} {e[1]:10.2e} {e[2]:10.2e} {e[3]:10.2e}{flag}")
  97. key = f"L{L}.hc_post.out0"
  98. if key in cps:
  99. x, residual = g(f"L{L}.hc_post.in0"), g(f"L{L}.hc_post.in1").view(n, hid)
  100. post, comb = g(f"L{L}.hc_post.in2").view(n, 1), g(f"L{L}.hc_post.in3").view(n, n)
  101. r = rel(g(key).view(n, hid), mk.mhc_post_torch(x, residual, post, comb))
  102. worst = max(worst, r)
  103. print(f"L{L:<5d} {'hc_post':10s} {r:10.2e}{' <--' if r > args.thr else ''}")
  104. print(f"worst relative difference: {worst:.3e} (threshold {args.thr})")
  105. if __name__ == "__main__":
  106. main()