{"id":23766,"date":"2026-09-06T04:18:46","date_gmt":"2026-09-06T04:18:46","guid":{"rendered":"https:\/\/scannn.com\/running-llms-in-the-browser-with-three-js\/"},"modified":"2026-09-06T04:18:46","modified_gmt":"2026-09-06T04:18:46","slug":"running-llms-in-the-browser-with-three-js","status":"publish","type":"post","link":"https:\/\/scannn.com\/lv\/running-llms-in-the-browser-with-three-js\/","title":{"rendered":"Running LLMs in the Browser with Three.js"},"content":{"rendered":"\n<div>\n<h2 class=\"deck-markdown-heading group\" id=\"tldr\">TL;DR<a aria-label=\"Link to section: TL;DR\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#tldr\">#<\/a><\/h2>\n<p>Three-LLM runs GPT-2, SmolLM2, Qwen, and Phi models locally in the browser by<br \/>\nturning their inference graphs into Three.js TSL compute shaders on WebGPU. Try<br \/>\nthe demo at <a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/three-llm.ben3d.ca\" node=\"[object Object]\">https:\/\/three-llm.ben3d.ca<\/a>.<\/p>\n<p>I started this project to test two things: how far I could push the general<br \/>\ncompute capabilities of Three.js and WebGPU, and what today&#8217;s small language<br \/>\nmodels can do when they run in a browser. The models range from a 3-million<br \/>\nparameter story generator to a modern 800-million parameter chat model.<\/p>\n<p>Three.js is known as a rendering library, but its WebGPU renderer also exposes<br \/>\nstorage buffers, compute dispatches, workgroup memory, atomics, and GPU<br \/>\nreadback. A language model puts all of those capabilities to work. It needs<br \/>\nhundreds of ordered compute dispatches, large matrix-vector products,<br \/>\nreductions, persistent attention or recurrent state, and a tight loop between<br \/>\nthe GPU and JavaScript as each new token appears.<\/p>\n<p>I built <a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/github.com\/bhouston\/three-llm\" node=\"[object Object]\">Three-LLM<\/a> as both a test of<br \/>\nthat compute stack and a usable inference library. It loads ordinary Hugging<br \/>\nFace configurations, tokenizers, and SafeTensors checkpoints in the browser,<br \/>\nbuilds the model from reusable<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/threejs.org\/docs\/TSL.html\" node=\"[object Object]\">Three.js Shading Language<\/a> (TSL) compute<br \/>\nkernels, and runs inference on the user&#8217;s GPU through WebGPU. It does not need<br \/>\na server-side inference runtime or model-specific WebAssembly binary.<\/p>\n<p>The package includes CPU reference implementations for validation and GPU<br \/>\nimplementations for actual use. The library currently recognizes GPT-2,<br \/>\nLlama-style, Gemma 3, Phi, and Qwen3.5 architectures. I also made a<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/three-llm.ben3d.ca\" node=\"[object Object]\">quick chat application<\/a> so you can try five<br \/>\ncheckpoints without writing an application first:<\/p>\n<ul>\n<li>TinyStories GPT-2 3M<\/li>\n<li>GPT-2 124M<\/li>\n<li>SmolLM2 135M<\/li>\n<li>Qwen3.5 0.8B<\/li>\n<li>Phi-1.5 1.3B<\/li>\n<\/ul>\n<p>I made the demo to test the library without first writing an application.<br \/>\nModel files range from 15 MB to 2.8 GB before Three-LLM expands their weights<br \/>\nto 32-bit floats. On a phone, stick with TinyStories or SmolLM2. Qwen and Phi<br \/>\nneed enough memory that many mobile devices will fail to load them.<\/p>\n<p>Using the library itself starts with a regular Three.js <code class=\"p-0\">WebGPURenderer<\/code>:<\/p>\n<pre><pre class=\"deck-syntax-block\" style=\"color:black;background:#f5f2f0;text-shadow:0 1px white;font-family:Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:0.75rem 1rem;margin:0;overflow:auto;border:none;border-radius:0;box-shadow:none\"><code class=\"language-typescript\" style=\"white-space:pre;color:black;background:none;text-shadow:0 1px white;font-family:Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;font-size:1em;text-align:left;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none\"><span class=\"token\" style=\"color:#07a\">import<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">{<\/span><span> createTSLRunner <\/span><span class=\"token\" style=\"color:#999\">}<\/span><span> <\/span><span class=\"token\" style=\"color:#07a\">from<\/span><span> <\/span><span class=\"token\" style=\"color:#690\">'three-llm'<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span><span\/><span class=\"token\" style=\"color:#07a\">import<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">{<\/span><span> WebGPURenderer <\/span><span class=\"token\" style=\"color:#999\">}<\/span><span> <\/span><span class=\"token\" style=\"color:#07a\">from<\/span><span> <\/span><span class=\"token\" style=\"color:#690\">'three\/webgpu'<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span>\n<span\/><span class=\"token\" style=\"color:#07a\">const<\/span><span> renderer <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">=<\/span><span> <\/span><span class=\"token\" style=\"color:#07a\">new<\/span><span> <\/span><span class=\"token\" style=\"color:#DD4A68\">WebGPURenderer<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span><span\/><span class=\"token\" style=\"color:#07a\">await<\/span><span> renderer<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">init<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span>\n<span\/><span class=\"token\" style=\"color:#07a\">const<\/span><span> runner <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">=<\/span><span> <\/span><span class=\"token\" style=\"color:#07a\">await<\/span><span> <\/span><span class=\"token\" style=\"color:#DD4A68\">createTSLRunner<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>\n<\/span><span>  <\/span><span class=\"token\" style=\"color:#690\">'https:\/\/huggingface.co\/HuggingFaceTB\/SmolLM2-135M\/resolve\/main\/'<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span\/><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span>\n<span\/><span class=\"token\" style=\"color:#07a\">const<\/span><span> result <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">=<\/span><span> <\/span><span class=\"token\" style=\"color:#07a\">await<\/span><span> runner<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">generate<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>renderer<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span> <\/span><span class=\"token\" style=\"color:#690\">'Once upon a time,'<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">{<\/span><span>\n<\/span><span>  maxNewTokens<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> <\/span><span class=\"token\" style=\"color:#905\">64<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span>  temperature<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> <\/span><span class=\"token\" style=\"color:#905\">0.7<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span>  topK<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> <\/span><span class=\"token\" style=\"color:#905\">10<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span>  <\/span><span class=\"token function-variable\" style=\"color:#DD4A68\">onToken<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>text<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span> <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">=&gt;<\/span><span> <\/span><span class=\"token\" style=\"color:#690\">console<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">log<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>text<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span\/><span class=\"token\" style=\"color:#999\">}<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span><\/code><\/pre>\n<h2 class=\"deck-markdown-heading group\" id=\"before-the-transformer\">Before the transformer<a aria-label=\"Link to section: Before the transformer\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#before-the-transformer\">#<\/a><\/h2>\n<p>I define model, weight, activation, MLP, and forward pass in<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/ben3d.ca\/blog\/neural-network-basics\" node=\"[object Object]\">Neural network basics, for graphics people<\/a>.<br \/>\nThe inference path here does not require the training details.<\/p>\n<p>The central operation remains the one from that primer:<\/p>\n<p><span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><mi>y<\/mi><mo>=<\/mo><mi>W<\/mi><mi>x<\/mi><mo>+<\/mo><mi>b<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">y = Wx + b<\/annotation><\/semantics><\/math><\/span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.625em;vertical-align:-0.1944em\"\/><span class=\"mord mathnormal\" style=\"margin-right:0.0359em\">y<\/span><span class=\"mspace\" style=\"margin-right:0.2778em\"\/><span class=\"mrel\">=<\/span><span class=\"mspace\" style=\"margin-right:0.2778em\"\/><\/span><span class=\"base\"><span class=\"strut\" style=\"height:0.7667em;vertical-align:-0.0833em\"\/><span class=\"mord mathnormal\" style=\"margin-right:0.1389em\">W<\/span><span class=\"mord mathnormal\">x<\/span><span class=\"mspace\" style=\"margin-right:0.2222em\"\/><span class=\"mbin\">+<\/span><span class=\"mspace\" style=\"margin-right:0.2222em\"\/><\/span><span class=\"base\"><span class=\"strut\" style=\"height:0.6944em\"\/><span class=\"mord mathnormal\">b<\/span><\/span><\/span><\/span><\/span><\/p>\n<p>An LLM owns many matrices containing learned weights. Inference repeatedly<br \/>\nmultiplies vectors by those matrices, applies a few nonlinear functions, and<br \/>\nmoves information between token positions. A transformer gives those<br \/>\noperations a particular structure.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"what-happens-when-an-llm-generates-one-token\">What happens when an LLM generates one token<a aria-label=\"Link to section: What happens when an LLM generates one token\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#what-happens-when-an-llm-generates-one-token\">#<\/a><\/h2>\n<p>The full loop has six parts:<\/p>\n<ol>\n<li>A tokenizer converts text into a sequence of integer token IDs.<\/li>\n<li>An embedding table turns each ID into a vector.<\/li>\n<li>A stack of transformer blocks updates that vector using the current token<br \/>\nand the preceding context.<\/li>\n<li>A final matrix projection produces one score, called a <strong>logit<\/strong>, for every<br \/>\ntoken in the vocabulary.<\/li>\n<li>A sampling rule chooses one token from those scores.<\/li>\n<li>The model appends that token and runs again.<\/li>\n<\/ol>\n<p>The model generates a sentence one token at a time. It predicts a token, adds<br \/>\nit to the context, and repeats until it selects a stop token or reaches a<br \/>\nlimit.<\/p>\n<p>Processing the supplied prompt is called <strong>prefill<\/strong>. Generating subsequent<br \/>\ntokens one at a time is called <strong>decode<\/strong>. These two phases run the same model<br \/>\nbut have different performance characteristics. Prefill has many known input<br \/>\ntokens that a specialized implementation can process together. Decode has<br \/>\nonly one new token per step and must repeatedly read most of the model&#8217;s<br \/>\nweights.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"gpt-2-one-block-at-a-time\">GPT-2, one block at a time<a aria-label=\"Link to section: GPT-2, one block at a time\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#gpt-2-one-block-at-a-time\">#<\/a><\/h2>\n<p><a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/cdn.openai.com\/better-language-models\/language_models_are_unsupervised_multitask_learners.pdf\" node=\"[object Object]\">GPT-2<\/a><br \/>\nis a useful starting point because its architecture contains the pieces that<br \/>\nstill define most decoder language models without many later variations. It<br \/>\nbuilds on the decoder half of the transformer introduced in<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/1706.03762\" node=\"[object Object]\">Attention Is All You Need<\/a>.<\/p>\n<p>GPT-2 first applies byte-level Byte Pair Encoding to the input. BPE repeatedly<br \/>\ncombines frequent adjacent symbols, giving the model a fixed vocabulary that<br \/>\ncan still represent any input text. The idea began as a<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/www.derczynski.com\/papers\/archive\/BPE_Gage.pdf\" node=\"[object Object]\">data-compression algorithm<\/a><br \/>\nand was later adapted to<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/aclanthology.org\/P16-1162\/\" node=\"[object Object]\">subword tokenization for neural models<\/a>.<\/p>\n<p>Each resulting token ID selects a row from a learned token-embedding table.<br \/>\nGPT-2 adds a second learned embedding for the token&#8217;s position in the<br \/>\nsequence. The sum is the first vector sent through the transformer.<\/p>\n<p>Every GPT-2 block then runs this sequence:<\/p>\n<ol>\n<li><a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/1607.06450\" node=\"[object Object]\">Layer normalization<\/a><\/li>\n<li>Causal multi-head self-attention<\/li>\n<li>A residual addition<\/li>\n<li>A second layer normalization<\/li>\n<li>A two-layer MLP using the<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/1606.08415\" node=\"[object Object]\">GELU activation<\/a><\/li>\n<li>Another residual addition<\/li>\n<\/ol>\n<p><a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/1512.03385\" node=\"[object Object]\">Residual connections<\/a> add each block&#8217;s<br \/>\nresult back to its input. They let later blocks refine a representation<br \/>\nwithout requiring every block to rebuild it.<\/p>\n<p>After the last block, GPT-2 applies one more normalization and projects the<br \/>\nresult to the vocabulary. If the vocabulary contains 50,257 tokens, that<br \/>\nprojection returns 50,257 logits. The sampler turns those scores into the next<br \/>\ntoken ID.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"attention-is-a-lookup-built-from-the-current-context\">Attention is a lookup built from the current context<a aria-label=\"Link to section: Attention is a lookup built from the current context\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#attention-is-a-lookup-built-from-the-current-context\">#<\/a><\/h2>\n<p>Self-attention lets the current token retrieve information from earlier<br \/>\ntokens. Each block projects its input into three vectors for every attention<br \/>\nhead:<\/p>\n<ul>\n<li>A <strong>query<\/strong> describes what the current position is looking for.<\/li>\n<li>A <strong>key<\/strong> describes what each position offers.<\/li>\n<li>A <strong>value<\/strong> contains the information retrieved from that position.<\/li>\n<\/ul>\n<p>The query takes a dot product with each preceding key. Dividing by the square<br \/>\nroot of the head dimension keeps the scale stable, and softmax converts the<br \/>\nscores into weights:<\/p>\n<p><span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><mi mathvariant=\"normal\">Attention<\/mi><mo>\u2061<\/mo><mo stretchy=\"false\">(<\/mo><mi>Q<\/mi><mo separator=\"true\">,<\/mo><mi>K<\/mi><mo separator=\"true\">,<\/mo><mi>V<\/mi><mo stretchy=\"false\">)<\/mo><mo>=<\/mo><mi mathvariant=\"normal\">softmax<\/mi><mo>\u2061<\/mo><mrow><mo fence=\"true\">(<\/mo><mfrac><mrow><mi>Q<\/mi><msup><mi>K<\/mi><mi>T<\/mi><\/msup><\/mrow><msqrt><mi>d<\/mi><\/msqrt><\/mfrac><mo fence=\"true\">)<\/mo><\/mrow><mi>V<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\operatorname{Attention}(Q,K,V)\n=\n\\operatorname{softmax}\\left(\\frac{QK^T}{\\sqrt{d}}\\right)V<\/annotation><\/semantics><\/math><\/span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:1em;vertical-align:-0.25em\"\/><span class=\"mop\"><span class=\"mord mathrm\">Attention<\/span><\/span><span class=\"mopen\">(<\/span><span class=\"mord mathnormal\">Q<\/span><span class=\"mpunct\">,<\/span><span class=\"mspace\" style=\"margin-right:0.1667em\"\/><span class=\"mord mathnormal\" style=\"margin-right:0.0715em\">K<\/span><span class=\"mpunct\">,<\/span><span class=\"mspace\" style=\"margin-right:0.1667em\"\/><span class=\"mord mathnormal\" style=\"margin-right:0.2222em\">V<\/span><span class=\"mclose\">)<\/span><span class=\"mspace\" style=\"margin-right:0.2778em\"\/><span class=\"mrel\">=<\/span><span class=\"mspace\" style=\"margin-right:0.2778em\"\/><\/span><span class=\"base\"><span class=\"strut\" style=\"height:2.4684em;vertical-align:-0.95em\"\/><span class=\"mop\"><span class=\"mord mathrm\">softmax<\/span><\/span><span class=\"mspace\" style=\"margin-right:0.1667em\"\/><span class=\"minner\"><span class=\"mopen delimcenter\" style=\"top:0em\"><span class=\"delimsizing size3\">(<\/span><\/span><span class=\"mord\"><span class=\"mopen nulldelimiter\"\/><span class=\"mfrac\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:1.5183em\"><span style=\"top:-2.1778em\"><span class=\"pstrut\" style=\"height:3em\"\/><span class=\"mord\"><span class=\"mord sqrt\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.9322em\"><span class=\"svg-align\" style=\"top:-3em\"><span class=\"pstrut\" style=\"height:3em\"\/><span class=\"mord\" style=\"padding-left:0.833em\"><span class=\"mord mathnormal\">d<\/span><\/span><\/span><span style=\"top:-2.8922em\"><span class=\"pstrut\" style=\"height:3em\"\/><span class=\"hide-tail\" style=\"min-width:0.853em;height:1.08em\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"400em\" height=\"1.08em\" viewbox=\"0 0 400000 1080\" preserveaspectratio=\"xMinYMin slice\"><path d=\"M95,702&#10;c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14&#10;c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54&#10;c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10&#10;s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429&#10;c69,-144,104.5,-217.7,106.5,-221&#10;l0 -0&#10;c5.3,-9.3,12,-14,20,-14&#10;H400000v40H845.2724&#10;s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7&#10;c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z&#10;M834 80h400000v40h-400000z\"\/><\/svg><\/span><\/span><\/span><span class=\"vlist-s\">\u200b<\/span><\/span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.1078em\"><span\/><\/span><\/span><\/span><\/span><\/span><\/span><span style=\"top:-3.23em\"><span class=\"pstrut\" style=\"height:3em\"\/><span class=\"frac-line\" style=\"border-bottom-width:0.04em\"\/><\/span><span style=\"top:-3.677em\"><span class=\"pstrut\" style=\"height:3em\"\/><span class=\"mord\"><span class=\"mord mathnormal\">Q<\/span><span class=\"mord\"><span class=\"mord mathnormal\" style=\"margin-right:0.0715em\">K<\/span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8413em\"><span style=\"top:-3.063em;margin-right:0.05em\"><span class=\"pstrut\" style=\"height:2.7em\"\/><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mathnormal mtight\" style=\"margin-right:0.1389em\">T<\/span><\/span><\/span><\/span><\/span><\/span><\/span><\/span><\/span><\/span><\/span><span class=\"vlist-s\">\u200b<\/span><\/span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.93em\"><span\/><\/span><\/span><\/span><\/span><span class=\"mclose nulldelimiter\"\/><\/span><span class=\"mclose delimcenter\" style=\"top:0em\"><span class=\"delimsizing size3\">)<\/span><\/span><\/span><span class=\"mspace\" style=\"margin-right:0.1667em\"\/><span class=\"mord mathnormal\" style=\"margin-right:0.2222em\">V<\/span><\/span><\/span><\/span><\/span><\/p>\n<p>A causal mask excludes future positions. During generation there are no<br \/>\nfuture tokens yet, but the same rule lets prompt tokens attend only to text<br \/>\nthat precedes them.<\/p>\n<p>Recomputing every earlier key and value for every generated token would waste<br \/>\nmost of the work. A <strong>KV cache<\/strong> stores them after their first computation.<br \/>\nEach decode step writes one new key and value, then compares the new query<br \/>\nagainst the cached keys. The cache grows with context length and becomes an<br \/>\nimportant memory and bandwidth cost. The paper<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/1911.02150\" node=\"[object Object]\">Fast Transformer Decoding: One Write-Head is All You Need<\/a><br \/>\ndescribes this incremental-inference bottleneck and introduces one of the<br \/>\nfirst widely used ways to reduce it.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"expressing-the-model-as-tsl-compute\">Expressing the model as TSL compute<a aria-label=\"Link to section: Expressing the model as TSL compute\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#expressing-the-model-as-tsl-compute\">#<\/a><\/h2>\n<p>Three-LLM represents each model operation as a small TSL compute kernel. A<br \/>\nrunner connects those kernels in the order required by the model recipe. The<br \/>\nmain building blocks include:<\/p>\n<ul>\n<li><code class=\"p-0\">TSLLinear<\/code> for matrix-vector multiplication<\/li>\n<li><code class=\"p-0\">TSLNormalize<\/code> and <code class=\"p-0\">TSLRMSNorm<\/code> for normalization<\/li>\n<li><code class=\"p-0\">TSLAttention<\/code> for full causal attention<\/li>\n<li><code class=\"p-0\">TSLMLP<\/code> and <code class=\"p-0\">TSLGatedMLP<\/code> for feed-forward blocks<\/li>\n<li><code class=\"p-0\">TSLAdd<\/code> for residual connections<\/li>\n<li><code class=\"p-0\">TSLLogitSampler<\/code> for reducing vocabulary logits on the GPU<\/li>\n<\/ul>\n<p>The linear layer shows the basic pattern. One GPU invocation owns one output<br \/>\nelement and loops across the input vector:<\/p>\n<pre><pre class=\"deck-syntax-block\" style=\"color:black;background:#f5f2f0;text-shadow:0 1px white;font-family:Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:0.75rem 1rem;margin:0;overflow:auto;border:none;border-radius:0;box-shadow:none\"><code class=\"language-typescript\" style=\"white-space:pre;color:black;background:none;text-shadow:0 1px white;font-family:Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;font-size:1em;text-align:left;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none\"><span class=\"token\" style=\"color:#07a\">return<\/span><span> <\/span><span class=\"token\" style=\"color:#DD4A68\">Fn<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span> <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">=&gt;<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">{<\/span><span>\n<\/span><span>  <\/span><span class=\"token\" style=\"color:#07a\">const<\/span><span> outputIndex <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">=<\/span><span> instanceIndex<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">toVar<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#690\">'outputIndex'<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span>\n<span>  <\/span><span class=\"token\" style=\"color:#DD4A68\">If<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>outputIndex<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">lessThan<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#DD4A68\">uint<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>outputSize<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span> <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">=&gt;<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">{<\/span><span>\n<\/span><span>    <\/span><span class=\"token\" style=\"color:#07a\">const<\/span><span> sum <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">=<\/span><span> biasNode<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">element<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>outputIndex<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">toVar<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#690\">'sum'<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span>\n<span>    <\/span><span class=\"token\" style=\"color:#DD4A68\">Loop<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>\n<\/span><span>      <\/span><span class=\"token\" style=\"color:#999\">{<\/span><span> start<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> <\/span><span class=\"token\" style=\"color:#DD4A68\">uint<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#905\">0<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span> end<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> <\/span><span class=\"token\" style=\"color:#DD4A68\">uint<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>inputSize<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span> type<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> <\/span><span class=\"token\" style=\"color:#690\">'uint'<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span> condition<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> <\/span><span class=\"token\" style=\"color:#690\">'&lt;'<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">}<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span>      <\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#999\">{<\/span><span> i <\/span><span class=\"token\" style=\"color:#999\">}<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span> <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">=&gt;<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">{<\/span><span>\n<\/span><span>        <\/span><span class=\"token\" style=\"color:#07a\">const<\/span><span> weightIndex <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">=<\/span><span> i<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">mul<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#DD4A68\">uint<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>outputSize<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">add<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>outputIndex<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span><span>        sum<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">addAssign<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>inputNode<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">element<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>i<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">mul<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>weightNode<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">element<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>weightIndex<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span><span>      <\/span><span class=\"token\" style=\"color:#999\">}<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span>    <\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span>\n<span>    outputNode<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">element<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>outputIndex<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">assign<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>sum<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span><span>  <\/span><span class=\"token\" style=\"color:#999\">}<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span><span\/><span class=\"token\" style=\"color:#999\">}<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span class=\"token\" style=\"color:#DD4A68\">compute<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>outputSize<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">[<\/span><span>workgroupSize<\/span><span class=\"token\" style=\"color:#999\">]<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span><\/code><\/pre>\n<p>I kept this kernel plain so you can see the operation it performs. Three-LLM<br \/>\nconstructs readable operators in the browser, and TSL generates WGSL for the<br \/>\nWebGPU backend. It does this work at runtime instead of asking a model<br \/>\ncompiler to select tiled matrix kernels ahead of time.<\/p>\n<p>For GPT-2, the general decoder runner connects those operators as:<\/p>\n<pre><pre class=\"deck-syntax-block\" style=\"color:black;background:#f5f2f0;text-shadow:0 1px white;font-family:Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:0.75rem 1rem;margin:0;overflow:auto;border:none;border-radius:0;box-shadow:none\"><code class=\"language-text\" style=\"white-space:pre;color:black;background:none;text-shadow:0 1px white;font-family:Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;font-size:1em;text-align:left;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none\"><span>embedding\n<\/span>  \u2192 LayerNorm\n<!-- -->  \u2192 packed QKV projection\n<!-- -->  \u2192 attention\n<!-- -->  \u2192 output projection\n<!-- -->  \u2192 residual add\n<!-- -->  \u2192 LayerNorm\n<!-- -->  \u2192 GELU MLP\n<!-- -->  \u2192 residual add\n<!-- -->  \u2192 final LayerNorm\n<!-- -->  \u2192 vocabulary projection\n<\/code><\/pre>\n<p>The GPU attention implementation uses four compute stages for each layer:<\/p>\n<ol>\n<li>Write the new key and value into the cache.<\/li>\n<li>Prepare the query.<\/li>\n<li>Compute one scaled query-key score per head and cached token.<\/li>\n<li>Run stable softmax and mix the cached values.<\/li>\n<\/ol>\n<p>The CPU runner performs the same operations in TypeScript. Browser checkpoint<br \/>\ntests compare greedy GPU output with the CPU reference for all five catalog<br \/>\nmodels. That reference path has been essential because a shader can run<br \/>\nwithout errors while still producing subtly wrong text.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"loading-hugging-face-checkpoints-at-runtime\">Loading Hugging Face checkpoints at runtime<a aria-label=\"Link to section: Loading Hugging Face checkpoints at runtime\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#loading-hugging-face-checkpoints-at-runtime\">#<\/a><\/h2>\n<p>Three-LLM reads a Hugging Face <code class=\"p-0\">config.json<\/code>, tokenizer files, and<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/github.com\/huggingface\/safetensors\/blob\/main\/README.md#format\" node=\"[object Object]\">SafeTensors<\/a><br \/>\nweights.<\/p>\n<p>The configuration selects a recipe. The recipe describes the graph family,<br \/>\nnormalization, MLP type, residual layout, position encoding, head counts, and<br \/>\ncheckpoint layout. Tensor-name aliases then map names such as GPT-2&#8217;s<br \/>\n<code class=\"p-0\">transformer.h.0.attn.c_attn.weight<\/code> or Llama&#8217;s<br \/>\n<code class=\"p-0\">model.layers.0.self_attn.q_proj.weight<\/code> into the common fields expected by<br \/>\nthe runners.<\/p>\n<p>The extra loader code lets the browser consume the checkpoint published by<br \/>\nthe model author. Three-LLM builds it without an offline conversion step or a<br \/>\nseparately compiled runtime for each model.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"from-gpt-2-to-smollm2\">From GPT-2 to SmolLM2<a aria-label=\"Link to section: From GPT-2 to SmolLM2\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#from-gpt-2-to-smollm2\">#<\/a><\/h2>\n<p><a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/huggingface.co\/HuggingFaceTB\/SmolLM2-135M\" node=\"[object Object]\">SmolLM2 135M<\/a> is close to<br \/>\nGPT-2 in parameter count, but it uses the newer Llama-style decoder pattern.<br \/>\nThe <a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/2502.02737\" node=\"[object Object]\">SmolLM2 report<\/a> covers the model family<br \/>\nand its training.<\/p>\n<p>Four architectural changes matter to the inference implementation:<\/p>\n<ul>\n<li><a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/1910.07467\" node=\"[object Object]\">RMSNorm<\/a> uses the root mean square<br \/>\nwithout subtracting the mean.<\/li>\n<li><a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/2104.09864\" node=\"[object Object]\">Rotary Position Embeddings<\/a> rotate query<br \/>\nand key components according to position instead of adding a learned<br \/>\nposition vector.<\/li>\n<li><a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/2305.13245\" node=\"[object Object]\">Grouped-Query Attention<\/a> lets groups of<br \/>\nquery heads share fewer key and value heads, shrinking the KV cache.<\/li>\n<li><a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/2002.05202\" node=\"[object Object]\">SwiGLU<\/a> replaces GPT-2&#8217;s dense GELU MLP<br \/>\nwith a gated MLP.<\/li>\n<\/ul>\n<p>The gated MLP computes two projections from the same input. One passes through<br \/>\nSiLU and gates the other before a final down projection:<\/p>\n<p><span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><mi mathvariant=\"normal\">SwiGLU<\/mi><mo>\u2061<\/mo><mo stretchy=\"false\">(<\/mo><mi>x<\/mi><mo stretchy=\"false\">)<\/mo><mo>=<\/mo><msub><mi>W<\/mi><mrow><mi mathvariant=\"normal\">d<\/mi><mi mathvariant=\"normal\">o<\/mi><mi mathvariant=\"normal\">w<\/mi><mi mathvariant=\"normal\">n<\/mi><\/mrow><\/msub><mrow><mo fence=\"true\">(<\/mo><mi mathvariant=\"normal\">SiLU<\/mi><mo>\u2061<\/mo><mo stretchy=\"false\">(<\/mo><msub><mi>W<\/mi><mrow><mi mathvariant=\"normal\">g<\/mi><mi mathvariant=\"normal\">a<\/mi><mi mathvariant=\"normal\">t<\/mi><mi mathvariant=\"normal\">e<\/mi><\/mrow><\/msub><mi>x<\/mi><mo stretchy=\"false\">)<\/mo><mo>\u2299<\/mo><msub><mi>W<\/mi><mrow><mi mathvariant=\"normal\">u<\/mi><mi mathvariant=\"normal\">p<\/mi><\/mrow><\/msub><mi>x<\/mi><mo fence=\"true\">)<\/mo><\/mrow><\/mrow><annotation encoding=\"application\/x-tex\">\\operatorname{SwiGLU}(x)\n=\nW_{\\mathrm{down}}\n\\left(\n\\operatorname{SiLU}(W_{\\mathrm{gate}}x)\n\\odot\nW_{\\mathrm{up}}x\n\\right)<\/annotation><\/semantics><\/math><\/span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:1em;vertical-align:-0.25em\"\/><span class=\"mop\"><span class=\"mord mathrm\">SwiGLU<\/span><\/span><span class=\"mopen\">(<\/span><span class=\"mord mathnormal\">x<\/span><span class=\"mclose\">)<\/span><span class=\"mspace\" style=\"margin-right:0.2778em\"\/><span class=\"mrel\">=<\/span><span class=\"mspace\" style=\"margin-right:0.2778em\"\/><\/span><span class=\"base\"><span class=\"strut\" style=\"height:1.0361em;vertical-align:-0.2861em\"\/><span class=\"mord\"><span class=\"mord mathnormal\" style=\"margin-right:0.1389em\">W<\/span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.3361em\"><span style=\"top:-2.55em;margin-left:-0.1389em;margin-right:0.05em\"><span class=\"pstrut\" style=\"height:2.7em\"\/><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mtight\"><span class=\"mord mathrm mtight\">down<\/span><\/span><\/span><\/span><\/span><\/span><span class=\"vlist-s\">\u200b<\/span><\/span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em\"><span\/><\/span><\/span><\/span><\/span><\/span><span class=\"mspace\" style=\"margin-right:0.1667em\"\/><span class=\"minner\"><span class=\"mopen delimcenter\" style=\"top:0em\">(<\/span><span class=\"mop\"><span class=\"mord mathrm\">SiLU<\/span><\/span><span class=\"mopen\">(<\/span><span class=\"mord\"><span class=\"mord mathnormal\" style=\"margin-right:0.1389em\">W<\/span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.2806em\"><span style=\"top:-2.55em;margin-left:-0.1389em;margin-right:0.05em\"><span class=\"pstrut\" style=\"height:2.7em\"\/><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mtight\"><span class=\"mord mathrm mtight\">gate<\/span><\/span><\/span><\/span><\/span><\/span><span class=\"vlist-s\">\u200b<\/span><\/span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.2861em\"><span\/><\/span><\/span><\/span><\/span><\/span><span class=\"mord mathnormal\">x<\/span><span class=\"mclose\">)<\/span><span class=\"mspace\" style=\"margin-right:0.2222em\"\/><span class=\"mbin\">\u2299<\/span><span class=\"mspace\" style=\"margin-right:0.2222em\"\/><span class=\"mord\"><span class=\"mord mathnormal\" style=\"margin-right:0.1389em\">W<\/span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.1514em\"><span style=\"top:-2.55em;margin-left:-0.1389em;margin-right:0.05em\"><span class=\"pstrut\" style=\"height:2.7em\"\/><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mtight\"><span class=\"mord mathrm mtight\">up<\/span><\/span><\/span><\/span><\/span><\/span><span class=\"vlist-s\">\u200b<\/span><\/span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.2861em\"><span\/><\/span><\/span><\/span><\/span><\/span><span class=\"mord mathnormal\">x<\/span><span class=\"mclose delimcenter\" style=\"top:0em\">)<\/span><\/span><\/span><\/span><\/span><\/span><\/p>\n<p>Three-LLM handles these changes with recipe flags and reusable kernels rather<br \/>\nthan a separate SmolLM2 runner.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"phi-changes-the-block-layout\">Phi changes the block layout<a aria-label=\"Link to section: Phi changes the block layout\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#phi-changes-the-block-layout\">#<\/a><\/h2>\n<p><a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/huggingface.co\/microsoft\/phi-1_5\" node=\"[object Object]\">Phi-1.5<\/a> is a 1.3-billion-parameter<br \/>\nmodel introduced in<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/2309.05463\" node=\"[object Object]\">Textbooks Are All You Need II<\/a>. Its<br \/>\ninference graph still uses LayerNorm and a dense GELU MLP, but it differs from<br \/>\nGPT-2 in two important ways.<\/p>\n<p>First, Phi uses partial RoPE. It rotates part of each query and key while<br \/>\nleaving the remaining dimensions unchanged.<\/p>\n<p>Second, its attention and MLP branches run in parallel from the same<br \/>\nnormalized input:<\/p>\n<pre><pre class=\"deck-syntax-block\" style=\"color:black;background:#f5f2f0;text-shadow:0 1px white;font-family:Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:0.75rem 1rem;margin:0;overflow:auto;border:none;border-radius:0;box-shadow:none\"><code class=\"language-text\" style=\"white-space:pre;color:black;background:none;text-shadow:0 1px white;font-family:Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;font-size:1em;text-align:left;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none\"><span>normalized = LayerNorm(x)\n<\/span>attentionOutput = Attention(normalized)\n<!-- -->mlpOutput = MLP(normalized)\n<!-- -->output = x + attentionOutput + mlpOutput\n<\/code><\/pre>\n<p>GPT-2 instead finishes its attention branch and residual addition before<br \/>\nnormalizing again for the MLP. The parameterized decoder runner supports both<br \/>\nlayouts. When the recipe says <code class=\"p-0\">residual: 'parallel'<\/code>, it constructs Phi&#8217;s<br \/>\nbranching graph.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"qwen35-needs-a-different-kind-of-memory\">Qwen3.5 needs a different kind of memory<a aria-label=\"Link to section: Qwen3.5 needs a different kind of memory\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#qwen35-needs-a-different-kind-of-memory\">#<\/a><\/h2>\n<p><a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/huggingface.co\/Qwen\/Qwen3.5-0.8B\" node=\"[object Object]\">Qwen3.5 0.8B<\/a> is the most complex<br \/>\nmodel in the demo. Three-LLM runs its text backbone and skips the checkpoint&#8217;s<br \/>\nvision tensors. Qwen&#8217;s<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/qwen.ai\/blog?id=qwen3.5\" node=\"[object Object]\">official introduction<\/a> and the<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/github.com\/huggingface\/transformers\/blob\/main\/docs\/source\/en\/model_doc\/qwen3_5.md\" node=\"[object Object]\">Transformers architecture documentation<\/a><br \/>\ndescribe a hybrid stack: three Gated DeltaNet layers for each gated<br \/>\nfull-attention layer.<\/p>\n<p>Full-attention layers retain a KV cache and can retrieve a specific earlier<br \/>\ntoken. Gated DeltaNet layers replace that growing history with fixed-size<br \/>\nrecurrent state. Their memory use stays fixed as the model processes more<br \/>\ntokens.<\/p>\n<p><a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/2412.06464\" node=\"[object Object]\">Gated Delta Networks<\/a> combine two ideas. A<br \/>\nlearned decay gate controls how much old state survives, while a delta update<br \/>\nchanges the association for the current key toward its new value. Earlier<br \/>\nwork describes this as a<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/proceedings.mlr.press\/v139\/schlag21a.html\" node=\"[object Object]\">fast-weight programming rule<\/a>.<br \/>\nThe relationship between linear attention and recurrent networks is developed<br \/>\nin<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/2006.16236\" node=\"[object Object]\">Transformers are RNNs<\/a>.<\/p>\n<p>For one decode step, Three-LLM&#8217;s <code class=\"p-0\">TSLGatedDeltaNet<\/code> performs:<\/p>\n<ol>\n<li>Query, key, value, gate, decay, and update projections<\/li>\n<li>A short causal convolution over recent state<\/li>\n<li>Query and key normalization<\/li>\n<li>Decay of the recurrent matrix<\/li>\n<li>A delta-rule correction using the new key and value<\/li>\n<li>A query against the updated matrix<\/li>\n<li>Gated normalization and an output projection<\/li>\n<\/ol>\n<p>The Qwen runner chooses the mixer from the checkpoint&#8217;s per-layer type:<\/p>\n<pre><pre class=\"deck-syntax-block\" style=\"color:black;background:#f5f2f0;text-shadow:0 1px white;font-family:Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:0.75rem 1rem;margin:0;overflow:auto;border:none;border-radius:0;box-shadow:none\"><code class=\"language-typescript\" style=\"white-space:pre;color:black;background:none;text-shadow:0 1px white;font-family:Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;font-size:1em;text-align:left;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none\"><span class=\"token\" style=\"color:#07a\">if<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>block<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span>layerType <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">===<\/span><span> <\/span><span class=\"token\" style=\"color:#690\">'linear_attention'<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">{<\/span><span>\n<\/span><span>  mixer <\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">=<\/span><span> <\/span><span class=\"token\" style=\"color:#07a\">new<\/span><span> <\/span><span class=\"token\" style=\"color:#DD4A68\">TSLGatedDeltaNet<\/span><span class=\"token\" style=\"color:#999\">(<\/span><span>ln1<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span>outputNode<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span> block<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span>delta<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">{<\/span><span>\n<\/span><span>    hiddenSize<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> <\/span><span class=\"token\" style=\"color:#07a\">this<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span>hiddenSize<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span>    numKHeads<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> weights<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span>linearKeyHeads<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span>    numVHeads<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> weights<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span>linearValueHeads<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span>    keyDim<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> weights<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span>linearKeyDim<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span>    valueDim<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> weights<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span>linearValueDim<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span>    kernelSize<\/span><span class=\"token\" style=\"color:#9a6e3a;background:hsla(0, 0%, 100%, .5)\">:<\/span><span> weights<\/span><span class=\"token\" style=\"color:#999\">.<\/span><span>linearConvKernel<\/span><span class=\"token\" style=\"color:#999\">,<\/span><span>\n<\/span><span>  <\/span><span class=\"token\" style=\"color:#999\">}<\/span><span class=\"token\" style=\"color:#999\">)<\/span><span class=\"token\" style=\"color:#999\">;<\/span><span>\n<\/span><span\/><span class=\"token\" style=\"color:#999\">}<\/span><span> <\/span><span class=\"token\" style=\"color:#07a\">else<\/span><span> <\/span><span class=\"token\" style=\"color:#999\">{<\/span><span>\n<\/span><span>  <\/span><span class=\"token\" style=\"color:slategray\">\/\/ Build gated full attention with a conventional KV cache.<\/span><span>\n<\/span><span\/><span class=\"token\" style=\"color:#999\">}<\/span><span>\n<\/span><\/code><\/pre>\n<p>Qwen&#8217;s architecture warrants a dedicated runner, recurrent state management,<br \/>\nand a full set of DeltaNet TSL kernels. The surrounding residual and gated MLP<br \/>\nstructure reuses the same operators as the other model families.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"optimizations-that-paid-off\">Optimizations that paid off<a aria-label=\"Link to section: Optimizations that paid off\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#optimizations-that-paid-off\">#<\/a><\/h2>\n<p>I tested kernel rewrites, command submission, prefill behavior, readback, and<br \/>\nmodel-loading changes while building the library. A few changes accounted for<br \/>\nmost of the measured gains.<\/p>\n<h3 class=\"deck-markdown-heading group\" id=\"submit-a-forward-pass-once\">Submit a forward pass once<a aria-label=\"Link to section: Submit a forward pass once\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#submit-a-forward-pass-once\">#<\/a><\/h3>\n<p>SmolLM2 executes more than 400 compute dispatches for one token. The first<br \/>\nimplementation submitted each dispatch separately. Recording the ordered<br \/>\nnodes into one Three.js compute call reduced 427 command submissions to one.<\/p>\n<p>That change raised TinyStories decode from about 120 to 566 tokens per second,<br \/>\na 4.7\u00d7 improvement. SmolLM2 rose from about 34 to 75 tokens per second, a<br \/>\n2.2\u00d7 improvement. JavaScript-to-WebGPU submission overhead had dominated<br \/>\nthese small models.<\/p>\n<p>The GPU still runs each dispatch, switches pipelines, and reads intermediate<br \/>\nbuffers. One command submission removes CPU and queue overhead. Fusing the<br \/>\nmodel into fewer shaders would require a separate optimization.<\/p>\n<h3 class=\"deck-markdown-heading group\" id=\"do-not-compute-unused-prompt-logits\">Do not compute unused prompt logits<a aria-label=\"Link to section: Do not compute unused prompt logits\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#do-not-compute-unused-prompt-logits\">#<\/a><\/h3>\n<p>Only the final prompt token needs vocabulary logits. Earlier prompt tokens<br \/>\nneed to update attention caches or recurrent state, but projecting every one<br \/>\nof them across a 49,152-token vocabulary throws the result away.<\/p>\n<p>Three-LLM records a prefill path without the final normalization and vocabulary<br \/>\nprojection, then computes logits for the last prompt token. Together with the<br \/>\nsubmission changes, warm SmolLM2 time to first token fell from roughly 760 ms<br \/>\nto about 260 ms in the measured test.<\/p>\n<h3 class=\"deck-markdown-heading group\" id=\"reuse-prompt-prefixes\">Reuse prompt prefixes<a aria-label=\"Link to section: Reuse prompt prefixes\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#reuse-prompt-prefixes\">#<\/a><\/h3>\n<p>Chat prompts repeat the earlier conversation on every turn. Three-LLM compares<br \/>\nthe new token sequence with the previous one and reuses the matching prefix.<br \/>\nThe runner only prefills the appended text when its cache state can be reused.<\/p>\n<h3 class=\"deck-markdown-heading group\" id=\"reduce-sampling-on-the-gpu\">Reduce sampling on the GPU<a aria-label=\"Link to section: Reduce sampling on the GPU\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#reduce-sampling-on-the-gpu\">#<\/a><\/h3>\n<p>Reading an entire vocabulary back to JavaScript creates a synchronization<br \/>\npoint after every generated token. Three-LLM can run a hierarchical maximum<br \/>\nor small top-k reduction on the GPU. Greedy generation reads four bytes for<br \/>\none token ID. A supported top-k request reads a small candidate set and<br \/>\nfinishes sampling on the CPU.<\/p>\n<p>The full-logit path remains available for sampling options that need every<br \/>\nscore. This keeps the API flexible while removing the common greedy readback<br \/>\nbottleneck.<\/p>\n<h3 class=\"deck-markdown-heading group\" id=\"free-copies-once-the-gpu-owns-the-weights\">Free copies once the GPU owns the weights<a aria-label=\"Link to section: Free copies once the GPU owns the weights\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#free-copies-once-the-gpu-owns-the-weights\">#<\/a><\/h3>\n<p>Loading a large checkpoint can leave the source tensors, unpacked arrays, and<br \/>\nGPU buffers alive at the same time. After Three.js creates the GPU bindings,<br \/>\nThree-LLM releases static CPU weight arrays while retaining the embedding data<br \/>\nneeded to look up future token rows. The demo&#8217;s model proxy also splits large<br \/>\nfiles into concurrent requests so one multi-gigabyte response does not become<br \/>\na serial loading bottleneck.<\/p>\n<p>Measurements rejected two plausible optimizations. Shared-workgroup<br \/>\nnormalization and broad <code class=\"p-0\">vec4<\/code> matrix-vector kernels failed to improve<br \/>\nrepresentative SmolLM2 throughput, so I left out the extra complexity.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"the-fp32-cost\">The FP32 cost<a aria-label=\"Link to section: The FP32 cost\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#the-fp32-cost\">#<\/a><\/h2>\n<p>Three-LLM currently runs every GPU kernel with 32-bit floating-point storage<br \/>\nand arithmetic. A checkpoint may arrive as FP16 or BF16, but the loader<br \/>\nexpands non-embedding tensors into <code class=\"p-0\">Float32Array<\/code> values. Embedding rows are<br \/>\nconverted as they are copied. KV caches, activations, logits, and Qwen&#8217;s<br \/>\nrecurrent state are FP32 too.<\/p>\n<p>This provides one simple baseline across WebGPU adapters, but it imposes a<br \/>\nlarge memory and bandwidth cost. SmolLM2&#8217;s 269 MB BF16 checkpoint becomes<br \/>\nroughly twice that size for its GPU weights. Qwen&#8217;s 1.7 GB and Phi&#8217;s 2.8 GB<br \/>\ncheckpoint files make the problem much more visible. Autoregressive decode<br \/>\nreads much of that weight data again for every token.<\/p>\n<p>In a near-matched SmolLM2-135M test, Three-LLM reached about 78 tokens per<br \/>\nsecond with the base checkpoint expanded to FP32. WebLLM reached about 80 with<br \/>\nan unquantized FP32 Instruct checkpoint using the same architecture and<br \/>\nparameter scale. The gap was much larger for prompt prefill and for larger<br \/>\nquantized models. <a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/arxiv.org\/abs\/2412.15803\" node=\"[object Object]\">WebLLM<\/a> compiles<br \/>\nmodel-specific kernels ahead of time, supplies separate prefill and decode<br \/>\nprograms, and ships reduced-precision or quantized weights. Three-LLM accepts<br \/>\nregular Hugging Face checkpoints and assembles general operators at runtime.<br \/>\nThe two designs optimize for different goals.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"where-this-can-go-next\">Where this can go next<a aria-label=\"Link to section: Where this can go next\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#where-this-can-go-next\">#<\/a><\/h2>\n<p>Native FP16 is the clearest next precision step. The<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/gpuweb.github.io\/gpuweb\/wgsl\/#extension-f16\" node=\"[object Object]\">WGSL <code class=\"p-0\">f16<\/code> extension<\/a><br \/>\nand WebGPU&#8217;s<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/gpuweb.github.io\/gpuweb\/#enumdef-gpufeaturename\" node=\"[object Object]\"><code class=\"p-0\">shader-f16<\/code> device feature<\/a><br \/>\nallow compatible GPUs to store and operate on half-precision values. Halving<br \/>\nweight storage and memory traffic should matter more as models grow, although<br \/>\nthe actual throughput gain will depend on the adapter. I have been working on<br \/>\nthe 16-bit support needed to expose this cleanly through Three.js TSL.<\/p>\n<p>Quantized weights offer a larger capacity gain. The WGSL<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/gpuweb.github.io\/gpuweb\/wgsl\/#language-extension-packed-4x8-integer-dot-product\" node=\"[object Object]\"><code class=\"p-0\">packed_4x8_integer_dot_product<\/code><\/a><br \/>\nextension exposes hardware instructions that multiply four packed 8-bit<br \/>\ninteger pairs at once. I have also been working to expose this language<br \/>\nextension through TSL. A Three-LLM path built around it would need a weight<br \/>\nformat, per-block scales, adapter feature detection, and kernels that account<br \/>\nfor dequantization cost. The extension supplies the dot-product primitive; a<br \/>\ncomplete quantization design must provide everything around it.<\/p>\n<p>Prefill also remains far behind a compiled engine. Three-LLM records several<br \/>\none-token passes together, but it does not yet turn prompt processing into<br \/>\ntrue batched matrix-matrix operations. A dedicated batched prefill graph could<br \/>\nreuse weights across prompt tokens and expose more parallel work.<\/p>\n<p>Finally, graph fusion could remove dispatches and intermediate buffers.<br \/>\nCandidates include the gate and up projections, projection plus residual<br \/>\naddition, and parts of attention. The failed normalization and <code class=\"p-0\">vec4<\/code><br \/>\nexperiments are a useful warning: each fused path needs full-model<br \/>\nmeasurements on representative GPUs.<\/p>\n<h2 class=\"deck-markdown-heading group\" id=\"try-it\">Try it<a aria-label=\"Link to section: Try it\" class=\"deck-markdown-heading-anchor no-underline\" href=\"#try-it\">#<\/a><\/h2>\n<p>You can run the <a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/three-llm.ben3d.ca\" node=\"[object Object]\">live Three-LLM chat demo<\/a>, install<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/www.npmjs.com\/package\/three-llm\" node=\"[object Object]\"><code class=\"p-0\">three-llm<\/code> from npm<\/a>, or read the<br \/>\n<a class=\"text-[rgb(var(--link))] no-underline transition-colors hover:text-[rgb(var(--link-hover))] hover:underline\" href=\"https:\/\/github.com\/bhouston\/three-llm\" node=\"[object Object]\">source on GitHub<\/a>.<\/p>\n<p>Start with TinyStories to see the complete GPT-2 path at a scale that loads<br \/>\nalmost anywhere. SmolLM2 shows the modern Llama-style changes without a huge<br \/>\ncheckpoint. Phi demonstrates a parallel transformer block, and Qwen exercises<br \/>\nthe hybrid Gated DeltaNet architecture. On mobile, stay with TinyStories or<br \/>\nSmolLM2.<\/p>\n<p>Building this test of Three.js compute support required tokenization,<br \/>\ncheckpoint loading, several transformer families, recurrent linear attention,<br \/>\nGPU sampling, prompt caching, and complete autoregressive generation. The same<br \/>\nWebGPU renderer can draw a scene and run the language model that describes it.<\/p>\n<\/div>\n<p><a href=\"https:\/\/ben3d.ca\/blog\/running-llms-in-the-browser-with-threejs?utm_source=tldrdev\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>TL;DR# Three-LLM runs GPT-2, SmolLM2, Qwen, and Phi models locally in the browser by turning their inference graphs into Three.js TSL compute shaders on WebGPU. Try the demo at https:\/\/three-llm.ben3d.ca. I started this project to test two things: how far I could push the general compute capabilities of Three.js and WebGPU, and what today&#8217;s small [&hellip;]<\/p>\n","protected":false},"author":16,"featured_media":23767,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[143],"tags":[],"class_list":["post-23766","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai"],"_links":{"self":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/23766","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/users\/16"}],"replies":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/comments?post=23766"}],"version-history":[{"count":0,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/23766\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media\/23767"}],"wp:attachment":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media?parent=23766"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/categories?post=23766"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/tags?post=23766"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}