DLPack Tensor Exchange#
ovstage exchanges attribute values as DLPack DLTensor s across three paths:
copy-in writes (write_attribute), copy-out reads (read_attributes →
fetch_read_next), and a zero-copy map/unmap path that fills ovstage-owned
storage directly. Tensors can be CPU- or CUDA-resident.
Copy-In Write and Copy-Out Read#
The minimal example builds a DLTensor over a CPU buffer, writes it at an
ordinal, seals the ordinal, and reads the column back:
# Write one float per prim into the "temperature" column at ordinal 1,
# seal it by advancing the write floor to 1, then read it back. Tensor
# data crosses as a numpy array (CPU) via DLPack; async ops return an
# Operation whose .wait() raises OvstageError on failure. The read is
# a context manager -- block exit releases its handle even when an
# error interrupts -- and the fetched group is released in a finally.
stage.write_attribute(
query, attr, ordinal=1, tensors=np.array([1.0, 2.0, 3.0], np.float32), is_array=False
).wait()
stage.advance_write_floor(ordinal=1).wait()
with stage.read_attributes(query, [attr], OrdinalRange.latest(1)) as read:
read.wait()
group = read.fetch_next()
if group is None:
raise SystemExit("read returned no group at ordinal 1")
try:
# group.array(i) is a zero-copy numpy view of tensor i (CPU).
print("read back ordinal", group.ordinal, group.array(0)) # -> [1. 2. 3.]
finally:
stage.release_group(group)
// Write one float per prim into the "temperature" column (UPSERT creates
// the prims on first write), seal it by advancing the write floor to
// ordinal 1, then read the column back.
float values[] = { 1.0f, 2.0f, 3.0f };
int64_t shape[] = { 3 };
int64_t strides[] = { 1 };
DLTensor tensor{};
tensor.data = values;
tensor.device = { kDLCPU, 0 };
tensor.ndim = 1;
tensor.dtype = { kDLFloat, 32, 1 }; // {code, bits, lanes}
tensor.shape = shape;
tensor.strides = strides;
ovstage_write_data_t write{};
write.tensors = &tensor;
write.tensor_count = 1;
write.is_array = false;
ovstage_enqueue_result_t enq =
ovstage_write_attribute(stage, query, attrArg, /*ordinal*/ 1, write, OVSTAGE_PRIM_MODE_UPSERT);
waitOp(stage, enq, "write_attribute");
ovstage_write_floor_desc_t writeFloor{};
writeFloor.ordinal = 1;
writeFloor.scope = OVSTAGE_SCOPE_ALL;
enq = ovstage_advance_write_floor(stage, &writeFloor);
waitOp(stage, enq, "advance_write_floor");
ovstage_ordinal_range_t range{};
range.end_ordinal = 1;
range.has_start_ordinal = false;
ovstage_read_handle_t read = OVSTAGE_INVALID_READ_HANDLE;
enq = ovstage_read_attributes(stage, query, &attr, 1, range, &read);
waitOp(stage, enq, "read_attributes");
ovstage_read_group_t group{};
status = ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group);
check(stage, status, "fetch_read_next");
if (group.data.tensor_count != 1 || !group.data.tensors[0].data)
{
std::fprintf(stderr, "unexpected read layout\n");
return EXIT_FAILURE;
}
const float* out = static_cast<const float*>(group.data.tensors[0].data);
std::printf("read back ordinal %llu: %.1f %.1f %.1f\n", (unsigned long long)group.ordinal, out[0], out[1], out[2]);
ovstage_release_group(stage, &group); // the tensor data is only valid until the group is released
For a write, exactly one of tensors (client-managed; must stay valid until
the op completes) or managed_tensors (storage takes ownership through the
DLManagedTensorVersioned deleter) is non-NULL.
In Python, the bindings accept and return DLPack-compatible arrays, so NumPy,
PyTorch, and Warp tensors interchange through from_dlpack with the same
residency model.
GPU Residency and Synchronization#
cuda_sync (ovstage_cuda_sync_t) coordinates GPU producer/consumer
ordering, and remains the caller’s responsibility:
typedef struct {
uintptr_t stream; /* 0 = none, 1 = the default stream, >1 = a specific cudaStream_t */
uintptr_t wait_event; /* 0 = none, else a cudaEvent_t to wait on */
} ovstage_cuda_sync_t;
The two fields are independent knobs — set either, both, or neither; each non-zero field adds its own synchronization before the op:
stream—0= none;1= the default stream;>1= a specificcudaStream_t. When set, all work currently queued on that stream is drained.wait_event—0= none; otherwise acudaEvent_tthat is waited on.{0, 0}— no synchronization (CPU-resident or already-synchronized data).
On a read, the synchronization is applied before your code accesses the returned data. On a write / unmap, it is what ovstage waits on before sealing your data.
Sparsity#
index_map (gather / reorder / dedup) and mask (per-element validity) are
mutually exclusive. Set count (the logical element count) whenever either
is present.
Zero-Copy Map/Unmap#
Note
The map/unmap path below is documented from the shipped headers; the current examples cover only the CPU, copy-in path, so this flow is not yet snippet-backed. Treat the sequence as an API reference and refer to C API Reference for exact signatures.
Instead of copying data in, a producer can fill ovstage-owned storage directly:
ovstage_map_attributewith anovstage_map_desc_t({ attribute; dtype; semantic; prim_mode }) — creating a new column requiresdesc.dtype.ovstage_fetch_map_nextto obtain each map group.Fill
group.data.tensors[i].datain place.ovstage_unmap_groupper group, thenovstage_unmap_attributeto commit the remainder and release, passing awrite_done_sync(anovstage_cuda_sync_t).
All map/unmap ops are ordinal-keyed at the session ordinal. Changing an
existing prim/name’s dtype or semantic fails — delete the attribute first.
Note
This build retains only the latest committed snapshot. Returned tensor data is valid for that snapshot only — copy, retain, or transfer ownership to use it later; do not hold a borrowed pointer across further commits.
Where to Go Next#
Writing Attributes / Reading Attributes — the write and read paths in context.
Asynchronous Submit/Observe Model — the enqueue/wait model these exchanges ride on.