mirror of
https://github.com/TheTom/turboquant_plus.git
synced 2026-10-22 02:53:13 -05:00
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>
206 lines
7.5 KiB
Python
206 lines
7.5 KiB
Python
"""REFRACT v0.1 composite scoring.
|
||
|
||
Combines per-axis scores into a single 0–100 number using the harmonic mean,
|
||
which penalises a single bad axis more aggressively than the arithmetic mean
|
||
(matches the "fail-loud" intent of the design).
|
||
|
||
Bands (tunable; align these with the paper's findings as we collect more data):
|
||
|
||
[90, 100] EXCELLENT — within reference noise / true equivalence
|
||
[80, 90) PASS — minor drift, safe to deploy
|
||
[60, 80) DEGRADED — visible drift, audit before use
|
||
[ 0, 60) FAIL — flag and treat as broken
|
||
|
||
Floor verification:
|
||
REFRACT(fp16-KV, fp16-KV) must be >= MIN_FLOOR (default 99.5).
|
||
If it is not, the reference itself is non-deterministic on this build
|
||
and KLD deltas cannot be trusted.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from dataclasses import dataclass, field
|
||
from typing import Optional
|
||
|
||
# Minimum REFRACT score the reference vs reference must hit. The paper §4.5
|
||
# shows bit-exact zero KLD on Metal, so 99.5 leaves headroom for non-Metal
|
||
# float-jitter without admitting a broken reference.
|
||
MIN_FLOOR = 99.5
|
||
|
||
|
||
def harmonic_mean(values: list[float]) -> float:
|
||
"""Harmonic mean clipped to [0, 100]. Returns 0 if any value is 0."""
|
||
clean = [max(v, 0.0) for v in values]
|
||
if not clean:
|
||
return 0.0
|
||
if any(v <= 0.0 for v in clean):
|
||
return 0.0
|
||
n = len(clean)
|
||
h = n / sum(1.0 / v for v in clean)
|
||
return min(max(h, 0.0), 100.0)
|
||
|
||
|
||
def band(score: float) -> str:
|
||
if score >= 90.0:
|
||
return "EXCELLENT"
|
||
if score >= 80.0:
|
||
return "PASS"
|
||
if score >= 60.0:
|
||
return "DEGRADED"
|
||
return "FAIL"
|
||
|
||
|
||
def interpret_pattern(
|
||
*,
|
||
gtm_score: float,
|
||
kld_score: float,
|
||
rniah_score: Optional[float] = None,
|
||
plad_score: Optional[float] = None,
|
||
) -> list[str]:
|
||
"""v0.2.0: turn the per-axis band pattern into plain-English diagnosis.
|
||
|
||
Returns 0-3 short sentences a non-techie reader can act on. Each sentence
|
||
names what's broken (in user terms, not "axis A score") and suggests a
|
||
direction. Does not duplicate the band labels — rendering callers should
|
||
print bands AND interpretation; the bands say "what" and the
|
||
interpretation says "what it means".
|
||
|
||
Pattern recognised:
|
||
- all axes ≥ 80 (PASS or above) → "all clear"
|
||
- short-context drift (A or B < 80) → "decode distribution shift"
|
||
- long-context drift (C < 80) → "retrieval at long context"
|
||
- brittleness (D < 80) → "perturbation brittleness"
|
||
- all axes < 60 (FAIL) → "catastrophic"
|
||
Multiple patterns can fire; sentences are emitted in order of severity.
|
||
"""
|
||
notes: list[str] = []
|
||
|
||
def low(s: Optional[float]) -> bool:
|
||
return s is not None and s < 80.0
|
||
|
||
def fail(s: Optional[float]) -> bool:
|
||
return s is not None and s < 60.0
|
||
|
||
short_drift = low(gtm_score) or low(kld_score)
|
||
long_drift = low(rniah_score)
|
||
brittle = low(plad_score)
|
||
|
||
# Catastrophic case first: every measured axis below FAIL.
|
||
measured = [s for s in (gtm_score, kld_score, rniah_score, plad_score)
|
||
if s is not None]
|
||
if measured and all(s < 60.0 for s in measured):
|
||
notes.append(
|
||
"Catastrophic: every measured surface is broken. Treat as a "
|
||
"non-functional quantization; revert to a higher-bit config."
|
||
)
|
||
return notes
|
||
|
||
if not (short_drift or long_drift or brittle):
|
||
notes.append(
|
||
"All axes intact. Quantization is faithful to the fp16 reference "
|
||
"across the surfaces tested."
|
||
)
|
||
return notes
|
||
|
||
if short_drift and not long_drift and not brittle:
|
||
if fail(gtm_score) and fail(kld_score):
|
||
notes.append(
|
||
"Per-token distribution is broken but high-level surfaces "
|
||
"(retrieval, robustness) are intact. The model decodes "
|
||
"materially different tokens than fp16; consider a higher-bit "
|
||
"V-cache or a non-rotating V scheme."
|
||
)
|
||
else:
|
||
notes.append(
|
||
"Mild short-context drift; long-context retrieval and "
|
||
"perturbation robustness are intact. Likely safe for typical "
|
||
"use; audit on your specific decoding workload before shipping."
|
||
)
|
||
return notes
|
||
|
||
# Mixed cases: name each axis that's affected.
|
||
if short_drift:
|
||
notes.append(
|
||
"Decode distribution drift detected: the candidate generates "
|
||
"different tokens than fp16 on short-context prompts."
|
||
)
|
||
if long_drift:
|
||
notes.append(
|
||
"Long-context retrieval degraded: candidate fails on prompts "
|
||
"where the fp16 reference still retrieves correctly. Inspect the "
|
||
"per-(length, position) cell breakdown to see where it breaks."
|
||
)
|
||
if brittle:
|
||
notes.append(
|
||
"Brittleness to small input changes: candidate's output drifts "
|
||
"more than fp16's under typo / casing / punctuation perturbations."
|
||
)
|
||
return notes
|
||
|
||
|
||
@dataclass
|
||
class CompositeScore:
|
||
"""REFRACT composite output.
|
||
|
||
v0.1 shipped two axes (gtm + kld); v0.2 adds rniah + plad as
|
||
optional axes. The composite is the harmonic mean of *all axes
|
||
actually scored* — None values are dropped before aggregation, so
|
||
a v0.1-style two-axis run still produces the same number it did
|
||
before. Per-axis scores are kept as separate fields so the report
|
||
layer can render bands per axis even when not all axes are run.
|
||
"""
|
||
|
||
composite: float # 0–100 (harmonic_mean of scored axes)
|
||
band: str # EXCELLENT / PASS / DEGRADED / FAIL
|
||
gtm_score: float # 0–100 (axis A: gtm or trajectory)
|
||
kld_score: float # 0–100 (axis B)
|
||
rniah_score: Optional[float] = None # 0–100 (axis C; v0.2)
|
||
plad_score: Optional[float] = None # 0–100 (axis D; v0.2)
|
||
floor_score: Optional[float] = None # measured floor (ref vs ref)
|
||
floor_ok: Optional[bool] = None
|
||
floor_min: float = MIN_FLOOR
|
||
notes: list[str] = field(default_factory=list)
|
||
|
||
|
||
def composite_score(
|
||
gtm_score: float,
|
||
kld_score: float,
|
||
rniah_score: Optional[float] = None,
|
||
plad_score: Optional[float] = None,
|
||
floor_score: Optional[float] = None,
|
||
) -> CompositeScore:
|
||
"""Combine the per-axis scores into a REFRACT composite.
|
||
|
||
Axes that weren't run (passed as ``None``) are dropped before
|
||
aggregation. v0.1 callers passing only gtm + kld get a 2-axis
|
||
harmonic mean (unchanged behaviour); v0.2 callers passing all four
|
||
get a 4-axis harmonic mean.
|
||
"""
|
||
axes = [gtm_score, kld_score]
|
||
if rniah_score is not None:
|
||
axes.append(rniah_score)
|
||
if plad_score is not None:
|
||
axes.append(plad_score)
|
||
composite = harmonic_mean(axes)
|
||
floor_ok: Optional[bool] = None
|
||
notes: list[str] = []
|
||
if floor_score is not None:
|
||
floor_ok = floor_score >= MIN_FLOOR
|
||
if not floor_ok:
|
||
notes.append(
|
||
f"Floor failed: REFRACT(ref, ref) = {floor_score:.2f} < {MIN_FLOOR}. "
|
||
"Reference is non-deterministic on this build; KLD deltas are unreliable."
|
||
)
|
||
return CompositeScore(
|
||
composite=composite,
|
||
band=band(composite),
|
||
gtm_score=gtm_score,
|
||
kld_score=kld_score,
|
||
rniah_score=rniah_score,
|
||
plad_score=plad_score,
|
||
floor_score=floor_score,
|
||
floor_ok=floor_ok,
|
||
notes=notes,
|
||
)
|