~/satyajit

Julia-1: one score per mask token, and a benchmark that turns on how the labels are worded

mdjsonmcp

2026-09-27 · 16 min · explainer · encoders · small-models · benchmarks · evaluation · fine-tuning

"Our decision model that runs on almost anything." — Supersonic Labs, Introducing Julia 1

Two reposts arrived in the same week, and they belong together. Julia-1 is a decision model, one more of the kind this site keeps taking apart: give it a state, a question and 2 to 20 options, and it returns one option with a probability, never a sentence. Maxime Rivest's post is a learning curve: tiny encoders fine-tuned on Banking77, with the headline "4 Million parameter! 10 000 examples can get even BERT tiny to beat opus and kimi!"

Both are small encoders answering "which of these labels?", from opposite ends of one axis: how much of the task the model saw before the test. This piece takes Julia apart, re-runs its evaluation, and then puts the two setups side by side.

Every number is labelled: measured means I computed it from a file or a run, reported is the publisher's figure, and reasoned is my arithmetic on the other two.

Julia-1
Interfacestate + question + 2–20 options → one option and a full softmax (reported)
Weights144,292,870 F32 parameters in one model.safetensors, Apache-2.0 (measured, from the header)
Backbonejhu-clsp/mmBERT-small: 22 ModernBERT layers, width 384, 256,000-token vocabulary (measured, encoder/config.json)
Decision path above the encoder3,699,073 parameters (measured)
Headline1,463 of 2,000 typed decisions (73.15%) against Jev's 72.70% (reported)
Reproduced here1,451 of 2,000 on CPU, identical to the card's CPU run; 94 and 86 of 100 on the AG News and Emotion pilots (measured)
Banking77 pilot, 72 labelscard: 64 of 100 (reported); released Router: 62 with the benchmark's label sentences, 91 with intent names (measured)
SupersonicLabs/Julia-1@a85b127 · snapshot 2026-09-27
parameters
144.3M
repo size
612.5 MB
task
text-classification
library
pytorch
license
apache-2.0
safetensors
1 shard
largest file
577.2 MB
files
46
downloads
274
likes
145
parameters by dtype
F32144.3M
decision-modeltext-classificationmultilingualrouting

144,292,870 parameters: 98,304,384 in the embedding table, 42,189,312 in the 22 encoder layers and final norm, 3,699,073 in the decision path, and a 100,098-parameter action head that no released inference path calls.

repo last modified 2026-09-26

From a state to one decision

An encoder cannot answer by generating, so Julia puts the options into the input and scores one position per option. The format is one function, sequence() in julia/data.py, mirrored by the ONNX repo's Rust encoder. For the card's routing example it writes:

<bos> choice question: Which team should handle this request? <eos>
<mask> Billing and payment disputes <mask> Shipping and delivery <mask> Account access and login <eos>
I was charged twice for the same order. <eos>

The question comes first, prefixed with the decision type in plain words. Each option follows its own <mask> token, mmBERT's masked-language-model token reused as a marker, and the state goes last. The model reads the line bidirectionally in one pass and scores only the three <mask> positions (12, 17 and 21 here; measured).

The same function sets the limits: 2 to 20 options, each at most 48 tokens, question plus options within a head budget (256 tokens by default, 512 in the card's example), and 8,192 tokens in all. With strict_encoding=True any overflow raises instead of truncating.

The named-question API adds one more detail. In criteria={"billing": "Billing and payment disputes", …} the model reads only the description; the key billing is returned to you but never reaches the encoder.

I did not run Supersonic's Python or JavaScript. I rewrote the serializer from data.py and ran the official ONNX export with onnxruntime 1.30 and the tokenizers wheel. It reproduces the Rust test's reference ids token for token, and all 100 of the ONNX repo's published PyTorch parity cases: same argmax on every one, largest logit difference 7.3e-5 (measured). The input format is known, not guessed.

The decision head

The encoder is mmBERT-small: ModernBERT's block, with 22 layers of width 384 and 6 heads. Every third layer attends globally and the rest use a 128-token sliding window (measured, config). On top of it, JuliaDecisionModel.forward does four things:

  1. Adds one learned row of a 3×384 type table to every position: row 0 for choice, 1 for score, 2 for noul.
  2. Runs two more pre-norm transformer layers (6 heads, feed-forward 1,536) over the whole sequence, so the markers attend to the state and to each other once more after the encoder.
  3. Gathers the hidden state at each <mask> position.
  4. Scores each one with LayerNorm → Linear(384, 384) → GELU → Linear(384, 1) and takes a softmax across the options.
sk=w2⊤ GELU ⁣(W1 LN(hmk′)+b1)+b2,h′=Head2 ⁣(Enc(x)+et),pk=esk∑jesjs_k = w_2^{\top}\,\mathrm{GELU}\!\left(W_1\,\mathrm{LN}(h'_{m_k}) + b_1\right) + b_2,\qquad h' = \mathrm{Head}_2\!\left(\mathrm{Enc}(x) + e_{t}\right),\qquad p_k = \frac{e^{s_k}}{\sum_j e^{s_j}}

Here xx is the token sequence, ete_t is the type row for decision type tt, mkm_k is the position of option kk's marker, and hmk′h'_{m_k} is that position's state after the two head layers. The head's output width is 1, so 2 options and 20 use the same weights. Reading the safetensors header (two HTTP range requests, no weights downloaded) gives the split:

componentparametersshareused in a decision?
token embeddings, 256,000 × 384, plus norm98,304,38468.1%one row per token
22 encoder layers and final norm42,189,31229.2%yes
two head transformer layers3,548,9282.5%yes
scorer MLP148,9930.1%yes
type embedding, 3 × 3841,152yes
act_head100,098no released path calls it
temperature buffer, (1, 1, 1)3never read
total144,292,870

All figures are measured. Two thirds of the model is the multilingual vocabulary; mmBERT-small's card lists 140M total and 42M non-embedding parameters (reported). The act_head reads the first position's state plus four features of the softmax (top probability, margin, normalised entropy, option count) and outputs two logits: it looks like an answer-or-abstain gate (reasoned). Nothing documents it, the ONNX export never calls it, and inference-policy.json says "calibration": null.

This is the family of GLiNER2.5-Decide and Laya: options as tokens in one bidirectional sequence, one scalar read per option. Julia adds the type row and two attention layers before the readout. The consequence is the same: order is part of the input. Reversing the option list on the 100-item pilots below changed 12 AG News answers (94 correct became 87) and 7 Emotion answers (86 became 87) (measured). GLiNER2.5-Decide changed 3.2% of its reorderings, on a different sample and procedure (reasoned comparison, not paired).

Three modes through one head

choice, score and noul share every weight; only the type row and the word in the question prefix change. They differ in what you read off the softmax:

The descriptions carry much of the load. In the card's CPU run, replacing the 400 descriptive Boolean criteria with literal false/true drops Boolean accuracy from 483 to 391 of 600 (reported).

one request, one forward pass · measured37 tokens · 3 markers
state: “I was charged twice for the same order.”
question: Which team should handle this request?
1 · what the encoder reads, to scale

■ CLS/SEP ■ “choice question: …” (10) ■ a <mask> plus each option (5 + 4 + 5) ■ state (9). Markers at positions 12, 17, 21.

2 · after the 22 encoder layers: add one type row to every position
type_emb[0] · choicetype_emb[1] · scoretype_emb[2] · noul→ 2 more transformer layers → read the 3 marker states
3 · scorer: one number per marker, softmax across them
  • billing0.8545
  • shipping0.1428
  • access0.0028

choice = billing (the key; the model never saw the word “billing”)

Recorded on 2026-09-27 from SupersonicLabs/Julia-1-ONNX on CPU, through my own reimplementation of the request serializer (checked against the Rust encoder's reference ids and the 100 published PyTorch logits). The descriptions, not the keys, are what the model reads. Gold labels are typed-decisions' own; its gold for action is itself split, 0.50 on answer_directly and 0.34 on escalate_to_human.

The widget replays outputs I recorded, including all five questions of typed-decisions case customer_service_000000. Julia gets four right, each at 0.73 or above, and misses action (request_information at 0.84), where the dataset's own gold is split: 0.50 on answer_directly, 0.34 on escalate_to_human.

The probabilities are a softmax at temperature 1, with nothing fitted. On the pilot items below I measured an expected calibration error of 0.021 on AG News and 0.129 on Emotion (10 bins), where mean confidence was 0.984 against 86% accuracy. Jev's figures on the same items were 0.064 and 0.351 (reported, pilot report).

More than 20 options: the Router

The head is trained for 2–20 options. For longer lists, julia/router/router.py runs a tournament:

  1. Split the candidates, in order, into consecutive groups of at most 20.
  2. Score every group in one batch.
  3. Keep the winner alone if its softmax is above 0.95 and every other option is below 0.045; otherwise keep the top two (survivors=2).
  4. Repeat until 20 or fewer remain, then make one final call.

Sixty options cost 3 calls in the first round and 1 final call. The 72-label Banking list costs 4 plus 1 (reasoned from the rule; the 5 is measured below).

The card states two consequences: a correct answer that loses its group is gone, and the final probabilities cover only the final candidates. A third is only in the code. Router.__init__ constructs a BendReducer, which raises unless the native libjulia_router.so was built with Bend 2.0.27 and Clang on Linux. The card's "no native router build is needed" is true of the engine, not, as far as I can read, of the Router (reasoned from code; not executed).

more than 20 options: the Routerwidth 20 · native limit
no group decisive · 4 model calls · 2 rounds
round 1
202020
→ 6
final
6
1 pick
every group decisive · 4 model calls · 2 rounds
round 1
202020
→ 3
final
1 pick
measured · Banking77 pilot, 72 labels, survivors 2
|

“I got my American Express in Apple Bay but top up is not working” · gold: apple pay or google pay (#3)

#0–19 · top p 0.72
keeps automatic top up
keeps card not working
gold dropped here
#20–39 · top p 0.67
keeps contactless not working
keeps disposable card limits
#40–59 · top p 0.53
keeps top up by card charge
keeps top up failed
#60–71 · top p 0.96
keeps transfer not received by recipient
  • automatic top up0.000
  • card not working0.000
  • contactless not working0.000
  • disposable card limits0.000
  • top up by card charge0.000
  • top up failed1.000
  • transfer not received by recipient0.000

gold never reached the final call · over all 100 items: gold reached the final call 78 times, final pick right 62 times, 5 model calls each

The schedule is the Router's rule worked out for any list length; the traces are measured. A final probability of 1.000 means certain among the final candidates, which may no longer include the right answer. Same 100 items and same Router both times; only the wording of the 72 labels changes.

I ran my reimplementation of the rule on the Banking77 pilot described below: 72 labels, two survivors, 5 model calls per decision. The correct intent reached the final call on 78 of 100 questions, and the final pick was right on 62 (measured). In one trace the right answer is dropped in round 1, and the final call then chooses among what is left at probability 1.000.

The evaluation, run again

The card is candid about its evaluation, and the candour holds up.

The Jev column was not re-run. It comes from AbdelStark/jev-benchmarks, pilot v1. That pilot ran on 17 September against jev-1.13.0, on 100 deterministically sampled, approximately class-balanced test items from each of three BTZSC datasets. BTZSC phrases every label as a sentence ("This example news text is about sports"). Its Banking77 has 72 of the 77 intents, and the pilot drops the 200 test rows whose intent has no sentence (reported). The typed-decisions reference, 72.7%, is Jev 1.13.0's row on that dataset's own leaderboard (reported).

I rebuilt the pilot. I rewrote the pilot's sampler from its source and hashed the 300-item manifest the way the runner writes it. The result is ec064c52…, the SHA-256 the pilot report publishes, so these are the same items Jev saw (measured). Julia, with the pilot's framing (the text as {"text": …}, "Which single label best describes the input text?", the BTZSC sentences as options), gets 94/100 on AG News and 86/100 on Emotion. Those are the card's numbers exactly (measured). On the full typed-decisions test set, 400 cases and 2,000 questions, it gets 426/600 choice, 542/800 score and 483/600 Boolean. That is the card's CPU result to the question, 1,451 (measured).

A hundred items is a wide interval. The 95% Wilson interval on 94/100 is 87.5–97.2%, and on Jev's 91/100 it is 83.8–95.2%: the AG News lead is not resolved. Emotion (86 against 48) and Banking77 (64 against 87) are separated (reasoned). I could not do a paired test, because Jev's per-item predictions are not published.

A grouped bar chart titled 'Julia 1 across four tasks'. Blue bars for Julia 1, grey for the Jev reference: typed decisions about 73% each, AG News 94% against 91%, DAIR Emotion 86% against 48%, Banking77 64% against 87%.
The headline chart, rendered from the SVG on Supersonic's page. Julia's three pilot bars are 100 items each, and the grey bars are a separate 17 September run of Jev, not a re-run. I reproduced the AG News and Emotion bars exactly on the same 100 items. (Supersonic Labs, Introducing Julia 1, results chart.)

The pilots are in Julia's training distribution. The ONNX repo publishes 100 parity requests drawn from data/fast-frontier-v2/validation.jsonl, Julia's own validation file. Of those, 26 are AG News topic questions, 13 are DAIR Emotion questions and 12 are Banking77 intent questions. Every one of those 51 states appears verbatim in its dataset's train split, and none appears in a test split (measured). provenance.json lists validation sets named agnews, emotiondair, banking-rank and banking-joint, and "replay" sets for all three (reported). It also lists sets named after typed-decisions' four workflows, a dataset whose leaderboard separates "general, zero-shot" entries from "specialist, fitted per workflow" ones. The card does not describe the training data. The reading that fits is that Julia was trained on these tasks' train splits and scored on their test splits (reasoned). If so, that is not contamination, since no test item turned up, but the pilots are not zero-shot either.

The Banking77 gap is mostly wording. The card's 64 went through a "ranking/top-16 shortlist" that is not in the repository. Its provenance reports 83/100 with a 99% top-16 recall on an internal Banking77 set (reported). The released Router scores 62 on the pilot with the BTZSC sentences. On the same 100 questions, with the same Router, I swapped each sentence for a bare intent name and the question for the one Julia's validation file uses ("Which banking intent best describes this request?"). The score went to 91/100, and the correct intent reached the final call 97 times instead of 78 (measured). The label strings are part of the model's input, and they move this result by 29 points.

Knowledge is outside what the pilots test, and the card says so: its provenance has MMLU at 52/198 (26.3%) and ARC-Challenge at 57/200 (28.5%) (reported), near the 25% of guessing among four options (reasoned). The same file carries an undocumented "quality_gate": false. MASSIVE scenario accuracy runs from 44.9% in Amharic to 86.8% in US English across 52 locales (reported).

The other repost: 9,493 labels and a learning curve

Rivest's primary source is examples/banking77_lmfn/RESULTS.md in his lmfn repository. The chart itself is only in the post, so I describe it rather than reproduce it.

MaximeRivest/lmfn@2ba6153 · snapshot 2026-09-27
tracked files
51
license
Apache-2.0
branch
master
tests
6 files
source
195.7 kB
commit date
2026-09-26
source by language
Python195.7 kB(33)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-09-27 at 2ba6153 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

The setup is ordinary supervised fine-tuning. Banking77 has 9,993 training questions; he holds 500 out for validation and trains on up to 9,493. Each student is trained end to end with a 77-way classification head on one RTX 3090, with three seeds, and scored on all 3,076 test questions (reported). The Ettin-17M curve, from RESULTS.md:

human-labelled rows (≈ per intent)Ettin-17M accuracytraining time
100 (1.3)14.6%7 s
500 (6.5)44.7%23 s
1,000 (13)66.7%44 s
2,000 (26)81.0%43 s
4,000 (52)87.2%42 s
9,493 (123)91.5%42 s

All reported. RESULTS.md also gives ModernBERT-base at 93.2% on the full data. The other endpoints appear only on the chart in the post (reported): BERT-tiny 89.2%, Ettin-32M 92.5%, Ettin-68M 93.2%, Qwen3.5-0.8B 93.8% and ModernBERT-large 94.2%.

The chart's dashed lines are "Claude Opus ≈ 92%" and "best distillation-allowed model ≈ 84%". In the file, Claude Opus 5.5 scores 92.0% on 200 random test questions, ±1.9 points. Models whose terms allow training on their outputs top out at 83.5% (Kimi K3, Qwen3.8 2.4T), 84.5% for the best five-model vote chosen on the same questions, and 85.0% for Kimi K2.6 at its highest reasoning setting (reported).

Read against its own chart, the headline claim is half right. BERT-tiny's 89.2% clears the distillation-allowed line and Kimi. It sits below the Opus line. The students that cross 92.0% are Ettin-32M and up, and Ettin-17M's 91.5% is inside Opus's ±1.9 (reasoned).

Rivest states the main caveat himself: the test labels come from the same annotators as the training labels, so accuracy against them "measures fit to this labelling scheme, not only understanding." And Banking77 has been public since 2020, so Opus may have memorised some of it.

Two setups, not a ranking

The numbers answer different questions:

Rivest's studentsJulia-1 on the pilot
where the label set livesin the weights: a 77-way output layerin the input: 2–20 strings per call
labelled data for this task100 to 9,493 human labels, statednot stated; its validation file holds Banking77 train questions
testall 3,076 questions, 77 intents100 class-balanced questions, 72 intents
what a new intent costsnew labels and a retrain (42 s at 9,493 rows)a new string, with accuracy unknown until measured
what moves the numberlabel count: 66.7% at 1,000 rows, 91.5% at 9,493label wording: 62 or 91 on the same items

The curve prices the supervised route: about 13 labelled messages per intent buys Ettin-17M 66.7%, and 26 buys 81.0%. A decision model promises to skip that bill by reading what a label means. Julia shows both sides: 91/100 on 72 intents with no retraining when they are written the way its own validation file writes them, 62 when they are sentences.

Jev's Banking77 number moves too: 87/100 on the BTZSC pilot, 79.4% on Rivest's full 77-intent test (reported by each). Sample, label count and wording all differ, so the gap cannot be apportioned (reasoned).

Neither Julia number is zero-shot on an unseen task, and Rivest's 91.5% is 9,493 labels. So the rule is short. With a fixed taxonomy and a few thousand labels, fine-tune a small encoder; the curve says under a minute on one GPU. With labels that change per call and no data, use a decision model, and the four gates apply. Either way, freeze the label strings and their order with the model version, and measure with your own wording.

Does it run on almost anything?

On CPUs, mostly yes. Supersonic reports medians of 33.15 ms per decision on an Apple M4 (four threads, 100-word state, four options), 294.81 ms per typed decision on an Intel Core i5-1235U, 203 ms on a Samsung tablet through ONNX Runtime, and 75.47 ms on WebGPU in a browser, with 100 of 100 predictions matching PyTorch (all reported).

Mine are from a shared 4-core Xeon at 2.10 GHz with a load average near 7.7, ONNX Runtime at four threads. A cold start takes 3.4 to 5.7 s, mostly loading the 34 MB tokenizer and the session. A warm decision takes a median of 25 ms for the card's 37-token example and 62 to 74 ms for a 136-token request. The full typed-decisions set, median request 308.5 tokens, took 572 s at two threads (all measured).

The outlier is Banking through the shortlist: a median of 3,713.54 ms per decision on the i5, against 107.83 ms for AG News (reported).

The ledger

Genuinely new. An Apache-2.0 decision model on a multilingual encoder: 3.7M parameters of head on 140M of backbone, three typed modes through one scorer. An ONNX export with published parity cases, which made an outside reproduction possible, and an evaluation that states its own losses.

Overstated, mildly. "No native router build is needed" holds for the engine, not the Router. The pilots follow a zero-shot protocol, but questions from the three tasks' train splits are in Julia's validation data. And Rivest's "beat opus" is not what his chart shows for BERT-tiny.

Unmeasured. The training data. The act_head, and the top-16 shortlist behind the published Banking77 number. Calibration. And accuracy under anybody's label wording but the benchmark's, which, for a model whose options are its input, decides the result.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Julia-1: one score per mask token, and a benchmark that turns on how the labels are worded", ai.thesatyajit.com, September 2026.

bibtex
@misc{ghana2026julia1decisionmodel,
  author = {Satyajit Ghana},
  title  = {Julia-1: one score per mask token, and a benchmark that turns on how the labels are worded},
  url    = {https://ai.thesatyajit.com/articles/julia-1-decision-model},
  year   = {2026}
}
share