[HAL/Remote] Add remote HAL device execution - #180
Open
benvanik wants to merge 246 commits into
Open
Conversation
FUTEX_WAIT, SQPOLL, and POLL_THREAD were commented-out placeholders from the design phase. FUTEX_WAIT is now automatic via proactor capability detection (not a carrier mode), and SQPOLL is a proactor-level option. Remove the dead comments. Co-Authored-By: Claude <noreply@anthropic.com>
Implement iree_net_control_channel_t: a thin protocol layer over iree_net_message_endpoint_t that parses 8-byte frame headers and dispatches by type (PING/PONG, GOAWAY, ERROR, DATA). Handles PING/PONG liveness automatically and manages a CREATED→OPERATIONAL→DRAINING→ERROR lifecycle state machine. Zero-copy on both paths: receive passes payload spans and buffer leases directly through to application callbacks; send builds stack-local headers and scatter-gathers them with caller payloads through the endpoint. Includes 48 tests covering lifecycle, all receive frame types, malformed frame rejection, send state enforcement, wire format verification, state machine integration, transport error handling, and send/activate failure paths. Fuzzer exercises both single-message injection and operation-stream modes with configurable send fault injection. Co-Authored-By: Claude <noreply@anthropic.com>
Extends the SHM carrier factory with cross-process support via Unix domain socket handshake. Two processes exchange SHM region handles and notification primitives, then independently create carriers backed by the same shared memory. Handshake protocol: - Server creates SHM region, sends OFFER (region handle + wake export) - Client opens SHM, sends ACCEPT (its own wake export) - Platform-specific handle exchange: SCM_RIGHTS on POSIX, DuplicateHandle on Windows Shared wake enhancement: - create_shared(): allocates dedicated SHM page for cross-process epoch, creates platform-native signal primitives (eventfd/pipe/Event) - export(): duplicates handles for IPC transfer to peers - Peer notification proxy: maps remote epoch SHM + signal primitive, enabling cross-process carrier wake signaling Factory cross-process paths: - "unix:/path" addresses dispatch to cross-process listener/connect - Listener: bind + listen + async accept + handshake per connection - Connect: async connect + client handshake - Simple name addresses use existing in-process path unchanged Co-Authored-By: Claude <noreply@anthropic.com>
Implements zero-copy direct memory access for SHM carriers. Buffers
already in shared memory can be written/read without copying through the
SPSC ring — the sender writes directly to SHM, then sends a lightweight
REFERENCE descriptor through the ring to notify the peer.
Region table: carriers maintain a FAM of known SHM regions, populated at
creation time from create_params. Region 0 is the carrier's main SHM
region. register_buffer scans the table to resolve a pointer to a
{region_id, offset} handle; unregister_buffer is a no-op (no kernel
resources). query_region provides SHM-specific region discovery.
direct_write has two modes: signaling (writes a REFERENCE entry to the
TX ring, completion fires on peer consumption) and non-signaling (pure
memcpy, synchronous return). This matches RDMA semantics where
unsignaled writes generate no CQE. direct_read is always synchronous.
REFERENCE entries (type 0x01) are now handled in drain_rx with full
bounds checking (region_id, offset+length vs region size).
Convention cleanup across SHM files: refactored expanding-cleanup
patterns to if-chains in factory.c/handshake.c/shared_wake.c, extracted
helper functions to eliminate goto and reduce nesting, grouped paired
fn+context fields into structs, separated _destroy from _release,
replaced manual size arithmetic with IREE_STRUCT_LAYOUT, moved extern
declarations from .c files to headers.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds cross-process SHM carrier support on Windows via named pipes, completing platform parity with the existing Unix domain socket path. The factory.c portable core is split into platform-specific files: - factory_unix.c: Unix domain socket listener/connect (extracted) - factory_win32.c: Named pipe listener/connect (new) - factory_internal.h: Shared struct definitions Named pipe listener uses EVENT_WAIT to monitor ConnectNamedPipe completion through the proactor (no IOCP association needed). Connect uses synchronous CreateFile + NOP-deferred handshake delivery. The handshake layer is updated for platform neutrality: - handshake.h: socket→channel rename throughout - handshake_win32.c: rewritten from Winsock to overlapped pipe I/O with DuplicateHandle for cross-process handle exchange - handshake_posix.c: EINTR retry loops for poll/sendmsg/recvmsg - handshake_test.cc: named pipe pairs on Windows instead of TCP loopback Addressing: "pipe:<name>" on Windows, "unix:<path>" on POSIX. Using a cross-process scheme on the wrong platform returns UNIMPLEMENTED. Co-Authored-By: Claude <noreply@anthropic.com>
Cross-process tests for the SHM carrier using the coordinated test harness. Spawns actual separate processes that establish carrier pairs via handshake over a local channel (Unix domain socket on POSIX, named pipe on Windows). Four test scenarios covering the full cross-process path: - HandshakeAndCreate: validates handshake completes and carriers activate - SendRecvRoundTrip: bidirectional inline send/recv across processes - DirectWriteSignaling: client writes at SHM offset with SIGNAL_RECEIVER - DirectReadAcrossProcesses: server writes SHM, client reads via direct_read Platform-specific code is isolated to channel helpers (MakeAddress, ServerBind, ServerAcceptAndHandshake, ClientConnectAndHandshake) while test scenarios, role functions, and configs are shared across all platforms. Co-Authored-By: Claude <noreply@anthropic.com>
Transport factories now use create/retain/release (ref-counted) semantics instead of allocate/free (single-owner). Multiple consumers — drivers, devices, registries — can safely share a factory without forced lifetime coupling, following the standard IREE pattern for shared non-HAL objects. The HAL remote client/server APIs now accept an iree_net_transport_factory_t* directly instead of a transport_name string. Transport resolution moves to the registration boundary: driver_module.c decides which factories to include based on IREE_HAVE_NET_*_TRANSPORT build-time defines, creates the appropriate factory, and passes it through. Core client/server code never knows or cares what transport it's using. Also fixes a pre-existing bug in iree_hal_remote_server_release where iree_atomic_ref_count_dec was used as a boolean instead of checking == 1 (it returns the previous value via fetch_sub). Co-Authored-By: Claude <noreply@anthropic.com>
Define the complete HAL remote protocol wire format across four headers: - common.h: Shared building blocks (resource IDs with encoding macros, buffer_params, binding, dispatch_config, memory_heap). - control.h: Control channel messages — envelope, response prefix, type enum (28 message types), and payload structs for all request/response pairs, fire-and-forget messages, and notifications. - queue.h: Queue channel operations — ADVANCE frame resolution entries, queue op type enum (12 ops), header, and all payload structs. - commands.h: Command buffer serialized commands — cmd type enum (9 cmds), header, barrier entries, and all payload structs. Wire format conventions: little-endian, naturally aligned, 8-byte padded variable-length data, reserved fields must be zero. All types are fixed-width with no HAL header dependencies (iree_device_size_t is always 64-bit on wire). Every struct has static_asserts for size and critical offsets. Cross-validated review caught three protocol design issues, fixed before commit: FILE_OPEN now carries a provisional_id for pipelining consistency with all other [epoch] messages, BUFFER_IMPORT uses extensible payload_length+payload[] instead of a fixed uint64_t handle, and FILE_REGISTER gained an external_type discriminant with the same extensible handle pattern. Co-Authored-By: Claude <noreply@anthropic.com>
Defines the bootstrap protocol (HELLO/HELLO_ACK/REJECT) carried as DATA frames on the control channel, and the session API for connection lifecycle management, topology exchange, and endpoint provisioning. bootstrap.h: Wire format structs for the bootstrap handshake protocol. Includes a session lifecycle diagram documenting the full flow from transport connect through bootstrap, operational, and shutdown phases. The bootstrap exchanges axis topology (device queues, host contexts) so each side can create proxy semaphores and register remote axes in its frontier_tracker. session.h: Public API for iree_net_session_t. Key operations: - connect(): Client-side async bootstrap (factory.connect -> HELLO -> HELLO_ACK -> register axes -> OPERATIONAL) - accept(): Server-side bootstrap from accepted connection - open_endpoint(): Provision application endpoints (queue, bulk) - send_control_data(): Forward HAL control messages via control channel - shutdown(): Graceful GOAWAY with endpoint drain and axis cleanup Design decisions: - Session does NOT propagate frontiers in steady state. Queue channel ADVANCE frames (parsed by the HAL layer) are the sole frontier propagation mechanism. No dedicated frontier endpoint needed. - Session does NOT interpret application traffic. Queue commands and bulk transfers are opaque to the session. - Session does NOT own the frontier_tracker (borrows it from the application, which spans machine lifetime). - Proxy semaphores for remote axes are owned by the session and failed/released on shutdown. Co-Authored-By: Claude <noreply@anthropic.com>
Implements iree_net_session_t: connection lifecycle, bootstrap protocol, and endpoint provisioning for the remote HAL networking stack. Bootstrap chain: factory.connect() → open_endpoint → create control channel → HELLO/HELLO_ACK exchange → register proxy semaphores for remote axes in frontier tracker → OPERATIONAL. Key design decisions: - All six callback entry points (on_data, on_goaway, on_error, on_transport_error, on_connect, on_control_endpoint_ready) retain the session for their duration to prevent use-after-free from re-entrant release in application callbacks. - fail() immediately cleans up remote axes in the frontier tracker so waiters are woken with errors rather than hanging indefinitely. - register_remote_axes uses cleanup_remote_axes on partial failure to properly fail registered tracker entries before releasing semaphores. - Session IDs are caller-provided (via options.session_id) rather than generated from a process-global counter, avoiding shared-library and multi-server-instance issues. - Local axis count is validated against UINT16_MAX (wire format limit) at session creation time. Co-Authored-By: Claude <noreply@anthropic.com>
Uses proactor inline progress callbacks for adaptive polling in the SHM carrier, switching between busy-poll and event-driven wake based on traffic patterns. Uses AIMD (Additive Increase Multiplicative Decrease) windowing for batch size adaptation. Co-Authored-By: Claude <noreply@anthropic.com>
Add a latency benchmark to the carrier CTS that isolates cold (notification) vs warm (poll mode) delivery latency by timestamping inside the recv handler. This makes the adaptive polling improvement directly visible: SHM Cold/64: 851ns (notification path through kernel) SHM Warm/64: 287ns (poll mode, user-space ring check) Loopback Cold: 938ns Loopback Warm: 950ns (no difference, no adaptive polling) Also adds the previously missing shm_benchmarks binary to the SHM CTS, enabling all CTS benchmarks (roundtrip, throughput, latency) to run against the SHM backend. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The PollUntil condition only waited for all 3 connects to fire, not for all 3 accepts. On io_uring, native multishot accept delivers multiple CQEs in a single poll iteration, so all 3 accepts completed alongside the connects. On kqueue (macOS), multishot accept is emulated by re-queuing the accept operation after each accept() call — one accept per poll iteration. When all 3 connects completed in the first poll iteration, PollUntil exited before the remaining 2 accepts were processed. Fixed by including accept count in the PollUntil condition. Co-Authored-By: Claude <noreply@anthropic.com>
Adds shared wake support to the SHM carrier, enabling efficient cross-process signaling through shared memory event primitives. Co-Authored-By: Claude <noreply@anthropic.com>
send() checked shutdown_initiated before acquiring tx_lock, but shutdown() sets the flag under the lock after writing the EOS marker. A concurrent shutdown() between the check and lock acquisition lets send() write data after the EOS marker — the peer stops at EOS and the data is silently lost despite send() returning OK. Fix: re-check shutdown_initiated inside the tx_lock critical section. The pre-lock check remains as a fast-path optimization. Also adds CTS test SendAfterShutdownFails covering the post-shutdown send rejection contract across all carrier backends. Co-Authored-By: Claude <noreply@anthropic.com>
Adapts loopback carrier and control channel to correctly handle send data lifetime when SQEs are submitted from recv callbacks. Co-Authored-By: Claude <noreply@anthropic.com>
Add session CTS test infrastructure (session_test.cc, session_test_base.h) with BUILD.bazel and CMakeLists.txt for testing session bootstrap, topology exchange, and shutdown across transport backends. Fix carrier destroy ordering in TCP and SHM carriers: defer the final carrier free until after all in-flight operations have completed and callbacks have fired, preventing use-after-free when destroy races with pending completions. Fix SHM carrier forward declaration. Fix session.c minor cleanup ordering issue. Add AIMD (Additive Increase / Multiplicative Decrease) to typos.toml allow-list — it's a standard congestion control algorithm name used in the SHM carrier's adaptive RX window. Co-Authored-By: Claude <noreply@anthropic.com>
Extracts frame sending logic into a reusable frame_sender utility and refactors the control channel to use it. Fixes send data lifetime issues in the control channel and adds framing_adapter for protocol framing. Co-Authored-By: Claude <noreply@anthropic.com>
Adds tests verifying semaphore proxy behavior including axis failure propagation through the session transport layer. Co-Authored-By: Claude <noreply@anthropic.com>
Adds iree_net_queue_channel_t, a protocol layer that parses 16-byte queue frame headers, extracts optional wait/signal frontiers from the payload, and dispatches command data to application callbacks. On the send path, it encodes frontiers into outgoing frames with zero-copy command payloads. The queue channel is payload-agnostic — HAL operations, pipeline commands, collective ops, etc. all use the same framing. It handles the wire format mechanics (header validation, frontier parsing, stream_id multiplexing) while consumers interpret the command data. Includes 26 unit tests covering lifecycle, receive path (with and without frontiers, error cases, truncated data), send path (wire format verification), error state transitions, and a round-trip test that verifies send_command output is correctly parsed by the receive path. Co-Authored-By: Claude <noreply@anthropic.com>
Upgrades loopback and SHM factories to support multiple endpoints per connection (matching TCP). Sessions need at least 2 endpoints (control + application), and tests exercise opening multiple application endpoints with queue channel round-trips over them. Multi-endpoint: - Loopback/SHM factories pre-create N carrier pairs at connect time using FAM endpoint slots. open_endpoint() hands out the next slot. - SHM cross-process path (factory_unix/factory_win32) unchanged — uses a backward-compatible 1-slot wrapper. - Removed multi_endpoint CTS tag since all transports now support it. Session endpoint tests: - OpenEndpointSucceeds, MultipleEndpointsSucceed: verify async endpoint provisioning across all backends. - QueueChannelCommandRoundTrip: full path validation — application → queue frame → message_endpoint → carrier → message_endpoint → application, bidirectional. Queue channel: - Added has_pending_sends() for callers to drain send completions before release. SHM carriers fire send completions asynchronously (one poll cycle after data receipt); the test uses this to avoid UAF. Options cleanup: - SHM _default() now sets explicit values (ring_capacity, max_endpoint_count) instead of returning zeros and relying on implementation-side fallbacks. - Removed all "0 = use default" fallbacks from loopback, SHM, and TCP factory/carrier code. If callers don't use _default(), they must set all fields — 0 is caught by existing validation (not silently replaced). - Removed "Default: X" comments from TCP carrier options struct. The _default() function is the single source of truth for defaults. Co-Authored-By: Claude <noreply@anthropic.com>
Replace C++20 designated initializer syntax with memset + field assignment for MSVC compatibility. Co-Authored-By: Claude <noreply@anthropic.com>
Add status_wire.h/c implementing a self-describing binary wire format for serializing iree_status_t across network boundaries. The format preserves status code, source location (file:line), primary message, and payload chain (annotations, stack traces) as typed entries with 8-byte alignment. To support self-contained deserialized statuses (no dangling pointers to transient wire buffers), add iree_status_allocate_copy() which copies both file and message into trailing storage. Also add public accessor functions iree_status_payload_type() and iree_status_payload_format() so the wire layer can inspect payloads without depending on internal headers. Move iree_status_payload_type_t enum from status_payload.h into status.h (the sole public header of :base), keeping struct definitions private in status_payload.h where they need iree_allocator_t. Rewrite iree_status_clone() to use iree_status_allocate_copy instead of iree_status_allocate_f with printf formatting — more efficient (memcpy vs vsnprintf) and preserves exact byte content. Co-Authored-By: Claude <noreply@anthropic.com>
Replace the control channel's flat (error_code, message_string) ERROR frame format with structured status_wire serialization, preserving source locations, annotations, and stack traces across the network. send_error() now takes ownership of an iree_status_t (consuming it on all paths including failure), serializes it as a status_wire blob into the ERROR frame payload, and transitions to ERROR state. The receive side deserializes via iree_net_status_wire_deserialize and transfers the reconstructed iree_status_t to the on_error callback. The session error bridge simplifies from manual status reconstruction to direct pass-through of the deserialized status. Also fixes a heap-buffer-overflow in iree_status_payload_message_formatter where a NUL terminator was written one byte past the buffer when the payload length was 8-byte aligned. IREE is string-view-based; NUL terminators are never required by callers of iree_status_payload_format. Co-Authored-By: Claude <noreply@anthropic.com>
Fix two issues found during cross-validated code review: 1. bootstrap_sends_in_flight was incremented after the send_data call. For synchronous-completion carriers (loopback, shm), the completion callback fires during send_data — before the increment. The handler sees count==0, misroutes the completion to the application callback path, and the counter gets permanently desynchronized. Fix: increment before the send, decrement on synchronous failure. 2. Bootstrap handler errors (REJECT, HELLO/HELLO_ACK validation failures, send_hello_ack failures) were returned as error statuses from the on_data callback, bubbling through the carrier → transport error path. This left the session stuck in BOOTSTRAPPING until the transport error callback fired, and lost the original diagnostic (e.g., the specific REJECT reason). Fix: route all bootstrap errors through iree_net_session_fail() directly so the session transitions to ERROR with the precise error reason. Co-Authored-By: Claude <noreply@anthropic.com>
When two carriers on the same shared_wake have a sender-receiver relationship (intra-process SHM), signal_peer during the scan increments the notification epoch. The re-posted NOTIFICATION_WAIT captures the already-incremented epoch as wait_token, so FUTEX_WAIT sees current==expected and sleeps forever — missing the signal. Fix: capture the notification epoch before iterating the sleeping list. If it changed after iteration (meaning a carrier's drain signaled this shared_wake), re-signal the notification to force the re-posted wait to complete immediately. Only affects intra-process SHM where both carriers share the same shared_wake. Cross-process SHM uses separate shared_wakes so signal_peer targets a different notification entirely. Co-Authored-By: Claude <noreply@anthropic.com>
Five tests covering session error state transitions, cleanup behavior, and proxy semaphore failure propagation: - ProtocolVersionMismatchCausesServerError: client sends bad protocol version, server enters ERROR state with INVALID_ARGUMENT - OperationsFailInErrorState: operations on errored session return FAILED_PRECONDITION - ShutdownCleanupFailsRemoteAxisWaiters: client shutdown fails pending frontier waiters on server with UNAVAILABLE - GoawayReceivedCleanupFailsRemoteAxisWaiters: GOAWAY propagates axis cleanup to frontier waiters - GoawayReceivedCleanupFailsSemaphoreTimepoints: GOAWAY fails pending proxy semaphore timepoints with UNAVAILABLE All tests run on loopback, SHM, and TCP backends. Co-Authored-By: Claude <noreply@anthropic.com>
When a loopback send's NOP completion fires after the peer has departed (deactivated or destroyed while the NOP was in flight), the completion callback was called with OK status despite the data never being delivered. The sender silently believed success while the peer never received anything. Fix: set delivery_status to UNAVAILABLE when the peer is gone at NOP completion time. This error propagates through the frame_sender and control channel to session_fail(), matching how TCP (EPIPE/ECONNRESET) and SHM (shared_wake detection) report peer departure. The fix is loopback-specific because loopback delivery is synchronous within the NOP completion. TCP and SHM have decoupled send/delivery — data reaches the transport layer before the peer link is cleared — so their sends genuinely succeed even when the peer departs afterward. Adds a loopback-specific carrier_test validating the in-flight send error reporting on peer departure. Co-Authored-By: Claude <noreply@anthropic.com>
The SHM carrier previously deferred send completion callbacks until the peer consumed data from the ring (tracking read_position advancement). This conflated buffer-release notification with consumption acknowledgment — the caller's buffers are copied into the ring during send(), so they're free to reuse immediately after commit_write, regardless of when the peer reads. Change send() and direct_write() to fire the completion callback synchronously after committing data to the ring, matching TCP's "commit to transport" semantics. This removes the entire deferred completion tracking infrastructure: - iree_net_shm_completion_entry_t and the 256-entry ring - head/tail atomics for producer/consumer tracking - drain_tx_completions() polled from wake and progress callbacks - cancel_tx_completions() called during deactivation - TX completion Dekker re-checks in both sleep and poll mode transitions The drain and progress callbacks now only handle RX data, simplifying the Dekker protocol from a 7-step to a 6-step sequence. query_send_budget() slots are now UINT32_MAX (no completion ring to fill; the only send limit is ring buffer space). The backpressure CTS tests correctly skip carriers with unlimited slot budgets. This also fixes a use-after-free exposed by the bootstrap timeout test: when the server never responds, the client's HELLO send completion was deferred via shared_wake eventfd until carrier destruction — by which point the session's control_header_pool was already freed. The completion callback tried to release a buffer lease into the freed pool. Co-Authored-By: Claude <noreply@anthropic.com>
Send each PING, PONG, and GOAWAY as one copied endpoint message so a backpressured operation cannot leave bytes that contaminate a later protocol frame. Remove fixed stack serialization limits and route asynchronous completion failures into the channel's terminal state. Application DATA remains scatter-gather so bulk payload ownership and zero-copy transport opportunities are unchanged. Keep batching available for self-delimiting protocols while making its retry and discard contract explicit. Make retained storage pools optional, share one inline/pool/heap allocation path, and remove the unused per-session control pool.
Prepare every RDMA SEND as a registered span list with optional transport-owned staging. Only unregistered bytes are copied, so framed sends retain registered payload addresses and lkeys through ibv_post_send. Unify staged and direct reservations around that representation. Pending sends retain one staging lease while borrowing caller payload spans through completion or cancellation, while strict zero-copy sends still reject unregistered bytes before admission.
Expose the shared carrier test and benchmark mains as test-only library targets. Backend CTS binaries now depend on those targets instead of compiling source files owned by another Bazel package. Regenerate the matching CMake targets so both build systems preserve the same ownership boundary.
Route session, queue, and bulk transport failures through one first-failure-wins transition. Preserve the exact status for admission and RPC waiters, fail the queue frontier, and close channel admission without releasing the network graph still reachable by racing submissions. Terminalize bulk state under its transfer mutex so active map and file operations wake while descriptors remain alive until admitted async I/O and zero-copy sends retire. Serialize channel publication and lifecycle callbacks with failure publication to preserve connect and error callback order. Copy stack-backed control requests into session-owned storage so a terminal wake cannot outlive the request payload.
Return frontier entries and command payload storage only after the message endpoint admits the complete frame. Keep wire layout inside the queue channel while allowing callers to populate sequencing metadata after admission. Consume and clear each reservation on commit or abort, including commit failure. Adapt remote HAL call sites without changing their epoch ordering yet.
Allocate and retain signal waiter state before transport admission. Assign an epoch only after the complete command frame and payload are writable, then publish waiters before making the frame visible. Leave admission failures retryable without consuming an epoch. Treat tracker publication or transport commit failure after assignment as a terminal device failure so no later frontier can wait behind a hole.
Application endpoints can detect unrecoverable failures after session bootstrap, but only graceful GOAWAY was public. Expose the existing thread-safe failure transition so endpoint owners can atomically fail remote axes and publish one terminal status through the session callback.
Queue allocations are immediately usable with provisional resource IDs, while the proactor replaces those IDs as ADVANCE frames arrive. Make the identity storage atomic so submission can overlap resolution without a data race. Load each ID once when constructing a wire reference or releasing a resource. Either provisional or canonical identity remains valid before the allocation frontier completes.
Server command completion previously released ADVANCE state when send admission failed, allowing client semaphore waits to hang and silently dropping terminal execution errors. Retain ordered ADVANCE records until transport admission, retry RESOURCE_EXHAUSTED sends on explicit readiness, and bound queued records. The first command failure terminalizes the queue, publishes one error frontier, and retires later submissions without reporting false success. Send completions own their records through transport completion, and terminal session failure is marshaled to the proactor. Session generations guard late callbacks so a detached channel cannot mutate a reused server slot.
Store the retained resource, assigned type, and generation together in each server resource-table slot. Lookup, release, and detach now require the wire ID and stored capability fields to agree, preventing cross-type IDs from reinterpreting or freeing another HAL resource. Retire slots before generation wrap can revive stale IDs. Split the table into its own build target and cover every resource type, stale reuse, generation exhaustion, and the virtual-reservation release fork.
Move frontier-to-local-semaphore storage out of the server session implementation and give it a single typed slot allocation with explicit retain, transfer, and teardown semantics. Use the same component lifecycle for session removal and server destruction, and cover duplicate keys, tombstones, growth, allocation failure, and retained semaphore ownership directly.
Remove accepted profile completions from the active bulk transfer table before they enter the profile sequence window. The transfer table now owns only reassembly, the sequence window owns deferred callbacks, and the ready list owns sink dispatch. ABORT and duplicate COMPLETE frames can no longer reclaim sequence-owned storage. Terminal cleanup drains pending sequence nodes directly, which also removes the compensating cross-lookup between the table and window.
Keep the caller-owned channel primitive untouched until bootstrap preparation has installed all resources and submitted its completion wait. Failure cleanup can therefore resume and join the suspended worker without borrowing, clearing, or closing caller state. Document the prepare and launch serialization contract and cover allocation failure after bootstrap creation with a native-handle ownership test.
Move the shared region ABI, overflow-checked geometry, initialization, and opening into one leaf used by in-process pairs and cross-process handshakes. Creators retain initialized queue handles, while openers require the exact page-rounded mapping and validate outer and inner capacity invariants before binding queues. Derive client mappings from creator-owned geometry before mapping, add focused malformed-region coverage, and correct stale SPSC descriptions now that the transport uses MPSC queues.
Represent empty handshake and shared-wake export ownership with constructors that install invalid SHM handles and NONE primitives. Initialize outputs before channel or precondition validation and caller locals before branches that may skip producers, preventing failure cleanup from interpreting fd 0 as owned. Move platform-independent handle cleanup into common handshake code and cover invalid-channel and export-precondition failures with native ownership witnesses.
Move the fixed 32-byte SHM handshake message into a dedicated untrusted-input boundary. Version 3 widens transport mapping extents to 64 bits, fixes the little-endian magic encoding, and makes every field offset and reserved byte part of the validated wire contract. Derive OFFER mappings through canonical region layout calculation before opening any received handle. ACCEPT and READY reject type-inapplicable geometry, while creator wake extents are constrained to supported normal page sizes. Add scalar coverage, a parser fuzzer, and a real malformed-peer witness using an initialized carrier region and valid transferred handles. Generate matching Bazel and CMake targets.
Win32 handle transfer previously trusted a sender process ID carried in the handshake payload. A local peer could therefore nominate an unrelated process as the DuplicateHandle source, and the same unverified PID persisted for later file transfers. Derive peer identity from the connected named-pipe endpoint instead. Handshake receive uses a transient process handle for bootstrap imports, while file transfer owns a stable process handle for the connection lifetime. Reserve and validate the former PID wire slot, remove process identity from common xproc state, and reject remote named-pipe clients.
Device catalog creation could fail after the server retained its factory and devices but before its trailing session pointer was bound. The common destruction path then walked a null session array instead of reporting the construction failure. Bind all trailing storage and complete infallible server initialization before creating the catalog. Exercise every allocator failure point and a missing device-spec provider to prove the ordinary destructor balances all initialized ownership without partial-state flags.
Command stream replay can fail after a local command buffer has entered its recording state. Own begin, replay, and end in one helper so that every successful begin is paired with end and finalization failures join the terminal replay status. Reusable uploads and one-shot execution continue to publish or submit only fully replayed command buffers. Failed partial recordings are finalized and released through the normal HAL lifecycle.
Recorded command streams could truncate updates, dispatch constants, and debug labels at the 16-bit common length field. Keep the header at eight bytes while replacing that limit with a 32-bit byte length, and centralize checked client growth and header emission so failed encodes leave the prior stream intact. Parse every externally supplied record through one canonical protocol boundary before replay. Exact command layouts and reserved fields now fail closed, while replay retains only resource resolution and HAL recording. Large records cross both one-shot and reusable delivery paths, and failed partial recordings are finalized without publication or submission.
The frontier tracker enforces a fixed configured limit independently from its internal hash table slot count. Expose that immutable admission limit so connection setup can reject impossible remote topology before allocating proxy state. Keep the query a direct read of immutable tracker storage and cover a non-power-of-two capacity to distinguish it from internal table sizing.
Move HELLO, HELLO_ACK, and REJECT decoding into a borrowed parser with no decoded-message allocation. Validate canonical framing, reserved fields, capability bits, status codes, and padding before session state can observe peer input. Share checked topology layout with producers and load axis entries through aligned copies. Apply role, phase, negotiated-capability, and tracker-capacity policy before state commitment or proxy allocation. Register peer topology before accepting it, and publish proxy semaphores only after tracker registration so failed duplicate registration cannot retire state owned by another participant. Bound peer-controlled rejection text copied into local diagnostics without limiting the wire reason. Add malformed-input unit coverage, an instrumented fuzzer, and real-carrier witnesses for bootstrap flags, capability injection, topology capacity, duplicate-axis unwind, and diagnostic amplification.
Add typed callback-lifetime storage for dispatch reference lists and command buffer binding tables. Common lists retain the existing 32-entry inline path, while wider lists use one checked host allocation that is released synchronously after HAL capture. The component owns no buffer references or persistent cache state.
Materialize direct and recorded dispatch bindings through shared inline-or-spill storage and collapse reusable and one-shot command buffer execution onto one cleanup path. Temporary arrays now support the full wire count without surviving local HAL capture, and remote command buffer creation rejects capacities that cannot be represented by the protocol instead of truncating them during upload.
Exercise a 33-entry binding table through the full client/server path with both one-shot and reusable command buffers. The indirect fill consumes slot 32 and verifies the resulting local-task buffer contents, covering the inline-to-heap spill boundary and both command-buffer ownership paths.
Add a reproducible VMVX fixture with 33 reflected bindings and observable output in slot 32. Exercise it through direct queue dispatch and both command-buffer lifecycles while sharing the existing two-binding dispatch harness. Run fixture generation from the repository root so embedded source paths and checked-in binaries do not vary with the worktree location.
Remove the obsolete event protocol and replace its command IDs with fixed-width atomic wait, store, and RMW records for direct queue operations and reusable command buffers. Validate peer-controlled fields before resolving resources, preserve queue affinity and binding-table semantics, and lower exclusively through generic HAL APIs. Consume immutable device memory and executable facts from bootstrap device specs instead of synchronous query RPCs. Return concrete allocation metadata so client proxies mirror server placement, and configure queue slabs from preferred server memory classes without coupling the remote libraries to any backend. Harden deferred host-call ownership and uploaded command-buffer replay while forwarding current barrier and executable metadata. Unsupported timestamp and external-capture vtable entries now fail explicitly instead of dispatching through null pointers.
Remote devices publish the server device spec, including queue timestamp capabilities, but could not issue the corresponding operation. Encode timestamp capture as a fixed-size queue record carrying the retained target buffer, root offset, exact queue affinity, and flags. Submit it through the existing frontier path and lower it through the generic HAL API on the server, requiring no response message or host wait. Move the documented queue-buffer range, access, visibility, and alignment checks to the public HAL entry point and share them with direct atomics. AMDGPU keeps only its allocation and resolved device-address checks. Add generic queue timestamp CTS coverage plus malformed-record, asynchronous failure, and remote backend witnesses.
Adapt remoting to the HAL file boundary hardened on main. Make FILE_OPEN request-response so a remote file is published only after the server returns its resolved resource ID, immutable extent, and granted access. Keep external file registration provisional and all data movement queue ordered while deleting FILE_OPEN pending-command state and returning open failures directly. Honor optional synchronous I/O in the generic CTS and update bulk staging failure doubles to intercept the queue-copy path used by storage-backed files. This keeps cleanup coverage aligned with the production lowering instead of invoking stale null vtable entries.
benvanik
force-pushed
the
users/benvanik/remoting
branch
from
August 16, 2026 20:00
210c880 to
499c600
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Remote HAL turns an IREE HAL device into a distributed object: applications use the normal HAL resource and queue model in a client process, while
iree-serve-deviceowns the native device and executes work in a server process. This separates applications, models, weights, and executable selection from the machine that physically hosts the accelerator without introducing a second execution API.The primary deployment is a trusted private network or compute fabric. A server listens on the network by default so another machine can connect without hidden bind configuration, and startup prints the actual listener plus a directly usable client URI. TCP is plaintext and the protocol deliberately provides no peer authentication, authorization, confidentiality, or active integrity; deployments that require those properties establish them outside IREE with fabric isolation, IPsec, WireGuard, or an equivalent network boundary. SHM relies on operating-system endpoint permissions, RDMA relies on fabric admission, and exposing any transport directly to an untrusted network is outside the deployment contract.
The implementation is a transport-independent networking substrate plus a remote HAL client and server:
Remote HAL maps HAL ownership and synchronization onto these channels instead of emulating every C function with a synchronous RPC. Buffers, executables, command buffers, semaphores, events, files, and virtual-memory reservations have explicit remote identities and release rules. Queue operations retain normal HAL semaphore dependencies, server failures advance or fail the affected signal frontiers, and disconnect converges outstanding work on one terminal session state.
The detailed code and debugging map lives in
runtime/src/iree/hal/remote/README.md.Native GPU proof
iree-remote-checkis the smallest complete proof of the intended product shape. The client embeds AMDGPU HSACO and Vulkan SPIR-V artifacts, connects to the server, selects a target advertised by the native device, uploads only the selected executable, performs host-to-device and device-to-host transfers, dispatches real GPU work, waits on a HAL semaphore, and verifies the exact result. The client links the remote HAL and driver-neutral target-selection utilities but no AMDGPU, HIP, or Vulkan HAL driver; the server is the only process that loads a native driver.Run the AMDGPU server and client on machines in the same trusted network:
Server startup prints the bound listener and a client device flag. For the wildcard default, replace
<server-host>with a hostname or interface address reachable from the client;0.0.0.0is a bind address, not a client destination. A same-host client can use127.0.0.1:5000against the same default listener.The same client can target a Vulkan server:
The smoke client is independent of IREE compiler and VM artifacts. Its AMDGPU artifact set follows the configured target selectors, and its Vulkan artifact is assembled independently as Vulkan 1.3 BDA SPIR-V. This keeps the proof focused on native HAL execution, executable compatibility, transfer, dispatch, synchronization, and process-boundary dependency isolation.
Architecture and review map
1. Exact asynchronous lifecycle contracts
The networking stack is built on production async primitives for socket operations, event sources, receive pools, proactor registration, cancellation, and notification-driven shutdown. io_uring, IOCP, POSIX, and fallback paths obey the same ownership contract: every accepted operation has exactly one terminal completion, callback storage remains alive until the backend proves release, and shutdown joins real completion state rather than sleeping for a guessed drain interval.
The highest-pressure invariant is teardown. Listener cancellation, event-source unregistration, multishot receive emulation, nested callback submission, io_uring registration, and proactor destruction all have explicit terminal states. Polling distinguishes internal progress and explicit wakeups from user callback count so completion-driven loops do not mistake a productive poll turn for failure.
2. Message carriers and portable TCP
iree_net_carrier_tdefines transport-independent framed message movement, send reservations, direct operations, capabilities, and terminal failure behavior. The begin/commit/abort reservation model allows storage to be prepared without copying and permits out-of-order producer commit while preserving completion ownership. Backpressure is represented explicitly instead of becoming an unbounded allocation path.Loopback provides an in-process semantic reference and TCP provides the default portable cross-machine path. Peer departure is terminal and observable, valid frames may wait for local stream activation during endpoint setup, send storage survives until completion, and listener shutdown cancels pending accepts. Carrier CTS applies the same lifecycle, reliability, pressure, reservation, direct-operation, and zero-copy contracts across implementations.
3. Sessions and semantic channels
iree_net_session_tturns one carrier connection into a negotiated set of endpoints. Bootstrap exchanges protocol versions, topology, frontier axes, transport capabilities, and immutable application payloads before endpoints become visible. The parser bounds every variable-length field and is fuzzed independently; incompatible protocol revisions fail instead of entering a compatibility mode.Control channels carry typed request, response, and notification traffic. Queue channels carry commands and advances ordered by explicit frontier values rather than carrier arrival or FIFO assumptions. Bulk channels carry transfer-ID-keyed START, DATA, COMPLETE, ABORT, and CREDIT frames. Endpoint detach, GOAWAY, and carrier failure converge on one session failure so a semantic channel cannot remain half-alive after its transport has become terminal.
4. Remote HAL resources and queue execution
The remote client exposes an ordinary
iree_hal_device_t; the server retains one or more ordinary local devices. Session-scoped resource IDs encode type, generation, server proactor placement, and table slot. Client-created resources may begin with provisional IDs so queue work can reference them before a control response returns; the server parks dependent work until the provisional ID resolves and reports the canonical mapping on the corresponding response or queue advance.The protocol covers allocation, mapping, semaphores, events, executable upload, reusable and one-shot command buffers, queue allocation and deallocation, fill, update, copy, dispatch, file operations, profiling, extension operations, and batched release. Release messages carry the greatest submission epoch that could still reference a resource, preventing control traffic from freeing an object ahead of previously submitted queue work. Queue failures are serialized into ADVANCE messages and surface through the client HAL semaphore path used by local devices.
5. Device specifications and executable selection
The server serializes immutable device specifications, memory heaps, and executable target tables into the session bootstrap payload. The remote device is not published until that catalog has been validated and the queue and bulk endpoints are ready. Clients can therefore cache device facts, choose a compatible executable locally, and upload only the selected artifact without paying one synchronous network round trip per query.
Executable loads carry the stable target-table ordinal, queue affinity, load flags, specialization constants, and backend-native bytes. AMD target parsing and family compatibility live in driver-neutral HAL executable support so remote clients, HRX, HIP, and AMDGPU share vocabulary without linking each other's driver implementations. Wire sizes use explicit fixed-width representations, protocol peers must be little-endian, and mismatched revisions are rejected.
6. Bounded bulk transfer, files, and profiling
Large payloads move independently of latency-sensitive control and queue traffic. Transfer tables, chunk descriptors, peer credits, receive windows, staging pools, and owned upload/download components keep memory bounded under pressure. DATA chunks may arrive out of order, but each transfer reservation covers a precise byte range and completion, cancellation, or failure retires every descriptor exactly once.
Remote file operations support both client-local files bridged over bulk transfer and server-local files exposed through an explicit logical allow-list. Clients never submit arbitrary server paths; final path and access validation happen against the server-owned namespace. Profiling relays server sink callbacks into a client-owned sink while retaining session and callback state through terminal completion, so profile backlog cannot outlive its connection.
7. Virtual-memory operations
Remote allocator support carries virtual address reservation, physical allocation, map, unmap, and access changes across the protocol. Virtual and physical resources have distinct message namespaces, queue affinity and access-agent selection are preserved, and the server returns opaque HAL addresses rather than exposing a native driver object to the client.
AMDGPU implements the corresponding allocator operations and advertises only capabilities it can execute. Partial failures unwind mappings, reservations, and physical resources through the same ownership graph. This is the generic pointer-stability mechanism; higher-level sparse pool policy and HIP pointer semantics are intentionally not encoded into the remote HAL allocator contract.
8. Cross-process shared memory
The SHM carrier provides same-host IPC with shared rings, adaptive progress, a cross-process wake primitive, direct registered-buffer reads and writes, and endpoint bootstrap over Unix domain sockets or Windows named pipes. Unix descriptor and Windows HANDLE transfer support remote file sidebands without copying file contents through control messages.
Operating-system endpoint permissions define admission. Shared mappings and transferred handles have one owner, wakeups cannot be lost across the arm/sleep boundary, peer death wakes stranded waiters, and in-process carrier tests are supplemented by real cross-process multi-endpoint coverage on both POSIX and Windows implementations.
9. RDMA transport and remote memory windows
The Linux RDMA carrier uses librdmacm and libibverbs through dynamic loaders, so an RDMA-enabled binary can still start on a system without those libraries. The implementation owns CM event handling, device and protection-domain context, completion queues, queue pairs, receive credits, send windows, work-request retirement, registered staging, direct read/write, endpoint negotiation, and exported memory windows. It supports RoCE and InfiniBand deployment shapes and includes DMA-BUF registration probing for GPU-direct paths.
CM and CQ failures remain terminal, every work request retires once, queue depth and explicit credits bound outstanding traffic, and registration lifetime spans every DMA that references it. Requesting RDMA is a hard capability requirement and never silently downgrades to ordinary TCP bulk semantics. RDMA is Linux-only and opt-in in both build systems; disabled
//...builds do not fetch its headers or pull its libraries.10. Deployment and dependency boundaries
iree-serve-devicedefaults totcp://0.0.0.0:5000because the intended use is another trusted machine, not an accidental loopback-only demo. Readiness is printed only after the listener starts, wildcard addresses become a usable client template, explicit loopback binds are classified as same-host, and SHM reports its operating-system permission boundary. Network-visible startup reports the surrounding trust boundary and the absence of protocol authentication or encryption; any peer admitted through that boundary can submit arbitrary HAL work to the exposed device.Build configuration lives under
//runtime/config/haland//runtime/config/net, with TCP enabled by default and SHM/RDMA explicit transport choices. Bazel is the source of build graph truth and generated CMake metadata carries equivalent conditions. Remote-only tools link the remote HAL client and neutral executable-target support but no native HAL driver; HIP never links AMDGPU, AMDGPU never links HIP, and native drivers remain on the server side of the process boundary.Test architecture
Carrier CTS is reusable across loopback, TCP, SHM, and RDMA and exercises reservation ordering, backpressure, reliability, lifecycle, direct operations, and terminal errors. Remote HAL CTS wraps ordinary HAL CTS backends behind an in-process server/client pair so the same queue and resource contracts are exercised through serialization. Dedicated integration coverage exercises real TCP sessions, cross-process SHM, server slot reuse, file transfer, bounded bulk pressure, profiling relay, virtual memory, disconnect, bootstrap parsing, malformed frames, and native AMDGPU/Vulkan executable selection. Bootstrap, framing, control-channel, and carrier parsers have fuzz targets at their untrusted length boundaries.
HRX scope boundary
This branch includes the narrow HRX device-selection and lifecycle bridge used to prove that an HRX process can create a remote HAL device, derive its AMDGPU architecture from the bootstrapped device spec, and run ordinary stream, buffer, and transfer paths without loading a native GPU driver in the client. That probe exposed generic session startup ordering, allocator capability, and receive-pool lifetime bugs that are fixed at their owning layers here.
It does not claim production HIP-on-HRX remoting. That consumer requires a stronger native device-pointer identity contract for nested pointers, sparse virtual-memory-backed pools that do not commit most of device memory on the first small allocation, post-connect failure propagation through HIP synchronization, and a remote-only HRX dependency graph without local-task or VM implementations. Those concerns form a separate consumer review surface and are not hidden behind compatibility fallbacks in this PR.
Reviewer notes
The highest-value review questions are:
The branch history is organized by these ownership and invariant boundaries so the individual mechanisms remain useful archaeology, but the review unit is the integrated system: lifecycle, transport, protocol, HAL semantics, native execution, and teardown must agree at the branch head.