Title: Design: cuda.core Texture/Surface API surface · Issue #2188 · NVIDIA/cuda-python · GitHub
Open Graph Title: Design: cuda.core Texture/Surface API surface · Issue #2188 · NVIDIA/cuda-python
X Title: Design: cuda.core Texture/Surface API surface · Issue #2188 · NVIDIA/cuda-python
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. I...
Open Graph 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 b...
X 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 b...
Opengraph URL: https://github.com/NVIDIA/cuda-python/issues/2188
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Design: cuda.core Texture/Surface API surface ","articleBody":"## Purpose\n\nDesign discussion for the **texture / surface API** in `cuda.core` — to settle the API shape and\nnaming *before* code review of the implementation. Reviewers asked for design sign-off in an issue\nbefore we commit to a ~9k-line feature.\n\n- Implementation PR: #2095\n- Feature request: #467\n\ncc @leofang @mdboom @Andy-Jost @kkraus14 — you asked for a design pass; this is the home for it.\n\n## Proposed public surface (from #2095)\n\n- `Array` + `ArrayFormat` — opaque, hardware-laid-out GPU allocations backing textures/surfaces.\n- `MipmappedArray` — wraps `CUmipmappedArray`; `get_level` returns a non-owning `Array` kept alive\n by a strong ref to the parent.\n- `TextureObject` + `TextureDescriptor` — bindless texture handle + sampling state. (read-only)\n- `SurfaceObject` — bindless surface handle; requires `Array(surface_load_store=True)`. (read/write)\n- `ResourceDescriptor` — factories `from_array`, `from_mipmapped_array`, `from_linear`, `from_pitch2d`.\n\n## Decisions to make\n\n1. **Name of `Array`. ✅ Decided — rename `Array` → `CUDAArray`.**\n\n This type is an opaque `cudaArray_t` — the GPU stores it in a scrambled, hardware-defined layout\n with no linear pointer, so it **cannot** expose `__cuda_array_interface__` / DLPack and cannot\n share memory zero-copy with cupy / numba-cuda / torch. The name `Array` implies an n-dimensional\n array that participates in that ecosystem — it can't. CuPy names the identical type `CUDAarray`,\n and its whole `cupy.cuda.texture` module already matches this PR's surface 1:1.\n\n **Resolution: use `CUDAArray`** — the PEP 8 CapWords form (deliberately differing from CuPy's exact\n `CUDAarray` casing to follow Python's class-naming standard). The name signals \"CUDA texture/surface\n backing store,\" not \"n-dimensional array.\"\n\n **Open detail resolved: keep `ArrayFormat` (do *not* rename to `CUDAArrayFormat`).** The sibling\n enums in these modules — `AddressMode`, `FilterMode`, `ReadMode` — are all unprefixed, so\n `ArrayFormat` matches the established enum-naming pattern; and the \"`Array` implies an\n ndarray/DLPack participant\" concern that motivated `CUDAArray` does not apply to a format enum\n (nobody mistakes `ArrayFormat` for an n-dimensional array).\n\n2. **Interop path. ✅ Decided — ship only `copy_from` / `copy_to`; no allocation helper.**\n\n Zero-copy is impossible (opaque layout, no linear pointer), so copying is the only option —\n this was purely about how polished the path is. The copy path to/from linear `cuda.core`\n `Buffer`s already exists: `copy_from` / `copy_to` accept a device `Buffer` or a host\n buffer-protocol object, in both directions. The only thing an extra helper would add is\n allocating the linear `Buffer` for the caller — folding `mr.allocate(arr.size_bytes, stream=s)`\n + `arr.copy_to(buf, stream=s)` into a one-liner, i.e. ~2 lines of convenience.\n\n The \"extra helper\" would collapse this:\n ```\n mr = DeviceMemoryResource(device)\n buf = mr.allocate(arr.size_bytes, stream=s)\n arr.copy_to(buf, stream=s)\n\n into something like \n buf = arr.to_buffer(mr, stream=s).\n ```\n\n **Resolution: ship `copy_from` / `copy_to` only, and document the copy-only contract.** We will\n not add an allocating convenience helper now. It is purely additive and non-breaking, so we can\n add one later if users request it.\n\n3. **Factory set. ✅ Not a real decision — driver-mandated, all four required.**\n\n A texture can be backed by four kinds of memory — the PR exposes one factory per kind:\n - `from_array` — texture over a `CUDAArray` *(the headline feature)*\n - `from_mipmapped_array` — texture over a `MipmappedArray` *(the headline feature)*\n - `from_linear` — texture over a plain 1D device buffer *(ordinary linear memory, no `CUDAArray`)*\n - `from_pitch2d` — texture over a plain 2D pitched buffer *(ordinary linear memory, no `CUDAArray`)*\n\n `ResourceDescriptor` binds `CUDA_RESOURCE_DESC`, a driver union whose `resType` is exactly one of\n `ARRAY` / `MIPMAPPED_ARRAY` / `LINEAR` / `PITCH2D` — one factory per union arm. A faithful binding\n of that type *must* cover all four; shipping only two would be an incomplete binding of a mandatory\n driver struct, not a smaller-but-valid surface. So there was no real optionality here — the CTK\n driver API dictates the set. (Listed only because it sat next to the genuine decisions.)\n\n **Resolution: ship all four factories — required by the driver API, not a tradeoff.**\n\n4. **Channel format. ✅ Decided — keep the folded `format` + `num_channels` parameters.**\n\n Each array element has a component type (e.g. 8-bit uint, 32-bit float) and a channel count\n (1 = grayscale … 4 = RGBA). Two ways to surface that:\n - **Folded (this PR):** `CUDAArray.from_descriptor(shape=..., format=ArrayFormat.FLOAT32, num_channels=4)`\n - **Separate (CuPy):** one `ChannelFormatDescriptor(...)` object passed as a unit\n\n The driver descriptor `cuda.core` actually fills in (`CUDA_ARRAY3D_DESCRIPTOR`) already stores\n these as two separate fields — `Format` (a `CUarray_format`, mirrored 1:1 by `ArrayFormat`) and\n `NumChannels`. So the folded form maps straight onto the driver struct with no translation, and\n read-back is already exposed as two properties (`.format`, `.num_channels`). The bundled\n `ChannelFormatDescriptor` is the *runtime* API's (`cudaChannelFormatDesc`) modeling — the form\n CuPy wraps because its texture module sits on the runtime API. Adopting it in a driver-based\n library would mean a translation wrapper the underlying API doesn't use (and the shapes don't even\n map cleanly: the driver uses one uniform component format × channel count, while\n `cudaChannelFormatDesc` allows per-channel bit widths).\n\n **Resolution: keep folded `format` + `num_channels`.** It's the driver-faithful surface\n (consistent with #1 favoring correctness over CuPy parity and #3 following the driver API); the\n bundled form's only wins are CuPy look-alike and a single read-back object, neither worth a\n runtime-style wrapper here.\n\n5. **Descriptor type consistency. ✅ Not a real decision — divergence is intentional and harmless.**\n\n *Note: the original framing here was factually wrong.* `ResourceDescriptor` is **not** a `cdef\n class` and holds **no** native C struct — it is a plain Python class with `__slots__`, storing a\n reference to the backing resource plus a few Python fields. The `CUDA_RESOURCE_DESC` struct is\n assembled later, in `TextureObject.from_descriptor`. So this was never a `@dataclass`-vs-`cdef\n class` / performance question. Both descriptors are pure Python.\n\n The genuine difference is only *how you construct each*, and it reflects what each type is:\n - `TextureDescriptor` — a flat bag of independent sampling settings, built directly with keyword\n args (`@dataclass` fits perfectly).\n - `ResourceDescriptor` — a \"pick exactly one of four backings\" union (array / mipmap / linear /\n pitch2d), built via `from_*` factories because each kind carries different fields. A single\n `__init__` would be a pile of mutually-exclusive optional args plus a kind tag.\n\n Consistency is not a goal in itself — it only matters when inconsistency makes the API harder to\n learn or use, and here it doesn't: a user learns each type once and never has to reconcile them.\n The only behavioral gap is equality (`TextureDescriptor` compares by value; `ResourceDescriptor`\n by identity), which is essentially never exercised on these objects and is arguably correct since\n `ResourceDescriptor` wraps a live device resource. Forcing both to the same *kind* of type would\n be uniformity for its own sake and would make `ResourceDescriptor`'s constructor worse.\n\n **Resolution: keep the split — the divergence is intentional and does not hurt usability.** Like\n #3, this resolves to a non-issue once examined (and on a mistaken premise to begin with).\n\n6. **Bool naming. ✅ Decided — adopt the `is_\u003csomething>` convention.**\n\n `surface_load_store` is a boolean on `Array`: it records whether the array was created with the\n surface load/store capability (CUDA's `CUDA_ARRAY3D_SURFACE_LDST`), which a `SurfaceObject`\n requires. Exposed both as a constructor keyword (`surface_load_store=True`) and a read-only\n property (`arr.surface_load_store`).\n\n The repo convention for boolean properties is `is_\u003csomething>`, so a property named\n `surface_load_store` doesn't read as a boolean the way `arr.is_managed` does. **Resolution: rename\n the property to follow the `is_\u003cx>` convention (e.g. `is_surface_load_store`) for consistency with\n the cuda-python codebase.**\n\n **Open detail resolved:** the property name is `is_surface_load_store` (already implemented), and\n the constructor keyword is renamed to match — `from_descriptor(..., is_surface_load_store=False)` —\n so one symmetric name serves both set and read-back. This follows the existing\n `StridedMemoryView(is_readonly=...)` precedent in cuda.core, where an `is_\u003cx>` boolean is used as\n both the constructor argument and the attribute. (The keyword rename is a small implementation\n follow-up in the PR; the property is already done.)\n\n7. **Scope. ✅ Decided — split the examples into a follow-up PR.**\n\n The nine `gl_interop_*.py` examples (~5k lines, not CI-wired, need a GL context CI lacks) are\n orthogonal to the core API. **Resolution: drop them from this PR and land them in a separate\n follow-up PR once this core texture/surface PR merges**, since the examples depend on the new API\n it introduces.\n\n\n\n\n\n\n\n","author":{"url":"https://github.com/rparolin","@type":"Person","name":"rparolin"},"datePublished":"2026-06-09T23:04:40Z","url":"https://github.com/NVIDIA/cuda-python/issues/2188"}
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:f8e59693-6782-e346-6c69-5e3c7609052f |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 8736:17907:6D6A30:8F41E9:6AF83693 |
| html-safe-nonce | 0eb9e03aeadfb0cb9ccf8c0f68de1a089314aa941de433c8f45c4b5f481b7187 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI4NzM2OjE3OTA3OjZENkEzMDo4RjQxRTk6NkFGODM2OTMiLCJ2aXNpdG9yX2lkIjoiNjcwMTY3ODczODIwODMzNDI3IiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | bcf554b5f170d6cadf3e63a0959caf6422d33e2d68627ba2591968d6fbf15d92 |
| hovercard-subject-tag | issue:4626327805 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/NVIDIA/cuda-python/2188/issue_layout |
| twitter:image | https://opengraph.githubassets.com/a7e4a4d0c3b8a5a80f4f3369074bf02edc1472e8aa748c7d492d3c63c04e07b7/NVIDIA/cuda-python/issues/2188 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/a7e4a4d0c3b8a5a80f4f3369074bf02edc1472e8aa748c7d492d3c63c04e07b7/NVIDIA/cuda-python/issues/2188 |
| og:image:alt | 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 b... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | rparolin |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5713ecd83001b2f55c3509fb7a292bf7d66d0f167c7ae87a9f7eaec5a52bf8db |
| turbo-cache-control | no-preview |
| go-import | github.com/NVIDIA/cuda-python git https://github.com/NVIDIA/cuda-python.git |
| octolytics-dimension-user_id | 1728152 |
| octolytics-dimension-user_login | NVIDIA |
| octolytics-dimension-repository_id | 381173759 |
| octolytics-dimension-repository_nwo | NVIDIA/cuda-python |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 381173759 |
| octolytics-dimension-repository_network_root_nwo | NVIDIA/cuda-python |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 653be32ff5c755082a4db662491bfbbcfc9112c1 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width