Skip to content

Commit 2380d21

Browse files
harrismclaude
andcommitted
NanoVDB: support single-space device buffers in GridHandle (CUDA)
GridHandle can now own a nanovdb::cuda::Buffer<std::byte, R>: a new hasDeviceSingle buffer trait selects a constructor (implemented in cuda/GridHandle.cuh) that parses the grid metadata through the device, with every operation -- the validated header walk, metadata scratch allocation through the buffer's resource, the copy kernel and the readback -- ordered on the buffer's retained stream. deviceData()/deviceGrid() work on such handles. The host accessors (data, grid, gridData, gridMetaData) are SFINAE-removed for them, since the handle owns no host-readable bytes; the read/write I/O members and splitGrids/mergeGrids stay addressable (the python bindings take write's address via overload_cast) and fail instead with an explanatory static_assert when instantiated for a single-space handle. The device parse validates the whole grid chain (per-header bounds checks against the allocation) before launching the metadata kernel, so truncated buffers and forged mGridCount/mGridSize headers are rejected with an exception instead of an out-of-bounds device read; the pre-existing dual-space parse had the same hole and now shares the validation helper. Its metadata scratch uses MallocResource so that long-standing path keeps working on devices without memory-pool support, and the dirty-flag scratch in splitGridHandles and mergeGridHandles is a resource-aware buffer as well -- together replacing the file's six raw cudaMalloc/mallocAsync sites, one of which was missing its cudaCheck. copy() dispatches on the traits: host buffers keep the memcpy path, single-space buffers deep-copy device-to-device through their own resource on the retained stream and reuse the host-resident metadata instead of re-parsing. Buffer gains ElementType/ResourceType typedefs, a resource() accessor, and a no-argument copy() for stream-ordered resources that orders the copy on the retained stream. Host accessibility is a property of the resource, not the element type: the new is_host_accessible_resource trait detects a HOST_ACCESSIBLE marker (declared by PinnedResource and forwarded by ResourceRef and AsyncFromSync). A cuda::Buffer over a host-accessible resource is rejected at handle scope with a named error for now -- GridHandle's host paths require an allocation interface cuda::Buffer does not yet provide -- and non-byte element types fail loudly in the single-space constructor. Both hasDeviceSingle and hasHostSingle are detected rather than required, so pre-existing BufferTraits specializations in and out of tree compile unchanged. Part of #2232 (step 3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Mark Harris <mharris@nvidia.com>
1 parent 7946f17 commit 2380d21

8 files changed

Lines changed: 533 additions & 40 deletions

File tree

nanovdb/nanovdb/GridHandle.h

Lines changed: 150 additions & 23 deletions
Large diffs are not rendered by default.

nanovdb/nanovdb/HostBuffer.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ namespace nanovdb {
9898
template<typename BufferT>
9999
struct BufferTraits
100100
{
101-
static constexpr bool hasDeviceDual = false;
101+
static constexpr bool hasDeviceDual = false;
102+
static constexpr bool hasDeviceSingle = false;
102103
};
103104

104105
// ----------------------------> HostBuffer <--------------------------------------

nanovdb/nanovdb/cuda/Buffer.h

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ class Buffer : private detail::StreamHolder<is_async_resource<R>::value>
6262

6363
static constexpr bool IsAsync = is_async_resource<R>::value;
6464

65+
public:
66+
/// @brief Element and resource types, for generic code that rebinds one
67+
/// or constructs sibling buffers over the same resource.
68+
using ElementType = T;
69+
using ResourceType = R;
70+
71+
private:
6572
R mResource;
6673
T* mData = nullptr;
6774
size_t mSize = 0; // element count
@@ -156,6 +163,11 @@ class Buffer : private detail::StreamHolder<is_async_resource<R>::value>
156163
return out;
157164
}
158165

166+
/// @brief Returns a deep copy of this buffer ordered on the retained
167+
/// stream, i.e. copy(this->stream()).
168+
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
169+
Buffer copy() const { return this->copy(this->stream()); }
170+
159171
/// @brief Returns a deep copy of this buffer, allocated from a copy of the
160172
/// synchronous resource.
161173
template<typename S = R, std::enable_if_t<!is_async_resource<S>::value && is_resource<S>::value, int> = 0>
@@ -255,6 +267,13 @@ class Buffer : private detail::StreamHolder<is_async_resource<R>::value>
255267
T* data() { return mData; }
256268
const T* data() const { return mData; }
257269

270+
/// @brief Returns a copy of the resource; for a ResourceRef this refers
271+
/// to the same underlying instance.
272+
/// @note Requires R to be copy-constructible (the cuda::mr convention:
273+
/// resources are cheap handles). A resource that owns its pool by
274+
/// value hands the caller an independent copy of that pool.
275+
R resource() const { return mResource; }
276+
258277
/// @brief Returns the number of elements.
259278
size_t size() const { return mSize; }
260279

@@ -402,6 +421,36 @@ class BufferView
402421

403422
} // namespace cuda
404423

424+
// Primary template defined in HostBuffer.h; declared here so this header
425+
// stays self-contained without pulling in the host-buffer machinery.
426+
template<typename BufferT>
427+
struct BufferTraits;
428+
429+
/// @brief GridHandle support for the single-space cuda::Buffer: the buffer
430+
/// owns exactly one allocation, resident on the device, so the handle
431+
/// parses metadata through a device read and exposes only the device
432+
/// accessors. Requires byte-addressed storage.
433+
/// @note A buffer whose trait sets hasDeviceSingle must provide the interface
434+
/// the single-space GridHandle constructor consumes: ElementType and
435+
/// ResourceType typedefs, data(), size_bytes(), resource(), stream()
436+
/// (stream-ordered resources), and the (stream, resource, count, noInit)
437+
/// constructor shape.
438+
template<typename T, typename R>
439+
struct BufferTraits<cuda::Buffer<T, R>>
440+
{
441+
static constexpr bool hasDeviceDual = false;
442+
// Device-resident storage; the byte-addressed requirement is enforced by
443+
// the single-space GridHandle constructor, so trait queries stay
444+
// answerable for any element type.
445+
static constexpr bool hasDeviceSingle = !cuda::is_host_accessible_resource<R>::value;
446+
// A buffer over a host-accessible resource (e.g. PinnedResource) is
447+
// host-readable, but GridHandle's host paths also require the create()
448+
// static interface and byte-count size semantics that cuda::Buffer does
449+
// not provide -- GridHandle rejects such buffers with a named error until
450+
// that adaptation lands.
451+
static constexpr bool hasHostSingle = cuda::is_host_accessible_resource<R>::value;
452+
};
453+
405454
} // namespace nanovdb
406455

407456
#endif // end of NANOVDB_CUDA_BUFFER_H_HAS_BEEN_INCLUDED

nanovdb/nanovdb/cuda/DeviceResource.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,17 @@ struct is_async_resource<R, std::void_t<
123123
decltype(std::declval<R&>().deallocate(std::declval<void*>(), size_t{0}, size_t{0}))>>
124124
: std::true_type {};
125125

126+
/// @brief Detection trait: @c is_host_accessible_resource<R>::value is true
127+
/// iff @c R declares `static constexpr bool HOST_ACCESSIBLE = true`,
128+
/// i.e. its allocations are mapped into the host address space (e.g.
129+
/// PinnedResource). Defaults to false: allocations are device-resident.
130+
/// @note Unlike the void_t detections above, this checks the member's VALUE:
131+
/// a resource declaring HOST_ACCESSIBLE = false stays device-resident.
132+
template<typename R, typename = void>
133+
struct is_host_accessible_resource : std::false_type {};
134+
template<typename R>
135+
struct is_host_accessible_resource<R, typename std::enable_if<bool(R::HOST_ACCESSIBLE)>::type> : std::true_type {};
136+
126137
/// @brief Detection trait: @c is_resource<R>::value is true iff @c R models
127138
/// the synchronous Resource concept, i.e. exposes
128139
/// allocate(size_t, size_t) and deallocate(void*, size_t, size_t).
@@ -244,6 +255,9 @@ struct AsyncFromSync
244255

245256
static constexpr size_t DEFAULT_ALIGNMENT = R::DEFAULT_ALIGNMENT;
246257

258+
/// @brief The adapter is host-accessible iff the adapted resource is.
259+
static constexpr bool HOST_ACCESSIBLE = is_host_accessible_resource<R>::value;
260+
247261
R resource;
248262

249263
/// @brief Allocates through the synchronous resource; the result is valid
@@ -286,6 +300,9 @@ struct ResourceRef
286300

287301
static constexpr size_t DEFAULT_ALIGNMENT = R::DEFAULT_ALIGNMENT;
288302

303+
/// @brief A reference is host-accessible iff the referenced resource is.
304+
static constexpr bool HOST_ACCESSIBLE = is_host_accessible_resource<R>::value;
305+
289306
/// @brief Constructs a ref borrowing @c resource.
290307
/// @param resource resource to allocate from; must outlive this ref
291308
ResourceRef(R& resource) : mResource(&resource) {}

nanovdb/nanovdb/cuda/GridHandle.cuh

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#ifndef NANOVDB_CUDA_GRIDHANDLE_CUH_HAS_BEEN_INCLUDED
1818
#define NANOVDB_CUDA_GRIDHANDLE_CUH_HAS_BEEN_INCLUDED
1919

20+
#include <nanovdb/cuda/Buffer.h>// for the resource-aware scratch buffers below
2021
#include <nanovdb/cuda/DeviceBuffer.h>// required for instantiation of move c-tor of GridHandle
2122
#include <nanovdb/tools/cuda/GridChecksum.cuh>// for cuda::updateChecksum
2223
#include <nanovdb/GridHandle.h>
@@ -43,6 +44,35 @@ static __global__ void updateGridCount(GridData *d_data, uint32_t gridIndex, uin
4344
}
4445
}
4546

47+
/// @brief Walks the grid chain with per-header device-to-host reads, checking
48+
/// that every header and every grid span lies inside the allocation, so
49+
/// the device-side metadata walk that follows can never read out of
50+
/// bounds from a truncated buffer or a forged header.
51+
/// @return the validated grid count
52+
inline uint32_t validGridChainCount(const GridData *d_head, uint64_t bytes, cudaStream_t stream)
53+
{
54+
uint64_t offset = 0;
55+
uint32_t count = 0, expected = 0;
56+
GridData tmp;
57+
do {
58+
if (offset + sizeof(GridData) > bytes)
59+
throw std::runtime_error("GridHandle: grid chain exceeds the device buffer (truncated or corrupt grid data)");
60+
cudaCheck(cudaMemcpyAsync(&tmp, util::PtrAdd<GridData>(d_head, offset), sizeof(GridData), cudaMemcpyDeviceToHost, stream));
61+
cudaCheck(cudaStreamSynchronize(stream));
62+
if (!tmp.isValid()) throw std::runtime_error("GridHandle was constructed with an invalid device buffer");
63+
if (count == 0) {
64+
expected = tmp.mGridCount;
65+
if (expected == 0) throw std::runtime_error("GridHandle: device buffer contains no grids");
66+
}
67+
if (tmp.mGridIndex != count || tmp.mGridCount != expected)
68+
throw std::runtime_error("GridHandle: inconsistent grid index/count in the device buffer's grid chain");
69+
if (tmp.mGridSize < sizeof(GridData) || tmp.mGridSize > bytes - offset)
70+
throw std::runtime_error("GridHandle: grid size field exceeds the device buffer (truncated or corrupt grid data)");
71+
offset += tmp.mGridSize;
72+
} while (++count < expected);
73+
return expected;
74+
}
75+
4676
}// namespace detail
4777

4878
template<typename BufferT, template <class, class...> class VectorT = std::vector>
@@ -52,8 +82,9 @@ splitGridHandles(const GridHandle<BufferT> &handle, const BufferT* other = nullp
5282
const void *ptr = handle.deviceData();
5383
if (ptr == nullptr) return VectorT<GridHandle<BufferT>>();
5484
VectorT<GridHandle<BufferT>> handles(handle.gridCount());
55-
bool dirty, *d_dirty;// use this to check if the checksum needs to be recomputed
56-
cudaCheck(util::cuda::mallocAsync((void**)&d_dirty, sizeof(bool), stream));
85+
bool dirty;// set when the checksum needs to be recomputed
86+
Buffer<bool, DeviceResource> dirtyBuf(stream, 1, noInit);
87+
bool *d_dirty = dirtyBuf.data();
5788
int device = util::cuda::currentDevice();
5889
for (uint32_t n=0; n<handle.gridCount(); ++n) {
5990
auto buffer = BufferT::create(handle.gridSize(n), other, device, stream);
@@ -68,7 +99,6 @@ splitGridHandles(const GridHandle<BufferT> &handle, const BufferT* other = nullp
6899
handles[n] = nanovdb::GridHandle<BufferT>(std::move(buffer));
69100
ptr = util::PtrAdd(ptr, handle.gridSize(n));
70101
}
71-
cudaCheck(util::cuda::freeAsync(d_dirty, stream));
72102
return handles;
73103
}// cuda::splitGridHandles
74104

@@ -85,8 +115,9 @@ mergeGridHandles(const VectorT<GridHandle<BufferT>> &handles, const BufferT* oth
85115
int device = util::cuda::currentDevice();
86116
auto buffer = BufferT::create(size, other, device, stream);
87117
void *dst = buffer.deviceData();
88-
bool dirty, *d_dirty;// use this to check if the checksum needs to be recomputed
89-
cudaCheck(util::cuda::mallocAsync((void**)&d_dirty, sizeof(bool), stream));
118+
bool dirty;// set when the checksum needs to be recomputed
119+
Buffer<bool, DeviceResource> dirtyBuf(stream, 1, noInit);
120+
bool *d_dirty = dirtyBuf.data();
90121
for (auto &h : handles) {
91122
const void *src = h.deviceData();
92123
for (uint32_t n=0; n<h.gridCount(); ++n) {
@@ -101,7 +132,6 @@ mergeGridHandles(const VectorT<GridHandle<BufferT>> &handles, const BufferT* oth
101132
src = util::PtrAdd(src, h.gridSize(n));
102133
}
103134
}
104-
cudaCheck(util::cuda::freeAsync(d_dirty, stream));
105135
return GridHandle<BufferT>(std::move(buffer));
106136
}// cuda::mergeGridHandles
107137

@@ -122,28 +152,57 @@ mergeDeviceGrids(const VectorT<GridHandle<BufferT>> &handles, const BufferT* oth
122152
template<typename BufferT>
123153
template<typename T, typename util::enable_if<BufferTraits<T>::hasDeviceDual, int>::type>
124154
GridHandle<BufferT>::GridHandle(T&& buffer)
155+
: mBuffer(std::move(buffer))
125156
{
126157
static_assert(util::is_same<T,BufferT>::value, "Expected U==BufferT");
127-
mBuffer = std::move(buffer);
128158
if (auto *data = reinterpret_cast<const GridData*>(mBuffer.data())) {
129159
if (!data->isValid()) throw std::runtime_error("GridHandle was constructed with an invalid host buffer");
130160
mMetaData.resize(data->mGridCount);
131161
cpyGridHandleMeta(data, mMetaData.data());
132162
} else {
133163
if (auto *d_data = reinterpret_cast<const GridData*>(mBuffer.deviceData())) {
134-
GridData tmp;
135-
cudaCheck(cudaMemcpy(&tmp, d_data, sizeof(GridData), cudaMemcpyDeviceToHost));
136-
if (!tmp.isValid()) throw std::runtime_error("GridHandle was constructed with an invalid device buffer");
137-
GridHandleMetaData *d_metaData;
138-
cudaMalloc((void**)&d_metaData, tmp.mGridCount*sizeof(GridHandleMetaData));
139-
cuda::detail::cpyGridHandleMeta<<<1,1>>>(d_data, d_metaData);
140-
mMetaData.resize(tmp.mGridCount);
141-
cudaCheck(cudaMemcpy(mMetaData.data(), d_metaData,tmp.mGridCount*sizeof(GridHandleMetaData), cudaMemcpyDeviceToHost));
142-
cudaCheck(cudaFree(d_metaData));
164+
const uint32_t count = cuda::detail::validGridChainCount(d_data, mBuffer.size(), cudaStream_t(0));
165+
// MallocResource: plain cudaMalloc, so this long-standing parse path
166+
// keeps working on devices without memory-pool support.
167+
cuda::Buffer<GridHandleMetaData, cuda::MallocResource> scratch(count, cuda::noInit);
168+
cuda::detail::cpyGridHandleMeta<<<1,1>>>(d_data, scratch.data());
169+
cudaCheckError();
170+
mMetaData.resize(count);
171+
cudaCheck(cudaMemcpy(mMetaData.data(), scratch.data(), count*sizeof(GridHandleMetaData), cudaMemcpyDeviceToHost));
143172
}
144173
}
145174
}// GridHandle(T&& buffer)
146175

176+
// move constructor from a single-space device buffer: all device work runs on
177+
// the buffer's retained stream (or the default stream for a synchronous
178+
// resource), and the metadata scratch allocates through the buffer's resource.
179+
template<typename BufferT>
180+
template<typename T, typename util::enable_if<BufferHasDeviceSingle<T>::value, int>::type, typename>
181+
GridHandle<BufferT>::GridHandle(T&& buffer)
182+
: mBuffer(std::move(buffer))
183+
{
184+
static_assert(util::is_same<T,BufferT>::value, "Expected U==BufferT");
185+
static_assert(sizeof(typename BufferT::ElementType) == 1,
186+
"GridHandle requires byte-addressed single-space storage, e.g. cuda::Buffer<std::byte, R>");
187+
using ResourceT = typename BufferT::ResourceType;
188+
if (const GridData *d_data = reinterpret_cast<const GridData*>(mBuffer.data())) {
189+
constexpr bool isAsync = cuda::is_async_resource<ResourceT>::value;
190+
cudaStream_t stream = cudaStream_t(0);
191+
if constexpr (isAsync) stream = mBuffer.stream();
192+
const uint32_t count = cuda::detail::validGridChainCount(d_data, mBuffer.size_bytes(), stream);
193+
using ScratchT = cuda::Buffer<GridHandleMetaData, ResourceT>;
194+
ScratchT scratch = [&]{
195+
if constexpr (isAsync) return ScratchT(stream, mBuffer.resource(), count, cuda::noInit);
196+
else return ScratchT(mBuffer.resource(), count, cuda::noInit);
197+
}();
198+
cuda::detail::cpyGridHandleMeta<<<1, 1, 0, stream>>>(d_data, scratch.data());
199+
cudaCheckError();
200+
mMetaData.resize(count);
201+
cudaCheck(cudaMemcpyAsync(mMetaData.data(), scratch.data(), count*sizeof(GridHandleMetaData), cudaMemcpyDeviceToHost, stream));
202+
cudaCheck(cudaStreamSynchronize(stream));
203+
}
204+
}// GridHandle(T&& buffer) for single-space device buffers
205+
147206
// Dummy function that ensures instantiation of the move-constructor above when BufferT=cuda::DeviceBuffer
148207
namespace {auto __dummy(){return GridHandle<cuda::DeviceBuffer>(std::move(cuda::DeviceBuffer()));}}
149208

nanovdb/nanovdb/cuda/PinnedResource.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ class PinnedResource
3030
// alignment is always satisfied; 256 is the nominal default advertised here.
3131
static constexpr size_t DEFAULT_ALIGNMENT = 256;
3232

33+
/// @brief Pinned allocations are mapped into the host address space, so
34+
/// containers over this resource remain host-readable (detected by
35+
/// nanovdb::cuda::is_host_accessible_resource).
36+
static constexpr bool HOST_ACCESSIBLE = true;
37+
3338
/// @brief Synchronous allocation of page-locked host memory.
3439
/// @param bytes number of bytes to allocate
3540
/// @param alignment requested alignment (ignored; cudaMallocHost is page-aligned)

0 commit comments

Comments
 (0)