NanoVDB: inject a resource into TopologyBuilder scratch, and align cuda::Buffer with cuda::buffer (CUDA) - #2268
Conversation
Name the free operation destroy, as cuda::buffer does, and add the stream-taking overload it also provides. clear stays but only as a transitional delegate, marked as such: it exists because GridHandle::reset still calls it, and goes away with the legacy dual buffers that cuda::Buffer replaces. Rename setStream to set_stream. Member names cannot be aliased, so matching the standard spelling is the whole reason the resource concept kept allocate_async; the same argument applies here. Note in passing that ours deliberately does not synchronize, matching cuda::buffer's set_stream_unsynchronized rather than its set_stream, whose own documentation and implementation disagree (NVIDIA/cccl#10649). Add swap. The generic std::swap already does the right thing through the move operations, but both std::vector and rmm::device_buffer provide one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
Eleven of the builder's buffers are device-only -- nothing reads them on the host -- yet they were cuda::DeviceBuffer, whose host pointer and per-device array they never use. Move them to the single-space cuda::Buffer and give the builder a resource parameter, so its scratch is injectable like PointsToGrid's already is, and freed by scope rather than by hand. mProcessedRoot and mData are read on the host and stay dual. This is not a speedup: DeviceBuffer::init allocates host or device memory but never both, and these buffers always passed a real device id, so no cudaMallocHost was on this path to begin with. Measured dilate on 1k/20k/ 200k points, and the difference is within run-to-run noise. Hoist the Data struct out of the class. It does not depend on the resource, and leaving it nested would give every ResourceT its own incompatible type for the device functors to name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
158cb38 to
a5ad1fc
Compare
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
|
@kmuseth ready for review — B1 of the #2232 retrofit: TopologyBuilder allocates scratch through an injected resource, plus the cuda::Buffer member-name alignment with cuda::buffer (destroy/set_stream/swap; details and the CCCL comparison are on #2232). All CUDA suites pass on a local GPU — numbers in the description; per #2264 CI builds but does not run them. #2269 stacks on this, so landing order matters. |
Each release site sits in a function that receives the stream, so pass it to destroy rather than relying on the retained stream matching -- they are the same on every current path, but the explicit form does not depend on that staying true. Also drop the cudaGetDevice calls whose result the Buffer conversion left unused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
|
Ran Copilot review cycles on this PR via a fork mirror (requesting Copilot upstream requires write access). Net changes here: scratch is now released with |
cuda::buffer's set_stream is deliberately non-synchronizing -- its documented synchronization is a stale note left behind when the synchronization was removed -- so our non-synchronizing set_stream matches it in both name and contract, and the comment claiming a divergence was wrong. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
kmuseth
left a comment
There was a problem hiding this comment.
added two requests for changes but otherwise it looks good
| /// @note Transitional, and not the name to use: it exists only because | ||
| /// GridHandle::reset still calls clear() on its buffer. It goes away | ||
| /// when the legacy dual buffers do and GridHandle moves to destroy(). | ||
| void clear() { this->destroy(); } |
There was a problem hiding this comment.
shouldn't this have a deprecation warning?
There was a problem hiding this comment.
Deprecation is part of step 3 of the plan in #2232, and this PR is part of Step 2. Adding [[deprecated]] to this now would cause warnings on internal calls of this function that users have no way to act on. And if they compile with -Werror their TU will fail to compile.
In step 3 we will be removing all internal calls to methods that will be removed, and then deprecate them.
There was a problem hiding this comment.
I added Doxygen @deprecated line to the docs to cover this for now.
| { | ||
| nanovdb::cuda::Buffer<float, StreamRecordingResource> buf(a, StreamRecordingResource{&log}, 32, nanovdb::cuda::noInit); | ||
| buf.setStream(b); // member update only, no synchronization | ||
| buf.set_stream(b); // member update only, no synchronization |
There was a problem hiding this comment.
This is another one of these camel-case issues. Why the need for abandoning the camel-case which is used elsewhere in OpenVDB? I understand that CUDA itself uses the other case, but I don't understand why this means we need to adopt it as well. Even worse, currently we use both semantics, which is inconsistent and confusing.
There was a problem hiding this comment.
This is as we discussed before, and documented in #2232:
Naming: types are CamelCase per OpenVDB style (
cuda::Buffer,cuda::BufferView) — type names are aliasable, so the opt-inusing Buffer = ::cuda::buffer<T>at CUDA ≥ 13.2 is preserved. Member names keep the standard spelling (data,size,size_bytes,allocate_async) because member matching is structural and cannot be aliased.
This is analogous to using std::vector rather than building your own vector class.
If we went with setStream then the above using alias will not work in the future. My goal, since long before I started working on VDB, is to maximize compatibility and shareability of resource management across GPU libraries in C++. CCCL is pushing this goal forward by standardizing memory resource and storage interfaces. I would like NanoVDB to be as compatible with that as possible. By matching the exact interface of CCCL types, they become drop-in compatible. So in the future (once our minimum CUDA version is high enough), we can eliminate this code (all buffer types) from our library and just rely on what comes with CUDA.
If you disagree with this change in favor of that goal, let me know.
Documentation-level only: the [[deprecated]] attribute would warn from GridHandle::reset and NodeManager::reset, template members in our own headers that must keep calling clear() until every buffer type provides destroy() -- an unactionable diagnostic for callers of reset(), and a build break under -Werror. The attribute lands when those callers migrate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
…trofit Signed-off-by: Mark Harris <mharris@nvidia.com>
The trait checks detect allocate/deallocate through unevaluated contexts, which never odr-use them, so nvcc warned that every synchronous pair and stub member was declared but never referenced (AcademySoftwareFoundation#177-D). The attribute route is closed -- nvcc's front end ignores [[maybe_unused]] for this diagnostic -- so reference them the honest way: a test that verifies the synchronous halves of the counting and stream-recording doubles behave like their stream-ordered halves, and one that pins the trait probes' stub behavior. The TU now builds with no warnings at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
|
Fixed the nvcc Note on provenance: these warnings are not introduced by this PR — they shipped with #2251 and current master produces the same twelve (verified). Building main shows them today; merging this PR clears them there. |
Follow-up to #2231 and #2251, part of #2232 (NanoVDB injectable CUDA memory resources). This is B1 of step 2's retrofit; see the roadmap for the B1/B2/B3 split.
Two related changes: the
TopologyBuilderscratch retrofit, and thecuda::Buffernaming alignment that the retrofit surfaced.1. TopologyBuilder scratch from an injected resource
tools::cuda::TopologyBuildergains aResourceTtemplate parameter, and its device-only scratch moves from the dualcuda::DeviceBufferto the single-spacecuda::Bufferadded in #2251. This is an injectability and ownership change, not a performance one — see below.An audit of host (
.data()) vs device (.deviceData()) use across the builder's ten buffers:Counts are call sites of
.data()(host pointer) and.deviceData()(device pointer) on each member:mUpperMasksmLowerMasksmUpperOffsetsmLowerOffsetsmLeafOffsetsmVoxelOffsetsmLowerParentsmLeafParentsmProcessedRootmDataThe eight with zero host uses are converted (plus three function-local buffers, eleven
DeviceBuffer::createcalls in total).mProcessedRootandmDataare genuinely dual-space and are left alone; they are step 3's problem, whenDeviceBufferis re-implemented as a composition of twocuda::Buffers.This is not a performance change
An earlier revision of this description claimed the conversion removes a synchronous
cudaMallocHostper buffer. That was wrong, and I have measured it.DeviceBuffer::initallocates host memory or device memory, never both:TopologyBuilderalways passed a real device id fromcudaGetDevice(), so every one of these buffers took theelsebranch. There was no host allocation on this path to remove.Dilate benchmark, 50 iterations per run, three runs each, RTX 6000 Ada / CUDA 12.6:
Within run-to-run noise at every size, as expected once the above is understood.
What it is worth
ResourceTinstead of a hard-wiredDeviceResource. That is the actual step 2 goal, andTopologyBuilderwas the last builder without the seam.clear(stream)calls.new void*[deviceCount]array it never used.The builder's
TempPoolalso becomesTempPool<ResourceT>, so cub scratch honors the injected resource too.Why
Datamoved.TopologyBuilder::Datais hoisted to namespace scope asTopologyBuilderData<BuildT>, with an in-class alias so existing uses read unchanged. It carries no dependence on the resource, and leaving it nested would give everyResourceTinstantiation its own incompatibleDatatype — which the device functors, templated onBuildTalone, could not name.Compatibility.
ResourceTdefaults tonanovdb::cuda::DeviceResource, matching howPointsToGridtook its resource parameter in #2231, so all six existing consumers —DilateGrid,MergeGrids,MeshToGrid,PruneGrid,RefineGrid,CoarsenGrid— compile unchanged. Three of them reach intomBuilder.mUpperMasks/mLowerMasksdirectly and needed.deviceData()->.data().DeviceBuffer::clear(stream)takes a stream wherecuda::Bufferfrees on its retained stream. These are the same stream on every path: the builder is constructed with the operator's stream and every subsequent call uses that same stream, so the translation preserves ordering exactly.2. cuda::Buffer naming alignment
Translating those
clear(stream)calls surfaced a gap, so this PR also closes it. A member-by-member comparison againstcuda::bufferis written up on #2232; the actionable parts:destroy()/destroy(stream)—cuda::buffernames the free operationdestroyand provides both a retained-stream and an explicit-stream overload. We had only the no-argument form, under a different name, which is why theclear(stream)translation above needed hand-verification.clear()stays as a transitional delegate todestroy(), and is marked as such in the header. It exists only becauseGridHandle::reset()still callsclear()on its buffer; it goes away in step 3's removal phase, when the legacy dual buffers are deleted andGridHandlemoves todestroy().cuda::Bufferis the type those buffers are being replaced by, so its interface should be the one we want to keep, not an accretion of theirs.setStream->set_stream— member names cannot be aliased, which is the stated reason the resource concept keptallocate_asyncin snake_case against house style; the same argument applies here.swap— the genericstd::swapalready behaves correctly through the move operations (verified: no allocation, no free, pointers and sizes exchanged), butstd::vectorandrmm::device_bufferboth provide a dedicated one, and the B3 retrofit leans on swapping owners as its central idiom.Note on
set_streamsemantics: ours deliberately does not synchronize — and that matchescuda::buffer::set_streamin both name and contract. NVIDIA/cccl#5697 removed the synchronization deliberately ("avoid implicit synchronization in fundamental primitives") but left the earlier "always synchronizes"@notein place; that stale doc is what NVIDIA/cccl#10649 reports. No divergence, no revisit needed.Testing
Three new tests in
TestBuffer.cucover the added API:destroy()frees and is idempotent;destroy(stream)frees on the given stream rather than the retained one and retains it afterwards;swapexchanges pointers and sizes with no allocation or free. TheTopologyBuilderconversion itself is behavior-preserving and is already covered by the existing suite through all six consumers.Verified locally on an RTX 6000 Ada, CUDA 12.6:
nanovdb_cuda_buffer_unit_test— 23/23 pass (20 existing + 3 new)nanovdb_test_cuda— 52/53 pass. The single failure,UnifiedBuffer_IO, is a missing test-data file (data/3_spheres.nvdb) in an unrelated component; the unmodified baseline fails identically, so it is pre-existing and environmental.Per #2264 the CUDA tests are excluded from CI (
ctest -E ".*cuda.*|.*mgpu.*"), so CI will build this but not run it — the numbers above are from a local GPU run.Follow-ups
TempPool's own bytes ontoBuffer, and aSyncFromAsyncmixin so a custom resource is two methods rather than four.PointsToGrid's 48 allocation sites, which need an ownership design first (device-visible raw pointers in an uploaded struct, ownership moved bystd::swap, agotoretry loop).DistributedPointsToGridstays deferred; NanoVDB: handle empty partitions in the distributed merge path search #2248 should land before any work there.