glm53_step_tap.py 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210
  1. """Per-step tap for GLM-5.3-Flash under vLLM's V2 GPU runner.
  2. What it answers, per decode step and per TP rank:
  3. * Pairs: two rows in the same batch with the same position and the same
  4. token prefix (the S2 setup) are compared sublayer by sublayer, in
  5. execution order, and the FIRST checkpoint where the rows differ is
  6. logged with the layer and sublayer name. Rows that differ at a
  7. sublayer's output but not at its input name the component.
  8. * Placement: KV slot, mamba state block, indexer pool slot and tail slot
  9. for every tapped row, plus the SM90 planned kv_len against the indexer's
  10. valid count for that row.
  11. * Continuity: the KDA conv/recurrent state of a request is hashed before
  12. and after every step; a "before" that differs from the previous step's
  13. "after" for the same block means something else wrote it in between.
  14. The MLA cache row written at step t is re-read at step t+1 the same way.
  15. * Feed: the token each rank feeds next (last_sampled_tokens after
  16. post_update) and the host/device num_computed_tokens, so cross-rank
  17. disagreement shows up in the per-rank files.
  18. Everything is off unless GLM53_TAP=1; the import then costs one env lookup.
  19. When armed, the hooks install lazily through a post-import hook on the vLLM
  20. modules, so importing this file never imports torch or vLLM by itself.
  21. Env:
  22. GLM53_TAP=1 arm
  23. GLM53_TAP_DIR output dir, default /tmp/glm53-tap (tap-rank<r>.jsonl)
  24. GLM53_TAP_REQ substring filter on request id ("" = every request)
  25. GLM53_TAP_DEEP=1 forward hooks on every decoder sublayer. Needs pure
  26. eager (--enforce-eager); refused under torch.compile
  27. unless GLM53_TAP_DEEP=force.
  28. GLM53_TAP_VECTORS=0|1|2 0 hashes only; 1 also dump the tapped rows of every
  29. checkpoint at the step a pair first splits; 2 every step
  30. GLM53_TAP_VEC_STEPS=A-B dump vectors for steps A..B regardless of pairs
  31. GLM53_TAP_MAX_ROWS rows recorded per step (default 64; the last N
  32. matching tokens of the batch)
  33. GLM53_TAP_PAIR_ALL=1 pair rows by position only, skip the prefix check
  34. GLM53_TAP_SELFTEST=1 (with `python3 -m glm53_step_tap`) exercise the
  35. machinery on a toy module, no vLLM needed
  36. Loading without a rebuild: bind-mount this file and glm53_step_tap.pth into
  37. /usr/local/lib/python3.12/dist-packages/ (see README.md).
  38. """
  39. from __future__ import annotations
  40. import os
  41. import sys
  42. _ENABLED = os.environ.get("GLM53_TAP", "0").strip().lower() not in ("", "0", "no", "false")
  43. def _env_int(name: str, default: int) -> int:
  44. try:
  45. return int(os.environ.get(name, "") or default)
  46. except ValueError:
  47. return default
  48. # --------------------------------------------------------------------------
  49. # Post-import hook: run a callback right after a named module finishes import.
  50. # --------------------------------------------------------------------------
  51. def _install_post_import_hooks(targets: dict) -> None:
  52. import importlib.abc
  53. import importlib.util
  54. class _Finder(importlib.abc.MetaPathFinder):
  55. def __init__(self) -> None:
  56. self._busy: set[str] = set()
  57. def find_spec(self, fullname, path=None, target=None):
  58. if fullname not in targets or fullname in self._busy:
  59. return None
  60. self._busy.add(fullname)
  61. try:
  62. spec = importlib.util.find_spec(fullname)
  63. finally:
  64. self._busy.discard(fullname)
  65. if spec is None or spec.loader is None:
  66. return None
  67. orig = spec.loader
  68. cb = targets[fullname]
  69. class _Loader(importlib.abc.Loader):
  70. def create_module(self, spec_):
  71. if hasattr(orig, "create_module"):
  72. return orig.create_module(spec_)
  73. return None
  74. def exec_module(self, module):
  75. orig.exec_module(module)
  76. try:
  77. cb(module)
  78. except Exception: # noqa: BLE001
  79. _log_exc(f"post-import hook for {fullname}")
  80. spec.loader = _Loader()
  81. return spec
  82. sys.meta_path.insert(0, _Finder())
  83. def _log_exc(where: str) -> None:
  84. import traceback
  85. sys.stderr.write(f"[glm53-tap] error in {where}:\n{traceback.format_exc()}\n")
  86. sys.stderr.flush()
  87. # --------------------------------------------------------------------------
  88. # The tap itself (imports torch lazily; only constructed when armed).
  89. # --------------------------------------------------------------------------
  90. _SHARDED = ("kda.", "mla.q", "mla.out", "mla.kvidx")
  91. class Tap:
  92. def __init__(self) -> None:
  93. import torch
  94. self.torch = torch
  95. self.dir = os.environ.get("GLM53_TAP_DIR", "/tmp/glm53-tap")
  96. self.req_filter = os.environ.get("GLM53_TAP_REQ", "")
  97. deep = os.environ.get("GLM53_TAP_DEEP", "0").strip().lower()
  98. self.deep = deep not in ("", "0", "no", "false")
  99. self.deep_force = deep == "force"
  100. self.vectors = _env_int("GLM53_TAP_VECTORS", 0)
  101. vs = os.environ.get("GLM53_TAP_VEC_STEPS", "")
  102. self.vec_steps = None
  103. if vs:
  104. a, _, b = vs.partition("-")
  105. self.vec_steps = (int(a or 0), int(b or 1 << 40))
  106. self.max_rows = max(1, _env_int("GLM53_TAP_MAX_ROWS", 64))
  107. self.pair_all = os.environ.get("GLM53_TAP_PAIR_ALL", "0") not in ("", "0")
  108. self.rank: int | None = None
  109. self.fh = None
  110. self.step = 0
  111. self.header_written = False
  112. # per-step state
  113. self.cur: dict | None = None
  114. self.n_tokens = 0
  115. self.n_pad = 0
  116. self.tapped: list[int] = []
  117. self.tapped_t = None
  118. self.req_of_tok = None # CPU LongTensor [n_tokens]
  119. self.row_req: list[str] = []
  120. self.row_pos: list[int] = []
  121. self.batch_req_ids: list[str] = []
  122. self.pairs: list[tuple[int, int]] = []
  123. self.cps: list[tuple[str, object]] = [] # (name, rows tensor [k, m] int view)
  124. self.ints: dict[str, object] = {}
  125. self.ints_cpu: dict[str, list] = {}
  126. self.sample_rows: list[dict] = []
  127. self.extra: dict[str, object] = {} # per-step, saved only in the vector blob
  128. self.err: list[str] = []
  129. self.call_counts: dict[str, int] = {}
  130. # cross-step memory
  131. self.last_state: dict[tuple[str, int], int] = {} # (cpname, state_idx) -> hash
  132. self.kv_prev: dict[tuple[str, str], tuple[int, int]] = {} # (layer, req) -> (slot, hash)
  133. self.pair_split: dict[tuple[str, str], int] = {} # first step a pair split
  134. self.runner = None
  135. self._w = None
  136. # ---- utilities -------------------------------------------------------
  137. def note_err(self, where: str) -> None:
  138. import traceback
  139. msg = f"{where}: {traceback.format_exc(limit=3)}"
  140. if self.cur is not None:
  141. self.cur.setdefault("err", []).append(msg)
  142. sys.stderr.write(f"[glm53-tap] {msg}\n")
  143. def _rank(self) -> int:
  144. if self.rank is not None:
  145. return self.rank
  146. r = None
  147. try:
  148. from vllm.distributed.parallel_state import get_tensor_model_parallel_rank
  149. r = int(get_tensor_model_parallel_rank())
  150. except Exception: # noqa: BLE001
  151. r = None
  152. if r is None:
  153. try:
  154. r = int(os.environ.get("RANK", ""))
  155. except ValueError:
  156. r = -1
  157. self.rank = r
  158. return r
  159. def _open(self):
  160. if self.fh is None:
  161. os.makedirs(self.dir, exist_ok=True)
  162. r = self._rank()
  163. path = os.path.join(self.dir, f"tap-rank{r}.jsonl")
  164. self.fh = open(path, "a", buffering=1)
  165. sys.stderr.write(f"[glm53-tap] rank {r} armed, writing {path}\n")
  166. return self.fh
  167. def _weights(self, m: int, device):
  168. torch = self.torch
  169. if self._w is None or self._w.numel() < m or self._w.device != device:
  170. n = max(m, 1 << 16)
  171. w = torch.arange(1, n + 1, dtype=torch.int64, device=device)
  172. self._w = w * 0x9E3779B1 | 1
  173. return self._w[:m]
  174. def _row_hash(self, iv):
  175. """[k, m] int32/uint8 -> [k] int64 order-sensitive hash (device)."""
  176. w = self._weights(iv.shape[1], iv.device)
  177. return (iv.to(self.torch.int64) * w).sum(dim=1)
  178. def _rows_view(self, t):
  179. """Return [n_tokens_view, -1] view if t has a token dim, else None."""
  180. torch = self.torch
  181. if not torch.is_tensor(t) or t.dim() == 0 or t.numel() == 0:
  182. return None
  183. if isinstance(t, torch.nn.Parameter) or t.requires_grad:
  184. return None
  185. n, npad = self.n_tokens, self.n_pad
  186. s = t.shape
  187. if s[0] in (n, npad):
  188. return t.reshape(s[0], -1)
  189. if t.dim() >= 2 and s[0] == 1 and s[1] in (n, npad):
  190. return t.reshape(s[1], -1)
  191. return None
  192. def _as_int_rows(self, rows):
  193. torch = self.torch
  194. rows = rows.contiguous()
  195. if rows.dtype == torch.bool:
  196. rows = rows.to(torch.uint8)
  197. k, m = rows.shape
  198. es = rows.element_size()
  199. if (m * es) % 4 == 0 and rows.dtype != torch.uint8:
  200. return rows.view(torch.int32) if es != 4 else rows.view(torch.int32)
  201. return rows.view(torch.uint8)
  202. def capturing(self) -> bool:
  203. try:
  204. return bool(self.torch.cuda.is_current_stream_capturing())
  205. except Exception: # noqa: BLE001
  206. return False
  207. # ---- checkpoints ------------------------------------------------------
  208. def cp(self, name: str, t) -> None:
  209. """Record tensor rows of the tapped tokens under a checkpoint name."""
  210. if self.cur is None or self.tapped_t is None:
  211. return
  212. try:
  213. v = self._rows_view(t)
  214. if v is None:
  215. return
  216. if self.capturing():
  217. return
  218. rows = v.index_select(0, self.tapped_t)
  219. self.cps.append((name, self._as_int_rows(rows), rows))
  220. except Exception: # noqa: BLE001
  221. self.note_err(f"cp {name}")
  222. def cp_rows(self, name: str, rows) -> None:
  223. """Record an already row-selected tensor [k, ...] (k = tapped rows)."""
  224. if self.cur is None or self.tapped_t is None:
  225. return
  226. try:
  227. if self.capturing():
  228. return
  229. k = len(self.tapped)
  230. rows = rows.reshape(k, -1)
  231. self.cps.append((name, self._as_int_rows(rows), rows))
  232. except Exception: # noqa: BLE001
  233. self.note_err(f"cp_rows {name}")
  234. def ival(self, name: str, t) -> None:
  235. """Small per-tapped-row integer values (device or CPU tensor / list)."""
  236. if self.cur is None:
  237. return
  238. try:
  239. torch = self.torch
  240. if torch.is_tensor(t):
  241. if t.device.type != "cpu":
  242. if self.capturing():
  243. return
  244. self.ints[name] = t.detach().reshape(-1).to(torch.int64)
  245. else:
  246. self.ints_cpu[name] = [int(x) for x in t.reshape(-1).tolist()]
  247. else:
  248. self.ints_cpu[name] = [int(x) for x in t]
  249. except Exception: # noqa: BLE001
  250. self.note_err(f"ival {name}")
  251. def ival_tok(self, name: str, t) -> None:
  252. """Per-token device tensor [n_tokens] -> keep only tapped rows."""
  253. if self.cur is None or self.tapped_t is None:
  254. return
  255. try:
  256. if self.capturing():
  257. return
  258. self.ints[name] = t.reshape(-1)[: self.n_pad].index_select(0, self.tapped_t).to(self.torch.int64)
  259. except Exception: # noqa: BLE001
  260. self.note_err(f"ival_tok {name}")
  261. def layer_of(self, prefix: str) -> str:
  262. # "model.layers.12.self_attn..." -> "L12"
  263. parts = prefix.split(".")
  264. for i, p in enumerate(parts):
  265. if p == "layers" and i + 1 < len(parts) and parts[i + 1].isdigit():
  266. return f"L{parts[i + 1]}"
  267. return prefix
  268. def count(self, key: str) -> int:
  269. c = self.call_counts.get(key, 0)
  270. self.call_counts[key] = c + 1
  271. return c
  272. # ---- step lifecycle ---------------------------------------------------
  273. def begin_step(self, runner, input_batch) -> None:
  274. torch = self.torch
  275. self.cur = None
  276. self.cps = []
  277. self.ints = {}
  278. self.ints_cpu = {}
  279. self.sample_rows = []
  280. self.extra = {}
  281. self.call_counts = {}
  282. self.pairs = []
  283. self.tapped = []
  284. self.tapped_t = None
  285. self.runner = runner
  286. try:
  287. req_ids = list(input_batch.req_ids)
  288. n = int(input_batch.num_tokens)
  289. npad = int(input_batch.num_tokens_after_padding)
  290. nreq = int(input_batch.num_reqs)
  291. qsl = [int(x) for x in input_batch.query_start_loc_np[: nreq + 1]]
  292. match = [i for i, r in enumerate(req_ids) if (not self.req_filter) or (self.req_filter in r)]
  293. if not match:
  294. return
  295. req_of_tok = torch.empty(n, dtype=torch.int64)
  296. for r in range(nreq):
  297. req_of_tok[qsl[r] : qsl[r + 1]] = r
  298. toks = [t for t in range(n) if int(req_of_tok[t]) in set(match)]
  299. toks = toks[-self.max_rows :]
  300. if not toks:
  301. return
  302. pos = input_batch.positions[:n].detach().to("cpu")
  303. ids = input_batch.input_ids[:n].detach().to("cpu")
  304. seq_dev = input_batch.seq_lens[:nreq].detach().to("cpu")
  305. seq_ub = input_batch.seq_lens_cpu_upper_bound[:nreq]
  306. nc_host = input_batch.num_computed_tokens_np[:nreq]
  307. idx_np = input_batch.idx_mapping_np[:nreq]
  308. nc_dev = runner.req_states.num_computed_tokens.gpu[input_batch.idx_mapping[:nreq]].detach().to("cpu")
  309. self.step += 1
  310. self.n_tokens, self.n_pad = n, npad
  311. self.req_of_tok = req_of_tok
  312. self.batch_req_ids = req_ids
  313. self.tapped = toks
  314. self.tapped_t = torch.tensor(toks, dtype=torch.int64, device=input_batch.positions.device)
  315. rows = []
  316. self.row_req, self.row_pos = [], []
  317. for t in toks:
  318. r = int(req_of_tok[t])
  319. rows.append(
  320. {
  321. "i": t,
  322. "req": req_ids[r],
  323. "state": int(idx_np[r]),
  324. "pos": int(pos[t]),
  325. "tok": int(ids[t]),
  326. "seq_dev": int(seq_dev[r]),
  327. "seq_ub": int(seq_ub[r]),
  328. "nc_host": int(nc_host[r]),
  329. "nc_dev": int(nc_dev[r]),
  330. }
  331. )
  332. self.row_req.append(req_ids[r])
  333. self.row_pos.append(int(pos[t]))
  334. self.cur = {
  335. "step": self.step,
  336. "rank": self._rank(),
  337. "t": __import__("time").time(),
  338. "n_tokens": n,
  339. "n_reqs": nreq,
  340. "rows": rows,
  341. }
  342. hd = [x["req"] for x in rows if x["nc_host"] != x["nc_dev"] or x["seq_dev"] != x["seq_ub"]]
  343. if hd:
  344. self.cur["host_dev_mismatch"] = sorted(set(hd))
  345. # pairs: distinct requests, same position (+ same prefix unless PAIR_ALL)
  346. all_ids = runner.req_states.all_token_ids.gpu
  347. for a in range(len(toks)):
  348. for b in range(a + 1, len(toks)):
  349. if len(self.pairs) >= 8:
  350. break
  351. if self.row_req[a] == self.row_req[b] or self.row_pos[a] != self.row_pos[b]:
  352. continue
  353. if rows[a]["tok"] != rows[b]["tok"]:
  354. continue
  355. if not self.pair_all:
  356. p = self.row_pos[a]
  357. sa, sb = rows[a]["state"], rows[b]["state"]
  358. if p > 0 and not bool(torch.equal(all_ids[sa, :p], all_ids[sb, :p])):
  359. continue
  360. self.pairs.append((a, b))
  361. self.cur["pairs"] = [
  362. {"a": self.row_req[a], "b": self.row_req[b], "pos": self.row_pos[a]} for a, b in self.pairs
  363. ]
  364. except Exception: # noqa: BLE001
  365. self.cur = None
  366. self.note_err("begin_step")
  367. def flush(self) -> None:
  368. if self.cur is None:
  369. return
  370. cur = self.cur
  371. torch = self.torch
  372. try:
  373. k = len(self.tapped)
  374. names = [n for n, _, _ in self.cps]
  375. # hashes: one stack, one D2H
  376. if self.cps:
  377. hs = torch.stack([self._row_hash(iv) for _, iv, _ in self.cps]) # [C, k]
  378. hs_cpu = hs.to("cpu").tolist()
  379. else:
  380. hs_cpu = []
  381. cps_out = {}
  382. for i, n in enumerate(names):
  383. nm = n if n not in cps_out else f"{n}#{i}"
  384. cps_out[nm] = hs_cpu[i]
  385. cur["cps"] = cps_out
  386. # pair diffs (physical kv_indices differ between rows by construction)
  387. if self.pairs and self.cps:
  388. cmp_idx = [c for c, n in enumerate(names) if not n.endswith(".mla.kvidx")]
  389. flags = []
  390. for a, b in self.pairs:
  391. flags.append(torch.stack([(self.cps[c][1][a] != self.cps[c][1][b]).any() for c in cmp_idx]))
  392. flags = torch.stack(flags).to("cpu") # [P, C']
  393. for pi, (a, b) in enumerate(self.pairs):
  394. diff = [names[c] for j, c in enumerate(cmp_idx) if bool(flags[pi, j])]
  395. first = diff[0] if diff else None
  396. key = (self.row_req[a], self.row_req[b])
  397. rec = cur["pairs"][pi]
  398. rec["first_diff"] = first
  399. rec["ndiff"] = len(diff)
  400. rec["diffs"] = diff[:60]
  401. rec["ncp"] = len(names)
  402. if first is not None and key not in self.pair_split:
  403. self.pair_split[key] = self.step
  404. rec["first_split_step"] = True
  405. # max-abs at the first diverging checkpoint, in the cp's own dtype view
  406. try:
  407. c = names.index(first)
  408. iv = self.cps[c][1]
  409. rec["first_diff_nbytes_ne"] = int(
  410. (iv[a].view(torch.uint8) != iv[b].view(torch.uint8)).sum().item()
  411. )
  412. except Exception: # noqa: BLE001
  413. pass
  414. # ints
  415. ints_out = dict(self.ints_cpu)
  416. for n, t in self.ints.items():
  417. ints_out[n] = t.to("cpu").tolist()
  418. cur["ints"] = ints_out
  419. # continuity: KDA state before(t) vs after(t-1); MLA row re-read
  420. changed = []
  421. for n, h in cps_out.items():
  422. if not (n.endswith(".conv_before") or n.endswith(".rec_before")):
  423. continue
  424. sidx_name = n.rsplit(".", 1)[0] + ".state_idx"
  425. sidx = ints_out.get(sidx_name)
  426. if sidx is None:
  427. continue
  428. for r in range(k):
  429. key = (n, int(sidx[r]))
  430. prev = self.last_state.get(key)
  431. if prev is not None and prev != h[r]:
  432. changed.append({"cp": n, "req": self.row_req[r], "state": int(sidx[r]), "pos": self.row_pos[r]})
  433. for n, h in cps_out.items():
  434. if n.endswith(".conv_after") or n.endswith(".rec_after"):
  435. sidx = ints_out.get(n.rsplit(".", 1)[0] + ".state_idx")
  436. if sidx is None:
  437. continue
  438. for r in range(k):
  439. self.last_state[(n.replace("_after", "_before"), int(sidx[r]))] = h[r]
  440. if changed:
  441. cur["state_changed"] = changed
  442. kvch = []
  443. for n, h in cps_out.items():
  444. if not n.endswith(".mla.cache_reread"):
  445. continue
  446. layer = n.split(".")[0]
  447. for r in range(k):
  448. prev = self.kv_prev.get((layer, self.row_req[r]))
  449. if prev is not None and prev[1] != h[r] and h[r] != 0:
  450. kvch.append({"layer": layer, "req": self.row_req[r], "slot": prev[0], "pos": self.row_pos[r]})
  451. for n, h in cps_out.items():
  452. if n.endswith(".mla.cache_row"):
  453. layer = n.split(".")[0]
  454. slots = ints_out.get(f"{layer}.mla.slot")
  455. if slots is None:
  456. continue
  457. for r in range(k):
  458. self.kv_prev[(layer, self.row_req[r])] = (int(slots[r]), h[r])
  459. if kvch:
  460. cur["kv_changed"] = kvch
  461. # SM90 plan vs valid
  462. pm = []
  463. for n, v in ints_out.items():
  464. if n.endswith(".mla.planned"):
  465. valid = ints_out.get(n.replace(".planned", ".valid"))
  466. if valid is None:
  467. continue
  468. for r in range(k):
  469. if v[r] != valid[r]:
  470. pm.append({"cp": n, "req": self.row_req[r], "planned": v[r], "valid": valid[r], "pos": self.row_pos[r]})
  471. if pm:
  472. cur["plan_mismatch"] = pm
  473. if self.sample_rows:
  474. cur["sample"] = self.sample_rows
  475. # vectors
  476. want_vec = self.vectors >= 2 or (
  477. self.vectors == 1 and any(p.get("first_split_step") for p in cur.get("pairs", []))
  478. )
  479. # A window file (GLM53_TAP_DIR/vec_steps, "A-B") overrides the env var and is
  480. # re-read every step, so the window can be set after the boot's self-test
  481. # requests have shifted the step count.
  482. try:
  483. with open(os.path.join(self.dir, "vec_steps")) as f:
  484. a, _, b = f.read().strip().partition("-")
  485. self.vec_steps = (int(a or 0), int(b or 1 << 40))
  486. except (OSError, ValueError):
  487. pass
  488. if self.vec_steps is not None:
  489. want_vec = self.vec_steps[0] <= self.step <= self.vec_steps[1]
  490. if want_vec and self.cps:
  491. blob = {"rows": cur["rows"], "cps": {}, "extra": {}}
  492. def _cpu(v):
  493. if torch.is_tensor(v):
  494. return v.to("cpu")
  495. if isinstance(v, (list, tuple)):
  496. return type(v)(_cpu(x) for x in v)
  497. if isinstance(v, dict):
  498. return {kk: _cpu(vv) for kk, vv in v.items()}
  499. return v
  500. for en, ev in self.extra.items():
  501. try:
  502. blob["extra"][en] = _cpu(ev)
  503. except Exception: # noqa: BLE001
  504. pass
  505. for i, (n, _iv, rows) in enumerate(self.cps):
  506. nm = n if n not in blob["cps"] else f"{n}#{i}"
  507. blob["cps"][nm] = rows.to("cpu")
  508. os.makedirs(self.dir, exist_ok=True)
  509. p = os.path.join(self.dir, f"vec-rank{self._rank()}-step{self.step}.pt")
  510. torch.save(blob, p)
  511. cur["vectors"] = p
  512. except Exception: # noqa: BLE001
  513. self.note_err("flush")
  514. try:
  515. import json
  516. fh = self._open()
  517. if not self.header_written:
  518. self.header_written = True
  519. fh.write(json.dumps({"header": True, "rank": self._rank(), "pid": os.getpid(), **self._config_summary()}) + "\n")
  520. fh.write(json.dumps(cur, default=str) + "\n")
  521. except Exception: # noqa: BLE001
  522. _log_exc("flush write")
  523. self.cur = None
  524. self.cps = []
  525. self.ints = {}
  526. self.ints_cpu = {}
  527. self.sample_rows = []
  528. def _config_summary(self) -> dict:
  529. out = {"deep": self.deep, "vectors": self.vectors, "req_filter": self.req_filter}
  530. try:
  531. cfg = self.runner.vllm_config
  532. cc = cfg.compilation_config
  533. out.update(
  534. {
  535. "compile_mode": str(cc.mode),
  536. "cudagraph_mode": str(cc.cudagraph_mode),
  537. "tp": int(cfg.parallel_config.tensor_parallel_size),
  538. "async_scheduling": bool(cfg.scheduler_config.async_scheduling),
  539. "max_concurrent_batches": int(getattr(cfg, "max_concurrent_batches", 0) or 0),
  540. "block_size": int(cfg.cache_config.block_size),
  541. "kv_cache_dtype": str(cfg.cache_config.cache_dtype),
  542. "enforce_eager": bool(cfg.model_config.enforce_eager),
  543. }
  544. )
  545. except Exception: # noqa: BLE001
  546. pass
  547. return out
  548. TAP: Tap | None = None
  549. def _tap() -> Tap:
  550. global TAP
  551. if TAP is None:
  552. TAP = Tap()
  553. return TAP
  554. # --------------------------------------------------------------------------
  555. # Patches per module
  556. # --------------------------------------------------------------------------
  557. def _patch_runner(mod) -> None:
  558. import functools
  559. R = mod.GPUModelRunner
  560. tap = _tap()
  561. o_prepare = R.prepare_inputs
  562. @functools.wraps(o_prepare)
  563. def prepare_inputs(self, *a, **k):
  564. ib = o_prepare(self, *a, **k)
  565. try:
  566. tap.begin_step(self, ib)
  567. except Exception: # noqa: BLE001
  568. tap.note_err("prepare_inputs")
  569. return ib
  570. R.prepare_inputs = prepare_inputs
  571. o_exec = R.execute_model
  572. @functools.wraps(o_exec)
  573. def execute_model(self, *a, **k):
  574. # a step whose sample_tokens never ran (or a dummy run) must not leak
  575. if tap.cur is not None:
  576. try:
  577. tap.cur.setdefault("err", []).append("flushed at next execute_model (sample_tokens not seen)")
  578. tap.flush()
  579. except Exception: # noqa: BLE001
  580. tap.note_err("late flush")
  581. tap.cur = None
  582. if k.get("dummy_run") or k.get("is_profile") or (len(a) > 2 and a[2]):
  583. tap.tapped_t = None
  584. return o_exec(self, *a, **k)
  585. R.execute_model = execute_model
  586. o_post = R.postprocess_sampled
  587. @functools.wraps(o_post)
  588. def postprocess_sampled(self, idx_mapping, sampled_tokens, num_sampled, num_rejected, *a, **k):
  589. out = o_post(self, idx_mapping, sampled_tokens, num_sampled, num_rejected, *a, **k)
  590. try:
  591. if tap.cur is not None and not tap.capturing():
  592. fed = self.req_states.last_sampled_tokens[idx_mapping].detach().to("cpu").tolist()
  593. nc = self.req_states.num_computed_tokens.gpu[idx_mapping].detach().to("cpu").tolist()
  594. st = sampled_tokens[:, 0].detach().to("cpu").tolist() if sampled_tokens.dim() == 2 else []
  595. reqs = tap.batch_req_ids or [str(i) for i in range(len(fed))]
  596. tap.cur["fed"] = {r: (v if isinstance(v, list) else int(v)) for r, v in zip(reqs, fed)} # [n, 1] in this nightly
  597. tap.cur["nc_after"] = {r: int(v) for r, v in zip(reqs, nc)}
  598. if st:
  599. tap.cur["sampled0"] = {r: int(v) for r, v in zip(reqs, st)}
  600. except Exception: # noqa: BLE001
  601. tap.note_err("postprocess_sampled")
  602. return out
  603. R.postprocess_sampled = postprocess_sampled
  604. o_sample_tokens = R.sample_tokens
  605. @functools.wraps(o_sample_tokens)
  606. def sample_tokens(self, *a, **k):
  607. try:
  608. out = o_sample_tokens(self, *a, **k)
  609. finally:
  610. try:
  611. if tap.cur is not None:
  612. tap.flush()
  613. except Exception: # noqa: BLE001
  614. tap.note_err("sample_tokens flush")
  615. return out
  616. R.sample_tokens = sample_tokens
  617. o_load = R.load_model
  618. @functools.wraps(o_load)
  619. def load_model(self, *a, **k):
  620. out = o_load(self, *a, **k)
  621. try:
  622. tap.runner = self
  623. if tap.deep:
  624. _install_deep_hooks(self, tap)
  625. except Exception: # noqa: BLE001
  626. tap.note_err("load_model hooks")
  627. return out
  628. R.load_model = load_model
  629. sys.stderr.write("[glm53-tap] runner patched\n")
  630. def _install_deep_hooks(runner, tap: Tap) -> None:
  631. torch = tap.torch
  632. try:
  633. mode = runner.vllm_config.compilation_config.mode
  634. eager = str(mode).endswith("NONE") or int(getattr(mode, "value", 0) or 0) == 0
  635. except Exception: # noqa: BLE001
  636. eager = False
  637. if not eager and not tap.deep_force:
  638. sys.stderr.write(
  639. "[glm53-tap] DEEP hooks refused: torch.compile is on (use --enforce-eager, or GLM53_TAP_DEEP=force)\n"
  640. )
  641. return
  642. model = runner.model
  643. layers = [(n, m) for n, m in model.named_modules() if type(m).__name__ == "Glm5NextDecoderLayer"]
  644. if not layers:
  645. sys.stderr.write("[glm53-tap] DEEP: no Glm5NextDecoderLayer modules found\n")
  646. return
  647. def mk(name: str, in_names: bool = True):
  648. def hook(mod, args, kwargs, out):
  649. if tap.cur is None:
  650. return
  651. try:
  652. c = tap.count(name)
  653. tag = name if c == 0 else f"{name}@{c}"
  654. if in_names:
  655. for i, t in enumerate(args):
  656. tap.cp(f"{tag}.in{i}", t)
  657. for kname, t in kwargs.items():
  658. tap.cp(f"{tag}.in_{kname}", t)
  659. outs = out if isinstance(out, (tuple, list)) else (out,)
  660. for i, t in enumerate(outs):
  661. tap.cp(f"{tag}.out{i}", t)
  662. except Exception: # noqa: BLE001
  663. tap.note_err(f"hook {name}")
  664. return hook
  665. n_hooks = 0
  666. for lname, layer in layers:
  667. L = tap.layer_of(lname + ".x")
  668. kind = type(getattr(layer, "self_attn", None)).__name__
  669. attn_tag = "kda" if "Linear" in kind else "mla"
  670. layer.register_forward_hook(mk(f"{L}.layer"), with_kwargs=True)
  671. n_hooks += 1
  672. for sub, tag in (
  673. ("mhc_pre_op", "hc_pre"),
  674. ("mhc_fused_post_pre_op", "hc_fused"),
  675. ("self_attn", f"attn_{attn_tag}"),
  676. ("mhc_post_op", "hc_post"),
  677. ("mlp", "mlp"),
  678. ("input_layernorm", "ln_in"),
  679. ("post_attention_layernorm", "ln_post"),
  680. ):
  681. m = getattr(layer, sub, None)
  682. if isinstance(m, torch.nn.Module):
  683. m.register_forward_hook(mk(f"{L}.{tag}"), with_kwargs=True)
  684. n_hooks += 1
  685. # Inside the MoE: the router (gate logits, top-k derivable offline),
  686. # the fused experts (their output before the TP all-reduce) and the
  687. # shared experts. mlp.in/out alone cannot show an expert-choice split.
  688. mlp = getattr(layer, "mlp", None)
  689. for sub, tag in (("gate", "moe_gate"), ("experts", "moe_experts"), ("shared_experts", "moe_shared")):
  690. m = getattr(mlp, sub, None)
  691. if isinstance(m, torch.nn.Module):
  692. m.register_forward_hook(mk(f"{L}.{tag}"), with_kwargs=True)
  693. n_hooks += 1
  694. sys.stderr.write(f"[glm53-tap] DEEP: {n_hooks} forward hooks on {len(layers)} decoder layers\n")
  695. def _patch_kda(mod) -> None:
  696. import functools
  697. tap = _tap()
  698. C = mod.Glm5NextLinearAttention
  699. o_forward = C._forward
  700. def _state_rows(self, md):
  701. """Gather the conv/recurrent state rows of the tapped tokens' requests."""
  702. torch = tap.torch
  703. idx = md.non_spec_state_indices_tensor
  704. if idx is None:
  705. idx = md.spec_state_indices_tensor[:, 0] if md.spec_state_indices_tensor is not None else None
  706. if idx is None:
  707. return None, None, None
  708. if len(set(tap.row_req)) < len(tap.tapped):
  709. return None, None, None # prefill rows: one state per request, skip
  710. req_rows = tap.req_of_tok[tap.tapped].to(idx.device)
  711. sidx = idx.reshape(-1)[req_rows].to(torch.int64)
  712. conv_state, rec_state = self.kv_cache
  713. return sidx, conv_state.index_select(0, sidx), rec_state.index_select(0, sidx)
  714. @functools.wraps(o_forward)
  715. def _forward(self, qkv_proj_states, g1, beta, core_attn_out, *a, **k):
  716. L = tap.layer_of(self.prefix)
  717. md = None
  718. sidx = None
  719. if tap.cur is not None:
  720. try:
  721. from vllm.forward_context import get_forward_context
  722. raw = get_forward_context().attn_metadata
  723. md = raw.get(self.prefix) if isinstance(raw, dict) else None
  724. if md is not None:
  725. tap.cp(f"{L}.kda.qkv", qkv_proj_states)
  726. tap.cp(f"{L}.kda.g1", g1)
  727. tap.cp(f"{L}.kda.beta", beta)
  728. sidx, conv_rows, rec_rows = _state_rows(self, md)
  729. if sidx is not None:
  730. tap.ival(f"{L}.kda.state_idx", sidx)
  731. tap.cp_rows(f"{L}.kda.conv_before", conv_rows)
  732. tap.cp_rows(f"{L}.kda.rec_before", rec_rows)
  733. if L == "L1" or L == "L0":
  734. tap.ival("kda.num_prefills", [int(md.num_prefills)])
  735. tap.ival("kda.num_decodes", [int(md.num_decodes)])
  736. if md.has_initial_state is not None:
  737. tap.ival("kda.has_initial_state", md.has_initial_state.to(tap.torch.int64))
  738. except Exception: # noqa: BLE001
  739. tap.note_err(f"kda pre {L}")
  740. out = o_forward(self, qkv_proj_states, g1, beta, core_attn_out, *a, **k)
  741. if tap.cur is not None and md is not None:
  742. try:
  743. tap.cp(f"{L}.kda.out", core_attn_out)
  744. if sidx is not None:
  745. _, conv_rows, rec_rows = _state_rows(self, md)
  746. tap.cp_rows(f"{L}.kda.conv_after", conv_rows)
  747. tap.cp_rows(f"{L}.kda.rec_after", rec_rows)
  748. elif tap.vectors or tap.vec_steps is not None:
  749. # prefill rows (several per request): keep ONE state per request
  750. # after the step, in the blob's extra, keyed by request id
  751. idx = md.non_spec_state_indices_tensor
  752. if idx is not None:
  753. conv_state, rec_state = self.kv_cache
  754. per_req = {}
  755. for t, r in zip(tap.tapped, tap.row_req):
  756. if r not in per_req:
  757. per_req[r] = int(idx.reshape(-1)[int(tap.req_of_tok[t])])
  758. tap.extra[f"{L}.kda.state_after_req"] = {
  759. r: (conv_state[si].clone(), rec_state[si].clone()) for r, si in per_req.items()
  760. }
  761. except Exception: # noqa: BLE001
  762. tap.note_err(f"kda post {L}")
  763. return out
  764. C._forward = _forward
  765. sys.stderr.write("[glm53-tap] KDA patched\n")
  766. def _patch_mla_layer(mod) -> None:
  767. import functools
  768. tap = _tap()
  769. C = mod.MLAAttention
  770. o_forward = C.forward
  771. def _cache(self):
  772. kv = self.kv_cache
  773. if isinstance(kv, (list, tuple)):
  774. kv = kv[0]
  775. return kv
  776. @functools.wraps(o_forward)
  777. def forward(self, q, kv_c_normed, k_pe, *a, **k):
  778. L = tap.layer_of(self.layer_name)
  779. slot_rows = None
  780. if tap.cur is not None:
  781. try:
  782. from vllm.forward_context import get_forward_context
  783. fc = get_forward_context()
  784. tap.cp(f"{L}.mla.q", q)
  785. tap.cp(f"{L}.mla.kvc", kv_c_normed)
  786. tap.cp(f"{L}.mla.kpe", k_pe)
  787. sm = fc.slot_mapping.get(self.layer_name) if isinstance(fc.slot_mapping, dict) else None
  788. kv = _cache(self)
  789. if sm is not None and tap.torch.is_tensor(kv) and kv.numel() > 0:
  790. slot_rows = sm.reshape(-1)[: tap.n_pad].index_select(0, tap.tapped_t).to(tap.torch.int64)
  791. tap.ival(f"{L}.mla.slot", slot_rows)
  792. flat = kv.reshape(-1, kv.shape[-1])
  793. # re-read the rows this request wrote last step (continuity)
  794. prev = [tap.kv_prev.get((L, r), (0, 0))[0] for r in tap.row_req]
  795. if any(p for p in prev):
  796. pv = tap.torch.tensor(prev, dtype=tap.torch.int64, device=flat.device)
  797. tap.cp_rows(f"{L}.mla.cache_reread", flat.index_select(0, pv.clamp_(min=0)))
  798. except Exception: # noqa: BLE001
  799. tap.note_err(f"mla pre {L}")
  800. out = o_forward(self, q, kv_c_normed, k_pe, *a, **k)
  801. if tap.cur is not None:
  802. try:
  803. tap.cp(f"{L}.mla.attn_out", out)
  804. if slot_rows is not None:
  805. kv = _cache(self)
  806. flat = kv.reshape(-1, kv.shape[-1])
  807. tap.cp_rows(f"{L}.mla.cache_row", flat.index_select(0, slot_rows.clamp(min=0)))
  808. except Exception: # noqa: BLE001
  809. tap.note_err(f"mla post {L}")
  810. return out
  811. C.forward = forward
  812. sys.stderr.write("[glm53-tap] MLAAttention patched\n")
  813. def _patch_sm90(mod) -> None:
  814. import functools
  815. tap = _tap()
  816. C = mod.FlashInferMLASparseSM90Impl
  817. o = C.forward_mqa
  818. @functools.wraps(o)
  819. def forward_mqa(self, q, kv_c_and_k_pe_cache, attn_metadata, layer):
  820. L = tap.layer_of(getattr(layer, "layer_name", ""))
  821. n = None
  822. if tap.cur is not None:
  823. try:
  824. torch = tap.torch
  825. qn = q[0] if isinstance(q, tuple) else q
  826. n = qn.shape[0]
  827. st = attn_metadata.state
  828. planned = st._lens_cpu[:n]
  829. tap.ival_tok(f"{L}.mla.planned", planned.to(qn.device))
  830. topk = self.topk_indices_buffer[:n]
  831. tap.ival_tok(f"{L}.mla.valid", (topk >= 0).sum(dim=1))
  832. tap.cp(f"{L}.mla.topk", topk)
  833. # the kernel's real query inputs (absorbed latent q) for an offline reference
  834. if isinstance(q, tuple):
  835. tap.cp(f"{L}.mla.q_nope", q[0])
  836. tap.cp(f"{L}.mla.q_pe", q[1])
  837. tap.extra[f"{L}.mla.k_scale"] = float(getattr(layer, "_k_scale_float", 1.0) or 1.0)
  838. except Exception: # noqa: BLE001
  839. tap.note_err(f"sm90 pre {L}")
  840. out = o(self, q, kv_c_and_k_pe_cache, attn_metadata, layer)
  841. if tap.cur is not None and n is not None:
  842. try:
  843. st = attn_metadata.state
  844. w = st.topk_width
  845. kvi = st.kv_indices[: n * w].reshape(n, w)
  846. tap.cp(f"{L}.mla.kvidx", kvi)
  847. tap.ival_tok(f"{L}.mla.kvidx0", kvi[:, 0])
  848. # the latent rows the kernel attended, per tapped row (variable length):
  849. # only when vectors are being kept this step, since this is the bulk
  850. if tap.vectors or tap.vec_steps is not None:
  851. flat = kv_c_and_k_pe_cache.reshape(-1, kv_c_and_k_pe_cache.shape[-1])
  852. valid = (self.topk_indices_buffer[:n] >= 0).sum(dim=1)
  853. rows = []
  854. for t in tap.tapped:
  855. nv = int(valid[t])
  856. rows.append(flat.index_select(0, kvi[t, :nv].to(torch.int64)).clone())
  857. tap.extra[f"{L}.mla.kv_rows"] = rows
  858. res = out[0] if isinstance(out, tuple) else out
  859. tap.cp(f"{L}.mla.out", res)
  860. except Exception: # noqa: BLE001
  861. tap.note_err(f"sm90 post {L}")
  862. return out
  863. C.forward_mqa = forward_mqa
  864. sys.stderr.write("[glm53-tap] SM90 forward_mqa patched\n")
  865. def _patch_indexer(mod) -> None:
  866. import functools
  867. tap = _tap()
  868. o = mod.sparse_attn_indexer_kpool
  869. # positional layout after `weights` (forward_cuda passes everything
  870. # positionally): quant_block_size, scale_fmt, topk_tokens, head_dim,
  871. # max_pool_len, total_seq_lens, topk_indices_buffer, skip_k_cache_insert,
  872. # use_fp4_cache, gate_score, compress_ape, index_kpool, positions,
  873. # tail_kv_cache, tail_prefix, topk_backend
  874. def _arg(a, k_, i, name):
  875. if name in k_:
  876. return k_[name]
  877. return a[i] if i < len(a) else None
  878. @functools.wraps(o)
  879. def sparse_attn_indexer_kpool(hidden_states, k_cache_prefix, kv_cache, q_quant, q_scale, k, weights, *a, **k_):
  880. L = None
  881. if tap.cur is not None:
  882. try:
  883. from vllm.forward_context import get_forward_context
  884. from vllm.utils.torch_utils import _resolve_layer_name
  885. name = _resolve_layer_name(k_cache_prefix)
  886. L = tap.layer_of(name)
  887. tap.cp(f"{L}.idx.hidden", hidden_states)
  888. tap.cp(f"{L}.idx.q", q_quant)
  889. tap.cp(f"{L}.idx.k", k)
  890. tap.cp(f"{L}.idx.w", weights)
  891. gs = _arg(a, k_, 9, "gate_score")
  892. if gs is not None:
  893. tap.cp(f"{L}.idx.gate", gs)
  894. raw = get_forward_context().attn_metadata
  895. if isinstance(raw, dict):
  896. md = raw.get(name)
  897. if md is not None and md.slot_mapping is not None:
  898. tap.ival_tok(f"{L}.idx.slot", md.slot_mapping)
  899. tp = _arg(a, k_, 14, "tail_prefix")
  900. if tp is not None:
  901. tm = raw.get(_resolve_layer_name(tp))
  902. if tm is not None and tm.slot_mapping is not None:
  903. tap.ival_tok(f"{L}.idx.tail_slot", tm.slot_mapping)
  904. except Exception: # noqa: BLE001
  905. tap.note_err("indexer pre")
  906. out = o(hidden_states, k_cache_prefix, kv_cache, q_quant, q_scale, k, weights, *a, **k_)
  907. if tap.cur is not None and L is not None:
  908. try:
  909. tap.cp(f"{L}.idx.topk", out)
  910. except Exception: # noqa: BLE001
  911. tap.note_err("indexer post")
  912. return out
  913. mod.sparse_attn_indexer_kpool = sparse_attn_indexer_kpool
  914. sys.stderr.write("[glm53-tap] indexer patched\n")
  915. def _patch_sampler(mod) -> None:
  916. import functools
  917. tap = _tap()
  918. C = mod.Sampler
  919. o = C.__call__
  920. @functools.wraps(o)
  921. def __call__(self, logits, input_batch):
  922. pre = None
  923. if tap.cur is not None:
  924. try:
  925. if logits.shape[0] > 0 and not tap.capturing():
  926. top = logits.float().topk(2, dim=-1)
  927. pre = (top.values.to("cpu"), top.indices.to("cpu"), list(input_batch.req_ids))
  928. except Exception: # noqa: BLE001
  929. tap.note_err("sampler pre")
  930. out = o(self, logits, input_batch)
  931. if tap.cur is not None and pre is not None:
  932. try:
  933. vals, ids, reqs = pre
  934. st = out.sampled_token_ids
  935. st = st[:, 0].to("cpu").tolist() if st.dim() == 2 else st.reshape(-1).to("cpu").tolist()
  936. for i, r in enumerate(reqs[: vals.shape[0]]):
  937. if tap.req_filter and tap.req_filter not in r:
  938. continue
  939. tap.sample_rows.append(
  940. {
  941. "req": r,
  942. "argmax": int(ids[i, 0]),
  943. "top2": int(ids[i, 1]),
  944. "gap": float(vals[i, 0] - vals[i, 1]),
  945. "sampled": int(st[i]) if i < len(st) else None,
  946. }
  947. )
  948. except Exception: # noqa: BLE001
  949. tap.note_err("sampler post")
  950. return out
  951. C.__call__ = __call__
  952. sys.stderr.write("[glm53-tap] sampler patched\n")
  953. _TARGETS = {
  954. "vllm.v1.worker.gpu.model_runner": _patch_runner,
  955. "vllm.models.glm5next.common.kda": _patch_kda,
  956. "vllm.model_executor.layers.attention.mla_attention": _patch_mla_layer,
  957. "vllm.v1.attention.backends.mla.flashinfer_mla_sparse_sm90": _patch_sm90,
  958. "vllm.models.glm5next.nvidia.sparse_indexer": _patch_indexer,
  959. "vllm.v1.worker.gpu.sample.sampler": _patch_sampler,
  960. }
  961. def _arm() -> None:
  962. # If a target is already imported (e.g. armed late), patch it now.
  963. for name, cb in _TARGETS.items():
  964. m = sys.modules.get(name)
  965. if m is not None:
  966. try:
  967. cb(m)
  968. except Exception: # noqa: BLE001
  969. _log_exc(f"late patch {name}")
  970. _install_post_import_hooks({n: cb for n, cb in _TARGETS.items() if n not in sys.modules})
  971. if _ENABLED and os.environ.get("GLM53_TAP_SELFTEST", "0") in ("", "0"):
  972. _arm()
  973. # --------------------------------------------------------------------------
  974. # Self-test: no vLLM. Exercises row views, pairing, hashing, diffs, hooks.
  975. # --------------------------------------------------------------------------
  976. def _selftest() -> int:
  977. import json
  978. import tempfile
  979. import types
  980. import torch
  981. os.environ["GLM53_TAP"] = "1"
  982. os.environ.setdefault("GLM53_TAP_DIR", tempfile.mkdtemp(prefix="glm53-tap-"))
  983. os.environ["GLM53_TAP_DEEP"] = "force"
  984. tap = _tap()
  985. dev = "cuda" if torch.cuda.is_available() else "cpu"
  986. # fake runner / input batch with 3 requests: A and B identical prefixes at pos 5, C at pos 9
  987. class RS:
  988. pass
  989. n = 3
  990. rs = RS()
  991. rs.num_computed_tokens = types.SimpleNamespace(gpu=torch.tensor([5, 5, 9, 0], device=dev))
  992. ids = torch.zeros(4, 32, dtype=torch.int64, device=dev)
  993. ids[0, :6] = torch.arange(6, device=dev)
  994. ids[1, :6] = torch.arange(6, device=dev)
  995. ids[2, :10] = torch.arange(10, device=dev)
  996. rs.all_token_ids = types.SimpleNamespace(gpu=ids)
  997. rs.last_sampled_tokens = torch.tensor([7, 7, 3, 0], device=dev)
  998. runner = types.SimpleNamespace(req_states=rs, execute_model_state=None, vllm_config=None)
  999. ib = types.SimpleNamespace(
  1000. req_ids=["reqA", "reqB", "reqC"],
  1001. num_tokens=n,
  1002. num_tokens_after_padding=4,
  1003. num_reqs=n,
  1004. query_start_loc_np=[0, 1, 2, 3],
  1005. positions=torch.tensor([5, 5, 9, 0], device=dev),
  1006. input_ids=torch.tensor([5, 5, 9, 0], device=dev),
  1007. seq_lens=torch.tensor([6, 6, 10, 0], device=dev),
  1008. seq_lens_cpu_upper_bound=torch.tensor([6, 6, 10, 0]),
  1009. num_computed_tokens_np=[5, 5, 9],
  1010. idx_mapping_np=[0, 1, 2],
  1011. idx_mapping=torch.tensor([0, 1, 2], device=dev),
  1012. )
  1013. tap.begin_step(runner, ib)
  1014. assert tap.cur is not None, "begin_step produced no record"
  1015. assert tap.pairs == [(0, 1)], f"pairs {tap.pairs}"
  1016. # hooks on a toy stack: layer0 row-local, layer1 leaks row 1's value into row 0
  1017. class Sub(torch.nn.Module):
  1018. def __init__(self, leak):
  1019. super().__init__()
  1020. self.leak = leak
  1021. def forward(self, x):
  1022. y = x * 2
  1023. if self.leak:
  1024. y = y.clone()
  1025. y[0] += 1.0
  1026. return y
  1027. class Layer(torch.nn.Module):
  1028. def __init__(self, leak):
  1029. super().__init__()
  1030. self.self_attn = Sub(False)
  1031. self.mlp = Sub(leak)
  1032. def forward(self, x):
  1033. return self.mlp(self.self_attn(x)), None
  1034. Layer.__name__ = "Glm5NextDecoderLayer"
  1035. Sub.__name__ = "Glm5NextLinearAttention"
  1036. model = torch.nn.Module()
  1037. model.layers = torch.nn.ModuleList([Layer(False), Layer(True)]).to(dev)
  1038. runner.model = model
  1039. runner.vllm_config = types.SimpleNamespace(compilation_config=types.SimpleNamespace(mode="CompilationMode.NONE"))
  1040. _install_deep_hooks(runner, tap)
  1041. x = torch.randn(4, 16, device=dev)
  1042. x[1] = x[0]
  1043. for layer in model.layers:
  1044. x, _ = layer(x)
  1045. # a fake KDA state continuity + plan mismatch + cache reread
  1046. tap.ival("L3.kda.state_idx", torch.tensor([11, 12, 13], device=dev))
  1047. st = torch.randn(3, 8, device=dev)
  1048. tap.cp_rows("L3.kda.rec_before", st)
  1049. tap.cp_rows("L3.kda.rec_after", st + 1)
  1050. tap.ival_tok("L2.mla.planned", torch.tensor([6, 6, 10, 0], device=dev))
  1051. tap.ival_tok("L2.mla.valid", torch.tensor([6, 5, 10, 0], device=dev))
  1052. tap.flush()
  1053. # step 2: state_before differs from step 1's after for state 12 -> state_changed
  1054. tap.begin_step(runner, ib)
  1055. tap.ival("L3.kda.state_idx", torch.tensor([11, 12, 13], device=dev))
  1056. st2 = st + 1
  1057. st2[1] += 5
  1058. tap.cp_rows("L3.kda.rec_before", st2)
  1059. tap.flush()
  1060. path = os.path.join(tap.dir, f"tap-rank{tap._rank()}.jsonl")
  1061. with open(path) as f:
  1062. recs = [json.loads(l) for l in f]
  1063. s1, s2 = recs[1], recs[2]
  1064. p = s1["pairs"][0]
  1065. assert p["first_diff"] == "L1.mlp.out0", f"first_diff {p['first_diff']} diffs {p['diffs']}"
  1066. assert "L1.mlp.in0" not in p["diffs"], p["diffs"]
  1067. assert s1["plan_mismatch"][0]["req"] == "reqB", s1.get("plan_mismatch")
  1068. assert s2["state_changed"][0]["req"] == "reqB", s2.get("state_changed")
  1069. print("selftest OK:", path)
  1070. print(" first_diff:", p["first_diff"], "| ndiff", p["ndiff"], "| ncp", p["ncp"])
  1071. print(" plan_mismatch:", s1["plan_mismatch"])
  1072. print(" state_changed:", s2["state_changed"])
  1073. return 0
  1074. if __name__ == "__main__":
  1075. if "--selftest" in sys.argv or os.environ.get("GLM53_TAP_SELFTEST", "0") not in ("", "0"):
  1076. sys.exit(_selftest())
  1077. print(__doc__)