crates/xlog-cuda) also offers the Arrow C Device
interface and host-serialized Arrow IPC.
Zero-copy versus copy at a glance
The distinction is the whole point of this page. A zero-copy path hands the consumer a pointer to the same device buffer XLOG already holds — no bytes move. A copy path moves bytes into a different allocation. Persistent DLPack ownership and retention copies stay on the GPU; Arrow IPC instead serializes through host memory and pays a device-to-host-to-device round trip for GPU-to-GPU interchange.Arrow IPC and the Arrow C Device interface are both “Arrow,” but they sit on
opposite sides of the copy boundary. IPC is a host-serialized byte stream; the C Device
interface passes CUDA device pointers. Reach for IPC only when the consumer genuinely
needs host-side Arrow (a
pyarrow.Table, a file on disk); reach for the C Device
interface or DLPack when the data must stay on the GPU.DLPack — zero-copy handoff and owned persistence
DLPack is the most direct way to share a column with a tensor framework. XLOG exports each relation column as a contiguous 1-D device tensor and consumes DLPack tensors the same way, so a query result flows into PyTorch without leaving the GPU:cols[0].device
reports cuda (the same device XLOG computed on). A host round-trip would surface as
cpu instead.
Input relations travel the same path in reverse. evaluate(dlpack_inputs=...) takes a
mapping from relation name to a sequence of 1-D DLPack columns — one tensor per column,
never a single 2-D tensor:
to_dlpack_table for export and
from_dlpack_tensors / from_dlpack_tensors_with_schema for import — the latter
validates the incoming columns against a declared schema instead of inferring it.
Persistent LogicRelationSession replacements (put_relation,
put_relation_with_provenance, and put_relation_from_manifest) take one
device-to-device snapshot after DLPack import. The session must own stable rows:
a producer tensor remains usable under the DLPack sharing contract, so retaining
its pointer would let later in-place writes bypass relation versions, callbacks,
and evidence validation. Transient evaluate(dlpack_inputs=...) inputs remain
zero-copy, as does wrapping a query-result capsule in the consumer framework.
Exporting a stored persistent relation first leaves a device-to-device clone in
the session so it can transfer ownership of the exported buffer; consuming the
returned capsule itself adds no further copy.
For each tensor-like input, XLOG calls __dlpack_device__() exactly once. Only
CUDA device memory (kDLCUDA) is accepted; another device raises BufferError
before any capsule request. XLOG then calls __dlpack__(stream=1) exactly once,
using the legacy default stream so pending producer work completes before XLOG
reads it. If that call fails, XLOG propagates the exception without retrying.
A raw capsule bypasses both calls and cannot perform the handshake: create it for
stream 1 or synchronize its producer before passing it to XLOG. Raw capsules
are single-consumer objects, and the native importer still validates the device
header. Capsules exported by XLOG are already ready for this consumer stream.
Provenance-aware DLPack round trips
A persistent logic session can export DLPack columns together with the native roles and whole-fact evidence needed to reconstruct the relation in another compatible session:target.relation("edge") returns a native RelationEvidence snapshot after the
import. Invalid role, whole-fact provenance, insert-evidence, or manifest input raises
pyxlog.RelationMetadataError, a ValueError subclass. relation(name) and
evidence(name) raise KeyError for an unstored relation, while
export_relation_with_provenance(name) raises ValueError. Without pyxlog._native,
both package-level type names remain importable: the fallback metadata error subclasses
ValueError, and constructing fallback RelationEvidence raises RuntimeError.
Native instances and session operations require the extension. Source builds with the
extension expose this API; packaged availability is determined by release notes.
The manifest has a strict, versioned shape:
metadata_present must be a Python bool; when false, roles and facts are empty and
the import resets any positional role contract. Each exact cell has only type and
hex: the type matches the compiled column, the lowercase hex has exactly the scalar’s
byte width, and a boolean is 00 or 01. A non-null span has only non-negative integer
start and end values (not booleans), both representable as u64, with
start <= end. Nullary relations reject both provenance-manifest import and export.
Import checks the predicate name, arity,
compiled schema fingerprint, fact identities, DLPack column contract, row count, and
complete-fact membership before replacing the target relation.
The schema fingerprint is a domain-separated SHA-256 digest of the predicate name and
arity plus each compiled argument’s ordered name, scalar type, and optional source-domain
alias. Each fact identity separately hashes the predicate name and arity plus every
ordered cell’s scalar type, byte length, and exact little-endian bytes. It is independent
of row position, role labels, and provenance records, and import recomputes it rather
than trusting the manifest string.
Fact order, record order, repeated fact entries, and exact duplicate records are
normalized deterministically. Different records attached to the same complete tuple are
preserved. A failed import leaves the target rows and evidence unchanged.
Joint-solver carrier buffers — writing into XLOG’s own allocation
The paths above move relation columns.pyxlog.JointConstraintCarrier is a different
shape of interop: it is a device-resident workspace for XLOG’s joint constraint solver,
and it lets a model write scores straight into the buffers the solver will read, with no
intermediate relation and no copy.
Create the carrier, register a schema, bind the per-label signature masks, then export
the buffer you want to fill:
export_buffer(name) accepts domains, scores, constraints, outputs,
feasible_sets, logical_counts, map_results, and solve_status; any other name
raises ValueError. XLOG owns every one of these allocations, so a view is valid only
while the carrier is alive — this is the opposite of the relation-export capsules above,
which keep their buffers alive on their own.
Two exceptions belong to this surface, both RuntimeError subclasses:
pyxlog.CarrierRefused— a typed refusal whose message names the exact precondition you broke and its values. It covers registering a schema twice, a zero-valued capacity dimension, binding signatures or solving beforeregister_schema, rebinding already-bound masks, a mask whose word count does not match the carrier’s capacity, running the top-two stage before feasibility, a malformed component plan, an out-of-range abstain label, and a joint-solve kernel that would not load on this device.pyxlog.SolverResourceExhausted— the fuel budget you passed to the constructor ran out, reported assolver fuel exhausted: spent {spent} of {limit} node expansions. The meter lives in the carrier session and saturates, so a retry repeats the identical refusal instead of slipping more work past the budget. Read the running total from thefuel_spentproperty.
Arrow C Device interface — zero-copy Arrow
When the consumer speaks Arrow but you still want to stay on the GPU, the Arrow C Device interface exports device-resident columns as anArrowDeviceArray carrying CUDA
device pointers. The device descriptor reports device_type = ARROW_DEVICE_CUDA and the
originating device_id, so a RAPIDS consumer knows the buffers are already on the
right device.
Export covers u32, u64, i32, i64, f32, f64, bool (bit-packed), and
symbol (exported as UInt32). Symbol columns carry xlog.symbol=true and
xlog.symbol_encoding=u32 in their schema metadata so the interned-string encoding
survives the boundary. Ownership stays explicit: ArrowDeviceArrayOwned keeps the GPU
buffers alive until the FFI handle is released.
Import over the same interface is experimental and compiled out by default. Build
pyxlog with the arrow-device-import feature to enable it:
ArrowDeviceArrayOwned’s device pointers as XLOG
columns without copying. It accepts numeric types and Symbol (as UInt32 marked
xlog.symbol=true); it currently rejects nulls and does not yet accept
bit-packed Bool.
Arrow IPC — the host-copy path
Arrow IPC is the interoperability escape hatch: it serializes relations to a standard Arrow stream that any Arrow-native tool can read, including plain CPUpyarrow. It is
not zero-copy — export downloads GPU to host, and import uploads host to GPU. Use it
when you need a portable artifact or a host-side table, not on a hot path.
The Rust side writes a stream file with write_arrow_ipc_stream_file; Python reads it
back through pyarrow and, if desired, up-loads it into cuDF:
from_arrow step is exactly the host-to-GPU copy the zero-copy paths avoid — a
reminder to prefer DLPack or the Arrow C Device interface whenever the consumer can take
device pointers directly.
How results stay on the GPU
Why zero-copy interop matters — XLOG keeps semantic state device-resident so a
downstream tensor computation reads its output without a host round-trip.