Introducing torchwright — Out of Distribution
Posts · Torchwright<br>Introducing torchwright<br>July 23, 2026
For a while now I’ve been chasing a question: what sort of algorithms is a<br>transformer even capable of expressing? I got here by wondering about arithmetic<br>— the first generation of LLMs were really bad at it, and I wanted to understand<br>whether that is a limitation of training or a limitation of the architecture<br>itself. After all, transformers are Turing complete — at least in proofs that<br>idealize the arithmetic and let the model generate as many tokens as it needs<br>along the way. Even so, naively one might expect elementary school arithmetic<br>to be within reach.
I decided to try a specific opinionated approach: instead of asking what a transformer could learn, I would<br>calculate the exact weights necessary for a transformer to implement an<br>algorithm explicitly. If the weights exist, the architecture can express the<br>algorithm, and the question of training never enters into it.
What I found when I pointed the finished tool at arithmetic is the next post.<br>This post is about the tool I created to get there — and the tool’s output,<br>concretely, looks like this:
from transformers import pipeline
generate = pipeline("text-generation", model="binary_increment_hf_bundle")<br>print(generate("1011\n", return_full_text=False)[0]["generated_text"])<br># 1100<br>That is an ordinary Phi-3 checkpoint incrementing a binary number, loaded by<br>vanilla huggingface with no custom code. Every weight in it was computed. Zero<br>training. The rest of this post is how.
The plan
Literally constructing the weight matrices by hand would not be feasible, so I<br>came up with a plan. If I could define primitive operations that can be easily<br>expressed in transformer weights, then maybe I could compose these primitive<br>operations in increasingly complicated ways. I would then build a compiler,<br>which would create an empty transformer and decide how to allocate the residual<br>stream to all of the intermediate values, and thus where in the various weight<br>matrices each operation belongs. If I could get that foundation in place, I<br>could compose the primitives into more and more sophisticated operations: a<br>library that would make it straightforward to express a real algorithm in a<br>transformer.
The idea of hand-built transformer weights was not new.<br>RASP defines a language whose primitives<br>map onto transformer sublayers, and Tracr<br>compiles RASP programs into actual weights. Reusing Tracr was not appealing to<br>me for a few reasons. I wanted to express any computational graph, in ordinary<br>Python, and when I read the RASP paper I didn’t find the language particularly<br>intuitive. I’m a firm believer in the principle articulated by Feynman, “What I<br>cannot create, I do not understand.” And to be honest, building it sounded fun.
That compiler became torchwright.<br>You define a computation graph in ordinary Python; torchwright produces the<br>weights of a transformer that executes it. There is no training anywhere in the<br>pipeline.
One decision mattered more than any other for making the problem tractable: I<br>did not start with a transformer anyone would recognize from a model card. I<br>started with the simplest substrate I could get away with — FFNs with ReLU,<br>the easiest nonlinearity to derive constructions in (also the Attention is All<br>You Need nonlinearity); a position encoding<br>of my own design (the same mechanism as a learned position embedding, just with<br>values I chose instead of trained); and no normalization layers at all. A decoder-only transformer in<br>shape, but with every part chosen for my convenience. Pushing it to a standard<br>architecture came later.
Where values live
The first thing the compiler has to manage is memory. The residual stream is the<br>only shared memory there is. Think of it as a whiteboard with numbered columns:<br>every value the graph computes owns a set of columns for as long as anything<br>downstream still needs it. The columns need not be contiguous. The compiler<br>scatters weight matrices to wherever the inputs happen to sit.
A transformer layer comprises two sublayers: attention, followed by a FFN. Each<br>sublayer has a skip connection that bypasses it, meaning that the sublayer can<br>only add to the residual stream. It computes out = in + f(in), and the skip<br>connection rearranges nothing. That one fact drives the whole compilation<br>scheme. A new value lands in zeroed columns (0+new=new).(0 + \mathit{new} = \mathit{new}).(0+new=new). Once a value is no<br>longer needed by subsequent layers, its associated columns are freed by writing<br>its negation (v+(−v)=0).(v + (-v) = 0).(v+(−v)=0). When a computational graph calls for two values to<br>be added, we can use the skip connection itself to represent the add.
Simple obvious operations first
With the plan in mind, I tackled simple obvious operations first,<br>and built up to increasing complexity.
Add came first — adding two values is trivially a linear operation. In<br>fact the substrate is embarrassingly...