umap-web 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -0
- package/LICENSE +29 -0
- package/NOTICE +19 -0
- package/README.md +205 -0
- package/dist/chunk-Bp6m_JJh.js +13 -0
- package/dist/core-BPpH5_PW.js +2816 -0
- package/dist/core-BPpH5_PW.js.map +1 -0
- package/dist/core-IHdw-3yY.d.ts +373 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.js +4 -0
- package/dist/index.d.ts +261 -0
- package/dist/index.js +2991 -0
- package/dist/index.js.map +1 -0
- package/dist/init-DLiEtGpx.js +802 -0
- package/dist/init-DLiEtGpx.js.map +1 -0
- package/dist/sparse-BxSeVhO4.d.ts +150 -0
- package/dist/webgpu.d.ts +111 -0
- package/dist/webgpu.js +955 -0
- package/dist/webgpu.js.map +1 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +1095 -0
- package/dist/worker.js.map +1 -0
- package/package.json +76 -0
package/dist/webgpu.js
ADDED
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import { _ as silentLogger, a as degreeInvSqrt, f as UmapBackendError, i as connectedComponents, r as noisyScaleCoords, s as lobpcgSmallestAsync, t as finalInitRescale, v as throwIfAborted } from "./init-DLiEtGpx.js";
|
|
2
|
+
|
|
3
|
+
//#region src/backends/webgpu/device.ts
|
|
4
|
+
function trackLost(ctx) {
|
|
5
|
+
ctx.device.lost.then((info) => {
|
|
6
|
+
ctx.lost = {
|
|
7
|
+
reason: String(info.reason),
|
|
8
|
+
message: info.message
|
|
9
|
+
};
|
|
10
|
+
}).catch(() => {
|
|
11
|
+
ctx.lost = {
|
|
12
|
+
reason: "unknown",
|
|
13
|
+
message: "device.lost rejected"
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** Throw (typed) if the device reported loss. */
|
|
18
|
+
function assertUsable(ctx, stage) {
|
|
19
|
+
if (ctx.lost) throw new UmapBackendError(`GPU device lost during ${stage} (${ctx.lost.reason}): ${ctx.lost.message}`);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate a planned buffer against the granted limits BEFORE creating it —
|
|
23
|
+
* an oversized storage binding is an async validation error that would
|
|
24
|
+
* otherwise surface as silently zero-filled results on low-limit adapters.
|
|
25
|
+
*/
|
|
26
|
+
function ensureBufferFits(ctx, stage, name, byteLength) {
|
|
27
|
+
if (byteLength > ctx.limits.maxBufferSize) throw new UmapBackendError(`${stage}: buffer '${name}' (${byteLength} bytes) exceeds this device's maxBufferSize (${ctx.limits.maxBufferSize}); the dataset is too large for this GPU`);
|
|
28
|
+
if (byteLength > ctx.limits.maxStorageBufferBindingSize) throw new UmapBackendError(`${stage}: buffer '${name}' (${byteLength} bytes) exceeds this device's maxStorageBufferBindingSize (${ctx.limits.maxStorageBufferBindingSize}); the dataset is too large for this GPU`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Push validation + out-of-memory scopes; the returned closer pops them and
|
|
32
|
+
* throws a typed error when either scope caught anything. The closer is
|
|
33
|
+
* idempotent so kernels can also invoke it from a finally block (keeping the
|
|
34
|
+
* device's scope stack balanced when the body throws first).
|
|
35
|
+
*/
|
|
36
|
+
function beginErrorScopes(device) {
|
|
37
|
+
device.pushErrorScope("out-of-memory");
|
|
38
|
+
device.pushErrorScope("validation");
|
|
39
|
+
let popped = false;
|
|
40
|
+
return async (stage) => {
|
|
41
|
+
if (popped) return;
|
|
42
|
+
popped = true;
|
|
43
|
+
const validation = await device.popErrorScope();
|
|
44
|
+
const oom = await device.popErrorScope();
|
|
45
|
+
if (oom) throw new UmapBackendError(`${stage}: GPU out of memory: ${oom.message}`);
|
|
46
|
+
if (validation) throw new UmapBackendError(`${stage}: GPU validation error: ${validation.message}`);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function acquireGpu(provided) {
|
|
50
|
+
if (provided) {
|
|
51
|
+
const device$1 = typeof provided === "function" ? await provided() : await Promise.resolve(provided);
|
|
52
|
+
const ctx$1 = {
|
|
53
|
+
device: device$1,
|
|
54
|
+
adapterInfo: {
|
|
55
|
+
vendor: "injected",
|
|
56
|
+
architecture: "injected",
|
|
57
|
+
device: "",
|
|
58
|
+
description: "user-provided GPUDevice"
|
|
59
|
+
},
|
|
60
|
+
limits: {
|
|
61
|
+
maxStorageBufferBindingSize: device$1.limits.maxStorageBufferBindingSize,
|
|
62
|
+
maxBufferSize: device$1.limits.maxBufferSize,
|
|
63
|
+
maxWorkgroupsX: device$1.limits.maxComputeWorkgroupsPerDimension
|
|
64
|
+
},
|
|
65
|
+
timestamps: device$1.features.has("timestamp-query"),
|
|
66
|
+
injected: true,
|
|
67
|
+
lost: null
|
|
68
|
+
};
|
|
69
|
+
trackLost(ctx$1);
|
|
70
|
+
return ctx$1;
|
|
71
|
+
}
|
|
72
|
+
const gpu = globalThis.navigator?.gpu;
|
|
73
|
+
if (!gpu) throw new UmapBackendError("WebGPU is not available (navigator.gpu undefined)");
|
|
74
|
+
const adapter = await gpu.requestAdapter();
|
|
75
|
+
if (!adapter) throw new UmapBackendError("WebGPU adapter unavailable (requestAdapter returned null)");
|
|
76
|
+
const wantTimestamps = adapter.features.has("timestamp-query");
|
|
77
|
+
const device = await adapter.requestDevice({
|
|
78
|
+
requiredLimits: {
|
|
79
|
+
maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
|
|
80
|
+
maxBufferSize: adapter.limits.maxBufferSize
|
|
81
|
+
},
|
|
82
|
+
requiredFeatures: wantTimestamps ? ["timestamp-query"] : []
|
|
83
|
+
});
|
|
84
|
+
const info = adapter.info;
|
|
85
|
+
const ctx = {
|
|
86
|
+
device,
|
|
87
|
+
adapterInfo: {
|
|
88
|
+
vendor: info?.vendor ?? "unknown",
|
|
89
|
+
architecture: info?.architecture ?? "unknown",
|
|
90
|
+
device: info?.device ?? "",
|
|
91
|
+
description: info?.description ?? ""
|
|
92
|
+
},
|
|
93
|
+
limits: {
|
|
94
|
+
maxStorageBufferBindingSize: device.limits.maxStorageBufferBindingSize,
|
|
95
|
+
maxBufferSize: device.limits.maxBufferSize,
|
|
96
|
+
maxWorkgroupsX: device.limits.maxComputeWorkgroupsPerDimension
|
|
97
|
+
},
|
|
98
|
+
timestamps: device.features.has("timestamp-query"),
|
|
99
|
+
injected: false,
|
|
100
|
+
lost: null
|
|
101
|
+
};
|
|
102
|
+
trackLost(ctx);
|
|
103
|
+
return ctx;
|
|
104
|
+
}
|
|
105
|
+
function createBuffer(device, data, usage) {
|
|
106
|
+
const buf = device.createBuffer({
|
|
107
|
+
size: Math.ceil(data.byteLength / 4) * 4,
|
|
108
|
+
usage
|
|
109
|
+
});
|
|
110
|
+
device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
|
|
111
|
+
return buf;
|
|
112
|
+
}
|
|
113
|
+
async function readBuffer(device, src, byteLength) {
|
|
114
|
+
const staging = device.createBuffer({
|
|
115
|
+
size: Math.ceil(byteLength / 4) * 4,
|
|
116
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
const enc = device.createCommandEncoder();
|
|
120
|
+
enc.copyBufferToBuffer(src, 0, staging, 0, staging.size);
|
|
121
|
+
device.queue.submit([enc.finish()]);
|
|
122
|
+
await staging.mapAsync(GPUMapMode.READ);
|
|
123
|
+
const out = staging.getMappedRange().slice(0);
|
|
124
|
+
staging.unmap();
|
|
125
|
+
return out;
|
|
126
|
+
} catch (e) {
|
|
127
|
+
throw e instanceof UmapBackendError ? e : new UmapBackendError(`GPU readback failed: ${e.message}`);
|
|
128
|
+
} finally {
|
|
129
|
+
staging.destroy();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/backends/webgpu/kernels/knn.wgsl
|
|
135
|
+
var knn_default = "// Tiled brute-force kNN (§A3): 256 queries per workgroup (one per thread),\n// candidate tiles of 64 rows staged through workgroup memory as vec4 chunks,\n// per-thread private top-k (k <= 32). No n×n buffer ever exists; fits the\n// default 16 KB workgroup-storage and 8-storage-buffer limits.\n//\n// Data layout: rows padded to d4 = ceil(d/4) vec4s (zero padding is neutral for\n// every supported metric). metricKind: 0 = squared euclidean, 1 = alternative\n// cosine (log2(sqrt(nx*ny)/dot)), 2 = manhattan, 3 = hamming (raw count; /d on\n// CPU), 4 = alternative jaccard. correlation = kind 1 on row-centered data.\n\nstruct Params {\n n: u32, // candidate rows\n d4: u32, // vec4s per row\n k: u32, // neighbors (<= 32)\n metricKind: u32,\n nQueries: u32,\n candStart: u32,\n candEnd: u32,\n pad0: u32,\n}\n\n@group(0) @binding(0) var<storage, read> data: array<vec4<f32>>; // n x d4\n@group(0) @binding(1) var<storage, read> queries: array<vec4<f32>>; // nQueries x d4\n@group(0) @binding(2) var<storage, read_write> outDist: array<f32>;\n@group(0) @binding(3) var<storage, read_write> outIdx: array<i32>;\n@group(0) @binding(4) var<uniform> params: Params;\n@group(0) @binding(5) var<storage, read> norms: array<f32>;\n@group(0) @binding(6) var<storage, read> qnorms: array<f32>;\n\nconst TQ: u32 = 256u; // queries per workgroup\nconst TC: u32 = 64u; // candidate tile rows\nconst DC4: u32 = 8u; // vec4s per dimension chunk (32 floats)\nconst KMAX: u32 = 32u;\nconst FLOAT32_MAX: f32 = 3.4028234663852886e38;\n\nvar<workgroup> cTile: array<vec4<f32>, 512>; // TC x DC4\n\n@compute @workgroup_size(256)\nfn knnQueryTile(\n @builtin(workgroup_id) wg: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n) {\n let qLocal = lid.x;\n let qRow = wg.x * TQ + qLocal;\n let isActive = qRow < params.nQueries;\n\n var heapDist: array<f32, KMAX>;\n var heapIdx: array<i32, KMAX>;\n let k = params.k;\n for (var i = 0u; i < KMAX; i = i + 1u) {\n heapDist[i] = FLOAT32_MAX;\n heapIdx[i] = -1;\n }\n if (isActive && params.candStart > 0u) {\n for (var i = 0u; i < k; i = i + 1u) {\n heapDist[i] = outDist[qRow * k + i];\n heapIdx[i] = outIdx[qRow * k + i];\n }\n }\n var worst = 0u;\n var worstVal = -FLOAT32_MAX;\n for (var i = 0u; i < k; i = i + 1u) {\n if (heapDist[i] > worstVal) { worstVal = heapDist[i]; worst = i; }\n }\n\n var acc: array<f32, TC>;\n var acc2: array<f32, TC>;\n\n var cBase = params.candStart;\n loop {\n if (cBase >= params.candEnd) { break; }\n let tileLen = min(TC, params.candEnd - cBase);\n\n for (var t = 0u; t < TC; t = t + 1u) { acc[t] = 0.0; acc2[t] = 0.0; }\n\n var dBase = 0u; // vec4 offset within the row\n loop {\n if (dBase >= params.d4) { break; }\n let chunk = min(DC4, params.d4 - dBase);\n workgroupBarrier();\n // cooperative tile load: 512 vec4 slots over 256 threads (2 each)\n for (var li = qLocal; li < TC * DC4; li = li + 256u) {\n let row = li / DC4;\n let slot = li % DC4;\n if (row < tileLen && slot < chunk) {\n cTile[li] = data[(cBase + row) * params.d4 + dBase + slot];\n } else {\n cTile[li] = vec4<f32>(0.0);\n }\n }\n workgroupBarrier();\n if (isActive) {\n // fully-unrolled registers (dynamic private-array indexing spills to local\n // memory on Metal); zero vec4s are neutral for every metric here\n let qb = qRow * params.d4 + dBase;\n var q0 = vec4<f32>(0.0);\n var q1 = vec4<f32>(0.0);\n var q2 = vec4<f32>(0.0);\n var q3 = vec4<f32>(0.0);\n var q4 = vec4<f32>(0.0);\n var q5 = vec4<f32>(0.0);\n var q6 = vec4<f32>(0.0);\n var q7 = vec4<f32>(0.0);\n q0 = queries[qb];\n if (chunk > 1u) { q1 = queries[qb + 1u]; }\n if (chunk > 2u) { q2 = queries[qb + 2u]; }\n if (chunk > 3u) { q3 = queries[qb + 3u]; }\n if (chunk > 4u) { q4 = queries[qb + 4u]; }\n if (chunk > 5u) { q5 = queries[qb + 5u]; }\n if (chunk > 6u) { q6 = queries[qb + 6u]; }\n if (chunk > 7u) { q7 = queries[qb + 7u]; }\n switch params.metricKind {\n case 0u: {\n for (var t = 0u; t < tileLen; t = t + 1u) {\n let tb = t * DC4;\n let d0 = q0 - cTile[tb];\n let d1 = q1 - cTile[tb + 1u];\n let d2 = q2 - cTile[tb + 2u];\n let d3 = q3 - cTile[tb + 3u];\n let d4v = q4 - cTile[tb + 4u];\n let d5 = q5 - cTile[tb + 5u];\n let d6 = q6 - cTile[tb + 6u];\n let d7 = q7 - cTile[tb + 7u];\n let s = d0 * d0 + d1 * d1 + d2 * d2 + d3 * d3 + d4v * d4v + d5 * d5 + d6 * d6 + d7 * d7;\n acc[t] = acc[t] + s.x + s.y + s.z + s.w;\n }\n }\n case 1u: {\n for (var t = 0u; t < tileLen; t = t + 1u) {\n let tb = t * DC4;\n let s = q0 * cTile[tb] + q1 * cTile[tb + 1u] + q2 * cTile[tb + 2u] + q3 * cTile[tb + 3u]\n + q4 * cTile[tb + 4u] + q5 * cTile[tb + 5u] + q6 * cTile[tb + 6u] + q7 * cTile[tb + 7u];\n acc[t] = acc[t] + s.x + s.y + s.z + s.w;\n }\n }\n case 2u: {\n for (var t = 0u; t < tileLen; t = t + 1u) {\n let tb = t * DC4;\n let s = abs(q0 - cTile[tb]) + abs(q1 - cTile[tb + 1u]) + abs(q2 - cTile[tb + 2u]) + abs(q3 - cTile[tb + 3u])\n + abs(q4 - cTile[tb + 4u]) + abs(q5 - cTile[tb + 5u]) + abs(q6 - cTile[tb + 6u]) + abs(q7 - cTile[tb + 7u]);\n acc[t] = acc[t] + s.x + s.y + s.z + s.w;\n }\n }\n case 3u: {\n // hamming: padded zero lanes compare equal on both sides — neutral\n for (var t = 0u; t < tileLen; t = t + 1u) {\n let tb = t * DC4;\n let s = select(vec4<f32>(0.0), vec4<f32>(1.0), q0 != cTile[tb]) +\n select(vec4<f32>(0.0), vec4<f32>(1.0), q1 != cTile[tb + 1u]) +\n select(vec4<f32>(0.0), vec4<f32>(1.0), q2 != cTile[tb + 2u]) +\n select(vec4<f32>(0.0), vec4<f32>(1.0), q3 != cTile[tb + 3u]) +\n select(vec4<f32>(0.0), vec4<f32>(1.0), q4 != cTile[tb + 4u]) +\n select(vec4<f32>(0.0), vec4<f32>(1.0), q5 != cTile[tb + 5u]) +\n select(vec4<f32>(0.0), vec4<f32>(1.0), q6 != cTile[tb + 6u]) +\n select(vec4<f32>(0.0), vec4<f32>(1.0), q7 != cTile[tb + 7u]);\n acc[t] = acc[t] + s.x + s.y + s.z + s.w;\n }\n }\n default: {\n for (var t = 0u; t < tileLen; t = t + 1u) {\n let tb = t * DC4;\n var eq = vec4<f32>(0.0);\n var nz = vec4<f32>(0.0);\n let c0 = cTile[tb];\n let c1 = cTile[tb + 1u];\n let c2 = cTile[tb + 2u];\n let c3 = cTile[tb + 3u];\n eq = eq + select(vec4<f32>(0.0), vec4<f32>(1.0), (q0 != vec4<f32>(0.0)) & (c0 != vec4<f32>(0.0)));\n eq = eq + select(vec4<f32>(0.0), vec4<f32>(1.0), (q1 != vec4<f32>(0.0)) & (c1 != vec4<f32>(0.0)));\n eq = eq + select(vec4<f32>(0.0), vec4<f32>(1.0), (q2 != vec4<f32>(0.0)) & (c2 != vec4<f32>(0.0)));\n eq = eq + select(vec4<f32>(0.0), vec4<f32>(1.0), (q3 != vec4<f32>(0.0)) & (c3 != vec4<f32>(0.0)));\n let c4 = cTile[tb + 4u];\n let c5 = cTile[tb + 5u];\n let c6 = cTile[tb + 6u];\n let c7 = cTile[tb + 7u];\n eq = eq + select(vec4<f32>(0.0), vec4<f32>(1.0), (q4 != vec4<f32>(0.0)) & (c4 != vec4<f32>(0.0)));\n eq = eq + select(vec4<f32>(0.0), vec4<f32>(1.0), (q5 != vec4<f32>(0.0)) & (c5 != vec4<f32>(0.0)));\n eq = eq + select(vec4<f32>(0.0), vec4<f32>(1.0), (q6 != vec4<f32>(0.0)) & (c6 != vec4<f32>(0.0)));\n eq = eq + select(vec4<f32>(0.0), vec4<f32>(1.0), (q7 != vec4<f32>(0.0)) & (c7 != vec4<f32>(0.0)));\n nz = nz + select(vec4<f32>(0.0), vec4<f32>(1.0), (q0 != vec4<f32>(0.0)) | (c0 != vec4<f32>(0.0)));\n nz = nz + select(vec4<f32>(0.0), vec4<f32>(1.0), (q1 != vec4<f32>(0.0)) | (c1 != vec4<f32>(0.0)));\n nz = nz + select(vec4<f32>(0.0), vec4<f32>(1.0), (q2 != vec4<f32>(0.0)) | (c2 != vec4<f32>(0.0)));\n nz = nz + select(vec4<f32>(0.0), vec4<f32>(1.0), (q3 != vec4<f32>(0.0)) | (c3 != vec4<f32>(0.0)));\n nz = nz + select(vec4<f32>(0.0), vec4<f32>(1.0), (q4 != vec4<f32>(0.0)) | (c4 != vec4<f32>(0.0)));\n nz = nz + select(vec4<f32>(0.0), vec4<f32>(1.0), (q5 != vec4<f32>(0.0)) | (c5 != vec4<f32>(0.0)));\n nz = nz + select(vec4<f32>(0.0), vec4<f32>(1.0), (q6 != vec4<f32>(0.0)) | (c6 != vec4<f32>(0.0)));\n nz = nz + select(vec4<f32>(0.0), vec4<f32>(1.0), (q7 != vec4<f32>(0.0)) | (c7 != vec4<f32>(0.0)));\n acc[t] = acc[t] + eq.x + eq.y + eq.z + eq.w;\n acc2[t] = acc2[t] + nz.x + nz.y + nz.z + nz.w;\n }\n }\n }\n }\n dBase = dBase + DC4;\n }\n\n if (isActive) {\n for (var t = 0u; t < tileLen; t = t + 1u) {\n var dist: f32;\n switch params.metricKind {\n case 0u, 2u, 3u: { dist = acc[t]; }\n case 1u: {\n let nx = qnorms[qRow];\n let ny = norms[cBase + t];\n if (nx == 0.0 && ny == 0.0) {\n dist = 0.0;\n } else if (nx == 0.0 || ny == 0.0 || acc[t] <= 0.0) {\n dist = FLOAT32_MAX;\n } else {\n dist = log2(sqrt(nx * ny) / acc[t]);\n }\n }\n default: {\n if (acc2[t] == 0.0) {\n dist = 0.0;\n } else if (acc[t] == 0.0) {\n dist = FLOAT32_MAX;\n } else {\n dist = -log2(acc[t] / acc2[t]);\n }\n }\n }\n if (dist < worstVal) {\n heapDist[worst] = dist;\n heapIdx[worst] = i32(cBase + t);\n worstVal = -FLOAT32_MAX;\n for (var i = 0u; i < k; i = i + 1u) {\n if (heapDist[i] > worstVal) { worstVal = heapDist[i]; worst = i; }\n }\n }\n }\n }\n cBase = cBase + TC;\n }\n\n if (isActive) {\n for (var i = 0u; i < k; i = i + 1u) {\n outDist[qRow * k + i] = heapDist[i];\n outIdx[qRow * k + i] = heapIdx[i];\n }\n }\n}\n\n// squared norms (cosine family); outDist reused as the norms output\n@compute @workgroup_size(256)\nfn rowNorms(@builtin(global_invocation_id) gid: vec3<u32>) {\n let row = gid.x;\n if (row >= params.n) { return; }\n var s = vec4<f32>(0.0);\n for (var c = 0u; c < params.d4; c = c + 1u) {\n let v = data[row * params.d4 + c];\n s = s + v * v;\n }\n outDist[row] = s.x + s.y + s.z + s.w;\n}\n";
|
|
136
|
+
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/backends/webgpu/knn.ts
|
|
139
|
+
/** GPU support matrix (§4.3). Returns null for unsupported metrics. */
|
|
140
|
+
function gpuMetricPlan(name) {
|
|
141
|
+
switch (name) {
|
|
142
|
+
case "euclidean":
|
|
143
|
+
case "l2": return {
|
|
144
|
+
kind: 0,
|
|
145
|
+
center: false,
|
|
146
|
+
correction: (d) => Math.sqrt(d)
|
|
147
|
+
};
|
|
148
|
+
case "sqeuclidean": return {
|
|
149
|
+
kind: 0,
|
|
150
|
+
center: false,
|
|
151
|
+
correction: (d) => d
|
|
152
|
+
};
|
|
153
|
+
case "cosine": return {
|
|
154
|
+
kind: 1,
|
|
155
|
+
center: false,
|
|
156
|
+
correction: (d) => 1 - 2 ** -d
|
|
157
|
+
};
|
|
158
|
+
case "correlation": return {
|
|
159
|
+
kind: 1,
|
|
160
|
+
center: true,
|
|
161
|
+
correction: (d) => 1 - 2 ** -d
|
|
162
|
+
};
|
|
163
|
+
case "manhattan":
|
|
164
|
+
case "taxicab":
|
|
165
|
+
case "l1": return {
|
|
166
|
+
kind: 2,
|
|
167
|
+
center: false,
|
|
168
|
+
correction: (d) => d
|
|
169
|
+
};
|
|
170
|
+
case "hamming": return {
|
|
171
|
+
kind: 3,
|
|
172
|
+
center: false,
|
|
173
|
+
correction: (d) => d
|
|
174
|
+
};
|
|
175
|
+
case "jaccard": return {
|
|
176
|
+
kind: 4,
|
|
177
|
+
center: false,
|
|
178
|
+
correction: (d) => 1 - 2 ** -d
|
|
179
|
+
};
|
|
180
|
+
default: return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const PARAMS_SIZE = 32;
|
|
184
|
+
const pipelineCache = /* @__PURE__ */ new WeakMap();
|
|
185
|
+
function getPipelines(device) {
|
|
186
|
+
let p = pipelineCache.get(device);
|
|
187
|
+
if (!p) {
|
|
188
|
+
const module = device.createShaderModule({ code: knn_default });
|
|
189
|
+
p = {
|
|
190
|
+
module,
|
|
191
|
+
tile: device.createComputePipeline({
|
|
192
|
+
layout: "auto",
|
|
193
|
+
compute: {
|
|
194
|
+
module,
|
|
195
|
+
entryPoint: "knnQueryTile"
|
|
196
|
+
}
|
|
197
|
+
}),
|
|
198
|
+
norms: device.createComputePipeline({
|
|
199
|
+
layout: "auto",
|
|
200
|
+
compute: {
|
|
201
|
+
module,
|
|
202
|
+
entryPoint: "rowNorms"
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
};
|
|
206
|
+
pipelineCache.set(device, p);
|
|
207
|
+
}
|
|
208
|
+
return p;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* kNN of `queries` against `data` (self-kNN when they alias) in INTERNAL metric
|
|
212
|
+
* space; returns corrected, ascending-sorted results.
|
|
213
|
+
*/
|
|
214
|
+
async function gpuKnn(ctx, X, queries, k, plan, { candChunk, onProgress, signal } = {}) {
|
|
215
|
+
if (k > 32) throw new UmapBackendError("GPU kNN supports k <= 32");
|
|
216
|
+
const device = ctx.device;
|
|
217
|
+
const n = X.rows;
|
|
218
|
+
const d = X.cols;
|
|
219
|
+
const d4 = Math.ceil(d / 4);
|
|
220
|
+
const dPad = d4 * 4;
|
|
221
|
+
const nQueries = queries === null ? n : queries.rows;
|
|
222
|
+
assertUsable(ctx, "gpuKnn");
|
|
223
|
+
ensureBufferFits(ctx, "gpuKnn", "data", n * dPad * 4);
|
|
224
|
+
if (queries !== null) ensureBufferFits(ctx, "gpuKnn", "queries", nQueries * dPad * 4);
|
|
225
|
+
ensureBufferFits(ctx, "gpuKnn", "outDistances", nQueries * k * 4);
|
|
226
|
+
ensureBufferFits(ctx, "gpuKnn", "outIndices", nQueries * k * 4);
|
|
227
|
+
const padRows = (src, rows, center) => {
|
|
228
|
+
if (dPad === d && !center) return src;
|
|
229
|
+
const out = new Float32Array(rows * dPad);
|
|
230
|
+
for (let i = 0; i < rows; i++) {
|
|
231
|
+
let mu = 0;
|
|
232
|
+
if (center) {
|
|
233
|
+
for (let c = 0; c < d; c++) mu += src[i * d + c];
|
|
234
|
+
mu /= d;
|
|
235
|
+
}
|
|
236
|
+
for (let c = 0; c < d; c++) out[i * dPad + c] = src[i * d + c] - mu;
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
};
|
|
240
|
+
const hostData = padRows(X.data, n, plan.center);
|
|
241
|
+
const sameQuery = queries === null;
|
|
242
|
+
const nq = sameQuery ? n : queries.rows;
|
|
243
|
+
const hostQ = sameQuery ? hostData : padRows(queries.data, nq, plan.center);
|
|
244
|
+
const pipes = getPipelines(device);
|
|
245
|
+
const scratch = /* @__PURE__ */ new Set();
|
|
246
|
+
const track = (buf) => {
|
|
247
|
+
scratch.add(buf);
|
|
248
|
+
return buf;
|
|
249
|
+
};
|
|
250
|
+
const endScopes = beginErrorScopes(device);
|
|
251
|
+
let dist;
|
|
252
|
+
let idx;
|
|
253
|
+
try {
|
|
254
|
+
const dataBuf = track(createBuffer(device, hostData, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST));
|
|
255
|
+
const qBuf = sameQuery ? dataBuf : track(createBuffer(device, hostQ, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST));
|
|
256
|
+
const outDist = track(device.createBuffer({
|
|
257
|
+
size: nq * k * 4,
|
|
258
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
259
|
+
}));
|
|
260
|
+
const outIdx = track(device.createBuffer({
|
|
261
|
+
size: nq * k * 4,
|
|
262
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
263
|
+
}));
|
|
264
|
+
const paramsBuf = track(device.createBuffer({
|
|
265
|
+
size: PARAMS_SIZE,
|
|
266
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
267
|
+
}));
|
|
268
|
+
const dummy = track(device.createBuffer({
|
|
269
|
+
size: 4,
|
|
270
|
+
usage: GPUBufferUsage.STORAGE
|
|
271
|
+
}));
|
|
272
|
+
let normsBuf = dummy;
|
|
273
|
+
let qnormsBuf = dummy;
|
|
274
|
+
if (plan.kind === 1) {
|
|
275
|
+
normsBuf = track(device.createBuffer({
|
|
276
|
+
size: n * 4,
|
|
277
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
278
|
+
}));
|
|
279
|
+
const np = track(device.createBuffer({
|
|
280
|
+
size: PARAMS_SIZE,
|
|
281
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
282
|
+
}));
|
|
283
|
+
device.queue.writeBuffer(np, 0, new Uint32Array([
|
|
284
|
+
n,
|
|
285
|
+
d4,
|
|
286
|
+
k,
|
|
287
|
+
plan.kind,
|
|
288
|
+
n,
|
|
289
|
+
0,
|
|
290
|
+
n,
|
|
291
|
+
0
|
|
292
|
+
]));
|
|
293
|
+
const bg = device.createBindGroup({
|
|
294
|
+
layout: pipes.norms.getBindGroupLayout(0),
|
|
295
|
+
entries: [
|
|
296
|
+
{
|
|
297
|
+
binding: 0,
|
|
298
|
+
resource: { buffer: dataBuf }
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
binding: 2,
|
|
302
|
+
resource: { buffer: normsBuf }
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
binding: 4,
|
|
306
|
+
resource: { buffer: np }
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
});
|
|
310
|
+
const enc = device.createCommandEncoder();
|
|
311
|
+
const pass = enc.beginComputePass();
|
|
312
|
+
pass.setPipeline(pipes.norms);
|
|
313
|
+
pass.setBindGroup(0, bg);
|
|
314
|
+
pass.dispatchWorkgroups(Math.ceil(n / 256));
|
|
315
|
+
pass.end();
|
|
316
|
+
device.queue.submit([enc.finish()]);
|
|
317
|
+
if (sameQuery) qnormsBuf = normsBuf;
|
|
318
|
+
else {
|
|
319
|
+
qnormsBuf = track(device.createBuffer({
|
|
320
|
+
size: nq * 4,
|
|
321
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
322
|
+
}));
|
|
323
|
+
const qp = track(device.createBuffer({
|
|
324
|
+
size: PARAMS_SIZE,
|
|
325
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
326
|
+
}));
|
|
327
|
+
device.queue.writeBuffer(qp, 0, new Uint32Array([
|
|
328
|
+
nq,
|
|
329
|
+
d4,
|
|
330
|
+
k,
|
|
331
|
+
plan.kind,
|
|
332
|
+
nq,
|
|
333
|
+
0,
|
|
334
|
+
nq,
|
|
335
|
+
0
|
|
336
|
+
]));
|
|
337
|
+
const bgq = device.createBindGroup({
|
|
338
|
+
layout: pipes.norms.getBindGroupLayout(0),
|
|
339
|
+
entries: [
|
|
340
|
+
{
|
|
341
|
+
binding: 0,
|
|
342
|
+
resource: { buffer: qBuf }
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
binding: 2,
|
|
346
|
+
resource: { buffer: qnormsBuf }
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
binding: 4,
|
|
350
|
+
resource: { buffer: qp }
|
|
351
|
+
}
|
|
352
|
+
]
|
|
353
|
+
});
|
|
354
|
+
const enc2 = device.createCommandEncoder();
|
|
355
|
+
const pass2 = enc2.beginComputePass();
|
|
356
|
+
pass2.setPipeline(pipes.norms);
|
|
357
|
+
pass2.setBindGroup(0, bgq);
|
|
358
|
+
pass2.dispatchWorkgroups(Math.ceil(nq / 256));
|
|
359
|
+
pass2.end();
|
|
360
|
+
device.queue.submit([enc2.finish()]);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const bindGroup = device.createBindGroup({
|
|
364
|
+
layout: pipes.tile.getBindGroupLayout(0),
|
|
365
|
+
entries: [
|
|
366
|
+
{
|
|
367
|
+
binding: 0,
|
|
368
|
+
resource: { buffer: dataBuf }
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
binding: 1,
|
|
372
|
+
resource: { buffer: qBuf }
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
binding: 2,
|
|
376
|
+
resource: { buffer: outDist }
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
binding: 3,
|
|
380
|
+
resource: { buffer: outIdx }
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
binding: 4,
|
|
384
|
+
resource: { buffer: paramsBuf }
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
binding: 5,
|
|
388
|
+
resource: { buffer: normsBuf }
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
binding: 6,
|
|
392
|
+
resource: { buffer: qnormsBuf }
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
});
|
|
396
|
+
const chunk = candChunk ?? Math.max(4096, Math.min(n, Math.floor(6e10 / Math.max(1, nq * d))));
|
|
397
|
+
const wgCount = Math.ceil(nq / 256);
|
|
398
|
+
for (let cs = 0; cs < n; cs += chunk) {
|
|
399
|
+
throwIfAborted(signal);
|
|
400
|
+
assertUsable(ctx, "gpuKnn");
|
|
401
|
+
const ce = Math.min(n, cs + chunk);
|
|
402
|
+
device.queue.writeBuffer(paramsBuf, 0, new Uint32Array([
|
|
403
|
+
n,
|
|
404
|
+
d4,
|
|
405
|
+
k,
|
|
406
|
+
plan.kind,
|
|
407
|
+
nq,
|
|
408
|
+
cs,
|
|
409
|
+
ce,
|
|
410
|
+
0
|
|
411
|
+
]));
|
|
412
|
+
const enc = device.createCommandEncoder();
|
|
413
|
+
const pass = enc.beginComputePass();
|
|
414
|
+
pass.setPipeline(pipes.tile);
|
|
415
|
+
pass.setBindGroup(0, bindGroup);
|
|
416
|
+
pass.dispatchWorkgroups(wgCount);
|
|
417
|
+
pass.end();
|
|
418
|
+
device.queue.submit([enc.finish()]);
|
|
419
|
+
await device.queue.onSubmittedWorkDone();
|
|
420
|
+
onProgress?.(ce, n);
|
|
421
|
+
}
|
|
422
|
+
await endScopes("gpuKnn");
|
|
423
|
+
assertUsable(ctx, "gpuKnn");
|
|
424
|
+
dist = new Float32Array(await readBuffer(device, outDist, nq * k * 4));
|
|
425
|
+
idx = new Int32Array(await readBuffer(device, outIdx, nq * k * 4));
|
|
426
|
+
} finally {
|
|
427
|
+
await endScopes("gpuKnn").catch(() => {});
|
|
428
|
+
for (const b of scratch) b.destroy();
|
|
429
|
+
}
|
|
430
|
+
const indices = new Int32Array(nq * k);
|
|
431
|
+
const distances = new Float32Array(nq * k);
|
|
432
|
+
const order = new Array(k);
|
|
433
|
+
for (let i = 0; i < nq; i++) {
|
|
434
|
+
for (let j = 0; j < k; j++) order[j] = j;
|
|
435
|
+
order.sort((a, b) => dist[i * k + a] - dist[i * k + b]);
|
|
436
|
+
for (let j = 0; j < k; j++) {
|
|
437
|
+
const o = order[j];
|
|
438
|
+
let raw = dist[i * k + o];
|
|
439
|
+
if (plan.kind === 3) raw /= d;
|
|
440
|
+
indices[i * k + j] = idx[i * k + o];
|
|
441
|
+
distances[i * k + j] = raw >= 34e37 ? Infinity : plan.correction(raw);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
indices,
|
|
446
|
+
distances,
|
|
447
|
+
k
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/backends/webgpu/kernels/sgd.wgsl
|
|
453
|
+
var sgd_default = "// SGD layout kernels (§A2). Two strategies, both implemented and measured\n// (DECISIONS D-011):\n// (A) vertexGather — one thread per head vertex walking its CSR edge range;\n// writes only its own row (conflict-free by ownership; Hogwild reads).\n// (B) edgeAtomic — one thread per edge; fixed-point atomicAdd on i32 embedding\n// (scale 2^20), converted to/from f32 by pack/unpack kernels. Epochs run\n// as ordered sub-batches; reads come from the previous sub-batch's\n// snapshot (binding 11), making the result bit-deterministic per seed.\n\nstruct LayoutParams {\n nVertices: u32,\n nEdges: u32,\n dim: u32,\n epoch: u32,\n alpha: f32,\n a: f32,\n b: f32,\n gamma: f32,\n negativeSampleRate: f32,\n moveOther: u32,\n scale: f32, // fixed-point scale for edgeAtomic\n pad0: u32,\n}\n\n// Ordered sub-batch of the edge list for one edgeAtomic dispatch. Epochs are\n// split into sequential sub-batches so an edge sees the committed updates of\n// every earlier sub-batch — approximating the CPU's in-order (Gauss–Seidel)\n// update visibility instead of one epoch-wide racy dispatch.\nstruct BatchParams {\n edgeOffset: u32,\n edgeCount: u32,\n}\n\n@group(0) @binding(0) var<storage, read_write> embedding: array<f32>;\n@group(0) @binding(1) var<storage, read> head: array<i32>;\n@group(0) @binding(2) var<storage, read> tail: array<i32>;\n@group(0) @binding(3) var<storage, read> epochsPerSample: array<f32>;\n// schedule[e] = epoch_of_next_sample; schedule[nEdges + e] = epoch_of_next_negative\n@group(0) @binding(4) var<storage, read_write> schedule: array<f32>;\n@group(0) @binding(6) var<storage, read_write> rngStates: array<u32>; // 3 per vertex (A) / per edge (B)\n@group(0) @binding(7) var<uniform> P: LayoutParams;\n@group(0) @binding(8) var<storage, read> rowPtr: array<u32>; // vertexGather CSR\n@group(0) @binding(9) var<storage, read_write> embeddingFx: array<atomic<i32>>; // edgeAtomic\n@group(0) @binding(10) var<uniform> B: BatchParams;\n// Fixed-point coordinate snapshot: the committed state after the previous\n// sub-batch. edgeAtomic reads ONLY from here and atomically accumulates into\n// embeddingFx, so every thread sees identical inputs regardless of scheduling\n// and i32 adds commute exactly — the layout is bit-deterministic per seed.\n@group(0) @binding(11) var<storage, read> snapFx: array<i32>;\n\nconst CLIP: f32 = 4.0;\n\nfn tau_step(s: ptr<function, vec3<u32>>) -> i32 {\n var s0 = (*s).x;\n var s1 = (*s).y;\n var s2 = (*s).z;\n s0 = ((s0 & 0xfffffffeu) << 12u) ^ (((s0 << 13u) ^ s0) >> 19u);\n s1 = ((s1 & 0xfffffff8u) << 4u) ^ (((s1 << 2u) ^ s1) >> 25u);\n s2 = ((s2 & 0xfffffff0u) << 17u) ^ (((s2 << 3u) ^ s2) >> 11u);\n (*s) = vec3<u32>(s0, s1, s2);\n return i32(s0 ^ s1 ^ s2);\n}\n\nfn clipv(v: f32) -> f32 {\n return clamp(v, -CLIP, CLIP);\n}\n\n// ---------------------------------------------------------------------------\n// (A) vertexGather: thread = head vertex; edges rowPtr[v]..rowPtr[v+1]\n// ---------------------------------------------------------------------------\n\n@compute @workgroup_size(256)\nfn vertexGather(@builtin(global_invocation_id) gid: vec3<u32>) {\n let v = gid.x;\n if (v >= P.nVertices) { return; }\n let dim = P.dim;\n let n = f32(P.epoch);\n var rng = vec3<u32>(rngStates[v * 3u], rngStates[v * 3u + 1u], rngStates[v * 3u + 2u]);\n\n for (var e = rowPtr[v]; e < rowPtr[v + 1u]; e = e + 1u) {\n if (schedule[e] > n) { continue; }\n let j = u32(head[e]);\n let kv = u32(tail[e]);\n var d2 = 0.0;\n for (var c = 0u; c < dim; c = c + 1u) {\n let diff = embedding[j * dim + c] - embedding[kv * dim + c];\n d2 = d2 + diff * diff;\n }\n var gradCoeff = 0.0;\n if (d2 > 0.0) {\n gradCoeff = (-2.0 * P.a * P.b * pow(d2, P.b - 1.0)) / (P.a * pow(d2, P.b) + 1.0);\n }\n for (var c = 0u; c < dim; c = c + 1u) {\n let diff = embedding[j * dim + c] - embedding[kv * dim + c];\n let g = clipv(gradCoeff * diff);\n // ownership: each endpoint gets one pull per undirected edge (the reverse\n // directed edge pulls the other endpoint)\n embedding[j * dim + c] = embedding[j * dim + c] + g * P.alpha;\n }\n schedule[e] = schedule[e] + epochsPerSample[e];\n\n let epn = epochsPerSample[e] / P.negativeSampleRate;\n let nNeg = i32((n - schedule[P.nEdges + e]) / epn);\n for (var p = 0; p < nNeg; p = p + 1) {\n let r = tau_step(&rng);\n var kk = r % i32(P.nVertices);\n if (kk < 0) { kk = kk + i32(P.nVertices); }\n if (u32(kk) == j) { continue; }\n var dn = 0.0;\n for (var c = 0u; c < dim; c = c + 1u) {\n let diff = embedding[j * dim + c] - embedding[u32(kk) * dim + c];\n dn = dn + diff * diff;\n }\n if (dn > 0.0) {\n let rep = (2.0 * P.gamma * P.b) / ((0.001 + dn) * (P.a * pow(dn, P.b) + 1.0));\n for (var c = 0u; c < dim; c = c + 1u) {\n let diff = embedding[j * dim + c] - embedding[u32(kk) * dim + c];\n embedding[j * dim + c] = embedding[j * dim + c] + clipv(rep * diff) * P.alpha;\n }\n }\n }\n schedule[P.nEdges + e] = schedule[P.nEdges + e] + f32(nNeg) * epn;\n }\n rngStates[v * 3u] = rng.x;\n rngStates[v * 3u + 1u] = rng.y;\n rngStates[v * 3u + 2u] = rng.z;\n}\n\n// ---------------------------------------------------------------------------\n// (B) edgeAtomic: thread = directed edge; fixed-point atomics\n// ---------------------------------------------------------------------------\n\n@compute @workgroup_size(256)\nfn packFx(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= P.nVertices * P.dim) { return; }\n atomicStore(&embeddingFx[i], i32(round(embedding[i] * P.scale)));\n}\n\n@compute @workgroup_size(256)\nfn unpackFx(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= P.nVertices * P.dim) { return; }\n embedding[i] = f32(atomicLoad(&embeddingFx[i])) / P.scale;\n}\n\n@compute @workgroup_size(256)\nfn edgeAtomic(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= B.edgeCount) { return; }\n let e = B.edgeOffset + gid.x;\n if (e >= P.nEdges) { return; }\n if (schedule[e] > f32(P.epoch)) { return; }\n let dim = P.dim;\n let j = u32(head[e]);\n let kv = u32(tail[e]);\n var rng = vec3<u32>(rngStates[e * 3u], rngStates[e * 3u + 1u], rngStates[e * 3u + 2u]);\n\n // Thread-local running coordinates for j (the CPU kernel's within-edge\n // update chain); every cross-vertex read comes from the sub-batch snapshot.\n var cj: array<f32, 8>;\n var net: array<f32, 8>;\n for (var c = 0u; c < dim; c = c + 1u) {\n cj[c] = f32(snapFx[j * dim + c]) / P.scale;\n net[c] = 0.0;\n }\n\n var d2 = 0.0;\n for (var c = 0u; c < dim; c = c + 1u) {\n let diff = cj[c] - f32(snapFx[kv * dim + c]) / P.scale;\n d2 = d2 + diff * diff;\n }\n if (d2 > 0.0) {\n let gradCoeff = (-2.0 * P.a * P.b * pow(d2, P.b - 1.0)) / (P.a * pow(d2, P.b) + 1.0);\n for (var c = 0u; c < dim; c = c + 1u) {\n let diff = cj[c] - f32(snapFx[kv * dim + c]) / P.scale;\n let g = clipv(gradCoeff * diff) * P.alpha;\n cj[c] = cj[c] + g;\n net[c] = net[c] + g;\n if (P.moveOther == 1u) {\n atomicAdd(&embeddingFx[kv * dim + c], i32(round(-g * P.scale)));\n }\n }\n }\n schedule[e] = schedule[e] + epochsPerSample[e];\n\n let epn = epochsPerSample[e] / P.negativeSampleRate;\n let nNeg = i32((f32(P.epoch) - schedule[P.nEdges + e]) / epn);\n for (var p = 0; p < nNeg; p = p + 1) {\n let r = tau_step(&rng);\n var kk = r % i32(P.nVertices);\n if (kk < 0) { kk = kk + i32(P.nVertices); }\n if (u32(kk) == j) { continue; }\n var dn = 0.0;\n for (var c = 0u; c < dim; c = c + 1u) {\n let diff = cj[c] - f32(snapFx[u32(kk) * dim + c]) / P.scale;\n dn = dn + diff * diff;\n }\n if (dn > 0.0) {\n let rep = (2.0 * P.gamma * P.b) / ((0.001 + dn) * (P.a * pow(dn, P.b) + 1.0));\n for (var c = 0u; c < dim; c = c + 1u) {\n let diff = cj[c] - f32(snapFx[u32(kk) * dim + c]) / P.scale;\n let g = clipv(rep * diff) * P.alpha;\n cj[c] = cj[c] + g;\n net[c] = net[c] + g;\n }\n }\n }\n schedule[P.nEdges + e] = schedule[P.nEdges + e] + f32(nNeg) * epn;\n // one rounded net update per component — less quantization than per-step adds\n for (var c = 0u; c < dim; c = c + 1u) {\n atomicAdd(&embeddingFx[j * dim + c], i32(round(net[c] * P.scale)));\n }\n rngStates[e * 3u] = rng.x;\n rngStates[e * 3u + 1u] = rng.y;\n rngStates[e * 3u + 2u] = rng.z;\n}\n";
|
|
454
|
+
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/backends/webgpu/layout.ts
|
|
457
|
+
const cache$1 = /* @__PURE__ */ new WeakMap();
|
|
458
|
+
function pipelines(device) {
|
|
459
|
+
let p = cache$1.get(device);
|
|
460
|
+
if (!p) {
|
|
461
|
+
const module = device.createShaderModule({ code: sgd_default });
|
|
462
|
+
const storage = (binding, type) => ({
|
|
463
|
+
binding,
|
|
464
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
465
|
+
buffer: { type }
|
|
466
|
+
});
|
|
467
|
+
const bindLayout = device.createBindGroupLayout({ entries: [
|
|
468
|
+
storage(0, "storage"),
|
|
469
|
+
storage(1, "read-only-storage"),
|
|
470
|
+
storage(2, "read-only-storage"),
|
|
471
|
+
storage(3, "read-only-storage"),
|
|
472
|
+
storage(4, "storage"),
|
|
473
|
+
storage(6, "storage"),
|
|
474
|
+
storage(7, "uniform"),
|
|
475
|
+
storage(8, "read-only-storage"),
|
|
476
|
+
storage(9, "storage")
|
|
477
|
+
] });
|
|
478
|
+
const bindLayoutEdge = device.createBindGroupLayout({ entries: [
|
|
479
|
+
storage(1, "read-only-storage"),
|
|
480
|
+
storage(2, "read-only-storage"),
|
|
481
|
+
storage(3, "read-only-storage"),
|
|
482
|
+
storage(4, "storage"),
|
|
483
|
+
storage(6, "storage"),
|
|
484
|
+
storage(7, "uniform"),
|
|
485
|
+
storage(9, "storage"),
|
|
486
|
+
storage(10, "uniform"),
|
|
487
|
+
storage(11, "read-only-storage")
|
|
488
|
+
] });
|
|
489
|
+
const layout = device.createPipelineLayout({ bindGroupLayouts: [bindLayout] });
|
|
490
|
+
const layoutEdge = device.createPipelineLayout({ bindGroupLayouts: [bindLayoutEdge] });
|
|
491
|
+
const mk = (entryPoint, pl = layout) => device.createComputePipeline({
|
|
492
|
+
layout: pl,
|
|
493
|
+
compute: {
|
|
494
|
+
module,
|
|
495
|
+
entryPoint
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
p = {
|
|
499
|
+
vertexGather: mk("vertexGather"),
|
|
500
|
+
edgeAtomic: mk("edgeAtomic", layoutEdge),
|
|
501
|
+
packFx: mk("packFx"),
|
|
502
|
+
unpackFx: mk("unpackFx"),
|
|
503
|
+
bindLayout,
|
|
504
|
+
bindLayoutEdge
|
|
505
|
+
};
|
|
506
|
+
cache$1.set(device, p);
|
|
507
|
+
}
|
|
508
|
+
return p;
|
|
509
|
+
}
|
|
510
|
+
const UNIFORM_SIZE = 48;
|
|
511
|
+
async function gpuOptimizeLayout(ctx, embedding, dim, head, tail, nEpochs, nVertices, epochsPerSample, a, b, rng, opts) {
|
|
512
|
+
const device = ctx.device;
|
|
513
|
+
const strategy = opts.strategy ?? "edgeAtomic";
|
|
514
|
+
const nEdges = head.length;
|
|
515
|
+
if (strategy === "edgeAtomic" && dim > 8) throw new UmapBackendError(`webgpu layout supports dim <= 8 (got ${dim})`);
|
|
516
|
+
const pipes = pipelines(device);
|
|
517
|
+
assertUsable(ctx, "gpuOptimizeLayout");
|
|
518
|
+
const fxSizePlanned = strategy === "edgeAtomic" ? nVertices * dim * 4 : 4;
|
|
519
|
+
ensureBufferFits(ctx, "gpuOptimizeLayout", "embedding", embedding.byteLength);
|
|
520
|
+
ensureBufferFits(ctx, "gpuOptimizeLayout", "edges", nEdges * 4);
|
|
521
|
+
ensureBufferFits(ctx, "gpuOptimizeLayout", "schedule", nEdges * 2 * 4);
|
|
522
|
+
ensureBufferFits(ctx, "gpuOptimizeLayout", "rngStates", (strategy === "vertexGather" ? nVertices : nEdges) * 3 * 4);
|
|
523
|
+
ensureBufferFits(ctx, "gpuOptimizeLayout", "fixedPoint", fxSizePlanned);
|
|
524
|
+
const epsF32 = Float32Array.from(epochsPerSample);
|
|
525
|
+
const scheduleInit = new Float32Array(nEdges * 2);
|
|
526
|
+
scheduleInit.set(epsF32, 0);
|
|
527
|
+
for (let i = 0; i < nEdges; i++) scheduleInit[nEdges + i] = epsF32[i] / opts.negativeSampleRate;
|
|
528
|
+
const scratch = /* @__PURE__ */ new Set();
|
|
529
|
+
const track = (buf) => {
|
|
530
|
+
scratch.add(buf);
|
|
531
|
+
return buf;
|
|
532
|
+
};
|
|
533
|
+
const endScopes = beginErrorScopes(device);
|
|
534
|
+
try {
|
|
535
|
+
const embBuf = track(createBuffer(device, embedding, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC));
|
|
536
|
+
const headBuf = track(createBuffer(device, head, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST));
|
|
537
|
+
const tailBuf = track(createBuffer(device, tail, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST));
|
|
538
|
+
const epsBuf = track(createBuffer(device, epsF32, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST));
|
|
539
|
+
const scheduleBuf = track(createBuffer(device, scheduleInit, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST));
|
|
540
|
+
const nStreams = strategy === "vertexGather" ? nVertices : nEdges;
|
|
541
|
+
const states = new Uint32Array(nStreams * 3);
|
|
542
|
+
for (let i = 0; i < states.length; i++) states[i] = rng.nextU32();
|
|
543
|
+
const rngBuf = track(createBuffer(device, states, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST));
|
|
544
|
+
const rowPtr = new Uint32Array(nVertices + 1);
|
|
545
|
+
for (let e = 0; e < nEdges; e++) rowPtr[head[e] + 1]++;
|
|
546
|
+
for (let v = 0; v < nVertices; v++) rowPtr[v + 1] += rowPtr[v];
|
|
547
|
+
const rowPtrBuf = track(createBuffer(device, rowPtr, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST));
|
|
548
|
+
const scale = 1048576;
|
|
549
|
+
const fxSize = fxSizePlanned;
|
|
550
|
+
const fxBuf = track(device.createBuffer({
|
|
551
|
+
size: fxSize,
|
|
552
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
553
|
+
}));
|
|
554
|
+
const snapBuf = track(device.createBuffer({
|
|
555
|
+
size: fxSize,
|
|
556
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
557
|
+
}));
|
|
558
|
+
const uni = track(device.createBuffer({
|
|
559
|
+
size: UNIFORM_SIZE,
|
|
560
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
561
|
+
}));
|
|
562
|
+
const writeUniform = (epoch, alpha$1) => {
|
|
563
|
+
const buf = new ArrayBuffer(UNIFORM_SIZE);
|
|
564
|
+
const u = new Uint32Array(buf);
|
|
565
|
+
const f = new Float32Array(buf);
|
|
566
|
+
u[0] = nVertices;
|
|
567
|
+
u[1] = nEdges;
|
|
568
|
+
u[2] = dim;
|
|
569
|
+
u[3] = epoch;
|
|
570
|
+
f[4] = alpha$1;
|
|
571
|
+
f[5] = a;
|
|
572
|
+
f[6] = b;
|
|
573
|
+
f[7] = opts.gamma;
|
|
574
|
+
f[8] = opts.negativeSampleRate;
|
|
575
|
+
u[9] = opts.moveOther ? 1 : 0;
|
|
576
|
+
f[10] = scale;
|
|
577
|
+
device.queue.writeBuffer(uni, 0, buf);
|
|
578
|
+
};
|
|
579
|
+
const SUBBATCHES = strategy === "edgeAtomic" ? Math.min(32, Math.max(1, nEdges)) : 1;
|
|
580
|
+
const chunk = Math.ceil(nEdges / SUBBATCHES);
|
|
581
|
+
const slotStride = Math.max(256, device.limits.minUniformBufferOffsetAlignment);
|
|
582
|
+
const batchUni = track(device.createBuffer({
|
|
583
|
+
size: slotStride * SUBBATCHES,
|
|
584
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
585
|
+
}));
|
|
586
|
+
for (let s = 0; s < SUBBATCHES; s++) {
|
|
587
|
+
const off = s * chunk;
|
|
588
|
+
const cnt = Math.max(0, Math.min(chunk, nEdges - off));
|
|
589
|
+
device.queue.writeBuffer(batchUni, s * slotStride, new Uint32Array([off, cnt]));
|
|
590
|
+
}
|
|
591
|
+
const bindGroup = device.createBindGroup({
|
|
592
|
+
layout: pipes.bindLayout,
|
|
593
|
+
entries: [
|
|
594
|
+
{
|
|
595
|
+
binding: 0,
|
|
596
|
+
resource: { buffer: embBuf }
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
binding: 1,
|
|
600
|
+
resource: { buffer: headBuf }
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
binding: 2,
|
|
604
|
+
resource: { buffer: tailBuf }
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
binding: 3,
|
|
608
|
+
resource: { buffer: epsBuf }
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
binding: 4,
|
|
612
|
+
resource: { buffer: scheduleBuf }
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
binding: 6,
|
|
616
|
+
resource: { buffer: rngBuf }
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
binding: 7,
|
|
620
|
+
resource: { buffer: uni }
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
binding: 8,
|
|
624
|
+
resource: { buffer: rowPtrBuf }
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
binding: 9,
|
|
628
|
+
resource: { buffer: fxBuf }
|
|
629
|
+
}
|
|
630
|
+
]
|
|
631
|
+
});
|
|
632
|
+
const edgeBindGroups = [];
|
|
633
|
+
for (let s = 0; s < SUBBATCHES; s++) edgeBindGroups.push(device.createBindGroup({
|
|
634
|
+
layout: pipes.bindLayoutEdge,
|
|
635
|
+
entries: [
|
|
636
|
+
{
|
|
637
|
+
binding: 1,
|
|
638
|
+
resource: { buffer: headBuf }
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
binding: 2,
|
|
642
|
+
resource: { buffer: tailBuf }
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
binding: 3,
|
|
646
|
+
resource: { buffer: epsBuf }
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
binding: 4,
|
|
650
|
+
resource: { buffer: scheduleBuf }
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
binding: 6,
|
|
654
|
+
resource: { buffer: rngBuf }
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
binding: 7,
|
|
658
|
+
resource: { buffer: uni }
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
binding: 9,
|
|
662
|
+
resource: { buffer: fxBuf }
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
binding: 10,
|
|
666
|
+
resource: {
|
|
667
|
+
buffer: batchUni,
|
|
668
|
+
offset: s * slotStride,
|
|
669
|
+
size: 8
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
binding: 11,
|
|
674
|
+
resource: { buffer: snapBuf }
|
|
675
|
+
}
|
|
676
|
+
]
|
|
677
|
+
}));
|
|
678
|
+
const run = (pipeline, bg, threads) => {
|
|
679
|
+
const enc = device.createCommandEncoder();
|
|
680
|
+
const pass = enc.beginComputePass();
|
|
681
|
+
pass.setPipeline(pipeline);
|
|
682
|
+
pass.setBindGroup(0, bg);
|
|
683
|
+
pass.dispatchWorkgroups(Math.ceil(threads / 256));
|
|
684
|
+
pass.end();
|
|
685
|
+
device.queue.submit([enc.finish()]);
|
|
686
|
+
};
|
|
687
|
+
if (strategy === "edgeAtomic") {
|
|
688
|
+
writeUniform(0, opts.initialAlpha);
|
|
689
|
+
const enc = device.createCommandEncoder();
|
|
690
|
+
const pass = enc.beginComputePass();
|
|
691
|
+
pass.setPipeline(pipes.packFx);
|
|
692
|
+
pass.setBindGroup(0, bindGroup);
|
|
693
|
+
pass.dispatchWorkgroups(Math.ceil(nVertices * dim / 256));
|
|
694
|
+
pass.end();
|
|
695
|
+
enc.copyBufferToBuffer(fxBuf, 0, snapBuf, 0, fxSize);
|
|
696
|
+
device.queue.submit([enc.finish()]);
|
|
697
|
+
}
|
|
698
|
+
let alpha = opts.initialAlpha;
|
|
699
|
+
const SYNC_EVERY = 24;
|
|
700
|
+
for (let epoch = 0; epoch < nEpochs; epoch++) {
|
|
701
|
+
throwIfAborted(opts.signal);
|
|
702
|
+
assertUsable(ctx, "gpuOptimizeLayout");
|
|
703
|
+
writeUniform(epoch, alpha);
|
|
704
|
+
if (strategy === "vertexGather") run(pipes.vertexGather, bindGroup, nVertices);
|
|
705
|
+
else {
|
|
706
|
+
const enc = device.createCommandEncoder();
|
|
707
|
+
for (let s = 0; s < SUBBATCHES; s++) {
|
|
708
|
+
const off = s * chunk;
|
|
709
|
+
const cnt = Math.min(chunk, nEdges - off);
|
|
710
|
+
if (cnt <= 0) break;
|
|
711
|
+
const pass = enc.beginComputePass();
|
|
712
|
+
pass.setPipeline(pipes.edgeAtomic);
|
|
713
|
+
pass.setBindGroup(0, edgeBindGroups[s]);
|
|
714
|
+
pass.dispatchWorkgroups(Math.ceil(cnt / 256));
|
|
715
|
+
pass.end();
|
|
716
|
+
enc.copyBufferToBuffer(fxBuf, 0, snapBuf, 0, fxSize);
|
|
717
|
+
}
|
|
718
|
+
device.queue.submit([enc.finish()]);
|
|
719
|
+
}
|
|
720
|
+
if (epoch % SYNC_EVERY === SYNC_EVERY - 1 || epoch === nEpochs - 1) await device.queue.onSubmittedWorkDone();
|
|
721
|
+
alpha = opts.initialAlpha * (1 - epoch / nEpochs);
|
|
722
|
+
if (opts.onEpoch) {
|
|
723
|
+
const info = {
|
|
724
|
+
epoch,
|
|
725
|
+
nEpochs,
|
|
726
|
+
phase: "layout"
|
|
727
|
+
};
|
|
728
|
+
if (opts.snapshotEvery && (epoch % opts.snapshotEvery === 0 || epoch === nEpochs - 1)) {
|
|
729
|
+
if (strategy === "edgeAtomic") run(pipes.unpackFx, bindGroup, nVertices * dim);
|
|
730
|
+
info.embedding = new Float32Array(await readBuffer(device, embBuf, embedding.length * 4));
|
|
731
|
+
}
|
|
732
|
+
opts.onEpoch(info);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (strategy === "edgeAtomic") {
|
|
736
|
+
writeUniform(nEpochs, alpha);
|
|
737
|
+
run(pipes.unpackFx, bindGroup, nVertices * dim);
|
|
738
|
+
}
|
|
739
|
+
await device.queue.onSubmittedWorkDone();
|
|
740
|
+
await endScopes("gpuOptimizeLayout");
|
|
741
|
+
assertUsable(ctx, "gpuOptimizeLayout");
|
|
742
|
+
const out = new Float32Array(await readBuffer(device, embBuf, embedding.length * 4));
|
|
743
|
+
embedding.set(out);
|
|
744
|
+
return embedding;
|
|
745
|
+
} finally {
|
|
746
|
+
await endScopes("gpuOptimizeLayout").catch(() => {});
|
|
747
|
+
for (const b$1 of scratch) b$1.destroy();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
//#endregion
|
|
752
|
+
//#region src/backends/webgpu/kernels/spmv.wgsl
|
|
753
|
+
var spmv_default = "// Blocked normalized-Laplacian SpMV for the spectral init (D-014):\n// Y[c*n+i] = X[c*n+i] − dinvsqrt[i] · Σ_p scaled[p]·X[c*n+indices[p]]\n// One thread per row; each thread walks its CSR row once and accumulates all\n// block columns. Reduction order per (row, column) is fixed → deterministic.\n\nstruct SpmvParams {\n n: u32,\n cols: u32,\n}\n\n@group(0) @binding(0) var<storage, read> indptr: array<u32>;\n@group(0) @binding(1) var<storage, read> indices: array<u32>;\n@group(0) @binding(2) var<storage, read> scaled: array<f32>;\n@group(0) @binding(3) var<storage, read> dinv: array<f32>;\n@group(0) @binding(4) var<storage, read> X: array<f32>;\n@group(0) @binding(5) var<storage, read_write> Y: array<f32>;\n@group(0) @binding(6) var<uniform> P: SpmvParams;\n\n@compute @workgroup_size(256)\nfn spmv(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= P.n) { return; }\n let n = P.n;\n let start = indptr[i];\n let end = indptr[i + 1u];\n var acc: array<f32, 18>; // up to 3·(dim+1) block columns, dim ≤ 5 on this path\n for (var c = 0u; c < P.cols; c = c + 1u) { acc[c] = 0.0; }\n for (var p = start; p < end; p = p + 1u) {\n let v = scaled[p];\n let j = indices[p];\n for (var c = 0u; c < P.cols; c = c + 1u) {\n acc[c] = acc[c] + v * X[c * n + j];\n }\n }\n let di = dinv[i];\n for (var c = 0u; c < P.cols; c = c + 1u) {\n Y[c * n + i] = X[c * n + i] - di * acc[c];\n }\n}\n";
|
|
754
|
+
|
|
755
|
+
//#endregion
|
|
756
|
+
//#region src/backends/webgpu/spectral.ts
|
|
757
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
758
|
+
function spmvPipeline(device) {
|
|
759
|
+
let p = cache.get(device);
|
|
760
|
+
if (!p) {
|
|
761
|
+
const module = device.createShaderModule({ code: spmv_default });
|
|
762
|
+
const entry = (binding, type) => ({
|
|
763
|
+
binding,
|
|
764
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
765
|
+
buffer: { type }
|
|
766
|
+
});
|
|
767
|
+
const bindLayout = device.createBindGroupLayout({ entries: [
|
|
768
|
+
entry(0, "read-only-storage"),
|
|
769
|
+
entry(1, "read-only-storage"),
|
|
770
|
+
entry(2, "read-only-storage"),
|
|
771
|
+
entry(3, "read-only-storage"),
|
|
772
|
+
entry(4, "read-only-storage"),
|
|
773
|
+
entry(5, "storage"),
|
|
774
|
+
entry(6, "uniform")
|
|
775
|
+
] });
|
|
776
|
+
const layout = device.createPipelineLayout({ bindGroupLayouts: [bindLayout] });
|
|
777
|
+
p = {
|
|
778
|
+
pipeline: device.createComputePipeline({
|
|
779
|
+
layout,
|
|
780
|
+
compute: {
|
|
781
|
+
module,
|
|
782
|
+
entryPoint: "spmv"
|
|
783
|
+
}
|
|
784
|
+
}),
|
|
785
|
+
bindLayout
|
|
786
|
+
};
|
|
787
|
+
cache.set(device, p);
|
|
788
|
+
}
|
|
789
|
+
return p;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Spectral init with GPU SpMV. Returns null for multi-component graphs (the
|
|
793
|
+
* caller must then run the CPU init path; no rng draws are consumed here in
|
|
794
|
+
* that case) and for dim > 5 (kernel block bound).
|
|
795
|
+
*/
|
|
796
|
+
async function gpuSpectralInit(ctx, graph, dim, rng, logger = silentLogger, signal) {
|
|
797
|
+
if (dim > 5) return null;
|
|
798
|
+
const { count } = connectedComponents(graph);
|
|
799
|
+
if (count > 1) return null;
|
|
800
|
+
const device = ctx.device;
|
|
801
|
+
const n = graph.rows;
|
|
802
|
+
const m = dim + 1;
|
|
803
|
+
const maxCols = 3 * m;
|
|
804
|
+
assertUsable(ctx, "gpuSpectralInit");
|
|
805
|
+
ensureBufferFits(ctx, "gpuSpectralInit", "graph", graph.data.length * 4);
|
|
806
|
+
ensureBufferFits(ctx, "gpuSpectralInit", "block", n * maxCols * 4);
|
|
807
|
+
const { deg, dinvsqrt } = degreeInvSqrt(graph);
|
|
808
|
+
const X0 = new Float64Array(n * m);
|
|
809
|
+
for (let c = 0; c < m; c++) for (let r = 0; r < n; r++) X0[c * n + r] = rng.normal();
|
|
810
|
+
let norm = 0;
|
|
811
|
+
for (let r = 0; r < n; r++) norm += deg[r];
|
|
812
|
+
norm = Math.sqrt(norm);
|
|
813
|
+
for (let r = 0; r < n; r++) X0[r] = Math.sqrt(deg[r]) / norm;
|
|
814
|
+
const nnz = graph.data.length;
|
|
815
|
+
const scaled = new Float32Array(nnz);
|
|
816
|
+
for (let i = 0; i < n; i++) for (let p = graph.indptr[i]; p < graph.indptr[i + 1]; p++) scaled[p] = Math.fround(graph.data[p] * dinvsqrt[graph.indices[p]]);
|
|
817
|
+
const indptrU32 = new Uint32Array(n + 1);
|
|
818
|
+
for (let i = 0; i <= n; i++) indptrU32[i] = graph.indptr[i];
|
|
819
|
+
const indicesU32 = new Uint32Array(nnz);
|
|
820
|
+
for (let p = 0; p < nnz; p++) indicesU32[p] = graph.indices[p];
|
|
821
|
+
const dinvF32 = Float32Array.from(dinvsqrt);
|
|
822
|
+
const { pipeline, bindLayout } = spmvPipeline(device);
|
|
823
|
+
const ro = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
|
|
824
|
+
const scratch = /* @__PURE__ */ new Set();
|
|
825
|
+
const track = (buf) => {
|
|
826
|
+
scratch.add(buf);
|
|
827
|
+
return buf;
|
|
828
|
+
};
|
|
829
|
+
const endScopes = beginErrorScopes(device);
|
|
830
|
+
try {
|
|
831
|
+
const indptrBuf = track(createBuffer(device, indptrU32, ro));
|
|
832
|
+
const indicesBuf = track(createBuffer(device, indicesU32, ro));
|
|
833
|
+
const scaledBuf = track(createBuffer(device, scaled, ro));
|
|
834
|
+
const dinvBuf = track(createBuffer(device, dinvF32, ro));
|
|
835
|
+
const xBuf = track(device.createBuffer({
|
|
836
|
+
size: n * maxCols * 4,
|
|
837
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
838
|
+
}));
|
|
839
|
+
const yBuf = track(device.createBuffer({
|
|
840
|
+
size: n * maxCols * 4,
|
|
841
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
842
|
+
}));
|
|
843
|
+
const uni = track(device.createBuffer({
|
|
844
|
+
size: 8,
|
|
845
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
846
|
+
}));
|
|
847
|
+
const bindGroup = device.createBindGroup({
|
|
848
|
+
layout: bindLayout,
|
|
849
|
+
entries: [
|
|
850
|
+
{
|
|
851
|
+
binding: 0,
|
|
852
|
+
resource: { buffer: indptrBuf }
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
binding: 1,
|
|
856
|
+
resource: { buffer: indicesBuf }
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
binding: 2,
|
|
860
|
+
resource: { buffer: scaledBuf }
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
binding: 3,
|
|
864
|
+
resource: { buffer: dinvBuf }
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
binding: 4,
|
|
868
|
+
resource: { buffer: xBuf }
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
binding: 5,
|
|
872
|
+
resource: { buffer: yBuf }
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
binding: 6,
|
|
876
|
+
resource: { buffer: uni }
|
|
877
|
+
}
|
|
878
|
+
]
|
|
879
|
+
});
|
|
880
|
+
const xStage = new Float32Array(n * maxCols);
|
|
881
|
+
const applyL = async (S, cols, out) => {
|
|
882
|
+
throwIfAborted(signal);
|
|
883
|
+
assertUsable(ctx, "gpuSpectralInit");
|
|
884
|
+
const len = n * cols;
|
|
885
|
+
for (let i = 0; i < len; i++) xStage[i] = Math.fround(S[i]);
|
|
886
|
+
device.queue.writeBuffer(uni, 0, new Uint32Array([n, cols]));
|
|
887
|
+
device.queue.writeBuffer(xBuf, 0, xStage.buffer, 0, len * 4);
|
|
888
|
+
const enc = device.createCommandEncoder();
|
|
889
|
+
const pass = enc.beginComputePass();
|
|
890
|
+
pass.setPipeline(pipeline);
|
|
891
|
+
pass.setBindGroup(0, bindGroup);
|
|
892
|
+
pass.dispatchWorkgroups(Math.ceil(n / 256));
|
|
893
|
+
pass.end();
|
|
894
|
+
device.queue.submit([enc.finish()]);
|
|
895
|
+
const y = new Float32Array(await readBuffer(device, yBuf, len * 4));
|
|
896
|
+
for (let i = 0; i < len; i++) out[i] = y[i];
|
|
897
|
+
return out;
|
|
898
|
+
};
|
|
899
|
+
const res = await lobpcgSmallestAsync(graph, dinvsqrt, X0, m, 1e-4, 5 * Math.min(n, 60), applyL);
|
|
900
|
+
await endScopes("gpuSpectralInit");
|
|
901
|
+
if (!res.converged) {
|
|
902
|
+
logger.warn("UmapConvergenceWarning: Spectral initialisation failed! The eigenvector solver failed. This is likely due to too small an eigengap. Consider adding some noise or jitter to your data. Falling back to random initialisation!");
|
|
903
|
+
const emb$1 = new Float64Array(n * dim);
|
|
904
|
+
for (let i = 0; i < emb$1.length; i++) emb$1[i] = rng.uniform(-10, 10);
|
|
905
|
+
return {
|
|
906
|
+
embedding: finalInitRescale(emb$1, n, dim),
|
|
907
|
+
spectralFallback: true
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
const emb = new Float64Array(n * dim);
|
|
911
|
+
for (let d = 0; d < dim; d++) {
|
|
912
|
+
const c = d + 1;
|
|
913
|
+
for (let r = 0; r < n; r++) emb[r * dim + d] = res.vectors[c * n + r];
|
|
914
|
+
}
|
|
915
|
+
noisyScaleCoords(emb, rng, 10, 1e-4);
|
|
916
|
+
return {
|
|
917
|
+
embedding: finalInitRescale(emb, n, dim),
|
|
918
|
+
spectralFallback: false
|
|
919
|
+
};
|
|
920
|
+
} finally {
|
|
921
|
+
await endScopes("gpuSpectralInit").catch(() => {});
|
|
922
|
+
for (const b of scratch) b.destroy();
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/backends/webgpu/index.ts
|
|
928
|
+
/** Which fit configurations the GPU backend supports end-to-end (§4.3). */
|
|
929
|
+
function gpuSupportsConfig(opts) {
|
|
930
|
+
const metric = opts.metric ?? "euclidean";
|
|
931
|
+
const knnOk = typeof metric === "string" && [
|
|
932
|
+
"euclidean",
|
|
933
|
+
"l2",
|
|
934
|
+
"sqeuclidean",
|
|
935
|
+
"cosine",
|
|
936
|
+
"correlation",
|
|
937
|
+
"manhattan",
|
|
938
|
+
"taxicab",
|
|
939
|
+
"l1",
|
|
940
|
+
"hamming",
|
|
941
|
+
"jaccard"
|
|
942
|
+
].includes(metric) && (opts.nNeighbors ?? 15) <= 32;
|
|
943
|
+
const outputMetric = opts.outputMetric ?? "euclidean";
|
|
944
|
+
const layoutOk = (outputMetric === "euclidean" || outputMetric === "l2") && !opts.densMap && (opts.nComponents ?? 2) <= 8;
|
|
945
|
+
const reason = !knnOk ? `metric '${String(metric)}' runs on CPU (GPU subset: euclidean, sqeuclidean, cosine, correlation, manhattan, hamming, jaccard; k<=32)` : !layoutOk ? "non-euclidean output / densMAP / dim>8 layout runs on CPU" : void 0;
|
|
946
|
+
return {
|
|
947
|
+
knn: knnOk,
|
|
948
|
+
layout: layoutOk,
|
|
949
|
+
...reason ? { reason } : {}
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
//#endregion
|
|
954
|
+
export { acquireGpu, gpuKnn, gpuMetricPlan, gpuOptimizeLayout, gpuSpectralInit, gpuSupportsConfig };
|
|
955
|
+
//# sourceMappingURL=webgpu.js.map
|