volvoxai 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 +145 -0
- package/bin/volvox.js +72 -0
- package/dist/v0.1.0/volvoxai.js +4664 -0
- package/dist/v0.1.0/volvoxai.min.js +1848 -0
- package/dist/v0.1.0/volvoxai.wasm +0 -0
- package/dist/volvoxai.js +4664 -0
- package/dist/volvoxai.min.js +1848 -0
- package/dist/volvoxai.wasm +0 -0
- package/docs/README.md +22 -0
- package/docs/browser-runtime.md +87 -0
- package/docs/efficientdet_tflite_vs_volvoxai.md +445 -0
- package/docs/microkernel_optimization_guide.md +153 -0
- package/docs/model-format.md +108 -0
- package/docs/models.md +103 -0
- package/docs/native-runtime.md +189 -0
- package/docs/operation_list.md +232 -0
- package/docs/operator_fusion_patterns.md +58 -0
- package/docs/quickstart.md +115 -0
- package/docs/roadmap.md +19 -0
- package/docs/testing.md +97 -0
- package/docs/textbook/01-foundations.md +233 -0
- package/docs/textbook/02-tinystories-language-model.md +300 -0
- package/docs/textbook/03-efficientdet-vision-model.md +281 -0
- package/docs/textbook/04-precision-and-quantization.md +208 -0
- package/docs/textbook/05-inside-the-engine.md +155 -0
- package/docs/textbook/06-native-engine-architecture.md +338 -0
- package/docs/textbook/07-glossary-and-next-steps.md +258 -0
- package/docs/textbook/README.md +85 -0
- package/docs/textbook/ko/01-foundations.md +231 -0
- package/docs/textbook/ko/02-tinystories-language-model.md +300 -0
- package/docs/textbook/ko/03-efficientdet-vision-model.md +277 -0
- package/docs/textbook/ko/04-precision-and-quantization.md +206 -0
- package/docs/textbook/ko/05-inside-the-engine.md +154 -0
- package/docs/textbook/ko/06-native-engine-architecture.md +333 -0
- package/docs/textbook/ko/07-glossary-and-next-steps.md +253 -0
- package/docs/textbook/ko/README.md +83 -0
- package/docs/xnnpack_optimization_guide.md +197 -0
- package/js/CPUEngine.js +241 -0
- package/js/Graph.js +49 -0
- package/js/GraphExecutor.js +1020 -0
- package/js/GraphLoader.js +282 -0
- package/js/ShaderLibrary.js +236 -0
- package/js/Tensor.js +25 -0
- package/js/Tokenizer.js +266 -0
- package/js/VolvoxAI.js +130 -0
- package/js/WasmEngine.js +378 -0
- package/js/WebNNEngine.js +169 -0
- package/js/index.js +11 -0
- package/js/ops/add.js +31 -0
- package/js/ops/argMax.js +33 -0
- package/js/ops/averagePool2D.js +38 -0
- package/js/ops/batchNorm2D.js +28 -0
- package/js/ops/cast.js +19 -0
- package/js/ops/clip.js +15 -0
- package/js/ops/concat2.js +18 -0
- package/js/ops/conv1D.js +35 -0
- package/js/ops/conv2D.js +70 -0
- package/js/ops/convTranspose2D.js +45 -0
- package/js/ops/crossAttention.js +69 -0
- package/js/ops/crossSDPA.js +41 -0
- package/js/ops/dequantizeLinear.js +9 -0
- package/js/ops/div.js +15 -0
- package/js/ops/embedding.js +14 -0
- package/js/ops/expand.js +24 -0
- package/js/ops/gELU.js +9 -0
- package/js/ops/gather.js +51 -0
- package/js/ops/gatherElements.js +33 -0
- package/js/ops/globalAveragePool.js +21 -0
- package/js/ops/hardSigmoid.js +12 -0
- package/js/ops/hardSwish.js +12 -0
- package/js/ops/interp1D.js +25 -0
- package/js/ops/layerNorm.js +25 -0
- package/js/ops/leakyReLU.js +10 -0
- package/js/ops/logSoftmax.js +15 -0
- package/js/ops/matMul.js +35 -0
- package/js/ops/maxPool2D.js +36 -0
- package/js/ops/meanHeight.js +17 -0
- package/js/ops/mul.js +31 -0
- package/js/ops/nonMaxSuppression.js +72 -0
- package/js/ops/pReLU.js +11 -0
- package/js/ops/pad.js +35 -0
- package/js/ops/profileX.js +22 -0
- package/js/ops/profileY.js +22 -0
- package/js/ops/rMSNorm.js +14 -0
- package/js/ops/reLU.js +8 -0
- package/js/ops/reduceMean.js +17 -0
- package/js/ops/reduceSum.js +19 -0
- package/js/ops/reshape.js +6 -0
- package/js/ops/resize.js +44 -0
- package/js/ops/sDPA.js +44 -0
- package/js/ops/siLU.js +8 -0
- package/js/ops/sigmoid.js +6 -0
- package/js/ops/slice.js +36 -0
- package/js/ops/softmax.js +18 -0
- package/js/ops/spatialSoftargmaxY.js +28 -0
- package/js/ops/split.js +24 -0
- package/js/ops/sub.js +11 -0
- package/js/ops/tanh.js +7 -0
- package/js/ops/transpose.js +34 -0
- package/js/ops/upsample2x.js +23 -0
- package/js/ops/where.js +15 -0
- package/package.json +33 -0
- package/shaders/add.wgsl +13 -0
- package/shaders/add3Relu.wgsl +23 -0
- package/shaders/addRelu.wgsl +22 -0
- package/shaders/averagePool2D.wgsl +24 -0
- package/shaders/batchNorm2D.wgsl +21 -0
- package/shaders/binaryBroadcast.wgsl +34 -0
- package/shaders/broadcastBinary.wgsl +26 -0
- package/shaders/clip.wgsl +10 -0
- package/shaders/concat2.wgsl +16 -0
- package/shaders/concatCopy.wgsl +10 -0
- package/shaders/concatSigmoidCopy.wgsl +16 -0
- package/shaders/conv1D.wgsl +37 -0
- package/shaders/conv2D.wgsl +80 -0
- package/shaders/conv2DDepthwise4.wgsl +74 -0
- package/shaders/conv2DDepthwise8.wgsl +66 -0
- package/shaders/conv2DPointwise16.wgsl +67 -0
- package/shaders/conv2DPointwise16Tile.wgsl +86 -0
- package/shaders/conv2DPointwise8.wgsl +85 -0
- package/shaders/conv2DPointwise8Vec2.wgsl +70 -0
- package/shaders/conv2DPointwise8Vec4.wgsl +65 -0
- package/shaders/conv2DRegularC3Out16.wgsl +75 -0
- package/shaders/convTranspose2D.wgsl +33 -0
- package/shaders/copy.wgsl +13 -0
- package/shaders/crossAttention.wgsl +140 -0
- package/shaders/crossAttentionF32.wgsl +98 -0
- package/shaders/crossSDPA.wgsl +74 -0
- package/shaders/dequantizeLinear.wgsl +14 -0
- package/shaders/div.wgsl +34 -0
- package/shaders/elementwise.wgsl +13 -0
- package/shaders/embedding.wgsl +22 -0
- package/shaders/expand.wgsl +18 -0
- package/shaders/gELU.wgsl +13 -0
- package/shaders/gather.wgsl +17 -0
- package/shaders/generalTranspose.wgsl +19 -0
- package/shaders/globalAveragePool.wgsl +19 -0
- package/shaders/hardSigmoid.wgsl +13 -0
- package/shaders/hardSwish.wgsl +13 -0
- package/shaders/interp1D.wgsl +28 -0
- package/shaders/layerNorm.wgsl +33 -0
- package/shaders/leakyReLU.wgsl +11 -0
- package/shaders/linearF32.wgsl +33 -0
- package/shaders/linearF32RowMajor.wgsl +24 -0
- package/shaders/linearInt8.wgsl +42 -0
- package/shaders/logSoftmax.wgsl +22 -0
- package/shaders/maxPool2D.wgsl +37 -0
- package/shaders/meanHeight.wgsl +18 -0
- package/shaders/mul.wgsl +32 -0
- package/shaders/nonMaxSuppression.wgsl +92 -0
- package/shaders/pReLU.wgsl +14 -0
- package/shaders/pad.wgsl +19 -0
- package/shaders/profileX.wgsl +28 -0
- package/shaders/profileY.wgsl +28 -0
- package/shaders/quantizeLinear.wgsl +69 -0
- package/shaders/rMSNorm.wgsl +21 -0
- package/shaders/reLU.wgsl +13 -0
- package/shaders/reduce.wgsl +17 -0
- package/shaders/resize.wgsl +52 -0
- package/shaders/sDPA.wgsl +71 -0
- package/shaders/siLU.wgsl +13 -0
- package/shaders/sigmoid.wgsl +13 -0
- package/shaders/slice.wgsl +26 -0
- package/shaders/softmax.wgsl +23 -0
- package/shaders/spatialSoftargmaxY.wgsl +32 -0
- package/shaders/split.wgsl +15 -0
- package/shaders/sub.wgsl +34 -0
- package/shaders/tanh.wgsl +13 -0
- package/shaders/upsample2x.wgsl +24 -0
- package/shaders/where.wgsl +12 -0
- package/volvoxai.wasm +0 -0
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
import { _pair } from './Tensor.js';
|
|
2
|
+
import { Graph } from './Graph.js';
|
|
3
|
+
|
|
4
|
+
// ShaderLibrary statically imports every .wgsl file as text, which only the
|
|
5
|
+
// bundler (esbuild's .wgsl=text loader) can resolve. It is loaded lazily inside
|
|
6
|
+
// compile() so that merely importing the engine under plain Node (the WASM/CPU
|
|
7
|
+
// tiers, the CLI smoke test) never touches a .wgsl file. The WebGPU tier is the
|
|
8
|
+
// only consumer, and it always goes through compile() first.
|
|
9
|
+
let ShaderLibrary;
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export class GraphExecutor {
|
|
13
|
+
constructor(device, graph) {
|
|
14
|
+
this.device = device;
|
|
15
|
+
this.graph = graph;
|
|
16
|
+
this.pipelines = [];
|
|
17
|
+
this.gpuBuffers = /* @__PURE__ */ new Map();
|
|
18
|
+
console.log("[VolvoxAI WebGPU] Starting Graph Compilation...");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Allocate VRAM for all tensors and compile shaders.
|
|
22
|
+
*/
|
|
23
|
+
async compile() {
|
|
24
|
+
({ ShaderLibrary } = await import('./ShaderLibrary.js'));
|
|
25
|
+
// The linearF32/linearInt8 shaders read the weight as [d_out, d_in]. GPT-Neo-style
|
|
26
|
+
// weights are stored [d_in, d_out] (node.wLayout==='din'), so transpose those once
|
|
27
|
+
// before they are uploaded to VRAM (keyed by weight name).
|
|
28
|
+
this._dinWeights = new Map();
|
|
29
|
+
for (const n of this.graph.nodes) {
|
|
30
|
+
if ((n.opType === "MatMul" || n.opType === "Linear" || n.opType === "Gemm") && n.wLayout === "din" && n.inputs.weight && !n.inputs.scale) {
|
|
31
|
+
const din = n.inputs.input.shape[n.inputs.input.shape.length - 1];
|
|
32
|
+
const dout = n.outputs.out.shape[n.outputs.out.shape.length - 1];
|
|
33
|
+
this._dinWeights.set(n.inputs.weight.name, { din, dout });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
this._allocateBuffers();
|
|
37
|
+
for (const node of this.graph.nodes) {
|
|
38
|
+
await this._buildNodePipeline(node);
|
|
39
|
+
}
|
|
40
|
+
console.log(`[VolvoxAI WebGPU] Compilation complete. Allocated ${this.gpuBuffers.size} VRAM buffers.`);
|
|
41
|
+
}
|
|
42
|
+
_allocateBuffers() {
|
|
43
|
+
for (const [name, tensor] of this.graph.tensors.entries()) {
|
|
44
|
+
let usage = GPUBufferUsage.STORAGE;
|
|
45
|
+
if (this.graph.nodes.some((n) => Object.values(n.inputs).some((t) => t.name === name))) {
|
|
46
|
+
usage |= GPUBufferUsage.COPY_DST;
|
|
47
|
+
}
|
|
48
|
+
if (this.graph.nodes.some((n) => Object.values(n.outputs).some((t) => t.name === name))) {
|
|
49
|
+
usage |= GPUBufferUsage.COPY_SRC;
|
|
50
|
+
}
|
|
51
|
+
if (!tensor.isWeight && !this.graph.nodes.some((n) => Object.values(n.outputs).some((t) => t.name === name))) {
|
|
52
|
+
usage |= GPUBufferUsage.COPY_DST;
|
|
53
|
+
}
|
|
54
|
+
// Any weight we upload below needs COPY_DST, even if it is a dangling
|
|
55
|
+
// constant not consumed as a node input (exporters emit these).
|
|
56
|
+
if (tensor.isWeight && tensor.buffer) {
|
|
57
|
+
usage |= GPUBufferUsage.COPY_DST;
|
|
58
|
+
}
|
|
59
|
+
const buffer = this.device.createBuffer({
|
|
60
|
+
label: `Tensor_${name}`,
|
|
61
|
+
size: Math.ceil(tensor.sizeBytes / 4) * 4,
|
|
62
|
+
// Align to 4 bytes
|
|
63
|
+
usage
|
|
64
|
+
});
|
|
65
|
+
tensor.gpuBuffer = buffer;
|
|
66
|
+
this.gpuBuffers.set(name, buffer);
|
|
67
|
+
// Upload learned-parameter data now. Weight buffers are consumed as node
|
|
68
|
+
// inputs (so they carry COPY_DST); without this, Conv/MatMul/BatchNorm read
|
|
69
|
+
// zeros on the GPU. Input tensors are written later at execute() time.
|
|
70
|
+
if (tensor.isWeight && tensor.buffer) {
|
|
71
|
+
let wbuf = tensor.buffer;
|
|
72
|
+
const dw = this._dinWeights && this._dinWeights.get(name);
|
|
73
|
+
if (dw) { // transpose [d_in, d_out] -> [d_out, d_in] for the shader
|
|
74
|
+
const { din, dout } = dw;
|
|
75
|
+
const t = new Float32Array(din * dout);
|
|
76
|
+
for (let k = 0; k < din; k++) for (let j = 0; j < dout; j++) t[j * din + k] = tensor.buffer[k * dout + j];
|
|
77
|
+
wbuf = t;
|
|
78
|
+
}
|
|
79
|
+
const src = new Uint8Array(wbuf.buffer, wbuf.byteOffset, wbuf.byteLength);
|
|
80
|
+
const padded = Math.ceil(src.byteLength / 4) * 4;
|
|
81
|
+
if (padded === src.byteLength) {
|
|
82
|
+
this.device.queue.writeBuffer(buffer, 0, src);
|
|
83
|
+
} else {
|
|
84
|
+
const tmp = new Uint8Array(padded);
|
|
85
|
+
tmp.set(src);
|
|
86
|
+
this.device.queue.writeBuffer(buffer, 0, tmp);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async _buildNodePipeline(node) {
|
|
92
|
+
let wgslCode = "";
|
|
93
|
+
let fallbackWgslCode = "";
|
|
94
|
+
let fallbackWorkgroupCount = null;
|
|
95
|
+
let bindGroupEntries = [];
|
|
96
|
+
let workgroupCount = [1, 1, 1];
|
|
97
|
+
if (node.opType === "Conv2D") {
|
|
98
|
+
wgslCode = ShaderLibrary.getConv2DShader();
|
|
99
|
+
const inputBuf = node.inputs.input.gpuBuffer;
|
|
100
|
+
const weightBuf = node.inputs.weight.gpuBuffer;
|
|
101
|
+
const outputBuf = node.outputs.out.gpuBuffer;
|
|
102
|
+
const [n, h, w, c] = node.inputs.input.shape;
|
|
103
|
+
const [kh, kw] = node.inputs.weight.shape;
|
|
104
|
+
const outC = node.outputs.out.shape[3];
|
|
105
|
+
const outH = node.outputs.out.shape[1];
|
|
106
|
+
const outW = node.outputs.out.shape[2];
|
|
107
|
+
const [sy, sx] = _pair(node.params.stride, 1);
|
|
108
|
+
const [pt, pl] = _pair(node.params.padding, 0);
|
|
109
|
+
const [dy, dx] = _pair(node.params.dilation, 1);
|
|
110
|
+
const groups = node.params.groups || 1;
|
|
111
|
+
const pads = Array.isArray(node.params.pads) ? node.params.pads : [pt, pl, pt, pl];
|
|
112
|
+
const noPad = pads.length >= 4 && pads[0] === 0 && pads[1] === 0 && pads[2] === 0 && pads[3] === 0;
|
|
113
|
+
const weightLayout = node.params.weight_layout || (groups === c ? "HWCM" : "HWIO");
|
|
114
|
+
fallbackWgslCode = wgslCode;
|
|
115
|
+
fallbackWorkgroupCount = [Math.ceil(outW / 8), Math.ceil(outH / 8), n * outC];
|
|
116
|
+
if (groups === 1 && weightLayout === "HWIO" && c === 3 && (outC & 15) === 0) {
|
|
117
|
+
wgslCode = ShaderLibrary.getConv2DRegularC3Out16Shader();
|
|
118
|
+
workgroupCount = [Math.ceil(outW / 8), Math.ceil(outH / 8), n * (outC / 16)];
|
|
119
|
+
} else if (groups === c && weightLayout === "HWCM" && outC === c && (outC & 7) === 0) {
|
|
120
|
+
wgslCode = ShaderLibrary.getConv2DDepthwise8Shader();
|
|
121
|
+
workgroupCount = [Math.ceil(outW / 8), Math.ceil(outH / 8), n * Math.ceil(outC / 8)];
|
|
122
|
+
} else if (groups === 1 && weightLayout === "HWIO" && kh === 1 && kw === 1 &&
|
|
123
|
+
sy === 1 && sx === 1 && noPad && dy === 1 && dx === 1 &&
|
|
124
|
+
outH === h && outW === w) {
|
|
125
|
+
if ((outC & 15) === 0) {
|
|
126
|
+
wgslCode = ShaderLibrary.getConv2DPointwise16TileShader();
|
|
127
|
+
workgroupCount = [Math.ceil(outW / 8), Math.ceil(outH / 8), n * (outC / 16)];
|
|
128
|
+
} else if ((outC & 3) === 0) {
|
|
129
|
+
wgslCode = ShaderLibrary.getConv2DPointwise8Vec4Shader();
|
|
130
|
+
workgroupCount = [Math.ceil(outW / 8), Math.ceil(outH / 8), n * Math.ceil(outC / 8)];
|
|
131
|
+
} else if ((outC & 1) === 0) {
|
|
132
|
+
wgslCode = ShaderLibrary.getConv2DPointwise8Vec2Shader();
|
|
133
|
+
workgroupCount = [Math.ceil(outW / 8), Math.ceil(outH / 8), n * Math.ceil(outC / 8)];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const biasBuf = this.device.createBuffer({
|
|
137
|
+
size: Math.ceil(outC * 4 / 4) * 4,
|
|
138
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
139
|
+
});
|
|
140
|
+
if (node.inputs.bias) {
|
|
141
|
+
this.device.queue.writeBuffer(biasBuf, 0, node.inputs.bias.buffer);
|
|
142
|
+
}
|
|
143
|
+
const p = new Uint32Array([
|
|
144
|
+
n,
|
|
145
|
+
h,
|
|
146
|
+
w,
|
|
147
|
+
c,
|
|
148
|
+
outC,
|
|
149
|
+
outH,
|
|
150
|
+
outW,
|
|
151
|
+
kh,
|
|
152
|
+
kw,
|
|
153
|
+
sy,
|
|
154
|
+
sx,
|
|
155
|
+
pt,
|
|
156
|
+
pl,
|
|
157
|
+
groups,
|
|
158
|
+
node.params.relu || 0,
|
|
159
|
+
dy,
|
|
160
|
+
dx
|
|
161
|
+
]);
|
|
162
|
+
const paramBuf = this.device.createBuffer({ size: Math.ceil(p.byteLength / 16) * 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
163
|
+
this.device.queue.writeBuffer(paramBuf, 0, p);
|
|
164
|
+
bindGroupEntries = [
|
|
165
|
+
{ binding: 0, resource: { buffer: inputBuf } },
|
|
166
|
+
{ binding: 1, resource: { buffer: weightBuf } },
|
|
167
|
+
{ binding: 2, resource: { buffer: biasBuf } },
|
|
168
|
+
{ binding: 3, resource: { buffer: outputBuf } },
|
|
169
|
+
{ binding: 4, resource: { buffer: paramBuf } }
|
|
170
|
+
];
|
|
171
|
+
if (wgslCode === fallbackWgslCode) workgroupCount = fallbackWorkgroupCount;
|
|
172
|
+
} else if (node.opType === "Conv1D") {
|
|
173
|
+
wgslCode = ShaderLibrary.getConv1DShader();
|
|
174
|
+
const inputBuf = node.inputs.input.gpuBuffer;
|
|
175
|
+
const weightBuf = node.inputs.weight.gpuBuffer;
|
|
176
|
+
const outputBuf = node.outputs.out.gpuBuffer;
|
|
177
|
+
const biasBuf = this.device.createBuffer({
|
|
178
|
+
size: Math.ceil((node.inputs.weight.shape[0] * 4) / 4) * 4 || 4,
|
|
179
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
180
|
+
});
|
|
181
|
+
if (node.inputs.bias) {
|
|
182
|
+
this.device.queue.writeBuffer(biasBuf, 0, node.inputs.bias.buffer);
|
|
183
|
+
}
|
|
184
|
+
const p = new Uint32Array([
|
|
185
|
+
node.inputs.input.shape[1],
|
|
186
|
+
node.inputs.input.shape[2],
|
|
187
|
+
node.outputs.out.shape[1],
|
|
188
|
+
node.inputs.weight.shape[2],
|
|
189
|
+
_pair(node.params.stride, 1)[0],
|
|
190
|
+
_pair(node.params.padding, 0)[0],
|
|
191
|
+
node.params.relu ? 1 : 0
|
|
192
|
+
]);
|
|
193
|
+
const paramBuf = this.device.createBuffer({ size: p.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
194
|
+
this.device.queue.writeBuffer(paramBuf, 0, p);
|
|
195
|
+
bindGroupEntries = [
|
|
196
|
+
{ binding: 0, resource: { buffer: inputBuf } },
|
|
197
|
+
{ binding: 1, resource: { buffer: weightBuf } },
|
|
198
|
+
{ binding: 2, resource: { buffer: biasBuf } },
|
|
199
|
+
{ binding: 3, resource: { buffer: outputBuf } },
|
|
200
|
+
{ binding: 4, resource: { buffer: paramBuf } }
|
|
201
|
+
];
|
|
202
|
+
workgroupCount = [Math.ceil(node.outputs.out.shape[2] / 64), node.outputs.out.shape[1], 1];
|
|
203
|
+
} else if (node.opType === "SpatialSoftargmaxY") {
|
|
204
|
+
wgslCode = ShaderLibrary.getSpatialSoftargmaxYShader();
|
|
205
|
+
const inputBuf = node.inputs.input.gpuBuffer;
|
|
206
|
+
const outputBuf = node.outputs.out.gpuBuffer;
|
|
207
|
+
const p = new Uint32Array([
|
|
208
|
+
node.inputs.input.shape[1],
|
|
209
|
+
node.inputs.input.shape[2],
|
|
210
|
+
node.inputs.input.shape[3]
|
|
211
|
+
]);
|
|
212
|
+
const paramBuf = this.device.createBuffer({ size: p.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
213
|
+
this.device.queue.writeBuffer(paramBuf, 0, p);
|
|
214
|
+
bindGroupEntries = [
|
|
215
|
+
{ binding: 0, resource: { buffer: inputBuf } },
|
|
216
|
+
{ binding: 1, resource: { buffer: outputBuf } },
|
|
217
|
+
{ binding: 2, resource: { buffer: paramBuf } }
|
|
218
|
+
];
|
|
219
|
+
workgroupCount = [Math.ceil(node.inputs.input.shape[2] / 64), node.inputs.input.shape[3], 1];
|
|
220
|
+
} else if (node.opType === "UpsampleNearest2D") {
|
|
221
|
+
wgslCode = ShaderLibrary.getUpsample2xShader();
|
|
222
|
+
const inputBuf = node.inputs.input.gpuBuffer;
|
|
223
|
+
const outputBuf = node.outputs.out.gpuBuffer;
|
|
224
|
+
const p = new Uint32Array([
|
|
225
|
+
node.inputs.input.shape[0],
|
|
226
|
+
node.inputs.input.shape[1],
|
|
227
|
+
node.inputs.input.shape[2],
|
|
228
|
+
node.inputs.input.shape[3]
|
|
229
|
+
]);
|
|
230
|
+
const paramBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
231
|
+
this.device.queue.writeBuffer(paramBuf, 0, p);
|
|
232
|
+
bindGroupEntries = [
|
|
233
|
+
{ binding: 0, resource: { buffer: inputBuf } },
|
|
234
|
+
{ binding: 1, resource: { buffer: outputBuf } },
|
|
235
|
+
{ binding: 2, resource: { buffer: paramBuf } }
|
|
236
|
+
];
|
|
237
|
+
workgroupCount = [
|
|
238
|
+
Math.ceil(node.outputs.out.shape[2] / 8),
|
|
239
|
+
Math.ceil(node.outputs.out.shape[1] / 8),
|
|
240
|
+
node.outputs.out.shape[0] * node.outputs.out.shape[3]
|
|
241
|
+
];
|
|
242
|
+
} else if (node.opType === "Concat") {
|
|
243
|
+
// N-way channel concat (batch=1): copy each input into the output at its
|
|
244
|
+
// cumulative element offset via one strided-copy pipeline per input.
|
|
245
|
+
const shaderModule = this.device.createShaderModule({ code: ShaderLibrary.getConcatCopyShader() });
|
|
246
|
+
const pipeline = await this.device.createComputePipelineAsync({
|
|
247
|
+
layout: "auto",
|
|
248
|
+
compute: { module: shaderModule, entryPoint: "main" }
|
|
249
|
+
});
|
|
250
|
+
let offset = 0;
|
|
251
|
+
for (const k of ["input", "a", "b", "c", "d", "e", "f", "g", "h"]) {
|
|
252
|
+
const t = node.inputs[k];
|
|
253
|
+
if (!t) continue;
|
|
254
|
+
const size = t.sizeBytes / 4;
|
|
255
|
+
const p = new Uint32Array([size, offset]);
|
|
256
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
257
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
258
|
+
const bindGroup = this.device.createBindGroup({
|
|
259
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
260
|
+
entries: [
|
|
261
|
+
{ binding: 0, resource: { buffer: t.gpuBuffer } },
|
|
262
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
263
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
264
|
+
]
|
|
265
|
+
});
|
|
266
|
+
this.pipelines.push({ pipeline, bindGroup, workgroupCount: [Math.ceil(size / 64), 1, 1], nodeName: `${node.id}_concat` });
|
|
267
|
+
offset += size;
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
} else if (node.opType === "ProfileY") {
|
|
271
|
+
wgslCode = ShaderLibrary.getProfileYShader();
|
|
272
|
+
const inputBuf = node.inputs.input.gpuBuffer;
|
|
273
|
+
const outputBuf = node.outputs.out.gpuBuffer;
|
|
274
|
+
const p = new Uint32Array([node.inputs.input.shape[1], node.inputs.input.shape[2], node.inputs.input.shape[3]]);
|
|
275
|
+
const paramBuf = this.device.createBuffer({ size: p.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
276
|
+
this.device.queue.writeBuffer(paramBuf, 0, p);
|
|
277
|
+
bindGroupEntries = [
|
|
278
|
+
{ binding: 0, resource: { buffer: inputBuf } },
|
|
279
|
+
{ binding: 1, resource: { buffer: outputBuf } },
|
|
280
|
+
{ binding: 2, resource: { buffer: paramBuf } }
|
|
281
|
+
];
|
|
282
|
+
workgroupCount = [Math.ceil(node.inputs.input.shape[1] / 64), node.inputs.input.shape[3], 1];
|
|
283
|
+
} else if (node.opType === "ProfileX") {
|
|
284
|
+
wgslCode = ShaderLibrary.getProfileXShader();
|
|
285
|
+
const inputBuf = node.inputs.input.gpuBuffer;
|
|
286
|
+
const outputBuf = node.outputs.out.gpuBuffer;
|
|
287
|
+
const p = new Uint32Array([node.inputs.input.shape[1], node.inputs.input.shape[2], node.inputs.input.shape[3]]);
|
|
288
|
+
const paramBuf = this.device.createBuffer({ size: p.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
289
|
+
this.device.queue.writeBuffer(paramBuf, 0, p);
|
|
290
|
+
bindGroupEntries = [
|
|
291
|
+
{ binding: 0, resource: { buffer: inputBuf } },
|
|
292
|
+
{ binding: 1, resource: { buffer: outputBuf } },
|
|
293
|
+
{ binding: 2, resource: { buffer: paramBuf } }
|
|
294
|
+
];
|
|
295
|
+
workgroupCount = [Math.ceil(node.inputs.input.shape[2] / 64), node.inputs.input.shape[3], 1];
|
|
296
|
+
} else if (node.opType === "InterpLinear1D") {
|
|
297
|
+
wgslCode = ShaderLibrary.getInterp1DShader();
|
|
298
|
+
const inputBuf = node.inputs.input.gpuBuffer;
|
|
299
|
+
const outputBuf = node.outputs.out.gpuBuffer;
|
|
300
|
+
const p = new Uint32Array([node.inputs.input.shape[1], node.inputs.input.shape[2], node.params.size]);
|
|
301
|
+
const paramBuf = this.device.createBuffer({ size: p.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
302
|
+
this.device.queue.writeBuffer(paramBuf, 0, p);
|
|
303
|
+
bindGroupEntries = [
|
|
304
|
+
{ binding: 0, resource: { buffer: inputBuf } },
|
|
305
|
+
{ binding: 1, resource: { buffer: outputBuf } },
|
|
306
|
+
{ binding: 2, resource: { buffer: paramBuf } }
|
|
307
|
+
];
|
|
308
|
+
workgroupCount = [Math.ceil(node.params.size / 64), node.inputs.input.shape[1], 1];
|
|
309
|
+
} else if (node.opType === "MatMul") {
|
|
310
|
+
if (node.inputs.scale) {
|
|
311
|
+
wgslCode = ShaderLibrary.getLinearInt8Shader();
|
|
312
|
+
} else {
|
|
313
|
+
wgslCode = ShaderLibrary.getLinearF32Shader();
|
|
314
|
+
}
|
|
315
|
+
const dummyBias = this.device.createBuffer({ size: 4096, usage: GPUBufferUsage.STORAGE });
|
|
316
|
+
const seq_len = node.inputs.input.shape.slice(0, -1).reduce((a, b) => a * b, 1);
|
|
317
|
+
const d_in = node.inputs.input.shape[node.inputs.input.shape.length - 1];
|
|
318
|
+
const d_out = node.outputs.out.shape[node.outputs.out.shape.length - 1];
|
|
319
|
+
const p = new Uint32Array([seq_len, d_in, d_out]);
|
|
320
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
321
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
322
|
+
const biasBuf = node.inputs.bias ? node.inputs.bias.gpuBuffer : dummyBias;
|
|
323
|
+
bindGroupEntries = [
|
|
324
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
325
|
+
{ binding: 1, resource: { buffer: node.inputs.weight.gpuBuffer } },
|
|
326
|
+
// binding 2 (scale) exists only in the int8 shader; the f32 shader's
|
|
327
|
+
// unused binding 2 is dropped by layout:"auto", so we must omit it.
|
|
328
|
+
...(node.inputs.scale ? [{ binding: 2, resource: { buffer: node.inputs.scale.gpuBuffer } }] : []),
|
|
329
|
+
{ binding: 3, resource: { buffer: biasBuf } },
|
|
330
|
+
{ binding: 4, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
331
|
+
{ binding: 5, resource: { buffer: paramsBuf } }
|
|
332
|
+
];
|
|
333
|
+
workgroupCount = [Math.ceil(d_out / 64), seq_len, 1];
|
|
334
|
+
} else if (node.opType === "LayerNorm") {
|
|
335
|
+
wgslCode = ShaderLibrary.getLayerNormShader();
|
|
336
|
+
const d_model = node.params.d_model;
|
|
337
|
+
const seq_len = node.inputs.input.shape.slice(0, -1).reduce((a, b) => a * b, 1);
|
|
338
|
+
const p = new Uint32Array([seq_len, d_model]);
|
|
339
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
340
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
341
|
+
bindGroupEntries = [
|
|
342
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
343
|
+
{ binding: 1, resource: { buffer: node.inputs.weight.gpuBuffer } },
|
|
344
|
+
{ binding: 2, resource: { buffer: node.inputs.bias.gpuBuffer } },
|
|
345
|
+
{ binding: 3, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
346
|
+
{ binding: 4, resource: { buffer: paramsBuf } }
|
|
347
|
+
];
|
|
348
|
+
workgroupCount = [Math.ceil(seq_len / 64), 1, 1];
|
|
349
|
+
} else if (node.opType === "GELU") {
|
|
350
|
+
wgslCode = ShaderLibrary.getGELUShader();
|
|
351
|
+
const num_elements = node.outputs.out.shape.reduce((a, b) => a * b, 1);
|
|
352
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
353
|
+
this.device.queue.writeBuffer(paramsBuf, 0, new Uint32Array([num_elements]));
|
|
354
|
+
bindGroupEntries = [
|
|
355
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
356
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
357
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
358
|
+
];
|
|
359
|
+
workgroupCount = [Math.ceil(num_elements / 64), 1, 1];
|
|
360
|
+
} else if (node.opType === "Embedding") {
|
|
361
|
+
wgslCode = ShaderLibrary.getEmbeddingShader();
|
|
362
|
+
const seq_len = node.inputs.input.shape.reduce((a, b) => a * b, 1);
|
|
363
|
+
const d_model = node.outputs.out.shape[node.outputs.out.shape.length - 1];
|
|
364
|
+
const p = new Uint32Array([seq_len, d_model]);
|
|
365
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
366
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
367
|
+
bindGroupEntries = [
|
|
368
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
369
|
+
{ binding: 1, resource: { buffer: node.inputs.weight.gpuBuffer } },
|
|
370
|
+
{ binding: 2, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
371
|
+
{ binding: 3, resource: { buffer: paramsBuf } }
|
|
372
|
+
];
|
|
373
|
+
workgroupCount = [Math.ceil(seq_len / 64), 1, 1];
|
|
374
|
+
} else if (node.opType === "SDPA") {
|
|
375
|
+
wgslCode = ShaderLibrary.getSDPAShader();
|
|
376
|
+
const seq_len = node.inputs.qkv.shape[1];
|
|
377
|
+
const d_model = node.outputs.out.shape[2];
|
|
378
|
+
const num_heads = node.params.heads || 8;
|
|
379
|
+
const head_dim = d_model / num_heads;
|
|
380
|
+
const scale = node.params.scale !== undefined ? node.params.scale : 1 / Math.sqrt(head_dim);
|
|
381
|
+
const p = new ArrayBuffer(20);
|
|
382
|
+
const p_u32 = new Uint32Array(p);
|
|
383
|
+
const p_f32 = new Float32Array(p);
|
|
384
|
+
p_u32[0] = seq_len;
|
|
385
|
+
p_u32[1] = d_model;
|
|
386
|
+
p_u32[2] = num_heads;
|
|
387
|
+
p_u32[3] = head_dim;
|
|
388
|
+
p_f32[4] = scale;
|
|
389
|
+
const paramsBuf = this.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
390
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
391
|
+
bindGroupEntries = [
|
|
392
|
+
{ binding: 0, resource: { buffer: node.inputs.qkv.gpuBuffer } },
|
|
393
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
394
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
395
|
+
];
|
|
396
|
+
workgroupCount = [Math.ceil(seq_len / 64), num_heads, 1];
|
|
397
|
+
} else if (node.opType === "CrossSDPA") {
|
|
398
|
+
wgslCode = ShaderLibrary.getCrossSDPAShader();
|
|
399
|
+
const seq_len_q = node.inputs.q.shape[1];
|
|
400
|
+
const seq_len_kv = node.inputs.k.shape[1];
|
|
401
|
+
const d_model = node.inputs.q.shape[2];
|
|
402
|
+
const num_heads = node.params.heads || 8;
|
|
403
|
+
const head_dim = d_model / num_heads;
|
|
404
|
+
const scale = 1 / Math.sqrt(head_dim);
|
|
405
|
+
const p = new ArrayBuffer(24);
|
|
406
|
+
const p_u32 = new Uint32Array(p);
|
|
407
|
+
const p_f32 = new Float32Array(p);
|
|
408
|
+
p_u32[0] = seq_len_q;
|
|
409
|
+
p_u32[1] = seq_len_kv;
|
|
410
|
+
p_u32[2] = d_model;
|
|
411
|
+
p_u32[3] = num_heads;
|
|
412
|
+
p_u32[4] = head_dim;
|
|
413
|
+
p_f32[5] = scale;
|
|
414
|
+
const paramsBuf = this.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
415
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
416
|
+
bindGroupEntries = [
|
|
417
|
+
{ binding: 0, resource: { buffer: node.inputs.q.gpuBuffer } },
|
|
418
|
+
{ binding: 1, resource: { buffer: node.inputs.k.gpuBuffer } },
|
|
419
|
+
{ binding: 2, resource: { buffer: node.inputs.v.gpuBuffer } },
|
|
420
|
+
{ binding: 3, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
421
|
+
{ binding: 4, resource: { buffer: paramsBuf } }
|
|
422
|
+
];
|
|
423
|
+
workgroupCount = [Math.ceil(seq_len_q / 64), num_heads, 1];
|
|
424
|
+
} else if (node.opType === "CrossAttention") {
|
|
425
|
+
wgslCode = ShaderLibrary.getCrossAttentionShader();
|
|
426
|
+
const seq_len_q = node.inputs.q.shape[1];
|
|
427
|
+
const seq_len_kv = node.inputs.kv.shape[1];
|
|
428
|
+
const d_model = node.outputs.out.shape[2];
|
|
429
|
+
const num_heads = node.params.heads || 8;
|
|
430
|
+
const head_dim = d_model / num_heads;
|
|
431
|
+
const scale_factor = 1 / Math.sqrt(head_dim);
|
|
432
|
+
const p = new ArrayBuffer(32);
|
|
433
|
+
const p_u32 = new Uint32Array(p);
|
|
434
|
+
const p_f32 = new Float32Array(p);
|
|
435
|
+
p_u32[0] = seq_len_q;
|
|
436
|
+
p_u32[1] = seq_len_kv;
|
|
437
|
+
p_u32[2] = d_model;
|
|
438
|
+
p_u32[3] = num_heads;
|
|
439
|
+
p_u32[4] = head_dim;
|
|
440
|
+
p_f32[5] = scale_factor;
|
|
441
|
+
p_u32[6] = node.inputs.scale ? 1 : 0;
|
|
442
|
+
p_u32[7] = node.inputs.bias ? 1 : 0;
|
|
443
|
+
const paramsBuf = this.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
444
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
445
|
+
|
|
446
|
+
const dummyScale = this.device.createBuffer({ size: 4096, usage: GPUBufferUsage.STORAGE });
|
|
447
|
+
const dummyBias = this.device.createBuffer({ size: 4096, usage: GPUBufferUsage.STORAGE });
|
|
448
|
+
|
|
449
|
+
bindGroupEntries = [
|
|
450
|
+
{ binding: 0, resource: { buffer: node.inputs.q.gpuBuffer } },
|
|
451
|
+
{ binding: 1, resource: { buffer: node.inputs.kv.gpuBuffer } },
|
|
452
|
+
{ binding: 2, resource: { buffer: node.inputs.weight.gpuBuffer } },
|
|
453
|
+
{ binding: 3, resource: { buffer: node.inputs.scale ? node.inputs.scale.gpuBuffer : dummyScale } },
|
|
454
|
+
{ binding: 4, resource: { buffer: node.inputs.bias ? node.inputs.bias.gpuBuffer : dummyBias } },
|
|
455
|
+
{ binding: 5, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
456
|
+
{ binding: 6, resource: { buffer: paramsBuf } }
|
|
457
|
+
];
|
|
458
|
+
workgroupCount = [Math.ceil(seq_len_q / 64), num_heads, 1];
|
|
459
|
+
} else if (node.opType === "MeanHeight") {
|
|
460
|
+
wgslCode = ShaderLibrary.getMeanHeightShader();
|
|
461
|
+
const p = new Uint32Array([node.inputs.input.shape[1], node.inputs.input.shape[2], node.inputs.input.shape[3]]);
|
|
462
|
+
const paramBuf = this.device.createBuffer({ size: Math.ceil(p.byteLength / 16) * 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
463
|
+
this.device.queue.writeBuffer(paramBuf, 0, p);
|
|
464
|
+
bindGroupEntries = [
|
|
465
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
466
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
467
|
+
{ binding: 2, resource: { buffer: paramBuf } }
|
|
468
|
+
];
|
|
469
|
+
workgroupCount = [Math.ceil(node.inputs.input.shape[2] / 64), node.inputs.input.shape[3], 1];
|
|
470
|
+
} else if (node.opType === "ReLU" || node.opType === "Sigmoid" || node.opType === "HardSwish"
|
|
471
|
+
|| node.opType === "HardSigmoid" || node.opType === "SiLU" || node.opType === "Swish"
|
|
472
|
+
|| node.opType === "Tanh" || node.opType === "Reshape" || node.opType === "Squeeze"
|
|
473
|
+
|| node.opType === "Unsqueeze" || node.opType === "Flatten" || node.opType === "Dropout"
|
|
474
|
+
|| node.opType === "Identity") {
|
|
475
|
+
// Elementwise unary ops, plus shape-only ops (Reshape/Squeeze/...) which
|
|
476
|
+
// just copy the data through to the newly-allocated output buffer.
|
|
477
|
+
if (node.opType === "ReLU") wgslCode = ShaderLibrary.getReLUShader();
|
|
478
|
+
else if (node.opType === "Sigmoid") wgslCode = ShaderLibrary.getSigmoidShader();
|
|
479
|
+
else if (node.opType === "HardSwish") wgslCode = ShaderLibrary.getHardSwishShader();
|
|
480
|
+
else if (node.opType === "HardSigmoid") wgslCode = ShaderLibrary.getHardSigmoidShader();
|
|
481
|
+
else if (node.opType === "SiLU" || node.opType === "Swish") wgslCode = ShaderLibrary.getSiLUShader();
|
|
482
|
+
else if (node.opType === "Tanh") wgslCode = ShaderLibrary.getTanhShader();
|
|
483
|
+
else wgslCode = ShaderLibrary.getCopyShader();
|
|
484
|
+
const elements = node.outputs.out.shape.reduce((a, b) => a * b, 1);
|
|
485
|
+
const p = new Uint32Array([elements]);
|
|
486
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
487
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
488
|
+
bindGroupEntries = [
|
|
489
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
490
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
491
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
492
|
+
];
|
|
493
|
+
workgroupCount = [Math.ceil(elements / 64), 1, 1];
|
|
494
|
+
} else if (node.opType === "Add" || node.opType === "Mul" || node.opType === "Sub" || node.opType === "Div") {
|
|
495
|
+
const binOp = { Add: "out_val = av + bv;", Mul: "out_val = av * bv;",
|
|
496
|
+
Sub: "out_val = av - bv;", Div: "out_val = av / bv;" }[node.opType];
|
|
497
|
+
wgslCode = ShaderLibrary.getBroadcastBinaryShader(binOp);
|
|
498
|
+
const outShape = node.outputs.out.shape;
|
|
499
|
+
const rank = outShape.length;
|
|
500
|
+
const contigStrides = (shape) => {
|
|
501
|
+
const st = new Array(shape.length);
|
|
502
|
+
let s = 1;
|
|
503
|
+
for (let i = shape.length - 1; i >= 0; i--) { st[i] = s; s *= shape[i]; }
|
|
504
|
+
return st;
|
|
505
|
+
};
|
|
506
|
+
const outStrides = contigStrides(outShape);
|
|
507
|
+
// numpy-style broadcast strides: left-pad the operand shape to the output
|
|
508
|
+
// rank, then zero the stride of any dimension that is broadcast (size 1).
|
|
509
|
+
const bcastStrides = (shape) => {
|
|
510
|
+
const padded = new Array(rank).fill(1);
|
|
511
|
+
for (let i = 0; i < shape.length; i++) padded[rank - shape.length + i] = shape[i];
|
|
512
|
+
const st = contigStrides(padded);
|
|
513
|
+
for (let i = 0; i < rank; i++) if (padded[i] === 1 && outShape[i] !== 1) st[i] = 0;
|
|
514
|
+
return st;
|
|
515
|
+
};
|
|
516
|
+
const aStrides = bcastStrides(node.inputs.a.shape);
|
|
517
|
+
const bStrides = bcastStrides(node.inputs.b.shape);
|
|
518
|
+
const total = outShape.reduce((a, b) => a * b, 1);
|
|
519
|
+
const meta = new Uint32Array(2 + rank * 3);
|
|
520
|
+
meta[0] = total; meta[1] = rank;
|
|
521
|
+
for (let d = 0; d < rank; d++) {
|
|
522
|
+
meta[2 + d] = outStrides[d];
|
|
523
|
+
meta[2 + rank + d] = aStrides[d];
|
|
524
|
+
meta[2 + 2 * rank + d] = bStrides[d];
|
|
525
|
+
}
|
|
526
|
+
const metaBuf = this.device.createBuffer({ size: Math.ceil(meta.byteLength / 4) * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
527
|
+
this.device.queue.writeBuffer(metaBuf, 0, meta);
|
|
528
|
+
bindGroupEntries = [
|
|
529
|
+
{ binding: 0, resource: { buffer: node.inputs.a.gpuBuffer } },
|
|
530
|
+
{ binding: 1, resource: { buffer: node.inputs.b.gpuBuffer } },
|
|
531
|
+
{ binding: 2, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
532
|
+
{ binding: 3, resource: { buffer: metaBuf } }
|
|
533
|
+
];
|
|
534
|
+
workgroupCount = [Math.ceil(total / 64), 1, 1];
|
|
535
|
+
} else if (node.opType === "Transpose") {
|
|
536
|
+
wgslCode = ShaderLibrary.getGeneralTransposeShader();
|
|
537
|
+
const inShape = node.inputs.input.shape;
|
|
538
|
+
const rank = inShape.length;
|
|
539
|
+
const perm = node.params.perm || [...Array(rank).keys()].reverse();
|
|
540
|
+
const inStrides = new Array(rank);
|
|
541
|
+
{ let s = 1; for (let i = rank - 1; i >= 0; i--) { inStrides[i] = s; s *= inShape[i]; } }
|
|
542
|
+
const outShape = perm.map((pp) => inShape[pp]);
|
|
543
|
+
const outStrides = new Array(rank);
|
|
544
|
+
{ let s = 1; for (let i = rank - 1; i >= 0; i--) { outStrides[i] = s; s *= outShape[i]; } }
|
|
545
|
+
const total = inShape.reduce((a, b) => a * b, 1);
|
|
546
|
+
const meta = new Uint32Array(2 + rank * 2);
|
|
547
|
+
meta[0] = total; meta[1] = rank;
|
|
548
|
+
for (let d = 0; d < rank; d++) { meta[2 + d] = outStrides[d]; meta[2 + rank + d] = inStrides[perm[d]]; }
|
|
549
|
+
const metaBuf = this.device.createBuffer({ size: Math.ceil(meta.byteLength / 4) * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
550
|
+
this.device.queue.writeBuffer(metaBuf, 0, meta);
|
|
551
|
+
bindGroupEntries = [
|
|
552
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
553
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
554
|
+
{ binding: 2, resource: { buffer: metaBuf } }
|
|
555
|
+
];
|
|
556
|
+
workgroupCount = [Math.ceil(total / 64), 1, 1];
|
|
557
|
+
} else if (node.opType === "Softmax" || node.opType === "LogSoftmax") {
|
|
558
|
+
// Normalizes over the last (innermost) axis: rows of `d`, `b` rows total.
|
|
559
|
+
wgslCode = node.opType === "Softmax" ? ShaderLibrary.getSoftmaxShader() : ShaderLibrary.getLogSoftmaxShader();
|
|
560
|
+
const shape = node.inputs.input.shape;
|
|
561
|
+
const d = shape[shape.length - 1];
|
|
562
|
+
const b = shape.reduce((a, x) => a * x, 1) / d;
|
|
563
|
+
if (node.params.axis !== undefined) {
|
|
564
|
+
let ax = node.params.axis; if (ax < 0) ax += shape.length;
|
|
565
|
+
if (ax !== shape.length - 1) console.warn(`[VolvoxAI WebGPU] ${node.opType} axis ${node.params.axis} != last; using last-axis.`);
|
|
566
|
+
}
|
|
567
|
+
const p = new Uint32Array([b, d]);
|
|
568
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
569
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
570
|
+
bindGroupEntries = [
|
|
571
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
572
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
573
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
574
|
+
];
|
|
575
|
+
workgroupCount = [Math.ceil(b / 64), 1, 1];
|
|
576
|
+
} else if (node.opType === "LeakyReLU") {
|
|
577
|
+
wgslCode = ShaderLibrary.getLeakyReLUShader();
|
|
578
|
+
const elements = node.outputs.out.shape.reduce((a, b) => a * b, 1);
|
|
579
|
+
const alpha = node.params.alpha !== undefined ? node.params.alpha : 0.01;
|
|
580
|
+
const p = new ArrayBuffer(16);
|
|
581
|
+
new Uint32Array(p, 0, 1)[0] = elements;
|
|
582
|
+
new Float32Array(p, 4, 1)[0] = alpha;
|
|
583
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
584
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
585
|
+
bindGroupEntries = [
|
|
586
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
587
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
588
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
589
|
+
];
|
|
590
|
+
workgroupCount = [Math.ceil(elements / 64), 1, 1];
|
|
591
|
+
} else if (node.opType === "PReLU") {
|
|
592
|
+
wgslCode = ShaderLibrary.getPReLUShader();
|
|
593
|
+
const shape = node.inputs.input.shape;
|
|
594
|
+
const c = shape[shape.length - 1] || 1;
|
|
595
|
+
const elements = shape.reduce((a, b) => a * b, 1);
|
|
596
|
+
const p = new Uint32Array([elements, c]);
|
|
597
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
598
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
599
|
+
bindGroupEntries = [
|
|
600
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
601
|
+
{ binding: 1, resource: { buffer: node.inputs.weight.gpuBuffer } },
|
|
602
|
+
{ binding: 2, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
603
|
+
{ binding: 3, resource: { buffer: paramsBuf } }
|
|
604
|
+
];
|
|
605
|
+
workgroupCount = [Math.ceil(elements / 64), 1, 1];
|
|
606
|
+
} else if (node.opType === "RMSNorm") {
|
|
607
|
+
wgslCode = ShaderLibrary.getRMSNormShader();
|
|
608
|
+
const shape = node.inputs.input.shape;
|
|
609
|
+
const d_model = node.params.d_model || shape[shape.length - 1];
|
|
610
|
+
const seq_len = shape.reduce((a, x) => a * x, 1) / d_model;
|
|
611
|
+
const eps = node.params.eps !== undefined ? node.params.eps : 1e-6;
|
|
612
|
+
const p = new ArrayBuffer(16);
|
|
613
|
+
new Uint32Array(p, 0, 2).set([seq_len, d_model]);
|
|
614
|
+
new Float32Array(p, 8, 1)[0] = eps;
|
|
615
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
616
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
617
|
+
bindGroupEntries = [
|
|
618
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
619
|
+
{ binding: 1, resource: { buffer: node.inputs.weight.gpuBuffer } },
|
|
620
|
+
{ binding: 2, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
621
|
+
{ binding: 3, resource: { buffer: paramsBuf } }
|
|
622
|
+
];
|
|
623
|
+
workgroupCount = [Math.ceil(seq_len / 64), 1, 1];
|
|
624
|
+
} else if (node.opType === "GlobalAveragePool") {
|
|
625
|
+
wgslCode = ShaderLibrary.getGlobalAveragePoolShader();
|
|
626
|
+
const inShape = node.inputs.input.shape;
|
|
627
|
+
const B = inShape[0];
|
|
628
|
+
const H = inShape[1] || 1;
|
|
629
|
+
const W = inShape[2] || 1;
|
|
630
|
+
const C = inShape[3] || 1;
|
|
631
|
+
const p = new Uint32Array([B, H, W, C]);
|
|
632
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
633
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
634
|
+
bindGroupEntries = [
|
|
635
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
636
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
637
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
638
|
+
];
|
|
639
|
+
workgroupCount = [Math.ceil(C / 64), B, 1];
|
|
640
|
+
} else if (node.opType === "BatchNorm2D") {
|
|
641
|
+
wgslCode = ShaderLibrary.getBatchNorm2DShader();
|
|
642
|
+
const [bn, hn, wn, cn] = node.inputs.input.shape;
|
|
643
|
+
const eps = node.params.eps !== undefined ? node.params.eps : 1e-5;
|
|
644
|
+
const p = new ArrayBuffer(32);
|
|
645
|
+
new Uint32Array(p, 0, 4).set([bn, cn, hn, wn]);
|
|
646
|
+
new Float32Array(p, 16, 1)[0] = eps;
|
|
647
|
+
const paramsBuf = this.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
648
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
649
|
+
bindGroupEntries = [
|
|
650
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
651
|
+
{ binding: 1, resource: { buffer: node.inputs.weight.gpuBuffer } },
|
|
652
|
+
{ binding: 2, resource: { buffer: node.inputs.bias.gpuBuffer } },
|
|
653
|
+
{ binding: 3, resource: { buffer: node.inputs.running_mean.gpuBuffer } },
|
|
654
|
+
{ binding: 4, resource: { buffer: node.inputs.running_var.gpuBuffer } },
|
|
655
|
+
{ binding: 5, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
656
|
+
{ binding: 6, resource: { buffer: paramsBuf } }
|
|
657
|
+
];
|
|
658
|
+
workgroupCount = [Math.ceil((bn * cn * hn * wn) / 64), 1, 1];
|
|
659
|
+
} else if (node.opType === "Resize" || node.opType === "ResizeNearest2D") {
|
|
660
|
+
wgslCode = ShaderLibrary.getResizeShader();
|
|
661
|
+
const [rb, inH, inW, rc] = node.inputs.input.shape;
|
|
662
|
+
const [, outH, outW] = node.outputs.out.shape;
|
|
663
|
+
const mode = (node.opType === "ResizeNearest2D" || node.params.mode === "nearest") ? 0 : 1;
|
|
664
|
+
const p = new Uint32Array([rb, inH, inW, rc, outH, outW, mode, 0]);
|
|
665
|
+
const paramsBuf = this.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
666
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
667
|
+
bindGroupEntries = [
|
|
668
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
669
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
670
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
671
|
+
];
|
|
672
|
+
workgroupCount = [Math.ceil(outW / 8), Math.ceil(outH / 8), rb * rc];
|
|
673
|
+
} else if (node.opType === "Split") {
|
|
674
|
+
// Split has multiple outputs; push one strided-copy pipeline per slice.
|
|
675
|
+
const inShape = node.inputs.input.shape;
|
|
676
|
+
let axis = node.params.axis || 0;
|
|
677
|
+
if (axis < 0) axis += inShape.length;
|
|
678
|
+
const outKeys = Object.keys(node.outputs).sort();
|
|
679
|
+
const numOutputs = outKeys.length;
|
|
680
|
+
const splitSize = inShape[axis] / numOutputs;
|
|
681
|
+
let inner = 1;
|
|
682
|
+
for (let i = axis + 1; i < inShape.length; i++) inner *= inShape[i];
|
|
683
|
+
const shaderModule = this.device.createShaderModule({ code: ShaderLibrary.getSplitShader() });
|
|
684
|
+
const pipeline = await this.device.createComputePipelineAsync({
|
|
685
|
+
layout: "auto",
|
|
686
|
+
compute: { module: shaderModule, entryPoint: "main" }
|
|
687
|
+
});
|
|
688
|
+
for (let o = 0; o < numOutputs; o++) {
|
|
689
|
+
const outT = node.outputs[outKeys[o]];
|
|
690
|
+
const total = outT.shape.reduce((a, b) => a * b, 1);
|
|
691
|
+
const p = new Uint32Array([total, inner, splitSize, inShape[axis], o * splitSize]);
|
|
692
|
+
const paramsBuf = this.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
693
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
694
|
+
const bindGroup = this.device.createBindGroup({
|
|
695
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
696
|
+
entries: [
|
|
697
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
698
|
+
{ binding: 1, resource: { buffer: outT.gpuBuffer } },
|
|
699
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
700
|
+
]
|
|
701
|
+
});
|
|
702
|
+
this.pipelines.push({
|
|
703
|
+
pipeline, bindGroup,
|
|
704
|
+
workgroupCount: [Math.ceil(total / 64), 1, 1],
|
|
705
|
+
nodeName: `${node.id}_split${o}`
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
} else if (node.opType === "Clip") {
|
|
710
|
+
wgslCode = ShaderLibrary.getClipShader();
|
|
711
|
+
const elements = node.inputs.input.sizeBytes / 4;
|
|
712
|
+
let minVal = node.params.min !== undefined ? node.params.min : -1e9;
|
|
713
|
+
let maxVal = node.params.max !== undefined ? node.params.max : 1e9;
|
|
714
|
+
if (node.inputs.min) minVal = new Float32Array(node.inputs.min.buffer)[0];
|
|
715
|
+
if (node.inputs.max) maxVal = new Float32Array(node.inputs.max.buffer)[0];
|
|
716
|
+
const p = new Float32Array([0, minVal, maxVal, 0]);
|
|
717
|
+
new Uint32Array(p.buffer)[0] = elements;
|
|
718
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
719
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
720
|
+
bindGroupEntries = [
|
|
721
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
722
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
723
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
724
|
+
];
|
|
725
|
+
workgroupCount = [Math.ceil(elements / 64), 1, 1];
|
|
726
|
+
} else if (node.opType === "MaxPool2D") {
|
|
727
|
+
wgslCode = ShaderLibrary.getMaxPool2DShader();
|
|
728
|
+
const [ky, kx] = _pair(node.params.kernel, 1);
|
|
729
|
+
const [sy, sx] = _pair(node.params.stride, 1);
|
|
730
|
+
const py = node.params.padding ? node.params.padding[0] : 0;
|
|
731
|
+
const px = node.params.padding ? node.params.padding[1] : 0;
|
|
732
|
+
const p = new Uint32Array([
|
|
733
|
+
node.inputs.input.shape[1],
|
|
734
|
+
node.inputs.input.shape[2],
|
|
735
|
+
node.inputs.input.shape[3],
|
|
736
|
+
node.outputs.out.shape[1],
|
|
737
|
+
node.outputs.out.shape[2],
|
|
738
|
+
ky, kx, sy, sx, py, px
|
|
739
|
+
]);
|
|
740
|
+
const paramBuf = this.device.createBuffer({ size: Math.ceil(p.byteLength / 16) * 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
741
|
+
this.device.queue.writeBuffer(paramBuf, 0, p);
|
|
742
|
+
bindGroupEntries = [
|
|
743
|
+
{ binding: 0, resource: { buffer: node.inputs.input.gpuBuffer } },
|
|
744
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
745
|
+
{ binding: 2, resource: { buffer: paramBuf } }
|
|
746
|
+
];
|
|
747
|
+
workgroupCount = [
|
|
748
|
+
Math.ceil(node.outputs.out.shape[2] / 8),
|
|
749
|
+
Math.ceil(node.outputs.out.shape[1] / 8),
|
|
750
|
+
node.outputs.out.shape[3]
|
|
751
|
+
];
|
|
752
|
+
} else if (node.opType === "Cast") {
|
|
753
|
+
// FP32 engine: a cast is a straight copy. Integer-target truncation is only
|
|
754
|
+
// done on the CPU/WASM tier (see ops/cast.js).
|
|
755
|
+
wgslCode = ShaderLibrary.getCopyShader();
|
|
756
|
+
const inp = node.inputs.input || node.inputs.data;
|
|
757
|
+
const elements = node.outputs.out.shape.reduce((a, b) => a * b, 1);
|
|
758
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
759
|
+
this.device.queue.writeBuffer(paramsBuf, 0, new Uint32Array([elements]));
|
|
760
|
+
bindGroupEntries = [
|
|
761
|
+
{ binding: 0, resource: { buffer: inp.gpuBuffer } },
|
|
762
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
763
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
764
|
+
];
|
|
765
|
+
workgroupCount = [Math.ceil(elements / 64), 1, 1];
|
|
766
|
+
} else if (node.opType === "Where" || node.opType === "Mask") {
|
|
767
|
+
wgslCode = ShaderLibrary.getWhereShader();
|
|
768
|
+
const cond = node.inputs.cond || node.inputs.condition;
|
|
769
|
+
const a = node.inputs.x || node.inputs.a;
|
|
770
|
+
const b = node.inputs.y || node.inputs.b;
|
|
771
|
+
const elements = node.outputs.out.shape.reduce((x, y) => x * y, 1);
|
|
772
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
773
|
+
this.device.queue.writeBuffer(paramsBuf, 0, new Uint32Array([elements]));
|
|
774
|
+
bindGroupEntries = [
|
|
775
|
+
{ binding: 0, resource: { buffer: cond.gpuBuffer } },
|
|
776
|
+
{ binding: 1, resource: { buffer: a.gpuBuffer } },
|
|
777
|
+
{ binding: 2, resource: { buffer: b.gpuBuffer } },
|
|
778
|
+
{ binding: 3, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
779
|
+
{ binding: 4, resource: { buffer: paramsBuf } }
|
|
780
|
+
];
|
|
781
|
+
workgroupCount = [Math.ceil(elements / 64), 1, 1];
|
|
782
|
+
} else if (node.opType === "DequantizeLinear") {
|
|
783
|
+
wgslCode = ShaderLibrary.getDequantizeLinearShader();
|
|
784
|
+
const inp = node.inputs.input || node.inputs.x;
|
|
785
|
+
const elements = node.outputs.out.shape.reduce((a, b) => a * b, 1);
|
|
786
|
+
const hasZp = node.inputs.zero_point ? 1 : 0;
|
|
787
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
788
|
+
this.device.queue.writeBuffer(paramsBuf, 0, new Uint32Array([elements, hasZp]));
|
|
789
|
+
const dummy = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE });
|
|
790
|
+
bindGroupEntries = [
|
|
791
|
+
{ binding: 0, resource: { buffer: inp.gpuBuffer } },
|
|
792
|
+
{ binding: 1, resource: { buffer: node.inputs.scale.gpuBuffer } },
|
|
793
|
+
{ binding: 2, resource: { buffer: hasZp ? node.inputs.zero_point.gpuBuffer : dummy } },
|
|
794
|
+
{ binding: 3, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
795
|
+
{ binding: 4, resource: { buffer: paramsBuf } }
|
|
796
|
+
];
|
|
797
|
+
workgroupCount = [Math.ceil(elements / 64), 1, 1];
|
|
798
|
+
} else if (node.opType === "Expand" || node.opType === "Broadcast") {
|
|
799
|
+
wgslCode = ShaderLibrary.getExpandShader();
|
|
800
|
+
const inp = node.inputs.input || node.inputs.data;
|
|
801
|
+
const pad4 = (sh) => [1, 1, 1, 1].slice(0, 4 - sh.length).concat(sh);
|
|
802
|
+
const [ib, ih, iw, ic] = pad4(inp.shape);
|
|
803
|
+
const [ob, oh, ow, oc] = pad4(node.outputs.out.shape);
|
|
804
|
+
const p = new Uint32Array([ib, ih, iw, ic, ob, oh, ow, oc]);
|
|
805
|
+
const paramsBuf = this.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
806
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
807
|
+
bindGroupEntries = [
|
|
808
|
+
{ binding: 0, resource: { buffer: inp.gpuBuffer } },
|
|
809
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
810
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
811
|
+
];
|
|
812
|
+
workgroupCount = [Math.ceil((ob * oh * ow * oc) / 64), 1, 1];
|
|
813
|
+
} else if (node.opType === "Pad") {
|
|
814
|
+
wgslCode = ShaderLibrary.getPadShader();
|
|
815
|
+
const inp = node.inputs.input || node.inputs.data;
|
|
816
|
+
const pads = node.params.pads || [];
|
|
817
|
+
const pt = pads.length === 8 ? pads[1] : (pads[0] || 0);
|
|
818
|
+
const pl = pads.length === 8 ? pads[2] : (pads[1] || 0);
|
|
819
|
+
const val = node.params.value || 0.0;
|
|
820
|
+
const is = [1, 1, 1, 1].slice(0, 4 - inp.shape.length).concat(inp.shape);
|
|
821
|
+
const os = [1, 1, 1, 1].slice(0, 4 - node.outputs.out.shape.length).concat(node.outputs.out.shape);
|
|
822
|
+
const buf = new ArrayBuffer(48);
|
|
823
|
+
const u = new Uint32Array(buf); const f = new Float32Array(buf);
|
|
824
|
+
u[0] = is[0]; u[1] = is[1]; u[2] = is[2]; u[3] = is[3]; u[4] = os[1]; u[5] = os[2]; u[6] = pt; u[7] = pl; f[8] = val;
|
|
825
|
+
const paramsBuf = this.device.createBuffer({ size: 48, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
826
|
+
this.device.queue.writeBuffer(paramsBuf, 0, buf);
|
|
827
|
+
bindGroupEntries = [
|
|
828
|
+
{ binding: 0, resource: { buffer: inp.gpuBuffer } },
|
|
829
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
830
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
831
|
+
];
|
|
832
|
+
workgroupCount = [Math.ceil((os[0] * os[1] * os[2] * os[3]) / 64), 1, 1];
|
|
833
|
+
} else if (node.opType === "Slice") {
|
|
834
|
+
wgslCode = ShaderLibrary.getSliceShader();
|
|
835
|
+
const inp = node.inputs.input || node.inputs.data;
|
|
836
|
+
const starts = node.params.starts || [0, 0, 0, 0];
|
|
837
|
+
const steps = node.params.steps || [1, 1, 1, 1];
|
|
838
|
+
const axes = node.params.axes || [0, 1, 2, 3];
|
|
839
|
+
const in_s = [1, 1, 1, 1].slice(0, 4 - inp.shape.length).concat(inp.shape);
|
|
840
|
+
const out_s = [1, 1, 1, 1].slice(0, 4 - node.outputs.out.shape.length).concat(node.outputs.out.shape);
|
|
841
|
+
const st = [0, 0, 0, 0], sp = [1, 1, 1, 1];
|
|
842
|
+
for (let i = 0; i < axes.length; i++) {
|
|
843
|
+
let ax = axes[i]; if (ax < 0) ax += inp.shape.length; ax += (4 - inp.shape.length);
|
|
844
|
+
st[ax] = starts[i] < 0 ? starts[i] + in_s[ax] : starts[i];
|
|
845
|
+
sp[ax] = steps[i];
|
|
846
|
+
}
|
|
847
|
+
const total = out_s[0] * out_s[1] * out_s[2] * out_s[3];
|
|
848
|
+
const p = new Uint32Array([out_s[0], out_s[1], out_s[2], out_s[3], in_s[1], in_s[2], in_s[3],
|
|
849
|
+
st[0], st[1], st[2], st[3], sp[0], sp[1], sp[2], sp[3], total]);
|
|
850
|
+
const paramsBuf = this.device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
851
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
852
|
+
bindGroupEntries = [
|
|
853
|
+
{ binding: 0, resource: { buffer: inp.gpuBuffer } },
|
|
854
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
855
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
856
|
+
];
|
|
857
|
+
workgroupCount = [Math.ceil(total / 64), 1, 1];
|
|
858
|
+
} else if (node.opType === "Gather") {
|
|
859
|
+
let axis = node.params.axis || 0;
|
|
860
|
+
if (axis < 0) axis += node.inputs.input.shape.length;
|
|
861
|
+
if (axis !== 0) {
|
|
862
|
+
console.warn(`[VolvoxAI WebGPU] Gather axis ${axis} not supported on GPU; node ${node.id} skipped (use WASM/CPU).`);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
wgslCode = ShaderLibrary.getGatherShader();
|
|
866
|
+
const inp = node.inputs.input;
|
|
867
|
+
const rowSize = inp.shape.slice(1).reduce((a, b) => a * b, 1) || 1;
|
|
868
|
+
const total = node.outputs.out.shape.reduce((a, b) => a * b, 1);
|
|
869
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
870
|
+
this.device.queue.writeBuffer(paramsBuf, 0, new Uint32Array([rowSize, total / rowSize, total]));
|
|
871
|
+
bindGroupEntries = [
|
|
872
|
+
{ binding: 0, resource: { buffer: inp.gpuBuffer } },
|
|
873
|
+
{ binding: 1, resource: { buffer: node.inputs.indices.gpuBuffer } },
|
|
874
|
+
{ binding: 2, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
875
|
+
{ binding: 3, resource: { buffer: paramsBuf } }
|
|
876
|
+
];
|
|
877
|
+
workgroupCount = [Math.ceil(total / 64), 1, 1];
|
|
878
|
+
} else if (node.opType === "ReduceSum" || node.opType === "ReduceMean") {
|
|
879
|
+
wgslCode = ShaderLibrary.getReduceShader();
|
|
880
|
+
const inp = node.inputs.input || node.inputs.data;
|
|
881
|
+
const in_shape = inp.shape.length === 2 ? inp.shape : [1, inp.shape.reduce((a, b) => a * b, 1)];
|
|
882
|
+
const b = in_shape[0], d = in_shape[1];
|
|
883
|
+
const inv = node.opType === "ReduceMean" ? 1.0 / d : 1.0;
|
|
884
|
+
const buf = new ArrayBuffer(16);
|
|
885
|
+
new Uint32Array(buf, 0, 2).set([b, d]); new Float32Array(buf, 8, 1)[0] = inv;
|
|
886
|
+
const paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
887
|
+
this.device.queue.writeBuffer(paramsBuf, 0, buf);
|
|
888
|
+
bindGroupEntries = [
|
|
889
|
+
{ binding: 0, resource: { buffer: inp.gpuBuffer } },
|
|
890
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
891
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
892
|
+
];
|
|
893
|
+
workgroupCount = [Math.ceil(b / 64), 1, 1];
|
|
894
|
+
} else if (node.opType === "AveragePool" || node.opType === "AveragePool2D") {
|
|
895
|
+
wgslCode = ShaderLibrary.getAveragePool2DShader();
|
|
896
|
+
const inp = node.inputs.input || node.inputs.x;
|
|
897
|
+
const [b, in_h, in_w, c] = inp.shape;
|
|
898
|
+
const [, out_h, out_w] = node.outputs.out.shape;
|
|
899
|
+
const [kh, kw] = _pair(node.params.kernel, 1);
|
|
900
|
+
const [sh, sw] = _pair(node.params.stride, 1);
|
|
901
|
+
const ph = node.params.padding ? node.params.padding[0] : 0;
|
|
902
|
+
const pw = node.params.padding ? node.params.padding[1] : 0;
|
|
903
|
+
const p = new Uint32Array([b, in_h, in_w, c, out_h, out_w, kh, kw, sh, sw, ph, pw]);
|
|
904
|
+
const paramsBuf = this.device.createBuffer({ size: 48, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
905
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
906
|
+
bindGroupEntries = [
|
|
907
|
+
{ binding: 0, resource: { buffer: inp.gpuBuffer } },
|
|
908
|
+
{ binding: 1, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
909
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
910
|
+
];
|
|
911
|
+
workgroupCount = [Math.ceil(out_w / 8), Math.ceil(out_h / 8), b * c];
|
|
912
|
+
} else if (node.opType === "ConvTranspose2D") {
|
|
913
|
+
wgslCode = ShaderLibrary.getConvTranspose2DShader();
|
|
914
|
+
const inp = node.inputs.input || node.inputs.x;
|
|
915
|
+
const [b, in_h, in_w, in_c] = inp.shape;
|
|
916
|
+
const [, out_h, out_w, out_c] = node.outputs.out.shape;
|
|
917
|
+
const kh = node.params.kernel[0], kw = node.params.kernel[1];
|
|
918
|
+
const sh = node.params.stride ? node.params.stride[0] : 1;
|
|
919
|
+
const sw = node.params.stride ? node.params.stride[1] : 1;
|
|
920
|
+
const ph = node.params.padding ? node.params.padding[0] : 0;
|
|
921
|
+
const pw = node.params.padding ? node.params.padding[1] : 0;
|
|
922
|
+
const hasBias = node.inputs.bias ? 1 : 0;
|
|
923
|
+
const p = new Uint32Array([b, in_h, in_w, in_c, out_h, out_w, out_c, kh, kw, sh, sw, ph, pw, hasBias]);
|
|
924
|
+
const paramsBuf = this.device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
925
|
+
this.device.queue.writeBuffer(paramsBuf, 0, p);
|
|
926
|
+
const dummyBias = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE });
|
|
927
|
+
bindGroupEntries = [
|
|
928
|
+
{ binding: 0, resource: { buffer: inp.gpuBuffer } },
|
|
929
|
+
{ binding: 1, resource: { buffer: node.inputs.weight.gpuBuffer } },
|
|
930
|
+
{ binding: 2, resource: { buffer: hasBias ? node.inputs.bias.gpuBuffer : dummyBias } },
|
|
931
|
+
{ binding: 3, resource: { buffer: node.outputs.out.gpuBuffer } },
|
|
932
|
+
{ binding: 4, resource: { buffer: paramsBuf } }
|
|
933
|
+
];
|
|
934
|
+
workgroupCount = [Math.ceil(out_w / 8), Math.ceil(out_h / 8), b * out_c];
|
|
935
|
+
} else {
|
|
936
|
+
console.warn(`[VolvoxAI WebGPU] Shader for ${node.opType} not implemented yet in Executor.`);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (!wgslCode) {
|
|
940
|
+
// A branch above delegated to a CPU helper (no GPU shader yet). Skip rather
|
|
941
|
+
// than crash on createShaderModule(""); its output buffer stays unwritten.
|
|
942
|
+
console.warn(`[VolvoxAI WebGPU] ${node.opType} has no native GPU shader; node ${node.id} skipped.`);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (isNaN(workgroupCount[0]) || isNaN(workgroupCount[1]) || isNaN(workgroupCount[2]) || workgroupCount[0] <= 0 || workgroupCount[1] <= 0 || workgroupCount[2] <= 0) {
|
|
946
|
+
console.error(`Invalid workgroupCount [${workgroupCount}] for node ${node.id} (${node.opType})`);
|
|
947
|
+
workgroupCount = [1, 1, 1];
|
|
948
|
+
}
|
|
949
|
+
let shaderModule = this.device.createShaderModule({ code: wgslCode });
|
|
950
|
+
let pipeline;
|
|
951
|
+
try {
|
|
952
|
+
pipeline = await this.device.createComputePipelineAsync({
|
|
953
|
+
layout: "auto",
|
|
954
|
+
compute: { module: shaderModule, entryPoint: "main" }
|
|
955
|
+
});
|
|
956
|
+
} catch (err) {
|
|
957
|
+
if (!fallbackWgslCode || fallbackWgslCode === wgslCode || !fallbackWorkgroupCount) throw err;
|
|
958
|
+
console.warn(`[VolvoxAI WebGPU] Specialized shader for ${node.id} failed; falling back to generic Conv2D.`, err);
|
|
959
|
+
wgslCode = fallbackWgslCode;
|
|
960
|
+
workgroupCount = fallbackWorkgroupCount;
|
|
961
|
+
shaderModule = this.device.createShaderModule({ code: wgslCode });
|
|
962
|
+
pipeline = await this.device.createComputePipelineAsync({
|
|
963
|
+
layout: "auto",
|
|
964
|
+
compute: { module: shaderModule, entryPoint: "main" }
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
const bindGroup = this.device.createBindGroup({
|
|
968
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
969
|
+
entries: bindGroupEntries
|
|
970
|
+
});
|
|
971
|
+
this.pipelines.push({ pipeline, bindGroup, workgroupCount, nodeName: node.id });
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Execute the compiled graph on the GPU.
|
|
975
|
+
* @param {Object} inputs - Key-value pair of input tensor names to Float32Array/Int32Array
|
|
976
|
+
*/
|
|
977
|
+
async execute(inputs) {
|
|
978
|
+
for (const [name, data] of Object.entries(inputs)) {
|
|
979
|
+
const buffer = this.gpuBuffers.get(name);
|
|
980
|
+
if (buffer) {
|
|
981
|
+
this.device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
let commandEncoder = this.device.createCommandEncoder();
|
|
985
|
+
let passEncoder = commandEncoder.beginComputePass();
|
|
986
|
+
for (let i = 0; i < this.pipelines.length; i++) {
|
|
987
|
+
const p = this.pipelines[i];
|
|
988
|
+
passEncoder.setPipeline(p.pipeline);
|
|
989
|
+
passEncoder.setBindGroup(0, p.bindGroup);
|
|
990
|
+
passEncoder.dispatchWorkgroups(p.workgroupCount[0], p.workgroupCount[1], p.workgroupCount[2]);
|
|
991
|
+
if ((i + 1) % 20 === 0) {
|
|
992
|
+
passEncoder.end();
|
|
993
|
+
this.device.queue.submit([commandEncoder.finish()]);
|
|
994
|
+
commandEncoder = this.device.createCommandEncoder();
|
|
995
|
+
passEncoder = commandEncoder.beginComputePass();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
passEncoder.end();
|
|
999
|
+
this.device.queue.submit([commandEncoder.finish()]);
|
|
1000
|
+
const lastNode = this.graph.nodes[this.graph.nodes.length - 1];
|
|
1001
|
+
const outName = Object.keys(lastNode.outputs)[0];
|
|
1002
|
+
return this.gpuBuffers.get(lastNode.outputs[outName].name);
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Helper to read a GPUBuffer back to CPU (Float32Array) for validation.
|
|
1006
|
+
*/
|
|
1007
|
+
async readBuffer(gpuBuffer, sizeBytes) {
|
|
1008
|
+
const stagingBuffer = this.device.createBuffer({
|
|
1009
|
+
size: sizeBytes,
|
|
1010
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
1011
|
+
});
|
|
1012
|
+
const commandEncoder = this.device.createCommandEncoder();
|
|
1013
|
+
commandEncoder.copyBufferToBuffer(gpuBuffer, 0, stagingBuffer, 0, sizeBytes);
|
|
1014
|
+
this.device.queue.submit([commandEncoder.finish()]);
|
|
1015
|
+
await stagingBuffer.mapAsync(GPUMapMode.READ);
|
|
1016
|
+
const copyArray = new Float32Array(stagingBuffer.getMappedRange().slice(0));
|
|
1017
|
+
stagingBuffer.unmap();
|
|
1018
|
+
return copyArray;
|
|
1019
|
+
}
|
|
1020
|
+
};
|