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.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/bin/volvox.js +72 -0
- package/dist/v0.1.0/volvoxai.js +4664 -0
- package/dist/v0.1.0/volvoxai.min.js +1848 -0
- package/dist/v0.1.0/volvoxai.wasm +0 -0
- package/dist/volvoxai.js +4664 -0
- package/dist/volvoxai.min.js +1848 -0
- package/dist/volvoxai.wasm +0 -0
- package/docs/README.md +22 -0
- package/docs/browser-runtime.md +87 -0
- package/docs/efficientdet_tflite_vs_volvoxai.md +445 -0
- package/docs/microkernel_optimization_guide.md +153 -0
- package/docs/model-format.md +108 -0
- package/docs/models.md +103 -0
- package/docs/native-runtime.md +189 -0
- package/docs/operation_list.md +232 -0
- package/docs/operator_fusion_patterns.md +58 -0
- package/docs/quickstart.md +115 -0
- package/docs/roadmap.md +19 -0
- package/docs/testing.md +97 -0
- package/docs/textbook/01-foundations.md +233 -0
- package/docs/textbook/02-tinystories-language-model.md +300 -0
- package/docs/textbook/03-efficientdet-vision-model.md +281 -0
- package/docs/textbook/04-precision-and-quantization.md +208 -0
- package/docs/textbook/05-inside-the-engine.md +155 -0
- package/docs/textbook/06-native-engine-architecture.md +338 -0
- package/docs/textbook/07-glossary-and-next-steps.md +258 -0
- package/docs/textbook/README.md +85 -0
- package/docs/textbook/ko/01-foundations.md +231 -0
- package/docs/textbook/ko/02-tinystories-language-model.md +300 -0
- package/docs/textbook/ko/03-efficientdet-vision-model.md +277 -0
- package/docs/textbook/ko/04-precision-and-quantization.md +206 -0
- package/docs/textbook/ko/05-inside-the-engine.md +154 -0
- package/docs/textbook/ko/06-native-engine-architecture.md +333 -0
- package/docs/textbook/ko/07-glossary-and-next-steps.md +253 -0
- package/docs/textbook/ko/README.md +83 -0
- package/docs/xnnpack_optimization_guide.md +197 -0
- package/js/CPUEngine.js +241 -0
- package/js/Graph.js +49 -0
- package/js/GraphExecutor.js +1020 -0
- package/js/GraphLoader.js +282 -0
- package/js/ShaderLibrary.js +236 -0
- package/js/Tensor.js +25 -0
- package/js/Tokenizer.js +266 -0
- package/js/VolvoxAI.js +130 -0
- package/js/WasmEngine.js +378 -0
- package/js/WebNNEngine.js +169 -0
- package/js/index.js +11 -0
- package/js/ops/add.js +31 -0
- package/js/ops/argMax.js +33 -0
- package/js/ops/averagePool2D.js +38 -0
- package/js/ops/batchNorm2D.js +28 -0
- package/js/ops/cast.js +19 -0
- package/js/ops/clip.js +15 -0
- package/js/ops/concat2.js +18 -0
- package/js/ops/conv1D.js +35 -0
- package/js/ops/conv2D.js +70 -0
- package/js/ops/convTranspose2D.js +45 -0
- package/js/ops/crossAttention.js +69 -0
- package/js/ops/crossSDPA.js +41 -0
- package/js/ops/dequantizeLinear.js +9 -0
- package/js/ops/div.js +15 -0
- package/js/ops/embedding.js +14 -0
- package/js/ops/expand.js +24 -0
- package/js/ops/gELU.js +9 -0
- package/js/ops/gather.js +51 -0
- package/js/ops/gatherElements.js +33 -0
- package/js/ops/globalAveragePool.js +21 -0
- package/js/ops/hardSigmoid.js +12 -0
- package/js/ops/hardSwish.js +12 -0
- package/js/ops/interp1D.js +25 -0
- package/js/ops/layerNorm.js +25 -0
- package/js/ops/leakyReLU.js +10 -0
- package/js/ops/logSoftmax.js +15 -0
- package/js/ops/matMul.js +35 -0
- package/js/ops/maxPool2D.js +36 -0
- package/js/ops/meanHeight.js +17 -0
- package/js/ops/mul.js +31 -0
- package/js/ops/nonMaxSuppression.js +72 -0
- package/js/ops/pReLU.js +11 -0
- package/js/ops/pad.js +35 -0
- package/js/ops/profileX.js +22 -0
- package/js/ops/profileY.js +22 -0
- package/js/ops/rMSNorm.js +14 -0
- package/js/ops/reLU.js +8 -0
- package/js/ops/reduceMean.js +17 -0
- package/js/ops/reduceSum.js +19 -0
- package/js/ops/reshape.js +6 -0
- package/js/ops/resize.js +44 -0
- package/js/ops/sDPA.js +44 -0
- package/js/ops/siLU.js +8 -0
- package/js/ops/sigmoid.js +6 -0
- package/js/ops/slice.js +36 -0
- package/js/ops/softmax.js +18 -0
- package/js/ops/spatialSoftargmaxY.js +28 -0
- package/js/ops/split.js +24 -0
- package/js/ops/sub.js +11 -0
- package/js/ops/tanh.js +7 -0
- package/js/ops/transpose.js +34 -0
- package/js/ops/upsample2x.js +23 -0
- package/js/ops/where.js +15 -0
- package/package.json +33 -0
- package/shaders/add.wgsl +13 -0
- package/shaders/add3Relu.wgsl +23 -0
- package/shaders/addRelu.wgsl +22 -0
- package/shaders/averagePool2D.wgsl +24 -0
- package/shaders/batchNorm2D.wgsl +21 -0
- package/shaders/binaryBroadcast.wgsl +34 -0
- package/shaders/broadcastBinary.wgsl +26 -0
- package/shaders/clip.wgsl +10 -0
- package/shaders/concat2.wgsl +16 -0
- package/shaders/concatCopy.wgsl +10 -0
- package/shaders/concatSigmoidCopy.wgsl +16 -0
- package/shaders/conv1D.wgsl +37 -0
- package/shaders/conv2D.wgsl +80 -0
- package/shaders/conv2DDepthwise4.wgsl +74 -0
- package/shaders/conv2DDepthwise8.wgsl +66 -0
- package/shaders/conv2DPointwise16.wgsl +67 -0
- package/shaders/conv2DPointwise16Tile.wgsl +86 -0
- package/shaders/conv2DPointwise8.wgsl +85 -0
- package/shaders/conv2DPointwise8Vec2.wgsl +70 -0
- package/shaders/conv2DPointwise8Vec4.wgsl +65 -0
- package/shaders/conv2DRegularC3Out16.wgsl +75 -0
- package/shaders/convTranspose2D.wgsl +33 -0
- package/shaders/copy.wgsl +13 -0
- package/shaders/crossAttention.wgsl +140 -0
- package/shaders/crossAttentionF32.wgsl +98 -0
- package/shaders/crossSDPA.wgsl +74 -0
- package/shaders/dequantizeLinear.wgsl +14 -0
- package/shaders/div.wgsl +34 -0
- package/shaders/elementwise.wgsl +13 -0
- package/shaders/embedding.wgsl +22 -0
- package/shaders/expand.wgsl +18 -0
- package/shaders/gELU.wgsl +13 -0
- package/shaders/gather.wgsl +17 -0
- package/shaders/generalTranspose.wgsl +19 -0
- package/shaders/globalAveragePool.wgsl +19 -0
- package/shaders/hardSigmoid.wgsl +13 -0
- package/shaders/hardSwish.wgsl +13 -0
- package/shaders/interp1D.wgsl +28 -0
- package/shaders/layerNorm.wgsl +33 -0
- package/shaders/leakyReLU.wgsl +11 -0
- package/shaders/linearF32.wgsl +33 -0
- package/shaders/linearF32RowMajor.wgsl +24 -0
- package/shaders/linearInt8.wgsl +42 -0
- package/shaders/logSoftmax.wgsl +22 -0
- package/shaders/maxPool2D.wgsl +37 -0
- package/shaders/meanHeight.wgsl +18 -0
- package/shaders/mul.wgsl +32 -0
- package/shaders/nonMaxSuppression.wgsl +92 -0
- package/shaders/pReLU.wgsl +14 -0
- package/shaders/pad.wgsl +19 -0
- package/shaders/profileX.wgsl +28 -0
- package/shaders/profileY.wgsl +28 -0
- package/shaders/quantizeLinear.wgsl +69 -0
- package/shaders/rMSNorm.wgsl +21 -0
- package/shaders/reLU.wgsl +13 -0
- package/shaders/reduce.wgsl +17 -0
- package/shaders/resize.wgsl +52 -0
- package/shaders/sDPA.wgsl +71 -0
- package/shaders/siLU.wgsl +13 -0
- package/shaders/sigmoid.wgsl +13 -0
- package/shaders/slice.wgsl +26 -0
- package/shaders/softmax.wgsl +23 -0
- package/shaders/spatialSoftargmaxY.wgsl +32 -0
- package/shaders/split.wgsl +15 -0
- package/shaders/sub.wgsl +34 -0
- package/shaders/tanh.wgsl +13 -0
- package/shaders/upsample2x.wgsl +24 -0
- package/shaders/where.wgsl +12 -0
- 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)
|