Image-Text-to-Text
MLX
Safetensors
step3p7
Mixture of Experts
vision-language
pruned
reap
quantized
conversational
custom_code
4-bit precision
Instructions to use True2456/Step-3.7-173B-REAP-4.6bit-MLX with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use True2456/Step-3.7-173B-REAP-4.6bit-MLX with MLX:
# Make sure mlx-vlm is installed # pip install --upgrade mlx-vlm from mlx_vlm import load, generate from mlx_vlm.prompt_utils import apply_chat_template from mlx_vlm.utils import load_config # Load the model model, processor = load("True2456/Step-3.7-173B-REAP-4.6bit-MLX") config = load_config("True2456/Step-3.7-173B-REAP-4.6bit-MLX") # Prepare input image = ["http://images.cocodataset.org/val2017/000000039769.jpg"] prompt = "Describe this image." # Apply chat template formatted_prompt = apply_chat_template( processor, config, prompt, num_images=1 ) # Generate output output = generate(model, processor, formatted_prompt, image) print(output) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Pi
How to use True2456/Step-3.7-173B-REAP-4.6bit-MLX with Pi:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "True2456/Step-3.7-173B-REAP-4.6bit-MLX"
Configure the model in Pi
# Install Pi: npm install -g @mariozechner/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "mlx-lm": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "True2456/Step-3.7-173B-REAP-4.6bit-MLX" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Hermes Agent
How to use True2456/Step-3.7-173B-REAP-4.6bit-MLX with Hermes Agent:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "True2456/Step-3.7-173B-REAP-4.6bit-MLX"
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default True2456/Step-3.7-173B-REAP-4.6bit-MLX
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use True2456/Step-3.7-173B-REAP-4.6bit-MLX with OpenClaw:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "True2456/Step-3.7-173B-REAP-4.6bit-MLX"
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "True2456/Step-3.7-173B-REAP-4.6bit-MLX" \ --custom-provider-id mlx-lm \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Public-ready card: correct numeric/tokenizer conclusion, add accuracy + tool-call validation, REAM rejection, evidence files
Browse files- README.md +86 -90
- artifacts/acc_ream.json +187 -0
- artifacts/acc_shared8.json +187 -0
- artifacts/accuracy_eval.py +113 -0
- artifacts/tc_ream.json +158 -0
- artifacts/tc_shared8.json +158 -0
- artifacts/toolcall_eval.py +138 -0
- docs/HEAD8-RESULT.md +113 -0
- docs/REAM-RESULT.md +104 -0
- docs/TOKENIZER-INVESTIGATION.md +127 -0
- scripts/numeric_guard.py +175 -0
README.md
CHANGED
|
@@ -127,6 +127,34 @@ smooth over quantization noise the way routed experts get) showed a real,
|
|
| 127 |
consistent, cross-category improvement -- about 10x any depth-window effect,
|
| 128 |
for roughly +0.03 bpw / +1GB.
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
### Also tested and rejected: per-expert mixed-precision tiering
|
| 131 |
|
| 132 |
Splitting each layer's 245 kept experts into 61 hot (6-bit) / 123 base
|
|
@@ -153,6 +181,14 @@ repo:
|
|
| 153 |
- `docs/SHARED8-RESULT.md` -- this model's final validation.
|
| 154 |
- `docs/TIERED-EXPERTS-RESULT.md` -- the rejected per-expert tiering
|
| 155 |
experiment.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
- `artifacts/*.json` -- raw per-category NLL/PPL from every eval run
|
| 157 |
referenced above, and `plan_p15_blend03.json`, the exact REAP plan
|
| 158 |
(per-layer kept/pruned expert indices and blended scores) used to build
|
|
@@ -182,97 +218,55 @@ ones are a poor fit for this tokenizer:
|
|
| 182 |
{"temperature": 0.5, "top_p": 0.9, "top_k": 100, "min_p": 0.05, "repetition_penalty": 1.0}
|
| 183 |
```
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
prompt: "...kill process ID 18452 with signal 9" (MAC-address context)
|
| 213 |
-
answer: kill -9 1845 5/5 at temperature 0
|
| 214 |
-
```
|
| 215 |
-
|
| 216 |
-
Also observed as delimiter insertion — `2456, 1337, 495` → `2 4 5 6, 1 3 3 7,
|
| 217 |
-
4 9 5`, and in float-dense code `12.0` → `1.2.0`, `sed -n '447,495p'` →
|
| 218 |
-
`sed -n '4.4.7,4.9.5p'`.
|
| 219 |
-
|
| 220 |
-
In the worst observed case the model does not corrupt the digits at all — it
|
| 221 |
-
**substitutes content from the context**, answering a request for
|
| 222 |
-
`2456, 1337, 495` with `2a:5b:c4:9f:1d:e0, 3c:df:a9:b2:44:`. The mild form
|
| 223 |
-
(`2456` → `2 4 5 6`) and this one appear to be the same failure at different
|
| 224 |
-
strengths: numeric output being captured by numerals already in the window.
|
| 225 |
-
The separator varies with conditions (`2 4 5 6` vs `2,4,5,6,...`).
|
| 226 |
-
|
| 227 |
-
**What it is not.** Measured on this checkpoint:
|
| 228 |
|
| 229 |
-
- *Not `repetition_penalty`.* Direct A/B, everything else identical:
|
| 230 |
-
penalty 1.02 gives 0/8 and 1/8 on the two failing cases, penalty pinned to
|
| 231 |
-
1.0 gives 0/8 and 0/8. No effect.
|
| 232 |
- *Not the quantized output head.* Digit rows carry 1.03x the all-row
|
| 233 |
-
reconstruction error
|
| 234 |
-
|
| 235 |
- *Not degraded digit copying.* Teacher-forced probes (no sampler in the path)
|
| 236 |
-
rank the correct next digit **first** at p = 0.91–0.9996
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
model re-
|
| 252 |
-
|
| 253 |
-
evidence in context.
|
| 254 |
-
|
| 255 |
-
**Practical guidance.** No sampler or template setting fixes this, so the
|
| 256 |
-
mitigation is operational. Verify numeric arguments programmatically rather
|
| 257 |
-
than relying on the model to self-check: a wrong value that is *syntactically
|
| 258 |
-
valid* (`idx+1.5`, a shortened PID) is caught by neither a syntax check nor the
|
| 259 |
-
model's own review. Prefer structured file-editing tools over line-numbered
|
| 260 |
-
`sed`. Where exact numerals matter, avoid keeping large delimiter-dense blocks
|
| 261 |
-
(ARP tables, IP configs, float-heavy source) in the same window as the request.
|
| 262 |
-
|
| 263 |
-
Note also that a runtime's own sampler settings override what this file
|
| 264 |
-
declares — an LM Studio per-model config with repeat penalty enabled will win
|
| 265 |
-
over the `repetition_penalty: 1.0` in `generation_config.json`.
|
| 266 |
-
|
| 267 |
-
Not yet isolated to this build versus upstream Step-3.7 — no unpruned
|
| 268 |
-
reference exists as a loadable quant, and the evidence above (clean probes,
|
| 269 |
-
clean reads, correct handling of other numbers) points away from the pruning
|
| 270 |
-
and quantization as the cause.
|
| 271 |
-
|
| 272 |
-
Temperature is deliberately below StepFun's suggested 0.7–1.0 for the same
|
| 273 |
-
reason: that guidance is tuned for prose diversity and does not account for
|
| 274 |
-
per-digit tokenization. Raise it for open-ended writing; keep it low for
|
| 275 |
-
tool calls, structured output, and anything numeric.
|
| 276 |
|
| 277 |
## Reasoning
|
| 278 |
|
|
@@ -325,8 +319,10 @@ limitations.
|
|
| 325 |
- No BF16 multimodal reference was run, so absolute (not just relative)
|
| 326 |
vision-task damage from quantization remains unmeasured.
|
| 327 |
- This checkpoint has not been evaluated on standard downstream benchmarks
|
| 328 |
-
(MMLU, GSM8K, SWE-bench, etc.)
|
| 329 |
-
proxies
|
|
|
|
|
|
|
| 330 |
- **JSON-schema structured output can crash `llguidance`-based runtimes.**
|
| 331 |
This is a base-model tokenizer interaction, not a defect introduced by the
|
| 332 |
pruning or quantization here — it reproduces with zero model weights loaded,
|
|
|
|
| 127 |
consistent, cross-category improvement -- about 10x any depth-window effect,
|
| 128 |
for roughly +0.03 bpw / +1GB.
|
| 129 |
|
| 130 |
+
### Capability validation (exact-answer accuracy, not perplexity)
|
| 131 |
+
|
| 132 |
+
Perplexity is a proxy: it rewards a confident distribution, which is not the
|
| 133 |
+
same as a correct one. To check that the numbers above reflect real capability,
|
| 134 |
+
this model was also scored **right/wrong** on exact-answer tasks where a good
|
| 135 |
+
proxy cannot substitute for a correct answer — greedy decoding, all sampler
|
| 136 |
+
fields pinned:
|
| 137 |
+
|
| 138 |
+
| test | items | score |
|
| 139 |
+
|---|---|---|
|
| 140 |
+
| multi-step arithmetic + factual recall | 24 | **24/24** |
|
| 141 |
+
| tool selection + argument extraction (from a provided catalog) | 15 | **14/15** |
|
| 142 |
+
|
| 143 |
+
These are spot checks, not a benchmark suite, but they confirm the model
|
| 144 |
+
reasons and calls tools correctly, not merely that it has low perplexity. Item
|
| 145 |
+
sets and per-item results: `artifacts/acc_*.json`, `artifacts/tc_*.json`.
|
| 146 |
+
|
| 147 |
+
### Also tested and rejected: expert merging (REAM)
|
| 148 |
+
|
| 149 |
+
Merging the 43 lowest-saliency experts per layer into their nearest kept
|
| 150 |
+
neighbour (instead of pruning them) *lowered perplexity substantially* —
|
| 151 |
+
overall text NLL −0.194, with tool_use perplexity dropping 41% (26.5 → 15.6).
|
| 152 |
+
On the exact-answer tests above, however, the merged model scored **identically**
|
| 153 |
+
(24/24 → 23/24, tool-call 14/15 → 14/15): the perplexity gain was distribution
|
| 154 |
+
smoothing with no capability behind it. A clean demonstration that perplexity
|
| 155 |
+
can move without capability following — and the reason pruning, not merging, was
|
| 156 |
+
shipped. Full write-up in `docs/REAM-RESULT.md`.
|
| 157 |
+
|
| 158 |
### Also tested and rejected: per-expert mixed-precision tiering
|
| 159 |
|
| 160 |
Splitting each layer's 245 kept experts into 61 hot (6-bit) / 123 base
|
|
|
|
| 181 |
- `docs/SHARED8-RESULT.md` -- this model's final validation.
|
| 182 |
- `docs/TIERED-EXPERTS-RESULT.md` -- the rejected per-expert tiering
|
| 183 |
experiment.
|
| 184 |
+
- `docs/REAM-RESULT.md` -- the rejected expert-merging experiment and the
|
| 185 |
+
perplexity-vs-capability demonstration behind it.
|
| 186 |
+
- `docs/TOKENIZER-INVESTIGATION.md` -- why the numeric behaviour is intrinsic
|
| 187 |
+
tokenization and not a checkpoint defect (and why the base tokenizer must not
|
| 188 |
+
be copied in).
|
| 189 |
+
- `artifacts/acc_*.json`, `artifacts/tc_*.json` -- per-item results for the
|
| 190 |
+
exact-answer accuracy and tool-call spot checks, with
|
| 191 |
+
`artifacts/accuracy_eval.py` and `artifacts/toolcall_eval.py`.
|
| 192 |
- `artifacts/*.json` -- raw per-category NLL/PPL from every eval run
|
| 193 |
referenced above, and `plan_p15_blend03.json`, the exact REAP plan
|
| 194 |
(per-layer kept/pruned expert indices and blended scores) used to build
|
|
|
|
| 218 |
{"temperature": 0.5, "top_p": 0.9, "top_k": 100, "min_p": 0.05, "repetition_penalty": 1.0}
|
| 219 |
```
|
| 220 |
|
| 221 |
+
`repetition_penalty` is pinned to **1.0**. It is the only common sampler
|
| 222 |
+
setting that *reorders* candidates rather than merely filtering them, so it is
|
| 223 |
+
the riskiest knob to raise — but note it was **measured not to be the cause** of
|
| 224 |
+
the numeric behaviour described under *Numeric handling* below (a direct A/B at
|
| 225 |
+
1.02 vs 1.0 was identical). Keep it at 1.0 as a safe default; use `min_p` for
|
| 226 |
+
loop suppression, raising it to 0.1 before reaching for a penalty.
|
| 227 |
+
|
| 228 |
+
Temperature is deliberately below StepFun's suggested 0.7–1.0. That guidance is
|
| 229 |
+
tuned for prose diversity; this tokenizer spells numbers one digit at a time
|
| 230 |
+
(see *Numeric handling*), which makes numeric output more temperature-sensitive
|
| 231 |
+
than prose. Raise it for open-ended writing; keep it low for tool calls,
|
| 232 |
+
structured output, and anything numeric.
|
| 233 |
+
|
| 234 |
+
### Numeric handling (a base-model property, not a defect)
|
| 235 |
+
|
| 236 |
+
Step-3.7's tokenizer emits **one token per digit** (ids 19–28 for `'0'`–`'9'`)
|
| 237 |
+
plus a **standalone space token** (223). Numbers therefore carry no
|
| 238 |
+
token-level redundancy: a single mis-sampled or mis-copied token corrupts an
|
| 239 |
+
entire number, while prose has enough redundancy to absorb it. In long,
|
| 240 |
+
number-dense contexts (IP/MAC tables, float-heavy source) this shows up as
|
| 241 |
+
digits dropped or split by a delimiter — `2456` → `2 4 5 6`, `12.0` → `1.2.0`,
|
| 242 |
+
`sed -n '447,495p'` → `sed -n '4.4.7,4.9.5p'` — with non-numeric output
|
| 243 |
+
unaffected.
|
| 244 |
+
|
| 245 |
+
**This is StepFun's tokenization design, not something introduced by the
|
| 246 |
+
pruning or quantization here.** It was investigated at length and each
|
| 247 |
+
plausible in-checkpoint cause was ruled out with measurements:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
|
|
|
|
|
|
|
|
|
|
| 249 |
- *Not the quantized output head.* Digit rows carry 1.03x the all-row
|
| 250 |
+
reconstruction error; the quantization perturbation is 12% of the tightest
|
| 251 |
+
inter-digit argmax margin.
|
| 252 |
- *Not degraded digit copying.* Teacher-forced probes (no sampler in the path)
|
| 253 |
+
rank the correct next digit **first** at p = 0.91–0.9996.
|
| 254 |
+
- *Not the sampler.* A direct `repetition_penalty` A/B (1.02 vs 1.0) was
|
| 255 |
+
identical.
|
| 256 |
+
- *Not a reading failure.* In the same context the model reports `18452` has 5
|
| 257 |
+
digits and echoes it back verbatim.
|
| 258 |
+
|
| 259 |
+
**Do not "fix" this by replacing `tokenizer.json` with the base checkpoint's.**
|
| 260 |
+
The upstream `stepfun-ai/Step-3.7-Flash` `tokenizer.json` is not usable
|
| 261 |
+
standalone — read directly by the `tokenizers` library (which is what MLX
|
| 262 |
+
runtimes do) it yields **zero tokens for Chinese** (`你好世界` → `[]`) and
|
| 263 |
+
mangles English word boundaries; it only works via `transformers`' load-time
|
| 264 |
+
reconstruction. This checkpoint ships the correct, standalone-usable tokenizer.
|
| 265 |
+
|
| 266 |
+
**Practical guidance.** Keep temperature low for numeric/tool work; for
|
| 267 |
+
agentic use, verify numeric arguments programmatically rather than relying on
|
| 268 |
+
the model to re-check its own output. A helper (`scripts/numeric_guard.py`) and
|
| 269 |
+
a Pi editor extension for exactly this are included in the source repo.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
## Reasoning
|
| 272 |
|
|
|
|
| 319 |
- No BF16 multimodal reference was run, so absolute (not just relative)
|
| 320 |
vision-task damage from quantization remains unmeasured.
|
| 321 |
- This checkpoint has not been evaluated on standard downstream benchmarks
|
| 322 |
+
(MMLU, GSM8K, SWE-bench, etc.). The category tables are perplexity/NLL
|
| 323 |
+
proxies; the *Capability validation* section adds small exact-answer accuracy
|
| 324 |
+
spot checks (39 items), but these are not a substitute for a full benchmark
|
| 325 |
+
suite.
|
| 326 |
- **JSON-schema structured output can crash `llguidance`-based runtimes.**
|
| 327 |
This is a base-model tokenizer interaction, not a defect introduced by the
|
| 328 |
pruning or quantization here — it reproduces with zero model weights loaded,
|
artifacts/acc_ream.json
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "step-3.7-p15-ream-shared8-head8",
|
| 3 |
+
"overall": [
|
| 4 |
+
23,
|
| 5 |
+
24
|
| 6 |
+
],
|
| 7 |
+
"by_cat": {
|
| 8 |
+
"math": [
|
| 9 |
+
13,
|
| 10 |
+
14
|
| 11 |
+
],
|
| 12 |
+
"factual": [
|
| 13 |
+
10,
|
| 14 |
+
10
|
| 15 |
+
]
|
| 16 |
+
},
|
| 17 |
+
"results": [
|
| 18 |
+
{
|
| 19 |
+
"cat": "math",
|
| 20 |
+
"expected": "330",
|
| 21 |
+
"ok": true,
|
| 22 |
+
"answer": "330",
|
| 23 |
+
"finish": "stop"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"cat": "math",
|
| 27 |
+
"expected": "84",
|
| 28 |
+
"ok": true,
|
| 29 |
+
"answer": "84",
|
| 30 |
+
"finish": "stop"
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"cat": "math",
|
| 34 |
+
"expected": "120",
|
| 35 |
+
"ok": false,
|
| 36 |
+
"answer": "52",
|
| 37 |
+
"finish": "stop"
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"cat": "math",
|
| 41 |
+
"expected": "355",
|
| 42 |
+
"ok": true,
|
| 43 |
+
"answer": "355",
|
| 44 |
+
"finish": "stop"
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"cat": "math",
|
| 48 |
+
"expected": "18",
|
| 49 |
+
"ok": true,
|
| 50 |
+
"answer": "18",
|
| 51 |
+
"finish": "stop"
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"cat": "math",
|
| 55 |
+
"expected": "270",
|
| 56 |
+
"ok": true,
|
| 57 |
+
"answer": "270",
|
| 58 |
+
"finish": "stop"
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"cat": "math",
|
| 62 |
+
"expected": "101",
|
| 63 |
+
"ok": true,
|
| 64 |
+
"answer": "101",
|
| 65 |
+
"finish": "stop"
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"cat": "math",
|
| 69 |
+
"expected": "114",
|
| 70 |
+
"ok": true,
|
| 71 |
+
"answer": "114",
|
| 72 |
+
"finish": "stop"
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"cat": "math",
|
| 76 |
+
"expected": "215",
|
| 77 |
+
"ok": true,
|
| 78 |
+
"answer": "215",
|
| 79 |
+
"finish": "stop"
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"cat": "math",
|
| 83 |
+
"expected": "60",
|
| 84 |
+
"ok": true,
|
| 85 |
+
"answer": "60",
|
| 86 |
+
"finish": "stop"
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"cat": "math",
|
| 90 |
+
"expected": "1",
|
| 91 |
+
"ok": true,
|
| 92 |
+
"answer": "1",
|
| 93 |
+
"finish": "stop"
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"cat": "math",
|
| 97 |
+
"expected": "25",
|
| 98 |
+
"ok": true,
|
| 99 |
+
"answer": "25",
|
| 100 |
+
"finish": "stop"
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"cat": "math",
|
| 104 |
+
"expected": "518.4",
|
| 105 |
+
"ok": true,
|
| 106 |
+
"answer": "To calculate the final price:\n1. First apply the 20% discount: $600 * (1 - 0.2) ",
|
| 107 |
+
"finish": "stop"
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"cat": "math",
|
| 111 |
+
"expected": "36",
|
| 112 |
+
"ok": true,
|
| 113 |
+
"answer": "36",
|
| 114 |
+
"finish": "stop"
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"cat": "factual",
|
| 118 |
+
"expected": "Au",
|
| 119 |
+
"ok": true,
|
| 120 |
+
"answer": "Au",
|
| 121 |
+
"finish": "stop"
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"cat": "factual",
|
| 125 |
+
"expected": "1969",
|
| 126 |
+
"ok": true,
|
| 127 |
+
"answer": "1969",
|
| 128 |
+
"finish": "stop"
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"cat": "factual",
|
| 132 |
+
"expected": "Ottawa",
|
| 133 |
+
"ok": true,
|
| 134 |
+
"answer": "Ottawa",
|
| 135 |
+
"finish": "stop"
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"cat": "factual",
|
| 139 |
+
"expected": "6",
|
| 140 |
+
"ok": true,
|
| 141 |
+
"answer": "6",
|
| 142 |
+
"finish": "stop"
|
| 143 |
+
},
|
| 144 |
+
{
|
| 145 |
+
"cat": "factual",
|
| 146 |
+
"expected": "Mars",
|
| 147 |
+
"ok": true,
|
| 148 |
+
"answer": "Mars",
|
| 149 |
+
"finish": "stop"
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"cat": "factual",
|
| 153 |
+
"expected": "Pacific",
|
| 154 |
+
"ok": true,
|
| 155 |
+
"answer": "Pacific",
|
| 156 |
+
"finish": "stop"
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"cat": "factual",
|
| 160 |
+
"expected": "Shakespeare",
|
| 161 |
+
"ok": true,
|
| 162 |
+
"answer": "Shakespeare",
|
| 163 |
+
"finish": "stop"
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"cat": "factual",
|
| 167 |
+
"expected": "0",
|
| 168 |
+
"ok": true,
|
| 169 |
+
"answer": "0",
|
| 170 |
+
"finish": "stop"
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"cat": "factual",
|
| 174 |
+
"expected": "carbon dioxide",
|
| 175 |
+
"ok": true,
|
| 176 |
+
"answer": "Carbon dioxide",
|
| 177 |
+
"finish": "stop"
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
"cat": "factual",
|
| 181 |
+
"expected": "90",
|
| 182 |
+
"ok": true,
|
| 183 |
+
"answer": "90",
|
| 184 |
+
"finish": "stop"
|
| 185 |
+
}
|
| 186 |
+
]
|
| 187 |
+
}
|
artifacts/acc_shared8.json
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "step-3.7-p15-vblend-shared8-head8",
|
| 3 |
+
"overall": [
|
| 4 |
+
24,
|
| 5 |
+
24
|
| 6 |
+
],
|
| 7 |
+
"by_cat": {
|
| 8 |
+
"math": [
|
| 9 |
+
14,
|
| 10 |
+
14
|
| 11 |
+
],
|
| 12 |
+
"factual": [
|
| 13 |
+
10,
|
| 14 |
+
10
|
| 15 |
+
]
|
| 16 |
+
},
|
| 17 |
+
"results": [
|
| 18 |
+
{
|
| 19 |
+
"cat": "math",
|
| 20 |
+
"expected": "330",
|
| 21 |
+
"ok": true,
|
| 22 |
+
"answer": "330",
|
| 23 |
+
"finish": "stop"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"cat": "math",
|
| 27 |
+
"expected": "84",
|
| 28 |
+
"ok": true,
|
| 29 |
+
"answer": "84",
|
| 30 |
+
"finish": "stop"
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"cat": "math",
|
| 34 |
+
"expected": "120",
|
| 35 |
+
"ok": true,
|
| 36 |
+
"answer": "120",
|
| 37 |
+
"finish": "stop"
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"cat": "math",
|
| 41 |
+
"expected": "355",
|
| 42 |
+
"ok": true,
|
| 43 |
+
"answer": "355",
|
| 44 |
+
"finish": "stop"
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"cat": "math",
|
| 48 |
+
"expected": "18",
|
| 49 |
+
"ok": true,
|
| 50 |
+
"answer": "18",
|
| 51 |
+
"finish": "stop"
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"cat": "math",
|
| 55 |
+
"expected": "270",
|
| 56 |
+
"ok": true,
|
| 57 |
+
"answer": "270",
|
| 58 |
+
"finish": "stop"
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"cat": "math",
|
| 62 |
+
"expected": "101",
|
| 63 |
+
"ok": true,
|
| 64 |
+
"answer": "101",
|
| 65 |
+
"finish": "stop"
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"cat": "math",
|
| 69 |
+
"expected": "114",
|
| 70 |
+
"ok": true,
|
| 71 |
+
"answer": "114",
|
| 72 |
+
"finish": "stop"
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"cat": "math",
|
| 76 |
+
"expected": "215",
|
| 77 |
+
"ok": true,
|
| 78 |
+
"answer": "To solve this:\n1. Interpret the formatted times correctly: departure is **09:45",
|
| 79 |
+
"finish": "stop"
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"cat": "math",
|
| 83 |
+
"expected": "60",
|
| 84 |
+
"ok": true,
|
| 85 |
+
"answer": "60",
|
| 86 |
+
"finish": "stop"
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"cat": "math",
|
| 90 |
+
"expected": "1",
|
| 91 |
+
"ok": true,
|
| 92 |
+
"answer": "1",
|
| 93 |
+
"finish": "stop"
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"cat": "math",
|
| 97 |
+
"expected": "25",
|
| 98 |
+
"ok": true,
|
| 99 |
+
"answer": "25",
|
| 100 |
+
"finish": "stop"
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"cat": "math",
|
| 104 |
+
"expected": "518.4",
|
| 105 |
+
"ok": true,
|
| 106 |
+
"answer": "518.4",
|
| 107 |
+
"finish": "stop"
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"cat": "math",
|
| 111 |
+
"expected": "36",
|
| 112 |
+
"ok": true,
|
| 113 |
+
"answer": "36",
|
| 114 |
+
"finish": "stop"
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"cat": "factual",
|
| 118 |
+
"expected": "Au",
|
| 119 |
+
"ok": true,
|
| 120 |
+
"answer": "Au",
|
| 121 |
+
"finish": "stop"
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"cat": "factual",
|
| 125 |
+
"expected": "1969",
|
| 126 |
+
"ok": true,
|
| 127 |
+
"answer": "1969",
|
| 128 |
+
"finish": "stop"
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"cat": "factual",
|
| 132 |
+
"expected": "Ottawa",
|
| 133 |
+
"ok": true,
|
| 134 |
+
"answer": "Ottawa",
|
| 135 |
+
"finish": "stop"
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"cat": "factual",
|
| 139 |
+
"expected": "6",
|
| 140 |
+
"ok": true,
|
| 141 |
+
"answer": "6",
|
| 142 |
+
"finish": "stop"
|
| 143 |
+
},
|
| 144 |
+
{
|
| 145 |
+
"cat": "factual",
|
| 146 |
+
"expected": "Mars",
|
| 147 |
+
"ok": true,
|
| 148 |
+
"answer": "Mars",
|
| 149 |
+
"finish": "stop"
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"cat": "factual",
|
| 153 |
+
"expected": "Pacific",
|
| 154 |
+
"ok": true,
|
| 155 |
+
"answer": "Pacific",
|
| 156 |
+
"finish": "stop"
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"cat": "factual",
|
| 160 |
+
"expected": "Shakespeare",
|
| 161 |
+
"ok": true,
|
| 162 |
+
"answer": "Shakespeare",
|
| 163 |
+
"finish": "stop"
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"cat": "factual",
|
| 167 |
+
"expected": "0",
|
| 168 |
+
"ok": true,
|
| 169 |
+
"answer": "0",
|
| 170 |
+
"finish": "stop"
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"cat": "factual",
|
| 174 |
+
"expected": "carbon dioxide",
|
| 175 |
+
"ok": true,
|
| 176 |
+
"answer": "Carbon dioxide.",
|
| 177 |
+
"finish": "stop"
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
"cat": "factual",
|
| 181 |
+
"expected": "90",
|
| 182 |
+
"ok": true,
|
| 183 |
+
"answer": "90",
|
| 184 |
+
"finish": "stop"
|
| 185 |
+
}
|
| 186 |
+
]
|
| 187 |
+
}
|
artifacts/accuracy_eval.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Discriminating eval: does REAM's -0.19 PPL reflect capability or smoothing?
|
| 2 |
+
|
| 3 |
+
PPL rewards a flatter output distribution; it cannot tell a real gain from a
|
| 4 |
+
harmless smoothing effect. This scores EXACT-ANSWER tasks right/wrong -- tasks
|
| 5 |
+
where there is one correct answer and smoothing cannot help. Same scorer for
|
| 6 |
+
both models, so the comparison is fair regardless of scorer strictness.
|
| 7 |
+
|
| 8 |
+
Prediction if -0.19 is smoothing: REAM accuracy ~= shared8-head8 accuracy,
|
| 9 |
+
especially on math (PPL already showed reasoning_math did NOT improve, +0.007).
|
| 10 |
+
If REAM is genuinely better, math accuracy rises.
|
| 11 |
+
|
| 12 |
+
Usage: python accuracy_eval.py <model-id> <out.json>
|
| 13 |
+
"""
|
| 14 |
+
import json, re, sys, urllib.request
|
| 15 |
+
|
| 16 |
+
MODEL = sys.argv[1]
|
| 17 |
+
OUT = sys.argv[2] if len(sys.argv) > 2 else None
|
| 18 |
+
|
| 19 |
+
# (category, prompt, expected). Hand-written to avoid benchmark contamination;
|
| 20 |
+
# multi-step so smoothing cannot luck into them, but within a strong model's reach.
|
| 21 |
+
ITEMS = [
|
| 22 |
+
("math", "A tank holds 480 liters. It drains at 12 liters per minute for 15 minutes, then is refilled by 30 liters. How many liters are in it now? Give only the number.", "330"),
|
| 23 |
+
("math", "A book has 342 pages. Maria reads 18 pages a day for 9 days, then 24 pages a day for 4 days. How many pages are left? Number only.", "84"),
|
| 24 |
+
("math", "There are 7 boxes with 23 apples each. 41 apples are rotten and removed. How many good apples remain? Number only.", "120"),
|
| 25 |
+
("math", "A car travels 65 km/h for 3 hours, then 80 km/h for 2 hours. Total distance in km? Number only.", "355"),
|
| 26 |
+
("math", "A store sells pens at 3 for $2. How much do 27 pens cost, in dollars? Number only.", "18"),
|
| 27 |
+
("math", "Compute 144 divided by 8, then multiply the result by 15. Number only.", "270"),
|
| 28 |
+
("math", "A rectangle is 14 by 9. A square of side 5 is cut out. Remaining area? Number only.", "101"),
|
| 29 |
+
("math", "Sarah has $250. She buys 6 shirts at $18 each and 2 hats at $14 each. How much money is left? Number only.", "114"),
|
| 30 |
+
("math", "A train departs at 09:45 and arrives at 13:20. Journey length in minutes? Number only.", "215"),
|
| 31 |
+
("math", "If 5 machines make 5 widgets in 5 minutes, how many widgets do 5 machines make in 60 minutes? Number only.", "60"),
|
| 32 |
+
("math", "A recipe needs 3 eggs per cake. You have 40 eggs. After making as many whole cakes as possible, how many eggs are left over? Number only.", "1"),
|
| 33 |
+
("math", "The sum of three consecutive integers is 72. What is the largest of them? Number only.", "25"),
|
| 34 |
+
("math", "A phone costs $600. It is discounted 20%, then 8% sales tax is added. Final price in dollars? Number only.", "518.4"),
|
| 35 |
+
("math", "A garden is 12 m by 8 m. A path 1 m wide runs around the inside edge. Area of the path in square metres? Number only.", "36"),
|
| 36 |
+
("factual", "What is the chemical symbol for gold? Symbol only.", "Au"),
|
| 37 |
+
("factual", "In what year did the first human land on the Moon? Year only.", "1969"),
|
| 38 |
+
("factual", "What is the capital city of Canada? One word.", "Ottawa"),
|
| 39 |
+
("factual", "How many sides does a hexagon have? Number only.", "6"),
|
| 40 |
+
("factual", "What planet is known as the Red Planet? One word.", "Mars"),
|
| 41 |
+
("factual", "What is the largest ocean on Earth? One word.", "Pacific"),
|
| 42 |
+
("factual", "Who wrote the play 'Romeo and Juliet'? Last name only.", "Shakespeare"),
|
| 43 |
+
("factual", "What is the freezing point of water in Celsius? Number only.", "0"),
|
| 44 |
+
("factual", "What gas do plants primarily absorb from the air for photosynthesis? Two words.", "carbon dioxide"),
|
| 45 |
+
("factual", "How many degrees are in a right angle? Number only.", "90"),
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def ask(prompt, mt=3500):
|
| 50 |
+
body = json.dumps({"model": MODEL, "messages": [{"role": "user", "content": prompt}],
|
| 51 |
+
"temperature": 0.0, "top_p": 1.0, "top_k": 0, "min_p": 0.0,
|
| 52 |
+
"repetition_penalty": 1.0, "max_tokens": mt, "stream": False}).encode()
|
| 53 |
+
r = urllib.request.Request("http://localhost:1234/v1/chat/completions", data=body,
|
| 54 |
+
headers={"Content-Type": "application/json"})
|
| 55 |
+
d = json.load(urllib.request.urlopen(r, timeout=600))["choices"][0]
|
| 56 |
+
m = d["message"]
|
| 57 |
+
return (m.get("content") or "").strip(), d.get("finish_reason")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def norm_num(s):
|
| 61 |
+
# strip digit-internal separators (handles the corruption AND legit commas);
|
| 62 |
+
# both models scored identically so this is fair.
|
| 63 |
+
return re.sub(r"(?<=\d)[ ,_](?=\d)", "", s)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def check(ans, expected, cat):
|
| 67 |
+
a = norm_num(ans)
|
| 68 |
+
if cat == "factual" and not expected.replace(".", "").isdigit():
|
| 69 |
+
return expected.lower() in a.lower()
|
| 70 |
+
# numeric: compare the set of numbers present; credit if expected appears
|
| 71 |
+
want = expected
|
| 72 |
+
nums = re.findall(r"-?\d+\.?\d*", a)
|
| 73 |
+
# exact match, or match ignoring trailing .0
|
| 74 |
+
for n in nums:
|
| 75 |
+
if n == want or n.rstrip("0").rstrip(".") == want.rstrip("0").rstrip("."):
|
| 76 |
+
return True
|
| 77 |
+
return False
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def self_test():
|
| 81 |
+
assert check("330", "330", "math")
|
| 82 |
+
assert check("The answer is 518.40 dollars.", "518.4", "math")
|
| 83 |
+
assert check("3 3 0", "330", "math") # space-corrupted
|
| 84 |
+
assert check("**Au**", "Au", "factual")
|
| 85 |
+
assert not check("The answer is 331.", "330", "math")
|
| 86 |
+
assert check("Ottawa is the capital.", "Ottawa", "factual")
|
| 87 |
+
print("[selftest] answer-extraction ok", flush=True)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
if __name__ == "__main__":
|
| 91 |
+
self_test()
|
| 92 |
+
if MODEL == "SELFTEST":
|
| 93 |
+
sys.exit(0)
|
| 94 |
+
from collections import defaultdict
|
| 95 |
+
tally = defaultdict(lambda: [0, 0]); results = []
|
| 96 |
+
for i, (cat, q, exp) in enumerate(ITEMS):
|
| 97 |
+
try:
|
| 98 |
+
ans, fin = ask(q)
|
| 99 |
+
except Exception as e:
|
| 100 |
+
print(f" item {i} ERROR {e}", flush=True); continue
|
| 101 |
+
ok = check(ans, exp, cat)
|
| 102 |
+
tally[cat][0] += ok; tally[cat][1] += 1
|
| 103 |
+
results.append({"cat": cat, "expected": exp, "ok": ok, "answer": ans[:80], "finish": fin})
|
| 104 |
+
print(f" [{'OK ' if ok else 'XX '}] {cat:8} want={exp:14} got={ans[:44]!r}", flush=True)
|
| 105 |
+
tot_ok = sum(v[0] for v in tally.values()); tot_n = sum(v[1] for v in tally.values())
|
| 106 |
+
print(f"\n{MODEL}", flush=True)
|
| 107 |
+
for cat, (ok, n) in sorted(tally.items()):
|
| 108 |
+
print(f" {cat:10} {ok}/{n}", flush=True)
|
| 109 |
+
print(f" OVERALL {tot_ok}/{tot_n} ({100*tot_ok/tot_n:.0f}%)", flush=True)
|
| 110 |
+
if OUT:
|
| 111 |
+
json.dump({"model": MODEL, "overall": [tot_ok, tot_n],
|
| 112 |
+
"by_cat": {k: v for k, v in tally.items()}, "results": results},
|
| 113 |
+
open(OUT, "w"), indent=2)
|
artifacts/tc_ream.json
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "step-3.7-p15-ream-shared8-head8",
|
| 3 |
+
"score": [
|
| 4 |
+
14,
|
| 5 |
+
15
|
| 6 |
+
],
|
| 7 |
+
"results": [
|
| 8 |
+
{
|
| 9 |
+
"req": "Set a timer for 25 minutes.",
|
| 10 |
+
"ok": true,
|
| 11 |
+
"why": "ok",
|
| 12 |
+
"name": "set_timer",
|
| 13 |
+
"args": {
|
| 14 |
+
"minutes": 25
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"req": "Convert 150 USD to EUR.",
|
| 19 |
+
"ok": true,
|
| 20 |
+
"why": "ok",
|
| 21 |
+
"name": "convert_currency",
|
| 22 |
+
"args": {
|
| 23 |
+
"amount": 150,
|
| 24 |
+
"from": "USD",
|
| 25 |
+
"to": "EUR"
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"req": "Book a flight from Denver to Miami for 3 passengers.",
|
| 30 |
+
"ok": true,
|
| 31 |
+
"why": "ok",
|
| 32 |
+
"name": "book_flight",
|
| 33 |
+
"args": {
|
| 34 |
+
"from_city": "Denver",
|
| 35 |
+
"to_city": "Miami",
|
| 36 |
+
"passengers": 3
|
| 37 |
+
}
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"req": "Resize the image to 800 by 600.",
|
| 41 |
+
"ok": true,
|
| 42 |
+
"why": "ok",
|
| 43 |
+
"name": "resize_image",
|
| 44 |
+
"args": {
|
| 45 |
+
"width": 800,
|
| 46 |
+
"height": 600
|
| 47 |
+
}
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"req": "Set the thermostat to 68 degrees.",
|
| 51 |
+
"ok": true,
|
| 52 |
+
"why": "ok",
|
| 53 |
+
"name": "set_thermostat",
|
| 54 |
+
"args": {
|
| 55 |
+
"temperature": 68
|
| 56 |
+
}
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"req": "Add 5 apples to my cart.",
|
| 60 |
+
"ok": true,
|
| 61 |
+
"why": "ok",
|
| 62 |
+
"name": "add_to_cart",
|
| 63 |
+
"args": {
|
| 64 |
+
"item": "apples",
|
| 65 |
+
"quantity": 5
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"req": "Calculate an 18% tip on an 80 dollar bill.",
|
| 70 |
+
"ok": true,
|
| 71 |
+
"why": "ok",
|
| 72 |
+
"name": "calculate_tip",
|
| 73 |
+
"args": {
|
| 74 |
+
"bill": 80,
|
| 75 |
+
"percent": 18
|
| 76 |
+
}
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"req": "Play Bohemian Rhapsody by Queen.",
|
| 80 |
+
"ok": true,
|
| 81 |
+
"why": "ok",
|
| 82 |
+
"name": "play_song",
|
| 83 |
+
"args": {
|
| 84 |
+
"title": "Bohemian Rhapsody",
|
| 85 |
+
"artist": "Queen"
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"req": "What's the weather in Paris?",
|
| 90 |
+
"ok": true,
|
| 91 |
+
"why": "ok",
|
| 92 |
+
"name": "get_weather",
|
| 93 |
+
"args": {
|
| 94 |
+
"city": "Paris"
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"req": "Get the stock price for AAPL.",
|
| 99 |
+
"ok": true,
|
| 100 |
+
"why": "ok",
|
| 101 |
+
"name": "get_stock_price",
|
| 102 |
+
"args": {
|
| 103 |
+
"ticker": "AAPL"
|
| 104 |
+
}
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"req": "Find headphones under 200 dollars.",
|
| 108 |
+
"ok": true,
|
| 109 |
+
"why": "ok",
|
| 110 |
+
"name": "search_products",
|
| 111 |
+
"args": {
|
| 112 |
+
"query": "headphones",
|
| 113 |
+
"max_price": 200
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"req": "Translate 'good morning' into Spanish.",
|
| 118 |
+
"ok": false,
|
| 119 |
+
"why": "target_lang: want 'Spanish' got 'es'",
|
| 120 |
+
"name": "translate",
|
| 121 |
+
"args": {
|
| 122 |
+
"text": "good morning",
|
| 123 |
+
"target_lang": "es"
|
| 124 |
+
}
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"req": "Transfer 340 dollars from checking to savings.",
|
| 128 |
+
"ok": true,
|
| 129 |
+
"why": "ok",
|
| 130 |
+
"name": "transfer_money",
|
| 131 |
+
"args": {
|
| 132 |
+
"amount": 340,
|
| 133 |
+
"from_account": "checking",
|
| 134 |
+
"to_account": "savings"
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"req": "Schedule a meeting on Tuesday at 3pm.",
|
| 139 |
+
"ok": true,
|
| 140 |
+
"why": "ok",
|
| 141 |
+
"name": "schedule_meeting",
|
| 142 |
+
"args": {
|
| 143 |
+
"day": "Tuesday",
|
| 144 |
+
"time": "3pm"
|
| 145 |
+
}
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"req": "Send 12 invitations to the birthday event.",
|
| 149 |
+
"ok": true,
|
| 150 |
+
"why": "ok",
|
| 151 |
+
"name": "send_invitations",
|
| 152 |
+
"args": {
|
| 153 |
+
"count": 12,
|
| 154 |
+
"event": "birthday event"
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
]
|
| 158 |
+
}
|
artifacts/tc_shared8.json
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "step-3.7-p15-vblend-shared8-head8",
|
| 3 |
+
"score": [
|
| 4 |
+
14,
|
| 5 |
+
15
|
| 6 |
+
],
|
| 7 |
+
"results": [
|
| 8 |
+
{
|
| 9 |
+
"req": "Set a timer for 25 minutes.",
|
| 10 |
+
"ok": true,
|
| 11 |
+
"why": "ok",
|
| 12 |
+
"name": "set_timer",
|
| 13 |
+
"args": {
|
| 14 |
+
"minutes": 25
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"req": "Convert 150 USD to EUR.",
|
| 19 |
+
"ok": true,
|
| 20 |
+
"why": "ok",
|
| 21 |
+
"name": "convert_currency",
|
| 22 |
+
"args": {
|
| 23 |
+
"amount": 150,
|
| 24 |
+
"from": "USD",
|
| 25 |
+
"to": "EUR"
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"req": "Book a flight from Denver to Miami for 3 passengers.",
|
| 30 |
+
"ok": true,
|
| 31 |
+
"why": "ok",
|
| 32 |
+
"name": "book_flight",
|
| 33 |
+
"args": {
|
| 34 |
+
"from_city": "Denver",
|
| 35 |
+
"to_city": "Miami",
|
| 36 |
+
"passengers": 3
|
| 37 |
+
}
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"req": "Resize the image to 800 by 600.",
|
| 41 |
+
"ok": true,
|
| 42 |
+
"why": "ok",
|
| 43 |
+
"name": "resize_image",
|
| 44 |
+
"args": {
|
| 45 |
+
"width": 800,
|
| 46 |
+
"height": 600
|
| 47 |
+
}
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"req": "Set the thermostat to 68 degrees.",
|
| 51 |
+
"ok": true,
|
| 52 |
+
"why": "ok",
|
| 53 |
+
"name": "set_thermostat",
|
| 54 |
+
"args": {
|
| 55 |
+
"temperature": 68
|
| 56 |
+
}
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"req": "Add 5 apples to my cart.",
|
| 60 |
+
"ok": true,
|
| 61 |
+
"why": "ok",
|
| 62 |
+
"name": "add_to_cart",
|
| 63 |
+
"args": {
|
| 64 |
+
"item": "apples",
|
| 65 |
+
"quantity": 5
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"req": "Calculate an 18% tip on an 80 dollar bill.",
|
| 70 |
+
"ok": true,
|
| 71 |
+
"why": "ok",
|
| 72 |
+
"name": "calculate_tip",
|
| 73 |
+
"args": {
|
| 74 |
+
"bill": 80,
|
| 75 |
+
"percent": 18
|
| 76 |
+
}
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"req": "Play Bohemian Rhapsody by Queen.",
|
| 80 |
+
"ok": true,
|
| 81 |
+
"why": "ok",
|
| 82 |
+
"name": "play_song",
|
| 83 |
+
"args": {
|
| 84 |
+
"title": "Bohemian Rhapsody",
|
| 85 |
+
"artist": "Queen"
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"req": "What's the weather in Paris?",
|
| 90 |
+
"ok": true,
|
| 91 |
+
"why": "ok",
|
| 92 |
+
"name": "get_weather",
|
| 93 |
+
"args": {
|
| 94 |
+
"city": "Paris"
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"req": "Get the stock price for AAPL.",
|
| 99 |
+
"ok": true,
|
| 100 |
+
"why": "ok",
|
| 101 |
+
"name": "get_stock_price",
|
| 102 |
+
"args": {
|
| 103 |
+
"ticker": "AAPL"
|
| 104 |
+
}
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"req": "Find headphones under 200 dollars.",
|
| 108 |
+
"ok": true,
|
| 109 |
+
"why": "ok",
|
| 110 |
+
"name": "search_products",
|
| 111 |
+
"args": {
|
| 112 |
+
"query": "headphones",
|
| 113 |
+
"max_price": 200
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"req": "Translate 'good morning' into Spanish.",
|
| 118 |
+
"ok": false,
|
| 119 |
+
"why": "target_lang: want 'Spanish' got 'es'",
|
| 120 |
+
"name": "translate",
|
| 121 |
+
"args": {
|
| 122 |
+
"text": "good morning",
|
| 123 |
+
"target_lang": "es"
|
| 124 |
+
}
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"req": "Transfer 340 dollars from checking to savings.",
|
| 128 |
+
"ok": true,
|
| 129 |
+
"why": "ok",
|
| 130 |
+
"name": "transfer_money",
|
| 131 |
+
"args": {
|
| 132 |
+
"amount": 340,
|
| 133 |
+
"from_account": "checking",
|
| 134 |
+
"to_account": "savings"
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"req": "Schedule a meeting on Tuesday at 3pm.",
|
| 139 |
+
"ok": true,
|
| 140 |
+
"why": "ok",
|
| 141 |
+
"name": "schedule_meeting",
|
| 142 |
+
"args": {
|
| 143 |
+
"day": "Tuesday",
|
| 144 |
+
"time": "3pm"
|
| 145 |
+
}
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"req": "Send 12 invitations to the birthday event.",
|
| 149 |
+
"ok": true,
|
| 150 |
+
"why": "ok",
|
| 151 |
+
"name": "send_invitations",
|
| 152 |
+
"args": {
|
| 153 |
+
"count": 12,
|
| 154 |
+
"event": "birthday event"
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
]
|
| 158 |
+
}
|
artifacts/toolcall_eval.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Agentic discriminator: does REAM pick the right tool with the RIGHT arguments?
|
| 2 |
+
|
| 3 |
+
The math/factual accuracy test covered the category where PPL predicted no gain.
|
| 4 |
+
REAM's biggest PPL gains were tool_use (27->15.6) and coding/agentic -- untested
|
| 5 |
+
for capability. This scores tool-call correctness right/wrong: correct function
|
| 6 |
+
name AND correct arguments. Argument extraction is exactly what smoothing cannot
|
| 7 |
+
fake -- a flatter distribution does not produce '800 by 600' or 'USD->EUR'
|
| 8 |
+
correctly. Same scorer for both models.
|
| 9 |
+
|
| 10 |
+
Usage: python toolcall_eval.py <model-id> <out.json>
|
| 11 |
+
"""
|
| 12 |
+
import json, re, sys, urllib.request
|
| 13 |
+
|
| 14 |
+
MODEL = sys.argv[1]
|
| 15 |
+
OUT = sys.argv[2] if len(sys.argv) > 2 else None
|
| 16 |
+
|
| 17 |
+
CATALOG = """Available functions (use EXACTLY these names and argument keys):
|
| 18 |
+
- set_timer(minutes)
|
| 19 |
+
- convert_currency(amount, from, to)
|
| 20 |
+
- book_flight(from_city, to_city, passengers)
|
| 21 |
+
- resize_image(width, height)
|
| 22 |
+
- set_thermostat(temperature)
|
| 23 |
+
- add_to_cart(item, quantity)
|
| 24 |
+
- calculate_tip(bill, percent)
|
| 25 |
+
- play_song(title, artist)
|
| 26 |
+
- get_weather(city)
|
| 27 |
+
- get_stock_price(ticker)
|
| 28 |
+
- search_products(query, max_price)
|
| 29 |
+
- translate(text, target_lang)
|
| 30 |
+
- transfer_money(amount, from_account, to_account)
|
| 31 |
+
- schedule_meeting(day, time)
|
| 32 |
+
- send_invitations(count, event)"""
|
| 33 |
+
|
| 34 |
+
SYS = (CATALOG + "\n\nCall exactly ONE function. Reply with ONLY a JSON object "
|
| 35 |
+
'{"name": <function>, "arguments": {<args>}} and nothing else. '
|
| 36 |
+
"Use the exact names above and the exact argument values implied by the request.")
|
| 37 |
+
|
| 38 |
+
# (request, expected_name, {arg: expected}). Numeric/string args are the
|
| 39 |
+
# discriminator; free-text args use keyword membership.
|
| 40 |
+
ITEMS = [
|
| 41 |
+
("Set a timer for 25 minutes.", "set_timer", {"minutes": "25"}),
|
| 42 |
+
("Convert 150 USD to EUR.", "convert_currency", {"amount": "150", "from": "USD", "to": "EUR"}),
|
| 43 |
+
("Book a flight from Denver to Miami for 3 passengers.", "book_flight",
|
| 44 |
+
{"from_city": "Denver", "to_city": "Miami", "passengers": "3"}),
|
| 45 |
+
("Resize the image to 800 by 600.", "resize_image", {"width": "800", "height": "600"}),
|
| 46 |
+
("Set the thermostat to 68 degrees.", "set_thermostat", {"temperature": "68"}),
|
| 47 |
+
("Add 5 apples to my cart.", "add_to_cart", {"item": "apples", "quantity": "5"}),
|
| 48 |
+
("Calculate an 18% tip on an 80 dollar bill.", "calculate_tip", {"bill": "80", "percent": "18"}),
|
| 49 |
+
("Play Bohemian Rhapsody by Queen.", "play_song", {"title": "Bohemian Rhapsody", "artist": "Queen"}),
|
| 50 |
+
("What's the weather in Paris?", "get_weather", {"city": "Paris"}),
|
| 51 |
+
("Get the stock price for AAPL.", "get_stock_price", {"ticker": "AAPL"}),
|
| 52 |
+
("Find headphones under 200 dollars.", "search_products", {"query": "headphones", "max_price": "200"}),
|
| 53 |
+
("Translate 'good morning' into Spanish.", "translate", {"text": "good morning", "target_lang": "Spanish"}),
|
| 54 |
+
("Transfer 340 dollars from checking to savings.", "transfer_money",
|
| 55 |
+
{"amount": "340", "from_account": "checking", "to_account": "savings"}),
|
| 56 |
+
("Schedule a meeting on Tuesday at 3pm.", "schedule_meeting", {"day": "Tuesday", "time": "3"}),
|
| 57 |
+
("Send 12 invitations to the birthday event.", "send_invitations", {"count": "12", "event": "birthday"}),
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def ask(prompt, mt=3000):
|
| 62 |
+
body = json.dumps({"model": MODEL,
|
| 63 |
+
"messages": [{"role": "system", "content": SYS},
|
| 64 |
+
{"role": "user", "content": prompt}],
|
| 65 |
+
"temperature": 0.0, "top_p": 1.0, "top_k": 0, "min_p": 0.0,
|
| 66 |
+
"repetition_penalty": 1.0, "max_tokens": mt, "stream": False}).encode()
|
| 67 |
+
r = urllib.request.Request("http://localhost:1234/v1/chat/completions", data=body,
|
| 68 |
+
headers={"Content-Type": "application/json"})
|
| 69 |
+
m = json.load(urllib.request.urlopen(r, timeout=600))["choices"][0]["message"]
|
| 70 |
+
return (m.get("content") or "").strip()
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def parse_call(text):
|
| 74 |
+
"""Extract {name, arguments} from a JSON object or ```json block."""
|
| 75 |
+
t = re.sub(r"```(?:json)?", "", text)
|
| 76 |
+
# find the outermost {...} that has a "name"
|
| 77 |
+
for m in re.finditer(r"\{.*\}", t, re.S):
|
| 78 |
+
try:
|
| 79 |
+
o = json.loads(m.group(0))
|
| 80 |
+
if isinstance(o, dict) and "name" in o:
|
| 81 |
+
return o.get("name"), (o.get("arguments") or o.get("args") or {})
|
| 82 |
+
except Exception:
|
| 83 |
+
continue
|
| 84 |
+
return None, {}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def norm(v):
|
| 88 |
+
return re.sub(r"(?<=\d)[ ,_](?=\d)", "", str(v)).strip().lower()
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def check(name, args, exp_name, exp_args):
|
| 92 |
+
if norm(name) != norm(exp_name):
|
| 93 |
+
return False, "name"
|
| 94 |
+
for k, want in exp_args.items():
|
| 95 |
+
got = args.get(k)
|
| 96 |
+
if got is None:
|
| 97 |
+
# allow the value to appear under any key (arg-naming can differ)
|
| 98 |
+
if any(norm(want) == norm(val) or norm(want) in norm(val)
|
| 99 |
+
for val in args.values()):
|
| 100 |
+
continue
|
| 101 |
+
return False, f"missing {k}={want}"
|
| 102 |
+
if norm(want) == norm(got) or norm(want) in norm(got):
|
| 103 |
+
continue
|
| 104 |
+
return False, f"{k}: want {want!r} got {got!r}"
|
| 105 |
+
return True, "ok"
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def self_test():
|
| 109 |
+
n, a = parse_call('```json\n{"name": "set_timer", "arguments": {"minutes": 25}}\n```')
|
| 110 |
+
assert n == "set_timer" and str(a["minutes"]) == "25"
|
| 111 |
+
ok, _ = check(n, a, "set_timer", {"minutes": "25"}); assert ok
|
| 112 |
+
ok, why = check("set_timer", {"minutes": "30"}, "set_timer", {"minutes": "25"})
|
| 113 |
+
assert not ok, why
|
| 114 |
+
ok, _ = check("resize_image", {"w": "800", "h": "600"}, "resize_image",
|
| 115 |
+
{"width": "800", "height": "600"})
|
| 116 |
+
assert ok, "value-under-any-key fallback"
|
| 117 |
+
print("[selftest] tool-call parser ok", flush=True)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
if __name__ == "__main__":
|
| 121 |
+
self_test()
|
| 122 |
+
if MODEL == "SELFTEST":
|
| 123 |
+
sys.exit(0)
|
| 124 |
+
ok_n = 0; results = []
|
| 125 |
+
for req, en, ea in ITEMS:
|
| 126 |
+
try:
|
| 127 |
+
raw = ask(req)
|
| 128 |
+
except Exception as e:
|
| 129 |
+
print(f" ERROR {e}", flush=True); continue
|
| 130 |
+
n, a = parse_call(raw)
|
| 131 |
+
ok, why = check(n, a, en, ea)
|
| 132 |
+
ok_n += ok
|
| 133 |
+
results.append({"req": req, "ok": ok, "why": why, "name": n, "args": a})
|
| 134 |
+
print(f" [{'OK ' if ok else 'XX '}] want {en}({ea}) -> got {n}({a}) {'' if ok else why}", flush=True)
|
| 135 |
+
print(f"\n{MODEL} TOOL-CALL {ok_n}/{len(ITEMS)} ({100*ok_n/len(ITEMS):.0f}%)", flush=True)
|
| 136 |
+
if OUT:
|
| 137 |
+
json.dump({"model": MODEL, "score": [ok_n, len(ITEMS)], "results": results},
|
| 138 |
+
open(OUT, "w"), indent=2)
|
docs/HEAD8-RESULT.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# head8: the always-on weight class the sweep never tested
|
| 2 |
+
|
| 3 |
+
**Date:** 2026-07-24. `lm_head` and `embed_tokens` at 8-bit instead of 4-bit,
|
| 4 |
+
on top of the shared8 deploy model. Improves every measured category for
|
| 5 |
+
+0.53 GB. Found while investigating a numeric-corruption report
|
| 6 |
+
(`TOKENIZER-INVESTIGATION.md`) — the head was the prime suspect, was cleared,
|
| 7 |
+
and was then noticed to be untested.
|
| 8 |
+
|
| 9 |
+
## Why it was invisible
|
| 10 |
+
|
| 11 |
+
`SHARED8-RESULT.md`'s thesis: components that fire on **every token** carry the
|
| 12 |
+
quantization damage, because they get no implicit smoothing from top-k partial
|
| 13 |
+
activation. `lm_head`/`embed_tokens` are the extreme case — every token, and
|
| 14 |
+
every one of the 128,896 vocabulary rows participates in every argmax.
|
| 15 |
+
|
| 16 |
+
But the build predicate returns bare `True` for them, so they inherited the
|
| 17 |
+
4-bit default, and `tomography_sweep.py`'s variants were six layer windows plus
|
| 18 |
+
shared8. The head was never a variant. It is the same blind spot twice over:
|
| 19 |
+
REAP saliency cannot see the shared expert, and the tomography sweep could not
|
| 20 |
+
see the head.
|
| 21 |
+
|
| 22 |
+
## Method
|
| 23 |
+
|
| 24 |
+
Rather than rebuild 93 GB (the disk had no room, and a fresh build could differ
|
| 25 |
+
elsewhere), `scripts/build_head8_inplace.py` takes the deployed shared8
|
| 26 |
+
checkpoint, **hardlinks the 18 untouched shards**, and rewrites only the two
|
| 27 |
+
holding the head tensors — 5.7 GB of new bytes, ~2 minutes. Shared inodes prove
|
| 28 |
+
nothing outside those two tensors changed.
|
| 29 |
+
|
| 30 |
+
The 8-bit weights are quantized from the **BF16 base**, not from the student's
|
| 31 |
+
existing 4-bit head. This matters — see the failure below.
|
| 32 |
+
|
| 33 |
+
## Result (ΔNLL vs shared8, 500 held-out prompts, negative = better)
|
| 34 |
+
|
| 35 |
+
| category | BF16 | shared8 | **+head8** | ΔNLL |
|
| 36 |
+
|---|---|---|---|---|
|
| 37 |
+
| agentic | 6.796 | 6.921 | **6.896** | −0.0037 |
|
| 38 |
+
| coding | 8.016 | 6.488 | **6.363** | −0.0194 |
|
| 39 |
+
| general_instruction | 6.184 | 5.115 | **5.082** | −0.0064 |
|
| 40 |
+
| reasoning_math | 2.472 | 2.485 | **2.482** | −0.0012 |
|
| 41 |
+
| tool_use | 43.855 | 27.177 | **26.544** | −0.0236 |
|
| 42 |
+
| **OVERALL** | 6.650 | 5.930 | **5.880** | **−0.0086** |
|
| 43 |
+
|
| 44 |
+
Every category improves, no regressions — the same clean sweep shared8 produced,
|
| 45 |
+
at half the cost. Magnitude is ~9% of shared8's −0.100, proportionate given
|
| 46 |
+
shared8 touched 177 modules and this touches one tensor pair. Largest gains are
|
| 47 |
+
`tool_use` and `coding`, the two most token-precision-sensitive categories,
|
| 48 |
+
mirroring shared8's pattern.
|
| 49 |
+
|
| 50 |
+
**Cost:** +0.53 GB (1.06B params, 4.5 → 8.5 bpw). 93 → 93.5 GB, ~4.68 bpw.
|
| 51 |
+
|
| 52 |
+
## The failed first attempt (kept — it measures something useful)
|
| 53 |
+
|
| 54 |
+
The first build dequantized the student's existing **4-bit** head and
|
| 55 |
+
requantized it to 8-bit. Same tensors, same bit-width, same +0.53 GB:
|
| 56 |
+
|
| 57 |
+
| | OVERALL | vs shared8 |
|
| 58 |
+
|---|---|---|
|
| 59 |
+
| shared8 (4-bit head) | 5.930 | — |
|
| 60 |
+
| head8 **from BF16** | **5.880** | **−0.0086** |
|
| 61 |
+
| head8 **from the 4-bit head** | 6.010 | **+0.013** |
|
| 62 |
+
|
| 63 |
+
The two runs bracket the baseline in opposite directions, differing *only* in
|
| 64 |
+
source precision. Quantization is one-way: the wider container stored
|
| 65 |
+
already-degraded values and added a second rounding pass. Written up as
|
| 66 |
+
`FINDINGS.md` §8a, because it generalizes — converting a released quantized
|
| 67 |
+
checkpoint to another format is a quantization of an already-damaged model, not
|
| 68 |
+
a requantization of the original.
|
| 69 |
+
|
| 70 |
+
It also yields a free invariant: **more bits cannot beat the source**, so a
|
| 71 |
+
precision *increase* that measures worse means the source is wrong, not the
|
| 72 |
+
target. That is what caught this one.
|
| 73 |
+
|
| 74 |
+
## How low can the head go?
|
| 75 |
+
|
| 76 |
+
`reap_stream/diag_head_digits.py` measures quantization perturbation on the
|
| 77 |
+
digit rows against the tightest inter-digit argmax margin (0.8463 in BF16):
|
| 78 |
+
|
| 79 |
+
| bits | gs | ratio | cosine | size |
|
| 80 |
+
|---|---|---|---|---|
|
| 81 |
+
| 8 | 64 | 0.007 | 0.999986 | 1.12 GB |
|
| 82 |
+
| 6 | 64 | 0.029 | 0.999764 | 0.86 GB |
|
| 83 |
+
| 4 | 64 | 0.121 | 0.995886 | 0.59 GB |
|
| 84 |
+
| 3 | 64 | 0.254 | — | 0.51 GB |
|
| 85 |
+
| 2 | 64 | 0.525 | — | 0.36 GB |
|
| 86 |
+
|
| 87 |
+
The ratio roughly doubles per bit removed; at 2-bit the noise exceeds half the
|
| 88 |
+
distance between adjacent digit rows. BF16 is not worth it — 8-bit is already
|
| 89 |
+
at 0.7% of the margin with cosine 0.999986, and BF16 costs +0.99 GB beyond
|
| 90 |
+
8-bit to remove an error nothing can resolve.
|
| 91 |
+
|
| 92 |
+
**Policy for the 64GB recipe:** head at 8-bit; never below 4-bit. Now stated
|
| 93 |
+
explicitly in the predicate rather than inherited from the default, so lowering
|
| 94 |
+
the global bit-width cannot drag it down silently.
|
| 95 |
+
|
| 96 |
+
## Status
|
| 97 |
+
|
| 98 |
+
- `scripts/build_student_shared8.py` predicate updated — head at 8-bit.
|
| 99 |
+
- **Promoted**: deployed at
|
| 100 |
+
`~/.lmstudio/models/truemod/Step-3.7-p15-vblend-shared8-head8` (symlinked into
|
| 101 |
+
`models/`), loaded at 262144 context / parallel 4. Note LM Studio does not
|
| 102 |
+
inherit per-model context or sampler settings across a checkpoint swap.
|
| 103 |
+
- **Not uploaded to HF** — blocked by the private-repo storage limit. The Hub
|
| 104 |
+
still carries the 4-bit-head weights.
|
| 105 |
+
- Multimodal NLL (250 held-out images) **not yet re-run** — shared8 was
|
| 106 |
+
validated on both instruments and this has only text PPL so far.
|
| 107 |
+
|
| 108 |
+
## Artifacts
|
| 109 |
+
|
| 110 |
+
`artifacts/ppl-shared8-head8-500.json`,
|
| 111 |
+
`artifacts/ppl-head8-requant-from-4bit-500.json`,
|
| 112 |
+
`artifacts/ppl-p15-vblend-shared8-500.json` (baseline),
|
| 113 |
+
`scripts/build_head8_inplace.py`, `reap_stream/diag_head_digits.py`.
|
docs/REAM-RESULT.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# REAM: built, measured, rejected — and a clean case of PPL over-crediting
|
| 2 |
+
|
| 3 |
+
**Date:** 2026-07-25. Merging low-saliency experts (instead of pruning them)
|
| 4 |
+
was built end-to-end and evaluated. **Verdict: rejected as a deploy candidate.**
|
| 5 |
+
Its large perplexity gain turned out to be a smoothing artifact with no
|
| 6 |
+
capability behind it — the most useful thing the exercise produced.
|
| 7 |
+
|
| 8 |
+
## What was built
|
| 9 |
+
|
| 10 |
+
`reap_stream/ream.py` (+ `test_ream.py`, 6/6 unit tests) and
|
| 11 |
+
`scripts/build_student_ream.py`. Each pruned expert is merged into its
|
| 12 |
+
most-similar kept expert (router-row cosine), and the kept expert becomes the
|
| 13 |
+
saliency-weighted average of itself plus everything it absorbed. Router matrix
|
| 14 |
+
and bias are merged the same way. Output count is identical to pruning (245/288)
|
| 15 |
+
— REAM changes the kept experts' *values*, not the count.
|
| 16 |
+
|
| 17 |
+
Built clean on the first end-to-end run: 1806 experts merged (43×42), 4.683
|
| 18 |
+
bpw, 93 GB, coherent and correct generation (17×23=391, exact Fibonacci, exact
|
| 19 |
+
tool-call JSON, correct Rayleigh-scattering prose). The merge code, which had
|
| 20 |
+
only seen synthetic tensors, worked on the real 198B model.
|
| 21 |
+
|
| 22 |
+
## The trap: perplexity said it was a big win
|
| 23 |
+
|
| 24 |
+
500 held-out prompts, vs the shared8-head8 deploy model:
|
| 25 |
+
|
| 26 |
+
| category | shared8-head8 ppl | REAM ppl | ΔNLL |
|
| 27 |
+
|---|---|---|---|
|
| 28 |
+
| tool_use | 26.54 | **15.57** | −0.533 |
|
| 29 |
+
| coding | 6.36 | **4.66** | −0.312 |
|
| 30 |
+
| general_instruction | 5.08 | **4.10** | −0.215 |
|
| 31 |
+
| agentic | 6.90 | **5.95** | −0.148 |
|
| 32 |
+
| reasoning_math | 2.48 | 2.50 | **+0.007** |
|
| 33 |
+
| **OVERALL** | 5.880 | **4.843** | **−0.194** |
|
| 34 |
+
|
| 35 |
+
−0.194 NLL overall is ~2× the shared8 + head8 gains *combined*, from a weight
|
| 36 |
+
merge, on a model measured to have no exploitable slack. Two tells said "don't
|
| 37 |
+
believe it": the magnitude was implausible, and the gains scaled with category
|
| 38 |
+
entropy while **reasoning_math — the one category with a single right answer —
|
| 39 |
+
did not improve.** That is the textbook signature of smoothing: averaging expert
|
| 40 |
+
weights flattens the output distribution, which perplexity rewards on
|
| 41 |
+
hedge-friendly text without improving the model.
|
| 42 |
+
|
| 43 |
+
## The arbiter: exact-answer accuracy
|
| 44 |
+
|
| 45 |
+
Perplexity cannot distinguish a real gain from smoothing, so both models were
|
| 46 |
+
scored right/wrong on 24 exact-answer items (14 multi-step arithmetic + 10
|
| 47 |
+
factual) where smoothing cannot help — same scorer, temperature 0, parallel 1.
|
| 48 |
+
`scripts/accuracy_eval.py`, results in `artifacts/acc_{ream,shared8}.json`.
|
| 49 |
+
|
| 50 |
+
| model | overall | math | factual | PPL |
|
| 51 |
+
|---|---|---|---|---|
|
| 52 |
+
| **shared8-head8 (prune)** | **24/24** | 14/14 | 10/10 | 5.880 |
|
| 53 |
+
| REAM (merge) | 23/24 | 13/14 | 10/10 | 4.843 |
|
| 54 |
+
|
| 55 |
+
**The −0.194 PPL advantage bought zero accuracy** — REAM is one item behind
|
| 56 |
+
(noise). Every prediction held: PPL said math wouldn't improve, and math
|
| 57 |
+
accuracy is level; the PPL gains were all in high-entropy categories, exactly
|
| 58 |
+
where a flatter distribution lowers perplexity for free.
|
| 59 |
+
|
| 60 |
+
## Follow-up: the agentic categories, tested directly (the important one)
|
| 61 |
+
|
| 62 |
+
The accuracy test above was math + factual -- the category where PPL predicted
|
| 63 |
+
no gain. But REAM's *biggest* PPL gains were tool_use (-0.53) and coding, which
|
| 64 |
+
that test did not cover. So it was re-run on 15 tool-call items scored right/
|
| 65 |
+
wrong: correct function name (from a provided catalog) AND correct extracted
|
| 66 |
+
arguments -- exactly what smoothing cannot fake. `scripts/toolcall_eval.py`,
|
| 67 |
+
`artifacts/tc_{ream,shared8}.json`.
|
| 68 |
+
|
| 69 |
+
| test | shared8-head8 (prune) | REAM (merge) |
|
| 70 |
+
|---|---|---|
|
| 71 |
+
| tool-call accuracy | **14/15** | **14/15** |
|
| 72 |
+
| tool_use *perplexity* | 26.5 | 15.6 (**-0.53 NLL**) |
|
| 73 |
+
|
| 74 |
+
**A 41% lower perplexity on tool_use converted to zero tool-call capability --
|
| 75 |
+
a dead tie.** This is the strongest case against PPL in the whole project: its
|
| 76 |
+
largest and most agentic-relevant signal (-0.53 NLL on the exact category that
|
| 77 |
+
matters for agent work) was worth nothing in practice. If PPL were trustworthy
|
| 78 |
+
anywhere it would be where its signal is biggest; that is precisely where it
|
| 79 |
+
most oversold.
|
| 80 |
+
|
| 81 |
+
## Conclusions
|
| 82 |
+
|
| 83 |
+
1. **Pruning stays.** REAM is not a deploy candidate. On this prune-resistant,
|
| 84 |
+
flat-saliency model, merging the least-salient experts produces a mushier
|
| 85 |
+
model that scores better on perplexity and no better on task accuracy -- including tool-call/agentic tasks, tested directly.
|
| 86 |
+
2. **This is the session's central lesson, demonstrated.** PPL moved −0.194
|
| 87 |
+
while real capability was flat-to-worse. Trusting the perplexity number would
|
| 88 |
+
have shipped a worse model as an upgrade. Perplexity is a proxy; on any
|
| 89 |
+
quality change, confirm with a non-PPL arbiter before promoting.
|
| 90 |
+
3. **The co-occurrence gate would have predicted this** (docs/FINDINGS.md §10):
|
| 91 |
+
flat saliency + prune-resistance implies merge partners are near-orthogonal,
|
| 92 |
+
so blending them averages away specialization. We skipped that gate to
|
| 93 |
+
measure directly; the direct measurement agrees.
|
| 94 |
+
|
| 95 |
+
## Kept
|
| 96 |
+
|
| 97 |
+
Build + eval code retained — REAM is a correct, working method, just not a win
|
| 98 |
+
*here*. It could still help a genuinely redundant MoE (one with a real
|
| 99 |
+
low-saliency tail), and `assign_merges` accepts a co-occurrence matrix for a
|
| 100 |
+
principled partner choice when that data exists. Artifacts:
|
| 101 |
+
`artifacts/ppl-ream-shared8-head8-500.json`, `artifacts/acc_ream.json`,
|
| 102 |
+
`artifacts/acc_shared8.json`, `scripts/accuracy_eval.py`, `reap_stream/ream.py`.
|
| 103 |
+
The 93 GB REAM checkpoint can be deleted (rebuildable from
|
| 104 |
+
`scripts/build_student_ream.py`).
|
docs/TOKENIZER-INVESTIGATION.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Tokenizer investigation: no defect found; digit fragmentation is by design
|
| 2 |
+
|
| 3 |
+
**Date:** 2026-07-24. Chasing numeric corruption in LM Studio led through a
|
| 4 |
+
wrong diagnosis and a reverted "fix". Conclusion: **there is no tokenizer bug.**
|
| 5 |
+
The build script's tokenizer is correct, single-digit number tokenization is
|
| 6 |
+
Step-3.7's intended behaviour, and the numeric fragility it causes is a property
|
| 7 |
+
of the model rather than something to repair in the checkpoint.
|
| 8 |
+
|
| 9 |
+
Recorded because the wrong version of this was believed for several hours and
|
| 10 |
+
acted on, including edits to four build scripts and two pushes to the Hub.
|
| 11 |
+
|
| 12 |
+
## The claim that was wrong
|
| 13 |
+
|
| 14 |
+
`processor.save_pretrained(out_dir)` writes a different `tokenizer.json` than
|
| 15 |
+
the base checkpoint ships — 2 pretokenizer stages instead of 4, dropping a
|
| 16 |
+
`\p{N}{1,3}` digit-grouping rule. That looked like the build silently corrupting
|
| 17 |
+
the tokenizer, and it fit the symptom: numbers tokenised one digit per token,
|
| 18 |
+
so a number arrives as loose fragments with nothing binding it into a unit.
|
| 19 |
+
|
| 20 |
+
Restoring the base file even *fixed the numeric cases*:
|
| 21 |
+
|
| 22 |
+
| case | built student | base tokenizer |
|
| 23 |
+
|---|---|---|
|
| 24 |
+
| `kill process ID 18452` | 0/12 | **6/6** |
|
| 25 |
+
| `sed -n '447,495p'` | corrupted to `'4.4.7,4.9.5p'` | **6/6** |
|
| 26 |
+
|
| 27 |
+
## Why it was wrong
|
| 28 |
+
|
| 29 |
+
The base `tokenizer.json` **cannot be used standalone.** Read directly by the
|
| 30 |
+
`tokenizers` library — which is what LM Studio's MLX backend does — it is not
|
| 31 |
+
merely imperfect, it is broken:
|
| 32 |
+
|
| 33 |
+
```
|
| 34 |
+
built student (deployed) base tokenizer.json
|
| 35 |
+
'hello world' -> ['hello','Ġworld'] OK ['hell','ow','orld'] 'helloworld'
|
| 36 |
+
'id is 5' -> ['id','Ġis','Ġ','5'] OK ['idis','5'] 'idis5'
|
| 37 |
+
'你好世界' -> ['ä½łå¥½','ä¸ĸçķĮ'] OK [] ''
|
| 38 |
+
'中文 with english' -> OK ['wit','heng','lish'] 'withenglish'
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
**Chinese produces zero tokens**, and English word boundaries land in the wrong
|
| 42 |
+
places. This is why `tokenizer_config.json` carries `fix_mistral_regex: true`:
|
| 43 |
+
`transformers` *reconstructs* a working tokenizer at load and never reads the
|
| 44 |
+
shipped file. `save_pretrained` then serialises that working reconstruction into
|
| 45 |
+
a standalone file — which is **correct behaviour, not corruption.** The build
|
| 46 |
+
script was fixing an unusable upstream artifact, not breaking a good one.
|
| 47 |
+
|
| 48 |
+
Deploying the base tokenizer broke generation outright: output came back as
|
| 49 |
+
`Thefindcommandfailedbecauseofspacesinthepath.`, shell commands failed on
|
| 50 |
+
unquoted paths, and the agent stalled. Reverted locally and on the Hub.
|
| 51 |
+
|
| 52 |
+
## Consequence
|
| 53 |
+
|
| 54 |
+
Single-digit number tokenization is **genuine and intended**. Step-3.7 emits one
|
| 55 |
+
token per digit (ids 19–28) and a standalone space token (223), so numbers carry
|
| 56 |
+
no token-level redundancy: one mis-sampled or mis-copied token corrupts an entire
|
| 57 |
+
number, while prose absorbs it. That is the real explanation for
|
| 58 |
+
"numbers are wrong, everything else is perfect", and it is not fixable in the
|
| 59 |
+
checkpoint.
|
| 60 |
+
|
| 61 |
+
The model's own chain of thought describing input as *"process ID 1 8 4 5"* is
|
| 62 |
+
therefore an accurate report of what it receives, not a hallucination.
|
| 63 |
+
|
| 64 |
+
## How the mistake happened
|
| 65 |
+
|
| 66 |
+
Three failures worth remembering:
|
| 67 |
+
|
| 68 |
+
1. **Evidence collected and dismissed.** The space-dropping round-trip
|
| 69 |
+
(`'The order id is 2456.'` → `'Theorderidis2456.'`) was measured early and
|
| 70 |
+
written off as a `transformers`-path quirk.
|
| 71 |
+
2. **Verification that could not fail.** The "fix" was checked by decoding
|
| 72 |
+
tokens *individually* (`['sed','-n','447',',','495','p']`), which
|
| 73 |
+
structurally cannot reveal a missing space.
|
| 74 |
+
3. **Only testing English.** One Chinese string would have exposed it
|
| 75 |
+
immediately — `你好世界` → `[]`. Step-3.7 is a Chinese-English model; that
|
| 76 |
+
should have been in the first test, not the last.
|
| 77 |
+
|
| 78 |
+
**Rule:** validate any tokenizer change with a full-string round-trip through
|
| 79 |
+
`mlx_lm`'s streaming detokenizer, in **both** languages.
|
| 80 |
+
|
| 81 |
+
## Guardrails: removed
|
| 82 |
+
|
| 83 |
+
An earlier version of this document instructed all four builders to copy the
|
| 84 |
+
base tokenizer over `save_pretrained`'s output. Those calls have been removed
|
| 85 |
+
from `build_student{,_shared8,_tiered}.py` and `tomography_sweep.py`, and each
|
| 86 |
+
now carries a comment explaining why the base file must not be copied.
|
| 87 |
+
|
| 88 |
+
## Ruled out along the way
|
| 89 |
+
|
| 90 |
+
Useful negatives, each measured:
|
| 91 |
+
|
| 92 |
+
- **Quantized output head.** Digit rows carry 1.033x the all-row reconstruction
|
| 93 |
+
error; perturbation is 12% of the tightest inter-digit argmax margin.
|
| 94 |
+
`reap_stream/diag_head_digits.py`.
|
| 95 |
+
- **REAP / degraded digit copying.** Teacher-forced probes, no sampler in path,
|
| 96 |
+
rank the correct next digit **first** at p = 0.91–0.9996.
|
| 97 |
+
`reap_stream/diag_digit_logits.py`.
|
| 98 |
+
- **`repetition_penalty`.** A/B at 1.02 vs 1.0: 0/8 and 1/8 vs 0/8 and 0/8.
|
| 99 |
+
(LM Studio's per-model UI sampler settings silently override
|
| 100 |
+
`generation_config.json` — pin sampler fields explicitly when testing.)
|
| 101 |
+
- **`reasoning_effort`.** `Reasoning: medium` fixed one case (0/12 → 12/12) and
|
| 102 |
+
made another much worse. Not a mitigation.
|
| 103 |
+
- **SFT / QLoRA.** Rejected: the capability is intact and no existing instrument
|
| 104 |
+
could verify success or catch regression.
|
| 105 |
+
- **PPL/NLL confounding.** Claimed at one point, false: `transformers` rebuilds
|
| 106 |
+
the tokenizer at load, so all evaluations used identical tokenisation
|
| 107 |
+
(identical `n_tokens: 383130`; 0-token diff over 40 held-out rows).
|
| 108 |
+
|
| 109 |
+
## Mitigation
|
| 110 |
+
|
| 111 |
+
Since the fragility is intrinsic, the mitigation is operational:
|
| 112 |
+
|
| 113 |
+
- `scripts/numeric_guard.py` — signature checks, provenance, sed bounds, and an
|
| 114 |
+
AST check for floats in integer-only positions (valid Python that fails only
|
| 115 |
+
at runtime).
|
| 116 |
+
- `~/.pi/agent/extensions/numeric-guard.ts` — blocks corrupted tool calls with
|
| 117 |
+
an actionable reason. Errors are surfaced and the call allowed through, so the
|
| 118 |
+
guard can never itself stall a turn. Retrying does not help — output is
|
| 119 |
+
identical every time; telling the model *what* was wrong does.
|
| 120 |
+
|
| 121 |
+
## Still open
|
| 122 |
+
|
| 123 |
+
- **The echo case.** `2456, 1337, 495` → `2 4 5 6, 1 3 3 7, 4 9 5`, 0/12, never
|
| 124 |
+
resolved.
|
| 125 |
+
- **Whether upstream Step-3.7 behaves the same.** No unpruned Step-3.7 exists
|
| 126 |
+
locally as a loadable quant; the Hub's 148B is a *different* REAP (212/288
|
| 127 |
+
experts, nvfp4), so it is not a control.
|
scripts/numeric_guard.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Catch Step-3.7's numeric corruption before a tool call executes.
|
| 3 |
+
|
| 4 |
+
Meant to be copied into an agent harness as a pre-tool-use check, not run as
|
| 5 |
+
part of the REAP pipeline. See the HF README's "numbers corrupted inside the
|
| 6 |
+
reasoning block" section for the measured behaviour this defends against.
|
| 7 |
+
|
| 8 |
+
Why code and not the model: the failure survives self-review. In one traced
|
| 9 |
+
run the model re-read the question 20+ times and reaffirmed the wrong value
|
| 10 |
+
each time, because by then its own output was the strongest evidence in
|
| 11 |
+
context. A checker that does not share that context does not share the bias.
|
| 12 |
+
|
| 13 |
+
Three checks, cheapest first:
|
| 14 |
+
|
| 15 |
+
scan_signature(text) known corruption shapes -- 4.4.7, "1 8 4 5",
|
| 16 |
+
2,4,5,6,1,3,3,7. No source needed.
|
| 17 |
+
check_provenance(cmd,src) every numeral in cmd must appear in src. Catches
|
| 18 |
+
invented numbers that happen to look well-formed.
|
| 19 |
+
check_sed_bounds(cmd) sed line ranges against the real file length.
|
| 20 |
+
|
| 21 |
+
Usage:
|
| 22 |
+
from numeric_guard import guard
|
| 23 |
+
problems = guard(proposed_command, source_text=file_contents)
|
| 24 |
+
if problems: ...retry instead of executing...
|
| 25 |
+
|
| 26 |
+
$ python3 scripts/numeric_guard.py "sed -n '4.4.7,4.9.5p' src/aero.py"
|
| 27 |
+
"""
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import re
|
| 31 |
+
import sys
|
| 32 |
+
from pathlib import Path
|
| 33 |
+
|
| 34 |
+
# 1.2.0 / 4.4.7 -- a numeral carrying more interior dots than a decimal can.
|
| 35 |
+
# Version-like strings are legitimate in many contexts, so this is reported
|
| 36 |
+
# rather than treated as certainly wrong; see guard()'s `strict` flag.
|
| 37 |
+
MULTI_DOT = re.compile(r"(?<![\w.])\d+\.\d+\.\d+(?![\w.])")
|
| 38 |
+
|
| 39 |
+
# "1 8 4 5" / "2 4 5 6" -- three or more single digits separated by spaces.
|
| 40 |
+
SPACED_DIGITS = re.compile(r"(?<!\d)\d(?: \d){2,}(?!\d)")
|
| 41 |
+
|
| 42 |
+
# "2,4,5,6,1,3,3,7" -- single digits comma-separated. Distinguished from a
|
| 43 |
+
# real list like "2456, 1337" by every element being exactly one digit.
|
| 44 |
+
COMMA_DIGITS = re.compile(r"(?<!\d)\d(?:,\d){3,}(?!\d)")
|
| 45 |
+
|
| 46 |
+
NUMERALS = re.compile(r"\d+")
|
| 47 |
+
SED_RANGE = re.compile(r"sed\s+-n\s+['\"]?(\d+),(\d+)p['\"]?\s+(\S+)")
|
| 48 |
+
|
| 49 |
+
# 5_1_8_4_0_0 -- underscore inserted between single digits. Python's numeric
|
| 50 |
+
# separators make `_` a legitimate delimiter, which primes exactly this. Note
|
| 51 |
+
# `5_1_8_4_0_0.0 == 518400.0` is True, so in Python this is cosmetic rather
|
| 52 |
+
# than wrong -- but it is a hard error in JSON, YAML and shell.
|
| 53 |
+
# Trailing `.` must be allowed -- these appear as float literals (5_1_8_4_0_0.0).
|
| 54 |
+
UNDERSCORE_NUM = re.compile(r"(?<![\w.])\d[\d_]*_[\d_]*\d(?!\w)")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _bad_underscore_grouping(lit: str) -> bool:
|
| 58 |
+
"""True unless the literal uses conventional 3-digit grouping.
|
| 59 |
+
|
| 60 |
+
Legitimate: 5_000_000, 1_234, 12_345_678 -- every group after the first is
|
| 61 |
+
exactly 3 digits and the first is 1-3. Anything else (5_1_8_4, 1_00_000)
|
| 62 |
+
is the corruption signature.
|
| 63 |
+
"""
|
| 64 |
+
groups = lit.split("_")
|
| 65 |
+
if any(g == "" for g in groups):
|
| 66 |
+
return True
|
| 67 |
+
return not (1 <= len(groups[0]) <= 3 and all(len(g) == 3 for g in groups[1:]))
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def scan_signature(text: str, strict: bool = False) -> list[str]:
|
| 71 |
+
"""Known corruption shapes. No source text required."""
|
| 72 |
+
out = []
|
| 73 |
+
for m in SPACED_DIGITS.finditer(text):
|
| 74 |
+
out.append(f"digits split by spaces: {m.group(0)!r}")
|
| 75 |
+
for m in COMMA_DIGITS.finditer(text):
|
| 76 |
+
out.append(f"digits split by commas: {m.group(0)!r}")
|
| 77 |
+
for m in UNDERSCORE_NUM.finditer(text):
|
| 78 |
+
if _bad_underscore_grouping(m.group(0)):
|
| 79 |
+
out.append(f"digits split by underscores: {m.group(0)!r} "
|
| 80 |
+
f"(valid Python, but wrong in JSON/YAML/shell)")
|
| 81 |
+
for m in MULTI_DOT.finditer(text):
|
| 82 |
+
label = "malformed number" if strict else "version-like numeral (check)"
|
| 83 |
+
out.append(f"{label}: {m.group(0)!r}")
|
| 84 |
+
return out
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def check_provenance(command: str, source: str, min_len: int = 3) -> list[str]:
|
| 88 |
+
"""Every numeral of >=min_len digits in `command` must occur in `source`.
|
| 89 |
+
|
| 90 |
+
min_len avoids flagging small incidental numbers (-9, exit codes, 0/1).
|
| 91 |
+
Numbers the model legitimately *computed* will also trip this, so treat
|
| 92 |
+
hits as "confirm before running", not as proof of corruption.
|
| 93 |
+
"""
|
| 94 |
+
return [f"numeral {n!r} does not appear in the source"
|
| 95 |
+
for n in {m.group(0) for m in NUMERALS.finditer(command)}
|
| 96 |
+
if len(n) >= min_len and n not in source]
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def check_sed_bounds(command: str, root: str | Path = ".") -> list[str]:
|
| 100 |
+
"""sed line ranges against the file's real length."""
|
| 101 |
+
out = []
|
| 102 |
+
for start, end, path in SED_RANGE.findall(command):
|
| 103 |
+
p = Path(root) / path
|
| 104 |
+
if not p.exists():
|
| 105 |
+
continue
|
| 106 |
+
n = sum(1 for _ in p.open(errors="replace"))
|
| 107 |
+
s, e = int(start), int(end)
|
| 108 |
+
if s > e:
|
| 109 |
+
out.append(f"sed range {s},{e} is inverted")
|
| 110 |
+
if s > n or e > n:
|
| 111 |
+
out.append(f"sed range {s},{e} exceeds {path} ({n} lines)")
|
| 112 |
+
return out
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def check_python_int_positions(code: str) -> list[str]:
|
| 116 |
+
"""Float literals where Python requires an int: slice indices, range().
|
| 117 |
+
|
| 118 |
+
This is the corruption's most dangerous form, because one inserted '.'
|
| 119 |
+
yields a *valid* float that no shape-based check can distinguish from a
|
| 120 |
+
legitimate one. `content[idx-5:idx+1.5]` parses fine and fails only at
|
| 121 |
+
runtime with "slice indices must be integers" -- the exact loop seen in
|
| 122 |
+
the wild. Catching it needs the syntactic position, not the literal.
|
| 123 |
+
"""
|
| 124 |
+
import ast
|
| 125 |
+
try:
|
| 126 |
+
tree = ast.parse(code)
|
| 127 |
+
except SyntaxError:
|
| 128 |
+
return []
|
| 129 |
+
|
| 130 |
+
out = []
|
| 131 |
+
|
| 132 |
+
def floats_in(node):
|
| 133 |
+
return [n for n in ast.walk(node)
|
| 134 |
+
if isinstance(n, ast.Constant) and isinstance(n.value, float)]
|
| 135 |
+
|
| 136 |
+
for node in ast.walk(tree):
|
| 137 |
+
if isinstance(node, ast.Subscript):
|
| 138 |
+
for f in floats_in(node.slice):
|
| 139 |
+
out.append(f"float {f.value!r} used as a slice index "
|
| 140 |
+
f"(line {f.lineno}) -- fails at runtime")
|
| 141 |
+
elif isinstance(node, ast.Call) and getattr(node.func, "id", "") == "range":
|
| 142 |
+
for arg in node.args:
|
| 143 |
+
for f in floats_in(arg):
|
| 144 |
+
out.append(f"float {f.value!r} passed to range() "
|
| 145 |
+
f"(line {f.lineno}) -- fails at runtime")
|
| 146 |
+
return out
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def guard(command: str, source_text: str | None = None,
|
| 150 |
+
root: str | Path = ".", strict: bool = False,
|
| 151 |
+
as_python: bool = False) -> list[str]:
|
| 152 |
+
"""All applicable checks. Empty list means nothing suspicious."""
|
| 153 |
+
problems = scan_signature(command, strict=strict)
|
| 154 |
+
if source_text is not None:
|
| 155 |
+
problems += check_provenance(command, source_text)
|
| 156 |
+
problems += check_sed_bounds(command, root)
|
| 157 |
+
if as_python:
|
| 158 |
+
problems += check_python_int_positions(command)
|
| 159 |
+
return problems
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
if __name__ == "__main__":
|
| 163 |
+
if len(sys.argv) < 2:
|
| 164 |
+
print(__doc__.strip().split("Usage:")[-1].strip())
|
| 165 |
+
raise SystemExit(2)
|
| 166 |
+
cmd = sys.argv[1]
|
| 167 |
+
src = Path(sys.argv[2]).read_text() if len(sys.argv) > 2 else None
|
| 168 |
+
found = guard(cmd, source_text=src)
|
| 169 |
+
if not found:
|
| 170 |
+
print("ok")
|
| 171 |
+
else:
|
| 172 |
+
print(f"SUSPECT: {cmd!r}")
|
| 173 |
+
for p in found:
|
| 174 |
+
print(f" - {p}")
|
| 175 |
+
raise SystemExit(1)
|