Writing
The scale of visual intelligence
We took a structured-extraction-from-images task from a 235-billion-parameter teacher down to a 2-billion-parameter student without losing accuracy. The 2B reads as well as the 8B of the same Qwen vision-language family once it can see the digits: on this task, intelligence scale saturates early. Perception does not. This post is how we found that saturation point, and why we stopped there. The 2B now serves in place of the 8B, 2.4× faster on the same GPU.
Brains come in sizes
Nervous systems span eight orders of magnitude, and each one is big enough for the life it leads. A nematode finds food and avoids harm with 302 neurons. A honeybee navigates, learns flowers and dances directions with under a million.
| Organism | Neurons | Synapses | What it manages |
|---|---|---|---|
| Nematode (C. elegans) | 302 | ~7,500 | Chemotaxis, avoidance, simple learning |
| Larval zebrafish | 100,000 | — | Hunting prey by sight |
| Fruit fly | 150,000 | — | Flight, courtship, odour memory |
| Honeybee | 960,000 | ~1 billion | Navigation, flower learning, the waggle dance |
| House mouse | 71 million | ~1 trillion | Mapping space, social behaviour |
| Human | 86 billion | — | Language, mathematics, writing blog posts |
A synapse is the closest biological cousin of a model parameter: a learned connection strength. The analogy is loose, but the lesson carries over. Evolution does not give a bee a mouse’s brain, because a bee’s tasks are already saturated at a million neurons.
Every task has a saturation point
We use intelligence scale to mean a model’s parameter count. For any one task, accuracy rises with intelligence scale and then goes flat. The flat part is the saturation point: past it, more parameters buy nothing for that task.
The practical question is where a given task saturates. Answer it, and you can serve the smallest model that does the job, on cheaper hardware, at lower cost. Our claim, from one vision task: the curve can look unsaturated when the real limit is somewhere else. Fix that limit, and a small model sits on the plateau.
A vision model has a second scale. We use perception scale to mean how finely the model samples its input: here, how many vision patches cover each character. The two scales saturate separately. A model can sit past its intelligence saturation point yet short of its perception one, and then its errors look like a lack of intelligence.
Task: structured extraction from images
The task is structured extraction from photographs. Given a photo and a list of fields, the model returns each field’s exact value as JSON. The test set has photographs with digital, analog and hybrid data, with 18 values per photograph set to capture.
The photographs are hard: faded signals, shadows and oblique angles.
Step one: 235B to 8B
We did not start small. The first reader was Qwen3-VL-30B-A3B, a mixture-of-experts model with about 3B parameters active per token. The teacher was Qwen3-VL-235B-A22B, which labelled the training photographs. Scored on about 510 check-verified fields:
| Reader | Check-verified fields correct |
|---|---|
| Qwen3-VL-30B-A3B, reading the whole photograph | 71.9% |
| Qwen3-VL-235B-A22B, the teacher | 81.1% |
| Qwen3-VL-8B, fine-tuned on the repaired labels | 94.9% |
The student scored above its teacher. It trained on the teacher’s answers after they were checked and repaired, not on the raw answers.
From here the reference is that 8B student, fine-tuned with LoRA on about 4,400 labelled photographs. On 50 held-out photograph sets, counting all 613 values, it reads 94.6% correctly.
Step two: 8B to 2B, and a false floor
We distilled the 8B into Qwen3-VL-4B and Qwen3-VL-2B: same training data plus 156 photographs the 8B labelled, three epochs each. The 4B reached 93.3% and the 2B 91.5%. On its own, that looks like the familiar story: halve the parameters, lose accuracy.
The errors told a different story. Of the 2B’s 52 wrong values, 35 differed from the truth by a single digit: 8 read as 0, 5 as 3, 6 as 4. The model found the right field and read the right number of characters. It confused shapes that differ by a few pixels.
Looking inside the 2B
The 2B’s decoder was doing its job; the evidence never reached it. We replayed 22 distinct misread digits, each paired with a neighbouring digit the model read correctly, and measured the model’s preference for the right digit over the wrong one.
| Measurement | Misread digits | Correct digits |
|---|---|---|
| Logit margin, right minus wrong digit | −1.6 | +8.4 |
| Right digit is the model's second choice | 18 of 22 | — |
| Attention on image tokens, decoder layers 17–25 | 0.24–0.28 | same |
| Fixed by upsampling the photo 1.5×, no retraining | 13 of 22 | 0 of 22 broken |
| Fixed by upsampling the photo 2×, no retraining | 17 of 22 | 0 of 22 broken |
Three readings follow:
- It looks in the right place. Attention on the image is the same whether the digit comes out right or wrong.
- It decides late, and only with evidence. A logit lens (projecting each layer’s state onto the vocabulary) shows correct digits locking in at layers 20–23 of 28. On misreads, the right digit never pulls ahead.
- The information is in the photo. Upsampling adds no new detail, yet it fixed 77% of misreads and broke none. The model could not resolve what was already there.
It was the eyes
A digit in the hybrid signal of our photographs is smaller than the patch the vision encoder reads it through. Qwen3-VL cuts an image into 16×16-pixel patches, then merges each 2×2 block into one token for the language model.
| What | Size in a 900×1,600 photo |
|---|---|
| One digit, hybrid signal | ~7 px wide, ~8 px tall |
| Line spacing | ~15 px |
| One vision patch | 16 × 16 px, about 2 characters |
| One language-model token | 32 × 32 px, about 4–5 characters across 2 lines |
So an 8 and a 0 that differ in a few pixels can land in the same patch and blur together before the decoder ever sees them. Upsampling the photo 2× gives each digit about 2×2 patches. The pixels are the same; the encoder simply samples them finely enough.
This is a sensory limit, not a cognitive one. A hawk does not need a bigger brain to spot a mouse; it needs a denser retina.
The curve after the fix
At the original resolution, accuracy climbs with size and looks unsaturated. Read the same photos at 2×, with no retraining, and every size lands on the same plateau: 96.7% for the 2B and the 8B, 97.4% for the 4B. For this task, the plateau starts at or below 2 billion parameters.
Set side by side, the two scales move accuracy by different amounts:
| Intelligence scale | Perception 1× | Perception 1.5× | Perception 2× |
|---|---|---|---|
| 2B | 91.5% | 94.8% | 96.7% |
| 4B | 93.3% | not tested | 97.4% |
| 8B | 94.6% | not tested | 96.7% |
Quadrupling parameters at 1× gains 3.1 points. Doubling the sampling of the same 2B gains 5.2. At 2× the intelligence axis is flat: the 4B gains 0.7 points over the 2B, and the 8B reads exactly what the 2B reads, 593 of 613 values. The perception axis was the one still climbing.
Retraining the 2B on 2× photographs added nothing: 96.4% against 96.7% for the same model simply shown 2× photographs. The gain is all in what the model is given to look at.
Real pixels did no better than resampled ones. The original photographs carry 1.4× the pixels of the copies above. Read at the same image-token budget they score 96.6%; enlarged 2×, at about 7,800 tokens, 97.2%, three values more. Perception saturates too, at about this budget.
Inside the 8B: most of it idles
With half of its MLP neurons switched off, the 8B still reads 98.3% of digits right. The 2B, cut the same way, falls to 34.9%. The larger model is not using its size on this task.
We gave all three models the same 150 photographs and the same target text (the 8B’s answers) and recorded every layer on about 13,700 answer tokens. Then we switched parts off and measured what was lost.
| Model | Residual width | Directions holding 90% of the variance (median layer) | Share of the width | Layers removable one at a time (≤ 1 pt digit loss) |
|---|---|---|---|---|
| 8B | 4,096 | 151 | 3.7% | 19 of 36 |
| 4B | 2,560 | 134 | 5.2% | 33 of 36 |
| 2B | 2,048 | 122 | 5.9% | 15 of 28 |
The task lives in about 120 to 150 directions, whatever the width; the participation ratio is about 38 in all three. The 8B spends 4,096 dimensions on what fits in 151. Its extra size buys spare copies, not new features: half its MLP can go and the reading holds, where the 2B has no slack at all. In every model only the first layer is irreplaceable.
Two cautions. The target text is the 8B’s own, so its starting accuracy (99.8%) is flattered; compare the drops, not the starting points. And the activations are dense: 74–82% of neurons carry 90% of the activation mass in every model, so the spare capacity shows only when parts are removed, not when activity is counted.
Serving it: the small model was slow until the engine changed
On the same GPU, the 2B at 2× first read a photograph set in 29 s, slower than the 8B’s 24 s. A serving engine took it to 10 s.
The time was not in the model. Per photograph, the vision encoder took 1.0 s and the prefill 0.4 s; then decoding ran at 25 ms a token. Reading 2B bf16 weights once per token takes about 5 ms on that card. The other 20 ms was the generation loop launching hundreds of small GPU kernels from Python, one after another. A large model hides that overhead behind real work in each kernel; a 2B model cannot.
A serving engine (vLLM) records a whole decode step once as a CUDA graph and replays it with a single launch. It also batches the three photographs of a set as they arrive, and drops each one as its answer ends. A set then takes about as long as its longest photograph, not the sum. Batching in the old loop made things worse (38 ms a token), because it pads every answer to the longest.
| Setup, one A10 GPU | Per photograph set, warm | Per set, from cold | Values read correctly |
|---|---|---|---|
| 8B, plain generation loop | 24 s | ~45–50 s | 94.6% |
| 2B at 2×, plain generation loop | 29 s | — | 96.7% |
| 2B at 2×, serving engine | 10 s | ~105 s | 96.7% |
| 2B at 2×, serving engine + GPU snapshot | 10 s | 14–19 s | 96.6% |
A serving engine is slow to start: loading weights and capturing graphs took about 105 s, and the reader scales to zero between sets. The fix is a GPU memory snapshot of the warmed engine, taken while it sleeps, with its weights parked in CPU memory and its empty KV cache dropped. A cold start restores the snapshot and wakes it.
The answers did not move. The served 2B makes the same 20 errors as the offline run. Between runs, about 6 of 1,250 raw values differ, all on faint photographs, because results depend slightly on which requests share a batch.
The GPU time billed per photograph set fell from about 120 s for the 8B to 45–65 s for the 2B, so a set costs less than half as much. Most of what remains is the platform keeping each container up about 44 s after its last read, whatever idle window we set.
Why we stopped at 2B
Warm, the served 2B spends about 1.75 s on each photograph before its first output token, then 12 ms per token. Nearly all of that 1.75 s is the GPU looking at the photograph.
| Step, per photograph | Time |
|---|---|
| Decode and enlarge the JPEG (CPU) | 0.07 s |
| Image preprocessing (CPU) | 0.28 s |
| Vision encoder and prefill (GPU), to the first token | ~1.45 s |
| Writing the answer (GPU) | 12 ms a token: 0.4 s for 34 tokens, 3.1 s for 263 |
At 2× a photograph becomes about 22,000 vision patches and 6,000 tokens. By our estimate that is around 9×1013 operations, which keeps an A10 busy for most of those 1.45 s.
We tried three more ways to go faster without losing accuracy. None was worth shipping:
- Speculative decoding from the prompt (n-gram lookup): 12 → 9.6 ms a token, but a set only 9.3 → 8.9 s, at 96.6% against 96.9%.
- Cropping to the foreground: the background is only 16% of the area, and accuracy fell to 96.2%.
- CPU instead of GPU: 126 s to the first token for one photograph on 8 cores, then 0.47 s a token, about 10 minutes a set.
A smaller student would not help much either. By our estimate about 70% of the work per photograph is the vision encoder, which is the same size in every model of the family. Shrinking the language model below 2B would save perhaps 3 s a set and restart the accuracy question.
In production, the first clean requests on the 2B took 27 s and 33 s end to end from a cold start, against 64–131 s with the 8B. About 10 s of that is reading; most of the rest is the GPU container starting.
How we found the saturation point
- Step down until accuracy drops. 8B 94.6%, 4B 93.3%, 2B 91.5%: it looked like the familiar size curve.
- Read the errors. 35 of the 2B’s 52 misses were one digit off, in the right field, at the right length.
- Look inside. On misreads the right digit was the second choice 18 times in 22, with the same attention on the image: the evidence was not reaching the decoder.
- Raise perception scale alone. Reading the same photographs at 2× fixed 17 of 22 misreads, broke none, and took the 2B to 96.7%.
- Check from above. The 8B still reads 98.3% of digits with half its MLP neurons off; the 2B has no such slack. The 2B sits at the edge of the plateau.
- Stop where the next step saves little. Below 2B only the language model would shrink, while the vision encoder does about 70% of the work.
What it means
Before buying a bigger brain, check the eyes. If a smaller model’s errors are near-misses on fine detail, its limit is perception, and resolution is cheaper than parameters.
- Read the errors before the score. A one-digit substitution and a wrong field are different failures with different fixes.
- Test the input before the architecture. Upsampling at inference cost us nothing to try and moved accuracy more than doubling the model did.
- Measure the serving stack, not just the model. At 2× the 2B sees 4× the image tokens, and in a plain generation loop it ran slower than the 8B. The loop’s overhead, not the model, set the speed: a serving engine made the same 2B 2.4× faster than the 8B on the same GPU.
- Stop where the curve goes flat. At 2× the 2B reads exactly what the 8B reads, at less than half the GPU time per set. Below it, the part that dominates the work, the vision encoder, would not shrink.
More head-to-head evals
| Measure | Qwen3 235B | GLM-5.2 | Gemini 3.8 Flash | Claude Opus 5 |
|---|---|---|---|---|
| Seconds to the plan, median | 27 | 48 | 57 | 94 |
| Plans written, of three runs | 2 | 3 | 3 | 3 |
| Measure | SubQ | jev | Decider | Nimble |
|---|---|---|---|---|
| Found, mean of five columns at section length | 64% | 65% | 66% | 68% |
| Mechanism of action, found at section length | 57% | 51% | 61% | 54% |
| Measure | ask | Apollo | treg |
|---|---|---|---|
| Usable leads | 13 of 40 | 7 of 40 | 6 of 28 |
| Cost per usable lead | $0.037 | $0.27 | $0.23 |
| Measure | ask with jev | ask with Decider |
|---|---|---|
| Questions right, of 92 | 89 | 89 |
| Wrong answers | 1 | 2 |
| Measure | jev | ask |
|---|---|---|
| Our 370 clinical decisions, right | 94.6% | 93.0% |
| PubMedQA, right (best EdgeEvals scored: 62.2%) | 77.0% | 74.4% |
| Measure | ask | Claude Opus 5.5 |
|---|---|---|
| Questions right, of 95 | 59 | 63 |
| Small companies right, of 20 | 13 | 9 |
| Measure | realestate-us | Best of four assistants |
|---|---|---|
| Floor-plan rents right, of 26 | 26 | 23 |
| Amenities claimed the listing doesn't state | 12 | 129 |
| Measure | ask | jev |
|---|---|---|
| Right, of the questions answered | 98% | 79% |
| Wrong answers, of 92 | 2 | 19 |
| Measure | Trained localiser | Best of four frontier models |
|---|---|---|
| Mean overlap with the defect (IoU, 0 to 1) | 0.421 | 0.109 |