Repository navigation
Design: cuda.core Texture/Surface API surface #2188
Description
Purpose
Design discussion for the texture / surface API in cuda.core — to settle the API shape and
naming before code review of the implementation. Reviewers asked for design sign-off in an issue
before we commit to a ~9k-line feature.
- Implementation PR: cuda.core: add CUDAArray, TextureObject, SurfaceObject, MipmappedArray (texture/surface API, #467) #2095
- Feature request: Add
TextureObjectandSurfaceObject? #467
cc @leofang @mdboom @Andy-Jost @kkraus14 — you asked for a design pass; this is the home for it.
Proposed public surface (from #2095)
Array+ArrayFormat— opaque, hardware-laid-out GPU allocations backing textures/surfaces.MipmappedArray— wrapsCUmipmappedArray;get_levelreturns a non-owningArraykept alive
by a strong ref to the parent.TextureObject+TextureDescriptor— bindless texture handle + sampling state. (read-only)SurfaceObject— bindless surface handle; requiresArray(surface_load_store=True). (read/write)ResourceDescriptor— factoriesfrom_array,from_mipmapped_array,from_linear,from_pitch2d.
Decisions to make
-
Name of
Array. ✅ Decided — renameArray→CUDAArray.This type is an opaque
cudaArray_t— the GPU stores it in a scrambled, hardware-defined layout
with no linear pointer, so it cannot expose__cuda_array_interface__/ DLPack and cannot
share memory zero-copy with cupy / numba-cuda / torch. The nameArrayimplies an n-dimensional
array that participates in that ecosystem — it can't. CuPy names the identical typeCUDAarray,
and its wholecupy.cuda.texturemodule already matches this PR's surface 1:1.Resolution: use
CUDAArray— the PEP 8 CapWords form (deliberately differing from CuPy's exact
CUDAarraycasing to follow Python's class-naming standard). The name signals "CUDA texture/surface
backing store," not "n-dimensional array."Open detail resolved: keep
ArrayFormat(do not rename toCUDAArrayFormat). The sibling
enums in these modules —AddressMode,FilterMode,ReadMode— are all unprefixed, so
ArrayFormatmatches the established enum-naming pattern; and the "Arrayimplies an
ndarray/DLPack participant" concern that motivatedCUDAArraydoes not apply to a format enum
(nobody mistakesArrayFormatfor an n-dimensional array). -
Interop path. ✅ Decided — ship only
copy_from/copy_to; no allocation helper.Zero-copy is impossible (opaque layout, no linear pointer), so copying is the only option —
this was purely about how polished the path is. The copy path to/from linearcuda.core
Buffers already exists:copy_from/copy_toaccept a deviceBufferor a host
buffer-protocol object, in both directions. The only thing an extra helper would add is
allocating the linearBufferfor the caller — foldingmr.allocate(arr.size_bytes, stream=s)arr.copy_to(buf, stream=s)into a one-liner, i.e. ~2 lines of convenience.
The "extra helper" would collapse this:
mr = DeviceMemoryResource(device) buf = mr.allocate(arr.size_bytes, stream=s) arr.copy_to(buf, stream=s) into something like buf = arr.to_buffer(mr, stream=s).Resolution: ship
copy_from/copy_toonly, and document the copy-only contract. We will
not add an allocating convenience helper now. It is purely additive and non-breaking, so we can
add one later if users request it. -
Factory set. ✅ Not a real decision — driver-mandated, all four required.
A texture can be backed by four kinds of memory — the PR exposes one factory per kind:
from_array— texture over aCUDAArray(the headline feature)from_mipmapped_array— texture over aMipmappedArray(the headline feature)from_linear— texture over a plain 1D device buffer (ordinary linear memory, noCUDAArray)from_pitch2d— texture over a plain 2D pitched buffer (ordinary linear memory, noCUDAArray)
ResourceDescriptorbindsCUDA_RESOURCE_DESC, a driver union whoseresTypeis exactly one of
ARRAY/MIPMAPPED_ARRAY/LINEAR/PITCH2D— one factory per union arm. A faithful binding
of that type must cover all four; shipping only two would be an incomplete binding of a mandatory
driver struct, not a smaller-but-valid surface. So there was no real optionality here — the CTK
driver API dictates the set. (Listed only because it sat next to the genuine decisions.)Resolution: ship all four factories — required by the driver API, not a tradeoff.
-
Channel format. ✅ Decided — keep the folded
format+num_channelsparameters.Each array element has a component type (e.g. 8-bit uint, 32-bit float) and a channel count
(1 = grayscale … 4 = RGBA). Two ways to surface that:- Folded (this PR):
CUDAArray.from_descriptor(shape=..., format=ArrayFormat.FLOAT32, num_channels=4) - Separate (CuPy): one
ChannelFormatDescriptor(...)object passed as a unit
The driver descriptor
cuda.coreactually fills in (CUDA_ARRAY3D_DESCRIPTOR) already stores
these as two separate fields —Format(aCUarray_format, mirrored 1:1 byArrayFormat) and
NumChannels. So the folded form maps straight onto the driver struct with no translation, and
read-back is already exposed as two properties (.format,.num_channels). The bundled
ChannelFormatDescriptoris the runtime API's (cudaChannelFormatDesc) modeling — the form
CuPy wraps because its texture module sits on the runtime API. Adopting it in a driver-based
library would mean a translation wrapper the underlying API doesn't use (and the shapes don't even
map cleanly: the driver uses one uniform component format × channel count, while
cudaChannelFormatDescallows per-channel bit widths).Resolution: keep folded
format+num_channels. It's the driver-faithful surface
(consistent with Renaming #1 favoring correctness over CuPy parity and Docs Updates #3 following the driver API); the
bundled form's only wins are CuPy look-alike and a single read-back object, neither worth a
runtime-style wrapper here. - Folded (this PR):
-
Descriptor type consistency. ✅ Not a real decision — divergence is intentional and harmless.
Note: the original framing here was factually wrong.
ResourceDescriptoris not acdef classand holds no native C struct — it is a plain Python class with__slots__, storing a
reference to the backing resource plus a few Python fields. TheCUDA_RESOURCE_DESCstruct is
assembled later, inTextureObject.from_descriptor. So this was never a@dataclass-vs-cdef class/ performance question. Both descriptors are pure Python.The genuine difference is only how you construct each, and it reflects what each type is:
TextureDescriptor— a flat bag of independent sampling settings, built directly with keyword
args (@dataclassfits perfectly).ResourceDescriptor— a "pick exactly one of four backings" union (array / mipmap / linear /
pitch2d), built viafrom_*factories because each kind carries different fields. A single
__init__would be a pile of mutually-exclusive optional args plus a kind tag.
Consistency is not a goal in itself — it only matters when inconsistency makes the API harder to
learn or use, and here it doesn't: a user learns each type once and never has to reconcile them.
The only behavioral gap is equality (TextureDescriptorcompares by value;ResourceDescriptor
by identity), which is essentially never exercised on these objects and is arguably correct since
ResourceDescriptorwraps a live device resource. Forcing both to the same kind of type would
be uniformity for its own sake and would makeResourceDescriptor's constructor worse.Resolution: keep the split — the divergence is intentional and does not hurt usability. Like
Docs Updates #3, this resolves to a non-issue once examined (and on a mistaken premise to begin with). -
Bool naming. ✅ Decided — adopt the
is_<something>convention.surface_load_storeis a boolean onArray: it records whether the array was created with the
surface load/store capability (CUDA'sCUDA_ARRAY3D_SURFACE_LDST), which aSurfaceObject
requires. Exposed both as a constructor keyword (surface_load_store=True) and a read-only
property (arr.surface_load_store).The repo convention for boolean properties is
is_<something>, so a property named
surface_load_storedoesn't read as a boolean the wayarr.is_manageddoes. Resolution: rename
the property to follow theis_<x>convention (e.g.is_surface_load_store) for consistency with
the cuda-python codebase.Open detail resolved: the property name is
is_surface_load_store(already implemented), and
the constructor keyword is renamed to match —from_descriptor(..., is_surface_load_store=False)—
so one symmetric name serves both set and read-back. This follows the existing
StridedMemoryView(is_readonly=...)precedent in cuda.core, where anis_<x>boolean is used as
both the constructor argument and the attribute. (The keyword rename is a small implementation
follow-up in the PR; the property is already done.) -
Scope. ✅ Decided — split the examples into a follow-up PR.
The nine
gl_interop_*.pyexamples (~5k lines, not CI-wired, need a GL context CI lacks) are
orthogonal to the core API. Resolution: drop them from this PR and land them in a separate
follow-up PR once this core texture/surface PR merges, since the examples depend on the new API
it introduces.
Activity
[-]Design: cuda.core Texture/Surface API surface (Array naming + interop contract) — pre-merge for #2095[/-][+]Design: cuda.core Texture/Surface API surface [/+]TextureObjectandSurfaceObject? #467TextureObjectandSurfaceObject? #467cuda.core: rename Array->CUDAArray, surface_load_store->is_surface_lo…
cuda.core: rename surface_load_store ctor keyword to is_surface_load_…
rparolin commentedon Jun 11, 2026
@leofang @mdboom @Andy-Jost Please review.
Andy-Jost commentedon Jun 16, 2026
Andy-Jost commentedon Jun 16, 2026
This introduces a family of 7 related types:
CUDAArray,MipmappedArray,TextureObject,SurfaceObject,ResourceDescriptor,TextureDescriptor. That's large enough that we should consider now whether to group these under a new level in the module hierarchy because it will be harder to do so after they're released.Perhaps
cuda.core.texturesorcuda.core.graphics?The CUDA documentation lists these under the heading "Texture and Surface" and there's no clean way to collapse that. Still, my preference would be to add these under
cuda.core.textures.cuda.core: address NVIDIA#2188 review feedback — copy parity + cuda.c…
cuda.core: document CUDAArray copy-only interop contract (NVIDIA#2188 …
rparolin commentedon Jun 17, 2026
Thanks @Andy-Jost — both points are addressed in the latest revision of #2095.
1. Copy API parity with
BufferCUDAArray.copy_from/copy_tonow acceptStream | GraphBuilder, coerced via the sameStream_accepthelperBufferuses — so array copies can take part in graph capture.copy_tonow returnsdst, matchingBuffer.copy_to.copy_toreturns the destination object, and aGraphBuilder-capturedcopy_from+copy_toround-trip that completes and launches.(commit
681e046b4c)2. Module grouping
cuda.core.texturesnamespace (your preferred name):CUDAArray,ArrayFormat,MipmappedArray,ResourceDescriptor,TextureDescriptor,TextureObject,SurfaceObject, and theAddressMode/FilterMode/ReadModeenums.cuda.coreexports are removed. Since this is pre-release (v1.1.0) there's no deprecation shim. Examples, docs (api.rst), and tests are migrated to the new namespace.(commit
681e046b4c)Separately, the resource-handle lifetime note is also addressed: all four types now route CUDA lifetime through
cuda.core's C++std::shared_ptrresource-handle layer (structural dependencies embedded in the box, GIL-guarded deleters), with no__dealloc__,_owningflags, or_parent_ref/_source_refPython keepalives (commite087f0b138).Full
cuda_coresuite is green (3449 passed / 230 skipped / 3 xfailed); the #2095 title/body have been refreshed to match. The one remaining design item is decision 7 (splitting thegl_interop_*examples into a follow-up PR).cuda.core: scope texture/surface PR to the core API + fluid examples (N…
TextureObjectandSurfaceObject? #467 as a duplicate of this issuecuda.core: rename ArrayHandle -> CUDAArrayHandle; fix CUDAArray artic…
cuda.core: rename CUDAArray -> OpaqueArray (and CUDAArrayHandle -> Op…
cuda.core: add CUDAArray, TextureObject, SurfaceObject, MipmappedArra…
leofang commentedon Jul 8, 2026
Follow-up: #2292.