lihe07 commited on
Commit
a17a1c6
·
verified ·
1 Parent(s): 4ec9a3b

Model card rewrite; launch.sh downloads respanai/span-02

Browse files
Files changed (2) hide show
  1. README.md +209 -199
  2. launch.sh +2 -2
README.md CHANGED
@@ -18,247 +18,257 @@ tags:
18
 
19
  # span-02
20
 
21
- span-02 is a **decision model**: it answers typed questions about a state (JSON or text, plus up to 4 images)
22
- with a full probability distribution over the options, in the Decision Index `/v1/systemone` wire format.
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
- ## Quantization
40
- **This checkpoint is engine-native.** It cannot be loaded by `transformers` or vLLM.
 
 
 
 
 
41
 
42
- | part | format |
43
  |---|---|
44
- | MoE experts | NVFP4 W4A4 (e2m1 weights, FP8-e4m3 scale per 16 elements, FP32 global scale; activations NVFP4 with static per-layer global scales) |
45
- | attention and dense MLP linears | FP8 e4m3 W8A8 (per-output-row weight scale, dynamic per-token activation scale) |
46
- | tied embedding / LM head | FP8 e4m3 per row |
47
- | vision tower, router, norms, KV cache | bf16 |
48
 
49
- `model.safetensors` is 16.4 GB. GPU memory in use at serving time is about 26 GB.
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
- Docker:
78
- ```
79
- hf download lihe07/span-02 --local-dir span-02 && cd span-02
 
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+ toolkit and Rust installed):
87
- ```
88
- hf download lihe07/span-02 --local-dir span-02 && cd span-02
89
  (cd engine && NVCC=/usr/local/cuda/bin/nvcc cargo build --release -p dllm-server)
90
- bash launch.sh # serves on 0.0.0.0:8000; PORT / HOST / AR_API_KEY are optional
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": "Exchanges, wrong or damaged items",
140
  "shipping": "Delivery status, delays, lost packages",
141
- "billing": "Charges, invoices, payment problems"}},
142
  "refund": {"type": "noul", "instructions": "Does the customer ask for a refund?"},
143
  "anger": {"type": "score", "instructions": "How frustrated is the customer?",
144
- "criteria": ["Not frustrated: calm or positive", "Somewhat frustrated: mild annoyance",
145
- "Very frustrated: angry, hostile or threatening to leave"]}},
146
- "reasoning": {"mode": "auto", "trace": true}}'
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": "Not frustrated: calm or positive", "1": "Somewhat frustrated: mild annoyance",
157
- "2": "Very frustrated: angry, hostile or threatening to leave"},
158
- "probabilities": {"0": 0.000008, "1": 0.00012, "2": 0.99987}, "confidence": 0.9998}},
159
- "usage": {"input_tokens": 412, "reasoning_tokens": 0, "output_tokens": 0, "latency_ms": 41.7}}
160
  ```
161
- (The token counts and latency in this response are illustrative.)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
- * An answer whose question thought has `"reasoning": {"tokens": <thought tokens>, "trace": "<thought text>"}`
164
- (`trace` only when the request asked for it); answers that did not think have no `reasoning` field.
165
- * `usage` holds request totals: `input_tokens` = the prompt tokens of every question (the shared state is counted
166
- once per question; each image counts its soft tokens + 2), `reasoning_tokens` = thought tokens generated,
167
- `output_tokens` = answer tokens generated (the tokens of `number` answers; choice, noul and score answers are read
168
- from one forward pass and generate none), `latency_ms` = server time from arrival to response.
169
- * With images, the response also has a top-level `image_tokens` field (the total image soft tokens).
 
170
 
171
  ### Images
172
- The model was trained with all images (up to 4) in front of the text, in order, and with the text calling them
173
- "Image 1", "Image 2", ... So the server puts the `images` first and then the assets in the order they are sent, and
174
- replaces every occurrence of an asset placeholder in the state, instructions and criteria by that image's label.
175
- For `{"state": {"photo": "a3f1c2e8-found"}, "assets": {"a3f1c2e8-found": ..., "77b0d4a1-poster": ...}}` the model
176
- reads `{"photo": "Image 1"}`, and an instruction "... as in this picture: 77b0d4a1-poster?" becomes "... as in this
177
- picture: Image 2?". Each asset is shown once, however often it is mentioned; an asset that is never mentioned is
178
- shown all the same (one unreferenced asset renders exactly like one `images` entry). A score `legend` echoes the placeholder,
179
- not the label. Requests without assets are rendered exactly as before. Assets are PNG or JPEG (the `format` must
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
- `{"error": {"type": ..., "message": ...}}`, one status per kind:
 
184
 
185
  | status | type | when |
186
  |---|---|---|
187
- | 400 | `invalid_request` | invalid JSON, a malformed compressed or multipart body |
188
- | 400 | `invalid_asset` | a rejected asset: unknown `type` or `format`, bad base64, not a PNG/JPEG, missing or unused multipart part, more than 4 images |
189
- | 400 | `max_tokens_exceeded` | the prompt (+ images + thought reservation) exceeds the context, or the request cannot fit in the whole KV cache; the message contains "maximum context length" |
190
- | 401 | `authentication_error` | missing or wrong bearer token (only with `AR_API_KEY`) |
191
- | 404 | `not_found` | any path other than `POST /v1/systemone` and `GET /health` |
192
- | 413 | `request_too_large` | body over 64 MB (also after decompression), an image over 16 MiB |
193
- | 415 | `unsupported_encoding` | a Content-Encoding other than zstd / gzip, or more than one layer |
194
- | 422 | `validation_error` | schema errors: missing fields, bad question types or criteria, too many options, bad `reasoning`, more than 4 or undecodable `images` |
195
- | 529 | `overloaded` | concurrent requests hold the KV cache; retry after a short delay |
196
  | 500 | `server_error` | an internal failure |
197
 
198
- The capacity errors carry the Decision Index capacity markers, so the DI client records `unsupported`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
- ## Serving settings (Decision Index submission)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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`: there is no wall-clock limit; the clock is never read for thinking decisions.
206
- * `--think-ms 750 --think-cost think_cost.json`: deterministic thought budget. Each request's per-question thought
207
- cap = floor((750 - fixed) / per_token(n)), where fixed is the modelled prompt, image and readout time of that
208
- request and per_token(n) the modelled time per thought token with n of its questions thinking together. It is a
209
- pure function of the request, so answers do not depend on the machine or its load. A thought ends at the
210
- end-of-thought token, at EOS, or at that cap (at most `--cap 1024`). A short single question gets about 330
211
- thought tokens, a request with many thinking questions or a long prompt fewer.
212
- * `--tau 0.8`: think when the no-think top probability is below 0.8. The final distribution is then
213
- 0.5 x no-think + 0.5 x post-thought (choice, score and noul alike).
214
- * `--spec-draft` (passed by `launch.sh`): MTP speculative decoding of thoughts and number answers with an adaptive draft
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
- | request mix | n | median | mean | p80 | accuracy (choice questions) |
232
- |---|---|---|---|---|---|
233
- | DI public text 0.3, stratified sample (42 benchmarks), DI kit | 400 | 71 | 308 | 712 | 0.923 (2725 questions) |
234
- | DI public vision: CV-Bench, BLINK, RealWorldQA, DI kit | 150 | 186 | 291 | 642 | 0.747 |
235
- | image rows of 6 DI vision benchmarks (incl. MMMU-Pro vision), DI kit | 210 | 292 | 389 | 699 | 0.652 |
236
- | think-heavy question set (78 % of questions think) | 150 | 616 | 514 | 770 | 0.562 |
237
-
238
- The rule is met on every mix measured, with margin. Against a plain 256-token cap on the same machine and requests,
239
- accuracy is the same within noise (pooled paired difference -0.03 points, 95 % CI [-0.39, +0.34]) and the tail is much
240
- shorter (DI text requests over 1 s: 34 -> 4 of 400; slowest 2.8 s -> 1.07 s). Against a blend that puts the post-thought
241
- half on the post-thought top option only (one-hot), the averaged distributions have the same accuracy within noise
242
- (+0.13 points, 95 % CI [-0.06, +0.35], 3,707 choice questions, same thoughts) and are better calibrated (log-loss
243
- 0.409 -> 0.397, Brier score 0.200 -> 0.193; on the questions that thought 1.368 -> 1.290 and 0.734 -> 0.689). Speculative decoding runs at about 480
244
- thought tokens/s at batch 1 on real thoughts on this GPU (2.1 ms per token).
245
-
246
- ## Capacity limits
247
- Requests over these limits are rejected with the DI capacity markers. Nothing is truncated.
248
- * Choice questions: 1 to 679 options.
249
- * Images: at most 4 per request (`images`: PNG, JPEG, GIF, BMP, TIFF or WebP; `assets`: PNG or JPEG). Each image uses at most 560 soft tokens.
250
- * Context: the prompt tokens + 562 per image + the reserved thought tokens + 3 must fit in 131072 tokens
251
- (`--max-len 131072`; the sliding-window layers keep only their 1024-token window, so long prompts stay cheap).
252
- The reservation is the deterministic cap of a lone thinking question of that size; at long prompts it is small
253
- or zero (the cost model leaves no thought time).
254
- * KV cache: a request whose prompt pages (shared prefix + every question's own tail) exceed the whole KV pool is
255
- rejected with 400 `max_tokens_exceeded` ("maximum context length"); none of the DI 0.3 public requests does with
256
- the pools above.
257
- * Request body: at most 64 MB.
258
 
259
  ## License
260
- The weights are a derivative of Google's Gemma 4 (`google/gemma-4-26B-A4B-it`) and are distributed under the
261
- Gemma 4 license terms, which are Apache 2.0:
262
- [ai.google.dev/gemma/docs/gemma_4_license](https://ai.google.dev/gemma/docs/gemma_4_license).
263
- The serving engine in `engine/` is Apache-2.0. It ports algorithms from vLLM, SGLang, FlashInfer and other
264
- projects, which are credited in `NOTICE` and in the source file headers.
 
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 (lihe07/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,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:-lihe07/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
 
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