# Tiny browser models: six weeks of parameters, one week of shipping

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/tiny-browser-models
> date: 2026-09-18
> tags: webgpu, small-models, on-device-inference, browser-ml, edge-ai, model-architecture
Six projects, six task-specific neural networks, six browser tabs, zero
servers. All six ship as static assets small enough to sit next to a
favicon, and every one of them runs inference on WebGPU (or, for a single
short query, plain CPU JavaScript — more on that twist below) rather than
calling an API. The seed was Vercel Labs engineer Shu Ding's **gpu-lexer**: a
**41,321-parameter**, **27.64 KiB** (28,305 bytes, Brotli-compressed) syntax
highlighter that guesses a source file's language instead of being told it.
Within days, the same trick landed on four more problems — natural-language
dates, natural-language search filters, natural-language cron schedules, and
Arabic word morphology — plus one deliberately silly one: centering a div.
This piece characterizes all six completely, because at these sizes that is
actually possible, then builds the economic and architectural case for why
this is a real, narrow, recurring shape of problem and not just a viral
weekend format.

<Callout type="note">
I cloned all five public repositories (`vercel-labs/gpu-lexer`,
`arikchakma/gpu-time`, `safzanpirani/gpu-query`, `aaronvanston/neural-flexbox`,
`AhmedAbdel-Aal/tinysarf`) and read their training code, model cards, and
architecture docs directly. **gpu-cron has no public repository I could
find** — its author, credited in the brief for this piece as @ManuSchiller,
does not link one from the live demo, and I could not locate one by search.
Everything below about gpu-cron comes from testing the live demo at
`gpu-cron.vercel.app` myself, headlessly, in a real (headless) Chromium via
Playwright — including its most interesting failure. Every other project's
numbers come from its own README, `MODEL_CARD.md`, or `architecture.md`,
cited inline.
</Callout>

## The cluster, characterized completely

| Project | Author | Params | Shipped size | Task | Headline metric |
|---|---|---|---|---|---|
| **gpu-lexer** | Shu Ding (Vercel Labs) | 41,321 | 28,305 B (27.64 KiB) | syntax highlighting, no language ID | 83.02% Shiki agreement (held-out) |
| **gpu-time** | Arik Chakma | 38,745 | 45,561 B (44.5 KiB, full pkg) | English text → dates / RFC 5545 | 97.6% exact match, real English |
| **gpu-query** | safzanpirani | 29,597 | 40,960 B (40 KiB, weights) | search phrase → structured filter | 98.88% transfer accuracy (unseen schemas) |
| **neural-flexbox** | Aaron Vanston | 36,354 | 33,801 B (33.0 KiB) | approximate one CSS flex row | 94.03% of coords within 1px |
| **gpu-cron** | @ManuSchiller (no repo found) | 35,783 | ~35,494 B (bundle, measured) | English text → cron expression | not published — see below |
| **tinySarf** | Ahmed Abdel-Aal | 245,063 | 239,894 B (234.3 KiB, full pkg) | Arabic morphological analysis | 87.02% teacher-label agreement (not human gold) |

Every number in that table is either read directly from a committed
`MODEL_CARD.md`/README, or — for gpu-cron — printed live in the demo's own
footer (`35,783 parameters · WebGPU · 6-bit weights`). None of it is
estimated. That is unusual for anything claiming to be a machine-learning
result, and it is the direct consequence of the size: you cannot hide much
inside 30-word footers and 82-line training scripts.

## The seed: gpu-lexer's 27.64 KiB

gpu-lexer's whole public surface is one function:

```js
import { parse } from "gpu-lexer"

const spans = await parse("const answer = 42")
// [{ type: "keyword", start: 0, end: 5 }, ...]
```

No language argument, no bundled grammar. A CPU pass splits source into
words, whitespace, newlines, and individual symbols and packs cheap features
per part — kind, a logarithmic length bucket, edge characters, hashes,
neighboring-symbol shape. A tiny WebGPU model (32 hidden channels: a summed
feature embedding, a five-part local window, an exact bidirectional affine
scan run as a parallel prefix — a Blelloch-style scan, the operation a GPU is
actually built for — then a shared-weight binary tree merging blocks
bottom-up and propagating context back down) assigns each part one of nine
visual classes: plain, comment, string, number, keyword, type, function,
constant, operator. Adjacent equal labels merge into the spans you get back.

The promoted checkpoint is trained against Shiki's own labels — so "accuracy"
here means agreement with a specific existing highlighter, not some
independent ground truth about programming-language semantics — and reaches
**83.02% per-part agreement, 77.73% styled macro F1**, on a corpus held
disjoint from training at the repository and package level. (An earlier,
higher-sounding 88.02% figure circulated publicly before a taxonomy
relabeling; the model card is explicit that the two numbers use different
label sets and are not comparable — a small, honest correction sitting in
the repo for anyone who reads past the README.) The whole training loop
warm-starts from the last promoted checkpoint and requires a **strict**
overall-accuracy improvement over that checkpoint to auto-promote; a run that
regresses a "protected" language beyond a statistical tolerance gets flagged
even when the aggregate score improves.

<Figure
  src="/articles/tiny-browser-models/gpu-lexer-demo.webp"
  alt="gpu-lexer's own project page: a 27.6KB language-agnostic WebGPU syntax highlighter, with a per-language agreement table running from 95-100% for angular/apex/jsx down below 50% for jinja, liquid, razor, vala and vb, a highlight-latency bar chart, a loaded-library-size comparison against Prism.js/Highlight.js/Shiki/Starry Night, and a live 75-language demo browser."
  caption="gpu-lexer's own benchmark page — the 27.6 KiB bundle beats every comparison library on size by roughly an order of magnitude, and the per-language table is where the honesty is: five languages score below 50% agreement (screenshot, gpu-lexer.vercel.app, 18 Sept 2026)."
/>

That per-language table is the single most useful thing on the page, because
it is a ceiling admitted in public: held-out agreement runs 95–100% for
Angular/JSX-family languages, sits in the 80s–90s for most general-purpose
languages, and drops **below 50%** for `jinja`, `liquid`, `razor`, `vala`,
and `vb` — templating and less-common languages that likely appear rarely in
training. "Language-agnostic" here means one shared architecture and
classifier, not equal accuracy on every language you might paste in; the
model guesses structure from local shape, and shape that is unfamiliar
enough gets it wrong.

Against comparison libraries the size story is not close: 27.6 KiB versus
Prism.js's 162.1 KiB (all 297 languages) or 8.6 KiB (six major web
languages), Highlight.js's 240.4 KiB, Shiki's 991.5 KiB, Starry Night's
1.46 MiB. On the same "top-25 GitHub-language, popularity-weighted"
agreement metric, gpu-lexer's own site reports 86.77% against Shiki's 100%
reference, ahead of Prism.js (84.05%), Sugar High (73.61%), Starry Night
(72.25%), and Highlight.js (70.12%) — one model, one bundle, covering every
language at once, rather than a grammar pack per language.

## The joke that is also a real artifact: neural-flexbox

Aaron Vanston's **neural-flexbox** states its own thesis on the homepage:
"Stop using a 10-trillion-parameter model to centre your divs. Use a
36,354-parameter model instead." The author is explicit that this is
"considerably less useful" than gpu-lexer — CSS flexbox is a deterministic
layout algorithm the browser already computes exactly, for free, so learning
an *approximation* of it has no practical upside. The point is what training
one costs.

The model is a plain MLP — **23 → 128 → 128 → 128 → 2**, ReLU activations,
per-output-channel int8 quantized hidden layers — trained on **152,000
layouts actually measured in Chromium 149**: 1 to 8 boxes in a single flex
row, integer basis/grow/shrink, six justify modes. Predict x-position and
width, in pixels, from 23 numeric features. That is the entire task.

Here is the training script's model definition and training loop, in full —
this is not an excerpt, this is the whole thing that produces the shipped
checkpoint, aside from argument parsing:

```python
# packages/training/src/experiment.py — neural-flexbox
x, y, w = dataset('train'); vx, vy, vw = dataset('validation')
modules = []; last = x.shape[1]
for _ in range(a.depth):
    modules.extend([nn.Linear(last, a.width), nn.ReLU()]); last = a.width
modules.append(nn.Linear(last, 2)); model = nn.Sequential(*modules).to(a.device)
# ... optional weight-resume/expansion here ...
optimizer = torch.optim.AdamW(model.parameters(), lr=a.lr, weight_decay=0)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, a.epochs, eta_min=a.lr * .01)
best = float('inf')
for epoch in range(a.epochs):
    for ids in torch.randperm(len(x), device=a.device).split(a.batch):
        pred = model(x[ids])
        errors = nn.functional.smooth_l1_loss(pred * w[ids, None], y[ids] * w[ids, None], beta=a.beta, reduction='none')
        loss = errors.mean()
        optimizer.zero_grad(); loss.backward(); optimizer.step()
    scheduler.step()
    with torch.no_grad():
        score = ((model(vx) - vy).abs() * vw[:, None]).mean().item()
    if score < best:
        best = score; best_state = copy.deepcopy(model.state_dict())
model.load_state_dict(best_state)
```

The whole file, `packages/training/src/experiment.py`, is **82 lines**
including the argparse setup and the JSON export at the end. The recipe that
produced the shipped `refined-v1` checkpoint chains five of these runs —
seed float, expanded float, balanced float, canonical float, then two
quantization-aware passes — and the final stage's own recorded
`report.json` states **`trainingSeconds: 177.53`** on a CUDA GPU for the
800-epoch run that produced the deployed weights: under three minutes. Not
three minutes per stage across a multi-day pipeline — 177.5 seconds is the
wall-clock time of the specific run whose weights are what ships today.

The result: **0.35px mean coordinate error**, p95 1.08px, on 5,000 held-out
generated layouts in the same 240–1,200px training-width range. The model
card is equally direct about where that breaks: an explicit out-of-distribution
split (1,000 layouts at 1,300–1,800px container widths, outside the training
range) scores **0.946px mean error, 3.51px p95, 76.67px maximum**, and the
fraction of predicted coordinates within 1px of the true answer falls from
94.4% in-distribution to **74.4%** out of it. Nothing crashes; the model just
gets quietly, measurably worse the moment the input leaves the box it was
trained inside — which is exactly the failure mode every one of these six
projects has to answer for, in its own way, below.

<Figure
  src="/articles/tiny-browser-models/neural-flexbox-demo.webp"
  alt="The neural-flexbox playground showing four draggable boxes in a flex row, with a dashed outline for the browser's true CSS layout and a green fill for the model's prediction, per-box pixel error labeled underneath (0.08px, 0.60px, 0.13px, 0.05px), and a live inference time of 2.40ms."
  caption="neural-flexbox's live playground: green boxes are the model's prediction, dashed outlines are Chromium's real layout, and the gap between them is labeled in pixels per box — 2.40ms of live WebGPU inference (screenshot, aaronvanston.github.io/neural-flexbox, 18 Sept 2026)."
/>

## Two ports of one idea: gpu-time and gpu-query

**gpu-time** (Arik Chakma) turns English time expressions into dates, time
ranges, and RFC 5545 recurrence rules — "Sat Sun 1pm-8pm Mon 10pm-12am" with a
reference instant and timezone becomes real ISO occurrences plus an `RRULE`
string. Its model card lists **38,745 parameters**, two bidirectional scan
layers, 580 embedding rows, and **35 role labels** decoded with a 40×40 CRF
transition matrix (Viterbi), so the model scores whole label sequences
rather than committing to each token independently. Weights pack to 22,501
Brotli bytes; the complete published package is 45,561 Brotli bytes, "under
the 50,000-byte release limit" the project holds itself to. On a
never-shown-to-training slice of a public sentence corpus it gets **5,866 of
6,011 real English sentences exactly right (97.6%)**.

It is also candid about exactly how it fails, in a limitations section that
reads like a bug tracker rather than marketing copy: "in four minutes"
(elapsed time) gets read as a future clock time; a friend literally named
Wednesday reads as the weekday; `07/19/27` misreads its trailing two digits
as a day rather than a year; the em dash is unsupported entirely (`13:20—15:50`
returns nothing, while an en dash works on a weaker feature path). The
homepage's own explainer video, separately, still cites an older
**24,761-parameter** figure for the model — a small, visible reminder that
when five people ship variations on one idea inside a week, even a project's
own marketing can lag its own model card by a checkpoint or two.

**gpu-query** (safzanpirani) turns a typed search phrase into a structured
filter against *your* schema, without ever learning your field names — a
29,597-parameter model that reads "anything record listens and performer
containing uncensored please" against a schema of `album`/`artist`/`genre`
fields (aliased as `record`/`performer`/`style`) and compiles it to
`[{ field: "album", cmp: "eq", value: "listens" }, { field: "artist", cmp:
"contains", value: "uncensored" }]`. Its own README does not hedge about
where the architecture came from:

> The architecture is a port of gpu-time's `TimeTagger`. Only the lexicon,
> the label set, the generator and the CPU resolver are new.

The model — 32 hidden channels, the same sum-embed → five-token convolution →
bidirectional gated affine scan → two-layer head shape as gpu-time — reads
your schema as three extra rows of *structural* features per token ("this
token matched a field of this kind," "this token is one position from a
field") with the literal field names dropped at the boundary, so renaming a
column changes the parse's *resolution* but not what the model itself sees.
On schemas drawn from four domains that share no field name, alias, or enum
value with training, it hits **98.88% exact-filter-match accuracy**, up from
98.65% once the training run *removed* a hashed word-identity feature —
evidence, the README argues, that the model was reading structure and the
word hashes were mostly noise.

```python
# spike/model.py — gpu-query's QueryTagger, architecture excerpt
HIDDEN = 32

def affine_scan(gate, candidate):
    """Inclusive scan for state[t] = gate[t] * state[t-1] + candidate[t]."""
    width = gate.shape[1]; stride = 1
    while stride < width:
        next_gate = gate[:, stride:] * gate[:, :-stride]
        next_candidate = candidate[:, stride:] + gate[:, stride:] * candidate[:, :-stride]
        gate = torch.cat((gate[:, :stride], next_gate), dim=1)
        candidate = torch.cat((candidate[:, :stride], next_candidate), dim=1)
        stride *= 2
    return candidate
```

That `affine_scan` — a textbook parallel-prefix (Blelloch) scan, log(n)
sequential steps instead of n — is the specific reason this architecture
maps onto a GPU at all: the recurrence that carries context across a
sequence is associative, so it decomposes into independent per-channel scans
that a WGSL compute kernel can run one workgroup per query, one thread per
hidden channel. Both backends — plain TypeScript and the WGSL kernel — read
the *same* 40 KiB of int6 weights and are checked against each other and
against the PyTorch reference to within 1e-3. A short single query stays on
the CPU by design, because dispatch and readback cost more than the parse
itself; WebGPU only pays for itself in batch:

| Batch | CPU | WebGPU | Speedup |
|---|---|---|---|
| 32 | 7.0 ms | 2.6 ms | 2.7× |
| 256 | 40.6 ms | 3.7 ms | 11.0× |
| 1,024 | 208.5 ms | 15.4 ms | 13.5× |
| 4,096 | 1,084.6 ms | 67.1 ms | 16.2× |

(Measured on an Apple M4, 1,024 queries per row above scaled to the stated
batch size.) The whole feasibility spike — never published to npm, by its
own description — trains in **under two minutes on an M-series Mac** for 40
epochs over 60,000 generated samples per epoch; the README notes plainly
that "these models do not need a GPU; Python data generation dominates the
loop," which is a genuinely different economic statement than anything
about a frontier model's training run.

<Figure
  src="/articles/tiny-browser-models/gpu-query-demo.webp"
  alt="The gpu-query demo page: an editable schema on the left (album, artist, genre, plays, released, explicit, with aliases shown), a query box on the right parsing 'anything record listens and performer containing uncensored please' into colored role tags (record/FIELD, listens/VALUE, and/AND, performer/FIELD, containing/OP_CONTAINS, uncensored/VALUE) and a compiled JSON filter below."
  caption="gpu-query live: the schema on the left never reaches the model as field names, only as structural rows — renaming every column would change the compiled filter's targets but not the parse itself (screenshot, gpu-query.safzan.dev, 18 Sept 2026)."
/>

<Figure
  src="/articles/tiny-browser-models/gpu-time-demo.webp"
  alt="The gpu-time homepage demo: a text box reading 'book dinner for October 2 at eight pm' with 'October 2' and 'at eight pm' highlighted, resolving to 'Fri, Oct 2, 2026, 8:00 PM', plus a grid of tappable examples (a reminder, dinner mid-sentence, standup, overnight shift, a trip, payday, a two-week cycle, a timer) and a code sample showing the npm package's parse() call."
  caption="gpu-time's live demo, mid-parse: 'book dinner for October 2 at eight pm' resolves against a stated reference instant and timezone to an exact ISO date and time (screenshot, gpu-time.arikko.dev, 18 Sept 2026)."
/>

## Testing the two I could push the hardest: gpu-cron and tinySarf

gpu-cron and tinySarf are the two projects in this cluster with the least
public accountability — gpu-cron because there is no repository at all to
read, tinySarf because it explicitly ships without an independent human-gold
evaluation. So instead of reading a model card, I drove both live demos
headlessly with Playwright (Chromium, WebGPU enabled) and typed at them.

gpu-cron converts English scheduling phrases into cron expressions. Loaded
cold, "Every weekday at 9:30am" resolves via WebGPU in 130ms into `30 9 * *
1-5`, with a correct human-readable readout and five real upcoming
occurrences in the selected timezone. The footer states its own numbers
plainly: **35,783 parameters · WebGPU · 6-bit weights**, and the three-stage
pipeline it advertises — "recognize" (the model), "resolve" (calendar
arithmetic), "be exact" (a compiler that refuses to guess) — is the same
shape every other project in this piece uses.

<Figure
  src="/articles/tiny-browser-models/gpu-cron-demo.webp"
  alt="gpu-cron's demo, parsing 'Every weekday at 9:30am' into the cron expression 30 9 * * 1-5, with 'What the network predicted' showing the token-level role tags (Every/recurrence, weekday/weekday, at/prefix, 9/hour, 30/minute, am/meridiem) at 100.0% model score, and five upcoming occurrences listed in Europe/Berlin time."
  caption="gpu-cron correctly parsing a routine weekday schedule at 100.0% self-reported model confidence, resolving to real upcoming occurrences (screenshot, gpu-cron.vercel.app, 18 Sept 2026)."
/>

Then I tried to break it. Feeding it a phrase whose grammar it recognizes but
whose *composition* plain cron cannot express — "The last Friday of every
month at 5pm" — the model tags every single token correctly, at a
self-reported **100.0% model score** ("last" as a prefix, "Friday" tagged as
an excluded weekday, "every"/"month" as recurrence, "5"/"pm" as
hour/meridiem) — and the compiler still refuses, with a specific, legible
diagnostic: *"A monthly ordinal combined with a weekday requires an
unsupported nth-weekday rule."* Same story for "Every 90 minutes" — perfectly
tagged, rejected because a plain five-field cron minute slot cannot express a
90-minute interval. Both are correct behavior: the *model* did its job; the
five-field cron format itself is the ceiling. That is a meaningfully
different failure than the model being wrong, and the UI is honest about the
distinction — model confidence and compiler success are two separate,
visible numbers, not one blended "success" indicator.

The one that is not fine: I typed `asdkfjasdf random gibberish text` — no
schedule content whatsoever. The model tagged it as a real, structured
recurrence at **95.8% model score**: `asdkfjasdf` as a weekday, `random` as
an excluded weekday, `gibberish` as a weekday, `text` as a recurrence marker,
predicted family "biweekly." It confidently hallucinated grammatical
structure onto noise. The compiler still rejected the output — but only
because cron happens to require a numeric field this particular gibberish
never produced ("The model did not identify a required numeric value"), not
because anything detected that the input was nonsense. Feed it four garbage
words that happen to include a number, and nothing in this pipeline would
catch it. The model's own confidence score is not a reliable
out-of-distribution detector; the only thing standing between this input and
a fabricated cron job was a downstream syntax requirement it happened to
violate.

<Figure
  src="/articles/tiny-browser-models/gpu-cron-gibberish.webp"
  alt="gpu-cron given the input 'asdkfjasdf random gibberish text': the model predicts a biweekly recurrence at 95.8% model score, tagging asdkfjasdf and gibberish as weekdays, random as an excluded weekday, and text as a recurrence marker — the compiler still rejects it, citing a missing required numeric value, not because it detected nonsense."
  caption="Pure gibberish, tagged as a 95.8%-confident biweekly schedule. Only a downstream syntax requirement — cron needs a number somewhere — happened to save this one (screenshot, gpu-cron.vercel.app, tested live, 18 Sept 2026)."
/>

**tinySarf** (Ahmed Abdel-Aal) analyzes Modern Standard Arabic word
morphology — segmenting a word into morphemes, recovering its triliteral
root, and tagging part of speech and grammatical features. Its own
homepage is unusually forthcoming about its status: **"Experimental,
unpromoted. Full-analysis and POS quality remain below release targets;
independent human gold is absent."** The model — 245,063 reachable int8
weights, a 220K-parameter multi-task CNN for segmentation and grammar plus a
separate 25K-parameter root CNN — is trained on just **5,470 words / 75,135
teacher-generated analyses**, entirely on a **CPU** (an Apple M2, per the
model card's own recorded environment; this one never touches a GPU during
training at all), and evaluated against labels generated by CAMELMORPH, an
existing rule-based Arabic morphological analyzer — not against independent
human annotation.

Segmentation exact match lands at **87.02%** and root exact match at
**78.24%** against that teacher. But the number that matters most for what
this tool is actually good for is buried two rows down: **full-analysis
top-1 accuracy is 12.52%** (17.18% at top-3). Individual fields — root, POS,
each grammatical feature — mostly look solid in isolation; getting every one
of them right *simultaneously*, which is what "analyze this word correctly"
actually requires, is a much harder bar, and errors across eight-plus output
slots compound fast. This is the clearest example in the whole cluster of
why a single headline accuracy number can flatter a system that is
genuinely useful for some of its outputs and not yet reliable for the
compound task.

I fed it a real inflected word first — `وبكتابهم` ("and with their book") —
and it correctly segmented all four morphemes (و conjunction, ب preposition,
كتاب stem, هم pronominal enclitic), recovered the root كتب, and tagged it
correctly as a masculine singular genitive-construct noun, in 285ms on the
CPU reference backend. Then I gave it a modern loanword with no Arabic
triliteral root at all: `كومبيوتر` ("computer"). It correctly declined to
force a fake root — Root and Pattern both come back explicitly **"Unknown,"**
and the whole word is tagged as a single unanalyzable stem, in 19.8ms. That
is the right behavior for exactly the reason gpu-cron's gibberish case was
the wrong one: the model recognized the limits of what it could honestly
claim, rather than manufacturing structure to fill the answer shape.

<Figure
  src="/articles/tiny-browser-models/tinysarf-demo.webp"
  alt="TinySarf's analysis of the Arabic word 'وبكتابهم', showing four colored morpheme tiles (هم pronominal enclitic, كتاب stem, ب preposition, و conjunction), root كتب, part of speech noun, and grammatical features gender masculine, number singular, case genitive, state construct, computed in 285.4ms on the CPU reference backend."
  caption="TinySarf correctly decomposing a real inflected word into its four morphemes and recovering its root, with alternative rankings shown rather than one silent answer (screenshot, tinySarf demo, 18 Sept 2026)."
/>

<Figure
  src="/articles/tiny-browser-models/tinysarf-loanword.webp"
  alt="TinySarf given the modern loanword 'كومبيوتر' (computer): Root and Pattern both read 'Unknown', and the whole word is tagged as a single unanalyzable stem rather than being forced into a fabricated triliteral root."
  caption="A modern loanword with no Arabic root, and the model says so — Root and Pattern both come back honestly 'Unknown' instead of a confident, fabricated answer (screenshot, tested live, 18 Sept 2026)."
/>

## The size, on one axis

<SizeCapabilityScatter />

None of these six numbers is directly comparable to another benchmark — a
syntax highlighter and an Arabic morphology analyzer are not competing on
the same task, so the Y axis above is two honestly-labeled series, not one
leaderboard. What the shared, logarithmic X axis earns is the only claim
this piece actually needs: **29,597 to 245,063 parameters**, **27.6 KiB to
234 KiB** shipped, for every model in this cluster, against **1.24 billion
to 405 billion parameters** for five ordinary open-weight instruction models
running the same well-known MMLU benchmark. That is five to seven orders of
magnitude on parameters, and the size gap is even more dramatic measured in
bytes actually sent to a client: an estimated 2.3 to 755 GiB of bf16 weights
for the general models, against literally hundreds of kilobytes, total, for
all six of these combined.

## The economics

<InferenceCostModel />

The chart above is not a crossover chart, on purpose — there is no point
where the lines meet, because the two costs are not the same *shape* of
cost. An API call to Claude Haiku 4.5 (Anthropic's cheapest current model,
published at \$1 / \$5 per million input/output tokens) costs something on
every single request: a typical 50-token-in, 30-token-out "parse this
phrase" call is \$0.0002, and that number is exactly as true on call one as
on call one million. A CDN-delivered browser model costs a fraction of a
cent to *ship* — gpu-lexer's real 28,305 bytes, at a representative \$0.10/GB
CDN egress rate, is about \$0.0000028 — and that cost is paid **once per
visitor**, not once per call. Every inference after the page loads runs on
that visitor's own GPU, for exactly \$0, with no server in the loop at all.
At the very first call the browser model is already roughly 76 times
cheaper; by a few hundred calls in one session it is tens of thousands of
times cheaper, and the ratio keeps climbing because one side scales with
usage and the other structurally does not.

That gap is the whole economic argument, but it is not the only one. Every
demo in this piece reported its own inference latency, unprompted, because
these teams treat milliseconds as a headline number: 2.40ms for
neural-flexbox, 19.8ms for tinySarf's WebGPU path (285.4ms on its CPU
reference), 130ms for gpu-cron's first WebGPU call, single-digit-to-low-double-digit
milliseconds for gpu-query and gpu-time at realistic batch sizes. A round
trip to an API endpoint — network, queue, generation, network again — rarely
clears that bar even under good conditions, and never clears it when the
network itself is the bottleneck (a flaky connection, a background tab, a
country with a slow route to the nearest datacenter). None of these six
models has a rate limit, a per-minute quota, a cold-start queue, or a
privacy surface where the input phrase leaves the device — because none of
them send the input anywhere.

## What shape of task this actually fits

Every one of these six problems shares four properties, and the shape is
worth naming because it recurs constantly in ordinary software and rarely
gets this treatment: **narrow** (one well-defined transformation, not
general reasoning), **high-volume** (called on every keystroke, every paste,
every page load — gpu-query explicitly targets a live search bar), **latency-
sensitive** (a syntax highlighter or a search-as-you-type parser that takes
200ms feels broken even if it is "fast" by API standards), and — the
property that makes a tiny model *sufficient* rather than merely cheap — a
**constrained output space**. gpu-lexer emits one of nine span classes.
gpu-time and gpu-cron emit roles from a closed set (35, in gpu-time's case)
decoded into a fixed calendar/cron grammar. gpu-query emits one of twelve
token roles compiled into a small filter algebra. tinySarf emits morpheme
spans, a root, and features from closed inventories, one already-known
per Arabic morphology.

That is exactly the same shape of problem this site covers from the decoding
side in
[Parallel constrained decoding](/articles/parallel-constrained-decoding):
when the space of valid outputs is small and known ahead of time, you do not
need a model that can produce *any* token sequence and then constrain it
after the fact — you can build the constraint into the architecture (or the
decoding process) itself and spend orders of magnitude less compute getting
there. This cluster and that one are two attacks on the same underlying
observation from opposite ends: parallel constrained decoding keeps a big
general model but restricts *what it is allowed to say*; these six projects
throw the big model away entirely and train something the size of a
favicon that could never say anything else in the first place. Both routes
exist because most decisions software actually needs to make — is this a
keyword, what date is "next Tuesday," which field does "genre" alias — do
not need a frontier model's generality. They need a fast, cheap, correct
answer to one small question, over and over.

## Where it breaks

None of this is a case for replacing general models with tiny ones broadly —
it is a case for recognizing which slice of problems never needed the big
model at all, and the honest edges of that slice showed up in every single
project tested here:

- **Out-of-distribution input degrades quietly, not loudly.** gpu-lexer's
  own agreement table drops below 50% on `jinja`, `liquid`, `razor`, `vala`,
  and `vb` — real, common-enough languages, just underrepresented in
  training. neural-flexbox's explicit out-of-distribution split shows mean
  error nearly tripling and the fraction of coordinates within 1px falling
  from 94.4% to 74.4% the moment container widths move outside the
  240–1,200px training range. Nothing throws an error in either case; the
  answer is simply worse, silently.
- **A confident model is not the same as a correct out-of-distribution
  detector.** gpu-cron tagged four gibberish words as a 95.8%-confident
  biweekly schedule. The self-reported score told me nothing useful about
  whether the input made sense; only a downstream syntax constraint
  (cron needs a number somewhere) happened to catch it this time.
- **Some ceilings belong to the output format, not the model.** gpu-cron
  parsed "the last Friday of every month" and "every 90 minutes" perfectly
  and still failed, because five-field cron cannot express either schedule.
  That is worth distinguishing from a model mistake, and the project's own
  UI does distinguish it — model score and compiler success are shown as two
  separate numbers.
- **A compound task's exact-match rate can crater even when every field
  looks fine alone.** tinySarf's segmentation (87.02%) and root (78.24%)
  numbers look production-ready in isolation; full-analysis top-1 accuracy —
  every field correct at once — is 12.52%. Eight-plus output slots compound
  errors fast.
- **The evaluation itself can be the weak link.** tinySarf's numbers are
  agreement with an existing rule-based analyzer's labels, not independent
  human-reviewed ground truth, and the project says so, prominently, on its
  own homepage. gpu-time's real-English figure (97.6%) is a genuinely
  held-out test; gpu-query's transfer figure (98.88%) is measured on schemas
  disjoint from training. Reading which claim rests on which kind of
  evidence is most of the actual diligence here.
- **Even WebGPU is not the default path.** gpu-query, gpu-time, and tinySarf
  all keep a single short query on the **CPU** by design, because dispatch
  and readback latency exceeds the parse time itself at that scale — WebGPU
  only earns its keep once inputs batch up. tinySarf's own browser benchmark
  measured WebGPU running *slower* than a plain JavaScript reference
  (0.89×) for a single word, and only ahead (2.34×) at a batch of 128. The
  "tab" in "browser-tab model" is doing real work, but for most of these,
  most of the time, that work is happening on the CPU.

## One weekend, five forks

The lineage here is explicit and self-reported, not inferred: neural-flexbox's
README credits gpu-lexer by name as its direct inspiration; gpu-query's
README states its architecture "is a port of gpu-time's `TimeTagger`," with
gpu-lexer and gpu-time both named under "prior art." The five public
repositories' most recent commits — 16 September (gpu-lexer and gpu-time), 12
September (gpu-query), 11 September (neural-flexbox), 14 September (tinySarf)
— sit inside a single week, with gpu-cron's live demo (no repo, no commit
history to check) part of the same cluster by every account describing it.
One person's small, well-documented idea — small model, one narrow task, real
model card, real benchmark page — turned into a template that four or five
other people could pick up, port, and ship inside days, because the hardest
part (proving a WebGPU-inference pipeline this small actually works in a
real browser) had already been done once, in public, with the training code
attached. That the fifth fork was a joke about centering a div, and that the
joke came with a real 82-line training script and an honest
out-of-distribution evaluation split, is the most telling detail in the
whole cluster: the format had gotten cheap and legible enough, in one week,
that even the parody was rigorous.

---

*Sources: [gpu-lexer](https://github.com/vercel-labs/gpu-lexer) ·
[gpu-time](https://github.com/arikchakma/gpu-time) ·
[gpu-query](https://github.com/safzanpirani/gpu-query) ·
[neural-flexbox](https://github.com/aaronvanston/neural-flexbox) ·
[tinySarf](https://github.com/AhmedAbdel-Aal/tinysarf) · gpu-cron demo at
[gpu-cron.vercel.app](https://gpu-cron.vercel.app) (no public repository
found). Model cards and architecture docs read directly from each repository;
gpu-cron's figures and failure cases are from live testing, 18 September
2026. MMLU figures for the general-model comparison are each model's own
published card (Llama 3.2 1B/3B, Llama 3 8B, Llama 3.1 405B — Meta; Phi-3-mini —
Microsoft). Claude Haiku 4.5 pricing is Anthropic's published API rate.*
