A workload is a pinned answer to "what exactly was run". One JSON file per
workload, schema in schemas/workload.schema.json,
shape summarised in SPEC §4.
A result is only comparable to another result when both name the same
workload_id. That is the whole reason this directory exists: the model, the
quantization, the engine and the flags are recorded in the result, and the
question that was asked of them is recorded here.
Immutability
A published workload is never edited. Not the concurrency, not the number of requests, not the dataset it points at, not a typo in a number. Every existing result that references it was produced under the old definition, and silently changing it turns those numbers into a lie.
immutable: true and the mandatory -vN suffix say so in the data. The only
edits ever allowed on a published file are the ones that cannot change what runs:
name, description, notes.
Proposing a v2
- Copy the file to
<same-name>-v2.jsonand changeidto match the filename. - Set
"supersedes": "<the v1 id>". - Change what you meant to change, set
createdto today. - Leave the v1 file exactly as it is. It keeps its results; the site can show them side by side and mark the v1 cells stale rather than wrong.
- If the change is really "the dataset changed", the dataset needs a
-v2too (seedatasets/README.md) — a workload pointing at different bytes is a different workload.
Naming: <kind>-<shape>-v<N>, lower-case kebab, with the shape readable at a
glance (c8 = concurrency 8, i1k = ~1k input tokens, o256 = 256 output
tokens).
Kinds
| kind | what it measures | runner | required shape |
|---|---|---|---|
serving |
steady-state throughput and latency at a fixed concurrency | serving |
sweep and eval are null |
sweep |
one axis walked, a metric block per point | sweep |
sweep has exactly one axis |
prefill |
time to first token on a long input, almost no decode | prefill |
tiny output_tokens |
longctx |
behaviour as the prompt grows, with a retrieval check | longctx |
may carry a sweep and/or an eval block |
eval |
capability, scored per item | eval |
eval and dataset_id required |
image |
seconds per generated picture at one fixed render shape | image |
params carries the whole shape (below) |
image is the one kind whose shape lives entirely in params: width, height, steps,
seed, images_per_request and ref_images are all required there, because the same
prompts at another size or step count are a different measurement rather than the same one
run differently. Its dataset supplies prompts and, for the edit workloads, the reference
images they condition on. Scored image suites are not this kind: they are kind: eval
with an image scorer (ocr, clip, rgba, fidelity), they produce a scores block like
every other eval, and their render spec is frozen per case in the dataset row.
metrics_required lists the metric paths a result must carry non-null. Dotted
paths address into a distribution (ttft_ms.p50). For kind: eval the three
required names — accuracy, success_rate, avg_latency_ms — live in the
result's scores block rather than in metrics; success_rate there means "the
share of requests that completed at all", independently of whether the answers
were right.
All workloads
| id | kind | dataset | shape |
|---|---|---|---|
serve-single-i256-o256-v1 |
serving | prompts-mixed-v1 |
c1, n=50, in≈256, out=256 |
serve-short-c16-i128-o128-v1 |
serving | prompts-mixed-v1 |
c16, n=320, in≈128, out=128 |
serve-chat-c8-i1k-o256-v1 |
serving | prompts-mixed-v1 |
c8, n=200, in≈1k, out=256 |
serve-chat-c32-i1k-o256-v1 |
serving | prompts-mixed-v1 |
c32, n=400, in≈1k, out=256 |
serve-chat-c64-i1k-o256-v1 |
serving | prompts-mixed-v1 |
c64, n=640, in≈1k, out=256 |
serve-long-c4-i8k-o512-v1 |
serving | prompts-mixed-v1 |
c4, n=40, in≈8k, out=512 |
serve-code-c8-i2k-o1k-v1 |
serving | prompts-code-v1 |
c8, n=160, in≈2k, out=1k |
serve-prefix-c16-v1 |
serving | prompts-shared-prefix-v1 |
c16, n=200, grouped by prefix |
sweep-parallel-1-32-i512-o256-v1 |
sweep | prompts-mixed-v1 |
concurrency 1,2,4,8,16,32 |
sweep-parallel-1-64-i1k-o256-v1 |
sweep | prompts-mixed-v1 |
concurrency 1,2,4,8,16,32,64 |
sweep-parallel-1-256-i512-o256-v1 |
sweep | prompts-mixed-v1 |
concurrency 1,2,4,…,64,128,256 |
prefill-8k-v1 |
prefill | haystack-v1 |
c1, n=10, in=8k, out=16 |
prefill-32k-v1 |
prefill | haystack-v1 |
c1, n=10, in=32k, out=16 |
prefill-128k-v1 |
prefill | haystack-v1 |
c1, n=10, in=128k, out=16 |
longctx-depth-sweep-v1 |
longctx | haystack-v1 |
input_tokens 1k…256k, out=256, needle checked |
longctx-needle-32k-v1 |
longctx | eval-longctx-v1 |
c1, n=6, in=32k, needle scored |
longctx-needle-128k-v1 |
longctx | eval-longctx-v1 |
c1, n=6, in=128k, needle scored |
eval-math-v1 |
eval | eval-math-v1 |
numeric, max_out 4096 |
eval-reasoning-v1 |
eval | eval-reasoning-v1 |
exact (rows may be mc), max_out 2048 |
eval-code-v1 |
eval | eval-code-v1 |
code-exec, max_out 4096 |
eval-knowledge-v1 |
eval | eval-knowledge-v1 |
mc, max_out 2048 |
eval-instruction-v1 |
eval | eval-instruction-v1 |
instruction, max_out 2048 |
eval-json-v1 |
eval | eval-json-v1 |
json, max_out 2048 |
eval-tools-v1 |
eval | eval-tools-v1 |
json on tool_calls[0], max_out 2048 |
eval-vision-v1 |
eval | eval-vision-v1 |
vision, max_out 2048 |
eval-multilingual-v1 |
eval | eval-multilingual-v1 |
contains, max_out 2048 |
eval-longctx-v1 |
eval | eval-longctx-v1 |
needle, max_out 1024, c1 |
eval-format-v1 |
eval | eval-format-v1 |
exact, max_out 256 |
eval-math-v2 |
eval | eval-math-v2 |
numeric, max_out 4096, supersedes v1 |
eval-reasoning-v2 |
eval | eval-reasoning-v2 |
mixed scorers, max_out 4096, supersedes v1 |
eval-knowledge-v2 |
eval | eval-knowledge-v2 |
mc, max_out 1024, supersedes v1 |
eval-science-v2 |
eval | eval-science-v2 |
numeric, max_out 4096, new suite |
eval-commonsense-v2 |
eval | eval-commonsense-v2 |
mixed scorers, max_out 2048, new suite |
eval-security-v2 |
eval | eval-security-v2 |
mixed scorers, max_out 4096, new suite |
eval-longgen-integrity-v1 |
eval | eval-longgen-integrity-v1 |
integrity, max_out 3000, c1, n=36 |
eval-rag-grounded-v1 |
eval | eval-rag-grounded-v1 |
needle, max_out 2048, n=120, half abstain |
eval-tools-small-v1 |
eval | eval-tools-small-v1 |
json on tool_calls[0], multi-turn, n=100 |
t2i-single-1k-40s-v1 |
image | t2i-prompts-v1 |
1024², 40 steps, c1, 6 prompts × 3, warmup 1 |
t2i-single-2k-40s-v1 |
image | t2i-prompts-v1 |
2048² native 2K, 40 steps, c1, 6 × 3 |
edit-ref1-1k-40s-v1 |
image | t2i-prompts-v1 |
1024², 1 reference image, c1, 3 × 3 |
edit-ref4-1k-40s-v1 |
image | t2i-prompts-v1 |
1024², 4 reference images, c1, 3 × 3 |
eval-t2i-text-v1 |
eval | eval-t2i-text-v1 |
ocr, 6 cases, exact strings + CER |
eval-t2i-adherence-v1 |
eval | eval-t2i-adherence-v1 |
clip, 20 cases, CLIPScore ViT-L-14 |
eval-t2i-rgba-v1 |
eval | eval-t2i-rgba-v1 |
rgba, 4 cases, alpha channel checks |
eval-t2i-fidelity-v1 |
eval | eval-t2i-fidelity-v1 |
fidelity, 24 cases, vs a bf16 bundle |
Small-model tier
Three workloads (2026-09-22) for 1-3B models, chosen from what the existing small-model
results show rather than from a wish list. The evidence is google/gemma-4-E2B-it on two
engines (vLLM bf16 on a GB10, LM Studio MLX 4-bit on an M2 Max):
- Saturated for small models already:
eval-tools-v11.00 / 1.00,eval-format-v11.00 / 0.87,eval-json-v10.93 / 0.99 (its extraction category 40 of 40),eval-multilingual-v10.96 / 0.96,eval-knowledge-v10.94 / 0.98. Short extraction, classification and JSON-from-a-sentence suites would land in the same place, so there are none here. - Still spreading, so no "easy tier" is needed:
eval-math-v10.62 / 0.98 andeval-reasoning-v10.18 / 0.73. The v1 suites are the easy tier for this size. No small model has run a-v2suite yet; run them before concluding they sit at the floor. - Batch-1 latency is covered:
serve-single-i256-o256-v1already records TTFT and TPOT for one short request at a time (45 ms and 25.5 ms p50 for the vLLM run). A tiny-prompt variant would differ by a few milliseconds of prefill. - Not covered: throughput past 64 streams, reading a provided passage without
inventing (closed-book
eval-hallucination-v1is 0.07 for the MLX run, and nothing measures the grounded case), and tool use that is harder than copying arguments.
| id | measures | ~2B model at 100+ tok/s |
|---|---|---|
sweep-parallel-1-256-i512-o256-v1 |
The 1-32 sweep continued to 64, 128 and 256 streams. The vLLM run above still gained 1.7x from 16 to 32 streams with per-request decode only 16 % slower, and its 64-stream point was capped by --max-num-seqs 32, so the knee has not been measured for any small model. |
5-10 min |
eval-rag-grounded-v1 |
120 questions over 30 invented 80-110-word passages. Half are answerable (lookup with a same-type distractor, a one-step sum or difference, a stated condition), half are not (never mentioned, or given only for the sibling entity). Always declining scores 0.5. | 1-5 min |
eval-tools-small-v1 |
100 items with one or two tools: derived arguments (relative dates, spoken times, dozens, °F into a °C tool), a choice between near-identical tools, asking when a required argument is missing, using a result in the next call, answering from a result, revising a call. | 1-5 min |
The eval runtimes are wide because thinking decides them: without it the answers are a few dozen tokens each; with it a small model can spend a few hundred to a thousand tokens per item at 4 streams. The sweep sends 2,112 requests of 256 output tokens.
Running them: the sweep only measures a point if the engine admits that many sequences
(vLLM --max-num-seqs, SGLang --max-running-requests, llama.cpp --parallel), and
prompts repeat within a point from 64 streams up, so run it with prefix caching off or
say that it was on. The tools suite needs the engine's tool-call parser enabled, and the
parser is part of the configuration.
Conventions a runner must honour
paramsis the contract,descriptionis prose. Anything a runner needs is a key inparams, never only in the text.input_tokensis nominal. It says which prompts to pick, not what the tokenizer will produce. Record the real count inmetrics.input_tokens_total.dataset_buckets(serving/sweep) filtersprompts-*rows by length bucket;dataset_categoriesanddataset_target_tokensfilter eval rows.- Sweep points. For the two parallelism sweeps, requests per point are
max(params.num_requests, concurrency * params.requests_per_concurrency), so a low-concurrency point is not dominated by warmup.params.concurrencyis the first point of the axis and is otherwise ignored. serve-prefix-c16-v1must send all rows of oneprefix_idback to back (params.group_by), must not shuffle, and excludeswarmup_per_grouprequests per group from the aggregate. Run it twice on the same engine, with and without the prefix-cache flag, and comparettft_ms.longctx-depth-sweep-v1checks the needle at every point. A wrong needle answer is a failed request — it lowerssuccess_rateand adds afailures[]entry with categorymalformed-output. A point that exceeds the servedmax-model-lenis recorded as a failure with categorycontext-overflow, never omitted, because "did not fit" is a result.- Eval concurrency does not change the score, only the wall clock. It is 4
everywhere except
eval-longctx-v1, which runs at 1 to keep KV cache pressure honest, andeval-longgen-integrity-v1, where concurrency 1 is part of the configuration under test: batching changes the scheduler, the KV-cache layout and the attention path, so a run at another concurrency measures something else. eval-longgen-integrity-v1scores token integrity, not correctness.accuracyis the share of 150-to-290-line generations that came back with no spliced token. A clean run of 36 is not proof of absence: at a one-in-five per-generation rate a clean pass is a plausible outcome for an affected build, so report the run rather than the absence.- Image workloads run one at a time.
concurrencyis 1 and the runner does not parallelise: a transformer over thousands of latent tokens saturates a single GPU on its own, so a second request in flight measures the queue. Each prompt is renderedrepeattimes ands_per_imageis the distribution over every measured render;warmup_requestsare rendered first and excluded. Where a lane reports its own inference time (timing.total_ms) that is used instead of the wall clock, and which of the two applied is inraw.payloadper render. guidance: nullmeans the lane's own default. Guidance above 1 runs a second transformer forward per denoising step, so two lanes whose defaults differ are doing different amounts of work; record the effective value in the result'sargsbefore comparing seconds per image.- No generated image is ever stored. The runners render into a temporary directory and
delete it; results carry numbers, and for the fidelity suite a 64-bit perceptual hash.
scores.items[].predictedis null on image suites — an OCR transcription is the generated content read back, and it does not belong in the repository either. - The fidelity suite needs a local bf16 reference.
atlas-bench t2i-reference --spec bf16.json --out DIRrenders it once on the same box; every quantized run then passes--reference-bundle DIR. The scorer compares the render digest first and refuses to score a case whose reference came from a different spec. params.reasoning: "default"means "leave the engine's own default alone". Whatever it was, record it in the result'sargs— reasoning settings move eval scores and latency more than most flags.- Scorer names.
eval.scoreruses the kebab-case vocabulary ofworkload.schema.json(code-exec); dataset rows use the snake_case scorer module name from SPEC §8 (code_exec). They mean the same thing. Always prefer the row's ownscorerwhen a dataset mixes them.
Adding a workload
A new workload is a PR that adds one file. Before writing it, check the table
above: a workload that differs from an existing one only in num_requests splits
the coverage map for no gain. Good reasons to add one: a new axis (a concurrency
the sweeps do not reach), a new dataset, or a shape that models a real deployment
nobody has captured yet.
Checklist:
- id ends in
-v1, filename equalsid + ".json"; dataset_idexists underdatasets/;metrics_requiredonly names metrics the runner can actually fill for that kind;immutable: true,createdtoday,supersedesnull;pnpm validatepasses.