hf_ref.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. #!/usr/bin/env python3
  2. """Independent ground truth for GLM-5.3-Flash: HF transformers' own glm5_next
  3. modeling code (torch-only paths: no fla, no causal_conv1d, no kernels hub, eager
  4. attention), fed the official FP8 checkpoint dequantised layer by layer.
  5. The text model is built on the meta device; each decoder layer's parameters are
  6. materialised on the GPU (FP8 e4m3 x 128x128 weight_scale_inv -> bf16) by a
  7. forward pre-hook right before the layer runs and released after it, so a
  8. 339-token forward of the 320 GB checkpoint fits in GB10 memory. One forward
  9. gives every position at once.
  10. Outputs (in OUT dir):
  11. hf_top5.json top-5 (token, logprob) at the requested positions
  12. hf_top5_all.json top-5 at every position, plus the logprob of the token the
  13. vLLM run actually produced at each generated index
  14. hf_layers.pt per-layer residual streams [4, 4096] at the watched
  15. positions, the final normed hidden, and the input ids
  16. python3 hf_ref.py --ckpt /ckpt --tapshot /hit/tapshot.json --out /out \
  17. --gen 310 --watch-gen 74,142,148,176,310
  18. """
  19. import argparse
  20. import gc
  21. import json
  22. import os
  23. import resource
  24. import time
  25. import torch
  26. import torch.nn.functional as F
  27. from safetensors import safe_open
  28. os.environ.setdefault("HF_HUB_OFFLINE", "1")
  29. os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
  30. dev = torch.device("cuda")
  31. BS = 128 # FP8 block size
  32. DTYPE = torch.bfloat16
  33. class Ckpt:
  34. def __init__(self, d):
  35. self.d = d
  36. self.map = json.load(open(os.path.join(d, "model.safetensors.index.json")))["weight_map"]
  37. self._open = {}
  38. def raw(self, name):
  39. f = self._open.get(self.map[name])
  40. if f is None:
  41. f = safe_open(os.path.join(self.d, self.map[name]), "pt", device="cpu")
  42. self._open[self.map[name]] = f
  43. return f.get_tensor(name)
  44. def has(self, name):
  45. return name in self.map
  46. def close(self):
  47. self._open.clear()
  48. def load(self, name):
  49. """bf16 tensor on the GPU; FP8 block-quantised weights are dequantised."""
  50. t = self.raw(name)
  51. if t.dtype == torch.float8_e4m3fn:
  52. s = self.raw(name[: -len(".weight")] + ".weight_scale_inv").to(dev).float()
  53. t = t.to(dev).float()
  54. out, inn = t.shape
  55. s = s.repeat_interleave(BS, 0)[:out].repeat_interleave(BS, 1)[:, :inn]
  56. return (t * s).to(DTYPE)
  57. return t.to(dev, DTYPE)
  58. def layer_tensor(ck, L, hf, E):
  59. P = f"model.language_model.layers.{L}."
  60. if hf.startswith("self_attn.forget_gate."):
  61. return ck.load(P + "self_attn." + hf.split("forget_gate.", 1)[1])
  62. if hf == "self_attn.conv1d.weight":
  63. return torch.cat([ck.load(P + f"self_attn.{c}_conv1d.weight") for c in "qkv"], 0)
  64. hc = {"attn_hc.fn": "hc_attn_fn", "attn_hc.base": "hc_attn_base", "attn_hc.scale": "hc_attn_scale",
  65. "ffn_hc.fn": "hc_ffn_fn", "ffn_hc.base": "hc_ffn_base", "ffn_hc.scale": "hc_ffn_scale"}
  66. if hf in hc:
  67. return ck.load(P + hc[hf])
  68. if hf == "mlp.experts.gate_up_proj":
  69. return torch.stack([torch.cat([ck.load(P + f"mlp.experts.{e}.gate_proj.weight"),
  70. ck.load(P + f"mlp.experts.{e}.up_proj.weight")], 0) for e in range(E)])
  71. if hf == "mlp.experts.down_proj":
  72. return torch.stack([ck.load(P + f"mlp.experts.{e}.down_proj.weight") for e in range(E)])
  73. return ck.load(P + hf)
  74. def set_param(root, name, tensor, buffer=False):
  75. mod = root
  76. parts = name.split(".")
  77. for p in parts[:-1]:
  78. mod = getattr(mod, p)
  79. if buffer:
  80. mod._buffers[parts[-1]] = tensor
  81. else:
  82. mod._parameters[parts[-1]] = torch.nn.Parameter(tensor, requires_grad=False)
  83. def main():
  84. ap = argparse.ArgumentParser()
  85. ap.add_argument("--ckpt", required=True)
  86. ap.add_argument("--tapshot", required=True)
  87. ap.add_argument("--out", required=True)
  88. ap.add_argument("--gen", type=int, default=310, help="generated tokens to append to the prompt")
  89. ap.add_argument("--watch-gen", default="74,142,148,176,310", help="generated indices whose predicting position is reported")
  90. ap.add_argument("--dtype", default="bf16", choices=["bf16", "fp32"], help="model/weight dtype (fp32 = perturbation/truth run)")
  91. ap.add_argument("--bias-dtype", default="model", choices=["model", "fp32"],
  92. help="dtype of the router e_score_correction_bias buffer: 'model' (bf16 when --dtype bf16, "
  93. "as from_pretrained does) or fp32 (what vLLM uses)")
  94. args = ap.parse_args()
  95. global DTYPE
  96. DTYPE = torch.float32 if args.dtype == "fp32" else torch.bfloat16
  97. os.makedirs(args.out, exist_ok=True)
  98. from transformers import AutoConfig, AutoTokenizer
  99. from transformers.models.glm5_next.modeling_glm5_next import Glm5NextTextModel
  100. snap = json.load(open(args.tapshot))
  101. prompt = list(snap["prompt"])
  102. tok = AutoTokenizer.from_pretrained(args.ckpt)
  103. gen_ids = tok("".join(snap["strs"][: args.gen]), add_special_tokens=False)["input_ids"]
  104. assert len(gen_ids) == args.gen, f"re-tokenised {len(gen_ids)} ids, expected {args.gen}"
  105. ids = prompt + gen_ids
  106. T = len(ids)
  107. P0 = len(prompt)
  108. watch_gen = [int(x) for x in args.watch_gen.split(",") if x]
  109. watch_pos = [P0 + g - 1 for g in watch_gen] # the row whose logits predict generated index g
  110. print(f"prompt {P0} + gen {args.gen} = {T} tokens; watch positions {dict(zip(watch_gen, watch_pos))}", flush=True)
  111. cfg = AutoConfig.from_pretrained(args.ckpt)
  112. tc = cfg.text_config
  113. tc._attn_implementation = "eager"
  114. E = int(tc.num_local_experts)
  115. torch.set_default_dtype(DTYPE)
  116. with torch.device("meta"):
  117. model = Glm5NextTextModel(tc)
  118. torch.set_default_dtype(torch.float32)
  119. model.eval()
  120. ck = Ckpt(args.ckpt)
  121. nb = sum(1 for _ in model.buffers())
  122. print(f"model built on meta: {len(list(model.parameters()))} params, {nb} buffers", flush=True)
  123. for name, buf in model.named_buffers():
  124. print(" buffer", name, tuple(buf.shape), buf.dtype)
  125. # non-layer weights
  126. set_param(model, "embed_tokens.weight", ck.load("model.language_model.embed_tokens.weight"))
  127. set_param(model, "norm.weight", ck.load("model.language_model.norm.weight"))
  128. lm_head = ck.load("lm_head.weight")
  129. saved = {"ids": ids, "watch_pos": watch_pos, "layers": {}}
  130. keep_pos = sorted(set(watch_pos + [T - 2, T - 1]))
  131. timing = {}
  132. def pre_hook(L):
  133. def f(module, args_, kwargs_):
  134. t0 = time.time()
  135. for name, p in list(module.named_parameters()):
  136. if p.device.type == "meta":
  137. set_param(module, name, layer_tensor(ck, L, name, E))
  138. for name, b in list(module.named_buffers()):
  139. if b.device.type == "meta":
  140. t = layer_tensor(ck, L, name, E)
  141. if args.bias_dtype == "fp32" and name.endswith("e_score_correction_bias"):
  142. t = ck.raw(f"model.language_model.layers.{L}.mlp.gate.e_score_correction_bias").to(dev).float()
  143. set_param(module, name, t, buffer=True)
  144. torch.cuda.synchronize()
  145. timing[L] = time.time() - t0
  146. return f
  147. def post_hook(L):
  148. def f(module, args_, kwargs_, out):
  149. hs = out[0] if isinstance(out, tuple) else out # [1, T, hc, hidden]
  150. saved["layers"][L] = hs[0, keep_pos].detach().to("cpu")
  151. for name, p in list(module.named_parameters()):
  152. set_param(module, name, torch.empty(p.shape, device="meta", dtype=p.dtype))
  153. for name, b in list(module.named_buffers()):
  154. set_param(module, name, torch.empty(b.shape, device="meta", dtype=b.dtype), buffer=True)
  155. ck.close()
  156. gc.collect()
  157. torch.cuda.empty_cache()
  158. rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1e6
  159. print(f"layer {L:2d} done: load {timing.get(L, 0):.1f}s cuda alloc {torch.cuda.memory_allocated() / 1e9:.1f} GB "
  160. f"reserved {torch.cuda.memory_reserved() / 1e9:.1f} GB host maxrss {rss:.1f} GB", flush=True)
  161. return f
  162. for L, layer in enumerate(model.layers):
  163. layer.register_forward_pre_hook(pre_hook(L), with_kwargs=True)
  164. layer.register_forward_hook(post_hook(L), with_kwargs=True)
  165. t0 = time.time()
  166. with torch.no_grad():
  167. out = model(input_ids=torch.tensor([ids], device=dev), use_cache=False)
  168. h = out.last_hidden_state[0].float() # [T, hidden], normed
  169. logits = F.linear(h, lm_head.float())
  170. logp = torch.log_softmax(logits, dim=-1)
  171. print(f"forward done in {time.time() - t0:.0f}s", flush=True)
  172. saved["final_hidden"] = h[keep_pos].to("cpu")
  173. saved["keep_pos"] = keep_pos
  174. torch.save(saved, os.path.join(args.out, "hf_layers.pt"))
  175. top = torch.topk(logp, 5, dim=-1)
  176. res = {}
  177. for g, p in zip(watch_gen, watch_pos):
  178. row = [(tok.decode([int(i)]), int(i), round(float(v), 4)) for v, i in zip(top.values[p], top.indices[p])]
  179. res[str(g)] = {"pos": p, "top5": row, "margin": round(float(top.values[p, 0] - top.values[p, 1]), 4)}
  180. print(f"gen {g} (pos {p}): " + " ".join(f"{s!r} {v}" for s, i, v in row) + f" margin {res[str(g)]['margin']}", flush=True)
  181. json.dump(res, open(os.path.join(args.out, "hf_top5.json"), "w"), indent=1)
  182. allrows = []
  183. for g in range(1, args.gen + 1):
  184. p = P0 + g - 1
  185. actual = ids[p + 1] if p + 1 < T else None
  186. row = {"gen": g, "pos": p,
  187. "top5": [(tok.decode([int(i)]), round(float(v), 4)) for v, i in zip(top.values[p], top.indices[p])],
  188. "actual": (tok.decode([actual]), round(float(logp[p, actual]), 4)) if actual is not None else None,
  189. "margin": round(float(top.values[p, 0] - top.values[p, 1]), 4)}
  190. allrows.append(row)
  191. json.dump(allrows, open(os.path.join(args.out, "hf_top5_all.json"), "w"))
  192. thin = [(r["gen"], r["margin"], r["top5"][0][0]) for r in allrows if r["margin"] < 2.0]
  193. wrong = [(r["gen"], r["top5"][0][0], r["actual"]) for r in allrows if r["actual"] and r["top5"][0][0] != r["actual"][0]]
  194. print(f"HF margins over {len(allrows)} generated positions: thin(<2 nats) {thin[:30]}", flush=True)
  195. print(f"HF argmax != token vLLM produced at: {wrong[:30]}", flush=True)
  196. if __name__ == "__main__":
  197. main()