@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,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Pure utility functions for GPU compute — type helpers, auto-boxing,
|
|
3
|
+
* JS→WGSL transpilation, and format detection.
|
|
4
|
+
*
|
|
5
|
+
* These functions have zero coupling to GPU state and can be used
|
|
6
|
+
* independently of the {@link GPUCompute} class.
|
|
7
|
+
*
|
|
8
|
+
* @module gpu/helpers
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { GPUComputeError } from '../error.js';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Output type helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** Map of human-readable names to TypedArray constructors. */
|
|
18
|
+
export const OUTPUT_TYPES = {
|
|
19
|
+
f32: Float32Array,
|
|
20
|
+
i32: Int32Array,
|
|
21
|
+
u32: Uint32Array,
|
|
22
|
+
f16: Float32Array, // JS has no Float16Array; use Float32Array
|
|
23
|
+
f64: Float64Array,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a type name or constructor to a TypedArray constructor.
|
|
28
|
+
*
|
|
29
|
+
* @param {string|typeof TypedArray} type
|
|
30
|
+
* @returns {typeof TypedArray}
|
|
31
|
+
*/
|
|
32
|
+
export function resolveType(type) {
|
|
33
|
+
if (typeof type === 'function') return type;
|
|
34
|
+
const resolved = OUTPUT_TYPES[type];
|
|
35
|
+
if (!resolved) throw new TypeError(`Unknown output type: "${type}"`);
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Auto-boxing helpers
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Auto-box a value into a TypedArray.
|
|
45
|
+
*
|
|
46
|
+
* - `number` → `Float32Array([n])`
|
|
47
|
+
* - `number[]` → `Float32Array(arr)`
|
|
48
|
+
* - `TypedArray` → returned as-is
|
|
49
|
+
*
|
|
50
|
+
* @param {*} val
|
|
51
|
+
* @returns {TypedArray}
|
|
52
|
+
*/
|
|
53
|
+
export function box(val) {
|
|
54
|
+
if (val instanceof Float32Array || val instanceof Int32Array || val instanceof Uint32Array) return val;
|
|
55
|
+
if (typeof val === 'number') return new Float32Array([val]);
|
|
56
|
+
if (Array.isArray(val)) return new Float32Array(val);
|
|
57
|
+
return val;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Box all values in a plain object.
|
|
62
|
+
*
|
|
63
|
+
* @param {Object<string, *>} obj
|
|
64
|
+
* @returns {Object<string, TypedArray>}
|
|
65
|
+
*/
|
|
66
|
+
export function boxAll(obj) {
|
|
67
|
+
if (!obj) return {};
|
|
68
|
+
const out = {};
|
|
69
|
+
for (const [k, v] of Object.entries(obj)) out[k] = box(v);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Format detection
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detect whether the second arg to `run()` is the old format
|
|
79
|
+
* `{ inputs, uniforms, outputs, ... }` or the new flat format `inputs`.
|
|
80
|
+
*
|
|
81
|
+
* @param {*} arg
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
export function isOldRunFormat(arg) {
|
|
85
|
+
return arg != null && typeof arg === 'object' && !ArrayBuffer.isView(arg) &&
|
|
86
|
+
('inputs' in arg || 'uniforms' in arg || 'outputBuffers' in arg || 'outputs' in arg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// JS → WGSL transpilation
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Transpile a WGSL expression string (JS-flavored) to valid WGSL.
|
|
95
|
+
*
|
|
96
|
+
* Handles: ternary→select, ===→==, &&→&, ||→|, **→^,
|
|
97
|
+
* Math.*→WGSL equivalents, integer→float literals.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} expr - JS-flavored expression
|
|
100
|
+
* @returns {string} Valid WGSL expression
|
|
101
|
+
*/
|
|
102
|
+
export function transpileExpression(expr) {
|
|
103
|
+
const MATH_MAP = {
|
|
104
|
+
'Math.sqrt': 'sqrt', 'Math.abs': 'abs', 'Math.sin': 'sin',
|
|
105
|
+
'Math.cos': 'cos', 'Math.tan': 'tan', 'Math.asin': 'asin',
|
|
106
|
+
'Math.acos': 'acos', 'Math.atan': 'atan', 'Math.atan2': 'atan2',
|
|
107
|
+
'Math.exp': 'exp', 'Math.log': 'log', 'Math.log2': 'log2',
|
|
108
|
+
'Math.log10': 'log10', 'Math.pow': 'pow', 'Math.max': 'max',
|
|
109
|
+
'Math.min': 'min', 'Math.floor': 'floor', 'Math.ceil': 'ceil',
|
|
110
|
+
'Math.round': 'round', 'Math.trunc': 'trunc', 'Math.sign': 'sign',
|
|
111
|
+
'Math.PI': '3.14159265', 'Math.E': '2.71828182',
|
|
112
|
+
'Math.tanh': 'tanh', 'Math.sinh': 'sinh', 'Math.cosh': 'cosh',
|
|
113
|
+
'Math.cbrt': 'cbrt', 'Math.expm1': '(exp($1) - 1.0)',
|
|
114
|
+
'Math.log1p': 'log(1.0 + $1)',
|
|
115
|
+
'Math.hypot': 'sqrt($1 * $1 + $2 * $2)',
|
|
116
|
+
'Math.random': 'fract(f32(hash(u32(i))))',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
let wgsl = expr;
|
|
120
|
+
|
|
121
|
+
// Replace ternary: `cond ? a : b` → `select(b, a, cond)`
|
|
122
|
+
wgsl = wgsl.replace(
|
|
123
|
+
/(.+?)\s*\?\s*(.+?)\s*:\s*(.+)/g,
|
|
124
|
+
(_, cond, a, b) => `select(${b.trim()}, ${a.trim()}, ${cond.trim()})`,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Replace JS comparison operators
|
|
128
|
+
wgsl = wgsl.replace(/===/g, '==');
|
|
129
|
+
wgsl = wgsl.replace(/!==/g, '!=');
|
|
130
|
+
|
|
131
|
+
// Replace JS logical operators
|
|
132
|
+
wgsl = wgsl.replace(/&&/g, '&');
|
|
133
|
+
wgsl = wgsl.replace(/\|\|/g, '|');
|
|
134
|
+
wgsl = wgsl.replace(/!(?=[a-zA-Z_])/g, '!');
|
|
135
|
+
|
|
136
|
+
// Replace JS operators
|
|
137
|
+
wgsl = wgsl.replace(/\*\*/g, '^');
|
|
138
|
+
|
|
139
|
+
// Replace JS math calls with WGSL equivalents (longest first)
|
|
140
|
+
const sortedKeys = Object.keys(MATH_MAP).sort((a, b) => b.length - a.length);
|
|
141
|
+
for (const jsName of sortedKeys) {
|
|
142
|
+
const wgslName = MATH_MAP[jsName];
|
|
143
|
+
if (wgslName.includes('$')) {
|
|
144
|
+
wgsl = wgsl.replace(
|
|
145
|
+
new RegExp(jsName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\(([^,)]+),\\s*([^)]+)\\)', 'g'),
|
|
146
|
+
wgslName.replace('$1', '$1').replace('$2', '$2'),
|
|
147
|
+
);
|
|
148
|
+
wgsl = wgsl.replace(
|
|
149
|
+
new RegExp(jsName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\(([^)]+)\\)', 'g'),
|
|
150
|
+
wgslName.replace('$1', '$1').replace(/, *\$2.*/, ''),
|
|
151
|
+
);
|
|
152
|
+
} else {
|
|
153
|
+
wgsl = wgsl.split(jsName).join(wgslName);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Replace bare integer literals in arithmetic with floats
|
|
158
|
+
wgsl = wgsl.replace(/(?<=[^a-zA-Z_0-9.\[])(\d+)(?=[^a-zA-Z_0-9.\]$)])/g, '$1.0');
|
|
159
|
+
wgsl = wgsl.replace(/true\.0/g, 'true').replace(/false\.0/g, 'false');
|
|
160
|
+
wgsl = wgsl.replace(/(\d+\.\d+)\.0/g, '$1');
|
|
161
|
+
|
|
162
|
+
return wgsl;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Transpile a `define(name, fn)` function body to a WGSL assignment expression.
|
|
167
|
+
*
|
|
168
|
+
* Converts `data.value` → `inputName[i]`, `data.index`/`data.i` → `f32(i)`,
|
|
169
|
+
* then runs the result through {@link transpileExpression}.
|
|
170
|
+
*
|
|
171
|
+
* @param {Function} fn - The JS function `(data, { uniform }) => expression`
|
|
172
|
+
* @param {string} inputName - The storage buffer name (first param name)
|
|
173
|
+
* @returns {string} WGSL body like `result[i] = data[i] * alpha + ...;`
|
|
174
|
+
*/
|
|
175
|
+
export function transpileDefineBody(fn, inputName) {
|
|
176
|
+
const src = fn.toString();
|
|
177
|
+
|
|
178
|
+
// Extract body from arrow function
|
|
179
|
+
let body;
|
|
180
|
+
const arrowMatch = src.match(/=>\s*\{?\s*(?:return\s+)?(.+?);?\s*\}?\s*$/);
|
|
181
|
+
if (arrowMatch) {
|
|
182
|
+
body = arrowMatch[1].trim();
|
|
183
|
+
} else {
|
|
184
|
+
throw new GPUComputeError(`Cannot parse define function: ${src}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Replace data proxy references with WGSL equivalents
|
|
188
|
+
// data.value → inputName[i]
|
|
189
|
+
body = body.replace(/data\.value/g, `${inputName}[i]`);
|
|
190
|
+
// data.index → f32(i)
|
|
191
|
+
body = body.replace(/data\.index/g, 'f32(i)');
|
|
192
|
+
// data.i → f32(i) (but not data.index which was already handled)
|
|
193
|
+
body = body.replace(/data\.i\b(?!\w)/g, 'f32(i)');
|
|
194
|
+
|
|
195
|
+
// Run through expression transpiler for math, ternary, etc.
|
|
196
|
+
return transpileExpression(body);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Convert a JS function body to a WGSL expression.
|
|
201
|
+
*
|
|
202
|
+
* Handles common patterns:
|
|
203
|
+
* `x => Math.sqrt(x)` → `sqrt(x)`
|
|
204
|
+
* `x => x * 2 + 1` → `x * 2.0 + 1.0`
|
|
205
|
+
* `x => Math.max(0, x)` → `max(0.0, x)`
|
|
206
|
+
* `(a, b) => a + b` → `a + b`
|
|
207
|
+
* `x => x > 0 ? x : 0` → select(0.0, x, x > 0.0)
|
|
208
|
+
* `x => x === 0 ? 1 : 0` → select(0.0, 1.0, x == 0.0)
|
|
209
|
+
* `x => !x ? 1 : 0` → select(0.0, 1.0, !x)
|
|
210
|
+
*
|
|
211
|
+
* @param {Function} fn
|
|
212
|
+
* @returns {string} WGSL expression
|
|
213
|
+
*/
|
|
214
|
+
export function jsToWgsl(fn) {
|
|
215
|
+
const src = fn.toString();
|
|
216
|
+
|
|
217
|
+
// Extract the body — works for `x => expr` and `(x) => { return expr; }`
|
|
218
|
+
let body;
|
|
219
|
+
const arrowMatch = src.match(/=>\s*\{?\s*(?:return\s+)?(.+?);?\s*\}?\s*$/);
|
|
220
|
+
if (arrowMatch) {
|
|
221
|
+
body = arrowMatch[1].trim();
|
|
222
|
+
} else {
|
|
223
|
+
throw new GPUComputeError(`Cannot parse function body: ${src}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return transpileExpression(body);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Extract parameter names from a JS function's signature.
|
|
231
|
+
*
|
|
232
|
+
* @param {Function} fn
|
|
233
|
+
* @returns {string[]}
|
|
234
|
+
*/
|
|
235
|
+
export function getParamNames(fn) {
|
|
236
|
+
const src = fn.toString();
|
|
237
|
+
const match = src.match(/\(([^)]*)\)/);
|
|
238
|
+
if (!match) return ['x'];
|
|
239
|
+
return match[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
240
|
+
}
|
package/src/gpu/hooks.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Framework-agnostic hooks for GPU compute.
|
|
3
|
+
*
|
|
4
|
+
* Provides lifecycle-bound hooks that mirror the thread/pool hooks
|
|
5
|
+
* but are specialised for {@link GPUCompute} instances.
|
|
6
|
+
*
|
|
7
|
+
* The framework (Preact, React, etc.) is resolved at load time via the
|
|
8
|
+
* thread config system.
|
|
9
|
+
*
|
|
10
|
+
* @module gpu/hooks
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getHooks } from '../config/index.js';
|
|
14
|
+
import { GPUCompute } from './gpu.js';
|
|
15
|
+
|
|
16
|
+
// Resolve framework hooks at module load time (top-level await)
|
|
17
|
+
let useRef, useState, useEffect, useCallback, useMemo;
|
|
18
|
+
try {
|
|
19
|
+
({ useRef, useState, useEffect, useCallback, useMemo } = await getHooks());
|
|
20
|
+
} catch {
|
|
21
|
+
const _stub = () => { throw new Error('[thread] No framework installed. Install preact or react to use GPU hooks.'); };
|
|
22
|
+
useRef = _stub; useState = _stub; useEffect = _stub; useCallback = _stub; useMemo = _stub;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// useGPU
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create and manage a {@link GPUCompute} instance bound to a component's
|
|
31
|
+
* lifecycle.
|
|
32
|
+
*
|
|
33
|
+
* The GPU device is lazily initialised on the first `run()` call.
|
|
34
|
+
* The instance is automatically destroyed on unmount.
|
|
35
|
+
*
|
|
36
|
+
* @param {import('../types.js').GPUComputeOptions & { destroy?: boolean }} [options={}]
|
|
37
|
+
* GPU options plus `destroy: false` to skip automatic cleanup.
|
|
38
|
+
* @returns {{
|
|
39
|
+
* gpu: GPUCompute,
|
|
40
|
+
* run: (...args: any[]) => Promise<any>,
|
|
41
|
+
* pipe: (name?: string, count?: number) => import('./chains.js').DataPipelineChain,
|
|
42
|
+
* define: (name: string, fn: Function) => void,
|
|
43
|
+
* result: any,
|
|
44
|
+
* loading: boolean,
|
|
45
|
+
* error: string|null,
|
|
46
|
+
* status: string,
|
|
47
|
+
* metrics: import('../types.js').MetricsSnapshot,
|
|
48
|
+
* }}
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```jsx
|
|
52
|
+
* function GPUDemo({ prices }) {
|
|
53
|
+
* const { run, result, loading, error, status } = useGPU();
|
|
54
|
+
*
|
|
55
|
+
* const handleEMA = () => {
|
|
56
|
+
* run('ema', new Float32Array(prices), { alpha: 0.3 });
|
|
57
|
+
* };
|
|
58
|
+
*
|
|
59
|
+
* return (
|
|
60
|
+
* <div>
|
|
61
|
+
* <p>GPU: {status}</p>
|
|
62
|
+
* <button onClick={handleEMA} disabled={loading}>
|
|
63
|
+
* {loading ? 'Computing...' : 'Run EMA'}
|
|
64
|
+
* </button>
|
|
65
|
+
* {error && <p className="error">{error}</p>}
|
|
66
|
+
* {result && <pre>{JSON.stringify(Array.from(result))}</pre>}
|
|
67
|
+
* </div>
|
|
68
|
+
* );
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function useGPU(options = {}) {
|
|
73
|
+
const { destroy = true, ...gpuOpts } = options;
|
|
74
|
+
|
|
75
|
+
const gpuRef = useRef(null);
|
|
76
|
+
const [result, setResult] = useState(null);
|
|
77
|
+
const [error, setError] = useState(null);
|
|
78
|
+
const [loading, setLoading] = useState(false);
|
|
79
|
+
const [status, setStatus] = useState('idle');
|
|
80
|
+
const [metricsSnapshot, setMetricsSnapshot] = useState({});
|
|
81
|
+
|
|
82
|
+
// Create instance once
|
|
83
|
+
if (gpuRef.current === null) {
|
|
84
|
+
gpuRef.current = new GPUCompute(gpuOpts);
|
|
85
|
+
}
|
|
86
|
+
const gpu = gpuRef.current;
|
|
87
|
+
|
|
88
|
+
// Cleanup on unmount
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
return () => {
|
|
91
|
+
if (gpu && destroy) {
|
|
92
|
+
gpu.destroy();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}, [gpu, destroy]);
|
|
96
|
+
|
|
97
|
+
// Poll metrics & status
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!gpu) return;
|
|
100
|
+
const interval = setInterval(() => {
|
|
101
|
+
setStatus(gpu.status);
|
|
102
|
+
setMetricsSnapshot(gpu.metrics);
|
|
103
|
+
}, 500);
|
|
104
|
+
return () => clearInterval(interval);
|
|
105
|
+
}, [gpu]);
|
|
106
|
+
|
|
107
|
+
// Stable run function
|
|
108
|
+
const run = useCallback(
|
|
109
|
+
async (...args) => {
|
|
110
|
+
setLoading(true);
|
|
111
|
+
setError(null);
|
|
112
|
+
setStatus(gpu.status);
|
|
113
|
+
try {
|
|
114
|
+
const res = await gpu.run(...args);
|
|
115
|
+
setResult(res);
|
|
116
|
+
setLoading(false);
|
|
117
|
+
setStatus(gpu.status);
|
|
118
|
+
setMetricsSnapshot(gpu.metrics);
|
|
119
|
+
return res;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const msg = err.message || String(err);
|
|
122
|
+
setError(msg);
|
|
123
|
+
setLoading(false);
|
|
124
|
+
setStatus(gpu.status);
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
[gpu],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Stable pipe
|
|
132
|
+
const pipe = useCallback(
|
|
133
|
+
(name, count) => gpu.pipe(name, count),
|
|
134
|
+
[gpu],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Stable define
|
|
138
|
+
const define = useCallback(
|
|
139
|
+
(name, fn) => gpu.define(name, fn),
|
|
140
|
+
[gpu],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return { gpu, run, pipe, define, result, loading, error, status, metrics: metricsSnapshot };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// useGPURun
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get a stable `run` wrapper around an existing {@link GPUCompute}
|
|
152
|
+
* instance, with reactive loading / result / error state.
|
|
153
|
+
*
|
|
154
|
+
* Use this when you manage the GPU instance yourself (e.g. in a store
|
|
155
|
+
* or module-level variable).
|
|
156
|
+
*
|
|
157
|
+
* @param {GPUCompute} gpu - Existing GPU instance.
|
|
158
|
+
* @returns {{
|
|
159
|
+
* run: (...args: any[]) => Promise<any>,
|
|
160
|
+
* result: any,
|
|
161
|
+
* loading: boolean,
|
|
162
|
+
* error: string|null,
|
|
163
|
+
* }}
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```jsx
|
|
167
|
+
* // Shared GPU across components
|
|
168
|
+
* const sharedGPU = new GPUCompute();
|
|
169
|
+
*
|
|
170
|
+
* function ComponentA() {
|
|
171
|
+
* const { run, result, loading } = useGPURun(sharedGPU);
|
|
172
|
+
* return <button onClick={() => run('add', data1, data2)}>A</button>;
|
|
173
|
+
* }
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export function useGPURun(gpu) {
|
|
177
|
+
const [result, setResult] = useState(null);
|
|
178
|
+
const [error, setError] = useState(null);
|
|
179
|
+
const [loading, setLoading] = useState(false);
|
|
180
|
+
|
|
181
|
+
const run = useCallback(
|
|
182
|
+
async (...args) => {
|
|
183
|
+
if (!gpu) {
|
|
184
|
+
const msg = 'No GPU instance';
|
|
185
|
+
setError(msg);
|
|
186
|
+
throw new Error(msg);
|
|
187
|
+
}
|
|
188
|
+
setLoading(true);
|
|
189
|
+
setError(null);
|
|
190
|
+
try {
|
|
191
|
+
const res = await gpu.run(...args);
|
|
192
|
+
setResult(res);
|
|
193
|
+
setLoading(false);
|
|
194
|
+
return res;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const msg = err.message || String(err);
|
|
197
|
+
setError(msg);
|
|
198
|
+
setLoading(false);
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
[gpu],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
return useMemo(() => ({ run, result, loading, error }), [run, result, loading, error]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// useGPUMetrics
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Subscribe to a {@link GPUCompute} instance's metrics and re-render
|
|
214
|
+
* on updates.
|
|
215
|
+
*
|
|
216
|
+
* @param {GPUCompute} [gpu] - GPU instance. If `null`/`undefined`,
|
|
217
|
+
* returns a zeroed snapshot.
|
|
218
|
+
* @param {number} [intervalMs=500] - Polling interval in ms.
|
|
219
|
+
* @returns {import('../types.js').MetricsSnapshot}
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```jsx
|
|
223
|
+
* function GPUDashboard({ gpu }) {
|
|
224
|
+
* const metrics = useGPUMetrics(gpu, 250);
|
|
225
|
+
*
|
|
226
|
+
* return (
|
|
227
|
+
* <div>
|
|
228
|
+
* <span>{metrics.count} dispatches</span>
|
|
229
|
+
* <span>{metrics.avg?.toFixed(1)}ms avg</span>
|
|
230
|
+
* </div>
|
|
231
|
+
* );
|
|
232
|
+
* }
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export function useGPUMetrics(gpu, intervalMs = 500) {
|
|
236
|
+
const [snapshot, setSnapshot] = useState(
|
|
237
|
+
gpu ? gpu.metrics : { count: 0, errors: 0, avg: 0, min: 0, max: 0, throughput: 0, errorRate: 0 },
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!gpu) return;
|
|
242
|
+
const interval = setInterval(() => {
|
|
243
|
+
setSnapshot(gpu.metrics);
|
|
244
|
+
}, intervalMs);
|
|
245
|
+
return () => clearInterval(interval);
|
|
246
|
+
}, [gpu, intervalMs]);
|
|
247
|
+
|
|
248
|
+
return snapshot;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// useGPUStatus
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Reactive status string for a {@link GPUCompute} instance.
|
|
257
|
+
*
|
|
258
|
+
* Returns one of: `'idle'`, `'running'`, `'error'`, `'unavailable'`.
|
|
259
|
+
*
|
|
260
|
+
* @param {GPUCompute} [gpu] - GPU instance.
|
|
261
|
+
* @param {number} [intervalMs=500] - Polling interval in ms.
|
|
262
|
+
* @returns {string}
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```jsx
|
|
266
|
+
* function StatusBadge({ gpu }) {
|
|
267
|
+
* const status = useGPUStatus(gpu);
|
|
268
|
+
* return <span className={`badge badge-${status}`}>{status}</span>;
|
|
269
|
+
* }
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
export function useGPUStatus(gpu, intervalMs = 500) {
|
|
273
|
+
const [status, setStatus] = useState(gpu ? gpu.status : 'idle');
|
|
274
|
+
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
if (!gpu) return;
|
|
277
|
+
const interval = setInterval(() => {
|
|
278
|
+
setStatus(gpu.status);
|
|
279
|
+
}, intervalMs);
|
|
280
|
+
return () => clearInterval(interval);
|
|
281
|
+
}, [gpu, intervalMs]);
|
|
282
|
+
|
|
283
|
+
return status;
|
|
284
|
+
}
|
package/src/gpu/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file GPU compute barrel export.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports every public symbol from the GPU sub-modules so
|
|
5
|
+
* consumers can do `import { GPUCompute, ... } from '../gpu/index.js'`.
|
|
6
|
+
*
|
|
7
|
+
* @module gpu
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { GPUCompute, createGPUCompute, createGPUWithFallback, outputSpec, uniform } from './gpu.js';
|
|
11
|
+
export { PipelineChain, DataPipelineChain } from './chains.js';
|
|
12
|
+
export { buildShader, BUILT_IN_OPS, BUILT_IN_OP_NAMES, SPECIAL_OPS } from './shaders.js';
|
|
13
|
+
export { runSpecial, runMatmul, buildMatmulShader, runReduce, getReduceBody,
|
|
14
|
+
runHistogram, runArgMaxMin, runScan } from './special.js';
|
|
15
|
+
export { useGPU, useGPURun, useGPUMetrics, useGPUStatus } from './hooks.js';
|
|
16
|
+
export { createGPUBinder, createGPUSignalBinder, createGPUStoreBinder } from './adapters.js';
|