volvoxai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/bin/volvox.js +72 -0
  4. package/dist/v0.1.0/volvoxai.js +4664 -0
  5. package/dist/v0.1.0/volvoxai.min.js +1848 -0
  6. package/dist/v0.1.0/volvoxai.wasm +0 -0
  7. package/dist/volvoxai.js +4664 -0
  8. package/dist/volvoxai.min.js +1848 -0
  9. package/dist/volvoxai.wasm +0 -0
  10. package/docs/README.md +22 -0
  11. package/docs/browser-runtime.md +87 -0
  12. package/docs/efficientdet_tflite_vs_volvoxai.md +445 -0
  13. package/docs/microkernel_optimization_guide.md +153 -0
  14. package/docs/model-format.md +108 -0
  15. package/docs/models.md +103 -0
  16. package/docs/native-runtime.md +189 -0
  17. package/docs/operation_list.md +232 -0
  18. package/docs/operator_fusion_patterns.md +58 -0
  19. package/docs/quickstart.md +115 -0
  20. package/docs/roadmap.md +19 -0
  21. package/docs/testing.md +97 -0
  22. package/docs/textbook/01-foundations.md +233 -0
  23. package/docs/textbook/02-tinystories-language-model.md +300 -0
  24. package/docs/textbook/03-efficientdet-vision-model.md +281 -0
  25. package/docs/textbook/04-precision-and-quantization.md +208 -0
  26. package/docs/textbook/05-inside-the-engine.md +155 -0
  27. package/docs/textbook/06-native-engine-architecture.md +338 -0
  28. package/docs/textbook/07-glossary-and-next-steps.md +258 -0
  29. package/docs/textbook/README.md +85 -0
  30. package/docs/textbook/ko/01-foundations.md +231 -0
  31. package/docs/textbook/ko/02-tinystories-language-model.md +300 -0
  32. package/docs/textbook/ko/03-efficientdet-vision-model.md +277 -0
  33. package/docs/textbook/ko/04-precision-and-quantization.md +206 -0
  34. package/docs/textbook/ko/05-inside-the-engine.md +154 -0
  35. package/docs/textbook/ko/06-native-engine-architecture.md +333 -0
  36. package/docs/textbook/ko/07-glossary-and-next-steps.md +253 -0
  37. package/docs/textbook/ko/README.md +83 -0
  38. package/docs/xnnpack_optimization_guide.md +197 -0
  39. package/js/CPUEngine.js +241 -0
  40. package/js/Graph.js +49 -0
  41. package/js/GraphExecutor.js +1020 -0
  42. package/js/GraphLoader.js +282 -0
  43. package/js/ShaderLibrary.js +236 -0
  44. package/js/Tensor.js +25 -0
  45. package/js/Tokenizer.js +266 -0
  46. package/js/VolvoxAI.js +130 -0
  47. package/js/WasmEngine.js +378 -0
  48. package/js/WebNNEngine.js +169 -0
  49. package/js/index.js +11 -0
  50. package/js/ops/add.js +31 -0
  51. package/js/ops/argMax.js +33 -0
  52. package/js/ops/averagePool2D.js +38 -0
  53. package/js/ops/batchNorm2D.js +28 -0
  54. package/js/ops/cast.js +19 -0
  55. package/js/ops/clip.js +15 -0
  56. package/js/ops/concat2.js +18 -0
  57. package/js/ops/conv1D.js +35 -0
  58. package/js/ops/conv2D.js +70 -0
  59. package/js/ops/convTranspose2D.js +45 -0
  60. package/js/ops/crossAttention.js +69 -0
  61. package/js/ops/crossSDPA.js +41 -0
  62. package/js/ops/dequantizeLinear.js +9 -0
  63. package/js/ops/div.js +15 -0
  64. package/js/ops/embedding.js +14 -0
  65. package/js/ops/expand.js +24 -0
  66. package/js/ops/gELU.js +9 -0
  67. package/js/ops/gather.js +51 -0
  68. package/js/ops/gatherElements.js +33 -0
  69. package/js/ops/globalAveragePool.js +21 -0
  70. package/js/ops/hardSigmoid.js +12 -0
  71. package/js/ops/hardSwish.js +12 -0
  72. package/js/ops/interp1D.js +25 -0
  73. package/js/ops/layerNorm.js +25 -0
  74. package/js/ops/leakyReLU.js +10 -0
  75. package/js/ops/logSoftmax.js +15 -0
  76. package/js/ops/matMul.js +35 -0
  77. package/js/ops/maxPool2D.js +36 -0
  78. package/js/ops/meanHeight.js +17 -0
  79. package/js/ops/mul.js +31 -0
  80. package/js/ops/nonMaxSuppression.js +72 -0
  81. package/js/ops/pReLU.js +11 -0
  82. package/js/ops/pad.js +35 -0
  83. package/js/ops/profileX.js +22 -0
  84. package/js/ops/profileY.js +22 -0
  85. package/js/ops/rMSNorm.js +14 -0
  86. package/js/ops/reLU.js +8 -0
  87. package/js/ops/reduceMean.js +17 -0
  88. package/js/ops/reduceSum.js +19 -0
  89. package/js/ops/reshape.js +6 -0
  90. package/js/ops/resize.js +44 -0
  91. package/js/ops/sDPA.js +44 -0
  92. package/js/ops/siLU.js +8 -0
  93. package/js/ops/sigmoid.js +6 -0
  94. package/js/ops/slice.js +36 -0
  95. package/js/ops/softmax.js +18 -0
  96. package/js/ops/spatialSoftargmaxY.js +28 -0
  97. package/js/ops/split.js +24 -0
  98. package/js/ops/sub.js +11 -0
  99. package/js/ops/tanh.js +7 -0
  100. package/js/ops/transpose.js +34 -0
  101. package/js/ops/upsample2x.js +23 -0
  102. package/js/ops/where.js +15 -0
  103. package/package.json +33 -0
  104. package/shaders/add.wgsl +13 -0
  105. package/shaders/add3Relu.wgsl +23 -0
  106. package/shaders/addRelu.wgsl +22 -0
  107. package/shaders/averagePool2D.wgsl +24 -0
  108. package/shaders/batchNorm2D.wgsl +21 -0
  109. package/shaders/binaryBroadcast.wgsl +34 -0
  110. package/shaders/broadcastBinary.wgsl +26 -0
  111. package/shaders/clip.wgsl +10 -0
  112. package/shaders/concat2.wgsl +16 -0
  113. package/shaders/concatCopy.wgsl +10 -0
  114. package/shaders/concatSigmoidCopy.wgsl +16 -0
  115. package/shaders/conv1D.wgsl +37 -0
  116. package/shaders/conv2D.wgsl +80 -0
  117. package/shaders/conv2DDepthwise4.wgsl +74 -0
  118. package/shaders/conv2DDepthwise8.wgsl +66 -0
  119. package/shaders/conv2DPointwise16.wgsl +67 -0
  120. package/shaders/conv2DPointwise16Tile.wgsl +86 -0
  121. package/shaders/conv2DPointwise8.wgsl +85 -0
  122. package/shaders/conv2DPointwise8Vec2.wgsl +70 -0
  123. package/shaders/conv2DPointwise8Vec4.wgsl +65 -0
  124. package/shaders/conv2DRegularC3Out16.wgsl +75 -0
  125. package/shaders/convTranspose2D.wgsl +33 -0
  126. package/shaders/copy.wgsl +13 -0
  127. package/shaders/crossAttention.wgsl +140 -0
  128. package/shaders/crossAttentionF32.wgsl +98 -0
  129. package/shaders/crossSDPA.wgsl +74 -0
  130. package/shaders/dequantizeLinear.wgsl +14 -0
  131. package/shaders/div.wgsl +34 -0
  132. package/shaders/elementwise.wgsl +13 -0
  133. package/shaders/embedding.wgsl +22 -0
  134. package/shaders/expand.wgsl +18 -0
  135. package/shaders/gELU.wgsl +13 -0
  136. package/shaders/gather.wgsl +17 -0
  137. package/shaders/generalTranspose.wgsl +19 -0
  138. package/shaders/globalAveragePool.wgsl +19 -0
  139. package/shaders/hardSigmoid.wgsl +13 -0
  140. package/shaders/hardSwish.wgsl +13 -0
  141. package/shaders/interp1D.wgsl +28 -0
  142. package/shaders/layerNorm.wgsl +33 -0
  143. package/shaders/leakyReLU.wgsl +11 -0
  144. package/shaders/linearF32.wgsl +33 -0
  145. package/shaders/linearF32RowMajor.wgsl +24 -0
  146. package/shaders/linearInt8.wgsl +42 -0
  147. package/shaders/logSoftmax.wgsl +22 -0
  148. package/shaders/maxPool2D.wgsl +37 -0
  149. package/shaders/meanHeight.wgsl +18 -0
  150. package/shaders/mul.wgsl +32 -0
  151. package/shaders/nonMaxSuppression.wgsl +92 -0
  152. package/shaders/pReLU.wgsl +14 -0
  153. package/shaders/pad.wgsl +19 -0
  154. package/shaders/profileX.wgsl +28 -0
  155. package/shaders/profileY.wgsl +28 -0
  156. package/shaders/quantizeLinear.wgsl +69 -0
  157. package/shaders/rMSNorm.wgsl +21 -0
  158. package/shaders/reLU.wgsl +13 -0
  159. package/shaders/reduce.wgsl +17 -0
  160. package/shaders/resize.wgsl +52 -0
  161. package/shaders/sDPA.wgsl +71 -0
  162. package/shaders/siLU.wgsl +13 -0
  163. package/shaders/sigmoid.wgsl +13 -0
  164. package/shaders/slice.wgsl +26 -0
  165. package/shaders/softmax.wgsl +23 -0
  166. package/shaders/spatialSoftargmaxY.wgsl +32 -0
  167. package/shaders/split.wgsl +15 -0
  168. package/shaders/sub.wgsl +34 -0
  169. package/shaders/tanh.wgsl +13 -0
  170. package/shaders/upsample2x.wgsl +24 -0
  171. package/shaders/where.wgsl +12 -0
  172. package/volvoxai.wasm +0 -0
@@ -0,0 +1,233 @@
1
+ # Chapter 1 — Foundations
2
+
3
+ *Goal: after this chapter you can look at any model and see it as a **graph of operations on
4
+ tensors**, and you understand the pieces VolvoxAI uses to run one.*
5
+
6
+ ---
7
+
8
+ ## 1.1 What "running an AI" actually means
9
+
10
+ When you use an AI model, three things happen:
11
+
12
+ 1. Your input (text, an image, audio) is turned into **numbers**.
13
+ 2. Those numbers flow through a long list of **arithmetic operations**. Each operation also
14
+ uses a big table of pre-computed numbers called **weights**.
15
+ 3. The final numbers are turned back into something meaningful (a word, a box, a label).
16
+
17
+ Step 2 is the model. Running it once is called a **forward pass**, or **inference**. That is
18
+ all inference is: a pipeline of multiply-and-add, arranged in a specific order that someone
19
+ *trained* to be useful.
20
+
21
+ > **Training vs. inference.** *Training* is the expensive process that searches for good weight
22
+ > values by showing the network millions of examples and nudging the weights to reduce mistakes.
23
+ > *Inference* just uses the finished weights. **VolvoxAI is an inference engine** — it never
24
+ > changes a weight. Everything in this book is about inference.
25
+
26
+ ---
27
+
28
+ ## 1.2 The tensor: the only data structure you need
29
+
30
+ Every number moving through a network lives in a **tensor**. A tensor is just a
31
+ multi-dimensional array (a grid of numbers) plus a **shape** that says how big each dimension is.
32
+
33
+ ```
34
+ scalar 3.14 shape [] (a single number)
35
+ vector [3.14, 2.71, 1.62] shape [3] (a list)
36
+ matrix [[1, 2, 3], shape [2, 3] (a table: 2 rows, 3 cols)
37
+ [4, 5, 6]]
38
+ tensor a batch of 224x224 RGB images shape [8, 224, 224, 3]
39
+ ```
40
+
41
+ That last shape, `[8, 224, 224, 3]`, reads as: **8** images, each **224** pixels tall, **224**
42
+ wide, with **3** color channels (red, green, blue). Four numbers fully describe millions of
43
+ values.
44
+
45
+ **In this repo**, a tensor is a tiny object (`js/Tensor.js`) — a name, a shape, a data type,
46
+ and a flat buffer of numbers:
47
+
48
+ ```javascript
49
+ // js/Tensor.js (paraphrased)
50
+ class Tensor {
51
+ name; // e.g. "hidden_0"
52
+ shape; // e.g. [1, 256, 64]
53
+ dtype; // "float32" | "int8" | "int32"
54
+ buffer; // a flat Float32Array / Int8Array holding shape-product numbers
55
+ isWeight; // true if it was learned during training (read-only at inference)
56
+ }
57
+ ```
58
+
59
+ ### Tensors are stored flat
60
+
61
+ A computer's memory is one long line of bytes — it has no idea about "rows" and "columns." A
62
+ tensor with shape `[2, 3]` is stored as **6 numbers in a row**, and we compute *where* element
63
+ `[row, col]` lives with index math:
64
+
65
+ ```
66
+ logical view flat memory (what actually exists)
67
+ [[a, b, c], [ a, b, c, d, e, f ]
68
+ [d, e, f]] 0 1 2 3 4 5
69
+
70
+ element [row, col] → flat index = row * 3 + col
71
+ element [1, 2] = 'f' → 1 * 3 + 2 = 5 ✓
72
+ ```
73
+
74
+ You will see this exact pattern — `((b * H + y) * W + x) * C + c` — all over the kernels. It is
75
+ how a 4-D image tensor is addressed inside a 1-D array. Memorize the shape, and the index math
76
+ follows.
77
+
78
+ > **NHWC vs NCHW.** The *order* of the dimensions matters. VolvoxAI's vision models use
79
+ > **NHWC** (batch, height, width, channels) — the color channels of one pixel sit next to each
80
+ > other in memory. PyTorch usually uses **NCHW**. Same data, different memory layout; kernels
81
+ > must agree on which one they're reading. (This repo's converter records `"internal_layout":
82
+ > "NHWC"` in every vision `config.json`.)
83
+
84
+ ---
85
+
86
+ ## 1.3 The operation: one small, well-defined job
87
+
88
+ An **operation** (or **op**, or **layer**) takes one or more input tensors, does a fixed piece
89
+ of math, and writes one or more output tensors. Examples you will meet:
90
+
91
+ | Op | In words | Used by |
92
+ |---|---|---|
93
+ | `MatMul` | Matrix multiply — the core "mixing" of features | both models |
94
+ | `Conv2D` | Slide a small filter over an image | the detector |
95
+ | `Add` | Element-wise add two tensors | both |
96
+ | `LayerNorm` | Re-center and re-scale a vector to be well-behaved | the LM |
97
+ | `SDPA` | Scaled-dot-product attention — "which words look at which" | the LM |
98
+ | `GELU` / `ReLU` | A nonlinear squashing function | both |
99
+ | `MaxPool2D` | Shrink an image by keeping the biggest value in each patch | the detector |
100
+
101
+ Every op in VolvoxAI has a plain-English **reference implementation** in `js/ops/`, one small
102
+ file each. Here is the *entire* `Add` op — this is not a simplification, it is the real code:
103
+
104
+ ```javascript
105
+ // js/ops/add.js — element-wise addition with broadcasting
106
+ for (let i = 0; i < out.length; i++) {
107
+ out[i] = a[i] + b[i % b.length]; // b.length may be smaller ("broadcast")
108
+ }
109
+ ```
110
+
111
+ That is the whole secret: a model is thousands of operations like this, each trivial, chained
112
+ together. **There is no step where something inexplicable happens.**
113
+
114
+ ---
115
+
116
+ ## 1.4 The graph: operations wired together
117
+
118
+ A model is a **graph** — a list of ops where each op's outputs feed later ops' inputs. VolvoxAI
119
+ stores this as a `Graph` object (`js/Graph.js`): a set of **tensors** and a list of **nodes**
120
+ (op + which tensors are its inputs/outputs + its parameters).
121
+
122
+ Because every op declares its inputs and outputs *by name*, the graph is just bookkeeping:
123
+
124
+ ```
125
+ tokens ─┐
126
+ ├─▶ [Embedding] ─▶ emb_tok ─┐
127
+ wte ───┘ ├─▶ [Add] ─▶ hidden_0 ─▶ [LayerNorm] ─▶ ...
128
+ positions ─▶ [Embedding] ─▶ emb_pos ┘
129
+ ```
130
+
131
+ To **run** the graph, VolvoxAI simply walks the node list top to bottom and executes each op.
132
+ The whole executor loop is this readable (`js/CPUEngine.js`):
133
+
134
+ ```javascript
135
+ // js/CPUEngine.js — the heart of the engine
136
+ for (const node of graph.nodes) {
137
+ this._runNode(node); // dispatch on node.opType → the matching kernel
138
+ }
139
+ ```
140
+
141
+ `_runNode` is a big `switch` on the op type (`"MatMul"` → matmul kernel, `"Conv2D"` → conv
142
+ kernel, …). That's it. **A neural network engine is a `for` loop over a list of function calls.**
143
+ Everything else is making those functions fast (Chapter 5) and numerically small (Chapter 4).
144
+
145
+ ---
146
+
147
+ ## 1.5 The blueprint: how a model is stored
148
+
149
+ A VolvoxAI model on disk is two files:
150
+
151
+ ```
152
+ model/
153
+ config.json # the GRAPH: a list of op nodes (topology + params + shapes)
154
+ model.safetensors # the WEIGHTS: the learned numbers, in a standard binary format
155
+ ```
156
+
157
+ - **`config.json`** is the *blueprint*: an ordered list of nodes. Each node names its op, its
158
+ input/output tensors, its parameters, and the exact output shape (pre-computed by the
159
+ exporter so the engine never has to guess). Here is one real node from TinyStories:
160
+
161
+ ```json
162
+ {
163
+ "op": "LayerNorm",
164
+ "inputs": { "input": "hidden_0", "weight": "h.0.ln_1.weight", "bias": "h.0.ln_1.bias" },
165
+ "outputs": { "out": "ln1_0" },
166
+ "outputs_shape": { "out": [1, 256, 64] },
167
+ "params": { "eps": 1e-05, "d_model": 64 }
168
+ }
169
+ ```
170
+
171
+ - **`model.safetensors`** holds the raw weight tensors (`h.0.ln_1.weight`, `wte.weight`, …) in
172
+ [safetensors](https://github.com/huggingface/safetensors) format — a simple, safe, standard
173
+ layout used across the ML world.
174
+
175
+ `js/GraphLoader.js` reads both, builds the `Graph`, and hands it to the engine. **You train in
176
+ PyTorch, export to this blueprint, and Volvox runs it** — with no PyTorch, no ONNX Runtime, no
177
+ dependencies at inference time.
178
+
179
+ ---
180
+
181
+ ## 1.6 One model, four ways to run it (the "tiers")
182
+
183
+ The same graph can be executed on very different hardware. VolvoxAI picks the best available
184
+ **tier** automatically, and every tier computes the *same* result:
185
+
186
+ ```mermaid
187
+ flowchart TD
188
+ G[Graph + weights] --> SEL{VolvoxAI.init<br/>picks best available}
189
+ SEL -->|browser NPU/GPU| T1[Tier 1 · WebNN]
190
+ SEL -->|browser GPU| T2[Tier 2 · WebGPU<br/>WGSL compute shaders]
191
+ SEL -->|any CPU, fast| T3[Tier 3 · WASM SIMD<br/>compiled C kernels]
192
+ SEL -->|any CPU, always works| T4[Tier 4 · Pure JS<br/>reference kernels]
193
+ N[Native binary · C<br/>Vulkan/OpenGL/Metal/CPU] -.same blueprint.-> G
194
+ ```
195
+
196
+ - **Tier 4 (Pure JS, `js/ops/*.js`)** is the *reference*: slow but obviously-correct, and the
197
+ ground truth every other tier is checked against. **We use it as our teaching text** because
198
+ it is the most readable.
199
+ - **Tier 3 (WASM)** runs the same math as compiled C for a big speedup.
200
+ - **Tier 2 (WebGPU)** re-expresses each op as a GPU compute shader (`shaders/*.wgsl`).
201
+ - **Tier 1 (WebNN)** hands the graph to the browser's own neural-network API (can hit an NPU).
202
+ - **Native** (`native/`) is a standalone C program that runs the *same* blueprint on a desktop
203
+ or phone, optionally on Vulkan/OpenGL/Metal.
204
+
205
+ For the rest of the book, when we "trace an op," we read the pure-JS or portable-C version,
206
+ because they say most directly *what the math is*.
207
+
208
+ ---
209
+
210
+ ## 1.7 The mental model, assembled
211
+
212
+ Put it together and you have the whole engine in one picture:
213
+
214
+ ```
215
+ INPUT ──encode──▶ TENSORS ──┐
216
+ │ for each node in graph.nodes:
217
+ WEIGHTS (from .safetensors)─┼──▶ op(inputs, params) → output tensor
218
+
219
+ (repeat for all ~85 or ~262 nodes)
220
+
221
+
222
+ OUTPUT TENSOR ──decode──▶ ANSWER
223
+ ```
224
+
225
+ Two questions define any model:
226
+
227
+ 1. **What are the ops, and in what order?** (the graph / `config.json`)
228
+ 2. **What do the weights make each op do?** (the `.safetensors`)
229
+
230
+ In the next two chapters we answer both questions for two real models — and you'll see that a
231
+ "language model" and an "image detector" are the *same idea* with different ops in the list.
232
+
233
+ **Next:** [Chapter 2 — A Language Model, op by op →](02-tinystories-language-model.md)
@@ -0,0 +1,300 @@
1
+ # Chapter 2 — A Language Model, op by op (TinyStories)
2
+
3
+ *Goal: follow the words **"Once upon a time, Lily"** through a real GPT-style transformer and
4
+ watch it predict the next word. Every op here is one of the small kernels from `js/ops/`.*
5
+
6
+ The model lives in `models/tinystories_1m/`. It is a tiny GPT (a **decoder-only transformer**)
7
+ trained on the [TinyStories](https://arxiv.org/abs/2305.07759) dataset of simple children's
8
+ stories. "Tiny" is real: its internal vector width is **64**, it has **8** layers, and yet it
9
+ writes coherent little stories. Studying it teaches you the *exact* architecture behind
10
+ GPT-2/3/4, LLaMA, and Mistral — those are this graph, scaled up.
11
+
12
+ ## 2.1 The model's dimensions (read them off the blueprint)
13
+
14
+ From `config.json`, one number at a time:
15
+
16
+ | Symbol | Value | Meaning |
17
+ |---|---|---|
18
+ | `d_model` | **64** | Width of the "thought vector" carried per token. |
19
+ | `n_layers` | **8** | Number of transformer blocks stacked. |
20
+ | `n_heads` | **16** | Attention heads per block (so `head_dim = 64/16 = 4`). |
21
+ | `d_mlp` | **256** | Width of the feed-forward hidden layer (`4 × d_model`). |
22
+ | `vocab` | **50257** | Number of distinct tokens it knows (GPT-2 vocabulary). |
23
+ | `context` | **256** | Max tokens it can look at in one pass. |
24
+
25
+ > The folder is called `tinystories_1m` (~1M transformer parameters), but `model.safetensors`
26
+ > is ~27 MB. Why? The **token embedding table** is `50257 × 64 ≈ 3.2M` numbers, and there are
27
+ > two such tables (input + output). In tiny LMs, the *vocabulary*, not the *layers*, dominates
28
+ > the file. That's a real lesson: model size ≠ model depth.
29
+
30
+ The whole graph is **85 nodes**. Their inventory:
31
+
32
+ ```
33
+ 33 MatMul 17 LayerNorm 17 Add 8 SDPA 8 GELU 2 Embedding
34
+ = 2 embeddings + 8 blocks × (2 LayerNorm + 4 MatMul + 1 SDPA + 1 GELU + 2 Add) + final LayerNorm + 1 lm_head
35
+ ```
36
+
37
+ ## 2.2 The pipeline at a glance
38
+
39
+ ```mermaid
40
+ flowchart TD
41
+ P["prompt: 'Once upon a time, Lily'"] --> TOK[Tokenizer BPE<br/>text → token ids]
42
+ TOK --> EMB
43
+ subgraph EMB[Input embedding]
44
+ T[token ids] --> WTE[Embedding · wte]
45
+ POSN[positions 0,1,2…] --> WPE[Embedding · wpe]
46
+ WTE --> ADD0[Add]
47
+ WPE --> ADD0
48
+ end
49
+ ADD0 --> BLK
50
+ subgraph BLK["× 8 transformer blocks"]
51
+ direction TB
52
+ L1[LayerNorm] --> QKV[MatMul: qkv_proj] --> SDPA[SDPA · causal] --> OP[MatMul: out_proj] --> R1((+ residual))
53
+ R1 --> L2[LayerNorm] --> FC[MatMul: c_fc] --> G[GELU] --> PR[MatMul: c_proj] --> R2((+ residual))
54
+ end
55
+ BLK --> LNF[LayerNorm · final] --> HEAD[MatMul: lm_head] --> LOG[logits: 50257 scores]
56
+ LOG --> ARGMAX[pick highest score] --> NEXT[next token] --> DETOK[decode → text]
57
+ NEXT -. append & repeat .-> TOK
58
+ ```
59
+
60
+ Now we walk it, stage by stage, with the real kernels.
61
+
62
+ ---
63
+
64
+ ## 2.3 Stage 0 — Tokenize: text becomes integers
65
+
66
+ A neural net cannot read letters; it reads numbers. The **tokenizer** (`js/Tokenizer.js`,
67
+ `native/tokenizer.c`) chops text into **tokens** (common word-pieces) and maps each to an
68
+ integer ID using a vocabulary + a list of **merge rules** (this is **Byte-Pair Encoding**, BPE).
69
+
70
+ ```
71
+ "Once upon a time, Lily"
72
+ │ regex splits into word-ish chunks, then BPE merges frequent byte-pairs
73
+
74
+ [ "Once", " upon", " a", " time", ",", " Lily" ] (illustrative)
75
+ │ each chunk → an integer id from the 50257-word vocab
76
+
77
+ tokens = [7454, 2402, 257, 640, 11, 20037, …] ← the model's real input
78
+ positions = [ 0, 1, 2, 3, 4, 5, …] ← "which slot am I?"
79
+ ```
80
+
81
+ Two integer tensors go into the graph: **`tokens`** (what the words are) and **`positions`**
82
+ (their order, `0,1,2,…`). Both are shape `[1, 256]` — the sequence is padded to the 256-token
83
+ context window.
84
+
85
+ > **Why positions?** The attention math (below) is order-blind by itself — it would treat
86
+ > "dog bites man" and "man bites dog" identically. Feeding in an explicit position number lets
87
+ > the model learn word order.
88
+
89
+ ---
90
+
91
+ ## 2.4 Stage 1 — Embedding: integers become vectors
92
+
93
+ An ID like `20037` ("Lily") is meaningless as a *number* (it isn't 20037× anything). We replace
94
+ it with a learned **vector** of 64 numbers — its **embedding** — that encodes meaning. The
95
+ `Embedding` op is pure table lookup. Here is the entire kernel (`js/ops/embedding.js`):
96
+
97
+ ```javascript
98
+ for (let i = 0; i < seq_len; i++) {
99
+ const token_id = tokens[i];
100
+ for (let j = 0; j < d_model; j++) {
101
+ out[i * d_model + j] = weight[token_id * d_model + j]; // copy row `token_id`
102
+ }
103
+ }
104
+ ```
105
+
106
+ The weight `wte.weight` is the `[50257, 64]` table; row `token_id` *is* that token's meaning
107
+ vector. It runs **twice**:
108
+
109
+ - `Embedding(tokens, wte)` → `emb_tok` `[1,256,64]` — *what* each token is.
110
+ - `Embedding(positions, wpe)` → `emb_pos` `[1,256,64]` — *where* it is.
111
+
112
+ Then `Add` fuses them: `hidden_0 = emb_tok + emb_pos`. Now every one of the 256 slots holds a
113
+ 64-number vector that mixes *word identity* and *position*. This tensor, `hidden_0 [1,256,64]`,
114
+ is the **residual stream** — the "conveyor belt" that every block reads from and writes back to.
115
+
116
+ ```
117
+ hidden_0: 256 rows (one per token position), each a 64-number vector
118
+
119
+ pos 0 "Once" [ 0.12, -0.4, ...(64) ]
120
+ pos 1 " upon" [-0.03, 0.9, ...(64) ]
121
+ pos 2 " a" [ ... ]
122
+
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 2.5 Stage 2 — A transformer block (this happens 8×)
128
+
129
+ Each block refines the residual stream with two sub-steps: **attention** (tokens share
130
+ information) and a **feed-forward MLP** (each token thinks on its own). Both are wrapped in the
131
+ **pre-norm + residual** pattern that makes deep networks trainable.
132
+
133
+ ### 2.5a LayerNorm — keep the numbers sane
134
+
135
+ Before each sub-step, `LayerNorm` rescales each token's 64-vector to have mean 0 and variance 1,
136
+ then applies a learned scale (`weight`) and shift (`bias`). This stops values from exploding or
137
+ vanishing across 8 layers. The real kernel (`js/ops/layerNorm.js`), per token row:
138
+
139
+ ```javascript
140
+ const mean = sum / d_model;
141
+ const variance = sq_sum / d_model - mean * mean;
142
+ const inv_std = 1 / Math.sqrt(variance + 1e-5); // eps guards ÷0
143
+ out[j] = (in[j] - mean) * inv_std * weight[j] + bias[j]; // normalize, then re-scale/shift
144
+ ```
145
+
146
+ ### 2.5b Attention — "which earlier words matter to me?"
147
+
148
+ This is the heart of a transformer. First a single `MatMul` projects each 64-vector up to **192**
149
+ numbers (`qkv_proj`, shape `[1,256,192]`). Those 192 are three 64-vectors glued together: the
150
+ **Query**, **Key**, and **Value** (Q, K, V). Intuition:
151
+
152
+ - **Query** = "what am I looking for?"
153
+ - **Key** = "what do I offer?"
154
+ - **Value** = "what I'll hand over if you pick me."
155
+
156
+ Then `SDPA` (**Scaled Dot-Product Attention**) does the actual looking. For each token *q*, it
157
+ compares its Query to every earlier token's Key (a dot product = similarity), turns the
158
+ similarities into weights with **softmax**, and returns a weighted blend of those tokens' Values.
159
+ The real causal kernel (`js/ops/sDPA.js`), lightly annotated:
160
+
161
+ ```javascript
162
+ for (let h = 0; h < num_heads; h++) { // 16 independent heads
163
+ for (let q = 0; q < seq_len; q++) { // for each query position
164
+ for (let k = 0; k <= q; k++) { // ← only look at k ≤ q (CAUSAL)
165
+ score = dot(Q[q,h], K[k,h]) * scale; // similarity of q to k
166
+ }
167
+ softmax(scores); // turn scores into weights that sum to 1
168
+ out[q,h] = Σ_k weight[k] * V[k,h]; // blended value
169
+ }
170
+ }
171
+ ```
172
+
173
+ Two ideas worth pausing on:
174
+
175
+ - **Causal masking** (`k <= q`): a token may only attend to itself and *earlier* tokens, never
176
+ future ones. That's what makes it a left-to-right text *generator* — position 3 can't cheat by
177
+ peeking at position 4.
178
+ - **Multi-head** (`h`): the 64 dims are split into 16 groups of 4. Each "head" learns a different
179
+ kind of relationship (e.g. one tracks subjects, another tracks punctuation) in parallel.
180
+
181
+ ```
182
+ Attention for the token " Lily" (illustrative weights after softmax):
183
+
184
+ " Lily" attends to → "Once" " upon" " a" " time" "," " Lily"
185
+ weight 0.05 0.05 0.05 0.30 0.05 0.50
186
+ ▲ ▲
187
+ "time" is relevant mostly itself
188
+ output = 0.05·V(Once) + … + 0.30·V(time) + 0.50·V(Lily)
189
+ ```
190
+
191
+ A final `MatMul` (`out_proj`, with bias) mixes the 16 heads' outputs back into a 64-vector, and a
192
+ **residual `Add`** adds it onto the stream: `add1 = hidden + attention_output`. "Residual" means
193
+ we *add* the block's result instead of replacing the stream — so information is never lost and
194
+ gradients flow during training.
195
+
196
+ ### 2.5c Feed-forward MLP — each token thinks
197
+
198
+ After tokens have shared info, each one is transformed on its own by a 2-layer MLP:
199
+
200
+ ```
201
+ c_fc : MatMul 64 → 256 (+bias) "expand: give it room to compute"
202
+ GELU : nonlinearity "let it make nonlinear decisions"
203
+ c_proj: MatMul 256 → 64 (+bias) "compress back to stream width"
204
+ Add : residual hidden_next = add1 + mlp_output
205
+ ```
206
+
207
+ `GELU` (`js/ops/gELU.js`) is the nonlinearity — a smooth gate that lets small negatives leak and
208
+ passes positives. Without a nonlinearity like this, stacking MatMuls would collapse into a single
209
+ MatMul and the network could only learn straight-line relationships:
210
+
211
+ ```javascript
212
+ out[i] = 0.5 * x * (1 + tanh(0.7978845608 * (x + 0.044715 * x*x*x))); // the GELU curve
213
+ ```
214
+
215
+ The block's output `hidden_next [1,256,64]` has the same shape as its input — which is exactly
216
+ why we can stack **8** of them. Each block reads the stream and writes a slightly smarter version
217
+ back.
218
+
219
+ ---
220
+
221
+ ## 2.6 Stage 3 — Head: vectors become word-scores
222
+
223
+ After the 8th block, one last `LayerNorm` (`ln_f`) cleans up the stream. Then the **language-model
224
+ head** — a single `MatMul` by `lm_head.weight [50257, 64]` — turns each 64-vector into **50257
225
+ scores**, one per vocabulary word:
226
+
227
+ ```
228
+ final_norm [1,256,64] ──MatMul lm_head──▶ logits [1,256,50257]
229
+ ```
230
+
231
+ These raw scores are called **logits**. `logits[0, p, w]` = "how strongly the model, having read
232
+ tokens `0..p`, expects word `w` to come next." We only care about the row for the **last real
233
+ token** — that's the prediction for what comes after the prompt.
234
+
235
+ ---
236
+
237
+ ## 2.7 Stage 4 — Sample: scores become the next word
238
+
239
+ We now have 50257 scores for the next token. This repo's generator (`native/main.c`,
240
+ `command_generate`) uses the simplest rule, **greedy / argmax** — just take the highest:
241
+
242
+ ```c
243
+ int best_id = 0; float best_val = -1e30f;
244
+ for (int i = 0; i < vocab_count; i++)
245
+ if (logits[i] > best_val) { best_val = logits[i]; best_id = i; } // argmax
246
+ // best_id is the next token; decode it back to text:
247
+ printf("%s", tokenizer_decode(tok, best_id));
248
+ ```
249
+
250
+ > **Real generators add randomness** — *temperature* (flatten/sharpen the scores), *top-k* /
251
+ > *top-p* (sample only from the most likely few). Those turn logits into a probability
252
+ > distribution with `softmax` and roll a weighted die, which is why ChatGPT gives different
253
+ > answers each time. Greedy is the deterministic special case; it's perfect for a textbook.
254
+
255
+ ---
256
+
257
+ ## 2.8 The loop — one word at a time (autoregression)
258
+
259
+ A transformer predicts **one** token per forward pass. To write a sentence, you append the new
260
+ token and run again. This is **autoregressive generation**:
261
+
262
+ ```mermaid
263
+ flowchart LR
264
+ A["tokens so far"] --> B[forward pass<br/>85 ops] --> C[logits] --> D[argmax → next token]
265
+ D --> E{stop?<br/>max length or EOS}
266
+ E -- no --> A
267
+ E -- yes --> F[done: full story]
268
+ ```
269
+
270
+ ```
271
+ step 0: "Once upon a time, Lily" → " was"
272
+ step 1: "Once upon a time, Lily was" → " a"
273
+ step 2: "Once upon a time, Lily was a" → " little"
274
+ step 3: … → " girl" → " who" → " loved" → " to" → " play" …
275
+ ```
276
+
277
+ That is literally how ChatGPT types word-by-word: it is running this loop, each new token fed
278
+ back in as input.
279
+
280
+ > **KV-cache (an optimization you'll hear about).** Naively, step *N* recomputes attention over
281
+ > all *N* tokens from scratch — wasteful. Production engines *cache* each token's Key and Value
282
+ > so each step only computes the new token's. VolvoxAI's native runner exposes this split as
283
+ > `engine_prefill()` (process the whole prompt once) and `engine_decode()` (one new token at a
284
+ > time). The math is identical; the cache just avoids repeating work.
285
+
286
+ ---
287
+
288
+ ## 2.9 What you just learned
289
+
290
+ - A language model is: **tokenize → embed → (LayerNorm, attention, MLP) × N → head → sample →
291
+ loop.** Nothing more.
292
+ - **Attention** lets tokens share information ("which earlier words matter to me?"); the **MLP**
293
+ lets each token compute on its own; **residuals + LayerNorm** make the stack trainable and deep.
294
+ - Every op is one small kernel in `js/ops/` — `MatMul`, `SDPA`, `LayerNorm`, `GELU`, `Add`,
295
+ `Embedding`. GPT-2/3/4 and LLaMA are **this exact graph, wider and deeper**.
296
+
297
+ Next we switch domains entirely — from text to pixels — and you'll see the *same skeleton*
298
+ (a graph of small ops on tensors) solve a completely different problem.
299
+
300
+ **Next:** [Chapter 3 — A Vision Model, op by op →](03-efficientdet-vision-model.md)