Skip to content

NanoVDB Python: remove memory-handle ceremony - #2305

Draft
swahtz wants to merge 3 commits into
AcademySoftwareFoundation:masterfrom
swahtz:feature/nanovdb_python_handle_ergonomics
Draft

NanoVDB Python: remove memory-handle ceremony#2305
swahtz wants to merge 3 commits into
AcademySoftwareFoundation:masterfrom
swahtz:feature/nanovdb_python_handle_ergonomics

Conversation

@swahtz

@swahtz swahtz commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Three ergonomics changes to the unreleased NanoVDB Python bindings (everything touched here landed after the last release via #2219, so nothing shipped is broken):

  1. GridHandle / DeviceGridHandle are sequenceslen(h), h[i] (negative indices, IndexError out of range), and iteration, using the same polymorphic typed-grid dispatch as h.grid(n). grid(n)'s return-None contract is unchanged. An explicit __iter__ is deliberately omitted: __getitem__ + IndexError gives the sequence-iteration protocol, whereas a separate __iter__ path would not carry the def-level keep_alive that parents each grid to its handle (accepted limitation: isinstance(h, collections.abc.Iterable) is False).

  2. nanovdb.createNodeManager(grid) returns the typed NodeManager directly — the NodeManagerHandle Python class and its mgr() 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 an nb::capsule, the returned manager is bound reference_internal to that capsule, and the existing keep_alive<0,1> ties the manager to the source grid whose memory leaf(i)/lower(i)/upper(i) point into. The gc.collect() temporary-grid lifetime test keeps its exact shape, minus the .mgr() hop.

  3. buffer= parameters removed from io.readGrid(s) / deviceReadGrid(s) and all 13 tools.create* primitive factories, and the zero-method HostBuffer / DeviceBuffer Python 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 (tools vs tools.cuda, readGrid vs deviceReadGrid).

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 of HostBuffer/DeviceBuffer (#2232, Step 3: single-space GridHandle<cuda::Buffer> with copyTo). If allocation control comes to Python later, it should arrive as resource injection in that CCCL cuda::mr-shaped model (the issue defers a SharedResource analog "until the Python-bindings need materializes"), not as buffer prototypes — removing buffer= now avoids ever having two competing allocation vocabularies in Python.

Note for #2225

The pending GPU-bindings PR rebinds a rich DeviceBuffer under nanovdb.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, the cuda/PyDeviceBuffer.{cc,h} deletion in this PR should resolve as "keep #2225's new files"; #2225 does not touch PyGridHandle.h, PyTree.cc, PyIO.cc, or PyPrimitives.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, IndexError bounds, iteration lifetime under gc.collect()) and the updated node-manager round-trip / bounds / zero-copy-lifetime tests.
  • TestExamples.py: 14/14 OK (load_inspect.py now demonstrates len(handle) / handle[i] / iteration; node_manager.py uses the direct createNodeManager return).

🤖 Generated with Claude Code

swahtz and others added 3 commits August 28, 2026 23:35
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
swahtz requested a review from kmuseth as a code owner August 28, 2026 12:16
@swahtz
swahtz marked this pull request as draft August 28, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant