Skip to content

NanoVDB: handle empty partitions in the distributed merge path search#2248

Open
harrism wants to merge 2 commits into
AcademySoftwareFoundation:masterfrom
harrism:nanovdb-mergepath-empty-partition
Open

NanoVDB: handle empty partitions in the distributed merge path search#2248
harrism wants to merge 2 commits into
AcademySoftwareFoundation:masterfrom
harrism:nanovdb-mergepath-empty-partition

Conversation

@harrism

@harrism harrism commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What

tools::cuda::DistributedPointsToGrid's merge path search (mergePath) mishandles an empty partition. Its bounds checks compare against keys1Count - 1 / keys2Count - 1 on size_t counts, so a zero count underflows to SIZE_MAX and the guards never fire; the binary search then reads the empty array (and out of bounds past either array) and returns wrong or garbage intervals — an inverted split in one direction, underflow garbage in the other.

An empty partition is reachable from radixSortAsync's ceil-split whenever a device receives no items — e.g. sorting a single element across two devices gives counts [1, 0].

The fix returns the trivial split before the search: with an empty side, every element up to the diagonal comes from the non-empty side.

Testing

New test TestNanoVDBMultiGPU.MergePathEmptyPartition — runs on a single GPU (mergePathKernel is a plain kernel over pointers), covering empty-right, empty-left, both-empty, and a non-empty control at the median diagonal. The adjacent memory is filled with controlled sentinels so the out-of-bounds search fails deterministically rather than by luck:

  • Without the fix: empty-right returns the inverted split (0, 2) instead of (2, 0); empty-left returns underflow garbage in both intervals.
  • With the fix: all cases pass, and compute-sanitizer --tool memcheck reports 0 errors.
  • Full nanovdb_test_mgpu suite: 8/8 pass (single GPU; RTX 6000 Ada, CUDA 12.6).

Found while investigating the (environmental, since closed) multi-GPU failures in #2245 — this defect is independent of that machine issue and affects any multi-GPU sort with an empty partition.

Non-CUDA builds

No impact on NANOVDB_USE_CUDA=OFF — the change is confined to a CUDA-only header and the multi-GPU test.

🤖 Generated with Claude Code

mergePath's bounds checks compare against (keys1Count - 1) and
(keys2Count - 1) on size_t counts, so a zero count underflows to
SIZE_MAX and the guards never fire; the binary search then reads the
empty array (and past either array) and produces wrong or garbage
intervals. An empty partition is reachable from radixSortAsync's
ceil-split whenever a device receives no items, e.g. sorting a single
element across two devices.

Return the trivial split before the search: with an empty side, every
element up to the diagonal comes from the non-empty side.

New single-GPU test MergePathEmptyPartition covers the empty-right,
empty-left, both-empty, and non-empty control cases; the empty cases
fail deterministically without the fix (inverted split / underflow
garbage) and run clean under compute-sanitizer with it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Mark Harris <mharris@nvidia.com>
@harrism harrism requested a review from kmuseth as a code owner July 7, 2026 01:19
@swahtz swahtz added the nanovdb label Jul 7, 2026
using key_type = typename ::cuda::std::iterator_traits<KeyIteratorIn>::value_type;

const size_t combinedIndex = intervalIndex * (keys1Count + keys2Count) / 2;

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.

Do we need to launch the kernel if either keys1Count or keys2Count is 0? I think this condition could be moved into the host code prior to kernel launch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — done in 3a8f760. The launch site now skips the kernel when either count is zero and computes the trivial split on the host (mergePathTrivial, kept as a small named function so the unit test can exercise it directly); mergePath reverts to the unguarded search with the non-empty precondition documented. The test's empty cases are now pure host calls, with the device search kept as the non-empty control. Verified: nanovdb_test_mgpu 8/8 (single GPU), compute-sanitizer clean.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indeed. Fixed in 3a8f760

An empty side needs no merge path search, so skip the kernel launch:
the trivial split (everything up to the diagonal comes from the
non-empty side) is computed on the host by mergePathTrivial at the
launch site. mergePath reverts to the unguarded search and documents
that both inputs must be non-empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants