[copilot mirror] B3: PointsToGrid onto cuda::Buffer - #3
Conversation
The bisection search over voxel size was written as a backward goto, which made the lifetimes of the buffers it retries over non-lexical. Rewrite it as while(true) with continue on retry and break on convergence; the six hand-written frees before the jump are unchanged. The change is easiest to review with whitespace ignored, since the loop body re-indents: git diff -w shows 27 changed lines. d_keys and d_node_count carry results past the loop, so their declarations move above it, as does the copy event, which was created inside the retried region on every iteration but destroyed only once at the end -- each retry leaked the previous handle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
Every device array PointsToGrid allocates is now owned by a Buffer<T, ResourceRef<ResourceT>> borrowing the injected resource -- members where the pipeline frees them in a later member function than the one that allocated them (countNodes allocates; processUpperNodes, processLeafNodes, processPoints and processBBox release), locals where the lifetime is contained. The raw pointers survive only as views: the device-visible fields inside mData, and the working pointers the cub and kernel calls take. Every owner event -- assignment, swap, destroy -- immediately refreshes its view, and released views are nulled so a stale use faults instead of reading a freed block. The index ping-pong becomes a swap of owners across the member/local boundary, replacing the bare pointer swap whose safety depended on nothing reading the device copy of d_indx between the two uploads. The density-search retry keeps its free-before-reallocate order via explicit destroy calls, so peak device memory is unchanged. The hand-matched byte sizes at every free site disappear, and the arrays released one line before scope exit now just leave scope. One behavior change worth naming: the too-many-points-per-leaf throw previously leaked the reduction scratch; ownership now releases it during unwind. Verified against the previous commit with a counting resource over three shapes (bulk segmented-sort branch, serial per-tile branch, and the bisection retry engaged): allocation count, free count, and total bytes are identical. Full CUDA and memory-resource suites unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
This PR migrates NanoVDB’s CUDA PointsToGrid builder scratch/intermediate device allocations to nanovdb::cuda::Buffer (backed by an injectable memory resource), reducing manual allocate/free management and aligning with other GPU builders.
Changes:
- Updated
PointsToGridto own device scratch + intermediate arrays viacuda::Buffermembers and refresh raw-pointer views inmData. - Reworked the
countNodes“dx search” loop to reuse a single CUDA event and to free buffers viaBuffer::destroy()instead of pairedallocate_async/deallocate_async. - Updated pending change notes to document the PointsToGrid migration.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| pendingchanges/nanovdb.txt | Documents the PointsToGrid scratch/intermediate allocation change to cuda::Buffer. |
| nanovdb/nanovdb/tools/cuda/PointsToGrid.cuh | Replaces raw async allocations with cuda::Buffer ownership and updates lifecycle management across the pipeline. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| cudaEvent_t copyEvent; | ||
| cudaCheck(cudaEventCreate(©Event)); |
There was a problem hiding this comment.
Fixed in 4957582 — the event is now owned by a scope guard, so the too-many-points-per-leaf throw releases it during unwinding, consistent with the buffer ownership this PR introduces.
Signed-off-by: Mark Harris <mharris@nvidia.com>
The too-many-points-per-leaf throw unwinds past the event's manual destroy, leaking the handle. Own it with a small guard so unwinding releases it, consistent with the buffer ownership in this function. Also assert the stream-ordered resource requirement on the class, so a synchronous-only resource fails naming PointsToGrid rather than the pool inside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
nanovdb/nanovdb/tools/cuda/PointsToGrid.cuh:653
- Calling
cudaCheck(...)in a destructor is risky ifcudaCheckcan throw (it can lead tostd::terminateduring stack unwinding). Prefer making the destructor non-throwing: callcudaEventDestroy(event)directly (optionally ignoring errors), or adjustcudaCheckusage here to guarantee noexcept behavior.
struct EventGuard {
cudaEvent_t event;
EventGuard() { cudaCheck(cudaEventCreate(&event)); }
~EventGuard() { cudaCheck(cudaEventDestroy(event)); }
EventGuard(const EventGuard&) = delete;
EventGuard& operator=(const EventGuard&) = delete;
} eventGuard;
nanovdb/nanovdb/tools/cuda/PointsToGrid.cuh:722
- This manual
new[]/delete[]is not exception-safe if any of the CUDA/CUB checks throw between allocation anddelete[]. Use an RAII container (e.g.,std::vector<uint32_t>orstd::unique_ptr<uint32_t[]>) so host memory is released automatically on early exits.
uint32_t *points_per_tile = new uint32_t[mData.nodeCount[2]];
cudaCheck(cudaMemcpyAsync(points_per_tile, d_points_per_tile, mData.nodeCount[2]*sizeof(uint32_t), cudaMemcpyDeviceToHost, mStream));
pointsPerTileScratch.destroy(); d_points_per_tile = nullptr;
for (uint32_t id = 0, offset = 0; id < mData.nodeCount[2]; ++id) {
const uint32_t count = points_per_tile[id];
util::cuda::offsetLambdaKernel<<<numBlocks(count), mNumThreads, 0, mStream>>>(count, offset, VoxelKeyFunctor<BuildT, PtrT>(), mDeviceData, points, id, d_keys, d_indx);
cudaCheckError();
CALL_CUBS(DeviceRadixSort::SortPairs, d_keys + offset, mData.d_keys + offset, d_indx + offset, mData.d_indx + offset, count, 0, 36);
offset += count;
}
delete [] points_per_tile;
nanovdb/nanovdb/tools/cuda/PointsToGrid.cuh:408
ref()does not modify object state and can be markedconst(and potentiallynoexcept) to improve API correctness and allow calling it from const contexts.
nanovdb::cuda::ResourceRef<ResourceT> ref() { return nanovdb::cuda::ResourceRef<ResourceT>(*mResource); }
| util::cuda::lambdaKernel<<<numBlocks(mData.nodeCount[1]), mNumThreads, 0, mStream>>>(mData.nodeCount[1], PropagateLowerBBoxFunctor<BuildT>(), mDeviceData); | ||
| mResource->deallocate_async(mData.d_lower_keys, mData.nodeCount[1]*sizeof(uint64_t), ResourceT::DEFAULT_ALIGNMENT, mStream); | ||
| mLowerKeysBuf.destroy(); mData.d_lower_keys = nullptr; | ||
| cudaCheckError() |
There was a problem hiding this comment.
Fixed in 2e1e237 — for consistency, not correctness: it compiled because cudaCheckError expands to a braced block, so no semicolon is required, but every other use in the file spells it as a statement. Pre-existing, incidentally (visible at line 1328 of the base).
Signed-off-by: Mark Harris <mharris@nvidia.com>
Legal without one -- the macro expands to a braced block -- but every other use in the file spells it as a statement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
|
Copilot review cycles converged (final cycle: no new findings). All accepted changes are on the shared branch and reflected in the upstream PR; threads here document the dismissals. Branch retained. |
Copilot-review mirror of AcademySoftwareFoundation#2270, based on the B2 branch so only B3's payload shows.