Batched leaf-mask topology construction for generated conv grids - #757
Open
swahtz wants to merge 4 commits into
Open
Batched leaf-mask topology construction for generated conv grids#757swahtz wants to merge 4 commits into
swahtz wants to merge 4 commits into
Conversation
Generated-topology grid construction built one NanoVDB grid per batch member serially (3+ stream syncs per member inside RefineGrid/CoarsenGrid, a host-side speculative root refinement readback, per-member GridHandle constructor blocking copies, a host proxy grid + H2D per empty member) and then merged with another sync per grid -- ~1.5 ms of fixed overhead per member per build, linear in batch size, which dominates per-iteration ConvolutionPlan construction in generative training (issue openvdb#755). BatchedTopologyBuilder.cuh runs each factor-2 refine or coarsen pass over ALL batch members at once: one emission kernel over every source leaf produces candidate output leaves as (tile sort key, node key, origin, 512-bit mask) slots segmented per grid; two stable segmented radix sorts put each grid's slots in canonical NanoVDB node order (PointsToGrid's offset-shifted tile keys, then x-major upper/lower child offsets); head-flag + scan passes dedup nodes and derive per-grid counts and parent linkage; ONE stream synchronization reads back the node counts; and batched kernels write every grid's headers (mGridIndex=g, mGridCount=B), nodes, leaf mOffset/mPrefixSum, and bboxes into a single buffer. Root tiles are derived on-device from the unique upper keys, and empty members become valid empty grids inline. The mask bit math is NanoVDB's own refineMask/coarsenMask; the build stages are transcriptions of TopologyBuilder's functors with (gridIndex, localIndex) indexing. Checksums are disabled on the output, matching contiguousGridHandle and mergeGridHandles. fineGridHandleFromCoarseCUDA / coarseGridHandleFromFineCUDA route through the batched passes for all batch sizes (multi-pass factors chain passes, matching the previous per-pass semantics); masked subdivision still prunes upstream. CPU / PrivateUse1 paths and non-power-of-two coordinate fallbacks unchanged. The header also ships a BoxDilate pass (per-axis bit-shift Minkowski sums with unit boxes); its conv consumers land in the next commit. conv_transpose_grid(2,2) at batch 16: 9.0 ms -> 0.80 ms, near-flat in batch size (23x at batch 48). New equivalence tests pin the canonical node order elementwise against from_ijk-built topologies and the CPU paths across empty members, root-tile boundaries, negative octants, and sliced batches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
The stride-1 uniform-K conv/conv-transpose paths ran per-member
DilateGrid/PadGrid loops (perItemGridHandle), and the k3s2 transpose ran a
per-member RefineGrid + negative PadGrid -- the last remaining serial
generated-topology builders on the conv paths after the previous commit.
The canonical supports are Minkowski sums with axis-aligned unit boxes
(odd K: [-1,1]^3 per pass; even K: one-sided {-1,0}^3 / {0,1}^3 passes;
k3s2: refine then {-1,0}^3), so they map directly onto the batched BoxDilate
pass: per (source leaf, target neighbor leaf) slot, the mask contribution is
computed with per-axis bit shifts and deduplicated (mask-OR) by the shared
back-end, all batch members per pass at once. Both perItemGridHandle drivers
and their per-grid merge synchronizations are deleted. Resource-stats
semantics are unchanged (morphology paths still report zero coordinate
staging).
conv_grid(3,1) at batch 16: 9.8 ms -> 0.83 ms (25 ms -> 1.0 ms at batch 48);
the 4-level plan-pyramid rebuild drops 114 ms -> ~18 ms at batch 16.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
…kend from_grid_batch(1, 1, g) / from_grid_batch_transposed(1, 1, g) rebuild identity plans every iteration in per-step classifier heads (issue openvdb#755's secondary item), yet spent ~1.2 ms per call building tensor-valued transform diagnostics (voxel size/origin metadata tensors, allclose checks, small-tensor .item() round trips) that are trivially exact when source and target share their GridBatchData. Add an identity fast path: K == S == 1 with target_grid=None (conv_grid / conv_transpose_grid are the identity there, preserving public and data identity of the generated target) or an explicit target sharing grid data returns a _MatmulBackend plan directly with exact-by-construction compatibility diagnostics. Channel-pair validation, unknown-backend rejection, the pred_gather_igemm path, and the general path for distinct-but-equal grids (including incompatible-transform errors) are unchanged. Identity plan construction: ~1.2 ms -> 0.08 ms per call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
Two per-member host loops taxed every grid construction and plan validation (issue openvdb#755): - makeGridBatchData built leafBatchIndices with one torch::full per batch member plus a torch::cat (B+1 kernel dispatches per grid build). Leaf counts are already host-side, so one repeat_interleave over a device arange does the same in two dispatches. - voxelSizesTensor / voxelOriginsTensor filled their [B,3] metadata tensors with per-element ATen indexing (6 dispatches per grid per call, on every ConvolutionPlan transform validation). A CPU accessor fill removes the dispatches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 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.
Batched leaf-mask topology construction for generated conv grids
Fixes #755.
Summary
conv_grid/conv_transpose_gridwith generated targets (andrefined_grid/coarsened_grid/ the stride-1 dilate-pad paths generally) built their output one batch member at a time: each member paid a full NanoVDBRefineGrid/CoarsenGrid/DilateGrid/PadGridbuild (3+ stream synchronizations each, a host-side speculative root refinement with a pageable D2H readback, aGridHandleconstructor with 2 blocking memcpys + rawcudaMalloc/cudaFree), plus a host-built proxy grid + H2D per empty member, and finallynanovdb::cuda::mergeGridHandlesadded another synchronization per grid. That is ~1.5 ms of fixed overhead x B members x 21 plans per training iteration in generative workloads where topology changes every step (#741 / #753) — construction cost scaled linearly in batch size and left the GPU at ~35% utilization.This PR batches the leaf-mask topology machinery across the whole grid batch (extending #712's leaf-mask direction — no return to coordinate staging), in four self-contained commits:
1. Batched topology builder + refine/coarsen passes
New
src/fvdb/detail/utils/nanovdb/BatchedTopologyBuilder.cuh: one batched pass builds ALL grids at once. Per pass:RefineLeafMasksFunctor::refineMask,CoarsenLeafMasksFunctor::coarsenMask);torch.equaltests);refineRootand its readback disappear);cudaStreamSynchronizereads back per-grid node counts, one buffer is allocated, and batched kernels (transcribed fromtools::cuda::TopologyBuilder's functors with(gridIndex, localIndex)indexing) write every grid's headers (mGridIndex=g, mGridCount=B), nodes, leafmOffset/mPrefixSum, and bboxes. Empty members become valid empty grids inline.fineGridHandleFromCoarseCUDA/coarseGridHandleFromFineCUDAroute through it for all batch sizes; multi-pass factors (4, 8) chain passes, mirroring the previous per-pass semantics. Checksums are disabled on the output (matchingops::contiguousGridHandleandmergeGridHandles). CPU / PrivateUse1 paths and non-power-of-two coordinate fallbacks unchanged; masked subdivision still prunes upstream, then batched-refines.2. Batched box-dilate passes (stride-1 K>1, k3s2)
A
BoxDilatepass computes the Minkowski sum with an axis-aligned unit box via pure per-axis bit shifts (scatter formulation, up to 27 target leaves per source leaf, deduplicated by the same back-end). This coversDilateGrid's 26-neighbor dilation ([-1,1]^3) and bothPadGridoctants ({-1,0}^3,{0,1}^3), so the stride-1 uniform-K conv/conv-transpose paths and the k3s2 transpose (refine + negative pad) become batched pass sequences. The per-memberperItemGridHandledrivers are deleted from both conv builders.3. Identity-plan fast path (issue's secondary item)
ConvolutionPlan.from_grid_batch[_transposed](1, 1, g[, g])short-circuits to the matmul backend without building tensor-valued transform diagnostics (which are trivially exact when source and target shareGridBatchData), preserving channel-pair validation, backend-name rejection, and the general path for distinct-but-equal-looking grids: ~1.2 ms -> 0.08 ms per call.4. Grid-construction cheap wins
makeGridBatchData:leafBatchIndicesvia onerepeat_interleaveinstead of B xtorch::full+torch::cat(B+1 dispatches on every grid construction).voxelSizesTensor/voxelOriginsTensor: accessor fill instead of per-element ATen indexing (6 dispatches per grid per call, on every plan construction's transform validation).Measurements
RTX PRO 6000 Blackwell, synthetic shell batches (~7.8k voxels/member, resolution 64), median of 20 CUDA-event-timed iterations (
src/benchmarks/convolution/benchmark_conv_grid_build.py):conv_transpose_gridk2s2conv_gridk2s2conv_gridk3s1from_grid_batch(2,2,g)from_grid_batch_transposed(2,2,g)from_grid_batch(1,1,g,g)Construction cost is now near-flat in batch size (B=1: within noise of the old single-grid path). The issue's per-iteration plan-construction share (~70 ms of a 165 ms shape-VAE iteration at B=16) drops to single-digit milliseconds.
Follow-ups (out of scope, same back-end): an ijk emission front-end to resolve
BuildGridFromIjk.cu's per-member FIXME (from_ijk/from_points/shifted-geometry fallbacks), anddilated_grid/BuildPaddedGrid's standalone per-member loops.Test plan
tests/unit/test_batched_topology_builder.py(18 tests): elementwise (torch.equalonijk.jdata,num_voxels, per-member bboxes) equivalence againstfrom_ijk-built expected topologies — pinning canonical node order — plus per-member coordinate-set equality against the CPU paths, across: mixed member sizes, empty members (first/middle/last/all), coordinates straddling +-4096 root-tile boundaries and negative octants (where the sort-key and storedTile::keyencodings order differently), single-grid and 16-grid batches, factors 2 and 4, masked refine,conv_grid/conv_transpose_gridK in {2,3,4,5} at stride 1, k3s2 transpose, k2s2 vs per-member, and a refine->coarsen round trip.pytest unit/test_conv_semantics_integration.py unit/test_conv_default.py unit/test_conv_transpose_default.py unit/test_batching.py unit/test_basic_ops.py unit/test_sliced_batch.py unit/test_conv_semantics.py unit/test_conv_ground_truth.py unit/test_nn_modules.py— includes the elementwise ijk order pins, sliced-view coverage, resource-stats path pinning, and the matmul/identity plan contract tests.python src/benchmarks/convolution/benchmark_conv_grid_build.py(numbers above;--gsoruns the issue's verbatim GSO repro).🤖 Generated with Claude Code