Compressed history of v0.1.4 → v0.3.2 in one alpha-ship commit so the
framework is testable end-to-end by friends. Detailed per-version
discussion lives in refract/CHANGELOG.md.
What ships
----------
Four scoring axes vs the model's own fp16-KV reference:
- Axis A (Trajectory, v0.1.4) — decode-time token IDs via patched
llama-completion, no detokenize round-trip. Replaces the buggy
v0.1.x GTM as the default.
- Axis B (KLD@D) — corpus KL divergence, bit-exact zero on Metal
when ref==cand. Native MLX implementation in addition to the
llama-perplexity subprocess path.
- Axis C (R-NIAH, v0.2.0) — long-context retrieval at multiple
(length, position) cells. Confidence guard for low base_acc.
- Axis D (PLAD, v0.2.0) — perturbation-locality drift across
typo/case/punct/paraphrase. Skipped-perturbation handling.
Composite = harmonic mean. Bands EXCELLENT/PASS/DEGRADED/FAIL.
Four CLI subcommands:
- score — run REFRACT on a candidate KV config
- selftest — 30s preflight: binaries, flags, model probe
- compare — multi-report side-by-side
- repeatability — run N times, report stdev/range health verdict
Three backends:
- llamacpp (production) subprocess wrappers around llama.cpp
- mlx (production) native via mlx-lm; verified end-to-end
on Qwen2.5-3B-Instruct-4bit
- vllm (skeleton) interface defined; plug-in pointers
in module docstring
Reporting
---------
- Text (ANSI colour, terminal)
- JSON schema refract.report.v0.3.1 with framework_version +
environment metadata (backend, llama_cpp_commit, mlx_lm_version)
- HTML (self-contained single file) with hardware info, model
params, repro command, R-NIAH heatmap, PLAD per-perturbation bars
Discoveries from the matrix runs (all logged in CHANGELOG):
- "secret password" needle wording triggers safety refusals on
instruct models. v0.2.1 switched to neutral "rare paint color"
- Thinking-mode models burn n_predict before answering. v0.3.0 added
-rea off + auto-detect at startup
- Symmetric turbo4/turbo4 is the canonical FAIL config; framework
correctly identifies (composite ~19) on gemma-4-26B-A4B-Q8
Documentation
-------------
- README.md what + alpha banner + "where do I go" navigator
- QUICKSTART.md 0 to first PASS in 30 minutes
- INTERPRETATION.md per-axis "what to do if low" guide
- LEADERBOARD.md cross-model rankings + "ranks the quant, not the
model" disclaimer
- PITFALLS.md known traps from today's matrix runs
- LIMITATIONS.md what v0.3 explicitly does not do
- CHANGELOG.md version history with discovery stories
- MATRIX-RESULTS.md the 7-model matrix log
- examples/ 4 sample JSONs + HTML reports + README
Tests
-----
80 → 82 unit tests, all green:
- 17 trajectory tests (incl. R-NIAH/PLAD impls + 4-axis composite)
- 5 strip-noise pinning tests
- 14 unit tests (math + KVConfig)
- 8 JSON schema tests (incl. v0.3.1 framework_version field)
- 6 KLD regex tests
- 5 corpus-identity tests
- 5 tokenize-to-ids tests
- 4 command-construction tests
Verified
--------
- 7-model v0.3.0 matrix end-to-end (~2h on M5 Max 128GB)
- Llama-4 Scout extras + symmetric-turbo negative control
- MLX backend on Qwen2.5-3B-Instruct-4bit
- selftest on llamacpp (no model) and mlx (with model)
- 2-run repeatability on qwen2.5-1.5b: composite stdev = 0.00 on
Trajectory + KLD (bit-exact reproducibility on cheap axes)
Open follow-ups (v0.4)
----------------------
- vLLM backend implementation (skeleton today)
- T-Call axis (tool-call fidelity) building on v0.3 chat-template
- Bundled corpus + prompts distribution
- `refract leaderboard --in dir/ --out LEADERBOARD.md` auto-gen
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.4 KiB
REFRACT — known pitfalls
Things that have actually bitten us, in priority order. Keep this discoverable so first-time users don't repeat the discovery.
Setup
Wrong default --axis-a in v0.1.x / v0.2.x
The gtm axis used llama-tokenize to retokenize the candidate's
decoded text and compare it against the reference's retokenized text.
Detokenize→retokenize is lossy: a 50-token greedy generation can come
back as 60–137 tokens after retokenization (gemma-4 31B v0.1.2 hit a
2.87× inflation). v0.1.4 added the trajectory axis (decode-time
token IDs via patched llama-completion); v0.3.1 made trajectory
the default. If you're seeing weird Axis A scores, confirm
--axis-a trajectory is in your invocation.
head_dim % block_size != 0 blocks q4/q8 KV
phi-2 and a few other models have head_dim = 80 which doesn't divide
the q4_0 / q8_0 cache block size of 32. llama.cpp will fail to create
the context with K cache type q4_0 with block size 32 does not divide n_embd_head_k=80. Use a different model OR pick a KV type compatible
with the head dim (e.g., q4_K with block 256 if your build has it).
llama.cpp lacking turbo support
If you're testing TurboQuant configs (ctv=turbo4), your llama.cpp
build needs the turbo branches compiled in. Check with:
llama-cli --help | grep -A2 'cache-type-k'
# look for 'turbo2, turbo3, turbo4' in the allowed values
If absent, build from TheTom/llama.cpp feature/turboquant-kv-cache
or set --candidate "ctk=q8_0,ctv=q8_0" (standard quant) instead.
MLX RotatingKVCache + quantization
Gemma family + a few other sliding-window-attention models use
RotatingKVCache in mlx-lm. As of mlx-lm 0.31.2, RotatingKVCache
quantization is NotImplementedYet. Symptom:
NotImplementedError: RotatingKVCache Quantization NYI
If you hit this on MLX, either:
- Switch to
--backend llamacppfor that model, OR - Test with
ctk=f16,ctv=f16only on the MLX side
Result interpretation traps
R-NIAH = 100 with confidence: low
Means base_acc averaged across cells is below 0.2 — the model isn't
engaging retrieval at all under either config. R-NIAH = 100 is then a
noise-floor reading rather than real signal. Look at the
confidence field in the JSON before posting the score.
Common causes (today, all v0.3+ should be fixed):
- Older versions used "secret password" needle wording → triggered safety refusals on instruct models. v0.2.1 changed to neutral "rare paint color" framing.
- Raw "Q: A:" prompt format didn't engage instruct models → they
continued the wikitext stylistically. v0.3.0 applies chat
templates via
--jinja. n_predict = 32with thinking-mode models → answer truncated inside the thinking trace. v0.2.1 bumped default to 256.
PLAD paraphrase = NaN
The built-in synonym table is small. If your prompts have no words in
the table, paraphrase never fires and the per-perturbation slot is
NaN. The cell is listed in skipped_perturbations (v0.3.1+) and the
overall PLAD score still uses the perturbations that DID fire. Don't
read NaN as FAIL — read it as "didn't apply".
Trajectory + KLD low, R-NIAH + PLAD high
Means per-token distribution drift but high-level reasoning intact. Common for aggressive turbo configs on certain models. The candidate generates different text but still retrieves facts and resists perturbations. Useful triage signal. If your workload doesn't require text-level reproducibility (just task success), this may be safe. Otherwise revert to a less aggressive quant.
KLD low but Trajectory high
Means distributions look fine on the corpus but the model decodes different text. Two common explanations:
- The corpus distribution doesn't match the prompts the trajectory axis is using. Try a corpus that better reflects your workload.
- There's a chat-template engagement difference between the two code paths (KLD is corpus-driven and never touches chat templates; Trajectory does).
Composite shifts across versions
REFRACT v0.2.0 → v0.3.0 chat-template fix produced ±2 composite
deltas on the same models / candidates. The number isn't a bug — the
new methodology is correct, but cross-version comparisons require
same version on both sides. Always include framework_version from
the JSON when sharing scores.
Discovered during the v0.2/v0.3 matrix runs (2026-04-30)
- "secret password" needle = safety refusal on RLHF'd instruct models. Fixed in v0.2.1 with neutral "rare paint color" framing.
- Thinking-mode models burn n_predict before answering. v0.2.1
bumped R-NIAH n_predict to 256. v0.3.0 added auto-detect at
startup +
-rea offon llama-cli. - Symmetric turbo (
ctk=turbo4,ctv=turbo4) is empirically worse than asymmetric on every distribution-level surface. This is the framework's negative-control config — if REFRACT doesn't FAIL it, the framework has problems. v0.2.0 result on gemma-4-26B-A4B: composite 19 FAIL, KLD 11.84 (2.13 nats), Trajectory 9.74. Use this as a sanity check. - R-NIAH substring matching is coarse on broken-distribution models — survives drift if the keyword shows up anywhere in the response (including thinking traces echoing the prompt).
--measure-flooris highly recommended for first runs — it verifies the reference itself is deterministic on your build. Without it, KLD deltas are theoretical.