c3749daf48
Tests / test-linux (3.13) (push) Failing after 0s
Tests / test-linux (3.11) (push) Failing after 1s
Tests / lint (push) Failing after 0s
Tests / test-linux (3.9) (push) Failing after 1s
Docker / build (push) Failing after 1s
Docker / build-gpu (push) Failing after 2s
Tests / test-windows (push) Has been cancelled
Tests / test-macos (push) Has been cancelled
238 lines
13 KiB
Markdown
238 lines
13 KiB
Markdown
# MemPalace small-model evaluation harness
|
|
|
|
Evaluates ≤4B-parameter Ollama models (plus optional cloud reference models) on MemPalace's classification and extraction tasks. Outputs accuracy, latency (TTFT, TPS, e2e p50/p95), and VRAM per `(model, task, mode)` triple. Replaces vibe-based model selection with data.
|
|
|
|
If you want to validate the findings on your own hardware, this README walks you end-to-end. The whole local matrix takes ~60 min on an RTX 3090.
|
|
|
|
---
|
|
|
|
## Reproducing the published results
|
|
|
|
### Prerequisites
|
|
|
|
- **GPU**: NVIDIA card, ~10 GB VRAM minimum for the Tier 1 set, ~14 GB for the FP16 variant. The published numbers are from an RTX 3090 (24 GB).
|
|
- **Ollama**: install per [ollama.com/download](https://ollama.com/download). Tested against Ollama 0.23.2. Newer should work; older may break the `think` parameter or the `/api/ps` endpoint shape (the harness has a fallback for the latter).
|
|
- **Python**: 3.10+ (project uses `uv` for env management).
|
|
- **Disk**: ~30 GB free for the full local candidate set, ~24 GB for Tier 1 only.
|
|
- **Ollama Cloud account** (optional): only needed if you want to re-run the cloud-tier ceiling measurements. Sign in via `ollama signin` before running.
|
|
|
|
### 1. Set up the environment
|
|
|
|
```bash
|
|
git clone https://github.com/MemPalace/mempalace.git
|
|
cd mempalace
|
|
uv sync
|
|
```
|
|
|
|
### 2. Pull candidate models
|
|
|
|
The full candidate list is in `benchmarks/model_eval/candidates.yaml`. Pull what you want; tier filters in the orchestrator only run what's installed locally.
|
|
|
|
**Bulk-pull the Tier 1 set (the must-evaluate set, ~24 GB total)**:
|
|
|
|
```bash
|
|
# This script reads candidates.yaml and pulls every tier-1 model + the embedding model
|
|
uv run python -c "
|
|
import yaml
|
|
import subprocess
|
|
with open('benchmarks/model_eval/candidates.yaml') as f:
|
|
cands = yaml.safe_load(f)['candidates']
|
|
for c in cands:
|
|
if c.get('tier') == 1:
|
|
print(f'pulling {c[\"tag\"]}')
|
|
subprocess.run(['ollama', 'pull', c['tag']], check=True)
|
|
subprocess.run(['ollama', 'pull', 'nomic-embed-text'], check=True)
|
|
"
|
|
```
|
|
|
|
To also pull Tier 2 (sub-3B sizes), the `modern` tier (Gemma 4, Granite 4.1, Ministral 3, Qwen 3.5), or Tier 3 (FP16 ceiling), substitute the filter: `c.get('tier') in (1, 2)`, `c.get('tier') == 'modern'`, etc.
|
|
|
|
**For the cloud comparison** (optional, requires `ollama signin`):
|
|
|
|
```bash
|
|
for tag in gpt-oss:20b-cloud gpt-oss:120b-cloud qwen3-coder:480b-cloud \
|
|
deepseek-v3.1:671b-cloud deepseek-v4-flash:cloud deepseek-v4-pro:cloud \
|
|
kimi-k2.6:cloud; do
|
|
ollama pull "$tag"
|
|
done
|
|
```
|
|
|
|
### 3. Smoke test (under 30 seconds)
|
|
|
|
Confirm the harness works against one model and one task before committing to a full run:
|
|
|
|
```bash
|
|
uv run python -m benchmarks.model_eval.runner \
|
|
--model qwen3:4b-instruct-2507-q4_K_M \
|
|
--task calibration \
|
|
--mode default \
|
|
--dataset-dir benchmarks/model_eval/datasets
|
|
```
|
|
|
|
You should see ~20 inferences and JSON output with `accuracy: 0.95` (or close). If accuracy is much lower, check that `ollama list` shows the model loaded and that `nomic-embed-text` is pulled (needed for any open-set or memory task even though calibration doesn't use it).
|
|
|
|
### 4. Run the matrix
|
|
|
|
**Tier 1 only (~30-40 min on RTX 3090)**:
|
|
|
|
```bash
|
|
uv run python -m benchmarks.model_eval.orchestrator \
|
|
--candidates tier1 \
|
|
--tasks all \
|
|
--dataset-dir benchmarks/model_eval/datasets \
|
|
--output benchmarks/model_eval/results/$(date -u +%Y-%m-%d)-$(hostname).csv
|
|
```
|
|
|
|
**Everything local (~60-80 min)**:
|
|
|
|
```bash
|
|
uv run python -m benchmarks.model_eval.orchestrator \
|
|
--candidates local \
|
|
--tasks all \
|
|
--dataset-dir benchmarks/model_eval/datasets \
|
|
--output benchmarks/model_eval/results/$(date -u +%Y-%m-%d)-$(hostname).csv
|
|
```
|
|
|
|
**Cloud only (~25-50 min, n=30 to control cost)**:
|
|
|
|
```bash
|
|
uv run python -m benchmarks.model_eval.orchestrator \
|
|
--candidates cloud \
|
|
--tasks all \
|
|
--n 30 \
|
|
--dataset-dir benchmarks/model_eval/datasets \
|
|
--output benchmarks/model_eval/results/$(date -u +%Y-%m-%d)-cloud-$(hostname).csv
|
|
```
|
|
|
|
The orchestrator prints `[i/N]` progress, `acc=… e2e_p50=…ms vram=…` after each run, and writes the CSV incrementally — safe to Ctrl-C if you only want partial data.
|
|
|
|
### 5. Render a report
|
|
|
|
```bash
|
|
uv run 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
|
|
```
|
|
|
|
Output: a markdown report with per-task rankings, production picks, open-set viability, and an instruct-vs-reasoning comparison.
|
|
|
|
### 6. Compare with the published baseline
|
|
|
|
The committed baseline CSVs are in `benchmarks/model_eval/results/`:
|
|
|
|
- `2026-05-10-z690-ex-glacial.csv` — full local matrix (RTX 3090)
|
|
- `2026-05-11-cloud-z690-ex-glacial.csv` — cloud tier
|
|
- `2026-05-11-modern-z690-ex-glacial.csv` — modern tier (Gemma 4, Granite 4.1, Ministral 3, Qwen 3.5)
|
|
- `2026-05-10-spotcheck-qwen3.csv` — reproducibility spot-check
|
|
|
|
For **accuracy**, your numbers should land within ~1% of the baseline. Bigger drift suggests something different in your setup (different Ollama version, different model digest, different system prompt rendering).
|
|
|
|
For **speed**, your numbers will differ — they depend on your GPU, driver, thermal state, and concurrent load. Use **relative rank within your machine** as the comparable signal, not absolute milliseconds.
|
|
|
|
For **VRAM resident**, expect close agreement on the same Ollama version. Peak VRAM is noisier (it depends on other GPU activity at measurement time).
|
|
|
|
### 7. Sharing your results
|
|
|
|
If your results disagree with the published baseline in a meaningful way:
|
|
|
|
1. Run the smoke test (step 3) on the specific model that disagrees, save the JSON output
|
|
2. Open an issue against this repo with: your `ollama --version`, GPU model, CSV file attached or pasted, and the smoke-test JSON
|
|
3. If the harness has a bug, we'll fix it. If the model behavior genuinely changed (Ollama Cloud model rotation, new quantization upstream), we'll re-run and document the drift in the report
|
|
|
|
To attach your CSV to a follow-up PR or comparison study, drop it in `benchmarks/model_eval/results/` with a name like `YYYY-MM-DD-yourhostname.csv` and reference it in the analysis report.
|
|
|
|
---
|
|
|
|
## What it measures
|
|
|
|
For each `(model, task, mode)`:
|
|
|
|
- **Accuracy** — task-specific scoring against the labeled dataset
|
|
- **TTFT** (time-to-first-token) — approximation from Ollama's `prompt_eval_duration + load_duration`, p50 and p95 over N=20 sample runs
|
|
- **TPS** (tokens/second) — from Ollama's `eval_count / eval_duration`, p50 and p95
|
|
- **e2e latency** — full single-classification time, p50 and p95
|
|
- **VRAM resident** — model memory after warmup (read from `/api/ps`)
|
|
- **VRAM peak** — peak GPU memory during inference (polled via `nvidia-smi` every 500ms)
|
|
|
|
The first run of each model is discarded (cache + GPU clock ramp).
|
|
|
|
## Tasks
|
|
|
|
- `room_classification` — closed-set (model picks from a provided room list) and open-set (model invents a slug). 101 samples.
|
|
- `entity_extraction` — JSON list of entities per sample. 50 samples, 247 ground-truth entities.
|
|
- `memory_extraction` — structured memory items per sample. 40 samples, 55 ground-truth memories across 5 types.
|
|
- `calibration` — simple 5-class sentence-type. 20 samples. Sanity check the harness.
|
|
|
|
All datasets are synthetic (no real-person info). Generated once and frozen so benchmark numbers stay comparable across runs.
|
|
|
|
If you need to extend the dataset, **add** samples; don't replace existing ones, otherwise prior numbers stop being comparable.
|
|
|
|
## CLI references
|
|
|
|
```bash
|
|
# Single (model, task, mode)
|
|
uv run python -m benchmarks.model_eval.runner --help
|
|
|
|
# Matrix runs with tier filtering
|
|
uv run python -m benchmarks.model_eval.orchestrator --help
|
|
|
|
# CSV → markdown report
|
|
uv run python -m benchmarks.model_eval.summarize --help
|
|
```
|
|
|
|
Tier filter values: `tier1`, `tier2`, `tier3`, `tier<=N`, `local` (everything not tier=cloud), `cloud` (everything tier=cloud), `modern` (the Gemma 4 / Granite 4.1 / Ministral 3 / Qwen 3.5 additions), or any exact model tag for a single-model run.
|
|
|
|
## Reusing existing infrastructure
|
|
|
|
The harness uses `mempalace.llm_client.get_provider("ollama", model=tag)` and `provider.classify(...)` — the same code path as production. For thinking-capable models, the runner always passes `think=False` so hybrid models stay in fast-classification mode.
|
|
|
|
No new HTTP plumbing. No reimplementation of provider abstraction. The harness benchmarks the same code that ships.
|
|
|
|
## Hardware reporting
|
|
|
|
Every result file includes the test machine's metadata (CPU, GPU, VRAM total, Ollama version, OS, hostname). Speed numbers are **not portable across machines** — use them for relative ranking on a single setup. Accuracy numbers cross-port cleanly.
|
|
|
|
## Contributions welcome
|
|
|
|
### Adding models we missed
|
|
|
|
The published candidate list is what one engineer + one search pass surfaced. There are absolutely small instruct models we didn't catch. If you know of a competitive ≤4B-parameter model that should be in the comparison, please:
|
|
|
|
1. Add an entry to `candidates.yaml` following the existing schema (`tag`, `family`, `size_b`, `variant`, `quantization`, `expected_vram_mb`, `tier`, `notes`)
|
|
2. Run it through the smoke test then the matrix using the existing `--candidates <your-tag>` filter
|
|
3. Open a PR with the new candidate row + CSV results + a one-line analysis-report addendum
|
|
|
|
Particularly interested in: function-calling-tuned models (Phi-4 mini, Nemotron-mini), recent instruction-tuned variants from research labs (Hermes, Dolphin, OpenHermes), and quantization-aware-trained variants of established families. If the model has multiple competitive quantizations, pick the smaller one that's within accuracy noise (per the project finding "newer ≠ better" and the quantization sweet-spot rule from the analysis report).
|
|
|
|
### Adding other inference backends
|
|
|
|
The harness is wired through `mempalace.llm_client.get_provider()`, which already supports three provider types: `ollama` (currently used here), `openai-compat`, and `anthropic`. That means any **OpenAI-compatible** local server should plug in with minimal work:
|
|
|
|
- **LM Studio** — serves an OpenAI-compatible API on `http://localhost:1234/v1` by default
|
|
- **llama.cpp server** — `./server` exposes OpenAI-compat on `http://localhost:8080/v1`
|
|
- **vLLM** — `--port 8000` runs an OpenAI-compatible endpoint
|
|
- **unsloth studio** — likewise serves OpenAI-compat for inference
|
|
- **Docker Model Runner** — exposes models over OpenAI-compat on a per-model port
|
|
- **Hugging Face TGI / TEI** — OpenAI-compatible endpoints
|
|
|
|
The plumbing pieces a contributor would need to add:
|
|
|
|
1. **A backend-selection flag** in `runner.py` and `orchestrator.py` (e.g. `--backend ollama|openai-compat|anthropic|lm-studio|...`) that constructs the right provider via `get_provider(backend_name, model=tag, endpoint=...)`.
|
|
2. **Backend-specific timing extraction in `metrics.py`.** The current `extract_timing()` reads Ollama's `eval_count`, `prompt_eval_duration`, etc. Other backends report timing differently (OpenAI's `usage.completion_tokens`, llama.cpp's `tokens_per_second`, etc.). The harness will currently fill those columns with zeros for non-Ollama backends — degrades gracefully but loses the per-request timing breakdown.
|
|
3. **Backend-specific VRAM probe.** Ollama exposes `/api/ps`; LM Studio has its own status endpoint; llama.cpp doesn't expose model memory directly. For non-Ollama backends, `vram_resident_mb` would return `None` (already handled). Peak-VRAM via `nvidia-smi` still works regardless.
|
|
4. **A `candidates.yaml` field** to mark backend per candidate (e.g. `backend: lm-studio`).
|
|
|
|
If you implement a new backend, the existing dataset and scoring code applies unchanged — the harness's accuracy numbers stay comparable across backends. Open a PR with the runner/orchestrator changes plus one CSV from your backend so we can validate the integration on a known model.
|
|
|
|
If you build something niche that's worth comparing (Apple MLX, Intel OpenVINO, AMD ROCm-specific runtimes, edge-device runtimes like Termux + llama.cpp on phones), please share the methodology. Cross-runtime comparisons are exactly the kind of follow-up this harness is designed to enable.
|
|
|
|
## Notes for harness maintainers
|
|
|
|
When modifying the harness internals:
|
|
|
|
- **`format: json` mode is enforced locally but ignored on Ollama Cloud.** Per Ollama's docs. Cloud models that "happen to" emit JSON do so by default behavior, not because Ollama enforces it. The `kimi-k2.6:cloud` memory-extraction `valid_json_rate: 0.37` is a documented manifestation.
|
|
- **The memory-extraction `hallucination_rate` metric over-penalizes thorough models.** See the analysis report. Trust `mean_coverage` until a follow-up refines the scoring.
|
|
- **Cloud reproducibility is worse than local.** ~6 points of drift on `gpt-oss:20b` between runs. Cloud numbers should be reported as ranges, not point estimates.
|
|
|
|
Read `reports/2026-05-10-analysis.md` for the full set of findings, surprises, and methodology notes from the original run.
|