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,85 @@
|
|
|
1
|
+
# How AI Actually Works — A VolvoxAI Textbook
|
|
2
|
+
|
|
3
|
+
*🌐 Language: **English** · [한국어](ko/README.md)*
|
|
4
|
+
|
|
5
|
+
> A hands-on tour of neural-network **inference**, built around the real code in this
|
|
6
|
+
> repository. We take two working models — a **language model** (TinyStories) and an
|
|
7
|
+
> **object detector** (EfficientDet-Lite0, in fp32 / fp16 / int8) — and follow a single
|
|
8
|
+
> input all the way to an answer, one operation at a time.
|
|
9
|
+
|
|
10
|
+
By the end you will be able to read any modern model as a **graph of small math
|
|
11
|
+
operations**, know what each operation actually computes, and understand the engineering
|
|
12
|
+
choices (memory layout, quantization, GPU vs CPU) that make it run fast on real hardware.
|
|
13
|
+
That is the working knowledge this textbook builds: inference mechanics and runtime
|
|
14
|
+
engineering.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Scope and current gaps
|
|
19
|
+
|
|
20
|
+
This textbook is centered on **inference**: loading trained weights, executing a model
|
|
21
|
+
graph, decoding outputs, quantizing weights, and mapping ops to browser/native backends.
|
|
22
|
+
It does **not** yet cover the broader training and research stack:
|
|
23
|
+
|
|
24
|
+
| Missing area | What is currently absent |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Training | Backpropagation, autograd, loss functions, optimizers such as Adam, learning-rate schedules, initialization, and regularization. |
|
|
27
|
+
| Math foundations | Linear algebra derivations, calculus for chain rule/gradients, probability, and information theory. |
|
|
28
|
+
| Data | Dataset construction, input pipelines, augmentation, tokenizer training, train/validation/test splits, and leakage checks. |
|
|
29
|
+
| Evaluation & experimentation | Metrics, validation methodology, ablations, overfitting, and bias-variance analysis. |
|
|
30
|
+
| Architecture breadth | Diffusion models, GNNs, RNN/LSTM, reinforcement learning, VAE/GAN, retrieval and embeddings, multimodal models, MoE, and SSMs. |
|
|
31
|
+
| Modern LLM training stack | Pretraining, supervised fine-tuning, LoRA/adapters, RLHF/DPO, distributed training, and FlashAttention internals. |
|
|
32
|
+
| Research practice | Reproducing papers, deriving results, reasoning about inductive biases, and scaling laws. |
|
|
33
|
+
|
|
34
|
+
Chapter 7 turns this list into a concrete map of what would need to be added next.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## How to read this book
|
|
39
|
+
|
|
40
|
+
The chapters build on each other. Read them in order the first time.
|
|
41
|
+
|
|
42
|
+
1. **[Foundations](01-foundations.md)** — What inference is. Tensors, graphs, operations.
|
|
43
|
+
The VolvoxAI mental model and its four "tiers." How a model is stored as a *blueprint*.
|
|
44
|
+
2. **[A Language Model, op by op (TinyStories)](02-tinystories-language-model.md)** —
|
|
45
|
+
Follow the sentence *"Once upon a time, Lily"* through a GPT-style transformer until it
|
|
46
|
+
predicts the next word. Tokenize → embed → attention → feed-forward → logits → sample → repeat.
|
|
47
|
+
3. **[A Vision Model, op by op (EfficientDet-Lite0)](03-efficientdet-vision-model.md)** —
|
|
48
|
+
Follow a 320×320 photo through a convolutional detector until it outputs boxes and labels.
|
|
49
|
+
Backbone → feature pyramid → detection heads → decode.
|
|
50
|
+
4. **[Precision & Quantization (fp32 / fp16 / int8)](04-precision-and-quantization.md)** —
|
|
51
|
+
How numbers are stored as bits, why the same detector ships in three sizes, and the exact
|
|
52
|
+
integer math that makes the int8 version 4× smaller.
|
|
53
|
+
5. **[Inside the Engine](05-inside-the-engine.md)** — How VolvoxAI runs a graph in the *browser*:
|
|
54
|
+
the four hardware tiers, the leap from a *naive* kernel to a *fast* one, and operator fusion.
|
|
55
|
+
6. **[The Native Engine](06-native-engine-architecture.md)** — The *other* half: a freestanding C
|
|
56
|
+
binary running the same blueprint on CPU + Vulkan/OpenGL/Metal/NNAPI, with GPU drivers loaded
|
|
57
|
+
at runtime. The overall dual-target architecture and its design choices.
|
|
58
|
+
7. **[Glossary & Next Steps](07-glossary-and-next-steps.md)** — Every term in one place, a
|
|
59
|
+
suggested learning path, exercises that use this repo, and the current gaps beyond inference.
|
|
60
|
+
|
|
61
|
+
> **About the diagrams.** Flowcharts are written in [Mermaid](https://mermaid.js.org/), which
|
|
62
|
+
> renders as an image on GitHub, in VS Code (with the Markdown Preview Mermaid extension), and
|
|
63
|
+
> in most Markdown viewers. Data-layout pictures use plain ASCII so they render everywhere.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## The one-paragraph version
|
|
68
|
+
|
|
69
|
+
A neural network is not magic and it is not a brain. It is a **fixed list of arithmetic
|
|
70
|
+
operations** — mostly multiply-and-add — applied to a big grid of numbers (your input) using
|
|
71
|
+
another big grid of numbers (the **weights**, learned during training). Running the list once,
|
|
72
|
+
from input to output, is called a **forward pass** or **inference**. VolvoxAI is an engine that
|
|
73
|
+
does exactly this: it reads the list of operations (the *graph*), reads the weights, and
|
|
74
|
+
computes the output. Training — the separate, harder process that *discovers* good weights — is
|
|
75
|
+
not in this repo. We study the part that turns a trained model into an answer.
|
|
76
|
+
|
|
77
|
+
```mermaid
|
|
78
|
+
flowchart LR
|
|
79
|
+
A[Input<br/>text or image] --> B[Encode to numbers<br/>tokens / pixels]
|
|
80
|
+
B --> C[Forward pass<br/>graph of math ops + weights]
|
|
81
|
+
C --> D[Raw output<br/>logits / boxes]
|
|
82
|
+
D --> E[Decode to meaning<br/>next word / labeled boxes]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Turn to **[Chapter 1: Foundations](01-foundations.md)** to start.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# 1장 — 기초
|
|
2
|
+
|
|
3
|
+
*목표: 이 장을 마치면 어떤 모델이든 **텐서 위의 연산 그래프(graph of operations on tensors)** 로
|
|
4
|
+
바라볼 수 있고, VolvoxAI가 그것을 실행하기 위해 쓰는 구성 요소들을 이해하게 됩니다.*
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1.1 "AI를 실행한다"는 것의 실제 의미
|
|
9
|
+
|
|
10
|
+
AI 모델을 사용할 때 실제로는 세 가지 일이 일어납니다.
|
|
11
|
+
|
|
12
|
+
1. 당신의 입력(텍스트, 이미지, 오디오)이 **숫자**로 바뀝니다.
|
|
13
|
+
2. 그 숫자들이 긴 **산술 연산(arithmetic operations)** 목록을 통과합니다. 각 연산은
|
|
14
|
+
**가중치(weights)** 라 부르는, 미리 계산된 큰 숫자 표도 함께 사용합니다.
|
|
15
|
+
3. 마지막 숫자들이 다시 의미 있는 것(단어, 박스, 라벨)으로 바뀝니다.
|
|
16
|
+
|
|
17
|
+
2번 단계가 곧 모델입니다. 이것을 한 번 실행하는 것을 **순전파(forward pass)** 또는
|
|
18
|
+
**추론(inference)** 이라고 합니다. 추론은 그게 전부입니다. 누군가가 유용하도록 *학습시킨* 특정
|
|
19
|
+
순서로 배열된, 곱하고 더하는 파이프라인이지요.
|
|
20
|
+
|
|
21
|
+
> **학습(training) vs. 추론(inference).** *학습*은 신경망에 수백만 개의 예제를 보여주고 실수를
|
|
22
|
+
> 줄이도록 가중치를 조금씩 조정하며 좋은 가중치 값을 찾아내는, 비용이 큰 과정입니다. *추론*은 그저
|
|
23
|
+
> 완성된 가중치를 사용합니다. **VolvoxAI는 추론 엔진입니다** — 가중치를 절대 바꾸지 않습니다. 이
|
|
24
|
+
> 책은 전부 추론에 관한 내용입니다.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1.2 텐서: 당신이 알아야 할 유일한 자료구조
|
|
29
|
+
|
|
30
|
+
신경망을 흐르는 모든 숫자는 **텐서(tensor)** 안에 담깁니다. 텐서란 그저 다차원 배열(숫자 격자)에,
|
|
31
|
+
각 차원의 크기를 알려주는 **형태(shape)** 가 붙은 것입니다.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
scalar 3.14 shape [] (숫자 하나)
|
|
35
|
+
vector [3.14, 2.71, 1.62] shape [3] (리스트)
|
|
36
|
+
matrix [[1, 2, 3], shape [2, 3] (표: 2행 3열)
|
|
37
|
+
[4, 5, 6]]
|
|
38
|
+
tensor 224x224 RGB 이미지 8장 shape [8, 224, 224, 3]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
마지막 형태 `[8, 224, 224, 3]` 은 이렇게 읽습니다: **8** 장의 이미지, 각각 높이 **224** 픽셀,
|
|
42
|
+
너비 **224** 픽셀, **3** 개의 색 채널(빨강, 초록, 파랑). 숫자 네 개가 수백만 개의 값을 완전히
|
|
43
|
+
설명합니다.
|
|
44
|
+
|
|
45
|
+
**이 저장소에서** 텐서는 아주 작은 객체(`js/Tensor.js`)입니다 — 이름, 형태, 자료형, 그리고 숫자들의
|
|
46
|
+
평평한(flat) 버퍼로 이루어집니다:
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
// js/Tensor.js (요약)
|
|
50
|
+
class Tensor {
|
|
51
|
+
name; // 예: "hidden_0"
|
|
52
|
+
shape; // 예: [1, 256, 64]
|
|
53
|
+
dtype; // "float32" | "int8" | "int32"
|
|
54
|
+
buffer; // shape의 곱만큼의 숫자를 담은 평평한 Float32Array / Int8Array
|
|
55
|
+
isWeight; // 학습으로 얻은 값이면 true (추론 시 읽기 전용)
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 텐서는 평평하게 저장된다
|
|
60
|
+
|
|
61
|
+
컴퓨터의 메모리는 하나의 긴 바이트 줄일 뿐 — "행"과 "열" 같은 개념을 모릅니다. 형태 `[2, 3]` 인
|
|
62
|
+
텐서는 **한 줄에 6개의 숫자**로 저장되고, 원소 `[row, col]` 이 *어디에* 있는지는 인덱스 계산으로
|
|
63
|
+
알아냅니다:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
논리적 관점 실제 메모리(실제로 존재하는 것)
|
|
67
|
+
[[a, b, c], [ a, b, c, d, e, f ]
|
|
68
|
+
[d, e, f]] 0 1 2 3 4 5
|
|
69
|
+
|
|
70
|
+
원소 [row, col] → flat index = row * 3 + col
|
|
71
|
+
원소 [1, 2] = 'f' → 1 * 3 + 2 = 5 ✓
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
이 정확한 패턴 — `((b * H + y) * W + x) * C + c` — 을 커널 곳곳에서 보게 됩니다. 4차원 이미지
|
|
75
|
+
텐서를 1차원 배열 안에서 주소 지정하는 방식이지요. 형태를 외우면 인덱스 계산은 자연히 따라옵니다.
|
|
76
|
+
|
|
77
|
+
> **NHWC vs NCHW.** 차원의 *순서* 가 중요합니다. VolvoxAI의 비전 모델은 **NHWC**(배치, 높이, 너비,
|
|
78
|
+
> 채널)를 씁니다 — 한 픽셀의 색 채널들이 메모리에서 서로 붙어 있습니다. PyTorch는 보통 **NCHW**를
|
|
79
|
+
> 씁니다. 같은 데이터, 다른 메모리 배치일 뿐이며, 커널들은 어느 쪽을 읽는지 서로 합의해야 합니다.
|
|
80
|
+
> (이 저장소의 변환기는 모든 비전 `config.json` 에 `"internal_layout": "NHWC"` 를 기록합니다.)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 1.3 연산: 작고 잘 정의된 하나의 일
|
|
85
|
+
|
|
86
|
+
**연산(operation, 줄여서 op, 또는 layer)** 은 입력 텐서 하나 이상을 받아, 정해진 수학 계산을 하고,
|
|
87
|
+
출력 텐서 하나 이상을 씁니다. 앞으로 만날 예시들:
|
|
88
|
+
|
|
89
|
+
| 연산 | 한마디로 | 사용처 |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `MatMul` | 행렬 곱 — 특징을 "섞는" 핵심 | 두 모델 모두 |
|
|
92
|
+
| `Conv2D` | 작은 필터를 이미지 위로 슬라이드 | 탐지기 |
|
|
93
|
+
| `Add` | 두 텐서를 원소별로 더하기 | 두 모델 모두 |
|
|
94
|
+
| `LayerNorm` | 벡터를 잘 다뤄지도록 재중심화·재스케일 | 언어 모델(LM) |
|
|
95
|
+
| `SDPA` | 스케일드 닷-프로덕트 어텐션 — "어떤 단어가 어떤 단어를 보는가" | LM |
|
|
96
|
+
| `GELU` / `ReLU` | 비선형 압축 함수 | 두 모델 모두 |
|
|
97
|
+
| `MaxPool2D` | 각 패치에서 가장 큰 값만 남겨 이미지 축소 | 탐지기 |
|
|
98
|
+
|
|
99
|
+
VolvoxAI의 모든 연산은 `js/ops/` 에 쉬운 말로 된 **참조 구현(reference implementation)** 을
|
|
100
|
+
가집니다. 파일 하나에 하나씩이지요. 다음은 `Add` 연산의 *전부* 입니다 — 단순화한 것이 아니라 실제
|
|
101
|
+
코드입니다:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
// js/ops/add.js — 브로드캐스팅을 포함한 원소별 덧셈
|
|
105
|
+
for (let i = 0; i < out.length; i++) {
|
|
106
|
+
out[i] = a[i] + b[i % b.length]; // b.length가 더 작을 수 있음 ("브로드캐스트")
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
이것이 비밀의 전부입니다. 모델이란 이런 연산 수천 개가, 각각은 사소하지만, 사슬처럼 이어진
|
|
111
|
+
것입니다. **설명 불가능한 무언가가 일어나는 단계는 없습니다.**
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 1.4 그래프: 연산들을 서로 연결하기
|
|
116
|
+
|
|
117
|
+
모델은 **그래프(graph)** 입니다 — 각 연산의 출력이 뒤의 연산의 입력이 되는 연산 목록이지요.
|
|
118
|
+
VolvoxAI는 이것을 `Graph` 객체(`js/Graph.js`)로 저장합니다: **텐서**들의 집합과 **노드(node)**
|
|
119
|
+
목록(연산 + 그 입력/출력 텐서 + 파라미터).
|
|
120
|
+
|
|
121
|
+
모든 연산이 자신의 입력과 출력을 *이름으로* 선언하기 때문에, 그래프는 단순한 장부 정리일 뿐입니다:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
tokens ─┐
|
|
125
|
+
├─▶ [Embedding] ─▶ emb_tok ─┐
|
|
126
|
+
wte ───┘ ├─▶ [Add] ─▶ hidden_0 ─▶ [LayerNorm] ─▶ ...
|
|
127
|
+
positions ─▶ [Embedding] ─▶ emb_pos ┘
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
그래프를 **실행**하려면, VolvoxAI는 그저 노드 목록을 위에서 아래로 훑으며 각 연산을 실행합니다.
|
|
131
|
+
실행기(executor) 루프 전체가 이만큼 읽기 쉽습니다(`js/CPUEngine.js`):
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
// js/CPUEngine.js — 엔진의 심장
|
|
135
|
+
for (const node of graph.nodes) {
|
|
136
|
+
this._runNode(node); // node.opType로 분기 → 알맞은 커널 호출
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`_runNode` 는 연산 종류에 대한 커다란 `switch` 입니다(`"MatMul"` → matmul 커널, `"Conv2D"` → conv
|
|
141
|
+
커널, …). 그게 전부입니다. **신경망 엔진이란 함수 호출 목록을 도는 `for` 루프입니다.** 나머지는 그
|
|
142
|
+
함수들을 빠르게(5장) 그리고 수치적으로 작게(4장) 만드는 일일 뿐입니다.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 1.5 설계도: 모델이 저장되는 방식
|
|
147
|
+
|
|
148
|
+
디스크 위의 VolvoxAI 모델은 두 개의 파일입니다:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
model/
|
|
152
|
+
config.json # 그래프: 연산 노드 목록 (토폴로지 + 파라미터 + 형태)
|
|
153
|
+
model.safetensors # 가중치: 학습된 숫자들, 표준 바이너리 형식
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- **`config.json`** 이 곧 *설계도(blueprint)* 입니다: 순서가 있는 노드 목록이지요. 각 노드는 자신의
|
|
157
|
+
연산, 입력/출력 텐서, 파라미터, 그리고 정확한 출력 형태(엔진이 절대 추측하지 않도록 익스포터가 미리
|
|
158
|
+
계산해 둠)를 이름 붙입니다. 다음은 TinyStories의 실제 노드 하나입니다:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"op": "LayerNorm",
|
|
163
|
+
"inputs": { "input": "hidden_0", "weight": "h.0.ln_1.weight", "bias": "h.0.ln_1.bias" },
|
|
164
|
+
"outputs": { "out": "ln1_0" },
|
|
165
|
+
"outputs_shape": { "out": [1, 256, 64] },
|
|
166
|
+
"params": { "eps": 1e-05, "d_model": 64 }
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- **`model.safetensors`** 는 원시 가중치 텐서(`h.0.ln_1.weight`, `wte.weight`, …)를
|
|
171
|
+
[safetensors](https://github.com/huggingface/safetensors) 형식으로 담습니다 — ML 세계 전반에서
|
|
172
|
+
쓰이는 단순하고 안전한 표준 레이아웃입니다.
|
|
173
|
+
|
|
174
|
+
`js/GraphLoader.js` 가 둘 다 읽어 `Graph` 를 만들고 엔진에 넘깁니다. **당신은 PyTorch에서 학습하고,
|
|
175
|
+
이 설계도로 익스포트하며, Volvox가 그것을 실행합니다** — 추론 시점에 PyTorch도, ONNX Runtime도,
|
|
176
|
+
어떤 의존성도 없이 말이지요.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 1.6 하나의 모델, 실행하는 네 가지 방법 ("계층")
|
|
181
|
+
|
|
182
|
+
같은 그래프를 아주 다른 하드웨어에서 실행할 수 있습니다. VolvoxAI는 사용 가능한 가장 좋은
|
|
183
|
+
**계층(tier)** 을 자동으로 고르며, 모든 계층은 *같은* 결과를 계산합니다:
|
|
184
|
+
|
|
185
|
+
```mermaid
|
|
186
|
+
flowchart TD
|
|
187
|
+
G["그래프 + 가중치"] --> SEL{"VolvoxAI.init<br/>가장 좋은 것을 선택"}
|
|
188
|
+
SEL -->|브라우저 NPU/GPU| T1["Tier 1 · WebNN"]
|
|
189
|
+
SEL -->|브라우저 GPU| T2["Tier 2 · WebGPU<br/>WGSL 컴퓨트 셰이더"]
|
|
190
|
+
SEL -->|모든 CPU, 빠름| T3["Tier 3 · WASM SIMD<br/>컴파일된 C 커널"]
|
|
191
|
+
SEL -->|모든 CPU, 항상 동작| T4["Tier 4 · 순수 JS<br/>참조 커널"]
|
|
192
|
+
N["네이티브 바이너리 · C<br/>Vulkan/OpenGL/Metal/CPU"] -.같은 설계도.-> G
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- **Tier 4 (순수 JS, `js/ops/*.js`)** 는 *참조(reference)* 입니다: 느리지만 명백히 올바르며, 다른 모든
|
|
196
|
+
계층이 대조하는 기준(ground truth)입니다. **가장 읽기 쉬워서 이 책의 교재로 사용합니다.**
|
|
197
|
+
- **Tier 3 (WASM)** 은 같은 수학을 컴파일된 C로 실행해 크게 빨라집니다.
|
|
198
|
+
- **Tier 2 (WebGPU)** 는 각 연산을 GPU 컴퓨트 셰이더(`shaders/*.wgsl`)로 다시 표현합니다.
|
|
199
|
+
- **Tier 1 (WebNN)** 은 그래프를 브라우저 자체의 신경망 API에 넘깁니다(NPU에 도달할 수 있음).
|
|
200
|
+
- **네이티브**(`native/`)는 데스크톱이나 폰에서 *같은* 설계도를 실행하는 독립 C 프로그램이며, 선택적으로
|
|
201
|
+
Vulkan/OpenGL/Metal 위에서 돕니다.
|
|
202
|
+
|
|
203
|
+
이 책의 나머지 부분에서 "연산을 따라간다"고 할 때는, *수학이 무엇인지* 를 가장 직접적으로 말해주는
|
|
204
|
+
순수 JS 또는 이식성 있는 C 버전을 읽습니다.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 1.7 멘탈 모델, 조립하기
|
|
209
|
+
|
|
210
|
+
이 모두를 합치면 엔진 전체가 그림 하나에 담깁니다:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
입력 ──인코딩──▶ 텐서들 ──┐
|
|
214
|
+
│ graph.nodes의 각 노드마다:
|
|
215
|
+
가중치 (.safetensors에서)─┼──▶ op(inputs, params) → 출력 텐서
|
|
216
|
+
│
|
|
217
|
+
(~85개 또는 ~262개 노드 전부 반복)
|
|
218
|
+
│
|
|
219
|
+
▼
|
|
220
|
+
출력 텐서 ──디코딩──▶ 답
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
어떤 모델이든 두 가지 질문이 정의합니다:
|
|
224
|
+
|
|
225
|
+
1. **연산은 무엇이며, 어떤 순서인가?** (그래프 / `config.json`)
|
|
226
|
+
2. **가중치는 각 연산이 무엇을 하게 만드는가?** (`.safetensors`)
|
|
227
|
+
|
|
228
|
+
다음 두 장에서 실제 모델 두 개에 대해 이 두 질문에 답합니다 — 그리고 "언어 모델"과 "이미지 탐지기"가
|
|
229
|
+
목록 속 연산만 다를 뿐 *같은 아이디어* 임을 보게 될 것입니다.
|
|
230
|
+
|
|
231
|
+
**다음:** [2장 — 언어 모델 한 연산씩 →](02-tinystories-language-model.md)
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# 2장 — 언어 모델 한 연산씩 (TinyStories)
|
|
2
|
+
|
|
3
|
+
*목표: 단어 **"Once upon a time, Lily"** 가 실제 GPT 스타일 트랜스포머를 통과해 다음 단어를 예측하는
|
|
4
|
+
과정을 따라갑니다. 여기 나오는 모든 연산은 `js/ops/` 의 작은 커널 중 하나입니다.*
|
|
5
|
+
|
|
6
|
+
이 모델은 `models/tinystories_1m/` 에 있습니다. 쉬운 어린이 이야기 데이터셋인
|
|
7
|
+
[TinyStories](https://arxiv.org/abs/2305.07759)로 학습한 아주 작은 GPT(**디코더 전용
|
|
8
|
+
트랜스포머, decoder-only transformer**)입니다. "작다"는 말은 진짜입니다: 내부 벡터 폭이 **64**,
|
|
9
|
+
층(layer)이 **8** 개인데도 앞뒤가 맞는 짧은 이야기를 씁니다. 이것을 공부하면 GPT-2/3/4, LLaMA,
|
|
10
|
+
Mistral 뒤에 있는 *바로 그* 아키텍처를 배우게 됩니다 — 그것들은 이 그래프를 키운 것일 뿐입니다.
|
|
11
|
+
|
|
12
|
+
## 2.1 모델의 차원 (설계도에서 읽어내기)
|
|
13
|
+
|
|
14
|
+
`config.json` 에서 하나씩:
|
|
15
|
+
|
|
16
|
+
| 기호 | 값 | 의미 |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `d_model` | **64** | 토큰마다 실려 다니는 "생각 벡터"의 폭. |
|
|
19
|
+
| `n_layers` | **8** | 쌓인 트랜스포머 블록 수. |
|
|
20
|
+
| `n_heads` | **16** | 블록당 어텐션 헤드 수 (따라서 `head_dim = 64/16 = 4`). |
|
|
21
|
+
| `d_mlp` | **256** | 피드포워드 은닉층의 폭 (`4 × d_model`). |
|
|
22
|
+
| `vocab` | **50257** | 아는 서로 다른 토큰의 수 (GPT-2 어휘). |
|
|
23
|
+
| `context` | **256** | 한 번에 볼 수 있는 최대 토큰 수. |
|
|
24
|
+
|
|
25
|
+
> 폴더 이름은 `tinystories_1m`(트랜스포머 파라미터 약 100만 개)이지만 `model.safetensors` 는 약
|
|
26
|
+
> 27 MB입니다. 왜일까요? **토큰 임베딩 표**가 `50257 × 64 ≈ 320만` 개의 숫자이고, 그런 표가 두
|
|
27
|
+
> 개(입력 + 출력)이기 때문입니다. 작은 LM에서는 *층* 이 아니라 *어휘* 가 파일 크기를 지배합니다.
|
|
28
|
+
> 진짜 교훈이지요: 모델 크기 ≠ 모델 깊이.
|
|
29
|
+
|
|
30
|
+
전체 그래프는 **85개 노드**입니다. 그 내역:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
33 MatMul 17 LayerNorm 17 Add 8 SDPA 8 GELU 2 Embedding
|
|
34
|
+
= 임베딩 2개 + 8블록 × (LayerNorm 2 + MatMul 4 + SDPA 1 + GELU 1 + Add 2) + 마지막 LayerNorm + lm_head 1
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 2.2 파이프라인 한눈에 보기
|
|
38
|
+
|
|
39
|
+
```mermaid
|
|
40
|
+
flowchart TD
|
|
41
|
+
P["프롬프트: 'Once upon a time, Lily'"] --> TOK["토크나이저 BPE<br/>텍스트 → 토큰 id"]
|
|
42
|
+
TOK --> EMB
|
|
43
|
+
subgraph EMB["입력 임베딩"]
|
|
44
|
+
T["토큰 id"] --> WTE["Embedding · wte"]
|
|
45
|
+
POSN["위치 0,1,2…"] --> WPE["Embedding · wpe"]
|
|
46
|
+
WTE --> ADD0["Add"]
|
|
47
|
+
WPE --> ADD0
|
|
48
|
+
end
|
|
49
|
+
ADD0 --> BLK
|
|
50
|
+
subgraph BLK["× 8 트랜스포머 블록"]
|
|
51
|
+
direction TB
|
|
52
|
+
L1["LayerNorm"] --> QKV["MatMul: qkv_proj"] --> SDPA["SDPA · 인과적"] --> OP["MatMul: out_proj"] --> R1(("+ 잔차"))
|
|
53
|
+
R1 --> L2["LayerNorm"] --> FC["MatMul: c_fc"] --> G["GELU"] --> PR["MatMul: c_proj"] --> R2(("+ 잔차"))
|
|
54
|
+
end
|
|
55
|
+
BLK --> LNF["LayerNorm · 최종"] --> HEAD["MatMul: lm_head"] --> LOG["로짓: 50257개 점수"]
|
|
56
|
+
LOG --> ARGMAX["가장 높은 점수 선택"] --> NEXT["다음 토큰"] --> DETOK["디코드 → 텍스트"]
|
|
57
|
+
NEXT -. 이어 붙이고 반복 .-> TOK
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
이제 실제 커널과 함께 단계별로 따라가 봅시다.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 2.3 0단계 — 토큰화: 텍스트가 정수가 되다
|
|
65
|
+
|
|
66
|
+
신경망은 글자를 읽지 못합니다. 숫자를 읽지요. **토크나이저(tokenizer)**(`js/Tokenizer.js`,
|
|
67
|
+
`native/tokenizer.c`)는 텍스트를 **토큰(token)**(자주 쓰이는 단어 조각)으로 쪼개고, 어휘 +
|
|
68
|
+
**병합 규칙(merge rules)** 목록을 써서 각각을 정수 ID로 매핑합니다(이것이 **바이트 페어
|
|
69
|
+
인코딩(Byte-Pair Encoding, BPE)** 입니다).
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
"Once upon a time, Lily"
|
|
73
|
+
│ 정규식으로 단어 비슷한 덩어리로 나눈 뒤, BPE가 자주 나오는 바이트 쌍을 병합
|
|
74
|
+
▼
|
|
75
|
+
[ "Once", " upon", " a", " time", ",", " Lily" ] (설명용 예시)
|
|
76
|
+
│ 각 덩어리 → 50257개 어휘 중 하나의 정수 id
|
|
77
|
+
▼
|
|
78
|
+
tokens = [7454, 2402, 257, 640, 11, 20037, …] ← 모델의 실제 입력
|
|
79
|
+
positions = [ 0, 1, 2, 3, 4, 5, …] ← "나는 몇 번째 자리인가?"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
정수 텐서 두 개가 그래프에 들어갑니다: **`tokens`**(단어가 무엇인지)와 **`positions`**(그 순서,
|
|
83
|
+
`0,1,2,…`). 둘 다 형태 `[1, 256]` 입니다 — 시퀀스는 256 토큰 컨텍스트 창에 맞춰 패딩됩니다.
|
|
84
|
+
|
|
85
|
+
> **왜 위치가 필요할까?** 아래의 어텐션 수학은 그 자체로는 순서를 모릅니다 — "개가 사람을 문다"와
|
|
86
|
+
> "사람이 개를 문다"를 똑같이 취급하지요. 명시적 위치 번호를 넣어주면 모델이 단어 순서를 배울 수
|
|
87
|
+
> 있습니다.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 2.4 1단계 — 임베딩: 정수가 벡터가 되다
|
|
92
|
+
|
|
93
|
+
`20037`("Lily") 같은 ID는 *숫자로서는* 의미가 없습니다(무언가의 20037배가 아니지요). 우리는 이것을
|
|
94
|
+
의미를 담은, 학습된 64개 숫자의 **벡터** — 그 토큰의 **임베딩(embedding)** — 으로 대체합니다.
|
|
95
|
+
`Embedding` 연산은 순수한 표 조회입니다. 다음이 커널 전부입니다(`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]; // `token_id` 행을 복사
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
가중치 `wte.weight` 는 `[50257, 64]` 표이고, `token_id` 행이 *곧* 그 토큰의 의미 벡터입니다. 이
|
|
107
|
+
연산은 **두 번** 실행됩니다:
|
|
108
|
+
|
|
109
|
+
- `Embedding(tokens, wte)` → `emb_tok` `[1,256,64]` — 각 토큰이 *무엇* 인지.
|
|
110
|
+
- `Embedding(positions, wpe)` → `emb_pos` `[1,256,64]` — 그것이 *어디* 에 있는지.
|
|
111
|
+
|
|
112
|
+
그다음 `Add` 가 둘을 합칩니다: `hidden_0 = emb_tok + emb_pos`. 이제 256개 자리 각각이 *단어 정체성*
|
|
113
|
+
과 *위치* 를 섞은 64개 숫자 벡터를 담습니다. 이 텐서 `hidden_0 [1,256,64]` 가 곧 **잔차
|
|
114
|
+
스트림(residual stream)** 입니다 — 모든 블록이 읽고 다시 쓰는 "컨베이어 벨트"이지요.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
hidden_0: 256개 행 (토큰 위치마다 하나), 각각 64개 숫자 벡터
|
|
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 2단계 — 트랜스포머 블록 (이것이 8× 반복된다)
|
|
128
|
+
|
|
129
|
+
각 블록은 두 개의 하위 단계로 잔차 스트림을 다듬습니다: **어텐션(attention)**(토큰들이 정보를
|
|
130
|
+
공유)과 **피드포워드 MLP**(각 토큰이 혼자 생각). 둘 다 깊은 신경망을 학습 가능하게 만드는
|
|
131
|
+
**사전 정규화(pre-norm) + 잔차(residual)** 패턴으로 감싸여 있습니다.
|
|
132
|
+
|
|
133
|
+
### 2.5a LayerNorm — 숫자를 정상 범위로 유지
|
|
134
|
+
|
|
135
|
+
각 하위 단계 전에 `LayerNorm` 은 각 토큰의 64-벡터를 평균 0, 분산 1로 재스케일한 뒤, 학습된 스케일
|
|
136
|
+
(`weight`)과 이동(`bias`)을 적용합니다. 이것이 8개 층에 걸쳐 값이 폭발하거나 소멸하는 것을
|
|
137
|
+
막습니다. 실제 커널(`js/ops/layerNorm.js`), 토큰 행마다:
|
|
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는 0으로 나누기 방지
|
|
143
|
+
out[j] = (in[j] - mean) * inv_std * weight[j] + bias[j]; // 정규화 후 재스케일/이동
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 2.5b 어텐션 — "앞의 어떤 단어가 나에게 중요한가?"
|
|
147
|
+
|
|
148
|
+
이것이 트랜스포머의 심장입니다. 먼저 하나의 `MatMul` 이 각 64-벡터를 **192** 개 숫자로 투영합니다
|
|
149
|
+
(`qkv_proj`, 형태 `[1,256,192]`). 이 192개는 세 개의 64-벡터를 이어 붙인 것입니다: **쿼리(Query)**,
|
|
150
|
+
**키(Key)**, **밸류(Value)** (Q, K, V). 직관:
|
|
151
|
+
|
|
152
|
+
- **Query** = "나는 무엇을 찾고 있는가?"
|
|
153
|
+
- **Key** = "나는 무엇을 제공하는가?"
|
|
154
|
+
- **Value** = "네가 나를 고르면 넘겨줄 내용."
|
|
155
|
+
|
|
156
|
+
그다음 `SDPA`(**Scaled Dot-Product Attention**)가 실제로 "보는" 일을 합니다. 각 토큰 *q* 에 대해,
|
|
157
|
+
자신의 Query를 앞선 모든 토큰의 Key와 비교하고(내적 = 유사도), 그 유사도를 **소프트맥스(softmax)**
|
|
158
|
+
로 가중치로 바꾼 뒤, 그 토큰들의 Value를 가중 혼합해서 돌려줍니다. 실제 인과적(causal) 커널
|
|
159
|
+
(`js/ops/sDPA.js`)에 가볍게 주석을 달면:
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
for (let h = 0; h < num_heads; h++) { // 16개의 독립적인 헤드
|
|
163
|
+
for (let q = 0; q < seq_len; q++) { // 각 쿼리 위치마다
|
|
164
|
+
for (let k = 0; k <= q; k++) { // ← k ≤ q 만 봄 (인과적)
|
|
165
|
+
score = dot(Q[q,h], K[k,h]) * scale; // q와 k의 유사도
|
|
166
|
+
}
|
|
167
|
+
softmax(scores); // 점수를 합이 1인 가중치로 변환
|
|
168
|
+
out[q,h] = Σ_k weight[k] * V[k,h]; // 혼합된 값
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
잠시 짚어볼 두 가지 아이디어:
|
|
174
|
+
|
|
175
|
+
- **인과적 마스킹(causal masking)** (`k <= q`): 토큰은 자신과 *앞선* 토큰만 볼 수 있고 미래 토큰은
|
|
176
|
+
절대 못 봅니다. 이것이 왼쪽에서 오른쪽으로 가는 텍스트 *생성기* 를 만듭니다 — 위치 3은 위치 4를
|
|
177
|
+
훔쳐볼 수 없습니다.
|
|
178
|
+
- **멀티 헤드(multi-head)** (`h`): 64개 차원을 4개씩 16개 그룹으로 나눕니다. 각 "헤드"는 서로 다른
|
|
179
|
+
종류의 관계(예: 하나는 주어를, 다른 하나는 문장부호를 추적)를 병렬로 학습합니다.
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
토큰 " Lily" 의 어텐션 (softmax 후의 설명용 가중치):
|
|
183
|
+
|
|
184
|
+
" Lily" 가 주목하는 대상 → "Once" " upon" " a" " time" "," " Lily"
|
|
185
|
+
가중치 0.05 0.05 0.05 0.30 0.05 0.50
|
|
186
|
+
▲ ▲
|
|
187
|
+
"time"이 관련 있음 대부분 자기 자신
|
|
188
|
+
output = 0.05·V(Once) + … + 0.30·V(time) + 0.50·V(Lily)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
마지막 `MatMul`(`out_proj`, 편향 포함)이 16개 헤드의 출력을 다시 64-벡터로 섞고, **잔차 `Add`** 가
|
|
192
|
+
그것을 스트림에 더합니다: `add1 = hidden + attention_output`. "잔차"란 스트림을 교체하는 대신 블록의
|
|
193
|
+
결과를 *더한다* 는 뜻입니다 — 그래서 정보가 절대 손실되지 않고 학습 중 기울기(gradient)가 잘
|
|
194
|
+
흐릅니다.
|
|
195
|
+
|
|
196
|
+
### 2.5c 피드포워드 MLP — 각 토큰이 생각한다
|
|
197
|
+
|
|
198
|
+
토큰들이 정보를 공유한 뒤, 각 토큰은 2층 MLP로 혼자서 변환됩니다:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
c_fc : MatMul 64 → 256 (+편향) "확장: 계산할 공간을 준다"
|
|
202
|
+
GELU : 비선형성 "비선형적 결정을 내리게 한다"
|
|
203
|
+
c_proj: MatMul 256 → 64 (+편향) "스트림 폭으로 다시 압축"
|
|
204
|
+
Add : 잔차 hidden_next = add1 + mlp_output
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`GELU`(`js/ops/gELU.js`)가 비선형성입니다 — 작은 음수는 조금 새어 나가게 하고 양수는 통과시키는
|
|
208
|
+
부드러운 게이트지요. 이런 비선형성이 없다면 MatMul을 쌓아도 하나의 MatMul로 붕괴되어, 신경망은 직선
|
|
209
|
+
관계밖에 배우지 못합니다:
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
out[i] = 0.5 * x * (1 + tanh(0.7978845608 * (x + 0.044715 * x*x*x))); // GELU 곡선
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
블록의 출력 `hidden_next [1,256,64]` 는 입력과 형태가 같습니다 — 바로 이 덕분에 **8** 개를 쌓을 수
|
|
216
|
+
있습니다. 각 블록은 스트림을 읽고 조금 더 똑똑해진 버전을 되돌려 씁니다.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 2.6 3단계 — 헤드: 벡터가 단어 점수가 되다
|
|
221
|
+
|
|
222
|
+
8번째 블록 뒤, 마지막 `LayerNorm`(`ln_f`)이 스트림을 정리합니다. 그다음 **언어 모델 헤드(language-model
|
|
223
|
+
head)** — `lm_head.weight [50257, 64]` 로의 단일 `MatMul` — 가 각 64-벡터를 어휘 단어마다 하나씩,
|
|
224
|
+
**50257개 점수**로 바꿉니다:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
final_norm [1,256,64] ──MatMul lm_head──▶ logits [1,256,50257]
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
이 원시 점수를 **로짓(logits)** 이라 부릅니다. `logits[0, p, w]` = "모델이 `0..p` 토큰을 읽은 상태에서
|
|
231
|
+
단어 `w` 가 다음에 올 것이라고 얼마나 강하게 기대하는가." 우리는 **마지막 실제 토큰** 의 행만 신경
|
|
232
|
+
씁니다 — 그것이 프롬프트 다음에 올 것에 대한 예측이지요.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 2.7 4단계 — 샘플링: 점수가 다음 단어가 되다
|
|
237
|
+
|
|
238
|
+
이제 다음 토큰에 대한 50257개 점수가 있습니다. 이 저장소의 생성기(`native/main.c`,
|
|
239
|
+
`command_generate`)는 가장 단순한 규칙인 **탐욕적(greedy) / argmax** 를 씁니다 — 그냥 가장 높은 것을
|
|
240
|
+
택합니다:
|
|
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가 다음 토큰; 텍스트로 다시 디코드:
|
|
247
|
+
printf("%s", tokenizer_decode(tok, best_id));
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
> **실제 생성기는 무작위성을 더합니다** — *온도(temperature)*(점수를 평평하게/날카롭게), *top-k* /
|
|
251
|
+
> *top-p*(가장 그럴듯한 몇 개에서만 샘플링). 이것들은 `softmax` 로 로짓을 확률 분포로 바꾼 뒤 가중된
|
|
252
|
+
> 주사위를 굴립니다. ChatGPT가 매번 다른 답을 주는 이유지요. 탐욕적 방식은 결정적(deterministic)인
|
|
253
|
+
> 특수 경우이며, 교과서에는 안성맞춤입니다.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 2.8 반복 — 한 번에 한 단어 (자기회귀, autoregression)
|
|
258
|
+
|
|
259
|
+
트랜스포머는 순전파 한 번에 **하나** 의 토큰을 예측합니다. 문장을 쓰려면 새 토큰을 이어 붙이고 다시
|
|
260
|
+
실행합니다. 이것이 **자기회귀 생성(autoregressive generation)** 입니다:
|
|
261
|
+
|
|
262
|
+
```mermaid
|
|
263
|
+
flowchart LR
|
|
264
|
+
A["지금까지의 토큰"] --> B["순전파<br/>85개 연산"] --> C["로짓"] --> D["argmax → 다음 토큰"]
|
|
265
|
+
D --> E{"멈출까?<br/>최대 길이 또는 EOS"}
|
|
266
|
+
E -- 아니오 --> A
|
|
267
|
+
E -- 예 --> F["완료: 전체 이야기"]
|
|
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
|
+
ChatGPT가 한 단어씩 타이핑하는 방식이 문자 그대로 이것입니다: 이 반복을 돌리며, 새 토큰마다 다시
|
|
278
|
+
입력으로 넣는 것이지요.
|
|
279
|
+
|
|
280
|
+
> **KV-캐시 (앞으로 듣게 될 최적화).** 순진하게 하면 *N* 번째 단계가 매번 *N* 개 토큰 전체에 대한
|
|
281
|
+
> 어텐션을 처음부터 다시 계산합니다 — 낭비지요. 실전 엔진은 각 토큰의 Key와 Value를 *캐시* 해서 각
|
|
282
|
+
> 단계가 새 토큰의 것만 계산하게 합니다. VolvoxAI의 네이티브 러너는 이 분할을
|
|
283
|
+
> `engine_prefill()`(프롬프트 전체를 한 번 처리)과 `engine_decode()`(한 번에 새 토큰 하나)로
|
|
284
|
+
> 노출합니다. 수학은 동일하고, 캐시는 그저 반복 작업을 피할 뿐입니다.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## 2.9 방금 배운 것
|
|
289
|
+
|
|
290
|
+
- 언어 모델이란: **토큰화 → 임베딩 → (LayerNorm, 어텐션, MLP) × N → 헤드 → 샘플링 → 반복.** 그
|
|
291
|
+
이상은 없습니다.
|
|
292
|
+
- **어텐션**은 토큰들이 정보를 공유하게 하고("앞의 어떤 단어가 나에게 중요한가?"), **MLP**는 각
|
|
293
|
+
토큰이 혼자 계산하게 하며, **잔차 + LayerNorm**은 이 층 쌓기를 학습 가능하고 깊게 만듭니다.
|
|
294
|
+
- 모든 연산은 `js/ops/` 의 작은 커널 하나입니다 — `MatMul`, `SDPA`, `LayerNorm`, `GELU`, `Add`,
|
|
295
|
+
`Embedding`. GPT-2/3/4와 LLaMA는 **바로 이 그래프를 더 넓고 깊게** 한 것입니다.
|
|
296
|
+
|
|
297
|
+
다음 장에서는 영역을 완전히 바꿉니다 — 텍스트에서 픽셀로 — 그리고 *같은 골격*(텐서 위 작은 연산의
|
|
298
|
+
그래프)이 전혀 다른 문제를 푸는 것을 보게 됩니다.
|
|
299
|
+
|
|
300
|
+
**다음:** [3장 — 비전 모델 한 연산씩 →](03-efficientdet-vision-model.md)
|