ADR-023: hurray-python Native Buffer Interchange Protocol
Status
Accepted
Context
hurray-python interoperates with the broader Python ecosystem through three
buffer-sharing surfaces:
- DLPack (
__dlpack__/__dlpack_device__) — the cross-library protocol used by PyTorch, JAX, NumPy, CuPy, etc. - NumPy (
__array__/from_numpy) — CPU-only, Tier 1 dtypes only. - Python buffer protocol — CPU-only, byte-level access.
DLPack is the right tool for cross-library zero-copy, but its device/memory model is
strictly less expressive than Hurray's. Per the device mapping table in
docs/impl/python-bindings.md, hurray.UnsupportedError is raised today in every
one of the following situations because DLPack v1.0 has no representation for them:
- ROCm +
UNIFIED—kDLROCMManageddoes not exist in the DLPackDLDeviceTypeenum. PEERmemory for any device — DLPack has no flat enum value for peer-mapped memory.- Private device tags (
0xF0–0xFE) — by design, the format reserves these for implementation-private use; DLPack cannot represent them.
For hurray-to-hurray transfers — hurray-python ↔ hurray-python,
hurray-python ↔ hurray-ffi consumer, or hurray-python ↔ another binding built
on hurray-ffi — DLPack is not the correct protocol. The Hurray C ABI buffer handle
(HurrayBuffer in hurray-ffi) already carries the full descriptor: device_tag,
memory_class, sync_mode, alignment, byte_size, release callback, release
context. A native protocol can share all of that losslessly without flattening into
DLPack's DLDeviceType enum.
The proposal: an opt-in __hurray_buffer__() / hurray.from_hurray_buffer() PyCapsule
protocol. The capsule wraps a HurrayBuffer pointer from hurray-ffi, with the same
lifetime discipline as DLPack capsules (refcount on create, decrement on
consume/delete; capsule renamed after consumption to prevent double-free).
This ADR does not propose replacing DLPack. DLPack remains the only protocol consumed by external libraries (PyTorch, JAX, NumPy). The native protocol exists strictly to plug the holes the device mapping table calls out, and to give two Hurray-aware peers a path that preserves the full descriptor.
What was decided in OQ-A (ADR-022)
ADR-022 establishes that runtime compliance modes gate exactly two things: Tier 2 /
quantized dtype admission and __array_namespace__ visibility on Tier 2 tensors. The
mode does not gate DLPack, __array__, error hierarchy, or buffer-protocol
semantics. That scope decision is load-bearing for this ADR: the native buffer protocol
is not an Array API construct and falls outside the modes' jurisdiction.
Decision
1. Protocol name: __hurray_buffer__ / from_hurray_buffer
Amended by ADR-033: the protocol is named
__hurray__/from_hurray, and the capsule is named"hurray_tensor"/"used_hurray_tensor". The original text follows unchanged.
hurray.Tensor MUST expose a dunder method
__hurray_buffer__(stream=None) -> PyCapsule. The hurray module MUST expose
hurray.from_hurray_buffer(obj, /) -> hurray.Tensor that accepts any object whose
__hurray_buffer__ returns a valid capsule.
The dunder name __hurray_buffer__ mirrors the established pattern of __dlpack__,
__array__, and __cuda_array_interface__. The hurray_ prefix is namespace-safe:
dunders prefixed with a project identifier are an accepted convention (PyTorch's
__torch_function__, JAX's __jax_array__).
2. Layer placement: deferred to Layer 8c (post-FFI Python exposure)
The protocol MUST NOT be implemented in Layer 8a. Layer 8a ships the core
hurray.Tensor, DLPack, NumPy interop, and strict mode only. The hurray-ffi C ABI
is not yet exposed to Python through any documented interface in Layer 8a.
The native buffer protocol logically belongs in a layer downstream of Layer 8b (file
I/O bridge) and downstream of the work that exposes hurray-ffi to Python. This ADR
designates that layer Layer 8c — Native buffer protocol. Layer 8c may run in
parallel with or after the relaxed-mode implementation; it has no dependency on relaxed
mode.
In Layer 8a and 8b:
__hurray_buffer__MUST NOT be present onhurray.Tensor.hasattr(t, '__hurray_buffer__')MUST returnFalse.hurray.from_hurray_bufferMUST NOT exist as a public name.
The names are reserved by this ADR to prevent third-party squatting.
3. Mode gating: neither strict nor relaxed; available unconditionally in Layer 8c
The native buffer protocol is not an Array API construct, and ADR-022's mode scope
explicitly excludes everything except __array_namespace__ visibility and Tier 2 dtype
admission. Therefore:
- Once Layer 8c is shipped,
__hurray_buffer__MUST be present onhurray.Tensorinstances of all dtypes (Tier 1, Tier 2, quantized) in both strict and relaxed modes. - The Array API conformance test suite (
array-api-tests) does not probe__hurray_buffer__and is therefore unaffected.
A Hurray-aware consumer that needs a Tier 2 / quantized tensor's buffer in strict mode
still has a path: the native protocol. A non-Hurray-aware consumer that probes
__array_namespace__ still sees False for Tier 2 in strict mode, as required by
ADR-022. The two surfaces are orthogonal.
4. Spec placement: implementation-only
The native buffer protocol MUST be documented only in
docs/impl/python-bindings.md. It MUST NOT appear in docs/spec/buffer-protocol.md
or any other file under docs/spec/.
Justification:
- The protocol is a Python-only transport — it does not affect the wire format, the binary descriptor, the C ABI, or any non-Python binding.
- The wire payload of the capsule is the
HurrayBufferpointer defined inhurray-ffi; the C FFI implementation guide is already authoritative for that handle. - Putting the protocol in the format spec would create a normative obligation on all
bindings (C, Java, JS, etc.) to expose a "native" protocol — out of scope for
non-Python bindings that already pass
HurrayBufferdirectly through C.
5. Capsule lifetime: same discipline as DLPack, with Hurray-specific deleter
Amended by ADR-034: the capsule context is a
HurrayTensorContextfromhurray-ffi, readable from any language, and the ABI version is read throughhurray_tensor_context_abi_version. C ABI version 4.
The PyCapsule lifetime rules MUST match DLPack semantics:
- Capsule name on creation:
"hurray_buffer". - Capsule name after consumption:
"used_hurray_buffer". The consumer MUST rename the capsule before transferring ownership, exactly as DLPack consumers rename"dltensor"to"used_dltensor". - Capsule destructor (producer side): If the capsule is destroyed while its name is
still
"hurray_buffer"(the consumer did not take ownership), the destructor MUST callhurray_buffer_destroyon the wrappedHurrayBufferpointer, which invokes the registered release callback exactly once. - Consumer-side responsibility: Once the capsule has been renamed to
"used_hurray_buffer", the consuminghurray.Tensorowns theHurrayBufferand MUST callhurray_buffer_destroyexactly once at its own finalisation. - Source
hurray.Tensorreference counting: When__hurray_buffer__()is called, the sourceTensor's Python refcount MUST be incremented and stored as the capsule context; the destructor MUST decrement it. This mirrors the rule required for__dlpack__in § Buffer Lifetime and Ownership.
Note (non-normative): The Python refcount on the source
Tensorand theHurrayBufferinternal release callback are independent. The Python refcount keeps the producer-side Python object alive while the capsule exists; the release callback governs the underlying buffer memory.HurrayBufferis not internally refcounted at the C ABI; producers wanting multi-consumer fan-out MUST issue distinctHurrayBufferhandles per consumer.
6. stream parameter: same semantics as DLPack
__hurray_buffer__(stream=None) MUST accept an optional stream parameter with the
same semantics as __dlpack__(stream=None) in § Stream parameter semantics:
stream | Requirement |
|---|---|
None | The tensor MUST have SyncMode::ProducerSynced. |
-1 | The binding layer MUST perform a device-level synchronisation before returning the capsule. |
| Positive integer (stream handle) | If ProducerSynced, the stream is ignored. For SyncMode::Event or SyncMode::ConsumerStream, the binding MUST raise BufferError. |
7. Discovery: hasattr(tensor, '__hurray_buffer__')
Amended by ADR-033: the protocol is named
__hurray__/from_hurray, and the capsule is named"hurray_tensor"/"used_hurray_tensor". The original text follows unchanged.
Consumers MUST discover support by probing hasattr(obj, '__hurray_buffer__'). There
MUST NOT be a capability flag on the hurray namespace. This matches the discovery
convention of __dlpack__, __array__, and __cuda_array_interface__. A consumer
that detects __hurray_buffer__ MAY still fall back to __dlpack__ if it does not
link hurray-ffi. The two probes are independent.
8. Error semantics
Amended by ADR-034: the capsule context is a
HurrayTensorContextfromhurray-ffi, readable from any language, and the ABI version is read throughhurray_tensor_context_abi_version. C ABI version 4.
Amended by ADR-033: the protocol is named
__hurray__/from_hurray, and the capsule is named"hurray_tensor"/"used_hurray_tensor". The original text follows unchanged.
- A consumer receiving an object lacking
__hurray_buffer__MUST raiseTypeError. - If the wrapped
HurrayBufferpointer is null or the capsule name is not"hurray_buffer"(already consumed),hurray.from_hurray_bufferMUST raisehurray.BufferError. - ABI version mismatch between producer and consumer MUST raise
hurray.UnsupportedError. The capsule context MUST includeHURRAY_C_ABI_VERSIONfrom the producer; the consumer MUST verify it before dereferencing the handle.
Alternatives Considered
Option A: Extend DLPack upstream. Lobby for kDLROCMManaged, peer-memory enum
values, and a private-tag escape hatch in DLPack itself. Rejected: upstream
evolution is slow; DLPack's structural flat-enum constraint cannot absorb Hurray's
full (device_tag, memory_class, sync_mode) space without losing the protocol's
cross-library simplicity.
Option B: Reuse __dlpack__ with a sentinel device-type integer. Allocate a
private DLDeviceType value for "Hurray native" and embed the HurrayBuffer pointer
in DLTensor.data. Rejected: misuses a published cross-library protocol; silently
breaks DLPack consumers that do not recognise the sentinel value.
Option C: Per-instance opt-in via a constructor flag
(Tensor(..., expose_native=True)). Only tensors created with expose_native=True
carry __hurray_buffer__. Rejected: protocol availability belongs to the call
site, not the tensor instance — consistent with ADR-022's mode-scope decision. An
always-on dunder (once Layer 8c ships) is the uniform rule.
Option D: Implement in Layer 8a. Rejected: hurray-ffi is not yet reachable
from Python in Layer 8a. Implementing against a moving target adds risk with no
corresponding user value.
Option E: Different capsule lifetime discipline. For example refcount-only, no name
change. Rejected: the "name" → "used_name" rename is what makes DLPack's capsule
destructor safe under all consumption paths. Reinventing the discipline creates a
divergent mental model for binding authors.
Option F: Capability flag on the hurray namespace (e.g.,
hurray.NATIVE_BUFFER_PROTOCOL_VERSION = 1). Rejected: redundant atop the
hasattr probe; creates a synchronisation burden between the flag and the dunder.
Consequences
- Closes the DLPack representability gaps for ROCm
UNIFIED, allPEERmemory, and private device tags0xF0–0xFE. Hurray-aware peers can transfer these buffers losslessly. - Preserves DLPack as the cross-library protocol. Non-Hurray consumers (PyTorch,
JAX, NumPy) continue to use
__dlpack__unchanged. - Mode-orthogonal. Tier 2 / quantized tensors gain a buffer-sharing path in strict mode without relaxing Array API conformance.
- No spec churn.
docs/spec/buffer-protocol.mdis untouched. Non-Python bindings are unaffected. - Layer 8c added to the roadmap. Scope: one dunder, one constructor function, one capsule destructor, ABI version check, and associated tests. Small, contained, and user-approvable independently.
- ABI version coupling. Changes to
HurrayBufferinhurray-ffiaffect the capsule payload. Mitigated byHURRAY_C_ABI_VERSIONembedding and consumer-side verification.
Required Spec Amendments
The following amendments to docs/impl/python-bindings.md are required as a follow-up
by format-spec-writer. Do NOT apply them here.
-
New section
## Native Buffer Interchange Protocol(after## DLPack Interoperability, before## Buffer Lifetime and Ownership). Covers: dunder name, constructor, capsule names, lifetime rules,streamsemantics, ABI versioning, mode independence, discovery, and reference to ADR-023. -
Amendment to
## DLPack Interoperability§ mapping table notes: add a note pointing thehurray.UnsupportedErrorrows (ROCmUNIFIED, allPEER, private tags) to the native protocol as the recommended fallback for Hurray-aware consumers. -
Amendment to
### Layer 8a status: add bullets stating that__hurray_buffer__andhurray.from_hurray_bufferare reserved but not implemented in Layer 8a;hasattrreturnsFalsefor the dunder. -
Amendment to
## Error Handling: addhurray.BufferErrorfor null / already-consumed capsules passed tofrom_hurray_buffer; andhurray.UnsupportedErrorfor ABI version mismatches. -
Amendment to
hurray-python/COMPAT-MATRIX.md: add a column recording, per release, whether the native buffer protocol is available and the minimumHURRAY_C_ABI_VERSIONrequired.
Open Questions Deferred
- Multi-buffer tensors (
SparseTensor). Should__hurray_buffer__return a tuple of capsules or be restricted to dense tensors? RESOLVED by ADR-030: one capsule carries every buffer, wrapping aHurrayBufferList. The initial recommendation recorded here — restrict to dense and add__hurray_sparse_buffer__later — is superseded; it conflated multi-buffer with sparse, while dense per-channel-quantized, block-paged, and composite tensors are multi-buffer too. - Cross-process (IPC) variant. A capsule only works in-process; cross-process Hurray-native exchange would need an OS-handle-based protocol. Deferred to a later layer.
- Async counterpart. Whether an
__ahurray_buffer__async variant is needed. Deferred. Initial recommendation: no;sync_modealready handles the synchronisation contract per-buffer.