MIP-0081: External LLM evaluation — standard benchmarks run by any provider, behind one seam, with every score kept in git and checked locally¶
| Status | Accepted — Tasks: docs/MIPs/MIP-0081.tasks.md (MIP-0081.tasks.md) |
| Author | Claude, for M. Hoffmann, from marola-dev/marola-ml#22 and an evaluation provider's offer of a partnership and a free first run (2026-10-08) |
| Created | 2026-10-08 |
| Phase | None: offline ML tooling, off the app's request path. A provider-backed run is paid compute, so AGENTS.md's cost rule applies per run (a person starts it, the cost goes in Cost:); no Phase 2 cloud backend is involved |
| Related | marola-dev/marola-ml#22 (the sketch this formalises), MIP-0025 (marola-sea, the model being scored), MIP-0032 (the model × strategy matrix on marola's own questions; this is its standard-benchmark half), MIP-0048 (which base to scale to, the question these numbers answer), MIP-0050 (Portuguese bases, and why English scores say little), MIP-0010 (the MLflow ledger, which stays the place for marola's own runs) |
| Effort | M — one stdlib Python seam with a local backend over lm-evaluation-harness, a JSON schema and a renderer, one harness task exported from the app's question set, one provider backend, a change to publish_hf.py's card. No Scala, no new service |
| Gain | community/outreach — marola-sea gets scores comparable with every model card, and a provider's credits become a partnership instead of a one-off; infra/dev-loop — the base-model choice MIP-0048 and MIP-0050 defer gets numbers, in Portuguese as well as English |
| Effort vs Gain | cheap win for tasks 1–3 (local, free, and they make the first provider run publishable); do when X lands for task 5, X = a provider whose answers to the brief's five questions meet §5.8 |
| Depends on | Nothing merges first. Task 5 depends on a provider agreement, which is a person's act. No Phase 1 gate |
| Blocked by | none |
| Risk | A provider's number is published without the local check because the check is slow or inconvenient, and a misconfigured run (a generation-scored ARC, a missing chat template) ends up on the model card; §5.3 makes the check a gate on publishing, not a guideline |
| Cost so far | — |
Readiness¶
| Manually reviewed | no |
| Written by | Hoffmann, with Claude Code |
| Tasks | MIP-0081.tasks.md |
| Tests | backend.py --self-test (fake_backend_round_trips_model_ref), check.py --self-test (eval_check_refuses_out_of_stderr, eval_check_accepts_within_stderr), publish_hf.py --self-test (model_index_from_results), marola_ocean/utils.py --self-test (coverage_matches_ocean_benchmark), ci.yml's runners job (§7) |
| Spec-kit | none |
| Issues | marola-dev/marola-ml#26, #27, #28, #29, #30; marola-dev/marola-site#94; #706 |
1. Summary¶
marola-sea is published to Hugging Face with no standard benchmark score, because running a
harness needs GPUs marola does not have. Evaluation providers offer to run standard benchmarks on
public Hugging Face models, and some offer credits to open projects in exchange for credit. This
MIP adopts them vendor-neutrally: an EvalBackend seam with a local implementation and one per
provider, chosen by config; one results schema in git; a local re-run that must agree with a
provider before its numbers are published; marola's own ocean questions exported as a portable
harness task; and a model card built from the results instead of free text. No provider is in the
critical path, and every number stays reproducible without them.
2. Motivation¶
- No comparable numbers. The only scores marola has are its 22-question keyword-coverage
benchmark (marola-ml's
docs/benchmarks/, enforced onllama3.2by the benchmark gate) and trainingeval_loss. Neither says whether fine-tuning SmolLM2-360M made it forget general ability, and the card's--eval-noteis free text. - The base-model decision is open. MIP-0048 wants a bigger base and MIP-0050 a Portuguese one; both defer the choice for lack of measurements. A matrix of five bases on English and Portuguese tasks is a few GPU-hours, which a provider can give and marola cannot.
- A provider asked first. One offered a partnership and a free
arc_challengerun on marola-sea-tiny, and asked which benchmarks on which models marola needs so it can estimate costs. That question needs an answer that is not tied to that provider: marola-ml's external evaluation page (docs/3-development_external-eval.md, marola-dev/marola-ml#25) is it.
3. User-visible change¶
None in the app. On Hugging Face, each marola-sea card gains a model-index block and a results
table naming the backend behind every number. In marola-ml:
# in a marola-ml checkout
$ just eval tiny arc_challenge # local backend, CPU is enough for tiny
eval: arc_challenge on h0ffmann/marola-sea-tiny-GGUF@e6d8b16 (Q8_0): acc_norm 0.xxxx ± 0.0xxx
wrote docs/benchmarks/results/2026-10-xx-marola-sea-tiny-arc_challenge-local.json
$ just eval-render # docs/benchmarks/results.md from every results file
$ just eval-check <provider-results.json> # refuses one whose local twin disagrees beyond stderr
4. Data sources and dependencies reviewed¶
- marola-ml at
main(2026-10-08):finetune/train_lora.py'sPRESETS(tinySmolLM2-360M,small/baseLlama 3.2 1B/3B,qwen-4bQwen3-4B-Instruct-2507,qwen-7b/qwen-14bQwen2.5,qwen-27b);finetune/publish_hf.py(card from--eval-note,CHECKSUMSsha256 per GGUF);Dockerfile.local(llama3.2is what the app answers with);scripts/benchmark_gate.py. - Hugging Face (2026-10-08):
h0ffmannpublishes one model,marola-sea-tiny-GGUF, ate6d8b16, with a singlemarola-sea-tiny-Q8_0.gguf(386 MB),CHECKSUMSand the card. There is noQ4_K_Mfile and no full-precision marola-sea checkpoint, so "Q4_K_M vs full precision" for the released model cannot be measured until one is published (§11). - lm-evaluation-harness at
d6de816(2026-09-14):arc_challenge,hellaswag,mmlu,truthfulqa_mc2,gsm8k,ifeval; Portuguesearc_pt,hellaswag_pt,m_mmlu_pt,truthfulqa_pt_mc1/2(Okapi, machine-translated),global_mmlu_pt,belebele_por_Latn. - lm-evaluation-harness-pt (eduagarcia,
ab24923, the Open Portuguese LLM Leaderboard's harness):enem_challenge,bluex,oab_exams(3-shot, acc),assin2_rte,assin2_sts,faquad_nli(15-shot, F1 macro), plus hate-speech and sentiment tasks that are not marola's job. - The provider's offer: one
arc_challengerun on marola-sea-tiny-GGUF at no cost. Its product, harness and GGUF path were not public enough to check from here; the brief's questions (§5.8) are how they get answered.
5. Design¶
5.1 The EvalBackend seam¶
scripts/eval/backend.py (stdlib): submit(model: ModelRef, tasks: list[TaskSpec]) -> list[Result],
where ModelRef is a Hugging Face repo, a revision SHA and, for a GGUF, the file and its sha256
from CHECKSUMS. scripts/eval/local.py shells out to lm_eval (a pinned version in
eval/requirements.txt, outside the standard-library rule like finetune/'s training
dependencies); GGUF runs go through llama.cpp's server with the gguf model type. Each provider
is one more module, picked by --backend; nothing outside scripts/eval/ knows which one ran.
5.2 One results schema, in git¶
docs/benchmarks/results/<date>-<model>-<task>-<backend>.json, validated by
scripts/eval/results.schema.json: model repo, revision SHA, GGUF file and sha256 (or dtype),
quantisation, task, task version, harness name, version and commit, few-shot count, chat template
on or off, metric, value, stderr, sample count, backend, date, the person who started it, and the
raw per-sample output, committed beside it (gzip) or linked from a release asset when large.
scripts/eval/render.py writes docs/benchmarks/results.md. A score that lives only on a
provider's dashboard is never cited.
5.3 The reproducibility check¶
Before any of a backend's results are published, at least one of its tasks on tiny is re-run by
the local backend; just eval-check refuses a file whose value differs from its local twin by
more than the larger of the two stderrs. A refusal blocks publishing that backend's numbers, not
the relationship: the next step is comparing configs.
5.4 The domain task¶
The app's question set (benchmark_questions.json, shipped in ml-resources-<tag>.tar.gz) is
exported to an lm-evaluation-harness task, marola_ocean: a YAML with the questions as
generate_until prompts and a utils.py scoring keyword coverage the way OceanBenchmark does.
It measures the model alone, without retrieval; the app-image gate in docker-local.yml, which
measures RAG, stays as it is.
5.5 Model card from data¶
publish_hf.py gains --results <glob>: the card's model-index metadata and results table come
from the results store, each row naming its backend and linking its file. --eval-note stays for
prose.
5.6 Gating stays local¶
External results inform. They never gate docker-local.yml or a publish: a provider's outage or
policy change must not block a release. Revisit after a backend has a track record.
5.7 Cost and safety¶
A provider-backed run is a workflow_dispatch (eval-external.yml), started by a person, never
reachable from a pull request (ci.yml's runners job enforces it, as for
marola-sea-publish.yml). The provider's key is a repository secret or a local environment
variable, never a file; a coupon or a free-run link is treated the same. Credits spent are stated in
the PR body. Only public models are sent.
5.8 Provider policy¶
The terms on marola-ml's external evaluation page, written once: configs and harness versions disclosed with every score; raw outputs exportable; no exclusivity; no claim on marola's data, models or results; credit on marola.dev's partners page, in the model card beside each score, and in the results file. The page also carries the five questions a provider answers before its first paid run (harness and config, how a GGUF is scored, custom and forked tasks, exportable outputs, gated models).
6. Scoring / safety impact¶
None: nothing here touches the swim score, the safety veto or what the app says.
7. Verification plan¶
The named test: eval_check_refuses_out_of_stderr in scripts/eval/check.py --self-test, where a
fake backend returns arc_challenge on tiny at the local run's value plus three stderrs and the
check refuses it, while the local run's own file validates against the schema and renders into a
model card (publish_hf.py --dry-run --results). Then, once: a real local arc_challenge run on
tiny committed as the first results file, and the provider's free run on the same model checked
against it.
8. Risks, limitations, and honest caveats¶
- A 0.36B model scores near chance on MMLU-class tasks; its useful signals are ARC, HellaSwag, TruthfulQA and the domain task, and the fine-tune delta on those.
- Okapi's Portuguese tasks are machine-translated; the pt fork's tasks are native but run on a harness fork that lags upstream. Both are listed so neither is the only Portuguese number.
marola_oceanis 22 questions scored by keyword coverage, with ±0.1 run-to-run noise measured in2026-09-05.md; it is a sanity check, not a ranking.- A GGUF scored by generation instead of log-likelihood gives a different number for the same task name; the results schema records how, and §5.3 catches the difference.
9. Alternatives considered¶
- Take one provider's scores as they come. Fastest, but the numbers would be unreproducible and the relationship exclusive in practice. Rejected by #22's own premise.
- Rent GPUs and run everything locally. Reproducible, but paid every run; the local backend
covers
tinyon CPU and the seam lets rented GPUs be one more backend later. - The Hugging Face Open LLM Leaderboard. Retired in 2025; it is no longer a submission target.
10. Phase and cost¶
Tasks 1–4 are free (CPU, standard library plus lm_eval). Task 5 spends a provider's credits or
money per run, each started by a person with the cost stated first.
11. Open questions¶
- Publish a full-precision
marola-sea-tinycheckpoint and aQ4_K_MGGUF, so the quantisation rows of the matrix exist? Default: no; the matrix sends the one publishedQ8_0file and the bf16 base. Hoffmann decides, since a Hugging Face upload cannot be taken back. - Which partners page shape on marola.dev? Default: one static page listing each partner, what it gave and a link to its results, linked from the about page (marola-dev/marola-site#94). Hoffmann decides on that issue.
- Does the first provider run the pt fork, or must its tasks be ported upstream first? Default: the provider is asked (question 3 of the brief); until it answers, the pt-fork tasks run locally only and the provider gets the upstream ones.