Skip to content

Commit f52acfa

Browse files
debug(mlx-fused): Phase-1 instrumentation for long-gen degeneration
Add stderr NDJSON 'KDBG' instrumentation to the fused spec-decode decode loops and the restored-prefill adapter to characterize the long-generation degeneration bug: * prefill: log prompt_len, restored coverage (evicted range + layers), full_kv layout, and per-layer cache class/max_size/keep. * per block (torch f_theta + mlx_trim loops): block idx, generated-token count, past_len, accepted, dt_ms, a cheap repetition signal (unique fraction + longest single-token run over last 32 tokens), the count of sliding-layer positions evicted DURING decode that have no restored K/V (lost), and a per-layer cache state summary. Temporary debug-only; reverted after the fix is verified. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
1 parent c396d0f commit f52acfa

1 file changed

Lines changed: 140 additions & 0 deletions

File tree

inference_engine/backends/mlx/fused_specdecode.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,88 @@
3535
restored_prefill_cache,
3636
)
3737

38+
# #region agent log (Phase-1 long-gen degeneration debug; remove after fix)
39+
import json as _kjson
40+
import sys as _ksys
41+
42+
43+
def _kdbg(ev: str, **kw: Any) -> None:
44+
"""Emit one compact NDJSON line to stderr (captured by the git-bus bridge)."""
45+
try:
46+
rec = {"ev": ev, **kw}
47+
_ksys.stderr.write("KDBG " + _kjson.dumps(rec, separators=(",", ":")) + "\n")
48+
_ksys.stderr.flush()
49+
except Exception:
50+
pass
51+
52+
53+
def _kdbg_rep(toks: List[int], k: int = 32) -> Dict[str, Any]:
54+
"""Cheap degeneration signal over the last ``k`` generated tokens:
55+
unique fraction + longest single-token run (repetition collapse spikes it)."""
56+
w = toks[-k:]
57+
if not w:
58+
return {"win": 0}
59+
n = len(w)
60+
uniq = len(set(w))
61+
run = best = 1
62+
for a, b in zip(w, w[1:]):
63+
run = run + 1 if a == b else 1
64+
if run > best:
65+
best = run
66+
return {"win": n, "uniq_frac": round(uniq / n, 3),
67+
"rep_frac": round(1.0 - uniq / n, 3), "max_run": best}
68+
69+
70+
def _kdbg_cache(cache: Any) -> Dict[str, Any]:
71+
"""Summarize per-layer cache state: pick the first sliding (RotatingKVCache)
72+
and first full (KVCache) layer and report global offset, physical resident
73+
seq-len, max_size and keep (sink) so we can correlate window-eviction with
74+
the restored-coverage boundary. Also returns layer-class counts."""
75+
sliding = full = None
76+
counts: Dict[str, int] = {}
77+
for c in (cache or []):
78+
cls = type(c).__name__
79+
counts[cls] = counts.get(cls, 0) + 1
80+
keys = getattr(c, "keys", None)
81+
info = {
82+
"cls": cls,
83+
"off": int(getattr(c, "offset", 0)),
84+
"phys": int(keys.shape[2]) if keys is not None else 0,
85+
"ms": (int(getattr(c, "max_size")) if getattr(c, "max_size", None) is not None else None),
86+
"keep": (int(getattr(c, "keep")) if getattr(c, "keep", None) is not None else None),
87+
}
88+
if "Rotating" in cls and sliding is None:
89+
sliding = info
90+
elif "Rotating" not in cls and full is None:
91+
full = info
92+
return {"counts": counts, "sliding": sliding, "full": full}
93+
94+
95+
def _kdbg_lost(cache: Any, restored: Any, prompt_len: int) -> Optional[Dict[str, Any]]:
96+
"""Phase-1 Q2: count sliding-layer positions evicted DURING decode that have
97+
NO restored K/V. For the first RotatingKVCache: positions [keep, evict_hi)
98+
are no longer resident, where evict_hi = offset - (max_size - keep). Of those,
99+
any not in the (prompt-only) restored coverage are 'lost' (no K/V anywhere)."""
100+
for c in (cache or []):
101+
if "Rotating" not in type(c).__name__:
102+
continue
103+
ms = getattr(c, "max_size", None)
104+
if ms is None:
105+
return None
106+
off = int(getattr(c, "offset", 0))
107+
keep = int(getattr(c, "keep", 0) or 0)
108+
ms = int(ms)
109+
evict_hi = off - (ms - keep) # exclusive upper bound of evicted region
110+
evicted_n = max(0, evict_hi - keep)
111+
rset = restored if isinstance(restored, set) else set()
112+
lost = sum(1 for p in range(keep, evict_hi) if p not in rset)
113+
return {"off": off, "ms": ms, "keep": keep, "evict_hi": evict_hi,
114+
"evicted_n": evicted_n, "restored_in_evicted": evicted_n - lost,
115+
"lost": lost, "prompt_len": int(prompt_len),
116+
"window_slid_off_prompt": bool(evict_hi > prompt_len)}
117+
return None
118+
# #endregion
119+
38120

39121
# --------------------------------------------------------------------------- #
40122
# Component A: capture verifier aux-layer hidden states (no transformers
@@ -193,6 +275,27 @@ def prefill(
193275
cache_factory=factory,
194276
)
195277
self._past_len = len(prompt_ids)
278+
# #region agent log (Phase-1)
279+
try:
280+
ev = sorted(int(p) for p in evicted_positions)
281+
rk_layers = sorted(int(k) for k in restored_k_per_layer.keys())
282+
# Stash restored coverage for the decode loop's lost-position check.
283+
self._dbg_restored_positions = set(ev)
284+
self._dbg_prompt_len = int(len(prompt_ids))
285+
_kdbg(
286+
"prefill",
287+
prompt_len=len(prompt_ids),
288+
evicted_count=len(ev),
289+
evicted_lo=(ev[0] if ev else None),
290+
evicted_hi=(ev[-1] if ev else None),
291+
restored_layers=rk_layers,
292+
restored_layer_count=len(rk_layers),
293+
full_kv=bool(self._full_kv),
294+
cache=_kdbg_cache(self._cache),
295+
)
296+
except Exception:
297+
pass
298+
# #endregion
196299

197300
def forward_block(self, tokens: Sequence[int]) -> Any:
198301
"""Incremental verify of ``tokens`` against the restored cache. Returns
@@ -402,6 +505,7 @@ def fused_specdecode_generate_mlx_trim(
402505
ctx_len = C
403506
try:
404507
while len(generated) < gen_tokens:
508+
_kblk_t0 = time.perf_counter() # agent log (Phase-1)
405509
L = min(block_size, gen_tokens - len(generated))
406510
base = adapter._past_len
407511
t_build = time.perf_counter()
@@ -440,6 +544,23 @@ def fused_specdecode_generate_mlx_trim(
440544
commit = check[:accepted]
441545
generated += commit
442546
accepts.append(accepted)
547+
# #region agent log (Phase-1)
548+
_kdbg(
549+
"block",
550+
loop="mlx_trim",
551+
blk=len(accepts) - 1,
552+
gen=len(generated),
553+
past_len=adapter._past_len,
554+
accepted=accepted,
555+
L=int(check_ids.shape[0]),
556+
dt_ms=round((time.perf_counter() - _kblk_t0) * 1e3, 1),
557+
rep=_kdbg_rep(generated),
558+
lost=_kdbg_lost(adapter._cache,
559+
getattr(adapter, "_dbg_restored_positions", set()),
560+
getattr(adapter, "_dbg_prompt_len", 0)),
561+
cache=_kdbg_cache(adapter._cache),
562+
)
563+
# #endregion
443564
adapter.next_token_logits = next_row
444565
aux_rows = adapter._last_aux_mx
445566
# KEEP accepted (positions base..base+accepted-1), TRIM rejected.
@@ -675,6 +796,7 @@ def fused_specdecode_generate(
675796
fallback_to_greedy = False
676797
try:
677798
while len(generated) < gen_tokens:
799+
_kblk_t0 = time.perf_counter() # agent log (Phase-1)
678800
L = min(block_size, gen_tokens - len(generated))
679801
cstart = adapter._past_len
680802
bonus = int(argmax_fn(adapter.next_token_logits))
@@ -731,6 +853,24 @@ def fused_specdecode_generate(
731853
commit = candidate[:accepted] + [correction]
732854
generated += commit
733855
accepts.append(accepted)
856+
# #region agent log (Phase-1)
857+
_kdbg(
858+
"block",
859+
loop="torch_ftheta",
860+
blk=len(accepts) - 1,
861+
gen=len(generated),
862+
gen_since_prompt=len(generated),
863+
past_len=adapter._past_len,
864+
accepted=accepted,
865+
L=len(candidate),
866+
dt_ms=round((time.perf_counter() - _kblk_t0) * 1e3, 1),
867+
rep=_kdbg_rep(generated),
868+
lost=_kdbg_lost(adapter._cache,
869+
getattr(adapter, "_dbg_restored_positions", set()),
870+
getattr(adapter, "_dbg_prompt_len", 0)),
871+
cache=_kdbg_cache(adapter._cache),
872+
)
873+
# #endregion
734874
if any(t in eos for t in commit):
735875
break
736876
if (allow_greedy_fallback and len(accepts) >= 2

0 commit comments

Comments
 (0)