~/satyajit

d3-turbo: the state stops looking at the question, so a photo is read once for 255 questions

mdjsonmcp

2026-10-23 · 18 min · calibration · benchmarks · multimodal · qwen · linear-attention · inference

A 1:55 narrated explainer, drawn in code. Every number and picture in it is this article's own; the sources are below.

› transcript

Hi, I'm Kedge! A decision model reads a photo and answers typed questions about it. This one reads the photo once, however many questions you ask. The idea: the state stops looking at the question. So it is the same for every question, and is computed once. In d3, the state reads the question, so every question pays for the photo again. In d3-turbo, the state reads only itself. The photo is read once. First, the photo and the text run through all sixty-four layers, once. The runtime keeps the attention keys and values, and the final state of each linear scan. Then the short question blocks run on top, each reading the cached state and itself. A readout over answer codes gives every option a probability. No text is generated. At two hundred fifty-five questions, the vendor times thirty-three seconds for the smaller d3-flash, and about two for d3-turbo. Box questions reuse the same readout. Each edge is a distribution over one hundred twenty-eight steps. The edge is its mean. Draw five hundred twelve boxes, and count how many overlap the answer. A table turns that share into the confidence. Four questions, four boxes, each with a confidence. The camera gets low confidence. That is the confidence doing its job. Number one on both boards was the team's own estimate. On the official text board it is tenth, tied with d3. Keep the state from reading the question, and you are allowed to read it once. Read once, cached, and the same accuracy as d3, with far less waiting. Every source is in the full article. I'm Kedge. Bye!

Why read this

Notabletop 60%

Reads the state-once mask and the Box decoder from d3-turbo's own runtime, and replays the Decision Index history behind '#1 on both boards'.

  • Checked against the source
  • Concrete numbers to act on
  • Explained from first principles

LLM architectureNeeds a workstation GPUApache-2.0Practitioner model

How this was scored
Is it new?
1 of 3: An incremental tweak
Can I trust it?
3 of 3: Reproduces the headline result, or shows from primary files it is wrong
Can I run it?
1 of 3: API-only, gated or restrictive licence
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
1 of 3: Relevant for months
Does it affect many?
1 of 3: A specialist community
Only here?
2 of 3: A teardown or measurement few others did

Score 61 of 100, ranked 304 of 577 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

Xunzhuo Liu's launch post for d3-turbo makes three kinds of claim. It is "#1 on both Jev Decision Index boards". It "points(box), follows and sees images in your questions". And "StateOnce brings 15x faster than d3 flash". The thread adds the numbers: 255 questions about one photo in 2.12 s against 32.94 s, a Box accuracy of 0.90, a track presence F1 of 0.97, +6.2 over d3 on the public suite, Apache-2.0 weights.

The speed claim is the interesting one, because it is an architecture claim. A decision model reads a piece of state, an email or a photo, and answers typed questions about it with a probability per option, without generating text. If it answers 255 questions, the cheap way is to read the state once and hang every question off it. Whether a model is allowed to do that is decided by its attention mask, and the last two releases in this family went in opposite directions on it.

So I went to the files: the model repository at commit a0f3e9e (its runtime, configs and card), the weights of d3-turbo, d3 and Qwen3.8-27B read by HTTP range request, and the Decision Index Space's own data files at seven commits. There is no paper. The model card is the only write-up, so every architecture statement below comes from code.

The short answer: the mechanism is real and small. One mask changed, and the speed follows from it exactly. The leaderboard claim was an estimate, and the official boards disagree with it on text.

vllm-sr/d3-turbo@a0f3e9e · snapshot 2026-10-23
parameters
26.09B
repo size
104.37 GB
architecture
Qwen3_5Model
task
zero-shot-classification
library
transformers
license
apache-2.0
safetensors
12 shards
largest file
5.00 GB
files
51
downloads
830
likes
81
parameters by dtype
BF1626.09B
zero-shot-classificationdecision-modelclassificationsystem-onemultimodalvisionvideoobject-detection

Qwen3.5-architecture 27B checkpoint (64 layers, 48 Gated DeltaNet and 16 full attention, 27-block vision tower) plus a 255 x 5,120 FP32 readout: 25,624,600,064 text, 460,730,096 vision and 1,305,600 readout parameters per MODEL_MANIFEST.json. attention_mode prefix_bidir, validated_length 32,768. Apache-2.0.

repo last modified 2026-10-20

What a decision model computes

Every model in this wave shares one output contract, the /v1/systemone shape that Jev made common. The prompt (d3_format.py) is a system turn, then State: and the state, then Question:, the instructions, and the options listed under one-token codes: A, B, …, Z, AA, … up to 255 of them. The model does not decode. It takes the final hidden state at the last prompt position, h∈R5120h \in \mathbb{R}^{5120}, and multiplies it by a 255-row readout WW:

p=softmax⁡ ⁣(W1:m hT),W∈R255×5120,  T=1p = \operatorname{softmax}\!\left(\frac{W_{1:m}\, h}{T}\right), \qquad W \in \mathbb{R}^{255 \times 5120},\; T = 1

where mm is the number of options and the codes past mm are masked to −∞-\infty. That readout is 1,305,600 parameters (measured: 255×5,120255 \times 5{,}120, and the manifest says the same). A yes/no question is a two-option choice. A score is a choice over levels. None of that changed from d3. What changed is how hh is computed when there are many questions.

The backbone is the Qwen3.5 hybrid that Qwen3.8-27B uses: 64 layers, every fourth a full softmax-attention layer and the other 48 Gated DeltaNet layers, a linear recurrence whose memory is a fixed-size matrix per head (measured from config.json's layer_types). That split matters, because the two kinds of layer can be made to share work in different ways.

The one change: prefix_bidir

d3's decision_config.json sets attention_mode to noncausal_full_attention (measured). In the 16 full-attention layers every token sees every other token, before and after it. That is what pplx-decider v1.1 introduced, and for the same reason: a classifier wants the state read with the question in view. The cost is that the state's hidden states now depend on the question. Change the question and the state's representation changes too. Nothing can be cached, so every question is a full pass over the state. d3-flash, the 9B sibling, uses the same mode (measured).

d3-turbo's config says prefix_bidir (measured). The runtime's docstring describes it, and StateBlocks and block_attention in d3_runtime.py implement it. Split each prompt at the end of the state text into a state block S\mathcal{S} (system turn, images, videos, State: and the state) and a question block Q\mathcal{Q}. In a full-attention layer, query ii may read key jj when

Mij={1i∈S,  j∈S1i∈Q,  j∈S∪Q0i∈S,  j∈QM_{ij} = \begin{cases} 1 & i \in \mathcal{S},\; j \in \mathcal{S} \\ 1 & i \in \mathcal{Q},\; j \in \mathcal{S} \cup \mathcal{Q} \\ 0 & i \in \mathcal{S},\; j \in \mathcal{Q} \end{cases}

The state is bidirectional within itself, the question is bidirectional within itself and reads the whole state, and the state never reads the question. The implementation does not even build the mask. It makes two unmasked SDPA calls per row, one with the state's queries against the state's keys and one with the question's queries against all keys, which together equal the dense masked attention.

The Gated DeltaNet layers stay causal over the whole prompt, as they are in d3 (measured). A causal scan is already shareable: its recurrent state after the last state token is the same whatever follows.

Put those together and the state's hidden states, in every layer, are a function of the state alone. That is the whole trick, and StateOnce is the code that cashes it in. A state pass runs the state block once as one row and keeps, per layer, either the full-attention keys and values (RoPE applied) or the Gated DeltaNet's final recurrent state plus the last three inputs of its width-4 short convolution. A question pass then runs the question blocks in batches of up to 64 rows, right padded. Each full-attention query reads the cached state keys and values plus its own block. Each DeltaNet layer continues its scan from the cached state. usage.input_tokens counts the state once plus each question block. With D3_STATE_ONCE=0 the runtime falls back to one full row per question, the reference path the cached one is meant to equal.

Toggle the two modes and the two kinds of layer below. The counter is the number of token positions pushed through the trunk for kk questions: k(s+q)k(s + q) when every question re-reads a state of ss tokens, s+kqs + kq when the state is read once.

Some numbers make the gap concrete. An image is resized to at most 1,638,400 pixels and each 32 x 32 patch becomes one token (measured from the runtime docstring), so a large photo is up to 1,600 tokens of state. Questions are tens of tokens. With a 1,600-token state, 40-token questions and 64 of them, re-reading costs 104,960 positions and state-once 4,160, 25 times fewer (reasoned).

This is the same idea as the tree that Agr and Liquid's d1-3B use to read the state once. The difference is that d3-turbo keeps the bidirectional view inside each block, where the earlier tree designs are causal. Perplexity's v1.2 reached a related layout from the other direction, putting every question in one sequence.

Is it a new model or a new mask?

The checkpoint is the same shape as d3, down to the tensor names: 1,183 tensors in both indexes, 26,085,330,160 backbone parameters (measured from the safetensors index and the Hub's count). I read slices of the same tensors from d3-turbo, d3 and Qwen3.8-27B by HTTP range request (the first 8 MB of each tensor) and compared them:

Tensor (slice)Qwen → d3Qwen → d3-turbod3 → d3-turbo
layer 0 mlp.up_proj0.687%0.944%0.617%
layer 3 self_attn.q_proj0.457%0.564%0.314%
layer 31 self_attn.q_proj0.526%0.701%0.434%
layer 31 mlp.down_proj0.606%0.861%0.565%
layer 63 self_attn.k_proj0.674%0.952%0.709%
layer 0 linear_attn.in_proj_qkv0.465%0.612%0.383%
vision block 0 attn.qkv000

Relative Frobenius distance, measured. The readout moved 1.22% from d3's (measured). The vision tower blocks I sampled are bit-identical to Qwen's in all three.

The pattern fits d3-turbo being d3 trained further. The distance from Qwen to d3-turbo is close to what you get by adding the other two as independent steps: for layer 0's up_proj, 0.6872+0.6172=0.923\sqrt{0.687^2 + 0.617^2} = 0.923 against a measured 0.944 (reasoned). Two independent fine-tunes of Qwen would sit about equally far from it, and further from each other. The card does not describe training, so this is inference from weights, not a documented recipe. Training under the new mask is necessary, though: a model trained to read the question from inside the state cannot be switched to a mask that hides it without relearning.

The speed claim, against its own table

The card times one request at a time on one AMD Instinct MI325X through AutoModel, 1 to 255 yes/no and choice questions about one 1,280 x 1,600 JPEG, median of 15 timed requests after 3 warm-ups (reported). On ROCm gfx942 the state-once path runs through fused kernels in d3_fast.py (measured from the code path, not run).

Log-scale line chart of median time per request against questions in the request, from 1 to 255, for one photo on one AMD Instinct MI325X. d3-flash rises from 154 ms at 1 question to 541 ms, 2.07 s, 8.26 s, 16.50 s and 32.94 s at 255. d3-turbo goes from 302 ms to 328 ms, 426 ms, 867 ms, 1.29 s and 2.12 s. A label reads 15.5x faster at 255 questions.
Median time to answer n questions about one photo, d3-turbo against d3-flash, AutoModel on one MI325X. The vendor's measurement, not re-run here (d3-turbo model card).
Questionsd3-turbo p50d3-flash p50flash ÷ turbo
1302 ms154 ms0.5×
4328 ms541 ms1.6×
16426 ms2.07 s4.9×
64867 ms8.26 s9.5×
1281.29 s16.50 s12.8×
2552.12 s32.94 s15.5×

The rows are reported; the ratios recompute (reasoned). A least-squares line through each column says what the two designs cost (reasoned). d3-flash is close to a constant 129 ms per question, from 135 ms at 4 to 129 ms at 255, which is what reading the photo once per question looks like. d3-turbo is a fixed 0.33 s plus about 7.2 ms per added question. The fixed part is the state pass: the vision tower plus 64 layers over about 1,600 image tokens. The marginal part is a question block on a cached state.

So the ratio is not a constant property of the model. It grows with the question count toward roughly 129/7.2≈18129 / 7.2 \approx 18 and is below 1 for a single question, where the 9B model is twice as fast as the 27B one (reasoned). The card says both of those things plainly. Two things the headline leaves out: the comparison is against d3-flash, a third of d3-turbo's size, not against d3 at the same size, which the card does not time; and the free API takes 64 questions per request, so 9.5× is the most an API user can see (reported limits, reasoned ratio).

One more thing the code shows. Box, track and image-in-question requests do not take the cached path in the shipped AutoModel runtime. _system_one_composed runs "every question as a row of its own under the block mask", so each Box question recomputes the state block, image included (measured from d3_runtime.py). Its usage still counts the state once. The card says the vllm-sr Engine "answers Box and track questions too" from one read and that 64 questions about one photo take 0.50 s there. I did not read or run the Engine, so that part is unverified.

Box, track and images in questions

These are the new question types, and they reuse the same readout rather than adding a detection head. A Box question's prompt ends with four lines, top:, bottom:, left:, right:, and the instruction to give each edge "as a code on a 128-step scale" (d3_compose.py). The runtime reads the readout at each colon. Each edge becomes a distribution over the first 128 codes, and the edge is its expectation:

e^=∑k=0127pk k+0.5128\hat e = \sum_{k=0}^{127} p_k \,\frac{k + 0.5}{128}

If the expected top lands below the expected bottom, order_edges collapses the pair to a one-bin sliver around their midpoint. There is always exactly one box, even when the object is absent, which is why the card says to pair a Box question with a yes/no one.

The confidence is not a probability the model outputs. It is computed from the four distributions. The runtime draws 512 boxes, one per point of a quasi-random R4 sequence, by inverting each edge's cumulative distribution. It counts the share cc whose IoU with the reported box is at least 0.5, and maps cc through a 38-point piecewise-linear table in decision_config.json:

confidence=g ⁣(1512∑i=15121 ⁣[IoU⁡(bi,b^)≥0.5])\text{confidence} = g\!\left(\frac{1}{512}\sum_{i=1}^{512} \mathbf{1}\!\left[\operatorname{IoU}(b_i, \hat b) \ge 0.5\right]\right)

The table lifts the middle of the range: a raw 0.53 becomes 0.67 and 0.35 becomes 0.50 (measured from the table). Where the calibration data came from is not published.

This is the one piece of the release I could run on a CPU without the weights, so I did, under the run exception for this piece. I loaded the repository's d3_compose.py (numpy 2.5.3, on a shared 4-core cloud VM) and fed it synthetic Gaussian edge distributions (measured):

Synthetic caseRaw shareCalibrated
40% box, edge spread 0.4% of the image1.00001.0000
40% box, spread 5%0.95510.9898
40% box, spread 10%0.44530.6341
10% box, spread 2%0.66990.8245
top and bottom crossed0.00000.0000

The fourth row is the one to remember. The same 2% edge uncertainty that gives a 40% box a confidence of 1.00 gives a 10% box 0.82, because IoU punishes a fixed error more on a small box. Confidence here is "how likely is this box to overlap the truth at IoU 0.5", which is the right question for a detector, and it means small objects read as less certain for the same localisation error. The widget below is a TypeScript port of the same functions; it matches the Python to four decimals on all five cases (measured).

A track is the same machinery over time. The clip is read in windows of up to 32 frames at twice the requested rate, so 16-second windows at 1 update per second and 8-second windows at 2 (measured from d3_compose.py). Each window is a state of its own, and each moment is a Box question with an extra visible: line read over two codes.

Images inside questions and options work because the composed path places an image's tokens in that question's block. Only that question sees it, and under prefix_bidir the state never does.

A photo of an Australian Shepherd leaping for a frisbee in front of a seated crowd, with four boxes drawn on it: the frisbee 0.99, the dog's tail 0.98, the orange cone 1.00, and the camera on the tripod 0.27, where the box covers a camera held by a photographer.
Four Box questions about one photo in one request, drawn as returned. The camera box at 0.27 is the confidence doing its job. Photo by Sally Wehner, CC BY 2.0 (d3-turbo model card).

The Box and tracking scores (0.90 accuracy at IoU 0.5, confidence AUROC 0.96, success AUC 0.79 and 0.80, presence F1 0.97, and 0.96 accuracy for images in options) are on "our Box test set" and "our tracking test clips" (reported). Neither set is published, so I cannot check them or say how hard they are.

"#1 on both boards"

The tweet does not say "estimate". The card does: "70.2 on the Jev Decision Index, +4.0 over d3 (66.3). Our estimate, not an official board result", and its rank figure is labelled a 0.3.2 snapshot of 16 October.

Two panels. Text board: d3 at number 2 with 66.3 official, an arrow to d3-turbo at number 1 with 70.2 marked our estimate, +4.0 points. Vision board: d3 at number 4 with 71.3 official, an arrow to d3-turbo at number 1 with 72.9 marked our estimate, +1.7 points.
The launch's rank claim: d3's official ranks on the 0.3.2 boards and d3-turbo's self-estimated ones. The vendor's estimate, not a board result (d3-turbo model card).

The estimate itself holds up against the snapshot it names. On the Space's data/v03.json at the 16 October commit, the text board is led by StartLux-Decision v1.1 at 69.12 with d3 second at 66.26, and the vision board by Amos 1 Large at 72.4 (measured). 70.2 and 72.9 would have been first on both.

But the board did not stay on 0.3.2. The post went out at 00:42 UTC on 20 October (from the post id). At 13:53 UTC the same day the Space published Decision Index 0.4, which scores the full index from the private parts only, 60% same skills and 40% new domains, with a 0.9-point tie band (measured from formula in the data file). d3-turbo was scored officially from then on (measured, data/v03.json and data/vision.json at each commit):

Board snapshotText: d3-turboText: d3Vision: d3-turboVision: d3
0.4, 20 Oct69.04, 6th69.06, 4th72.35, 2nd71.26, 5th
0.4.1, 21 Oct67.31, 9th67.15, 10th72.46, 1st71.26, 7th
0.4.1, 22 Oct (current)67.31, 10th of 22967.15, 11th72.46, 2nd of 5571.26, 9th

On the current text board, d3-turbo is 10th of 229 by score, tied with d3. Ranks are by score; v1.0 was scored on 20 October and v1.1, the weights released before the post, from 21 October. On text, d3-turbo and d3 have shared a tie group in every snapshot, 0.02 and 0.16 points apart against a 0.9 band. It was never first. On vision it was first from 21 October 15:13 UTC until RBDash-Decision v0.2 joined at 73.3 on 22 October 20:12 UTC, about 29 hours (measured from the commit times).

The components say where the gain is (measured, current snapshot):

Partd3-turbo v1.1d3Difference
Text, public benchmarks71.6265.45+6.17
Text, private same skills64.2962.83+1.46
Text, private new domains56.4157.86−1.45
Vision, public76.8474.08+2.76
Vision, private68.0868.45−0.37

The thread's "+6.2 on the public suite" is real: the board's own public score agrees to 0.03 (reasoned from the two). That gain is mostly on the public benchmarks, the part of the index a team can measure for itself and that 0.4 stopped counting. On the private tests nobody can train on, d3-turbo is a wash against d3 on text and a third of a point behind on vision.

A third-party run says the same. SaylorTwift's Typed Decisions evaluation, linked from the card's own eval results, puts d3-turbo at 0.736 accuracy and ECE 0.069 over 400 cases and 2,000 decisions, behind d3 at 0.749 and 0.049 and d3-mini at 0.743 (reported, from the bucket's leaderboard.md). d3-turbo ran through transformers and the others through vLLM, which "does not yet support" the new attention mode, so the serving paths differ. The 0.013 accuracy gap is about 26 decisions, a little over one standard error at that sample size (reasoned). It does not show d3-turbo worse. It does show it is not better.

Smaller things I found

What to take from it

Encoders and decoders have long been one mask apart. Decision models have now been through three masks in a month. Causal reads the state once but sees the question only at the end. Fully noncausal sees everything and pays for the state on every question. prefix_bidir gives the state a bidirectional view of itself, gives the question a bidirectional view of itself and the state, and keeps the one property that makes caching legal: the state never looks forward at the question.

That is a good design for exactly the workload these models are sold for, many cheap questions about one expensive input. The 15.5× is real for that workload, and smaller everywhere else. The accuracy claim is weaker than the post makes it: d3-turbo is d3's accuracy on held-out tests, with d3's latency problem fixed.

Sources, read on 2026-10-23: the launch post and its thread; vllm-sr/d3-turbo at a0f3e9e (README.md, config.json, decision_config.json, MODEL_MANIFEST.json, d3_runtime.py, d3_compose.py, d3_format.py, .eval_results/); vllm-sr/d3 and vllm-sr/d3-flash configs; Qwen/Qwen3.8-27B weights by range request; the Decision Index Space data files at commits 0ddf6758, de1f4ea8, 16f312e2, 76fbe376 and 0b5ecaf2; and the Typed Decisions eval bucket. I ran only the repository's Box decoder, on synthetic inputs; no weights were loaded. Figures are reproduced for commentary from the Apache-2.0 model card; the two interactives are original.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "d3-turbo: the state stops looking at the question, so a photo is read once for 255 questions", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026d3turbo,
  author = {Satyajit Ghana},
  title  = {d3-turbo: the state stops looking at the question, so a photo is read once for 255 questions},
  url    = {https://ai.thesatyajit.com/articles/d3-turbo},
  year   = {2026}
}
share