Add DESIGN_DIRECTIVES, GPU_WORKER_INTERFACE_SPEC, ORCHESTRATION_SPEC, and RING_BACKPRESSURE_SPEC. Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
1522 lines
48 KiB
Markdown
1522 lines
48 KiB
Markdown
# 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<u8>,
|
|
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<RingId>` 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<EdgeId, RecvSpec>
|
|
pending_streams: HashMap<EdgeId, RecvStream>
|
|
```
|
|
|
|
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<EdgeId, EdgeRecord>
|
|
ring_to_edge: HashMap<RingId, EdgeId>
|
|
|
|
struct EdgeRecord {
|
|
edge_id: EdgeId,
|
|
direction: RingDirection,
|
|
state: EdgeProvisionState,
|
|
lease_request_id: Option<LeaseRequestId>,
|
|
ring_id: Option<RingId>,
|
|
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.
|
|
|
|
---
|