mirror of
https://github.com/TheTom/turboquant_plus.git
synced 2026-10-22 19:13:12 -05:00
v0.1.3 ships no new functional axes — only the test coverage, fail-loud guards, and corpus-identity machinery needed to stop the v0.1.x patch-round-after-matrix-run cycle. Code fixes: - axes/gtm.py: tokenizer-failure fallback now RAISES instead of silently using whitespace (was producing wrong-unit scores with no detection). - axes/gtm.py: GTM score now normalizes by mean_cand_length instead of n_predict (fixes gemma-31B 2.87× retokenize inflation that re-clipped the score to 100 in v0.1.2). - cli.py: --measure-floor additionally asserts mean_prefix == mean_cand on ref-vs-ref greedy (KLD ref-vs-ref is bit-exact zero on Metal so the composite floor passes even when GTM is broken). - runner.py: _TOPP_RE regex now tolerates 'Same top p' (space) form (test surfaced that v0.1.2 only matched the dashed form). - runner.py: new corpus_identity / sidecar machinery; KLDResult includes corpus path/size/SHA; refuses to score against a base built from a different corpus. New tests (30 across 6 files; all pass in <0.05s, no subprocess): - test_strip_noise.py: 5 — pins runner._strip_noise (banner / backspace / block-char regressions from v0.1.1). - test_command_construction.py: 4 — pins --single-turn presence, --no-conversation absence, --kl-divergence-base, errors='replace'. - test_tokenize_to_ids.py: 5 — parser + raise-on-error contract. - test_kld_regex.py: 6 — perplexity output regexes incl. gemma Same-top-p missing case (this test caught the regex bug). - test_report_json_layout.py: 5 — pins schema + composite-is-scalar. - test_corpus_identity.py: 5 — sidecar machinery. - test_validation.py: +4 GTM assertions (full_match_rate < 1.0, mean_prefix > 0, ≥3 distinct refs, ratio == 1.0 on ref-vs-ref). Total: 14 → 44 unit tests + 1 strengthened integration test. Docs: - LIMITATIONS.md: NEW — documents the limitations we ship with (detokenize→retokenize gap, corpus-anchored KLD, provisional bands, single-platform tested, GTM/EOS conflation, v0.1.3 fail-loud removal). - README.md: GTM scoring formula updated; new test files listed; points at LIMITATIONS.md. - axes/gtm.py module docstring: refreshed to reflect v0.1.2 + v0.1.3. - CHANGELOG.md: v0.1.3 entry at top with the v0.1.2 matrix table and the file-by-file change summary. - report.py: schema bumped refract.report.v0.1.1 → v0.1.3. - __init__.py: __version__ = "0.1.3". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: tturney <tturney1@gmail.com>
219 lines
8.2 KiB
Python
219 lines
8.2 KiB
Python
"""REFRACT v0.1 CLI.
|
|
|
|
Usage:
|
|
python3 -m refract.cli score \\
|
|
--model MODEL.gguf \\
|
|
--reference "ctk=f16,ctv=f16" \\
|
|
--candidate "ctk=q8_0,ctv=turbo4,attn_rot_v=0" \\
|
|
--prompts refract/prompts/v0.1.jsonl \\
|
|
--corpus ~/local_llms/llama.cpp/wikitext-2-raw/wiki.test.raw \\
|
|
--chunks 32 -c 512 -ngl 99
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from .axes.gtm import run_gtm
|
|
from .axes.kld import run_kld
|
|
from .report import json_report, text_report, to_json_string
|
|
from .runner import KVConfig
|
|
from .score import MIN_FLOOR, composite_score
|
|
|
|
|
|
def _add_score_parser(sub):
|
|
p = sub.add_parser(
|
|
"score",
|
|
help="Run REFRACT v0.1 on a candidate KV config vs the reference.",
|
|
)
|
|
p.add_argument("--model", required=True, type=Path,
|
|
help="Path to the GGUF model.")
|
|
p.add_argument("--reference", default="ctk=f16,ctv=f16",
|
|
help="Reference KV config (default: ctk=f16,ctv=f16).")
|
|
p.add_argument("--candidate", required=True,
|
|
help="Candidate KV config to score.")
|
|
p.add_argument("--prompts", required=True, type=Path,
|
|
help="Path to JSONL prompts file (e.g. prompts/v0.1.jsonl).")
|
|
p.add_argument("--corpus", required=True, type=Path,
|
|
help="Path to plain-text corpus for KLD axis "
|
|
"(e.g. wiki.test.raw).")
|
|
p.add_argument("--chunks", type=int, default=32,
|
|
help="--chunks for llama-perplexity (default: 32).")
|
|
p.add_argument("-c", "--ctx", type=int, default=512,
|
|
help="Context size (default: 512).")
|
|
p.add_argument("-ngl", "--n-gpu-layers", type=int, default=99,
|
|
help="-ngl flag (default: 99).")
|
|
p.add_argument("--n-predict", type=int, default=128,
|
|
help="Tokens to greedy-decode per GTM prompt (default: 128).")
|
|
p.add_argument("--seed", type=int, default=42,
|
|
help="Greedy seed (default: 42).")
|
|
p.add_argument("--measure-floor", action="store_true",
|
|
help="Measure REFRACT(ref, ref) and abort if < %.1f."
|
|
% MIN_FLOOR)
|
|
p.add_argument("--skip-gtm", action="store_true",
|
|
help="Skip Axis A. Composite uses KLD only (debug).")
|
|
p.add_argument("--skip-kld", action="store_true",
|
|
help="Skip Axis B. Composite uses GTM only (debug).")
|
|
p.add_argument("--json-out", type=Path, default=None,
|
|
help="Path to write the JSON report to.")
|
|
p.add_argument("--no-progress", action="store_true",
|
|
help="Suppress per-prompt progress output.")
|
|
return p
|
|
|
|
|
|
def _run_score(args) -> int:
|
|
ref_kv = KVConfig.parse(args.reference)
|
|
cand_kv = KVConfig.parse(args.candidate)
|
|
|
|
print(f"REFRACT v0.1")
|
|
print(f" model : {args.model}")
|
|
print(f" reference : {ref_kv.label()}")
|
|
print(f" candidate : {cand_kv.label()}")
|
|
print()
|
|
|
|
# ---- Floor check ------------------------------------------------------
|
|
floor_score = None
|
|
if args.measure_floor:
|
|
print("Measuring noise floor: REFRACT(ref, ref) ...")
|
|
floor_gtm = run_gtm(
|
|
model=args.model, reference_kv=ref_kv, candidate_kv=ref_kv,
|
|
prompts_path=args.prompts, n_predict=args.n_predict,
|
|
ctx=args.ctx, n_gpu_layers=args.n_gpu_layers, seed=args.seed,
|
|
progress=not args.no_progress,
|
|
)
|
|
floor_kld = run_kld(
|
|
model=args.model, corpus=args.corpus,
|
|
reference_kv=ref_kv, candidate_kv=ref_kv,
|
|
chunks=args.chunks, ctx=args.ctx,
|
|
n_gpu_layers=args.n_gpu_layers,
|
|
progress=not args.no_progress,
|
|
)
|
|
floor_composite = composite_score(floor_gtm.score, floor_kld.score)
|
|
floor_score = floor_composite.composite
|
|
print(f" floor: {floor_score:.2f} (gtm={floor_gtm.score:.2f}, "
|
|
f"kld={floor_kld.score:.2f}, kld nats={floor_kld.mean_kld:.6f})")
|
|
if floor_score < MIN_FLOOR:
|
|
print()
|
|
print(f"ERROR: noise floor {floor_score:.2f} < {MIN_FLOOR}.")
|
|
print("The reference itself is non-deterministic on this build.")
|
|
print("KLD deltas vs this reference cannot be trusted. Aborting.")
|
|
return 2
|
|
# v0.1.3: composite-level floor passes if KLD is ~100 (bit-exact zero
|
|
# on Metal) even when GTM is in a "high-score broken" state. Add a
|
|
# GTM-level byte-identity check: ref-vs-ref greedy must match for
|
|
# the FULL retokenized candidate length on every prompt. If
|
|
# mean_prefix != mean_cand, the tokenizer or runner is broken.
|
|
if floor_gtm.mean_cand_length > 0:
|
|
ratio = (
|
|
floor_gtm.mean_prefix_agreement_length
|
|
/ floor_gtm.mean_cand_length
|
|
)
|
|
else:
|
|
ratio = 0.0
|
|
if abs(ratio - 1.0) > 1e-9:
|
|
print()
|
|
print(
|
|
f"ERROR: GTM ref-vs-ref ratio = {ratio:.6f} (expected 1.0). "
|
|
f"mean_prefix={floor_gtm.mean_prefix_agreement_length:.2f}, "
|
|
f"mean_cand_length={floor_gtm.mean_cand_length:.2f}."
|
|
)
|
|
print(
|
|
"GTM ref-vs-ref isn't byte-identical, your tokenizer or "
|
|
"runner is broken. Aborting."
|
|
)
|
|
return 2
|
|
print()
|
|
|
|
# ---- GTM --------------------------------------------------------------
|
|
if args.skip_gtm:
|
|
gtm = _stub_gtm()
|
|
else:
|
|
print("Running Axis A (GTM)...")
|
|
gtm = run_gtm(
|
|
model=args.model, reference_kv=ref_kv, candidate_kv=cand_kv,
|
|
prompts_path=args.prompts, n_predict=args.n_predict,
|
|
ctx=args.ctx, n_gpu_layers=args.n_gpu_layers, seed=args.seed,
|
|
progress=not args.no_progress,
|
|
)
|
|
print(f" GTM score: {gtm.score:.2f}")
|
|
|
|
# ---- KLD --------------------------------------------------------------
|
|
if args.skip_kld:
|
|
kld = _stub_kld(args.chunks, args.ctx)
|
|
else:
|
|
print("Running Axis B (KLD@D, corpus proxy)...")
|
|
kld = run_kld(
|
|
model=args.model, corpus=args.corpus,
|
|
reference_kv=ref_kv, candidate_kv=cand_kv,
|
|
chunks=args.chunks, ctx=args.ctx,
|
|
n_gpu_layers=args.n_gpu_layers,
|
|
progress=not args.no_progress,
|
|
)
|
|
print(f" KLD score: {kld.score:.2f} (mean KLD = {kld.mean_kld:.6f} nats)")
|
|
|
|
# ---- Composite --------------------------------------------------------
|
|
composite = composite_score(gtm.score, kld.score, floor_score=floor_score)
|
|
|
|
print()
|
|
print(text_report(
|
|
model=str(args.model),
|
|
reference_label=ref_kv.label(),
|
|
candidate_label=cand_kv.label(),
|
|
composite=composite,
|
|
gtm=gtm, kld=kld,
|
|
))
|
|
|
|
if args.json_out:
|
|
rep = json_report(
|
|
model=str(args.model),
|
|
reference_label=ref_kv.label(),
|
|
candidate_label=cand_kv.label(),
|
|
composite=composite,
|
|
gtm=gtm, kld=kld,
|
|
)
|
|
args.json_out.write_text(to_json_string(rep))
|
|
print(f"\nJSON report written to {args.json_out}")
|
|
|
|
return 0
|
|
|
|
|
|
# Stubs for --skip-gtm / --skip-kld dev modes (composite still computes).
|
|
def _stub_gtm():
|
|
from .axes.gtm import GTMResult
|
|
return GTMResult(
|
|
score=100.0, full_match_rate=1.0,
|
|
median_first_divergence=None, mean_prefix_agreement_length=0.0,
|
|
mean_cand_length=0.0, mean_ref_length=0.0,
|
|
n_prompts=0, n_tokens_each=0, per_prompt=[],
|
|
)
|
|
|
|
|
|
def _stub_kld(chunks: int, ctx: int):
|
|
from .axes.kld import KLDResult
|
|
return KLDResult(
|
|
score=100.0, mean_kld=0.0, ppl=None,
|
|
rms_dp_pct=None, same_topp_pct=None,
|
|
base_path="", chunks=chunks, ctx=ctx, is_self_reference=False,
|
|
)
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
prog="refract",
|
|
description="REFRACT v0.1 — KV-cache quantization quality oracle.",
|
|
)
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
_add_score_parser(sub)
|
|
args = parser.parse_args(argv)
|
|
if args.cmd == "score":
|
|
return _run_score(args)
|
|
parser.error(f"unknown subcommand: {args.cmd}")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|