@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.
- package/LICENSE +21 -0
- package/README.md +1156 -0
- package/package.json +133 -0
- package/src/adapters.js +404 -0
- package/src/config/adapters.js +188 -0
- package/src/config/define.js +171 -0
- package/src/config/env.js +218 -0
- package/src/config/frameworks.js +268 -0
- package/src/config/index.js +256 -0
- package/src/config/schema.js +375 -0
- package/src/config.js +52 -0
- package/src/deno.js +29 -0
- package/src/edge.js +38 -0
- package/src/env.js +361 -0
- package/src/error.js +340 -0
- package/src/factory.js +591 -0
- package/src/gpu/adapters.js +227 -0
- package/src/gpu/chains.js +261 -0
- package/src/gpu/env.js +372 -0
- package/src/gpu/gpu.js +1199 -0
- package/src/gpu/helpers.js +240 -0
- package/src/gpu/hooks.js +284 -0
- package/src/gpu/index.js +16 -0
- package/src/gpu/shaders.js +834 -0
- package/src/gpu/special.js +493 -0
- package/src/hooks.js +448 -0
- package/src/index.js +557 -0
- package/src/metrix.js +222 -0
- package/src/node.js +36 -0
- package/src/pool.js +740 -0
- package/src/serializer.js +139 -0
- package/src/thread.js +1246 -0
- package/src/types.js +1354 -0
- package/src/worker-factory.js +347 -0
|
@@ -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
|
+
}
|