@openfluke/welvet 0.2.0 → 0.74.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.
@@ -0,0 +1,244 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Loom — Tiling Benchmark</title>
6
+ <style>
7
+ * { box-sizing: border-box; margin: 0; padding: 0; }
8
+ body { background: #0d0d0d; color: #c8c8c8; font-family: 'Courier New', monospace; font-size: 13px; padding: 20px; }
9
+ h1 { color: #7ec8e3; margin-bottom: 4px; font-size: 16px; }
10
+ .subtitle { color: #555; margin-bottom: 12px; font-size: 11px; }
11
+ #output { white-space: pre; line-height: 1.65; }
12
+ .pass { color: #4ec94e; }
13
+ .fail { color: #e04040; }
14
+ .warn { color: #e0a020; }
15
+ .info { color: #7ec8e3; }
16
+ .dim { color: #555; }
17
+ .exact { color: #ffd700; }
18
+ button { background: #1e3a4a; color: #7ec8e3; border: 1px solid #2d5a70; padding: 6px 18px;
19
+ font-family: inherit; font-size: 13px; cursor: pointer; margin-right: 8px; }
20
+ button:hover { background: #2d5a70; }
21
+ button:disabled { opacity: 0.4; cursor: not-allowed; }
22
+ #status { margin: 8px 0; color: #555; font-size: 11px; }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <h1>=== M-POLY-VTD Performance Showdown: CPU vs WebGPU (WASM) ===</h1>
27
+ <div class="subtitle">Browser port of benchmark_tiling.go — forward pass timing across layer types</div>
28
+ <button id="runBtn" onclick="runBench()">Run Benchmark</button>
29
+ <button onclick="runBench(true)">Run (GPU)</button>
30
+ <div id="status">Loading WASM...</div>
31
+ <pre id="output"></pre>
32
+
33
+ <script src="wasm_exec.js"></script>
34
+ <script>
35
+ let wasmReady = false;
36
+ const outEl = document.getElementById('output');
37
+
38
+ function print(msg, cls) {
39
+ const s = document.createElement('span');
40
+ if (cls) s.className = cls;
41
+ s.textContent = msg + '\n';
42
+ outEl.appendChild(s);
43
+ }
44
+ const p = (m) => print(m);
45
+ const pInfo = (m) => print(m, 'info');
46
+ const pPass = (m) => print(m, 'pass');
47
+ const pFail = (m) => print(m, 'fail');
48
+ const pWarn = (m) => print(m, 'warn');
49
+ const pDim = (m) => print(m, 'dim');
50
+
51
+ async function loadWASM() {
52
+ if (wasmReady) return;
53
+ const go = new Go();
54
+ const r = await WebAssembly.instantiateStreaming(fetch('main.wasm'), go.importObject);
55
+ go.run(r.instance);
56
+ await new Promise(res => setTimeout(res, 150));
57
+ wasmReady = true;
58
+ }
59
+
60
+ // ─────────────────────────────────────────────────────────────
61
+ // Network configs — mirrors the 11 layer types in benchmark_tiling.go
62
+ // ─────────────────────────────────────────────────────────────
63
+ const BENCH_CASES = [
64
+ {
65
+ name: 'Dense (Linear)', inputSize: [1024], iters: 20,
66
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
67
+ {z:0,y:0,x:0,l:0,type:"Dense",input_height:1024,output_height:1024,activation:"Linear",dtype:"F32"}
68
+ ]})
69
+ },
70
+ {
71
+ name: 'RNN Cell', inputSize: [512], iters: 10,
72
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
73
+ {z:0,y:0,x:0,l:0,type:"RNN",input_height:512,output_height:512,dtype:"F32"}
74
+ ]})
75
+ },
76
+ {
77
+ name: 'LSTM Cell', inputSize: [512], iters: 10,
78
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
79
+ {z:0,y:0,x:0,l:0,type:"LSTM",input_height:512,output_height:512,dtype:"F32"}
80
+ ]})
81
+ },
82
+ {
83
+ name: 'CNN 1D', inputSize: [64, 512], iters: 10,
84
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
85
+ {z:0,y:0,x:0,l:0,type:"CNN1",input_channels:64,filters:64,kernel_size:3,stride:1,padding:1,input_height:512,output_height:512,dtype:"F32"}
86
+ ]})
87
+ },
88
+ {
89
+ name: 'CNN 2D', inputSize: [32, 64, 64], iters: 5,
90
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
91
+ {z:0,y:0,x:0,l:0,type:"CNN2",input_channels:32,filters:32,kernel_size:3,stride:1,padding:1,input_height:64,input_width:64,output_height:64,output_width:64,dtype:"F32"}
92
+ ]})
93
+ },
94
+ {
95
+ name: 'CNN 3D', inputSize: [16, 16, 16, 16], iters: 5,
96
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
97
+ {z:0,y:0,x:0,l:0,type:"CNN3",input_channels:16,filters:16,kernel_size:3,stride:1,padding:1,input_depth:16,input_height:16,input_width:16,output_depth:16,output_height:16,output_width:16,dtype:"F32"}
98
+ ]})
99
+ },
100
+ {
101
+ name: 'Embedding', inputSize: [64], iters: 20, isEmbedding: true,
102
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
103
+ {z:0,y:0,x:0,l:0,type:"Embedding",vocab_size:2048,embedding_dim:128,dtype:"F32"}
104
+ ]})
105
+ },
106
+ {
107
+ name: 'RMSNorm', inputSize: [1024], iters: 30,
108
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
109
+ {z:0,y:0,x:0,l:0,type:"RMSNorm",input_height:1024,output_height:1024,dtype:"F32"}
110
+ ]})
111
+ },
112
+ {
113
+ name: 'MHA (Attn)', inputSize: [128], iters: 10,
114
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
115
+ {z:0,y:0,x:0,l:0,type:"MHA",input_height:128,output_height:128,num_heads:4,d_model:128,dtype:"F32"}
116
+ ]})
117
+ },
118
+ {
119
+ name: 'SwiGLU (MLP)', inputSize: [512], iters: 10,
120
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
121
+ {z:0,y:0,x:0,l:0,type:"SwiGLU",input_height:512,output_height:1024,dtype:"F32"}
122
+ ]})
123
+ },
124
+ {
125
+ name: 'Residual Add', inputSize: [1024], iters: 30,
126
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
127
+ {z:0,y:0,x:0,l:0,type:"Residual",input_height:1024,output_height:1024,dtype:"F32"}
128
+ ]})
129
+ },
130
+ ];
131
+
132
+ function makeInput(sizes, isEmbedding) {
133
+ const total = sizes.reduce((a,b) => a*b, 1);
134
+ const arr = new Float32Array(total);
135
+ if (isEmbedding) {
136
+ for (let i = 0; i < arr.length; i++) arr[i] = i % 2048;
137
+ } else {
138
+ arr.fill(0.5);
139
+ }
140
+ return arr;
141
+ }
142
+
143
+ async function runBenchFor(bc, useGPU, sysState) {
144
+ const net = window.createLoomNetwork(bc.cfg);
145
+ const input = makeInput(bc.inputSize, bc.isEmbedding);
146
+
147
+ if (useGPU && !sysState) {
148
+ // GPU path via systolic state
149
+ try {
150
+ await net.initGPU();
151
+ await net.syncToGPU();
152
+ } catch(e) { net.free(); return { ms: -1, sample: null }; }
153
+ }
154
+
155
+ // warm-up
156
+ net.sequentialForward(input);
157
+
158
+ const t0 = performance.now();
159
+ let lastOut;
160
+ for (let i = 0; i < bc.iters; i++) {
161
+ lastOut = net.sequentialForward(input);
162
+ }
163
+ const elapsed = (performance.now() - t0) / bc.iters;
164
+ const sample = lastOut ? [lastOut[0], lastOut[1], lastOut[2]] : null;
165
+ net.free();
166
+ return { ms: elapsed, sample };
167
+ }
168
+
169
+ function diffLabel(a, b) {
170
+ if (!a || !b) return 'N/A ';
171
+ let max = 0;
172
+ for (let i = 0; i < Math.min(a.length, b.length, 100); i++) {
173
+ const d = Math.abs(a[i] - b[i]);
174
+ if (d > max) max = d;
175
+ }
176
+ if (max < 1e-7) return 'EXACT ⭐';
177
+ if (max < 1e-4) return 'OK ✅ ';
178
+ if (max < 1e-2) return 'OFF ⚠️ ';
179
+ return 'BROKEN❌';
180
+ }
181
+
182
+ async function runBench(tryGPU = false) {
183
+ outEl.innerHTML = '';
184
+ document.getElementById('runBtn').disabled = true;
185
+ await loadWASM();
186
+
187
+ pInfo('=== M-POLY-VTD Performance Showdown: CPU vs WebGPU (WASM) ===\n');
188
+ p('Layer sizes mirror benchmark_tiling.go. Timings are per-iteration averages.');
189
+ p('');
190
+
191
+ // GPU init check
192
+ let gpuAvail = false;
193
+ if (tryGPU && navigator.gpu) {
194
+ try {
195
+ const adapter = await navigator.gpu.requestAdapter();
196
+ gpuAvail = !!adapter;
197
+ } catch(e) {}
198
+ }
199
+ if (tryGPU) {
200
+ if (gpuAvail) pPass('WebGPU available.');
201
+ else pWarn('WebGPU not available — showing CPU-only results.');
202
+ }
203
+ p('');
204
+
205
+ const cols = ['Layer type'.padEnd(16), 'CPU (ms/it)'.padEnd(13), 'Sample[0]'.padEnd(12), 'Sanity'];
206
+ p('| ' + cols.join(' | ') + ' |');
207
+ p('|' + '-'.repeat(17) + '|' + '-'.repeat(14) + '|' + '-'.repeat(13) + '|' + '-'.repeat(10) + '|');
208
+
209
+ const samples = [];
210
+
211
+ for (const bc of BENCH_CASES) {
212
+ document.getElementById('status').textContent = 'Running: ' + bc.name + '...';
213
+ await new Promise(r => setTimeout(r, 0)); // yield to browser
214
+
215
+ const { ms, sample } = await runBenchFor(bc, false);
216
+ const sanity = sample && sample.some(v => Math.abs(v) > 1e-9) ? 'REAL 💎 ' : 'ZERO 💀 ';
217
+ const msStr = ms.toFixed(3).padEnd(12);
218
+ const s0 = sample ? sample[0].toFixed(4).padEnd(11) : 'N/A'.padEnd(11);
219
+
220
+ samples.push({ name: bc.name, sample });
221
+ p(`| ${bc.name.padEnd(15)} | ${msStr} | ${s0} | ${sanity} |`);
222
+ }
223
+
224
+ p('');
225
+ pInfo('=== Output Sample Comparison (first 3 values) ===\n');
226
+ p('| Layer'.padEnd(18) + '| Sample[0..2]');
227
+ p('|' + '-'.repeat(17) + '|' + '-'.repeat(45));
228
+ for (const s of samples) {
229
+ const vals = s.sample ? s.sample.map(v => v.toFixed(4)).join(', ') : 'N/A';
230
+ p(`| ${s.name.padEnd(15)} | ${vals}`);
231
+ }
232
+
233
+ document.getElementById('status').textContent = 'Done.';
234
+ document.getElementById('runBtn').disabled = false;
235
+ }
236
+
237
+ window.addEventListener('load', async () => {
238
+ await loadWASM();
239
+ document.getElementById('status').textContent = wasmReady
240
+ ? 'WASM ready. Click Run Benchmark.' : 'WASM load failed.';
241
+ });
242
+ </script>
243
+ </body>
244
+ </html>
@@ -0,0 +1,249 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Loom — Training Benchmark</title>
6
+ <style>
7
+ * { box-sizing: border-box; margin: 0; padding: 0; }
8
+ body { background: #0d0d0d; color: #c8c8c8; font-family: 'Courier New', monospace; font-size: 13px; padding: 20px; }
9
+ h1 { color: #7ec8e3; margin-bottom: 4px; font-size: 16px; }
10
+ .subtitle { color: #555; margin-bottom: 12px; font-size: 11px; }
11
+ #output { white-space: pre; line-height: 1.65; }
12
+ .pass { color: #4ec94e; }
13
+ .fail { color: #e04040; }
14
+ .warn { color: #e0a020; }
15
+ .info { color: #7ec8e3; }
16
+ .dim { color: #555; }
17
+ button { background: #1e3a4a; color: #7ec8e3; border: 1px solid #2d5a70; padding: 6px 18px;
18
+ font-family: inherit; font-size: 13px; cursor: pointer; margin-right: 8px; }
19
+ button:hover { background: #2d5a70; }
20
+ button:disabled { opacity: 0.4; cursor: not-allowed; }
21
+ #status { margin: 8px 0; color: #555; font-size: 11px; }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <h1>=== M-POLY-VTD Training Showdown: Forward Pass Performance (WASM) ===</h1>
26
+ <div class="subtitle">Browser port of benchmark_training.go — measures per-layer forward+train timing across 9 layer types</div>
27
+ <button id="runBtn" onclick="runBench()">Run Benchmark</button>
28
+ <div id="status">Loading WASM...</div>
29
+ <pre id="output"></pre>
30
+
31
+ <script src="wasm_exec.js"></script>
32
+ <script>
33
+ let wasmReady = false;
34
+ const outEl = document.getElementById('output');
35
+
36
+ function print(msg, cls) {
37
+ const s = document.createElement('span');
38
+ if (cls) s.className = cls;
39
+ s.textContent = msg + '\n';
40
+ outEl.appendChild(s);
41
+ }
42
+ const p = m => print(m);
43
+ const pInfo= m => print(m,'info');
44
+ const pPass= m => print(m,'pass');
45
+ const pFail= m => print(m,'fail');
46
+ const pWarn= m => print(m,'warn');
47
+ const pDim = m => print(m,'dim');
48
+
49
+ async function loadWASM() {
50
+ if (wasmReady) return;
51
+ const go = new Go();
52
+ const r = await WebAssembly.instantiateStreaming(fetch('main.wasm'), go.importObject);
53
+ go.run(r.instance);
54
+ await new Promise(res => setTimeout(res, 150));
55
+ wasmReady = true;
56
+ }
57
+
58
+ // ─────────────────────────────────────────────────────────────
59
+ // 9 layer types matching benchmark_training.go
60
+ // ─────────────────────────────────────────────────────────────
61
+ const TRAINING_CASES = [
62
+ {
63
+ name: 'Dense (Linear)', iters: 5, inDim: 512, outDim: 512,
64
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
65
+ {z:0,y:0,x:0,l:0,type:"Dense",input_height:512,output_height:512,activation:"Linear",dtype:"F32"}
66
+ ]})
67
+ },
68
+ {
69
+ name: 'RMSNorm', iters: 5, inDim: 512, outDim: 512,
70
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
71
+ {z:0,y:0,x:0,l:0,type:"RMSNorm",input_height:512,output_height:512,dtype:"F32"}
72
+ ]})
73
+ },
74
+ {
75
+ name: 'SwiGLU (MLP)', iters: 5, inDim: 512, outDim: 1024,
76
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
77
+ {z:0,y:0,x:0,l:0,type:"SwiGLU",input_height:512,output_height:1024,dtype:"F32"}
78
+ ]})
79
+ },
80
+ {
81
+ name: 'Embedding', iters: 5, inDim: 16, outDim: 2048, isEmbedding: true,
82
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
83
+ {z:0,y:0,x:0,l:0,type:"Embedding",vocab_size:1024,embedding_dim:128,dtype:"F32"}
84
+ ]})
85
+ },
86
+ {
87
+ name: 'Residual Add', iters: 5, inDim: 512, outDim: 512,
88
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
89
+ {z:0,y:0,x:0,l:0,type:"Residual",input_height:512,output_height:512,dtype:"F32"}
90
+ ]})
91
+ },
92
+ {
93
+ name: 'MHA (Fused)', iters: 5, inDim: 128, outDim: 128,
94
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
95
+ {z:0,y:0,x:0,l:0,type:"MHA",input_height:128,output_height:128,num_heads:4,d_model:128,dtype:"F32"}
96
+ ]})
97
+ },
98
+ {
99
+ name: 'CNN1D', iters: 5, inDim: 192, outDim: 512,
100
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
101
+ {z:0,y:0,x:0,l:0,type:"CNN1",input_channels:3,filters:8,kernel_size:3,stride:1,padding:1,input_height:64,output_height:64,dtype:"F32"}
102
+ ]})
103
+ },
104
+ {
105
+ name: 'CNN2D', iters: 5, inDim: 3072, outDim: 8192,
106
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
107
+ {z:0,y:0,x:0,l:0,type:"CNN2",input_channels:3,filters:8,kernel_size:3,stride:1,padding:1,input_height:32,input_width:32,output_height:32,output_width:32,dtype:"F32"}
108
+ ]})
109
+ },
110
+ {
111
+ name: 'CNN3D', iters: 5, inDim: 12288, outDim: 16384,
112
+ cfg: JSON.stringify({ depth:1,rows:1,cols:1,layers_per_cell:1, layers:[
113
+ {z:0,y:0,x:0,l:0,type:"CNN3",input_channels:3,filters:4,kernel_size:3,stride:1,padding:1,input_depth:16,input_height:16,input_width:16,output_depth:16,output_height:16,output_width:16,dtype:"F32"}
114
+ ]})
115
+ },
116
+ ];
117
+
118
+ function makeTrainBatches(inDim, outDim, nBatches, batchSize, isEmbedding) {
119
+ const batches = [];
120
+ for (let b = 0; b < nBatches; b++) {
121
+ const inp = new Float32Array(batchSize * inDim);
122
+ const tgt = new Float32Array(batchSize * outDim);
123
+ if (isEmbedding) {
124
+ for (let i = 0; i < inp.length; i++) inp[i] = i % 1024;
125
+ } else {
126
+ for (let i = 0; i < inp.length; i++) inp[i] = (Math.random() * 2 - 1) * 0.5;
127
+ }
128
+ for (let i = 0; i < tgt.length; i++) tgt[i] = Math.random() * 0.1;
129
+ batches.push({
130
+ input: { shape: [batchSize, inDim], data: Array.from(inp) },
131
+ target: { shape: [batchSize, outDim], data: Array.from(tgt) }
132
+ });
133
+ }
134
+ return batches;
135
+ }
136
+
137
+ function formatDiff(d) {
138
+ if (d < 0) return 'N/A ';
139
+ if (d < 1e-7) return 'EXACT⭐';
140
+ if (d < 1e-4) return 'OK ✅ ';
141
+ if (d < 1e-2) return 'OFF ⚠️ ';
142
+ return 'BROKEN❌';
143
+ }
144
+
145
+ async function runCase(tc) {
146
+ const net = window.createLoomNetwork(tc.cfg);
147
+ const batchSize = 4;
148
+ const nBatches = 4;
149
+ const epochs = 3;
150
+
151
+ // Build simple batches JSON for the train() method
152
+ const batches = makeTrainBatches(tc.inDim, tc.outDim, nBatches, batchSize, tc.isEmbedding);
153
+ const batchesJSON = JSON.stringify(batches);
154
+
155
+ // Forward timing (sequentialForward × iters)
156
+ const input = new Float32Array(tc.inDim);
157
+ input.fill(0.5);
158
+ if (tc.isEmbedding) for (let i = 0; i < input.length; i++) input[i] = i % 1024;
159
+
160
+ // warm-up
161
+ net.sequentialForward(input);
162
+
163
+ const t0 = performance.now();
164
+ let lastOut;
165
+ for (let i = 0; i < tc.iters; i++) {
166
+ lastOut = net.sequentialForward(input);
167
+ }
168
+ const fwdMs = (performance.now() - t0) / tc.iters;
169
+
170
+ // Training timing
171
+ let trainMs = -1;
172
+ let initialLoss = null, finalLoss = null;
173
+ try {
174
+ const t1 = performance.now();
175
+ const trainResult = await net.train(batchesJSON, epochs, 0.001);
176
+ trainMs = performance.now() - t1;
177
+ if (typeof trainResult === 'string') {
178
+ try {
179
+ const r = JSON.parse(trainResult);
180
+ if (r.loss_history && r.loss_history.length > 0) {
181
+ initialLoss = r.loss_history[0];
182
+ finalLoss = r.loss_history[r.loss_history.length - 1];
183
+ }
184
+ } catch(e) {}
185
+ }
186
+ } catch(e) {
187
+ trainMs = -1;
188
+ }
189
+
190
+ const sample = lastOut ? [lastOut[0]||0, lastOut[1]||0, lastOut[2]||0] : null;
191
+ const sanity = sample && sample.some(v => Math.abs(v) > 1e-9);
192
+ net.free();
193
+ return { fwdMs, trainMs, sample, sanity, initialLoss, finalLoss };
194
+ }
195
+
196
+ async function runBench() {
197
+ outEl.innerHTML = '';
198
+ document.getElementById('runBtn').disabled = true;
199
+ await loadWASM();
200
+
201
+ pInfo('=== M-POLY-VTD Training Showdown: CPU vs GPU Backward Pass ===\n');
202
+ p('9 layer types | Forward + Training timing | Loss convergence');
203
+ p('(GPU backward: requires initGPU() — forward-only GPU not shown here)');
204
+ p('');
205
+
206
+ p('| ' + 'Layer'.padEnd(15) + ' | ' + 'Fwd ms/it'.padEnd(11) + ' | ' + 'Train ms'.padEnd(10) +
207
+ ' | ' + 'Init Loss'.padEnd(11) + ' | ' + 'Final Loss'.padEnd(11) + ' | Sanity |');
208
+ p('|' + '-'.repeat(16) + '|' + '-'.repeat(12) + '|' + '-'.repeat(11) + '|' +
209
+ '-'.repeat(12) + '|' + '-'.repeat(12) + '|' + '-'.repeat(10) + '|');
210
+
211
+ const allSamples = [];
212
+
213
+ for (const tc of TRAINING_CASES) {
214
+ document.getElementById('status').textContent = 'Running: ' + tc.name + '...';
215
+ await new Promise(r => setTimeout(r, 0));
216
+
217
+ const res = await runCase(tc);
218
+ allSamples.push({ name: tc.name, sample: res.sample });
219
+
220
+ const fwdStr = res.fwdMs >= 0 ? res.fwdMs.toFixed(3).padEnd(10) : 'N/A'.padEnd(10);
221
+ const trainStr = res.trainMs >= 0 ? res.trainMs.toFixed(1).padEnd(9) : 'N/A'.padEnd(9);
222
+ const iLoss = res.initialLoss != null ? res.initialLoss.toFixed(4).padEnd(10) : 'N/A'.padEnd(10);
223
+ const fLoss = res.finalLoss != null ? res.finalLoss.toFixed(4).padEnd(10) : 'N/A'.padEnd(10);
224
+ const sanStr = res.sanity ? 'REAL 💎 ' : 'ZERO 💀 ';
225
+
226
+ p(`| ${tc.name.padEnd(15)} | ${fwdStr} | ${trainStr} | ${iLoss} | ${fLoss} | ${sanStr}|`);
227
+ }
228
+
229
+ p('');
230
+ pInfo('=== Output Sample: forward pass first 3 values ===\n');
231
+ p('| ' + 'Layer'.padEnd(15) + ' | Sample[0..2]');
232
+ p('|' + '-'.repeat(16) + '|' + '-'.repeat(40));
233
+ for (const s of allSamples) {
234
+ const vals = s.sample ? s.sample.map(v => v.toFixed(5)).join(', ') : 'N/A';
235
+ p(`| ${s.name.padEnd(15)} | ${vals}`);
236
+ }
237
+
238
+ document.getElementById('status').textContent = 'Done.';
239
+ document.getElementById('runBtn').disabled = false;
240
+ }
241
+
242
+ window.addEventListener('load', async () => {
243
+ await loadWASM();
244
+ document.getElementById('status').textContent = wasmReady
245
+ ? 'WASM ready. Click Run Benchmark.' : 'WASM load failed.';
246
+ });
247
+ </script>
248
+ </body>
249
+ </html>