feat: add NIAH eval to cache_rate_tester#9
Conversation
Thinking models (e.g. Qwen3) consume their entire 200-token budget in <think> reasoning before writing the answer, causing eval prompts to always fail. Separate eval_output_tokens (default 512) lets eval prompts get a larger budget without inflating the output budget for regular perf-measurement requests. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Aggregate eval pass/fail across all requests (not just peak concurrency) so every NIAH probe in the run contributes to the summary number. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Sustained mode builds AggregatedMetrics from period DataFrames rather than calling calculate_aggregated_metrics, so eval fields were never populated. Sum eval_total and eval_passed across all periods. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Shows eval_accuracy in the terminal at end of run for both fixed and sustained mode. Prints in warning color when accuracy < 100%. Shows '-' when eval is not enabled (--eval-mode none). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
run_command_*.sh was missing --eval-mode, --eval-fraction, --eval-passkey-digits, --eval-output-tokens since save_run_command was not updated when the eval flags were added. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Qwen3-32B at high concurrency (c512 TP=4) exhausted the 512-token budget mid-think, causing 2 consistent failures on the same prompt. 1024 gives thinking models sufficient headroom. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
callanjfox
left a comment
There was a problem hiding this comment.
Thanks for this — really nice addition. The design is clean: in-band probes that share the same load and concurrency as regular requests, deterministic seed-driven selection, correct gating to cache_hit_rate=100, and the CSV/summary plumbing all look good. It's fully additive too — --eval-mode none (the default) preserves every existing path. I'd like to land it. 👍
One blocker — needs a rebase. The branch currently conflicts with master. Since you opened this, master picked up two prompt-path optimizations ("Cache decoded prompt text to avoid re-decoding full context per request" and "Pre-generate unique-token pool…") that rewrote the exact region this PR touches — the cache_hit_rate == 100 branch of construct_prompt where you add the eval_metadata = working_set.get_eval_metadata(session_id) lookup. The resolution is mechanical: re-apply the eval-metadata lookup and the "skip QUESTION_BANK for eval prompts" logic on top of the new decode-caching code. Could you rebase onto latest master and push?
Non-blocking follow-ups (happy to take these as separate issues if you'd rather land first):
--eval-output-tokensdefault is inconsistent. argparse usesdefault=1024andTestConfiguses1024, but the flag's help text says "(default: 512)" and the PR description table also says 512. Actual default is 1024 — let's reconcile (I'd keep 1024 and fix the help text/description).- Thinking models can cause false failures. Grading substring-matches the response, capped at
eval_output_tokens. If a thinking model spends its budget in<think>and gets truncated before the answer, a healthy cache scores a fail. 1024 helps but isn't a guarantee — worth a doc note that this budget must exceed the model's thinking allotment. - Grader can false-pass.
grade_niah_responsematches the passkey anywhere in the output, and the prompt itself contains "The secret pass key is {passkey}". A model that echoes prompt text in reasoning but emits garbage as the final answer still passes. Grading the final-answer segment would be more robust. - Middle/needle truncation edge case. The safety-net
full_tokens = full_tokens[-target_tokens:]in_generate_niah_prompt_textcuts from the front; on a large tokenizer round-trip overshoot with the needle near the 10% depth, the passkey can be clipped out of the prompt entirely → guaranteed (spurious) fail. Rare, but consider trimming suffix filler / preserving the needle span instead. - Eval probes count toward perf aggregates. Eval prompts use a 4× output budget and are interleaved into the timed run, so they nudge aggregate output tok/s upward. Since accuracy is tracked separately, consider excluding eval requests from the throughput aggregates.
None of 1–5 block merge — the rebase is the only hard gate. Thanks again!
Motivation
At high concurrency, KV cache corruption or block-reuse bugs can cause the model to emit wrong or garbage
output — a failure mode that is completely invisible in TTFT and throughput metrics alone. This PR makes it
detectable with zero external dependencies and no separate eval phase.
What this adds
An optional in-band output correctness eval for
cache_rate_tester.py, based on the passkey retrieval taskintroduced in Landmark Attention: Random-Access Infinite Context Length for Transformers (Mohtashami & Jaggi,
2023).
When
--eval-mode niahis enabled, a configurable fraction of working-set prompts (default 10%) are replacedwith needle-in-a-haystack probes: a coherent English haystack of the same context length as the rest of the
test, with a random N-digit passkey embedded at a random position (10–90% depth), followed by a retrieval
question. Eval prompts are interleaved with the regular synthetic prompts throughout the timed run, so they
exercise the same KV cache behavior and the same concurrency pressure as every other request.
Each response is graded by substring match against the expected passkey. With greedy decoding and a healthy
cache, any modern model trivially retrieves the passkey — a sustained drop below 100% accuracy is a strong
signal that something is wrong with the inference path, not noise.
We use 7-digit passkeys by default, giving a false-positive rate of ~10⁻⁷ per response — effectively zero
across hundreds of eval probes per run.
New flags
--eval-mode {none,niah}nonenonepreserves all existing behavior.--eval-fraction FLOAT0.1--eval-passkey-digits INT7--eval-output-tokens INT512<think>reasoning before writing the answer, so eval prompts need a larger budget than regular requests without inflating the perf-measurement budget.Output
detailed_results_*.csv— per-request columns on every eval probe:eval_expected— the passkey that should appear in the responseeval_passed—True/Falseeval_response_excerpt— first 300 chars of the response (for debugging failures)sustained_periods_*.csv— per 30-second assessment window:eval_total,eval_passed,eval_accuracy— shows when during the run accuracy degradedsummary_*.csv— single aggregate across all requests in the run:eval_total,eval_passed,eval_accuracySample run