@nadrif/thread 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.
@@ -0,0 +1,493 @@
1
+ /**
2
+ * @file Multi-pass GPU operations — reductions, matmul, histogram, argmax/min, scan.
3
+ *
4
+ * These are standalone functions that receive a {@link GPUCompute} instance
5
+ * as their first argument, keeping the main class focused on single-pass compute.
6
+ *
7
+ * @module gpu/special
8
+ */
9
+
10
+ import { GPUComputeError } from '../error.js';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Dispatcher
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Execute a special multi-pass operation.
18
+ *
19
+ * @param {import('./gpu.js').GPUCompute} gpu
20
+ * @param {string} name - Operation name.
21
+ * @param {Object} specialDef - Special op definition from SPECIAL_OPS.
22
+ * @param {Object} input - User input.
23
+ * @returns {Promise<Object<string, TypedArray>>}
24
+ */
25
+ export async function runSpecial(gpu, name, specialDef, input) {
26
+ const { signal } = input;
27
+
28
+ if (signal?.aborted) {
29
+ throw new GPUComputeError('Operation cancelled', signal.reason ?? new DOMException('Aborted', 'AbortError'));
30
+ }
31
+
32
+ if (specialDef.special === 'matmul') {
33
+ return runMatmul(gpu, input);
34
+ }
35
+
36
+ if (specialDef.special === 'histogram') {
37
+ return runHistogram(gpu, input);
38
+ }
39
+
40
+ if (specialDef.special === 'argmax') {
41
+ return runArgMaxMin(gpu, 'argmax', input);
42
+ }
43
+
44
+ if (specialDef.special === 'argmin') {
45
+ return runArgMaxMin(gpu, 'argmin', input);
46
+ }
47
+
48
+ if (specialDef.special === 'scan') {
49
+ return runScan(gpu, input);
50
+ }
51
+
52
+ // Reduction ops (reduce_sum, reduce_min, reduce_max)
53
+ if (specialDef.special?.startsWith('reduce_')) {
54
+ return runReduce(gpu, specialDef.special, input);
55
+ }
56
+
57
+ throw new GPUComputeError(`Unknown special op: "${name}"`);
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Matrix multiply
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /**
65
+ * Execute a matrix multiply.
66
+ *
67
+ * @param {import('./gpu.js').GPUCompute} gpu
68
+ * @param {Object} input - { inputs: { A, B }, uniforms: { M, N, K } }
69
+ * @returns {Promise<Object<string, TypedArray>>}
70
+ */
71
+ export async function runMatmul(gpu, input) {
72
+ const { A, B } = input.inputs || {};
73
+ const { M, N, K } = input.uniforms || {};
74
+
75
+ if (!A || !B) throw new GPUComputeError('matmul requires inputs A and B');
76
+ if (!M || !N || !K) throw new GPUComputeError('matmul requires uniforms M, N, K');
77
+
78
+ const mVal = M[0], nVal = N[0], kVal = K[0];
79
+
80
+ // Build WGSL for this specific M, N, K
81
+ const wgsl = buildMatmulShader(mVal, nVal, kVal);
82
+ const pipelineName = `_matmul_${mVal}x${nVal}x${kVal}`;
83
+
84
+ if (!gpu._pipelines.has(pipelineName)) {
85
+ gpu._pipelines.set(pipelineName, { shader: wgsl, pipeline: null, entryPoint: 'main' });
86
+ if (gpu._isInitialised && gpu._device) {
87
+ const entry = gpu._pipelines.get(pipelineName);
88
+ entry.pipeline = await gpu._compileShader(wgsl, 'main', pipelineName);
89
+ }
90
+ }
91
+
92
+ gpu.setActive(pipelineName);
93
+
94
+ const result = await gpu.compute({
95
+ inputs: { A, B },
96
+ uniforms: new Float32Array([mVal, nVal, kVal]),
97
+ outputBuffers: { C: mVal * nVal },
98
+ workgroups: [Math.ceil(nVal / 16), Math.ceil(mVal / 16), 1],
99
+ });
100
+
101
+ return { result: result.C };
102
+ }
103
+
104
+ /**
105
+ * Build a WGSL shader for matrix multiply with specific dimensions.
106
+ *
107
+ * @param {number} M - Rows of A / C.
108
+ * @param {number} N - Columns of B / C.
109
+ * @param {number} K - Columns of A / Rows of B.
110
+ * @returns {string} WGSL source.
111
+ */
112
+ export function buildMatmulShader(M, N, K) {
113
+ return `
114
+ @group(0) @binding(0) var<storage, read> A: array<f32>;
115
+ @group(0) @binding(1) var<storage, read> B: array<f32>;
116
+ @group(0) @binding(2) var<uniform> dims: vec3<f32>;
117
+ @group(0) @binding(3) var<storage, read_write> C: array<f32>;
118
+
119
+ @compute @workgroup_size(16, 16)
120
+ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
121
+ let row = id.y;
122
+ let col = id.x;
123
+ let m = u32(dims.x);
124
+ let n = u32(dims.y);
125
+ let k = u32(dims.z);
126
+
127
+ if (row >= m || col >= n) { return; }
128
+
129
+ var sum: f32 = 0.0;
130
+ for (var i = 0u; i < k; i++) {
131
+ sum += A[row * k + i] * B[i * n + col];
132
+ }
133
+ C[row * n + col] = sum;
134
+ }
135
+ `;
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Reduction (sum, min, max)
140
+ // ---------------------------------------------------------------------------
141
+
142
+ /**
143
+ * Execute a reduction operation (sum, min, max).
144
+ *
145
+ * Two-pass approach:
146
+ * 1. GPU reduces each workgroup into a partial result.
147
+ * 2. CPU combines the partial results.
148
+ *
149
+ * @param {import('./gpu.js').GPUCompute} gpu
150
+ * @param {string} opName - 'reduce_sum', 'reduce_min', or 'reduce_max'.
151
+ * @param {Object} input - { inputs: { data } }
152
+ * @returns {Promise<Object<string, TypedArray>>}
153
+ */
154
+ export async function runReduce(gpu, opName, input) {
155
+ const { signal } = input;
156
+ const data = input.inputs?.data;
157
+ if (!data) throw new GPUComputeError(`reduce requires inputs.data`);
158
+
159
+ if (signal?.aborted) {
160
+ throw new GPUComputeError('Operation cancelled', signal.reason ?? new DOMException('Aborted', 'AbortError'));
161
+ }
162
+
163
+ const count = data.byteLength / (data.BYTES_PER_ELEMENT || 4);
164
+ if (count === 0) {
165
+ const identity = opName === 'reduce_min' ? Infinity : opName === 'reduce_max' ? -Infinity : 0;
166
+ return { result: new Float32Array([identity]) };
167
+ }
168
+
169
+ const workgroupSize = 256;
170
+ const numWorkgroups = Math.ceil(count / workgroupSize);
171
+
172
+ // Build the reduction shader
173
+ const reduceBody = getReduceBody(opName);
174
+ const wgsl = `
175
+ @group(0) @binding(0) var<storage, read> data: array<f32>;
176
+ @group(0) @binding(1) var<uniform> count: vec4<u32>;
177
+ @group(0) @binding(2) var<storage, read_write> partial: array<f32>;
178
+
179
+ var<workgroup> shared_data: array<f32, 256>;
180
+
181
+ @compute @workgroup_size(256)
182
+ fn main(@builtin(global_invocation_id) id: vec3<u32>, @builtin(local_invocation_id) local_id: vec3<u32>, @builtin(workgroup_id) wg_id: vec3<u32>) {
183
+ let tid = local_id.x;
184
+ let i = id.x;
185
+ let n = count.x;
186
+
187
+ if (i < n) {
188
+ shared_data[tid] = data[i];
189
+ } else {
190
+ shared_data[tid] = ${reduceBody.identity};
191
+ }
192
+ workgroupBarrier();
193
+
194
+ for (var stride = 128u; stride > 0u; stride >>= 1u) {
195
+ if (tid < stride) {
196
+ shared_data[tid] = ${reduceBody.op};
197
+ }
198
+ workgroupBarrier();
199
+ }
200
+
201
+ if (tid == 0u) {
202
+ partial[wg_id.x] = shared_data[0];
203
+ }
204
+ }
205
+ `;
206
+
207
+ const pipelineName = `_reduce_${opName}`;
208
+ if (!gpu._pipelines.has(pipelineName)) {
209
+ gpu._pipelines.set(pipelineName, { shader: wgsl, pipeline: null, entryPoint: 'main' });
210
+ if (gpu._isInitialised && gpu._device) {
211
+ const entry = gpu._pipelines.get(pipelineName);
212
+ entry.pipeline = await gpu._compileShader(wgsl, 'main', pipelineName);
213
+ }
214
+ }
215
+
216
+ gpu.setActive(pipelineName);
217
+
218
+ // Pass 1: GPU reduction
219
+ const partial = await gpu.compute({
220
+ inputs: { data },
221
+ uniforms: new Uint32Array([count, 0, 0, 0]),
222
+ outputBuffers: { partial: numWorkgroups },
223
+ workgroups: [numWorkgroups, 1, 1],
224
+ });
225
+
226
+ // Pass 2: CPU combine
227
+ const partials = partial.partial;
228
+ let result;
229
+ switch (opName) {
230
+ case 'reduce_sum':
231
+ result = 0;
232
+ for (let i = 0; i < partials.length; i++) result += partials[i];
233
+ break;
234
+ case 'reduce_min':
235
+ result = Infinity;
236
+ for (let i = 0; i < partials.length; i++) result = Math.min(result, partials[i]);
237
+ break;
238
+ case 'reduce_max':
239
+ result = -Infinity;
240
+ for (let i = 0; i < partials.length; i++) result = Math.max(result, partials[i]);
241
+ break;
242
+ }
243
+
244
+ return { result: new Float32Array([result]) };
245
+ }
246
+
247
+ /**
248
+ * Get the reduction operation body for a given op name.
249
+ *
250
+ * @param {string} opName
251
+ * @returns {{ op: string, identity: string }}
252
+ */
253
+ export function getReduceBody(opName) {
254
+ switch (opName) {
255
+ case 'reduce_sum':
256
+ return { op: 'shared_data[tid] + shared_data[tid + stride]', identity: '0.0' };
257
+ case 'reduce_min':
258
+ return { op: 'min(shared_data[tid], shared_data[tid + stride])', identity: '3.4028235e+38' };
259
+ case 'reduce_max':
260
+ return { op: 'max(shared_data[tid], shared_data[tid + stride])', identity: '-3.4028235e+38' };
261
+ default:
262
+ throw new GPUComputeError(`Unknown reduce op: "${opName}"`);
263
+ }
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Histogram
268
+ // ---------------------------------------------------------------------------
269
+
270
+ /**
271
+ * Execute a histogram operation.
272
+ *
273
+ * Maps float data into bins and counts occurrences.
274
+ * The data range [0, 1000] is mapped to [0, numBins).
275
+ *
276
+ * @param {import('./gpu.js').GPUCompute} gpu
277
+ * @param {Object} input - { inputs: { data }, uniforms?: { numBins } }
278
+ * @returns {Promise<{ bins: Uint32Array, count: number }>}
279
+ */
280
+ export async function runHistogram(gpu, input) {
281
+ const { signal } = input;
282
+ const data = input.inputs?.data;
283
+ if (!data) throw new GPUComputeError('histogram requires inputs.data');
284
+
285
+ if (signal?.aborted) {
286
+ throw new GPUComputeError('Operation cancelled', signal.reason ?? new DOMException('Aborted', 'AbortError'));
287
+ }
288
+
289
+ const count = data.byteLength / (data.BYTES_PER_ELEMENT || 4);
290
+ const numBins = input.uniforms?.numBins?.[0] || 256;
291
+
292
+ if (count === 0) {
293
+ return { bins: new Uint32Array(numBins), count: 0 };
294
+ }
295
+
296
+ const workgroupSize = 256;
297
+ const numWorkgroups = Math.ceil(count / workgroupSize);
298
+
299
+ const wgsl = `
300
+ @group(0) @binding(0) var<storage, read> data: array<f32>;
301
+ @group(0) @binding(1) var<uniform> params: vec4<u32>;
302
+ @group(0) @binding(2) var<storage, read_write> bins: array<u32>;
303
+
304
+ var<workgroup> local_bins: array<u32, 256>;
305
+
306
+ @compute @workgroup_size(256)
307
+ fn main(@builtin(global_invocation_id) id: vec3<u32>, @builtin(local_invocation_id) local_id: vec3<u32>, @builtin(workgroup_id) wg_id: vec3<u32>) {
308
+ let tid = local_id.x;
309
+ let i = id.x;
310
+ let n = params.x;
311
+ let nb = params.y;
312
+
313
+ if (tid < 256u) { local_bins[tid] = 0u; }
314
+ workgroupBarrier();
315
+
316
+ if (i < n && nb > 0u) {
317
+ let bin = clamp(u32(f32(data[i]) / 1000.0 * f32(nb)), 0u, nb - 1u);
318
+ atomicAdd(&local_bins[bin], 1u);
319
+ }
320
+ workgroupBarrier();
321
+
322
+ if (tid < nb && tid < 256u) {
323
+ atomicAdd(&bins[tid], local_bins[tid]);
324
+ }
325
+ }
326
+ `;
327
+
328
+ const pipelineName = '_histogram';
329
+ if (!gpu._pipelines.has(pipelineName)) {
330
+ gpu._pipelines.set(pipelineName, { shader: wgsl, pipeline: null, entryPoint: 'main' });
331
+ if (gpu._isInitialised && gpu._device) {
332
+ const entry = gpu._pipelines.get(pipelineName);
333
+ entry.pipeline = await gpu._compileShader(wgsl, 'main', pipelineName);
334
+ }
335
+ }
336
+
337
+ gpu.setActive(pipelineName);
338
+
339
+ const result = await gpu.compute({
340
+ inputs: { data },
341
+ uniforms: new Uint32Array([count, numBins, 0, 0]),
342
+ outputBuffers: { bins: numBins },
343
+ workgroups: [numWorkgroups, 1, 1],
344
+ });
345
+
346
+ return { bins: new Uint32Array(result.bins), count };
347
+ }
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // ArgMax / ArgMin
351
+ // ---------------------------------------------------------------------------
352
+
353
+ /**
354
+ * Execute an argmax or argmin operation.
355
+ *
356
+ * Two-pass: GPU finds per-workgroup extreme + index, CPU combines.
357
+ *
358
+ * @param {import('./gpu.js').GPUCompute} gpu
359
+ * @param {string} opName - 'argmax' or 'argmin'.
360
+ * @param {Object} input - { inputs: { data } }
361
+ * @returns {Promise<{ value: number, index: number }>}
362
+ */
363
+ export async function runArgMaxMin(gpu, opName, input) {
364
+ const { signal } = input;
365
+ const data = input.inputs?.data;
366
+ if (!data) throw new GPUComputeError(`${opName} requires inputs.data`);
367
+
368
+ if (signal?.aborted) {
369
+ throw new GPUComputeError('Operation cancelled', signal.reason ?? new DOMException('Aborted', 'AbortError'));
370
+ }
371
+
372
+ const count = data.byteLength / (data.BYTES_PER_ELEMENT || 4);
373
+ if (count === 0) {
374
+ return { value: opName === 'argmax' ? -Infinity : Infinity, index: -1 };
375
+ }
376
+
377
+ const workgroupSize = 256;
378
+ const numWorkgroups = Math.ceil(count / workgroupSize);
379
+ const isMax = opName === 'argmax';
380
+
381
+ const wgsl = `
382
+ @group(0) @binding(0) var<storage, read> data: array<f32>;
383
+ @group(0) @binding(1) var<uniform> count: vec4<u32>;
384
+ @group(0) @binding(2) var<storage, read_write> partial_val: array<f32>;
385
+ @group(0) @binding(3) var<storage, read_write> partial_idx: array<u32>;
386
+
387
+ var<workgroup> shared_val: array<f32, 256>;
388
+ var<workgroup> shared_idx: array<u32, 256>;
389
+
390
+ @compute @workgroup_size(256)
391
+ fn main(@builtin(global_invocation_id) id: vec3<u32>, @builtin(local_invocation_id) local_id: vec3<u32>, @builtin(workgroup_id) wg_id: vec3<u32>) {
392
+ let tid = local_id.x;
393
+ let i = id.x;
394
+ let n = count.x;
395
+
396
+ if (i < n) {
397
+ shared_val[tid] = data[i];
398
+ shared_idx[tid] = u32(i);
399
+ } else {
400
+ shared_val[tid] = ${isMax ? '-3.4028235e+38' : '3.4028235e+38'};
401
+ shared_idx[tid] = 0u;
402
+ }
403
+ workgroupBarrier();
404
+
405
+ for (var stride = 128u; stride > 0u; stride >>= 1u) {
406
+ if (tid < stride) {
407
+ let shouldSwap = ${isMax
408
+ ? 'shared_val[tid + stride] > shared_val[tid]'
409
+ : 'shared_val[tid + stride] < shared_val[tid]'};
410
+ if (shouldSwap) {
411
+ shared_val[tid] = shared_val[tid + stride];
412
+ shared_idx[tid] = shared_idx[tid + stride];
413
+ }
414
+ }
415
+ workgroupBarrier();
416
+ }
417
+
418
+ if (tid == 0u) {
419
+ partial_val[wg_id.x] = shared_val[0];
420
+ partial_idx[wg_id.x] = shared_idx[0];
421
+ }
422
+ }
423
+ `;
424
+
425
+ const pipelineName = `_${opName}`;
426
+ if (!gpu._pipelines.has(pipelineName)) {
427
+ gpu._pipelines.set(pipelineName, { shader: wgsl, pipeline: null, entryPoint: 'main' });
428
+ if (gpu._isInitialised && gpu._device) {
429
+ const entry = gpu._pipelines.get(pipelineName);
430
+ entry.pipeline = await gpu._compileShader(wgsl, 'main', pipelineName);
431
+ }
432
+ }
433
+
434
+ gpu.setActive(pipelineName);
435
+
436
+ const result = await gpu.compute({
437
+ inputs: { data },
438
+ uniforms: new Uint32Array([count, 0, 0, 0]),
439
+ outputBuffers: { partial_val: numWorkgroups, partial_idx: numWorkgroups },
440
+ workgroups: [numWorkgroups, 1, 1],
441
+ });
442
+
443
+ // CPU combine
444
+ const vals = result.partial_val;
445
+ const idxs = result.partial_idx;
446
+ let bestVal = isMax ? -Infinity : Infinity;
447
+ let bestIdx = 0;
448
+ for (let i = 0; i < vals.length; i++) {
449
+ if (isMax ? vals[i] > bestVal : vals[i] < bestVal) {
450
+ bestVal = vals[i];
451
+ bestIdx = idxs[i];
452
+ }
453
+ }
454
+
455
+ return { value: bestVal, index: bestIdx };
456
+ }
457
+
458
+ // ---------------------------------------------------------------------------
459
+ // Scan (parallel prefix sum)
460
+ // ---------------------------------------------------------------------------
461
+
462
+ /**
463
+ * Execute a prefix sum (scan) operation.
464
+ *
465
+ * Computes inclusive prefix sum. Currently CPU-only for arbitrary sizes.
466
+ *
467
+ * @param {import('./gpu.js').GPUCompute} gpu
468
+ * @param {Object} input - { inputs: { data } }
469
+ * @returns {Promise<{ result: Float32Array }>}
470
+ */
471
+ export async function runScan(gpu, input) {
472
+ const { signal } = input;
473
+ const data = input.inputs?.data;
474
+ if (!data) throw new GPUComputeError('scan requires inputs.data');
475
+
476
+ if (signal?.aborted) {
477
+ throw new GPUComputeError('Operation cancelled', signal.reason ?? new DOMException('Aborted', 'AbortError'));
478
+ }
479
+
480
+ const count = data.byteLength / (data.BYTES_PER_ELEMENT || 4);
481
+ if (count === 0) {
482
+ return { result: new Float32Array(0) };
483
+ }
484
+
485
+ // For simplicity, do the scan on CPU (GPU scan is very complex for arbitrary sizes)
486
+ const result = new Float32Array(count);
487
+ result[0] = data[0];
488
+ for (let i = 1; i < count; i++) {
489
+ result[i] = result[i - 1] + data[i];
490
+ }
491
+
492
+ return { result };
493
+ }