Implementing GPT-2 in CSS Developer Shubham Patil implemented a working GPT-2 inference engine in pure CSS, with the ~124M-parameter model generating roughly one token every 15 seconds at gpt2css.shubhampatil.dev. The project uses CSS calc(), pow(), sqrt(), exp() and max() for the transformer's arithmetic and non-linearities, plus variable font files for matrix multiplication, with no JavaScript or WebAssembly used at inference time. Patil found that untyped CSS variables are unrolled as nested expressions rather than stored as numbers, so only 265 of the stylesheet's 4,509 variables received values until each was registered with the @property at-rule. Implementing GPT-2 Entirely In CSS A journey through the Wild West of web trickery. TL;DR: I got GPT-2 running a webpage, using only CSS and interestingly, a bunch of variable font files . The model contains ~124M parameters and inferences at around 15 s/tok on a good day . You can try it out here https://gpt2css.shubhampatil.dev Why? This adventure started as usual, when I saw a tweet on my timeline: CMake is an almost -programming-language. Taking this category and stretching it to the farthest end: what's the most absurd "programming" language? CSS. The only real constraint I imposed on myself here was that I couldn't use any JavaScript/WASM for inference, or any other language for that matter I did use Python to write our stylesheets ahead of time, which isn't cheating since none of it runs when you open the page . Below is a rough overview of how we're able to take what you type and calculate the words that come after it, without a turing-complete language in sight: Getting to this point took hundreds of experiments, and hours of trial-and-error, but I'd like to walk you through how we produce a singular token, with the various tricks involved with CSS math and variable fonts for matrix multiplication. Math in CSS The first question I had was, can CSS even perform the arithmetic and non-linearities needed for a neural network, much less a transformer? Turns out that modern CSS gets quite close to turing-completeness, and has some really interesting bells and whistles. I was already roughly familiar with calc , which lets us add, subtract, multiply and divide numbers in our stylesheet, and CSS also lets us define variables and reference them like so: :root { --a: 0.5; --b: 2; } .neuron { --out: calc var --a var --b ; } / 1 / That calc is already a multiplication that the browser works out for us, which means that we could, at least in theory, write down any weighted sum in a neural network as one long calc expression. To test this, I built a tiny transformer with 7 characters in its vocabulary and random weights, since all I cared about at this point was whether it could spit out a token at all. CSS doesn't have any primitives for arrays, so I had to write each weight straight into our stylesheet ok, I had a Python script write them . A single number in a query vector looked something like this: --generation-0-q-2-0: calc 0 / bias / + var --generation-0-ln1-2-value-0 -0.01020212 + var --generation-0-ln1-2-value-1 -0.03555801 / … through value-11 / ; The rest of this tiny model was fairly trivial. For LayerNorm we can find the mean with calc and let pow and sqrt cover the rest, and for the softmax in attention we can subtract the largest score with max before raising each one with exp . After I implemented our softmax, though, the page showed all 7 characters after each step, as if our model hadn't predicted a particular next token at all. When I went through our variables one by one, I found that only the first 265 of the 4,509 in our stylesheet ever got a value. Huh, odd. Variables in CSS Intuitively, you'd think that our variables are storing values in memory and retrieving them when necessary. WRONG When you write var --variable-name , the browser's style engine is actually copying over whatever expression that variable corresponds to, and if that expression has variables nested inside it, it recursively unrolls those as well: Never once is a number registered in this process, isn't that peculiar? After a while, you get a huge chain of operations, just far too much for our page to handle. Thankfully, CSS has a way around this, which is registering each variable with the @property at-rule. Once a variable has a declared type, the browser works it out once and keeps the actual number: @property --variable-name { syntax: "