Transfer.LedgerA copy or launch enqueued on a stream may still be reading or writing its operands after the call returns. The backends see raw addresses, which are not GC roots, so SPOC roots the operands here — Bigarray sources and destinations, the vectors involved — until it KNOWS the work has completed:
synchronize_stream, destroy_stream), or its device was (flush, Device.synchronize);b that waited on an event of stream a completes past that wait, a's work up to the event has completed too. This is what releases a copy stream that is never synchronised itself — the spoc_llm M3 pattern (copy on a, record e, b waits on e, sync b).Entries are ordered by a per-stream sequence number. The ledger is protected by a mutex and is never touched from a GC finaliser (which could run inside Mutex.protect on the same domain and self-deadlock).
type entry = {seq : int;keep : keep;Rooted for liveness only; never inspected.
*)vec_ids : int list;host_ranges : (nativeint * int) list;(address, bytes) in flight.
*)}type stream_state = {sid : int;sdev : Device.t;mutable next_seq : int;mutable pending : entry list;mutable waits : (int * stream_state * int) list;(seq of the wait on this stream, waited stream, its seq)
*)nonblocking : bool;Created with ~non_blocking:true: the default stream does not wait for it, so implicit paths must settle it themselves.
mutable drain : unit -> unit;Synchronise the stream; set by create_stream.
mutable destroyed : bool;Set by destroy_stream. A destroyed handle is refused: the driver may have recycled it, and work enqueued through it would land in a ledger nothing consults any more.
}val live : (int, stream_state) Stdlib.Hashtbl.tval new_stream : ?nonblocking:bool -> Device.t -> stream_stateval forget : stream_state -> unitval check_live : op:string -> stream_state -> unitval enqueue :
stream_state ->
keep:keep ->
vec_ids:int list ->
host_ranges:(nativeint * int) list ->
unitval mark : stream_state -> intSequence number of the last operation enqueued so far: what an event recorded now covers.
val add_wait : stream_state -> on:(stream_state * int) -> unitA wait is itself an operation on b: once b has completed past it, the waited work on the other stream has completed.
val release_locked : stream_state -> int -> unitval release : stream_state -> int -> unitval release_all : stream_state -> unitval device_marks : Device.t -> (stream_state * int) listThe last operation enqueued so far on each live stream of dev: what a device sync started now covers.
val release_marks : (stream_state * int) list -> unitval pending_count : stream_state -> intval nonblocking_holding : int -> stream_state listLive non-blocking streams with pending work that references id.