Advance NanoVDB pin to openvdb #2319 and adopt single-space cuda::Buffer where it now fits (#770 steps 1-2) - #773
Conversation
17979f9 to
05f5037
Compare
…ndle Two places copy a NanoVDB GridData header out of a multi-grid batch into a single-grid buffer and construct a GridHandle around it without resetting the fields that describe the buffer the header now lives in: - SaveNanoVDB.cu indexToGridHost(): `*dstGrid->data() = *srcGrid->data()` carries the source's mGridIndex (its batch position), mGridCount (the batch size) and mGridSize (the index grid's size, not the typed grid's) into the destination. tools::cuda::indexToGrid, which this function is a host port of, resets index/count to 0/1 (IndexToGrid.cuh:167-168); the port omitted that and never set mGridSize. - LoadNanovdb.cpp copyIndexGridToHandle(): sets mGridCount = 1 and mGridSize, but leaves mGridIndex at the source's batch position. It also copies the grid only up to its first blind-metadata record, yet leaves mBlindMetadataCount/Offset claiming the record it dropped; those are cleared too, so the header describes the buffer it is in. Every one of these writes also invalidates the checksum the file carried, which was already left stale; it is now marked disabled, the convention the other header-fixup sites (MakeContiguous, ConcatenateGrids) follow. This has been harmless so far: GridHandle took the header at its word, and the callers either overwrote the fields later (patchGridWithBlindShape) or never read them. NanoVDB #2292/#2301 make GridHandle validate a host buffer's grid chain at construction -- every header must be valid, carry index i of count N, and have a size that fits the buffer. Once the pin advances past them, saving a batch of N > 1 grids to a standard NanoVDB type fails with "GridHandle was constructed with an invalid host buffer (grid 1 of N)" (the stale mGridSize points the chain walk into node data), and loading grid i > 0 of a tensor-grid file fails with "inconsistent grid index/count (grid 0 of 1)". 275 of the 622 cases in tests/unit/test_io.py. Set index 0 / count 1 / the destination's own size before constructing the handle in both places. This is correct at the current pin as well, so it lands ahead of the bump that enforces it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
…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>
openvdb #2301 (in the pin advanced by the previous commit) removes the reasons three device-only scratch/output buffers were still routed through dual-space types. Each now uses nanovdb::cuda::Buffer<T, BuilderResource> directly, so it allocates from torch's active CUDA allocator, stream-ordered on the stream that uses it, with no adapter in between. - ReinitializeSdf: delete the TorchVbmBuffer adapter. It existed because VoxelBlockManagerHandle's device accessors were gated on hasDeviceDual and buildVoxelBlockManager allocated through the legacy BufferT::create. #2301 adds hasDeviceSingle accessors to the handle and routes its allocation through createDeviceStorage with an optional prototype buffer, so VoxelBlockManagerHandle<cuda::Buffer<std::byte, BuilderResource>> works as-is: BuilderResource is default-constructible and stream-ordered, so the nullptr prototype yields Buffer(stream, bytes, noInit) on the reinit stream, and reset() releases through destroy(). This also drops the adapter's host-side data() returning nullptr and the const_cast in its deviceData(), neither of which had a legitimate caller. - BuildPrunedGrid: the per-leaf keep-mask scratch becomes cuda::Buffer<Mask<3>, BuilderResource> on the fill kernel's stream, with a typed data() replacing two reinterpret_casts. noInit is behaviour- preserving: TorchDeviceBuffer never zeroed the allocation either, and InjectPredicateToMaskFunctor writes every mask word (zero then setOnAtomic) before PruneGrid reads it. PruneGrid is now also constructed on that stream: it defaults to the legacy stream 0, so its reads of the mask (and its two internal synchronizations) were not ordered after the fill kernel when torch's current stream is a non-blocking one -- a pre-existing race, closed here since the comment above would otherwise be false. - SaveNanoVDB: the indexToGrid output handle becomes GridHandle<cuda::Buffer<std::byte, BuilderResource>>. IndexToGrid's getHandle allocates through createDeviceStorage at this pin, so the default-constructed pool buffer supplies the resource and the previous TorchDeviceBuffer(0, device) guide is no longer needed. The D2H staging copy reads buffer().data() instead of deviceData(). With this, no TorchDeviceBuffer is allocated in SaveNanoVDB; the remaining mentions read the grid batch's own handle and belong to the storage flip in openvdb#770. Grid storage itself (GridBatchData's GridHandle<TorchDeviceBuffer>) is unchanged here; that is the subject of openvdb#770 steps 3-5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
05f5037 to
d6e9300
Compare
Revised since the first push, after a Codex review of the full diff (which should have preceded opening this — it didn't; corrected now). Two of its confirmed findings were real and are folded in:
Two further pre-existing items from the review are not fixed here, deliberately, and are in the body under Review: stale checksums in exported standard-type files ( Re-verified at the new head: |
swahtz
left a comment
There was a problem hiding this comment.
Review of steps 1–2 against the pinned headers at 31d1e596 and the local c10 source. One regression that should be fixed before merge (inline on TorchDeviceBuffer.cpp), one comment that mis-states an upstream invariant, and cleanups. Plan-level items that belong to later steps are on #770 rather than here.
Two notes on lines outside the diff hunks:
BuildPrunedGrid.cu:103—nanovdb::cuda::mergeGridHandles(handles, &guide)still runs on legacy stream 0 while this PR is making the rest of the file stream-correct (the new comment on thePruneGridcall describes exactly this read-before-write class). Suggest passingstream.stream()here since the file is already open; the other eightmergeGridHandlessites and threevoxelsToGridsites are step 5's.SaveNanoVDB.cu:11,16—<nanovdb/cuda/DeviceBuffer.h>and<c10/cuda/CUDACachingAllocator.h>have no remaining uses in the file; the diff added<nanovdb/cuda/Buffer.h>next toDeviceBuffer.hrather than replacing it.
Not raised: the unreachable H2D fallback in fvdbToNanovdbGridWithValues (rewritten in step 4) and the raw_alloc ternary (class is deprecated in step 6).
🤖 Generated with Claude Code
CI is 41/41 on |
I also noted that flaky test. I have a fix for it in the #753 PR because I hit it there too. |
Review of openvdb#773 (swahtz) found that forwarding a nanovdb builder's stream into raw_alloc_with_stream keys the block to that stream in torch's native caching allocator. There the allocation stream is a partition key, not an ordering point: only later allocations on the same stream can reuse the block, and freeing does no synchronization. At pin 31d1e596 DistributedPointsToGrid creates its output storage on its DeviceMesh stream, which the PrivateUse1 dispatches of BuildGridFromIjk and BuildDenseGrid create per batch item and destroy with the mesh. A grid block keyed to it would never be reused (reserved memory grows one segment per build until an OOM release), expandable segments would synchronize on the destroyed handle when releasing it, and the cudaMallocAsync backend would cudaFreeAsync on it. The path is not reachable from fvdb-core today (nothing registers the PrivateUse1 device), but the storage resource planned in openvdb#770 step 3 would inherit the policy. Fix it in the resource, once. TorchResource::allocate_async always allocates on the current device's current torch stream; when the caller's write stream differs, it waits on an event recorded right after the allocation, so the caller's writes are ordered behind the block's previous tenant, whose work may still be queued on the torch stream. TorchDeviceBuffer's CUDA paths allocate and free through TorchResource instead of calling c10 directly, so it carries no allocation policy of its own. The stream parameter on both is now documented as the stream the caller writes on, not the allocation stream. Verified: -Werror clean, 47/47 gtests, 1605 passed / 1 skipped across nine pytest suites, clang-format clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
…ment Three items from swahtz's review of openvdb#773. - BuilderResource.h gains `template <class T> using BuilderBuffer = nanovdb::cuda::Buffer<T, BuilderResource>;` and the six spelled-out Buffer<..., BuilderResource> sites (SaveNanoVDB, ReinitializeSdf, BuildPrunedGrid) use it, so retargeting the resource retargets the ops' own staging and scratch buffers too. The header comment now describes the category of allocation the alias covers instead of listing files, which had already fallen out of date. - TorchDeviceBuffer no longer treats a null stream as "unspecified". Null is the legacy default stream, and TorchResource orders it after the allocation like any other write stream (no-op when torch's current stream is also the default). This removes the documented ambiguity between an omitted stream and explicit stream zero, and it makes the synchronous cudaMemcpy paths in to() correctly ordered behind the allocation even when a non-blocking torch stream is current. - SaveNanoVDB indexToGridHost: the comment now states what the code guarantees (one grid, header index/count/size match this buffer) and drops the claim that the CUDA indexToGrid does the same; at this pin it does not reset mGridSize. The empty-grid divergence between the two paths is tracked on openvdb#770 for step 4. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
swahtz
left a comment
There was a problem hiding this comment.
Nice, thanks for addressing my notes. Looks good to go.
Codex review of d4d4cbf found a race it introduced. TorchResource had been changed to allocate every block on torch's current stream and event-order the caller's stream after it. That is the right policy for storage but wrong for scratch: deallocate_async does not order the free, so the block must be keyed to the stream its work runs on. The pinned PointsToGrid destroys its per-tile counts immediately after enqueuing the scan that reads them, and the 13 builders that bind BuilderResource run on legacy stream 0. With the block keyed to a non-blocking torch stream instead, torch could reuse it (native) or cudaFreeAsync it (cudaMallocAsync backend) ahead of that scan. - TorchResource::allocate_async keys the block to the caller's stream again, as before d4d4cbf. The comment now says why that is load-bearing and that the stream must outlive the block. - TorchStorageResource (same header) carries the storage policy: allocate on the current torch stream through TorchResource, and if the caller's write stream differs make it wait on an event recorded after the allocation. If any event call fails the block is freed before the error is raised. Nothing orders the free: as for a tensor, a caller whose writer stream differs must synchronize it before releasing the storage. The builders fvdb hands TorchDeviceBuffer to (PointsToGrid, DistributedPointsToGrid, the topology builders, PadGrid) do so before returning their handle. This is the body openvdb#770's TorchDeviceResource should adopt. - TorchDeviceBuffer's CUDA paths use TorchStorageResource. to()'s CUDA copies move off the legacy default stream onto the writing device's current torch stream (the block's stream), are synchronized before returning and error-checked; a cudaMemcpy on stream 0 was neither ordered after the source's writers on a non-blocking torch stream nor guaranteed complete before the block could be reused (pre-existing). Device-to-device first synchronizes the source device's stream and uses cudaMemcpyPeerAsync, which is correct without peer access enabled between the two pools (a plain device-to-device memcpy is not, and c10 itself selects the peer copy for this case). The destination is owned by a scope guard until the copy has succeeded, so a failed copy no longer leaks it or leaves the buffer half-moved. The guard's destructor swallows a secondary CUDA error so a failing copy propagates instead of terminating the process. - BuilderResource.h no longer claims to cover every op-lifetime device allocation; ops that still take CUB scratch from c10 directly are outside it until they migrate (openvdb#770 step 5). Correction to d4d4cbf's message: BuildDenseGrid's PrivateUse1 dispatch builds one DeviceMesh for the first batch item and copies that grid for the rest; BuildGridFromIjk's builds one per item. The destroyed-stream rationale is unchanged. Reviewed with codex across five rounds before push; each round's findings are folded in above. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
|
Follow-up 6f605b9, after a codex review of d4d4cbf and 6e8420c that should have preceded pushing them. Its main finding was a race d4d4cbf introduced: keying scratch to torch's current stream is wrong, because
Also in the commit, all surfaced by the same review: Five codex rounds to clean. Re-verified at 6f605b9: |
|
[P3] Preserve zero-byte buffer transfers to CUDA — src/fvdb/TorchDeviceBuffer.cpp:177 The new allocation path changes the behavior of an empty C++ buffer: fvdb::TorchDeviceBuffer buf;
buf.to(torch::Device(torch::kCUDA, 0));Previously this succeeded: the native allocator returns a null pointer for a zero-byte allocation, and the zero-byte H2D Please handle zero-sized transfers without allocating, while preserving the requested destination device, and add a focused buffer-level regression test. This concerns zero-byte Validation: confirmed the native allocator's null result and the old zero-byte H2D copy's success with a local CUDA probe; the new exception follows from the allocation call chain at |
swahtz: after 6f605b9 an empty buffer could no longer be moved to CUDA. to() reached TorchStorageResource, whose null-pointer check treats the allocator's null result for a zero-byte request as failure, where the old raw_alloc(0) plus zero-byte cudaMemcpy succeeded. An empty buffer owns no allocation on either device, so to() now just records the new device for size 0, for every device combination. The size-0 return also precedes the same-device check, which used to reject an empty buffer moved to the device it was already on. New TorchDeviceBufferTest, written so each test fails deterministically without the behaviour it guards: the empty moves (CPU to CUDA and back, same device); a 1 MiB move to CUDA under a non-default current torch stream held open by a spin kernel, read back on the legacy stream, so an upload that was only enqueued would read stale bytes; and construction with a foreign write stream after giving the block a previous tenant still being filled on the current stream, so a writer not ordered after the allocation would have its memset overwritten. The last test skips when the allocator does not hand the same block back or the tenant has already finished. Verified by mutation: with both guarded behaviours removed the two tests fail 3/3; restored, all four pass 3/3. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
|
Confirmed, thank you: New |
Supersedes #752 (carrying its commit, with @swahtz as author). Steps 1–2 of #770, plus a latent-bug fix the new pin's validation exposed.
Summary
Advances the NanoVDB pin to
31d1e596(merge of openvdb #2319) — past the whole "introduce + deprecate" arc of openvdb #2232 that has landed so far — and uses the single-spacenanovdb::cuda::Bufferdirectly at the three device-only scratch/output sites where #2301 removed the reasons for an adapter or a dual-space type.Grid storage is unchanged.
GridBatchData'sGridHandle<TorchDeviceBuffer>stays as it is; that flip is #770 steps 3–5 and deliberately not in this PR.Commit 1 — normalize grid headers before constructing
GridHandles (new)A pre-existing bug, surfaced by the pin: NanoVDB #2292/#2301 make
GridHandlevalidate a host buffer's grid chain at construction — every header must be valid, carry index i of count N, and have a size that fits. Two fvdb sites copied a header out of a multi-grid batch into a single-grid buffer and built a handle around it without resetting those fields:SaveNanoVDB.cuindexToGridHost()—*dstGrid->data() = *srcGrid->data()kept the source'smGridIndex,mGridCountand (index-grid)mGridSize. The CUDAtools::cuda::indexToGridit ports resets index/count (IndexToGrid.cuh:167-168); the port didn't.LoadNanovdb.cppcopyIndexGridToHandle()— setmGridCount = 1but leftmGridIndexat the batch position.copyIndexGridToHandlealso copies the grid only up to its first blind-metadata record yet leftmBlindMetadataCount/Offsetclaiming it, and every one of these header writes left the file's checksum stale; both are now normalized too (checksum marked disabled, asMakeContiguous/ConcatenateGridsdo after their fixups).Harmless before (the fields were overwritten later or never read); at the new pin, saving N>1 grids to a standard type fails with "invalid host buffer (grid 1 of N)" and loading grid i>0 of a tensor-grid file fails with "inconsistent grid index/count (grid 0 of 1)" — 275 of 622
test_io.pycases. Nine lines, correct at the old pin too, so it leads the series and every commit passes.Commit 2 — pin +
-Werrorfallout (swahtz's #752, rebased)9df9ec06→31d1e596. Update NanoVDB for single-space GridHandle and widen Torch-pool coverage #752's original targete679862fis now behindmain(which moved to9df9ec06in Use fused NanoVDBReadAccessor::getDimAndActivein the HDDA iterators #754), so the conflict was resolved forward inside the commit to keep it building.PadGrid.cuh: #2301 makescuda::DeviceBuffera[[deprecated]]alias ofDualDeviceBufferand removesTopologyBuilder::mProcessedRootoutright. ThegetHandle()default template argument takes the non-deprecated spelling;padRoot()is ported toallocateProcessedRoot()/uploadProcessedRoot()mirroring upstreamDilateGrid::dilateRoot. Net effect: the root upload is now asynchronous (pinned staging) and its device copy allocates throughBuilderResource. A now-deadcudaGetDeviceis removed.cuda::Buffer<…, BuilderResource>,TorchDeviceBuffer::createforwarding the builder's stream toraw_alloc_with_stream, and the VBM adapter (removed in commit 2).Commit 3 — single-space buffers where NanoVDB now allows them
ReinitializeSdf.cuVBM metadataTorchVbmBufferadapter (−79 lines)VoxelBlockManagerHandle<cuda::Buffer<std::byte, BuilderResource>>directlyBuildPrunedGrid.cuper-leaf mask scratchTorchDeviceBuffer+ tworeinterpret_cast;PruneGridon legacy stream 0cuda::Buffer<Mask<3>, BuilderResource>, typeddata();PruneGridon the fill streamSaveNanoVDB.cuindexToGridoutputGridHandle<TorchDeviceBuffer>via aTorchDeviceBuffer(0, device)guideGridHandle<cuda::Buffer<std::byte, BuilderResource>>via a default-constructed pool bufferWhy each is safe now, verified against the pinned headers rather than assumed:
VoxelBlockManagerHandle's accessors were gated onhasDeviceDualandbuildVoxelBlockManagerallocated throughBufferT::create. #2301 addsBufferHasDeviceSingleaccessors and routes allocation throughcreateDeviceStorage(bytes, proto, …);BufferTraits<cuda::Buffer<T,R>>::hasDeviceSingleis!is_host_accessible_resource<R>, true forBuilderResource, which is default-constructible and stream-ordered — so thenullptrprototype yieldsBuffer(stream, bytes, noInit)on the reinit stream.reset()releases viadestroy().noInitfor the mask scratch is behaviour-preserving:TorchDeviceBuffernever zeroed the allocation, andInjectPredicateToMaskFunctorwrites every word (zero, thensetOnAtomic) beforePruneGridreads it.PruneGridtakes acudaStream_tdefaulting to 0 and fvdb never passed one, so its mask reads and its two internalcudaStreamSynchronizes were not ordered after the fill kernel whenever torch's current stream is a non-blocking one. Pre-existing; closed here because the new comment on that block would otherwise be false.IndexToGrid::getHandleallocates throughcreateDeviceStorage<BufferT>(size, &pool, …)at this pin, so a default-constructed pool supplies the resource and no device guide is needed.With this, no
TorchDeviceBufferis allocated inSaveNanoVDB.cu; the remaining mentions read the grid batch's own handle and belong to the storage flip.Not in scope
openvdb #2319's
UnifiedBufferdeprecation (fvdb never uses it); openvdb #2322 (open) which, after #757, touches onlyPadGrid.cuh— the one file this PR already reworks; everything under #770 steps 3–6.Verification
On an RTX PRO 6000 Blackwell (sm_120), torch 2.13.0 / CUDA 13.0 env built from
env/build_environment.yml+ thetest_environment.ymldelta:./build.sh install gtests—-Werror=all-warningsclean, zero diagnostics.ctest: 47/47 passed.pytest unit/test_sdf.py unit/test_io.py unit/test_basic_ops.py unit/test_basic_ops_single.py unit/test_prune_single_voxel.py unit/test_sliced_batch.py unit/test_inject.py unit/test_batched_topology_builder.py: 1270 passed, 1 skipped, 18 subtests passed.test_io.py— that is the regression test for the header fix.Each commit builds on its own; commit 1 is also correct at the old pin. Note for anyone reproducing:
./build.sh ctestcurrently picks up the CPMblosc-subbuild'sCMakeCache.txt(find … -print -quit) and reports no tests; runctestinbuild/<tag>/srcdirectly. Pre-existing, not touched here.clang-format18 /black24 check: clean.Review
Codex-reviewed (read-only, full diff + tree) before this revision. It confirmed the
PruneGridstream omission and the blind-metadata/checksum staleness above — all folded in. Two further pre-existing items it raised are not fixed here and are noted for follow-up:patchGridWithBlindShape(and the CUDA export path) rewrite checksum-coveredGridDatafields after the checksum is computed, so exported standard-type files carry a stale checksum; andTorchDeviceBuffer::createtreats a null stream as "unspecified", which makes the legacy default stream 0 indistinguishable from "use torch's current stream" — this is the design documented in the header comment (commit 2), flagged for @swahtz rather than changed.🤖 Generated with Claude Code