NanoVDB Python: remove memory-handle ceremony - #2305
Draft
swahtz wants to merge 3 commits into
Draft
Conversation
Bind __len__ (== gridCount()) and __getitem__ on the shared defineGridHandle<BufferT> template so both the host GridHandle and the CUDA DeviceGridHandle support len(handle), handle[i] with negative indices, and iteration (__getitem__ raising IndexError enables Python's sequence-iteration protocol; an explicit __iter__ is deliberately omitted — it would type as Iterator[object] in the stub and would not carry the def-level keep_alive that parents each grid to its handle). handle[i] uses the same polymorphic typed-grid dispatch as grid(n). grid(n)'s return-None-out-of-range contract is unchanged; handle[i] raises IndexError instead, per sequence semantics. Grids whose BuildT is not bound in Python are still returned as None, matching grid(n). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove the NodeManagerHandle Python class and its mgr() hop (unreleased API, introduced by AcademySoftwareFoundation#2219). nanovdb.createNodeManager(grid) now returns the typed <T>NodeManager (e.g. FloatNodeManager) directly. The lifetime chain is preserved: the C++ NodeManagerHandle that owns the node-index buffer is moved to the heap and owned by an nb::capsule, and the returned manager is cast with rv_policy::reference_internal against that capsule; the existing def-level keep_alive<0,1> on createNodeManager additionally ties the manager to the source grid, whose memory the manager's leaf(i)/lower(i)/upper(i) node views point into (those remain reference_internal to the manager). The gc.collect() temporary-grid lifetime test keeps its exact shape, minus the .mgr() hop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove the vestigial buffer= keyword from io.readGrid(s) / deviceReadGrid(s) and all 13 tools.create* primitive factories, and unbind the zero-method HostBuffer and DeviceBuffer Python classes. The parameter was unreachable ceremony: neither buffer class had a bound constructor, so the default-constructed prototype (equivalent to omitting the argument entirely in C++) was the only value Python could ever pass, and buffer choice is already expressed by which function or submodule is called (tools vs tools.cuda, readGrid vs deviceReadGrid). Removing the vocabulary now also aligns with issue AcademySoftwareFoundation#2232, which puts the C++ HostBuffer/DeviceBuffer types on a deprecation path toward resource-injected single-space buffers; if allocation control comes to Python it will arrive as resource injection in that model, not as buffer prototypes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
swahtz
marked this pull request as draft
August 28, 2026 12:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three ergonomics changes to the unreleased NanoVDB Python bindings (everything touched here landed after the last release via #2219, so nothing shipped is broken):
GridHandle/DeviceGridHandleare sequences —len(h),h[i](negative indices,IndexErrorout of range), and iteration, using the same polymorphic typed-grid dispatch ash.grid(n).grid(n)'s return-Nonecontract is unchanged. An explicit__iter__is deliberately omitted:__getitem__+IndexErrorgives the sequence-iteration protocol, whereas a separate__iter__path would not carry the def-levelkeep_alivethat parents each grid to its handle (accepted limitation:isinstance(h, collections.abc.Iterable)isFalse).nanovdb.createNodeManager(grid)returns the typedNodeManagerdirectly — theNodeManagerHandlePython class and itsmgr()hop are removed. The lifetime chain is preserved: the C++ handle that owns the node-index buffer is moved to the heap and owned by annb::capsule, the returned manager is boundreference_internalto that capsule, and the existingkeep_alive<0,1>ties the manager to the source grid whose memoryleaf(i)/lower(i)/upper(i)point into. Thegc.collect()temporary-grid lifetime test keeps its exact shape, minus the.mgr()hop.buffer=parameters removed fromio.readGrid(s)/deviceReadGrid(s)and all 13tools.create*primitive factories, and the zero-methodHostBuffer/DeviceBufferPython classes are unbound. The parameter was unreachable ceremony: neither buffer class had a bound constructor, so the default-constructed prototype (equivalent to omitting the argument in C++) was the only value Python could ever pass, and buffer choice is already expressed by which function or submodule you call (toolsvstools.cuda,readGridvsdeviceReadGrid).Deliberately out of scope: factories still return
GridHandles rather than grids, keeping host/device and single/multi-grid usage uniform.Relationship to #2232 (injectable CUDA memory resources)
Removing the
buffer=vocabulary now aligns the Python surface with the planned C++ retirement ofHostBuffer/DeviceBuffer(#2232, Step 3: single-spaceGridHandle<cuda::Buffer>withcopyTo). If allocation control comes to Python later, it should arrive as resource injection in that CCCLcuda::mr-shaped model (the issue defers aSharedResourceanalog "until the Python-bindings need materializes"), not as buffer prototypes — removingbuffer=now avoids ever having two competing allocation vocabularies in Python.Note for #2225
The pending GPU-bindings PR rebinds a rich
DeviceBufferundernanovdb.cuda— a return-side interop object (__cuda_array_interface__/__dlpack__/device_ptr/recordUse/from_external), never a factory input, so it is a different job than the zero-method class removed here. On rebase, thecuda/PyDeviceBuffer.{cc,h}deletion in this PR should resolve as "keep #2225's new files"; #2225 does not touchPyGridHandle.h,PyTree.cc,PyIO.cc, orPyPrimitives.cc, so the rest rebases cleanly.Testing
CUDA build (sm_120) with
NANOVDB_USE_CUDA=ON,NANOVDB_USE_OPENVDB=ON, BLOSC/ZIP/TBB on:TestNanoVDB.py: 172/172 OK on a real GPU, including new host + device sequence-protocol tests (mixed-type merged handles, negative indices,IndexErrorbounds, iteration lifetime undergc.collect()) and the updated node-manager round-trip / bounds / zero-copy-lifetime tests.TestExamples.py: 14/14 OK (load_inspect.pynow demonstrateslen(handle)/handle[i]/ iteration;node_manager.pyuses the directcreateNodeManagerreturn).🤖 Generated with Claude Code