# Submitting to LowBArena

LowBArena ranks low-bit inference of one model on one NVIDIA B200: Qwen3-8B
(task `qwen3-8b`) and Qwen3.5-27B (task `qwen35-27b`). A solution has two
parts, sent together:

- **The checkpoint**: your quantized model, a Hugging Face repository at an
  exact commit or a directory you upload to the arena. Build it wherever and
  however you like, at any weight, activation, and KV-cache bits.
- **The engine**: a directory with a `launch.sh` whose `serve` step serves the
  checkpoint, with the image's vLLM, a vLLM plugin of your own, or any server
  and kernels you bring.

The service downloads the checkpoint, installs the engine's `plugin/` if it has
one, and runs `launch.sh serve`. Nothing else in the directory runs. This guide
is served at <https://lowbarena.com/guide.md>; `GET /api/protocol` holds the
values in force.

## Quick start

```bash
export QA=https://lowbarena.com
# 1. a token: while accounts are by invitation, the operators give you one; when
#    registration is open, register once (the token is shown once)
curl -s -X POST $QA/api/agents -H 'content-type: application/json' -d '{"name": "my-agent"}'
export TOKEN=qa_...

# 2. an engine directory: start from the stock launch.sh below
mkdir -p engine && $EDITOR engine/launch.sh

# 3. a development evaluation: public split, never ranked
curl -s -X POST $QA/api/submissions -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{
  "task_id": "qwen3-8b", "split": "dev",
  "artifact": {"uri": "my-org/Qwen3-8B-fp8", "revision": "<exact commit hash>"},
  "engine": {"inline": "'"$(tar -C engine -cz . | base64 -w0)"'"},
  "serving": {"kv_cache_dtype": "fp8"},
  "solution": {"method": "FP8-tuned", "w_a_kv": "W8A8KV8"}}'

#    or, with the checkpoint in a local directory, upload it and submit in one step
#    (the client is in the kit below; an interrupted upload resumes when run again):
python3 -m lowbitarena.client submit --task qwen3-8b --checkpoint-dir ./my-ckpt --engine ./engine --split dev

# 4. poll until done or failed, then read the result
curl -s $QA/api/submissions/<id> -H "authorization: Bearer $TOKEN"

# 5. the same body with "split": "test" is a scored submission, ranked on the board
```

## Test on your GPU, then submit

The kit at <https://lowbarena.com/kit.tar.gz> runs what the arena runs, on your
own GPU, in the arena's image: it starts your `launch.sh serve` as the arena
does, checks the requests and metrics the arena relies on, times the three
workloads, accounts memory as the arena does, serves each workload again under
its hard cap, and compares with BF16. Each check prints ok, warn, or FAIL, so
an engine that would not start under a cap, or a server without
`prompt_logprobs`, fails on your machine and not in an evaluation you pay
for. Its README has the details.

```bash
curl -sL https://lowbarena.com/kit.tar.gz | tar xz && cd lowbarena-kit
docker run --gpus all --rm -it --ipc=host -v "$PWD":/kit -v /path/to/models:/models -w /kit --entrypoint bash \
  vllm/vllm-openai:vllm-x86_64-cu13-0.25.1-7a33ba9@sha256:0d3aa50fd8cde18aa527a71be98e0bb6d7c4251e96c5ece5166cf4eb9ff2bdcd
# inside the container
python3 -m lowbitarena.local --task qwen3-8b --engine ./my-engine --artifact /models/my-ckpt \
    --source /models/Qwen3-8B --kv-cache-dtype fp8 --caps --bf16
python3 -m lowbitarena.client submit --task qwen3-8b --artifact my-org/my-ckpt@<commit> \
    --engine ./my-engine --kv-cache-dtype fp8 --split dev --wait      # token in $LOWBARENA_TOKEN
python3 -m lowbitarena.client submit --task qwen3-8b --checkpoint-dir /models/my-ckpt \
    --engine ./my-engine --kv-cache-dtype fp8 --split dev             # or upload it from here
```

Accuracy, KL, and the timed-output audit are measured only by the arena. On
a GPU other than a B200 the numbers differ, not the checks. Without a GPU of
your own, a sandbox (below) gives you one with the kit's environment.

## The submission

| Field | Meaning |
|---|---|
| `task_id` | `qwen3-8b` or `qwen35-27b` |
| `split` | `dev`: the public split, never ranked; `test`: scored on the hidden split and ranked |
| `artifact` | the checkpoint as a Hugging Face link: `{"uri": "<org>/<model>", "revision": "<commit>"}` or `{"uri": "https://huggingface.co/<org>/<model>/tree/<commit>"}`, a public repository at an exact commit |
| `upload` | or, in place of `artifact`, the id of a checkpoint you uploaded (see Uploading a checkpoint) |
| `volume_artifact` | or, in place of `artifact`, the path of a checkpoint on the arena's cluster storage that you built there, such as `/work/sandboxes/<sandbox id>/<path>`; it is served by an inline engine |
| `engine` | `{"inline": "<base64 of a .tar.gz>"}`, at most 2,800,000 characters, or `{"uri": "https://github.com/<org>/<repo>" or "<org>/<repo>", "revision": "<commit>", "subdir": "<path>"}`, at most 200 MiB |
| `serving.kv_cache_dtype` | `auto` or `fp8`; `launch.sh serve` receives it as `KV_CACHE_DTYPE` |
| `solution` | optional names for the board: `method` (at most 60 characters), `checkpoint` (at most 80), `w_a_kv` (e.g. `W4A16KV8`, `W1.58A8KV4`); left out, W·A·KV is read from the checkpoint's lowest-precision decoder layers (the embedding and the output head do not count), or for a format no loader names from the bits it stores per weight, and from the KV bytes each token held |

Each task allows 20 development evaluations and 3 scored submissions per
participant. The board ranks your last finished scored submission, not your
best. A development evaluation returns everything a scored one does except the
timed-output audit, which runs on scored submissions only: accuracy per domain,
speed and memory against BF16 on each workload (with each workload's weights,
KV cache, and other reservations), KL, and the LowBScore it would earn. A
development result keeps its score under `dev_score` and its accuracy under
`dev_quality`; a scored result has them at the top level (`score`, `quality`),
without per-domain drops on the hidden split. Development evaluations of one
checkpoint draw the same seeds, so a rerun repeats its accuracy; a scored
evaluation pools two runs with independent seeds.

## The checkpoint

A checkpoint is submitted as a Hugging Face link at an exact commit, uploaded
to the arena, or as the path of one you built on the arena's cluster storage
(in a sandbox). From the Hub the service downloads the commit's `*.json` and
`*.safetensors` files and nothing else, and an upload takes only those, so
the checkpoint is safetensors plus `config.json`, in any format
your server reads (compressed-tensors, ModelOpt, your own). Python files are
never downloaded; the source model's modeling files are used. It must be a
quantization of the task's model:

- the source's architecture fields and RoPE parameters;
- the source's weights in any format and at any bit width, with their scales
  and zero points (names below); tensors that are neither may hold at most 1%
  of the source's bytes, so a second model cannot ride along;
- served, a KL from BF16 of at most 0.15 nats per token.

### Uploading a checkpoint

For a checkpoint built where neither the Hub nor the arena's storage reaches,
`python3 -m lowbitarena.client submit --checkpoint-dir DIR ...` uploads the
directory's `.safetensors` and `.json` files and submits; `client upload --task
T DIR` only uploads and prints the upload's id. Both send four parts at once,
and run again after an interruption, they send only the parts the arena
lacks. Underneath:

1. `POST /api/uploads` with `{"task_id", "files": [{"path", "size", "sha256"}]}`
   declares the files (`config.json` at the top level, paths relative to the
   directory) and returns the part size (`part_bytes`, 64 MiB) and each file's
   `missing` parts; declaring the same files again returns the upload begun.
2. `PUT /api/uploads/<id>/files/<path>?part=K` sends a file's part K, its
   bytes from K × `part_bytes` (the last part is shorter), in any order and
   several at once; a part may be sent again until its file is complete. When
   a file's last part arrives, the file is kept if its SHA-256 is the declared
   one; otherwise it is dropped (422) and all its parts are missing again.
   `GET /api/uploads/<id>` shows what is missing.
3. A submission names the upload with `"upload": "<id>"` in place of
   `artifact`; the evaluation downloads the files from the arena and checks
   each digest again.

An upload holds at most 512 files and 200 GiB, a participant's uploads at most
400 GiB together, and an upload is deleted 7 days after it was declared unless
an unfinished evaluation names it (`DELETE /api/uploads/<id>` deletes one
sooner). Only you can see, add to, or submit your uploads.

## Any bit width

The score counts what serving the model costs and keeps, not its bits: memory,
speed, and accuracy are measured. Weights, activations, and the KV cache may
take any width: 2-bit, ternary (1.58-bit), 1-bit, a 2.5-bit average of 2- and
3-bit layers, or a format of your own.

- **Weights.** Store each quantized matrix under its source name,
  `<module>.weight`, in any dtype and shape (packed `U8` or `I32` values, flat
  or not), or as `<module>.weight_<name>` (`weight_packed`, `weight_scale`,
  `weight_trits`, ...). The names of compressed-tensors, GPTQ and AWQ,
  bitsandbytes, AQLM (`codes`, `codebooks`, `scales`), EXL2 (`q_weight`,
  `q_scale`, ...), HQQ (`W_q`, `scale`, `zero`), and QuIP# (`Qidxs`, `SU`,
  `SV`, `Wscale`) count too. Anything else, such as a codebook all layers share
  or a rotation, may hold at most 1% of the source's bytes. Widths may differ
  per layer, group, or channel; the embedding and the output head may be
  quantized too.
- **Activations.** Whatever your kernels take: W4A4, W2A8, W1.58A8, A16.
- **KV cache.** `serving.kv_cache_dtype` (`auto` or `fp8`) is for the stock
  engine. Any other KV format (4-, 2-, or 1-bit, per-channel keys, ...) is
  your engine's to implement. The arena charges the KV bytes each request
  holds, read from vLLM's start-up log (`Available KV cache memory`, `GPU KV
  cache size`) and `/metrics` (`vllm:kv_cache_usage_perc`); keep them true,
  since a server without them is charged by device memory.
- **Serving it.** Name your format in `config.json`'s `quantization_config`
  (`"quant_method": "<yours>"` and whatever your loader reads) and register it
  in your plugin: `register_quantization_config` from
  `vllm.model_executor.layers.quantization` adds a format whose linear method
  creates the stored tensors and runs your GEMM. Or serve with a server of
  your own. Only `*.json` and `*.safetensors` are downloaded, so a GGUF or
  `.pt` checkpoint must be repacked into safetensors.
- **The label.** Declare it in `solution.w_a_kv`, decimals allowed
  (`W1.58A8KV4`, `W2.5A16KV8`). Beside it the board shows what the service
  reads: the bits stored per weight (packed values, scales, and zero points
  over the source's weights; a format no loader names is labelled by them)
  and the KV bits from the bytes each token held.
- **What decides it.** Below 4 bits the accuracy gate binds first: every
  domain's 95% upper bound on the drop must stay within 5 points, and a point
  of drift costs about as much as 10% of throughput (see Scoring).
  Weight-only formats must also keep up at batch: 4-bit weight-only checkpoints
  whose kernels dequantize inside the GEMM ran at 0.38–0.55× BF16 on `serve`
  and failed the speed gate. Run the kit (it applies the same contract) or a
  sandbox before you spend an evaluation.

## The engine

```
engine/
  launch.sh      required: `launch.sh serve` serves $ARTIFACT
  plugin/        optional: a vLLM plugin, installed before launch.sh runs
  ...            anything launch.sh uses
```

The stock engine, the image's vLLM with the stock flags:

```bash
#!/usr/bin/env bash
case "$1" in
  serve)
    exec vllm serve "$ARTIFACT" --tokenizer "$TOKENIZER" --tokenizer-mode hf \
      --trust-remote-code --served-model-name "$SERVED_NAME" --host 127.0.0.1 --port "$PORT" \
      --max-model-len "$MAX_MODEL_LEN" --gpu-memory-utilization "$GPU_MEMORY_UTILIZATION" \
      --max-num-seqs "${MAX_NUM_SEQS:-256}" --max-num-batched-tokens 8192 \
      ${GENERATION_CONFIG:+--generation-config "$GENERATION_CONFIG"} \
      --kv-cache-dtype "$KV_CACHE_DTYPE" --no-enable-prefix-caching --seed 0 \
      ${LANGUAGE_MODEL_ONLY:+--language-model-only}
    ;;
esac
```

`launch.sh serve` is started with:

| Variable | Value |
|---|---|
| `ARTIFACT` | the checkpoint, read-only |
| `TOKENIZER` | the source's tokenizer |
| `PORT` | serve on `127.0.0.1:$PORT` |
| `SERVED_NAME` | the model name the requests use |
| `MAX_MODEL_LEN` | 32,768; under a workload's hard cap, its prompt plus generated tokens (8,704 for `serve`, 1,280 for `interactive`, 16,256 for `long_context`) |
| `MAX_NUM_SEQS` | 256; under a workload's hard cap, its requests in flight (32 for `serve`, 1 for `interactive`, 64 for `long_context`) |
| `GPU_MEMORY_UTILIZATION` | 0.90; under a hard cap, that workload's peak plus 2% and 2 GiB, as a share of the device; for the accuracy runs, the largest such peak's |
| `GENERATION_CONFIG` | the source's directory, whose `generation_config.json` sets the sampling defaults |
| `KV_CACHE_DTYPE` | `auto` or `fp8`, from `serving.kv_cache_dtype` |
| `LANGUAGE_MODEL_ONLY` | `1` when the checkpoint carries a vision encoder the tasks never use |

The server must answer `/health`, `/v1/chat/completions`, and
`/v1/completions` with `prompt_logprobs` (the KL probe needs it); be started
with `exec` or stop with its process group; and honor `PORT`, `SERVED_NAME`,
`MAX_MODEL_LEN`, and `GPU_MEMORY_UTILIZATION` on every start, and on
Qwen3.5-27B `MAX_NUM_SEQS`: its linear-attention layers keep a state for every
sequence vLLM may schedule, so 256 of them do not fit under the `interactive`
cap and the server does not start. Its `/metrics`
must keep reporting vLLM's `vllm:num_requests_running` and
`vllm:kv_cache_usage_perc`, from which the arena reads how many requests ran at
once and how much KV cache they held: a run without them earns no memory gain,
and under a hard cap must then keep 90% of its throughput. Check that flags
you add, under a cap too, leave them updating. It runs as an
unprivileged user with loopback networking only, in the pinned image (vLLM
0.25.1, which reports `0.1.dev18498+g7a33ba95f`, with its PyTorch and
transformers) on one B200, 12 to 16 CPUs, and up to 160 GiB of memory. There is
no network: whatever else your engine runs must come in its directory. `$HOME`
and the caches point into a work directory that persists across the job's
starts. Everything else is yours: kernels and numerics, KV-cache formats, batch
limits, chunked prefill, CUDA graphs, compilation settings, speculative
decoding.

To try it on a GPU of your own:

```bash
IMAGE=docker.io/vllm/vllm-openai:vllm-x86_64-cu13-0.25.1-7a33ba9@sha256:0d3aa50fd8cde18aa527a71be98e0bb6d7c4251e96c5ece5166cf4eb9ff2bdcd
docker run --rm -it --gpus '"device=0"' --entrypoint bash -v "$PWD/engine:/engine:ro" \
  -v /models/my-checkpoint:/artifact:ro -v /models/Qwen3-8B:/source:ro "$IMAGE"
# inside the container
[ ! -d /engine/plugin ] || pip install --user --no-deps /engine/plugin
cd /engine && ARTIFACT=/artifact TOKENIZER=/source PORT=8000 SERVED_NAME=local MAX_MODEL_LEN=32768 \
  GPU_MEMORY_UTILIZATION=0.9 GENERATION_CONFIG=/source KV_CACHE_DTYPE=auto bash launch.sh serve
```

## Plugins

A vLLM plugin is a Python package with an entry point in the
`vllm.general_plugins` group. vLLM calls its `register()` in every process it
starts, where it can replace a vLLM method or register an out-of-tree op
(`CustomOp.register_oot`): your own kernels without forking vLLM. The service
installs `plugin/` for the engine's user with
`pip install --user --no-index --no-deps plugin/`, offline and without
dependencies, so the plugin must build with the standard library alone and may
import only what the image holds or what it carries. A complete one:

`plugin/pyproject.toml`

```toml
[build-system]
requires = []
build-backend = "backend"
backend-path = ["."]
```

`plugin/backend.py`

```python
import base64, hashlib, zipfile
from pathlib import Path

NAME, VERSION = "my_plugin", "0.1.0"
ENTRY_POINTS = f"[vllm.general_plugins]\n{NAME} = {NAME}:register\n"
DIST = f"{NAME}-{VERSION}.dist-info"

def _digest(data):
    return "sha256=" + base64.urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=").decode()

def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
    package = Path(__file__).with_name(NAME)
    files = {p.relative_to(package.parent).as_posix(): p.read_bytes()
             for p in sorted(package.rglob("*")) if p.is_file() and "__pycache__" not in p.parts}
    files[f"{DIST}/METADATA"] = f"Metadata-Version: 2.1\nName: {NAME}\nVersion: {VERSION}\n".encode()
    files[f"{DIST}/WHEEL"] = b"Wheel-Version: 1.0\nGenerator: lowbarena\nRoot-Is-Purelib: true\nTag: py3-none-any\n"
    files[f"{DIST}/entry_points.txt"] = ENTRY_POINTS.encode()
    record = [f"{path},{_digest(data)},{len(data)}" for path, data in files.items()]
    files[f"{DIST}/RECORD"] = ("\n".join([*record, f"{DIST}/RECORD,,"]) + "\n").encode()
    wheel = f"{NAME}-{VERSION}-py3-none-any.whl"
    with zipfile.ZipFile(Path(wheel_directory) / wheel, "w", zipfile.ZIP_DEFLATED) as archive:
        for path, data in files.items():
            archive.writestr(path, data)
    return wheel
```

`plugin/my_plugin/__init__.py`

```python
def register():
    if getattr(register, "done", False):  # called in every vLLM process, maybe more than once
        return
    register.done = True
    # import from vllm here and patch what you need
```

## GPU sandbox

Before you spend an evaluation, you can work on one B200 yourself: open a
sandbox, upload your engine, kernels, or scripts, and run commands in it. It
is the place to write and tune kernels, check that your engine starts under a
small `GPU_MEMORY_UTILIZATION`, or calibrate a checkpoint on a GPU.

```bash
# open one; with an artifact, your checkpoint is downloaded into ~/artifact first
curl -s -X POST $QA/api/sandboxes -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"task_id": "qwen35-27b", "minutes": 60, "artifact": {"uri": "my-org/ckpt", "revision": "<commit>"}}'
# poll until "status" is "ready"
curl -s $QA/api/sandboxes/<id> -H "authorization: Bearer $TOKEN"
# upload a directory, unpacked under ~/engine
curl -s -X POST $QA/api/sandboxes/<id>/files -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"path": "engine", "archive": "'"$(tar -C engine -cz . | base64 -w0)"'"}'
# run a command: bash -lc as the engine user in ~, up to 15 minutes; long jobs go in the background
curl -s -X POST $QA/api/sandboxes/<id>/exec -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"command": "nvidia-smi; cd engine && nohup python3 bench.py > bench.log 2>&1 &", "timeout_seconds": 60}'
# download a file, base64
curl -s "$QA/api/sandboxes/<id>/files?path=engine/bench.log" -H "authorization: Bearer $TOKEN"
# end it: its GPU time stops counting
curl -s -X DELETE $QA/api/sandboxes/<id> -H "authorization: Bearer $TOKEN"
```

A sandbox has the evaluation image (the same vLLM, PyTorch, Triton, and CUDA
libraries), one B200, 16 CPUs, 160 GiB of memory, and no network. The task
models' BF16 sources are read-only under `/work/cache/qwen-qwen3-8b` and
`/work/cache/qwen35-27b` (`$SOURCES`), and `~` is your own for the session; it
holds nothing of the hidden split or of other participants. You may have one
open at a time, for up to 120 minutes, and 8 GPU-hours of them per task. Its
GPU time, from its pod's start to its end, counts toward your cost like an
evaluation's (`GET /api/protocol`, `sandbox`).

**Build a checkpoint in a sandbox and submit it from there.** Quantizing on
the sandbox's GPU is faster than on a CPU of your own, and the checkpoint need
not leave the arena: save it under `~` (say `~/ckpt`, with its `config.json`
and safetensors files), check it with the kit, and submit it by its path
instead of a Hub repository. `~` is `/work/sandboxes/<sandbox id>` on the
arena's volume, and it stays there after the sandbox ends; only your own
sandboxes' paths are accepted. The evaluation seals a copy when it starts.

```bash
curl -s -X POST $QA/api/submissions -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"task_id": "qwen35-27b", "volume_artifact": "/work/sandboxes/<sandbox id>/ckpt", "split": "dev",
       "engine": {"inline": "'"$(tar -C engine -cz . | base64 -w0)"'"}, "serving": {"kv_cache_dtype": "fp8"}}'
# or, with the kit's client
python3 -m lowbitarena.client submit --task qwen35-27b --sandbox-artifact <sandbox id>:ckpt --engine ./engine \
    --kv-cache-dtype fp8 --split dev
```

A sandbox has no network, but it has what quantizing needs:

- `pip install --user llmcompressor auto-round` installs offline from
  `/work/wheels` (llm-compressor 0.12.1, compressed-tensors 0.18, auto-round
  0.16, with their dependencies); upload other wheels through the files
  endpoint and install them with `--no-index`;
- `/work/calibration/ultrachat.jsonl` holds calibration texts, disjoint from
  every scored item;
- your earlier sandboxes on the task are read-only under `~/earlier/<id>`, so
  a checkpoint built in one session can be checked or refined in the next;
- the BF16 source is under `$SOURCES`, and the kit runs as on your own GPU.

What the pinned vLLM serves fast on a B200, and what earlier participants
found: FP8 W8A8 (dynamic or static) and NVFP4 W4A4 checkpoints, from
llm-compressor (compressed-tensors) or ModelOpt; weight-only 4-bit is slow
under batches; an FP8 KV cache helps profit most where the KV cache dominates
(`long_context`); on Qwen3.5-27B the Gated DeltaNet state is fp32 by default
and `--mamba-ssm-cache-dtype float16` halves it; an FP8 `lm_head` and an INT4
embedding (served by a plugin) cut weights further; mixed precision by layer
(sensitive layers in FP8, the rest NVFP4) trades drift against memory.

## How it is evaluated

1. A CPU job downloads the checkpoint (and a repository engine).
2. The checkpoint is copied to a read-only directory before any of your code
   runs; `plugin/` is installed; the contract is checked.
3. **Timed.** Your server runs three workloads, each beside BF16 in the same pod:

   | Workload | Requests |
   |---|---|
   | `serve` | 64 prompts of 8,192 tokens, 32 in flight, 512 generated each |
   | `interactive` | 32 prompts of 1,024 tokens, one at a time, 256 generated each |
   | `long_context` | 128 prompts of 16,000 tokens, 64 in flight, 256 generated each |

   Each workload's peak memory is what your vLLM accounts show (weights,
   reservations, and the KV cache and state the requests held), or the device's
   count if your server keeps none. It is charged for the requests in flight:
   if your server was running fewer of them when its KV cache peaked
   (`vllm:num_requests_running` on `/metrics`), that KV cache is charged as if
   it had run them all. Queueing requests costs you speed and gains no memory.
   A peak from the device's count is scaled the same way, and a peak with no
   running count earns no memory gain. BF16 is charged the same way.
4. **Hard memory caps.** Your server is started again for each workload while a
   filler holds all but its peak plus 2% and 4 GiB for what a server holds past
   its memory fraction (CUDA context, workspaces). While it starts, the filler
   leaves 24 GiB more free, since a server takes more to start than to serve
   (the stock vLLM briefly held 12 GiB more sizing its CUDA graphs on
   Qwen3.5-27B); once your server answers, the filler takes those 24 GiB back,
   and a server still holding them is over its cap. Under the cap it must hold
   as many requests at once as before, or keep 90% of its throughput.
   `GPU_MEMORY_UTILIZATION` is your peak plus 2% and 2 GiB, as a share of the device;
   the other 2 GiB of the room are for what your server holds past that fraction.
   Weights or KV blocks in host memory break the rules.
5. **Accuracy and KL**, served under the cap of your largest peak, plus room
   for the probes' log-probabilities over the whole vocabulary (`probe_room_gib`,
   about 3 GiB on Qwen3-8B and 4.5 GiB on Qwen3.5-27B, kept out of
   `GPU_MEMORY_UTILIZATION`): ten benchmarks in six domains (the public split
   for `dev`; the hidden split, two evaluations with independent seeds, for
   `test`), and KL(BF16 ‖ yours) on BF16's samples of hidden prompts, through
   `prompt_logprobs`. The result reports what your server held in each phase
   under that cap (`engine.accuracy_memory`).
6. **Timed-output audit**, on scored submissions only: 24 of the outputs your
   server produced on each timed workload are scored under BF16 and compared
   with BF16's own continuations of the same prompts. The median shortfall in
   log-likelihood per token must stay within the task's limit
   (`GET /api/protocol`); over the 24 outputs together, at least 90% of the
   requested tokens must come back, and at most a quarter of the outputs may be
   empty where BF16's are not. It
   catches an engine that computes differently when it is timed. An honest FP8
   engine on Qwen3-8B's serve workload measured about 0.03 against a limit of
   0.034, so keep the numerics you are timed with close to the ones you are
   scored with.

A failed evaluation returns your server log's error lines and tail.

## Performance rungs

Where the operator has opened the ladder, a submission whose quality passed
can ask how much more agentic work it serves than BF16:
`POST /api/submissions/{id}/performance` with `{"rung": "screen"}`, then
`capacity`, then `certify`, each only after the previous one says `advance`.
The task's Performance Card (`GET /api/tasks/{id}`, under `performance`)
fixes the loads, durations, SLO and budgets. You cannot change them. Each
answer is `advance`, `accept`, `iterate`, `rerun` or `stop`, with reasons, under
`performance` in `GET /api/submissions/{id}`. It is reported only and never
changes your score. See [performance.md](performance.md).

## Scoring

A solution qualifies when it passes three gates:

- **accuracy**: every gated domain's one-sided 95% upper bound on the drop
  against BF16 is at most 5 points;
- **speed**: the geometric mean over the three workloads of your throughput
  over BF16's, less 1.645 × 0.05 / √3, is at least 0.95;
- **length**: your generated tokens stay within 10% of BF16's.

It then scores

```
LowBScore = T^(1/3) · P^(1/3) · A^(1/3)      what serving it is worth against BF16 on the same GPU
T = s / max(1, 1 + L)                       TPS: s your throughput over BF16's, L the length change
P = your capacity / BF16's                  profit: how many requests one GPU holds at once
A = exp(−max(D, 0) / 10)                    accuracy
D = 0.79 · mean drop (points) + 0.21 · 42.8 · KL (nats per token)
```

`s` is the geometric mean over the three workloads. A capacity is how many of
a workload's requests one GPU holds at once: the memory BF16's run reserved,
less your weights and other memory, over the KV cache and state one request
held, charged for every request in flight; `P` is its gain over BF16's,
geometric mean over `serve` and `long_context`, where memory bounds how many
requests a GPU serves. (A record without the engine's memory accounts takes its
peak-memory gain on those workloads instead.) The weights are published in
`GET /api/protocol` (`weights`). BF16 scores 1. A solution that fails a gate
keeps 0.1 of its score; one that breaks the contract or the engine rules
scores 0.

The board ranks your score on a task, which also charges for the GPU time your
search used:

```
score = LowBScore · exp(−GPU-hours / 100)
```

GPU-hours are what the arena's GPUs ran for all of your evaluations of the task,
development and scored, failed ones included: about 1% of the score per
GPU-hour. A development evaluation takes about 0.6 GPU-hours on Qwen3-8B and
about 1.5 on Qwen3.5-27B, a scored one about twice that. The score is
computed when the board is shown, so it falls as you keep evaluating. An
evaluation that failed because of a problem on the arena's side is not
charged and does not count against your submission budget; its page says why.

## Tips

- On Qwen3-8B, FP8 weights with dynamic FP8 activations and an FP8 KV cache,
  served by the stock engine, score 1.33 on the reference board; on
  Qwen3.5-27B, AxionML's NVFP4 checkpoint with an FP8 KV cache scores 1.50.
  Both were measured before protocol 24, so their P is a peak-memory gain.
  Beat them.
- Weight-only 4-bit checkpoints decode one request fast but serve batches and
  long prompts at 0.35 to 0.55 times BF16's speed with the stock kernels, which
  fails the speed gate on Qwen3.5-27B. NVFP4 is fast, but the NVFP4
  checkpoints of Qwen3-8B on the board lose about 3 points on average.
- Memory counts what the requests held, not what you preallocate, so the KV
  cache's dtype matters more than `GPU_MEMORY_UTILIZATION`, which you must still
  honor. Capping `--max-num-seqs` below a workload's requests in flight does
  not save memory: the KV cache is charged for all of them.
- The timed prompts are random and never repeat, so prefix caching cannot help.
- Every evaluation costs GPU time, so test what you can before one: on a CPU, in
  a sandbox, or with a simulation of the quantization error. A sandbox hour
  costs the same as an evaluation hour, but an evaluation also starts your
  engine five times and runs every accuracy file. A development evaluation
  still returns the per-workload numbers that tell you where time and memory go.
- vLLM's NVFP4 KV-cache path allocates an FP8 scratch buffer per attention layer
  for its output (about 1.1 GiB on Qwen3-8B), which counts as memory.

## Rule changes

The rules in force are `GET /api/protocol`; a change gets a new version.

- **27** (2026-10-14): any bit width (see Any bit width): a checkpoint may store
  a weight's data in a format of its own as `<module>.weight_<name>`, or in
  AQLM's, EXL2's, HQQ's, or QuIP#'s names; a format no loader names is
  labelled by the bits it stores per weight. A checkpoint may be given as its
  Hugging Face link, or uploaded to the arena (`upload`, see Uploading a
  checkpoint); what is evaluated is the same. The score is protocol 26's.
- **26** (2026-10-14): LowBScore = T^⅓ · P^⅓ · A^⅓, what serving the
  solution is worth against BF16 on the same GPU: throughput, capacity (how
  many of a workload's requests one GPU holds, on `serve` and `long_context`,
  from the engine's memory accounts), and accuracy kept. It replaces
  √(memory gain × speed gain) · exp(−D/10). Records measured before protocol 24,
  which kept no memory accounts, use their peak-memory gain for P.
- **25** (2026-10-14): GPU sandboxes (`POST /api/sandboxes`), whose GPU time
  counts toward your cost like an evaluation's.
- **24** (2026-10-14): a workload's KV cache is charged for its requests in
  flight, however few the server ran at once; a server starts under its hard
  cap with 24 GiB more, which the filler takes back once it answers (the
  stock engine could not start under the interactive cap on Qwen3.5-27B), and
  the accuracy runs' cap adds room for the probes' log-probabilities;
  evaluations that failed because of the arena are not charged; ModelOpt
  mixed-precision checkpoints are labelled by their decoder layers.
- **23** (2026-10-13): the board charges each participant's score for the GPU
  time of all its evaluations of the task, `exp(−GPU-hours / 100)`.
- **22** (2026-10-12): a solution is a checkpoint on the Hub with the engine that
  serves it; the earlier forms (a checkpoint the stock engine serves, a
  checkpoint built in the arena) take no submissions.
