wgblas 1.2.1 → 2.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 +1 -1
- package/README.md +54 -72
- package/dist/wgblas.browser.js +1637 -858
- package/index.d.mts +51 -6
- package/index.mjs +8 -0
- package/package.json +56 -2
- package/src/classes/GpuMatrix.mjs +17 -10
- package/src/classes/GpuVector.mjs +28 -10
- package/src/dasum/dasum.d.mts +6 -6
- package/src/dasum/dasum.mjs +25 -21
- package/src/devdocs.mjs +13 -0
- package/src/idamax/idamax.d.mts +69 -0
- package/src/idamax/idamax.mjs +130 -0
- package/src/init.mjs +115 -49
- package/src/isamax/isamax.d.mts +21 -3
- package/src/isamax/isamax.mjs +16 -14
- package/src/random/random.d.mts +1 -0
- package/src/sasum/sasum.d.mts +3 -3
- package/src/sasum/sasum.mjs +15 -13
- package/src/saxpy/saxpy.d.mts +3 -3
- package/src/saxpy/saxpy.mjs +10 -8
- package/src/scopy/scopy.d.mts +3 -3
- package/src/scopy/scopy.mjs +10 -8
- package/src/sdot/sdot.d.mts +3 -3
- package/src/sdot/sdot.mjs +16 -14
- package/src/sgemm/sgemm.d.mts +102 -0
- package/src/sgemm/sgemm.mjs +208 -0
- package/src/sgemmtr/sgemmtr.d.mts +104 -0
- package/src/sgemmtr/sgemmtr.mjs +204 -0
- package/src/sgemv/sgemv.d.mts +1 -38
- package/src/sgemv/sgemv.mjs +42 -26
- package/src/sger/sger.d.mts +1 -34
- package/src/sger/sger.mjs +12 -8
- package/src/shaders/block_transfer.wgsl +42 -0
- package/src/shaders/dasum.wgsl +3 -2
- package/src/shaders/f64/dekker.wgsl +4 -85
- package/src/shaders/f64/utils/abs.wgsl +10 -0
- package/src/shaders/f64/utils/add.wgsl +77 -0
- package/src/shaders/f64/utils/equal.wgsl +7 -0
- package/src/shaders/f64/utils/greater.wgsl +12 -0
- package/src/shaders/f64/utils/multiply.wgsl +81 -0
- package/src/shaders/idamax.wgsl +96 -0
- package/src/shaders/index.mjs +164 -14
- package/src/shaders/reduction/argmaxF64.wgsl +50 -0
- package/src/shaders/reduction/scaledSum.wgsl +65 -0
- package/src/shaders/reduction/sumF64.wgsl +2 -2
- package/src/shaders/sgemm_large.wgsl +206 -0
- package/src/shaders/sgemm_small.wgsl +212 -0
- package/src/shaders/sgemmtr_large.wgsl +120 -0
- package/src/shaders/sgemmtr_small.wgsl +113 -0
- package/src/shaders/sgemv_n.wgsl +3 -1
- package/src/shaders/sgemv_t.wgsl +3 -1
- package/src/shaders/snrm2.wgsl +72 -23
- package/src/shaders/ssymv.wgsl +3 -1
- package/src/shaders/symmetrize.wgsl +31 -0
- package/src/shaders/triangularize.wgsl +44 -0
- package/src/snrm2/snrm2.d.mts +3 -3
- package/src/snrm2/snrm2.mjs +33 -21
- package/src/srot/srot.d.mts +3 -5
- package/src/srot/srot.mjs +11 -9
- package/src/srotm/srotm.d.mts +3 -5
- package/src/srotm/srotm.mjs +19 -10
- package/src/sscal/sscal.d.mts +4 -4
- package/src/sscal/sscal.mjs +12 -10
- package/src/sswap/sswap.d.mts +3 -3
- package/src/sswap/sswap.mjs +11 -9
- package/src/ssymm/ssymm.d.mts +103 -0
- package/src/ssymm/ssymm.mjs +218 -0
- package/src/ssymv/ssymv.d.mts +1 -36
- package/src/ssymv/ssymv.mjs +12 -8
- package/src/ssyr/ssyr.d.mts +1 -30
- package/src/ssyr/ssyr.mjs +11 -7
- package/src/ssyr2/ssyr2.d.mts +1 -34
- package/src/ssyr2/ssyr2.mjs +12 -8
- package/src/ssyr2k/ssyr2k.d.mts +100 -0
- package/src/ssyr2k/ssyr2k.mjs +202 -0
- package/src/ssyrk/ssyrk.d.mts +90 -0
- package/src/ssyrk/ssyrk.mjs +177 -0
- package/src/strmm/strmm.d.mts +100 -0
- package/src/strmm/strmm.mjs +226 -0
- package/src/strmv/strmv.d.mts +1 -36
- package/src/strmv/strmv.mjs +12 -8
- package/src/strsm/strsm.d.mts +99 -0
- package/src/strsm/strsm.mjs +360 -0
- package/src/strsv/strsv.d.mts +1 -32
- package/src/strsv/strsv.mjs +18 -12
- package/src/util/benchmark.mjs +4 -6
- package/src/util/bindgroup.mjs +1 -3
- package/src/util/buffer.mjs +116 -20
- package/src/util/compute.mjs +12 -12
- package/src/util/constants.mjs +57 -0
- package/src/util/device.mjs +34 -0
- package/src/util/f64.mjs +3 -3
- package/src/util/pipeline.mjs +5 -6
- package/src/util/workgroup.mjs +55 -7
- package/src/shaders/browser-shaders.mjs +0 -55
package/src/util/buffer.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/** @module devdocs/utility-functions/buffer */
|
|
2
|
-
import { getDevice } from "../init.mjs";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Destroys one or more GPU buffers. Accepts individual buffers or arrays of buffers.
|
|
@@ -10,12 +9,38 @@ export function destroyBuffers(...buffers) {
|
|
|
10
9
|
buffers.flat().forEach((b) => b.destroy());
|
|
11
10
|
}
|
|
12
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Throws if `byteSize` is more than this device can bind as a storage buffer.
|
|
14
|
+
*
|
|
15
|
+
* Every storage buffer the library creates goes through here. WebGPU accepts an
|
|
16
|
+
* oversized `createBuffer` and only rejects it later, when it is bound — as a
|
|
17
|
+
* `GPUValidationError` naming a bind group index rather than an operand, which
|
|
18
|
+
* gives no clue which allocation was at fault. Failing at creation, with the
|
|
19
|
+
* buffer's own label, points straight at it.
|
|
20
|
+
*
|
|
21
|
+
* @param {GPUDevice} device
|
|
22
|
+
* @param {number} byteSize
|
|
23
|
+
* @param {string} label - the buffer's debug label, quoted in the error
|
|
24
|
+
* @throws {Error} if `byteSize` exceeds `maxStorageBufferBindingSize`
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
function requireStorageSize(device, byteSize, label) {
|
|
28
|
+
const maxSize = device.limits.maxStorageBufferBindingSize;
|
|
29
|
+
if (byteSize > maxSize) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Buffer "${label}" needs ${byteSize} bytes, exceeding this device's ` +
|
|
32
|
+
`maxStorageBufferBindingSize (${maxSize} bytes). The operands are too large for this device.`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
13
37
|
/**
|
|
14
38
|
* Creates a GPU storage buffer and uploads `data` into it via mapped-at-creation.
|
|
15
39
|
* The mapped view is constructed from `data`'s own typed-array constructor, so
|
|
16
40
|
* bits are copied as-is regardless of element type (e.g. a Uint32Array's raw
|
|
17
41
|
* bit patterns are preserved — critical for dasum's aux half, which must
|
|
18
42
|
* never pass through a Float32Array view and risk NaN-bit-pattern canonicalization).
|
|
43
|
+
* @param {GPUDevice} device
|
|
19
44
|
* @param {Float32Array|Uint32Array|Int32Array} data
|
|
20
45
|
* @param {string} [label] - debug label visible in browser DevTools GPU inspection
|
|
21
46
|
* @param {boolean} [readback=false] - add `COPY_SRC` so the buffer can be copied to a readback buffer
|
|
@@ -26,17 +51,9 @@ export function destroyBuffers(...buffers) {
|
|
|
26
51
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUBuffer/unmap GPUBuffer.unmap()}
|
|
27
52
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUSupportedLimits GPUSupportedLimits} (`maxStorageBufferBindingSize`)
|
|
28
53
|
*/
|
|
29
|
-
export function uploadBuffer(data, label = "blas-input", readback = false) {
|
|
30
|
-
const device = getDevice();
|
|
31
|
-
|
|
32
|
-
// User-facing boundary: give a clear error instead of a cryptic GPUValidationError.
|
|
33
|
-
const maxSize = device.limits.maxStorageBufferBindingSize;
|
|
54
|
+
export function uploadBuffer(device, data, label = "blas-input", readback = false) {
|
|
34
55
|
const byteSize = data.byteLength;
|
|
35
|
-
|
|
36
|
-
throw new Error(
|
|
37
|
-
`Buffer size ${byteSize} bytes exceeds device limit of ${maxSize} bytes.`,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
56
|
+
requireStorageSize(device, byteSize, label);
|
|
40
57
|
|
|
41
58
|
const usage = readback
|
|
42
59
|
? GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
@@ -60,30 +77,36 @@ export function uploadBuffer(data, label = "blas-input", readback = false) {
|
|
|
60
77
|
/**
|
|
61
78
|
* Creates an uninitialised GPU storage buffer. Used for intermediate buffers
|
|
62
79
|
* that are written by a shader before being read.
|
|
80
|
+
* @param {GPUDevice} device
|
|
63
81
|
* @param {number} size - byte size
|
|
64
82
|
* @param {string} [label] - debug label visible in browser DevTools GPU inspection
|
|
83
|
+
* @param {number} [extraUsage=0] - additional `GPUBufferUsage` flags OR'd in alongside `STORAGE`
|
|
84
|
+
* (e.g. `GPUBufferUsage.COPY_DST` for a buffer that's also a `copyBufferToBuffer` destination)
|
|
65
85
|
* @returns {GPUBuffer}
|
|
86
|
+
* @throws {Error} if `size` exceeds the device's `maxStorageBufferBindingSize`
|
|
66
87
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createBuffer GPUDevice.createBuffer()}
|
|
67
88
|
*/
|
|
68
|
-
export function createStorageBuffer(size, label = "blas-storage") {
|
|
69
|
-
|
|
89
|
+
export function createStorageBuffer(device, size, label = "blas-storage", extraUsage = 0) {
|
|
90
|
+
requireStorageSize(device, size, label);
|
|
70
91
|
return device.createBuffer({
|
|
71
92
|
label,
|
|
72
93
|
size,
|
|
73
|
-
usage: GPUBufferUsage.STORAGE,
|
|
94
|
+
usage: GPUBufferUsage.STORAGE | extraUsage,
|
|
74
95
|
});
|
|
75
96
|
}
|
|
76
97
|
|
|
77
98
|
/**
|
|
78
99
|
* Creates a GPU storage buffer with `COPY_SRC` so its contents can be
|
|
79
100
|
* copied to a CPU-readable readback buffer after the shader runs.
|
|
101
|
+
* @param {GPUDevice} device
|
|
80
102
|
* @param {number} size - byte size
|
|
81
103
|
* @param {string} [label] - debug label visible in browser DevTools GPU inspection
|
|
82
104
|
* @returns {GPUBuffer}
|
|
105
|
+
* @throws {Error} if `size` exceeds the device's `maxStorageBufferBindingSize`
|
|
83
106
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createBuffer GPUDevice.createBuffer()}
|
|
84
107
|
*/
|
|
85
|
-
export function createResultBuffer(size, label = "blas-result") {
|
|
86
|
-
|
|
108
|
+
export function createResultBuffer(device, size, label = "blas-result") {
|
|
109
|
+
requireStorageSize(device, size, label);
|
|
87
110
|
return device.createBuffer({
|
|
88
111
|
label,
|
|
89
112
|
size,
|
|
@@ -95,13 +118,13 @@ export function createResultBuffer(size, label = "blas-result") {
|
|
|
95
118
|
* Appends a `copyBufferToBuffer` command to `commandEncoder` that copies
|
|
96
119
|
* `sourceBuffer` into a new `MAP_READ` buffer. Returns that readback buffer;
|
|
97
120
|
* call `readBuffer.mapAsync(GPUMapMode.READ)` after submitting the encoder.
|
|
121
|
+
* @param {GPUDevice} device
|
|
98
122
|
* @param {GPUCommandEncoder} commandEncoder
|
|
99
123
|
* @param {GPUBuffer} sourceBuffer
|
|
100
124
|
* @returns {GPUBuffer}
|
|
101
125
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/copyBufferToBuffer GPUCommandEncoder.copyBufferToBuffer()}
|
|
102
126
|
*/
|
|
103
|
-
export function stageReadback(commandEncoder, sourceBuffer) {
|
|
104
|
-
const device = getDevice();
|
|
127
|
+
export function stageReadback(device, commandEncoder, sourceBuffer) {
|
|
105
128
|
|
|
106
129
|
// COPY_DST: receives the copyBufferToBuffer transfer; MAP_READ: lets the CPU map and read it back.
|
|
107
130
|
const readBuffer = device.createBuffer({
|
|
@@ -119,17 +142,90 @@ export function stageReadback(commandEncoder, sourceBuffer) {
|
|
|
119
142
|
return readBuffer;
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
// Minimum bindable size for an array<vec4<f32>> view: one 16-byte element.
|
|
146
|
+
const VEC4_ELEM_BYTES = 16;
|
|
147
|
+
|
|
148
|
+
// Dummy STORAGE buffer bound into a kernel's unused vec4-view slot when the
|
|
149
|
+
// real operand can't host even one vec4 (tiny-matrix edge cases, where the
|
|
150
|
+
// stride check forces the scalar path anyway). Cached per device.
|
|
151
|
+
const _vec4Fallbacks = new WeakMap();
|
|
152
|
+
function vec4FallbackBuffer(device) {
|
|
153
|
+
let b = _vec4Fallbacks.get(device);
|
|
154
|
+
if (!b) {
|
|
155
|
+
b = device.createBuffer({
|
|
156
|
+
label: "blas-vec4-fallback",
|
|
157
|
+
size: VEC4_ELEM_BYTES,
|
|
158
|
+
usage: GPUBufferUsage.STORAGE,
|
|
159
|
+
});
|
|
160
|
+
_vec4Fallbacks.set(device, b);
|
|
161
|
+
}
|
|
162
|
+
return b;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Bind-group entry exposing `buffer`'s bytes as an `array<vec4<f32>>` view —
|
|
167
|
+
* the twin binding that lets a shader issue 16-byte vector loads alongside
|
|
168
|
+
* scalar access of the same storage (bind the same GPUBuffer twice: once as
|
|
169
|
+
* `array<f32>`, once through this). The view's size is rounded down to a
|
|
170
|
+
* multiple of 16 because some backends reject non-multiple-of-16 ranges for
|
|
171
|
+
* vec4 arrays; whenever a kernel's vector path is usable (stride % 4 == 0)
|
|
172
|
+
* the buffer size is itself a multiple of 16, so the rounding never truncates
|
|
173
|
+
* a component the vector path would actually read. Buffers smaller than one
|
|
174
|
+
* vec4 element get a shared dummy storage buffer bound instead — the shader
|
|
175
|
+
* never dereferences it on those shapes.
|
|
176
|
+
* @param {GPUBuffer|{buffer: GPUBuffer, offset?: number, size?: number}} entry - whole buffer or sub-range, matching what the scalar slot binds
|
|
177
|
+
* @returns {{buffer: GPUBuffer, offset: number, size: number}}
|
|
178
|
+
*/
|
|
179
|
+
export function vec4ViewBinding(device, entry) {
|
|
180
|
+
const buffer = entry instanceof GPUBuffer ? entry : entry.buffer;
|
|
181
|
+
const offset = entry instanceof GPUBuffer ? 0 : (entry.offset ?? 0);
|
|
182
|
+
const avail = (entry instanceof GPUBuffer ? entry.size : (entry.size ?? buffer.size - offset));
|
|
183
|
+
const size = Math.floor(avail / VEC4_ELEM_BYTES) * VEC4_ELEM_BYTES;
|
|
184
|
+
if (size < VEC4_ELEM_BYTES) {
|
|
185
|
+
return { buffer: vec4FallbackBuffer(device), offset: 0, size: VEC4_ELEM_BYTES };
|
|
186
|
+
}
|
|
187
|
+
return { buffer, offset, size };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Whether every in-bounds element of a matrix operand is reachable through
|
|
192
|
+
* the `array<vec4<f32>>` view that {@link vec4ViewBinding} produces for it.
|
|
193
|
+
* The view truncates the binding down to a multiple of 16 bytes, so a
|
|
194
|
+
* tightly-uploaded array can hold valid matrix elements past the view's end
|
|
195
|
+
* even while its stride is 4-aligned (e.g. a column-major m×1 operand with
|
|
196
|
+
* padded lda uploaded without padding cells). Call this with the kernel-side
|
|
197
|
+
* dimensions and only take a shader's vectorized path when it returns true;
|
|
198
|
+
* the scalar fallback reads the full storage and is always correct.
|
|
199
|
+
* @param {GPUBuffer|{buffer: GPUBuffer, offset?: number, size?: number}} entry - what the scalar slot binds
|
|
200
|
+
* @param {number} stride - the operand's leading dimension as seen by the kernel
|
|
201
|
+
* @param {number} outerCount - extent of the stride-multiplied dimension
|
|
202
|
+
* @param {number} innerCount - extent of the contiguous dimension
|
|
203
|
+
* @returns {boolean}
|
|
204
|
+
*/
|
|
205
|
+
export function vec4Usable(entry, stride, outerCount, innerCount) {
|
|
206
|
+
if (stride % 4 !== 0) return false;
|
|
207
|
+
const buffer = entry instanceof GPUBuffer ? entry : entry.buffer;
|
|
208
|
+
const offset = entry instanceof GPUBuffer ? 0 : (entry.offset ?? 0);
|
|
209
|
+
const avail = (entry instanceof GPUBuffer ? buffer.size : (entry.size ?? buffer.size - offset));
|
|
210
|
+
const viewFloats = Math.floor(avail / VEC4_ELEM_BYTES) * 4;
|
|
211
|
+
if (viewFloats <= 0) return false;
|
|
212
|
+
// Highest flat index any masked-in component can touch; usable iff its
|
|
213
|
+
// containing vec4 ends within the view.
|
|
214
|
+
const maxFlat = (Math.max(outerCount, 1) - 1) * stride + (Math.max(innerCount, 1) - 1);
|
|
215
|
+
return Math.floor(maxFlat / 4) * 4 + 4 <= viewFloats;
|
|
216
|
+
}
|
|
217
|
+
|
|
122
218
|
/**
|
|
123
219
|
* Packs an array of typed scalar values into a uniform buffer aligned to 16 bytes.
|
|
124
220
|
* Each entry specifies the value and its WGSL type (`"f32"`, `"u32"`, or `"i32"`).
|
|
125
221
|
* The order of entries must match the field order in the shader's `Params` struct.
|
|
222
|
+
* @param {GPUDevice} device
|
|
126
223
|
* @param {{ value: number, type: "f32"|"u32"|"i32" }[]} params
|
|
127
224
|
* @param {string} [label] - debug label visible in browser DevTools GPU inspection
|
|
128
225
|
* @returns {GPUBuffer}
|
|
129
226
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUQueue/writeBuffer GPUQueue.writeBuffer()}
|
|
130
227
|
*/
|
|
131
|
-
export function createParamsBuffer(params, label = "blas-params") {
|
|
132
|
-
const device = getDevice();
|
|
228
|
+
export function createParamsBuffer(device, params, label = "blas-params") {
|
|
133
229
|
|
|
134
230
|
const rawSize = params.length * 4;
|
|
135
231
|
const size = Math.ceil(rawSize / 16) * 16;
|
package/src/util/compute.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/** @module devdocs/utility-functions/compute */
|
|
2
|
-
import { getDevice } from "../init.mjs";
|
|
3
2
|
import { beginTimestamp, resolveTimestamp } from "./benchmark.mjs";
|
|
4
3
|
|
|
5
4
|
// Anchors the pass encoder to its command encoder to prevent premature GC.
|
|
@@ -10,8 +9,7 @@ const _passEncoders = new WeakMap();
|
|
|
10
9
|
* @param {GPUCommandEncoder} commandEncoder
|
|
11
10
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUQueue/submit GPUQueue.submit()}
|
|
12
11
|
*/
|
|
13
|
-
export function submit(commandEncoder) {
|
|
14
|
-
const device = getDevice();
|
|
12
|
+
export function submit(device, commandEncoder) {
|
|
15
13
|
device.queue.submit([commandEncoder.finish()]);
|
|
16
14
|
}
|
|
17
15
|
|
|
@@ -23,9 +21,8 @@ export function submit(commandEncoder) {
|
|
|
23
21
|
* @returns {{ commandEncoder: GPUCommandEncoder, querySet: GPUQuerySet|null, passDescriptor: object|undefined }}
|
|
24
22
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createCommandEncoder GPUDevice.createCommandEncoder()}
|
|
25
23
|
*/
|
|
26
|
-
export function beginTimedEncoder() {
|
|
27
|
-
const
|
|
28
|
-
const { querySet, passDescriptor } = beginTimestamp();
|
|
24
|
+
export function beginTimedEncoder(device) {
|
|
25
|
+
const { querySet, passDescriptor } = beginTimestamp(device);
|
|
29
26
|
const commandEncoder = device.createCommandEncoder();
|
|
30
27
|
return { commandEncoder, querySet, passDescriptor };
|
|
31
28
|
}
|
|
@@ -38,7 +35,8 @@ export function beginTimedEncoder() {
|
|
|
38
35
|
* @param {GPUCommandEncoder} commandEncoder
|
|
39
36
|
* @param {GPUComputePipeline} pipeline
|
|
40
37
|
* @param {GPUBindGroup} bindGroup
|
|
41
|
-
* @param {number | { x: number, y: number }} workgroups - workgroup count;
|
|
38
|
+
* @param {number | { x: number, y: number, z?: number }} workgroups - workgroup count;
|
|
39
|
+
* number for 1D dispatch, `{x, y}` for 2D, `{x, y, z}` for 3D (z defaults to 1)
|
|
42
40
|
* @param {object} [passDescriptor] - passed to `beginComputePass`, e.g. for timestamp writes
|
|
43
41
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginComputePass GPUCommandEncoder.beginComputePass()}
|
|
44
42
|
*/
|
|
@@ -51,7 +49,8 @@ export function encodePass(commandEncoder, pipeline, bindGroup, workgroups, pass
|
|
|
51
49
|
if (typeof workgroups === "number") {
|
|
52
50
|
passEncoder.dispatchWorkgroups(workgroups);
|
|
53
51
|
} else {
|
|
54
|
-
|
|
52
|
+
// `?? 1` is load-bearing — confirmed passing undefined (from an {x,y}-only caller) crashes the process, not just no-ops.
|
|
53
|
+
passEncoder.dispatchWorkgroups(workgroups.x, workgroups.y, workgroups.z ?? 1);
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
passEncoder.end();
|
|
@@ -64,15 +63,16 @@ export function encodePass(commandEncoder, pipeline, bindGroup, workgroups, pass
|
|
|
64
63
|
* dispatches workgroups, and optionally wraps the pass in GPU timestamp queries.
|
|
65
64
|
* @param {GPUComputePipeline} pipeline
|
|
66
65
|
* @param {GPUBindGroup} bindGroup
|
|
67
|
-
* @param {number | { x: number, y: number }} workgroups - workgroup count;
|
|
66
|
+
* @param {number | { x: number, y: number, z?: number }} workgroups - workgroup count;
|
|
67
|
+
* number for 1D dispatch, `{x, y}` for 2D, `{x, y, z}` for 3D (z defaults to 1)
|
|
68
68
|
* @returns {{ commandEncoder: GPUCommandEncoder, ts: any }} encoded commands and timestamp handle
|
|
69
69
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createCommandEncoder GPUDevice.createCommandEncoder()}
|
|
70
70
|
*/
|
|
71
|
-
export function runComputePass(pipeline, bindGroup, workgroups) {
|
|
72
|
-
const { commandEncoder, querySet, passDescriptor } = beginTimedEncoder();
|
|
71
|
+
export function runComputePass(device, pipeline, bindGroup, workgroups) {
|
|
72
|
+
const { commandEncoder, querySet, passDescriptor } = beginTimedEncoder(device);
|
|
73
73
|
encodePass(commandEncoder, pipeline, bindGroup, workgroups, passDescriptor);
|
|
74
74
|
|
|
75
|
-
const ts = resolveTimestamp(commandEncoder, querySet);
|
|
75
|
+
const ts = resolveTimestamp(device, commandEncoder, querySet);
|
|
76
76
|
|
|
77
77
|
return { commandEncoder, ts };
|
|
78
78
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** @module devdocs/utility-functions/constants */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Single source of truth for every constant the JS dispatch side shares with a
|
|
5
|
+
* shader.
|
|
6
|
+
*
|
|
7
|
+
* WGSL has no import mechanism, so each shader necessarily declares its own
|
|
8
|
+
* copy of these values. That makes them a silent-corruption hazard: change
|
|
9
|
+
* `BM` in `sgemm_large.wgsl` without changing `BM_LARGE` here and the host
|
|
10
|
+
* dispatches the wrong grid — too few workgroups computes part of the matrix
|
|
11
|
+
* and reports success. Hoisting them here removes the JS-to-JS duplication;
|
|
12
|
+
* `tests/utils/test.constants.js` closes the remaining JS-to-WGSL gap
|
|
13
|
+
* by parsing the shader sources and asserting they still agree.
|
|
14
|
+
*
|
|
15
|
+
* Every export below names the shader declaration it mirrors. Changing one
|
|
16
|
+
* means changing both, and the test will tell you if you forget.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// --- gemm block tiles ------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** `BM` in sgemm_small.wgsl / sgemmtr_small.wgsl. */
|
|
22
|
+
export const BM_SMALL = 32;
|
|
23
|
+
/** `BN` in sgemm_small.wgsl / sgemmtr_small.wgsl. */
|
|
24
|
+
export const BN_SMALL = 32;
|
|
25
|
+
/** `BM` in sgemm_large.wgsl / sgemmtr_large.wgsl. */
|
|
26
|
+
export const BM_LARGE = 64;
|
|
27
|
+
/** `BN` in sgemm_large.wgsl / sgemmtr_large.wgsl. */
|
|
28
|
+
export const BN_LARGE = 64;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The large tile only pays for its bigger workgroups once the problem needs at
|
|
32
|
+
* least a 6x6 grid of them; below that the small tile wins. JS-only (no shader
|
|
33
|
+
* counterpart) — it selects *which* shader runs.
|
|
34
|
+
*/
|
|
35
|
+
export const LARGE_TILE_WORKGROUP_THRESHOLD = 36;
|
|
36
|
+
|
|
37
|
+
// --- 1D / reduction kernels ------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `const WGS: u32 = 64` — declared by every 1D and reduction shader, and the
|
|
41
|
+
* workgroup size `calcWorkgroups` divides by for a 1D dispatch.
|
|
42
|
+
*/
|
|
43
|
+
export const WGS = 64;
|
|
44
|
+
|
|
45
|
+
// --- 2D helper kernels -----------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `@workgroup_size(8, 8)` in symmetrize.wgsl, triangularize.wgsl and
|
|
49
|
+
* block_transfer.wgsl, and the size `calcWorkgroups` divides by per dimension
|
|
50
|
+
* for a 2D dispatch.
|
|
51
|
+
*/
|
|
52
|
+
export const TILE_WG_2D = 8;
|
|
53
|
+
|
|
54
|
+
// --- triangular solve ------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/** `BLOCK_SIZE` in strsv_invert_block.wgsl — the diagonal block order. */
|
|
57
|
+
export const BLOCK_SIZE = 64;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** @module devdocs/utility-functions/device */
|
|
2
|
+
import { GpuVector } from "../classes/GpuVector.mjs";
|
|
3
|
+
import { GpuMatrix } from "../classes/GpuMatrix.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Throws if any GPU-resident operand belongs to a device other than the one
|
|
7
|
+
* the routine was called with.
|
|
8
|
+
*
|
|
9
|
+
* A `GPUBuffer` is bound to the device that created it, and WebGPU has no way
|
|
10
|
+
* to share one across devices. Handing a routine a `GpuMatrix` from device A
|
|
11
|
+
* while passing device B fails deep inside bind-group creation as a
|
|
12
|
+
* `GPUValidationError` with no indication that two devices are involved —
|
|
13
|
+
* this turns it into a named, actionable error at the call boundary.
|
|
14
|
+
*
|
|
15
|
+
* Scalars, plain typed arrays and `undefined` entries are ignored, so callers
|
|
16
|
+
* can pass their whole operand set without filtering.
|
|
17
|
+
*
|
|
18
|
+
* @param {GPUDevice} device - the device the routine will dispatch on
|
|
19
|
+
* @param {string} routine - routine name, for the error message
|
|
20
|
+
* @param {Record<string, unknown>} operands - operand name -> value
|
|
21
|
+
* @throws {Error} if an operand is GPU-resident on a different device
|
|
22
|
+
*/
|
|
23
|
+
export function requireSameDevice(device, routine, operands) {
|
|
24
|
+
for (const [name, value] of Object.entries(operands)) {
|
|
25
|
+
if (!(value instanceof GpuVector) && !(value instanceof GpuMatrix)) continue;
|
|
26
|
+
if (value.device !== device) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`${routine}: ${name} belongs to a different GPUDevice than the one passed in. ` +
|
|
29
|
+
"GPU buffers cannot be shared across devices — recreate the operand on this " +
|
|
30
|
+
"device, or call the routine with the device that owns it.",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/util/f64.mjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// Double-double f64 emulation — splits a double into a (hi, lo) pair of f32
|
|
4
4
|
// values with hi+lo approximating the original, hi holding the leading bits
|
|
5
|
-
// and lo the rounding error hi lost on its own. See
|
|
6
|
-
//
|
|
7
|
-
// (Dekker's algorithm).
|
|
5
|
+
// and lo the rounding error hi lost on its own. See src/shaders/f64/ (the DD
|
|
6
|
+
// struct in dekker.wgsl, operations in utils/) for the GPU-side arithmetic
|
|
7
|
+
// this pairs with (Dekker's algorithm).
|
|
8
8
|
//
|
|
9
9
|
// Not a value-preserving exact split: double-double buys roughly 2x f32's
|
|
10
10
|
// mantissa (~48 bits vs f32's 24), less than real f64's 52-bit mantissa.
|
package/src/util/pipeline.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/** @module devdocs/utility-functions/pipeline */
|
|
2
|
-
import { getDevice } from "../init.mjs";
|
|
3
2
|
|
|
4
3
|
// WeakMap keyed by GPUDevice so pipelines are released automatically when the device is destroyed.
|
|
5
4
|
const _pipelines = new WeakMap();
|
|
@@ -24,21 +23,21 @@ export async function getPipeline(device, shaderName, entryPoint = "main") {
|
|
|
24
23
|
const names = Array.isArray(shaderName) ? shaderName : [shaderName];
|
|
25
24
|
const key = `${names.join("+")}::${entryPoint}`;
|
|
26
25
|
if (!byName.has(key)) {
|
|
27
|
-
byName.set(key, await loadShader(names, entryPoint));
|
|
26
|
+
byName.set(key, await loadShader(device, names, entryPoint));
|
|
28
27
|
}
|
|
29
28
|
return byName.get(key);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
32
|
* Loads WGSL source for `shaderName`. In the browser, reads from the inline bundle
|
|
34
|
-
* (`
|
|
33
|
+
* (`shaders/index.mjs`'s `shaderSources`); in Node.js, reads the `.wgsl` file directly from disk.
|
|
35
34
|
* @param {string} shaderName
|
|
36
35
|
* @returns {Promise<string>}
|
|
37
36
|
*/
|
|
38
37
|
async function loadCode(shaderName) {
|
|
39
38
|
// Check for Node.js explicitly — `window` is undefined in Web Workers too, so it's not a reliable signal.
|
|
40
39
|
if (typeof process === "undefined" || !process.versions?.node) {
|
|
41
|
-
const { shaderSources } = await import("../shaders/
|
|
40
|
+
const { shaderSources } = await import("../shaders/index.mjs");
|
|
42
41
|
const src = shaderSources[shaderName];
|
|
43
42
|
if (!src) throw new Error(`Shader "${shaderName}" not found in browser bundle.`);
|
|
44
43
|
return src;
|
|
@@ -56,6 +55,7 @@ async function loadCode(shaderName) {
|
|
|
56
55
|
* `GPUComputePipeline`. Throws with line-level detail if compilation fails, rather than
|
|
57
56
|
* surfacing a raw GPU error. Uses `layout: "auto"` so the pipeline derives its bind group
|
|
58
57
|
* layout from the shader — no manual layout definition needed.
|
|
58
|
+
* @param {GPUDevice} device
|
|
59
59
|
* @param {string[]} shaderNames - filenames without `.wgsl`, concatenated in array order
|
|
60
60
|
* @param {string} [entryPoint="main"] - which `@compute` function in the combined module to run
|
|
61
61
|
* @returns {Promise<GPUComputePipeline>}
|
|
@@ -64,8 +64,7 @@ async function loadCode(shaderName) {
|
|
|
64
64
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUShaderModule/getCompilationInfo GPUShaderModule.getCompilationInfo()}
|
|
65
65
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createComputePipeline GPUDevice.createComputePipeline()}
|
|
66
66
|
*/
|
|
67
|
-
export async function loadShader(shaderNames, entryPoint = "main") {
|
|
68
|
-
const device = getDevice();
|
|
67
|
+
export async function loadShader(device, shaderNames, entryPoint = "main") {
|
|
69
68
|
const label = shaderNames.join("+");
|
|
70
69
|
const code = (await Promise.all(shaderNames.map(loadCode))).join("\n");
|
|
71
70
|
|
package/src/util/workgroup.mjs
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
/** @module devdocs/utility-functions/workgroup */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
const WORKGROUP_SIZE_2D = 8;
|
|
2
|
+
// Fixed sizes match the shader declarations (WGS = 64 for 1D, 8×8 = 64 threads
|
|
3
|
+
// for 2D) — see constants.mjs, which is where both values are defined and
|
|
4
|
+
// where the WGSL cross-check hangs off.
|
|
5
|
+
import { WGS as WORKGROUP_SIZE_1D, TILE_WG_2D as WORKGROUP_SIZE_2D } from "./constants.mjs";
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Calculates the number of workgroups to dispatch, clamped to the device's
|
|
10
9
|
* `maxComputeWorkgroupsPerDimension` limit (default 65535 across most devices).
|
|
11
10
|
*
|
|
11
|
+
* ONLY for shaders whose kernel is a grid-stride loop driven by
|
|
12
|
+
* `num_workgroups` — those re-walk the whole domain regardless of how many
|
|
13
|
+
* workgroups actually launch, so clamping costs a little parallelism and
|
|
14
|
+
* nothing else. A shader that indexes straight off `workgroup_id` or
|
|
15
|
+
* `global_invocation_id` silently drops every row past the clamp; those must
|
|
16
|
+
* use {@link requireWorkgroups} / {@link requireWorkgroupCount} instead.
|
|
17
|
+
*
|
|
12
18
|
* - 1D (pass only `rows`): returns a single count for `dispatchWorkgroups(n)`.
|
|
13
19
|
* - 2D (pass both `rows` and `cols`): returns `{ x, y }` for `dispatchWorkgroups(x, y)`.
|
|
14
20
|
* `rows` maps to the y dimension and `cols` maps to the x dimension.
|
|
@@ -18,8 +24,8 @@ const WORKGROUP_SIZE_2D = 8;
|
|
|
18
24
|
* @returns {number | { x: number, y: number }}
|
|
19
25
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUSupportedLimits GPUSupportedLimits} (`maxComputeWorkgroupsPerDimension`)
|
|
20
26
|
*/
|
|
21
|
-
export function calcWorkgroups(rows, cols) {
|
|
22
|
-
const max =
|
|
27
|
+
export function calcWorkgroups(device, rows, cols) {
|
|
28
|
+
const max = device.limits.maxComputeWorkgroupsPerDimension;
|
|
23
29
|
if (cols === undefined) {
|
|
24
30
|
return Math.min(Math.ceil(rows / WORKGROUP_SIZE_1D), max);
|
|
25
31
|
} else {
|
|
@@ -29,3 +35,45 @@ export function calcWorkgroups(rows, cols) {
|
|
|
29
35
|
};
|
|
30
36
|
}
|
|
31
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns `count` unchanged if it fits the device's dispatch limit, and throws
|
|
41
|
+
* otherwise. The counterpart to {@link calcWorkgroups} for shaders with no
|
|
42
|
+
* grid-stride fallback: silently clamping those computes only part of the
|
|
43
|
+
* result and reports success, so a refusal is the safer failure.
|
|
44
|
+
*
|
|
45
|
+
* @param {number} count - workgroups this dispatch requires in one dimension
|
|
46
|
+
* @param {string} routine - routine name, for the error message
|
|
47
|
+
* @param {string} [dim] - dimension label ("x"/"y"), for the error message
|
|
48
|
+
* @returns {number} `count`
|
|
49
|
+
* @throws {Error} when `count` exceeds `maxComputeWorkgroupsPerDimension`
|
|
50
|
+
*/
|
|
51
|
+
export function requireWorkgroupCount(device, count, routine, dim = "x") {
|
|
52
|
+
const max = device.limits.maxComputeWorkgroupsPerDimension;
|
|
53
|
+
if (count > max)
|
|
54
|
+
throw new Error(
|
|
55
|
+
`${routine}: this problem needs ${count} workgroups in ${dim}, but the device allows ` +
|
|
56
|
+
`${max} (maxComputeWorkgroupsPerDimension). The operands are too large for this device — ` +
|
|
57
|
+
`split the operation into smaller blocks.`,
|
|
58
|
+
);
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* {@link calcWorkgroups} with the clamp replaced by a throw — same arguments
|
|
64
|
+
* and same return shape, for shaders without a grid-stride fallback.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} routine - routine name, for the error message
|
|
67
|
+
* @param {number} rows - row count (1D: element count)
|
|
68
|
+
* @param {number} [cols] - column count; omit for a 1D dispatch
|
|
69
|
+
* @returns {number | { x: number, y: number }}
|
|
70
|
+
* @throws {Error} when either dimension exceeds `maxComputeWorkgroupsPerDimension`
|
|
71
|
+
*/
|
|
72
|
+
export function requireWorkgroups(device, routine, rows, cols) {
|
|
73
|
+
if (cols === undefined)
|
|
74
|
+
return requireWorkgroupCount(device, Math.ceil(rows / WORKGROUP_SIZE_1D), routine);
|
|
75
|
+
return {
|
|
76
|
+
x: requireWorkgroupCount(device, Math.ceil(cols / WORKGROUP_SIZE_2D), routine, "x"),
|
|
77
|
+
y: requireWorkgroupCount(device, Math.ceil(rows / WORKGROUP_SIZE_2D), routine, "y"),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import argmax from "./reduction/argmax.wgsl";
|
|
2
|
-
import sum from "./reduction/sum.wgsl";
|
|
3
|
-
import sumF64 from "./reduction/sumF64.wgsl";
|
|
4
|
-
import sscal from "./sscal.wgsl";
|
|
5
|
-
import sswap from "./sswap.wgsl";
|
|
6
|
-
import saxpy from "./saxpy.wgsl";
|
|
7
|
-
import scopy from "./scopy.wgsl";
|
|
8
|
-
import sdot from "./sdot.wgsl";
|
|
9
|
-
import sasum from "./sasum.wgsl";
|
|
10
|
-
import snrm2 from "./snrm2.wgsl";
|
|
11
|
-
import srot from "./srot.wgsl";
|
|
12
|
-
import srotm from "./srotm.wgsl";
|
|
13
|
-
import isamax from "./isamax.wgsl";
|
|
14
|
-
import sgemv_n from "./sgemv_n.wgsl";
|
|
15
|
-
import sgemv_t from "./sgemv_t.wgsl";
|
|
16
|
-
import ssymv from "./ssymv.wgsl";
|
|
17
|
-
import strmv from "./strmv.wgsl";
|
|
18
|
-
import sger from "./sger.wgsl";
|
|
19
|
-
import ssyr from "./ssyr.wgsl";
|
|
20
|
-
import ssyr2 from "./ssyr2.wgsl";
|
|
21
|
-
import f64add from "./f64add.wgsl";
|
|
22
|
-
import dekker from "./f64/dekker.wgsl";
|
|
23
|
-
import dasum from "./dasum.wgsl";
|
|
24
|
-
import strsv_invert_block from "./strsv_invert_block.wgsl";
|
|
25
|
-
import strsv_apply_inverse from "./strsv_apply_inverse.wgsl";
|
|
26
|
-
import strsv_update from "./strsv_update.wgsl";
|
|
27
|
-
|
|
28
|
-
export const shaderSources = {
|
|
29
|
-
"reduction/argmax": argmax,
|
|
30
|
-
"reduction/sum": sum,
|
|
31
|
-
"reduction/sumF64": sumF64,
|
|
32
|
-
sscal,
|
|
33
|
-
sswap,
|
|
34
|
-
saxpy,
|
|
35
|
-
scopy,
|
|
36
|
-
sdot,
|
|
37
|
-
sasum,
|
|
38
|
-
snrm2,
|
|
39
|
-
srot,
|
|
40
|
-
srotm,
|
|
41
|
-
isamax,
|
|
42
|
-
sgemv_n,
|
|
43
|
-
sgemv_t,
|
|
44
|
-
ssymv,
|
|
45
|
-
strmv,
|
|
46
|
-
sger,
|
|
47
|
-
ssyr,
|
|
48
|
-
ssyr2,
|
|
49
|
-
f64add,
|
|
50
|
-
"f64/dekker": dekker,
|
|
51
|
-
dasum,
|
|
52
|
-
strsv_invert_block,
|
|
53
|
-
strsv_apply_inverse,
|
|
54
|
-
strsv_update,
|
|
55
|
-
};
|