Alternative stencil resolvers for VoxelBlockManager - #2263
Draft
swahtz wants to merge 8 commits into
Draft
Conversation
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>
…lBlockManager Add computeCrossStencil (7-point cross) and computeBoxStencilCached (full 3x3x3 box). A block's voxels span few distinct, consecutive leaves, so the thread block cooperatively stages a per-spanned-leaf neighbor table in shared memory (built by ALL threads) and each stencil tap becomes a direct in-leaf lookup instead of a root-down tree traversal. Blocks spanning more than MaxCachedLeaves leaves fall back to per-tap traversal. Byte-exact with computeBoxStencil at block widths 64/128/256/512. Measured on Blackwell with value-accumulating filters over an Index-Grid sidecar (1-16 channels), on sparse and dense topology: the 7-tap cross filter is 4.1-6.8x at one channel (1.5-2.0x at 16), and the 27-tap cached box is 1.1-1.6x; both widen at larger block widths and narrow as the per-tap payload gather grows. Partial-tap consumers keep the naive computeBoxStencil, whose tap-level dead-code elimination already skips unread taps. Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
…StencilCached) Combine the cached leaf table of computeBoxStencilCached with the streaming output of forEachBoxStencil: taps are resolved through the staged per-leaf neighbor table AND handed to a device-inlined callback, so a 27-tap consumer pays neither the 27 root-down traversals nor the 27-element per-thread array. The two costs are independent, so fusing them beats either technique alone. Byte-exact with computeBoxStencil at block widths 64/128/256/512. On a real value- accumulating filter over an Index-Grid sidecar: 1.85x (width 128) and 2.59x (width 512) over the naive resolution at one channel, versus 1.28x/1.54x for the cached table alone and 1.33x/1.91x for streaming alone. Requires all threads in the block (cooperative table build), so plain forEachBoxStencil remains the option for divergent call sites. Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
…mputeCrossStencilCached Give the 7-point cross the same two forms as the box stencil, and state the naming convention on the class: <name> resolves each tap by a root-down traversal, uses no shared memory and is safe from divergent threads; <name>Cached stages a cooperative per-leaf table in shared memory, is substantially faster, and must be called by all threads. The naive cross is not merely an intent/symmetry API: resolving the 7 taps directly measured 1.57x (width 128) and 1.75x (width 512) faster than calling computeBoxStencil and reading only the cross slots, i.e. the compiler does not fully eliminate the 20 unread taps. The cached form remains fastest at 2.13x/2.26x on the same consumer. Byte-exact with the naive box stencil for the cross slots at widths 64/128/256/512. No streaming (forEach) cross variant is provided: a 7-element stencil is cheap enough to materialize that removing the array measured as a wash. Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
…vers The cached resolvers shared two pieces of copied code: the span-detection preamble (appearing three times) and, between computeBoxStencilCached and forEachBoxStencilCached, an entire table-staging and tap-resolution body that differed only in whether each tap was stored or handed to a callback. Extract the preamble as cachedLeafSpan(), and express computeBoxStencilCached as forEachBoxStencilCached with a storing callback. No behaviour change: registers, stack and shared memory are identical (74 regs / 216 B stack / 4228 B smem for a materialized cached box kernel; 38 / 16 / 1668 for the cross), timings are within +-0.3% run-to-run noise, and all VBM goldens still pass byte-exact. Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
…d resolvers Both cached resolvers staged their per-spanned-leaf table with the same strided loop: decompose the flat entry index into (slot, entry), special-case the entry holding the leaf itself, probe for the rest, and barrier. Extract that as stageLeafTable(), parameterised on the table width, the self entry, and the shift from a leaf origin to its n-th neighbor's origin - so each resolver keeps the entry ordering its lookup expects and its own cheap offset arithmetic. No behaviour change and no codegen cost: the cross kernel is unchanged at 38 registers / 16 B stack / 1668 B smem and the materialized cached box improves slightly to 73 registers (from 74) at the same stack and shared memory; timings match the pre-refactor baseline and all VBM goldens still pass byte-exact. An earlier attempt that unified the offset arithmetic too - mapping each entry onto a 3x3x3 spoke id and decomposing that back into per-axis offsets - measured ~0.9% slower on the cross resolver, because it replaced a single-axis add with an encode/decode round trip. Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
…resolvers Rename the terse identifiers introduced with the cached resolvers to read as plain English, matching the naming used elsewhere in this file: c/slot/n -> entry/leafSlot/neighborID, nSpanned -> spannedLeafCount, firstSpanned -> firstLeaf, nl -> neighborLeaf, cached -> isCached, vi/vj/vk -> voxelX/Y/Z, li/lj/lk -> leafX/Y/Z, and sSpannedCount -> sSpannedLeafCount. Also note why the cross lookup tests (voxelOnAxis + dir) & ~7, i.e. whether the tap steps off the leaf on that axis. Comments and identifiers only. Registers, stack and shared memory are unchanged (38/16/1668 for the cross kernel, 73/216/4228 for the materialized cached box) and all VBM goldens still pass byte-exact. Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR focuses on two independent costs that are part of resolving a 3×3×3 stencil through the VoxelBlockManager: 27 root-down tree traversals (one per tap), and a 27-element per-thread index array to hold the result. This PR adds new resolver functions (alongside the existing
computeBoxStencil) that address each cost, and one that addresses both:Each stencil shape now comes in a plain and a
Cachedform, with a single convention stated on the class:<name>— resolves each tap by a root-down tree traversal. No shared memory, no barrier, safe from divergent threads.<name>Cached— the block cooperatively stages a per-leaf neighbour table in shared memory, so each tap is a direct in-leaf lookup. Substantially faster, but must be called by all threads (uses__syncthreads) and costs shared memory.Independently,
compute<Name>materializes taps into a 27-slot array whileforEach<Name>streams(tap, index)to a device-inlined callback, avoiding the per-thread array entirely.computeBoxStencil(existing, unchanged)forEachBoxStencilcomputeBoxStencilCachedforEachBoxStencilCachedcomputeCrossStencilcomputeCrossStencilCachedThe cached resolvers exploit a structural property of the VBM: a block's voxels span few distinct, consecutive leaves, so the block cooperatively stages a per-leaf neighbour table in shared memory (built by all threads, not a leader) and each tap becomes a direct in-leaf lookup. A 3×3×3 stencil touches at most 7 neighbouring leaves, not 27 independent lookups. Blocks spanning more than
MaxCachedLeavesfall back to per-tap traversal.Measurements
All six resolvers measured in one run, on one grid, against one baseline, with the same consumer: resolve the taps, then accumulate
Csidecar channels per tap (box variants accumulate their 27, cross variants their 7). Sparse sphere shell: 1.29M active voxels across 9140 leaves, mean leaf occupancy 141/512 (~72% sparse). Output is byte-exact within each stencil shape.One sidecar channel
computeBoxStencil(existing)forEachBoxStencilcomputeBoxStencilCachedforEachBoxStencilCachedcomputeCrossStencilcomputeCrossStencilCachedSpeedups share the
computeBoxStencilbaseline — noting that the cross resolvers deliberately resolve 7 taps rather than 27, which is the point of matching the resolver to the stencil shape.Throughout the rest of this description, a pair like 1.26× / 1.56× means BlockWidth 128 / BlockWidth 512.
Shared memory is the resolver's table only:
MaxCachedLeaves(16) x entries x 8 B. Confirmed againstptxas -v— a width-128 kernel reports 4228 B total for the cached box (512 B leaf-index array + 256 B voxel-offset array + 3456 B table + 4 B span counter) and 1668 B for the cross.What each technique contributes
Feature width erodes every margin
At 8 sidecar channels the
taps x Cpayload gather (identical for all resolvers) starts to dominate:forEachBoxStencilCached1.41x / 2.14x,computeBoxStencilCached1.22x / 1.26x,computeCrossStencilCached3.92x / 4.56x. The ordering is unchanged.Register cost of materialization
For a 27-tap consumer (
ptxas -v, sm_120) theuint64_t st[27]array costs 26 registers and a 216-byte stack frame (70 -> 44 registers, 216 B -> 0; the 216 B is exactly27 x sizeof(uint64)). On sm_120 that is 28 -> 42 resident warps, 58.3% -> 87.5% occupancy — the relief the streaming variants capture.Validation
computeBoxStencil, and the cross resolvers all agree with each other, at block widths 64/128/256/512, on sparse and dense topology and at every feature width measured. The resolvers change how a tap index is found, never which index. (A cross resolver writes only the 7 cross slots, so it is compared against a cross-shaped consumer rather than the full box.)VoxelBlockManager_ValueOnIndexunit-test path passes: 250,047 (voxel, tap) comparisons againstgetAccessor().getValue(), zero mismatches.Notes
computeBoxStencilkeeps its documented contract (no shared memory, no barrier, callable from divergent threads); the cached forms are additive. Callers choose between them on two axes that are genuinely theirs to weigh: whether the call site is uniform, and whether the block can spare the shared memory for the table (16 x 27 leaf pointers for the box, 16 x 7 for the cross).MaxCachedLeavesis 16; the box table costs 16 × 27 leaf pointers of shared memory per block (the cross table 16 × 7), which is the main resource trade-off against the naive path.forEachCrossStencilwas not measured to win any performance gains overcomputeCrossStencil, no additional register pressure was created instantiating the 7-integer table vs. the full 27-integer table for the cross stencil. I made the decision to drop this implementation.