2026-09-18 · 18 min · multimodal · vision-language-models · benchmarks · quantization · inference-optimization · explainer
On 15 September 2026 the Taichu lab (紫东太初) open-sourced ZDTaichu5.0-9B: a Qwen3.5-9B
decoder with an NVIDIA C-RADIOv4-H vision tower bolted on, pitched at spatial reasoning and
embodied AI. There is no arXiv preprint and no tech report — the entire technical record is a
model card, a GitHub
README, and one subdirectory called
recurrent_reasoning/. I read all of it, in both languages, and pulled every safetensors
header over HTTP range requests without downloading a single weight.
The artifact is in good shape. The bytes reconcile exactly, the config matches the tensor shapes, and the vision tower is really there. The documentation is where this release falls over — and it falls over hardest on exactly the benchmarks where it beats a frontier closed model.
- architecture
- ZDTaichu5_0_ForConditionalGeneration
- task
- image-text-to-text
- safetensors
- 5 shards
- largest file
- 4.27 GB
- files
- 39
- downloads
- 1.8K
- likes
- 178
- languages
- en, zh
Measured from the safetensors headers: 9,794,197,512 parameters in 820 tensors — 8.95B language model, 651.6M vision tower, 188.7M projector. config.json sets max_position_embeddings to 262,144 and the README's own recommended vLLM command asks for --max-model-len 220000, neither of which is 128K.
repo last modified 2026-09-15
The two READMEs
The model card has one table for spatial and embodied reasoning. recurrent_reasoning/README.md
in the GitHub repo has another, for the same model on four of the same benchmarks. They do
not agree, and nothing in either document reconciles them:
| Benchmark | Model card | recurrent_reasoning/README.md | Difference |
|---|---|---|---|
| ViewSpatial | 62.50 | 54.10 | +8.40 |
| MMSI-Bench | 47.20 | 36.80 | +10.40 |
| MindCube-tiny | 78.27 | 74.04 | +4.23 |
| RoboSpatial | 56.00 | 68.00 | −12.00 |
The first three have an explanation, buried in a <sub> footnote under the model card's
tables. For ViewSpatial, MMSI-Bench, MindCube-tiny and VSI-Bench, an output-format
instruction was appended to the evaluation prompt:
You FIRST think about the reasoning process as an internal monologue and then provide the final answer. The reasoning process MUST BE enclosed within
<think></think>tags. The final answer MUST BE put in\boxed{}.
The EARR README's footnote says its numbers were taken without that instruction, through
plain Transformers at batch_size=1. So the delta between the two tables is the price of
that one paragraph of prompt. On MMSI-Bench it is worth 10.4 points.
MMSI-Bench is a fixed set of 1,000 multiple-choice questions
(arXiv:2505.23764), which makes the denominator easy:
47.20 is 472 questions, 36.80 is 368. The <think>/\boxed{} wrapper is worth 104
questions out of 1,000.
RoboSpatial is worse, because it has no explanation at all. It is not in the list of benchmarks that received the format instruction, yet the same model is reported at 56.00 in one file and 68.00 in the other. One of those two numbers came from a different harness, a different split, or a different checkpoint, and the release does not say which. The Highlights section leads with "Scores of 48 on ERQA and 56 on RoboSpatial" — the lower of the two, which is at least not the flattering direction.
I checked the Chinese README_zh.md files on the assumption that the more candid document
might be the untranslated one. It is not: both EARR READMEs are faithful translations of
each other, same numbers, same footnote, and — a small tell about how carefully this was
proofread — the same broken install line in both.
What the release figure shows, and what its own tables print
The headline figure is twelve bar-chart panels. The tables above it have twenty-seven rows.

I parsed all three HTML tables out of the model card and scored every row against the best of the three open baselines the release itself chose. Toggle between the figure's twelve panels and the full twenty-seven:
what the release figure draws
| benchmark | ZDTaichu | best open | which | Δ | in figure |
|---|---|---|---|---|---|
| ViewSpatialComplex spatial reasoning | 62.50 | 48.20 | Qwen3.5-9B | +14.30 | yes |
| MMSI-BenchComplex spatial reasoning | 47.20 | 38.70 | Qwen3.5-9B | +8.50 | yes |
| MindCube-tinyComplex spatial reasoning | 78.27 | 62.81 | STEP3-VL-10B | +15.46 | yes |
| VSI-BenchEmbodied interaction | 59.69 | 55.68 | Qwen3.5-9B | +4.01 | yes |
| WeMathMultimodal reasoning | 75.90 | 75.20 | Qwen3.5-9B | +0.70 | yes |
| MMStarGeneral VQA | 76.80 | 79.70 | Qwen3.5-9B | -2.90 | yes |
| AI2DGeneral VQA | 91.48 | 90.20 | Qwen3.5-9B | +1.28 | yes |
| RealWorldQAGeneral VQA | 76.99 | 80.30 | Qwen3.5-9B | -3.31 | yes |
| IFEvalInstruction following | 93.70 | 88.72 | Qwen3.5-9B | +4.98 | yes |
| LiveCodeBench v6Reasoning and coding | 73.40 | 65.60 | Qwen3.5-9B | +7.80 | yes |
| TAU2-BenchGeneral agent | 87.70 | 81.70 | STEP3-VL-10B | +6.00 | yes |
| Claw-Eval (general)General agent | 71.40 | 66.60 | STEP3-VL-10B | +4.80 | yes |
Across the full tables ZDTaichu5.0-9B leads the open field on 17 of 27 rows — 63.0%. Across the twelve panels promoted into the figure, 10 of 12 — 83.3%. Choosing a subset for a release graphic is normal, and every one of these losses is printed in the tables the figure sits under. The number worth carrying away is the one the graphic cannot show you: eight of the ten losses are to Qwen3.5-9B, the model's own language backbone. Both MMLU rows, OCRBench, MMStar, RealWorldQA, MathVista and MathVerse all go backwards relative to the text model this was built from. That is the visual-instruction-tuning tax, it is substantial (MathVerse Mini drops 7.74 points), and it is the honest cost of the spatial gains.
One more thing about the figure: in it, ZDTaichu's value label is bold in all twelve panels, including MMStar and RealWorldQA where it loses. In the tables, bold means best score among open models. Two conventions, one document.
Measuring the artifact
None of the above touches the weights, so I measured those separately. A safetensors file opens with an 8-byte little-endian header length followed by a JSON header listing every tensor's name, dtype and byte offsets — two range requests per file recover the complete tensor manifest for free.
# first 8 bytes -> header length n
curl -sSL -H 'Range: bytes=0-7' \
https://huggingface.co/TaichuAI/ZDTaichu5.0-9B/resolve/main/model-1-of-5.safetensors \
| xxd -e -g8
# then bytes 8..8+n-1 -> the JSON headerZDTaichu5.0-9B ships 9,794,197,512 parameters in 19,588,395,036 tensor bytes across five safetensors shards. Every shard's real size, tensor list and parameter count was recovered by HTTP range read without downloading any weights, and the three totals reconcile exactly: 8 length bytes per file, plus 106,344 bytes of JSON headers, plus tensor bytes, equals the sum of the CDN's Content-Length values to the byte.
| file | CDN Content-Length (B) | git-lfs pointer size (B) | safetensors header (B) | tensors | parameters |
|---|---|---|---|---|---|
| model-1-of-5.safetensors | 4,268,658,872 | 4,268,658,872 | 9,776 | 75 | 2,134,324,544 |
| model-2-of-5.safetensors | 4,265,727,128 | 4,265,727,128 | 16,656 | 128 | 2,132,855,232 |
| model-3-of-5.safetensors | 4,215,379,968 | 4,215,379,968 | 16,760 | 128 | 2,107,681,600 |
| model-4-of-5.safetensors | 3,123,658,752 | 3,123,658,752 | 12,408 | 95 | 1,561,823,168 |
| model-5-of-5.safetensors | 3,715,076,700 | 3,715,076,700 | 50,744 | 394 | 1,857,512,968 |
| — total — | 19,588,501,420 | 19,588,501,420 | 106,344 | 820 | 9,794,197,512 |
The index file's declared metadata.total_size is 19,588,395,036, which is tensor bytes only and excludes headers — it matches the summed data_offsets exactly, so nothing is missing or double-counted. The parameter total splits into 8,953,803,264 in the language model, 651,645,448 in the C-RADIOv4-H vision tower (390 tensors, one of them an int64 index buffer) and 188,748,800 in the mlp1 projector. config.json labels the release "9B" via _model_size_label; the shipped artifact is 9.79B, of which 2,034,237,440 (20.8%) is the untied input embedding plus LM head over a 248,320-token vocabulary.
Everything closes. 40 bytes of length prefixes plus 106,344 bytes of headers plus
19,588,395,036 tensor bytes equals 19,588,501,420, which is the sum of the five CDN
Content-Length values to the byte. The model.safetensors.index.json declares
total_size: 19588395036 — tensor bytes only, which is correct and not a discrepancy. The
git-lfs pointer sizes match the post-redirect Content-Length exactly. No missing gigabytes.
The config matches too, including the two places where it looks like it shouldn't. q_proj
ships as [8192, 4096] while num_attention_heads is 16 and head_dim is 256, which
multiplies out to 4096 — but configuration.py's own header comment gives the reason:
"Attention output gating (sigmoid gate on q_proj)", so the projection is 16 × 256 × 2.
The DSpark draft model confirms it from the other side: its config is plain qwen3 with the
same 16 heads and 256 head_dim, and its q_proj really is [4096, 4096].
The Gated DeltaNet layers line up the same way: in_proj_qkv is [8192, 4096] = 16×128
keys + 16×128 queries + 32×128 values, and in_proj_z is the 32×128 gate. The projector is
exactly what mlp1 claims: [5120] RMSNorm → [20480, 5120] → [4096, 20480], where 5120
is the 1280-wide ViT output after a 0.5 pixel-shuffle. The vision tower is real — 390
tensors, 32 ViT-H/16 blocks, 651,645,448 parameters — not a config entry pointing at nothing.
Two things the config advertises that the checkpoint does not carry:
mtp_num_layers: 1andmtp_loss_scaling_factor: 0.1describe a multi-token-prediction head. There are zero tensors matchingmtp.*in the index, andmodeling.pyexplicitly listsr"^mtp\..*"in_keys_to_ignore_on_load_unexpected. The MTP head was stripped before release; the config was not updated.llm_config.use_cache: false, alongsidemtp_loss_scaling_factorand avision_config.argsblob containing the entire RADIO training argparse namespace (world_size: 256,epoch: 299,wandb_project, and the teacher list —siglip2-g-384,dinov3_vit7b16,sam3). This config.json is a training-run export shipped as-is. It is harmless, but it is not a release artifact.
What "9B" is made of
Of the 9,794,197,512 parameters, 2,034,237,440 — 20.8% — is the untied input embedding plus LM head over a 248,320-token vocabulary. The non-embedding model is 7.76B. That framing matters as soon as you look at the quantized builds, because nobody quantized those two matrices.
C-RADIOv4-H tower — 32-block ViT-H/16, 651.6M params
| ZDTaichu5.0-9B | 1,303,290,908 B | BF16 1.30 GB · other 16 B |
| ZDTaichu5.0-9B-FP8 | 1,303,290,896 B | BF16 1.30 GB · other 16 B |
| ZDTaichu5.0-9B-NVFP4 | 1,303,290,896 B | BF16 1.30 GB · other 16 B |
Read the bits/weight figure. The FP8 build lands at 10.35 bits/weight, not 8, because
45.4% of it is still BF16: the embedding, the LM head, the entire C-RADIOv4-H tower and the
projector. The NVFP4 build lands at 8.01 bits/weight, not 4, because only the MLP
projections in layers 0 through 27 are actually 4-bit — 2.11 GB of U8-packed weights with
E4M3 block scales at block size 16. Layers 28–31 keep FP8 MLPs, every attention and
linear-attention projection stays FP8, the LM head is FP8, and embed_tokens is still BF16.
Measured in parameters, 43.2% of the model is 4-bit in the "NVFP4" checkpoint.
The 1.68 GB vision tower and projector are byte-for-byte identical across all three checkpoints. For a model whose pitch is embodied and spatial deployment, the visual encoder is the one component that never gets quantized.
The fourth entry in that download table is ZDTaichu5.0-9B-DSpark, which is not a
quantization at all and is never mentioned again anywhere in the README. Its header says
3,210,004,992 parameters in 62 tensors: embed_tokens, five Qwen3 decoder layers, an fc of
[4096, 20480] that fuses five auxiliary hidden states, a markov_head of 2 × [248320, 256],
and lm_head. It is an EAGLE-3-style speculative-decoding draft model, and its own card
reports 2.46×–3.17× speedups on a single H800 at concurrency 1 with
num_speculative_tokens: 7 — best-case conditions for speculative decoding, and a number that
will not survive real batching. Two-thirds of its 6.42 GB (63.4%) is the two vocabulary
matrices. It is tagged pipeline_tag: image-text-to-text on Hugging Face and contains no
vision tensors, which is correct by design for a draft model and wrong as metadata.
The headline feature ships in neither place you would look for it
The fourth bullet in Highlights is Entropy-Gated Adaptive Recurrent Reasoning: "dynamically allocates additional recurrent refinement steps in latent space to more challenging tokens." It is the only architectural novelty claimed over stock Qwen3.5 + RADIO.

It is not in the Hugging Face repo. modeling.py there has no entropy gate at all. To get
EARR you clone GitHub and copy two files over the downloaded checkpoint:
# recurrent_reasoning/README.md, verbatim
cp ./modeling.py /path/to/ZDTaichu5.0-9B/
cp ./recurrent_reasoning.py /path/to/ZDTaichu5.0-9B/Diffing the two modeling.py files shows how thin the seam is — 22 changed lines, all of
them plumbing, and the one that matters is the class declaration:
-class ZDTaichu5_0_ForConditionalGeneration(PreTrainedModel, GenerationMixin):
+class ZDTaichu5_0_ForConditionalGeneration(RecurrentReasoningMixin, PreTrainedModel, GenerationMixin):It is also absent from the deployment path the README actually recommends. The Online Serving
section serves the model with a forked vLLM, and vLLM does not run the repo's modeling.py.
So the headline mechanism is available only through AutoModel, only at batch_size=1 (the
code says so: "Batch size is always 1, so entropy is handled as one scalar"), and only if
you configure it through process-level environment variables — recurrent_reasoning.py reads
them with os.getenv at import time, and the README is explicit that they "are not passed
as arguments to model.generate(...) and are not read from config.json."
What the mechanism actually does
Strip the prose and the loop is short. One ordinary forward pass produces logits; the entropy
of the last position's distribution is compared against a threshold; if it is exceeded, the
block of layers named by RECURRENT_REASONING_LAYER_INDICES (default "12,13,14,15") is
re-run on a damped blend of its own input and output, and the new logits are kept only if
entropy went down.
H > 1.0 → gate fires on this token
- damping weight w = 1/(MAX_ITERS+1)
- 0.5000
- extra decoder-layer applications
- 4
- as a share of the 32-layer stack
- 12.5%
h' = 0.5000 * h_in(layer 12) + 0.5000 * h_out(layer 15)Two things fall out of that. The "damped update" is not a tuned constant — it is
1.0 / (RECURRENT_REASONING_MAX_ITERS + 1), so at the shipped default of MAX_ITERS=1 it is
a flat 50/50 average, and the re-blend line inside the loop (guarded by
if reasoning_iter < RECURRENT_REASONING_MAX_ITERS) never executes at all. And the "greater
computational depth" the Highlights promise is, at default settings, one extra pass over
four of thirty-two layers, on gated tokens only: 12.5% more decoder depth, once.
Raising MAX_ITERS does not simply buy more depth, either, because it shrinks the step: at
MAX_ITERS=3 the damping weight drops to 0.25, so each of the three iterations moves the
hidden state a quarter of the way instead of half.
Five of the diagram's terms return zero grep hits
The figure's bottom row lists five mechanisms. I grepped the released
recurrent_reasoning.py — all 45 KB of it — for the term behind each. Five of those terms
(margin, normaliz, residual, anchor, rollback) return zero hits, case-insensitive;
one design point survives intact, and it is the one with the least marketing attached:
| Claim in the figure / README | In recurrent_reasoning.py |
|---|---|
| Trigger: "normalized entropy / margin" | Raw entropy in nats against a constant. margin and normaliz appear 0 times. |
| Termination: "KL + hidden residual" | KL early-stop is there. residual appears 0 times; the second stop rule is "entropy went up". |
| Stability: "damping + anchor" | Damping is there. anchor appears 0 times — the blend at iteration k is against iteration k's own input, not the initial representation. |
| Safety: "best-state rollback" | rollback appears 0 times. The loop keeps the last accepted candidate and breaks on the first entropy rise — greedy, not best-of-trajectory. |
| Deployment: "KV-cache policy" | Genuinely implemented, and the most substantial part of the file: cache forking, frozen history rebuild, DeltaNet recurrent-state snapshots. |
The one the README under-sells is the one that is real. Re-running four layers over cached
decode state without corrupting a hybrid KV/DeltaNet cache is fiddly, and
_build_recurrent_reasoning_history_cache, _fork_recurrent_reasoning_cache and
_snapshot_recurrent_reasoning_linear_states are where the engineering went. During prefill
there is no such luck — the comment in the code is blunt: "Prefill reasoning recomputes the
current input tokens without a KV/recurrent cache", which for a long multi-image prompt means
those four layers get re-run over the entire sequence.
And the gains it buys
| Benchmark | ungated | with EARR | Δ |
|---|---|---|---|
| ViewSpatial | 54.10 | 54.27 | +0.17 |
| MMSI-Bench | 36.80 | 37.40 | +0.60 |
| MindCube-tiny | 74.04 | 75.00 | +0.96 |
| RoboSpatial | 68.00 | 70.00 | +2.00 |
Four benchmarks, one run each, no seeds, no item counts, no error bars. On MMSI-Bench's 1,000 questions, +0.60 is 6 questions. The release bolds all four as wins. To its credit, the README states the limitation itself, and states it well:
Output entropy reflects the model's own uncertainty and does not establish whether an answer is correct.
That is exactly right, and it is the reason a gate that accepts whichever candidate has lower entropy is selecting for confidence, not accuracy. A more confident wrong answer passes this filter.
The install line does not work
Three files — the Hugging Face model card, recurrent_reasoning/README.md, and
recurrent_reasoning/README_zh.md — give the same installation command:
pip install tranformer==5.3.0 torch==2.10.0 torchvision==0.25.0 accelerate timmtranformer is a typo for transformers, and there is no PyPI project by that name:
https://pypi.org/pypi/tranformer/json returns 404, as does transformer without the
trailing s. Nobody who copy-pasted the quickstart got past line one. (No typosquat exists on
either name today, which is luck rather than design.) Everything else in the quickstart checks
out — processor.from_messages(...) really is defined, at processing.py:424.
The context length is muddled the same way. The model card's spec table and prose both say
128K. config.json sets max_position_embeddings: 262144. The vLLM command the README tells
you to run asks for --max-model-len 220000. Three numbers, no reconciliation — and unusually,
the marketing figure is the smallest of the three.
The demo
The GitHub repo ships agent demo recordings. This is the one for a damped-harmonic-oscillator task — solve it analytically with Wolfram, then numerically in Python, then compare the phase portraits — driven through an agent harness called ScienceClaw, which is presumably the source of the release's Claw-Eval numbers.
Worth noting what the toolbar tells you that the README doesn't: this is a session replay, not real time, and its own speed selector is set to 2x. The 16-second clip is a sped-up excerpt of a session the header labels 34 minutes old. That is a normal thing for a product viewer to do and an easy thing to forget when you are watching an agent look fast.
What I would actually do with this
Set the documentation aside and there is a real model here. But which model depends on a number the release does not publish.
At the model card's gated scores, ZDTaichu5.0-9B is 8.5 to 15.5 points clear of the best open baseline on the three complex-spatial rows. At the lab's own ungated scores it is 36.80 on MMSI-Bench, and the model card's number for its own backbone, Qwen3.5-9B, is 38.70. Those two are not directly comparable — the card never says whether Qwen was given the format instruction — and that is precisely the problem. Depending on the answer, ZDTaichu5.0-9B is either 8.5 points ahead of Qwen3.5-9B on MMSI-Bench or 1.9 points behind it.
That is one missing sentence away from resolved: were the baselines in the spatial table
evaluated with the same <think>/\boxed{} addendum? If they were, the lead is real and
recurrent_reasoning/README.md is just an ablation at different settings. If they were not,
the four marquee spatial results compare a prompted model against unprompted baselines. Until
that is stated, the defensible claim is narrower than the one on the front page:
ZDTaichu5.0-9B is a 9.79B multimodal model that responds unusually well to an explicit
reasoning-format instruction on multi-image spatial tasks — worth 10.4 points on MMSI-Bench
alone. That is a useful thing to know, and you can get it from the release's own two tables —
just not from either one alone.
What would change my mind
5 claims above, and what would falsify each
The model card's spatial scores and recurrent_reasoning/README.md's scores were produced under different prompts, and the gap is the prompt.
The model card's own footnote says the format instruction was added for ViewSpatial, MMSI-Bench, MindCube-tiny and VSI-Bench; the EARR footnote says it was not used. If the lab publishes an eval log showing the two tables differ for some other reason — a checkpoint revision, a different harness — the attribution is wrong, though the 10.4-point contradiction on MMSI-Bench stands either way. RoboSpatial's 12-point gap has no such explanation at all.
ZDTaichu5.0-9B's MMSI-Bench lead over Gemini 3 Pro depends on the prompt addendum.
Run MMSI-Bench's 1,000 questions against Gemini 3 Pro with the identical
<think>/\boxed{}instruction. If Gemini stays at 45.20 with the addendum, the 2.0-point lead is real and this reservation is unfounded.Most of the 'Key Design Points' in EARR.png have no implementation in the released code.
A grep of
recurrent_reasoning.py(github.com/Taichu-AI/ZDTaichu5.0-9B, read 2026-09-18) returns zero hits formargin,normaliz,residual,anchorandrollback. If a later commit adds them, or if they live in unreleased training code and the diagram describes the trained model rather than the shipped inference path, this reads as a labelling problem rather than a missing feature.The NVFP4 checkpoint is 8.01 bits/weight, not 4.
Read the header: 2,113,929,216 bytes of
U8packed weights against a 9,811,117,776-byte total and 9,794,197,512 parameters. If TaichuAI reuploads with the embedding, the vision tower and layers 28–31 quantized, the number moves. As shipped on 2026-09-18, it does not.The parameter count, shard sizes and config all agree with the artifact.
Every shard's post-redirect
Content-Lengthequals its git-lfs pointer size, and 8×5 + 106,344 + 19,588,395,036 = 19,588,501,420 = their sum. A mismatch on any shard, or a tensor shape contradictingconfig.jsonbeyond the documentedq_projoutput gate, would break this.

Sources. TaichuAI/ZDTaichu5.0-9B · Taichu-AI/ZDTaichu5.0-9B · FP8 · NVFP4 · DSpark · MMSI-Bench. All headers read 2026-09-18; weights under the NVIDIA Open Model License, upstream Qwen3.5 under Apache-2.0.