Skip to content

Use fused NanoVDB ReadAccessor::getDimAndActive in the HDDA iterators - #754

Merged
swahtz merged 1 commit into
openvdb:mainfrom
swahtz:hdda_fused_dim_active
Sep 1, 2026
Merged

swahtz merged 1 commit into
openvdb:mainfrom
swahtz:hdda_fused_dim_active

Conversation

@swahtz

@swahtz swahtz commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

OpenVDB PR #2220 added the fused ReadAccessor::getDimAndActive API to NanoVDB: a single Root→Internal→Leaf descent (or accessor-cache lookup) that returns both the HDDA step size and the active state of a voxel, packed into one register. This PR bumps the NanoVDB pin to that merge commit (9df9ec06) and adopts the fused call in both HDDA iterators, replacing the separate getDim + isActive pair that previously cost two tree walks per iteration.

This restores the methodology originally prototyped alongside #663, which had to be reverted before merge because the fused accessor only existed in a fork at the time.

Changes

HDDASegmentIterator::nextSegment

  • One getDimAndActive call per iteration under the default ActiveExact policy, which is byte-for-byte equivalent to the separate calls. ActiveExact is required here because the iterator reads active unconditionally, including at coarse-tile levels.
  • When the HDDA re-aligns to a different level, HDDA::update snaps mVoxel to the new grid, so the result is re-queried after the update to keep active consistent with the voxel the TimeSpan logic reads. The two-descent cost is thus only paid on (rare) level-change iterations.

HDDAValueIteratorImpl::nextVoxel

  • The realign-loop getDim queries and the post-convergence isActive fuse into one getDimAndActive per pass; each re-query runs at the voxel HDDA::update just snapped to, so on loop exit the result always describes the current voxel. The force-unrolled ≤3-pass structure is unchanged.
  • Policy is selected per instantiation: HDDALeafVoxelIterator (LeafOnly=true) gates on dim == 1 before consulting active, which is exactly the ActiveOnLeafOnly contract, so it keeps getDim's skip-flag fast path. HDDAActiveValueIterator (LeafOnly=false) must yield active coarse tiles, so it uses ActiveExact.

No callers change — ray_implicit_intersection, uniform_ray_samples, voxels_along_rays, and segments_along_rays consume the iterators through their public API.

Benchmarks

RTX PRO 6000 Blackwell, 512×512 pinhole-camera rays, fp32, median of 20 CUDA-event-timed iterations (top/bottom-2 trimmed), NanoVDB example grids. Output checksums are bit-identical to main across all workloads — this is a pure performance change.

Op Dataset main (ms) fused (ms) Speedup
voxels_along_rays dragon 4.902 2.911 1.68×
voxels_along_rays emu 8.343 5.031 1.66×
voxels_along_rays wdas_cloud 24.140 16.970 1.42×
voxels_along_rays crawler 18.920 13.939 1.36×
segments_along_rays crawler 7.544 5.795 1.30×
segments_along_rays emu 3.703 2.900 1.28×
segments_along_rays wdas_cloud 12.676 9.945 1.27×
segments_along_rays dragon 1.634 1.351 1.21×
uniform_ray_samples wdas_cloud 12.798 10.033 1.28×
uniform_ray_samples dragon 1.669 1.385 1.21×
ray_implicit_intersection emu 0.587 0.478 1.23×
ray_implicit_intersection crawler 1.901 1.630 1.17×
ray_implicit_intersection dragon 0.384 0.374 1.03×

ray_implicit_intersection gains the least because its leaf-only iterator already skips coarse tiles cheaply (and now additionally benefits from ActiveOnLeafOnly keeping the skip-flag fast path).

Test plan

🤖 Generated with Claude Code

OpenVDB PR 2220 (upstream merge 9df9ec06) adds the fused
ReadAccessor::getDimAndActive API: a single Root->Internal->Leaf descent
(or accessor-cache lookup) that returns both the HDDA step size and the
active state of a voxel, packed into one register. Bump the NanoVDB pin
to that commit and adopt the fused call in both HDDA iterators, restoring
the methodology first prototyped on the wip_raytrace branch (which had to
be reverted pre-merge because the API only existed in a fork at the time).

HDDASegmentIterator::nextSegment:

  - Replace the per-iteration getDim + isActive pair with one
    getDimAndActive call under the default ActiveExact policy, which is
    byte-for-byte equivalent to the separate calls. ActiveExact is
    required here because the iterator reads `active` unconditionally,
    including at coarse-tile levels.
  - When the HDDA re-aligns to a different level, HDDA::update snaps
    mVoxel to the new grid, so re-query after the update to keep
    `active` consistent with the voxel the TimeSpan logic reads. The
    two-descent cost is thus only paid on (rare) level-change iterations.

HDDAValueIteratorImpl::nextVoxel:

  - Fuse the realign-loop getDim queries and the post-convergence
    isActive into one getDimAndActive per pass; each re-query runs at the
    voxel HDDA::update just snapped to, so on loop exit the result always
    describes the current voxel. The force-unrolled <=3-pass structure is
    unchanged.
  - Select the policy per instantiation: HDDALeafVoxelIterator
    (LeafOnly=true) gates on dim == 1 before consulting `active`, which
    is exactly the ActiveOnLeafOnly contract, so it keeps getDim's
    skip-flag fast path. HDDAActiveValueIterator (LeafOnly=false) must
    yield active coarse tiles, so it uses ActiveExact.

No callers change: ray_implicit_intersection, uniform_ray_samples,
voxels_along_rays, and segments_along_rays consume the iterators through
their public API.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@swahtz
swahtz requested a review from a team as a code owner September 1, 2026 05:02
@swahtz swahtz added optimization Performance or memory optimization core library Core fVDB library. i.e. anything in the _Cpp module (C++) or fvdb python module labels Sep 1, 2026
@swahtz swahtz added this to the v0.6 milestone Sep 1, 2026

@harrism harrism 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.

Nice speedup!

@swahtz
swahtz merged commit 171fc63 into openvdb:main Sep 1, 2026
40 checks passed
@swahtz
swahtz deleted the hdda_fused_dim_active branch September 1, 2026 09:12
harrism pushed a commit to harrism/fvdb-core that referenced this pull request Sep 11, 2026
…coverage

Bump the NanoVDB pin to 31d1e596, the merge commit of openvdb #2319. This
carries the whole "introduce + deprecate" arc of openvdb #2232 that has
landed so far: #2288 (single-space GridHandle<cuda::Buffer<T,R>>), #2292
(cuda::copyTo), #2293 (reset() via destroy()), #2301 (DeviceBuffer
deprecated in favour of DualDeviceBuffer; single-space VBM accessors;
HandleStorage.h), and #2319 (per-device DistributedPointsToGrid
resources; UnifiedBuffer deprecated). The hasDeviceSingle/hasHostSingle
traits are detected with false defaults, so TorchDeviceBuffer's
dual-trait specialization is unaffected.

#2301 also changes what PadGrid.cuh, our local TopologyBuilder-based padding
op, can rely on, and this repo builds with -Werror=all-warnings:

- nanovdb::cuda::DeviceBuffer becomes a [[deprecated]] alias of the renamed
  DualDeviceBuffer. PadGrid's getHandle() default template argument spelled
  the old name; it now names DualDeviceBuffer (the same class). No fvdb caller
  uses the default -- they pass TorchDeviceBuffer explicitly.

- TopologyBuilder::mProcessedRoot is removed outright, replaced by a pinned
  host staging buffer plus device scratch behind allocateProcessedRoot() /
  uploadProcessedRoot() / deviceProcessedRoot(). PadGrid::padRoot() built the
  padded root into a DeviceBuffer it assigned to mProcessedRoot and then
  deviceUpload()ed; it now fills the builder's staging area and uploads on
  mStream, mirroring upstream DilateGrid::dilateRoot. The upload is now
  asynchronous (pinned source) and the device copy allocates through the
  builder's ResourceT (BuilderResource) instead of DeviceBuffer's pool. The
  kernels already consumed deviceProcessedRoot(), so they are unchanged.

On top of the bump, three allocator-coverage improvements:

- ReinitializeSdf: the VoxelBlockManager's firstLeafID/jumpMap buffers
  now allocate through BuilderResource (torch's active CUDA allocator)
  via a local TorchVbmBuffer adapter, instead of DeviceBuffer's separate
  pool. (The adapter is removed in the next commit: #2301 gives the VBM
  handle the single-space accessors and prototype-buffer allocation the
  adapter was working around.)

- SaveNanoVDB: the device staging buffers (the defensive host-grid
  upload and the per-batch (N+1)-element value buffer) become
  nanovdb::cuda::Buffer over BuilderResource, stream-ordered on the save
  stream. The indexToGrid output handle stays TorchDeviceBuffer for now.

- TorchDeviceBuffer: CUDA allocations can now be associated with an
  explicit stream (raw_alloc_with_stream), and create() forwards the
  stream nanovdb builders pass instead of discarding it. Previously the
  allocation was silently associated with the device's current torch
  stream, which is only correct when that coincides with the builder's
  stream.

Rebased onto main past openvdb#754 (pin 9df9ec06) and openvdb#757; the pin conflict was
resolved forward to 31d1e596 inside this commit so that it builds, since
the original e679862f is behind main's pin.

Originally verified by the author at pin e679862f: full rebuild (112
targets, sm_120, -Werror=all-warnings clean); tests/unit/test_sdf.py
10/10, test_io.py 622/622, test_basic_ops.py 276/276 (+1 skip) on an RTX
PRO 6000 Blackwell. Re-verification at 31d1e596 is recorded on the PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
(cherry picked from commit 2d0eb61)
Signed-off-by: Mark Harris <mharris@nvidia.com>
harrism pushed a commit to harrism/fvdb-core that referenced this pull request Sep 11, 2026
…coverage

Bump the NanoVDB pin to 31d1e596, the merge commit of openvdb #2319. This
carries the whole "introduce + deprecate" arc of openvdb #2232 that has
landed so far: #2288 (single-space GridHandle<cuda::Buffer<T,R>>), #2292
(cuda::copyTo), #2293 (reset() via destroy()), #2301 (DeviceBuffer
deprecated in favour of DualDeviceBuffer; single-space VBM accessors;
HandleStorage.h), and #2319 (per-device DistributedPointsToGrid
resources; UnifiedBuffer deprecated). The hasDeviceSingle/hasHostSingle
traits are detected with false defaults, so TorchDeviceBuffer's
dual-trait specialization is unaffected.

#2301 also changes what PadGrid.cuh, our local TopologyBuilder-based padding
op, can rely on, and this repo builds with -Werror=all-warnings:

- nanovdb::cuda::DeviceBuffer becomes a [[deprecated]] alias of the renamed
  DualDeviceBuffer. PadGrid's getHandle() default template argument spelled
  the old name; it now names DualDeviceBuffer (the same class). No fvdb caller
  uses the default -- they pass TorchDeviceBuffer explicitly.

- TopologyBuilder::mProcessedRoot is removed outright, replaced by a pinned
  host staging buffer plus device scratch behind allocateProcessedRoot() /
  uploadProcessedRoot() / deviceProcessedRoot(). PadGrid::padRoot() built the
  padded root into a DeviceBuffer it assigned to mProcessedRoot and then
  deviceUpload()ed; it now fills the builder's staging area and uploads on
  mStream, mirroring upstream DilateGrid::dilateRoot. The upload is now
  asynchronous (pinned source) and the device copy allocates through the
  builder's ResourceT (BuilderResource) instead of DeviceBuffer's pool. The
  kernels already consumed deviceProcessedRoot(), so they are unchanged.

On top of the bump, three allocator-coverage improvements:

- ReinitializeSdf: the VoxelBlockManager's firstLeafID/jumpMap buffers
  now allocate through BuilderResource (torch's active CUDA allocator)
  via a local TorchVbmBuffer adapter, instead of DeviceBuffer's separate
  pool. (The adapter is removed in the next commit: #2301 gives the VBM
  handle the single-space accessors and prototype-buffer allocation the
  adapter was working around.)

- SaveNanoVDB: the device staging buffers (the defensive host-grid
  upload and the per-batch (N+1)-element value buffer) become
  nanovdb::cuda::Buffer over BuilderResource, stream-ordered on the save
  stream. The indexToGrid output handle stays TorchDeviceBuffer for now.

- TorchDeviceBuffer: CUDA allocations can now be associated with an
  explicit stream (raw_alloc_with_stream), and create() forwards the
  stream nanovdb builders pass instead of discarding it. Previously the
  allocation was silently associated with the device's current torch
  stream, which is only correct when that coincides with the builder's
  stream.

Rebased onto main past openvdb#754 (pin 9df9ec06) and openvdb#757; the pin conflict was
resolved forward to 31d1e596 inside this commit so that it builds, since
the original e679862f is behind main's pin.

Originally verified by the author at pin e679862f: full rebuild (112
targets, sm_120, -Werror=all-warnings clean); tests/unit/test_sdf.py
10/10, test_io.py 622/622, test_basic_ops.py 276/276 (+1 skip) on an RTX
PRO 6000 Blackwell. Re-verification at 31d1e596 is recorded on the PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
(cherry picked from commit 2d0eb61)
Signed-off-by: Mark Harris <mharris@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core library Core fVDB library. i.e. anything in the _Cpp module (C++) or fvdb python module optimization Performance or memory optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants