TONER-1: a language model that runs inside a laser printer
A 260,032-parameter language model packed into one 595 KB PostScript file. Send it to a laser printer, and the printer’s own processor writes and typesets a children’s story about printers. The computer that sent it does none of the math.
Today we’re introducing TONER-1, a language model that runs entirely on-device, as long as that device is a laser printer.
TONER-1 ships as a single 595 KB print job. Inside are 260,032 parameters, a 512-token vocabulary, a forward pass and the model’s own page layout. Send the job to a PostScript printer, and the printer’s processor writes a children’s story, one token at a time, typesets it and prints it. The computer that sent the job does zero FLOPs of inference. It sends bytes, then waits.
TONER-1 needs no GPU, no cloud and no API key. It does not accept prompts: it chooses the plot (which usually involves a printer). It is open-weight in the most literal sense available, because every weight is two hexadecimal digits in a plain-text file. A printer that doesn’t speak PostScript may print the file as text, weights and all.
We’re also introducing TONER-1-V, which adds vision, in the output direction. When the story is done, the same print job reads it, picks out the first thing it knows how to draw, and hands that to a second model, 103,536 parameters, which draws it: 16 × 16 pixels, one bit each, in color.
And for the copy room that needs more, there’s TONER-1 Pro: our own 12.2-million-parameter model, in a 24.9 MB print job. It has never been sent to a printer.
Inference cost includes toner. Outputs are fused to paper and can no longer execute. The kill switch is the power button.
We’re proudly pushing forward the PostScript Pareto Frontier.
Okay, what is this actually?
TONER-1 is a joke with working parts. The model started as Andrej Karpathy’s stories260K, a tiny Llama-2-style model from his llama2.c project, trained on TinyStories, a dataset of simple children’s stories. I fine-tuned it on 8,757 short stories about printers (more on those below), exported its weights to 8-bit integers, and wrote the forward pass, the sampler and the page layout in PostScript, the language most laser printers speak. Everything after the upload happens inside the printer’s PostScript interpreter.
Status: Printed on an HP Color LaserJet Pro MFP M281fdw on 2026-11-05: 200 tokens in 3:37 by the printer's own clock (0.92 tok/s), 3:39 from send to finished job.
A language model that ran on an actual printer: one print job, zero host FLOPs.
What the printer itself sent back over the network while the job ran: its own output and its own timer. These are the first and last lines; the story in between is left out here.
TONER-1 | 260032 params | int8 | PostScript-native inference 200 tokens in 217.235 s = 0.920662 tok/s [STEPS limit]
The TONER-1-V job printed the same day, doodle included: the story in 3:38 and the doodle in 25 s by the printer's own clock, 4:04 from send to finished job.
Full logs, with every byte the printer sent back and when it arrived: docs/printer-run-2026-11-05 in the GitHub repo.
A print job is a program
PostScript looks like a file format, but it’s a complete programming language, stack-based like Forth. Send 3 4 add and the interpreter leaves 7 on its stack. It has variables, procedures, loops, strings, arrays, dictionaries and floating-point math. A print job is a program, and the printed page is a side effect of the drawing operators it calls.
To run those programs, a PostScript printer carries its own processor and memory. It always has: the 1985 Apple LaserWriter had a 12 MHz Motorola 68000, faster than the 7.8 MHz 68000 in the Macintosh it plugged into. A modern office laser has far more, and spends most of it waiting for spreadsheets.
The model, as text
stories260K has 5 layers, a width of 64, 8 query heads sharing 4 key/value heads, a SwiGLU feed-forward block, RoPE and RMSNorm. The embedding table doubles as the output layer, so every token touches all 260,032 weights.
The export script stores each weight matrix a row at a time. For each row it takes the largest absolute weight m and sets the row’s scale to m / 127. It rounds every weight to an integer from −127 to 127, adds 128, and writes the result as one byte of a PostScript hex string. A row ends up as <96917e84…> next to its scale, two characters per weight. That’s most of the 595,372-byte file: about 578 KB of weights, norms and vocabulary, and 17.8 KB of program.
The forward pass, in PostScript
The forward pass is about 100 lines. Nearly all the arithmetic, about 259,000 multiply-adds per token, runs through one eight-line procedure, mv, matrix times vector:
% o x M mv ; M = [scales rows], rows are int8 strings offset by 128
/mv { /mM exch def /mx exch def /mo exch def
/mS mM 0 get def /mQ mM 1 get def /mn mx length 1 sub def
/msx 0 mx { add } forall 128 mul def
0 1 mQ length 1 sub { /mi exch def /mrow mQ mi get def
0 0 1 mn { dup mrow exch get exch mx exch get mul add } for
msx sub mS mi get mul mo mi 3 -1 roll put } for
} bind def
get on a string returns a byte as an integer. So instead of subtracting 128 from every weight, mv takes the dot product with the raw bytes, subtracts 128 times the sum of the input once, and multiplies by the row’s scale.
The rest is a transformer, translated, with a PostScript accent. sin and cos take degrees, so RoPE multiplies its angles by 57.29577951. exp raises one number to the power of another, so ex is spelled 2.718281828 x exp. The KV cache is an array of arrays that grows by one position per token.
Sampling, typesetting and the 0 FLOPs
The last layer produces 512 logits. The sampler turns them into a softmax at temperature 0.8, draws a number with PostScript’s built-in rand, and walks the running sum until it passes that number. The chosen token becomes the next input. After 200 tokens, or an end-of-story token, the same job decodes the pieces, word-wraps the story in Times-Roman (shrinking from 13 to 8 points until it fits), draws a run card with the token count, speed, seed and the interpreter’s own name, and calls showpage. Until then, the printer was just a computer.
That’s the whole trick. The computer opens a connection to port 9100 and sends 595 KB of plain text. It never touches a weight. Even the timings on the run card come from the interpreter’s own clock.
Teaching it about printers
Out of the box, stories260K writes about whatever TinyStories is about, which is rarely office equipment. In 300 sampled stories, 3.7% used a printing-related word at all, and none said “printer.”
There’s no dataset of children’s stories about printers, so I made one the TinyStories way: ask a language model for very simple stories, each with a theme (68 of them, from “a paper jam” to “a printer that prints a bedtime story”), a few required words and a story feature such as a twist or a moral. The writer was Xing4.0-29B-A4B, an Apache-2.0 model from China Telecom’s TeleAI, on OpenRouter’s free tier, so the stories cost $0. Scripts threw out anything too short, too long, too fancy or not about printers enough. That left 9,218 stories: 8,757 to train on and 461 to test with. They’re on Hugging Face as TONER-1-printer-stories.
The fine-tune took about 11 minutes on the M4: 3,000 steps, mixing the printer stories with ordinary TinyStories so the model wouldn’t forget how to write. Afterwards, 87.3% of its stories use a printing-related word and 71.7% say “printer.” The architecture, the size and the engine are the same, and the job grew by 195 bytes. An AI rater, scoring blind, found the new stories a little less coherent (not a significant difference) and a little more fun.
TONER-1-V: doodles
TONER-1-V is a second, smaller model, trained from scratch for this: 103,536 parameters in the same architecture, at width 48, 4 layers and 6 query heads sharing 3 key/value heads. It draws 16 × 16 one-bit doodles in 18 classes, from cat and frog to soccer ball and calculator. It learned them from 171,000 drawings in Google’s Quick, Draw! dataset.
A doodle is 64 tokens. The image is cut into an 8 × 8 grid of 2 × 2 patches. A patch has four pixels, so 16 possible values, and each value is a token. Generation starts from a start token and a class token, then samples 64 patch tokens, considering only the 16 patch values. That’s 65 forward passes through the same PostScript forward pass as the story model: the doodle engine copies it byte for byte and runs it inside its own dictionary, so each model finds its own weights.
Choosing what to draw is not machine learning. A keyword router scans the story for the first word that names a class (a puppy counts as a dog, a bunny as a rabbit, home as a house). If the story names nothing drawable, it picks a class from the seed and says so in the caption. The 64 tokens decode into a bitmap, which the job scales up 8× so every pixel is a solid block, then draws with imagemask, PostScript’s operator for one-bit stencils, in the class’s color. The page calls this “proprietary toner super-resolution.”
The combined job is 859,530 bytes and 363,568 parameters. A doodle takes about 0.3 seconds in Ghostscript and took 25 seconds on the HP. TONER-1-V’s PostScript logits match a float64 reference to within 4.2 × 10−5, which is float32 rounding.
Results, with caveats
Speed. In Ghostscript 10.07.1 on an M4 Mac, TONER-1 writes about 80 tokens per second, so a page of story takes a few seconds. Before printing anything, I estimated 0.3 to 5 tokens per second on a real printer, or roughly 1 to 10 minutes a page. On an HP Color LaserJet Pro MFP M281fdw it wrote 200 tokens in 3:37 by the printer’s own clock, 0.92 tokens per second, and took 3:39 from send to finished job. The TONER-1-V job took 4:04 from send to finished job; by the printer’s clock, the story took 3:38 and the doodle 25 seconds. For scale, EXO Labs ran the same stories260K on a Windows 98 Pentium II at 39.31 tokens per second. In Ghostscript, TONER-1 is faster than that. On the printer, it is not.
Quality. The stories are what 260K parameters buy: TinyStories-shaped, mostly about Lily, Tim and the printer, occasionally surreal. At the default seed, 42, the story begins “Once upon a time, there was a little girl named Lily. She loved to bring paper to a avocado.” Nothing is edited.
Doodles. I gave an AI model the 36 doodles of a blind grid, unlabeled, and it named 24 correctly. Chance would get 2. It got 17 of 20 objects and 7 of 16 animals; dogs, rabbits and frogs went 0 for 2 each. That’s an AI judge, not a human study, and 36 doodles is a small sample.
Seeds. PostScript’s rand is implementation-defined, so a printer may tell a different story from the same seed. The HP didn’t: at the default seed, 42, it sent back the same story as Ghostscript, byte for byte apart from line endings. That story names nothing drawable, so TONER-1-V draws a frog anyway, the same frog on both.
Printers. The jobs use only PostScript Level 1 operators plus rectstroke, so they need a Level 2 or later interpreter. Printers that speak only PCL or GDI may print the file as text. Long jobs can also hit a printer’s job timeout or memory limit.
TONER-1 Pro. There’s a bigger tier, and this one is ours: 12,194,688 parameters and our own 4,096-token tokenizer, trained from scratch on TinyStories (2.4 hours on the M4), then fine-tuned on the same printer stories. No part of it comes from Llama 2, and all of it is MIT-licensed. It’s a 24.9 MB print job. Ghostscript manages about 2.8 tokens per second, about 70 seconds a page. On a printer it would take, by estimate, 20 minutes to 5 hours a page, if it fits in the printer’s memory at all; scaled from TONER-1’s measured speed on the HP, about 1.7 hours. It has never been run on one. The same blind rater liked its stories much better than TONER-1’s (coherence 3.6 against 1.7 out of 5), on only 10 stories.
The benchmarks (parody)
The model card comes with benchmarks. They are parody, not evaluations. Frontier models from major labs were not run on these benchmarks and are not named:
| Benchmark (parody) | TONER-1 | Frontier models from major labs |
|---|---|---|
| TPS (Tokens Per Sheet), higher is better | 200 | N/A |
| HelloPaperJam: copy-room commonsense, 4-way multiple choice | 27.9% | N/A |
| PC LOAD LETTER: outputs printed on the paper size the job asked for | 100.0% | N/A |
| Jam Rate: invented objects per 100 tokens, lower is better | 1.5 | N/A |
| TonerBench: dollars of toner per million output tokens | $125 | N/A |
N/A means only that a model cannot execute inside a print job. On that axis, every TONER model scores 100% and everything else scores 0%, which is how you get a frontier.
Try it yourself
All three print jobs are on GitHub, and the models are on Hugging Face as TONER-1 and TONER-1-V, with TONER-1 Pro and the printer stories alongside. You don’t need a printer to try them, since Ghostscript is a PostScript interpreter too:
# Preview on your own computer: each command writes a one-page PDF
gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dSEED=7 -dTEMP=0.9 \
-o story.pdf TONER-1_print-job.ps
gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dSEED=7 \
-o page.pdf TONER-1-V_print-job.ps
gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dSEED=7 \
-o pro.pdf TONER-1-Pro_print-job.ps # about 70 seconds
To print, send the file raw to a PostScript printer you own or have permission to use:
nc <printer-ip> 9100 < TONER-1_print-job.ps
# or through a CUPS queue
lpr -P <queue> -o raw TONER-1_print-job.ps
A printer runs the default seed. To change it, edit the 42 in /SEED 42 def. On the HP, TONER-1 took 3:39 from send to finished job. Be patient, and be kind to the queue behind you.
Prior art
TONER-1 stands on some wonderfully odd shoulders:
- EXO Labs’ llama98.c ran stories260K on a Windows 98 Pentium II at 39.31 tokens per second.
- Nicolas Seriot’s PSChess plays chess in PostScript, on real printers.
- int-llm-postscript (August 2026) runs a 14K-parameter microGPT in PostScript under Ghostscript.
- llama.ttf puts a language model inside a font file.
Credits
- Base model, training code and inference reference: Andrej Karpathy’s llama2.c and tinyllamas, MIT License. TONER-1 is a fine-tune of his stories260K; TONER-1 Pro was trained from scratch with llama2.c’s code.
- TinyStories: Ronen Eldan and Yuanzhi Li, 2023 (arXiv:2305.07759), CDLA-Sharing-1.0.
- The printer stories: written by Xing4.0-29B-A4B (China Telecom’s TeleAI, Apache-2.0) through OpenRouter’s free tier, and released under Apache-2.0.
- Quick, Draw! dataset: Google, CC BY 4.0.
TONER-1 is a working print job and a parody of frontier-model launches. Not affiliated with or endorsed by Adobe, HP, Artifex, Google, or any AI lab.