- Python 72.5%
- Go 22.9%
- C 3.8%
- Rust 0.3%
- Lua 0.2%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| bench | ||
| image | ||
| tasks | ||
| tests | ||
| tools | ||
| .gitignore | ||
| bench.py | ||
| bench.toml | ||
| README.md | ||
banana-benchmark
The banana-benchmark is a benchmark suite for local language models that llama.cpp serves.
The Pi coding agent (@earendil-works/pi-coding-agent, default configuration) works on each task in a podman container with no network.
The suite has more than 60 tasks in several categories (see # Task categories).
Run ./bench.py list to see them all.
Quick start
./bench.py build-image # once: builds Pi and the toolchains (python, rust, go, g++, java, node, chromium)
llama-server -m model.gguf --port 8081 --jinja -c 65536 # or start a llama.cpp router with a models.ini
./bench.py run --name my-model --base-url http://127.0.0.1:8081 --tag quick
./bench.py report # text table for all models in results/
./bench.py report --md # one markdown report per model: results/<model>-report.md
./bench.py gallery # results/gallery.html, to judge the frontend tasks by hand
./bench.py grade my-model frontend/kanban-board 4 # record a 0-5 human grade
- Use
--tag quickfor a subset of 13 tasks that gives a fast first impression of a model - Use no selector for the full suite, which takes many hours
- Use
--dry-runto preview the attempts without running them - Use
./bench.py lintto check the category and tag scheme of every task - Use
./bench.py validateto check that each task fails on its start state and passes with its reference solution- This command needs podman
- Without podman, use
tools/validate_native.py(see# Tools)
Servers and models
The runner works with a single-model llama-server and with a llama.cpp router (a models.ini with several models).
The runner always selects the model that you name.
- With
--model-id, the runner uses that model - Without
--model-id, the runner uses the only model, the loaded model, or the model that matches--name- If the choice is unclear, the runner stops and lists the candidates
The runner finds the context window of the model with the sources below, in the same order, and prints the source at startup.
--ctx/props?model=<id>(n_ctx)- the launch arguments of the router (
-c, divided by-npunless you set--kv-unified) /slots- If no source gives a number, the runner stops and asks for
--ctx
Pi receives this value as the context window of the model. A wrong value makes the agent compact too early.
The runner keeps the server configuration in results/<model>.json under model.server.
modeisrouterorsinglepropsis the full/props(build, slots, sampling defaults, chat template)argsandpresetare the launch flags and the preset text (router mode only)router_modelsandslotshold the raw/modelsand/slotsanswersfingerprintidentifies the configuration, and each attempt stores the fingerprint that was activeserver_historyholds the old configurations, if you restart the server with other flags between two runs
Run control
The runner saves the results after each finished attempt.
- Press Ctrl-C (or send SIGTERM) to stop cleanly
- The runner kills the running container and discards the unfinished attempt
- Run the same command again to resume
- Use
--rerunto repeat the selected tasks
- Use
- A lock file prevents two runs for the same model
- The runner removes stale containers from a crashed run at startup
Select tasks with --only, --exclude, --category and --tag.
These options accept globs and may repeat.
Other options:
--attempts N--thinking LEVEL--reasoning--max-tokens N--keep-workspaces--timeout-s,--idle-sand--max-tool-calls
Pi runs with its default configuration.
The runner generates only models.json, which points to the loopback bridge in the container.
To use a specific Pi version, run ./bench.py build-image --pi-version X.
Isolation
- The agent container has
--network none, so only a loopback interface exists - The container drops all capabilities and sets
no-new-privileges- The container runs as a non-root user and has a read-only root file system
- The container has process, memory and CPU limits
- The container reaches the model through a unix socket
- A small node bridge in the container maps
127.0.0.1:8080to the socket - A filtering proxy on the host (
bench/proxy.py) forwards onlyPOST /v1/chat/completions,POST /v1/completionsandGET /v1/models
- A small node bridge in the container maps
- Host tools do not process directories that the agent changed
- The diff, the screenshot and the verification run in new containers without network
- Hidden tests stay on the host until the verification starts
- The agent container uses
--security-opt label=disable(SELinux)- A confined container connects only to sockets of confined processes, and the proxy is an unconfined host process
- The other containers keep SELinux labels
- The runner rejects
network = truein a task, so a task has no network access
Results
The runner writes results/<model>.json, and the artifacts of each attempt go to results/<model>/<task>/attempt-N/.
Each attempt has these fields:
statusispassed,failed,stuck,timeout,manualorerrorerrormarks an infrastructure failure, and the score of the model excludes that attempt
score,wall_s,verify_s,turnsandtool_calls(total, by name, errors)stuck_reason, if the monitor stopped the attempttokens, with the input, output and cache totals- The field also holds the final and the peak context, and their share of the window
compactionsandllm(requests, processed and cached prompt tokens, completion tokens)prefill_tpsanddecode_tps, taken from thetimingsof llama.cpp- the time to first token and the largest context used
The artifacts are events.jsonl (the raw Pi events), changes.diff, final_message.md, verify.log and screenshot.png.
Run ./bench.py backfill to compute the token and compaction metrics again from saved event streams.
Each model gets its own markdown report, results/<model>-report.md.
./bench.py runwrites the report when the run ends./bench.py report --mdwrites the reports for all models, and--model NAMEselects one model- Add
--baseline OLD.jsonto compare with an older run of the same model
- Add
- The report lists infrastructure errors apart from model failures, so a server outage counts as an error, not as a failed task
./bench.py report --comparewrites one table that compares all models, inresults/report.md
Stuck agents
A monitor kills the container with podman kill and records stuck or timeout.
The attempt then counts as failed, and the runner skips the verification.
The monitor reacts to each event below:
- the wall-clock limit passes
- no Pi event and no model traffic occur for
idle_s - the same tool call repeats 8 times, a cycle of 2 to 4 calls repeats 4 times, or the same message repeats 5 times
- one response passes 32,000 chunks (runaway generation)
- the agent makes too many tool calls
The limits are in bench.toml, and a task can override them in its [agent] section.
tests/stuck_selftest.sh tests loops, idle time and runaway generation with tests/mock_llama.py.
Task categories
Run ./bench.py lint to check the scheme below.
bugfix/large-projecthas bugs in big repositories, injected or latentfeature/large-projectandfeature/small-projecthave features with hidden testsrefactor/large-projecthas refactorings that keep the behavior the same- A hidden golden test compares thousands of random inputs, or the full exported API of a Go package, with the original code
- A structural check (function length, one shared helper, files split by concern) proves that the refactoring happened
issue/bugfixandissue/featurehave real GitHub issues from SymPy, gin, echo, chi, cobra, networkx, sqlparse and click- The prompt is the text of the issue only
- The repository is at the parent of the fix
- The hidden tests come from the fix, and
solution/holds the source change of the fix
coding/*has LeetCode-hard problems (Python, Go, Rust), C++ template errors and a Rust borrow-checker taskfrontendhas tasks that a human grades (see# Frontend tasks)reviewhas a code review with 6 planted defects, and the grade is the recall of the defectssmokehas a sanity check
Each task has one language tag. Tasks in the project categories also have the tags below:
- a kind tag (
bugfix,featureorrefactor) - a size tag (
largeorsmall) - an origin tag (
real-issueorsynthetic)
The tag hard marks the difficult tasks, and the tag quick marks the quick subset.
Advent of Code tasks are not in the repository, because the license of the puzzle text forbids redistribution.
- Put your session cookie in
aoc_session.txt - Run
./bench.py import-aocto createtasks/aoc/<year>/dayNN/for puzzles that you solved- Git ignores these files
Frontend tasks
Frontend tasks have grade = "manual", so they end with the status manual.
- The finished app stays in
results/<model>/<task>/attempt-N/workspace/ ./bench.py gallerywritesresults/gallery.html- The page shows a desktop screenshot and an optional mobile screenshot (
mobile_widthin[screenshot]) - The page has an "open the app" link for each attempt, and a link to the
checklist.mdof the task
- The page shows a desktop screenshot and an optional mobile screenshot (
- Open the apps over
file://, or runpython3 -m http.serverinresults/ - Record a grade from 0 to 5 with
./bench.py grade
Playwright tests for the behavior of the apps are a possible later addition.
Add a task
Create tasks/<category>/<name>/ with these files:
task.tomlwithtitle,category,tags,[verify] cmd, and optional[agent],[repo],[screenshot]andgrade = "manual"prompt.md, the text that the agent receivesworkspace/, the start files, copied to/workspaceverify/, hidden files that the runner copies over the workspace beforeverify.cmdstartssolution/, the reference fix thatvalidatecopies over the workspacechecklist.md, optional, for manual frontend tasks
The task passes when verify.cmd exits with code 0.
To give partial credit, print SCORE: 0.75 as the last line.
For a task in a big project, use [repo] url, rev, subdir, patch, prepare.
- The runner clones the pinned repository one time on the host and caches the clone in
cache/ patch(-p1) injects a bug, so the task differs from the public repositorypreparecommands (for examplego mod vendor) run one time on the host, because the sandbox has no network- Skip tests that need sockets or network in
verify.cmd, because the sandbox blocks them
Check a new task before you use the task.
- Run
./bench.py validate, ortools/validate_native.pywithout podman - A good task fails on the start state and passes with
solution/
Tools
tools/import_pr.py OWNER/REPO PR NAME --verify 'CMD {tests}'creates an issue task from a merged pull request that links an issue- Use
--commit SHA --git DIRto import from a commit in a local clone, which saves GitHub API quota - Use
--kind featurefor feature requests
- Use
tools/scan_fixes.py REPO.gitlists commits that fix an issue and change both source and teststools/validate_native.py [--path-prefix DIR] 'glob'...runs the validation on the host without podman- Use the script only for trusted tasks, because the script runs the code without isolation
Limits of the suite
- The code-review grade is a regex recall over the planted defects, so a list of keywords can game the grade
- The click tasks skip three tests that need network (package metadata and the pager)
- The echo and chi tasks skip the tests that open sockets
- Some tasks (for example
sympy-function-range-composed) use the full timeout if the model fails them - The suite has no Rust and no Django real-issue tasks, because the authoring environment has no access to the required hosts