* llama-eval : add sandboxed execution for code suites
Adds a lightweight replacement for the Docker containers usually used to run
generated code, so a benchmark can be executed without a daemon, an image or
root.
A cached venv is provisioned per suite and reused; candidate programs run in a
throwaway working directory as their own process group, under the strongest
isolation the host offers:
bwrap read-only root, tmpfs /tmp and $HOME, no network
unshare no network, via --map-current-user
rlimit cpu/memory/file-size/process caps only
Candidate code must not run as uid 0, which is why "unshare -r" is a
last-resort tier that warns: root bypasses file permission bits, so a test
asserting that a write to a chmod 0444 file fails would instead see it succeed.
Two details worth noting for the bwrap tier. Only paths hidden by the $HOME
tmpfs are bound back, because bwrap cannot mount onto a symlink and
version-manager alias directories usually are one. And the venv's base
interpreter tree is resolved by walking the symlink chain, since a
version-managed interpreter is commonly reached through an unversioned alias
that startup resolves through.
Suites can declare provisioning steps, which run once at build time while the
network is still up, so test time never needs it.
Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
* llama-eval : add HumanEval and ClassEval code suites
Adds two execution-graded suites alongside the existing text-graded ones. The
model's reply is parsed for a python code block, combined with the task's own
unit tests, and run in the sandbox.
--dataset humaneval 164 function-level problems, scored pass@k
--dataset classeval 100 class-level tasks, scored at class level (feeding
pass@k) and method level, matching upstream
pass@k reuses the existing chunk mechanism: --n_cases at a multiple of the
dataset size gives that many samples per problem, aggregated with the unbiased
estimator from the Codex paper.
Program construction and result interpretation are dataset-owned hooks, because
the two suites differ: one HumanEval problem is a function judged by a single
assert, whereas one ClassEval task is a class judged by many independent test
methods, and reducing that to pass/fail would throw away partial credit. Suites
also declare their own package set, provisioning steps, interpreter and timeout.
The package set is derived from the dataset's own imports rather than a
requirements file. HumanEval turns out to need no third-party packages at all;
ClassEval needs thirteen, two of which must be declared because no import
mentions them -- lxml is selected by string in ClassEval_44, and scipy is a
gensim requirement.
Notes on ClassEval specifically:
* gensim publishes no wheel past cp313, so 3.13 is preferred; on 3.14+ the two
pure-Python APIs it uses are provided by a small shim instead.
* The three nltk corpora it needs are pre-seeded at venv build time. These are
the modern names -- the dataset asks for the since-renamed punkt and
averaged_perceptron_tagger.
* It defaults to a patched fork, ilintar/ClassEval, because three upstream
tests cannot be passed by anything: a hard-coded 2024 date checked against
datetime.now(), an assertion on the dataset author's own hostname, and an
exact float comparison. Reference solutions go from 96/100 to 100/100 classes
and 2182/2196 to 2196/2196 test methods. No skeleton is touched, so what the
model is asked to write is unchanged; --dataset-source selects the original.
--exec-seed (default 0) seeds the RNG before each test, so suites whose tests
draw random inputs score reproducibly. ClassEval_58 fails its own reference
solution about 30% of the time without it.
The simulator gains both suites, so the harness can be exercised without a
model.
Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
* llama-eval : fix silent isolation downgrade, generalise the sandbox runner
The isolation probe ran the interpreter without binding it back, so whenever
this tool is itself started from a venv under $HOME -- the usual case, since it
needs `datasets` -- bwrap's tmpfs hid the interpreter, the probe failed, and the
sandbox quietly fell back to a tier that does not hide $HOME at all. Nothing in
the output looked wrong when that happened, and the existing suites have been
running that way. The probe now uses the same interpreter and the same binds the
suite will actually get.
Two additions the agentic suite needs, neither of which changes the existing
suites:
* run_argv() runs an arbitrary command in a caller-owned directory that survives
the call. run() is now a thin wrapper over it, so HumanEval and ClassEval take
exactly the same path as before. A multi-step agent needs its edits to persist
across many invocations, and a suite for a language other than Python needs to
drive a linter and a test runner rather than a single source string.
* RLIMIT_AS is now per-sandbox, defaulting to what it was. It caps reserved
address space rather than resident memory, which is the wrong instrument for a
runtime that reserves a large region and commits little of it: node's
TypeScript type-stripping goes through WebAssembly, reserves gigabytes per
instance, and aborts at 4 GiB with an out-of-memory error that reads like a
fault in the code under test.
HumanEval stays at 164/164 and ClassEval at 100/100.
Assisted-by: Claude Opus 5 <noreply@anthropic.com>
* llama-eval : add SACB, a multi-turn agentic coding suite
`--dataset agentic` is a different shape from the other code suites. Rather than
answering a single prompt, the model is given a small but real repository, a
narrow tool set and a bug report, drives its own multi-turn conversation, and is
graded by running a test suite it was never allowed to see.
The corpus is SACB (Simple Agent Coding Benchmark), 60 tasks over three
hand-written repositories in Python and TypeScript, hosted at ilintar/SACB.
Grading needs no container: a cached venv gives ruff and mypy for Python, and
node's own test runner plus tsc for TypeScript, so the whole dependency set is
four packages.
Three constraints are deliberate:
* No shell, and no way to run the tests. The only feedback channel is `lint`. A
syntax or type error can be driven out mechanically; a logic error has to be
reasoned about. A harness that lets an agent iterate against the test suite
measures something closer to search than to understanding.
* Two edit tools, one line-addressed and one content-addressed. Which one a
model reaches for, and whether it keeps line numbers straight after its own
earlier edit, is itself a signal.
* The tests never touch the working tree. They are held outside it and copied
into a throwaway copy only once the agent has stopped, so they can be neither
read nor edited. Every tool path is resolved and confinement-checked against
the repository root, so a symlink planted inside the tree cannot reach out of
it.
A task counts as resolved when every test that was failing passes and nothing
that already passed broke. Both sets are derived by running the tests against
the defective tree and against the reference fix, never written by hand, so a
task whose defect no test exercises cannot enter the corpus. Many tasks carry
several independent defects, so the fraction of target tests passed is reported
alongside, separating "fixed two of the three faults" from "changed nothing".
An episode that arrives with no tool call and no `finish` is nudged rather than
ended. Models routinely narrate their analysis mid-task, and treating that as
completion ends the episode with no edits and scores a reasoning failure that
never happened; about one episode in four exits that way.
Nothing here is installed unless this suite is selected, and then only for the
language being run: agentic_eval is imported lazily and each toolchain is keyed
per language.
Assisted-by: Claude Opus 5 <noreply@anthropic.com>
* llama-eval : make the agentic hooks typed rather than discovered
`ty` flagged nine unresolved attributes, of which the seven in main() predate
the agentic suite: `EvalState.dataset` is Optional and was dereferenced without
narrowing.
* `loaded_dataset` turns the Optional into a value in one place, raising rather
than handing back None, so callers do not each have to narrow it.
* `is_agentic`, `prepare()` and `run()` are declared on BaseDataset. They were
reached through getattr with a default at three call sites, which no checker
can see through; `run()` now raises a clear error naming the suite instead of
failing as a missing attribute.
* The sandbox setup is one branch on `is_agentic` rather than two tests of
`grader_type`, which also drops a duplicated condition.
No behaviour change: HumanEval stays at 164/164, and `ty check` on
examples/llama-eval is clean.
Assisted-by: Claude Opus 5 <noreply@anthropic.com>
* feat: API key
* fix reasoning and response length for agentic runs, add option to save entire transcripts
* llama-eval : fix ty unresolved-attribute on loaded_dataset.ae
Assisted-by: AI coding agent
* llama-eval : trim comments and remove non-ASCII from README
Assisted-by: AI coding agent
|
||
|---|---|---|
| .. | ||
| agentic_eval.py | ||
| eval_sandbox.py | ||
| llama-eval.py | ||
| llama-server-simulator.py | ||
| README.md | ||
| test-simulator.sh | ||
llama-eval
Simple evaluation tool for llama.cpp with support for multiple datasets.
For a full description, usage examples, and sample results, see:
Quick start
# Single server
python3 llama-eval.py \
--server http://localhost:8033 \
--model my-model \
--dataset gsm8k --n_cases 100 \
--grader-type regex --threads 32
# Multiple servers (comma-separated URLs and thread counts)
python3 llama-eval.py \
--server http://server1:8033,http://server2:8033 \
--server-name server1,server2 \
--threads 16,16 \
--dataset aime2025 --n_cases 240 \
--grader-type regex
Authentication
A server started with --api-key (or any OpenAI-compatible endpoint behind a
token) needs one here too. --api-key is sent as Authorization: Bearer on
every eval request; it falls back to $LLAMA_API_KEY, which keeps the secret
out of your shell history and off the process list.
# One key for every server
python3 llama-eval.py --server http://localhost:8033 --api-key sk-... ...
# From the environment
export LLAMA_API_KEY=sk-...
python3 llama-eval.py --server http://localhost:8033 ...
# One key per server, in --server order
python3 llama-eval.py \
--server http://server1:8033,http://server2:8033 \
--api-key sk-one,sk-two --threads 16,16 ...
The LLM grader authenticates too. It reuses the key of the server it runs on,
so --grader-type llm without --grader-server needs nothing extra; a grader
on a separate endpoint takes --grader-api-key.
Endpoints on an opencode host additionally get an x-opencode-session header,
generated fresh for every task. An agentic episode sends the same id on all of
its turns, so opencode sees one session per task instead of one per request;
other servers never see the header.
Agentic suite
--dataset agentic is a different shape from the other code suites. It runs
SACB (Simple Agent Coding Benchmark), hosted at
ilintar/SACB -- 60 tasks over
three hand-written repositories, in Python and TypeScript. Rather than
answering one prompt, the model is given a small but real repository, a
deliberately narrow tool set, and a bug report, and drives its own multi-turn
conversation until it declares itself finished. It is then graded by running a
test suite it was never allowed to see.
python3 llama-eval.py \
--server http://localhost:8033 --model my-model \
--dataset agentic --threads 1 --temperature 0
The tools
| tool | purpose |
|---|---|
list_files |
enumerate the tree |
read_file |
read, with 1-based line numbers and optional line-range narrowing |
search |
regular-expression search across the repository |
edit_lines |
replace an inclusive line range |
edit_replace |
replace an exact snippet, which must be unique |
write_file |
write a whole file |
lint |
run the language's linter and type checker |
finish |
declare the change complete |
Three constraints are deliberate:
There is no shell and no way to run the tests. The only feedback channel is
lint. A syntax or type error can be driven out mechanically; a logic error has
to be reasoned about. A harness that lets an agent iterate against the test
suite measures something closer to search than to understanding.
There are two edit tools, one line-addressed and one content-addressed. Which one a model reaches for, and whether it keeps line numbers straight after its own earlier edit, is itself a signal.
The tests never touch the working tree. They are held outside it and copied into a throwaway copy only after the agent has stopped, so they can be neither read nor edited. Every tool path is resolved and confinement-checked against the repository root, so a symlink planted inside the tree cannot reach outside it.
Scoring
A task is resolved when every test that was failing now passes and nothing that was already passing broke. Both sets are derived by running the tests twice when the corpus is built -- once against the defective tree and once against the reference fix -- never hand-written, so a task whose defect no test exercises is rejected rather than shipped.
Because many tasks contain several independent defects, the fraction of target tests passed is reported alongside, which separates "fixed two of the three faults" from "changed nothing". Process metrics are reported too: turns, tool calls, rejected calls, edits, and the peak conversation size.
Dependencies
Nothing here is installed unless you select this suite, and then only for the
language you actually run. --agentic-lang python provisions ruff and mypy;
--agentic-lang typescript provisions the TypeScript compiler and uses node's
own test runner. Neither touches the other, and no other suite touches either.
Useful flags
| flag | meaning |
|---|---|
--agentic-lang |
restrict to one language, and provision only its toolchain |
--agentic-max-turns |
give up after this many assistant turns (default 50) |
--agentic-max-tokens |
cap generation per turn (default: no cap); bounds a model that falls into a repetition loop, but also cuts reasoning off mid-thought. Truncated turns are counted as truncated_turns |
--agentic-context-limit |
abandon an episode once its context exceeds this many tokens |
--agentic-transcripts DIR |
save each episode's full message history to DIR/<task_id>.json, plus a DIR/<task_id>.html showing every reasoning block, reply, tool call and tool result; the main report links to it |
Code suites (HumanEval, ClassEval)
--dataset humaneval runs the 164 OpenAI HumanEval problems; --dataset classeval runs the 100 ClassEval class-generation tasks. Unlike the other
suites these are not graded by matching text: the model's reply is parsed for a
```python block, that code is combined with the task's own unit tests,
and the result is executed.
ClassEval asks for a whole class rather than one function, so it is scored the way upstream does - class level (every one of the task's tests passes, which is what feeds pass@k) and method level (what fraction of all 2196 test methods passed). The second number separates "generated nothing usable" from "got most of the class right", and both are printed at the end of a run.
Give it more room to generate than HumanEval - a class with ten methods does not fit in a short budget:
python3 llama-eval.py \
--server http://localhost:8033 --model my-model \
--dataset classeval --threads 8 --n_predict 4096 --temperature 0
# pass@1, greedy
python3 llama-eval.py \
--server http://localhost:8033 --model my-model \
--dataset humaneval --threads 8 --n_predict 1024 --temperature 0
# pass@10 -- each extra multiple of 164 is another sample per problem
python3 llama-eval.py \
--server http://localhost:8033 --model my-model \
--dataset humaneval --n_cases 1640 --temperature 0.8 --top-p 0.95
--grader-type exec is the default for code suites, so neither it nor
--dataset-source normally needs to be passed.
Execution sandbox - no Docker required
Generated code is untrusted, but these suites need far less than a container to
run safely. The package set is derived from the dataset's own imports at
startup, not hardcoded and not taken from the upstream requirements.txt -
which for ClassEval lists torch, openai and transformers that its tests
never touch. So each suite installs exactly what it needs:
| suite | third-party packages |
|---|---|
| HumanEval | none - every problem imports only stdlib (typing, math, random, copy, string, collections, hashlib, re) |
| ClassEval | 13: beautifulsoup4, gensim, lxml, netifaces, nltk, numpy, openpyxl, pandas, pillow, pypdf2, python-docx, reportlab, scipy |
Two of ClassEval's are declared rather than derived, because no import
statement mentions them: lxml is selected by string
(BeautifulSoup(html, 'lxml'), ClassEval_44 - without it that task silently
drops from 23/23 to 7/23), and scipy is a hard gensim requirement. Note also
that PyPDF2 must not be modernised to pypdf: ClassEval_69's test imports
PdfFileReader, a name pypdf removed.
The venv is cached under ~/.cache/llama-eval/ (override with
LLAMA_EVAL_CACHE) and rebuilt only when the package set, interpreter version
or provisioning steps change; force one with --rebuild-sandbox. uv is used
when available, which makes even the ClassEval venv build in seconds.
Interpreter selection. A suite can prefer a different Python than the one
running the tool, and declares it: ClassEval asks for 3.13, because gensim
publishes no wheel past cp313 and its generated C no longer compiles on 3.14
(it still uses PyLongObject.ob_digit). That interpreter is found automatically,
including via uv python; --sandbox-python overrides it.
3.14 is fully supported anyway. ClassEval uses exactly two gensim APIs
(utils.decode_htmlentities, matutils.unitvec), both pure Python upstream, so
on 3.14+ the suite drops gensim from the install set and provisions a small shim
instead. Both paths score 100/100.
Offline provisioning. A suite that would otherwise fetch data at test time
declares a provisioning step that runs once at venv build time, while the
network is still up. ClassEval uses this to pre-seed exactly three nltk corpora
(punkt_tab, averaged_perceptron_tagger_eng, wordnet, ~33 MB), so the tests
themselves run with no network at all. Those are the current names - the
dataset asks for punkt and averaged_perceptron_tagger, which nltk has since
renamed, and seeding the legacy names too is not harmless: with a network
available nltk considers them satisfied and re-fetches the modern ones anyway.
The corpora are written inside the venv, not ~/nltk_data, because $HOME is
a tmpfs under the strongest isolation tier.
Each candidate program runs in a throwaway directory as its own process group, under the strongest isolation the host offers:
| tier | requires | isolates |
|---|---|---|
bwrap |
bubblewrap installed | read-only root, tmpfs /tmp and $HOME, no network |
unshare |
unshare --map-current-user |
no network |
unshare-root |
any unshare -r |
no network, but runs as uid 0 - see below |
rlimit |
always available | cpu/memory/file-size/process caps only |
Candidate code must not run as root, which is why unshare -r is a
last-resort tier that prints a warning. Root bypasses file permission bits, so a
test asserting that writing to a chmod 0444 file fails instead sees it
succeed - ClassEval_50 scores 14/16 under uid 0 and 16/16 under uid 1000, with
nothing in the output to suggest the sandbox caused it. Install bubblewrap.
All tiers additionally cap CPU time, address space (4 GiB), file size and core
dumps, and kill the whole process group on timeout (--exec-timeout, default
15 s - raise it for ClassEval, whose tests do real work). Writes to the
throwaway working directory are allowed, which is all the file-handling tasks in
ClassEval need.
Note that the weakest tier does not contain filesystem access. If you are scoring output from a model you do not trust, check the tier printed at startup.
Reproducibility
Several tasks draw their test inputs from the global, unseeded RNG, so a
verdict is not reproducible run to run - which adds noise exactly when you are
comparing two models or two quantisations. --exec-seed (default 0) seeds the
RNG before each test; pass --exec-seed -1 for the stock upstream behaviour.
This does not change what the model is asked to compute.
It matters more than it sounds. HumanEval/38, /50 and /53 are affected, and ClassEval_58 (duplicate mine placement) fails about 30% of the time on its own reference solution - measured here at 21/30 passes unseeded versus 30/30 seeded.
Dataset fidelity
The canonical dataset runs unmodified on current Python - all 164 reference
solutions pass on CPython 3.14 with no patches, so no forked or re-hosted copy
is needed. --dataset-source accepts a local .jsonl or a HuggingFace repo id
if you do want to point at a modified copy.
HumanEval does contain a handful of long-standing defects where the prompt contradicts the graded test (notably HumanEval/47, /116 and /148). Those penalise a model that reads the docstring carefully, but correcting them makes the benchmark marginally easier and the resulting scores no longer comparable with published HumanEval numbers, so nothing is patched by default.
ClassEval is different: the upstream data has defects that cap the achievable
score below 100% for reasons unrelated to the model, so --dataset classeval
defaults to a patched fork,
ilintar/ClassEval.
Six edits over five tasks lift the reference solutions from 96/100 classes and
2182/2196 test methods to 100/100 and 2196/2196. Three repair tests nothing
could pass:
- ClassEval_17 was a time bomb - a hard-coded 2024 date checked against
datetime.now(), unpassable since January 2024. - ClassEval_48 asserted
get_hostname('0.0.0.0') == 'LAPTOP-2CS86KUM', the dataset author's own machine name. - ClassEval_31 compared floats with
assertEqual, failing on a 4-ULP numpy difference.
The other three fix only the reference solutions (NumPy 2.0 removals in
ClassEval_51, a 30%-flaky mine generator in ClassEval_58) and cannot affect a
model's score. No skeleton was touched, so what the model is asked to write is
byte-identical to upstream. Pass --dataset-source FudanSELab/ClassEval for the
unpatched original.