Model card rewrite; launch.sh downloads respanai/span-02
Browse files
README.md
CHANGED
|
@@ -18,247 +18,257 @@ tags:
|
|
| 18 |
|
| 19 |
# span-02
|
| 20 |
|
| 21 |
-
span-02 is a
|
| 22 |
-
|
| 23 |
-
It is a fine-tune of Gemma 4 26B-A4B (mixture of experts, about 4B active parameters per token), quantized
|
| 24 |
-
(NVFP4 experts, FP8 dense layers) and served by a custom Rust/CUDA engine for Blackwell GPUs (sm_120: RTX 5090,
|
| 25 |
-
RTX PRO 6000) that is included in this repo (`engine/`).
|
| 26 |
-
|
| 27 |
-
Before answering it may think. If the top probability of the no-think answer is below 0.8, the model writes a
|
| 28 |
-
greedy thought. Its length limit comes from a deterministic thought budget: a fixed cost model (`think_cost.json`)
|
| 29 |
-
turns a 750 ms target into a per-question token cap that depends only on the request (prompt length, images,
|
| 30 |
-
number of questions and of thinking questions), so the same request always gets the same thought. After a thought
|
| 31 |
-
the answer is read out again, and the final distribution is the average of the two:
|
| 32 |
-
0.5 x the no-think distribution + 0.5 x the post-thought distribution, for every question type. A choice answer is
|
| 33 |
-
the option with the highest final probability. Thoughts and number answers are decoded with speculative decoding
|
| 34 |
-
(Gemma's public MTP draft). The verification is greedy, so both are token-identical to plain greedy decoding.
|
| 35 |
-
|
| 36 |
-
Outputs are typed: `choice`, `noul` (yes/no probability), `score` (ordinal levels) and `number` (a free-form number).
|
| 37 |
-
The model never returns free text as an answer.
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
|
| 43 |
|---|---|
|
| 44 |
-
|
|
| 45 |
-
|
|
| 46 |
-
| tied embedding / LM head | FP8 e4m3 per row |
|
| 47 |
-
| vision tower, router, norms, KV cache | bf16 |
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
## Hardware
|
| 52 |
-
* An NVIDIA Blackwell GPU with compute capability 12.0 (`sm_120a`). Tested on an **RTX PRO 6000 Blackwell** (96 GB),
|
| 53 |
-
where the latency below was measured, and on an **RTX 5090** (32 GB).
|
| 54 |
-
* NVIDIA driver with CUDA 12.9 support (R575 or newer).
|
| 55 |
-
* To build: CUDA toolkit 12.8+ (`nvcc`) and Rust. No GPU is needed at build time.
|
| 56 |
-
|
| 57 |
-
Other GPU architectures are not supported, because the kernels use the sm_120a block-scaled FP4/FP8 tensor-core
|
| 58 |
-
instructions.
|
| 59 |
-
|
| 60 |
-
## Files
|
| 61 |
-
| file | what |
|
| 62 |
-
|---|---|
|
| 63 |
-
| `model.safetensors`, `config.json` | quantized weights and the Gemma-4 config |
|
| 64 |
-
| `tokenizer.json`, `tokenizer_config.json`, `chat_template.jinja`, `generation_config.json` | tokenizer and chat template |
|
| 65 |
-
| `processor_config.json` | Gemma-4 image processor at the served budget: `max_soft_tokens` 560 (about 1.3 MP) |
|
| 66 |
-
| `draft_vocab_16384.json` | pruned 16384-token vocabulary for the MTP draft head |
|
| 67 |
-
| `think_cost.json` | fixed cost model of the deterministic thought budget (`--think-cost`; calibrated on an RTX PRO 6000) |
|
| 68 |
-
| `engine/` | source of `dllm-server` (Rust + CUDA; see `engine/README.md`) |
|
| 69 |
-
| `launch.sh` | finds or downloads the weights and the draft, then starts the server with the settings below |
|
| 70 |
-
| `Dockerfile`, `.dockerignore` | two-stage image: CUDA devel build, then CUDA base runtime |
|
| 71 |
-
| `NOTICE` | licenses and attributions |
|
| 72 |
-
|
| 73 |
-
The MTP draft model is **not** in this repo. `launch.sh` downloads the public
|
| 74 |
-
[`google/gemma-4-26B-A4B-it-assistant`](https://huggingface.co/google/gemma-4-26B-A4B-it-assistant).
|
| 75 |
|
| 76 |
## Quickstart
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
| 80 |
docker build -t span-02 .
|
| 81 |
docker run --gpus all -p 8000:8000 -v "$PWD":/model:ro -e DLLM_MODEL_DIR=/model -v span02-data:/data span-02
|
| 82 |
```
|
| 83 |
-
Without `-v "$PWD":/model ... DLLM_MODEL_DIR`, the container downloads this repo itself. Pass `-e HF_TOKEN` for
|
| 84 |
-
that while the repo is private.
|
| 85 |
|
| 86 |
-
Without Docker (CUDA 12.8+
|
| 87 |
-
```
|
| 88 |
-
hf download
|
| 89 |
(cd engine && NVCC=/usr/local/cuda/bin/nvcc cargo build --release -p dllm-server)
|
| 90 |
-
bash launch.sh
|
| 91 |
-
```
|
| 92 |
-
Startup takes about 10 s plus about 7 s of warmup. `GET /health` returns `{"ok":true,"model":"span-02"}` when the server is ready.
|
| 93 |
-
|
| 94 |
-
## API
|
| 95 |
-
`POST /v1/systemone`; the format is TypeSafe's
|
| 96 |
-
System One API (docs.typesafe.ai/api) plus the extensions marked below. `GET /health` returns `{"ok": true, "model": "span-02"}`.
|
| 97 |
-
|
| 98 |
-
Request fields:
|
| 99 |
-
* `state` (required): any JSON value (string, object, array, ...).
|
| 100 |
-
* `questions` (required): {question id: question}; answers come back in this order.
|
| 101 |
-
* `model`: accepted and ignored (e.g. `"jev-latest"`); the response `model` is always the served id `span-02`.
|
| 102 |
-
* `assets`: TypeSafe image assets, `{placeholder: {"type": "image", "format": "png" | "jpg" | "jpeg", "data_base64": ...}}`,
|
| 103 |
-
or `{"part": name}` with a `multipart/form-data` body (part `request` = the JSON request, one file part per image).
|
| 104 |
-
Referencing an asset is optional: a placeholder that occurs in `state` or in a question's `instructions` /
|
| 105 |
-
`criteria` (any string value, also mid-string; object keys are never replaced) is replaced by the image's label.
|
| 106 |
-
See "Images" below.
|
| 107 |
-
* `images` (extension, sent by the Decision Index kit's vision runner): a list of images as data URIs or bare base64. At most 4 images per
|
| 108 |
-
request in total (`images` + `assets`).
|
| 109 |
-
* `reasoning` (extension): `{"mode": "auto" | "on" | "off", "budget_ms": <ms>, "trace": <bool>}`, all optional.
|
| 110 |
-
`mode`: `auto` (default) uses the tau 0.8 gate, `on` thinks for every question, `off` never thinks.
|
| 111 |
-
`budget_ms` (0 to 600000) is the wall-clock thinking budget of the request; `0` means none (a thought ends at the
|
| 112 |
-
end-of-thought token, EOS or the cap). Without it, `mode: "on"` thinks to completion (up to the cap) and other
|
| 113 |
-
requests use the server's `--budget-ms`. The deterministic `--think-ms` cap of the submission settings applies
|
| 114 |
-
regardless; `budget_ms` only adds or removes a wall-clock deadline (the submission server has none), so requests
|
| 115 |
-
without it stay deterministic. `trace: true` returns each thought's text.
|
| 116 |
-
* Other top-level fields are ignored.
|
| 117 |
-
* Bodies may be compressed with `Content-Encoding: zstd` (the TypeSafe Python SDK default) or `gzip`.
|
| 118 |
-
|
| 119 |
-
Question types:
|
| 120 |
-
* `choice`: `criteria` maps each option key to its description (1 to 679 options). The answer is the argmax,
|
| 121 |
-
its `confidence` = (p_max - 1/n) / (1 - 1/n), and the probabilities in criteria order.
|
| 122 |
-
* `noul`: the probability that the statement is true. `criteria` is optional: `{"true": ..., "false": ...}`.
|
| 123 |
-
* `score`: `criteria` is an ordered array of 2 to 10 level descriptions, from low to high. The answer is
|
| 124 |
-
`score` = sum_i i x p_i, with the per-level probabilities, a `legend` (the levels as sent) and a `confidence`.
|
| 125 |
-
* `number` (extension): a free-form number, no `criteria`. The model writes the number greedily after its
|
| 126 |
-
(optional) thought, as it was trained (the plain decimal, e.g. `391` or `2.5`, at most 32 tokens). The answer is
|
| 127 |
-
`{"type": "number", "number": 391.0, "text": "391", "confidence": 0.98}`: `number` is the parsed value (`null` if
|
| 128 |
-
the text is not a number), `confidence` = exp(mean log-probability of the generated answer tokens). The think gate
|
| 129 |
-
is trained for option questions only, so number questions think only with `"reasoning": {"mode": "on"}`.
|
| 130 |
-
|
| 131 |
-
Request:
|
| 132 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
curl -s localhost:8000/v1/systemone -H 'content-type: application/json' -d '{
|
| 134 |
-
"model": "span-02",
|
| 135 |
"state": {"customer": "My package arrived damaged and nobody answers my emails. I want a refund now.",
|
| 136 |
"order": {"id": 123, "days_late": 4}},
|
| 137 |
"questions": {
|
| 138 |
"dept": {"type": "choice", "instructions": "Which team should handle this?",
|
| 139 |
-
"criteria": {"returns":
|
| 140 |
"shipping": "Delivery status, delays, lost packages",
|
| 141 |
-
"billing":
|
| 142 |
"refund": {"type": "noul", "instructions": "Does the customer ask for a refund?"},
|
| 143 |
"anger": {"type": "score", "instructions": "How frustrated is the customer?",
|
| 144 |
-
"criteria": ["
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
Response:
|
| 149 |
```
|
|
|
|
| 150 |
{"model": "span-02",
|
| 151 |
"answers": {
|
| 152 |
"dept": {"type": "choice", "choice": "returns", "confidence": 0.945,
|
| 153 |
"probabilities": {"returns": 0.963, "shipping": 0.024, "billing": 0.013}},
|
| 154 |
"refund": {"type": "noul", "noul": 0.99999},
|
| 155 |
-
"anger": {"type": "score", "score": 1.9999,
|
| 156 |
-
"legend": {"0": "
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
"usage": {"input_tokens":
|
| 160 |
```
|
| 161 |
-
(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
| 170 |
|
| 171 |
### Images
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
match the bytes), at most 16 MiB each; they are resized to the image budget, not rejected for their dimensions.
|
| 181 |
|
| 182 |
### Errors
|
| 183 |
-
|
|
|
|
| 184 |
|
| 185 |
| status | type | when |
|
| 186 |
|---|---|---|
|
| 187 |
-
| 400 | `invalid_request` | invalid JSON, a
|
| 188 |
-
| 400 | `invalid_asset` |
|
| 189 |
-
| 400 | `max_tokens_exceeded` | the
|
| 190 |
-
| 401 | `authentication_error` | missing or wrong
|
| 191 |
-
| 404 | `not_found` | any
|
| 192 |
-
| 413 | `request_too_large` | body over 64 MB
|
| 193 |
-
| 415 | `unsupported_encoding` | a Content-Encoding other than zstd
|
| 194 |
-
| 422 | `validation_error` |
|
| 195 |
-
| 529 | `overloaded` |
|
| 196 |
| 500 | `server_error` | an internal failure |
|
| 197 |
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
```
|
| 202 |
--max-len 131072 --budget-ms 0 --tau 0.8 --cap 1024 --think-ms 750 --think-cost think_cost.json
|
| 203 |
--image-soft-tokens 560 --serial --no-prefix-cache --spec-max-branches 16 --pages 12000 --kv-gb 24
|
| 204 |
```
|
| 205 |
-
* `--budget-ms 0
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
* `--
|
| 215 |
-
depth and a pruned 16384-token draft vocabulary. It is lossless (token-identical to greedy decoding).
|
| 216 |
-
* `--image-soft-tokens 560`: the Gemma-4 image budget (about 1.3 MP; aspect ratio kept).
|
| 217 |
-
* `--serial --no-prefix-cache --spec-max-branches 16`: each answer is a deterministic function of its request.
|
| 218 |
-
It does not depend on load or on earlier requests (for up to 16 questions of one request thinking at once).
|
| 219 |
-
`--serial` runs one request at a time; concurrent clients queue.
|
| 220 |
-
* `--max-len 131072`: the context limit per question (prompt + images + thought).
|
| 221 |
-
* `--pages 12000 --kv-gb 24`: KV cache pools (full-attention layers 12000 x 16 = 192K tokens, 3.9 GB;
|
| 222 |
-
sliding-window layers get the rest of `--kv-gb 24`, ~20 GB), sized so that every Decision Index request fits on its own under `--serial` (the
|
| 223 |
-
largest needs about 3.7K full pages: many questions with long tails). The pool size does not change any answer.
|
| 224 |
-
The server uses about 41 GB of GPU memory.
|
| 225 |
-
|
| 226 |
-
## Latency
|
| 227 |
-
Measured with the submission settings on one RTX PRO 6000 Blackwell, one request at a time, as client wall time
|
| 228 |
-
per request including HTTP (ms). The Decision Index rule requires the median, mean and p80 each to be at most
|
| 229 |
-
1000 ms.
|
| 230 |
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
* Request body: at most 64 MB.
|
| 258 |
|
| 259 |
## License
|
| 260 |
-
|
| 261 |
-
Gemma 4
|
| 262 |
-
[
|
| 263 |
-
|
| 264 |
-
|
|
|
|
| 18 |
|
| 19 |
# span-02
|
| 20 |
|
| 21 |
+
**span-02 is a decision model.** Give it a state (text or JSON, plus up to 4 images) and a set of typed questions,
|
| 22 |
+
and it returns a calibrated probability distribution for every answer. No free text, no parsing.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
- **Typed answers:** pick one option (`choice`), yes/no (`noul`), a rating on a scale (`score`), or a number
|
| 25 |
+
(`number`).
|
| 26 |
+
- **Fast:** a typical request answers in tens of milliseconds. When the model is unsure, it thinks briefly first,
|
| 27 |
+
and the request still finishes well under a second.
|
| 28 |
+
- **Small and efficient:** a fine-tune of Gemma 4 26B-A4B (mixture of experts, ~4B active parameters), quantized to
|
| 29 |
+
NVFP4/FP8 (16.4 GB) and served by its own Rust/CUDA engine, which is in this repo.
|
| 30 |
+
- **Runs on one Blackwell GPU:** RTX PRO 6000 or RTX 5090.
|
| 31 |
|
| 32 |
+
| Decision Index 0.3 (public) | score |
|
| 33 |
|---|---|
|
| 34 |
+
| Text index | **63.62** (all 140,178 requests answered) |
|
| 35 |
+
| Vision | **70.69** (all 12,210 requests answered) |
|
|
|
|
|
|
|
| 36 |
|
| 37 |
+
> The weights only run with the included engine (`engine/`). They cannot be loaded with `transformers` or vLLM.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
## Quickstart
|
| 40 |
+
|
| 41 |
+
With Docker (needs an NVIDIA driver with CUDA 12.9 support and the NVIDIA container toolkit):
|
| 42 |
+
```bash
|
| 43 |
+
hf download respanai/span-02 --local-dir span-02 && cd span-02
|
| 44 |
docker build -t span-02 .
|
| 45 |
docker run --gpus all -p 8000:8000 -v "$PWD":/model:ro -e DLLM_MODEL_DIR=/model -v span02-data:/data span-02
|
| 46 |
```
|
|
|
|
|
|
|
| 47 |
|
| 48 |
+
Without Docker (CUDA toolkit 12.8+ and Rust; no GPU needed to build):
|
| 49 |
+
```bash
|
| 50 |
+
hf download respanai/span-02 --local-dir span-02 && cd span-02
|
| 51 |
(cd engine && NVCC=/usr/local/cuda/bin/nvcc cargo build --release -p dllm-server)
|
| 52 |
+
bash launch.sh # serves on 0.0.0.0:8000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
```
|
| 54 |
+
`launch.sh` also downloads the speculative-decoding draft model
|
| 55 |
+
([`google/gemma-4-26B-A4B-it-assistant`](https://huggingface.co/google/gemma-4-26B-A4B-it-assistant)).
|
| 56 |
+
The server is ready in about 20 s; `GET /health` then returns `{"ok": true, "model": "span-02"}`.
|
| 57 |
+
|
| 58 |
+
## Example
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
curl -s localhost:8000/v1/systemone -H 'content-type: application/json' -d '{
|
|
|
|
| 62 |
"state": {"customer": "My package arrived damaged and nobody answers my emails. I want a refund now.",
|
| 63 |
"order": {"id": 123, "days_late": 4}},
|
| 64 |
"questions": {
|
| 65 |
"dept": {"type": "choice", "instructions": "Which team should handle this?",
|
| 66 |
+
"criteria": {"returns": "Exchanges, wrong or damaged items",
|
| 67 |
"shipping": "Delivery status, delays, lost packages",
|
| 68 |
+
"billing": "Charges, invoices, payment problems"}},
|
| 69 |
"refund": {"type": "noul", "instructions": "Does the customer ask for a refund?"},
|
| 70 |
"anger": {"type": "score", "instructions": "How frustrated is the customer?",
|
| 71 |
+
"criteria": ["Calm or positive", "Mildly annoyed", "Angry or threatening to leave"]},
|
| 72 |
+
"late": {"type": "number", "instructions": "How many days late is the order?"}
|
| 73 |
+
}
|
| 74 |
+
}'
|
|
|
|
| 75 |
```
|
| 76 |
+
```json
|
| 77 |
{"model": "span-02",
|
| 78 |
"answers": {
|
| 79 |
"dept": {"type": "choice", "choice": "returns", "confidence": 0.945,
|
| 80 |
"probabilities": {"returns": 0.963, "shipping": 0.024, "billing": 0.013}},
|
| 81 |
"refund": {"type": "noul", "noul": 0.99999},
|
| 82 |
+
"anger": {"type": "score", "score": 1.9999, "confidence": 0.9998,
|
| 83 |
+
"legend": {"0": "Calm or positive", "1": "Mildly annoyed", "2": "Angry or threatening to leave"},
|
| 84 |
+
"probabilities": {"0": 0.000008, "1": 0.00012, "2": 0.99987}},
|
| 85 |
+
"late": {"type": "number", "number": 4.0, "text": "4", "confidence": 0.99}},
|
| 86 |
+
"usage": {"input_tokens": 540, "reasoning_tokens": 0, "output_tokens": 2, "latency_ms": 45.2}}
|
| 87 |
```
|
| 88 |
+
(Values are illustrative.)
|
| 89 |
+
|
| 90 |
+
The API follows TypeSafe's [System One API](https://docs.typesafe.ai/api), so its clients work unchanged; `number`,
|
| 91 |
+
`reasoning` and `images` are span-02 extensions that those clients can ignore.
|
| 92 |
+
|
| 93 |
+
## How it works
|
| 94 |
+
|
| 95 |
+
1. **Read the answer directly.** Each question is one prompt; the answer is read from the model's probabilities
|
| 96 |
+
for the option labels in a single forward pass. All questions of a request share the state, and the engine
|
| 97 |
+
computes it once.
|
| 98 |
+
2. **Think if unsure.** If the top answer has probability below 0.8, the model first writes a short thought, then
|
| 99 |
+
the answer is read again. The final distribution is the average of the two:
|
| 100 |
+
0.5 × before thinking + 0.5 × after thinking.
|
| 101 |
+
3. **Stay on time.** Thoughts are limited by a time budget (700 ms by default) and decoded with speculative decoding
|
| 102 |
+
(Gemma's public MTP draft, verified greedily), at about 480 thought tokens/s on an RTX PRO 6000.
|
| 103 |
+
|
| 104 |
+
Number answers are generated token by token after the (optional) thought, with the same speculative decoding.
|
| 105 |
+
|
| 106 |
+
## API reference
|
| 107 |
+
|
| 108 |
+
`POST /v1/systemone` takes a request and returns the answers. `GET /health` reports readiness.
|
| 109 |
+
|
| 110 |
+
### Request
|
| 111 |
+
|
| 112 |
+
| field | | |
|
| 113 |
+
|---|---|---|
|
| 114 |
+
| `state` | required | Any JSON value. The model treats it as data, not instructions. |
|
| 115 |
+
| `questions` | required | `{id: question}`. Answers come back in the same order. |
|
| 116 |
+
| `assets` | optional | Images keyed by id: `{"type": "image", "format": "png" \| "jpg" \| "jpeg", "data_base64": "..."}`. Mentioning an id in the state or a question is optional; see [Images](#images). |
|
| 117 |
+
| `images` | optional | A list of images as data URIs or base64 (used by the Decision Index kit). At most 4 images in total. |
|
| 118 |
+
| `reasoning` | optional | `{"mode": "auto" \| "on" \| "off", "budget_ms": 0-600000, "trace": true \| false}` |
|
| 119 |
+
| `model` | optional | Accepted and ignored; responses always say `span-02`. |
|
| 120 |
+
|
| 121 |
+
- **`mode`:** `auto` (default) thinks only when unsure; `on` always thinks; `off` never thinks.
|
| 122 |
+
- **`budget_ms`:** the thinking time limit for this request; `0` means no limit. If omitted, `on` thinks until the
|
| 123 |
+
thought ends (at most 4,096 tokens) and `auto` uses the server default (`--budget-ms`, 700).
|
| 124 |
+
- **`trace`:** return each thought's text.
|
| 125 |
+
- Bodies can be JSON, `multipart/form-data` (a `request` part plus one file part per image, referenced as
|
| 126 |
+
`{"part": name}`), or compressed with `Content-Encoding: zstd` or `gzip`. Unknown top-level fields are ignored.
|
| 127 |
+
|
| 128 |
+
### Question types
|
| 129 |
+
|
| 130 |
+
| type | `criteria` | answer |
|
| 131 |
+
|---|---|---|
|
| 132 |
+
| `choice` | `{key: description}`, 1 to 679 options | `choice`, `confidence`, `probabilities` |
|
| 133 |
+
| `noul` | optional `{"true": ..., "false": ...}` | `noul` = probability of true |
|
| 134 |
+
| `score` | ordered list of 2 to 10 levels, low to high | `score` = Σ i·pᵢ, `legend`, `probabilities`, `confidence` |
|
| 135 |
+
| `number` | none | `number` (`null` if the text is not a number), `text`, `confidence` |
|
| 136 |
+
|
| 137 |
+
- `choice` confidence = (p_max − 1/n) / (1 − 1/n): 0 for a uniform guess, 1 for certainty.
|
| 138 |
+
- `score` confidence = 1 − (expected distance from the most likely level) / (the same for a uniform guess).
|
| 139 |
+
- `number` confidence = exp(mean log-probability of the answer tokens). Number questions think only with
|
| 140 |
+
`mode: "on"`, because the think gate is trained on option questions.
|
| 141 |
|
| 142 |
+
### Response
|
| 143 |
+
|
| 144 |
+
- `answers[id]`: the typed answer. If the question thought, it also has `reasoning: {"tokens": N}` (plus `trace`
|
| 145 |
+
when requested).
|
| 146 |
+
- `usage`: `input_tokens` (the prompt tokens of every question; the shared state counts once per question and each
|
| 147 |
+
image its image tokens + 2), `reasoning_tokens` (thought tokens), `output_tokens` (number-answer tokens; other
|
| 148 |
+
answers are read in one pass and generate none), `latency_ms` (server time).
|
| 149 |
+
- `image_tokens`: total image tokens, present only when the request has images.
|
| 150 |
|
| 151 |
### Images
|
| 152 |
+
|
| 153 |
+
The model was trained with all images (up to 4) before the text, referred to as "Image 1", "Image 2", and so on.
|
| 154 |
+
The server shows `images` first, then `assets` in the order sent, and replaces every mention of an asset id in the
|
| 155 |
+
state, instructions and criteria with that image's label. For example
|
| 156 |
+
`{"state": {"photo": "a3f1c2e8-found"}, "assets": {"a3f1c2e8-found": ..., "77b0d4a1-poster": ...}}` reaches the model
|
| 157 |
+
as `{"photo": "Image 1"}`, and "the dog in this picture: 77b0d4a1-poster" becomes "the dog in this picture: Image 2".
|
| 158 |
+
Assets that are never mentioned are still shown. Images are PNG or JPEG, at most 16 MiB each, and are resized to the
|
| 159 |
+
image budget (280 tokens per image by default, 560 in the Decision Index settings).
|
|
|
|
| 160 |
|
| 161 |
### Errors
|
| 162 |
+
|
| 163 |
+
Errors look like `{"error": {"type": "...", "message": "..."}}`.
|
| 164 |
|
| 165 |
| status | type | when |
|
| 166 |
|---|---|---|
|
| 167 |
+
| 400 | `invalid_request` | invalid JSON, or a broken compressed or multipart body |
|
| 168 |
+
| 400 | `invalid_asset` | unknown asset type or format, bad base64, not a PNG/JPEG, more than 4 images |
|
| 169 |
+
| 400 | `max_tokens_exceeded` | the request does not fit the context or the KV cache ("maximum context length") |
|
| 170 |
+
| 401 | `authentication_error` | missing or wrong key (only when the server sets `AR_API_KEY`) |
|
| 171 |
+
| 404 | `not_found` | any other path |
|
| 172 |
+
| 413 | `request_too_large` | body over 64 MB, or an image over 16 MiB |
|
| 173 |
+
| 415 | `unsupported_encoding` | a `Content-Encoding` other than zstd or gzip |
|
| 174 |
+
| 422 | `validation_error` | missing fields, bad question types or criteria, too many options, bad `reasoning` |
|
| 175 |
+
| 529 | `overloaded` | the server is busy; retry shortly |
|
| 176 |
| 500 | `server_error` | an internal failure |
|
| 177 |
|
| 178 |
+
## Performance
|
| 179 |
+
|
| 180 |
+
### Latency (Decision Index settings, one RTX PRO 6000, one request at a time)
|
| 181 |
+
|
| 182 |
+
Client wall time per request, including HTTP, in ms. The Decision Index requires median, mean and p80 of at most
|
| 183 |
+
1,000 ms each.
|
| 184 |
+
|
| 185 |
+
| request mix | n | median | mean | p80 | accuracy (choice questions) |
|
| 186 |
+
|---|---|---|---|---|---|
|
| 187 |
+
| DI public text 0.3, stratified sample (42 benchmarks) | 400 | 71 | 308 | 712 | 0.923 |
|
| 188 |
+
| DI public vision: CV-Bench, BLINK, RealWorldQA | 150 | 186 | 291 | 642 | 0.747 |
|
| 189 |
+
| image rows of 6 DI vision benchmarks | 210 | 292 | 389 | 699 | 0.652 |
|
| 190 |
+
| think-heavy set (78% of questions think) | 150 | 616 | 514 | 770 | 0.562 |
|
| 191 |
+
|
| 192 |
+
On the full public text run: median 36 ms, mean 241 ms, p95 850 ms.
|
| 193 |
|
| 194 |
+
### Throughput (64 requests in flight)
|
| 195 |
+
|
| 196 |
+
Short single-question requests, 700 ms thinking budget. RTX PRO 6000 with
|
| 197 |
+
`--max-rows 16384 --think-max-rows 8192 --kv-gb 48 --pages 24000`; RTX 5090 with the defaults.
|
| 198 |
+
|
| 199 |
+
| GPU | requests/s, no thinking | requests/s, `auto` thinking | p99, `auto` |
|
| 200 |
+
|---|---|---|---|
|
| 201 |
+
| RTX PRO 6000 | 167 | 150 | 0.85 s |
|
| 202 |
+
| RTX 5090 | 127 | 102 | 0.70 s |
|
| 203 |
+
|
| 204 |
+
On the same RTX 5090, the engine is 1.3 to 7× faster than vLLM without thinking and keeps a lower tail latency
|
| 205 |
+
with it.
|
| 206 |
+
|
| 207 |
+
## Hardware
|
| 208 |
+
|
| 209 |
+
- An NVIDIA Blackwell GPU with compute capability 12.0 (`sm_120a`): **RTX PRO 6000 Blackwell** (96 GB) or
|
| 210 |
+
**RTX 5090** (32 GB). Other architectures are not supported: the kernels use sm_120a FP4/FP8 tensor-core
|
| 211 |
+
instructions.
|
| 212 |
+
- NVIDIA driver with CUDA 12.9 support (R575 or newer).
|
| 213 |
+
- GPU memory in use: about 28 GB with the default settings; the Decision Index settings below use about 41 GB.
|
| 214 |
+
|
| 215 |
+
## Quantization
|
| 216 |
+
|
| 217 |
+
| part | format |
|
| 218 |
+
|---|---|
|
| 219 |
+
| MoE experts | NVFP4 W4A4: e2m1 weights with an FP8 scale per 16 elements and an FP32 global scale; activations NVFP4 with static per-layer global scales |
|
| 220 |
+
| attention and dense MLP | FP8 e4m3 W8A8: per-row weight scales, dynamic per-token activation scales |
|
| 221 |
+
| tied embedding / LM head | FP8 e4m3, per row |
|
| 222 |
+
| vision tower, router, norms, KV cache | bf16 |
|
| 223 |
+
|
| 224 |
+
## Decision Index settings
|
| 225 |
+
|
| 226 |
+
The Decision Index submission runs `launch.sh` as shipped:
|
| 227 |
```
|
| 228 |
--max-len 131072 --budget-ms 0 --tau 0.8 --cap 1024 --think-ms 750 --think-cost think_cost.json
|
| 229 |
--image-soft-tokens 560 --serial --no-prefix-cache --spec-max-branches 16 --pages 12000 --kv-gb 24
|
| 230 |
```
|
| 231 |
+
- **Deterministic thinking budget:** `--budget-ms 0 --think-ms 750 --think-cost think_cost.json`. The wall clock is
|
| 232 |
+
never read. Instead, a fixed cost model turns a 750 ms target into a thought-token cap that depends only on the
|
| 233 |
+
request (prompt length, images, number of thinking questions), so the result does not depend on the machine or
|
| 234 |
+
its load. A short single question gets about 330 thought tokens; requests with many thinking questions or long
|
| 235 |
+
prompts get fewer. `--cap 1024` is the hard limit.
|
| 236 |
+
- **One request at a time, no prefix cache:** `--serial --no-prefix-cache`, so an answer does not depend on other
|
| 237 |
+
requests. Requests where several questions think at once can still vary very slightly in their post-thought
|
| 238 |
+
probabilities between runs.
|
| 239 |
+
- **Image budget:** `--image-soft-tokens 560`, about 1.3 MP with the aspect ratio kept.
|
| 240 |
+
- **KV cache:** `--pages 12000 --kv-gb 24`, sized so every Decision Index request fits on its own.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
+
## Limits
|
| 243 |
+
|
| 244 |
+
- Choice questions: 1 to 679 options. Score questions: 2 to 10 levels.
|
| 245 |
+
- Images: at most 4 per request.
|
| 246 |
+
- Context: 131,072 tokens per question (prompt + images + thought). Long prompts stay cheap: the sliding-window
|
| 247 |
+
layers keep only their 1,024-token window.
|
| 248 |
+
- KV cache: a request too large for the whole KV cache is rejected with 400 `max_tokens_exceeded`. With the default
|
| 249 |
+
`--kv-gb 9` (sized for a 32 GB RTX 5090) this can happen for requests with several long questions, for example
|
| 250 |
+
4 questions of 8K tokens each. On a larger GPU, raise `--kv-gb` and `--pages` (e.g. `--kv-gb 48 --pages 24000` on
|
| 251 |
+
an RTX PRO 6000).
|
| 252 |
+
- Number answers: at most 32 tokens. Thoughts: at most 4,096 tokens (1,024 in the Decision Index settings).
|
| 253 |
+
- Request body: at most 64 MB. Nothing is truncated; requests over a limit are rejected.
|
| 254 |
+
|
| 255 |
+
## Files
|
| 256 |
+
|
| 257 |
+
| file | contents |
|
| 258 |
+
|---|---|
|
| 259 |
+
| `model.safetensors`, `config.json` | quantized weights (16.4 GB) and the Gemma 4 config |
|
| 260 |
+
| `tokenizer.json`, `tokenizer_config.json`, `chat_template.jinja`, `generation_config.json` | tokenizer and chat template |
|
| 261 |
+
| `processor_config.json` | Gemma 4 image processor at 560 image tokens |
|
| 262 |
+
| `draft_vocab_16384.json` | pruned vocabulary for the speculative-decoding draft |
|
| 263 |
+
| `think_cost.json` | cost model for the deterministic thinking budget (calibrated on an RTX PRO 6000) |
|
| 264 |
+
| `engine/` | the `dllm-server` source (Rust + CUDA); see `engine/README.md` for build and flags |
|
| 265 |
+
| `launch.sh` | downloads what is missing and starts the server with the Decision Index settings |
|
| 266 |
+
| `Dockerfile`, `.dockerignore` | two-stage image: CUDA build, then a small runtime |
|
| 267 |
+
| `NOTICE` | licenses and attributions |
|
|
|
|
| 268 |
|
| 269 |
## License
|
| 270 |
+
|
| 271 |
+
The weights are a derivative of Google's Gemma 4 (`google/gemma-4-26B-A4B-it`), distributed under the
|
| 272 |
+
[Gemma 4 license](https://ai.google.dev/gemma/docs/gemma_4_license) (Apache 2.0). The engine in `engine/` is
|
| 273 |
+
Apache-2.0; it ports algorithms from vLLM, SGLang, FlashInfer and other projects, credited in `NOTICE` and in the
|
| 274 |
+
source file headers.
|
launch.sh
CHANGED
|
@@ -8,7 +8,7 @@
|
|
| 8 |
# Env:
|
| 9 |
# DLLM_MODEL_DIR directory with model.safetensors, config.json, tokenizer.json, draft_vocab_16384.json, think_cost.json
|
| 10 |
# (default: this script's directory if it holds model.safetensors, else $DLLM_DATA/model, downloaded from $DLLM_HF_REPO)
|
| 11 |
-
# DLLM_HF_REPO model repo to download (
|
| 12 |
# DLLM_DRAFT_REPO MTP draft for speculative decoding (google/gemma-4-26B-A4B-it-assistant; public)
|
| 13 |
# DLLM_DATA download directory (/data/dllm)
|
| 14 |
# DLLM_SERVER_BIN server binary (default: dllm-server on PATH, else engine/target/release/dllm-server next to this script)
|
|
@@ -18,7 +18,7 @@
|
|
| 18 |
# Extra arguments are passed to dllm-server (`dllm-server --help` lists them).
|
| 19 |
set -euo pipefail
|
| 20 |
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
| 21 |
-
REPO=${DLLM_HF_REPO:-
|
| 22 |
DRAFT=${DLLM_DRAFT_REPO:-google/gemma-4-26B-A4B-it-assistant}
|
| 23 |
D=${DLLM_DATA:-/data/dllm}
|
| 24 |
if [ -n "${DLLM_MODEL_DIR:-}" ]; then M=$DLLM_MODEL_DIR
|
|
|
|
| 8 |
# Env:
|
| 9 |
# DLLM_MODEL_DIR directory with model.safetensors, config.json, tokenizer.json, draft_vocab_16384.json, think_cost.json
|
| 10 |
# (default: this script's directory if it holds model.safetensors, else $DLLM_DATA/model, downloaded from $DLLM_HF_REPO)
|
| 11 |
+
# DLLM_HF_REPO model repo to download (respanai/span-02)
|
| 12 |
# DLLM_DRAFT_REPO MTP draft for speculative decoding (google/gemma-4-26B-A4B-it-assistant; public)
|
| 13 |
# DLLM_DATA download directory (/data/dllm)
|
| 14 |
# DLLM_SERVER_BIN server binary (default: dllm-server on PATH, else engine/target/release/dllm-server next to this script)
|
|
|
|
| 18 |
# Extra arguments are passed to dllm-server (`dllm-server --help` lists them).
|
| 19 |
set -euo pipefail
|
| 20 |
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
| 21 |
+
REPO=${DLLM_HF_REPO:-respanai/span-02}
|
| 22 |
DRAFT=${DLLM_DRAFT_REPO:-google/gemma-4-26B-A4B-it-assistant}
|
| 23 |
D=${DLLM_DATA:-/data/dllm}
|
| 24 |
if [ -n "${DLLM_MODEL_DIR:-}" ]; then M=$DLLM_MODEL_DIR
|