Skip to content

NanoVDB VBM: Streaming box-stencil - #2262

Draft
swahtz wants to merge 2 commits into
AcademySoftwareFoundation:masterfrom
swahtz:nanovdb-vbm-streaming-stencil
Draft

NanoVDB VBM: Streaming box-stencil#2262
swahtz wants to merge 2 commits into
AcademySoftwareFoundation:masterfrom
swahtz:nanovdb-vbm-streaming-stencil

Conversation

@swahtz

@swahtz swahtz commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds VoxelBlockManager::forEachBoxStencil to the NanoVDB VBM — a streaming resolver for the 3×3×3 box stencil. It visits the 27 taps and streams (tap, index) to a device-inlined callback in the same deterministic tap order as computeBoxStencil, without materializing the 27-element per-thread index array. Consumers that reduce or accumulate taps (the common case — Laplacians, divergences, sums) fuse the lookup and the arithmetic and avoid the array's register/stack cost.

The existing computeBoxStencil is unchanged for callers that genuinely need random access to all 27 materialized taps.

What changed

In an Index-Grid the stencil resolves to indices; a real filter looks each index up in the sidecar (the actual per-voxel data payload) and accumulates:

// A box filter over an Index-Grid + sidecar.
// (materialized): fill a 27-element index array, then gather + accumulate
uint64_t st[27] = {};
VBM::computeBoxStencil(grid, sLeaf, sOff, st);
float acc = 0; for (int i = 0; i < 27; ++i) if (st[i]) acc += sidecar[st[i]];

// (streaming): index -> sidecar lookup + arithmetic stay fused; no 27-element array
float acc = 0;
VBM::forEachBoxStencil(grid, sLeaf, sOff, [&](int tap, uint64_t index){ if (index) acc += sidecar[index]; });

Same 27 taps, same tap order ((di+1)*9 + (dj+1)*3 + (dk+1)), so results are identical — but the uint64_t st[27] (a 216-byte per-thread array, exactly 27 × sizeof(uint64)) is gone. Isolating that overhead (ptxas -v, sm_120), the materialized array costs 26 registers + a 216-byte stack frame (70 → 44 registers, 216 B → 0), which a streaming filter avoids regardless of its arithmetic.

That register drop crosses an occupancy tier. On sm_120 (65536 regs/SM, 1536 threads = 48 warps max, allocation granule 8 regs/thread):

variant registers/thread resident warps occupancy
materialized 70 28 58.3%
streaming 44 42 87.5%

— a 1.5× jump in resident warps, which is what the runtime win reflects. (Full occupancy on sm_120 needs ≤40 registers, so streaming lands one 8-register granule short of 100%.)

A/B — real value-accumulating box filter (streaming vs materialized)

A genuine 3×3×3 box filter over an Index-Grid similar to the example usage above: both paths resolve the same 27 tap indices, then gather C sidecar channels per tap (sidecar[index*C + ch]) and accumulate per channel — so the only difference is the materialized st[27] array. Byte-exact at every (width, C); min of 40, sphere-shell grid (~1.29M active voxels).

Benchmarked on an RTX PRO 6000 (sm_120).

BlockWidth C=1 C=3 C=8 C=16
128 1.32× (−24.5%) 1.28× (−22.0%) 1.17× (−14.5%) 1.07× (−6.5%)
512 1.90× (−47.3%) 1.89× (−47.1%) 1.63× (−38.6%) 1.29× (−22.7%)

The win is largest at narrow feature width and tapers as feature width grows: the sidecar gather (27 × C loads, identical for both paths) increasingly dominates the fixed materialization saving. It is also larger at wider blocks (more register/occupancy relief).

Validation

  • Byte-identical output to the materialized path at all block widths and feature widths — the streaming filter's per-channel accumulation matches the materialized one bit-for-bit (same taps, same order). Also verified on other large VDB example data (dragon/emu/crawler/wdas_cloud).

swahtz added 2 commits July 24, 2026 04:02
Add VoxelBlockManager::forEachBoxStencil, which visits the 27 taps of the 3x3x3 box
stencil and streams (tap, index) to a device-inlined callback in the same deterministic
tap order as computeBoxStencil, without materializing the 27-element per-thread index
array. Consumers that reduce or accumulate taps (looking each index up in the sidecar)
avoid the stack frame and register pressure of the materialized form; callers needing
random access to all 27 taps keep computeBoxStencil.

Byte-exact with computeBoxStencil (identical output). Eliminating the st[27] index array
saves 26 registers + a 216-byte per-thread stack frame (70->44 regs, 216B->0; ptxas -v,
sm_120). On a real value-accumulating box filter the streaming path is 1.3-1.9x at 1 sidecar
channel, tapering with feature width as the per-tap gather dominates (~1.07x at 16 channels,
width 128; ~1.29x at 16 channels, width 512).

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant