Skip to content

Commit 46410ad

Browse files
K3: fix integrated NIAH eval to use real niah_eval API (chat-template encode, aggregate_recall, v04_dlm_restored window)
Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
1 parent d257a11 commit 46410ad

1 file changed

Lines changed: 117 additions & 127 deletions

File tree

scripts/research/k3_integrated_niah_eval.py

Lines changed: 117 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
from __future__ import annotations
6363

6464
import argparse
65+
import dataclasses
6566
import json
6667
import math
6768
import random
@@ -168,129 +169,128 @@ def main() -> int:
168169

169170
# ---------- NIAH dataset ----------
170171
samples: List[NIAHSample] = make_niah_dataset(
171-
tokenizer,
172172
n_samples=args.n_samples,
173173
haystack_min_lines=args.haystack_min_lines,
174174
haystack_max_lines=args.haystack_max_lines,
175175
seed=args.seed,
176176
)
177-
print(f"[k3-integrated] generated {len(samples)} NIAH samples", file=sys.stderr)
177+
178+
# Encode prompts via chat template (ADR 0008 §2.4: the runtime is
179+
# template-free; the harness applies the template), matching the
180+
# K1.E NIAH harness convention.
181+
def encode_chat(prompt_text: str) -> torch.Tensor:
182+
messages = [{"role": "user", "content": prompt_text}]
183+
ids = tokenizer.apply_chat_template(
184+
messages, add_generation_prompt=True, tokenize=True,
185+
return_tensors="pt",
186+
)
187+
if isinstance(ids, list):
188+
ids = torch.tensor([ids])
189+
return ids.to(device)
190+
191+
sample_ids = [encode_chat(s.prompt_text) for s in samples]
192+
seq_lens = [int(t.size(1)) for t in sample_ids]
193+
eos_id = tokenizer.eos_token_id
194+
print(
195+
f"[k3-integrated] dataset: {len(samples)} samples, prompt token len "
196+
f"min={min(seq_lens)} max={max(seq_lens)} "
197+
f"mean={sum(seq_lens) // len(seq_lens)}",
198+
file=sys.stderr,
199+
)
200+
201+
def _greedy(decode_step) -> Tuple[List[str], List[float], List[int]]:
202+
"""Run greedy decode over all samples with a per-step callable
203+
``decode_step(cur_ids) -> logits[0, -1]``. Returns per-sample
204+
(decoded_text, latency_s, decode_token_count)."""
205+
decoded_all: List[str] = []
206+
lat_all: List[float] = []
207+
tok_all: List[int] = []
208+
for i in range(len(samples)):
209+
cur = sample_ids[i]
210+
gen: List[int] = []
211+
t0 = time.perf_counter()
212+
for _ in range(args.max_new_tokens):
213+
last_logits = decode_step(cur)
214+
nxt = int(torch.argmax(last_logits).item())
215+
gen.append(nxt)
216+
if eos_id is not None and nxt == eos_id:
217+
break
218+
cur = torch.cat(
219+
[cur, torch.tensor([[nxt]], device=device, dtype=torch.long)],
220+
dim=1,
221+
)
222+
lat_all.append(time.perf_counter() - t0)
223+
decoded_all.append(tokenizer.decode(gen, skip_special_tokens=True))
224+
tok_all.append(len(gen))
225+
print(
226+
f"[k3-integrated] sample {i}: T={seq_lens[i]} tokens={len(gen)} "
227+
f"decoded[:48]={decoded_all[-1][:48]!r}",
228+
file=sys.stderr,
229+
)
230+
return decoded_all, lat_all, tok_all
178231

179232
# ---------- Run integrated cross-model verifier ----------
180-
cross_results: List[Dict[str, Any]] = []
181-
cross_attn_window: List[Dict[str, Any]] = []
233+
print("[k3-integrated] running K3 cross-model verifier (f_θ restoration)",
234+
file=sys.stderr, flush=True)
182235
reset_memory_peak(device)
183236

184-
for i, sample in enumerate(samples):
185-
input_ids = torch.tensor(
186-
[sample.input_ids], dtype=torch.long, device=device,
187-
)
188-
T = int(input_ids.size(1))
189-
190-
# Run cross-model verifier
191-
outputs = cross_verifier.forward(
192-
input_ids,
237+
def _cross_step(cur):
238+
out = cross_verifier.forward(
239+
cur,
193240
apply_rotary_pos_emb=apply_rotary_pos_emb,
194241
eager_attention_forward=eager_attention_forward,
195242
all_attention_functions=ALL_ATTENTION_FUNCTIONS,
196243
)
197-
# Greedy decode max_new_tokens after the prompt
198-
cur = input_ids
199-
gen_tokens: List[int] = []
200-
for _ in range(args.max_new_tokens):
201-
out = cross_verifier.forward(
202-
cur,
203-
apply_rotary_pos_emb=apply_rotary_pos_emb,
204-
eager_attention_forward=eager_attention_forward,
205-
all_attention_functions=ALL_ATTENTION_FUNCTIONS,
206-
)
207-
nxt = int(torch.argmax(out.logits[0, -1]).item())
208-
gen_tokens.append(nxt)
209-
cur = torch.cat(
210-
[cur, torch.tensor([[nxt]], device=device, dtype=torch.long)],
211-
dim=1,
212-
)
213-
214-
decoded = tokenizer.decode(gen_tokens, skip_special_tokens=True)
215-
is_correct = recall_predicate(decoded, sample)
216-
cross_results.append({
217-
"sample_idx": i,
218-
"decoded": decoded[:200],
219-
"is_correct": is_correct,
220-
"seq_len": T,
221-
})
222-
223-
# effective_attention_fraction at the last query position
224-
attn_w = compute_effective_attention_window(
225-
seq_len=T,
226-
sink_size=args.sink_size,
227-
window_size=args.window_size,
228-
evicted_kv_restored=True, # K3 architecture: evicted K/V are restored
229-
structural_constraint=(
230-
f"causal_with_dlm_reconstruction "
231-
f"(local_cache=sink={args.sink_size}+window={args.window_size}, "
232-
f"k3_cross_model_f_theta)"
233-
),
234-
)
235-
cross_attn_window.append(attn_w)
244+
return out.logits[0, -1]
236245

237-
print(
238-
f"[k3-integrated] sample {i}: T={T} correct={is_correct} "
239-
f"decoded[:60]={decoded[:60]!r}",
240-
file=sys.stderr,
241-
)
242-
243-
# ---------- Aggregate ----------
244-
cross_recall = aggregate_recall(cross_results)
245-
cross_attn_agg = aggregate_attention_window_metrics(cross_attn_window)
246-
cross_mem = record_memory(device, label="after_k3_cross_model")
246+
cross_decoded, cross_lat, cross_tok = _greedy(_cross_step)
247+
cross_res = aggregate_recall(
248+
"k3_cross_model", samples, cross_decoded, cross_lat, cross_tok,
249+
)
250+
cross_mem = record_memory(device)
251+
cross_attn_agg = aggregate_attention_window_metrics(
252+
"v04_dlm_restored",
253+
prompt_token_lens=seq_lens,
254+
sink_size=args.sink_size,
255+
window_size=args.window_size,
256+
)
257+
print(
258+
f"[k3-integrated] cross-model recall={cross_res.recall:.3f} "
259+
f"({cross_res.samples_correct}/{cross_res.samples_total})",
260+
file=sys.stderr,
261+
)
247262

248263
# ---------- Optional oracle baseline ----------
249-
oracle_results = None
250-
oracle_recall = None
264+
oracle_res = None
251265
oracle_mem = None
252266
if not args.skip_oracle:
253267
print("[k3-integrated] running full-attention oracle baseline",
254268
file=sys.stderr, flush=True)
255269
reset_memory_peak(device)
256-
oracle_results = []
257-
for i, sample in enumerate(samples):
258-
input_ids = torch.tensor(
259-
[sample.input_ids], dtype=torch.long, device=device,
260-
)
261-
cur = input_ids
262-
gen_tokens = []
263-
for _ in range(args.max_new_tokens):
264-
with torch.no_grad():
265-
out = verifier(input_ids=cur, use_cache=False)
266-
nxt = int(torch.argmax(out.logits[0, -1]).item())
267-
gen_tokens.append(nxt)
268-
cur = torch.cat(
269-
[cur, torch.tensor([[nxt]], device=device, dtype=torch.long)],
270-
dim=1,
271-
)
272-
decoded = tokenizer.decode(gen_tokens, skip_special_tokens=True)
273-
is_correct = recall_predicate(decoded, sample)
274-
oracle_results.append({
275-
"sample_idx": i,
276-
"decoded": decoded[:200],
277-
"is_correct": is_correct,
278-
"seq_len": int(input_ids.size(1)),
279-
})
280-
print(
281-
f"[k3-integrated] oracle sample {i}: correct={is_correct}",
282-
file=sys.stderr,
283-
)
284-
oracle_recall = aggregate_recall(oracle_results)
285-
oracle_mem = record_memory(device, label="after_oracle")
270+
271+
def _oracle_step(cur):
272+
with torch.no_grad():
273+
out = verifier(input_ids=cur, use_cache=False)
274+
return out.logits[0, -1]
275+
276+
oracle_decoded, oracle_lat, oracle_tok = _greedy(_oracle_step)
277+
oracle_res = aggregate_recall(
278+
"oracle", samples, oracle_decoded, oracle_lat, oracle_tok,
279+
)
280+
oracle_mem = record_memory(device)
281+
print(
282+
f"[k3-integrated] oracle recall={oracle_res.recall:.3f} "
283+
f"({oracle_res.samples_correct}/{oracle_res.samples_total})",
284+
file=sys.stderr,
285+
)
286286

287287
# ---------- Build report ----------
288288
recall_delta = (
289-
abs(cross_recall["recall"] - oracle_recall["recall"])
290-
if oracle_recall else None
289+
abs(cross_res.recall - oracle_res.recall) if oracle_res else None
291290
)
291+
eff_frac_mean = cross_attn_agg.get("effective_attention_fraction_mean")
292292
report = {
293-
"schema_version": 1,
293+
"schema_version": 2,
294294
"kind": "k3_integrated_niah_acceptance",
295295
"config": {
296296
"verifier_id": args.verifier_id,
@@ -305,18 +305,11 @@ def main() -> int:
305305
"max_new_tokens": args.max_new_tokens,
306306
"seed": args.seed,
307307
"skip_oracle": bool(args.skip_oracle),
308+
"prompt_token_lens": seq_lens,
308309
},
309310
"results": {
310-
"k3_cross_model": {
311-
"name": "k3_cross_model",
312-
**cross_recall,
313-
"per_sample": cross_results,
314-
},
315-
**(
316-
{"oracle": {"name": "oracle", **oracle_recall,
317-
"per_sample": oracle_results}}
318-
if oracle_recall else {}
319-
),
311+
"k3_cross_model": dataclasses.asdict(cross_res),
312+
**({"oracle": dataclasses.asdict(oracle_res)} if oracle_res else {}),
320313
},
321314
"attention_window": {
322315
"per_config": {"k3_cross_model": cross_attn_agg},
@@ -326,9 +319,9 @@ def main() -> int:
326319
**({"oracle": oracle_mem} if oracle_mem else {}),
327320
},
328321
"gate": {
329-
"architectural_correctness": (
330-
cross_attn_agg.get("effective_attention_fraction_mean") == 1.0
331-
),
322+
"architectural_correctness": (eff_frac_mean == 1.0),
323+
"recall_cross_model": cross_res.recall,
324+
"recall_oracle": oracle_res.recall if oracle_res else None,
332325
"recall_delta_vs_oracle_pp": (
333326
recall_delta * 100 if recall_delta is not None else None
334327
),
@@ -343,31 +336,28 @@ def main() -> int:
343336
)
344337
out_path.parent.mkdir(parents=True, exist_ok=True)
345338
out_path.write_text(json.dumps(report, indent=2))
339+
340+
print(f"\n[k3-integrated] DONE.", file=sys.stderr)
346341
print(
347-
f"\n[k3-integrated] DONE.\n"
348-
f" cross-model recall: {cross_recall['recall']:.3f} "
349-
f"({cross_recall['samples_correct']}/{cross_recall['samples_total']})\n"
350-
f" oracle recall: "
351-
f"{oracle_recall['recall']:.3f} ({oracle_recall['samples_correct']}/{oracle_recall['samples_total']})"
352-
if oracle_recall else
353-
f"\n[k3-integrated] DONE.\n"
354-
f" cross-model recall: {cross_recall['recall']:.3f} "
355-
f"({cross_recall['samples_correct']}/{cross_recall['samples_total']})\n"
356-
f" oracle: skipped",
342+
f" cross-model recall: {cross_res.recall:.3f} "
343+
f"({cross_res.samples_correct}/{cross_res.samples_total})",
357344
file=sys.stderr,
358345
)
359-
if recall_delta is not None:
360-
print(f" |delta vs oracle|: {recall_delta * 100:.2f} pp", file=sys.stderr)
346+
if oracle_res is not None:
347+
print(
348+
f" oracle recall: {oracle_res.recall:.3f} "
349+
f"({oracle_res.samples_correct}/{oracle_res.samples_total})",
350+
file=sys.stderr,
351+
)
352+
print(f" |delta vs oracle|: {recall_delta * 100:.2f} pp", file=sys.stderr)
361353
print(
362354
f" ADR §11.8 1a gate (≤ 5pp): "
363355
f"{'PASS' if recall_delta <= 0.05 else 'FAIL'}",
364356
file=sys.stderr,
365357
)
366-
print(
367-
f" effective_attention_fraction: "
368-
f"{cross_attn_agg.get('effective_attention_fraction_mean')}",
369-
file=sys.stderr,
370-
)
358+
else:
359+
print(" oracle: skipped", file=sys.stderr)
360+
print(f" effective_attention_fraction: {eff_frac_mean}", file=sys.stderr)
371361
print(f" Report: {out_path}", file=sys.stderr)
372362
return 0
373363

0 commit comments

Comments
 (0)