@danielsimonjr/mathts-parallel 0.1.2 → 0.2.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/dist/index.d.ts +297 -4
- package/dist/index.js +949 -228
- package/dist/matrix.worker.d.ts +2 -0
- package/dist/matrix.worker.js +126 -0
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -3,11 +3,390 @@ import {
|
|
|
3
3
|
MathWorkerPool,
|
|
4
4
|
Transfer
|
|
5
5
|
} from "@danielsimonjr/mathts-workerpool";
|
|
6
|
+
|
|
7
|
+
// src/strategies/chunk.ts
|
|
8
|
+
var DEFAULT_CHUNK_OPTIONS = {
|
|
9
|
+
minChunkSize: 1e3,
|
|
10
|
+
maxChunks: typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4,
|
|
11
|
+
targetChunkSize: 0,
|
|
12
|
+
// 0 = auto
|
|
13
|
+
balanced: true
|
|
14
|
+
};
|
|
15
|
+
function calculateOptimalChunks(totalElements, options = {}) {
|
|
16
|
+
const { minChunkSize, maxChunks, targetChunkSize } = {
|
|
17
|
+
...DEFAULT_CHUNK_OPTIONS,
|
|
18
|
+
...options
|
|
19
|
+
};
|
|
20
|
+
if (totalElements === 0) return 0;
|
|
21
|
+
if (targetChunkSize > 0) {
|
|
22
|
+
return Math.min(maxChunks, Math.ceil(totalElements / targetChunkSize));
|
|
23
|
+
}
|
|
24
|
+
const maxPossibleChunks = Math.floor(totalElements / minChunkSize);
|
|
25
|
+
return Math.max(1, Math.min(maxChunks, maxPossibleChunks));
|
|
26
|
+
}
|
|
27
|
+
function chunkFloat64Array(data, options = {}) {
|
|
28
|
+
const { minChunkSize, balanced } = { ...DEFAULT_CHUNK_OPTIONS, ...options };
|
|
29
|
+
const totalElements = data.length;
|
|
30
|
+
if (totalElements === 0) {
|
|
31
|
+
return {
|
|
32
|
+
chunks: [],
|
|
33
|
+
chunkInfo: [],
|
|
34
|
+
totalElements: 0,
|
|
35
|
+
numChunks: 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const numChunks = calculateOptimalChunks(totalElements, options);
|
|
39
|
+
if (numChunks <= 1) {
|
|
40
|
+
return {
|
|
41
|
+
chunks: [data],
|
|
42
|
+
chunkInfo: [
|
|
43
|
+
{
|
|
44
|
+
startIndex: 0,
|
|
45
|
+
endIndex: totalElements,
|
|
46
|
+
length: totalElements,
|
|
47
|
+
chunkIndex: 0
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
totalElements,
|
|
51
|
+
numChunks: 1
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const chunks = [];
|
|
55
|
+
const chunkInfo = [];
|
|
56
|
+
if (balanced) {
|
|
57
|
+
const baseChunkSize = Math.floor(totalElements / numChunks);
|
|
58
|
+
const remainder = totalElements % numChunks;
|
|
59
|
+
let currentIndex = 0;
|
|
60
|
+
for (let i = 0; i < numChunks; i++) {
|
|
61
|
+
const thisChunkSize = baseChunkSize + (i < remainder ? 1 : 0);
|
|
62
|
+
const endIndex = currentIndex + thisChunkSize;
|
|
63
|
+
chunks.push(data.subarray(currentIndex, endIndex));
|
|
64
|
+
chunkInfo.push({
|
|
65
|
+
startIndex: currentIndex,
|
|
66
|
+
endIndex,
|
|
67
|
+
length: thisChunkSize,
|
|
68
|
+
chunkIndex: i
|
|
69
|
+
});
|
|
70
|
+
currentIndex = endIndex;
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
const chunkSize = Math.max(minChunkSize, Math.ceil(totalElements / numChunks));
|
|
74
|
+
let currentIndex = 0;
|
|
75
|
+
let chunkIndex = 0;
|
|
76
|
+
while (currentIndex < totalElements) {
|
|
77
|
+
const endIndex = Math.min(currentIndex + chunkSize, totalElements);
|
|
78
|
+
const thisChunkSize = endIndex - currentIndex;
|
|
79
|
+
chunks.push(data.subarray(currentIndex, endIndex));
|
|
80
|
+
chunkInfo.push({
|
|
81
|
+
startIndex: currentIndex,
|
|
82
|
+
endIndex,
|
|
83
|
+
length: thisChunkSize,
|
|
84
|
+
chunkIndex
|
|
85
|
+
});
|
|
86
|
+
currentIndex = endIndex;
|
|
87
|
+
chunkIndex++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
chunks,
|
|
92
|
+
chunkInfo,
|
|
93
|
+
totalElements,
|
|
94
|
+
numChunks: chunks.length
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function chunkArray(data, options = {}) {
|
|
98
|
+
const { balanced } = { ...DEFAULT_CHUNK_OPTIONS, ...options };
|
|
99
|
+
const totalElements = data.length;
|
|
100
|
+
if (totalElements === 0) {
|
|
101
|
+
return {
|
|
102
|
+
chunks: [],
|
|
103
|
+
chunkInfo: [],
|
|
104
|
+
totalElements: 0,
|
|
105
|
+
numChunks: 0
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const numChunks = calculateOptimalChunks(totalElements, options);
|
|
109
|
+
if (numChunks <= 1) {
|
|
110
|
+
return {
|
|
111
|
+
chunks: [data],
|
|
112
|
+
chunkInfo: [
|
|
113
|
+
{
|
|
114
|
+
startIndex: 0,
|
|
115
|
+
endIndex: totalElements,
|
|
116
|
+
length: totalElements,
|
|
117
|
+
chunkIndex: 0
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
totalElements,
|
|
121
|
+
numChunks: 1
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const chunks = [];
|
|
125
|
+
const chunkInfo = [];
|
|
126
|
+
if (balanced) {
|
|
127
|
+
const baseChunkSize = Math.floor(totalElements / numChunks);
|
|
128
|
+
const remainder = totalElements % numChunks;
|
|
129
|
+
let currentIndex = 0;
|
|
130
|
+
for (let i = 0; i < numChunks; i++) {
|
|
131
|
+
const thisChunkSize = baseChunkSize + (i < remainder ? 1 : 0);
|
|
132
|
+
const endIndex = currentIndex + thisChunkSize;
|
|
133
|
+
chunks.push(data.slice(currentIndex, endIndex));
|
|
134
|
+
chunkInfo.push({
|
|
135
|
+
startIndex: currentIndex,
|
|
136
|
+
endIndex,
|
|
137
|
+
length: thisChunkSize,
|
|
138
|
+
chunkIndex: i
|
|
139
|
+
});
|
|
140
|
+
currentIndex = endIndex;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
const chunkSize = Math.ceil(totalElements / numChunks);
|
|
144
|
+
let currentIndex = 0;
|
|
145
|
+
let chunkIndex = 0;
|
|
146
|
+
while (currentIndex < totalElements) {
|
|
147
|
+
const endIndex = Math.min(currentIndex + chunkSize, totalElements);
|
|
148
|
+
const thisChunkSize = endIndex - currentIndex;
|
|
149
|
+
chunks.push(data.slice(currentIndex, endIndex));
|
|
150
|
+
chunkInfo.push({
|
|
151
|
+
startIndex: currentIndex,
|
|
152
|
+
endIndex,
|
|
153
|
+
length: thisChunkSize,
|
|
154
|
+
chunkIndex
|
|
155
|
+
});
|
|
156
|
+
currentIndex = endIndex;
|
|
157
|
+
chunkIndex++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
chunks,
|
|
162
|
+
chunkInfo,
|
|
163
|
+
totalElements,
|
|
164
|
+
numChunks: chunks.length
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function mergeFloat64Chunks(chunks) {
|
|
168
|
+
if (chunks.length === 0) return new Float64Array(0);
|
|
169
|
+
if (chunks.length === 1) return chunks[0];
|
|
170
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
171
|
+
const result = new Float64Array(totalLength);
|
|
172
|
+
let offset = 0;
|
|
173
|
+
for (const chunk of chunks) {
|
|
174
|
+
result.set(chunk, offset);
|
|
175
|
+
offset += chunk.length;
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
function mergeArrayChunks(chunks) {
|
|
180
|
+
if (chunks.length === 0) return [];
|
|
181
|
+
if (chunks.length === 1) return chunks[0];
|
|
182
|
+
return chunks.flat();
|
|
183
|
+
}
|
|
184
|
+
function shouldParallelize(elementCount, threshold = 1e4) {
|
|
185
|
+
return elementCount >= threshold;
|
|
186
|
+
}
|
|
187
|
+
function partitionRange(start, end, options = {}) {
|
|
188
|
+
const totalElements = end - start;
|
|
189
|
+
const numChunks = calculateOptimalChunks(totalElements, options);
|
|
190
|
+
if (numChunks <= 1) {
|
|
191
|
+
return [[start, end]];
|
|
192
|
+
}
|
|
193
|
+
const partitions = [];
|
|
194
|
+
const baseChunkSize = Math.floor(totalElements / numChunks);
|
|
195
|
+
const remainder = totalElements % numChunks;
|
|
196
|
+
let currentIndex = start;
|
|
197
|
+
for (let i = 0; i < numChunks; i++) {
|
|
198
|
+
const thisChunkSize = baseChunkSize + (i < remainder ? 1 : 0);
|
|
199
|
+
const endIndex = currentIndex + thisChunkSize;
|
|
200
|
+
partitions.push([currentIndex, endIndex]);
|
|
201
|
+
currentIndex = endIndex;
|
|
202
|
+
}
|
|
203
|
+
return partitions;
|
|
204
|
+
}
|
|
205
|
+
function partition2D(rows, cols, options = {}) {
|
|
206
|
+
const rowPartitions = partitionRange(0, rows, options);
|
|
207
|
+
return rowPartitions.map(([rowStart, rowEnd]) => [rowStart, rowEnd, 0, cols]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/ops/bitwise.ts
|
|
211
|
+
function applyBinaryChunk(a, b, out, start, length, outStart, op) {
|
|
212
|
+
const end = start + length;
|
|
213
|
+
switch (op) {
|
|
214
|
+
case "bitAnd":
|
|
215
|
+
for (let i = start; i < end; i++) out[outStart + (i - start)] = a[i] & b[i];
|
|
216
|
+
return;
|
|
217
|
+
case "bitOr":
|
|
218
|
+
for (let i = start; i < end; i++) out[outStart + (i - start)] = a[i] | b[i];
|
|
219
|
+
return;
|
|
220
|
+
case "bitXor":
|
|
221
|
+
for (let i = start; i < end; i++) out[outStart + (i - start)] = a[i] ^ b[i];
|
|
222
|
+
return;
|
|
223
|
+
case "leftShift":
|
|
224
|
+
for (let i = start; i < end; i++) out[outStart + (i - start)] = a[i] << b[i];
|
|
225
|
+
return;
|
|
226
|
+
case "rightArithShift":
|
|
227
|
+
for (let i = start; i < end; i++) out[outStart + (i - start)] = a[i] >> b[i];
|
|
228
|
+
return;
|
|
229
|
+
case "rightLogShift":
|
|
230
|
+
for (let i = start; i < end; i++) out[outStart + (i - start)] = a[i] >>> b[i];
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function runBinary(a, b, op, options = {}) {
|
|
235
|
+
if (a.length !== b.length) {
|
|
236
|
+
throw new Error(`Array lengths must match: ${a.length} vs ${b.length}`);
|
|
237
|
+
}
|
|
238
|
+
const out = new Int32Array(a.length);
|
|
239
|
+
if (a.length === 0) return out;
|
|
240
|
+
const numChunks = Math.max(1, calculateOptimalChunks(a.length, options));
|
|
241
|
+
const base = Math.floor(a.length / numChunks);
|
|
242
|
+
const remainder = a.length % numChunks;
|
|
243
|
+
let start = 0;
|
|
244
|
+
for (let c = 0; c < numChunks; c++) {
|
|
245
|
+
const len = base + (c < remainder ? 1 : 0);
|
|
246
|
+
if (len === 0) continue;
|
|
247
|
+
applyBinaryChunk(a, b, out, start, len, start, op);
|
|
248
|
+
start += len;
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
function bitAnd(a, b, options) {
|
|
253
|
+
return runBinary(a, b, "bitAnd", options);
|
|
254
|
+
}
|
|
255
|
+
function bitOr(a, b, options) {
|
|
256
|
+
return runBinary(a, b, "bitOr", options);
|
|
257
|
+
}
|
|
258
|
+
function bitXor(a, b, options) {
|
|
259
|
+
return runBinary(a, b, "bitXor", options);
|
|
260
|
+
}
|
|
261
|
+
function leftShift(a, b, options) {
|
|
262
|
+
if (typeof b === "number") return runBinaryScalar(a, b, "leftShift", options);
|
|
263
|
+
return runBinary(a, b, "leftShift", options);
|
|
264
|
+
}
|
|
265
|
+
function rightArithShift(a, b, options) {
|
|
266
|
+
if (typeof b === "number") return runBinaryScalar(a, b, "rightArithShift", options);
|
|
267
|
+
return runBinary(a, b, "rightArithShift", options);
|
|
268
|
+
}
|
|
269
|
+
function rightLogShift(a, b, options) {
|
|
270
|
+
if (typeof b === "number") return runBinaryScalar(a, b, "rightLogShift", options);
|
|
271
|
+
return runBinary(a, b, "rightLogShift", options);
|
|
272
|
+
}
|
|
273
|
+
function bitNot(a, options = {}) {
|
|
274
|
+
const out = new Int32Array(a.length);
|
|
275
|
+
if (a.length === 0) return out;
|
|
276
|
+
const numChunks = Math.max(1, calculateOptimalChunks(a.length, options));
|
|
277
|
+
const base = Math.floor(a.length / numChunks);
|
|
278
|
+
const remainder = a.length % numChunks;
|
|
279
|
+
let start = 0;
|
|
280
|
+
for (let c = 0; c < numChunks; c++) {
|
|
281
|
+
const len = base + (c < remainder ? 1 : 0);
|
|
282
|
+
if (len === 0) continue;
|
|
283
|
+
const end = start + len;
|
|
284
|
+
for (let i = start; i < end; i++) out[i] = ~a[i];
|
|
285
|
+
start += len;
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
function runBinaryScalar(a, scalar, op, options = {}) {
|
|
290
|
+
const out = new Int32Array(a.length);
|
|
291
|
+
if (a.length === 0) return out;
|
|
292
|
+
const numChunks = Math.max(1, calculateOptimalChunks(a.length, options));
|
|
293
|
+
const base = Math.floor(a.length / numChunks);
|
|
294
|
+
const remainder = a.length % numChunks;
|
|
295
|
+
let start = 0;
|
|
296
|
+
for (let c = 0; c < numChunks; c++) {
|
|
297
|
+
const len = base + (c < remainder ? 1 : 0);
|
|
298
|
+
if (len === 0) continue;
|
|
299
|
+
const end = start + len;
|
|
300
|
+
switch (op) {
|
|
301
|
+
case "leftShift":
|
|
302
|
+
for (let i = start; i < end; i++) out[i] = a[i] << scalar;
|
|
303
|
+
break;
|
|
304
|
+
case "rightArithShift":
|
|
305
|
+
for (let i = start; i < end; i++) out[i] = a[i] >> scalar;
|
|
306
|
+
break;
|
|
307
|
+
case "rightLogShift":
|
|
308
|
+
for (let i = start; i < end; i++) out[i] = a[i] >>> scalar;
|
|
309
|
+
break;
|
|
310
|
+
// The non-shift ops never go through the scalar path — caller
|
|
311
|
+
// routes only the three shift ops here. Listed for exhaustiveness.
|
|
312
|
+
case "bitAnd":
|
|
313
|
+
for (let i = start; i < end; i++) out[i] = a[i] & scalar;
|
|
314
|
+
break;
|
|
315
|
+
case "bitOr":
|
|
316
|
+
for (let i = start; i < end; i++) out[i] = a[i] | scalar;
|
|
317
|
+
break;
|
|
318
|
+
case "bitXor":
|
|
319
|
+
for (let i = start; i < end; i++) out[i] = a[i] ^ scalar;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
start += len;
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/ComputePool.ts
|
|
328
|
+
function resolveOpThreshold(t) {
|
|
329
|
+
if (t === "never") return Number.MAX_SAFE_INTEGER;
|
|
330
|
+
if (t === "always") return 0;
|
|
331
|
+
return t;
|
|
332
|
+
}
|
|
333
|
+
var DEFAULT_THRESHOLD_BY_OP = {
|
|
334
|
+
// element-wise: overhead dominates at all tested sizes
|
|
335
|
+
add: "never",
|
|
336
|
+
subtract: "never",
|
|
337
|
+
multiply: "never",
|
|
338
|
+
divide: "never",
|
|
339
|
+
pow: "never",
|
|
340
|
+
scale: "never",
|
|
341
|
+
abs: "never",
|
|
342
|
+
negate: "never",
|
|
343
|
+
exp: "never",
|
|
344
|
+
log: "never",
|
|
345
|
+
sin: "never",
|
|
346
|
+
cos: "never",
|
|
347
|
+
tan: "never",
|
|
348
|
+
sign: "never",
|
|
349
|
+
// tensor contraction: parallelize when contracted-axis volume >= 8K
|
|
350
|
+
tensordot: 8192,
|
|
351
|
+
// reductions: overhead dominates
|
|
352
|
+
sum: "never",
|
|
353
|
+
mean: "never",
|
|
354
|
+
variance: "never",
|
|
355
|
+
parallelStatProd: "never",
|
|
356
|
+
normalCDF: "never",
|
|
357
|
+
// signal: overhead dominates or break-even never reached
|
|
358
|
+
parallelFFT: "never",
|
|
359
|
+
parallelConv: "never",
|
|
360
|
+
fft2d: "never",
|
|
361
|
+
// geometry: overhead dominates
|
|
362
|
+
distanceMatrix: "never",
|
|
363
|
+
// special functions: parallel wins above these sizes
|
|
364
|
+
erfc: 1e5,
|
|
365
|
+
besselJ: 1e6,
|
|
366
|
+
spectrogram: 65536,
|
|
367
|
+
// linear algebra: parallel wins above these element counts
|
|
368
|
+
matmul: 4096,
|
|
369
|
+
// break-even at 64×64 (4,096 elements)
|
|
370
|
+
matrixPower: 9216,
|
|
371
|
+
// break-even at ~96×96
|
|
372
|
+
characteristicPolynomial: 9216,
|
|
373
|
+
// break-even at ~96×96
|
|
374
|
+
// hypothesis tests: element-wise reduction (chiSquare) and post-sort
|
|
375
|
+
// dot-product (KS/MW/SW). Threshold 4096 per Slice 3.10 spec.
|
|
376
|
+
chiSquareTest: 4096,
|
|
377
|
+
kolmogorovSmirnovTest: 4096,
|
|
378
|
+
mannWhitneyTest: 4096,
|
|
379
|
+
shapiroWilkTest: 4096,
|
|
380
|
+
// distribution batch sampling: per-sample cost (rejection/transcendental) amortises
|
|
381
|
+
// worker overhead above 100 000 samples (Slice 5.12).
|
|
382
|
+
sampleChunk: 1e5
|
|
383
|
+
};
|
|
6
384
|
var DEFAULT_POOL_CONFIG = {
|
|
7
385
|
enabled: true,
|
|
8
386
|
minWorkers: 1,
|
|
9
387
|
maxWorkers: typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4,
|
|
10
388
|
thresholdElements: 5e4,
|
|
389
|
+
thresholdByOp: DEFAULT_THRESHOLD_BY_OP,
|
|
11
390
|
chunkSize: 1e4,
|
|
12
391
|
workerType: "auto",
|
|
13
392
|
workerIdleTimeout: 6e4,
|
|
@@ -34,6 +413,80 @@ function toWorkerConfig(config) {
|
|
|
34
413
|
taskTimeout: config.taskTimeout
|
|
35
414
|
};
|
|
36
415
|
}
|
|
416
|
+
function _rowMajorStrides(shape) {
|
|
417
|
+
const strides = new Array(shape.length);
|
|
418
|
+
let stride = 1;
|
|
419
|
+
for (let i = shape.length - 1; i >= 0; i--) {
|
|
420
|
+
strides[i] = stride;
|
|
421
|
+
stride *= shape[i];
|
|
422
|
+
}
|
|
423
|
+
return strides;
|
|
424
|
+
}
|
|
425
|
+
function _flatIndex(idx, strides) {
|
|
426
|
+
let flat = 0;
|
|
427
|
+
for (let i = 0; i < idx.length; i++) flat += idx[i] * strides[i];
|
|
428
|
+
return flat;
|
|
429
|
+
}
|
|
430
|
+
function _forEachIndex(sizes, cb) {
|
|
431
|
+
const rank = sizes.length;
|
|
432
|
+
if (rank === 0) {
|
|
433
|
+
cb([]);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const idx = new Array(rank).fill(0);
|
|
437
|
+
const total = sizes.reduce((a, b) => a * b, 1);
|
|
438
|
+
for (let n = 0; n < total; n++) {
|
|
439
|
+
cb(idx.slice());
|
|
440
|
+
for (let d = rank - 1; d >= 0; d--) {
|
|
441
|
+
idx[d]++;
|
|
442
|
+
if (idx[d] < sizes[d]) break;
|
|
443
|
+
idx[d] = 0;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function tensordotChunkKernel(a, aShape, b, bShape, axesA, axesB, resultShape, outStart, outEnd) {
|
|
448
|
+
const aStrides = _rowMajorStrides(aShape);
|
|
449
|
+
const bStrides = _rowMajorStrides(bShape);
|
|
450
|
+
const outStrides = _rowMajorStrides(resultShape);
|
|
451
|
+
const aRank = aShape.length;
|
|
452
|
+
const bRank = bShape.length;
|
|
453
|
+
const contractedAxesA = Array.from(axesA);
|
|
454
|
+
const contractedAxesB = Array.from(axesB);
|
|
455
|
+
const contractedSetA = new Set(contractedAxesA);
|
|
456
|
+
const contractedSetB = new Set(contractedAxesB);
|
|
457
|
+
const aFreeAxes = [];
|
|
458
|
+
const bFreeAxes = [];
|
|
459
|
+
for (let i = 0; i < aRank; i++) if (!contractedSetA.has(i)) aFreeAxes.push(i);
|
|
460
|
+
for (let i = 0; i < bRank; i++) if (!contractedSetB.has(i)) bFreeAxes.push(i);
|
|
461
|
+
const contractedSizes = contractedAxesA.map((ax) => aShape[ax]);
|
|
462
|
+
const chunkLen = outEnd - outStart;
|
|
463
|
+
const result = new Float64Array(chunkLen);
|
|
464
|
+
const outRank = resultShape.length;
|
|
465
|
+
const outIdx = new Array(outRank);
|
|
466
|
+
for (let outFlat = outStart; outFlat < outEnd; outFlat++) {
|
|
467
|
+
let rem = outFlat;
|
|
468
|
+
for (let d = 0; d < outRank; d++) {
|
|
469
|
+
outIdx[d] = Math.floor(rem / outStrides[d]);
|
|
470
|
+
rem -= outIdx[d] * outStrides[d];
|
|
471
|
+
}
|
|
472
|
+
const aFreeVals = outIdx.slice(0, aFreeAxes.length);
|
|
473
|
+
const bFreeVals = outIdx.slice(aFreeAxes.length);
|
|
474
|
+
let acc = 0;
|
|
475
|
+
_forEachIndex(contractedSizes, (contractVals) => {
|
|
476
|
+
const aIdx = new Array(aRank).fill(0);
|
|
477
|
+
for (let fi = 0; fi < aFreeAxes.length; fi++) aIdx[aFreeAxes[fi]] = aFreeVals[fi];
|
|
478
|
+
for (let ci = 0; ci < contractedAxesA.length; ci++)
|
|
479
|
+
aIdx[contractedAxesA[ci]] = contractVals[ci];
|
|
480
|
+
const bIdx = new Array(bRank).fill(0);
|
|
481
|
+
for (let fi = 0; fi < bFreeAxes.length; fi++) bIdx[bFreeAxes[fi]] = bFreeVals[fi];
|
|
482
|
+
for (let ci = 0; ci < contractedAxesB.length; ci++)
|
|
483
|
+
bIdx[contractedAxesB[ci]] = contractVals[ci];
|
|
484
|
+
acc += a[_flatIndex(aIdx, aStrides)] * b[_flatIndex(bIdx, bStrides)];
|
|
485
|
+
});
|
|
486
|
+
result[outFlat - outStart] = acc;
|
|
487
|
+
}
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
37
490
|
var ComputePool = class {
|
|
38
491
|
workerPool;
|
|
39
492
|
config;
|
|
@@ -54,9 +507,22 @@ var ComputePool = class {
|
|
|
54
507
|
return this.workerPool.isReady();
|
|
55
508
|
}
|
|
56
509
|
/**
|
|
57
|
-
* Determine if operation should be parallelized
|
|
510
|
+
* Determine if operation should be parallelized.
|
|
511
|
+
*
|
|
512
|
+
* When `op` is supplied the per-op threshold map is consulted first;
|
|
513
|
+
* if the op is absent from the map the global `thresholdElements` fallback
|
|
514
|
+
* is used. When `op` is omitted the global threshold is always used (for
|
|
515
|
+
* backward-compatibility with existing callers that don't know the op name).
|
|
58
516
|
*/
|
|
59
|
-
shouldParallelize(elementCount) {
|
|
517
|
+
shouldParallelize(elementCount, op) {
|
|
518
|
+
const byOp = this.config.thresholdByOp;
|
|
519
|
+
if (op !== void 0 && byOp !== void 0 && op in byOp) {
|
|
520
|
+
const raw = byOp[op];
|
|
521
|
+
if (raw !== void 0) {
|
|
522
|
+
const threshold = resolveOpThreshold(raw);
|
|
523
|
+
return this.config.enabled && elementCount >= threshold && this.workerPool.isReady();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
60
526
|
return this.workerPool.shouldParallelize(elementCount);
|
|
61
527
|
}
|
|
62
528
|
/**
|
|
@@ -78,6 +544,13 @@ var ComputePool = class {
|
|
|
78
544
|
const result = await this.workerPool.sum(data);
|
|
79
545
|
return toParallelResult(result);
|
|
80
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* Parallel product of array elements
|
|
549
|
+
*/
|
|
550
|
+
async prod(data) {
|
|
551
|
+
const result = await this.workerPool.prod(data);
|
|
552
|
+
return toParallelResult(result);
|
|
553
|
+
}
|
|
81
554
|
/**
|
|
82
555
|
* Parallel dot product
|
|
83
556
|
*/
|
|
@@ -188,6 +661,141 @@ var ComputePool = class {
|
|
|
188
661
|
const result = await this.workerPool.unary(data, fn);
|
|
189
662
|
return toParallelResult(result);
|
|
190
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Apply a caller-supplied unary numeric function in parallel.
|
|
666
|
+
*
|
|
667
|
+
* @param data - Input values
|
|
668
|
+
* @param fnSource - Source of a self-contained `(x: number) => number`
|
|
669
|
+
* expression. It must not reference free variables / closures, since it
|
|
670
|
+
* is eval'd in an isolated worker context. Used to parallelize element-wise
|
|
671
|
+
* math (special functions, distribution PDFs/CDFs).
|
|
672
|
+
*/
|
|
673
|
+
async applyKernel(data, fnSource) {
|
|
674
|
+
const result = await this.workerPool.applyKernel(data, fnSource);
|
|
675
|
+
return toParallelResult(result);
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Apply a caller-supplied binary numeric function in parallel.
|
|
679
|
+
*
|
|
680
|
+
* @param a - First operand array
|
|
681
|
+
* @param b - Second operand array (must match the length of `a`)
|
|
682
|
+
* @param fnSource - Source of a self-contained `(a: number, b: number) =>
|
|
683
|
+
* number` expression (no free variables / closures).
|
|
684
|
+
*/
|
|
685
|
+
async applyKernel2(a, b, fnSource) {
|
|
686
|
+
const result = await this.workerPool.applyKernel2(a, b, fnSource);
|
|
687
|
+
return toParallelResult(result);
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Fan-out Gauss-Legendre quadrature over `workerCount` sub-intervals.
|
|
691
|
+
*
|
|
692
|
+
* Partitions `[a, b]` into `workerCount` equal sub-domains and dispatches one
|
|
693
|
+
* `integrateChunk` worker task per sub-domain. Each task evaluates the
|
|
694
|
+
* integrand (supplied as a stringified closure) with the given GL nodes and
|
|
695
|
+
* weights and returns its partial integral. The partial sums are aggregated
|
|
696
|
+
* on the main thread.
|
|
697
|
+
*
|
|
698
|
+
* **Closure contract:** `fnSource` must be a self-contained `(x: number) =>
|
|
699
|
+
* number` expression — no free variables referencing outer scope, because it
|
|
700
|
+
* is eval'd inside an isolated worker context. Call-site validation (via
|
|
701
|
+
* `validateClosureSource`) should have already rejected unsafe closures before
|
|
702
|
+
* reaching this method.
|
|
703
|
+
*
|
|
704
|
+
* @param fnSource - Stringified integrand, e.g. `"(x) => Math.sin(x)"`
|
|
705
|
+
* @param a - Lower integration bound
|
|
706
|
+
* @param b - Upper integration bound
|
|
707
|
+
* @param workerCount - Number of sub-intervals / worker tasks
|
|
708
|
+
* @param nodes - GL nodes on [-1, 1]
|
|
709
|
+
* @param weights - Corresponding GL weights
|
|
710
|
+
* @returns Sum of partial integrals = approximate value of ∫ₐᵇ f(x) dx
|
|
711
|
+
*/
|
|
712
|
+
async integrateFanOut(fnSource, a, b, workerCount, nodes, weights) {
|
|
713
|
+
const h = (b - a) / workerCount;
|
|
714
|
+
const tasks = [];
|
|
715
|
+
for (let k = 0; k < workerCount; k++) {
|
|
716
|
+
const subA = a + k * h;
|
|
717
|
+
const subB = a + (k + 1) * h;
|
|
718
|
+
tasks.push(
|
|
719
|
+
this.workerPool.exec("integrateChunk", [fnSource, subA, subB, nodes, weights])
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const partials = await Promise.all(tasks);
|
|
723
|
+
return partials.reduce((acc, v) => acc + v, 0);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Fan-out distribution batch sampling across `workerCount` workers.
|
|
727
|
+
*
|
|
728
|
+
* Partitions `n` samples into `workerCount` equal chunks (last chunk absorbs
|
|
729
|
+
* the remainder) and dispatches one `sampleChunk` task per chunk. Each chunk
|
|
730
|
+
* uses an independent seed derived from `baseSeed` via SplitMix64-style
|
|
731
|
+
* hashing:
|
|
732
|
+
*
|
|
733
|
+
* `chunkSeed = (baseSeed ^ (chunkIdx * 0x9E3779B97F4A7C15)) >>> 0`
|
|
734
|
+
*
|
|
735
|
+
* This Fibonacci-hashing multiplier gives good avalanche: even baseSeed=0
|
|
736
|
+
* produces K distinct seeds, and adjacent chunk indices produce uncorrelated
|
|
737
|
+
* sequences. See the `sampleChunk` worker kernel for the full rationale.
|
|
738
|
+
*
|
|
739
|
+
* @param distName - One of 'normal', 'gamma', 'beta', 'studentT', 'exponential'
|
|
740
|
+
* @param params - Distribution parameters (order matches factory functions)
|
|
741
|
+
* @param n - Total number of samples to generate
|
|
742
|
+
* @param baseSeed - Base seed for reproducible output
|
|
743
|
+
* @param workerCount - Number of parallel worker tasks
|
|
744
|
+
* @returns Concatenated Float64Array of `n` samples
|
|
745
|
+
*/
|
|
746
|
+
async distributionSampleFanOut(distName, params, n, baseSeed, workerCount) {
|
|
747
|
+
const SPLIT_MIX_CONST = 2654435769;
|
|
748
|
+
const baseChunkSize = Math.floor(n / workerCount);
|
|
749
|
+
const tasks = [];
|
|
750
|
+
for (let k = 0; k < workerCount; k++) {
|
|
751
|
+
const chunkCount = k < workerCount - 1 ? baseChunkSize : n - baseChunkSize * (workerCount - 1);
|
|
752
|
+
const chunkSeed = (baseSeed ^ (Math.imul(k, SPLIT_MIX_CONST) | 0)) >>> 0;
|
|
753
|
+
tasks.push(
|
|
754
|
+
this.workerPool.exec("sampleChunk", [distName, params, chunkSeed, chunkCount])
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
const buffers = await Promise.all(tasks);
|
|
758
|
+
const out = new Float64Array(n);
|
|
759
|
+
let offset = 0;
|
|
760
|
+
for (const buf of buffers) {
|
|
761
|
+
const chunk = new Float64Array(buf);
|
|
762
|
+
out.set(chunk, offset);
|
|
763
|
+
offset += chunk.length;
|
|
764
|
+
}
|
|
765
|
+
return out;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Compute a batch of independent radix-2 FFTs in parallel.
|
|
769
|
+
*
|
|
770
|
+
* `real` and `imag` each hold `frameCount` concatenated frames of
|
|
771
|
+
* `frameLength` samples (`frameLength` must be a power of two). Each frame
|
|
772
|
+
* is FFT'd independently and the frames are distributed across workers.
|
|
773
|
+
*
|
|
774
|
+
* Used to parallelize the embarrassingly-parallel FFT batches in
|
|
775
|
+
* `spectrogram` (one FFT per windowed frame) and `fft2d` (rows then columns).
|
|
776
|
+
*
|
|
777
|
+
* @param real - Concatenated real parts (`frameCount * frameLength` values)
|
|
778
|
+
* @param imag - Concatenated imaginary parts (same layout as `real`)
|
|
779
|
+
* @param frameCount - Number of independent frames
|
|
780
|
+
* @param frameLength - Samples per frame (power of two)
|
|
781
|
+
* @param inverse - Compute the inverse FFT when true
|
|
782
|
+
*/
|
|
783
|
+
async fftBatch(real, imag, frameCount, frameLength, inverse = false) {
|
|
784
|
+
const result = await this.workerPool.fftBatch(real, imag, frameCount, frameLength, inverse);
|
|
785
|
+
return toParallelResult(result);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Compute an all-pairs Euclidean distance matrix in parallel.
|
|
789
|
+
*
|
|
790
|
+
* @param points - Flattened `n * dim` coordinate array (row-major)
|
|
791
|
+
* @param n - Number of points
|
|
792
|
+
* @param dim - Coordinate dimension
|
|
793
|
+
* @returns Flat row-major `n * n` distance matrix
|
|
794
|
+
*/
|
|
795
|
+
async distanceMatrix(points, n, dim) {
|
|
796
|
+
const result = await this.workerPool.distanceMatrix(points, n, dim);
|
|
797
|
+
return toParallelResult(result);
|
|
798
|
+
}
|
|
191
799
|
/**
|
|
192
800
|
* Parallel absolute value
|
|
193
801
|
*/
|
|
@@ -298,49 +906,350 @@ var ComputePool = class {
|
|
|
298
906
|
return this.elementwise(a, b, "multiply");
|
|
299
907
|
}
|
|
300
908
|
/**
|
|
301
|
-
* Parallel element-wise division
|
|
909
|
+
* Parallel element-wise division
|
|
910
|
+
*/
|
|
911
|
+
async divide(a, b) {
|
|
912
|
+
return this.elementwise(a, b, "divide");
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Parallel element-wise exponentiation.
|
|
916
|
+
*
|
|
917
|
+
* `out[i] = a[i] ** b[i]`
|
|
918
|
+
*
|
|
919
|
+
* Delegates to `applyKernel2` with the `Math.pow` kernel. The default
|
|
920
|
+
* per-op threshold for `pow` is `'never'` (overhead dominates); the
|
|
921
|
+
* sequential path is taken unless the caller overrides `thresholdByOp.pow`.
|
|
922
|
+
*/
|
|
923
|
+
async pow(a, b) {
|
|
924
|
+
if (a.length !== b.length) {
|
|
925
|
+
throw new Error(`Array lengths must match: ${a.length} vs ${b.length}`);
|
|
926
|
+
}
|
|
927
|
+
if (this.shouldParallelize(a.length, "pow")) {
|
|
928
|
+
return this.applyKernel2(a, b, "(a, b) => Math.pow(a, b)");
|
|
929
|
+
}
|
|
930
|
+
const start = performance.now();
|
|
931
|
+
const result = new Float64Array(a.length);
|
|
932
|
+
for (let i = 0; i < a.length; i++) {
|
|
933
|
+
result[i] = a[i] ** b[i];
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
result,
|
|
937
|
+
duration: performance.now() - start,
|
|
938
|
+
chunks: 1,
|
|
939
|
+
parallelized: false
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Parallel element-wise sign function.
|
|
944
|
+
*
|
|
945
|
+
* `out[i] = Math.sign(data[i])` → -1 | 0 | 1, with NaN → NaN (IEEE 754).
|
|
946
|
+
*
|
|
947
|
+
* Delegates to `applyKernel` with the `Math.sign` kernel. The default
|
|
948
|
+
* per-op threshold for `sign` is `'never'` (overhead dominates); the
|
|
949
|
+
* sequential path is taken unless the caller overrides `thresholdByOp.sign`.
|
|
950
|
+
*/
|
|
951
|
+
async sign(data) {
|
|
952
|
+
if (this.shouldParallelize(data.length, "sign")) {
|
|
953
|
+
return this.applyKernel(data, "(x) => Math.sign(x)");
|
|
954
|
+
}
|
|
955
|
+
const start = performance.now();
|
|
956
|
+
const result = new Float64Array(data.length);
|
|
957
|
+
for (let i = 0; i < data.length; i++) {
|
|
958
|
+
result[i] = Math.sign(data[i]);
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
result,
|
|
962
|
+
duration: performance.now() - start,
|
|
963
|
+
chunks: 1,
|
|
964
|
+
parallelized: false
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* General tensor contraction (tensordot) over specified axis pairs.
|
|
969
|
+
*
|
|
970
|
+
* Contracts axis `axesA[i]` of `a` with axis `axesB[i]` of `b` for each
|
|
971
|
+
* pair index `i`. The result shape is the non-contracted axes of `a` followed
|
|
972
|
+
* by the non-contracted axes of `b` (NumPy convention).
|
|
973
|
+
*
|
|
974
|
+
* Parallelization threshold: contracted-axis volume (product of contracted
|
|
975
|
+
* dimensions) >= 8 192. When parallelized, output elements are distributed
|
|
976
|
+
* in contiguous slices across workers via the `tensordotChunk` kernel.
|
|
977
|
+
*
|
|
978
|
+
* @param a - Flat row-major Float64Array for tensor A
|
|
979
|
+
* @param aShape - Shape of tensor A
|
|
980
|
+
* @param b - Flat row-major Float64Array for tensor B
|
|
981
|
+
* @param bShape - Shape of tensor B
|
|
982
|
+
* @param axesA - Axes of A to contract (same length as axesB)
|
|
983
|
+
* @param axesB - Axes of B to contract (same length as axesA)
|
|
984
|
+
* @returns `{ data: Float64Array; shape: number[] }` — flat result + result shape
|
|
985
|
+
*/
|
|
986
|
+
async tensordot(a, aShape, b, bShape, axesA, axesB) {
|
|
987
|
+
if (axesA.length !== axesB.length) {
|
|
988
|
+
throw new Error(
|
|
989
|
+
`tensordot: axesA.length (${axesA.length}) !== axesB.length (${axesB.length})`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
const aRank = aShape.length;
|
|
993
|
+
const bRank = bShape.length;
|
|
994
|
+
const aContracted = /* @__PURE__ */ new Set();
|
|
995
|
+
const bContracted = /* @__PURE__ */ new Set();
|
|
996
|
+
for (let i = 0; i < axesA.length; i++) {
|
|
997
|
+
const ax = axesA[i];
|
|
998
|
+
const bx = axesB[i];
|
|
999
|
+
if (!Number.isInteger(ax) || ax < 0 || ax >= aRank) {
|
|
1000
|
+
throw new Error(`tensordot: axesA[${i}]=${ax} is out of range for aShape (rank ${aRank})`);
|
|
1001
|
+
}
|
|
1002
|
+
if (!Number.isInteger(bx) || bx < 0 || bx >= bRank) {
|
|
1003
|
+
throw new Error(`tensordot: axesB[${i}]=${bx} is out of range for bShape (rank ${bRank})`);
|
|
1004
|
+
}
|
|
1005
|
+
if (aShape[ax] !== bShape[bx]) {
|
|
1006
|
+
throw new Error(
|
|
1007
|
+
`tensordot: dimension mismatch on contracted axes: aShape[${ax}]=${aShape[ax]} vs bShape[${bx}]=${bShape[bx]}`
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
aContracted.add(ax);
|
|
1011
|
+
bContracted.add(bx);
|
|
1012
|
+
}
|
|
1013
|
+
const resultShape = [];
|
|
1014
|
+
for (let i = 0; i < aRank; i++) {
|
|
1015
|
+
if (!aContracted.has(i)) resultShape.push(aShape[i]);
|
|
1016
|
+
}
|
|
1017
|
+
for (let i = 0; i < bRank; i++) {
|
|
1018
|
+
if (!bContracted.has(i)) resultShape.push(bShape[i]);
|
|
1019
|
+
}
|
|
1020
|
+
const resultSize = resultShape.reduce((acc, d) => acc * d, 1);
|
|
1021
|
+
const contractedVolume = Array.from(axesA).reduce((acc, ax) => acc * aShape[ax], 1);
|
|
1022
|
+
const start = performance.now();
|
|
1023
|
+
if (this.shouldParallelize(contractedVolume, "tensordot") && resultSize > 0) {
|
|
1024
|
+
const chunkSize = this.config.chunkSize;
|
|
1025
|
+
const chunks = [];
|
|
1026
|
+
for (let i = 0; i < resultSize; i += chunkSize) {
|
|
1027
|
+
chunks.push([i, Math.min(i + chunkSize, resultSize)]);
|
|
1028
|
+
}
|
|
1029
|
+
const aShapeArr = Array.from(aShape);
|
|
1030
|
+
const bShapeArr = Array.from(bShape);
|
|
1031
|
+
const axesAArr = Array.from(axesA);
|
|
1032
|
+
const axesBArr = Array.from(axesB);
|
|
1033
|
+
const chunkResults = await Promise.all(
|
|
1034
|
+
chunks.map(
|
|
1035
|
+
([outStart, outEnd]) => this.workerPool.exec("tensordotChunk", [
|
|
1036
|
+
a.buffer,
|
|
1037
|
+
aShapeArr,
|
|
1038
|
+
b.buffer,
|
|
1039
|
+
bShapeArr,
|
|
1040
|
+
axesAArr,
|
|
1041
|
+
axesBArr,
|
|
1042
|
+
resultShape,
|
|
1043
|
+
outStart,
|
|
1044
|
+
outEnd
|
|
1045
|
+
])
|
|
1046
|
+
)
|
|
1047
|
+
);
|
|
1048
|
+
const data2 = new Float64Array(resultSize);
|
|
1049
|
+
let offset = 0;
|
|
1050
|
+
for (const buf of chunkResults) {
|
|
1051
|
+
const chunk = new Float64Array(buf);
|
|
1052
|
+
data2.set(chunk, offset);
|
|
1053
|
+
offset += chunk.length;
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
result: { data: data2, shape: resultShape },
|
|
1057
|
+
duration: performance.now() - start,
|
|
1058
|
+
chunks: chunks.length,
|
|
1059
|
+
parallelized: true
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
const data = resultSize === 0 ? new Float64Array(0) : tensordotChunkKernel(a, aShape, b, bShape, axesA, axesB, resultShape, 0, resultSize);
|
|
1063
|
+
return {
|
|
1064
|
+
result: { data, shape: resultShape },
|
|
1065
|
+
duration: performance.now() - start,
|
|
1066
|
+
chunks: 1,
|
|
1067
|
+
parallelized: false
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Compute mean in parallel (uses variance internally)
|
|
1072
|
+
*/
|
|
1073
|
+
async mean(data) {
|
|
1074
|
+
const result = await this.variance(data);
|
|
1075
|
+
return {
|
|
1076
|
+
...result,
|
|
1077
|
+
result: result.result.mean
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Compute standard deviation in parallel
|
|
1082
|
+
*/
|
|
1083
|
+
async std(data) {
|
|
1084
|
+
const result = await this.variance(data);
|
|
1085
|
+
return {
|
|
1086
|
+
...result,
|
|
1087
|
+
result: result.result.std
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Find minimum value in parallel
|
|
1092
|
+
*/
|
|
1093
|
+
async min(data) {
|
|
1094
|
+
const result = await this.minMax(data);
|
|
1095
|
+
return {
|
|
1096
|
+
...result,
|
|
1097
|
+
result: result.result.min
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Find maximum value in parallel
|
|
1102
|
+
*/
|
|
1103
|
+
async max(data) {
|
|
1104
|
+
const result = await this.minMax(data);
|
|
1105
|
+
return {
|
|
1106
|
+
...result,
|
|
1107
|
+
result: result.result.max
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
// =========================================================================
|
|
1111
|
+
// Bitwise Operations (Int32Array)
|
|
1112
|
+
// =========================================================================
|
|
1113
|
+
//
|
|
1114
|
+
// Above the elementwise threshold these dispatch to the `Int32Array`-keyed
|
|
1115
|
+
// worker kernels in `@danielsimonjr/mathts-workerpool` (`bitwiseChunk`,
|
|
1116
|
+
// `bitwiseScalarChunk`, `bitwiseNotChunk`). Below threshold they fall back
|
|
1117
|
+
// to the pure in-process drivers in `./ops/bitwise.ts`. The fallback path
|
|
1118
|
+
// uses those drivers verbatim so the algorithm lives in one place.
|
|
1119
|
+
/**
|
|
1120
|
+
* Element-wise bitwise AND on two `Int32Array`s.
|
|
1121
|
+
*/
|
|
1122
|
+
async bitAnd(a, b) {
|
|
1123
|
+
if (a.length !== b.length) {
|
|
1124
|
+
throw new Error(`Array lengths must match: ${a.length} vs ${b.length}`);
|
|
1125
|
+
}
|
|
1126
|
+
if (this.shouldParallelize(a.length)) {
|
|
1127
|
+
return toParallelResult(await this.workerPool.bitwiseBinary(a, b, "bitAnd"));
|
|
1128
|
+
}
|
|
1129
|
+
const start = performance.now();
|
|
1130
|
+
const result = bitAnd(a, b);
|
|
1131
|
+
return {
|
|
1132
|
+
result,
|
|
1133
|
+
duration: performance.now() - start,
|
|
1134
|
+
chunks: 1,
|
|
1135
|
+
parallelized: false
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Element-wise bitwise OR on two `Int32Array`s.
|
|
1140
|
+
*/
|
|
1141
|
+
async bitOr(a, b) {
|
|
1142
|
+
if (a.length !== b.length) {
|
|
1143
|
+
throw new Error(`Array lengths must match: ${a.length} vs ${b.length}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (this.shouldParallelize(a.length)) {
|
|
1146
|
+
return toParallelResult(await this.workerPool.bitwiseBinary(a, b, "bitOr"));
|
|
1147
|
+
}
|
|
1148
|
+
const start = performance.now();
|
|
1149
|
+
const result = bitOr(a, b);
|
|
1150
|
+
return {
|
|
1151
|
+
result,
|
|
1152
|
+
duration: performance.now() - start,
|
|
1153
|
+
chunks: 1,
|
|
1154
|
+
parallelized: false
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Element-wise bitwise XOR on two `Int32Array`s.
|
|
302
1159
|
*/
|
|
303
|
-
async
|
|
304
|
-
|
|
1160
|
+
async bitXor(a, b) {
|
|
1161
|
+
if (a.length !== b.length) {
|
|
1162
|
+
throw new Error(`Array lengths must match: ${a.length} vs ${b.length}`);
|
|
1163
|
+
}
|
|
1164
|
+
if (this.shouldParallelize(a.length)) {
|
|
1165
|
+
return toParallelResult(await this.workerPool.bitwiseBinary(a, b, "bitXor"));
|
|
1166
|
+
}
|
|
1167
|
+
const start = performance.now();
|
|
1168
|
+
const result = bitXor(a, b);
|
|
1169
|
+
return {
|
|
1170
|
+
result,
|
|
1171
|
+
duration: performance.now() - start,
|
|
1172
|
+
chunks: 1,
|
|
1173
|
+
parallelized: false
|
|
1174
|
+
};
|
|
305
1175
|
}
|
|
306
1176
|
/**
|
|
307
|
-
*
|
|
1177
|
+
* Unary bitwise NOT on an `Int32Array`.
|
|
308
1178
|
*/
|
|
309
|
-
async
|
|
310
|
-
|
|
1179
|
+
async bitNot(a) {
|
|
1180
|
+
if (this.shouldParallelize(a.length)) {
|
|
1181
|
+
return toParallelResult(await this.workerPool.bitwiseNot(a));
|
|
1182
|
+
}
|
|
1183
|
+
const start = performance.now();
|
|
1184
|
+
const result = bitNot(a);
|
|
311
1185
|
return {
|
|
312
|
-
|
|
313
|
-
|
|
1186
|
+
result,
|
|
1187
|
+
duration: performance.now() - start,
|
|
1188
|
+
chunks: 1,
|
|
1189
|
+
parallelized: false
|
|
314
1190
|
};
|
|
315
1191
|
}
|
|
316
1192
|
/**
|
|
317
|
-
*
|
|
1193
|
+
* Element-wise left shift. `b` may be a per-element `Int32Array` of
|
|
1194
|
+
* shift counts or a single scalar `number` applied uniformly.
|
|
318
1195
|
*/
|
|
319
|
-
async
|
|
320
|
-
|
|
1196
|
+
async leftShift(a, b) {
|
|
1197
|
+
if (typeof b !== "number" && a.length !== b.length) {
|
|
1198
|
+
throw new Error(`Array lengths must match: ${a.length} vs ${b.length}`);
|
|
1199
|
+
}
|
|
1200
|
+
if (this.shouldParallelize(a.length)) {
|
|
1201
|
+
const result2 = typeof b === "number" ? await this.workerPool.bitwiseScalar(a, b, "leftShift") : await this.workerPool.bitwiseBinary(a, b, "leftShift");
|
|
1202
|
+
return toParallelResult(result2);
|
|
1203
|
+
}
|
|
1204
|
+
const start = performance.now();
|
|
1205
|
+
const result = leftShift(a, b);
|
|
321
1206
|
return {
|
|
322
|
-
|
|
323
|
-
|
|
1207
|
+
result,
|
|
1208
|
+
duration: performance.now() - start,
|
|
1209
|
+
chunks: 1,
|
|
1210
|
+
parallelized: false
|
|
324
1211
|
};
|
|
325
1212
|
}
|
|
326
1213
|
/**
|
|
327
|
-
*
|
|
1214
|
+
* Element-wise arithmetic (sign-preserving) right shift.
|
|
328
1215
|
*/
|
|
329
|
-
async
|
|
330
|
-
|
|
1216
|
+
async rightArithShift(a, b) {
|
|
1217
|
+
if (typeof b !== "number" && a.length !== b.length) {
|
|
1218
|
+
throw new Error(`Array lengths must match: ${a.length} vs ${b.length}`);
|
|
1219
|
+
}
|
|
1220
|
+
if (this.shouldParallelize(a.length)) {
|
|
1221
|
+
const result2 = typeof b === "number" ? await this.workerPool.bitwiseScalar(a, b, "rightArithShift") : await this.workerPool.bitwiseBinary(a, b, "rightArithShift");
|
|
1222
|
+
return toParallelResult(result2);
|
|
1223
|
+
}
|
|
1224
|
+
const start = performance.now();
|
|
1225
|
+
const result = rightArithShift(a, b);
|
|
331
1226
|
return {
|
|
332
|
-
|
|
333
|
-
|
|
1227
|
+
result,
|
|
1228
|
+
duration: performance.now() - start,
|
|
1229
|
+
chunks: 1,
|
|
1230
|
+
parallelized: false
|
|
334
1231
|
};
|
|
335
1232
|
}
|
|
336
1233
|
/**
|
|
337
|
-
*
|
|
1234
|
+
* Element-wise logical (zero-filling) right shift. Results are stored
|
|
1235
|
+
* as `Int32Array`, so values ≥ 2^31 wrap into the negative range —
|
|
1236
|
+
* matching `(x >>> n) | 0` JavaScript semantics.
|
|
338
1237
|
*/
|
|
339
|
-
async
|
|
340
|
-
|
|
1238
|
+
async rightLogShift(a, b) {
|
|
1239
|
+
if (typeof b !== "number" && a.length !== b.length) {
|
|
1240
|
+
throw new Error(`Array lengths must match: ${a.length} vs ${b.length}`);
|
|
1241
|
+
}
|
|
1242
|
+
if (this.shouldParallelize(a.length)) {
|
|
1243
|
+
const result2 = typeof b === "number" ? await this.workerPool.bitwiseScalar(a, b, "rightLogShift") : await this.workerPool.bitwiseBinary(a, b, "rightLogShift");
|
|
1244
|
+
return toParallelResult(result2);
|
|
1245
|
+
}
|
|
1246
|
+
const start = performance.now();
|
|
1247
|
+
const result = rightLogShift(a, b);
|
|
341
1248
|
return {
|
|
342
|
-
|
|
343
|
-
|
|
1249
|
+
result,
|
|
1250
|
+
duration: performance.now() - start,
|
|
1251
|
+
chunks: 1,
|
|
1252
|
+
parallelized: false
|
|
344
1253
|
};
|
|
345
1254
|
}
|
|
346
1255
|
/**
|
|
@@ -362,6 +1271,13 @@ var ComputePool = class {
|
|
|
362
1271
|
getConfig() {
|
|
363
1272
|
return { ...this.config };
|
|
364
1273
|
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Maximum number of workers configured for this pool.
|
|
1276
|
+
* Used by distribution sampling fan-out to determine chunk count.
|
|
1277
|
+
*/
|
|
1278
|
+
get workerCount() {
|
|
1279
|
+
return this.config.maxWorkers;
|
|
1280
|
+
}
|
|
365
1281
|
/**
|
|
366
1282
|
* Get the underlying MathWorkerPool for advanced operations
|
|
367
1283
|
*/
|
|
@@ -566,209 +1482,6 @@ async function parallelCount(data, predicate, options = {}) {
|
|
|
566
1482
|
};
|
|
567
1483
|
}
|
|
568
1484
|
|
|
569
|
-
// src/strategies/chunk.ts
|
|
570
|
-
var DEFAULT_CHUNK_OPTIONS = {
|
|
571
|
-
minChunkSize: 1e3,
|
|
572
|
-
maxChunks: typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4,
|
|
573
|
-
targetChunkSize: 0,
|
|
574
|
-
// 0 = auto
|
|
575
|
-
balanced: true
|
|
576
|
-
};
|
|
577
|
-
function calculateOptimalChunks(totalElements, options = {}) {
|
|
578
|
-
const { minChunkSize, maxChunks, targetChunkSize } = {
|
|
579
|
-
...DEFAULT_CHUNK_OPTIONS,
|
|
580
|
-
...options
|
|
581
|
-
};
|
|
582
|
-
if (totalElements === 0) return 0;
|
|
583
|
-
if (targetChunkSize > 0) {
|
|
584
|
-
return Math.min(maxChunks, Math.ceil(totalElements / targetChunkSize));
|
|
585
|
-
}
|
|
586
|
-
const maxPossibleChunks = Math.floor(totalElements / minChunkSize);
|
|
587
|
-
return Math.max(1, Math.min(maxChunks, maxPossibleChunks));
|
|
588
|
-
}
|
|
589
|
-
function chunkFloat64Array(data, options = {}) {
|
|
590
|
-
const { minChunkSize, balanced } = { ...DEFAULT_CHUNK_OPTIONS, ...options };
|
|
591
|
-
const totalElements = data.length;
|
|
592
|
-
if (totalElements === 0) {
|
|
593
|
-
return {
|
|
594
|
-
chunks: [],
|
|
595
|
-
chunkInfo: [],
|
|
596
|
-
totalElements: 0,
|
|
597
|
-
numChunks: 0
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
const numChunks = calculateOptimalChunks(totalElements, options);
|
|
601
|
-
if (numChunks <= 1) {
|
|
602
|
-
return {
|
|
603
|
-
chunks: [data],
|
|
604
|
-
chunkInfo: [
|
|
605
|
-
{
|
|
606
|
-
startIndex: 0,
|
|
607
|
-
endIndex: totalElements,
|
|
608
|
-
length: totalElements,
|
|
609
|
-
chunkIndex: 0
|
|
610
|
-
}
|
|
611
|
-
],
|
|
612
|
-
totalElements,
|
|
613
|
-
numChunks: 1
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
const chunks = [];
|
|
617
|
-
const chunkInfo = [];
|
|
618
|
-
if (balanced) {
|
|
619
|
-
const baseChunkSize = Math.floor(totalElements / numChunks);
|
|
620
|
-
const remainder = totalElements % numChunks;
|
|
621
|
-
let currentIndex = 0;
|
|
622
|
-
for (let i = 0; i < numChunks; i++) {
|
|
623
|
-
const thisChunkSize = baseChunkSize + (i < remainder ? 1 : 0);
|
|
624
|
-
const endIndex = currentIndex + thisChunkSize;
|
|
625
|
-
chunks.push(data.subarray(currentIndex, endIndex));
|
|
626
|
-
chunkInfo.push({
|
|
627
|
-
startIndex: currentIndex,
|
|
628
|
-
endIndex,
|
|
629
|
-
length: thisChunkSize,
|
|
630
|
-
chunkIndex: i
|
|
631
|
-
});
|
|
632
|
-
currentIndex = endIndex;
|
|
633
|
-
}
|
|
634
|
-
} else {
|
|
635
|
-
const chunkSize = Math.max(minChunkSize, Math.ceil(totalElements / numChunks));
|
|
636
|
-
let currentIndex = 0;
|
|
637
|
-
let chunkIndex = 0;
|
|
638
|
-
while (currentIndex < totalElements) {
|
|
639
|
-
const endIndex = Math.min(currentIndex + chunkSize, totalElements);
|
|
640
|
-
const thisChunkSize = endIndex - currentIndex;
|
|
641
|
-
chunks.push(data.subarray(currentIndex, endIndex));
|
|
642
|
-
chunkInfo.push({
|
|
643
|
-
startIndex: currentIndex,
|
|
644
|
-
endIndex,
|
|
645
|
-
length: thisChunkSize,
|
|
646
|
-
chunkIndex
|
|
647
|
-
});
|
|
648
|
-
currentIndex = endIndex;
|
|
649
|
-
chunkIndex++;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
return {
|
|
653
|
-
chunks,
|
|
654
|
-
chunkInfo,
|
|
655
|
-
totalElements,
|
|
656
|
-
numChunks: chunks.length
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
function chunkArray(data, options = {}) {
|
|
660
|
-
const { balanced } = { ...DEFAULT_CHUNK_OPTIONS, ...options };
|
|
661
|
-
const totalElements = data.length;
|
|
662
|
-
if (totalElements === 0) {
|
|
663
|
-
return {
|
|
664
|
-
chunks: [],
|
|
665
|
-
chunkInfo: [],
|
|
666
|
-
totalElements: 0,
|
|
667
|
-
numChunks: 0
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
const numChunks = calculateOptimalChunks(totalElements, options);
|
|
671
|
-
if (numChunks <= 1) {
|
|
672
|
-
return {
|
|
673
|
-
chunks: [data],
|
|
674
|
-
chunkInfo: [
|
|
675
|
-
{
|
|
676
|
-
startIndex: 0,
|
|
677
|
-
endIndex: totalElements,
|
|
678
|
-
length: totalElements,
|
|
679
|
-
chunkIndex: 0
|
|
680
|
-
}
|
|
681
|
-
],
|
|
682
|
-
totalElements,
|
|
683
|
-
numChunks: 1
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
const chunks = [];
|
|
687
|
-
const chunkInfo = [];
|
|
688
|
-
if (balanced) {
|
|
689
|
-
const baseChunkSize = Math.floor(totalElements / numChunks);
|
|
690
|
-
const remainder = totalElements % numChunks;
|
|
691
|
-
let currentIndex = 0;
|
|
692
|
-
for (let i = 0; i < numChunks; i++) {
|
|
693
|
-
const thisChunkSize = baseChunkSize + (i < remainder ? 1 : 0);
|
|
694
|
-
const endIndex = currentIndex + thisChunkSize;
|
|
695
|
-
chunks.push(data.slice(currentIndex, endIndex));
|
|
696
|
-
chunkInfo.push({
|
|
697
|
-
startIndex: currentIndex,
|
|
698
|
-
endIndex,
|
|
699
|
-
length: thisChunkSize,
|
|
700
|
-
chunkIndex: i
|
|
701
|
-
});
|
|
702
|
-
currentIndex = endIndex;
|
|
703
|
-
}
|
|
704
|
-
} else {
|
|
705
|
-
const chunkSize = Math.ceil(totalElements / numChunks);
|
|
706
|
-
let currentIndex = 0;
|
|
707
|
-
let chunkIndex = 0;
|
|
708
|
-
while (currentIndex < totalElements) {
|
|
709
|
-
const endIndex = Math.min(currentIndex + chunkSize, totalElements);
|
|
710
|
-
const thisChunkSize = endIndex - currentIndex;
|
|
711
|
-
chunks.push(data.slice(currentIndex, endIndex));
|
|
712
|
-
chunkInfo.push({
|
|
713
|
-
startIndex: currentIndex,
|
|
714
|
-
endIndex,
|
|
715
|
-
length: thisChunkSize,
|
|
716
|
-
chunkIndex
|
|
717
|
-
});
|
|
718
|
-
currentIndex = endIndex;
|
|
719
|
-
chunkIndex++;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
return {
|
|
723
|
-
chunks,
|
|
724
|
-
chunkInfo,
|
|
725
|
-
totalElements,
|
|
726
|
-
numChunks: chunks.length
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
function mergeFloat64Chunks(chunks) {
|
|
730
|
-
if (chunks.length === 0) return new Float64Array(0);
|
|
731
|
-
if (chunks.length === 1) return chunks[0];
|
|
732
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
733
|
-
const result = new Float64Array(totalLength);
|
|
734
|
-
let offset = 0;
|
|
735
|
-
for (const chunk of chunks) {
|
|
736
|
-
result.set(chunk, offset);
|
|
737
|
-
offset += chunk.length;
|
|
738
|
-
}
|
|
739
|
-
return result;
|
|
740
|
-
}
|
|
741
|
-
function mergeArrayChunks(chunks) {
|
|
742
|
-
if (chunks.length === 0) return [];
|
|
743
|
-
if (chunks.length === 1) return chunks[0];
|
|
744
|
-
return chunks.flat();
|
|
745
|
-
}
|
|
746
|
-
function shouldParallelize(elementCount, threshold = 1e4) {
|
|
747
|
-
return elementCount >= threshold;
|
|
748
|
-
}
|
|
749
|
-
function partitionRange(start, end, options = {}) {
|
|
750
|
-
const totalElements = end - start;
|
|
751
|
-
const numChunks = calculateOptimalChunks(totalElements, options);
|
|
752
|
-
if (numChunks <= 1) {
|
|
753
|
-
return [[start, end]];
|
|
754
|
-
}
|
|
755
|
-
const partitions = [];
|
|
756
|
-
const baseChunkSize = Math.floor(totalElements / numChunks);
|
|
757
|
-
const remainder = totalElements % numChunks;
|
|
758
|
-
let currentIndex = start;
|
|
759
|
-
for (let i = 0; i < numChunks; i++) {
|
|
760
|
-
const thisChunkSize = baseChunkSize + (i < remainder ? 1 : 0);
|
|
761
|
-
const endIndex = currentIndex + thisChunkSize;
|
|
762
|
-
partitions.push([currentIndex, endIndex]);
|
|
763
|
-
currentIndex = endIndex;
|
|
764
|
-
}
|
|
765
|
-
return partitions;
|
|
766
|
-
}
|
|
767
|
-
function partition2D(rows, cols, options = {}) {
|
|
768
|
-
const rowPartitions = partitionRange(0, rows, options);
|
|
769
|
-
return rowPartitions.map(([rowStart, rowEnd]) => [rowStart, rowEnd, 0, cols]);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
1485
|
// src/strategies/threshold.ts
|
|
773
1486
|
var DEFAULT_THRESHOLDS = {
|
|
774
1487
|
matmul: 1e4,
|
|
@@ -897,12 +1610,17 @@ export {
|
|
|
897
1610
|
DEFAULT_THRESHOLDS,
|
|
898
1611
|
ThresholdDispatcher,
|
|
899
1612
|
Transfer,
|
|
1613
|
+
bitAnd,
|
|
1614
|
+
bitNot,
|
|
1615
|
+
bitOr,
|
|
1616
|
+
bitXor,
|
|
900
1617
|
calculateChunks,
|
|
901
1618
|
calculateOptimalChunks,
|
|
902
1619
|
chunkArray,
|
|
903
1620
|
chunkFloat64Array,
|
|
904
1621
|
computePool,
|
|
905
1622
|
dispatch,
|
|
1623
|
+
leftShift,
|
|
906
1624
|
mergeArrayChunks,
|
|
907
1625
|
mergeFloat64Chunks,
|
|
908
1626
|
parallelAbs,
|
|
@@ -947,6 +1665,9 @@ export {
|
|
|
947
1665
|
parallelVariance,
|
|
948
1666
|
partition2D,
|
|
949
1667
|
partitionRange,
|
|
1668
|
+
resolveOpThreshold,
|
|
1669
|
+
rightArithShift,
|
|
1670
|
+
rightLogShift,
|
|
950
1671
|
shouldParallelize as shouldChunkParallelize,
|
|
951
1672
|
shouldParallelize2 as shouldParallelize,
|
|
952
1673
|
thresholdDispatcher
|