# Ring-Backpressured Data Movement - Canonical Specification **Status:** design specification. This document supersedes the previous root workflow drafts for edge establishment, blob streaming, driver streams, stream transport, and arena management: - `EDGE_ESTABLISHMENT.md` - `BLOB_STREAMING.md` - `STREAM_TRANSPORT.md` - `DRIVER_STREAMS.md` - `ARENA_MANAGEMENT.md` `DESIGN_DIRECTIVES.md` remains steering context. This file is the buildout specification. **Scope.** Node-local host memory, process-crossing rings, worker loading, edge establishment, persistent QUIC streams, actor/control messages, safety contracts, backpressure, and teardown. This covers activations, weights, and other large objects moving between disk, network, host memory, and GPU memory. **Out of scope.** Compute overlap with partial tensors, placement policy, churn placement, trustless verification, object-store semantics, and high-level model graph scheduling. This spec permits transfer and VRAM upload to overlap with network streaming. It deliberately does not permit matmuls or other compute to observe an object until the whole logical object is loaded. --- ## 1. Shape A node is an async backpressure machine built from one stable host-memory arena and many bounded byte rings. The arena is address space. It is one large sparse `memfd`, created by the Rust node process, mapped once by the Rust node process, and mapped once by the GPU worker process. It does not define flow control and it does not define object ownership. Rings define flow control. Every payload-moving boundary is represented as a bounded single-producer/single-consumer byte ring backed by a lease inside the arena: - QUIC ingress stream -> host ingress ring -> worker -> GPU memory - GPU memory -> worker -> host egress ring -> QUIC egress stream - disk reader -> host ring -> worker/GPU memory - future GPU download/upload and network paths follow the same ring contract Actors establish, supervise, and tear down rings. Actors do not move payload bytes, do not relay per-range readiness, and do not track per-byte ownership. Once a ring is active, the hot path is shared ring metadata plus coalesced wake hints: ```text producer writes bytes into ring producer advances commit cursor producer sends/coalesces a readable wake hint consumer wakes consumer drains every committed byte it can use consumer advances consume cursor consumer sends/coalesces a writable wake hint if space was released ``` The wake hint is a signal that ring state may have changed. It carries a `ring_id` and a reason such as readable or writable, not byte counts or ranges. The producer may send it as soon as bytes are committed; it does not wait for the ring to fill. Duplicate hints may be coalesced through a ready set or pending bit, but a ring that has become readable or writable must remain discoverable until the other side reloads the cursors. The cursors are the state. The worker can load ingress into VRAM while the stream is still arriving. It does that by greedily consuming the committed prefix of the ingress byte ring and copying those bytes into the correct offsets of a device allocation. The object becomes compute-visible only after all expected bytes have been copied and the worker has observed object completion. --- ## 2. Vocabulary **node process** - The Rust process hosting the swactor runtime, actors, driver, arena manager, iroh endpoint, pump tasks, and worker supervisor. **GPU worker** - A separate OS process, normally Python plus tinygrad and a small native ring helper. It maps the arena, receives coalesced ring wake hints, parses object streams, copies payload bytes into or out of GPU memory, and emits coarse events. **arena** - One sparse `memfd` reservation per node. The node process and worker process map the same bytes. Arena offsets are stable for the node lifetime. **arena lease** - A non-overlapping byte range in the arena assigned to one ring. The ArenaManager mints leases and releases them after quiescence. **ring** - A bounded single-producer/single-consumer byte stream backed by an arena lease. A ring has shared metadata, data bytes, one producer, one consumer, and coalesced wake hints. **edge** - A one-way typed conduit from one role to another role. An edge has one producer node, one consumer node, one `edge_id`, and one persistent QUIC uni-stream once data begins flowing. **`edge_id`** - A run-global edge identifier assigned by the orchestrator. It is the control-plane name of the edge and the fixed-width stream preamble used by the receiver's driver demux. **object** - One logical payload on an edge, such as an activation tensor, a weight tensor, a token batch, or a model shard. Objects are sent as object records inside the edge byte stream. **extent** - The actual byte length of an object. It is a runtime fact and may be smaller than the edge's maximum object capacity. **object spec** - The role-known contract for objects on an edge: maximum extent, dtype family, shape/layout rules, object kind, and any alignment requirements. The worker uses this spec to turn raw bytes into a correctly shaped device allocation. **ring spec** - The size and operating parameters of a ring: data capacity, alignment, optional host-pinning requirement, wake coalescing mode, and whether the ring is ingress or egress. **pump** - A driver-owned async task. A recv-pump copies QUIC bytes into an ingress ring. A send-pump copies egress ring bytes onto QUIC. Pumps do not parse payload objects after the `edge_id` preamble. **Tx/Rx edge actors** - Small swactor actors representing the local edge end. They hold edge identity and lifecycle state, receive coarse completion/fault events, and integrate with the role layer. They do not receive per-byte or per-range messages. --- ## 3. Process Topology Each node has two payload-relevant OS processes: ```text node process (Rust) GPU worker process ------------------- ------------------ swactor runtime tinygrad / CUDA ArenaManager actor ring parser EdgeEstablisher actor device allocator Tx/Rx edge actors host-to-device / device-to-host copies Driver mailbox iroh endpoint recv/send pumps shared memfd arena, mapped by both processes actor/control messages for install, wake hints, and coarse events ``` Host-to-device means copying from arena-backed host memory into GPU memory. Device-to-host means copying from GPU memory into arena-backed host memory. The node process creates the `memfd` without `CLOEXEC` before spawning the worker, or otherwise passes the fd explicitly during worker startup. The worker maps the same reservation once. Neither process remaps the arena during node lifetime. For a process-crossing ring: - `RingReadable { ring_id }` wakes the consumer after the producer commits bytes. - `RingWritable { ring_id }` wakes the producer after the consumer releases bytes. For ingress, the producer is the Rust recv-pump and the consumer is the worker. For egress, the producer is the worker and the consumer is the Rust send-pump. The worker control pipe carries lifecycle messages, wake hints, and coarse events such as `InstallRing`, `RingInstalled`, `ObjectLoaded`, and `RingFault`. It does not carry payload bytes, byte counts, or per-range ownership. --- ## 4. Arena Manager The ArenaManager is the single per-node authority for arena layout. ### 4.1 Responsibilities The ArenaManager owns: - the arena `memfd` - the node process mapping base pointer - the reservation ceiling - the arena free-list - the pending lease queue - the table of live ring leases The ArenaManager does not own: - any worker process - any QUIC stream - any pump task - any object parser - any payload byte It never reads or writes payload bytes. It only mints stable offsets. ### 4.2 Boot At node boot: 1. The ArenaManager creates the `memfd`. 2. It truncates it to a generous sparse ceiling. 3. It maps the whole reservation once in the node process. 4. It exposes the base pointer to the driver and ring constructors. 5. It makes the fd available to the worker process at spawn. The reservation costs virtual address space. Physical pages are backed lazily by the kernel when touched. The mapping is not moved or resized. Any offset minted by the ArenaManager remains meaningful until the node shuts down. ### 4.3 FSM ```text Booting on ConstructArena{ceiling} -> Ready if memfd, truncate, and mmap succeed -> Failed if any boot resource cannot be created Ready on LeaseRing{request_id, requester, edge_id, direction, ring_spec} -> lease immediately and emit RingLeased if a range fits -> enqueue request if the request is satisfiable but no current range fits -> emit RingLeaseRejected if the ring_spec can never fit in the ceiling Ready on CancelLease{request_id} -> remove queued request if it has not been leased yet Ready on ReleaseRing{ring_id, proof} -> return range to free-list -> retry queued leases serially Ready on Shutdown -> ShuttingDown ShuttingDown no new leases are accepted ``` ### 4.4 Messages Inbound: ```rust ConstructArena { ceiling: u64, } LeaseRing { request_id: LeaseRequestId, requester: ActorAddress, edge_id: EdgeId, direction: RingDirection, ring_spec: RingSpec, } CancelLease { request_id: LeaseRequestId, } ReleaseRing { ring_id: RingId, proof: QuiescenceProof, } Shutdown ``` Outbound: ```rust ArenaReady { base_ptr: NonNull, ceiling: u64, } RingLeased { request_id: LeaseRequestId, requester: ActorAddress, edge_id: EdgeId, ring_id: RingId, direction: RingDirection, arena_offset: u64, layout: RingLayout, } RingLeaseRejected { request_id: LeaseRequestId, requester: ActorAddress, edge_id: EdgeId, reason: LeaseRejectReason, } ``` There is no temporary allocation-failure message. Temporary pressure is encoded by absence of `RingLeased`: the request waits in the lease queue. Permanent impossibility is explicit because no future release can make an oversized ring fit. A queued request can be cancelled by `request_id` if the edge establishment record stops before the lease is granted. ### 4.5 Correctness Two live leases cannot overlap because all lease and release operations pass through one ArenaManager mailbox. A handler mutates the free-list to completion before the next handler runs. The allocator either removes one complete range from the free-list and records it in the live table, or it leaves the free-list unchanged and queues/rejects the request. There is no state in which a partial lease is visible downstream. A pump or worker cannot observe an unleased range because `RingLeased` is the only message that contains a usable ring offset. Edge establishment does not install a worker ring or spawn a pump until that message exists. A range cannot be reused under a live pump or worker because `ReleaseRing` is a proof, not a request. The EdgeEstablisher emits it only after the driver has stopped the pump, the worker has uninstalled the ring, and in-flight DMA for the ring has completed or been abandoned with the worker process dead. The ArenaManager does not infer quiescence; it relies on the upstream teardown FSM to earn the proof. Queued lease requests cannot corrupt establishment because the requester receives nothing while queued. No ring offset exists, so no driver or worker hot-path state can be created for that ring. If a stop races with a grant, the EdgeEstablisher accepts `RingLeased` only when the matching edge record is still waiting on the same `request_id`; otherwise it releases the unused lease without installing a worker ring or spawning a pump. --- ## 5. Ring Contract A ring is a bounded SPSC byte stream in shared memory. It is the universal payload handoff primitive. The process-crossing ring is a fixed shared-memory ABI, not a Rust collection placed inside the arena. In-process queues such as `crossbeam_queue::ArrayQueue` may be used for local actor channels, ready sets, or wake scheduling, but the arena ABI stores only offsets, cursors, state bits, and payload bytes. This keeps the mapped bytes valid even when the node process and worker process map the same `memfd` at different virtual addresses. ### 5.1 Single Producer, Single Consumer Each ring has exactly one producer and one consumer. Ingress: ```text producer = recv-pump consumer = worker ``` Egress: ```text producer = worker consumer = send-pump ``` Disk or future GPU rings follow the same rule. Fan-in or fan-out is represented by multiple rings or by a higher-level mux/demux component that itself owns one side of a ring. A ring never has multiple hot-path producers or consumers. ### 5.2 Shared-Arena ABI The ring header lives in shared memory and is aligned for cross-process atomic operations. ```rust #[repr(C, align(64))] struct RingHeader { magic: u32, version: u16, header_len: u16, ring_id: u64, capacity: u64, commit: AtomicU64, consume: AtomicU64, state: AtomicU32, wake: AtomicU32, } ``` `ring_id` is unique for the node lifetime. Arena ranges may be reused after quiescence, but ring identifiers are not reused. Stale control or wake events therefore cannot alias a later ring that happens to occupy the same arena range. `commit` is the first byte after the committed readable prefix. Bytes with logical positions `< commit` are valid for the consumer to read. `consume` is the first byte not yet released by the consumer. Bytes with logical positions `< consume` are free for the producer to reuse. The producer also keeps a local `write` cursor. `write` is the first byte after the producer's reserved or in-progress write prefix. It is not shared with the consumer because bytes in `commit..write` are not readable yet. The readable interval is: ```text consume .. commit ``` The producer-owned reserved interval is: ```text commit .. write ``` Cursor values are monotonic logical byte positions. The physical byte index is: ```text physical_index = cursor % capacity ``` The ring is empty when `consume == commit`. The ring is full when: ```text write - consume == capacity ``` At producer startup for a fresh ring, `write == commit == consume`. After a producer reserves a span for an async read or copy, `write` may be greater than `commit`. The consumer still cannot read the reserved bytes because `commit` has not advanced. The `state` word records coarse lifecycle state such as active, closing, closed, or faulted. The `wake` word records coalescing bits such as readable-pending and writable-pending. Wake bits are hints for scheduling; `commit` and `consume` are the ownership authority. ### 5.3 Native Helper Surface Both Rust hot-path code and the Python worker access process-crossing rings through the same native implementation. Python does not implement shared atomics, wrap arithmetic, or cursor publication directly. The helper exposes operations equivalent to: ```text ring_readable_span(handle) -> ptr, len ring_advance_consume(handle, len) ring_writable_span(handle) -> ptr, len ring_advance_commit(handle, len) ring_state(handle) -> state ``` Returned pointers are process-local addresses derived from the caller's mapped arena base plus arena offsets. The shared header never stores process-local pointers. ### 5.4 Producer Rule The producer may write only into free space: ```text free = capacity - (write - consume) ``` Before writing, the producer acquires `consume`. It computes free space against its local `write` cursor, reserves a contiguous physical span by advancing `write`, and then writes into that reserved span. If the ring wraps, the producer reserves at most to the end of the physical buffer, commits that prefix after it is written, and then reserves the wrapped span. After bytes are written, the producer stores the new `commit` with release ordering and sends or coalesces a readable wake hint. The producer may advance `commit` after any successful network read, disk read, or GPU copy. It does not wait for a complete tensor, complete object, complete range, or complete frame before committing newly valid bytes. ### 5.5 Consumer Rule The consumer acquires `commit` and may read any byte in: ```text consume .. commit ``` The consumer greedily drains all bytes it can make progress on. For the worker, "make progress" means: - parse complete control headers when enough bytes are available - copy available payload bytes into the correct device allocation - stop only when the ring is empty, the next header is incomplete, the target device allocation cannot currently accept more bytes, or the ring is faulted After the consumer no longer needs a prefix of bytes, it stores the new `consume` with release ordering and sends or coalesces a writable wake hint if the release may unblock the producer. If the consumer uses asynchronous DMA from host memory, it cannot release bytes until the DMA no longer reads those bytes. The MVP uses synchronous copy or event-tracked asynchronous copy that advances `consume` only after the copy event completes. ### 5.6 Wakeups Wakeups are edge-trigger hints, not state. The producer sends `RingReadable { ring_id }` after advancing `commit` when the consumer may be asleep. The consumer must still load `commit` from the ring header, because several commits may have coalesced into one wakeup. The consumer sends `RingWritable { ring_id }` after advancing `consume` when the producer may be blocked on free space. The producer must still load `consume` from the ring header, because several releases may have coalesced into one wakeup. Wake coalescing must preserve liveness. Dropping duplicate wake hints is allowed only while a pending bit, ready-set entry, or equivalent durable scheduler state still makes the ring discoverable. Losing the only transition from empty to readable, or from full to writable, is a liveness bug even though it does not corrupt memory. The cursors remain the source of truth for ownership. ### 5.7 Safety The consumer cannot read unwritten bytes because the producer publishes `commit` only after the bytes have been written, and the consumer reads `commit` with acquire ordering before reading the bytes. The producer cannot overwrite unread bytes because it computes free space from the consumer-owned `consume` cursor and the producer-local `write` cursor. Bytes at or after `consume` remain unavailable for reuse until the consumer advances `consume`; bytes in `commit..write` are also unavailable because the producer has reserved them but not yet published them. Two consumers cannot both consume the same byte because a ring has one consumer and only that consumer writes `consume`. Any design needing two consumers must split the stream into two rings or insert an explicit fan-out component. Wraparound cannot cause stale bytes to be mistaken for new bytes because ownership is determined by monotonic logical cursors, not by physical indices. The same physical index can be reused only after `consume` has advanced past the previous logical byte range that occupied it. Cursor overflow cannot occur during a node lifetime if the implementation treats the `u64` logical cursor space as a runtime ceiling and tears the ring down before approaching wrap. A ring carrying 1 TB/s would take centuries to exhaust `u64` byte positions, so this is an operationally unreachable limit for the intended workloads. --- ## 6. Object Stream Protocol The ring carries a byte stream. The stream contains small object headers and raw payload bytes. Headers define how the worker maps subsequent payload bytes into VRAM. Payload bytes are not actor messages. ### 6.1 Edge Stream Each edge uses one persistent QUIC uni-stream from producer node to consumer node. Wire shape: ```text [edge_id preamble] [object record] [object record] ... ``` The receiver's edge-demux reader consumes the fixed-width `edge_id` preamble and hands the stream to the driver rendezvous. After that preamble, the recv-pump is byte-blind. It copies stream bytes into the ingress ring and advances `commit`. The worker parses object records from the ring. ### 6.2 Object Record An object record is: ```text ObjectHeader payload bytes, exactly header.extent bytes ``` The header is fixed-size in the MVP so the worker can parse it without heap allocation: ```rust struct ObjectHeader { magic: u32, version: u16, header_len: u16, object_id: u64, sequence: u64, extent: u64, flags: u32, reserved: u32, } ``` The edge's `ObjectSpec` supplies dtype, shape family, max extent, layout, and alignment. The header supplies runtime facts: which object this is, its sequence on the edge, and its actual extent. `extent` may be larger than the ingress ring capacity. That is normal. The ring is a transfer window, not the object storage location. ### 6.3 Parsing Partial Records Headers and payloads may arrive partially. The worker parser therefore has two states per ingress ring: ```text NeedHeader NeedPayload{object_id, remaining, device_offset} ``` In `NeedHeader`, the worker waits until at least `header_len` committed bytes are available. It may copy those header bytes into a small parser scratch buffer and release them. Header bytes are control, not payload. In `NeedPayload`, the worker copies any committed payload prefix into the target device allocation. It does not wait for the full payload. If 60 percent of a large object has arrived and the worker is free, it can copy that 60 percent to VRAM, release the ring space, and let the recv-pump continue reading the remaining 40 percent. An object is complete when the worker has copied exactly `extent` payload bytes for that object and any required copy-completion event has fired. ### 6.4 Validation The worker rejects a record before allocating or exposing a device object if: - `magic` or `version` is not supported - `extent > ObjectSpec.max_extent` - `extent` violates the edge's alignment/layout rules - the sequence violates the edge ordering policy - the ring closes before `extent` bytes arrive These checks prevent malformed control bytes from becoming a visible tensor. The payload content itself is trusted. The worker does not inspect tensor values. --- ## 7. Worker Design The worker is the consumer of ingress rings and the producer of egress rings. It is responsible for translating raw stream bytes into GPU-resident objects. ### 7.1 Worker Control Surface The control pipe carries lifecycle messages: ```rust InstallRing { ring_id: RingId, direction: RingDirection, layout: RingLayout, object_spec: ObjectSpec, } UninstallRing { ring_id: RingId, } AbortObject { ring_id: RingId, object_id: u64, reason: AbortReason, } RingReadable { ring_id: RingId, } RingWritable { ring_id: RingId, } Shutdown ``` Worker outbound control events and wake hints: ```rust RingInstalled { ring_id: RingId, } ObjectLoaded { ring_id: RingId, edge_id: EdgeId, object_id: u64, device_handle: DeviceObjectHandle, } ObjectProduced { ring_id: RingId, edge_id: EdgeId, object_id: u64, } ObjectFailed { ring_id: RingId, edge_id: EdgeId, object_id: u64, reason: ObjectFailure, } RingFault { ring_id: RingId, reason: RingFaultReason, } RingQuiesced { ring_id: RingId, } RingReadable { ring_id: RingId, } RingWritable { ring_id: RingId, } ``` There is no `ReadyRange`, `ConsumedRange`, `LandedRange`, or `FreeBytes` control message. Wake hints name a ring, not a byte range. Per-range messages would put the actor/control path back into the hot loop. ### 7.2 Ingress Worker FSM Each ingress ring has an independent worker-side FSM: ```text Uninstalled on InstallRing -> NeedHeader NeedHeader on committed bytes < header_len -> wait on complete header -> validate and allocate device object -> NeedPayload on invalid header -> Faulted NeedPayload on committed payload bytes -> copy greedy prefix to device on copied bytes == extent -> ObjectComplete on stream/ring abort -> Faulted ObjectComplete wait for outstanding copy completion emit ObjectLoaded -> NeedHeader Faulted emit RingFault or ObjectFailed stop consuming until control resolves or uninstalls the ring ``` The worker greedily drains. Wake delivery maintains a ready set, typically backed by an in-process `ArrayQueue` plus per-ring pending bits, so the worker drains rings that were reported readable instead of scanning every installed ring on each wake. It continues until no ready ring can make progress. ### 7.3 Device Assembly On a valid `ObjectHeader`, the worker creates an assembly record: ```rust struct ObjectAssembly { object_id: u64, sequence: u64, extent: u64, object_spec: ObjectSpec, device_allocation: DeviceAllocation, bytes_copied: u64, copies_in_flight: CopyTracker, } ``` `ObjectSpec` tells the worker how to interpret the raw bytes: - dtype width and dtype family - shape rule for turning `extent` into rows/tokens/elements - memory layout expected by the compute role - alignment requirements - maximum extent For activations, the shape family is known at provisioning and `extent` selects the runtime row/token count. For weights, the extent and shape may be fixed by the role or model shard. The worker does not deserialize through Python objects; it creates or reserves a device buffer whose byte layout matches the role's expected tensor layout. The required primitive is a range copy into a device allocation: ```text arena[ring_span] -> device_allocation[object_offset .. object_offset + len] ``` The worker may implement this through a lower-level tinygrad device buffer API, a native CUDA helper, or another backend-specific range-copy primitive. A high-level API that only supports "copy this entire host buffer into this entire tensor" is insufficient for ingress streaming, because the object may be larger than the host ring and the worker must copy partial committed prefixes. ### 7.4 Host Memory and DMA Safety If the worker uses synchronous host-to-device copies, it may advance `consume` immediately after the copy call returns. If it uses asynchronous copies, it may advance `consume` for a byte range only after the copy no longer depends on that host memory. With pinned host memory, that means recording the CUDA/event backend completion and releasing the range after the event fires. Without this rule, the recv-pump could overwrite a ring span still being read by DMA. The overwrite cannot occur when `consume` is held back until copy completion, because producer free space is computed from `consume`. ### 7.5 Compute Visibility The role/compute layer receives an object only after `ObjectLoaded`. `ObjectLoaded` is emitted after: 1. a valid object header was parsed 2. exactly `extent` payload bytes were copied into the device allocation 3. all copy events for those bytes completed 4. the resulting device allocation was associated with the role's expected dtype, shape, and layout Because no compute-visible handle exists before `ObjectLoaded`, compute cannot observe a partially loaded object. --- ## 8. Driver and Transport The driver is the node's swactor-to-iroh boundary. It owns the endpoint, connection cache, edge demux, and pump tasks. ### 8.1 Connection Model The node uses one iroh endpoint. Edge streams use a dedicated ALPN, for example: ```text swactor/edge/1 ``` Connections are cached per `(peer_node_id, ALPN)`. A QUIC connection is the transport object that performs a real handshake. A uni-stream is opened unilaterally by the sender and is cheap relative to the connection. All edges between the same node pair and ALPN reuse the same connection. Each edge has one persistent uni-stream within that connection. ### 8.2 Driver Mailbox Driver inbound messages: ```rust EstablishSend { edge_id: EdgeId, rx_node_id: NodeId, ring_id: RingId, ring_layout: RingLayout, tx_addr: ActorAddress, } EstablishRecv { edge_id: EdgeId, ring_id: RingId, ring_layout: RingLayout, rx_addr: ActorAddress, } StreamArrived { edge_id: EdgeId, stream: RecvStream, } StopEdge { edge_id: EdgeId, } ``` Driver outbound actor events: ```rust DriverEdgeReady { edge_id: EdgeId, } StreamClosed { edge_id: EdgeId, } StreamFault { edge_id: EdgeId, reason: StreamFaultReason, } PumpStopped { edge_id: EdgeId, ring_id: RingId, } ``` There are no per-object, per-range, or per-buffer-fragment driver mailbox messages in the hot path. ### 8.3 Receive Demux Rendezvous A recv-pump needs two resources: 1. local receive establishment state, including the ingress ring 2. the arriving QUIC stream They can arrive in either order. The driver stores both halves: ```text recv_specs: HashMap pending_streams: HashMap ``` On `EstablishRecv`, if a pending stream exists, the driver spawns the recv-pump. Otherwise it stores the spec. On `StreamArrived`, if a recv spec exists, the driver spawns the recv-pump. Otherwise it stores the stream. The orchestrator therefore does not need an inter-end readiness handshake. If a stream arrives before the receiver is locally established, it waits in `pending_streams`. Because no recv-pump reads from it yet, QUIC flow control eventually stalls the sender instead of dropping bytes. ### 8.4 Recv-Pump FSM ```text WaitingForSpecAndStream -> Streaming when ring and stream are both present Streaming read free ring span read QUIC bytes into that span advance commit send/coalesce RingReadable{ring_id} to worker repeat Backpressured entered when no ring free space exists wait for RingWritable{ring_id} return to Streaming Closed entered on stream EOF or edge teardown Faulted entered on read error, protocol edge failure, or ring fault ``` The recv-pump is byte-blind after stream demux. It does not parse `ObjectHeader` and it does not know where object boundaries are. Its only correctness responsibility is to copy bytes into free ring space and publish `commit` after those bytes are valid. Because the recv-pump is byte-blind, it does not classify EOF as object-aligned or mid-object. It reports stream closure. The worker parser classifies the close against its per-ring parser state: EOF with no partial record is a clean stream close; EOF while a header or payload is incomplete is an object failure. If the worker is slow, `consume` stops advancing. The recv-pump computes no free space, stops reading QUIC, and waits. Since the recv-pump stops reading the stream, QUIC's stream flow control stalls the remote sender. No actor credit protocol is needed. ### 8.5 Send-Pump FSM ```text WaitingForConnection ensure or await cached edge-ALPN connection WaitingForBytes wait for RingReadable{ring_id} OpenStream open one uni-stream write edge_id preamble -> Streaming Streaming acquire commit write committed egress bytes to QUIC advance consume after bytes are accepted by write_all send/coalesce RingWritable{ring_id} repeat Backpressured write_all is pending because network/QUIC flow control is slow keep ownership of unread ring bytes until write completes Closed/Faulted emit coarse driver event ``` If the network is slow, the send-pump stops advancing `consume`. The worker then runs out of free egress ring space and stalls before producing more outbound bytes. This is egress backpressure. --- ## 9. Edge Establishment Establishment is local actor setup plus transport rendezvous. The two remote edge ends do not exchange actor messages with each other. ### 9.1 Orchestrator The orchestrator owns graph placement. For each edge, it sends local provision messages to the producer node and consumer node: ```rust ProvisionTx { edge_id: EdgeId, rx_node_id: NodeId, object_spec: ObjectSpec, ring_spec: RingSpec, } ProvisionRx { edge_id: EdgeId, object_spec: ObjectSpec, ring_spec: RingSpec, } ``` The producer needs the consumer's `node_id`, not the consumer's actor address. The consumer needs the shared `edge_id`, not the producer's actor address. ### 9.2 EdgeEstablisher Records Each node has one EdgeEstablisher actor. The actor itself stays live and able to receive new messages. It does not enter `WaitingForLease` globally. Instead, it owns a table of per-edge establishment records: ```rust edge_records: HashMap ring_to_edge: HashMap struct EdgeRecord { edge_id: EdgeId, direction: RingDirection, state: EdgeProvisionState, lease_request_id: Option, ring_id: Option, local_edge_actor: ActorAddress, } ``` The record FSM is: ```text New on ProvisionTx/ProvisionRx spawn local Tx/Rx edge actor create LeaseRequestId send LeaseRing{request_id, ...} to ArenaManager -> WaitingForLease WaitingForLease on RingLeased matching request_id record ring_id send InstallRing to WorkerCtl -> WaitingForWorkerRing on RingLeaseRejected matching request_id notify local edge actor failure -> Failed on StopEdge send CancelLease{request_id} notify local edge actor stopped -> Stopped WaitingForWorkerRing on RingInstalled send EstablishSend/EstablishRecv to Driver -> WaitingForDriver on RingFault or StopEdge -> Stopping WaitingForDriver on DriverEdgeReady notify local Tx/Rx edge actor Ready -> Ready on StreamFault/RingFault/StopEdge -> Stopping Ready hot path runs without this actor coarse ObjectLoaded/ObjectProduced/StreamFault events may pass through Stopping stop pump if one exists uninstall worker ring if installed wait for quiescence proofs release ring if leased -> Stopped ``` An edge cannot become `Ready` without a ring lease because the record transition out of `WaitingForLease` requires a `RingLeased` carrying the same `LeaseRequestId`. An edge cannot spawn a pump for an unmapped worker ring because `EstablishSend/Recv` is sent only after the same record observes `RingInstalled`. If a stale `RingLeased`, `RingInstalled`, `RingFault`, or `PumpStopped` arrives for a record that has already stopped or for a ring id no longer present in `ring_to_edge`, the EdgeEstablisher ignores it except for releasing an unused fresh lease that was granted after cancellation raced with allocation. ### 9.3 Tx and Rx Edge Actors Tx and Rx actors are role-facing lifecycle gates. Tx actor state: ```text Provisioning -> Ready -> Producing -> Stopping -> Stopped -> Faulted ``` Rx actor state: ```text Provisioning -> Ready -> LoadingObject -> ObjectReady -> Stopping -> Stopped -> Faulted ``` They receive: ```rust EdgeReady { edge_id } ObjectLoaded { edge_id, object_id, device_handle } ObjectProduced { edge_id, object_id } ObjectFailed { edge_id, object_id, reason } StreamFault { edge_id, reason } StopEdge { edge_id } ``` They do not receive: - bytes - host pointers to payload - per-range readiness - per-range consumed events - free-space events This keeps actor execution deterministic and bounded by coarse workflow events, while ring cursors handle the high-frequency byte path. ### 9.4 Race Freedom A sender may open its stream before the receiver has completed local establishment. This does not lose bytes because the receiving driver demux stores the stream by `edge_id` until `EstablishRecv` supplies a ring. The stream is not read until the recv-pump exists. Unread QUIC streams apply transport flow control to the sender. A receiver may establish before the sender opens its stream. This does not need a remote ack because the receive spec waits in the driver demux table. When the stream arrives, `edge_id` pairs the two halves. The two sides do not need each other's actor addresses because data-plane routing is `(node_id, edge_id)`: the sender dials the consumer node and writes `edge_id` as the stream preamble; the receiver demuxes by `edge_id`. --- ## 10. Ingress Flow Ingress is the path: ```text remote worker/disk/GPU -> remote egress ring -> QUIC -> local ingress ring -> local worker -> local GPU memory -> ObjectLoaded ``` ### 10.1 End-to-End Sequence On the receiving node: 1. The driver demux reads the stream's `edge_id` preamble. 2. The driver rendezvous pairs the stream with the local ingress ring. 3. The recv-pump waits for free ring space. 4. The recv-pump reads QUIC bytes directly into the ring's free span. 5. The recv-pump advances `commit` after each successful read. 6. The recv-pump sends or coalesces `RingReadable { ring_id }`. 7. The worker wakes and reads `consume..commit`. 8. The worker parses object headers as soon as enough committed bytes exist. 9. The worker allocates the target device object after validating the header. 10. The worker copies every committed payload prefix it can into VRAM. 11. The worker advances `consume` after copies are safe to release. 12. The recv-pump sees free space and continues reading. 13. When all bytes for an object are copied, the worker emits `ObjectLoaded`. ### 10.2 Partial Ring Fill If an object is larger than the ingress ring, the ring may fill with only a prefix of the object. This is normal. Example: ```text object extent = 10 GiB ingress ring = 512 MiB recv-pump fills 512 MiB and stalls worker receives a readable wake and copies committed bytes to VRAM worker advances consume recv-pump resumes and reads the next bytes ``` The pipeline remains live because the worker consumes the committed prefix, not completed ranges. The object is not compute-visible until the worker has copied all 10 GiB and emitted `ObjectLoaded`. ### 10.3 Why No Per-Range Actor Messages Are Needed The worker does not need `Rx` to tell it that bytes landed. `RingReadable` and the ring cursors already provide that information at the process boundary. The worker does not need `Rx` to return buffer space. Advancing `consume` releases bytes to the producer; a coalesced `RingWritable` wake only tells the producer to reload the cursor. Removing per-range actor messages is correct because actor state is not the authority for ring ownership. The producer and consumer cursors are the authority. An actor message would be a slower duplicate of state the worker can read directly. --- ## 11. Egress Flow Egress is the path: ```text local GPU memory -> local worker -> local egress ring -> QUIC -> remote ingress ring -> remote worker/GPU ``` ### 11.1 Sequence 1. The worker receives or creates a compute result in GPU memory. 2. It creates an `ObjectHeader` according to the edge's `ObjectSpec`. 3. It waits for free egress ring space. 4. It writes header bytes into the egress ring and advances `commit`. 5. It copies payload bytes from the device object into free egress ring spans. 6. It advances `commit` as host bytes become valid. 7. It sends or coalesces `RingReadable { ring_id }`. 8. The send-pump wakes and opens the persistent edge uni-stream on first bytes. 9. The send-pump writes the `edge_id` preamble once. 10. It writes committed egress bytes to QUIC. 11. It advances `consume` after bytes have been accepted by `write_all`. 12. It sends or coalesces `RingWritable { ring_id }`, allowing the worker to produce more bytes. ### 11.2 Egress Backpressure If QUIC or the remote receiver is slow, `write_all` stops completing. The send-pump cannot advance `consume`. The egress ring fills. The worker eventually finds no free space and stops copying more bytes out of GPU memory. This cannot overwrite egress bytes because the worker computes free space from the send-pump-owned `consume` cursor. Until the send-pump advances `consume`, those bytes remain owned by the send-pump. --- ## 12. Backpressure Model There are no credits, no RTS/CTS, and no per-range acknowledgements. Backpressure is always absence of writable ring space. ### 12.1 Worker Slow on Ingress ```text worker slow -> consume does not advance -> recv-pump sees no free ingress space -> recv-pump stops reading QUIC -> QUIC flow control stalls sender -> sender send-pump stops draining its egress ring -> sender worker eventually stalls on egress free space ``` No data is dropped because each layer stops before overwriting unread bytes. ### 12.2 Network Slow on Egress ```text network slow -> send-pump write_all remains pending -> egress consume does not advance -> worker sees no free egress space -> worker stops copying more outbound bytes ``` No actor credit protocol is needed because the bounded ring and QUIC flow control already encode the pressure. ### 12.3 Arena Pressure ```text arena temporarily exhausted -> ArenaManager queues LeaseRing -> the EdgeEstablisher edge record remains WaitingForLease -> no worker ring is installed -> no pump is spawned -> no hot-path state exists for that edge ``` When a ring is released, the ArenaManager retries queued leases. Allocation pressure is establishment backpressure. --- ## 13. Teardown and Churn Teardown must prove quiescence before releasing an arena lease. ### 13.1 Teardown FSM For one edge: ```text Ready on StopEdge or fault -> StoppingPump StoppingPump driver stops recv/send pump driver emits PumpStopped -> StoppingWorkerRing StoppingWorkerRing WorkerCtl sends UninstallRing worker removes the ring from ready queues worker waits for in-flight copies or abandons them by process death worker emits RingQuiesced -> ReleasingArena ReleasingArena EdgeEstablisher sends ReleaseRing{proof} ArenaManager returns lease to free-list -> Stopped ``` ### 13.2 Why Release Is Safe The ArenaManager can safely reuse the range after `ReleaseRing` because the proof requires both hot-path owners to be gone: - the driver pump has stopped, so no Rust task will write/read the ring bytes - the worker has uninstalled the ring or died, so no worker loop will read/write the ring bytes - copy events have completed or the process owning them is gone, so no DMA will read/write the ring bytes Since every live user of the lease is stopped before the release message is sent, the next lease cannot alias a live user. ### 13.3 Churn Churn replaces edges. It does not mutate a live edge into a different peer. A replacement edge gets a new `edge_id`, a new establishment sequence, and either a fresh ring lease or a reused lease after the old edge proves quiescence. This keeps old streams, old ring cursors, and old object sequence numbers from merging with replacement state. --- ## 14. Failure Handling ### 14.1 Boot Failure If `memfd_create`, `ftruncate`, or `mmap` fails, the node does not enter steady state. No arena offsets have been minted, no rings exist, and no payload state can be corrupted. ### 14.2 Oversized Ring Request If a `RingSpec` can never fit in the arena ceiling, the ArenaManager emits `RingLeaseRejected`. The edge fails before worker install or pump spawn. Since no ring offset is emitted, no hot-path state can reference invalid memory. ### 14.3 Temporary Arena Exhaustion Temporary exhaustion queues the lease. The edge waits before establishment. This does not lose data because no stream pump has been created for the edge. If a remote stream arrives early, the receiver's driver holds it in `pending_streams` and QUIC flow control stalls the sender until local establishment catches up. ### 14.4 Malformed Object Header The worker faults the object if header validation fails. It does not allocate a compute-visible object. Since compute receives only `ObjectLoaded`, malformed objects cannot be consumed by the role layer. ### 14.5 EOF Mid-Object If the stream closes before `extent` bytes are copied, the worker emits `ObjectFailed`. The driver emits `StreamClosed` for transport EOF or `StreamFault` for a read error; it does not decide whether the close was object-aligned. The partially allocated device object is discarded. It is not exposed because completion requires exactly `extent` copied bytes and copy completion. ### 14.6 Worker Crash If the worker process exits, WorkerCtl marks installed rings faulted and asks the driver to stop their pumps. The ArenaManager does not release leases until the worker process is reaped and the driver reports `PumpStopped`. The arena itself survives worker restart because it is owned by the node process. ### 14.7 Pump Failure If a pump fails, the driver emits `StreamFault` and stops touching the ring. The worker is told to uninstall the matching ring. The arena lease is released only after both sides quiesce. --- ## 15. Implementation Requirements ### 15.1 Shared Atomics Ring cursors must be accessed with atomic acquire/release semantics across the process boundary. The Python worker must use the native ring helper for ring header access, span calculation, cursor publication, and wake coalescing. Python may own parser and tinygrad logic, but it does not directly implement cross-process atomic cursor operations. ### 15.2 Alignment Ring headers must be aligned for atomic cursor operations. Ring data must be aligned to the largest required host-copy and device-copy alignment for the backend. Alignment is a performance and DMA requirement; non-overlap correctness still comes from ArenaManager leases. ### 15.3 Host Pinning The MVP may use pageable host memory and synchronous copies. Host pinning is the path to efficient asynchronous DMA. If host memory is pinned, it must be pinned per ring lease and unpinned only after ring quiescence. A pinned range cannot be returned to the free-list while a backend may still DMA from it. ### 15.4 Object Layout The object byte layout must be fixed by `ObjectSpec`. The worker may parse headers and validate sizes, but it does not reinterpret or transform payload values. Payload bytes are copied into device memory in the layout the compute role expects. ### 15.5 Persistent Streams Each edge uses one persistent stream, not one stream per object. The `edge_id` preamble is written once. Object records follow back-to-back. This avoids per-object stream allocation and preserves the ring's natural pipeline. --- ## 16. Invariants and Why They Hold **Arena offsets remain valid.** The arena mapping is created once and never moved or resized. Offsets are relative to that mapping and remain meaningful until node shutdown. **Live rings do not overlap.** The ArenaManager serializes all lease/release operations through one mailbox and records every live lease. A range is removed from the free-list before `RingLeased` is emitted and returned only after `ReleaseRing`. **Stale ring events cannot alias replacement rings.** `ring_id` values are unique for the node lifetime. Churn may reuse arena ranges after quiescence, but it does not reuse the identifier that control messages and wake hints carry. **The worker cannot read bytes before the recv-pump writes them.** The recv-pump writes bytes first, then advances `commit` with release ordering. The worker loads `commit` with acquire ordering and never reads beyond it. **The recv-pump cannot overwrite bytes still needed by the worker.** Free space is computed from the worker-owned `consume` cursor and the producer-local `write` cursor. The worker advances `consume` only after it has parsed/copied the bytes and, for DMA, after the copy no longer depends on that host memory. Until then, the producer's free-space calculation cannot include that physical range. **Wakeups do not own data.** Wakeups carry no ownership information. The ring cursors are durable shared state. A consumer that wakes late still sees all bytes in `consume..commit`. Liveness requires the wake implementation to preserve a pending ring in a ready set, pending bit, or equivalent durable scheduler state until the other side reloads the relevant cursor. **Partial objects cannot reach compute.** The only compute-visible event is `ObjectLoaded`, and the worker emits it only after the full declared extent has been copied into device memory and copy completion is known. **A stream can arrive before local receive establishment without dropping bytes.** The driver demux stores the stream by `edge_id`. It does not read the stream payload until a recv-pump exists. QUIC flow control stalls the sender if buffers fill while the stream is pending. **Actor scheduling cannot corrupt payload state.** Actors do not own payload bytes or per-byte cursors. The hot-path state is in ring atomics owned by exactly one producer and one consumer. **A released arena range has no live users.** `ReleaseRing` is sent only after the driver pump has stopped, the worker ring has quiesced, and copy lifetimes have ended. The ArenaManager reuses ranges only after that proof. ---