rsi-jev-v4.0-vl-qwen3.5-2b
A typed-decision model: Qwen/Qwen3.5-2B-Base with its tower fine-tuned and a
trained option-scoring head. It answers choice / noul / score questions about
a document and up to four images in one forward pass, and speaks the
Jev HTTP API.
Images need the vision extra (Pillow, torchvision): pip install "rsi-jev[vision] @ git+https://github.com/Shanghua-Gao/RSI-Jev". The vision encoder and image processor are read from the base model.
This checkpoint (seed 17, one seed):
| typed-decisions | reversed order | MMLU-Pro 1k | reproduces its training run |
|---|---|---|---|
| 0.787 | 0.7835 | 0.385 | 1.0000 |
Re-computed from these exact files after upload-time reload. Kernel stack fla-0.5.2/torch-2.7.1+cu128; full recipe in meta.json.
import sys; sys.path.insert(0, 'code') # after snapshot_download of this repo
from load_release import load_release
model, tok, enc, meta = load_release('.', device='cuda')
print(meta['calibration']) # the fitted calibration this checkpoint ships, or 'none'
load_release applies calibration.safetensors when the checkpoint carries one, so the probabilities are the calibrated ones this card reports. A loader that skips those files returns the raw distribution instead: the same chosen option, different numbers.
Everything below is the version record from the project repository, unchanged.
RSI-Jev v4.0-VL
Describes release v4.0-VL · updated 2026-09-30
RSI-Jev now reads images. Send one to four pictures with a request — a photo, a screenshot, a scanned page, a chart — and ask typed questions about them: yes/no, pick-one-of-k, rate-on-a-rubric. As before, one forward pass returns a calibrated probability for every option, with no generated text. Text requests work exactly as they did.
On five image benchmarks held out from training it answers 80.3% of questions correctly; with the images blanked out it drops to 45.8%, so the answers come from the pictures. On text it keeps v3.0's level: 0.756 on the fifteen-benchmark suite, 0.653 on the held-out set.
v4.0-VL is v3.0's lineage with three rounds of image training, a calibration data round, and a new RL stage on top. It is a 2B model, one seed.
1. The task
Unchanged from v1.0, plus images: a document (the state), now
optionally with 1–4 images placed in it by <image> markers, and one or more typed questions
— noul, choice, score — with a probability per option, returned in one forward pass.
Top-1 is the fraction of decisions whose highest-probability option matches the label;
ECE is expected calibration error, the average gap between the confidence a model reports
and how often it is right at that confidence, lower being better.
2. How the model is built and trained
Qwen3.5-2B-Base, with v1.0's option scorer, v2.0's confidence head and v3.0's MLP readout.
Images go through the base model's own vision tower, frozen: its weights are not in this
checkpoint and are loaded from Qwen/Qwen3.5-2B-Base. Every request gets at most 1,024 image
tokens per question, split evenly across its images.
The stages, each continued from the one before:
- Text, supervised. v3.0's data-scale-2 corpus with the Jev-Style sources capped at 3,000 questions each, 14,233 steps.
- Images, supervised, 4,000 steps. 33,906 image questions from the train splits of twelve public datasets (DocVQA, ChartQA, VQAv2, A-OKVQA, NLVR2, Hateful Memes, Screen2Words, AI2D, ScienceQA, TallyQA, RVL-CDIP, TQA), mixed with 30,094 questions of text replay. Free-form answers became four-way choices with real distractors, or balanced yes/no questions.
- v3.0's listwise reranking RL stage, run on the image model.
- Images, second round, 3,000 steps. 17,352 new image questions — more public VQA train sets and six synthetic generators (UI state, change detection, line rules, grid games and others) — with 51% text replay.
- Calibration data, 3,000 steps. 8,492 generated reasoning questions (lie chains, object swaps, orderings) at depths where the model is at chance, trained toward soft targets so it learns to say so. These share task families with BIG-Bench Hard; see §4.1.
- An asymmetric RL stage, 1,500 steps. The reward is correctness minus a confidence
penalty that charges a confident mistake four times what it charges a timid correct answer:
R = c − 4·max(0, p − c)² − 1·max(0, c − p)², withpthe probability on the chosen option andcwhether it was right. 30% of its rows carry images. It calibrates in the weights: held-out ECE before any calibration falls from 0.200 to 0.082.
Against a matched supervised stage — the same data, steps and seed, with only the objective changed — the asym stage gave better held-out accuracy and calibration, with no detectable ranking difference (one seed):
| both after the same calibrator selection | asym stage (v4.0-VL) | matched supervised stage |
|---|---|---|
| held-out, pooled top-1 | 0.6528 | 0.6442 |
| 15-benchmark suite, top-1 | 0.7564 | 0.7578 |
| held-out without BIG-Bench Hard, top-1 | 0.703 | 0.694 |
| held-out ECE | 0.0822 | 0.0871 |
| suite ECE | 0.043 | 0.055 |
| MMLU-Pro 1k | 0.385 | 0.375 |
| images, held-out top-1 | 0.803 | 0.804 |
| held-out without BIG-Bench Hard, AUROC | 0.6935 | 0.7017 |
The AUROC gap is −0.008 with a paired bootstrap standard error of 0.006.
Calibration. The confidence head (oof_head_scorefloor, as in v2.0–v3.0) is refitted on
8,464 held-out questions: one in ten of the text corpus, plus 1,251 image questions weighted
0.15. Cases whose document or image also appears in a trained case were dropped first. It
rescales each question's probabilities without changing which option wins.
3. Checkpoints
| model | base | 15-benchmark suite | held-out (eval_final_v2) | suite ECE | images | download |
|---|---|---|---|---|---|---|
| RSI-Jev-v4.0-VL-2B | Qwen3.5-2B-Base | 0.756 | 0.653 | 0.043 | yes | 🤗 download |
| RSI-Jev-v3.0-2B | Qwen3.5-2B-Base | 0.756 | 0.649 | 0.066 | no | 🤗 download |
| RSI-Jev-v2.1-2B | Qwen3.5-2B-Base | 0.736 | 0.633 | 0.059 | no | 🤗 download |
| RSI-Jev-v1.0-2B | Qwen3.5-2B-Base | 0.622 | 0.604 | – | no | 🤗 download |
v4.0-VL and v3.0 were scored in one job by the same evaluator; the v2.1 and v1.0 rows are from v3.0's record, on the same evaluator, and v3.0 reproduces its own published numbers exactly in this job. 2B only, one seed's checkpoint.
Reproducing its training run.
Re-scored with this repo's loader, the checkpoint agrees with its training record on every
question: typed-decisions 0.787 and 0.7835 (reversed order), MMLU-Pro 0.385, agreement 1.0 on
each (verify.json). Reading a document once and answering every question from it gives the
same numbers as reading it per question: on CPU in fp32 the largest difference is 2e-6 over 100
questions (cached_agreement.json); on the GPU the fp32 kernels leave up to 5e-4 and no answer
changes over 500. The served image path matches the offline image encoder on all 50 image items.
Reloaded from the saved files with calibration applied, the checkpoint agrees with the
in-memory model on 200 of 200 text questions (largest probability difference 0.0005) and 50
of 50 image questions (roundtrip_200.json). On a second GPU architecture (Blackwell), this
repo's image path and the training code agree with each other to 1e-7 and differ from the
first GPU on one of the 50 image questions, whose top two options sit at 0.376 and 0.400:
the same order-of-summation effect v2.1 §3 describes for text.
MANIFEST.json holds the sha256 of every file.
The checkpoint ships calibration.safetensors and calibration.json; load_release.py applies
them automatically. Without them the model loads uncalibrated and its probabilities match no
number in this card.
Apache-2.0, following the base model, as every release is.
4. Results
4.1 The held-out comparison — read this first
eval_final_v2, as in v3.0: LegalBench (11 tasks), MuSR, BIG-Bench Hard (5 tasks), the relevance split of the Berkeley Function-Calling Leaderboard, and ConditionalQA, built from sources in no training corpus.
| held out from training | n | v3.0 | v4.0-VL | v4.0-VL − v3.0 | paired p |
|---|---|---|---|---|---|
| LegalBench | 983 | 0.683 | 0.667 | −0.015 | 0.18 |
| MuSR | 756 | 0.546 | 0.546 | 0.000 | 1.0 |
| BIG-Bench Hard | 1,109 | 0.500 | 0.533 | +0.033 | 0.018 |
| BFCL relevance | 758 | 0.910 | 0.902 | −0.008 | 0.35 |
| ConditionalQA | 135 | 0.741 | 0.726 | −0.015 | 0.79 |
| pooled | 3,741 | 0.649 | 0.653 | +0.004 | 0.56 |
| pooled without BIG-Bench Hard | 2,632 | 0.712 | 0.703 | −0.009 | 0.18 |
Both models were scored in one job, post-calibration. p is an exact two-sided McNemar test on the questions where the two disagree (pooled: 258 won by v4.0-VL, 244 by v3.0). BIG-Bench Hard is not held out for this release: stage 5's generated questions share its task families, so the row without it is the held-out number. Held-out ECE is 0.082.
4.2 The fifteen-benchmark suite
| benchmark | weight | trained on its train split? | v3.0 | v4.0-VL | v4.0-VL ECE |
|---|---|---|---|---|---|
| typed-decisions | 0.20 | yes | 0.791 | 0.787 | 0.025 |
| Nimble public | 0.121 | yes | 0.801 | 0.801 | 0.022 |
| MMLU-Pro 1k | 0.081 | no | 0.364 | 0.385 | 0.078 |
| Jev-Style panel | 0.081 | partly (emotion, SST-5, ANLI) | 0.835 | 0.823 | 0.026 |
| Kev transfer | 0.065 | no | 0.792 | 0.784 | 0.034 |
| Kev hard | 0.065 | yes | 0.760 | 0.762 | 0.020 |
| JevBench | 0.049 | no | 0.700 | 0.696 | 0.106 |
| tasksource | 0.05 | yes | 0.701 | 0.707 | 0.033 |
| SemIf external | 0.05 | no | 0.921 | 0.913 | 0.122 |
| scienthoon OOD | 0.05 | no | 0.705 | 0.754 | 0.042 |
| Nimble holdout | 0.041 | yes (Nimble train) | 0.778 | 0.769 | 0.066 |
| Kev documents | 0.041 | yes | 0.869 | 0.856 | 0.060 |
| Kev devtools | 0.041 | yes | 0.715 | 0.717 | 0.043 |
| procedural | 0.033 | yes | 0.871 | 0.862 | 0.017 |
| Open-Jev OOD | 0.032 | yes (Open-Jev train) | 0.838 | 0.828 | 0.045 |
| suite mean | 0.756 | 0.756 | |||
| suite ECE | 0.066 | 0.043 |
Ten of the fifteen benchmarks contribute training data in some form, so none of these numbers is zero-shot; §4.1 is the comparison that is.
4.3 Images
Held out from training. Five public benchmarks, 4,173 questions, equal weights. Their images were checked against every training image (sha256 and perceptual hash) and their text against the training text; InfographicVQA was kept out of training entirely. "Blank" replaces every image with a blank one and asks the same questions.
| benchmark | n | top-1 | ECE | blank |
|---|---|---|---|---|
| MMBench (dev) | 992 | 0.845 | 0.040 | 0.290 |
| RealWorldQA | 591 | 0.707 | 0.085 | 0.371 |
| POPE | 853 | 0.912 | 0.052 | 0.502 |
| HallusionBench | 937 | 0.695 | 0.144 | 0.503 |
| InfographicVQA (val) | 800 | 0.858 | 0.026 | 0.624 |
| mean | 4,173 | 0.803 | 0.069 | 0.458 |
The confidence head brings image ECE from 0.120 to 0.069 without changing any answer.
To score these yourself: scripts/vision_benches/heldout.py.
Other public benchmarks, not checked against the training images, run as served (bf16 tower, calibration on):
| benchmark | n | result | ECE | wrong with p ≥ 0.99 |
|---|---|---|---|---|
| MMStar | 1,498 | 62.4% | 0.108 | 1 |
| BLINK | 1,901 | 56.3% | 0.071 | 0 |
| VisA anomaly detection, 12 categories | 2,162 images | image AUROC 86.6 (95% CI 85.1–88.1) |
VisA is zero-shot: the model is shown one image and asked whether the part is defective, and
has seen no examples of normal parts. The score averages both option orders.
To re-run all three, with the paired comparisons: scripts/vision_benches/.
Laya Vision's validation set (6,357 questions over 34 sets), against Laya Vision's own calibrated predictions on the same items. "Clean sets" drops 14 whole sets that overlap this release's training data, among them IconQA, Visual7W and OCR-VQA, which it trained on.
| questions | n | v4.0-VL | Laya Vision | difference (95% CI) |
|---|---|---|---|---|
| choice and yes/no, clean sets | 2,516 | 0.828 | 0.713 | +0.115 (+0.094, +0.136) |
| all types, all sets | 6,357 | 0.763 | 0.717 | +0.046 (+0.032, +0.060) |
| all types, clean sets | 3,716 | 0.683 | 0.664 | +0.019 (−0.001, +0.038) |
It loses on the rating sets: aesthetic scores on AVA (0.140 against 0.845), CrisisMMD, RichHF
and VLFeedback. Calibrated ECE is lower than Laya Vision's on every pool (0.061 against 0.087 on
the clean sets).
Re-run: scripts/vision_benches/laya.py.
The demo gallery. 143 pictures and 227 questions across road scenes, "is it there?",
counting, charts, screens and factory parts: 187 answered right (82%), median 62 ms per call on
the GB10. Every miss stays in the gallery.
It also plays Breakout from pixels: 10 of 10 on the
game's lane check, and in a live game it cleared 7 of 24 bricks without losing a life before the
game's 200-decision limit.
Re-run: examples/gallery/ (every image ships with it) and
examples/breakout/.
4.4 Calibration and the properties we kept
Suite ECE after the confidence head is 0.043 (0.052 before it). Three properties external testing found useful were checked with the same scripts, in the same job as v3.0:
| property | v4.0-VL | v3.0 |
|---|---|---|
| no ties in the sort key: distinct P(true) values over 360 yes/no questions | 360 / 360 | 360 / 360 |
| questions bundled in one request do not affect each other: 16 questions on one document vs one at a time | 0 flips / 640 | 0 / 640 |
| stays uncertain when the answer is pure chance: mean probability on the picked option, 240 dice-style items (lower is more honest) | 0.447 | 0.480 |
5. Speed, the wire format, and what confidence means
Text inference is unchanged: one tower pass per question, the calibration a division inside the readout, nothing generated.
Images (../serve/README.md has the limits):
- A request carries
"images": ["data:image/png;base64,…"]besidestate: 1–4 PNG, JPEG or WebP data URLs. The state marks where each goes with<image>; without markers the images go first. URLs are not fetched. - The vision tower runs once per request, and the state with its image tokens is read once for
all the questions. With
--profile agent, a repeated image state is not read again. - Up to 1,024 image tokens per question (about one megapixel for a single image, 256 tokens each for four) on top of the 2,048 text tokens.
- On the GB10, one 640 px photo takes 80 ms with one question and 110 ms with four; a
1,600 px photo, 245 ms and 273 ms. Asking again about the same image under
--profile agenttakes 36–101 ms.../docs/inference.mdhas the table. - Image requests need Pillow and torchvision:
pip install "rsi-jev[vision]". Without them the server still starts and answers text; an image request gets a 422 saying what to install.
A structured criterion (an object rather than a string) is still rejected with 422, and a document longer than 2,048 tokens is cut from its start. With images, a request whose cut would reach an image is refused rather than cut.
The API returns a probability and a confidence, and they are different numbers: confidence
is the peak statistic Jev documents, (K * p_max - 1) / (K - 1). Route on the probability of the
option you intend to act on.
6. Limitations
- Images are capped at four per request and 1,024 tokens per question. A large photo is scaled down to fit, so small text in a full-page scan can be lost.
- Image requests are slower than text: an image adds hundreds of tokens, and a large photo up to 1,024 (§5).
- The vision tower is the base model's, unchanged. Everything this release learned about images is in the text tower and the scorer.
- BIG-Bench Hard is not held out for this release (§4.1).
- The weaknesses external testing found in v2.1 are still here: a lean toward the more severe option on ordered choices, rarely choosing "unknown" when a question cannot be answered from the document, and inconsistent answers to a question and its negation. Do not use it where an "unknown" option has to be honoured without checking it on your own data.
- The recipe has not been retrained from this repo. Its code, stage specs and data builders are here (§7). The stage corpora and the synthetic image questions were rebuilt with them to the same bytes (the rendered images differ slightly with font versions); the public image corpora need their downloads and were not rebuilt.
- Documents over 2,048 tokens lose their beginning (§5).
- One model, one size, one seed. The comparison with the matched supervised stage is one seed too.
7. How it got here
| step | arm | what it established |
|---|---|---|
| base | v3.0's stage-1 corpus (v4-rep-b-s17ck) | the text starting point |
| images | vis-ct-1 | the model reads images: held-out image top-1 0.809 against 0.796 for its text-only control |
| RL | rl5-listwise-vis | v3.0's listwise reranking stage, on the image model |
| images | vis-ct-3 | a second image round with synthetic generators |
| calibration data | calA-ce | generated reasoning questions with soft targets |
| RL | asym-calA | the asymmetric stage: held-out ECE before calibration 0.200 → 0.082. This checkpoint, with the confidence head refit |
Rejected along the way: a second image data portfolio that raised its own probes but not the
held-out image set (−0.006), and training the calibration round on true answers instead of soft
targets (it learned the reasoning families and failed held-out calibration). The full record is
../EXPLORE.md.
Rerunning it
Each stage's spec is in ../data/v4.0-vl_recipe/;
asym-calA.json is this checkpoint's own. Paths in them are relative to one data directory,
D below. The data steps run on CPU and download their public sources; nothing is shipped.
A fix that also applies to v2.1 and v3.0. Until this release, this repo's fit.py held the
lower layers' learning rate constant after warm-up (lower_layers_n), where the code that
trained v2.1, v3.0 and this release's supervised stages decayed them with the rest of the tower.
It now matches that code, so those recipes rerun as they were trained. The RL stages were never
affected.
# image corpora (vision_v3 is generated; it needs fonts, see the script)
python scripts/build_vision_v1.py --root D/vision_v1
python scripts/build_eval_vision.py --build D/eval_build
python scripts/decontam_vision.py eval --build D/eval_build --train D/vision_v1 --out D/eval_vision_v1
python scripts/build_vision_v2.py --root D/vision_v2 --v1 D/vision_v1 --eval-build D/eval_build
python scripts/decontam_vision.py v2 --root D/vision_v2 --v1 D/vision_v1 --eval-build D/eval_build --eval D/eval_vision_v1
python scripts/build_vision_v3.py --root D/vision_v3 --font-dir FONTS
python scripts/decontam_vision.py v3 --root D/vision_v3 --eval-build D/eval_build
# stage corpora; V3 is v3.0's SFT corpus and RL2 its RL file (versions/v3.0.md)
B="python scripts/build_stage_corpora.py"
$B rep-b --root D --ds2 V3
$B vis-ct-1 --root D --vision-v1 D/vision_v1
$B rl5-vis --root D --rl-hrr RL2/rl_hrr.jsonl
$B vis-ct-3 --root D --vision-v2 D/vision_v2 --vision-v3 D/vision_v3
python scripts/build_depthdial.py folds --folds D/depthdial/folds
python scripts/build_depthdial.py build --folds D/depthdial/folds \
--cells data/depthdial_cells_vis-ct-3.json --parent vis-ct-3 --out D/depthdial/vis-ct-3
$B calA-ce --root D --dd D/depthdial/vis-ct-3
$B rl2-b2 --root D --ds2 V3
$B calasym-stage --root D
# the six stages, each continuing from the one before (one GPU each; the image
# stages need an 80 GB card)
for s in v4-rep-b-s17ck vis-ct-1 rl5-listwise-vis vis-ct-3 calA-ce asym-calA; do
c=$(python -c "import json;print(json.load(open('data/v4.0-vl_recipe/$s.json'))['corpus_dir'])")
python scripts/release_train.py --model Qwen/Qwen3.5-2B-Base --seed 17 --root D \
--spec data/v4.0-vl_recipe/$s.json --corpus D/$c --save-dir D/$s/s17 --out D/runs --name $s
done
# the confidence head: one in ten of stage 1's corpus plus the vision_v1 holdout
python scripts/fit_release_calibration.py --ckpt D/asym-calA/s17 --root D --lineage \
--dev-corpus D/v4-rep-b-s17ck \
--dev-sources "$(python -c "import json;print(json.load(open('data/v4.0-vl_recipe/v4-rep-b-s17ck.json'))['sources'])")" \
--vis-dev-root D/vision_v1 --out D/asym-calA/s17
data/depthdial_cells_vis-ct-3.json is vis-ct-3's measured accuracy per task and depth,
from which the calibration round's soft targets are set; scripts/depthdial_measure.py
remeasures it from a vis-ct-3 checkpoint.
Image sources and their licences. The images are the original datasets', under their own terms; each builder's docstring records the exact revision. Of vision_v2, only the four sources below were trained on. Five sources are non-commercial or research-only, and several others carry no clear grant for their images:
| source | corpus | licence |
|---|---|---|
| DocVQA | vision_v1 | DocVQA terms; UCSF Industry Documents images — unclear |
| ChartQA | vision_v1 | GPL-3.0 (repository); charts from Statista, Pew, OWID, OECD — unclear |
| VQAv2, TallyQA, A-OKVQA | vision_v1 | annotations CC BY 4.0 / Apache-2.0; COCO images under Flickr terms — unclear |
| NLVR2 | vision_v1 | annotations CC BY 4.0; web images — unclear |
| Hateful Memes | vision_v1 | Facebook licence, research only |
| Screen2Words (RICO) | vision_v1 | CC BY 4.0; RICO screenshots — unclear |
| AI2D | vision_v1 | CC BY-SA 4.0 |
| ScienceQA | vision_v1 | CC BY-NC-SA 4.0 |
| TQA | vision_v1 | CC BY-NC 3.0 |
| RVL-CDIP | vision_v1 | IIT-CDIP / Legacy Tobacco terms — unclear |
| IconQA | vision_v2 | CC BY-NC-SA 4.0 |
| OCR-VQA | vision_v2 | non-commercial |
| TextVQA | vision_v2 | CC BY 4.0; OpenImages images |
| Visual7W | vision_v2 | none found — unclear |
| six synthetic generators | vision_v3 | generated by code |
The weights are released under Apache-2.0, as earlier releases were. Whether weights trained on non-commercial data inherit those terms is not settled; check the sources above against your use.