GPU throughput: measured bottlenecks and the plan to remove them
Status: accepted, 2026-10-21, as the order of work (owner request: maximum parallel throughput). Each item is verified by its own pull request. No kernel changes in this document's pull request. It adds the
measurement script scripts/profile-kernels.mjs and this record. Each item in "The plan" is its
own pull request with its own evidence.
Request source: the owner, 2026-10-21 (translated): more memory only lets a stage use more within its limit. What matters more is to accelerate, to process with the maximum possible parallelism, to split work finer where it can be split, and to put data in workgroup memory and the other faster tiers. Moving memory up and down is a bottleneck. Recent papers, techniques from games, other libraries and compression are all open.
Context: pull request #161 (dense MVS residency, docs/dense-mvs.md Amendment 3) and #167 (BA and
triangulation streaming) made every stage run within any memory budget. They did not make it
faster. This record asks where the time goes on the owner's GPU and what changes it.
Baseline: main at 4b16cd5, compiler pin 3c6e964. TypeShade documents are those of the pin
(node_modules/typeshade/AUTHORING.md, docs/use-typeshade-surface.md, changes/).
1. Summary
Facts (section 3):
- On the owner's RTX 5060 Ti, five stages hold 80 to 90 percent of a run: guided matching, pair verification, bundle adjustment, dense photometric propagation and dense geometric propagation. Triangulation and PnP hold another 11 to 14 percent.
- The GPU is mostly idle in the sparse stages. They run 6 to 40 ms per dispatch and 0.5 ms per dispatch in pair verification, with 8-lane and 64-lane workgroups of tiny grids. This is latency and launch bound, not arithmetic or bandwidth bound.
- The BA's PCG iteration costs about 7 ms (newest pin) to 17 ms (older pins). The bytes it moves would take about 25 microseconds at the card's memory bandwidth. The iteration is over 100 times above its bandwidth floor.
- Chunk timing measures the fence, not the kernel. Many timed chunks end at 3 to 8 ms whatever their size. The learned rate then makes chunks too small, and the GPU never fills.
- Dense propagation is f32 and holds 97 percent of the dense GPU time on SwiftShader. It rereads the reference window and recomputes a weight and a relative pose for every hypothesis and every source.
Plan (section 7), by measured gain per effort. Estimates are inferences, and each pull request measures its own gain.
| # | Item | Stage hit | Expected gain on the 5060 Ti |
|---|---|---|---|
| 1 | Per-kernel GPU time and adapter features in the record | all (measurement) | none by itself, decides the rest |
| 2 | Fill the device before splitting a chunk | triangulation, PnP, pair verification | 8 to 12 s of 75 s |
| 3 | The guide of guided matching estimates E only | guided matching | 8 to 12 s of 75 s |
| 4 | Dense propagation: hoist, then pack the gray | dense photometric and geometric | 25 to 40 percent of the dense time |
| 5 | BA: one workgroup per camera row group in the PCG product | bundle | 1.5 to 3 times on the BA stage |
| 6 | BA: explicit reduced camera system and tiled Cholesky | bundle | 3 to 8 times on the BA stage |
| 7 | Hardware bilinear lookups for the source images | dense propagation | 1.5 to 2.5 times on propagation |
| 8 | Multi-resolution propagation (ACMM style) | dense photometric | 2 to 3 times, needs a quality gate |
Open decisions for the owner are in section 9.
2. Method and configurations
Run records. The owner's collector (Cloudflare KV namespace 1c8702ece02640b7bfdf7dca1af2e6e2,
keys record/<ts>-<8hex>) was read on 2026-10-21 with wrangler kv key get. A record holds
timing.stages (wall time, calls, dispatches and submissions per stage), timing.chunks,
timing.kernels (the kernel names in first-use order, no times), gpu, mainThread, transfers
and dense. A record holds no time for a single kernel. Section 4 says what that costs.
| Record | Site commit | Adapter | Input | Total |
|---|---|---|---|---|
ed932d77 |
351fe7e |
NVIDIA Blackwell | pyramid video, 22 of 43 frames, dense | 88.5 s |
28dcfe16 |
351fe7e |
NVIDIA Blackwell | pyramid video, 24 of 47 frames, dense | 88.9 s |
0de918f8 |
a121ae0 |
NVIDIA Blackwell | COLMAP south-building, 24 photos, dense | 78.2 s |
4a8aa2ff |
d89ef71 |
NVIDIA Blackwell | pyramid video, 22 of 87 frames, dense | 101.3 s |
f1edec5c |
53d3a7c |
NVIDIA Blackwell | Chimney Rock video, 27 frames, 1280x720 | 75.1 s |
c39246e7 |
5200253 |
NVIDIA Turing (RTX 20) | COLMAP south-building, 24 photos, dense | 318.0 s |
19f18274 |
edb0d35 |
NVIDIA Blackwell | multi-video, 354 frames, 93 registered | 628.8 s |
fad31ad6 |
f2473c9 |
NVIDIA Blackwell | the same multi-video input | 169.5 s |
d40f8950 |
f2473c9 |
NVIDIA Blackwell | multi-video, 839 frames, failed | no stage |
Local profile. scripts/profile-kernels.mjs wraps the TypeShade device's fuse() and sums, for
each kernel name, the gpuTime that timestamp-query reports. It ran on 2026-10-21 in headless
Chromium 1194 with SwiftShader, on a shared 4-core container, at 4b16cd5:
npm run build
node scripts/profile-kernels.mjs --case mvs --scene south-building-24 --ids 0,1,2,3,4,5 --scale 2
node scripts/profile-kernels.mjs --case bundle --scene south-building-24 --scale 1
node scripts/profile-kernels.mjs --case sift --scene south-building-24 --ids 0,1,2,3
node scripts/profile-kernels.mjs --case pair --scene south-building-24 --scale 1
Warning about SwiftShader. It is a CPU emulation. It ranks kernels by arithmetic and by barrier
cost, and it has no launch latency, no occupancy and no memory hierarchy worth the name. A
SwiftShader share is evidence of where arithmetic goes. It is not evidence of where a
physical GPU waits. The earlier comparison in docs/lab-run-records.md ("SwiftShader against the
RTX 2080") shows the gap: features ran 111 times faster on the card, pair verification 1.4 times.
Adapter facts. SwiftShader in this container offers the features subgroups, timestamp-query,
float32-filterable, indirect-first-instance and the texture-compression set. It does not offer
shader-f16. Its limits: 32768 bytes of workgroup storage, 256 invocations per workgroup, 10
storage buffers per stage. The run record keeps neither the features nor the limits of the
owner's adapter, so this record cannot say whether the 5060 Ti under Chrome 153 offers shader-f16
and subgroups. Chrome ships subgroups since version 134
(new in WebGPU 134), so the owner's browser
is expected to offer it. Item 1 records it.
3. Measured baseline
3.1 Time per stage on the owner's GPU
Seconds, with the share of the run's total. Source: timing.stages and dense.stageMs of the
records. Blank means the record has no such stage. The two multi-video records are in 3.4.
| Stage | ed932d77 |
28dcfe16 |
0de918f8 |
4a8aa2ff |
f1edec5c |
c39246e7 (Turing) |
|---|---|---|---|---|---|---|
| features | 1.3 (2%) | 1.4 (2%) | 1.6 (2%) | 1.5 (1%) | 1.7 (2%) | - |
| matches | 0.8 (1%) | 0.8 (1%) | 1.4 (2%) | 0.7 (1%) | 1.0 (1%) | - |
| guided matching | 17.5 (20%) | 18.1 (20%) | 13.2 (17%) | 17.5 (17%) | 17.6 (23%) | - |
| pair verification | 14.6 (16%) | 17.1 (19%) | 10.0 (13%) | 15.3 (15%) | 16.1 (21%) | 68.0 (21%) |
| triangulation | 5.4 (6%) | 5.0 (6%) | 5.3 (7%) | 5.7 (6%) | 5.4 (7%) | 11.4 (4%) |
| bundle | 39.3 (44%) | 35.9 (40%) | 38.3 (49%) | 37.9 (37%) | 11.0 (15%) | 150.2 (47%) |
| observations | 1.5 (2%) | 1.8 (2%) | 1.4 (2%) | 1.5 (1%) | 1.9 (3%) | 4.6 (1%) |
| PnP | 5.0 (6%) | 5.0 (6%) | 5.3 (7%) | 5.5 (5%) | 5.0 (7%) | 10.5 (3%) |
| dense photometric | 15.6 (18%) | 7.3 (8%) | 26.3 (34%) | 17.9 (18%) | - | 66.9 (21%) |
| dense geometric | 4.5 (5%) | 3.3 (4%) | 3.6 (5%) | 4.4 (4%) | - | 13.9 (4%) |
| dense fusion | 0.6 (1%) | 0.6 (1%) | 0.7 (1%) | 0.6 (1%) | - | 1.3 (0%) |
| total | 88.5 | 88.9 | 78.2 | 101.3 | 75.1 | 318.0 |
The sum of the rows is below the total by 5 to 10 percent: frame decode, upload, mapper control on the host and the viewer.
Observation (fact). The bundle row falls from 36 to 39 s in four records to 11 s in f1edec5c
(27 views and 33458 observations, more than 28dcfe16's 24 views and 27062). Between them the pin
moved to 3c6e964 (#164, the removal of the typeshade/typeshade#717 stopgap) and #152 cut the host
cost of pair verification. Inference: the older BA was bound by the host cost of checking bound
arrays before each dispatch, and #164 removed most of it. The records do not isolate the cause. The
same inference fits the multi-video pair (3.4).
3.2 Dispatches, submissions and the time of one dispatch
Source: timing.stages. "ms per dispatch" is the stage's wall time divided by its dispatch count.
It includes everything: the host, the queue, the fence and the GPU.
Stage (f1edec5c, older record in brackets) |
Calls | Dispatches | Submissions | ms per call | ms per dispatch |
|---|---|---|---|---|---|
| features | 27 | 4563 | 27 | 64 | 0.38 |
| matches | 98 | 588 | 98 | 10 | 1.7 |
| guided matching | 98 | 588 | 98 | 180 | 30 |
| pair verification | 98 | 19020 | 3460 | 165 | 0.85 |
| triangulation | 77 | 159 | 159 | 70 | 34 |
| bundle | 27 | 21590 | 403 | 406 | 0.51 (3.0) |
| observations | 69 | 207 | 69 | 28 | 9.3 |
| PnP | 25 | 2563 | 208 | 199 | 1.9 |
Readings (inferences from these counts):
- Guided matching runs 6 dispatches and one submission per pair, yet costs 180 ms. The plain
matcher runs the same 6 dispatches for 10 ms.
docs/lab-run-records.mdshows the scalar guided kernel runs about 3 percent of the plain matcher's sums. So the 170 ms difference is not the match kernels. The stage also runs the guide: a fullverifyImagePairGpu(50 hypotheses) on the same correspondences (guidedMatchesinwebsite/src/workers/sfm.worker.ts). Pair verification then runs the full 500-hypothesis search on the same pair again. - Pair verification runs about 195 dispatches and 35 submissions per pair. Its kernels are
8-lane workgroups (
preparePairModels,appendPairModelRows,solvePairModelNull,finishPairModels) with one model per lane. A batch of 32 models is 4 workgroups. The batch size is the "32 trials up" floor of the time budget (docs/gpu-geometry.md). - Triangulation and
observationsrun 9 to 42 ms per dispatch. These are single dispatches that wait for the GPU (submissionsequalsdispatches). Their cost is the fence plus a per-lane serial solve, 16 lanes to a workgroup.
3.3 The chunk rates show a floor of 3 to 8 ms
timing.chunks of ed932d77, 28dcfe16, 0de918f8, 4a8aa2ff, f1edec5c:
| Key | Units of the last chunk | Longest chunk (ms) | Learned ms per unit | The cost of a unit if the longest chunk is the fence |
|---|---|---|---|---|
scorePairModels |
1 | 3.0 to 5.4 | 1.0 to 3.6 | one model costs microseconds, not 1 to 3 ms |
scorePnP |
500 | 4.1 to 8.2 | 0.005 to 0.010 | 500 trials in 4 ms is the same floor |
triangulateTracks |
64 to 284 | 4.6 to 5.8 | 0.02 to 0.25 | a chunk of 118 tracks is 8 workgroups |
mvsInitialize |
1536 | 2.1 to 6.7 | 0.001 to 0.003 | the same floor |
mvsPropagate |
1536 | 3.6 to 6.1 | 0.001 to 0.004 | the same floor |
Fact: the longest chunk of nearly every key is 3 to 8 ms, whatever its units. The one exception
class is bundleCg (57 to 182 ms), the only key whose chunks do real work per unit.
Inference: a chunk is timed with a queue fence (device.wait()), and a fence on this stack costs
about 3 to 5 ms. The learned rate adds that latency to the first timed chunks. The rate is then
too high, the planner picks small chunks, and each chunk fills a small part of the GPU. The
time budget is 250 ms (timing.budgetMs), but the chunks run at 3 to 8 ms. An RTX 5060 Ti has about
36 streaming multiprocessors and holds about 55000 resident threads. A 118-track chunk of 16-lane
workgroups does not fill one tenth of it. For a latency-bound kernel the cost of 118 and of 6341
tracks is nearly the same. Item 2 tests this. The test is a SwiftShader-independent one: the
timestamp of the chunk (item 1) against its fence time.
3.4 Multi-video (host-bound)
| Stage | 19f18274 (edb0d35) |
fad31ad6 (f2473c9) |
|---|---|---|
| features | 5.1 s | 5.0 s |
| pair matching | 8.4 s | 9.1 s |
| pair verification | 176.8 s (882 pairs) | 69.5 s |
| reconstruction | 443.3 s | 90.7 s |
| total | 628.8 s | 169.5 s |
Fact: the same input ran 3.7 times faster between two site builds that changed no kernel (#152
host-cost cuts, #164 pin move). Pair verification is 79 ms per pair after the change. Inference:
for this input the host, not the GPU kernels, held most of the time. The record's mainThread
shows frame gaps of 41 s (19f18274), 15 s (fad31ad6) and 33 s with 164 long tasks (d40f8950,
a failed 839-frame run). The page stalls even when the worker does the work. This record leaves the
page's own stalls to the multi-video record (docs/multi-video.md).
3.5 Transfers
transfers of the records: application uploads 204 to 584 MB per run, readbacks 7 to 11 MB.
applicationUploadBytes counts the values given to allocate*() and also the zeros of every new
f64 array (ShadeDevice.statistics). Much of the upload is therefore zero fill. At 10 GB/s
effective, 584 MB is 60 ms in a 75 s run. Fact: the volume is small in time. Transfer is not a
bottleneck in these runs. A GPU-side clear would still save the host copy and the allocation
(item 9).
3.6 Per-kernel profile on SwiftShader
Warning: SwiftShader. Read as the distribution of emulated arithmetic.
Dense propagation (--case mvs, 6 views of south-building-24 at 320x240, default iterations,
gpuTime sum 315 s):
| Kernel | Groups | GPU ms | Share |
|---|---|---|---|
mvsPropagate (photometric and geometric) |
1022 | 305684 | 96.9% |
mvsInitialize |
27 | 3350 | 1.1% |
mvsGeometricInitialize |
23 | 2543 | 0.8% |
mvsFilter |
29 | 2535 | 0.8% |
mvsScatterPoints, mvsFuseFlags, the rest |
2500 | under 1000 | 0.4% |
Bundle adjustment (--case bundle, 24 views, refineSharedFocal, 4 LM iterations, gpuTime
sum 9522 ms; 401 PCG iterations):
| Kernel | Groups | GPU ms | Share | Threads of one dispatch |
|---|---|---|---|---|
bundleSchurCamera |
401 | 4816 | 50.6% | 6C + 1 rows, 145 for 24 cameras (3 workgroups of 64) |
bundleSchurPoint |
401 | 3706 | 38.9% | one per landmark, a few thousand |
bundleCameraRows |
4 | 322 | 3.4% | per camera row |
bundleCandidatePartials, bundlePointBlocks, bundleFocalBlock, linearizeBundle, advanceBundleCg |
555 | 5.8% |
Pair verification (--case pair, 23 consecutive pairs, gpuTime sum 6120 ms):
scorePairModels 55.5%, appendPairModelRows 16.6% (1229 groups, 53 per pair),
triangulatePoseCandidates 10.1%, solvePairModelNull 6.6%, the rest 11%.
SIFT (--case sift, 4 frames at 640x480, first octave -1, gpuTime sum 25 s):
scanFlags 49.1%, siftBlurV and siftBlurH 21.3%, siftScatterExtrema and siftFlagExtrema
18.6%, siftDescribe 6.8%. scanFlags is a 64-lane Hillis-Steele scan with 12 barriers.
SwiftShader pays dearly for barriers, a card does not. Do not act on this row.
3.7 What the BA numbers say about the card
bundleCg learned 0.0007 ms per unit on ed932d77 and 0.0002 on f1edec5c. A unit is one row of
a PCG iteration (M + 6C, docs/gpu-sfm.md). For f1edec5c that is
0.0002 x (33458 + 162) = 6.7 ms per PCG iteration, and for ed932d77 0.0007 x 24312 = 17 ms.
The data one iteration reads, assuming 8 bytes per emulated f64 and 21 stored cross-block words
per observation (crossAt in src/gpu/joint-focal.shade.ts): 33458 x 168 B = 5.6 MB per pass, two
passes, about 12 MB with the vectors. At about 450 GB/s that is 25 microseconds. The arithmetic is
about 33458 x 21 MACs x 2 passes x 20 f32 operations per emulated MAC = 28 MFLOP, below 2
microseconds at 20 TFLOP/s. Inference: the iteration is more than 100 times above its roofline, and
the cause is the critical path, not the volume. In bundleSchurCameraRow, one invocation walks all
observations of its camera, about 1200 for f1edec5c, with four dependent loads per step
(cameraObservation, index, blocks, vectors), and only 6C + 1 invocations exist.
4. What the records cannot tell, and how to close it
- No kernel time.
timing.kernelslists names only. Item 1 addsgpuMs,groupsanddispatchesper kernel, summed fromFuseReport.groups[].gpuTime, and the wall time of the plans that held them. The difference of a stage's wall time and its summed GPU time is its host and fence time. This one table decides the order of items 2 to 8 on the owner's card. - No adapter features.
gpuholds two limits. Item 1 addsfeaturesand the compute limits (maxComputeWorkgroupStorageSize,maxComputeInvocationsPerWorkgroup,maxStorageBuffersPerShaderStage), andshader-f16andsubgroupsin particular. mvsPropagatedoes not separate the photometric and geometric pass indense.stageMs's kernels. The stage times separate them (3.1). Item 1 keeps the kernel'spassin the key.- Guided matching has no split between the guide and the match kernels. Item 1 times them.
5. Research
Each row: the method, the source, what it removes here and the fit to WebGPU. The WebGPU limits
that decide fit: no native f64; subgroups is optional and absent from many adapters; 16 KiB of
workgroup memory is the guaranteed minimum (maxComputeWorkgroupStorageSize), 32 KiB is common;
8 storage buffers per stage is the guaranteed minimum; no device-wide barrier inside a dispatch;
timestamp-query is optional; a forward-progress guarantee between workgroups does not exist.
5.1 Bundle adjustment
| Method | Source | Fit here |
|---|---|---|
| Implicit Schur complement with PCG and a block-Jacobi preconditioner; a thread per point and per camera row; Jacobians cached | Wu, Agarwal, Curless, Seitz, "Multicore Bundle Adjustment", CVPR 2011, project | This is what bundleSchurPoint and bundleSchurCamera do. The paper targets thousands of cameras, where a thread per camera row is enough. At 20 to 100 cameras it is not. |
| GPU BA library: batched Jacobian by forward-mode AD, PCG with distributed Schur elimination, all kernels over observations | Ren et al., "MegBA: A GPU-Based Distributed Library for Large-Scale Bundle Adjustment", ECCV 2022, arXiv:2112.01349 | Reports up to 41x over Ceres. Its parallel axis is the observation, not the camera. Adopt that axis for the camera-side product (item 5). The distribution part has no use here. |
| Landmark elimination by QR nullspace marginalization, single precision as accurate as double Schur on BAL | Demmel, Sommer, Cremers, Usenko, "Square Root Bundle Adjustment for Large-Scale Reconstruction", CVPR 2021, arXiv:2103.01843 | Good numerics in f32, which removes the emulated f64 of the Jacobian blocks. It needs per-landmark QR with a larger working set. Candidate for a later item. Single precision there still needs care at our depth range. |
| Power series of the inverse Schur complement as a preconditioner, fewer PCG iterations at high accuracy | Weber, Demmel, Chan, Cremers, "Power Bundle Adjustment for Large-Scale 3D Reconstruction", arXiv:2204.12834 (v4 2023) | Reduces the iteration count, so it multiplies with every kernel gain. It adds one product with the point blocks per iteration. Fit: good, small kernels. Its gain is large for ill-conditioned, many-camera problems and unknown for our small ones. |
| Dense reduced camera system, assembled per camera pair, factored by Cholesky | Ceres DENSE_SCHUR (docs); Agarwal et al., "Bundle Adjustment in the Large", ECCV 2010 |
Best at 20 to 200 cameras: the reduced system is 6C square (162 x 162 for 27 views). One factorization replaces 20 to 100 PCG iterations of 3 dispatches. Fits workgroup memory (tiles) and needs no atomics. This repository's schurCache already assembles the dense matrix (limit 16 cameras). |
| Mixed precision: low-precision products with compensated accumulation, refinement in high precision | Haidar et al., "Harnessing GPU Tensor Cores for Fast FP16 Arithmetic to Speed up Mixed-Precision Iterative Refinement Solvers", SC 2018 | Store Jacobian and cross blocks as f32 (halves bytes, removes the emulated-f64 multiplies), accumulate in emulated f64 or in a compensated f32 pair, keep the LM step acceptance in f64. Needs a tolerance test, not bit equality. |
| Batching many small local BAs | MegBA batches observations across the problem. The local BAs of localBundleSubset are separate problems today. |
Each local BA is 4 to 8 LM iterations. A batch of independent local problems in one dispatch amortizes the launch chain. High effort. Defer. |
5.2 Features and matching
| Method | Source | Fit here |
|---|---|---|
| GPU SIFT with separable Gaussian passes through workgroup memory, one workgroup per image tile and halo | Wu, "SiftGPU" (project); AliceVision, PopSift, described in "AliceVision Meshroom", ACM MMSys 2021 | siftBlurH and siftBlurV read the image from storage once per tap. A tile with halo in 16 KiB of workgroup memory removes the repeated reads. Features are 1 to 3 percent of a run, so low priority. |
| Brute-force kNN as tiled GEMM with a fused top-2 selection | Johnson, Douze, Jegou, "Billion-scale similarity search with GPUs", IEEE TBD 2021, arXiv:1702.08734; Garcia, Debreuve, Barlaud, CVPR Workshops 2008 | This repository already has a tiled L2 matcher (nearestFloatForward, tiles of the plain matcher). Matching is 1 to 2 percent of a run. Int8 descriptors with packed4x8Dot (a derived capability of the pin) would quarter the bytes. Low priority. |
| RootSIFT (Hellinger kernel) | Arandjelovic, Zisserman, "Three things everyone should know to improve object retrieval", CVPR 2012 | Already used (rootSift: true default). |
| Epipolar-guided matching with the band from a robust E | COLMAP guided_matching, Schonberger and Frahm, "Structure-from-Motion Revisited", CVPR 2016 |
The cost here is the guide's RANSAC, not the band test (3.2). See item 3. |
5.3 Dense MVS
| Method | Source | Fit here |
|---|---|---|
| PatchMatch stereo: slanted planes, random init, propagation, refinement | Bleyer, Rhemann, Rother, BMVC 2011 | The algorithm of mvsPropagate. |
| Red-black (checkerboard) propagation for massively parallel GPU MVS | Galliani, Lasinger, Schindler, "Massively Parallel Multiview Stereopsis by Surface Normal Diffusion", ICCV 2015 (Gipuma) | Already used (mvsControl.color). |
| Multi-hypothesis joint view selection, adaptive window, structured region information | Xu, Tao, "Multi-Scale Geometric Consistency Guided Multi-View Stereo", CVPR 2019, arXiv:1904.08103 (ACMM) | Coarse-to-fine pyramid: converge at 1/4 and 1/2 resolution, then refine at full resolution with fewer full-resolution iterations. 2 to 3 times fewer full-resolution pixels. Changes results, needs a quality gate (item 8). |
| Planar priors from sparse credible points for textureless areas | Xu, Tao, "Planar Prior Assisted PatchMatch Multi-View Stereo", AAAI 2020, arXiv:1912.11744 (ACMP) | Quality, not speed. Our sparse landmarks are the credible points. Note for a later quality record. |
| Pixelwise view selection with a probabilistic occlusion model; geometric consistency term | Schonberger, Zheng, Pollefeys, Frahm, "Pixelwise View Selection for Unstructured Multi-View Stereo", ECCV 2016 (COLMAP) | The design of docs/dense-mvs.md. |
| Zero-mean NCC from running sums (box sums, integral images) | Lewis, "Fast Normalized Cross-Correlation", Vision Interface 1995 | The window is sampled with step 2 on a homography-warped grid, so the source side cannot use a box filter. The reference side (mean, variance, weights) can be computed once per pixel and held in registers. |
| Hardware texture filtering for bilinear lookups; 2D-local cache | Standard GPU practice (texture objects in CUDA PatchMatch implementations) | sourceAt does four storage loads and three mixes per sample. A filtered texture does one instruction. Hardware weights have limited fractional precision, so results differ from the CPU oracle by a tolerance. |
| Reduced-precision storage of depth, normal, cost, gray | Octahedral normals: Cigolle et al., JCGT 2014, paper | Normals are already octahedral u32. Gray as unorm8 quarters the source bytes. Depth as f16 loses range, keep f32. |
5.4 Game-engine and library techniques
| Technique | Source | Fit here |
|---|---|---|
| GPU-driven pipelines: the GPU writes the counts and the next dispatch reads them | Haar, Aaltonen, "GPU-Driven Rendering Pipelines", SIGGRAPH 2015 Advances course | Already used: indirect dispatch, TypeShade change 0075. Extend it from batch loops to the guide and the PnP chain (item 2). |
| Async upload with ring buffers and double buffering to overlap copy and compute | Standard in engines (staging rings) | Uploads are 60 ms of 75 s (3.5). No gain now. Reopen when images grow to 4K. |
| Virtual texturing and sparse residency | Barrett, "Sparse Virtual Textures", GDC 2008 | #161's windowed residency does this by view window. A page-table version helps only at memory limits, not speed. |
| Tile binning and a per-tile workgroup for cooperative work | Kerbl et al., "3D Gaussian Splatting for Real-Time Radiance Field Rendering", SIGGRAPH 2023, arXiv:2308.04079; gsplat, arXiv:2409.06765 | The idea is a tile of pixels sharing one workgroup memory fill. It is the shape of the reference-window tile in item 4. A radix sort for binning (Onesweep, Adinets, Merrill, arXiv:2206.01784) is not needed here. |
| Single-pass scan with decoupled look-back | Merrill, Garland, NVIDIA TR NVR-2016-002 | Needs forward progress between workgroups. WebGPU does not guarantee it. Do not use. Keep the two-level scan (scanFlags, scanFlagBlocks). |
| Parallel reduction by tree in workgroup memory, then subgroup operations | Harris, "Optimizing Parallel Reduction in CUDA", NVIDIA 2007 | The tree is portable (bundleMetricPartials uses it). Subgroup operations need the subgroups feature and compiler intrinsics the pin lacks (gap G2). |
| GPU-side convergence tests instead of readbacks | Standard (device-resident flags) | Already used: snapshotBundleActivity reads 8 bytes per chunk. |
| Kernel fusion | TypeShade fuse, changes 0058 and 0069 |
Already used. The remaining fusions are across the Schur point and camera kernels, which need a grid-wide barrier (not available). Do not fuse them. |
| Data compression: f16 storage, quantized positions, block compression | Standard | TypeShade has pack2x16float, unpack2x16float, pack4x8unorm. It has no f16 type (gap G1). Bytes are not the bottleneck now (3.5, 3.7), so use compression only where an item above is bandwidth bound. |
6. TypeShade capabilities for each technique
Source: node_modules/typeshade at the pin (AUTHORING.md, docs/use-typeshade-surface.md,
changes/). Status "available" means the surface documents it. Nothing here was run on a card.
| Needed | Status at the pin |
|---|---|
| Workgroup memory and barriers | Available. let tile: workgroup<array<f32, 64>>, workgroupBarrier() (surface section 24, 25). Used in scan.shade.ts, joint-focal.shade.ts. |
| Barrier under divergent control flow | Refused (TS8052). An item must keep every lane at each barrier. The idle-lane rule of docs/gpu-sfm.md already says so. |
| CPU tier for a barrier kernel | Not available. "The oracle runs one invocation per call" (surface section 25). Every tiled kernel needs an Oracle sibling, as bundleMetricPartialsOracle does. G4. |
| Integer atomics | Available (atomic<u32>, atomic<i32>). No f32 atomics, as in WGSL. Items below use none. |
| Indirect dispatch | Available (change 0075, used in pair-models.shade.ts). |
| Workgroup size override | Available (change 0057, @compute([N]) and overrides). |
f16 type and arithmetic |
Not available. The capability f16 exists, but no f16 scalar type is authorable (AUTHORING.md, "Capabilities"). Storage packing with pack2x16float works. G1. |
| Subgroup intrinsics | Not available. The built-ins subgroup_invocation_id and subgroup_size are; subgroupAdd and the rest are absent (AUTHORING.md). G2. |
| Sampled texture and sampler in a compute entry | Documented: textureSampleLevel is accepted in any stage (surface table at line 5011). Not run in this repository. The CPU oracle's bilinear is not the GPU's. G3. |
| Texture written by a kernel function | Accepted, not implemented at the pin (change 0077, accepted). |
dot4I8Packed, packed4x8Dot |
Derived capability packed4x8Dot exists (AUTHORING.md table). |
| Timestamp per fuse group | Available (change 0066, FuseReport.groups[].gpuTime). One value for a group, not for one entry call in a fused chain. Not used by the time budget. G5. |
| Chunk learner on GPU time, fixed latency removed | Not available. Change 0074 times the chunk with a queue fence. G5. |
| Large workgroup memory | Limited by the adapter (maxComputeWorkgroupStorageSize, 16 KiB minimum, 32 KiB on SwiftShader). The kernel must read the limit; G6 asks for a host-readable limit. |
| Device-wide barrier, persistent kernel | Not available in WebGPU. Do not plan on it. |
| Async readback pipeline | device.read is one round trip. TypeCV reads flags only every fourth batch and per chunk; no further gap. |
Draft issues for typeshade/typeshade (not filed)
G1. Title: An f16 scalar type for "use typeshade" kernels, so that half-precision storage and
arithmetic are authorable.
Body: AUTHORING.md lists f16 as a capability (enable f16;, host feature shader-f16) and says
no f16 scalar type exists. TypeCV's dense MVS and BA store gray, depth, cost and Jacobian blocks
in f32. It can pack two halves with pack2x16float but cannot compute on them. Expected: f16
scalar and vec2h, vec4h types, with the CPU oracle rounding to binary16 after each operation
(as quantizeToF16 does) and the WebGL2 backend refusing them. Tests owed: a kernel with f16
storage and f32 accumulation against the oracle; the SD0030 refusal on GLSL.
Pinned commit: 3c6e964.
G2. Title: Subgroup intrinsics (subgroupAdd, subgroupMax, subgroupShuffle, subgroupBallot)
with a CPU oracle meaning.
Body: The pin accepts @builtin("subgroup_invocation_id") and @builtin("subgroup_size") and
derives the subgroups capability, but no intrinsic. TypeCV's reductions (bundleMetricPartials,
the PCG dot products, the top-2 selection of matching) use a workgroup-memory tree with several
barriers. A subgroup reduction needs none. Expected: the intrinsics behind the subgroups
capability, with the oracle running a declared subgroup size of 1 or a stated simulated size, and a
refusal on WebGL2. Tests owed: a reduction against the tree reduction on SwiftShader (which offers
subgroups).
G3. Title: A sampled texture with a filtering sampler read from a compute entry, with a stated
tolerance class for the CPU oracle.
Body: The surface table lists textureSampleLevel as valid in any stage. TypeCV would store the
gray image of each view as an r16float or r8unorm texture and read bilinear samples in
mvsPropagate. Hardware bilinear weights have fewer than 32 bits of fraction, so the CPU oracle
cannot match bit for bit. Expected: documented behavior of textureSampleLevel in a @compute
entry on WebGPU and the CPU tier, and a tolerance class in the surface's accuracy table for the
filtered result (a bound in units of the texel's last place). Tests owed: a compute entry
that samples at fractional coordinates, compared with a four-texel oracle within the bound.
G4. Title: The CPU tier runs a tiled entry (workgroup memory and barriers) as a whole workgroup.
Body: Surface section 25 says the oracle runs one invocation per call, so a barrier has no
partner. TypeCV keeps a sequential ...Oracle sibling for each tiled entry (scanPairRowsOracle,
bundleMetricPartialsOracle, scorePairModelsOracle). Each pair is a second kernel to write and
to hold to the first. Expected: a CPU tier that runs the lanes of a workgroup between barriers
(a generator or a thread pool), so that one source runs on every tier, as change 0069 allows for a
tiled entry in a fuse scope. Tests owed: the existing tile examples on the CPU tier.
G5. Title: The chunk learner of a device fits time = latency + rate x units from GPU
timestamps when the adapter has timestamp-query.
Body: Change 0074 times a chunk from its plan start to the queue fence. On the owner's RTX 5060 Ti
the longest chunk of nearly every key is 3 to 8 ms whatever its size (TypeCV record ed932d77:
scorePairModels 1 unit in 3.0 ms, scorePnP 500 units in 4.1 ms, triangulateTracks 118 units
in 5.4 ms). The fence latency enters the rate, the rate sizes small chunks, and small chunks leave
the GPU empty. Expected: when FuseReport.groups[].gpuTime exists, learn the rate from it and keep
the fence time only as a watchdog check. Without timestamps, fit two chunk sizes to separate the
latency. Also expose a per-entry gpuTime for the entries of one plan. Tests owed: a synthetic
kernel with a known latency floor, and a check that the chosen chunk grows to the budget.
G6. Title: A host-readable workgroup-memory limit and a way to size a tile from it.
Body: A tile kernel must fit maxComputeWorkgroupStorageSize (16 KiB guaranteed). The device
owner exposes limits (change 0051). Expected: a documented read of that limit in the program
runtime, and an override constant that a kernel can size its tile from (change 0057 covers
the workgroup size). Tests owed: one kernel built at two tile sizes.
7. The plan
Order: measured gain per effort, on the owner's card. Gains are inferences until the pull request
measures them. Each pull request states its before and after numbers: SwiftShader with
scripts/profile-kernels.mjs for the work done (dispatches, groups, GPU ms), and, by request,
the owner's GPU through a run record (item 1 makes that table).
Verification classes:
- Exact. The pull request shows equal output on the CPU tier and on SwiftShader (the tests named in each item). A pull request in this class changes order of loop iterations only where it says so.
- Tolerance. A kernel reorders a floating-point sum or reads a filtered texel. The pull request states the bound, shows the CPU oracle held to it, and shows the end-to-end figures (landmarks, mean reprojection error, fused points) within the run-to-run spread of the baseline.
Item 1. Per-kernel GPU time and adapter features in the run record
- Stage: all. Technique: measurement. Cost: small (
website/src/lib/run-record.ts,website/src/workers/sfm.worker.ts,docs/lab-run-records.md, record test). - Change: record format v6.
timing.gpuis a list of{ name, groups, dispatches, gpuMs }from the summedFuseReport.groups(a wrapper on the TypeShade device's report, as inscripts/profile-kernels.mjs), plustiming.stages[*].gpuMs.gpu.featuresholds the adapter's feature list andgpu.limitsthe compute limits. The cap is 120 kernel rows. - Gain: none directly. It turns sections 3.2 and 3.3 from inference into fact on the owner's card and decides items 2 to 8.
- Risk:
timestamp-queryrounds timestamps coarsely in some browsers; a group may carry nogpuTime. A kernel row keepsgpuMs: nullthen. Timestamp queries cost a little per plan. - Verification: record validator test (
website/src/lib/run-record.tstests), a record sent from SwiftShader carries the rows, v5 records still validate. Exact class (no kernel changes). - TypeShade gaps: none (G5 later improves it).
Item 2. Fill the device before splitting a chunk
- Stage: triangulation (5.4 s), PnP (5.0 s), pair verification (16 s, scoring and model batches),
and the same rule for
observations. Technique: occupancy-aware chunks. A chunk key gets a minimum number of units that fills the device (for example 2048 workgroups), and the rate learned from GPU time (item 1) replaces the fence time as soon as the pin offers it (G5). Until G5, TypeCV computes the rate itself fromgpuTimeof the chunk's report. - Cost: medium-small. Files:
src/gpu/device.ts(planSteps),src/gpu/triangulation.ts,src/gpu/pnp.ts,src/gpu/pair-estimation.ts(thetrialsexpression, 32 floor),src/gpu/observations.ts. - Expected gain: triangulation 5.4 s to about 1 s (a call of 6341 tracks as one 400-workgroup dispatch, because a latency-bound serial solve costs the same for 118 and 6341 tracks until the card fills); PnP 5 s to about 2 s; pair verification 16 s to about 9 s (a batch of 256 to 512 models instead of 32 cuts the 195 dispatches per pair to about 60). Sum 8 to 12 s of 75 s.
- Risk: the Windows watchdog (TDR) and the earlier Blackwell hang in
triangulateTracks(docs/hardware-observations.md, A1). Mitigation: a ceiling in measured GPU milliseconds (for example 100 ms) per dispatch, taken fromgpuTime, and an upper bound of the first dispatch of each key at the old chunk size times four. The idle-lane rule stays. - Verification: exact.
tests/typeshade/pnp-batch.test.mjs,pair-batch.test.mjs,tests/typeshade/streaming.test.mjs,plan-steps.test.mjspass unchanged; the RANSAC search stops "where a sequential RANSAC would, whatever the batch" (docs/gpu-sfm.md), so the models and inliers are equal. The pull request adds a test that a call of N tracks plans at mostceil(N / floor)dispatches. Benchmark: dispatch and submission counts per stage on SwiftShader (must fall), GPU ms from the profile script, and the owner's run record (timing.stages). - TypeShade gaps: G5 (rate from GPU time). Not blocking.
Item 3. The guide of guided matching estimates E only
- Stage: guided matching (17.6 s, 23 percent). Technique: the guide needs a band, not a pose. Run
the essential-matrix RANSAC alone (
estimateEssentialRansacGpu,src/gpu/two-view.ts) in one batch of 64 to 128 hypotheses and build the band from E directly. Skip pose recovery, triangulation and refinement (triangulatePoseCandidatesis 10 percent of pair verification GPU time on SwiftShader) and theessentialFromPoseround trip. - Cost: small to medium. Files:
website/src/workers/sfm.worker.ts(guidedMatches),website/src/lib/guided-matching.ts, maybesrc/gpu/two-view.ts. - Expected gain: the guide drops from about 170 ms to about 40 to 60 ms per pair: 17.6 s to 6 to 8 s. A further step, reuse of the mapper's E (verify first, then rematch), would remove the guide entirely but reorders the worker. Record it as a follow-up.
- Risk: the band differs slightly (E unscaled against
[t]x R). The mapper still verifies the chosen matches, so a wrong band costs matches, not correctness. - Verification: tolerance.
tests/typeshade/guided-matching-webgpu.test.mjsand the end-to-end figures. The pull request shows the guided match count per pair on the pyramid and south-building samples within 2 percent of the baseline, and equal landmarks within the baseline spread. - TypeShade gaps: none.
Item 4. Dense propagation: hoist the loop-invariant work, then pack the gray
- Stage: dense photometric (7 to 26 s) and geometric (3 to 5 s). On SwiftShader
mvsPropagateis 97 percent of the dense GPU time. Technique, in two exact steps and one tolerance step:- Reference window cache (exact).
referenceWindowandsourceCostread the reference window again for each of up to 10 hypotheses and 6 sources per pixel and compute anexpbilateral weight for each of 36 samples each time. Read the 36 reference values and weights once intoarray<f32, 36>registers per pixel and iteration. This removes about 2000 global loads and 2000expper pixel and iteration. - Relative pose cache (exact).
sourceCostrebuildsR_rsandt_rs(about 100 flops and 12 loads) for each hypothesis and source. They depend on the reference and source only. Compute them once per pass into a small storage array (mvsRelative, 6 x 16 floats), the waymvsCamerasis held. - Gray as
unorm8(tolerance). The gray comes from 8-bit RGB. Store the undistorted gray withpack4x8unorm(4 samples per word). Source bytes fall 4 times. Quantization error is at most 0.5/255 of the range per sample and enters NCC as noise below the cost's own noise.
- Reference window cache (exact).
- Cost: step 1 and 2 small (
src/gpu/mvs.shade.ts,src/gpu/mvs.ts); step 3 medium (mvsUndistort,mvsGraylayout indocs/dense-mvs.md4.1 and the residency plan of Amendment 3). - Expected gain: steps 1 and 2: 20 to 30 percent of the propagate time (a count of the loads and transcendental calls, to be measured on SwiftShader first); step 3: a further 10 to 20 percent where sampling is bandwidth bound. Together 25 to 40 percent of the dense time (4 to 12 s per run).
- Risk: more registers per lane (36 values and 36 weights), which can lower occupancy. The pull request measures register-bound effects with a second variant (weights recomputed from 36 offsets, values cached).
- Verification: steps 1 and 2 exact:
tests/typeshade/mvs.test.mjs,mvs-geometric.test.mjsandmvs-webgpu.test.mjscompare depth and normal bit for bit; the fused point count equals the baseline. Step 3 tolerance: the fused point count and the 99th percentile depth change within 1 percent on south-building-24 and the pyramid sample. Benchmark:mvsPropagateGPU ms before and after with the profile script, same views and iterations. - TypeShade gaps: none for steps 1 and 2.
unorm8usespack4x8unorm(available).
Item 5. BA: one workgroup per camera in the Schur camera product
- Stage: bundle (11 to 39 s). Technique: parallelize over observations, as MegBA does, and reduce in
workgroup memory. Today
bundleSchurCameraruns 6C + 1 invocations, each walking its camera's whole observation list with four dependent loads per step. New: one workgroup of 64 lanes per camera, lanes stride over that camera's observations, each lane keeps sixf64partial sums, and a tree reduction inworkgroup<array<f64, 64>>finishes the six rows (512 B per row, 3 KiB for six rows, well inside 16 KiB). Sort the camera's observation index so lane loads are contiguous. Keep the existing kernel as the CPU tier'sOracle. - Cost: medium. Files:
src/gpu/joint-focal.shade.ts(bundleSchurCamera, the cached variants),src/gpu/bundle.shade.ts(fixed-focal route),src/gpu/joint-focal.ts,src/gpu/bundle.ts, the oracle siblings, tests. The camera count per workgroup stays small, so grid size is C workgroups: at 27 cameras still fewer than a card's SMs. Item 6 is the answer to that; this item cuts the critical path from about 1200 serial steps to about 20. - Expected gain: the critical path of the camera product falls about 20 to 50 times. The PCG iteration is the camera product plus the point product plus launches, so the iteration falls 1.5 to 3 times (from 6.7 ms toward 2 to 4 ms). BA stage: 11 s to 4 to 7 s on the newest pin. SwiftShader cannot show this gain (it has no latency); the owner's record decides.
- Risk: reduction order changes the
f64sum, so results differ in the last bits. LM acceptance is a threshold test and tolerates this; the CPU oracle keeps the sequential order. Barriers need uniform control flow (TS8052): every lane runs every step, with masked loads. - Verification: tolerance, relative 1e-9 on the PCG vectors per iteration against the oracle and
equal LM iteration counts on the fixtures of
tests/typeshade/bundle-checkpoint.test.mjs,solver-checkpoint.test.mjs,schur-cache.test.mjs,bundle-camera-webgpu.test.mjs; final cost and mean reprojection error within 1e-6 relative. Benchmark:profile-kernels.mjs --case bundle(dispatch counts equal; GPU ms for the kernel may not fall on SwiftShader, state it) and the owner'sbundleCgms per unit (timing.chunks), which must fall. - TypeShade gaps: G4 (a CPU twin per tiled kernel). Subgroup intrinsics G2 would shorten the tree.
Item 6. BA: explicit reduced camera system and tiled Cholesky
- Stage: bundle. Technique: assemble the 6C x 6C reduced camera system once per LM iteration
(one workgroup per camera pair that shares landmarks, accumulating over their common landmarks in a
fixed order, no atomics) and factor it with a blocked Cholesky in workgroup memory (Ceres
DENSE_SCHURon a GPU). At C = 27 the matrix is 162 x 162, 26244 entries, about 210 KB inf64pairs. This is theschurCacheassembly (cacheBundleSchurCross,cacheBundleSchurRows), lifted from its 16-camera guard and followed by a direct solve in place of PCG. - Cost: large. Files:
src/gpu/joint-focal.shade.ts,src/gpu/joint-focal.ts, a newsrc/gpu/cholesky.shade.ts(blocked, 16 x 16 tiles, 2 KiB each inf64), tests, and a record indocs/gpu-geometry.md. A direct solve also needs a damping retry loop for LM. - Expected gain: 20 to 100 PCG iterations (60 to 300 dispatches) per LM step become about 10
dispatches. A Cholesky of 162 costs 1.4 M emulated MACs, tens of microseconds of arithmetic on a
card with a latency-bound chain of 162 column steps, so about 1 to 3 ms. The recorded
schurCacheresult (RTX 2080, 2.4 times on the final BA,docs/gpu-sfm.md) is the lower evidence. Estimate 3 to 8 times on the BA stage. The solver also stops depending on the preconditioner and onmaxCgIterations. - Risk: memory grows as (6C)^2 (a 300-camera problem is 25 MB in
f64pairs, fine; 1000 cameras is 288 MB, so keep PCG above a camera limit chosen fromdevice.workingBytes). Cholesky in emulatedf64needs a pivot guard for gauge-free directions. - Verification: tolerance. The step equals the PCG step to the PCG tolerance (
cgRelativeTolerance) on every BA fixture; LM trajectories compared per iteration on the CPU tier; end-to-end figures as item 5. Benchmark: dispatch count per BA call (must fall 5 to 10 times), GPU ms on SwiftShader, and the owner'sbundlestage. - TypeShade gaps: G4 (CPU twin), G6 (tile size from the limit). Subgroups (G2) are optional.
Item 7. Hardware bilinear lookups for the source images
- Stage: dense propagation. Technique: upload each undistorted gray as an
r16floatorr8unormtexture;sourceAtbecomes onetextureSampleLevel. This keeps the 2D locality of the warped window in the texture cache, and the four loads and three mixes become one instruction. Needs the device owner to make textures (change 0077 for a kernel-written texture; an uploaded texture is available). - Cost: medium. Files:
src/gpu/mvs.shade.ts,src/gpu/mvs.ts,src/gpu/mvs-residency.ts(bytes per view change),docs/dense-mvs.md. - Expected gain: 1.5 to 2.5 times on propagation where it is texture-bound. Unproven: it depends on the card (item 1 tells whether propagation is bandwidth or ALU bound).
- Risk: the CPU oracle's bilinear differs from the filter hardware (weights are 8-bit fractions).
The "no data" sentinel (
-1) cannot ride through filtering: the validity check moves to a mask texture or totextureLoadat the four corners. - Verification: tolerance (G3 sets the class). Fused points within 1 percent, depth median within 0.1 percent on the two scenes.
- TypeShade gaps: G3.
Item 8. Multi-resolution propagation (ACMM style)
- Stage: dense photometric. Technique: run 2 to 3 iterations at 1/4 and 1/2 resolution first, upsample the depth and normal as the full-resolution initialization, and run 2 to 3 iterations at full resolution instead of 8. Estimated 2 to 3 times on the photometric stage.
- Cost: large (
src/gpu/mvs.tsplan,mvs-residency.ts, a design amendment todocs/dense-mvs.md3.5 and 4.x). Result changes; needs a quality gate (ETH3D-style accuracy or the repository's TUM and south-building comparisons). - Verification: tolerance with a quality gate against
scripts/compare-mvs-tum.mjs. Own design amendment first. - TypeShade gaps: none.
Items later or not worth it now
| Candidate | Why not now |
|---|---|
| Tiled SIFT blur, int8 matching, tiled GEMM | Features and matching are 3 percent of a run (3.1). The SwiftShader scanFlags share is a barrier artifact. |
| Ring-buffer uploads, compression of transfers | Uploads are 60 ms of 75 s (3.5). |
| Square-root BA, mixed-precision BA blocks | Gain overlaps items 5 and 6. Revisit after item 6 with the owner's kernel table. |
| Power BA preconditioner | Item 6 removes PCG for the common case. Keep for the large-camera path. |
| Decoupled look-back scan | Needs forward progress that WebGPU does not guarantee. |
Zero-filled f64 arrays by clearBuffer |
Saves host copies (item 9), under 1 percent of time. |
Page stalls (mainThread, up to 41 s gaps) |
Not a kernel. Belongs to the multi-video record. |
Item 9. Small items for the same pull request series
- Clear new
f64arrays on the GPU instead of uploading zeros (ShadeDevice.allocate*). - Add the
pass(photometric or geometric) to the kernel key of item 1.
8. The first three pull requests
PR 1. Run record v6: GPU time per kernel, adapter features, the profile script
- Move
scripts/profile-kernels.mjsinto the pull request (it exists in the branch of this record). Keep its wrapper ofdevice.shade.fuseas the model. - In
src/gpu/device.ts, add a small collector inShadeDevice.plan: for each report, addgroupsandgpuTimeto a map keyed bycalls.join('+'). Expose it asstatistics.kernels. Do not keep more than 160 keys. - In
website/src/workers/sfm.worker.ts, add the collector's rows to the record'stiming.gpuand a per-stagegpuMs.website/src/lib/run-record.tsvalidates them (v6; v5 stays valid). - In
website/src/lib/send-record.tsand the worker, addadapter.featuresand the three compute limits togpu. - Update
docs/lab-run-records.md("What a record holds"). - Tests: the validator accepts a v6 record and a v5 record, and rejects a negative
gpuMs;website/worker.test.mjssees the rows. - Ask the owner for one run of the pyramid sample with Dense MVS on the 5060 Ti. The table of section 3.2 then gets a GPU-ms column.
Checks: npm run format:check, npm run typecheck, npm test, npm run test:website,
npm run test:shade.
PR 2. Fill the device before splitting a chunk (item 2)
- Add a
minUnitsoption toGpuChunkinsrc/gpu/device.tsand use it inShadeDevice.chunkSizeso that a key never returns less thanminUnits, unless a measured dispatch exceeded the ceiling. - Set
minUnitsfrom workgroups: triangulation 16 lanes x 1024 workgroups,scorePnP512 trials,scorePairModels256 trials,observationsall units. Replace theMath.min(batchSize, 32)floor insrc/gpu/pair-estimation.tsand the equal floor insrc/gpu/pnp.ts. - Teach
ShadeDeviceto read the chunk'sgpuTimeand to learn the rate from it when it exists (device.shade.chunkRatesstays the fence-based fallback). Add a ceiling of 100 GPU ms per dispatch that halves the key'sminUnitsafter one overrun. - Tests: a planner test that N tracks give
ceil(N / floor)dispatches; the existing batch tests unchanged. - Measure: dispatches and submissions per stage on SwiftShader through
scripts/benchmark-sfm-pyramid-e2e.mjs; the owner's record againstf1edec5c.
PR 3. The guide estimates E only (item 3)
- In
website/src/workers/sfm.worker.tsguidedMatches, replaceverifyImagePairGpuwithestimateEssentialRansacGpuat 64 to 128 hypotheses (readsrc/gpu/two-view.tsfor the options), and buildEfrom its result. - Remove the pose round trip (
essentialFromPose) andguideVerificationOptions's unused options; keepGUIDE_RANSAC_ITERATIONSas the hypothesis count. - Tests:
website/guided-matching.test.mjsfor the new helpers;tests/typeshade/guided-matching-webgpu.test.mjsunchanged. - Measure: guided stage time on SwiftShader through the pyramid benchmark, the guided match
count per pair against the baseline (2 percent), landmarks and mean error against the
baseline spread, and the owner's
guidedMatchesstage againstf1edec5c.
After these, order: item 4 (steps 1 and 2 are exact and small), item 5, item 6, then 7 and 8. If the owner's kernel table (PR 1) shows dense propagation or BA holds the GPU time, move items 4 to 6 before PR 2 and PR 3.
9. Open items
| Item | Reason | Next action |
|---|---|---|
| The fence-latency reading of 3.3 is an inference | Records hold no kernel time. | PR 1, then one owner run. |
| The bundle drop (36 to 11 s) is not isolated | #152 and #164 landed in one interval. | PR 1's table; an owner run on ed932d77's input. |
shader-f16 and subgroups on the owner's card |
The record has no feature list. | PR 1. |
| SwiftShader cannot show latency or occupancy gains | CPU emulation. | Each item states its physical-GPU measure. |
| Guided matching and matching kernels were not profiled | The scripts cover MVS, BA, SIFT and pair verification only. | Add --case guided in PR 3. |
| TypeShade gaps G1 to G6 | Drafted here, not filed. The lab files them on typeshade/typeshade. | The owner or the next session files them, as the log says. |
| Item 6 camera limit and memory | Depends on the working-byte policy. | Decide in item 6's own record. |
| Quality gate for item 8 | The repository has TUM and south-building comparisons; no accuracy bar for multi-resolution. | Owner decision before item 8. |
10. References
- Wu, Agarwal, Curless, Seitz. Multicore Bundle Adjustment. CVPR 2011. https://grail.cs.washington.edu/projects/mcba/
- Ren, Liang, Yan, Mai, Liu, Liu. MegBA: A GPU-Based Distributed Library for Large-Scale Bundle Adjustment. ECCV 2022. https://arxiv.org/abs/2112.01349
- Demmel, Sommer, Cremers, Usenko. Square Root Bundle Adjustment for Large-Scale Reconstruction. CVPR 2021. https://arxiv.org/abs/2103.01843
- Weber, Demmel, Chan, Cremers. Power Bundle Adjustment for Large-Scale 3D Reconstruction. arXiv 2022 (v4 2023). https://arxiv.org/abs/2204.12834
- Agarwal, Snavely, Seitz, Szeliski. Bundle Adjustment in the Large. ECCV 2010. https://grail.cs.washington.edu/projects/bal/
- Haidar, Tomov, Dongarra, Higham. Harnessing GPU Tensor Cores for Fast FP16 Arithmetic to Speed up Mixed-Precision Iterative Refinement Solvers. SC 2018.
- Bleyer, Rhemann, Rother. PatchMatch Stereo. BMVC 2011.
- Galliani, Lasinger, Schindler. Massively Parallel Multiview Stereopsis by Surface Normal Diffusion. ICCV 2015.
- Xu, Tao. Multi-Scale Geometric Consistency Guided Multi-View Stereo. CVPR 2019. https://arxiv.org/abs/1904.08103
- Xu, Tao. Planar Prior Assisted PatchMatch Multi-View Stereo. AAAI 2020. https://arxiv.org/abs/1912.11744
- Schonberger, Zheng, Pollefeys, Frahm. Pixelwise View Selection for Unstructured Multi-View Stereo. ECCV 2016.
- Schonberger, Frahm. Structure-from-Motion Revisited. CVPR 2016.
- Johnson, Douze, Jegou. Billion-scale similarity search with GPUs. IEEE Transactions on Big Data 2021. https://arxiv.org/abs/1702.08734
- Arandjelovic, Zisserman. Three things everyone should know to improve object retrieval. CVPR 2012.
- AliceVision PopSift. https://github.com/alicevision/popsift
- Cigolle, Donow, Evangelakos, Mara, McGuire, Meyer. A Survey of Efficient Representations for Independent Unit Vectors. JCGT 2014. https://jcgt.org/published/0003/02/01/
- Kerbl, Kopanas, Leimkuehler, Drettakis. 3D Gaussian Splatting for Real-Time Radiance Field Rendering. SIGGRAPH 2023. https://arxiv.org/abs/2308.04079
- Ye et al. gsplat: An Open-Source Library for Gaussian Splatting. https://arxiv.org/abs/2409.06765
- Adinets, Merrill. Onesweep: A Faster Least Significant Digit Radix Sort for GPUs. https://arxiv.org/abs/2206.01784
- Merrill, Garland. Single-pass Parallel Prefix Scan with Decoupled Look-back. NVIDIA TR NVR-2016-002.
- Haar, Aaltonen. GPU-Driven Rendering Pipelines. SIGGRAPH 2015 Advances in Real-Time Rendering.
- Barrett. Sparse Virtual Textures. GDC 2008.
- Harris. Optimizing Parallel Reduction in CUDA. NVIDIA 2007.
- Lewis. Fast Normalized Cross-Correlation. Vision Interface 1995.
- Chrome for Developers. What's new in WebGPU (Chrome 134), subgroups. https://developer.chrome.com/blog/new-in-webgpu-134
Citations were checked for title, authors and venue only where a link is given. The others are cited from the authors' publications and need a check before a pull request relies on them.