@holoscript/core 8.0.14 → 8.0.16
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/README.md +6 -13
- package/bin/holo-decision.mjs +0 -0
- package/bin/holoscript.cjs +0 -0
- package/dist/{GLTFPipeline-6LS3UDYX.js → GLTFPipeline-FRBZIATS.js} +6 -6
- package/dist/{GLTFPipeline-VTI6PRRQ.cjs → GLTFPipeline-MTPP444P.cjs} +10 -10
- package/dist/{HoloCompositionParser-GILABNN3.js → HoloCompositionParser-2GVQAZ2X.js} +7 -7
- package/dist/HoloCompositionParser-3NDF6BMX.cjs +45 -0
- package/dist/HoloScriptPlusParser-URSIBEAW.cjs +30 -0
- package/dist/HoloScriptPlusParser-YCV6L7GI.js +13 -0
- package/dist/{HoloScriptRuntime-YBYXJHJN.cjs → HoloScriptRuntime-CRWI6C3P.cjs} +5 -5
- package/dist/{HoloScriptRuntime-UTRLJI5L.js → HoloScriptRuntime-IAZGHPFP.js} +4 -4
- package/dist/{SceneIRCompiler-VX75RB6H.js → SceneIRCompiler-BJQMKLWL.js} +6 -6
- package/dist/{SceneIRCompiler-7KQ5NKAU.cjs → SceneIRCompiler-RTPPDQAV.cjs} +11 -11
- package/dist/{chunk-Y2OCKXVD.js → chunk-22AULOAL.js} +5 -5
- package/dist/{chunk-D44CEFY4.cjs → chunk-2DJIS7TH.cjs} +5 -5
- package/dist/{chunk-LHUDAJQT.cjs → chunk-2PVEXEOL.cjs} +5 -5
- package/dist/{chunk-M7XR3F53.js → chunk-2UP2J3OK.js} +4 -4
- package/dist/{chunk-3KHRZTNW.js → chunk-3AUYBQ5I.js} +3 -3
- package/dist/{chunk-UKEK2NZJ.js → chunk-3MQAOMKD.js} +3 -3
- package/dist/{chunk-FWKEY4LD.js → chunk-4AGNARX6.js} +3 -3
- package/dist/{chunk-7ITJP33Q.cjs → chunk-4CQ7ULXZ.cjs} +3 -3
- package/dist/{chunk-47XZ62KM.js → chunk-4UA5OBWR.js} +5 -5
- package/dist/{chunk-ORBVCOTC.js → chunk-5LCJDX5A.js} +3 -3
- package/dist/{chunk-IK6Z2HNC.js → chunk-6OQHLNTT.js} +2 -2
- package/dist/{chunk-EI3J6LTW.cjs → chunk-6RDHWSZM.cjs} +217 -217
- package/dist/{chunk-73EDVY5D.js → chunk-7AY66PZP.js} +3 -3
- package/dist/{chunk-6FCNMTL5.js → chunk-7HDRN5BW.js} +4 -4
- package/dist/{chunk-N7MIIDLI.cjs → chunk-7IBXTJP7.cjs} +8 -8
- package/dist/{chunk-R5VKLUMR.js → chunk-AAUQTR7I.js} +4 -4
- package/dist/{chunk-HCONR4J6.cjs → chunk-AN3ZH7M7.cjs} +38 -38
- package/dist/{chunk-YW3I2FQL.js → chunk-ANMME7XT.js} +4 -2
- package/dist/{chunk-W4AJMMQR.js → chunk-AUF7HFOO.js} +60 -22
- package/dist/{chunk-SKVBUGMC.cjs → chunk-AUMTQV64.cjs} +7 -7
- package/dist/{chunk-7ZKQ32XK.js → chunk-B5K3WHJ4.js} +4 -4
- package/dist/{chunk-QQFB4RT2.cjs → chunk-BTVHY3UB.cjs} +2 -2
- package/dist/{chunk-SE2Y5BSL.cjs → chunk-CMEINO5I.cjs} +186 -70
- package/dist/{chunk-YMI3ED3B.cjs → chunk-CRUIGXF7.cjs} +5 -5
- package/dist/chunk-DDBYJJZK.js +1571 -0
- package/dist/{chunk-2E6PP5UE.cjs → chunk-E2SNPMBO.cjs} +11 -11
- package/dist/{chunk-GI5YYZH6.js → chunk-E4YS5SIZ.js} +90 -25
- package/dist/{chunk-62OYZT6Q.js → chunk-EKNWUSGW.js} +3 -3
- package/dist/{chunk-RVN7OPCP.js → chunk-ETIJOUIO.js} +4 -4
- package/dist/chunk-EUNELE7Y.cjs +1605 -0
- package/dist/{chunk-C73GVAYC.js → chunk-F33KLYOS.js} +5 -5
- package/dist/{chunk-4CRMVEON.cjs → chunk-F47FEV4X.cjs} +40 -40
- package/dist/{chunk-FGXJPJ3W.cjs → chunk-FCGTI7FO.cjs} +4 -4
- package/dist/{chunk-OSKGCXAQ.cjs → chunk-FGTXAVKG.cjs} +5 -5
- package/dist/{chunk-XZC75AJ4.cjs → chunk-FPGYWNNA.cjs} +5 -5
- package/dist/{chunk-5B2AJKA5.cjs → chunk-FQ4MYZYU.cjs} +20 -20
- package/dist/{chunk-KOGPVAP6.js → chunk-FT4FGTR6.js} +3 -3
- package/dist/{chunk-N7IYRAFG.cjs → chunk-FVNV4WHQ.cjs} +14 -14
- package/dist/{chunk-SJIRCTIS.cjs → chunk-HSGJFCVJ.cjs} +2 -2
- package/dist/{chunk-TKDDIE3T.cjs → chunk-ILO2KMER.cjs} +4 -4
- package/dist/{chunk-GGH3ZYDB.cjs → chunk-IPBSNGAN.cjs} +92 -27
- package/dist/{chunk-JQCXLKAU.js → chunk-IX2U3RQT.js} +3 -3
- package/dist/{chunk-P65OIMJT.cjs → chunk-IYK6LMQ7.cjs} +7 -7
- package/dist/{chunk-NWKARJM2.js → chunk-J3VVM6GV.js} +4 -4
- package/dist/{chunk-FOM36K5I.cjs → chunk-J74S4ZMF.cjs} +18 -18
- package/dist/{chunk-36NPLOI4.cjs → chunk-JERWCCEJ.cjs} +4 -2
- package/dist/{chunk-VXHB6ZHY.js → chunk-JTDDLCSP.js} +4 -4
- package/dist/{chunk-GYJLP4BO.cjs → chunk-K53L47NH.cjs} +30 -30
- package/dist/{chunk-FHR2AVMA.cjs → chunk-LJTM5ONX.cjs} +23 -23
- package/dist/{chunk-JCVSCKUH.js → chunk-LJVVOVA6.js} +4 -4
- package/dist/{chunk-LQNL3IYV.cjs → chunk-LNJKBQ2B.cjs} +38 -38
- package/dist/{chunk-2GKF7JQ6.js → chunk-M2GDE3NV.js} +3 -3
- package/dist/{chunk-Z7NUL73U.js → chunk-MSZNGI5P.js} +3 -3
- package/dist/{chunk-B6JHTTMR.cjs → chunk-N2BOTBPG.cjs} +15 -15
- package/dist/{chunk-L4DBUX3N.cjs → chunk-NFALJJCH.cjs} +12 -12
- package/dist/{chunk-YBM34FUJ.cjs → chunk-NRZRBK25.cjs} +6 -6
- package/dist/{chunk-SNMQUWIO.cjs → chunk-NTSLNIY7.cjs} +10 -10
- package/dist/{chunk-645EWDZW.cjs → chunk-NZJOAWJM.cjs} +4 -4
- package/dist/{chunk-LGRKBTPM.js → chunk-OG7RKWWX.js} +3 -3
- package/dist/{chunk-OOP4S3GU.cjs → chunk-OQVHCYY4.cjs} +5 -5
- package/dist/{chunk-TMNAKNV2.cjs → chunk-PEBNX3VM.cjs} +5 -5
- package/dist/{chunk-5T6BWIB7.js → chunk-PFZ5WLIP.js} +4 -4
- package/dist/{chunk-CESMPUEZ.cjs → chunk-PHLNABJD.cjs} +159 -145
- package/dist/{chunk-CHIZSMWD.js → chunk-PLK3DLTF.js} +3 -3
- package/dist/{chunk-4NEPHK2K.js → chunk-PMACVMHL.js} +3 -3
- package/dist/{chunk-EYHPTJ3F.js → chunk-PV2OXNP5.js} +40 -26
- package/dist/{chunk-KLM6QD6D.cjs → chunk-Q6VW7MGX.cjs} +14 -14
- package/dist/{chunk-YDSNDCV3.js → chunk-QC3CAVFN.js} +186 -69
- package/dist/{chunk-REN3LHO6.cjs → chunk-QKLEDRHN.cjs} +19 -19
- package/dist/{chunk-2EGGAZT3.js → chunk-QS27OYBU.js} +3 -3
- package/dist/{chunk-4376VHNJ.js → chunk-RUB7WL6C.js} +3 -3
- package/dist/chunk-S2BSXG3B.js +3308 -0
- package/dist/{chunk-TZQKJOPC.js → chunk-TW2AH2ZS.js} +3 -3
- package/dist/{chunk-Q4J2K7FI.js → chunk-TZM6DKSJ.js} +6 -6
- package/dist/{chunk-ESSEF6U2.js → chunk-U227RWXB.js} +3 -3
- package/dist/{chunk-ERYKKPHW.cjs → chunk-U6CQOM5Q.cjs} +13 -13
- package/dist/{chunk-VMCR5QVI.js → chunk-ULT2QAV5.js} +4 -4
- package/dist/chunk-VQOTDNFL.cjs +3337 -0
- package/dist/{chunk-VZI7S7P6.cjs → chunk-VSO6HPDZ.cjs} +7 -7
- package/dist/{chunk-QLPRI3QW.js → chunk-WQJMSSJT.js} +4 -4
- package/dist/{chunk-GILYQ5VH.cjs → chunk-WXSDBWGM.cjs} +4 -4
- package/dist/{chunk-42Z3SVHQ.js → chunk-X5PMMK7H.js} +3 -3
- package/dist/{chunk-ML4V5PVU.cjs → chunk-X5ZLSPU2.cjs} +74 -32
- package/dist/{chunk-PSWCJQS4.js → chunk-X727CQ7X.js} +5 -5
- package/dist/{chunk-YM7UUYR5.cjs → chunk-XHZYS4QO.cjs} +5 -5
- package/dist/{chunk-PZ46WRHX.cjs → chunk-XRQCTTDC.cjs} +8 -8
- package/dist/{chunk-IXFNA5TT.js → chunk-YBIHSN55.js} +4 -4
- package/dist/{chunk-BLCWQ225.js → chunk-Z574GVI3.js} +4 -4
- package/dist/{chunk-ZVQMKUGJ.js → chunk-Z5V3KA7L.js} +2 -2
- package/dist/{chunk-VTFLKSJL.cjs → chunk-ZHW6TDZ6.cjs} +14 -14
- package/dist/{chunk-KYJD2UMV.js → chunk-ZPHCTPR6.js} +4 -4
- package/dist/{chunk-VA7THINF.cjs → chunk-ZXXC2GXB.cjs} +38 -38
- package/dist/cli/holoscript-runner.cjs +168 -19
- package/dist/cli/holoscript-runner.js +162 -13
- package/dist/compiler/agent-inference.cjs +7 -7
- package/dist/compiler/agent-inference.js +3 -3
- package/dist/compiler/android-xr.cjs +6 -6
- package/dist/compiler/android-xr.js +4 -4
- package/dist/compiler/android.cjs +5 -5
- package/dist/compiler/android.js +3 -3
- package/dist/compiler/context.cjs +5 -5
- package/dist/compiler/context.js +2 -2
- package/dist/compiler/daimon-seed.cjs +8 -8
- package/dist/compiler/daimon-seed.js +3 -3
- package/dist/compiler/domain-block-utils.cjs +164 -164
- package/dist/compiler/domain-block-utils.js +3 -3
- package/dist/compiler/dtdl.cjs +6 -6
- package/dist/compiler/dtdl.js +3 -3
- package/dist/compiler/gltf-pipeline.cjs +8 -8
- package/dist/compiler/gltf-pipeline.js +1 -1
- package/dist/compiler/godot.cjs +5 -5
- package/dist/compiler/godot.js +4 -4
- package/dist/compiler/incremental.cjs +7 -7
- package/dist/compiler/incremental.js +2 -2
- package/dist/compiler/index.cjs +1603 -204
- package/dist/compiler/index.d.ts +8 -0
- package/dist/compiler/index.js +1431 -55
- package/dist/compiler/ios.cjs +5 -5
- package/dist/compiler/ios.js +3 -3
- package/dist/compiler/llm-provider-capabilities.cjs +5 -5
- package/dist/compiler/llm-provider-capabilities.js +2 -2
- package/dist/compiler/openxr.cjs +5 -5
- package/dist/compiler/openxr.js +4 -4
- package/dist/compiler/sdf.cjs +6 -6
- package/dist/compiler/sdf.js +4 -4
- package/dist/compiler/state.cjs +5 -5
- package/dist/compiler/state.js +3 -3
- package/dist/compiler/trait-composition.cjs +6 -6
- package/dist/compiler/trait-composition.js +2 -2
- package/dist/compiler/unity.cjs +5 -5
- package/dist/compiler/unity.js +4 -4
- package/dist/compiler/unreal.cjs +6 -6
- package/dist/compiler/unreal.js +4 -4
- package/dist/compiler/urdf.cjs +13 -13
- package/dist/compiler/urdf.js +4 -4
- package/dist/compiler/usd-physics.cjs +6 -6
- package/dist/compiler/usd-physics.js +3 -3
- package/dist/compiler/visionos.cjs +5 -5
- package/dist/compiler/visionos.js +4 -4
- package/dist/compiler/vrchat.cjs +6 -6
- package/dist/compiler/vrchat.js +4 -4
- package/dist/compiler/wasm.cjs +9 -9
- package/dist/compiler/wasm.js +4 -4
- package/dist/compiler/webgpu.cjs +5 -5
- package/dist/compiler/webgpu.js +4 -4
- package/dist/constants.cjs +99 -99
- package/dist/constants.js +3 -3
- package/dist/debugger.cjs +5 -5
- package/dist/debugger.js +3 -3
- package/dist/{dist-GRMOP2UL.cjs → dist-YGATNURH.cjs} +705 -9
- package/dist/{dist-ZBUKU2JG.js → dist-ZEZXRCBG.js} +701 -10
- package/dist/entries/scripting.cjs +8 -8
- package/dist/entries/scripting.js +4 -4
- package/dist/evolution/index.cjs +20 -20
- package/dist/evolution/index.js +8 -8
- package/dist/index.cjs +871 -809
- package/dist/index.d.ts +8 -0
- package/dist/index.js +132 -70
- package/dist/parser/HoloCompositionTypes.d.ts +1 -1
- package/dist/parser.cjs +21 -21
- package/dist/parser.js +7 -7
- package/dist/reconstruction/index.cjs +63 -63
- package/dist/reconstruction/index.js +7 -7
- package/dist/runtime.cjs +9 -9
- package/dist/runtime.js +3 -3
- package/dist/traits/botanical-lotus.d.ts +61 -0
- package/dist/traits/index.cjs +664 -671
- package/dist/traits/index.d.ts +65 -0
- package/dist/traits/index.js +14 -21
- package/dist/types/HoloScriptPlus.d.ts +1 -1
- package/package.json +65 -63
- package/dist/HoloCompositionParser-ZVRYVEA5.cjs +0 -45
- package/dist/HoloScriptPlusParser-WTPRPRPR.cjs +0 -30
- package/dist/HoloScriptPlusParser-ZP53ECSL.js +0 -13
|
@@ -0,0 +1,3337 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkLM2VU56C_cjs = require('./chunk-LM2VU56C.cjs');
|
|
4
|
+
var chunkN2ILK7BF_cjs = require('./chunk-N2ILK7BF.cjs');
|
|
5
|
+
|
|
6
|
+
// src/version.ts
|
|
7
|
+
var HOLOSCRIPT_VERSION = "8.0.16" ;
|
|
8
|
+
var GIT_COMMIT_SHA = "c55048b35" ;
|
|
9
|
+
var BUILD_TIMESTAMP = "2026-07-17T05:25:25.840Z" ;
|
|
10
|
+
function getVersionString() {
|
|
11
|
+
const sha = `+${GIT_COMMIT_SHA}` ;
|
|
12
|
+
return `${HOLOSCRIPT_VERSION}${sha}`;
|
|
13
|
+
}
|
|
14
|
+
function getVersionInfo() {
|
|
15
|
+
return {
|
|
16
|
+
version: HOLOSCRIPT_VERSION,
|
|
17
|
+
gitCommitSha: GIT_COMMIT_SHA,
|
|
18
|
+
buildTimestamp: BUILD_TIMESTAMP,
|
|
19
|
+
versionString: getVersionString()
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/reconstruction/imagePatchEmbedKernel.ts
|
|
24
|
+
var WGSL_IMAGE_PATCH_EMBED = `
|
|
25
|
+
struct Params {
|
|
26
|
+
imgH: u32,
|
|
27
|
+
imgW: u32,
|
|
28
|
+
patchH: u32,
|
|
29
|
+
patchW: u32,
|
|
30
|
+
numChannels:u32,
|
|
31
|
+
embedDim: u32,
|
|
32
|
+
patchLen: u32, // = patchH * patchW * numChannels
|
|
33
|
+
numPatchesX:u32, // = imgW / patchW
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@group(0) @binding(0) var<storage, read> img: array<f32>; // [imgH, imgW, numChannels]
|
|
37
|
+
@group(0) @binding(1) var<storage, read> proj: array<f32>; // [embedDim, patchLen]
|
|
38
|
+
@group(0) @binding(2) var<storage, read_write> out: array<f32>; // [numPatches, embedDim]
|
|
39
|
+
@group(0) @binding(3) var<uniform> p: Params;
|
|
40
|
+
|
|
41
|
+
@compute @workgroup_size(64, 1, 1)
|
|
42
|
+
fn main(
|
|
43
|
+
@builtin(workgroup_id) wg: vec3<u32>,
|
|
44
|
+
@builtin(local_invocation_id) lid: vec3<u32>,
|
|
45
|
+
) {
|
|
46
|
+
let patchIdx = wg.x;
|
|
47
|
+
let patchRow = patchIdx / p.numPatchesX;
|
|
48
|
+
let patchCol = patchIdx % p.numPatchesX;
|
|
49
|
+
|
|
50
|
+
// Top-left corner of patch in the image
|
|
51
|
+
let startRow = patchRow * p.patchH;
|
|
52
|
+
let startCol = patchCol * p.patchW;
|
|
53
|
+
|
|
54
|
+
// Fan out over embedDim
|
|
55
|
+
var d = lid.x;
|
|
56
|
+
while (d < p.embedDim) {
|
|
57
|
+
let projRow = d * p.patchLen;
|
|
58
|
+
var dot: f32 = 0.0;
|
|
59
|
+
|
|
60
|
+
for (var pr = 0u; pr < p.patchH; pr++) {
|
|
61
|
+
for (var pc2 = 0u; pc2 < p.patchW; pc2++) {
|
|
62
|
+
for (var ch = 0u; ch < p.numChannels; ch++) {
|
|
63
|
+
let imgRow = startRow + pr;
|
|
64
|
+
let imgCol = startCol + pc2;
|
|
65
|
+
let imgIdx = (imgRow * p.imgW + imgCol) * p.numChannels + ch;
|
|
66
|
+
let projIdx = projRow + (pr * p.patchW + pc2) * p.numChannels + ch;
|
|
67
|
+
dot = dot + img[imgIdx] * proj[projIdx];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
out[patchIdx * p.embedDim + d] = dot;
|
|
72
|
+
d = d + 64u;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
function storageBuffer(device, data) {
|
|
77
|
+
const buf = device.createBuffer({
|
|
78
|
+
size: data.byteLength,
|
|
79
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
80
|
+
});
|
|
81
|
+
device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
|
|
82
|
+
return buf;
|
|
83
|
+
}
|
|
84
|
+
function createImagePatchEmbedKernel(device) {
|
|
85
|
+
const shader = device.createShaderModule({ code: WGSL_IMAGE_PATCH_EMBED });
|
|
86
|
+
const pipeline = device.createComputePipeline({
|
|
87
|
+
layout: "auto",
|
|
88
|
+
compute: { module: shader, entryPoint: "main" }
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
async run(image, proj, params) {
|
|
92
|
+
const { imgH, imgW, patchH, patchW, numChannels, embedDim } = params;
|
|
93
|
+
if (imgH % patchH !== 0)
|
|
94
|
+
throw new Error(`imagePatchEmbed: imgH=${imgH} not divisible by patchH=${patchH}`);
|
|
95
|
+
if (imgW % patchW !== 0)
|
|
96
|
+
throw new Error(`imagePatchEmbed: imgW=${imgW} not divisible by patchW=${patchW}`);
|
|
97
|
+
const patchLen = patchH * patchW * numChannels;
|
|
98
|
+
const numPatchesY = imgH / patchH;
|
|
99
|
+
const numPatchesX = imgW / patchW;
|
|
100
|
+
const numPatches = numPatchesY * numPatchesX;
|
|
101
|
+
if (image.length !== imgH * imgW * numChannels)
|
|
102
|
+
throw new Error(`imagePatchEmbed: image size mismatch`);
|
|
103
|
+
if (proj.length !== embedDim * patchLen)
|
|
104
|
+
throw new Error(`imagePatchEmbed: proj size mismatch`);
|
|
105
|
+
const outBytes = numPatches * embedDim * 4;
|
|
106
|
+
const imgBuf = storageBuffer(device, image);
|
|
107
|
+
const projBuf = storageBuffer(device, proj);
|
|
108
|
+
const outBuf = device.createBuffer({
|
|
109
|
+
size: outBytes,
|
|
110
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
111
|
+
});
|
|
112
|
+
const paramAB = new ArrayBuffer(32);
|
|
113
|
+
const pu = new Uint32Array(paramAB);
|
|
114
|
+
pu[0] = imgH;
|
|
115
|
+
pu[1] = imgW;
|
|
116
|
+
pu[2] = patchH;
|
|
117
|
+
pu[3] = patchW;
|
|
118
|
+
pu[4] = numChannels;
|
|
119
|
+
pu[5] = embedDim;
|
|
120
|
+
pu[6] = patchLen;
|
|
121
|
+
pu[7] = numPatchesX;
|
|
122
|
+
const paramsBuf = device.createBuffer({
|
|
123
|
+
size: paramAB.byteLength,
|
|
124
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
125
|
+
});
|
|
126
|
+
device.queue.writeBuffer(paramsBuf, 0, paramAB);
|
|
127
|
+
const bg = device.createBindGroup({
|
|
128
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
129
|
+
entries: [
|
|
130
|
+
{ binding: 0, resource: { buffer: imgBuf } },
|
|
131
|
+
{ binding: 1, resource: { buffer: projBuf } },
|
|
132
|
+
{ binding: 2, resource: { buffer: outBuf } },
|
|
133
|
+
{ binding: 3, resource: { buffer: paramsBuf } }
|
|
134
|
+
]
|
|
135
|
+
});
|
|
136
|
+
const enc = device.createCommandEncoder();
|
|
137
|
+
const pass = enc.beginComputePass();
|
|
138
|
+
pass.setPipeline(pipeline);
|
|
139
|
+
pass.setBindGroup(0, bg);
|
|
140
|
+
pass.dispatchWorkgroups(numPatches);
|
|
141
|
+
pass.end();
|
|
142
|
+
const staging = device.createBuffer({
|
|
143
|
+
size: outBytes,
|
|
144
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
145
|
+
});
|
|
146
|
+
enc.copyBufferToBuffer(outBuf, 0, staging, 0, outBytes);
|
|
147
|
+
device.queue.submit([enc.finish()]);
|
|
148
|
+
await staging.mapAsync(GPUMapMode.READ);
|
|
149
|
+
const result = new Float32Array(staging.getMappedRange().slice(0));
|
|
150
|
+
staging.unmap();
|
|
151
|
+
imgBuf.destroy();
|
|
152
|
+
projBuf.destroy();
|
|
153
|
+
outBuf.destroy();
|
|
154
|
+
paramsBuf.destroy();
|
|
155
|
+
staging.destroy();
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/reconstruction/layerNormKernel.ts
|
|
162
|
+
var WGSL_LAYER_NORM_SOURCE = `
|
|
163
|
+
struct Params {
|
|
164
|
+
rows: u32,
|
|
165
|
+
dModel: u32,
|
|
166
|
+
eps: f32,
|
|
167
|
+
}
|
|
168
|
+
@group(0) @binding(0) var<storage, read> input: array<f32>;
|
|
169
|
+
@group(0) @binding(1) var<storage, read> gamma: array<f32>;
|
|
170
|
+
@group(0) @binding(2) var<storage, read> beta: array<f32>;
|
|
171
|
+
@group(0) @binding(3) var<storage, read_write> output: array<f32>;
|
|
172
|
+
@group(0) @binding(4) var<uniform> params: Params;
|
|
173
|
+
var<workgroup> reduceScratch: array<f32, 64>;
|
|
174
|
+
fn inputIndex(row: u32, col: u32) -> u32 {
|
|
175
|
+
return row * params.dModel + col;
|
|
176
|
+
}
|
|
177
|
+
@compute @workgroup_size(64, 1, 1)
|
|
178
|
+
fn main(
|
|
179
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>,
|
|
180
|
+
@builtin(local_invocation_id) localId: vec3<u32>,
|
|
181
|
+
) {
|
|
182
|
+
let row = workgroupId.x;
|
|
183
|
+
let lane = localId.x;
|
|
184
|
+
if (row >= params.rows || params.dModel == 0u) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
var sum = 0.0;
|
|
188
|
+
for (var c: u32 = lane; c < params.dModel; c = c + 64u) {
|
|
189
|
+
sum = sum + input[inputIndex(row, c)];
|
|
190
|
+
}
|
|
191
|
+
reduceScratch[lane] = sum;
|
|
192
|
+
workgroupBarrier();
|
|
193
|
+
var stride: u32 = 32u;
|
|
194
|
+
loop {
|
|
195
|
+
if (lane < stride) {
|
|
196
|
+
reduceScratch[lane] = reduceScratch[lane] + reduceScratch[lane + stride];
|
|
197
|
+
}
|
|
198
|
+
workgroupBarrier();
|
|
199
|
+
if (stride == 1u) {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
stride = stride >> 1u;
|
|
203
|
+
}
|
|
204
|
+
let mean = reduceScratch[0] / f32(params.dModel);
|
|
205
|
+
var sq = 0.0;
|
|
206
|
+
for (var c: u32 = lane; c < params.dModel; c = c + 64u) {
|
|
207
|
+
let d = input[inputIndex(row, c)] - mean;
|
|
208
|
+
sq = sq + d * d;
|
|
209
|
+
}
|
|
210
|
+
reduceScratch[lane] = sq;
|
|
211
|
+
workgroupBarrier();
|
|
212
|
+
stride = 32u;
|
|
213
|
+
loop {
|
|
214
|
+
if (lane < stride) {
|
|
215
|
+
reduceScratch[lane] = reduceScratch[lane] + reduceScratch[lane + stride];
|
|
216
|
+
}
|
|
217
|
+
workgroupBarrier();
|
|
218
|
+
if (stride == 1u) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
stride = stride >> 1u;
|
|
222
|
+
}
|
|
223
|
+
let variance = reduceScratch[0] / f32(params.dModel);
|
|
224
|
+
let invStd = inverseSqrt(variance + params.eps);
|
|
225
|
+
for (var c: u32 = lane; c < params.dModel; c = c + 64u) {
|
|
226
|
+
let idx = inputIndex(row, c);
|
|
227
|
+
let normalized = (input[idx] - mean) * invStd;
|
|
228
|
+
output[idx] = normalized * gamma[c] + beta[c];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
`;
|
|
232
|
+
function createStorageBuffer(device, data) {
|
|
233
|
+
const buf = device.createBuffer({
|
|
234
|
+
size: data.byteLength,
|
|
235
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
236
|
+
});
|
|
237
|
+
device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
|
|
238
|
+
return buf;
|
|
239
|
+
}
|
|
240
|
+
function createParamsBuffer(device, rows, dModel, eps) {
|
|
241
|
+
const paramsBytes = new ArrayBuffer(16);
|
|
242
|
+
const view = new DataView(paramsBytes);
|
|
243
|
+
view.setUint32(0, rows, true);
|
|
244
|
+
view.setUint32(4, dModel, true);
|
|
245
|
+
view.setFloat32(8, eps, true);
|
|
246
|
+
const params = device.createBuffer({
|
|
247
|
+
size: paramsBytes.byteLength,
|
|
248
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
249
|
+
});
|
|
250
|
+
device.queue.writeBuffer(params, 0, paramsBytes);
|
|
251
|
+
return params;
|
|
252
|
+
}
|
|
253
|
+
function createLayerNormKernel(device) {
|
|
254
|
+
const shader = device.createShaderModule({ code: WGSL_LAYER_NORM_SOURCE });
|
|
255
|
+
const pipeline = device.createComputePipeline({
|
|
256
|
+
layout: "auto",
|
|
257
|
+
compute: {
|
|
258
|
+
module: shader,
|
|
259
|
+
entryPoint: "main"
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
return {
|
|
263
|
+
async run(input, gamma, beta) {
|
|
264
|
+
if (gamma.length === 0) {
|
|
265
|
+
throw new Error("LayerNorm: gamma must be non-empty");
|
|
266
|
+
}
|
|
267
|
+
if (beta.length !== gamma.length) {
|
|
268
|
+
throw new Error("LayerNorm: beta length must match gamma length");
|
|
269
|
+
}
|
|
270
|
+
if (input.length % gamma.length !== 0) {
|
|
271
|
+
throw new Error("LayerNorm: input length must be a multiple of gamma length (dModel)");
|
|
272
|
+
}
|
|
273
|
+
const dModel = gamma.length;
|
|
274
|
+
const rows = input.length / dModel;
|
|
275
|
+
const eps = 1e-5;
|
|
276
|
+
const inputBuf = createStorageBuffer(device, input);
|
|
277
|
+
const gammaBuf = createStorageBuffer(device, gamma);
|
|
278
|
+
const betaBuf = createStorageBuffer(device, beta);
|
|
279
|
+
const outputBuf = device.createBuffer({
|
|
280
|
+
size: input.byteLength,
|
|
281
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
282
|
+
});
|
|
283
|
+
const paramsBuf = createParamsBuffer(device, rows, dModel, eps);
|
|
284
|
+
const readback2 = device.createBuffer({
|
|
285
|
+
size: input.byteLength,
|
|
286
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
287
|
+
});
|
|
288
|
+
const bindGroup = device.createBindGroup({
|
|
289
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
290
|
+
entries: [
|
|
291
|
+
{ binding: 0, resource: { buffer: inputBuf } },
|
|
292
|
+
{ binding: 1, resource: { buffer: gammaBuf } },
|
|
293
|
+
{ binding: 2, resource: { buffer: betaBuf } },
|
|
294
|
+
{ binding: 3, resource: { buffer: outputBuf } },
|
|
295
|
+
{ binding: 4, resource: { buffer: paramsBuf } }
|
|
296
|
+
]
|
|
297
|
+
});
|
|
298
|
+
const encoder = device.createCommandEncoder();
|
|
299
|
+
{
|
|
300
|
+
const pass = encoder.beginComputePass();
|
|
301
|
+
pass.setPipeline(pipeline);
|
|
302
|
+
pass.setBindGroup(0, bindGroup);
|
|
303
|
+
pass.dispatchWorkgroups(rows, 1, 1);
|
|
304
|
+
pass.end();
|
|
305
|
+
}
|
|
306
|
+
encoder.copyBufferToBuffer(outputBuf, 0, readback2, 0, input.byteLength);
|
|
307
|
+
device.queue.submit([encoder.finish()]);
|
|
308
|
+
await readback2.mapAsync(GPUMapMode.READ);
|
|
309
|
+
const bytes = readback2.getMappedRange();
|
|
310
|
+
const out = new Float32Array(bytes.slice(0));
|
|
311
|
+
readback2.unmap();
|
|
312
|
+
inputBuf.destroy();
|
|
313
|
+
gammaBuf.destroy();
|
|
314
|
+
betaBuf.destroy();
|
|
315
|
+
outputBuf.destroy();
|
|
316
|
+
paramsBuf.destroy();
|
|
317
|
+
readback2.destroy();
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/reconstruction/gemmKernel.ts
|
|
324
|
+
var TILE = 8;
|
|
325
|
+
var WGSL_GEMM_SOURCE = `
|
|
326
|
+
const TILE: u32 = 8u;
|
|
327
|
+
struct Params {
|
|
328
|
+
M: u32,
|
|
329
|
+
N: u32,
|
|
330
|
+
K: u32,
|
|
331
|
+
}
|
|
332
|
+
@group(0) @binding(0) var<storage, read> A: array<f32>; // [M * K]
|
|
333
|
+
@group(0) @binding(1) var<storage, read> B: array<f32>; // [K * N]
|
|
334
|
+
@group(0) @binding(2) var<storage, read_write> C: array<f32>; // [M * N]
|
|
335
|
+
@group(0) @binding(3) var<uniform> p: Params;
|
|
336
|
+
var<workgroup> tileA: array<f32, 64>; // TILE * TILE = 8*8
|
|
337
|
+
var<workgroup> tileB: array<f32, 64>;
|
|
338
|
+
@compute @workgroup_size(8, 8, 1)
|
|
339
|
+
fn main(
|
|
340
|
+
@builtin(workgroup_id) wg: vec3<u32>,
|
|
341
|
+
@builtin(local_invocation_id) lid: vec3<u32>,
|
|
342
|
+
) {
|
|
343
|
+
let row = wg.y * TILE + lid.y;
|
|
344
|
+
let col = wg.x * TILE + lid.x;
|
|
345
|
+
var acc: f32 = 0.0;
|
|
346
|
+
let numTiles = (p.K + TILE - 1u) / TILE;
|
|
347
|
+
for (var t = 0u; t < numTiles; t++) {
|
|
348
|
+
// Load tile of A (row of A) into shared
|
|
349
|
+
let aCol = t * TILE + lid.x;
|
|
350
|
+
if (row < p.M && aCol < p.K) {
|
|
351
|
+
tileA[lid.y * TILE + lid.x] = A[row * p.K + aCol];
|
|
352
|
+
} else {
|
|
353
|
+
tileA[lid.y * TILE + lid.x] = 0.0;
|
|
354
|
+
}
|
|
355
|
+
// Load tile of B (col of B) into shared
|
|
356
|
+
let bRow = t * TILE + lid.y;
|
|
357
|
+
if (bRow < p.K && col < p.N) {
|
|
358
|
+
tileB[lid.y * TILE + lid.x] = B[bRow * p.N + col];
|
|
359
|
+
} else {
|
|
360
|
+
tileB[lid.y * TILE + lid.x] = 0.0;
|
|
361
|
+
}
|
|
362
|
+
workgroupBarrier();
|
|
363
|
+
for (var k = 0u; k < TILE; k++) {
|
|
364
|
+
acc = acc + tileA[lid.y * TILE + k] * tileB[k * TILE + lid.x];
|
|
365
|
+
}
|
|
366
|
+
workgroupBarrier();
|
|
367
|
+
}
|
|
368
|
+
if (row < p.M && col < p.N) {
|
|
369
|
+
C[row * p.N + col] = acc;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
`;
|
|
373
|
+
function storageBuffer2(device, data) {
|
|
374
|
+
const buf = device.createBuffer({
|
|
375
|
+
size: data.byteLength,
|
|
376
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
377
|
+
});
|
|
378
|
+
device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
|
|
379
|
+
return buf;
|
|
380
|
+
}
|
|
381
|
+
function createGemmKernel(device) {
|
|
382
|
+
const shader = device.createShaderModule({ code: WGSL_GEMM_SOURCE });
|
|
383
|
+
const pipeline = device.createComputePipeline({
|
|
384
|
+
layout: "auto",
|
|
385
|
+
compute: { module: shader, entryPoint: "main" }
|
|
386
|
+
});
|
|
387
|
+
return {
|
|
388
|
+
async run(A, B, M, N, K) {
|
|
389
|
+
if (A.length !== M * K) throw new Error(`GEMM: A.length=${A.length} != M*K=${M * K}`);
|
|
390
|
+
if (B.length !== K * N) throw new Error(`GEMM: B.length=${B.length} != K*N=${K * N}`);
|
|
391
|
+
const aBuf = storageBuffer2(device, A);
|
|
392
|
+
const bBuf = storageBuffer2(device, B);
|
|
393
|
+
const cBuf = device.createBuffer({
|
|
394
|
+
size: M * N * 4,
|
|
395
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
396
|
+
});
|
|
397
|
+
const paramAB = new ArrayBuffer(16);
|
|
398
|
+
const pu = new Uint32Array(paramAB);
|
|
399
|
+
pu[0] = M;
|
|
400
|
+
pu[1] = N;
|
|
401
|
+
pu[2] = K;
|
|
402
|
+
const paramsBuf = device.createBuffer({
|
|
403
|
+
size: paramAB.byteLength,
|
|
404
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
405
|
+
});
|
|
406
|
+
device.queue.writeBuffer(paramsBuf, 0, paramAB);
|
|
407
|
+
const bg = device.createBindGroup({
|
|
408
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
409
|
+
entries: [
|
|
410
|
+
{ binding: 0, resource: { buffer: aBuf } },
|
|
411
|
+
{ binding: 1, resource: { buffer: bBuf } },
|
|
412
|
+
{ binding: 2, resource: { buffer: cBuf } },
|
|
413
|
+
{ binding: 3, resource: { buffer: paramsBuf } }
|
|
414
|
+
]
|
|
415
|
+
});
|
|
416
|
+
const wgX = Math.ceil(N / TILE);
|
|
417
|
+
const wgY = Math.ceil(M / TILE);
|
|
418
|
+
const enc = device.createCommandEncoder();
|
|
419
|
+
const pass = enc.beginComputePass();
|
|
420
|
+
pass.setPipeline(pipeline);
|
|
421
|
+
pass.setBindGroup(0, bg);
|
|
422
|
+
pass.dispatchWorkgroups(wgX, wgY, 1);
|
|
423
|
+
pass.end();
|
|
424
|
+
const staging = device.createBuffer({
|
|
425
|
+
size: M * N * 4,
|
|
426
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
427
|
+
});
|
|
428
|
+
enc.copyBufferToBuffer(cBuf, 0, staging, 0, M * N * 4);
|
|
429
|
+
device.queue.submit([enc.finish()]);
|
|
430
|
+
await staging.mapAsync(GPUMapMode.READ);
|
|
431
|
+
const result = new Float32Array(staging.getMappedRange().slice(0));
|
|
432
|
+
staging.unmap();
|
|
433
|
+
aBuf.destroy();
|
|
434
|
+
bBuf.destroy();
|
|
435
|
+
cBuf.destroy();
|
|
436
|
+
paramsBuf.destroy();
|
|
437
|
+
staging.destroy();
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/reconstruction/ropeKernel.ts
|
|
444
|
+
var WGSL_ROPE_SOURCE = `
|
|
445
|
+
struct Params {
|
|
446
|
+
seqLen: u32,
|
|
447
|
+
numHeads: u32,
|
|
448
|
+
headDim: u32,
|
|
449
|
+
base: f32,
|
|
450
|
+
posOffset: u32,
|
|
451
|
+
}
|
|
452
|
+
@group(0) @binding(0) var<storage, read> qIn: array<f32>;
|
|
453
|
+
@group(0) @binding(1) var<storage, read_write> qOut: array<f32>;
|
|
454
|
+
@group(0) @binding(2) var<uniform> p: Params;
|
|
455
|
+
fn flatIdx(token: u32, head: u32, dim: u32) -> u32 {
|
|
456
|
+
return token * p.numHeads * p.headDim + head * p.headDim + dim;
|
|
457
|
+
}
|
|
458
|
+
@compute @workgroup_size(32, 1, 1)
|
|
459
|
+
fn main(
|
|
460
|
+
@builtin(workgroup_id) wgid: vec3<u32>,
|
|
461
|
+
@builtin(local_invocation_id) lid: vec3<u32>,
|
|
462
|
+
) {
|
|
463
|
+
let wg = wgid.x;
|
|
464
|
+
let token = wg / p.numHeads;
|
|
465
|
+
let head = wg % p.numHeads;
|
|
466
|
+
if (token >= p.seqLen) { return; }
|
|
467
|
+
let pairs = p.headDim / 2u;
|
|
468
|
+
let pos = token + p.posOffset;
|
|
469
|
+
var pairIdx = lid.x;
|
|
470
|
+
while (pairIdx < pairs) {
|
|
471
|
+
let d = pairIdx;
|
|
472
|
+
let exponent = -2.0 * f32(d) / f32(p.headDim);
|
|
473
|
+
let theta = f32(pos) * pow(p.base, exponent);
|
|
474
|
+
let c = cos(theta);
|
|
475
|
+
let s = sin(theta);
|
|
476
|
+
let i0 = flatIdx(token, head, 2u * d);
|
|
477
|
+
let i1 = flatIdx(token, head, 2u * d + 1u);
|
|
478
|
+
let x0 = qIn[i0];
|
|
479
|
+
let x1 = qIn[i1];
|
|
480
|
+
qOut[i0] = x0 * c - x1 * s;
|
|
481
|
+
qOut[i1] = x0 * s + x1 * c;
|
|
482
|
+
pairIdx = pairIdx + 32u;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
`;
|
|
486
|
+
function createStorageBuffer2(device, data) {
|
|
487
|
+
const buf = device.createBuffer({
|
|
488
|
+
size: data.byteLength,
|
|
489
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
490
|
+
});
|
|
491
|
+
device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
|
|
492
|
+
return buf;
|
|
493
|
+
}
|
|
494
|
+
function createOutputBuffer(device, byteLength) {
|
|
495
|
+
return device.createBuffer({
|
|
496
|
+
size: byteLength,
|
|
497
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
function createParamsBuffer2(device, rp) {
|
|
501
|
+
const buf = new ArrayBuffer(32);
|
|
502
|
+
const u = new Uint32Array(buf);
|
|
503
|
+
const f = new Float32Array(buf);
|
|
504
|
+
u[0] = rp.seqLen;
|
|
505
|
+
u[1] = rp.numHeads;
|
|
506
|
+
u[2] = rp.headDim;
|
|
507
|
+
f[3] = rp.base;
|
|
508
|
+
u[4] = rp.posOffset;
|
|
509
|
+
const gpuBuf = device.createBuffer({
|
|
510
|
+
size: buf.byteLength,
|
|
511
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
512
|
+
});
|
|
513
|
+
device.queue.writeBuffer(gpuBuf, 0, buf);
|
|
514
|
+
return gpuBuf;
|
|
515
|
+
}
|
|
516
|
+
function createRopeKernel(device) {
|
|
517
|
+
const shader = device.createShaderModule({ code: WGSL_ROPE_SOURCE });
|
|
518
|
+
const pipeline = device.createComputePipeline({
|
|
519
|
+
layout: "auto",
|
|
520
|
+
compute: { module: shader, entryPoint: "main" }
|
|
521
|
+
});
|
|
522
|
+
return {
|
|
523
|
+
async run(input, params) {
|
|
524
|
+
const { seqLen, numHeads, headDim } = params;
|
|
525
|
+
const base = params.base ?? 1e4;
|
|
526
|
+
const posOffset = params.posOffset ?? 0;
|
|
527
|
+
if (headDim % 2 !== 0) {
|
|
528
|
+
throw new Error("RoPE: headDim must be even");
|
|
529
|
+
}
|
|
530
|
+
const expected = seqLen * numHeads * headDim;
|
|
531
|
+
if (input.length !== expected) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`RoPE: input length ${input.length} does not match seqLen*numHeads*headDim=${expected}`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
const rp = { seqLen, numHeads, headDim, base, posOffset };
|
|
537
|
+
const inBuf = createStorageBuffer2(device, input);
|
|
538
|
+
const outBuf = createOutputBuffer(device, input.byteLength);
|
|
539
|
+
const paramsBuf = createParamsBuffer2(device, rp);
|
|
540
|
+
const stagingBuf = device.createBuffer({
|
|
541
|
+
size: input.byteLength,
|
|
542
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
543
|
+
});
|
|
544
|
+
const bg = device.createBindGroup({
|
|
545
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
546
|
+
entries: [
|
|
547
|
+
{ binding: 0, resource: { buffer: inBuf } },
|
|
548
|
+
{ binding: 1, resource: { buffer: outBuf } },
|
|
549
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
550
|
+
]
|
|
551
|
+
});
|
|
552
|
+
const enc = device.createCommandEncoder();
|
|
553
|
+
const pass = enc.beginComputePass();
|
|
554
|
+
pass.setPipeline(pipeline);
|
|
555
|
+
pass.setBindGroup(0, bg);
|
|
556
|
+
pass.dispatchWorkgroups(seqLen * numHeads);
|
|
557
|
+
pass.end();
|
|
558
|
+
enc.copyBufferToBuffer(outBuf, 0, stagingBuf, 0, input.byteLength);
|
|
559
|
+
device.queue.submit([enc.finish()]);
|
|
560
|
+
await stagingBuf.mapAsync(GPUMapMode.READ);
|
|
561
|
+
const result = new Float32Array(stagingBuf.getMappedRange().slice(0));
|
|
562
|
+
stagingBuf.unmap();
|
|
563
|
+
inBuf.destroy();
|
|
564
|
+
outBuf.destroy();
|
|
565
|
+
paramsBuf.destroy();
|
|
566
|
+
stagingBuf.destroy();
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/reconstruction/fusedMHAKernel.ts
|
|
573
|
+
var WGSL_FUSED_MHA = `
|
|
574
|
+
struct Params {
|
|
575
|
+
numHeads: u32,
|
|
576
|
+
qLen: u32,
|
|
577
|
+
kLen: u32,
|
|
578
|
+
dHead: u32, // head dimension (= sqrt-scale base)
|
|
579
|
+
vHead: u32, // output feature dim per head (usually == dHead)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
@group(0) @binding(0) var<storage, read> Q: array<f32>; // [h, qLen, dHead]
|
|
583
|
+
@group(0) @binding(1) var<storage, read> K: array<f32>; // [h, kLen, dHead]
|
|
584
|
+
@group(0) @binding(2) var<storage, read> V: array<f32>; // [h, kLen, vHead]
|
|
585
|
+
@group(0) @binding(3) var<storage, read_write> O: array<f32>; // [h, qLen, vHead]
|
|
586
|
+
@group(0) @binding(4) var<uniform> p: Params;
|
|
587
|
+
|
|
588
|
+
// Shared memory: reuse for scores then for v-accumulation
|
|
589
|
+
var<workgroup> scores: array<f32, 1024>; // max supported kLen per workgroup
|
|
590
|
+
|
|
591
|
+
@compute @workgroup_size(64, 1, 1)
|
|
592
|
+
fn main(
|
|
593
|
+
@builtin(workgroup_id) wg: vec3<u32>,
|
|
594
|
+
@builtin(local_invocation_id) lid: vec3<u32>,
|
|
595
|
+
) {
|
|
596
|
+
// workgroup index encodes (head, qRow)
|
|
597
|
+
let head = wg.x / p.qLen;
|
|
598
|
+
let qRow = wg.x % p.qLen;
|
|
599
|
+
|
|
600
|
+
if (head >= p.numHeads || qRow >= p.qLen) { return; }
|
|
601
|
+
|
|
602
|
+
let scale: f32 = 1.0 / sqrt(f32(p.dHead));
|
|
603
|
+
|
|
604
|
+
// ---- Phase 1: compute dot products Q[qRow] \xB7 K[k] for all k ----
|
|
605
|
+
// Thread 0 does sequential dot products and stores in shared scores[]
|
|
606
|
+
// (for kLen \u2264 1024; if larger, falls back to partial accumulation)
|
|
607
|
+
if (lid.x == 0u) {
|
|
608
|
+
let qBase = (head * p.qLen + qRow) * p.dHead;
|
|
609
|
+
for (var ki = 0u; ki < p.kLen; ki++) {
|
|
610
|
+
let kBase = (head * p.kLen + ki) * p.dHead;
|
|
611
|
+
var dot: f32 = 0.0;
|
|
612
|
+
for (var d = 0u; d < p.dHead; d++) {
|
|
613
|
+
dot = dot + Q[qBase + d] * K[kBase + d];
|
|
614
|
+
}
|
|
615
|
+
scores[ki] = dot * scale;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
workgroupBarrier();
|
|
619
|
+
|
|
620
|
+
// ---- Phase 2: stable softmax in-place ----
|
|
621
|
+
if (lid.x == 0u) {
|
|
622
|
+
var maxS: f32 = scores[0];
|
|
623
|
+
for (var ki = 1u; ki < p.kLen; ki++) {
|
|
624
|
+
maxS = max(maxS, scores[ki]);
|
|
625
|
+
}
|
|
626
|
+
var sumExp: f32 = 0.0;
|
|
627
|
+
for (var ki = 0u; ki < p.kLen; ki++) {
|
|
628
|
+
scores[ki] = exp(scores[ki] - maxS);
|
|
629
|
+
sumExp = sumExp + scores[ki];
|
|
630
|
+
}
|
|
631
|
+
for (var ki = 0u; ki < p.kLen; ki++) {
|
|
632
|
+
scores[ki] = scores[ki] / sumExp;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
workgroupBarrier();
|
|
636
|
+
|
|
637
|
+
// ---- Phase 3: weighted sum over V ----
|
|
638
|
+
// Threads fan out over vHead dimension
|
|
639
|
+
let outBase = (head * p.qLen + qRow) * p.vHead;
|
|
640
|
+
var vi = lid.x;
|
|
641
|
+
while (vi < p.vHead) {
|
|
642
|
+
var acc: f32 = 0.0;
|
|
643
|
+
for (var ki = 0u; ki < p.kLen; ki++) {
|
|
644
|
+
acc = acc + scores[ki] * V[(head * p.kLen + ki) * p.vHead + vi];
|
|
645
|
+
}
|
|
646
|
+
O[outBase + vi] = acc;
|
|
647
|
+
vi = vi + 64u;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
`;
|
|
651
|
+
function storageBuffer3(device, data) {
|
|
652
|
+
const buf = device.createBuffer({
|
|
653
|
+
size: data.byteLength,
|
|
654
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
655
|
+
});
|
|
656
|
+
device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
|
|
657
|
+
return buf;
|
|
658
|
+
}
|
|
659
|
+
function createFusedMHAKernel(device) {
|
|
660
|
+
const shader = device.createShaderModule({ code: WGSL_FUSED_MHA });
|
|
661
|
+
const pipeline = device.createComputePipeline({
|
|
662
|
+
layout: "auto",
|
|
663
|
+
compute: { module: shader, entryPoint: "main" }
|
|
664
|
+
});
|
|
665
|
+
return {
|
|
666
|
+
async run(Q, K, V, params) {
|
|
667
|
+
const { numHeads, qLen, kLen, dHead } = params;
|
|
668
|
+
const vHead = params.vHead ?? dHead;
|
|
669
|
+
if (kLen > 1024) throw new Error(`fusedMHA: kLen=${kLen} exceeds shared memory limit 1024`);
|
|
670
|
+
if (Q.length !== numHeads * qLen * dHead) throw new Error(`fusedMHA: Q size mismatch`);
|
|
671
|
+
if (K.length !== numHeads * kLen * dHead) throw new Error(`fusedMHA: K size mismatch`);
|
|
672
|
+
if (V.length !== numHeads * kLen * vHead) throw new Error(`fusedMHA: V size mismatch`);
|
|
673
|
+
const outBytes = numHeads * qLen * vHead * 4;
|
|
674
|
+
const qBuf = storageBuffer3(device, Q);
|
|
675
|
+
const kBuf = storageBuffer3(device, K);
|
|
676
|
+
const vBuf = storageBuffer3(device, V);
|
|
677
|
+
const oBuf = device.createBuffer({
|
|
678
|
+
size: outBytes,
|
|
679
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
680
|
+
});
|
|
681
|
+
const paramAB = new ArrayBuffer(24);
|
|
682
|
+
const pu = new Uint32Array(paramAB);
|
|
683
|
+
pu[0] = numHeads;
|
|
684
|
+
pu[1] = qLen;
|
|
685
|
+
pu[2] = kLen;
|
|
686
|
+
pu[3] = dHead;
|
|
687
|
+
pu[4] = vHead;
|
|
688
|
+
const paramsBuf = device.createBuffer({
|
|
689
|
+
size: paramAB.byteLength,
|
|
690
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
691
|
+
});
|
|
692
|
+
device.queue.writeBuffer(paramsBuf, 0, paramAB);
|
|
693
|
+
const bg = device.createBindGroup({
|
|
694
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
695
|
+
entries: [
|
|
696
|
+
{ binding: 0, resource: { buffer: qBuf } },
|
|
697
|
+
{ binding: 1, resource: { buffer: kBuf } },
|
|
698
|
+
{ binding: 2, resource: { buffer: vBuf } },
|
|
699
|
+
{ binding: 3, resource: { buffer: oBuf } },
|
|
700
|
+
{ binding: 4, resource: { buffer: paramsBuf } }
|
|
701
|
+
]
|
|
702
|
+
});
|
|
703
|
+
const workgroups = numHeads * qLen;
|
|
704
|
+
const enc = device.createCommandEncoder();
|
|
705
|
+
const pass = enc.beginComputePass();
|
|
706
|
+
pass.setPipeline(pipeline);
|
|
707
|
+
pass.setBindGroup(0, bg);
|
|
708
|
+
pass.dispatchWorkgroups(workgroups);
|
|
709
|
+
pass.end();
|
|
710
|
+
const staging = device.createBuffer({
|
|
711
|
+
size: outBytes,
|
|
712
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
713
|
+
});
|
|
714
|
+
enc.copyBufferToBuffer(oBuf, 0, staging, 0, outBytes);
|
|
715
|
+
device.queue.submit([enc.finish()]);
|
|
716
|
+
await staging.mapAsync(GPUMapMode.READ);
|
|
717
|
+
const result = new Float32Array(staging.getMappedRange().slice(0));
|
|
718
|
+
staging.unmap();
|
|
719
|
+
qBuf.destroy();
|
|
720
|
+
kBuf.destroy();
|
|
721
|
+
vBuf.destroy();
|
|
722
|
+
oBuf.destroy();
|
|
723
|
+
paramsBuf.destroy();
|
|
724
|
+
staging.destroy();
|
|
725
|
+
return result;
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/reconstruction/geluKernel.ts
|
|
731
|
+
var WGSL_GELU_SOURCE = `
|
|
732
|
+
struct Params {
|
|
733
|
+
elements: u32,
|
|
734
|
+
}
|
|
735
|
+
@group(0) @binding(0) var<storage, read> input: array<f32>;
|
|
736
|
+
@group(0) @binding(1) var<storage, read_write> output: array<f32>;
|
|
737
|
+
@group(0) @binding(2) var<uniform> params: Params;
|
|
738
|
+
fn gelu(x: f32) -> f32 {
|
|
739
|
+
let k0 = 0.7978845608;
|
|
740
|
+
let k1 = 0.044715;
|
|
741
|
+
let x3 = x * x * x;
|
|
742
|
+
let inner = k0 * (x + k1 * x3);
|
|
743
|
+
return 0.5 * x * (1.0 + tanh(inner));
|
|
744
|
+
}
|
|
745
|
+
@compute @workgroup_size(64, 1, 1)
|
|
746
|
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
747
|
+
var idx = gid.x;
|
|
748
|
+
let stride = 64u;
|
|
749
|
+
while (idx < params.elements) {
|
|
750
|
+
output[idx] = gelu(input[idx]);
|
|
751
|
+
idx = idx + stride;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
`;
|
|
755
|
+
function createInputBuffer(device, data) {
|
|
756
|
+
const buf = device.createBuffer({
|
|
757
|
+
size: data.byteLength,
|
|
758
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
759
|
+
});
|
|
760
|
+
device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
|
|
761
|
+
return buf;
|
|
762
|
+
}
|
|
763
|
+
function createParamsBuffer3(device, elements) {
|
|
764
|
+
const paramsBytes = new ArrayBuffer(16);
|
|
765
|
+
const view = new DataView(paramsBytes);
|
|
766
|
+
view.setUint32(0, elements, true);
|
|
767
|
+
const params = device.createBuffer({
|
|
768
|
+
size: paramsBytes.byteLength,
|
|
769
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
770
|
+
});
|
|
771
|
+
device.queue.writeBuffer(params, 0, paramsBytes);
|
|
772
|
+
return params;
|
|
773
|
+
}
|
|
774
|
+
function createGeluKernel(device) {
|
|
775
|
+
const shader = device.createShaderModule({ code: WGSL_GELU_SOURCE });
|
|
776
|
+
const pipeline = device.createComputePipeline({
|
|
777
|
+
layout: "auto",
|
|
778
|
+
compute: {
|
|
779
|
+
module: shader,
|
|
780
|
+
entryPoint: "main"
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
return {
|
|
784
|
+
async run(input) {
|
|
785
|
+
if (input.length === 0) {
|
|
786
|
+
throw new Error("GELU: input must be non-empty");
|
|
787
|
+
}
|
|
788
|
+
const inputBuf = createInputBuffer(device, input);
|
|
789
|
+
const outputBuf = device.createBuffer({
|
|
790
|
+
size: input.byteLength,
|
|
791
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
792
|
+
});
|
|
793
|
+
const paramsBuf = createParamsBuffer3(device, input.length);
|
|
794
|
+
const readback2 = device.createBuffer({
|
|
795
|
+
size: input.byteLength,
|
|
796
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
797
|
+
});
|
|
798
|
+
const bindGroup = device.createBindGroup({
|
|
799
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
800
|
+
entries: [
|
|
801
|
+
{ binding: 0, resource: { buffer: inputBuf } },
|
|
802
|
+
{ binding: 1, resource: { buffer: outputBuf } },
|
|
803
|
+
{ binding: 2, resource: { buffer: paramsBuf } }
|
|
804
|
+
]
|
|
805
|
+
});
|
|
806
|
+
const encoder = device.createCommandEncoder();
|
|
807
|
+
{
|
|
808
|
+
const pass = encoder.beginComputePass();
|
|
809
|
+
pass.setPipeline(pipeline);
|
|
810
|
+
pass.setBindGroup(0, bindGroup);
|
|
811
|
+
pass.dispatchWorkgroups(1, 1, 1);
|
|
812
|
+
pass.end();
|
|
813
|
+
}
|
|
814
|
+
encoder.copyBufferToBuffer(outputBuf, 0, readback2, 0, input.byteLength);
|
|
815
|
+
device.queue.submit([encoder.finish()]);
|
|
816
|
+
await readback2.mapAsync(GPUMapMode.READ);
|
|
817
|
+
const bytes = readback2.getMappedRange();
|
|
818
|
+
const out = new Float32Array(bytes.slice(0));
|
|
819
|
+
readback2.unmap();
|
|
820
|
+
inputBuf.destroy();
|
|
821
|
+
outputBuf.destroy();
|
|
822
|
+
paramsBuf.destroy();
|
|
823
|
+
readback2.destroy();
|
|
824
|
+
return out;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/reconstruction/holoMapMicroEncoder.ts
|
|
830
|
+
var MICRO = 14;
|
|
831
|
+
var EMBED_DIM = 32;
|
|
832
|
+
var NUM_HEADS = 4;
|
|
833
|
+
var HEAD_DIM = 8;
|
|
834
|
+
var PATCH_LEN = MICRO * MICRO * 3;
|
|
835
|
+
var weightCache = /* @__PURE__ */ new Map();
|
|
836
|
+
var weightBlobKeyCache = /* @__PURE__ */ new WeakMap();
|
|
837
|
+
function hashConfigSeed(config) {
|
|
838
|
+
let h = config.seed >>> 0;
|
|
839
|
+
const s = config.modelHash || "";
|
|
840
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
841
|
+
h = Math.imul(h ^ s.charCodeAt(i), 2654435769);
|
|
842
|
+
}
|
|
843
|
+
return h >>> 0;
|
|
844
|
+
}
|
|
845
|
+
function mulberry32(seed) {
|
|
846
|
+
let t = seed >>> 0;
|
|
847
|
+
return () => {
|
|
848
|
+
t += 1831565813;
|
|
849
|
+
let r = Math.imul(t ^ t >>> 15, t | 1);
|
|
850
|
+
r ^= r + Math.imul(r ^ r >>> 7, r | 61);
|
|
851
|
+
return ((r ^ r >>> 14) >>> 0) / 4294967296;
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
function fillGaussian2D(out, rng, scale) {
|
|
855
|
+
for (let i = 0; i < out.length; i += 1) out[i] = (rng() * 2 - 1) * scale;
|
|
856
|
+
}
|
|
857
|
+
function microWeightKey(config) {
|
|
858
|
+
return `${config.seed}\0${config.modelHash || ""}`;
|
|
859
|
+
}
|
|
860
|
+
var MICRO_WEIGHT_MAGIC = [72, 77, 87, 49];
|
|
861
|
+
var MICRO_WEIGHT_VERSION = 1;
|
|
862
|
+
var MICRO_WEIGHT_FLOATS = EMBED_DIM * PATCH_LEN + // proj
|
|
863
|
+
3 * EMBED_DIM * EMBED_DIM + // Wq, Wk, Wv
|
|
864
|
+
EMBED_DIM * 3 + // Wxyz
|
|
865
|
+
4 * EMBED_DIM;
|
|
866
|
+
var MICRO_WEIGHT_BYTES = 8 + MICRO_WEIGHT_FLOATS * 4;
|
|
867
|
+
function toUint8(bytes) {
|
|
868
|
+
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
869
|
+
}
|
|
870
|
+
function fnv1a(bytes) {
|
|
871
|
+
let h = 2166136261;
|
|
872
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
873
|
+
h ^= bytes[i];
|
|
874
|
+
h = Math.imul(h, 16777619);
|
|
875
|
+
}
|
|
876
|
+
return (h >>> 0).toString(16);
|
|
877
|
+
}
|
|
878
|
+
function cachedWeightBlobKey(input) {
|
|
879
|
+
const bytes = toUint8(input);
|
|
880
|
+
const cached = weightBlobKeyCache.get(input);
|
|
881
|
+
if (cached) return { bytes, key: cached };
|
|
882
|
+
const key = `${bytes.byteLength}:${fnv1a(bytes)}`;
|
|
883
|
+
weightBlobKeyCache.set(input, key);
|
|
884
|
+
return { bytes, key };
|
|
885
|
+
}
|
|
886
|
+
function serializeMicroWeights(w) {
|
|
887
|
+
const bytes = new Uint8Array(MICRO_WEIGHT_BYTES);
|
|
888
|
+
bytes.set(MICRO_WEIGHT_MAGIC, 0);
|
|
889
|
+
const view = new DataView(bytes.buffer);
|
|
890
|
+
view.setUint32(4, MICRO_WEIGHT_VERSION, true);
|
|
891
|
+
let off = 8;
|
|
892
|
+
const put = (arr) => {
|
|
893
|
+
for (let i = 0; i < arr.length; i += 1) {
|
|
894
|
+
view.setFloat32(off, arr[i], true);
|
|
895
|
+
off += 4;
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
put(w.proj);
|
|
899
|
+
put(w.Wq);
|
|
900
|
+
put(w.Wk);
|
|
901
|
+
put(w.Wv);
|
|
902
|
+
put(w.Wxyz);
|
|
903
|
+
put(w.gamma1);
|
|
904
|
+
put(w.beta1);
|
|
905
|
+
put(w.gamma2);
|
|
906
|
+
put(w.beta2);
|
|
907
|
+
return bytes;
|
|
908
|
+
}
|
|
909
|
+
function deserializeMicroWeights(input) {
|
|
910
|
+
const bytes = toUint8(input);
|
|
911
|
+
if (bytes.byteLength !== MICRO_WEIGHT_BYTES) return null;
|
|
912
|
+
for (let i = 0; i < 4; i += 1) {
|
|
913
|
+
if (bytes[i] !== MICRO_WEIGHT_MAGIC[i]) return null;
|
|
914
|
+
}
|
|
915
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
916
|
+
if (view.getUint32(4, true) !== MICRO_WEIGHT_VERSION) return null;
|
|
917
|
+
let off = 8;
|
|
918
|
+
const take = (count) => {
|
|
919
|
+
const out = new Float32Array(count);
|
|
920
|
+
for (let i = 0; i < count; i += 1) {
|
|
921
|
+
out[i] = view.getFloat32(off, true);
|
|
922
|
+
off += 4;
|
|
923
|
+
}
|
|
924
|
+
return out;
|
|
925
|
+
};
|
|
926
|
+
return {
|
|
927
|
+
proj: take(EMBED_DIM * PATCH_LEN),
|
|
928
|
+
Wq: take(EMBED_DIM * EMBED_DIM),
|
|
929
|
+
Wk: take(EMBED_DIM * EMBED_DIM),
|
|
930
|
+
Wv: take(EMBED_DIM * EMBED_DIM),
|
|
931
|
+
Wxyz: take(EMBED_DIM * 3),
|
|
932
|
+
gamma1: take(EMBED_DIM),
|
|
933
|
+
beta1: take(EMBED_DIM),
|
|
934
|
+
gamma2: take(EMBED_DIM),
|
|
935
|
+
beta2: take(EMBED_DIM)
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function getMicroWeights(config) {
|
|
939
|
+
if (config.weightBytes) {
|
|
940
|
+
const { bytes: wb, key: blobKey } = cachedWeightBlobKey(config.weightBytes);
|
|
941
|
+
const ckptKey = `${microWeightKey(config)}\0ckpt:${blobKey}`;
|
|
942
|
+
const cachedReal = weightCache.get(ckptKey);
|
|
943
|
+
if (cachedReal) return cachedReal;
|
|
944
|
+
const real = deserializeMicroWeights(wb);
|
|
945
|
+
if (real) {
|
|
946
|
+
weightCache.set(ckptKey, real);
|
|
947
|
+
return real;
|
|
948
|
+
}
|
|
949
|
+
console.warn(
|
|
950
|
+
`[holoMapMicroEncoder] weightBytes present (${wb.byteLength} B) but not a valid HMW1 checkpoint (expected ${MICRO_WEIGHT_BYTES} B + magic) \u2014 falling back to PRNG-synthesized weights.`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const key = microWeightKey(config);
|
|
954
|
+
const cached = weightCache.get(key);
|
|
955
|
+
if (cached) return cached;
|
|
956
|
+
const rng = mulberry32(hashConfigSeed(config));
|
|
957
|
+
const weights = {
|
|
958
|
+
proj: new Float32Array(EMBED_DIM * PATCH_LEN),
|
|
959
|
+
Wq: new Float32Array(EMBED_DIM * EMBED_DIM),
|
|
960
|
+
Wk: new Float32Array(EMBED_DIM * EMBED_DIM),
|
|
961
|
+
Wv: new Float32Array(EMBED_DIM * EMBED_DIM),
|
|
962
|
+
Wxyz: new Float32Array(EMBED_DIM * 3),
|
|
963
|
+
gamma1: new Float32Array(EMBED_DIM).fill(1),
|
|
964
|
+
beta1: new Float32Array(EMBED_DIM).fill(0),
|
|
965
|
+
gamma2: new Float32Array(EMBED_DIM).fill(1),
|
|
966
|
+
beta2: new Float32Array(EMBED_DIM).fill(0)
|
|
967
|
+
};
|
|
968
|
+
fillGaussian2D(weights.proj, rng, 0.02);
|
|
969
|
+
fillGaussian2D(weights.Wq, rng, 0.02);
|
|
970
|
+
fillGaussian2D(weights.Wk, rng, 0.02);
|
|
971
|
+
fillGaussian2D(weights.Wv, rng, 0.02);
|
|
972
|
+
fillGaussian2D(weights.Wxyz, rng, 0.05);
|
|
973
|
+
weightCache.set(key, weights);
|
|
974
|
+
return weights;
|
|
975
|
+
}
|
|
976
|
+
function frameToMicroImage(frame) {
|
|
977
|
+
const out = new Float32Array(MICRO * MICRO * 3);
|
|
978
|
+
for (let y = 0; y < MICRO; y += 1) {
|
|
979
|
+
const sy = Math.min(frame.height - 1, Math.floor(y * frame.height / MICRO));
|
|
980
|
+
for (let x = 0; x < MICRO; x += 1) {
|
|
981
|
+
const sx = Math.min(frame.width - 1, Math.floor(x * frame.width / MICRO));
|
|
982
|
+
const src = (sy * frame.width + sx) * frame.stride;
|
|
983
|
+
const dst = (y * MICRO + x) * 3;
|
|
984
|
+
out[dst] = frame.rgb[src] / 255;
|
|
985
|
+
out[dst + 1] = frame.rgb[src + 1] / 255;
|
|
986
|
+
out[dst + 2] = frame.rgb[src + 2] / 255;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return out;
|
|
990
|
+
}
|
|
991
|
+
function patchEmbedCpu(image, proj, embedDim) {
|
|
992
|
+
const out = new Float32Array(embedDim);
|
|
993
|
+
for (let d = 0; d < embedDim; d += 1) {
|
|
994
|
+
let dot = 0;
|
|
995
|
+
const row = d * PATCH_LEN;
|
|
996
|
+
for (let i = 0; i < PATCH_LEN; i += 1) dot += image[i] * proj[row + i];
|
|
997
|
+
out[d] = dot;
|
|
998
|
+
}
|
|
999
|
+
return out;
|
|
1000
|
+
}
|
|
1001
|
+
function gemmCpu(a, b, M, K, N) {
|
|
1002
|
+
const c = new Float32Array(M * N);
|
|
1003
|
+
for (let m = 0; m < M; m += 1) {
|
|
1004
|
+
for (let n = 0; n < N; n += 1) {
|
|
1005
|
+
let acc = 0;
|
|
1006
|
+
for (let k = 0; k < K; k += 1) acc += a[m * K + k] * b[k * N + n];
|
|
1007
|
+
c[m * N + n] = acc;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return c;
|
|
1011
|
+
}
|
|
1012
|
+
function layerNormCpu(x, gamma, beta, rows, d) {
|
|
1013
|
+
const y = new Float32Array(x.length);
|
|
1014
|
+
const eps = 1e-5;
|
|
1015
|
+
for (let r = 0; r < rows; r += 1) {
|
|
1016
|
+
let sum = 0;
|
|
1017
|
+
for (let j = 0; j < d; j += 1) sum += x[r * d + j];
|
|
1018
|
+
const mean = sum / d;
|
|
1019
|
+
let sq = 0;
|
|
1020
|
+
for (let j = 0; j < d; j += 1) {
|
|
1021
|
+
const t = x[r * d + j] - mean;
|
|
1022
|
+
sq += t * t;
|
|
1023
|
+
}
|
|
1024
|
+
const inv = 1 / Math.sqrt(sq / d + eps);
|
|
1025
|
+
for (let j = 0; j < d; j += 1) {
|
|
1026
|
+
const norm = (x[r * d + j] - mean) * inv;
|
|
1027
|
+
y[r * d + j] = norm * gamma[j] + beta[j];
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return y;
|
|
1031
|
+
}
|
|
1032
|
+
function geluCpu(x) {
|
|
1033
|
+
const y = new Float32Array(x.length);
|
|
1034
|
+
const k0 = 0.7978845608;
|
|
1035
|
+
const k1 = 0.044715;
|
|
1036
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
1037
|
+
const v = x[i];
|
|
1038
|
+
const x3 = v * v * v;
|
|
1039
|
+
const inner = k0 * (v + k1 * x3);
|
|
1040
|
+
y[i] = 0.5 * v * (1 + Math.tanh(inner));
|
|
1041
|
+
}
|
|
1042
|
+
return y;
|
|
1043
|
+
}
|
|
1044
|
+
function mhaCpu(Q, K, V, numHeads, qLen, kLen, dHead, vHead) {
|
|
1045
|
+
const out = new Float32Array(numHeads * qLen * vHead);
|
|
1046
|
+
const scale = 1 / Math.sqrt(Math.max(dHead, 1));
|
|
1047
|
+
for (let h = 0; h < numHeads; h += 1) {
|
|
1048
|
+
for (let qi = 0; qi < qLen; qi += 1) {
|
|
1049
|
+
const scores = new Float32Array(kLen);
|
|
1050
|
+
let maxS = -Infinity;
|
|
1051
|
+
const qBase = (h * qLen + qi) * dHead;
|
|
1052
|
+
for (let ki = 0; ki < kLen; ki += 1) {
|
|
1053
|
+
const kBase = (h * kLen + ki) * dHead;
|
|
1054
|
+
let dot = 0;
|
|
1055
|
+
for (let d = 0; d < dHead; d += 1) dot += Q[qBase + d] * K[kBase + d];
|
|
1056
|
+
const s = dot * scale;
|
|
1057
|
+
scores[ki] = s;
|
|
1058
|
+
maxS = Math.max(maxS, s);
|
|
1059
|
+
}
|
|
1060
|
+
let denom = 0;
|
|
1061
|
+
for (let ki = 0; ki < kLen; ki += 1) {
|
|
1062
|
+
scores[ki] = Math.exp(scores[ki] - maxS);
|
|
1063
|
+
denom += scores[ki];
|
|
1064
|
+
}
|
|
1065
|
+
const oBase = (h * qLen + qi) * vHead;
|
|
1066
|
+
for (let vi = 0; vi < vHead; vi += 1) {
|
|
1067
|
+
let acc = 0;
|
|
1068
|
+
for (let ki = 0; ki < kLen; ki += 1) {
|
|
1069
|
+
acc += scores[ki] / Math.max(denom, 1e-9) * V[(h * kLen + ki) * vHead + vi];
|
|
1070
|
+
}
|
|
1071
|
+
out[oBase + vi] = acc;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return out;
|
|
1076
|
+
}
|
|
1077
|
+
function ropeCpuSync(q, seqLen, numHeads, headDim, posOffset) {
|
|
1078
|
+
const out = q.slice();
|
|
1079
|
+
const base = 1e4;
|
|
1080
|
+
for (let t = 0; t < seqLen; t += 1) {
|
|
1081
|
+
const pos = t + posOffset;
|
|
1082
|
+
for (let h = 0; h < numHeads; h += 1) {
|
|
1083
|
+
for (let p = 0; p < headDim / 2; p += 1) {
|
|
1084
|
+
const exp = -2 * (p / headDim);
|
|
1085
|
+
const theta = pos * base ** exp;
|
|
1086
|
+
const c = Math.cos(theta);
|
|
1087
|
+
const s = Math.sin(theta);
|
|
1088
|
+
const i0 = (t * numHeads + h) * headDim + 2 * p;
|
|
1089
|
+
const i1 = i0 + 1;
|
|
1090
|
+
const x0 = q[i0];
|
|
1091
|
+
const x1 = q[i1];
|
|
1092
|
+
out[i0] = x0 * c - x1 * s;
|
|
1093
|
+
out[i1] = x0 * s + x1 * c;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return out;
|
|
1098
|
+
}
|
|
1099
|
+
async function tryCreateHoloMapEncoderDevice() {
|
|
1100
|
+
try {
|
|
1101
|
+
const nav = globalThis.navigator;
|
|
1102
|
+
if (!nav?.gpu) return null;
|
|
1103
|
+
const adapter = await nav.gpu.requestAdapter();
|
|
1104
|
+
if (!adapter) return null;
|
|
1105
|
+
return await adapter.requestDevice();
|
|
1106
|
+
} catch {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function createHoloMapMicroEncoder(device) {
|
|
1111
|
+
const patchEmbed = createImagePatchEmbedKernel(device);
|
|
1112
|
+
const layerNorm = createLayerNormKernel(device);
|
|
1113
|
+
const gemm = createGemmKernel(device);
|
|
1114
|
+
const rope = createRopeKernel(device);
|
|
1115
|
+
const fusedMha = createFusedMHAKernel(device);
|
|
1116
|
+
const gelu = createGeluKernel(device);
|
|
1117
|
+
return {
|
|
1118
|
+
async run(frame, config) {
|
|
1119
|
+
const weights = getMicroWeights(config);
|
|
1120
|
+
const image = frameToMicroImage(frame);
|
|
1121
|
+
let tokens = await patchEmbed.run(image, weights.proj, {
|
|
1122
|
+
imgH: MICRO,
|
|
1123
|
+
imgW: MICRO,
|
|
1124
|
+
patchH: MICRO,
|
|
1125
|
+
patchW: MICRO,
|
|
1126
|
+
numChannels: 3,
|
|
1127
|
+
embedDim: EMBED_DIM
|
|
1128
|
+
});
|
|
1129
|
+
tokens = await layerNorm.run(tokens, weights.gamma1, weights.beta1);
|
|
1130
|
+
const qFlat = await gemm.run(tokens, weights.Wq, 1, EMBED_DIM, EMBED_DIM);
|
|
1131
|
+
const kFlat = await gemm.run(tokens, weights.Wk, 1, EMBED_DIM, EMBED_DIM);
|
|
1132
|
+
const vFlat = await gemm.run(tokens, weights.Wv, 1, EMBED_DIM, EMBED_DIM);
|
|
1133
|
+
const q3 = await rope.run(qFlat, {
|
|
1134
|
+
seqLen: 1,
|
|
1135
|
+
numHeads: NUM_HEADS,
|
|
1136
|
+
headDim: HEAD_DIM,
|
|
1137
|
+
posOffset: frame.index
|
|
1138
|
+
});
|
|
1139
|
+
const k3 = await rope.run(kFlat, {
|
|
1140
|
+
seqLen: 1,
|
|
1141
|
+
numHeads: NUM_HEADS,
|
|
1142
|
+
headDim: HEAD_DIM,
|
|
1143
|
+
posOffset: frame.index
|
|
1144
|
+
});
|
|
1145
|
+
let attn = await fusedMha.run(q3, k3, vFlat, {
|
|
1146
|
+
numHeads: NUM_HEADS,
|
|
1147
|
+
qLen: 1,
|
|
1148
|
+
kLen: 1,
|
|
1149
|
+
dHead: HEAD_DIM,
|
|
1150
|
+
vHead: HEAD_DIM
|
|
1151
|
+
});
|
|
1152
|
+
attn = await layerNorm.run(attn, weights.gamma2, weights.beta2);
|
|
1153
|
+
attn = await gelu.run(attn);
|
|
1154
|
+
return gemm.run(attn, weights.Wxyz, 1, EMBED_DIM, 3);
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
async function runHoloMapMicroEncoderCpu(frame, config) {
|
|
1159
|
+
const weights = getMicroWeights(config);
|
|
1160
|
+
const image = frameToMicroImage(frame);
|
|
1161
|
+
let tokens = patchEmbedCpu(image, weights.proj, EMBED_DIM);
|
|
1162
|
+
tokens = layerNormCpu(tokens, weights.gamma1, weights.beta1, 1, EMBED_DIM);
|
|
1163
|
+
const qFlat = gemmCpu(tokens, weights.Wq, 1, EMBED_DIM, EMBED_DIM);
|
|
1164
|
+
const kFlat = gemmCpu(tokens, weights.Wk, 1, EMBED_DIM, EMBED_DIM);
|
|
1165
|
+
const vFlat = gemmCpu(tokens, weights.Wv, 1, EMBED_DIM, EMBED_DIM);
|
|
1166
|
+
const qR = ropeCpuSync(qFlat, 1, NUM_HEADS, HEAD_DIM, frame.index);
|
|
1167
|
+
const kR = ropeCpuSync(kFlat, 1, NUM_HEADS, HEAD_DIM, frame.index);
|
|
1168
|
+
let attn = mhaCpu(qR, kR, vFlat, NUM_HEADS, 1, 1, HEAD_DIM, HEAD_DIM);
|
|
1169
|
+
attn = layerNormCpu(attn, weights.gamma2, weights.beta2, 1, EMBED_DIM);
|
|
1170
|
+
attn = geluCpu(attn);
|
|
1171
|
+
return gemmCpu(attn, weights.Wxyz, 1, EMBED_DIM, 3);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/reconstruction/holoMapTelemetry.ts
|
|
1175
|
+
var highVolumeEvents = /* @__PURE__ */ new Set(["step", "step_throttled", "step_evict"]);
|
|
1176
|
+
var pendingStepSummaries = /* @__PURE__ */ new Map();
|
|
1177
|
+
function loggingDisabled() {
|
|
1178
|
+
try {
|
|
1179
|
+
return typeof process !== "undefined" && process.env?.HOLOMAP_LOG === "0";
|
|
1180
|
+
} catch {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function verboseStepLoggingEnabled() {
|
|
1185
|
+
try {
|
|
1186
|
+
if (typeof process === "undefined") {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
const value = process.env?.HOLOMAP_LOG?.toLowerCase();
|
|
1190
|
+
return value === "verbose" || value === "debug" || process.env?.HOLOMAP_LOG_STEPS === "1";
|
|
1191
|
+
} catch {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function numericDetail(detail, key) {
|
|
1196
|
+
const value = detail?.[key];
|
|
1197
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1198
|
+
}
|
|
1199
|
+
function getOrCreateStepSummary(runId) {
|
|
1200
|
+
const existing = pendingStepSummaries.get(runId);
|
|
1201
|
+
if (existing) {
|
|
1202
|
+
return existing;
|
|
1203
|
+
}
|
|
1204
|
+
const summary = {
|
|
1205
|
+
stepCount: 0,
|
|
1206
|
+
firstFrameIndex: null,
|
|
1207
|
+
lastFrameIndex: null,
|
|
1208
|
+
pointCountTotal: 0,
|
|
1209
|
+
totalStepMs: 0,
|
|
1210
|
+
maxStepMs: 0,
|
|
1211
|
+
minStepMs: Infinity,
|
|
1212
|
+
throttledCount: 0,
|
|
1213
|
+
evictionCount: 0,
|
|
1214
|
+
evictedPointTotal: 0,
|
|
1215
|
+
lastThrottledFrameIndex: null,
|
|
1216
|
+
lastEvictedFrameIndex: null,
|
|
1217
|
+
lastRetainedSteps: null,
|
|
1218
|
+
latestAvgStepMs: null,
|
|
1219
|
+
latestThrottledSoFar: null
|
|
1220
|
+
};
|
|
1221
|
+
pendingStepSummaries.set(runId, summary);
|
|
1222
|
+
return summary;
|
|
1223
|
+
}
|
|
1224
|
+
function accumulateStepSummary(runId, event, detail) {
|
|
1225
|
+
const summary = getOrCreateStepSummary(runId);
|
|
1226
|
+
const frameIndex = numericDetail(detail, "frameIndex");
|
|
1227
|
+
if (event === "step") {
|
|
1228
|
+
summary.stepCount += 1;
|
|
1229
|
+
if (frameIndex !== null) {
|
|
1230
|
+
summary.firstFrameIndex = summary.firstFrameIndex ?? frameIndex;
|
|
1231
|
+
summary.lastFrameIndex = frameIndex;
|
|
1232
|
+
}
|
|
1233
|
+
summary.pointCountTotal += numericDetail(detail, "pointCount") ?? 0;
|
|
1234
|
+
const stepMs = numericDetail(detail, "stepMs");
|
|
1235
|
+
if (stepMs !== null) {
|
|
1236
|
+
summary.totalStepMs += stepMs;
|
|
1237
|
+
summary.maxStepMs = Math.max(summary.maxStepMs, stepMs);
|
|
1238
|
+
summary.minStepMs = Math.min(summary.minStepMs, stepMs);
|
|
1239
|
+
}
|
|
1240
|
+
summary.latestAvgStepMs = numericDetail(detail, "avgStepMs") ?? summary.latestAvgStepMs;
|
|
1241
|
+
summary.latestThrottledSoFar = numericDetail(detail, "throttledSoFar") ?? summary.latestThrottledSoFar;
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (event === "step_throttled") {
|
|
1245
|
+
summary.throttledCount += 1;
|
|
1246
|
+
summary.lastThrottledFrameIndex = frameIndex ?? summary.lastThrottledFrameIndex;
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
summary.evictionCount += 1;
|
|
1250
|
+
summary.lastEvictedFrameIndex = frameIndex ?? summary.lastEvictedFrameIndex;
|
|
1251
|
+
summary.evictedPointTotal += numericDetail(detail, "evictedPoints") ?? 0;
|
|
1252
|
+
summary.lastRetainedSteps = numericDetail(detail, "retainedSteps") ?? summary.lastRetainedSteps;
|
|
1253
|
+
}
|
|
1254
|
+
function emitLogPayload(runId, event, detail) {
|
|
1255
|
+
const payload = {
|
|
1256
|
+
t: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1257
|
+
runId,
|
|
1258
|
+
event,
|
|
1259
|
+
...detail
|
|
1260
|
+
};
|
|
1261
|
+
console.log(`[HoloMap] ${JSON.stringify(payload)}`);
|
|
1262
|
+
}
|
|
1263
|
+
function flushStepSummary(runId, reason) {
|
|
1264
|
+
const summary = pendingStepSummaries.get(runId);
|
|
1265
|
+
if (!summary) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
pendingStepSummaries.delete(runId);
|
|
1269
|
+
const throttledCount = Math.max(summary.throttledCount, summary.latestThrottledSoFar ?? 0);
|
|
1270
|
+
emitLogPayload(runId, "step_summary", {
|
|
1271
|
+
reason,
|
|
1272
|
+
stepCount: summary.stepCount,
|
|
1273
|
+
firstFrameIndex: summary.firstFrameIndex,
|
|
1274
|
+
lastFrameIndex: summary.lastFrameIndex,
|
|
1275
|
+
pointCountTotal: summary.pointCountTotal,
|
|
1276
|
+
avgStepMs: summary.stepCount > 0 ? Math.round(summary.totalStepMs / summary.stepCount) : summary.latestAvgStepMs ?? 0,
|
|
1277
|
+
maxStepMs: summary.stepCount > 0 ? Math.round(summary.maxStepMs) : 0,
|
|
1278
|
+
minStepMs: summary.stepCount > 0 ? Math.round(summary.minStepMs) : 0,
|
|
1279
|
+
throttledCount,
|
|
1280
|
+
evictionCount: summary.evictionCount,
|
|
1281
|
+
evictedPointTotal: summary.evictedPointTotal,
|
|
1282
|
+
lastThrottledFrameIndex: summary.lastThrottledFrameIndex,
|
|
1283
|
+
lastEvictedFrameIndex: summary.lastEvictedFrameIndex,
|
|
1284
|
+
lastRetainedSteps: summary.lastRetainedSteps
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
function createHoloMapRunId() {
|
|
1288
|
+
try {
|
|
1289
|
+
const c = globalThis.crypto;
|
|
1290
|
+
if (c && "randomUUID" in c) {
|
|
1291
|
+
return `hm_${c.randomUUID().slice(0, 8)}`;
|
|
1292
|
+
}
|
|
1293
|
+
} catch {
|
|
1294
|
+
}
|
|
1295
|
+
return `hm_${Math.random().toString(36).slice(2, 10)}`;
|
|
1296
|
+
}
|
|
1297
|
+
function logHoloMapEvent(runId, event, detail) {
|
|
1298
|
+
if (loggingDisabled()) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (highVolumeEvents.has(event) && !verboseStepLoggingEnabled()) {
|
|
1302
|
+
accumulateStepSummary(runId, event, detail);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (event === "finalize" || event === "dispose" || event === "error") {
|
|
1306
|
+
flushStepSummary(runId, event);
|
|
1307
|
+
}
|
|
1308
|
+
emitLogPayload(runId, event, detail);
|
|
1309
|
+
}
|
|
1310
|
+
function resetHoloMapTelemetryForTests() {
|
|
1311
|
+
pendingStepSummaries.clear();
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/reconstruction/holoMapWeightLoader.ts
|
|
1315
|
+
async function sha256Digest(bytes) {
|
|
1316
|
+
const c = globalThis.crypto;
|
|
1317
|
+
if (c?.subtle) {
|
|
1318
|
+
const buf = await c.subtle.digest(
|
|
1319
|
+
"SHA-256",
|
|
1320
|
+
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
|
|
1321
|
+
);
|
|
1322
|
+
return bufferToHex(new Uint8Array(buf));
|
|
1323
|
+
}
|
|
1324
|
+
const { createHash } = await import('crypto');
|
|
1325
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
1326
|
+
}
|
|
1327
|
+
function bufferToHex(bytes) {
|
|
1328
|
+
let s = "";
|
|
1329
|
+
for (let i = 0; i < bytes.length; i += 1) s += bytes[i].toString(16).padStart(2, "0");
|
|
1330
|
+
return s;
|
|
1331
|
+
}
|
|
1332
|
+
function normalizeWeightCidDigest(cid) {
|
|
1333
|
+
const t = cid.trim().toLowerCase();
|
|
1334
|
+
if (t.startsWith("sha256:")) return t.slice("sha256:".length);
|
|
1335
|
+
return t;
|
|
1336
|
+
}
|
|
1337
|
+
async function assertWeightCid(bytes, weightCid) {
|
|
1338
|
+
const expect = normalizeWeightCidDigest(weightCid);
|
|
1339
|
+
if (!/^[0-9a-f]{64}$/.test(expect)) {
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
`HoloMap weights: weightCid must be sha256:hex64 or 64-char hex (got "${weightCid.slice(0, 24)}...")`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
const actual = await sha256Digest(bytes);
|
|
1345
|
+
if (actual !== expect) {
|
|
1346
|
+
throw new Error(
|
|
1347
|
+
`HoloMap weights: digest mismatch \u2014 expected ${expect.slice(0, 12)}\u2026 got ${actual.slice(0, 12)}\u2026`
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
function isFileUrl(url) {
|
|
1352
|
+
return url.startsWith("file://");
|
|
1353
|
+
}
|
|
1354
|
+
async function readFileUrl(url) {
|
|
1355
|
+
const { readFile } = await import('fs/promises');
|
|
1356
|
+
const path = url.slice("file://".length);
|
|
1357
|
+
const buf = await readFile(path);
|
|
1358
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
1359
|
+
}
|
|
1360
|
+
async function sleep(ms) {
|
|
1361
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1362
|
+
}
|
|
1363
|
+
async function loadHoloMapWeightBlob(options) {
|
|
1364
|
+
const { weightUrl, weightUrls = [], weightCid, fetchImpl, skipCache, localResolver } = options;
|
|
1365
|
+
const f = fetchImpl ?? globalThis.fetch;
|
|
1366
|
+
if (localResolver && weightCid) {
|
|
1367
|
+
try {
|
|
1368
|
+
const local = await localResolver(weightCid);
|
|
1369
|
+
if (local) {
|
|
1370
|
+
await assertWeightCid(new Uint8Array(local), weightCid);
|
|
1371
|
+
await putCachedWeightBlob(weightCid, local);
|
|
1372
|
+
return { bytes: local, verified: true, source: "file" };
|
|
1373
|
+
}
|
|
1374
|
+
} catch {
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (weightCid && !skipCache) {
|
|
1378
|
+
const cached = await getCachedWeightBlob(weightCid);
|
|
1379
|
+
if (cached) {
|
|
1380
|
+
return { bytes: cached, verified: true, source: "cache" };
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
const candidates = [weightUrl, ...weightUrls];
|
|
1384
|
+
for (const url of candidates) {
|
|
1385
|
+
if (isFileUrl(url)) {
|
|
1386
|
+
try {
|
|
1387
|
+
const buf = await readFileUrl(url);
|
|
1388
|
+
if (weightCid) {
|
|
1389
|
+
await assertWeightCid(new Uint8Array(buf), weightCid);
|
|
1390
|
+
}
|
|
1391
|
+
if (weightCid) {
|
|
1392
|
+
await putCachedWeightBlob(weightCid, buf);
|
|
1393
|
+
}
|
|
1394
|
+
return { bytes: buf, verified: !!weightCid, source: "file" };
|
|
1395
|
+
} catch {
|
|
1396
|
+
}
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (typeof f !== "function") {
|
|
1400
|
+
throw new Error("HoloMap weights: fetch is not available (provide fetchImpl)");
|
|
1401
|
+
}
|
|
1402
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1403
|
+
try {
|
|
1404
|
+
const res = await f(url);
|
|
1405
|
+
if (!res.ok) {
|
|
1406
|
+
throw new Error(`GET ${url} failed (${res.status} ${res.statusText})`);
|
|
1407
|
+
}
|
|
1408
|
+
const buf = await res.arrayBuffer();
|
|
1409
|
+
if (weightCid) {
|
|
1410
|
+
await assertWeightCid(new Uint8Array(buf), weightCid);
|
|
1411
|
+
}
|
|
1412
|
+
if (weightCid) {
|
|
1413
|
+
await putCachedWeightBlob(weightCid, buf);
|
|
1414
|
+
}
|
|
1415
|
+
return { bytes: buf, verified: !!weightCid, source: "network" };
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
if (attempt < 2) {
|
|
1418
|
+
await sleep(250 * (attempt + 1));
|
|
1419
|
+
} else {
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
throw new Error(`HoloMap weights: all sources exhausted (tried ${candidates.length} URL(s))`);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/reconstruction/holoMapWeightCache.ts
|
|
1429
|
+
var DB_NAME = "HoloMapWeightCache";
|
|
1430
|
+
var STORE_NAME = "weights";
|
|
1431
|
+
var DB_VERSION = 1;
|
|
1432
|
+
async function openIndexedDB() {
|
|
1433
|
+
return new Promise((resolve, reject) => {
|
|
1434
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
1435
|
+
req.onerror = () => reject(req.error);
|
|
1436
|
+
req.onsuccess = () => resolve(req.result);
|
|
1437
|
+
req.onupgradeneeded = () => {
|
|
1438
|
+
req.result.createObjectStore(STORE_NAME);
|
|
1439
|
+
};
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
async function idbGet(db, key) {
|
|
1443
|
+
return new Promise((resolve, reject) => {
|
|
1444
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
1445
|
+
const store = tx.objectStore(STORE_NAME);
|
|
1446
|
+
const req = store.get(key);
|
|
1447
|
+
req.onsuccess = () => {
|
|
1448
|
+
const result = req.result;
|
|
1449
|
+
if (result instanceof ArrayBuffer) resolve(result);
|
|
1450
|
+
else if (result && typeof result.byteLength === "number") resolve(result);
|
|
1451
|
+
else resolve(void 0);
|
|
1452
|
+
};
|
|
1453
|
+
req.onerror = () => reject(req.error);
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
async function idbPut(db, key, value) {
|
|
1457
|
+
return new Promise((resolve, reject) => {
|
|
1458
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
1459
|
+
const store = tx.objectStore(STORE_NAME);
|
|
1460
|
+
const req = store.put(value, key);
|
|
1461
|
+
req.onsuccess = () => resolve();
|
|
1462
|
+
req.onerror = () => reject(req.error);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
var nodeCacheDeps = null;
|
|
1466
|
+
async function getNodeCacheDeps() {
|
|
1467
|
+
if (nodeCacheDeps) return nodeCacheDeps;
|
|
1468
|
+
try {
|
|
1469
|
+
const fs = await import('fs/promises');
|
|
1470
|
+
const os = await import('os');
|
|
1471
|
+
const path = await import('path');
|
|
1472
|
+
nodeCacheDeps = {
|
|
1473
|
+
readFile: (p) => fs.readFile(p),
|
|
1474
|
+
writeFile: (p, d) => fs.writeFile(p, d),
|
|
1475
|
+
mkdir: (p, o) => fs.mkdir(p, o),
|
|
1476
|
+
stat: (p) => fs.stat(p),
|
|
1477
|
+
tmpdir: () => os.tmpdir(),
|
|
1478
|
+
homedir: () => os.homedir()
|
|
1479
|
+
};
|
|
1480
|
+
return nodeCacheDeps;
|
|
1481
|
+
} catch {
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
async function nodeCachePath(deps, cid) {
|
|
1486
|
+
const path = await import('path');
|
|
1487
|
+
const base = process.env.HOLOMAP_WEIGHT_CACHE_DIR ?? path.join(deps.homedir(), ".cache", "holoscript", "holomap-weights");
|
|
1488
|
+
return path.join(base, `${cid}.bin`);
|
|
1489
|
+
}
|
|
1490
|
+
async function nodeCacheGet(deps, cid) {
|
|
1491
|
+
const p = await nodeCachePath(deps, cid);
|
|
1492
|
+
try {
|
|
1493
|
+
const buf = await deps.readFile(p);
|
|
1494
|
+
const copy = new ArrayBuffer(buf.byteLength);
|
|
1495
|
+
new Uint8Array(copy).set(buf);
|
|
1496
|
+
return copy;
|
|
1497
|
+
} catch {
|
|
1498
|
+
return void 0;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
async function nodeCachePut(deps, cid, value) {
|
|
1502
|
+
const p = await nodeCachePath(deps, cid);
|
|
1503
|
+
const path = await import('path');
|
|
1504
|
+
const dir = path.dirname(p);
|
|
1505
|
+
await deps.mkdir(dir, { recursive: true });
|
|
1506
|
+
await deps.writeFile(p, new Uint8Array(value));
|
|
1507
|
+
}
|
|
1508
|
+
function isIndexedDBAvailable() {
|
|
1509
|
+
return typeof indexedDB !== "undefined";
|
|
1510
|
+
}
|
|
1511
|
+
async function getCachedWeightBlob(weightCid) {
|
|
1512
|
+
const cid = normalizeWeightCidDigest(weightCid);
|
|
1513
|
+
if (isIndexedDBAvailable()) {
|
|
1514
|
+
try {
|
|
1515
|
+
const db = await openIndexedDB();
|
|
1516
|
+
const cached = await idbGet(db, cid);
|
|
1517
|
+
db.close();
|
|
1518
|
+
return cached;
|
|
1519
|
+
} catch {
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
const deps = await getNodeCacheDeps();
|
|
1523
|
+
if (deps) {
|
|
1524
|
+
return nodeCacheGet(deps, cid);
|
|
1525
|
+
}
|
|
1526
|
+
return void 0;
|
|
1527
|
+
}
|
|
1528
|
+
async function putCachedWeightBlob(weightCid, value) {
|
|
1529
|
+
const cid = normalizeWeightCidDigest(weightCid);
|
|
1530
|
+
if (isIndexedDBAvailable()) {
|
|
1531
|
+
try {
|
|
1532
|
+
const db = await openIndexedDB();
|
|
1533
|
+
await idbPut(db, cid, value);
|
|
1534
|
+
db.close();
|
|
1535
|
+
return;
|
|
1536
|
+
} catch {
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
const deps = await getNodeCacheDeps();
|
|
1540
|
+
if (deps) {
|
|
1541
|
+
await nodeCachePut(deps, cid, value);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// src/reconstruction/holoMapAnchoredManifest.ts
|
|
1546
|
+
function mergeAnchoredProvenance(manifest, urls) {
|
|
1547
|
+
return {
|
|
1548
|
+
...manifest,
|
|
1549
|
+
provenance: {
|
|
1550
|
+
...manifest.provenance,
|
|
1551
|
+
anchorHash: urls.anchorHash,
|
|
1552
|
+
opentimestampsProof: urls.opentimestampsProof,
|
|
1553
|
+
baseCalldataTx: urls.baseCalldataTx,
|
|
1554
|
+
capturedAtIso: manifest.provenance.capturedAtIso
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
function stripAnchoredProvenance(manifest) {
|
|
1559
|
+
return {
|
|
1560
|
+
...manifest,
|
|
1561
|
+
provenance: {
|
|
1562
|
+
capturedAtIso: manifest.provenance.capturedAtIso
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function selfAttestReconstructionManifest(manifest) {
|
|
1567
|
+
const stripped = stripAnchoredProvenance(manifest);
|
|
1568
|
+
return {
|
|
1569
|
+
...stripped,
|
|
1570
|
+
provenance: {
|
|
1571
|
+
...stripped.provenance,
|
|
1572
|
+
anchorHash: `self-attested:${stripped.replayHash}`
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
async function computeReconstructionManifestDigest(manifest) {
|
|
1577
|
+
const canonical = stableStringify(stripAnchoredProvenance(manifest));
|
|
1578
|
+
return `sha256:${await sha256Hex(canonical)}`;
|
|
1579
|
+
}
|
|
1580
|
+
async function anchorReconstructionManifest(manifest, provider) {
|
|
1581
|
+
const stripped = stripAnchoredProvenance(manifest);
|
|
1582
|
+
if (!provider) return selfAttestReconstructionManifest(stripped);
|
|
1583
|
+
const manifestDigest = await computeReconstructionManifestDigest(stripped);
|
|
1584
|
+
const urls = await provider.anchorManifest({
|
|
1585
|
+
manifestDigest,
|
|
1586
|
+
replayHash: stripped.replayHash,
|
|
1587
|
+
replayFingerprint: stripped.simulationContract.replayFingerprint,
|
|
1588
|
+
capturedAtIso: stripped.provenance.capturedAtIso,
|
|
1589
|
+
manifest: stripped
|
|
1590
|
+
});
|
|
1591
|
+
if (!urls) return selfAttestReconstructionManifest(stripped);
|
|
1592
|
+
return mergeAnchoredProvenance(stripped, urls);
|
|
1593
|
+
}
|
|
1594
|
+
function stableStringify(value) {
|
|
1595
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1596
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
1597
|
+
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a.localeCompare(b));
|
|
1598
|
+
return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`).join(",")}}`;
|
|
1599
|
+
}
|
|
1600
|
+
async function sha256Hex(input) {
|
|
1601
|
+
const bytes = new TextEncoder().encode(input);
|
|
1602
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1603
|
+
if (subtle) {
|
|
1604
|
+
const digest = await subtle.digest("SHA-256", bytes);
|
|
1605
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1606
|
+
}
|
|
1607
|
+
const { createHash } = await import('crypto');
|
|
1608
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/reconstruction/pagedKVKernels.ts
|
|
1612
|
+
var WGSL_PAGED_KV_APPEND = `
|
|
1613
|
+
struct AppendParams {
|
|
1614
|
+
numVecs: u32, // number of NEW vectors to append
|
|
1615
|
+
headDim: u32, // dimension of each vector
|
|
1616
|
+
pageSize: u32, // vectors per page
|
|
1617
|
+
}
|
|
1618
|
+
// kv_pages: flat storage for all pages. page p, slot s, dim d:
|
|
1619
|
+
// index = pageTable[p] * pageSize * headDim + s * headDim + d
|
|
1620
|
+
@group(0) @binding(0) var<storage, read> newVecs: array<f32>; // [numVecs * headDim]
|
|
1621
|
+
@group(0) @binding(1) var<storage, read> pageTable: array<u32>; // logical page \u2192 base offset in kv_pages (in elements)
|
|
1622
|
+
@group(0) @binding(2) var<storage, read> slotMap: array<u32>; // per-vector: (logicalPage << 16 | slotInPage)
|
|
1623
|
+
@group(0) @binding(3) var<storage, read_write> kvPages: array<f32>; // output KV store
|
|
1624
|
+
@group(0) @binding(4) var<uniform> p: AppendParams;
|
|
1625
|
+
@compute @workgroup_size(64, 1, 1)
|
|
1626
|
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
1627
|
+
// gid.x spans numVecs * headDim
|
|
1628
|
+
let total = p.numVecs * p.headDim;
|
|
1629
|
+
if (gid.x >= total) { return; }
|
|
1630
|
+
let vecIdx = gid.x / p.headDim;
|
|
1631
|
+
let dimIdx = gid.x % p.headDim;
|
|
1632
|
+
let slot = slotMap[vecIdx];
|
|
1633
|
+
let logPage = slot >> 16u;
|
|
1634
|
+
let inPage = slot & 0xFFFFu;
|
|
1635
|
+
let base = pageTable[logPage] * p.pageSize * p.headDim;
|
|
1636
|
+
kvPages[base + inPage * p.headDim + dimIdx] = newVecs[vecIdx * p.headDim + dimIdx];
|
|
1637
|
+
}
|
|
1638
|
+
`;
|
|
1639
|
+
var WGSL_PAGED_KV_LOOKUP = `
|
|
1640
|
+
struct LookupParams {
|
|
1641
|
+
numVecs: u32, // number of vectors to read
|
|
1642
|
+
headDim: u32,
|
|
1643
|
+
pageSize: u32,
|
|
1644
|
+
startSlot: u32, // logical first slot (token offset)
|
|
1645
|
+
}
|
|
1646
|
+
@group(0) @binding(0) var<storage, read> kvPages: array<f32>;
|
|
1647
|
+
@group(0) @binding(1) var<storage, read> pageTable: array<u32>; // logical page \u2192 base offset (in elements)
|
|
1648
|
+
@group(0) @binding(2) var<storage, read_write> out: array<f32>; // [numVecs * headDim]
|
|
1649
|
+
@group(0) @binding(3) var<uniform> p: LookupParams;
|
|
1650
|
+
@compute @workgroup_size(64, 1, 1)
|
|
1651
|
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
1652
|
+
let total = p.numVecs * p.headDim;
|
|
1653
|
+
if (gid.x >= total) { return; }
|
|
1654
|
+
let vecIdx = gid.x / p.headDim;
|
|
1655
|
+
let dimIdx = gid.x % p.headDim;
|
|
1656
|
+
let logSlot = (vecIdx + p.startSlot);
|
|
1657
|
+
let logPage = logSlot / p.pageSize;
|
|
1658
|
+
let inPage = logSlot % p.pageSize;
|
|
1659
|
+
let base = pageTable[logPage] * p.pageSize * p.headDim;
|
|
1660
|
+
out[gid.x] = kvPages[base + inPage * p.headDim + dimIdx];
|
|
1661
|
+
}
|
|
1662
|
+
`;
|
|
1663
|
+
function storageBuffer4(device, data) {
|
|
1664
|
+
const buf = device.createBuffer({
|
|
1665
|
+
size: data.byteLength,
|
|
1666
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
1667
|
+
});
|
|
1668
|
+
device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
|
|
1669
|
+
return buf;
|
|
1670
|
+
}
|
|
1671
|
+
function outputBuffer(device, bytes) {
|
|
1672
|
+
return device.createBuffer({
|
|
1673
|
+
size: bytes,
|
|
1674
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
function uniformBuffer(device, data) {
|
|
1678
|
+
const buf = device.createBuffer({
|
|
1679
|
+
size: data.byteLength,
|
|
1680
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
1681
|
+
});
|
|
1682
|
+
device.queue.writeBuffer(buf, 0, data);
|
|
1683
|
+
return buf;
|
|
1684
|
+
}
|
|
1685
|
+
async function readback(device, src, bytes) {
|
|
1686
|
+
const staging = device.createBuffer({
|
|
1687
|
+
size: bytes,
|
|
1688
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
1689
|
+
});
|
|
1690
|
+
const enc = device.createCommandEncoder();
|
|
1691
|
+
enc.copyBufferToBuffer(src, 0, staging, 0, bytes);
|
|
1692
|
+
device.queue.submit([enc.finish()]);
|
|
1693
|
+
await staging.mapAsync(GPUMapMode.READ);
|
|
1694
|
+
const result = new Float32Array(staging.getMappedRange().slice(0));
|
|
1695
|
+
staging.unmap();
|
|
1696
|
+
staging.destroy();
|
|
1697
|
+
return result;
|
|
1698
|
+
}
|
|
1699
|
+
function createPagedKVAppendKernel(device) {
|
|
1700
|
+
const shader = device.createShaderModule({ code: WGSL_PAGED_KV_APPEND });
|
|
1701
|
+
const pipeline = device.createComputePipeline({
|
|
1702
|
+
layout: "auto",
|
|
1703
|
+
compute: { module: shader, entryPoint: "main" }
|
|
1704
|
+
});
|
|
1705
|
+
return {
|
|
1706
|
+
async run(kvPagesData, pageTableData, slotMapData, newVectors, pageSize, headDim) {
|
|
1707
|
+
const numVecs = newVectors.length / headDim;
|
|
1708
|
+
const kvBuf = device.createBuffer({
|
|
1709
|
+
size: kvPagesData.byteLength,
|
|
1710
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
|
|
1711
|
+
});
|
|
1712
|
+
device.queue.writeBuffer(
|
|
1713
|
+
kvBuf,
|
|
1714
|
+
0,
|
|
1715
|
+
kvPagesData.buffer,
|
|
1716
|
+
kvPagesData.byteOffset,
|
|
1717
|
+
kvPagesData.byteLength
|
|
1718
|
+
);
|
|
1719
|
+
const newVecsBuf = storageBuffer4(device, newVectors);
|
|
1720
|
+
const pageTableBuf = storageBuffer4(device, pageTableData);
|
|
1721
|
+
const slotMapBuf = storageBuffer4(device, slotMapData);
|
|
1722
|
+
const paramAB = new ArrayBuffer(16);
|
|
1723
|
+
new Uint32Array(paramAB)[0] = numVecs;
|
|
1724
|
+
new Uint32Array(paramAB)[1] = headDim;
|
|
1725
|
+
new Uint32Array(paramAB)[2] = pageSize;
|
|
1726
|
+
const paramsBuf = uniformBuffer(device, paramAB);
|
|
1727
|
+
const bg = device.createBindGroup({
|
|
1728
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
1729
|
+
entries: [
|
|
1730
|
+
{ binding: 0, resource: { buffer: newVecsBuf } },
|
|
1731
|
+
{ binding: 1, resource: { buffer: pageTableBuf } },
|
|
1732
|
+
{ binding: 2, resource: { buffer: slotMapBuf } },
|
|
1733
|
+
{ binding: 3, resource: { buffer: kvBuf } },
|
|
1734
|
+
{ binding: 4, resource: { buffer: paramsBuf } }
|
|
1735
|
+
]
|
|
1736
|
+
});
|
|
1737
|
+
const total = numVecs * headDim;
|
|
1738
|
+
const workgroups = Math.ceil(total / 64);
|
|
1739
|
+
const enc = device.createCommandEncoder();
|
|
1740
|
+
const pass = enc.beginComputePass();
|
|
1741
|
+
pass.setPipeline(pipeline);
|
|
1742
|
+
pass.setBindGroup(0, bg);
|
|
1743
|
+
pass.dispatchWorkgroups(workgroups);
|
|
1744
|
+
pass.end();
|
|
1745
|
+
device.queue.submit([enc.finish()]);
|
|
1746
|
+
const result = await readback(device, kvBuf, kvPagesData.byteLength);
|
|
1747
|
+
newVecsBuf.destroy();
|
|
1748
|
+
pageTableBuf.destroy();
|
|
1749
|
+
slotMapBuf.destroy();
|
|
1750
|
+
kvBuf.destroy();
|
|
1751
|
+
paramsBuf.destroy();
|
|
1752
|
+
return result;
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
function createPagedKVLookupKernel(device) {
|
|
1757
|
+
const shader = device.createShaderModule({ code: WGSL_PAGED_KV_LOOKUP });
|
|
1758
|
+
const pipeline = device.createComputePipeline({
|
|
1759
|
+
layout: "auto",
|
|
1760
|
+
compute: { module: shader, entryPoint: "main" }
|
|
1761
|
+
});
|
|
1762
|
+
return {
|
|
1763
|
+
async run(kvPagesData, pageTableData, numVecs, startSlot, pageSize, headDim) {
|
|
1764
|
+
const outBytes = numVecs * headDim * 4;
|
|
1765
|
+
const kvBuf = storageBuffer4(device, kvPagesData);
|
|
1766
|
+
const pageTableBuf = storageBuffer4(device, pageTableData);
|
|
1767
|
+
const outBuf = outputBuffer(device, outBytes);
|
|
1768
|
+
const paramAB = new ArrayBuffer(16);
|
|
1769
|
+
const pu = new Uint32Array(paramAB);
|
|
1770
|
+
pu[0] = numVecs;
|
|
1771
|
+
pu[1] = headDim;
|
|
1772
|
+
pu[2] = pageSize;
|
|
1773
|
+
pu[3] = startSlot;
|
|
1774
|
+
const paramsBuf = uniformBuffer(device, paramAB);
|
|
1775
|
+
const bg = device.createBindGroup({
|
|
1776
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
1777
|
+
entries: [
|
|
1778
|
+
{ binding: 0, resource: { buffer: kvBuf } },
|
|
1779
|
+
{ binding: 1, resource: { buffer: pageTableBuf } },
|
|
1780
|
+
{ binding: 2, resource: { buffer: outBuf } },
|
|
1781
|
+
{ binding: 3, resource: { buffer: paramsBuf } }
|
|
1782
|
+
]
|
|
1783
|
+
});
|
|
1784
|
+
const total = numVecs * headDim;
|
|
1785
|
+
const enc = device.createCommandEncoder();
|
|
1786
|
+
const pass = enc.beginComputePass();
|
|
1787
|
+
pass.setPipeline(pipeline);
|
|
1788
|
+
pass.setBindGroup(0, bg);
|
|
1789
|
+
pass.dispatchWorkgroups(Math.ceil(total / 64));
|
|
1790
|
+
pass.end();
|
|
1791
|
+
device.queue.submit([enc.finish()]);
|
|
1792
|
+
const result = await readback(device, outBuf, outBytes);
|
|
1793
|
+
kvBuf.destroy();
|
|
1794
|
+
pageTableBuf.destroy();
|
|
1795
|
+
outBuf.destroy();
|
|
1796
|
+
paramsBuf.destroy();
|
|
1797
|
+
return result;
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// src/reconstruction/PagedKVCache.ts
|
|
1803
|
+
var PAGE_TABLE_EMPTY = 4294967295;
|
|
1804
|
+
function createPagedKVCache(config) {
|
|
1805
|
+
validateConfig(config);
|
|
1806
|
+
const layers = Array.from({ length: config.numLayers }, () => ({
|
|
1807
|
+
pages: /* @__PURE__ */ new Map(),
|
|
1808
|
+
nextToken: 0
|
|
1809
|
+
}));
|
|
1810
|
+
const pageBytes = config.pageSize * config.hiddenDim * Float32Array.BYTES_PER_ELEMENT;
|
|
1811
|
+
const keyPages = new Float32Array(config.maxResidentPages * config.pageSize * config.hiddenDim);
|
|
1812
|
+
const valuePages = new Float32Array(config.maxResidentPages * config.pageSize * config.hiddenDim);
|
|
1813
|
+
const freePhysicalPages = Array.from({ length: config.maxResidentPages }, (_, i) => i).reverse();
|
|
1814
|
+
const appendKernel = config.device ? createPagedKVAppendKernel(config.device) : null;
|
|
1815
|
+
const lookupKernel = config.device ? createPagedKVLookupKernel(config.device) : null;
|
|
1816
|
+
const residentPageStates = /* @__PURE__ */ new Set();
|
|
1817
|
+
let disposed = false;
|
|
1818
|
+
let nextPageId = 0;
|
|
1819
|
+
let accessClock = 0;
|
|
1820
|
+
let devicePageCount = 0;
|
|
1821
|
+
let hostPageCount = 0;
|
|
1822
|
+
function assertLive() {
|
|
1823
|
+
if (disposed) throw new Error("PagedKVCache has been disposed.");
|
|
1824
|
+
}
|
|
1825
|
+
function validateLayer(layer) {
|
|
1826
|
+
if (!Number.isInteger(layer) || layer < 0 || layer >= config.numLayers) {
|
|
1827
|
+
throw new Error(`PagedKVCache layer ${layer} out of range 0..${config.numLayers - 1}`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
function pageSnapshot(page) {
|
|
1831
|
+
return {
|
|
1832
|
+
pageId: page.pageId,
|
|
1833
|
+
layer: page.layer,
|
|
1834
|
+
residency: page.residency,
|
|
1835
|
+
firstToken: page.firstToken,
|
|
1836
|
+
lastToken: page.lastToken
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
function pageOffset(physicalPage) {
|
|
1840
|
+
return physicalPage * config.pageSize * config.hiddenDim;
|
|
1841
|
+
}
|
|
1842
|
+
function copyResidentPage(page, keys, values) {
|
|
1843
|
+
if (page.physicalPage === null) return;
|
|
1844
|
+
const offset = pageOffset(page.physicalPage);
|
|
1845
|
+
const end = offset + config.pageSize * config.hiddenDim;
|
|
1846
|
+
keys.set(keyPages.subarray(offset, end));
|
|
1847
|
+
values.set(valuePages.subarray(offset, end));
|
|
1848
|
+
}
|
|
1849
|
+
function zeroPhysicalPage(physicalPage) {
|
|
1850
|
+
const offset = pageOffset(physicalPage);
|
|
1851
|
+
const end = offset + config.pageSize * config.hiddenDim;
|
|
1852
|
+
keyPages.fill(0, offset, end);
|
|
1853
|
+
valuePages.fill(0, offset, end);
|
|
1854
|
+
}
|
|
1855
|
+
function residentPages() {
|
|
1856
|
+
return Array.from(residentPageStates);
|
|
1857
|
+
}
|
|
1858
|
+
function evictPage(page) {
|
|
1859
|
+
if (page.physicalPage === null) return pageSnapshot(page);
|
|
1860
|
+
const hostKeys = new Float32Array(config.pageSize * config.hiddenDim);
|
|
1861
|
+
const hostValues = new Float32Array(config.pageSize * config.hiddenDim);
|
|
1862
|
+
copyResidentPage(page, hostKeys, hostValues);
|
|
1863
|
+
zeroPhysicalPage(page.physicalPage);
|
|
1864
|
+
freePhysicalPages.push(page.physicalPage);
|
|
1865
|
+
page.physicalPage = null;
|
|
1866
|
+
page.hostKeys = hostKeys;
|
|
1867
|
+
page.hostValues = hostValues;
|
|
1868
|
+
page.residency = "host";
|
|
1869
|
+
residentPageStates.delete(page);
|
|
1870
|
+
devicePageCount -= 1;
|
|
1871
|
+
hostPageCount += 1;
|
|
1872
|
+
return pageSnapshot(page);
|
|
1873
|
+
}
|
|
1874
|
+
function evictOldestInternal(count, protectedPage) {
|
|
1875
|
+
if (count <= 0) return [];
|
|
1876
|
+
const victims = residentPages().filter((page) => page !== protectedPage).sort((a, b) => a.lastAccess - b.lastAccess).slice(0, count);
|
|
1877
|
+
return victims.map((page) => evictPage(page));
|
|
1878
|
+
}
|
|
1879
|
+
function allocatePhysicalPage(protectedPage) {
|
|
1880
|
+
if (freePhysicalPages.length === 0) {
|
|
1881
|
+
evictOldestInternal(1, protectedPage);
|
|
1882
|
+
}
|
|
1883
|
+
const physicalPage = freePhysicalPages.pop();
|
|
1884
|
+
if (physicalPage === void 0) {
|
|
1885
|
+
throw new Error("PagedKVCache could not allocate a resident page.");
|
|
1886
|
+
}
|
|
1887
|
+
zeroPhysicalPage(physicalPage);
|
|
1888
|
+
return physicalPage;
|
|
1889
|
+
}
|
|
1890
|
+
function ensurePageResident(page) {
|
|
1891
|
+
if (page.residency === "device" && page.physicalPage !== null) {
|
|
1892
|
+
page.lastAccess = ++accessClock;
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
const physicalPage = allocatePhysicalPage(page);
|
|
1896
|
+
page.physicalPage = physicalPage;
|
|
1897
|
+
if (page.residency === "host") hostPageCount -= 1;
|
|
1898
|
+
page.residency = "device";
|
|
1899
|
+
residentPageStates.add(page);
|
|
1900
|
+
devicePageCount += 1;
|
|
1901
|
+
page.lastAccess = ++accessClock;
|
|
1902
|
+
if (page.hostKeys) {
|
|
1903
|
+
keyPages.set(page.hostKeys, pageOffset(physicalPage));
|
|
1904
|
+
page.hostKeys = void 0;
|
|
1905
|
+
}
|
|
1906
|
+
if (page.hostValues) {
|
|
1907
|
+
valuePages.set(page.hostValues, pageOffset(physicalPage));
|
|
1908
|
+
page.hostValues = void 0;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
function allocatePageInternal(layer, logicalPage) {
|
|
1912
|
+
const layerState = layers[layer];
|
|
1913
|
+
const existing = layerState.pages.get(logicalPage);
|
|
1914
|
+
if (existing) {
|
|
1915
|
+
ensurePageResident(existing);
|
|
1916
|
+
return existing;
|
|
1917
|
+
}
|
|
1918
|
+
const physicalPage = allocatePhysicalPage();
|
|
1919
|
+
const page = {
|
|
1920
|
+
pageId: nextPageId++,
|
|
1921
|
+
logicalPage,
|
|
1922
|
+
layer,
|
|
1923
|
+
residency: "device",
|
|
1924
|
+
firstToken: logicalPage * config.pageSize,
|
|
1925
|
+
lastToken: logicalPage * config.pageSize + config.pageSize - 1,
|
|
1926
|
+
physicalPage,
|
|
1927
|
+
lastAccess: ++accessClock
|
|
1928
|
+
};
|
|
1929
|
+
layerState.pages.set(logicalPage, page);
|
|
1930
|
+
residentPageStates.add(page);
|
|
1931
|
+
devicePageCount += 1;
|
|
1932
|
+
return page;
|
|
1933
|
+
}
|
|
1934
|
+
function buildPageTable(layer, logicalPages) {
|
|
1935
|
+
const requiredPages = Array.from(new Set(logicalPages)).sort((a, b) => a - b);
|
|
1936
|
+
const maxLogicalPage = requiredPages[requiredPages.length - 1] ?? 0;
|
|
1937
|
+
const pageTable = new Uint32Array(maxLogicalPage + 1);
|
|
1938
|
+
pageTable.fill(PAGE_TABLE_EMPTY);
|
|
1939
|
+
const layerState = layers[layer];
|
|
1940
|
+
for (const logicalPage of requiredPages) {
|
|
1941
|
+
const page = layerState.pages.get(logicalPage);
|
|
1942
|
+
if (page) {
|
|
1943
|
+
ensurePageResident(page);
|
|
1944
|
+
pageTable[logicalPage] = page.physicalPage ?? PAGE_TABLE_EMPTY;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
return pageTable;
|
|
1948
|
+
}
|
|
1949
|
+
function appendCpuInPlace(pages, touchedPages, vectors, startToken) {
|
|
1950
|
+
const numVecs = vectors.length / config.hiddenDim;
|
|
1951
|
+
for (let v = 0; v < numVecs; v += 1) {
|
|
1952
|
+
const token = startToken + v;
|
|
1953
|
+
const logicalPage = Math.floor(token / config.pageSize);
|
|
1954
|
+
const inPage = token % config.pageSize;
|
|
1955
|
+
const physicalPage = touchedPages.get(logicalPage)?.physicalPage;
|
|
1956
|
+
if (physicalPage === void 0 || physicalPage === null) {
|
|
1957
|
+
throw new Error(`PagedKVCache append misses resident logical page ${logicalPage}.`);
|
|
1958
|
+
}
|
|
1959
|
+
const base = physicalPage * config.pageSize * config.hiddenDim;
|
|
1960
|
+
for (let d = 0; d < config.hiddenDim; d += 1) {
|
|
1961
|
+
pages[base + inPage * config.hiddenDim + d] = vectors[v * config.hiddenDim + d];
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
function lookupCpuDirect(pages, layerState, tokenCount, startToken) {
|
|
1966
|
+
const out = new Float32Array(tokenCount * config.hiddenDim);
|
|
1967
|
+
for (let v = 0; v < tokenCount; v += 1) {
|
|
1968
|
+
const logicalSlot = startToken + v;
|
|
1969
|
+
const logicalPage = Math.floor(logicalSlot / config.pageSize);
|
|
1970
|
+
const inPage = logicalSlot % config.pageSize;
|
|
1971
|
+
const physicalPage = layerState.pages.get(logicalPage)?.physicalPage;
|
|
1972
|
+
if (physicalPage === void 0 || physicalPage === null) {
|
|
1973
|
+
throw new Error(`PagedKVCache lookup misses resident logical page ${logicalPage}.`);
|
|
1974
|
+
}
|
|
1975
|
+
const base = physicalPage * config.pageSize * config.hiddenDim;
|
|
1976
|
+
for (let d = 0; d < config.hiddenDim; d += 1) {
|
|
1977
|
+
out[v * config.hiddenDim + d] = pages[base + inPage * config.hiddenDim + d];
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return out;
|
|
1981
|
+
}
|
|
1982
|
+
return {
|
|
1983
|
+
config,
|
|
1984
|
+
allocatePage(layer, firstToken) {
|
|
1985
|
+
assertLive();
|
|
1986
|
+
validateLayer(layer);
|
|
1987
|
+
if (!Number.isInteger(firstToken) || firstToken < 0) {
|
|
1988
|
+
throw new Error(`PagedKVCache firstToken must be a non-negative integer, got ${firstToken}`);
|
|
1989
|
+
}
|
|
1990
|
+
const logicalPage = Math.floor(firstToken / config.pageSize);
|
|
1991
|
+
return pageSnapshot(allocatePageInternal(layer, logicalPage));
|
|
1992
|
+
},
|
|
1993
|
+
residentPagesForLayer(layer) {
|
|
1994
|
+
assertLive();
|
|
1995
|
+
validateLayer(layer);
|
|
1996
|
+
return Array.from(residentPageStates).filter((page) => page.layer === layer).sort((a, b) => a.firstToken - b.firstToken).map((page) => pageSnapshot(page));
|
|
1997
|
+
},
|
|
1998
|
+
evictOldest(count) {
|
|
1999
|
+
assertLive();
|
|
2000
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
2001
|
+
throw new Error(`PagedKVCache evict count must be a non-negative integer, got ${count}`);
|
|
2002
|
+
}
|
|
2003
|
+
return evictOldestInternal(count);
|
|
2004
|
+
},
|
|
2005
|
+
memoryUsage() {
|
|
2006
|
+
assertLive();
|
|
2007
|
+
return {
|
|
2008
|
+
device: devicePageCount * pageBytes * 2,
|
|
2009
|
+
host: hostPageCount * pageBytes * 2
|
|
2010
|
+
};
|
|
2011
|
+
},
|
|
2012
|
+
async dispose() {
|
|
2013
|
+
disposed = true;
|
|
2014
|
+
for (const layer of layers) layer.pages.clear();
|
|
2015
|
+
residentPageStates.clear();
|
|
2016
|
+
freePhysicalPages.length = 0;
|
|
2017
|
+
devicePageCount = 0;
|
|
2018
|
+
hostPageCount = 0;
|
|
2019
|
+
},
|
|
2020
|
+
async append(layer, keyVectors, valueVectors = keyVectors, firstToken) {
|
|
2021
|
+
assertLive();
|
|
2022
|
+
validateLayer(layer);
|
|
2023
|
+
if (keyVectors.length !== valueVectors.length) {
|
|
2024
|
+
throw new Error("PagedKVCache append requires key/value vectors with equal length.");
|
|
2025
|
+
}
|
|
2026
|
+
if (keyVectors.length % config.hiddenDim !== 0) {
|
|
2027
|
+
throw new Error(
|
|
2028
|
+
`PagedKVCache append vector length ${keyVectors.length} is not divisible by hiddenDim=${config.hiddenDim}`
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
const tokenCount = keyVectors.length / config.hiddenDim;
|
|
2032
|
+
if (tokenCount === 0) return [];
|
|
2033
|
+
const startToken = firstToken ?? layers[layer].nextToken;
|
|
2034
|
+
if (!Number.isInteger(startToken) || startToken < 0) {
|
|
2035
|
+
throw new Error(`PagedKVCache append firstToken must be non-negative, got ${startToken}`);
|
|
2036
|
+
}
|
|
2037
|
+
const touchedPages = /* @__PURE__ */ new Map();
|
|
2038
|
+
const requiredPages = /* @__PURE__ */ new Set();
|
|
2039
|
+
for (let i = 0; i < tokenCount; i += 1) {
|
|
2040
|
+
const token = startToken + i;
|
|
2041
|
+
const logicalPage = Math.floor(token / config.pageSize);
|
|
2042
|
+
requiredPages.add(logicalPage);
|
|
2043
|
+
}
|
|
2044
|
+
if (requiredPages.size > config.maxResidentPages) {
|
|
2045
|
+
throw new Error(
|
|
2046
|
+
`PagedKVCache append touches ${requiredPages.size} pages but only ${config.maxResidentPages} can be resident.`
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
for (const logicalPage of requiredPages) {
|
|
2050
|
+
const page = allocatePageInternal(layer, logicalPage);
|
|
2051
|
+
touchedPages.set(logicalPage, page);
|
|
2052
|
+
}
|
|
2053
|
+
if (appendKernel) {
|
|
2054
|
+
const slotMap = new Uint32Array(tokenCount);
|
|
2055
|
+
for (let i = 0; i < tokenCount; i += 1) {
|
|
2056
|
+
const token = startToken + i;
|
|
2057
|
+
const logicalPage = Math.floor(token / config.pageSize);
|
|
2058
|
+
const slot = token % config.pageSize;
|
|
2059
|
+
slotMap[i] = logicalPage << 16 | slot;
|
|
2060
|
+
}
|
|
2061
|
+
const pageTable = buildPageTable(layer, touchedPages.keys());
|
|
2062
|
+
keyPages.set(
|
|
2063
|
+
await appendKernel.run(
|
|
2064
|
+
keyPages,
|
|
2065
|
+
pageTable,
|
|
2066
|
+
slotMap,
|
|
2067
|
+
keyVectors,
|
|
2068
|
+
config.pageSize,
|
|
2069
|
+
config.hiddenDim
|
|
2070
|
+
)
|
|
2071
|
+
);
|
|
2072
|
+
valuePages.set(
|
|
2073
|
+
await appendKernel.run(
|
|
2074
|
+
valuePages,
|
|
2075
|
+
pageTable,
|
|
2076
|
+
slotMap,
|
|
2077
|
+
valueVectors,
|
|
2078
|
+
config.pageSize,
|
|
2079
|
+
config.hiddenDim
|
|
2080
|
+
)
|
|
2081
|
+
);
|
|
2082
|
+
} else {
|
|
2083
|
+
appendCpuInPlace(keyPages, touchedPages, keyVectors, startToken);
|
|
2084
|
+
appendCpuInPlace(valuePages, touchedPages, valueVectors, startToken);
|
|
2085
|
+
}
|
|
2086
|
+
for (const page of touchedPages.values()) page.lastAccess = ++accessClock;
|
|
2087
|
+
layers[layer].nextToken = Math.max(layers[layer].nextToken, startToken + tokenCount);
|
|
2088
|
+
return Array.from(touchedPages.values()).sort((a, b) => a.firstToken - b.firstToken).map((page) => pageSnapshot(page));
|
|
2089
|
+
},
|
|
2090
|
+
async lookup(layer, startToken, tokenCount) {
|
|
2091
|
+
assertLive();
|
|
2092
|
+
validateLayer(layer);
|
|
2093
|
+
if (!Number.isInteger(startToken) || startToken < 0) {
|
|
2094
|
+
throw new Error(`PagedKVCache lookup startToken must be non-negative, got ${startToken}`);
|
|
2095
|
+
}
|
|
2096
|
+
if (!Number.isInteger(tokenCount) || tokenCount < 0) {
|
|
2097
|
+
throw new Error(`PagedKVCache lookup tokenCount must be non-negative, got ${tokenCount}`);
|
|
2098
|
+
}
|
|
2099
|
+
if (tokenCount === 0) {
|
|
2100
|
+
return { keys: new Float32Array(), values: new Float32Array() };
|
|
2101
|
+
}
|
|
2102
|
+
const endToken = startToken + tokenCount - 1;
|
|
2103
|
+
const firstLogicalPage = Math.floor(startToken / config.pageSize);
|
|
2104
|
+
const maxLogicalPage = Math.floor(endToken / config.pageSize);
|
|
2105
|
+
const requiredPages = [];
|
|
2106
|
+
for (let logicalPage = firstLogicalPage; logicalPage <= maxLogicalPage; logicalPage += 1) {
|
|
2107
|
+
const page = layers[layer].pages.get(logicalPage);
|
|
2108
|
+
if (!page) {
|
|
2109
|
+
throw new Error(`PagedKVCache lookup misses layer ${layer} logical page ${logicalPage}.`);
|
|
2110
|
+
}
|
|
2111
|
+
requiredPages.push(logicalPage);
|
|
2112
|
+
}
|
|
2113
|
+
if (requiredPages.length > config.maxResidentPages) {
|
|
2114
|
+
throw new Error(
|
|
2115
|
+
`PagedKVCache lookup needs ${requiredPages.length} pages but only ${config.maxResidentPages} can be resident.`
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
for (const logicalPage of requiredPages) {
|
|
2119
|
+
ensurePageResident(layers[layer].pages.get(logicalPage));
|
|
2120
|
+
}
|
|
2121
|
+
if (lookupKernel) {
|
|
2122
|
+
const pageTable = buildPageTable(layer, requiredPages);
|
|
2123
|
+
return {
|
|
2124
|
+
keys: await lookupKernel.run(
|
|
2125
|
+
keyPages,
|
|
2126
|
+
pageTable,
|
|
2127
|
+
tokenCount,
|
|
2128
|
+
startToken,
|
|
2129
|
+
config.pageSize,
|
|
2130
|
+
config.hiddenDim
|
|
2131
|
+
),
|
|
2132
|
+
values: await lookupKernel.run(
|
|
2133
|
+
valuePages,
|
|
2134
|
+
pageTable,
|
|
2135
|
+
tokenCount,
|
|
2136
|
+
startToken,
|
|
2137
|
+
config.pageSize,
|
|
2138
|
+
config.hiddenDim
|
|
2139
|
+
)
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
return {
|
|
2143
|
+
keys: lookupCpuDirect(keyPages, layers[layer], tokenCount, startToken),
|
|
2144
|
+
values: lookupCpuDirect(valuePages, layers[layer], tokenCount, startToken)
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
function validateConfig(config) {
|
|
2150
|
+
const fields = [
|
|
2151
|
+
["pageSize", config.pageSize],
|
|
2152
|
+
["maxResidentPages", config.maxResidentPages],
|
|
2153
|
+
["hiddenDim", config.hiddenDim],
|
|
2154
|
+
["numHeads", config.numHeads],
|
|
2155
|
+
["numLayers", config.numLayers]
|
|
2156
|
+
];
|
|
2157
|
+
for (const [field, value] of fields) {
|
|
2158
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2159
|
+
throw new Error(`PagedKVCache config ${String(field)} must be a positive integer.`);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
if (config.pageSize > 65535) {
|
|
2163
|
+
throw new Error("PagedKVCache pageSize must fit the 16-bit slot packing used by WGSL kernels.");
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// src/reconstruction/HoloMapRuntime.ts
|
|
2168
|
+
var HOLOMAP_DEFAULTS = {
|
|
2169
|
+
inputResolution: { width: 518, height: 378 },
|
|
2170
|
+
targetFPS: 15,
|
|
2171
|
+
maxSequenceLength: 1e4,
|
|
2172
|
+
seed: 0,
|
|
2173
|
+
modelHash: "unset",
|
|
2174
|
+
tileGrid: 4,
|
|
2175
|
+
cpuOffload: false,
|
|
2176
|
+
weightStrategy: "distill",
|
|
2177
|
+
reconstructionMode: "preview",
|
|
2178
|
+
allowCpuFallback: true
|
|
2179
|
+
};
|
|
2180
|
+
var HOLOMAP_DENSE_PROOF_DEFAULTS = {
|
|
2181
|
+
...HOLOMAP_DEFAULTS,
|
|
2182
|
+
tileGrid: 16,
|
|
2183
|
+
reconstructionMode: "dense-proof"
|
|
2184
|
+
};
|
|
2185
|
+
var _HoloMapRuntimeImpl = class _HoloMapRuntimeImpl {
|
|
2186
|
+
constructor() {
|
|
2187
|
+
this.config = { ...HOLOMAP_DEFAULTS };
|
|
2188
|
+
this.initialized = false;
|
|
2189
|
+
this.steps = [];
|
|
2190
|
+
this.replayKey = "unset";
|
|
2191
|
+
this.runId = createHoloMapRunId();
|
|
2192
|
+
this.encoderDevice = null;
|
|
2193
|
+
this.microEncoder = null;
|
|
2194
|
+
/** Loaded weight blob (optional; GPU upload wiring follows R3+). */
|
|
2195
|
+
this.weightBytes = null;
|
|
2196
|
+
this.weightVerified = false;
|
|
2197
|
+
this.weightSource = null;
|
|
2198
|
+
this.streamingKVCache = null;
|
|
2199
|
+
this.streamingKVNextToken = 0;
|
|
2200
|
+
// ── Sprint-3 performance / determinism state ──
|
|
2201
|
+
/** Running bounding box (updated incrementally per step). */
|
|
2202
|
+
this.boundsMin = [0, 0, 0];
|
|
2203
|
+
this.boundsMax = [0, 0, 0];
|
|
2204
|
+
this.boundsValid = false;
|
|
2205
|
+
/** Running total point count (avoids O(n) re-scan in finalize). */
|
|
2206
|
+
this.totalPointCount = 0;
|
|
2207
|
+
// ── Scan-derived trajectory / drift state ──
|
|
2208
|
+
// Anchor pose, trajectory keyframes, and drift are derived from the scan
|
|
2209
|
+
// geometry rather than emitted as constants. Drift is an uncertainty-weighted
|
|
2210
|
+
// inter-frame pose delta accumulated over the capture.
|
|
2211
|
+
/** Accumulated drift estimate (meters) over the capture. */
|
|
2212
|
+
this.cumulativeDriftMeters = 0;
|
|
2213
|
+
/**
|
|
2214
|
+
* Running sum of every observed point position + confidence (eviction-adjusted).
|
|
2215
|
+
* The anchor pose is the centroid (sum/count) of all observed points and the
|
|
2216
|
+
* descriptor carries the global mean confidence, so a single tampered point
|
|
2217
|
+
* moves the anchor — bounds center/extent alone are insensitive to a one-point
|
|
2218
|
+
* nudge that does not touch an extremum. (E4 acceptance bar.)
|
|
2219
|
+
*/
|
|
2220
|
+
this.globalPosSum = [0, 0, 0];
|
|
2221
|
+
this.globalConfSum = 0;
|
|
2222
|
+
this.globalPrincipalMomentXX = 0;
|
|
2223
|
+
this.globalPrincipalMomentZZ = 0;
|
|
2224
|
+
this.globalPrincipalMomentXZ = 0;
|
|
2225
|
+
/** Previous frame camera pose (centroid of that frame's points). */
|
|
2226
|
+
this.prevPose = null;
|
|
2227
|
+
/** Previous full camera pose, including scan-derived principal-axis rotation. */
|
|
2228
|
+
this.prevCameraPose = null;
|
|
2229
|
+
/** Previous inter-frame velocity, for constant-velocity drift prediction. */
|
|
2230
|
+
this.prevVelocity = null;
|
|
2231
|
+
/** Frame index of the last detected loop closure (−1 if none). */
|
|
2232
|
+
this.lastLoopClosureFrameIdx = -1;
|
|
2233
|
+
/** Compact camera-pose history for loop-closure (revisit) detection. */
|
|
2234
|
+
this.poseHistory = [];
|
|
2235
|
+
/** Compact trajectory snapshot emitted with every accepted step. */
|
|
2236
|
+
this.trajectoryKeyframes = [];
|
|
2237
|
+
/** Last accepted capture timestamp (ms) for deterministic FPS throttling. */
|
|
2238
|
+
this.lastAcceptedFrameTimestampMs = null;
|
|
2239
|
+
/** Deterministic session start timestamp. */
|
|
2240
|
+
this.sessionStartMs = 0;
|
|
2241
|
+
/** Performance metrics accumulated across steps. */
|
|
2242
|
+
this.perfMetrics = { stepCount: 0, throttledCount: 0, totalStepMs: 0, maxStepMs: 0, minStepMs: Infinity };
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Nearest-neighbour sample of a row-major depth map at a normalized UV.
|
|
2246
|
+
* Used to read MEASURED sensor depth at a tile centre (frame.depth). Nearest
|
|
2247
|
+
* (not bilinear) keeps it deterministic and cheap; tiles are small.
|
|
2248
|
+
*/
|
|
2249
|
+
static sampleDepthNearestUv(depth, width, height, uv) {
|
|
2250
|
+
const px = Math.min(width - 1, Math.max(0, Math.round(uv[0] * (width - 1))));
|
|
2251
|
+
const py = Math.min(height - 1, Math.max(0, Math.round(uv[1] * (height - 1))));
|
|
2252
|
+
const v = depth[py * width + px];
|
|
2253
|
+
return Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : 0.5;
|
|
2254
|
+
}
|
|
2255
|
+
static sampleMetricDepthNearestUv(depthMeters, width, height, uv) {
|
|
2256
|
+
const px = Math.min(width - 1, Math.max(0, Math.round(uv[0] * (width - 1))));
|
|
2257
|
+
const py = Math.min(height - 1, Math.max(0, Math.round(uv[1] * (height - 1))));
|
|
2258
|
+
const v = depthMeters[py * width + px];
|
|
2259
|
+
return Number.isFinite(v) && v > 0 ? v : void 0;
|
|
2260
|
+
}
|
|
2261
|
+
static hasUsableIntrinsics(intrinsics) {
|
|
2262
|
+
return !!intrinsics && Number.isFinite(intrinsics.width) && Number.isFinite(intrinsics.height) && Number.isFinite(intrinsics.fx) && Number.isFinite(intrinsics.fy) && Number.isFinite(intrinsics.cx) && Number.isFinite(intrinsics.cy) && intrinsics.width > 0 && intrinsics.height > 0 && intrinsics.fx > 0 && intrinsics.fy > 0;
|
|
2263
|
+
}
|
|
2264
|
+
static normalizeQuaternion(q) {
|
|
2265
|
+
const len = Math.hypot(q[0], q[1], q[2], q[3]);
|
|
2266
|
+
if (len <= 1e-12) return [0, 0, 0, 1];
|
|
2267
|
+
return [q[0] / len, q[1] / len, q[2] / len, q[3] / len];
|
|
2268
|
+
}
|
|
2269
|
+
static rotateByQuaternion(v, q) {
|
|
2270
|
+
const [x, y, z, w] = _HoloMapRuntimeImpl.normalizeQuaternion(q);
|
|
2271
|
+
const [vx, vy, vz] = v;
|
|
2272
|
+
const tx = 2 * (y * vz - z * vy);
|
|
2273
|
+
const ty = 2 * (z * vx - x * vz);
|
|
2274
|
+
const tz = 2 * (x * vy - y * vx);
|
|
2275
|
+
return [
|
|
2276
|
+
vx + w * tx + (y * tz - z * ty),
|
|
2277
|
+
vy + w * ty + (z * tx - x * tz),
|
|
2278
|
+
vz + w * tz + (x * ty - y * tx)
|
|
2279
|
+
];
|
|
2280
|
+
}
|
|
2281
|
+
static projectMetricDepthToWorld(uv, depthMeters, intrinsics, pose) {
|
|
2282
|
+
const pixelX = uv[0] * intrinsics.width;
|
|
2283
|
+
const pixelY = uv[1] * intrinsics.height;
|
|
2284
|
+
const local = [
|
|
2285
|
+
(pixelX - intrinsics.cx) * depthMeters / intrinsics.fx,
|
|
2286
|
+
(intrinsics.cy - pixelY) * depthMeters / intrinsics.fy,
|
|
2287
|
+
-depthMeters
|
|
2288
|
+
];
|
|
2289
|
+
if (!pose) return local;
|
|
2290
|
+
const rotated = _HoloMapRuntimeImpl.rotateByQuaternion(local, pose.rotation);
|
|
2291
|
+
return [
|
|
2292
|
+
pose.position[0] + rotated[0],
|
|
2293
|
+
pose.position[1] + rotated[1],
|
|
2294
|
+
pose.position[2] + rotated[2]
|
|
2295
|
+
];
|
|
2296
|
+
}
|
|
2297
|
+
static fitShiftScaleDepth(samples) {
|
|
2298
|
+
const paired = samples.filter((sample) => sample.metricDepthMeters !== void 0).map((sample) => ({
|
|
2299
|
+
x: sample.rawDepthSignal,
|
|
2300
|
+
y: sample.metricDepthMeters
|
|
2301
|
+
}));
|
|
2302
|
+
if (paired.length === 0) return void 0;
|
|
2303
|
+
let meanX = 0;
|
|
2304
|
+
let meanY = 0;
|
|
2305
|
+
for (const sample of paired) {
|
|
2306
|
+
meanX += sample.x;
|
|
2307
|
+
meanY += sample.y;
|
|
2308
|
+
}
|
|
2309
|
+
meanX /= paired.length;
|
|
2310
|
+
meanY /= paired.length;
|
|
2311
|
+
let varianceX = 0;
|
|
2312
|
+
let covariance = 0;
|
|
2313
|
+
for (const sample of paired) {
|
|
2314
|
+
const dx = sample.x - meanX;
|
|
2315
|
+
varianceX += dx * dx;
|
|
2316
|
+
covariance += dx * (sample.y - meanY);
|
|
2317
|
+
}
|
|
2318
|
+
const scale = varianceX > 1e-12 ? covariance / varianceX : 0;
|
|
2319
|
+
const shift = meanY - scale * meanX;
|
|
2320
|
+
let squared = 0;
|
|
2321
|
+
for (const sample of paired) {
|
|
2322
|
+
const err = scale * sample.x + shift - sample.y;
|
|
2323
|
+
squared += err * err;
|
|
2324
|
+
}
|
|
2325
|
+
return {
|
|
2326
|
+
kind: "shift-scale",
|
|
2327
|
+
sampleCount: paired.length,
|
|
2328
|
+
scale,
|
|
2329
|
+
shift,
|
|
2330
|
+
rmseMeters: Math.sqrt(squared / paired.length)
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
static alignedDepthMeters(sample, fit) {
|
|
2334
|
+
if (fit) {
|
|
2335
|
+
const aligned = fit.scale * sample.rawDepthSignal + fit.shift;
|
|
2336
|
+
if (Number.isFinite(aligned) && aligned > 0) return aligned;
|
|
2337
|
+
}
|
|
2338
|
+
return sample.metricDepthMeters;
|
|
2339
|
+
}
|
|
2340
|
+
static buildLoopClosureDescriptor(samples) {
|
|
2341
|
+
if (samples.length === 0) return new Float32Array([0, 0, 0, 0, 0, 0]);
|
|
2342
|
+
let r = 0;
|
|
2343
|
+
let g = 0;
|
|
2344
|
+
let b = 0;
|
|
2345
|
+
let luma = 0;
|
|
2346
|
+
let texture = 0;
|
|
2347
|
+
let depth = 0;
|
|
2348
|
+
let depthCount = 0;
|
|
2349
|
+
for (const sample of samples) {
|
|
2350
|
+
r += sample.meanColor[0] / 255;
|
|
2351
|
+
g += sample.meanColor[1] / 255;
|
|
2352
|
+
b += sample.meanColor[2] / 255;
|
|
2353
|
+
luma += sample.luminance;
|
|
2354
|
+
texture += sample.texture;
|
|
2355
|
+
if (sample.metricDepthMeters !== void 0) {
|
|
2356
|
+
depth += Math.min(1, sample.metricDepthMeters / 10);
|
|
2357
|
+
depthCount += 1;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const inv = 1 / samples.length;
|
|
2361
|
+
return new Float32Array([
|
|
2362
|
+
r * inv,
|
|
2363
|
+
g * inv,
|
|
2364
|
+
b * inv,
|
|
2365
|
+
luma * inv,
|
|
2366
|
+
texture * inv,
|
|
2367
|
+
depthCount > 0 ? depth / depthCount : 0
|
|
2368
|
+
]);
|
|
2369
|
+
}
|
|
2370
|
+
static descriptorDistance(a, b) {
|
|
2371
|
+
const n = Math.min(a.length, b.length);
|
|
2372
|
+
if (n === 0) return Number.POSITIVE_INFINITY;
|
|
2373
|
+
let squared = 0;
|
|
2374
|
+
for (let i = 0; i < n; i += 1) {
|
|
2375
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
2376
|
+
squared += d * d;
|
|
2377
|
+
}
|
|
2378
|
+
return Math.sqrt(squared / n);
|
|
2379
|
+
}
|
|
2380
|
+
static computeBounds(steps) {
|
|
2381
|
+
if (steps.length === 0) {
|
|
2382
|
+
return {
|
|
2383
|
+
min: [0, 0, 0],
|
|
2384
|
+
max: [0, 0, 0]
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
2388
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
2389
|
+
let minZ = Number.POSITIVE_INFINITY;
|
|
2390
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
2391
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
2392
|
+
let maxZ = Number.NEGATIVE_INFINITY;
|
|
2393
|
+
for (const step of steps) {
|
|
2394
|
+
const { positions } = step.points;
|
|
2395
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
2396
|
+
const x = positions[i] ?? 0;
|
|
2397
|
+
const y = positions[i + 1] ?? 0;
|
|
2398
|
+
const z = positions[i + 2] ?? 0;
|
|
2399
|
+
if (x < minX) minX = x;
|
|
2400
|
+
if (y < minY) minY = y;
|
|
2401
|
+
if (z < minZ) minZ = z;
|
|
2402
|
+
if (x > maxX) maxX = x;
|
|
2403
|
+
if (y > maxY) maxY = y;
|
|
2404
|
+
if (z > maxZ) maxZ = z;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
if (!Number.isFinite(minX)) {
|
|
2408
|
+
return {
|
|
2409
|
+
min: [0, 0, 0],
|
|
2410
|
+
max: [0, 0, 0]
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
return {
|
|
2414
|
+
min: [minX, minY, minZ],
|
|
2415
|
+
max: [maxX, maxY, maxZ]
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
/** Update running bounds with new point positions. */
|
|
2419
|
+
updateBounds(positions) {
|
|
2420
|
+
if (positions.length === 0) return;
|
|
2421
|
+
if (!this.boundsValid) {
|
|
2422
|
+
this.boundsMin = [
|
|
2423
|
+
Number.POSITIVE_INFINITY,
|
|
2424
|
+
Number.POSITIVE_INFINITY,
|
|
2425
|
+
Number.POSITIVE_INFINITY
|
|
2426
|
+
];
|
|
2427
|
+
this.boundsMax = [
|
|
2428
|
+
Number.NEGATIVE_INFINITY,
|
|
2429
|
+
Number.NEGATIVE_INFINITY,
|
|
2430
|
+
Number.NEGATIVE_INFINITY
|
|
2431
|
+
];
|
|
2432
|
+
}
|
|
2433
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
2434
|
+
const x = positions[i] ?? 0;
|
|
2435
|
+
const y = positions[i + 1] ?? 0;
|
|
2436
|
+
const z = positions[i + 2] ?? 0;
|
|
2437
|
+
if (x < this.boundsMin[0]) this.boundsMin[0] = x;
|
|
2438
|
+
if (y < this.boundsMin[1]) this.boundsMin[1] = y;
|
|
2439
|
+
if (z < this.boundsMin[2]) this.boundsMin[2] = z;
|
|
2440
|
+
if (x > this.boundsMax[0]) this.boundsMax[0] = x;
|
|
2441
|
+
if (y > this.boundsMax[1]) this.boundsMax[1] = y;
|
|
2442
|
+
if (z > this.boundsMax[2]) this.boundsMax[2] = z;
|
|
2443
|
+
}
|
|
2444
|
+
this.boundsValid = true;
|
|
2445
|
+
}
|
|
2446
|
+
/** Get current bounds (running or computed). */
|
|
2447
|
+
getBounds() {
|
|
2448
|
+
if (this.boundsValid) {
|
|
2449
|
+
return { min: [...this.boundsMin], max: [...this.boundsMax] };
|
|
2450
|
+
}
|
|
2451
|
+
return _HoloMapRuntimeImpl.computeBounds(this.steps);
|
|
2452
|
+
}
|
|
2453
|
+
static *retainedPositionChunks(steps, current) {
|
|
2454
|
+
for (const step of steps) yield step.points.positions;
|
|
2455
|
+
yield current;
|
|
2456
|
+
}
|
|
2457
|
+
static estimatePrincipalAxisRotation(chunks, centroid) {
|
|
2458
|
+
let xx = 0;
|
|
2459
|
+
let zz = 0;
|
|
2460
|
+
let xz = 0;
|
|
2461
|
+
let n = 0;
|
|
2462
|
+
for (const positions of chunks) {
|
|
2463
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
2464
|
+
const dx = (positions[i] ?? 0) - centroid[0];
|
|
2465
|
+
const dz = (positions[i + 2] ?? 0) - centroid[2];
|
|
2466
|
+
xx += dx * dx;
|
|
2467
|
+
zz += dz * dz;
|
|
2468
|
+
xz += dx * dz;
|
|
2469
|
+
n += 1;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
if (n < 2 || xx + zz < 1e-12) return [0, 0, 0, 1];
|
|
2473
|
+
const yaw = 0.5 * Math.atan2(2 * xz, xx - zz);
|
|
2474
|
+
return _HoloMapRuntimeImpl.yawToQuaternion(yaw);
|
|
2475
|
+
}
|
|
2476
|
+
static estimatePrincipalAxisRotationFromMoments(momentXX, momentZZ, momentXZ, pointCount, centroid) {
|
|
2477
|
+
if (pointCount < 2) return [0, 0, 0, 1];
|
|
2478
|
+
const xx = momentXX - pointCount * centroid[0] * centroid[0];
|
|
2479
|
+
const zz = momentZZ - pointCount * centroid[2] * centroid[2];
|
|
2480
|
+
const xz = momentXZ - pointCount * centroid[0] * centroid[2];
|
|
2481
|
+
if (xx + zz < 1e-12) return [0, 0, 0, 1];
|
|
2482
|
+
const yaw = 0.5 * Math.atan2(2 * xz, xx - zz);
|
|
2483
|
+
return _HoloMapRuntimeImpl.yawToQuaternion(yaw);
|
|
2484
|
+
}
|
|
2485
|
+
static yawToQuaternion(yaw) {
|
|
2486
|
+
const half = yaw / 2;
|
|
2487
|
+
return [0, Math.sin(half), 0, Math.cos(half)];
|
|
2488
|
+
}
|
|
2489
|
+
static poseDistanceMeters(a, b) {
|
|
2490
|
+
const dx = a.position[0] - b.position[0];
|
|
2491
|
+
const dy = a.position[1] - b.position[1];
|
|
2492
|
+
const dz = a.position[2] - b.position[2];
|
|
2493
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
2494
|
+
}
|
|
2495
|
+
static poseRotationDeltaRadians(a, b) {
|
|
2496
|
+
const dot = Math.abs(
|
|
2497
|
+
a.rotation[0] * b.rotation[0] + a.rotation[1] * b.rotation[1] + a.rotation[2] * b.rotation[2] + a.rotation[3] * b.rotation[3]
|
|
2498
|
+
);
|
|
2499
|
+
const clamped = Math.max(-1, Math.min(1, dot));
|
|
2500
|
+
return 2 * Math.acos(clamped);
|
|
2501
|
+
}
|
|
2502
|
+
static buildTrajectoryEmbedding(pose, anchorDescriptor, interFrameDeltaMeters, interFrameRotationRadians) {
|
|
2503
|
+
return new Float32Array([
|
|
2504
|
+
pose.position[0],
|
|
2505
|
+
pose.position[1],
|
|
2506
|
+
pose.position[2],
|
|
2507
|
+
pose.rotation[0],
|
|
2508
|
+
pose.rotation[1],
|
|
2509
|
+
pose.rotation[2],
|
|
2510
|
+
pose.rotation[3],
|
|
2511
|
+
pose.confidence,
|
|
2512
|
+
interFrameDeltaMeters,
|
|
2513
|
+
interFrameRotationRadians,
|
|
2514
|
+
anchorDescriptor[0] ?? 0,
|
|
2515
|
+
anchorDescriptor[1] ?? 0,
|
|
2516
|
+
anchorDescriptor[2] ?? 0,
|
|
2517
|
+
anchorDescriptor[3] ?? 0
|
|
2518
|
+
]);
|
|
2519
|
+
}
|
|
2520
|
+
static cloneTrajectoryKeyframes(keyframes, maxCount = keyframes.length) {
|
|
2521
|
+
const start = Math.max(0, keyframes.length - Math.max(0, maxCount));
|
|
2522
|
+
const out = [];
|
|
2523
|
+
for (let i = start; i < keyframes.length; i += 1) {
|
|
2524
|
+
const keyframe = keyframes[i];
|
|
2525
|
+
out.push({
|
|
2526
|
+
frameIndex: keyframe.frameIndex,
|
|
2527
|
+
timestampMs: keyframe.timestampMs,
|
|
2528
|
+
pose: {
|
|
2529
|
+
position: [...keyframe.pose.position],
|
|
2530
|
+
rotation: [...keyframe.pose.rotation],
|
|
2531
|
+
confidence: keyframe.pose.confidence
|
|
2532
|
+
},
|
|
2533
|
+
embedding: new Float32Array(keyframe.embedding)
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
return out;
|
|
2537
|
+
}
|
|
2538
|
+
pushTrajectoryKeyframe(keyframe) {
|
|
2539
|
+
this.trajectoryKeyframes.push(keyframe);
|
|
2540
|
+
if (this.trajectoryKeyframes.length > _HoloMapRuntimeImpl.TRAJECTORY_KEYFRAME_CAP) {
|
|
2541
|
+
this.trajectoryKeyframes.shift();
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
async init(config) {
|
|
2545
|
+
this.config = {
|
|
2546
|
+
...config,
|
|
2547
|
+
reconstructionMode: config.reconstructionMode ?? "preview"
|
|
2548
|
+
};
|
|
2549
|
+
if (this.config.reconstructionMode === "dense-proof") {
|
|
2550
|
+
this.config.tileGrid = Math.max(
|
|
2551
|
+
this.config.tileGrid ?? _HoloMapRuntimeImpl.GRID_N,
|
|
2552
|
+
_HoloMapRuntimeImpl.DENSE_PROOF_GRID_N
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
this.config.tileGrid = _HoloMapRuntimeImpl.normalizeTileGrid(this.config.tileGrid);
|
|
2556
|
+
if (this.config.reconstructionMode === "dense-proof") {
|
|
2557
|
+
_HoloMapRuntimeImpl.assertDenseProofConfig(this.config);
|
|
2558
|
+
}
|
|
2559
|
+
const allowCpu = this.config.allowCpuFallback !== false;
|
|
2560
|
+
if (!allowCpu && !chunkN2ILK7BF_cjs.isWebGpuEnvironmentPresent()) {
|
|
2561
|
+
const err = "HoloMap: native reconstruction requires WebGPU (allowCpuFallback=false). Run in a WebGPU-capable browser, or set allowCpuFallback=true for CPU fallback, or use compatibility scene ingest (Marble) for headless benchmarks.";
|
|
2562
|
+
logHoloMapEvent(this.runId, "error", { message: err });
|
|
2563
|
+
throw new Error(err);
|
|
2564
|
+
}
|
|
2565
|
+
this.steps.length = 0;
|
|
2566
|
+
this.totalPointCount = 0;
|
|
2567
|
+
this.boundsValid = false;
|
|
2568
|
+
this.boundsMin = [0, 0, 0];
|
|
2569
|
+
this.boundsMax = [0, 0, 0];
|
|
2570
|
+
this.cumulativeDriftMeters = 0;
|
|
2571
|
+
this.prevPose = null;
|
|
2572
|
+
this.prevCameraPose = null;
|
|
2573
|
+
this.prevVelocity = null;
|
|
2574
|
+
this.lastLoopClosureFrameIdx = -1;
|
|
2575
|
+
this.poseHistory.length = 0;
|
|
2576
|
+
this.trajectoryKeyframes.length = 0;
|
|
2577
|
+
this.globalPosSum = [0, 0, 0];
|
|
2578
|
+
this.globalConfSum = 0;
|
|
2579
|
+
this.globalPrincipalMomentXX = 0;
|
|
2580
|
+
this.globalPrincipalMomentZZ = 0;
|
|
2581
|
+
this.globalPrincipalMomentXZ = 0;
|
|
2582
|
+
this.lastAcceptedFrameTimestampMs = null;
|
|
2583
|
+
this.sessionStartMs = performance.now();
|
|
2584
|
+
this.perfMetrics = {
|
|
2585
|
+
stepCount: 0,
|
|
2586
|
+
throttledCount: 0,
|
|
2587
|
+
totalStepMs: 0,
|
|
2588
|
+
maxStepMs: 0,
|
|
2589
|
+
minStepMs: Infinity
|
|
2590
|
+
};
|
|
2591
|
+
this.replayKey = chunkLM2VU56C_cjs.computeHoloMapReplayFingerprint({
|
|
2592
|
+
modelHash: this.config.modelHash,
|
|
2593
|
+
seed: this.config.seed,
|
|
2594
|
+
weightStrategy: this.config.weightStrategy ?? "distill",
|
|
2595
|
+
videoHash: this.config.videoHash,
|
|
2596
|
+
tileGrid: this.config.tileGrid,
|
|
2597
|
+
weightCid: this.config.weightCid,
|
|
2598
|
+
verticalProfile: this.config.verticalProfile
|
|
2599
|
+
});
|
|
2600
|
+
this.weightBytes = null;
|
|
2601
|
+
this.weightVerified = false;
|
|
2602
|
+
this.weightSource = null;
|
|
2603
|
+
const weightUrl = this.config.weightUrl ?? (this.config.weightCid && this.config.localResolver ? `holomap-local://${this.config.weightCid}` : void 0);
|
|
2604
|
+
if (weightUrl) {
|
|
2605
|
+
const result = await loadHoloMapWeightBlob({
|
|
2606
|
+
weightUrl,
|
|
2607
|
+
weightUrls: this.config.weightUrls,
|
|
2608
|
+
weightCid: this.config.weightCid,
|
|
2609
|
+
localResolver: this.config.localResolver
|
|
2610
|
+
});
|
|
2611
|
+
this.weightBytes = result.bytes;
|
|
2612
|
+
this.weightVerified = result.verified;
|
|
2613
|
+
this.weightSource = result.source;
|
|
2614
|
+
}
|
|
2615
|
+
if (this.config.reconstructionMode === "dense-proof" && (!this.weightBytes || !this.weightVerified)) {
|
|
2616
|
+
throw new Error(
|
|
2617
|
+
"HoloMap dense-proof mode requires verified checkpoint bytes; preview mode is the only mode allowed to fall back to PRNG weights."
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
this.encoderDevice = await tryCreateHoloMapEncoderDevice();
|
|
2621
|
+
this.microEncoder = this.encoderDevice ? createHoloMapMicroEncoder(this.encoderDevice) : null;
|
|
2622
|
+
const streamingTokens = Math.max(
|
|
2623
|
+
1,
|
|
2624
|
+
this.config.maxSequenceLength * (this.config.tileGrid ?? _HoloMapRuntimeImpl.GRID_N) ** 2
|
|
2625
|
+
);
|
|
2626
|
+
const streamingPageSize = Math.max(
|
|
2627
|
+
1,
|
|
2628
|
+
Math.min(64, this.config.tileGrid ?? _HoloMapRuntimeImpl.GRID_N)
|
|
2629
|
+
);
|
|
2630
|
+
this.streamingKVCache = createPagedKVCache({
|
|
2631
|
+
pageSize: streamingPageSize,
|
|
2632
|
+
maxResidentPages: Math.max(1, Math.min(256, Math.ceil(streamingTokens / streamingPageSize))),
|
|
2633
|
+
hiddenDim: 3,
|
|
2634
|
+
numHeads: 1,
|
|
2635
|
+
numLayers: 1,
|
|
2636
|
+
device: this.encoderDevice
|
|
2637
|
+
});
|
|
2638
|
+
this.streamingKVNextToken = 0;
|
|
2639
|
+
this.initialized = true;
|
|
2640
|
+
logHoloMapEvent(this.runId, "init", {
|
|
2641
|
+
modelHash: this.config.modelHash,
|
|
2642
|
+
seed: this.config.seed,
|
|
2643
|
+
allowCpuFallback: allowCpu,
|
|
2644
|
+
webgpu: chunkN2ILK7BF_cjs.isWebGpuEnvironmentPresent(),
|
|
2645
|
+
microEncoder: this.microEncoder ? "webgpu" : "cpu",
|
|
2646
|
+
reconstructionMode: this.config.reconstructionMode,
|
|
2647
|
+
tileGrid: this.config.tileGrid,
|
|
2648
|
+
weightLoadedBytes: this.weightBytes?.byteLength ?? 0,
|
|
2649
|
+
weightVerified: this.weightVerified,
|
|
2650
|
+
weightSource: this.weightSource,
|
|
2651
|
+
replayFingerprint: this.replayKey
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
static normalizeTileGrid(value) {
|
|
2655
|
+
const raw = value ?? _HoloMapRuntimeImpl.GRID_N;
|
|
2656
|
+
if (!Number.isFinite(raw)) {
|
|
2657
|
+
throw new Error(`HoloMapRuntime.init invalid tileGrid: ${String(value)}`);
|
|
2658
|
+
}
|
|
2659
|
+
const tileGrid = Math.floor(raw);
|
|
2660
|
+
if (tileGrid < 1 || tileGrid > _HoloMapRuntimeImpl.MAX_GRID_N) {
|
|
2661
|
+
throw new Error(
|
|
2662
|
+
`HoloMapRuntime.init tileGrid must be an integer from 1 to ${_HoloMapRuntimeImpl.MAX_GRID_N}`
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
return tileGrid;
|
|
2666
|
+
}
|
|
2667
|
+
static assertDenseProofConfig(config) {
|
|
2668
|
+
const missing = [];
|
|
2669
|
+
if (!config.modelHash || config.modelHash === "unset") missing.push("modelHash");
|
|
2670
|
+
if (!config.weightCid) missing.push("weightCid");
|
|
2671
|
+
if (!config.weightUrl && !config.localResolver) missing.push("weightUrl or localResolver");
|
|
2672
|
+
if (missing.length > 0) {
|
|
2673
|
+
throw new Error(
|
|
2674
|
+
`HoloMap dense-proof mode requires ${missing.join(
|
|
2675
|
+
", "
|
|
2676
|
+
)}; preview mode is the only mode allowed to use PRNG/default weights.`
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Carve `frame` into gridN×gridN tiles. Each tile carries its own
|
|
2682
|
+
* (rgb, width, height, stride, index) and the mean RGB color over its
|
|
2683
|
+
* pixels (used to color the corresponding output point).
|
|
2684
|
+
*
|
|
2685
|
+
* Tiles inherit `frame.index` shifted by tile id so micro-encoder
|
|
2686
|
+
* per-frame seeds remain deterministic and distinct across tiles.
|
|
2687
|
+
*/
|
|
2688
|
+
static tileFrame(frame, gridN) {
|
|
2689
|
+
const out = [];
|
|
2690
|
+
const tileW = Math.max(1, Math.floor(frame.width / gridN));
|
|
2691
|
+
const tileH = Math.max(1, Math.floor(frame.height / gridN));
|
|
2692
|
+
const stride = frame.stride;
|
|
2693
|
+
for (let ty = 0; ty < gridN; ty += 1) {
|
|
2694
|
+
for (let tx = 0; tx < gridN; tx += 1) {
|
|
2695
|
+
const x0 = tx * tileW;
|
|
2696
|
+
const y0 = ty * tileH;
|
|
2697
|
+
const x1 = tx === gridN - 1 ? frame.width : x0 + tileW;
|
|
2698
|
+
const y1 = ty === gridN - 1 ? frame.height : y0 + tileH;
|
|
2699
|
+
const w = x1 - x0;
|
|
2700
|
+
const h = y1 - y0;
|
|
2701
|
+
const tileBytes = new Uint8Array(w * h * stride);
|
|
2702
|
+
let rSum = 0;
|
|
2703
|
+
let gSum = 0;
|
|
2704
|
+
let bSum = 0;
|
|
2705
|
+
let lumaSum = 0;
|
|
2706
|
+
let lumaSqSum = 0;
|
|
2707
|
+
let count = 0;
|
|
2708
|
+
for (let y = 0; y < h; y += 1) {
|
|
2709
|
+
const srcRow = (y0 + y) * frame.width * stride;
|
|
2710
|
+
const dstRow = y * w * stride;
|
|
2711
|
+
for (let x = 0; x < w; x += 1) {
|
|
2712
|
+
const sIdx = srcRow + (x0 + x) * stride;
|
|
2713
|
+
const dIdx = dstRow + x * stride;
|
|
2714
|
+
const r = frame.rgb[sIdx] ?? 0;
|
|
2715
|
+
const g = frame.rgb[sIdx + 1] ?? 0;
|
|
2716
|
+
const b = frame.rgb[sIdx + 2] ?? 0;
|
|
2717
|
+
tileBytes[dIdx] = r;
|
|
2718
|
+
tileBytes[dIdx + 1] = g;
|
|
2719
|
+
tileBytes[dIdx + 2] = b;
|
|
2720
|
+
if (stride === 4) tileBytes[dIdx + 3] = frame.rgb[sIdx + 3] ?? 255;
|
|
2721
|
+
rSum += r;
|
|
2722
|
+
gSum += g;
|
|
2723
|
+
bSum += b;
|
|
2724
|
+
const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
2725
|
+
lumaSum += luma;
|
|
2726
|
+
lumaSqSum += luma * luma;
|
|
2727
|
+
count += 1;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
const denom = Math.max(1, count);
|
|
2731
|
+
const meanLuma = lumaSum / denom;
|
|
2732
|
+
const lumaVariance = Math.max(0, lumaSqSum / denom - meanLuma * meanLuma);
|
|
2733
|
+
const tileId = ty * gridN + tx;
|
|
2734
|
+
out.push({
|
|
2735
|
+
tile: {
|
|
2736
|
+
// Encode (frameIndex, tileId) into the per-tile micro index so
|
|
2737
|
+
// each tile gets a distinct deterministic micro-encoder seed.
|
|
2738
|
+
index: frame.index * gridN * gridN + tileId,
|
|
2739
|
+
rgb: tileBytes,
|
|
2740
|
+
width: w,
|
|
2741
|
+
height: h,
|
|
2742
|
+
stride
|
|
2743
|
+
},
|
|
2744
|
+
centerUv: [
|
|
2745
|
+
(x0 + x1) * 0.5 / Math.max(1, frame.width),
|
|
2746
|
+
(y0 + y1) * 0.5 / Math.max(1, frame.height)
|
|
2747
|
+
],
|
|
2748
|
+
luminance: meanLuma / 255,
|
|
2749
|
+
texture: Math.sqrt(lumaVariance) / 255,
|
|
2750
|
+
meanColor: [Math.round(rSum / denom), Math.round(gSum / denom), Math.round(bSum / denom)]
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
return out;
|
|
2755
|
+
}
|
|
2756
|
+
async encodeTile(tile, microCfg) {
|
|
2757
|
+
if (!this.microEncoder) {
|
|
2758
|
+
return runHoloMapMicroEncoderCpu(tile, microCfg);
|
|
2759
|
+
}
|
|
2760
|
+
try {
|
|
2761
|
+
return await this.microEncoder.run(tile, microCfg);
|
|
2762
|
+
} catch (error) {
|
|
2763
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2764
|
+
if (this.config.allowCpuFallback === false) {
|
|
2765
|
+
throw new Error(`HoloMap WebGPU micro-encoder failed: ${message}`);
|
|
2766
|
+
}
|
|
2767
|
+
logHoloMapEvent(this.runId, "micro_encoder_fallback", {
|
|
2768
|
+
frameIndex: tile.index,
|
|
2769
|
+
reason: message
|
|
2770
|
+
});
|
|
2771
|
+
this.microEncoder = null;
|
|
2772
|
+
return runHoloMapMicroEncoderCpu(tile, microCfg);
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
async updateStreamingKVCache(encodedTiles) {
|
|
2776
|
+
if (!this.streamingKVCache || encodedTiles.length === 0) return void 0;
|
|
2777
|
+
const keys = new Float32Array(encodedTiles.length * 3);
|
|
2778
|
+
const values = new Float32Array(encodedTiles.length * 3);
|
|
2779
|
+
for (let i = 0; i < encodedTiles.length; i += 1) {
|
|
2780
|
+
const sample = encodedTiles[i];
|
|
2781
|
+
keys.set(sample.latent, i * 3);
|
|
2782
|
+
values[i * 3] = sample.rawDepthSignal;
|
|
2783
|
+
values[i * 3 + 1] = sample.texture;
|
|
2784
|
+
values[i * 3 + 2] = sample.luminance;
|
|
2785
|
+
}
|
|
2786
|
+
const firstToken = this.streamingKVNextToken;
|
|
2787
|
+
await this.streamingKVCache.append(0, keys, values, firstToken);
|
|
2788
|
+
this.streamingKVNextToken += encodedTiles.length;
|
|
2789
|
+
const retainedTokenCapacity = this.streamingKVCache.config.pageSize * this.streamingKVCache.config.maxResidentPages;
|
|
2790
|
+
const kLen = Math.min(this.streamingKVNextToken, 128, retainedTokenCapacity);
|
|
2791
|
+
const lookupStart = this.streamingKVNextToken - kLen;
|
|
2792
|
+
const lookup = await this.streamingKVCache.lookup(0, lookupStart, kLen);
|
|
2793
|
+
let checksum = 0;
|
|
2794
|
+
for (let i = 0; i < lookup.keys.length; i += 1) checksum += lookup.keys[i] ?? 0;
|
|
2795
|
+
for (let i = 0; i < lookup.values.length; i += 1) checksum += lookup.values[i] ?? 0;
|
|
2796
|
+
return {
|
|
2797
|
+
layer: 0,
|
|
2798
|
+
kLen,
|
|
2799
|
+
cachedPages: this.streamingKVCache.residentPagesForLayer(0).length,
|
|
2800
|
+
lookupChecksum: checksum,
|
|
2801
|
+
memory: this.streamingKVCache.memoryUsage()
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
async step(frame) {
|
|
2805
|
+
if (!this.initialized) {
|
|
2806
|
+
throw new Error("HoloMapRuntime not initialized. Call init(config) before step(frame).");
|
|
2807
|
+
}
|
|
2808
|
+
if (!Number.isFinite(frame.timestampMs)) {
|
|
2809
|
+
throw new Error(`HoloMapRuntime.step invalid frame timestamp: ${frame.timestampMs}`);
|
|
2810
|
+
}
|
|
2811
|
+
const expectedBytes = frame.width * frame.height * frame.stride;
|
|
2812
|
+
if (frame.rgb.byteLength !== expectedBytes) {
|
|
2813
|
+
throw new Error(
|
|
2814
|
+
`HoloMapRuntime.step invalid frame byte length: got ${frame.rgb.byteLength}, expected ${expectedBytes} (w=${frame.width}, h=${frame.height}, stride=${frame.stride})`
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
if (frame.depth && frame.depth.length !== frame.width * frame.height) {
|
|
2818
|
+
throw new Error(
|
|
2819
|
+
`HoloMapRuntime.step invalid measured depth length: got ${frame.depth.length}, expected ${frame.width * frame.height} (w=${frame.width}, h=${frame.height})`
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
if (frame.depthMeters && frame.depthMeters.length !== frame.width * frame.height) {
|
|
2823
|
+
throw new Error(
|
|
2824
|
+
`HoloMapRuntime.step invalid metric depth length: got ${frame.depthMeters.length}, expected ${frame.width * frame.height} (w=${frame.width}, h=${frame.height})`
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
if (frame.depthConfidence && frame.depthConfidence.length !== frame.width * frame.height) {
|
|
2828
|
+
throw new Error(
|
|
2829
|
+
`HoloMapRuntime.step invalid measured depth confidence length: got ${frame.depthConfidence.length}, expected ${frame.width * frame.height} (w=${frame.width}, h=${frame.height})`
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
const minIntervalMs = 1e3 / Math.max(1, this.config.targetFPS);
|
|
2833
|
+
if (this.lastAcceptedFrameTimestampMs !== null && frame.timestampMs - this.lastAcceptedFrameTimestampMs < minIntervalMs) {
|
|
2834
|
+
this.perfMetrics.throttledCount += 1;
|
|
2835
|
+
logHoloMapEvent(this.runId, "step_throttled", {
|
|
2836
|
+
frameIndex: frame.index,
|
|
2837
|
+
elapsedFrameMs: Math.round(frame.timestampMs - this.lastAcceptedFrameTimestampMs),
|
|
2838
|
+
targetIntervalMs: Math.round(minIntervalMs)
|
|
2839
|
+
});
|
|
2840
|
+
return null;
|
|
2841
|
+
}
|
|
2842
|
+
const stepStartMs = performance.now();
|
|
2843
|
+
this.lastAcceptedFrameTimestampMs = frame.timestampMs;
|
|
2844
|
+
if (this.steps.length >= this.config.maxSequenceLength) {
|
|
2845
|
+
const evicted = this.steps.shift();
|
|
2846
|
+
const evictedPoints = evicted.points.positions.length / 3;
|
|
2847
|
+
this.totalPointCount -= evictedPoints;
|
|
2848
|
+
const ep = evicted.points.positions;
|
|
2849
|
+
for (let i = 0; i < ep.length; i += 3) {
|
|
2850
|
+
const x = ep[i];
|
|
2851
|
+
const y = ep[i + 1];
|
|
2852
|
+
const z = ep[i + 2];
|
|
2853
|
+
this.globalPosSum[0] -= x;
|
|
2854
|
+
this.globalPosSum[1] -= y;
|
|
2855
|
+
this.globalPosSum[2] -= z;
|
|
2856
|
+
this.globalPrincipalMomentXX -= x * x;
|
|
2857
|
+
this.globalPrincipalMomentZZ -= z * z;
|
|
2858
|
+
this.globalPrincipalMomentXZ -= x * z;
|
|
2859
|
+
}
|
|
2860
|
+
const ec = evicted.points.confidence;
|
|
2861
|
+
for (let i = 0; i < ec.length; i += 1) this.globalConfSum -= ec[i];
|
|
2862
|
+
this.boundsValid = false;
|
|
2863
|
+
for (const s of this.steps) {
|
|
2864
|
+
this.updateBounds(s.points.positions);
|
|
2865
|
+
}
|
|
2866
|
+
logHoloMapEvent(this.runId, "step_evict", {
|
|
2867
|
+
frameIndex: evicted.frameIndex,
|
|
2868
|
+
evictedPoints,
|
|
2869
|
+
retainedSteps: this.steps.length
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
const microCfg = {
|
|
2873
|
+
seed: this.config.seed,
|
|
2874
|
+
modelHash: this.config.modelHash,
|
|
2875
|
+
// Wire the loaded checkpoint through to the encoder. Previously weightBytes
|
|
2876
|
+
// was fetched + verified in init() then never passed here, so the encoder
|
|
2877
|
+
// always ran on PRNG weights even when a real checkpoint was provided.
|
|
2878
|
+
weightBytes: this.weightBytes
|
|
2879
|
+
};
|
|
2880
|
+
const configuredGrid = this.config.tileGrid ?? _HoloMapRuntimeImpl.GRID_N;
|
|
2881
|
+
const gridN = Math.max(1, Math.min(configuredGrid, frame.width, frame.height));
|
|
2882
|
+
const tiles = _HoloMapRuntimeImpl.tileFrame(frame, gridN);
|
|
2883
|
+
const numPoints = tiles.length;
|
|
2884
|
+
const positions = new Float32Array(numPoints * 3);
|
|
2885
|
+
const colors = new Uint8Array(numPoints * 3);
|
|
2886
|
+
const confidence = new Float32Array(numPoints);
|
|
2887
|
+
const aspect = frame.width / Math.max(1, frame.height);
|
|
2888
|
+
const metricIntrinsics = _HoloMapRuntimeImpl.hasUsableIntrinsics(frame.cameraIntrinsics) ? frame.cameraIntrinsics : void 0;
|
|
2889
|
+
const encodedTiles = [];
|
|
2890
|
+
for (let t = 0; t < numPoints; t += 1) {
|
|
2891
|
+
const sample = tiles[t];
|
|
2892
|
+
const xyz = await this.encodeTile(sample.tile, microCfg);
|
|
2893
|
+
const latentX = xyz[0] ?? 0;
|
|
2894
|
+
const latentY = xyz[1] ?? 0;
|
|
2895
|
+
const latentZ = xyz[2] ?? 0;
|
|
2896
|
+
const depthHint = (sample.luminance - 0.5) * 0.34 + sample.texture * 0.24;
|
|
2897
|
+
encodedTiles.push({
|
|
2898
|
+
...sample,
|
|
2899
|
+
latent: [latentX, latentY, latentZ],
|
|
2900
|
+
rawDepthSignal: depthHint + latentZ * 0.12,
|
|
2901
|
+
metricDepthMeters: frame.depthMeters ? _HoloMapRuntimeImpl.sampleMetricDepthNearestUv(
|
|
2902
|
+
frame.depthMeters,
|
|
2903
|
+
frame.width,
|
|
2904
|
+
frame.height,
|
|
2905
|
+
sample.centerUv
|
|
2906
|
+
) : void 0
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
const streamingKV = await this.updateStreamingKVCache(encodedTiles);
|
|
2910
|
+
const depthAlignment = _HoloMapRuntimeImpl.fitShiftScaleDepth(encodedTiles);
|
|
2911
|
+
const loopDescriptor = _HoloMapRuntimeImpl.buildLoopClosureDescriptor(encodedTiles);
|
|
2912
|
+
const rawDevicePose = frame.devicePose ? {
|
|
2913
|
+
position: [...frame.devicePose.position],
|
|
2914
|
+
rotation: [...frame.devicePose.rotation],
|
|
2915
|
+
confidence: frame.devicePose.confidence
|
|
2916
|
+
} : void 0;
|
|
2917
|
+
let loopCorrectedPose;
|
|
2918
|
+
if (frame.depthMeters && metricIntrinsics) {
|
|
2919
|
+
for (const kf of this.poseHistory) {
|
|
2920
|
+
if (frame.index - kf.frameIndex < _HoloMapRuntimeImpl.LOOP_CLOSURE_MIN_GAP) continue;
|
|
2921
|
+
const descriptorDistance = _HoloMapRuntimeImpl.descriptorDistance(
|
|
2922
|
+
loopDescriptor,
|
|
2923
|
+
kf.descriptor
|
|
2924
|
+
);
|
|
2925
|
+
if (descriptorDistance <= _HoloMapRuntimeImpl.LOOP_CLOSURE_DESCRIPTOR_DISTANCE) {
|
|
2926
|
+
this.lastLoopClosureFrameIdx = frame.index;
|
|
2927
|
+
if (rawDevicePose) {
|
|
2928
|
+
loopCorrectedPose = {
|
|
2929
|
+
...rawDevicePose,
|
|
2930
|
+
position: [...kf.position],
|
|
2931
|
+
confidence: Math.min(1, Math.max(rawDevicePose.confidence, 0.9))
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
break;
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
const metricProjectionPose = loopCorrectedPose ?? rawDevicePose;
|
|
2939
|
+
let centroidX = 0;
|
|
2940
|
+
let centroidY = 0;
|
|
2941
|
+
let centroidZ = 0;
|
|
2942
|
+
let confidenceSum = 0;
|
|
2943
|
+
let framePrincipalMomentXX = 0;
|
|
2944
|
+
let framePrincipalMomentZZ = 0;
|
|
2945
|
+
let framePrincipalMomentXZ = 0;
|
|
2946
|
+
for (let t = 0; t < numPoints; t += 1) {
|
|
2947
|
+
const { meanColor, centerUv, texture, latent, rawDepthSignal } = encodedTiles[t];
|
|
2948
|
+
const [latentX, latentY, latentZ] = latent;
|
|
2949
|
+
const planeX = (centerUv[0] - 0.5) * 2 * aspect;
|
|
2950
|
+
const planeY = (0.5 - centerUv[1]) * 2;
|
|
2951
|
+
let px = planeX + latentX * 0.08;
|
|
2952
|
+
let py = planeY + latentY * 0.08;
|
|
2953
|
+
let pz;
|
|
2954
|
+
const metricDepth = _HoloMapRuntimeImpl.alignedDepthMeters(encodedTiles[t], depthAlignment);
|
|
2955
|
+
if (metricDepth !== void 0 && metricIntrinsics) {
|
|
2956
|
+
const worldPoint = _HoloMapRuntimeImpl.projectMetricDepthToWorld(
|
|
2957
|
+
centerUv,
|
|
2958
|
+
metricDepth,
|
|
2959
|
+
metricIntrinsics,
|
|
2960
|
+
metricProjectionPose
|
|
2961
|
+
);
|
|
2962
|
+
px = worldPoint[0];
|
|
2963
|
+
py = worldPoint[1];
|
|
2964
|
+
pz = worldPoint[2];
|
|
2965
|
+
} else if (metricDepth !== void 0) {
|
|
2966
|
+
pz = -metricDepth;
|
|
2967
|
+
} else if (frame.depth) {
|
|
2968
|
+
const measured = _HoloMapRuntimeImpl.sampleDepthNearestUv(
|
|
2969
|
+
frame.depth,
|
|
2970
|
+
frame.width,
|
|
2971
|
+
frame.height,
|
|
2972
|
+
centerUv
|
|
2973
|
+
);
|
|
2974
|
+
pz = (0.5 - measured) * _HoloMapRuntimeImpl.DEPTH_Z_SCALE;
|
|
2975
|
+
} else {
|
|
2976
|
+
pz = rawDepthSignal;
|
|
2977
|
+
}
|
|
2978
|
+
positions[t * 3] = px;
|
|
2979
|
+
positions[t * 3 + 1] = py;
|
|
2980
|
+
positions[t * 3 + 2] = pz;
|
|
2981
|
+
colors[t * 3] = meanColor[0];
|
|
2982
|
+
colors[t * 3 + 1] = meanColor[1];
|
|
2983
|
+
colors[t * 3 + 2] = meanColor[2];
|
|
2984
|
+
const latentMag = Math.sqrt(latentX * latentX + latentY * latentY + latentZ * latentZ);
|
|
2985
|
+
const baseConfidence = Math.min(
|
|
2986
|
+
1,
|
|
2987
|
+
0.45 + 0.35 / (1 + latentMag) + Math.min(0.2, texture * 1.5)
|
|
2988
|
+
);
|
|
2989
|
+
if (frame.depthConfidence) {
|
|
2990
|
+
const sensorConfidence = _HoloMapRuntimeImpl.sampleDepthNearestUv(
|
|
2991
|
+
frame.depthConfidence,
|
|
2992
|
+
frame.width,
|
|
2993
|
+
frame.height,
|
|
2994
|
+
centerUv
|
|
2995
|
+
);
|
|
2996
|
+
confidence[t] = baseConfidence * sensorConfidence;
|
|
2997
|
+
} else {
|
|
2998
|
+
confidence[t] = baseConfidence;
|
|
2999
|
+
}
|
|
3000
|
+
confidenceSum += confidence[t];
|
|
3001
|
+
centroidX += px;
|
|
3002
|
+
centroidY += py;
|
|
3003
|
+
centroidZ += pz;
|
|
3004
|
+
framePrincipalMomentXX += px * px;
|
|
3005
|
+
framePrincipalMomentZZ += pz * pz;
|
|
3006
|
+
framePrincipalMomentXZ += px * pz;
|
|
3007
|
+
}
|
|
3008
|
+
const inv = 1 / Math.max(1, numPoints);
|
|
3009
|
+
const poseX = centroidX * inv;
|
|
3010
|
+
const poseY = centroidY * inv;
|
|
3011
|
+
const poseZ = centroidZ * inv;
|
|
3012
|
+
const meanConfidence = confidenceSum * inv;
|
|
3013
|
+
const derivedPosition = [poseX, poseY, poseZ];
|
|
3014
|
+
const poseConfidence = Math.max(0, Math.min(1, meanConfidence));
|
|
3015
|
+
const cameraPose = metricProjectionPose ? {
|
|
3016
|
+
position: [...metricProjectionPose.position],
|
|
3017
|
+
rotation: [...metricProjectionPose.rotation],
|
|
3018
|
+
confidence: metricProjectionPose.confidence
|
|
3019
|
+
} : {
|
|
3020
|
+
position: derivedPosition,
|
|
3021
|
+
rotation: _HoloMapRuntimeImpl.estimatePrincipalAxisRotation([positions], derivedPosition),
|
|
3022
|
+
confidence: poseConfidence
|
|
3023
|
+
};
|
|
3024
|
+
const curPose = [
|
|
3025
|
+
cameraPose.position[0],
|
|
3026
|
+
cameraPose.position[1],
|
|
3027
|
+
cameraPose.position[2]
|
|
3028
|
+
];
|
|
3029
|
+
let interFrameDeltaMeters = 0;
|
|
3030
|
+
let interFrameRotationRadians = 0;
|
|
3031
|
+
if (this.prevCameraPose) {
|
|
3032
|
+
interFrameDeltaMeters = _HoloMapRuntimeImpl.poseDistanceMeters(
|
|
3033
|
+
cameraPose,
|
|
3034
|
+
this.prevCameraPose
|
|
3035
|
+
);
|
|
3036
|
+
interFrameRotationRadians = _HoloMapRuntimeImpl.poseRotationDeltaRadians(
|
|
3037
|
+
cameraPose,
|
|
3038
|
+
this.prevCameraPose
|
|
3039
|
+
);
|
|
3040
|
+
const uncertaintyWeight = Math.max(
|
|
3041
|
+
0.05,
|
|
3042
|
+
1 - Math.min(cameraPose.confidence, this.prevCameraPose.confidence)
|
|
3043
|
+
);
|
|
3044
|
+
this.cumulativeDriftMeters += interFrameDeltaMeters * uncertaintyWeight + interFrameRotationRadians * 0.05;
|
|
3045
|
+
}
|
|
3046
|
+
if (this.prevPose) {
|
|
3047
|
+
const velocity = [
|
|
3048
|
+
curPose[0] - this.prevPose[0],
|
|
3049
|
+
curPose[1] - this.prevPose[1],
|
|
3050
|
+
curPose[2] - this.prevPose[2]
|
|
3051
|
+
];
|
|
3052
|
+
if (this.prevVelocity) {
|
|
3053
|
+
const predX = this.prevPose[0] + this.prevVelocity[0];
|
|
3054
|
+
const predY = this.prevPose[1] + this.prevVelocity[1];
|
|
3055
|
+
const predZ = this.prevPose[2] + this.prevVelocity[2];
|
|
3056
|
+
const rx = curPose[0] - predX;
|
|
3057
|
+
const ry = curPose[1] - predY;
|
|
3058
|
+
const rz = curPose[2] - predZ;
|
|
3059
|
+
this.cumulativeDriftMeters += Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
3060
|
+
}
|
|
3061
|
+
this.prevVelocity = velocity;
|
|
3062
|
+
}
|
|
3063
|
+
this.prevPose = curPose;
|
|
3064
|
+
this.prevCameraPose = cameraPose;
|
|
3065
|
+
if (this.lastLoopClosureFrameIdx !== frame.index) {
|
|
3066
|
+
for (const kf of this.poseHistory) {
|
|
3067
|
+
if (frame.index - kf.frameIndex < _HoloMapRuntimeImpl.LOOP_CLOSURE_MIN_GAP) continue;
|
|
3068
|
+
const dx = curPose[0] - kf.position[0];
|
|
3069
|
+
const dy = curPose[1] - kf.position[1];
|
|
3070
|
+
const dz = curPose[2] - kf.position[2];
|
|
3071
|
+
if (Math.sqrt(dx * dx + dy * dy + dz * dz) < _HoloMapRuntimeImpl.LOOP_CLOSURE_RADIUS) {
|
|
3072
|
+
this.lastLoopClosureFrameIdx = frame.index;
|
|
3073
|
+
break;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
this.poseHistory.push({
|
|
3078
|
+
frameIndex: frame.index,
|
|
3079
|
+
position: curPose,
|
|
3080
|
+
descriptor: loopDescriptor
|
|
3081
|
+
});
|
|
3082
|
+
if (this.poseHistory.length > _HoloMapRuntimeImpl.POSE_HISTORY_CAP) this.poseHistory.shift();
|
|
3083
|
+
this.updateBounds(positions);
|
|
3084
|
+
this.globalPosSum[0] += centroidX;
|
|
3085
|
+
this.globalPosSum[1] += centroidY;
|
|
3086
|
+
this.globalPosSum[2] += centroidZ;
|
|
3087
|
+
this.globalConfSum += confidenceSum;
|
|
3088
|
+
this.globalPrincipalMomentXX += framePrincipalMomentXX;
|
|
3089
|
+
this.globalPrincipalMomentZZ += framePrincipalMomentZZ;
|
|
3090
|
+
this.globalPrincipalMomentXZ += framePrincipalMomentXZ;
|
|
3091
|
+
const totalPoints = this.totalPointCount + numPoints;
|
|
3092
|
+
const ginv = 1 / Math.max(1, totalPoints);
|
|
3093
|
+
const anchorCenter = [
|
|
3094
|
+
this.globalPosSum[0] * ginv,
|
|
3095
|
+
this.globalPosSum[1] * ginv,
|
|
3096
|
+
this.globalPosSum[2] * ginv
|
|
3097
|
+
];
|
|
3098
|
+
const globalMeanConfidence = this.globalConfSum * ginv;
|
|
3099
|
+
const b = this.getBounds();
|
|
3100
|
+
const extentX = b.max[0] - b.min[0];
|
|
3101
|
+
const extentY = b.max[1] - b.min[1];
|
|
3102
|
+
const extentZ = b.max[2] - b.min[2];
|
|
3103
|
+
const anchorRotation = _HoloMapRuntimeImpl.estimatePrincipalAxisRotationFromMoments(
|
|
3104
|
+
this.globalPrincipalMomentXX,
|
|
3105
|
+
this.globalPrincipalMomentZZ,
|
|
3106
|
+
this.globalPrincipalMomentXZ,
|
|
3107
|
+
totalPoints,
|
|
3108
|
+
anchorCenter
|
|
3109
|
+
);
|
|
3110
|
+
const anchorDescriptor = new Float32Array([extentX, extentY, extentZ, globalMeanConfidence]);
|
|
3111
|
+
this.pushTrajectoryKeyframe({
|
|
3112
|
+
frameIndex: frame.index,
|
|
3113
|
+
timestampMs: frame.timestampMs,
|
|
3114
|
+
pose: cameraPose,
|
|
3115
|
+
embedding: _HoloMapRuntimeImpl.buildTrajectoryEmbedding(
|
|
3116
|
+
cameraPose,
|
|
3117
|
+
anchorDescriptor,
|
|
3118
|
+
interFrameDeltaMeters,
|
|
3119
|
+
interFrameRotationRadians
|
|
3120
|
+
)
|
|
3121
|
+
});
|
|
3122
|
+
const trajectoryKeyframes = _HoloMapRuntimeImpl.cloneTrajectoryKeyframes(
|
|
3123
|
+
this.trajectoryKeyframes,
|
|
3124
|
+
_HoloMapRuntimeImpl.TRAJECTORY_STEP_SNAPSHOT_CAP
|
|
3125
|
+
);
|
|
3126
|
+
const step = {
|
|
3127
|
+
frame,
|
|
3128
|
+
pose: cameraPose,
|
|
3129
|
+
points: {
|
|
3130
|
+
positions,
|
|
3131
|
+
colors,
|
|
3132
|
+
confidence
|
|
3133
|
+
},
|
|
3134
|
+
...streamingKV ? { streamingKV } : {},
|
|
3135
|
+
trajectory: {
|
|
3136
|
+
keyframes: trajectoryKeyframes,
|
|
3137
|
+
estimatedDriftMeters: this.cumulativeDriftMeters,
|
|
3138
|
+
lastLoopClosureFrame: this.lastLoopClosureFrameIdx,
|
|
3139
|
+
revision: frame.index + 1
|
|
3140
|
+
},
|
|
3141
|
+
anchor: {
|
|
3142
|
+
anchorFrameIndex: this.trajectoryKeyframes[0]?.frameIndex ?? frame.index,
|
|
3143
|
+
anchorPose: {
|
|
3144
|
+
position: anchorCenter,
|
|
3145
|
+
rotation: anchorRotation,
|
|
3146
|
+
confidence: Math.max(0, Math.min(1, globalMeanConfidence))
|
|
3147
|
+
},
|
|
3148
|
+
anchorDescriptor,
|
|
3149
|
+
revision: frame.index + 1
|
|
3150
|
+
},
|
|
3151
|
+
...depthAlignment ? { depthAlignment } : {}
|
|
3152
|
+
};
|
|
3153
|
+
this.steps.push({
|
|
3154
|
+
frameIndex: frame.index,
|
|
3155
|
+
points: {
|
|
3156
|
+
positions,
|
|
3157
|
+
confidence
|
|
3158
|
+
}
|
|
3159
|
+
});
|
|
3160
|
+
this.totalPointCount += numPoints;
|
|
3161
|
+
const stepMs = performance.now() - stepStartMs;
|
|
3162
|
+
this.perfMetrics.stepCount += 1;
|
|
3163
|
+
this.perfMetrics.totalStepMs += stepMs;
|
|
3164
|
+
if (stepMs > this.perfMetrics.maxStepMs) this.perfMetrics.maxStepMs = stepMs;
|
|
3165
|
+
if (stepMs < this.perfMetrics.minStepMs) this.perfMetrics.minStepMs = stepMs;
|
|
3166
|
+
logHoloMapEvent(this.runId, "step", {
|
|
3167
|
+
frameIndex: frame.index,
|
|
3168
|
+
pointCount: numPoints,
|
|
3169
|
+
stepMs: Math.round(stepMs),
|
|
3170
|
+
avgStepMs: Math.round(this.perfMetrics.totalStepMs / this.perfMetrics.stepCount),
|
|
3171
|
+
throttledSoFar: this.perfMetrics.throttledCount
|
|
3172
|
+
});
|
|
3173
|
+
return step;
|
|
3174
|
+
}
|
|
3175
|
+
async finalize() {
|
|
3176
|
+
if (!this.initialized) {
|
|
3177
|
+
throw new Error("HoloMapRuntime not initialized. Call init(config) before finalize().");
|
|
3178
|
+
}
|
|
3179
|
+
const frameCount = this.steps.length;
|
|
3180
|
+
const pointCount = this.totalPointCount;
|
|
3181
|
+
const bounds = this.getBounds();
|
|
3182
|
+
const avgStepMs = this.perfMetrics.stepCount > 0 ? this.perfMetrics.totalStepMs / this.perfMetrics.stepCount : 0;
|
|
3183
|
+
logHoloMapEvent(this.runId, "finalize", {
|
|
3184
|
+
frameCount,
|
|
3185
|
+
pointCount,
|
|
3186
|
+
avgStepMs: Math.round(avgStepMs),
|
|
3187
|
+
maxStepMs: Math.round(this.perfMetrics.maxStepMs),
|
|
3188
|
+
minStepMs: Math.round(this.perfMetrics.minStepMs),
|
|
3189
|
+
throttledCount: this.perfMetrics.throttledCount,
|
|
3190
|
+
sessionDurationMs: Math.round(performance.now() - this.sessionStartMs)
|
|
3191
|
+
});
|
|
3192
|
+
const reconstruction = {
|
|
3193
|
+
mode: this.config.reconstructionMode ?? "preview",
|
|
3194
|
+
modelHash: this.config.modelHash,
|
|
3195
|
+
tileGrid: this.config.tileGrid ?? _HoloMapRuntimeImpl.GRID_N,
|
|
3196
|
+
weightLoadedBytes: this.weightBytes?.byteLength ?? 0,
|
|
3197
|
+
weightVerified: this.weightVerified,
|
|
3198
|
+
...this.config.weightCid ? { weightCid: this.config.weightCid } : {},
|
|
3199
|
+
...this.weightSource ? { weightSource: this.weightSource } : {}
|
|
3200
|
+
};
|
|
3201
|
+
const manifest = {
|
|
3202
|
+
version: "1.0.0",
|
|
3203
|
+
worldId: `holomap-${this.replayKey}`,
|
|
3204
|
+
displayName: "HoloMap Reconstruction",
|
|
3205
|
+
pointCount,
|
|
3206
|
+
frameCount,
|
|
3207
|
+
bounds,
|
|
3208
|
+
replayHash: this.replayKey,
|
|
3209
|
+
simulationContract: {
|
|
3210
|
+
kind: chunkN2ILK7BF_cjs.HOLOMAP_SIMULATION_CONTRACT_KIND,
|
|
3211
|
+
replayFingerprint: this.replayKey,
|
|
3212
|
+
holoScriptBuild: getVersionString()
|
|
3213
|
+
},
|
|
3214
|
+
provenance: {
|
|
3215
|
+
anchorHash: `self-attested:${this.replayKey}`,
|
|
3216
|
+
capturedAtIso: (/* @__PURE__ */ new Date()).toISOString()
|
|
3217
|
+
},
|
|
3218
|
+
assets: {
|
|
3219
|
+
points: "reconstruction.points.bin",
|
|
3220
|
+
trajectory: "reconstruction.trajectory.json",
|
|
3221
|
+
anchors: "reconstruction.anchors.json"
|
|
3222
|
+
},
|
|
3223
|
+
weightStrategy: this.config.weightStrategy ?? "distill",
|
|
3224
|
+
reconstruction
|
|
3225
|
+
};
|
|
3226
|
+
try {
|
|
3227
|
+
return await anchorReconstructionManifest(manifest, this.config.provenanceAnchorProvider);
|
|
3228
|
+
} catch (error) {
|
|
3229
|
+
logHoloMapEvent(this.runId, "anchor_failed", {
|
|
3230
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
3231
|
+
});
|
|
3232
|
+
return selfAttestReconstructionManifest(manifest);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
replayHash() {
|
|
3236
|
+
return this.replayKey;
|
|
3237
|
+
}
|
|
3238
|
+
async dispose() {
|
|
3239
|
+
logHoloMapEvent(this.runId, "dispose", {
|
|
3240
|
+
stepsRetained: this.steps.length,
|
|
3241
|
+
totalPointCount: this.totalPointCount,
|
|
3242
|
+
throttledCount: this.perfMetrics.throttledCount
|
|
3243
|
+
});
|
|
3244
|
+
this.initialized = false;
|
|
3245
|
+
this.steps.length = 0;
|
|
3246
|
+
this.microEncoder = null;
|
|
3247
|
+
this.encoderDevice = null;
|
|
3248
|
+
await this.streamingKVCache?.dispose();
|
|
3249
|
+
this.streamingKVCache = null;
|
|
3250
|
+
this.streamingKVNextToken = 0;
|
|
3251
|
+
this.weightBytes = null;
|
|
3252
|
+
this.weightVerified = false;
|
|
3253
|
+
this.weightSource = null;
|
|
3254
|
+
this.totalPointCount = 0;
|
|
3255
|
+
this.cumulativeDriftMeters = 0;
|
|
3256
|
+
this.prevPose = null;
|
|
3257
|
+
this.prevCameraPose = null;
|
|
3258
|
+
this.prevVelocity = null;
|
|
3259
|
+
this.lastLoopClosureFrameIdx = -1;
|
|
3260
|
+
this.poseHistory.length = 0;
|
|
3261
|
+
this.trajectoryKeyframes.length = 0;
|
|
3262
|
+
this.globalPosSum = [0, 0, 0];
|
|
3263
|
+
this.globalConfSum = 0;
|
|
3264
|
+
this.globalPrincipalMomentXX = 0;
|
|
3265
|
+
this.globalPrincipalMomentZZ = 0;
|
|
3266
|
+
this.globalPrincipalMomentXZ = 0;
|
|
3267
|
+
this.boundsValid = false;
|
|
3268
|
+
this.lastAcceptedFrameTimestampMs = null;
|
|
3269
|
+
this.perfMetrics = {
|
|
3270
|
+
stepCount: 0,
|
|
3271
|
+
throttledCount: 0,
|
|
3272
|
+
totalStepMs: 0,
|
|
3273
|
+
maxStepMs: 0,
|
|
3274
|
+
minStepMs: Infinity
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
};
|
|
3278
|
+
_HoloMapRuntimeImpl.LOOP_CLOSURE_MIN_GAP = 3;
|
|
3279
|
+
_HoloMapRuntimeImpl.LOOP_CLOSURE_RADIUS = 0.05;
|
|
3280
|
+
_HoloMapRuntimeImpl.POSE_HISTORY_CAP = 512;
|
|
3281
|
+
_HoloMapRuntimeImpl.TRAJECTORY_KEYFRAME_CAP = 512;
|
|
3282
|
+
_HoloMapRuntimeImpl.TRAJECTORY_STEP_SNAPSHOT_CAP = 64;
|
|
3283
|
+
/** Maps normalized measured depth [0=near,1=far] to a point Z range matching
|
|
3284
|
+
* the monocular estimate's spread (±~0.17), so sensor and estimate paths
|
|
3285
|
+
* produce geometry in the same coordinate scale. */
|
|
3286
|
+
_HoloMapRuntimeImpl.DEPTH_Z_SCALE = 0.34;
|
|
3287
|
+
_HoloMapRuntimeImpl.LOOP_CLOSURE_DESCRIPTOR_DISTANCE = 0.015;
|
|
3288
|
+
/**
|
|
3289
|
+
* Default number of tiles per axis used to fan the encoder across the frame.
|
|
3290
|
+
* Total points emitted = tileGrid * tileGrid (one per tile).
|
|
3291
|
+
* Each tile runs the full 8-kernel transformer chain via the micro encoder
|
|
3292
|
+
* (imagePatchEmbed → layerNorm → gemm Q/K/V → rope → fusedMHA →
|
|
3293
|
+
* layerNorm → gelu → gemm xyz). pagedKV append/lookup remains available
|
|
3294
|
+
* for future streaming kLen>1 paths.
|
|
3295
|
+
*/
|
|
3296
|
+
_HoloMapRuntimeImpl.GRID_N = 4;
|
|
3297
|
+
_HoloMapRuntimeImpl.DENSE_PROOF_GRID_N = 16;
|
|
3298
|
+
_HoloMapRuntimeImpl.MAX_GRID_N = 32;
|
|
3299
|
+
var HoloMapRuntimeImpl = _HoloMapRuntimeImpl;
|
|
3300
|
+
function createHoloMapRuntime(_config) {
|
|
3301
|
+
const runtime = new HoloMapRuntimeImpl();
|
|
3302
|
+
if (_config) {
|
|
3303
|
+
void runtime.init({ ...HOLOMAP_DEFAULTS, ..._config });
|
|
3304
|
+
}
|
|
3305
|
+
return runtime;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
exports.BUILD_TIMESTAMP = BUILD_TIMESTAMP;
|
|
3309
|
+
exports.GIT_COMMIT_SHA = GIT_COMMIT_SHA;
|
|
3310
|
+
exports.HOLOMAP_DEFAULTS = HOLOMAP_DEFAULTS;
|
|
3311
|
+
exports.HOLOMAP_DENSE_PROOF_DEFAULTS = HOLOMAP_DENSE_PROOF_DEFAULTS;
|
|
3312
|
+
exports.HOLOSCRIPT_VERSION = HOLOSCRIPT_VERSION;
|
|
3313
|
+
exports.anchorReconstructionManifest = anchorReconstructionManifest;
|
|
3314
|
+
exports.assertWeightCid = assertWeightCid;
|
|
3315
|
+
exports.computeReconstructionManifestDigest = computeReconstructionManifestDigest;
|
|
3316
|
+
exports.createHoloMapMicroEncoder = createHoloMapMicroEncoder;
|
|
3317
|
+
exports.createHoloMapRunId = createHoloMapRunId;
|
|
3318
|
+
exports.createHoloMapRuntime = createHoloMapRuntime;
|
|
3319
|
+
exports.createPagedKVCache = createPagedKVCache;
|
|
3320
|
+
exports.deserializeMicroWeights = deserializeMicroWeights;
|
|
3321
|
+
exports.frameToMicroImage = frameToMicroImage;
|
|
3322
|
+
exports.getCachedWeightBlob = getCachedWeightBlob;
|
|
3323
|
+
exports.getVersionInfo = getVersionInfo;
|
|
3324
|
+
exports.getVersionString = getVersionString;
|
|
3325
|
+
exports.loadHoloMapWeightBlob = loadHoloMapWeightBlob;
|
|
3326
|
+
exports.logHoloMapEvent = logHoloMapEvent;
|
|
3327
|
+
exports.mergeAnchoredProvenance = mergeAnchoredProvenance;
|
|
3328
|
+
exports.normalizeWeightCidDigest = normalizeWeightCidDigest;
|
|
3329
|
+
exports.putCachedWeightBlob = putCachedWeightBlob;
|
|
3330
|
+
exports.resetHoloMapTelemetryForTests = resetHoloMapTelemetryForTests;
|
|
3331
|
+
exports.runHoloMapMicroEncoderCpu = runHoloMapMicroEncoderCpu;
|
|
3332
|
+
exports.selfAttestReconstructionManifest = selfAttestReconstructionManifest;
|
|
3333
|
+
exports.serializeMicroWeights = serializeMicroWeights;
|
|
3334
|
+
exports.stripAnchoredProvenance = stripAnchoredProvenance;
|
|
3335
|
+
exports.tryCreateHoloMapEncoderDevice = tryCreateHoloMapEncoderDevice;
|
|
3336
|
+
//# sourceMappingURL=chunk-VQOTDNFL.cjs.map
|
|
3337
|
+
//# sourceMappingURL=chunk-VQOTDNFL.cjs.map
|