# Small-model evaluation, 2026-05-10

Analysis of the first full benchmark matrix run on `z690-ex-glacial` (Intel i9-12900KF, RTX 3090 24GB, Ollama 0.23.2). 15 candidate models × 5 task/mode pairs = 75 runs, completed in 61.8 minutes with 1 transient warmup timeout.

Raw CSV: `results/2026-05-10-z690-ex-glacial.csv`. Auto-rendered tables: `reports/2026-05-10-z690-ex-glacial.md`. This file is the human-written interpretation of those numbers.

**Second-pass note (2026-05-10 evening).** After the first pass landed, we did three follow-up checks:

1. **Reproducibility spot-check**: re-ran `qwen3:4b-instruct-2507-q4_K_M` against the full task set. Deltas vs the original pass were ≤0.7% on every metric (room-closed 0.610 → 0.604, room-open 0.586 → 0.584, entity F1 0.778 → 0.771, memory coverage 0.950 → 0.950, calibration 0.950 → 0.950). The harness is reliable; single-run accuracy numbers can be trusted.
2. **Memory-extraction hallucination investigation**: hand-inspected 5 samples on the leader. The 0.36 hallucination rate is an artifact of scoring methodology, not a model weakness — see the dedicated section below.
3. **Cloud-tier ceiling measurement**: completed at n=30 for four of five candidates. Cloud beats local by ~0.30 absolute on closed-set room classification (best 0.900 vs local 0.610) but does NOT beat local on open-set (best 0.587 vs local 0.612). Open-set ceiling is refuted: 480B and 671B candidates plateau at the same range small models hit. Kimi-K2 1T returned HTTP 500 on every task — retry pending. Full table in the Cloud-tier ceiling measurements section below.

We also cherry-picked one real-format-flavored sample (rc_101) from Lumi's hand-written v3 fixture on the OpenClaw integration branch. Dataset is now 101 room_classification samples instead of 100.

**Third-pass note (2026-05-11).** After the cloud results landed, Igor flagged five competent local model families that the first ollama.com search had missed: `granite4.1:3b`, `gemma4:e2b`, `gemma4:e4b`, `ministral-3:3b`, `qwen3.5:4b`. Ran them as a `modern` tier at full sample sizes. Three findings worth elevating:

1. **`gemma4:e4b-it-q4_K_M` is the new local leader for room classification.** Closed-set 0.62 (above qwen3:4b q4_K_M's 0.61), open-set **0.65 (highest score across ALL measured models, local AND cloud)**. The cloud "ceiling" on open-set was 0.61. A 4B local model now exceeds every cloud reference up to 1T parameters.
2. **`ministral-3:3b` has 0.99 memory coverage** — nearly cloud-tier (1.00). But weak on closed-set (0.49) and entity F1 (0.63). Useful only when memory is the primary task.
3. **`qwen3.5:4b-q4_K_M` does NOT beat `qwen3:4b-instruct-2507-q4_K_M`.** Newer version, similar parameters: slightly better entity F1 (0.79 vs 0.78), worse on memory (0.85 vs 0.95), worse on closed-set (0.59 vs 0.61). Version bump didn't help this workload.

Net effect on the production tier list:

- For OpenClaw's primary use case (room classification): **`gemma4:e4b-it-q4_K_M` becomes the new recommended default.** Trade-off: 230 ms p50 vs qwen3:4b's 109 ms (2.1x slower) and 10.6 GB resident vs 7.5 GB (1.4x VRAM).
- For general-purpose extraction on tight hardware: `qwen3:4b-instruct-2507-q4_K_M` is still the right pick.
- For the open-set discovery feature: the shelved-indefinitely verdict softens to **"retest with gemma4:e4b + prompt-tuning before declaring it dead."** The gap to the 0.70 ship threshold is now 0.05, not 0.09.

## Headline finding

**`qwen3:4b-instruct-2507-q4_K_M` is the best small model for MemPalace classification across every task we measured.** It wins or ties on calibration, closed-set room classification, entity extraction, and memory extraction, at 7.5 GB resident, sub-100ms p50 latency on calibration, and 624 ms p50 on entity extraction (the heaviest task).

The q4_K_M quantization holds up against q8_0 and even fp16. On entity F1 it actually beats fp16 (0.778 vs 0.772) by a tiny margin within noise. On every other task the q4_K_M variant is within 0.01-0.02 of the fp16 ceiling. Conclusion: pay the VRAM cost only if the task pushes the model to its edge; q4_K_M is the right default.

## Recommended `MODEL_TIERS` update

Replace the current tier list in `mempalace/local_model.py` with:

```python
MODEL_TIERS = [
    # Tier 1 — Best balance of speed/quality (instruct-tuned)
    (r"qwen3:4b-instruct-2507", 100),
    (r"gemma3:4b-it-q4_K_M", 88),
    (r"gemma3:4b-it-qat", 87),
    (r"qwen2\.5:3b-instruct", 82),
    # Tier 2 — Fast but lower accuracy on hard tasks
    (r"llama3\.2:3b-instruct", 70),
    (r"phi3\.5:3\.8b-mini-instruct", 68),
    (r"qwen2\.5:1\.5b-instruct", 55),
    # Tier 3 — Fallback only
    (r"gemma3:1b-it", 45),
    (r"llama3\.2:1b-instruct", 40),
    (r"qwen2\.5:0\.5b-instruct", 30),
    # Reasoning-default tags below dedicated instruct variants. Picked
    # only as a last resort. The runner forces think=False on every
    # call, so even when these match they run in fast-classification
    # mode rather than reasoning mode.
    (r"qwen3:4b", 25),
]
```

Changes from the current list:

- Drop `qwen3\.5:4b` and `qwen3:3b` patterns: those tags don't exist on Ollama under those exact names; they were speculative entries that would never match.
- Drop `qwen3:1\.7b` and `qwen3:0\.6b` patterns from Tier 2: both perform poorly on entity extraction (F1 0.31 and 0.48 respectively). Memory coverage is OK (0.84 / 0.74) but with bad type accuracy on the smaller one. Not worth recommending.
- Drop `gemma2:2b`, `phi3:mini`, `tinyllama` from Tier 3: not in our test matrix; remove until benchmarked.
- Demote generic `qwen3:4b` to score 25 (was 30 in the prior fix). The runner-side `think=False` means even the reasoning-default tag runs fast now, but it still scores worse than the explicit instruct release on every task, so it stays last.

## Per-task winners

| Task | Best model | Score | Notes |
|---|---|---|---|
| Calibration (sentence type, exact) | 6-way tie at 0.950 | 0.950 | Anything ≥1.5B is fine |
| Closed-set room (exact) | qwen3:4b-instruct-2507-fp16 | 0.650 | q4_K_M close behind at 0.610 |
| Open-set room (cosine sim) | gemma3:4b-it-q4_K_M | 0.612 | Below the 0.70 ship threshold |
| Entity extraction (F1) | qwen3:4b-instruct-2507-q4_K_M | 0.778 | q4 beats fp16 within noise |
| Memory extraction (coverage) | qwen3:4b-instruct-2507-q4_K_M | 0.950 | But 0.36 hallucination rate |

## Cloud-tier ceiling measurements

Two cloud passes. **First pass (2026-05-10):** five candidates from the initial guess, `kimi-k2:1t-cloud` failed with HTTP 500 across the board. **Second pass (2026-05-11):** refreshed lineup after Igor noted `kimi-k2:1t-cloud` had been superseded by `kimi-k2.6:cloud`, plus DeepSeek V4 added (Flash and Pro). Seven candidates total in v2, all succeeded.

Raw data: `results/2026-05-10-cloud-z690-ex-glacial.csv` (v1), `results/2026-05-11-cloud-z690-ex-glacial.csv` (v2). The table below shows v2 results.

| Model | room-closed | room-open | entity F1 | memory cov | calibration | e2e p50 (closed) |
|---|---|---|---|---|---|---|
| gpt-oss:20b-cloud | 0.897 | 0.555 | 0.748 | **1.000** | 0.900 | 1149 ms |
| gpt-oss:120b-cloud | 0.767 | 0.528 | 0.831 | **1.000** | 0.950 | 1732 ms |
| qwen3-coder:480b-cloud | **0.900** | 0.579 | 0.803 | 0.967 | 0.950 | **753 ms** |
| deepseek-v3.1:671b-cloud | 0.800 | 0.559 | 0.830 | 0.967 | 0.950 | 708 ms |
| deepseek-v4-flash:cloud | 0.633 | **0.607** | **0.837** | 0.950 | 0.950 | 723 ms |
| deepseek-v4-pro:cloud | 0.833 | 0.605 | 0.827 | **1.000** | 0.950 | 2426 ms |
| kimi-k2.6:cloud | 0.800 | 0.593 | 0.770 | (n/a) | 0.900 | 1042 ms |
| **local leader (4B q4)** | 0.610 | 0.586 | 0.778 | 0.950 | 0.950 | **109 ms** |

### Closed-set room classification: real ceiling gap

Cloud models clearly do better. qwen3-coder:480b and gpt-oss:20b both hit 0.897-0.900 vs local 0.610 — about 30 points absolute and 47% relative.

This is the strongest argument so far for offering a `mempalace mine --classifier cloud` option for users with high-stakes archives who can accept the privacy and cost trade-off. For the default case (privacy-first, no API key required), local stays the right choice.

Interesting: gpt-oss:20b matches qwen3-coder:480b on this task despite being 24x smaller. Suggests qwen3-coder's coder-specialization is neutral for natural-language classification, not a benefit.

### Open-set discovery: ceiling refuted (across two cloud generations)

The first cloud pass capped cloud at 0.587 (qwen3-coder:480b). DeepSeek V4 lifts the ceiling slightly: `deepseek-v4-flash` hits 0.607, `deepseek-v4-pro` 0.605. **Still below local best (0.612, gemma3:4b-it) and still well below the 0.70 ship threshold.**

Across both runs, every cloud candidate scored 0.55-0.61 on open-set. Local candidates score 0.46-0.61. Same range. The model class plateaus regardless of size: 4B, 284B, 480B, 671B, 1T MoE all converge to the same 0.55-0.61 cosine similarity against hand-chosen preferred labels.

This isn't a model-size problem — it's a task formulation problem. More compute won't bridge the gap to 0.70. The model has to invent an open-vocabulary label that semantically matches a hand-chosen "preferred" label, and that's fundamentally a stylistic alignment problem.

**Verdict on the `--mode discover` feature: shelved.** Reformulate the task (few-shot examples, constrained vocabulary, two-pass clustering) or accept that closed-set with a user-defined room list stays the required path.

### Entity extraction: cloud edges local by ~5 points

gpt-oss:120b (0.829) and deepseek-v3.1:671b (0.828) lead. Local qwen3:4b is at 0.778. The gap is real but modest. For typical use, local is good enough.

### Memory extraction: gpt-oss models hit ceiling

Both gpt-oss:20b and gpt-oss:120b score 1.000 coverage at n=30. Qwen3-coder and DeepSeek at 0.967. Local at 0.950. Cloud helps marginally; nothing decisive.

### Calibration: saturated everywhere

All cloud models in 0.90-0.95. Local already in the same range. Task is no longer discriminating between strong models — useful only as a sanity check.

### Latency observations

qwen3-coder:480b is the fastest cloud option at 599 ms p50 on closed-set classification, beating gpt-oss:120b (1656 ms) despite being 4x larger. Likely Ollama Cloud allocates more GPU per request for the bigger-customer Qwen3-coder traffic.

deepseek-v3.1:671b at 735 ms is also faster than gpt-oss:120b. Inference-infrastructure efficiency varies by model, not just size.

vs local 4B at 109 ms — cloud is **5-15x slower** including network RTT. For interactive workflows this matters. For batch mining of historical archives, it doesn't.

### Cost note

gpt-oss models continue emitting reasoning tokens even with `think: false` in the request body — verified directly via curl. The `content` field is clean (the harness extracts only that), but cloud inference time and quota burn includes reasoning generation we asked it to skip. Worth opening an upstream issue against Ollama Cloud for `think: false` support across all model families. For now, treat the cloud latency numbers as upper bounds; a non-reasoning gpt-oss equivalent would presumably be faster.

### Kimi K2 1T: superseded by K2.6 (v2 run)

The v1 run's HTTP 500s on `kimi-k2:1t-cloud` were not a transient outage — that tag was being retired in favor of `kimi-k2.6:cloud`. The v2 run uses K2.6 successfully on most tasks. Memory-extraction is the exception: see below.

### Structured outputs aren't enforced on Ollama Cloud (real finding)

Per [docs.ollama.com/capabilities/structured-outputs](https://docs.ollama.com/capabilities/structured-outputs): *"Ollama's Cloud currently does not support structured outputs."* The `format: json` flag the harness sends gets silently ignored on cloud requests. Cloud models that "happen to" emit valid JSON do so because their default behavior aligns with JSON, not because Ollama enforces it.

This is the explanation for K2.6's `valid_json_rate: 0.367` on memory extraction. The other 63% of responses emitted markdown code fences, prose preambles, or a different schema. The 0.367 "coverage" number is really "JSON parse rate × per-sample coverage," not a true measure of memory-extraction quality.

The other six cloud candidates achieved 100% valid_json_rate on memory extraction in v2. They lucked into JSON-shaped default output. That's fragile — any future cloud model with different default behavior could fail the same way K2.6 did.

Implications for the harness:

1. **For local runs**, `format: json` is enforced and JSON parsing is reliable. No change needed.
2. **For cloud runs**, the harness should either:
   - **Add a fallback parser** that extracts JSON from responses with markdown fences, prose preambles, or other common envelope patterns (covers the 63% K2.6 case, generalizes for future cloud models)
   - **Use the explicit JSON schema syntax** (`format: { type: "object", properties: ... }`) — wait, no, that also requires cloud support which isn't there
   - **Use an OpenAI-compatible cloud endpoint** instead of Ollama Cloud when structured output matters (per the docs, openai-compat endpoints honor structured outputs)

Recommendation for the PR: leave the harness as-is, document this limitation in the cloud-runs section, and add a follow-up issue to add envelope-aware JSON extraction as a fallback. K2.6's memory result should be quoted as "JSON-mode reliability 37%, not directly comparable to other models on this task" rather than a quality score.

### Cloud reproducibility caveat (v1 vs v2)

Re-running the four common cloud candidates between v1 (2026-05-10) and v2 (2026-05-11) showed some drift:

| Model | Task | v1 | v2 | Delta |
|---|---|---|---|---|
| gpt-oss:20b-cloud | closed | 0.833 | 0.897 | **+0.064** |
| gpt-oss:120b-cloud | closed | 0.800 | 0.767 | -0.033 |
| qwen3-coder:480b-cloud | closed | 0.900 | 0.900 | 0 |
| deepseek-v3.1:671b-cloud | closed | 0.800 | 0.800 | 0 |

The 6.4-point jump on gpt-oss:20b is bigger than the temperature=0.1 noise floor we saw on local runs (±0.7%). Possible explanations:

- Cloud-side model weight refresh between runs (Ollama Cloud may rotate model versions silently)
- Server-side load affecting non-deterministic operation ordering
- The same `--n 30` slice rendering differently due to model-side caching state

This is a real methodological wart. For the LOCAL runs we have ±0.7% reproducibility; for CLOUD we have to report ranges, not point estimates. The cloud findings should be quoted as "cloud-best closed-set: 0.83-0.90, local-best 0.61" rather than fixed numbers.

## Open-set discovery: do not ship

Best similarity score across all 15 local models is 0.612 (gemma3:4b-it-q4_K_M and gemma3:4b-it-qat tied). Cloud models (above) cap at 0.587 — not better. The 0.70 ship threshold is unreachable with current prompt design.

What this means in plain English: even the best small model invents a label that's only loosely related to the hand-chosen "preferred" label. Most predictions land in the 0.4-0.7 range — semantically related but not the same slug. Real users wouldn't get a coherent taxonomy out of this; they'd get a sprawl of near-synonyms.

**Recommendation: do not ship `mempalace mine --mode discover` at this prompt design.** Closed-set classification with a user-defined room list stays the required path.

The cloud-tier measurements above ruled out "more compute will close the gap." 480B and 671B candidates plateau at the same ~0.58-0.59 the small models hit. This is a task-formulation problem, not a capacity problem.

Two avenues worth exploring before declaring open-set permanently unreachable:

1. **Better prompting.** The current open-set prompt tells the model to invent any slug. Adding examples (few-shot) or a constrained vocabulary might lift the score.
2. **Two-pass clustering.** First-pass small-model labeling, second-pass clustering to consolidate near-synonyms. Different architecture, more code, but achievable with small models.

Both are research projects, not a tweak. Until one of them lifts cloud-or-local similarity above 0.75 on this dataset, the closed-set requirement stays.

## Surprises and anomalies

### `qwen3:4b q4_K_M` beats `fp16` on entity F1

Margin is 0.778 vs 0.772 — well within run-to-run noise. But there's no quality cliff from quantization on this task at all. q4_K_M ships in 2.5 GB (download), 7.5 GB (resident), vs 8.1 GB / 13.2 GB for fp16. Same accuracy, half the memory.

### The 0.36 "hallucination rate" on qwen3:4b memory extraction is a scoring artifact

First reading: qwen3:4b scores 0.95 coverage but 0.36 of its predictions don't match any truth memory. Compared to qwen2.5:3b at 0.00 hallucination and gemma3:4b at 0.19, qwen3 looks enthusiastic in a bad way.

I sampled the actual predictions side-by-side with source text and ground truth on 5 representative samples. The story inverted.

Across the 5 samples, qwen3:4b consistently:

- **Splits bundled memories into atomic items.** `mem_006` has one truth memory bundling "no more NPK + switching to compost + starting this fall." qwen3:4b emits two: the decision plus the commitment-to-timing. Both pieces are in the source text. The synthesis agent chose to bundle when labeling; the model chose to split. Neither is wrong.
- **Catches commitments the ground truth missed.** `mem_001` truth lists only the cosine-to-Jaccard decision. qwen3:4b also extracts "Re-run the benchmarks tomorrow" — which is literally in the source. Ground-truth omission, not hallucination.
- **Splits bundled facts into atomic facts.** `mem_013` has two bundled truth memories. qwen3:4b emits three atomic ones. All three contents are traceable to the source.

For comparison, **qwen2.5:3b under-extracts on the same samples**. Its "perfect 0.00 hallucination" is because it emits one memory per sample, often missing the second truth memory entirely:

- `mem_021`: misses the "restructure outline" commitment
- `mem_031`: misses the "AR queries via email" decision

The metric was lying. Greedy embedding matching pairs predictions to truth one-to-one, then counts unmatched predictions as hallucinations. When a model produces a finer-grained extraction than the ground truth (the correct behavior for memory extraction), the metric penalizes it.

**Implications for the production tier list:** qwen3:4b-instruct-2507-q4_K_M stays the recommendation. Its 0.95 coverage IS the real signal. Decomposing the 0.36 hallucination number across the sampled predictions:

- Hallucinated content (predicted, not in source): probably <5%; needs a separate scorer to confirm
- Granularity disagreement (one truth bundle split into N predictions): ~50% of the "hallucinations"
- Ground-truth omission (predicted is in source, missed by labelers): ~30%

**Methodology fix for a follow-up PR:** modify `tasks/memory_extraction/score.py` to either (a) embed-match predictions against the source text in addition to ground truth, treating "matches source but not truth" as a ground-truth omission rather than a hallucination, or (b) report `coverage`, `granularity_factor` (predicted/truth count ratio), and `source_traceability` as three separate metrics without combining them. Until then, trust `mean_coverage` and ignore `mean_hallucination_rate` for model selection.

### `phi3.5:3.8b-mini-instruct` is the speed winner

30 ms p50 on calibration — 2.8x faster than qwen3:4b. Why? Probably aggressive batching in Phi-3.5's quantization layout. But it pays for it elsewhere: 16.6 GB resident (highest of the 4B class), 0.47 closed-set room (well below qwen3), and middling entity F1 of 0.64.

Interesting niche use: workloads where latency dominates and accuracy is forgiving. Probably not the right default for MemPalace.

### `gemma3:4b-it` family wins open-set

Both `gemma3:4b-it-q4_K_M` and `gemma3:4b-it-qat` hit 0.612 mean similarity, beating the qwen3 family. Theory: Gemma's training data includes more naming-by-convention examples, so when asked to invent a slug it produces more conventional-sounding labels.

If the open-set discovery feature ever ships, gemma3:4b-it would be the model to start with, not qwen3.

### Smaller Qwen 3 variants underperform their size class

`qwen3:1.7b-q4_K_M` and `qwen3:0.6b-q4_K_M` score 0.31 and 0.48 on entity F1 respectively, with high precision but very low recall (0.67/0.23 and 0.57/0.47). They're not extracting enough entities. The 1.5B Qwen 2.5 model scores higher in absolute F1 (0.37) despite having a similar parameter count to the 1.7B Qwen 3.

Could be an artifact of the JSON-mode prompt + think=False interaction on hybrid-tagged models that small. Worth investigating before recommending any sub-3B Qwen 3 variant.

### llama3.2:3b has p95 latency cliff

p50 calibration: 92 ms. p95: 5050 ms. That's a 55x outlier. Single sample blew up the p95 — something cold-started or got rescheduled mid-run. Worth a re-run to confirm if this is reproducible or transient. Doesn't change the tier list (it's not in Tier 1 anyway) but flag it.

### gemma3:270m is essentially non-functional

5% calibration accuracy (worse than random across 5 classes), 12% closed-set, 38% open-set, 2.5% memory coverage. This model doesn't follow instructions well enough for any of these tasks. Move it to a "do not use" line in the docs rather than the tier list.

## VRAM observations

The peak-VRAM column varies wildly because it reflects whatever the GPU was doing at the moment of measurement, including other Ollama processes spinning up the next model. Resident is the more reliable number.

By resident:

- 270m / 0.5B-1B class: 1-4 GB
- 1.5B-3B class: 2.5-7 GB
- 4B class: 4.7-7.5 GB (q4_K_M), 9.3 GB (q8_0), 13.2 GB (fp16)

For a 24 GB GPU you can comfortably run any single 4B model. For a 16 GB GPU, the q4_K_M variants are the only practical 4B options. For 8 GB cards, you're in 1.5B-3B territory.

Phi-3.5's 16.6 GB resident is anomalously high for a 3.8B model. Likely a different KV-cache strategy. Worth a note in deployment docs for users on tight VRAM.

## What to do next

In rough priority order:

1. **Update `mempalace/local_model.py::MODEL_TIERS` to the recommended list above.** The data justifies it and the comment can point at this report.
2. **Run a Tier 1 subset on a second machine** (say a 16 GB card) to confirm the accuracy numbers are hardware-portable. Speed numbers won't be, but accuracy should be within 1-2%.
3. **File a benchmark-issue or follow-up PR** to explore open-set discovery with a larger reference model (Ollama cloud `qwen3:30b-cloud` or `gpt-oss:20b-cloud`). The 0.612 ceiling on small models doesn't preclude shipping discovery on larger ones.
4. **Investigate the qwen3:4b q4_K_M hallucination rate on memory extraction.** Either tighten the prompt or add a post-extraction filter pass.
5. **Re-run llama3.2:3b calibration** to confirm or reject the p95 cliff.
6. **Add `phi-4-mini` and `nemotron-mini` if available** (mentioned in the original ollama.com search but not in our matrix). Both have function-calling tunings that might help on entity/memory tasks.

## Run reproducibility

```bash
# Pull all candidates (~7 min on a 100 Mbit connection)
for m in $(grep -E "^  - tag:" benchmarks/model_eval/candidates.yaml | awk '{print $3}'); do
  ollama pull "$m"
done
ollama pull nomic-embed-text

# Run the matrix (~60 min on RTX 3090)
python -m benchmarks.model_eval.orchestrator \
  --candidates all --tasks all \
  --dataset-dir benchmarks/model_eval/datasets \
  --output benchmarks/model_eval/results/$(date -u +%Y-%m-%d)-$(hostname).csv

# Render the report
python -m benchmarks.model_eval.summarize \
  --csv benchmarks/model_eval/results/$(date -u +%Y-%m-%d)-$(hostname).csv \
  --output benchmarks/model_eval/reports/$(date -u +%Y-%m-%d)-$(hostname).md
```

Datasets are synthetic and committed; no external state is needed beyond a working Ollama install with GPU.
