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,277 @@
1
+ # 3장 — 비전 모델 한 연산씩 (EfficientDet-Lite0)
2
+
3
+ *목표: **320×320 사진** 이 객체 탐지기를 통과해 라벨링된 박스("(x0,y0,x1,y1)에 개")를 내놓기까지를
4
+ 따라갑니다. 2장과는 다른 연산들이지만 — 아이디어는 같습니다: 텐서 위 작은 커널들의 그래프.*
5
+
6
+ 이 모델은 `models/efficientdet_lite0_*/` 에 있습니다. **EfficientDet-Lite0** 는 소형
7
+ [객체 탐지기](https://arxiv.org/abs/1911.09070)입니다: 이미지가 주어지면 *무엇* 이 있고 *어디* 에
8
+ 있는지 찾아냅니다. 여기서는 세 가지 수치 정밀도 — **fp32, fp16, int8** — 로 배포되며, 같은 것을
9
+ 서로 다른 크기/속도 절충으로 계산합니다. 이 장은 **fp32** 버전(`Conv2D` 라는 연산)을 따라가고,
10
+ int8이 `QConv2D` 로 어떻게 바뀌는지는 4장에서 설명합니다.
11
+
12
+ ## 3.1 모델이 받아들이고 내놓는 것
13
+
14
+ ```
15
+ 입력 input0 : shape [1, 320, 320, 3] 320×320 RGB 이미지 한 장 (NHWC)
16
+ (int8 변형은 uint8 픽셀 0–255를 받고, fp32는 정규화된 float를 받음)
17
+
18
+ 출력 scores : shape [1, 19206, 90] 19206개 후보 박스 각각에 대해 90개 클래스 점수
19
+ boxes : shape [1, 19206, 4] 후보 박스마다 4개 좌표
20
+ ```
21
+
22
+ 신경망은 이미지를 여러 위치와 크기로 덮는 **19,206개의 후보 박스**를 제안하고, 각각을 **90개 객체
23
+ 클래스**(COCO 라벨 집합 — 사람, 자동차, 개, …)에 대해 채점하며, 당신은 확신도가 높고 겹치지 않는
24
+ 소수를 남깁니다. 19,206은 어디서 나올까요? 해상도가 점점 작아지는 다섯 개 탐지 격자, 셀마다 9개의
25
+ 후보 박스("앵커, anchor"):
26
+
27
+ ```
28
+ 격자 40×40 × 9 = 14400 (작은 객체를 찾음)
29
+ 격자 20×20 × 9 = 3600
30
+ 격자 10×10 × 9 = 900
31
+ 격자 5× 5 × 9 = 225
32
+ 격자 3× 3 × 9 = 81 (큰 객체를 찾음)
33
+ ─────
34
+ 합 19206
35
+ ```
36
+
37
+ 전체 그래프는 **262개 노드**(int8은 265개)입니다. 내역:
38
+
39
+ ```
40
+ 182 Conv2D 42 Add 14 MaxPool2D 12 ResizeNearest2D 10 Reshape 2 Concat
41
+ └─ conv 중: 102개는 "포인트와이즈/일반", 80개는 "뎁스와이즈" (§3.3 참고)
42
+ ```
43
+
44
+ ## 3.2 파이프라인 한눈에 보기
45
+
46
+ ```mermaid
47
+ flowchart TD
48
+ IMG["이미지 [1,320,320,3]"] --> BB
49
+ subgraph BB["1 · 백본 · EfficientNet-Lite0"]
50
+ direction TB
51
+ STEM["stem Conv2D<br/>320→160, 3→32 채널"] --> MB["16 × MBConv 블록<br/>뎁스와이즈 + 포인트와이즈 + 잔차"]
52
+ end
53
+ BB -->|5개 스케일의 피처<br/>40,20,10,5,3| FPN
54
+ subgraph FPN["2 · BiFPN · 다중 스케일 피처 융합"]
55
+ direction TB
56
+ FUSE["Resize ↑ / MaxPool ↓ 후 가중 Add<br/>(몇 번 반복)"]
57
+ end
58
+ FPN --> HEADS
59
+ subgraph HEADS["3 · 탐지 헤드"]
60
+ direction TB
61
+ CLS["클래스 헤드 Conv2D<br/>→ 앵커당 90개 점수"]
62
+ BOX["박스 헤드 Conv2D<br/>→ 앵커당 4개 좌표"]
63
+ end
64
+ HEADS --> DEC["Reshape + Concat<br/>5개 격자를 하나의 리스트로 평탄화"]
65
+ DEC --> OUT["scores [1,19206,90]<br/>boxes [1,19206,4]"]
66
+ OUT --> POST["후처리<br/>sigmoid · 앵커 대비 디코드 · NMS"]
67
+ POST --> RES["최종: 소수의 라벨링된 박스"]
68
+ ```
69
+
70
+ 학습된 세 단계(백본 → BiFPN → 헤드)가 원시 숫자를 만들고, 고정된 후처리가 그것을 그릴 수 있는
71
+ 박스로 바꿉니다. 순서대로 살펴봅시다 — 하지만 먼저, 압도적으로 지배적인 연산 하나: **합성곱**.
72
+
73
+ ---
74
+
75
+ ## 3.3 핵심 연산: 합성곱 (`Conv2D`)
76
+
77
+ 언어 모델이 `MatMul` 에 기대는 것처럼, 비전 모델은 `Conv2D` 에 기댑니다. 합성곱은 작은
78
+ **필터(filter)**(가중치의 작은 격자, 예: 3×3)를 이미지 위로 슬라이드합니다. 각 위치에서 필터와 그
79
+ 아래의 픽셀을 곱해 합합니다 — **내적(dot product)** — 그렇게 출력 값 하나를 만듭니다. 이것을 온
80
+ 사방으로 슬라이드하면, 그 필터의 패턴(모서리, 질감, 눈)이 나타나는 곳마다 불이 켜지는 새 이미지
81
+ ("피처 맵, feature map")를 얻습니다.
82
+
83
+ ```
84
+ 입력 패치 필터 (3×3) 출력 픽셀 하나
85
+ ┌──────────┐ ┌──────────┐
86
+ │ a b c │ │ w1 w2 w3 │ out = a·w1 + b·w2 + c·w3
87
+ │ d e f │ ⊙ │ w4 w5 w6 │ = + d·w4 + e·w5 + f·w6
88
+ │ g h i │ │ w7 w8 w9 │ + g·w7 + h·w8 + i·w9
89
+ └──────────┘ └──────────┘ (그다음 `stride`만큼 오른쪽으로 슬라이드하고 반복)
90
+ ```
91
+
92
+ 실제 참조 커널(`js/ops/conv2D.js`)은 그 슬라이드를 중첩 루프로 쓴 것일 뿐입니다 — 배치, 출력 행,
93
+ 출력 열, 출력 채널, 그다음 필터 탭:
94
+
95
+ ```javascript
96
+ for (let oh = 0; oh < out_h; oh++)
97
+ for (let ow = 0; ow < out_w; ow++)
98
+ for (let oc = 0; oc < out_c; oc++) {
99
+ let sum = 0;
100
+ for (let ic = 0; ic < in_c; ic++) // 입력 채널에 대해
101
+ for (let kh = 0; kh < k_h; kh++) // 필터 높이에 대해
102
+ for (let kw = 0; kw < k_w; kw++) { // 필터 너비에 대해
103
+ const ih = oh*stride_y + kh*dil_y - pad; // 어느 입력 픽셀인지
104
+ const iw = ow*stride_x + kw*dil_x - pad;
105
+ if (in-bounds) sum += input[…ih,iw,ic…] * weight[…kh,kw,ic,oc…];
106
+ }
107
+ if (bias) sum += bias[oc];
108
+ if (relu) sum = clamp(sum, 0, 6); // 융합된 ReLU6 활성화
109
+ output[…oh,ow,oc…] = sum;
110
+ }
111
+ ```
112
+
113
+ 두 종류의 합성곱이 등장하며, 그 조합이 이 모델 계열의 효율성 비결 전부입니다:
114
+
115
+ | | **포인트와이즈** (1×1) | **뎁스와이즈** (3×3, `groups = 채널 수`) |
116
+ |---|---|---|
117
+ | 필터 | 1×1, **채널**을 섞음 | 3×3, **공간**을 섞음, 채널마다 따로 |
118
+ | 역할 | "특징을 재조합" | "국소 패턴을 봄" |
119
+ | 비용 | 픽셀당은 저렴하나 채널 간 전결합 | 매우 저렴 — 채널 혼합 없음 |
120
+
121
+ 일반 conv는 둘을 한 번에 합니다(비쌈). **뎁스와이즈 분리형(depthwise-separable)** 합성곱은 이를
122
+ 뎁스와이즈(공간) + 포인트와이즈(채널) 쌍으로 나눠, 거의 같은 성능을 몇 분의 일 비용에 냅니다. 이
123
+ 쌍이 곧 `MBConv` 블록, 백본의 레고 블록이며 — 182개 conv 중 80개가 뎁스와이즈인 이유입니다.
124
+
125
+ > 코드의 **`groups`**: `groups = in_c` 는 "각 채널이 자기만의 필터로 합성곱됨"(뎁스와이즈)을
126
+ > 뜻합니다. `groups = 1` 은 "모든 출력 채널이 모든 입력 채널을 봄"(일반)입니다. 같은 커널이 올바른
127
+ > 채널 범위를 도는 것으로 둘 다 처리합니다.
128
+
129
+ ---
130
+
131
+ ## 3.4 1단계 — 백본: 이미지 → 피처
132
+
133
+ **백본(backbone)**(*EfficientNet-Lite0*)은 피처 추출기입니다. 반복적으로:
134
+
135
+ 1. 공간 크기를 **줄이고**(stride-2 conv와 풀링으로) — 320→160→80→40→20→10→…
136
+ 2. 채널 수를 **늘립니다** — 3→32→…→320+ — "어디"를 "무엇"과 맞바꾸지요.
137
+
138
+ 초기 층은 모서리와 색을 감지하고, 중간 층은 질감과 부품(눈, 바퀴)을, 후기 층은 온전한 객체를
139
+ 감지합니다. 이 계층 구조는 프로그래밍된 것이 아니라 *학습된* 것입니다.
140
+
141
+ ```
142
+ stem: [1,320,320, 3] --Conv2D stride2--> [1,160,160, 32]
143
+ MBConv ×16: … 뎁스와이즈 + 포인트와이즈 + 잔차 Add … (채널 증가, 크기 감소)
144
+ stride 8,16,32,64,128에서 5개 피처 맵 출력:
145
+ P3 [1,40,40,C] P4 [1,20,20,C] P5 [1,10,10,C] P6 [1,5,5,C] P7 [1,3,3,C]
146
+ ```
147
+
148
+ 각 MBConv 안의 **잔차 `Add`** 는 트랜스포머와 같은 기법입니다: 블록의 출력을 입력에 다시 더해 깊은
149
+ 층 쌓기가 학습 가능한 상태를 유지하게 하지요. 같은 아이디어, 다른 영역.
150
+
151
+ 왜 하나가 아니라 다섯 개의 피처 맵일까요? **스케일** 때문입니다. 40×40 맵은 세밀한 디테일을
152
+ 가지고(작은 객체에 유리), 3×3 맵은 거대한 수용 영역(receptive field)을 봅니다(큰 객체에 유리).
153
+ 여러 스케일에서 탐지하는 것이 하나의 신경망으로 멀리 있는 새와 가까이 있는 버스를 모두 찾는
154
+ 방법입니다.
155
+
156
+ ---
157
+
158
+ ## 3.5 2단계 — BiFPN: 스케일들을 섞다
159
+
160
+ 작은 피처 맵은 *"여기 객체가 있다"* 는 것은 알지만 공간적으로 거칠고, 큰 맵은 정밀하지만 의미적으로
161
+ 얕습니다. **BiFPN**(Bi-directional Feature Pyramid Network)은 다섯 개 스케일이 위→아래, 아래→위로
162
+ 정보를 교환하게 해, 모든 스케일이 세밀한 디테일과 고수준 의미를 모두 얻게 합니다. 당신이 이미 아는
163
+ 정확히 세 가지 연산을 씁니다:
164
+
165
+ ```mermaid
166
+ flowchart TB
167
+ P7b["P7 3×3"] -->|Resize ↑| u6
168
+ P6b["P6 5×5"] --> u6(("가중 Add")) -->|Resize ↑| u5
169
+ P5b["P5 10×10"] --> u5(("가중 Add")) -->|Resize ↑| u4
170
+ P4b["P4 20×20"] --> u4(("가중 Add")) -->|Resize ↑| u3
171
+ P3b["P3 40×40"] --> u3(("가중 Add"))
172
+ u3 -->|MaxPool ↓| d4(("가중 Add"))
173
+ u4 --> d4 -->|MaxPool ↓| d5(("가중 Add"))
174
+ u5 --> d5 -->|MaxPool ↓| d6
175
+ style u6 fill:#eef
176
+ ```
177
+
178
+ - **`ResizeNearest2D`** 는 작은 맵을 큰 맵으로 업샘플링합니다(위→아래 경로). *12개.*
179
+ - **`MaxPool2D`** 는 큰 맵을 작은 맵으로 다운샘플링합니다(아래→위 경로). *14개.* 커널
180
+ (`js/ops/maxPool2D.js`)은 각 창(window)에서 최댓값만 남깁니다.
181
+ - **`Add`**(흔히 *가중* — 입력마다 학습 가능한 중요도)는 정렬된 두 맵을 융합합니다. 모델 전체의
182
+ *42개 Add* 가 이 융합과 백본 잔차를 담당합니다.
183
+
184
+ 그게 전부입니다 — BiFPN은 "두 맵이 같은 크기가 될 때까지 resize한 뒤 더하기"의 반복입니다. 새로운
185
+ 수학은 없습니다.
186
+
187
+ ---
188
+
189
+ ## 3.6 3단계 — 헤드: 피처 → 앵커별 예측
190
+
191
+ 두 개의 작은 conv 스택(다섯 스케일에서 공유)이 융합된 피처를 읽고, **모든** 격자 셀에서 그 셀의
192
+ **9개 앵커**(셀 중심의, 크기/종횡비가 다른 9개 기준 박스 모양)에 대한 예측을 출력합니다:
193
+
194
+ - **클래스 헤드** → 앵커당 `90`개 숫자: 객체 클래스마다의 원시 점수.
195
+ - **박스 헤드** → 앵커당 `4`개 숫자: 앵커의 위치와 크기에 대한 조정값(dx, dy, dw, dh).
196
+
197
+ > **앵커(anchor)** 가 영리한 부분입니다. 아무것도 없는 데서 박스를 예측하는 대신, 모델은 고정된
198
+ > 기준 박스 격자에 대한 작은 *보정값* 을 예측합니다. "이 기준 박스를 조금 옮겨라"를 배우는 것이
199
+ > "(173, 92, 240, 210)에 박스를 처음부터 만들어라"보다 훨씬 쉽습니다.
200
+
201
+ ---
202
+
203
+ ## 3.7 4단계 — Reshape + Concat: 다섯 격자를 하나의 리스트로 평탄화
204
+
205
+ 다섯 스케일 각각은 자기 격자 형태로 예측을 만들었습니다. `Reshape` 가 각 격자를 앵커의 단순한
206
+ 리스트로 평탄화하고, `Concat` 이 다섯 리스트를 하나로 쌓습니다(이것이 그래프의 꼬리 부분 — 실제 노드
207
+ 형태를 보세요):
208
+
209
+ ```
210
+ 클래스 예측: [1,40,40,…] → [1,14400,90] ┐
211
+ [1,20,20,…] → [1, 3600,90] │
212
+ [1,10,10,…] → [1, 900,90] ├─Concat─▶ scores [1,19206,90]
213
+ [1, 5, 5,…] → [1, 225,90] │
214
+ [1, 3, 3,…] → [1, 81,90] ┘
215
+ 박스 예측: … 같은 다섯 격자 … ─Concat─▶ boxes [1,19206,4]
216
+ ```
217
+
218
+ `Reshape` 는 메모리에서 숫자를 전혀 옮기지 않습니다 — 같은 평평한 버퍼를 새 형태로 재해석할 뿐이죠
219
+ (§1.2 참고). 이 저장소에서는 통과 복사(copy-through) 연산입니다. **이제 신경망의 일은 끝났습니다:**
220
+ 텐서 두 개, 채점된 후보 박스 19,206개.
221
+
222
+ ---
223
+
224
+ ## 3.8 5단계 — 후처리: 후보 19,206개 → 소수의 박스
225
+
226
+ 원시 출력은 아직 그릴 수 없습니다. 학습되지 않은 고정 단계 세 개가 마무리합니다:
227
+
228
+ 1. 클래스 점수에 **시그모이드(Sigmoid)** → `[0,1]` 범위의 확률. (int8 익스포트에서는 이 `Sigmoid`
229
+ 가 속도를 위해 접혀 없어졌으므로 `scores` 는 원시 로짓입니다. 직접 시그모이드를 적용하거나, 그냥
230
+ 비교만 해도 됩니다 — 여전히 클수록 확신도가 높습니다.)
231
+ 2. **박스 디코드**: 각 앵커의 4개 델타를, 그 앵커의 기준 박스에 적용해 실제 픽셀 모서리
232
+ `(x0,y0,x1,y1)` 로 변환.
233
+ 3. **비최대 억제(Non-Max Suppression, NMS)**: 같은 객체는 보통 여러 개의 겹치는 앵커를 발화시킵니다.
234
+ NMS는 점수가 가장 높은 박스를 남기고 그것과 너무 많이 겹치는(높은 *IoU*, 교집합/합집합) 다른
235
+ 박스들을 클래스별로 지웁니다. VolvoxAI에는 이것이 연산으로 있습니다 —
236
+ `js/ops/nonMaxSuppression.js`.
237
+
238
+ ```
239
+ NMS 전: ▢▢▢ 겹치는 "개" 박스 세 개, 점수 0.91, 0.88, 0.72
240
+ NMS 후: ▢ 0.91을 남기고; 그것과 50% 넘게 겹치는 둘을 억제
241
+ ```
242
+
243
+ 네이티브 `detect` 명령(`native/main.c`, `print_detections`)은 데모를 위해 3단계의 **단순화된**
244
+ 버전을 씁니다: 최상위 `max_det` 개 박스를 최고 클래스 점수 기준으로 뽑아 순위 표로 출력합니다
245
+ (라벨 조회는 `labels.txt` 로):
246
+
247
+ ```
248
+ rank index score class x0 y0 x1 y1
249
+ 1 4213 0.91 17 0.31 0.44 0.62 0.88 ← "dog"
250
+ 2 991 0.86 2 0.05 0.10 0.40 0.95 ← "bicycle"
251
+ ```
252
+
253
+ 그 사각형들을 원본 사진 위에 그리면 객체 탐지가 완성됩니다.
254
+
255
+ ---
256
+
257
+ ## 3.9 두 모델, 나란히 놓고 보기
258
+
259
+ 이제 두 세계를 모두 따라가 봤습니다. 얼마나 많이 공유하는지 보세요:
260
+
261
+ | | TinyStories (언어) | EfficientDet-Lite0 (비전) |
262
+ |---|---|---|
263
+ | 입력 텐서 | tokens `[1,256]` | image `[1,320,320,3]` |
264
+ | 지배적 연산 | `MatMul` | `Conv2D` |
265
+ | "혼합" 메커니즘 | 토큰 간 어텐션(`SDPA`) | 픽셀 간 합성곱 |
266
+ | 비선형성 | `GELU` | `ReLU6` (conv에 융합) |
267
+ | 깊은 층 쌓기 기법 | 잔차 `Add` + `LayerNorm` | 잔차 `Add` (MBConv 안) |
268
+ | 출력 | 로짓 `[…,50257]` → 다음 단어 | scores/boxes `[…,19206,…]` → 객체 |
269
+ | 후처리 | argmax / 샘플링 | sigmoid + 디코드 + NMS |
270
+
271
+ 같은 골격 — *학습된 가중치를 가진 작은 텐서 연산의 그래프* — 이 텍스트와 픽셀을 풉니다. 이 전이가
272
+ 핵심입니다: 골격을 한 번 배우면 모든 모델이 읽히게 됩니다.
273
+
274
+ 마지막 큰 질문은 세 개의 EfficientDet 폴더가 던지는 것입니다: **fp32 vs fp16 vs int8.** 그것들은
275
+ 무엇이고, 왜 같은 모델을 세 번 배포할까요? 그것이 4장입니다.
276
+
277
+ **다음:** [4장 — 정밀도와 양자화 →](04-precision-and-quantization.md)
@@ -0,0 +1,206 @@
1
+ # 4장 — 정밀도와 양자화 (fp32 / fp16 / int8)
2
+
3
+ *목표: 같은 탐지기가 왜 세 개의 폴더로 배포되는지, 숫자가 비트로 어떻게 저장되는지, 그리고 int8
4
+ 모델을 4배 작게 만드는 — 정확도 손실은 거의 없이 — 정확한 정수 연산을 이해합니다.*
5
+
6
+ 세 개의 EfficientDet 폴더를 보세요. 같은 아키텍처(3장), 같은 262개 남짓 노드, 그런데 **파일 크기는
7
+ 매우 다릅니다**:
8
+
9
+ | 폴더 | 각 숫자를 이렇게 저장… | `model.safetensors` | 상대 크기 |
10
+ |---|---|---|---|
11
+ | `efficientdet_lite0_fp32` | 32비트 float | **12.67 MB** | 1.0× |
12
+ | `efficientdet_lite0_fp16` | 16비트 float | **6.34 MB** | 0.50× |
13
+ | `efficientdet_lite0_int8` | 8비트 정수 | **3.39 MB** | 0.27× |
14
+
15
+ *모델* 은 동일합니다. 바뀐 것은 **숫자 형식** — **정밀도(precision)** — 뿐입니다. 더 작은 숫자 →
16
+ 더 작은 다운로드, 더 적은 메모리, 그리고 (알맞은 하드웨어라면) 더 빠른 연산. 이 장은 그 절충에 대한
17
+ 것입니다.
18
+
19
+ ---
20
+
21
+ ## 4.1 컴퓨터는 숫자를 어떻게 저장하는가
22
+
23
+ `0.10125` 같은 신경망 가중치는 고정된 비트 패턴이 되어야 합니다. 두 가지 계열이 있습니다.
24
+
25
+ ### 부동소수점 (fp32, fp16): "이진법의 과학적 표기법"
26
+
27
+ float는 비트를 **부호(sign)**, **지수(exponent)**(얼마나 큰가), **가수(mantissa)**(정밀한 자릿수)로
28
+ 나눕니다. 비트가 많을수록 → 더 높은 정밀도와 더 넓은 범위.
29
+
30
+ ```
31
+ fp32 (4바이트) [S][ 8비트 지수 ][ 23비트 가수 ] ~소수 7자리
32
+ fp16 (2바이트) [S][ 5비트 지수 ][ 10비트 가수 ] ~소수 3자리
33
+ ```
34
+
35
+ - **fp32**("단정밀도")는 어디서나 기본값입니다 — 넓은 범위, 약 7자리 정밀도. 학습이 쓰는 형식이자
36
+ VolvoxAI가 활성값(activation)에 쓰는 형식입니다.
37
+ - **fp16**("반정밀도")은 저장 공간을 절반으로 줄입니다. 추론에는 충분히 정밀하지만, 범위가 더
38
+ 작고(큰/작은 값이 오버플로/언더플로할 수 있음), 결정적으로 — 속도 향상을 얻으려면 GPU가 fp16
39
+ 연산을 *지원* 해야 합니다. (브라우저에서 Volvox가 fp16에 신중한 이유는 §4.5 참고.)
40
+
41
+ ### 정수 (int8): "256개의 균등 눈금이 있는 자"
42
+
43
+ **int8** 은 **−128부터 127까지**의 정수를 저장합니다 — 가능한 값 256개, 1바이트뿐이죠. `0.10125`
44
+ 를 직접 담기에는 너무 거칩니다. 비결 — **양자화(quantization)** — 은 저렴한 정수 하나에, 그것을 다시
45
+ float로 되돌리는 *레시피* 를 함께 저장하는 것입니다.
46
+
47
+ ---
48
+
49
+ ## 4.2 양자화: 스케일 + 제로포인트 레시피
50
+
51
+ 한 층의 실제 가중치는 어떤 범위, 예컨대 `−0.4 … +0.4` 안에 있습니다. 양자화는 int8 자
52
+ (`−128 … 127`)를 그 범위에 걸쳐 늘리며, 두 개의 숫자를 씁니다:
53
+
54
+ - **`scale`(스케일)** — 눈금 하나의 크기(정수 한 칸당 실제 단위).
55
+ - **`zero_point`(제로포인트)** — 어떤 정수가 실제 `0.0` 을 나타내는가.
56
+
57
+ ```
58
+ 실제 값 r ≈ (q − zero_point) × scale ← 역양자화 (정수 → float)
59
+ 정수 q = round(r / scale) + zero_point ← 양자화 (float → 정수)
60
+
61
+ 실제: -0.4 -0.2 0.0 0.2 0.4
62
+ │ │ │ │ │
63
+ int8: -128 -64 0 64 127 (scale ≈ 0.4/127)
64
+ ```
65
+
66
+ 두 공식 모두 *코드베이스에 그대로* 있습니다. 역양자화(`js/ops/dequantizeLinear.js`):
67
+
68
+ ```javascript
69
+ out[i] = (in[i] - zero_point) * scale; // int8 → float
70
+ ```
71
+
72
+ 양자화(`native/quant_cpu_opt.c`, `quantize_scalar_i8`):
73
+
74
+ ```c
75
+ q = clamp_i8( lrintf(x / scale) + zero_point ); // float → int8, [-128,127]로 클램프
76
+ ```
77
+
78
+ 아이디어의 전부입니다. int8 가중치는 *표* 이고, `scale` 과 `zero_point` 가 그 값어치를 알려줍니다.
79
+ 표를 저장하는 데는 1바이트가 들고, 레시피는 채널 전체가 공유하므로 거의 공짜입니다.
80
+
81
+ > **채널별 스케일(per-channel scales).** 층 전체에 스케일 하나만 쓰면 거칠어집니다 — 큰 가중치 하나가
82
+ > 자를 늘려 나머지 모두의 정밀도를 뭉개버리지요. 그래서 각 출력 **채널** 이 자기만의 `weight_scale`
83
+ > 을 가집니다(§`weight_scale: "w1"` 이 `QConv2D` 의 *텐서* 입력이었던 것 기억나죠). 이 "채널별
84
+ > 양자화"가 int8 정확도를 fp32의 약 1% 이내로 유지하는 비결입니다.
85
+
86
+ ---
87
+
88
+ ## 4.3 실제 계산 예시 (모델의 진짜 숫자)
89
+
90
+ int8 EfficientDet의 첫 노드는 `QuantizeLinear` 로, `input_scale = 0.0078125`(정확히 `1/128`)입니다
91
+ — 들어오는 픽셀을 int8로 바꾸지요. 그다음 첫 `QConv2D` 는 `output_scale = 0.0235294`,
92
+ `output_zero_point = -128` 입니다. 가중치 하나와 출력 하나를 양자화해 봅시다.
93
+
94
+ ```
95
+ 가중치 양자화 r = 0.101, scale = 0.008, zero_point = 0:
96
+ q = round(0.101 / 0.008) + 0 = round(12.625) = 13 → 바이트 13으로 저장
97
+
98
+ 나중에 복원:
99
+ r ≈ (13 - 0) × 0.008 = 0.104 → 0.104 vs 0.101, 오차 0.003
100
+ ```
101
+
102
+ 이 0.003 오차가 **양자화 잡음(quantization noise)** 입니다. 큰 내적에 걸쳐 퍼지면 이 작은 반올림
103
+ 오차들이 대부분 상쇄됩니다 — 8비트 모델이 여전히 개를 제대로 탐지하는 이유지요.
104
+
105
+ ---
106
+
107
+ ## 4.4 int8 합성곱은 실제로 어떻게 도는가 (`QConv2D`)
108
+
109
+ 여기가 보상을 받는 지점입니다. 양자화된 conv는 무거운 곱-누적(multiply-accumulate) 루프를
110
+ **저렴한 정수 연산** 으로 하고, 맨 마지막에 딱 한 번만 실제 숫자로 되돌립니다. 출력 값 하나의
111
+ 파이프라인(`native/quant_cpu_opt.c`):
112
+
113
+ ```mermaid
114
+ flowchart LR
115
+ A["int8 입력<br/>(−128…127)"] --> B["int32 누적<br/>Σ (in_q − in_zp) × w_q<br/>(전부 정수 연산)"]
116
+ B --> C["float로:<br/>v = acc × in_scale × w_scale + bias"]
117
+ C --> D["ReLU6 클램프<br/>(융합된 활성화)"]
118
+ D --> E["재양자화:<br/>q = round(v / out_scale) + out_zp"]
119
+ E --> F["int8 출력<br/>다음 층으로"]
120
+ ```
121
+
122
+ 1. **정수 누적.** int8×int8을 곱해 **int32** 누산기에 합합니다. 정수 곱-덧셈은 모든 CPU에서 빠르고
123
+ 저렴하며(AVX2/NEON으로 8–32 레인 폭으로 벡터화됨).
124
+ 2. **재양자화(requantize).** int32 합을 한 단계로 그 층의 int8 스케일로 되돌립니다. 실제 코드는 모든
125
+ 스케일을 하나의 곱으로 합성합니다:
126
+
127
+ ```c
128
+ // acc (int32) → float → relu6 → int8, 출력 원소 하나에 대해:
129
+ float v = acc * (input_scale * weight_scale) + bias; // 결합된 재스케일
130
+ int8 q = requantize_i8(v, output_scale, output_zp, relu);
131
+ // = clamp_i8( round( relu6(v) / output_scale ) + output_zp );
132
+ ```
133
+
134
+ 핵심 통찰: **활성값이 층에서 층으로 int8로 유지됩니다**("양자화 섬, quantized island"). 그래서 백본
135
+ 전체가 바이트 단위로 돕니다. 맨 마지막에서야 `DequantizeLinear` 가 최종 `scores`/`boxes` 를 읽을 수
136
+ 있는 float로 바꿉니다. 이것이 정확히 VolvoxAI의 네이티브 CPU 경로가 하는 일입니다
137
+ (`native/quant_cpu_opt.c`). 브라우저 계층은 대신 int8 conv 가중치를 로드 시점에 fp32로 접습니다(더
138
+ 단순하지만 디스크에서는 여전히 작음).
139
+
140
+ ---
141
+
142
+ ## 4.5 각 형식이 존재하는 이유 — 절충
143
+
144
+ ```
145
+ 더 작음 / 더 빠름 ◀───────────────────────────────▶ 더 정확함 / 더 단순함
146
+ int8 fp16 fp32
147
+ 가중치당 1바이트 가중치당 2바이트 가중치당 4바이트
148
+ 정수 연산 fp16 HW 필요 어디서나 동작
149
+ ~4× 작음 ~2× 작음 기준 정확도
150
+ 미세한 정확도 하락 ~무손실 무손실
151
+ ```
152
+
153
+ | 질문 | fp32 | fp16 | int8 |
154
+ |---|---|---|---|
155
+ | 디스크 / 메모리 | 가장 큼 | 절반 | 4분의 1 |
156
+ | fp32 대비 정확도 | 기준 | ~동일 | 보통 ~1% 이내 |
157
+ | 특수 하드웨어 필요? | 아니오 | **예** (fp16 유닛) | 아니오 (정수는 보편적) |
158
+ | 언제 최선인가… | 최대 정확도, 또는 나중에 양자화할 때 | GPU에 fp16이 있고 손쉬운 2×를 원할 때 | 엣지/모바일/브라우저, 크기·속도가 중요할 때 |
159
+
160
+ **VolvoxAI가 fp32 + int8에 기대고 fp16에 신중한 이유 (README에서):**
161
+
162
+ - **fp16** 은 WebGPU `shader-f16` 확장이 필요한데, 이것이 소비자 기기 전반에 보편적이지 않습니다 —
163
+ 그래서 브라우저 엔진이 이에 의존할 수 없습니다. (여기 fp16 폴더는 주로 그것을 지원하는
164
+ 플랫폼/형식을 위한 것입니다.)
165
+ - **int8** 은 정확도 손실이 거의 없이 크기를 4배 줄이고, 정수 연산은 *어떤* CPU/GPU에서도 빠르게
166
+ 돕니다 — 이식성 있는 추론의 스위트 스폿이지요. Volvox는 **int4** 를 건너뜁니다. 4비트는 성가신
167
+ 비트 언패킹이 필요해 저사양 모바일 GPU에 부담이 되기 때문입니다.
168
+
169
+ ---
170
+
171
+ ## 4.6 세 개의 config, 나란히 놓고 보기
172
+
173
+ 정밀도는 *저장* 선택이므로 그래프는 거의 동일합니다 — conv 연산과 약간의 양자화 장부만 다릅니다:
174
+
175
+ ```
176
+ fp32 / fp16 그래프: int8 그래프:
177
+ input (float) input (uint8)
178
+ │ │ QuantizeLinear ← float/uint8 → int8 (한 번)
179
+ Conv2D ─┐ QConv2D ─┐ ← 정수 conv, int8 입출력
180
+ Conv2D │ float conv 182개 QConv2D │ int8 conv 182개 (내내 int8 유지)
181
+ … │ … │
182
+ Add / MaxPool / Resize Add / MaxPool / Resize (int8 인식)
183
+ │ │ DequantizeLinear ← int8 → float (끝에서 두 번)
184
+ scores, boxes (float) scores, boxes (float)
185
+ ```
186
+
187
+ 그래서 int8 config에는 **노드 3개가 더** 있고(`QuantizeLinear` 1 + `DequantizeLinear` 2), 182개
188
+ conv가 `Conv2D` 대신 `QConv2D` 입니다. 같은 탐지기, 세 가지 크기 — 당신의 기기가 필요로 하는
189
+ 곡선 위의 지점을 고르는 것이지요.
190
+
191
+ ---
192
+
193
+ ## 4.7 방금 배운 것
194
+
195
+ - **정밀도** 는 각 숫자가 몇 비트를 받는가입니다: fp32(4 B), fp16(2 B), int8(1 B).
196
+ - **양자화** 는 `scale` + `zero_point` 로 float를 256개 값의 int8 자에 매핑합니다. 공식
197
+ `(q−zp)×scale` 와 `round(r/scale)+zp` 가 비결의 전부이며, 저장소에 실제로 있습니다.
198
+ - **int8 conv** 는 저렴한 int32로 누적하고 한 번 재양자화합니다 — 활성값이 층마다 int8로 유지되어,
199
+ 약 1% 정확도 비용으로 ~4× 작고 빠릅니다.
200
+ - 형식은 배포마다 **당신이 선택** 합니다: 충실도를 위한 fp32, 엣지를 위한 int8, 하드웨어가 지원할 때의
201
+ fp16.
202
+
203
+ 다음: VolvoxAI가 이 그래프들을 *빠르게* 실행하는 방법 — 네 개의 하드웨어 계층, 소박한 커널에서
204
+ 최적화된 커널로의 도약, 그리고 연산 융합.
205
+
206
+ **다음:** [5장 — 엔진 내부 →](05-inside-the-engine.md)
@@ -0,0 +1,154 @@
1
+ # 5장 — 엔진 내부
2
+
3
+ *목표: VolvoxAI가 "연산 목록"을 실제 하드웨어에서 **빠르게** 도는 무언가로 바꾸는 방법을 이해합니다
4
+ — 네 개의 계층, 소박한 커널에서 최적화된 커널로의 도약, 그리고 연산 융합.*
5
+
6
+ 이제 모델이 *무엇을* 계산하는지 알게 되었습니다. 이 장은 *그것을 빠르게 만드는* 것에 대한
7
+ 것입니다 — 교과서적 구현과 출시 가능한 구현을 가르는 엔지니어링이지요. 실제 AI 시스템 작업의 많은
8
+ 부분이 여기에 있습니다.
9
+
10
+ ---
11
+
12
+ ## 5.1 하나의 그래프, 네 개의 엔진 (계층)
13
+
14
+ 1장을 떠올려 보세요: VolvoxAI는 같은 그래프를 브라우저에서 네 가지 방법으로(그리고 네이티브
15
+ 바이너리로) 실행할 수 있습니다. 이들은 오직 **누가 산술을 하는가** 와 **텐서가 어디에 사는가** 만
16
+ 다릅니다.
17
+
18
+ ```mermaid
19
+ flowchart TD
20
+ G["그래프 + 가중치"] --> I["VolvoxAI.init(backend)"]
21
+ I --> T1["Tier 1 · WebNN<br/>그래프를 브라우저 ML API에 넘김 → NPU/GPU/CPU"]
22
+ I --> T2["Tier 2 · WebGPU<br/>노드당 컴퓨트 파이프라인 하나, 전부 GPU에서"]
23
+ I --> T3["Tier 3 · WASM SIMD<br/>선형 메모리 위의 컴파일된 C 커널"]
24
+ I --> T4["Tier 4 · 순수 JS<br/>참조 커널 — 느리지만 항상 정확"]
25
+ T1 -.폴백.-> T3
26
+ T3 -.폴백.-> T4
27
+ ```
28
+
29
+ - **Tier 4 — 순수 JS** (`js/ops/*.js`): 이 책 내내 읽은 소박한 커널들. 느리지만 단순하고 의존성이
30
+ 없습니다. **기준(ground truth)** 입니다: 더 빠른 모든 계층이 이것과 대조됩니다.
31
+ - **Tier 3 — WASM** (`js/WasmEngine.js` + `native/kernels/*.c`): *같은* 연산을 SIMD와 함께
32
+ WebAssembly로 컴파일한 것. 범프 할당기(bump allocator)가 모든 텐서를 하나의 평평한 선형 메모리
33
+ 블록에 넣고, `execute()` 가 노드마다 컴파일된 C 커널을 호출합니다. 흔히 순수 JS보다 5–50× 빠릅니다.
34
+ - **Tier 2 — WebGPU** (`js/GraphExecutor.js` + `shaders/*.wgsl`): 각 연산이 GPU **컴퓨트
35
+ 셰이더** 가 됩니다. 컴파일 시점에 모든 가중치를 VRAM에 올리고 *노드당 파이프라인 하나* 를
36
+ 만듭니다. `execute()` 는 이들을 하나의 명령 스트림으로 재생하며, **노드 사이에 CPU 왕복이 없어서**
37
+ 모델 전체가 GPU에 상주합니다. 필요한 셰이더가 없으면 현재 executor는 경고를 내고 그 노드를
38
+ 건너뜁니다. 지원되지 않는 연산이 필요한 모델은 WASM/CPU를 써야 합니다.
39
+ - **Tier 1 — WebNN** (`js/WebNNEngine.js`): 그래프를 브라우저 자체의 신경망 API에 넘기며, 이것은
40
+ 전용 **NPU** 로 디스패치될 수 있습니다. 지원하지 않는 연산이 있으면 하위 계층으로 넘어갑니다.
41
+
42
+ 설계 원칙: **실행 가능한 백엔드를 먼저 고르기.** WebNN은 그래프 컴파일에 실패하면 하위 계층으로
43
+ 넘어갈 수 있고, WASM은 순수 JS helper로 보완될 수 있습니다. WebGPU는 필요한 연산이 셰이더로
44
+ 덮여 있는 그래프에 써야 합니다.
45
+
46
+ ---
47
+
48
+ ## 5.2 소박한 커널이 느린 이유
49
+
50
+ 3장의 소박한 `Conv2D` 를 다시 보세요: 일곱 겹 중첩 루프, 한 번에 곱셈 하나. *정확* 하지만, 현대
51
+ CPU는 두 가지 이유로 이것을 싫어합니다:
52
+
53
+ 1. **SIMD 유닛을 낭비합니다.** CPU 코어는 *하나의* 명령으로 float 8개(AVX2) 이상을 곱할 수 있습니다.
54
+ 소박한 루프는 그것을 하나… 씩… 차례로 하며, 실리콘의 일부만 씁니다.
55
+ 2. **캐시를 망가뜨립니다(thrash).** RAM은 CPU보다 ~100× 느립니다. 코어는 최근 쓴 데이터를 작고 빠른
56
+ **캐시(cache)** 에 둡니다. 소박한 conv는 메모리 곳곳을 뛰어다녀서(스트라이드된 이미지 읽기),
57
+ 계산 대신 끊임없이 RAM을 기다립니다.
58
+
59
+ 결과: 소박한 커널은 칩 실제 처리량의 **2–5%** 만 쓸 수 있습니다. 최적화란 SIMD 유닛에 먹이를 주고
60
+ 캐시를 존중하는 것입니다.
61
+
62
+ ---
63
+
64
+ ## 5.3 소박함에서 빠름으로: 같은 수학, 재배치
65
+
66
+ 최적화된 커널은 *무엇을* 계산하는지는 절대 바꾸지 않습니다(출력은 Tier 4와 동일) — 작업의 *순서와
67
+ 배치* 를 바꿉니다. VolvoxAI의 핫 경로는 `native/conv_f32_opt.c`(fp32)와
68
+ `native/quant_cpu_opt.c`(int8)에 있습니다. 주요 기법:
69
+
70
+ | 기법 | 아이디어 | 이득 |
71
+ |---|---|---|
72
+ | **im2col + GEMM** | conv 패치를 큰 행렬로 펼친 뒤 빠른 행렬 곱을 호출 | 수십 년간 튜닝된 matmul 재사용; SIMD에 먹이 공급 |
73
+ | **포인트와이즈 GEMM** | 1×1 conv는 *곧* 행렬 곱 — 바로 그렇게 취급 | 단일 최대 이득 (대부분의 conv가 1×1) |
74
+ | **가중치 사전 패킹(prepacking)** | 로드 시점에 가중치를 내부 루프가 읽는 정확한 순서로 재배치 | 캐시 미스 읽기를 순차 읽기로 전환 |
75
+ | **블로킹 / 타일링** | 캐시에 들어가는 작은 타일을 다 쓰고 넘어감 | 데이터가 뜨거울 때 재사용 |
76
+ | **SIMD (AVX2/NEON)** | 명령당 8–32개 곱-덧셈 | 사이클당 ~8–32× 산술 |
77
+ | **멀티스레딩** | 출력 행을 CPU 코어들에 분배 | 그 위에 ~코어 수× |
78
+
79
+ ```
80
+ 소박한 conv 최적화된 conv (im2col + GEMM)
81
+ 각 출력 픽셀마다: [1] 입력 패치를 펼침 → 큰 행렬 하나 (한 번)
82
+ 각 필터 탭마다: [2] 크고, 캐시 친화적, SIMD, 스레드된
83
+ 스칼라 곱 하나 사전 패킹된 가중치와의 행렬 곱
84
+ (흩어진 메모리, 1 레인) (순차 메모리, 여러 레인, 여러 코어)
85
+ ```
86
+
87
+ > **이 저장소의 실제 결과.** 이 프로젝트의 메모 기록에 따르면, *아레나 버퍼 재사용 플래너*(노드마다
88
+ > 새로 할당하는 대신 스크래치 메모리를 재사용)를 추가하니 최대 메모리가 **88.8 MB → 18.8 MB** 로
89
+ > 줄었고, 포인트와이즈 GEMM 경로와 결합해 EfficientDet 순전파를 의미 있게 빠르게 만들어 — Google
90
+ > TFLite/XNNPACK과의 격차를 (warm) ~1.16× 까지 좁혔습니다. 나오는 숫자는 같고, 메모리와 시간은
91
+ > 극적으로 줄었죠. *그것이* 곧 커널 엔지니어링입니다.
92
+
93
+ 교훈은 간단합니다: **정확성은 소박한 커널에 살고, 성능은 메모리 배치에 산다.**
94
+ `js/ops/conv2D.js` 를 읽은 뒤 `native/conv_f32_opt.c` 와 비교해 보면 이 기술의 두 반쪽을 보게 됩니다.
95
+
96
+ ---
97
+
98
+ ## 5.4 연산 융합: 메모리를 그만 건드리기
99
+
100
+ 연산과 연산 사이에서 결과는 메모리에 쓰이고 다음 연산이 다시 읽습니다. 큰 피처 맵에서는 데이터를
101
+ *옮기는* 것이 계산보다 더 비쌀 수 있습니다. **연산 융합(operator fusion)** 은 인접 연산을 병합해
102
+ 데이터를 한 번만 건드리게 합니다. VolvoxAI는 컴파일 시점 융합 패스를 실행합니다
103
+ (`native/graph_opt_fusion.c`):
104
+
105
+ ```
106
+ 융합 전: Conv2D ─▶ [피처 맵 쓰기] ─▶ ReLU6 ─▶ [다시 쓰기]
107
+ 융합 후: Conv2D+ReLU6 ─▶ [한 번 쓰기, 이미 활성화됨]
108
+ ```
109
+
110
+ 적용하는 융합 패턴(`docs/operator_fusion_patterns.md` 참고):
111
+
112
+ - **Conv + ReLU6** → conv의 재양자화 단계 안에서 바로 클램프(3–4장에서 `relu` 가 커널에 접혀 들어간
113
+ 것을 봤죠).
114
+ - **연쇄 `Add`** → 여러 잔차를 한 패스에 합산.
115
+ - **뎁스와이즈 → 포인트와이즈** → 중간 텐서를 흘리지 않고 MBConv 쌍을 연달아 실행.
116
+ - **Concat + Sigmoid**, **별칭 제거(alias elision)**(일부 `Reshape` 같은 무의미한 복사 제거).
117
+
118
+ 각 융합 쌍은 큰 텐서의 전체 읽기+쓰기를 하나씩 줄입니다. 262개 노드에 걸치면 쌓이지요.
119
+
120
+ ---
121
+
122
+ ## 5.5 메모리: 텐서는 일시적이다
123
+
124
+ 미묘하지만 중요한 점: 순전파의 대부분 텐서는 **스크래치(scratch)** 입니다 — 잠깐 필요하고 다시는 안
125
+ 쓰이지요(예: 어텐션 MatMul이 소비한 뒤 `ln1_0` 은 죽습니다). 소박한 엔진은 노드마다 새 버퍼를
126
+ 할당합니다(단순하지만 낭비). 똑똑한 엔진은 어느 버퍼들이 메모리를 공유할 수 있는지 **계획** 합니다,
127
+ 그 수명이 겹치지 않기 때문이죠 — 앞서 언급한 **아레나 버퍼 재사용 플래너** 입니다. 이것이 VolvoxAI가
128
+ 텐서 총합이 수백 MB인 모델을 그 최대치의 일부 RAM으로 돌릴 수 있는 이유입니다: 같은 물리 바이트를
129
+ 노드마다 재활용하지요.
130
+
131
+ ```
132
+ 노드 수명 (─ = 살아있음): 버퍼 재사용:
133
+ A: ─── A와 C는 절대 겹치지 않음 → 같은 버퍼를 줌
134
+ B: ───── B와 D는 절대 겹치지 않음 → 버퍼 공유
135
+ C: ────
136
+ D: ─── 텐서 4개, 물리 버퍼 2개
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 5.6 엔진 전체를 한 문장으로
142
+
143
+ > **VolvoxAI는 그래프의 노드 목록을 훑으며 각 노드를 사용 가능한 가장 빠른 백엔드의 커널로 디스패치하고,
144
+ > 그렇게 하면서 메모리 버퍼를 재사용하고 인접 연산을 융합합니다 — 소박한 참조와 정확히 같은 숫자를,
145
+ > 훨씬 빠르고 작게 만들어 냅니다.**
146
+
147
+ 그것이 시스템 전부입니다. 나머지는 이 골격 위에 연산 하나 더, 백엔드 하나 더, 또는 최적화 하나 더일
148
+ 뿐입니다.
149
+
150
+ > **이 장은 네 개의 *브라우저* 계층을 다뤘습니다.** VolvoxAI는 *같은 설계도* 를 실행하는 완전한
151
+ > **네이티브** 엔진(CPU + Vulkan / OpenGL / Metal / Android NNAPI를 아우르는 독립형 C 바이너리)도
152
+ > 제공합니다. 그것이 다음 장 전체의 내용입니다.
153
+
154
+ **다음:** [6장 — 네이티브 엔진 →](06-native-engine-architecture.md)