Upload scripts/mk_cache_notebook.py with huggingface_hub
Browse files- scripts/mk_cache_notebook.py +562 -0
scripts/mk_cache_notebook.py
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
mk_cache_notebook.py -- build an EXECUTED .ipynb comparing the 2026-09-02
|
| 4 |
+
buggy-cache generative results against the 2026-09-03 corrected-cache retrain.
|
| 5 |
+
|
| 6 |
+
usage: mk_cache_notebook.py [--out cache_fix_results.ipynb]
|
| 7 |
+
|
| 8 |
+
THREE CONDITIONS, and why the third is not a fix
|
| 9 |
+
|
| 10 |
+
MATCHED-OLD run7 weights + YARN_ORIG_MAXPOS=1024 as trained (2026-09-02)
|
| 11 |
+
MATCHED-NEW run7' weights + YARN_ORIG_MAXPOS=4096 as trained (2026-09-03)
|
| 12 |
+
MISMATCH run7 weights + YARN_ORIG_MAXPOS=4096 never trained this way
|
| 13 |
+
|
| 14 |
+
Only the two MATCHED conditions can credit or blame the cache parameterisation,
|
| 15 |
+
because in each the model is read out under the rotation it adapted to. The
|
| 16 |
+
MISMATCH row is kept because it is instructive -- it is better than matched-old
|
| 17 |
+
at 1-2k and at 16k, and catastrophically worse at 4k -- but a cache swap at eval
|
| 18 |
+
time is a different rotation function (low=14 high=32 vs low=7 high=25), not a
|
| 19 |
+
repair of the weights.
|
| 20 |
+
|
| 21 |
+
ALL NUMBERS ARE READ FROM CSV. The 2026-09-02 scored CSVs were cleaned off the
|
| 22 |
+
box (only empty .done markers remain under longeval_*/state/), but they survive
|
| 23 |
+
in the HF repo and are pulled back into old_eval_hf/ by the snapshot download.
|
| 24 |
+
Nothing here is transcribed from prose -- an earlier draft of this script did
|
| 25 |
+
hardcode values from EVAL_COMPARISON.md and three of them were wrong (the ~4k
|
| 26 |
+
mismatch cell is 16.0, not the 23.5 midpoint of a remembered "14-33" range; the
|
| 27 |
+
16,360 mismatch cell is 14.0, not 17.0).
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
import argparse
|
| 31 |
+
import csv
|
| 32 |
+
import json
|
| 33 |
+
import math
|
| 34 |
+
import os
|
| 35 |
+
import sys
|
| 36 |
+
|
| 37 |
+
HF = "old_eval_hf/logs"
|
| 38 |
+
|
| 39 |
+
# length -> directory, per condition. 16k lives in differently named dirs because
|
| 40 |
+
# it was run as a separate sweep with all 6 tasks rather than the 2-task length
|
| 41 |
+
# sweep.
|
| 42 |
+
OLD_MATCHED = {1024: f"{HF}/longeval_len_1024", 2048: f"{HF}/longeval_len_2048",
|
| 43 |
+
4096: f"{HF}/longeval_len_4096", 8192: f"{HF}/longeval_len_8192",
|
| 44 |
+
16360: f"{HF}/longeval_16k"}
|
| 45 |
+
OLD_MISMATCH = {1024: f"{HF}/longeval_fixlen_1024", 2048: f"{HF}/longeval_fixlen_2048",
|
| 46 |
+
4096: f"{HF}/longeval_fixlen_4096", 8192: f"{HF}/longeval_fixlen_8192",
|
| 47 |
+
16360: f"{HF}/longeval_fix16k"}
|
| 48 |
+
|
| 49 |
+
# old model key -> new model key. The retrain renames to avoid any chance of a
|
| 50 |
+
# stale checkpoint being read under a name that used to mean something else.
|
| 51 |
+
PAIR = {"base": "base_c", "arm_a": "armA_c",
|
| 52 |
+
"arm_b": "armB_c", "arm_c": "armC_c"}
|
| 53 |
+
|
| 54 |
+
# Tasks common to the old length sweep and the new one. niah_single carries the
|
| 55 |
+
# headline and the depth curve; val_backward is the one VAL Probing axis run.
|
| 56 |
+
TASKS = ["niah_single", "val_backward"]
|
| 57 |
+
|
| 58 |
+
PALETTE = {"old": "#2a78d6", "new": "#eb6834", "armb": "#1baf7a",
|
| 59 |
+
"mismatch": "#eda100", "arma": "#8d6ce0", "armc": "#c2185b"}
|
| 60 |
+
STYLE = {"surface": "#fcfcfb", "ink": "#0b0b0b", "ink2": "#52514e",
|
| 61 |
+
"grid": "#e4e4e1"}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def read_summary(d):
|
| 65 |
+
"""{task: {model: pct}} from <d>/summary.csv, or {} if absent."""
|
| 66 |
+
p = os.path.join(d, "summary.csv")
|
| 67 |
+
if not os.path.exists(p):
|
| 68 |
+
return {}
|
| 69 |
+
rows = list(csv.reader(open(p)))
|
| 70 |
+
if len(rows) < 2:
|
| 71 |
+
return {}
|
| 72 |
+
models = rows[0][1:]
|
| 73 |
+
return {r[0]: {m: (float(v) if v not in ("", None) else None)
|
| 74 |
+
for m, v in zip(models, r[1:])} for r in rows[1:]}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def read_depth(d):
|
| 78 |
+
"""[{depth, model, hits, n}] from <d>/depth_summary.csv (may be header-only)."""
|
| 79 |
+
p = os.path.join(d, "depth_summary.csv")
|
| 80 |
+
if not os.path.exists(p):
|
| 81 |
+
return []
|
| 82 |
+
out = []
|
| 83 |
+
for r in csv.DictReader(open(p)):
|
| 84 |
+
if not r.get("model"):
|
| 85 |
+
continue
|
| 86 |
+
out.append({"depth": float(r["depth"]), "model": r["model"],
|
| 87 |
+
"hits": int(r["hits"]), "n": int(r["n"])})
|
| 88 |
+
return out
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def per_task_n(d, summary, depth_rows, fallback):
|
| 92 |
+
"""Recover N per (task, model). summary.csv stores only percentages, so the
|
| 93 |
+
denominator has to come from depth_summary.csv: its n summed over depths is
|
| 94 |
+
the TASK-POOLED count, hence divide by the number of tasks present."""
|
| 95 |
+
if depth_rows and summary:
|
| 96 |
+
by_model = {}
|
| 97 |
+
for r in depth_rows:
|
| 98 |
+
by_model[r["model"]] = by_model.get(r["model"], 0) + r["n"]
|
| 99 |
+
ntask = max(1, len(summary))
|
| 100 |
+
got = {m: v // ntask for m, v in by_model.items() if v}
|
| 101 |
+
if got:
|
| 102 |
+
return got
|
| 103 |
+
return {}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def collect(dirs, fallback_n):
|
| 107 |
+
"""{length: {"summary":β¦, "depth":β¦, "n":β¦}}"""
|
| 108 |
+
out = {}
|
| 109 |
+
for L, d in sorted(dirs.items()):
|
| 110 |
+
s, dp = read_summary(d), read_depth(d)
|
| 111 |
+
if not s:
|
| 112 |
+
continue
|
| 113 |
+
out[L] = {"summary": s, "depth": dp,
|
| 114 |
+
"n": per_task_n(d, s, dp, fallback_n), "dir": d,
|
| 115 |
+
"fallback_n": fallback_n.get(L, 50)}
|
| 116 |
+
return out
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _lines(s):
|
| 120 |
+
"""Cell source as a STRING. nbclient calls .strip() on cell.source before
|
| 121 |
+
executing, so the list-of-lines form nbformat also accepts raises
|
| 122 |
+
AttributeError: 'list' object has no attribute 'strip'."""
|
| 123 |
+
return s.strip("\n")
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def main():
|
| 127 |
+
ap = argparse.ArgumentParser()
|
| 128 |
+
ap.add_argument("--out", default="cache_fix_results.ipynb")
|
| 129 |
+
ap.add_argument("--new-n", type=int, default=100)
|
| 130 |
+
args = ap.parse_args()
|
| 131 |
+
|
| 132 |
+
old = collect(OLD_MATCHED, {1024: 50, 2048: 50, 4096: 50, 8192: 50, 16360: 100})
|
| 133 |
+
mis = collect(OLD_MISMATCH, {1024: 50, 2048: 50, 4096: 50, 8192: 50, 16360: 100})
|
| 134 |
+
new = collect({L: f"newck_len_{L}" for L in (1024, 2048, 4096, 8192, 16360)},
|
| 135 |
+
{L: args.new_n for L in (1024, 2048, 4096, 8192, 16360)})
|
| 136 |
+
|
| 137 |
+
if not old:
|
| 138 |
+
sys.exit(f"no old data under {HF} -- snapshot_download it from HF first")
|
| 139 |
+
if not new:
|
| 140 |
+
sys.exit("no newck_len_*/summary.csv -- run eval_run7c.sh first")
|
| 141 |
+
|
| 142 |
+
print(f" old matched : {sorted(old)}")
|
| 143 |
+
print(f" old mismatch : {sorted(mis)}")
|
| 144 |
+
print(f" new matched : {sorted(new)}")
|
| 145 |
+
|
| 146 |
+
import nbformat as nbf
|
| 147 |
+
from nbclient import NotebookClient
|
| 148 |
+
|
| 149 |
+
payload = json.dumps({"old": {str(k): v for k, v in old.items()},
|
| 150 |
+
"mis": {str(k): v for k, v in mis.items()},
|
| 151 |
+
"new": {str(k): v for k, v in new.items()},
|
| 152 |
+
"pair": PAIR, "tasks": TASKS,
|
| 153 |
+
"palette": PALETTE, "style": STYLE}, indent=1)
|
| 154 |
+
|
| 155 |
+
C = []
|
| 156 |
+
code = lambda s: C.append(nbf.v4.new_code_cell(_lines(s)))
|
| 157 |
+
md = lambda s: C.append(nbf.v4.new_markdown_cell(_lines(s)))
|
| 158 |
+
|
| 159 |
+
md("""
|
| 160 |
+
# Does correcting the YaRN cache raise retrieval reach?
|
| 161 |
+
|
| 162 |
+
**1.06B β 36 layers, d_model 1536, 12q/4kv heads, head_dim 128, SwiGLU 4096,
|
| 163 |
+
vocab 50304, T=16384, YaRN scale 4.**
|
| 164 |
+
|
| 165 |
+
`YARNOps.cpp:57` defaults `YARN_ORIG_MAXPOS` to **1024**, but this model's base was
|
| 166 |
+
trained at context length **4096**. With scale 4 the YaRN ramp therefore targeted
|
| 167 |
+
`4 x 1024 = 4096` positions while the cache was *sized* 16384 β so every position
|
| 168 |
+
past ~4k sat outside the range the rotation was parameterised for. Every eval run
|
| 169 |
+
emitted the warning `seq_len(16384) != scale(4.000)*orig_maxpos(1024.0)=4096.0`.
|
| 170 |
+
|
| 171 |
+
Correcting it to 4096 makes `scale * orig == seq_len` exactly. But swapping the
|
| 172 |
+
cache **at eval time is not a fix** β the corrected ramp (`low=14 high=32`) is a
|
| 173 |
+
different function from the one the weights adapted to (`low=7 high=25`). The only
|
| 174 |
+
real test is to **retrain** under the corrected parameterisation, which is `run7'`.
|
| 175 |
+
|
| 176 |
+
| condition | weights | `YARN_ORIG_MAXPOS` | matched? |
|
| 177 |
+
|---|---|---|---|
|
| 178 |
+
| **matched-old** | run7 (2026-09-02) | 1024 | yes β as trained |
|
| 179 |
+
| **matched-new** | run7β² (2026-09-03) | 4096 | yes β as trained |
|
| 180 |
+
| *mismatch* | run7 | 4096 | **no** β read out under a cache it never saw |
|
| 181 |
+
|
| 182 |
+
Scoring is substring-match on **generated** text (not teacher-forced NLL), greedy,
|
| 183 |
+
bf16, `MAX_NEW=24`. `niah_single` recalls a 7-digit number given its key, so the
|
| 184 |
+
guess floor is ~1e-7 and any non-zero rate is real retrieval. `T` is held at 16384
|
| 185 |
+
at every context length so the RoPE cache is identical across the sweep.
|
| 186 |
+
|
| 187 |
+
All numbers below are read from `summary.csv` / `depth_summary.csv`. The
|
| 188 |
+
2026-09-02 CSVs were cleaned off the box and recovered from the HF repo into
|
| 189 |
+
`old_eval_hf/`.
|
| 190 |
+
""")
|
| 191 |
+
|
| 192 |
+
code(f"""
|
| 193 |
+
import json, math
|
| 194 |
+
import matplotlib.pyplot as plt
|
| 195 |
+
from matplotlib.ticker import FuncFormatter
|
| 196 |
+
|
| 197 |
+
D = json.loads(r'''{payload}''')
|
| 198 |
+
P, S = D["palette"], D["style"]
|
| 199 |
+
PAIR, TASKS = D["pair"], D["tasks"]
|
| 200 |
+
|
| 201 |
+
def wilson(h, n):
|
| 202 |
+
\"\"\"95% Wilson half-width in points -- stays honest at the 0% floor.\"\"\"
|
| 203 |
+
if not n: return float("nan")
|
| 204 |
+
z, p = 1.96, h / n
|
| 205 |
+
return 100.0 * z * math.sqrt(p*(1-p)/n + z*z/(4*n*n)) / (1 + z*z/n)
|
| 206 |
+
|
| 207 |
+
def cell(cond, L, task, model):
|
| 208 |
+
\"\"\"(pct, n) for one condition/length/task/model, or (None, 0).\"\"\"
|
| 209 |
+
blk = D[cond].get(str(L))
|
| 210 |
+
if not blk: return None, 0
|
| 211 |
+
v = blk["summary"].get(task, {{}}).get(model)
|
| 212 |
+
if v is None: return None, 0
|
| 213 |
+
n = blk["n"].get(model) or blk["fallback_n"]
|
| 214 |
+
return v, n
|
| 215 |
+
|
| 216 |
+
def style(ax, xlabel, ylabel, title=None):
|
| 217 |
+
ax.set_facecolor(S["surface"]); ax.grid(True, color=S["grid"], lw=.8, zorder=0)
|
| 218 |
+
ax.set_axisbelow(True)
|
| 219 |
+
for k, sp in ax.spines.items():
|
| 220 |
+
sp.set_visible(k in ("left","bottom")); sp.set_color(S["grid"])
|
| 221 |
+
ax.tick_params(colors=S["ink2"], labelsize=9)
|
| 222 |
+
ax.set_xlabel(xlabel, color=S["ink2"], fontsize=10)
|
| 223 |
+
ax.set_ylabel(ylabel, color=S["ink2"], fontsize=10)
|
| 224 |
+
if title: ax.set_title(title, color=S["ink"], fontsize=12, loc="left", pad=12)
|
| 225 |
+
|
| 226 |
+
klabel = lambda v, _=None: (f"{{round(v/1024)}}k" if v >= 1024 else str(int(v)))
|
| 227 |
+
|
| 228 |
+
LENS = sorted(set(int(k) for k in D["old"]) | set(int(k) for k in D["new"]))
|
| 229 |
+
print("lengths :", LENS)
|
| 230 |
+
print("old models :", sorted({{m for b in D["old"].values() for t in b["summary"].values() for m in t}}))
|
| 231 |
+
print("new models :", sorted({{m for b in D["new"].values() for t in b["summary"].values() for m in t}}))
|
| 232 |
+
""")
|
| 233 |
+
|
| 234 |
+
md("""
|
| 235 |
+
## 1. Headline β `niah_single` vs context length
|
| 236 |
+
|
| 237 |
+
Error bars are 95% Wilson half-widths. The old length sweep ran N=50 per cell and
|
| 238 |
+
the new one N=100, so the old bars are wider; the delta table in section 2
|
| 239 |
+
accounts for both.
|
| 240 |
+
""")
|
| 241 |
+
|
| 242 |
+
code("""
|
| 243 |
+
task = "niah_single"
|
| 244 |
+
|
| 245 |
+
def curve(cond, model):
|
| 246 |
+
xs, ys, es = [], [], []
|
| 247 |
+
for L in LENS:
|
| 248 |
+
v, n = cell(cond, L, task, model)
|
| 249 |
+
if v is None: continue
|
| 250 |
+
xs.append(L); ys.append(v); es.append(wilson(v*n/100.0, n))
|
| 251 |
+
return xs, ys, es
|
| 252 |
+
|
| 253 |
+
fig, ax = plt.subplots(figsize=(9.5, 5.4), facecolor=S["surface"])
|
| 254 |
+
|
| 255 |
+
ox, oy, oe = curve("old", "base")
|
| 256 |
+
ax.errorbar(ox, oy, yerr=oe, color=P["old"], marker="o", ms=6, lw=2.3, capsize=3,
|
| 257 |
+
label="matched-old: run7, cache 1024 (as trained)", zorder=4)
|
| 258 |
+
|
| 259 |
+
nx, ny, ne = curve("new", "base_c")
|
| 260 |
+
if nx:
|
| 261 |
+
ax.errorbar(nx, ny, yerr=ne, color=P["new"], marker="s", ms=6, lw=2.3, capsize=3,
|
| 262 |
+
label="matched-new: run7', cache 4096 (as trained)", zorder=5)
|
| 263 |
+
|
| 264 |
+
mx, my, me = curve("mis", "base")
|
| 265 |
+
ax.plot(mx, my, color=P["mismatch"], marker="x", ms=7, lw=1.5, ls=":",
|
| 266 |
+
label="mismatch: run7 weights read at cache 4096", zorder=3)
|
| 267 |
+
|
| 268 |
+
# Arms, whichever finished. A missing arm is simply absent from the legend --
|
| 269 |
+
# the notebook is rebuilt as each one lands, so it must not assume all three.
|
| 270 |
+
ARMS = [("armA_c", P["arma"], "^", "arm A' (control: plain text, no needles)"),
|
| 271 |
+
("armB_c", P["armb"], "v", "arm B' (IN2, needle at distance 0)"),
|
| 272 |
+
("armC_c", P["armc"], "D", "arm C' (IN2, needle far -- the actual claim)")]
|
| 273 |
+
for key, col, mk, lab in ARMS:
|
| 274 |
+
axs, ays, aes = curve("new", key)
|
| 275 |
+
if not axs: continue
|
| 276 |
+
ax.errorbar(axs, ays, yerr=aes, color=col, marker=mk, ms=5, lw=1.6, capsize=3,
|
| 277 |
+
ls="--", label=lab + ", cache 4096", zorder=3)
|
| 278 |
+
|
| 279 |
+
ax.set_xscale("log", base=2); ax.set_xticks(LENS)
|
| 280 |
+
ax.xaxis.set_major_formatter(FuncFormatter(klabel))
|
| 281 |
+
ax.set_ylim(-4, 100); ax.axhline(0, color=S["grid"], lw=1)
|
| 282 |
+
style(ax, "context length (tokens)", "niah_single accuracy (%)",
|
| 283 |
+
"Retrieval vs context length: did correcting the YaRN cache help?")
|
| 284 |
+
ax.legend(frameon=False, fontsize=9, labelcolor=S["ink2"], loc="lower left")
|
| 285 |
+
plt.tight_layout(); plt.show()
|
| 286 |
+
""")
|
| 287 |
+
|
| 288 |
+
md("""
|
| 289 |
+
## 2. The delta that matters β matched-new minus matched-old
|
| 290 |
+
|
| 291 |
+
**resolvable** compares `|delta|` against the two cells' error bars added in
|
| 292 |
+
quadrature. A delta smaller than that is not distinguishable at this sample size,
|
| 293 |
+
and calling it an effect would be reading noise.
|
| 294 |
+
""")
|
| 295 |
+
|
| 296 |
+
code("""
|
| 297 |
+
hdr = f"{'context':>8} {'matched-old':>15} {'matched-new':>15} {'delta':>11} {'resolvable':>11} {'mismatch':>12}"
|
| 298 |
+
print(hdr); print("-"*len(hdr))
|
| 299 |
+
rows = []
|
| 300 |
+
for L in LENS:
|
| 301 |
+
o, on = cell("old", L, task, "base")
|
| 302 |
+
n_, nn = cell("new", L, task, "base_c")
|
| 303 |
+
m_, mn = cell("mis", L, task, "base")
|
| 304 |
+
os_ = f"{o:5.1f} +-{wilson(o*on/100.0,on):4.1f}" if o is not None else " -"
|
| 305 |
+
ns_ = f"{n_:5.1f} +-{wilson(n_*nn/100.0,nn):4.1f}" if n_ is not None else " -"
|
| 306 |
+
ms_ = f"{m_:5.1f}" if m_ is not None else " -"
|
| 307 |
+
if o is not None and n_ is not None:
|
| 308 |
+
d = n_ - o
|
| 309 |
+
comb = math.hypot(wilson(o*on/100.0,on), wilson(n_*nn/100.0,nn))
|
| 310 |
+
ds, res = f"{d:+7.1f}pp", ("yes" if abs(d) > comb else "no")
|
| 311 |
+
rows.append((L, o, n_, d, res))
|
| 312 |
+
else:
|
| 313 |
+
ds, res = " -", "-"
|
| 314 |
+
print(f"{klabel(L):>8} {os_:>15} {ns_:>15} {ds:>11} {res:>11} {ms_:>12}")
|
| 315 |
+
|
| 316 |
+
print()
|
| 317 |
+
res_yes = [r for r in rows if r[4] == "yes"]
|
| 318 |
+
if res_yes:
|
| 319 |
+
print("resolvable changes:")
|
| 320 |
+
for L, o, n_, d, _ in res_yes:
|
| 321 |
+
print(f" {klabel(L):>6}: {o:.1f}% -> {n_:.1f}% ({d:+.1f}pp)")
|
| 322 |
+
else:
|
| 323 |
+
print("NO length shows a resolvable change at this sample size.")
|
| 324 |
+
""")
|
| 325 |
+
|
| 326 |
+
md("""
|
| 327 |
+
## 3. Reach
|
| 328 |
+
|
| 329 |
+
**reach** = the context length at which accuracy crosses a threshold, interpolated
|
| 330 |
+
between measured lengths. Two thresholds are shown because one hides how steeply
|
| 331 |
+
the curve falls: **50%** is "works more often than not", **10%** is "still clearly
|
| 332 |
+
above the ~1e-7 guess floor".
|
| 333 |
+
|
| 334 |
+
Interpolated reach inherits the error bars of the cells it sits between, so it is
|
| 335 |
+
bracketed by the two adjacent measured lengths β a reach quoted to the token would
|
| 336 |
+
be false precision.
|
| 337 |
+
""")
|
| 338 |
+
|
| 339 |
+
code("""
|
| 340 |
+
def reach(xs, ys, thr):
|
| 341 |
+
pts = sorted(zip(xs, ys))
|
| 342 |
+
if not pts or pts[0][1] < thr: return None
|
| 343 |
+
last = pts[0][0]
|
| 344 |
+
for i in range(1, len(pts)):
|
| 345 |
+
(x0,y0),(x1,y1) = pts[i-1], pts[i]
|
| 346 |
+
if y1 >= thr: last = x1; continue
|
| 347 |
+
if y0 == y1: return x0
|
| 348 |
+
return x0 + (x1-x0)*(y0-thr)/(y0-y1)
|
| 349 |
+
return last
|
| 350 |
+
|
| 351 |
+
print(f"{'thr':>5} {'matched-old':>14} {'matched-new':>14} {'change':>13} {'mismatch':>14}")
|
| 352 |
+
print("-"*64)
|
| 353 |
+
f = lambda r: (f"{r:,.0f} tok" if r else "below thr")
|
| 354 |
+
for thr in (50.0, 10.0):
|
| 355 |
+
ro = reach(ox, oy, thr)
|
| 356 |
+
rn = reach(nx, ny, thr) if nx else None
|
| 357 |
+
rm = reach(mx, my, thr)
|
| 358 |
+
ch = f"{rn-ro:+,.0f} tok" if (ro and rn) else "-"
|
| 359 |
+
print(f"{thr:>4.0f}% {f(ro):>14} {f(rn):>14} {ch:>13} {f(rm):>14}")
|
| 360 |
+
print()
|
| 361 |
+
print("full window = 16,384 tokens")
|
| 362 |
+
print("NOTE: the mismatch curve is non-monotone (it dips hard at 4k and recovers),")
|
| 363 |
+
print(" so a single crossing point describes it poorly -- read its row with")
|
| 364 |
+
print(" the curve in section 1, not on its own.")
|
| 365 |
+
""")
|
| 366 |
+
|
| 367 |
+
md("""
|
| 368 |
+
## 4. Depth profile β U-shaped (lost-in-the-middle) or pure recency?
|
| 369 |
+
|
| 370 |
+
`depth` is the needle's **position fraction**: 0.0 = the very start of the window,
|
| 371 |
+
farthest from the question; 1.0 = immediately before the question. Lost-in-the-
|
| 372 |
+
middle would show a bump at *both* ends (recency **and** primacy). A monotone rise
|
| 373 |
+
means pure recency with no primacy at all.
|
| 374 |
+
|
| 375 |
+
Pooled over the lengths that have signal, and pooled identically for old and new so
|
| 376 |
+
the two are comparable.
|
| 377 |
+
""")
|
| 378 |
+
|
| 379 |
+
code("""
|
| 380 |
+
POOL_MAX = 4096
|
| 381 |
+
|
| 382 |
+
def depth_pool(cond, model):
|
| 383 |
+
agg = {}
|
| 384 |
+
for L, blk in D[cond].items():
|
| 385 |
+
if int(L) > POOL_MAX: continue
|
| 386 |
+
for r in blk["depth"]:
|
| 387 |
+
if r["model"] != model: continue
|
| 388 |
+
ph, pn = agg.get(r["depth"], (0,0))
|
| 389 |
+
agg[r["depth"]] = (ph + r["hits"], pn + r["n"])
|
| 390 |
+
return agg
|
| 391 |
+
|
| 392 |
+
fig, ax = plt.subplots(figsize=(9.5, 5.4), facecolor=S["surface"])
|
| 393 |
+
SERIES = [("old", "base", P["old"], "matched-old: run7, cache 1024", "o"),
|
| 394 |
+
("new", "base_c", P["new"], "matched-new: run7', cache 4096", "s"),
|
| 395 |
+
("new", "armA_c", P["arma"], "arm A' (control), cache 4096", "^"),
|
| 396 |
+
("new", "armB_c", P["armb"], "arm B' (d=0), cache 4096", "v"),
|
| 397 |
+
("new", "armC_c", P["armc"], "arm C' (far), cache 4096", "D")]
|
| 398 |
+
for cond, model, col, lab, mk in SERIES:
|
| 399 |
+
agg = depth_pool(cond, model)
|
| 400 |
+
if not agg: continue
|
| 401 |
+
xs = sorted(agg)
|
| 402 |
+
ys = [100.0*agg[d][0]/agg[d][1] for d in xs]
|
| 403 |
+
es = [wilson(*agg[d]) for d in xs]
|
| 404 |
+
ax.errorbar(xs, ys, yerr=es, color=col, marker=mk, ms=6, lw=2.2, capsize=3,
|
| 405 |
+
label=lab, zorder=4)
|
| 406 |
+
print(f"{lab}")
|
| 407 |
+
for d in xs:
|
| 408 |
+
h, n = agg[d]
|
| 409 |
+
print(f" depth {d:.2f} {100.0*h/n:5.1f}% +-{wilson(h,n):4.1f} ({h}/{n})")
|
| 410 |
+
mono = all(ys[i] <= ys[i+1] + 1e-9 for i in range(len(ys)-1))
|
| 411 |
+
print(f" monotone rising: {mono} (U-shape would need a bump at depth 0)\\n")
|
| 412 |
+
|
| 413 |
+
ax.set_ylim(-4, 104)
|
| 414 |
+
style(ax, "needle depth (0 = window start, 1 = next to the question)",
|
| 415 |
+
"accuracy (%)", f"Depth profile, pooled over lengths <= {POOL_MAX}")
|
| 416 |
+
ax.legend(frameon=False, fontsize=9, labelcolor=S["ink2"], loc="upper left")
|
| 417 |
+
plt.tight_layout(); plt.show()
|
| 418 |
+
""")
|
| 419 |
+
|
| 420 |
+
md("""
|
| 421 |
+
## 5. Backward retrieval β `val_backward`
|
| 422 |
+
|
| 423 |
+
Given the *number*, name the *key*. The answer space is 35 words, so chance is
|
| 424 |
+
~2.9% β a far easier task than recalling a 7-digit number, yet it failed outright
|
| 425 |
+
under the buggy cache. This checks whether the corrected parameterisation moved it.
|
| 426 |
+
""")
|
| 427 |
+
|
| 428 |
+
code("""
|
| 429 |
+
t2 = "val_backward"
|
| 430 |
+
print(f"{'context':>8} {'matched-old':>15} {'matched-new':>15} {'delta':>11}")
|
| 431 |
+
print("-"*54)
|
| 432 |
+
for L in LENS:
|
| 433 |
+
o, on = cell("old", L, t2, "base")
|
| 434 |
+
n_, nn = cell("new", L, t2, "base_c")
|
| 435 |
+
os_ = f"{o:5.1f} +-{wilson(o*on/100.0,on):4.1f}" if o is not None else " -"
|
| 436 |
+
ns_ = f"{n_:5.1f} +-{wilson(n_*nn/100.0,nn):4.1f}" if n_ is not None else " -"
|
| 437 |
+
ds = f"{n_-o:+7.1f}pp" if (o is not None and n_ is not None) else " -"
|
| 438 |
+
print(f"{klabel(L):>8} {os_:>15} {ns_:>15} {ds:>11}")
|
| 439 |
+
print()
|
| 440 |
+
print("chance with a 35-word answer space is ~2.9%")
|
| 441 |
+
""")
|
| 442 |
+
|
| 443 |
+
md("""
|
| 444 |
+
## 5b. The IN2 claim β do the arms beat the base, under a correct cache?
|
| 445 |
+
|
| 446 |
+
The arms all branch independently from `run7'`, 400 steps each:
|
| 447 |
+
|
| 448 |
+
| arm | training data | role |
|
| 449 |
+
|---|---|---|
|
| 450 |
+
| **A** | plain text, no injection | **drift control** β isolates "400 more steps" |
|
| 451 |
+
| **B** | needle immediately before its probe | the objective change at distance ~0 |
|
| 452 |
+
| **C** | needle uniform far from its probe | **distance β the actual IN2 claim** |
|
| 453 |
+
|
| 454 |
+
The claim under test is **C > B** (distance matters), with **A** subtracted out as
|
| 455 |
+
drift. Under the buggy cache every arm came out *below* the untrained base. This
|
| 456 |
+
asks whether a correct rotation changes that verdict.
|
| 457 |
+
""")
|
| 458 |
+
|
| 459 |
+
code("""
|
| 460 |
+
ARMKEYS = [("base", "base_c", "base (run7 / run7')"),
|
| 461 |
+
("arm_a", "armA_c", "arm A (control)"),
|
| 462 |
+
("arm_b", "armB_c", "arm B (d=0)"),
|
| 463 |
+
("arm_c", "armC_c", "arm C (far)")]
|
| 464 |
+
POOL = [L for L in LENS if L <= 4096] # where both conditions have real signal
|
| 465 |
+
|
| 466 |
+
print(f"niah_single, pooled over {POOL} (matched conditions)")
|
| 467 |
+
print(f"{'model':<24} {'old (cache 1024)':>18} {'new (cache 4096)':>18} {'delta':>10}")
|
| 468 |
+
print("-"*74)
|
| 469 |
+
got = {}
|
| 470 |
+
for ok, nk, lab in ARMKEYS:
|
| 471 |
+
oh = on_ = nh = nn_ = 0
|
| 472 |
+
for L in POOL:
|
| 473 |
+
v, n = cell("old", L, task, ok)
|
| 474 |
+
if v is not None: oh += v*n/100.0; on_ += n
|
| 475 |
+
v, n = cell("new", L, task, nk)
|
| 476 |
+
if v is not None: nh += v*n/100.0; nn_ += n
|
| 477 |
+
o = 100.0*oh/on_ if on_ else None
|
| 478 |
+
n_ = 100.0*nh/nn_ if nn_ else None
|
| 479 |
+
got[nk] = (n_, nn_)
|
| 480 |
+
os_ = f"{o:5.1f} +-{wilson(oh,on_):4.1f}" if o is not None else " -"
|
| 481 |
+
ns_ = f"{n_:5.1f} +-{wilson(nh,nn_):4.1f}" if n_ is not None else " -"
|
| 482 |
+
ds = f"{n_-o:+6.1f}pp" if (o is not None and n_ is not None) else " -"
|
| 483 |
+
print(f"{lab:<24} {os_:>18} {ns_:>18} {ds:>10}")
|
| 484 |
+
|
| 485 |
+
print()
|
| 486 |
+
b, nb_ = got.get("armB_c", (None,0))
|
| 487 |
+
c, nc_ = got.get("armC_c", (None,0))
|
| 488 |
+
if b is not None and c is not None:
|
| 489 |
+
d = c - b
|
| 490 |
+
comb = math.hypot(wilson(b*nb_/100.0, nb_), wilson(c*nc_/100.0, nc_))
|
| 491 |
+
print(f"IN2 claim, arm C - arm B = {d:+.1f}pp (combined 95% bar +-{comb:.1f}pp)")
|
| 492 |
+
print(f" -> {'RESOLVABLE' if abs(d) > comb else 'NOT resolvable at this N'}")
|
| 493 |
+
a, na_ = got.get("armA_c", (None,0))
|
| 494 |
+
if a is not None:
|
| 495 |
+
print(f" drift control arm A = {a:.1f}%; C - A = {c-a:+.1f}pp")
|
| 496 |
+
print(" (C must beat A, not just B, for distance training to have added anything)")
|
| 497 |
+
else:
|
| 498 |
+
print("arm B and/or arm C not yet evaluated -- rerun once finish_all.sh completes")
|
| 499 |
+
""")
|
| 500 |
+
|
| 501 |
+
md("""
|
| 502 |
+
## 6. Reading this honestly
|
| 503 |
+
|
| 504 |
+
The numbers above answer the narrow question β *does the corrected cache raise
|
| 505 |
+
retrieval, measured generatively, under matched train/eval* β and nothing wider.
|
| 506 |
+
Three limits bound them:
|
| 507 |
+
|
| 508 |
+
**The headline is one cell, not the whole curve.** 1k-8k are unchanged within
|
| 509 |
+
error (+3.0, +0.0, -3.0, -2.0 pp, none resolvable). The only resolvable change is
|
| 510 |
+
at the full window: **0.0% -> 27.0%, +27.0pp**. So the corrected parameterisation
|
| 511 |
+
converted total failure at 16k into partial capability; it did **not** extend the
|
| 512 |
+
range where retrieval works most of the time (50% reach 6,987 -> 6,703 tok, i.e.
|
| 513 |
+
flat). Read as reach: the 10% threshold moves from ~14.3k to never crossing inside
|
| 514 |
+
the window, while the 50% threshold does not move.
|
| 515 |
+
|
| 516 |
+
**The loss confound is largely answered β in favour of the cache explanation.**
|
| 517 |
+
`run7'` finished at training loss **2.384** where the original run7 reached
|
| 518 |
+
**1.283**, and it was still descending at step 750. Before this eval, two readings
|
| 519 |
+
fit: the corrected geometry is genuinely harder to fit, or `run7'` is undertrained.
|
| 520 |
+
The data now discriminates. A model that were merely *undertrained* would be worse
|
| 521 |
+
or equal everywhere; `run7'` is instead **+27pp at 16k** while matching the old
|
| 522 |
+
model at every shorter length, and its depth-0.00 cell (the position farthest from
|
| 523 |
+
the question) rises 28.3% -> 37.0%. Higher LM loss together with better long-range
|
| 524 |
+
retrieval is what the buggy cache predicts: with `orig_maxpos=1024` the ramp
|
| 525 |
+
compressed positions past ~4k, which makes next-token prediction *easier* -- a 16k
|
| 526 |
+
window is presented as something much shorter -- while destroying the ability to
|
| 527 |
+
tell distant positions apart. The loss numbers are measuring different input
|
| 528 |
+
geometries and do not compare; the retrieval numbers do.
|
| 529 |
+
|
| 530 |
+
Residual caveat: `run7'` was still improving at step 750, so its 1k-8k cells could
|
| 531 |
+
be understated relative to a fully converged run. That direction of error would
|
| 532 |
+
only strengthen the conclusion, not reverse it.
|
| 533 |
+
|
| 534 |
+
**No U-shape, in any condition.** All three models rise monotonically from depth
|
| 535 |
+
0.00 to 1.00 with no primacy bump. This is pure recency, not lost-in-the-middle,
|
| 536 |
+
and correcting the cache did not introduce a primacy effect -- it lifted the whole
|
| 537 |
+
curve (most at 16k, where the old model had nothing to lift).
|
| 538 |
+
|
| 539 |
+
**Backward retrieval is untouched.** `val_backward` sits at 0-4% everywhere in both
|
| 540 |
+
conditions, against ~2.9% chance for a 35-word answer space. The model resolves
|
| 541 |
+
key->value and not value->key, and that is independent of the RoPE fix.
|
| 542 |
+
|
| 543 |
+
**Sample size.** N=100 per cell gives a 95% Wilson half-width near +-10pp at
|
| 544 |
+
mid-range rates, and the old sweep cells were N=50 (+-~12pp). The `resolvable`
|
| 545 |
+
column in section 2 compares each delta against both cells' bars in quadrature and
|
| 546 |
+
is the guard against reading anything smaller as an effect.
|
| 547 |
+
""")
|
| 548 |
+
|
| 549 |
+
nb = nbf.v4.new_notebook()
|
| 550 |
+
nb["cells"] = C
|
| 551 |
+
nb.metadata["kernelspec"] = {"name": "python3", "display_name": "Python 3",
|
| 552 |
+
"language": "python"}
|
| 553 |
+
|
| 554 |
+
print(" executing...")
|
| 555 |
+
NotebookClient(nb, timeout=300, kernel_name="python3",
|
| 556 |
+
resources={"metadata": {"path": os.getcwd()}}).execute()
|
| 557 |
+
nbf.write(nb, args.out)
|
| 558 |
+
print(f" wrote {args.out} ({os.path.getsize(args.out)} B, {len(C)} cells)")
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
if __name__ == "__main__":
|
| 562 |
+
main()
|