NanoVDB: SyncFromAsync, ResourceRef, and resource seams for MeshToGrid and TempPool (CUDA) - #2269
NanoVDB: SyncFromAsync, ResourceRef, and resource seams for MeshToGrid and TempPool (CUDA)#2269harrism wants to merge 18 commits into
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>
A stream-ordered resource must also model the synchronous concept, which means writing four methods where two would do. The synchronous pair is not a bare delegate -- memory from allocate must be usable on any stream when it returns, so the null-stream allocation has to be synchronized first -- and omitting that yields memory which satisfies the concept but is not actually synchronous. Put it in one place rather than leaving each author to rediscover it. The two resources in TestMemoryResource are the first users, and were already wrong in exactly that way: they provide only the async pair, so they never modelled is_async_resource. TempPool duck-typed and never checked, so nothing caught it. MeshToGrid was the last builder allocating from a hard-wired DeviceResource, through TempDevicePool. Give it a ResourceT parameter and thread it into both its TopologyBuilder and its pool. As with Data in the builder, BoxTrianglePair is hoisted 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>
…uffer Buffer holds its resource by value, matching cuda::buffer -- whose model this completes: in CCCL the ownership semantics are selected by what is placed in the by-value slot, an owning any_resource or a borrowing resource_ref. We adopted the slot without the borrowing type, so a container like TempPool, whose contract is a non-owning pointer to a possibly stateful resource, had no way to hold a Buffer without copying that resource and stranding its state. ResourceRef is the missing piece: a non-owning reference that is itself a resource, so copying the ref shares the underlying instance. Its async methods exist only when R models AsyncResource, so a ref over a synchronous resource does not misreport its tier, and two refs compare equal exactly when they reference the same resource. TempPool now keeps its bytes in a Buffer<std::byte, ResourceRef<R>>: same resource contract, same stream retention, same discard-on-growth reallocation, but the block is freed by ownership rather than by hand. The TempPool unit tests, which assert traffic against the caller's own resource instance, pass unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
|
@kmuseth ready for review after #2268 (stacked on it — the two commits after #2268's set are the payload). Adds SyncFromAsync, ResourceRef (the resource-ownership reconciliation recorded on #2232), MeshToGrid's resource seam, and TempPool onto cuda::Buffer. The TempPool unit tests pass unchanged, which is the regression guard for the borrow semantics. |
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>
Signed-off-by: Mark Harris <mharris@nvidia.com>
The scratch buffers held their resource by value, so each of the eight carried its own copy -- fine for the stateless default, wrong for a stateful resource, whose accounting would be split across copies while the caller's instance saw nothing. Borrow through ResourceRef instead, the same reconciliation TempPool uses. Assert the stream-ordered requirement directly in TopologyBuilder and MeshToGrid so a synchronous-only resource fails with a diagnostic that names the builder, not just the pool inside it. Note SyncFromAsync's synchronize cost on its allocate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
e3c8930 to
f619056
Compare
The byte scratch is reinterpreted as word-sized types, which is valid for every resource whose DEFAULT_ALIGNMENT is at least word alignment -- all CUDA allocation paths give 256 -- but nothing said so. Assert it, so a custom resource with a weaker guarantee fails at compile time instead of misaligning on the device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
|
Copilot cycles (fork mirror: harrism#2). Net changes: TopologyBuilder's scratch now borrows its resource through |
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>
Signed-off-by: Mark Harris <mharris@nvidia.com>
kmuseth
left a comment
There was a problem hiding this comment.
looks good but I have two questions
| /// avoids implicit synchronization in fundamental primitives. | ||
| template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0> | ||
| void setStream(cudaStream_t stream) { this->mStream = stream; } | ||
| void set_stream(cudaStream_t stream) { this->mStream = stream; } |
There was a problem hiding this comment.
These changes are from #2268, which you already commented on there.
| /// @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.
Same line as your #2268 thread — this PR is stacked on #2268 and its diff includes that payload. Answered there: the [[deprecated]] attribute is scheduled for step 3 when GridHandle/NodeManager migrate off clear() (it would currently warn from our own headers on every reset() call); a doxygen @deprecated tag now marks it at the documentation level (7d58a33).
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>
Signed-off-by: Mark Harris <mharris@nvidia.com>
…trofit Signed-off-by: Mark Harris <mharris@nvidia.com>
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>
Signed-off-by: Mark Harris <mharris@nvidia.com>
…buffer Signed-off-by: Mark Harris <mharris@nvidia.com> # Conflicts: # nanovdb/nanovdb/tools/cuda/TopologyBuilder.cuh # nanovdb/nanovdb/unittest/TestBuffer.cu
|
@kmuseth #2268 merged — thanks! I've merged latest master into this branch (no rebase, so the commits you looked at are unchanged) and the diff now shows only this PR's payload: SyncFromAsync, ResourceRef, TempPool/TopologyBuilder/MeshToGrid resource injection, and their tests. Both of your comment threads were on Buffer.h lines that belonged to #2268's diff; that code merged there with the doxygen |
Follow-up to #2231 and #2251, part of #2232. This is B2 of step 2's retrofit.
SyncFromAsync<Derived>A stream-ordered resource must also model the synchronous concept (
is_async_resource<R>impliesis_resource<R>, matchingcuda::mr's refinement), so a custom resource has to write four methods where two would do. This CRTP base supplies the synchronous pair in terms of the stream-ordered one:The synchronous pair is deliberately not a bare delegate. Memory returned by
allocatemust be usable immediately on any stream, so the null-stream allocation is synchronized before it is returned — which is whatDeviceResource::allocatealready did, in a place nobody else could reuse. Omitting that synchronization produces memory that satisfies the concept but is not actually synchronous: a race, not a compile error.deallocatedoes not synchronize, because the synchronous concept's contract is that the memory is already quiescent.The two test resources in
TestMemoryResource.cuare the first users, and were already wrong in exactly this way — they provide only the async pair, so they never modelledis_async_resource. Nothing caught it becauseTempPoolduck-types rather than asserting. They now derive from the mixin and conform.MeshToGridresource seamMeshToGridwas the last builder allocating from a hard-wiredDeviceResource, via theTempDevicePoolalias. It now takes aResourceTparameter (defaulted, so existing callers are unaffected) and threads it into both itsTopologyBuilderand its pool.As with
Datain #2268,BoxTrianglePairis hoisted out of the class asMeshToGridBoxTrianglePair, with an in-class alias. It carries no dependence on the resource, and leaving it nested would give everyResourceTinstantiation its own incompatible type — which the device functors, templated onBuildTalone, could not name.ResourceRef, and theTempPoolconversion it unblocksThe first attempt at converting
TempPoolfailed its own unit tests, and the failure exposed a real gap rather than a bug in the conversion:TempPoolholdsResource*— non-owning, documented "must outlive this pool" — andTestMemoryResource's resources carry their counters inline, so they are stateful and lossy to copy.cuda::BufferholdsRby value, so a pool holding aBufferhad to copy the resource, and the pool then recorded traffic into its copy while the tests asserted on the original.Checking the model we borrowed showed we had adopted half of a two-part design.
cuda::buffergenuinely owns its resource — its constructor says so in astatic_assert: "Buffer owns a copy of the memory resource…" — but in CCCL the ownership semantics are selected by what is placed in the by-value slot: an owningany_resource, a borrowingresource_ref, or a refcountedshared_resource. We shipped the slot without the borrowing type, so a non-owning container had no legitimate way to express itself.nanovdb::cuda::ResourceRef<R>is the missing piece — a non-owning reference that is itself a resource:Bufferover a ref borrows;Rmodels AsyncResource (enable_if-gated, the same pattern asBuffer's stream API), so a ref over a synchronous resource does not misreport its tier — covered bystatic_asserts overResourceRef<PinnedResource>;equality_comparablesemantics CCCL's concepts require ("memory allocated byxmay be deallocated byy");cuda::mr::resource_ref, and the same shape asstd::pmr::polymorphic_allocatorovermemory_resource*.TempPoolnow keeps its bytes in aBuffer<std::byte, ResourceRef<Resource>>: sameResource*contract, same stream retention, same discard-on-growth reallocation (viadestroy(stream)+ move-assignment — scratch is never prefix-copied), but the block is freed by ownership rather than by hand. TheTempPoolunit tests — the ones the by-value attempt failed — pass unchanged, which is the regression guard for the borrow semantics.The ownership rule of record is written up on #2232:
Bufferstays by-value; a concrete resource in the slot is owned, aResourceRefborrows; a refcountedSharedResourceanalog is deferred until the Python-bindings need materializes.Testing
New tests in
TestBuffer.cucover the mixin (supplies a working synchronous pair; aBufferover a mixin-based resource round-trips; a mixin user models both concepts) andResourceRef(traffic reaches the original stateful resource, not a copy; tier gating viaPinnedResource; equality is identity). A pendingchanges note covers the builder seams, the mixin, and the ref.Verified locally on an RTX 6000 Ada, CUDA 12.6:
nanovdb_cuda_buffer_unit_test— 27/27 (two new: a stateful resource observes all traffic through a by-valueBufferover a ref; ref equality is identity)nanovdb_cuda_memory_resource_unit_test— 9/9, including the twoTempPooltests the by-value conversion failed, unchangednanovdb_test_cuda— 52/53; the failure isUnifiedBuffer_IO, a missing test-data file (data/3_spheres.nvdb) in an unrelated component, which fails identically on an unmodified baseline.Per #2264 the CUDA tests are excluded from CI, so CI will build but not run these.