Skip to content

Build dual/padded grids via leaf-mask morphology to cut memory - #710

Merged
swahtz merged 12 commits into
openvdb:mainfrom
swahtz:dual-grid-leaf-mask-morphology
Aug 3, 2026
Merged

Build dual/padded grids via leaf-mask morphology to cut memory#710
swahtz merged 12 commits into
openvdb:mainfrom
swahtz:dual-grid-leaf-mask-morphology

Conversation

@swahtz

@swahtz swahtz commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

dual_grid materialized an 8x-expanded candidate coordinate list before deduplicating it, costing ~420-450 bytes of transient memory per input voxel (and overflowing an int32 cast in NanoVDB's segmented radix sort above ~2^31 candidates). A ~500M-voxel grid OOMed on an 80GB GPU.

Build the padded topology directly from the 512-bit leaf activity masks via nanovdb::tools::cuda::TopologyBuilder instead, so scratch is O(node count) rather than O(voxel count).

  • New src/fvdb/detail/utils/nanovdb/PadGrid.cuh: a one-sided (octant) analogue of nanovdb's DilateGrid. The internal-node functor reuses DilateInternalNodesFunctor's scatter verbatim with only the neighbor stencil swapped for a one-sided one; the leaf and erosion functors are the one-sided shift-OR / shift-AND derivations.
  • BuildPaddedGrid.cu CUDA path composes bmax positive + -bmin negative unit passes (pad, or erode + PruneGrid for exclude_border), preserving the primal/dual transform swap. dual_grid is one positive pass.
  • Guard the empty-erosion case (PruneGrid cannot build an empty grid); use GridHandle::copy for the bmin==bmax==0 identity.
  • Reject exclude_border on PrivateUse1 explicitly (was a latent crash) and require bmin <= 0 <= bmax.
  • Add a low-level build_padded_grid python binding for the generic box, plus tests covering CUDA/CPU parity, leaf/root boundary crossings, empty batch items, erode-to-empty, and peak memory.

dual_grid materialized an 8x-expanded candidate coordinate list before
deduplicating it, costing ~420-450 bytes of transient memory per input
voxel (and overflowing an int32 cast in NanoVDB's segmented radix sort
above ~2^31 candidates). A ~500M-voxel grid OOMed on an 80GB GPU.

Build the padded topology directly from the 512-bit leaf activity masks
via nanovdb::tools::cuda::TopologyBuilder instead, so scratch is
O(node count) rather than O(voxel count).

- New src/fvdb/detail/utils/nanovdb/PadGrid.cuh: a one-sided (octant)
  analogue of nanovdb's DilateGrid. The internal-node functor reuses
  DilateInternalNodesFunctor's scatter verbatim with only the neighbor
  stencil swapped for a one-sided one; the leaf and erosion functors are
  the one-sided shift-OR / shift-AND derivations.
- BuildPaddedGrid.cu CUDA path composes bmax positive + -bmin negative
  unit passes (pad, or erode + PruneGrid for exclude_border), preserving
  the primal/dual transform swap. dual_grid is one positive pass.
- Guard the empty-erosion case (PruneGrid cannot build an empty grid);
  use GridHandle::copy for the bmin==bmax==0 identity.
- Reject exclude_border on PrivateUse1 explicitly (was a latent crash)
  and require bmin <= 0 <= bmax.
- Add a low-level build_padded_grid binding for the generic box, plus
  tests covering CUDA/CPU parity, leaf/root boundary crossings, empty
  batch items, erode-to-empty, and peak memory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
@swahtz
swahtz requested a review from a team as a code owner July 31, 2026 04:13
@swahtz
swahtz requested review from harrism and sifakis July 31, 2026 04:13
@swahtz swahtz added optimization Performance or memory optimization Topology Operations Issues related to topology operations (prune, merge, dilate, etc. labels Jul 31, 2026
@swahtz swahtz self-assigned this Jul 31, 2026
@swahtz swahtz added this to the v0.6 milestone Jul 31, 2026
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reworks the CUDA implementation of dual_grid / padded-grid construction to avoid materializing an 8× expanded coordinate list by instead building padded topology directly from NanoVDB leaf activity masks via TopologyBuilder, significantly reducing transient memory usage and avoiding large-candidate overflows.

Changes:

  • Adds a new device-side one-sided morphology implementation (PadGrid.cuh) to build padded/dual topologies from leaf masks.
  • Updates the CUDA padded-grid path (BuildPaddedGrid.cu) to use mask-morphology passes (and erosion+prune for exclude_border), with explicit guards for identity and empty-erosion cases.
  • Exposes a low-level Python binding build_padded_grid(...) and adds CUDA/CPU parity + boundary + memory tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/unit/test_dual.py Adds CUDA/CPU parity tests for dual_grid and generic padded boxes, plus boundary/empty/memory coverage.
src/python/GridBatchOps.cpp Exposes build_padded_grid binding and routes dual_grid through buildPaddedGrid.
src/fvdb/detail/utils/nanovdb/PadGrid.cuh New one-sided (octant) mask-morphology topology builder for padded/dual grids on CUDA.
src/fvdb/detail/ops/BuildPaddedGrid.cu Switches CUDA implementation to TopologyBuilder-based mask morphology; adds erosion+prune path and guards.
fvdb/_fvdb_cpp.pyi Adds the build_padded_grid type stub.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fvdb/detail/utils/nanovdb/PadGrid.cuh
Comment thread src/fvdb/detail/utils/nanovdb/PadGrid.cuh
Comment thread src/fvdb/detail/ops/BuildPaddedGrid.cu
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
@swahtz
swahtz requested a review from Copilot July 31, 2026 04:30
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/python/GridBatchOps.cpp:675

  • build_padded_grid is exposed as a general padding primitive, but it delegates to ops::buildPaddedGrid, which always swaps primal/dual transforms (see BuildPaddedGrid.cu where mDualTransform/mPrimalTransform are swapped). For callers, build_padded_grid(bmin=0,bmax=0) is therefore not a pure identity (it still swaps transforms), which is surprising given the name/signature. Please either document this behavior on the binding or rename the binding to make the dual-grid semantics explicit.
        [](const GBI &grid, int bmin, int bmax, bool excludeBorder) {
            if (grid.batchSize() == 0) {
                return fvdb::detail::makeEmptyGridBatchData(grid.device());
            }
            return ops::buildPaddedGrid(grid, bmin, bmax, excludeBorder);

buildPaddedGrid unconditionally swapped the source's primal/dual transforms
in its metadata tail. That is correct for dual_grid, whose result voxels sit
on the corner (dual) lattice, but wrong for the general build_padded_grid
primitive exposed in this PR: a plain padded grid stays on the *same* lattice
as its source, so build_padded_grid(0, 0) was not an identity (it shifted the
origin by half a voxel) and every box pad silently moved to the dual lattice.

Thread a dualTransform flag through ops::buildPaddedGrid: dual_grid passes
true (keep the swap), build_padded_grid passes false (carry the source
transforms verbatim). populateGridMetadata already recomputes the correct
primal/dual transforms from the source's (voxelSize, origin), so the non-dual
path just copies them over unchanged.

Adds test_dual_grid_transform_is_dual_lattice (origin shifts by -0.5 voxel)
and test_build_padded_grid_preserves_transform (origin/voxel size unchanged
for every box), both on CPU and CUDA.

Addresses the review on openvdb#710.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
@swahtz

swahtz commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Re: the suppressed Copilot note on GridBatchOps.cpp — that build_padded_grid inherits buildPaddedGrid's unconditional primal/dual transform swap, so build_padded_grid(bmin=0, bmax=0) isn't a pure identity.

Good catch, and it's a real correctness bug rather than just a naming/doc nit. The swap is only correct for dual_grid, whose result voxels sit on the corner (dual) lattice; a plain padded grid stays on the same lattice as its source and must keep the source's transforms.

Fixed: ops::buildPaddedGrid now takes a dualTransform flag.

  • dual_gridtrue: keeps the primal↔dual swap (origin shifts by −½ voxel). Behavior unchanged.
  • build_padded_gridfalse: carries the source transforms verbatim, so build_padded_grid(0, 0) is a true identity and every box pad stays on the source lattice.

populateGridMetadata already recomputes the correct primal/dual transforms from the source's (voxelSize, origin), so the non-dual path just copies them across unchanged.

Added regression tests on CPU + CUDA: test_dual_grid_transform_is_dual_lattice (dual origin == base − ½·voxel_size) and test_build_padded_grid_preserves_transform (origin and voxel size unchanged for every box, including asymmetric [0, 2] / [-2, 1]). Full unit suite green.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/fvdb/detail/utils/nanovdb/PadGrid.cuh:179

  • valueMask is declared but never used in PadInternalNodesFunctor. This will trigger an unused-variable warning (and can fail builds if warnings are treated as errors). Remove it or explicitly mark it unused.
        const auto &srcTree = srcGrid->tree();
        const auto &lower   = srcTree.template getFirstNode<1>()[lowerID];
        auto &valueMask     = const_cast<LowerMaskT &>(lower.valueMask());

The valueMask reference bound in PadInternalNodesFunctor was never read --
the padding stencil reads each child leaf's valueMask() directly -- so it
only produced an unused-variable warning. The const_cast reference binding
had no side effect, so drop it. Addresses the Copilot review note on openvdb#710.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
@swahtz

swahtz commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Re: the suppressed Copilot note on PadGrid.cuhvalueMask declared but never used in PadInternalNodesFunctor.

Fixed — removed it. The stencil reads each child leaf's mask directly (padNeighborMaskStencil<Positive>(leaf.valueMask())), so the outer const_cast reference to the lower node's valueMask() was dead and had no side effect. Clean build, unit suite green.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

src/fvdb/detail/ops/BuildPaddedGrid.cu:337

  • This retrieves the per-item grid via mGridHdl->deviceGrid(i), which assumes batch item i corresponds to the i-th grid in the underlying handle. That is not true for non-contiguous/sliced GridBatchData (which uses per-item mCumBytes offsets; see GridBatchData::Accessor::grid). Use the batch metadata’s byte offsets to compute the grid pointer so CUDA padding works correctly on indexed batches too.
        nanovdb::OnIndexGrid *grid = baseBatchHdl.mGridHdl->deviceGrid<nanovdb::ValueOnIndex>(i);
        TORCH_CHECK(grid, "Grid is null");

Comment thread src/fvdb/detail/ops/BuildPaddedGrid.cu Outdated
A sliced or indexed GridBatchData is a view: it shares the underlying NanoVDB
handle (which keeps every grid) and only shrinks batchSize() and the metadata
array, so batchSize() < nanoGridHandle().gridCount() and each item's grid is
located by its mCumBytes byte offset, not by physical index.

The CUDA path indexed the handle physically (mGridHdl->deviceGrid(i)) and the
identity fast-path copied the whole handle, so on a sliced batch dual_grid
returned the wrong grids and build_padded_grid pulled in the excluded siblings
(with an out-of-bounds metadata read in the transform fix-up). The CPU path
looped gridCount() with the same effect, corrupting the heap. main's CUDA path
was correct here (it went through the Accessor); this restores that.

- CUDA padding loop: resolve each source grid as bufBase + cumBytesAt(i) (the
  mapping Accessor::grid() uses), iterating batchSize(). No input copy.
- CUDA identity (bmin==bmax==0): keep the one-shot whole-handle copy for a
  contiguous batch; for a sliced view rebuild from Accessor-mapped coordinates
  (only build_padded_grid(0,0) reaches this; dual_grid, being (0,1), never does).
- CPU paths: iterate batchSize() and resolve grids by byte offset.

Adds test_sliced_batch_padding_matches_contiguous (CPU+CUDA) over tail, gap,
reversed, and single-item selections. Addresses the review on openvdb#710.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
@swahtz
swahtz requested a review from Copilot August 2, 2026 22:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

src/fvdb/detail/utils/nanovdb/PadGrid.cuh:534

  • Same UB pattern as in PadLeafNodesFunctor: originalWords is defined via reinterpret_cast<WordStencilT>(originalWordsShifted[1][1][1]) and later indexed with negative indices (originalWords[-1][…], originalWords[i][…][-1], etc.). Because the reference does not denote an actual [10][3][3] array object, this relies on undefined behavior and could be miscompiled.

Recommend switching to direct indexing into originalWordsShifted with explicit +1 offsets (or an accessor helper) so all indices stay within the declared array object.

        uint64_t originalWordsShifted[10][3][3] = {};
        using WordStencilT                      = uint64_t(&)[10][3][3];
        auto &originalWords = reinterpret_cast<WordStencilT>(originalWordsShifted[1][1][1]);

Comment thread src/fvdb/detail/utils/nanovdb/PadGrid.cuh
…dexing

PadLeafNodesFunctor and ErodeKeepMaskFunctor built a uint64_t(&)[10][3][3]
reference by reinterpret_cast'ing the address of the middle element
originalWordsShifted[1][1][1], then indexed it with negative and out-of-range
subscripts (originalWords[-1][..], originalWords[8][..], originalWords[i-1][..][-1]).
The notional [10][3][3] array does not exist at that address (it also extends
past the real buffer), so the negative/out-of-range subscripting is undefined
behavior an optimizer may miscompile.

Replace the reference with a small centered-accessor lambda that indexes the
real array with an explicit +1 offset -- originalWords(i, dBj, dBk) maps to
originalWordsShifted[i + 1][dBj + 1][dBk + 1] -- so every access is in bounds.
The computed addresses (and generated code) are identical; the CUDA<->CPU
parity tests are unchanged. Addresses the review on openvdb#710.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/fvdb/detail/ops/BuildPaddedGrid.cu:376

  • When excludeBorder==true, a grid with numVoxelsAt(i) > 0 but leafCount == 0 (e.g. a grid represented by value tiles, or otherwise malformed metadata) will hit break before handle is assigned. The code then pushes a default-constructed GridHandle, which can lead to crashes later.

Even if this situation “shouldn’t happen”, it’s safer to make the leafCount==0 branch produce an explicit empty handle (or throw) so behavior is well-defined.

                              .mNodeCount[0]
                        : baseBatchHdl.numLeavesAt(i);
                if (leafCount == 0) {
                    break; // already eroded to empty; further erosion is a no-op
                }

src/fvdb/detail/ops/BuildPaddedGrid.cu:277

  • keepTensor is allocated as a uint8 tensor and then reinterpreted as nanovdb::Mask<3>*. This relies on the returned pointer being suitably aligned for Mask<3> (likely 8-byte alignment); if it isn't, dereferencing in the CUDA kernels is undefined behavior and can cause misaligned global memory accesses.

Allocate the sidecar as a torch::kUInt64 tensor (or otherwise ensure alignment) before casting to Mask<3>*.

This issue also appears on line 372 of the same file.

    const int64_t maskBytes = static_cast<int64_t>(sizeof(nanovdb::Mask<3>)) * leafCount;
    torch::Tensor keepTensor =
        torch::empty({maskBytes}, torch::TensorOptions().dtype(torch::kUInt8).device(device));
    auto *keepMasks = reinterpret_cast<nanovdb::Mask<3> *>(keepTensor.data_ptr());

…grids

Two defensive fixes from the openvdb#710 review, both on the exclude_border erosion path:

- Allocate the per-leaf keep-mask sidecar as uint64 rather than uint8 so its
  data pointer is guaranteed 8-byte aligned for Mask<3> (8 uint64_t words),
  which the kernels dereference directly. torch's allocators over-align in
  practice, but the tensor API doesn't guarantee it. The emptiness check runs
  on a zero-copy uint8 view, since torch's any() (an 'or' reduction) isn't
  implemented for uint64.

- In the erosion pass loop, if the first pass sees leafCount == 0 (a grid with
  voxels but no leaf nodes, e.g. a tile-only grid), push an explicit empty grid
  handle instead of the default-constructed one, which would crash downstream.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
@swahtz

swahtz commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the two suppressed Copilot notes on BuildPaddedGrid.cu (in 39847f2):

  • Erosion leafCount == 0: a grid with voxels but no leaf nodes (a tile-only grid) would break before handle was assigned and push a default-constructed GridHandle. It now pushes an explicit createEmptyGridHandle in that case, so the result is always well-defined.
  • kUInt8 keep-mask sidecar → Mask<3>*: now allocated as kUInt64 so the data pointer is guaranteed 8-byte aligned for Mask<3> (8 uint64 words). The emptiness check runs on a zero-copy uint8 view, since torch's any() (an or reduction) isn't implemented for uint64. (There's a single such sidecar; the "also on line 372" reference didn't correspond to a second allocation.)

test_dual (including the exclude_border erosion paths) and the full unit suite are green.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/fvdb/detail/ops/BuildPaddedGrid.cu:334

  • numNegative is computed as -bmin using int arithmetic. If a caller passes bmin == INT_MIN, this negation overflows (undefined behavior) before any range checks can catch it, and totalPasses becomes incorrect. Compute counts in a wider signed type and validate that the pass count is non-negative and fits in an int loop bound.
    const int numPositive = bmax;
    const int numNegative = -bmin;
    const int totalPasses = numPositive + numNegative;

src/fvdb/detail/utils/nanovdb/PadGrid.cuh:720

  • cudaMemcpyAsync(..., cudaMemcpyDeviceToHost, mStream) is followed by immediate dereference of srcRootAndUpperBuffer on the host without synchronizing the stream. This is only safe if HostBuffer::create() always returns pageable memory (making the copy implicitly synchronous); if it ever returns pinned memory, this becomes a real race and can read uninitialized data. Prefer making the copy explicitly synchronous (either cudaMemcpy or cudaStreamSynchronize(mStream) after the async copy) to keep correctness independent of the host-buffer allocation strategy.
        auto srcRootAndUpperBuffer = nanovdb::HostBuffer::create(rootAndUpperSize);
        cudaCheck(cudaMemcpyAsync(srcRootAndUpperBuffer.data(),
                                  deviceSrcRoot,
                                  rootAndUpperSize,
                                  cudaMemcpyDeviceToHost,
                                  mStream));
        auto srcRootAndUpper = static_cast<RootT *>(srcRootAndUpperBuffer.data());

        for (uint32_t t = 0; t < srcRootAndUpper->tileCount(); t++) {

The root-speculation step copies the source root+upper nodes to a host buffer
with cudaMemcpyAsync(..., cudaMemcpyDeviceToHost, mStream) and then dereferences
it on the host to enumerate tiles. nanovdb::HostBuffer::create() returns pageable
memory today, which makes that D2H copy implicitly host-synchronous, so it works
-- but a pinned host buffer would leave the copy in flight and the host read
would race on uninitialized data.

Add an explicit cudaStreamSynchronize(mStream) before the dereference so
correctness no longer depends on the host-buffer allocation strategy. Cost is
negligible: one small copy per grid. Addresses the review on openvdb#710.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
swahtz added a commit to swahtz/fvdb-core that referenced this pull request Aug 3, 2026
A sliced/indexed GridBatch (grid[idx], grid[a:b]) is a view: it shares the
underlying handle (which keeps every grid) and only shrinks batchSize() and the
per-grid metadata, so batchSize() < nanoGridHandle().gridCount() and each item's
grid is located by its mCumBytes byte offset, not by physical index. The
mask-morphology fast paths resolved grids with mGridHdl->deviceGrid(i) (physical)
and returned whole-handle copies, so on an indexed batch they silently produced
the wrong grids or a voxelSizes/gridCount mismatch -- the same bug class fixed in
buildPaddedGrid (openvdb#710), across the rest of the openvdb#711 ops and the pre-existing
dilate/prune/clone helpers they use.

- GridBatchData gains view-aware accessors deviceGridPtrAt(i)/hostGridPtrAt(i):
  the i-th *logical* grid resolved by byte offset (like Accessor::grid(i)). Every
  physical-index site (conv, conv_transpose, coarsen, subdivide, dilate, prune --
  CUDA fast paths and CPU proxy loops) now uses them.

- MakeContiguous exposes contiguousGridHandle() (compact the selected grids: a
  per-grid byte copy + mGridIndex/mGridCount header fixup, no radix sort) and
  cloneGridHandleAt(); makeContiguous and cloneGrid build on them. cloneGrid had
  copied *every physical grid* of a shared handle -- wrong for a sliced batch.

- Zero-copy identity: coarsened_grid(1) / refined_grid(1) / dilated_grid(0) now
  return the input grid unchanged in Python, preserving its view and metadata with
  no copy (the C++ ops keep a compacting-copy fallback for direct callers).

Adds test_sliced_batch.py: every op on tail/gap/reversed/single-item views
(CPU+CUDA) matches an independently-built contiguous reference. Full unit suite
green (2775 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
The erode keep-mask emptiness check reduced over a uint8 view of the uint64
sidecar because any() (an 'or' reduction) is not implemented for uint64 on CUDA.
(mask != 0).any() is equivalent and clearer -- the != yields a bool tensor whose
any() reduction is implemented, so no dtype-reinterpreting view is needed.
(.view(dtype) did compile and work; this just avoids the byte-reinterpretation.)
Addresses the review on openvdb#712.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
swahtz added a commit to swahtz/fvdb-core that referenced this pull request Aug 3, 2026
A sliced/indexed GridBatch (grid[idx], grid[a:b]) is a view: it shares the
underlying handle (which keeps every grid) and only shrinks batchSize() and the
per-grid metadata, so batchSize() < nanoGridHandle().gridCount() and each item's
grid is located by its mCumBytes byte offset, not by physical index. The
mask-morphology fast paths resolved grids with mGridHdl->deviceGrid(i) (physical)
and returned whole-handle copies, so on an indexed batch they silently produced
the wrong grids or a voxelSizes/gridCount mismatch -- the same bug class fixed in
buildPaddedGrid (openvdb#710), across the rest of the openvdb#711 ops and the pre-existing
dilate/prune/clone helpers they use.

- GridBatchData gains view-aware accessors deviceGridPtrAt(i)/hostGridPtrAt(i):
  the i-th *logical* grid resolved by byte offset (like Accessor::grid(i)). Every
  physical-index site (conv, conv_transpose, coarsen, subdivide, dilate, prune --
  CUDA fast paths and CPU proxy loops) now uses them.

- MakeContiguous exposes contiguousGridHandle() (compact the selected grids: a
  per-grid byte copy + mGridIndex/mGridCount header fixup, no radix sort) and
  cloneGridHandleAt(); makeContiguous and cloneGrid build on them. cloneGrid had
  copied *every physical grid* of a shared handle -- wrong for a sliced batch.

- Zero-copy identity: coarsened_grid(1) / refined_grid(1) / dilated_grid(0) now
  return the input grid unchanged in Python, preserving its view and metadata with
  no copy (the C++ ops keep a compacting-copy fallback for direct callers).

Adds test_sliced_batch.py: every op on tail/gap/reversed/single-item views
(CPU+CUDA) matches an independently-built contiguous reference. Full unit suite
green (2775 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
Comment thread src/fvdb/detail/ops/BuildPaddedGrid.h Outdated
Comment thread src/fvdb/detail/utils/nanovdb/PadGrid.cuh
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
@swahtz
swahtz enabled auto-merge (squash) August 3, 2026 05:57
swahtz added a commit to swahtz/fvdb-core that referenced this pull request Aug 3, 2026
A sliced/indexed GridBatch (grid[idx], grid[a:b]) is a view: it shares the
underlying handle (which keeps every grid) and only shrinks batchSize() and the
per-grid metadata, so batchSize() < nanoGridHandle().gridCount() and each item's
grid is located by its mCumBytes byte offset, not by physical index. The
mask-morphology fast paths resolved grids with mGridHdl->deviceGrid(i) (physical)
and returned whole-handle copies, so on an indexed batch they silently produced
the wrong grids or a voxelSizes/gridCount mismatch -- the same bug class fixed in
buildPaddedGrid (openvdb#710), across the rest of the openvdb#711 ops and the pre-existing
dilate/prune/clone helpers they use.

- GridBatchData gains view-aware accessors deviceGridPtrAt(i)/hostGridPtrAt(i):
  the i-th *logical* grid resolved by byte offset (like Accessor::grid(i)). Every
  physical-index site (conv, conv_transpose, coarsen, subdivide, dilate, prune --
  CUDA fast paths and CPU proxy loops) now uses them.

- MakeContiguous exposes contiguousGridHandle() (compact the selected grids: a
  per-grid byte copy + mGridIndex/mGridCount header fixup, no radix sort) and
  cloneGridHandleAt(); makeContiguous and cloneGrid build on them. cloneGrid had
  copied *every physical grid* of a shared handle -- wrong for a sliced batch.

- Zero-copy identity: coarsened_grid(1) / refined_grid(1) / dilated_grid(0) now
  return the input grid unchanged in Python, preserving its view and metadata with
  no copy (the C++ ops keep a compacting-copy fallback for direct callers).

Adds test_sliced_batch.py: every op on tail/gap/reversed/single-item views
(CPU+CUDA) matches an independently-built contiguous reference. Full unit suite
green (2775 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
@swahtz
swahtz merged commit 940630c into openvdb:main Aug 3, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

optimization Performance or memory optimization Topology Operations Issues related to topology operations (prune, merge, dilate, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants