github ↗
docs menu

Module Rig_program

Programs of steps on one machine's devices, as data that loads once and runs many times.

A program is the memory and images a computation needs on the devices of one machine, and the steps each run of it takes: submissions, copies and loops. Its description (t) is plain data that names each of these by its index in the description, so it travels between processes as bytes (to_string). What only the loading process knows, such as an address or an image's function, stands in the description's bytes as a hole, which load fills, or each run for a value it reads (hole). What changes from one run to the next is the run's frame (frame): its input buffers and the first words of its ints, 64-bit integers in host memory that loops and the work of its steps also read and write.

Memory the program makes may have two copies (copies). Run n uses copy n mod 2, so a run's work may write its copy while the previous run's work still reads the other. The run's ints always have two copies.

The life of a program goes like this:

  1. A caller builds its description, in this process or in another one that sends it as bytes.
  2. load checks the description against the devices of one machine, makes its memory, loads its images and prepares a Rig.Submission for each Submit step, one per copy where the step names memory of two copies.
  3. run runs the steps once, in order, over a frame, and returns its ends, the points its work ends at. A loaded program runs any number of times.
  4. Once the loaded program is unreachable, its memory and images return after the work of its last run.
  t ──load devices──> loaded ──run frame──> ends
  │                     │
  memory, images,       Rig.Buffer, Rig.Image,
  steps                 a Rig.Submission per Submit step and copy

A program for another machine's devices loads and runs through that machine's agent (Rig_remote, Programs of another machine), and programs on two machines exchange bytes through rails (Rails). Its work is ordered as every submission's is (Order).

Order

A step's work follows other work by the memory the step names, as every Rig.submit's does (Order). A Submit step names the memory of its slots, of its parts' views and of its fixed, and a Move the memory of its two slots. A step therefore follows the earlier steps whose memory it names, and its device's earlier work.

Work that reaches memory through an address it was given escapes this order, so an address or handle may stand only where its step names the memory. A step names memory whole if one of its views starts at the memory's first byte and covers all of it, and fixes it whole if that view is in its fixed. load returns Error unless:

  • an image holds no Address or Handle;
  • an Address in a launch's or a fill's parameters or geometry is of memory its step fixes whole, on its step's device, and a Handle there is of memory its step names whole;
  • a loop's trips hold no Address, and a Handle there is of memory the loop's flag names whole;
  • a Submit step that names memory whose init holds an Address or a Handle of other memory names that other memory whole too;
  • an Address on device d in an init is of memory that a Submit step on d fixes whole.

Host code runs as a Submit step on the machine's host, whose launches are done when its submit returns (Launches).

Rails

A program reaches its machine's end of a rail (Rig_remote_abi.end_) as Rail memory, in three areas: Outbound holds the bytes it sends, Inbound the bytes it receives, and Counts the counts that number the transfers. Outbound and Inbound each hold two copies of their bytes, independent of the program's copies, and Rail memory covers both, so a step's views select the copy a transfer uses.

No stamp records the rail's own reads and writes, so rig does not order them. A program orders them by the rail's counts, as Rails states. To send, a Submit step on the host after the work that writes Outbound names Outbound, so that it follows that work, and Counts, and its launch calls the rail's ready function, given as a Ready leaf, with the address of Counts and the count.

Domains

load and run may be called from any domain. Runs of one loaded program share its memory, so a run waits for the previous one to return before its first step.

Descriptions

#type leaf =
  1. #| Address of {
    1. #memory : int;
    2. #on : int;
    }
    (*

    The address of memory memory's first byte as device on's work addresses it, as Rig.Submission.address gives it for a step on on that fixes the memory whole. For memory of two copies (copies), the address of the copy the run uses.

    *)
  2. #| Handle of int
    (*

    The driver's object for memory i (Rig.Buffer.handle), such as an MTLBuffer. For memory of two copies, the object of the copy the run uses.

    *)
  3. #| Entry of {
    1. #image : int;
    2. #name : string;
    }
    (*

    What compiled code names image image's function name by (Rig.Image.entry): a kernel descriptor's address, a CUfunction, an executable graph, the address of a function of the host's code. The loaded program keeps the image until the work of each of its runs is done.

    *)
  4. #| Ready of int
    (*

    The address of the ready function of this machine's end of the rail of id i (ready_fn of Rig_remote_abi.end_).

    *)

The type for what only a loaded program knows: a word of its memory or its images, filled once at load.

#type width =
  1. #| W32
  2. #| W64

The type for the widths of holes.

#type 'a hole = {
  1. #at : int;
    (*

    The hole's first byte.

    *)
  2. #width : width;
  3. #terms : ('a * int) iarray;
    (*

    Each value with its factor.

    *)
  4. #add : int;
  5. #shift : int;
}

The type for holes: places in a description's bytes that receive a value when the program loads or runs. A hole ORs the low width bits of s + add, as a 64-bit word shifted right by shift bits, into the little-endian word at byte at, where s is the sum, over terms, of each value times its factor. The bytes hold the word's constant bits, and 0 where the value goes. For example:

  • the high half of an address a is { width = W32; terms = [: (a, 1) :]; add = 0; shift = 32 };
  • a word with an address in its low bits and a length in its high bits holds the length in the bytes;
  • at a ref's at, whose bytes hold the offset the ref adds to its slot's address (Rig.Submission.ref), the terms [: (Int i, n) :] reach row r of rows of n bytes, where word i of the run's ints holds r, such as the trip a loop stores there.

The rules:

  • shift is 0 to 62, and no two holes of one bytes share a byte.
  • A value that meets a set bit of the bytes under it would overwrite a constant. load raises for it, and run for a parameter's value read per run.
  • The sum is computed on OCaml ints, which wrap modulo 2{^63}. The 64-bit word is the sum's two's complement, and the shift brings in zeros at the top, so the high half of -8 is 0xFFFFFFFF. The value sets bit 63 of a W64 word iff it is negative and shift is 0. A word whose bit 63 is a constant holds it in the bytes.
#type 'a data = {
  1. #bytes : string;
  2. #holes : 'a hole iarray;
}

The type for bytes with holes.

#type value =
  1. #| Fixed of int
    (*

    The integer itself.

    *)
  2. #| Int of int
    (*

    Word i of the run's ints (t.ints), as it holds when read: after the frame's ints were written, the enclosing loops' trips stored and the run's earlier work that writes the ints is done.

    *)
  3. #| Leaf of leaf

The type for what a run knows: a word read when the step that needs it runs.

#type copies =
  1. #| One
    (*

    One, which every run uses.

    *)
  2. #| Two
    (*

    Two: run n uses copy n mod 2, once the work of run n - 2 on that copy is done.

    *)

The type for how many copies of its memory a program keeps.

#type area =
  1. #| Outbound
  2. #| Inbound
  3. #| Counts

The type for the areas of a rail's end (Rails).

#type memory =
  1. #| Alloc of {
    1. #device : int;
    2. #kind : Rig.Buffer.kind;
    3. #bytes : int;
    4. #init : leaf data;
      (*

      Its first bytes, written at load into each copy, each hole filled for that copy.

      *)
    5. #copies : copies;
    }
    (*

    Memory made at load (Rig.Buffer.create).

    *)
  2. #| Rail of {
    1. #rail : int;
    2. #area : area;
    }
    (*

    The area area of this machine's end of the rail of id rail: host memory the rail made, which the program borrows and every run uses (Rails).

    *)

The type for a program's memory.

#type image = {
  1. #device : int;
  2. #binary : leaf data;
}

The type for images: binary in device's format, loaded in the description's order (Rig.Image.load). A hole may name an earlier image's entry, so an image can be a vendor object over loaded code, such as a CUDA graph over a module's functions, on a driver that loads such objects.

#type view = {
  1. #memory : int;
  2. #offset : int;
  3. #length : int;
}

The type for length bytes of memory memory from its byte offset. For memory of two copies, the bytes are those of the copy the run uses.

#type slot =
  1. #| Memory of view
  2. #| Input of int
    (*

    The run's input i, whole.

    *)
  3. #| Ints
    (*

    The run's ints, whole: 8 * ints bytes of host memory.

    *)

The type for the buffers a step names.

#type use =
  1. #| Read
  2. #| Write
  3. #| Unordered

The type for how a ref's work uses its slot, as Rig.Submission.read, Rig.Submission.write and Rig.Submission.unordered.

#type ref = {
  1. #slot : int;
  2. #at : int;
  3. #use : use;
}

The type for refs, as Rig.Submission.ref. The work uses the submit's slot slot as use says, and the 8 parameter bytes at at hold an offset into that slot's buffer.

#type part =
  1. #| Words of view
  2. #| Fill of {
    1. #fill : leaf;
      (*

      The function the fill calls: an Entry or a Ready.

      *)
    2. #params : value data;
      (*

      Its parameters' bytes, a whole number of 32-bit words, each hole written into the run's block before each submit.

      *)
    3. #refs : ref iarray;
    }
  3. #| Copy of {
    1. #src : int;
    2. #src_offset : (value * int) iarray;
    3. #dst : int;
    4. #dst_offset : (value * int) iarray;
    5. #length : int;
    }
    (*

    A copy of length bytes from byte src_offset of the submit's slot src into byte dst_offset of its slot dst, as Rig.Submission.copy. An offset is the sum, over its terms, of each value times its factor, an Int read when the step runs, so a copy in a loop may move with the loop's trips. The sum is exact: a product or a partial sum past an OCaml int is refused, by load for an offset of Fixed terms and by the run at the step for one that reads an Int. An end whose slot is Memory and whose offset reads no Int is those bytes of the memory, set once at load. Any other end is a Slot end (Rig.Submission.side), whose buffer each run passes.

    *)
  4. #| Launch of {
    1. #image : int;
    2. #name : string;
    3. #params : value data;
      (*

      Its parameters' bytes, a whole number of 32-bit words, each hole written into the run's block before each submit.

      *)
    4. #refs : ref iarray;
    5. #groups : value * value * value;
    6. #threads : value * value * value;
    7. #shared : value;
    }

The type for parts, as Rig.Submission.part.

#type submit = {
  1. #device : int;
  2. #parts : part iarray;
  3. #slots : slot iarray;
  4. #fixed : (view * Rig.access) iarray;
}

The type for submissions: parts on device device. Each run passes slots, which the parts' refs and copies index, and every run uses the fixed memory fixed (Rig.Submission.make). Memory of another device is named through its borrow on device, made at load, and an input of another device through a borrow made for each run.

#type step =
  1. #| Submit of submit
    (*

    One Rig.submit.

    *)
  2. #| Move of {
    1. #src : slot;
    2. #src_offset : (value * int) iarray;
    3. #dst : slot;
    4. #dst_offset : (value * int) iarray;
    5. #length : int;
    }
    (*

    One Rig.Buffer.submit_copy of length bytes from byte src_offset of src's buffer into byte dst_offset of dst's. An offset is the sum of its terms, as a Copy part's, so a move in a loop may move with the loop's trips. Between the program's host memory, the ints included, and memory of another of its devices, the move names the host memory's borrow on that device, made at the first such move and kept with the program, so that the device copies it directly and the host does not wait for the bytes (Copying). Host memory a move names is made so that a device can borrow it (Rig.Buffer.create). Where the device does not borrow it, or has no room to map it, the program's moves through it pass through staging memory.

    *)
  3. #| Loop of {
    1. #trips : value;
    2. #trip : int option;
    3. #flag : (slot * int) option;
    4. #body : step iarray;
    }
    (*

    body, trips times, none where trips <= 0, storing each trip's index from 0 into word trip of the run's ints. With flag = Some (s, at), it reads byte at of s's buffer before each trip, once the work that wrote it is done, and stops at 0.

    *)

The type for the steps of a run.

#type input = {
  1. #device : int;
  2. #bytes : int;
}

The type for inputs: a buffer of device of at least bytes bytes.

#type t = {
  1. #devices : string iarray;
    (*

    The Rig.arch each device's code is compiled for, checked at load: a device runs code of its own arch, a host of an x86-64 level also runs code of each level below it, and an arm64 host code of each arm64 arch whose features are among its own.

    *)
  2. #memory : memory iarray;
  3. #images : image iarray;
  4. #inputs : input iarray;
  5. #ints : int;
    (*

    The number of 64-bit words of the run's ints, host memory that load makes in two copies (copies). A frame writes the first words, loops store their trips there, and the work of its steps may write others.

    *)
  6. #steps : step iarray;
}

The type for descriptions.

Bytes

#val to_string : t -> string

to_string t is t as bytes, in a format with a version of its own.

#val of_string : string -> (t, string) Stdlib.result

of_string s is the description to_string wrote as s. It only decodes, and load checks the description.

Error why if s is malformed, why saying where, or is of another version of the format, why naming both versions.

Loading

#type loaded

The type for loaded programs. A loaded program keeps its memory, images and the rail ends rails gave it while it is reachable. Once it is unreachable, they return as any memory does, after the work of its last run (Reclamation).

#val load : ?rails:(int -> Rig_remote_abi.end_ option) -> t -> Rig.t iarray -> (loaded, string) Stdlib.result

load ~rails t devices is t loaded on devices, all of one machine (Rig.host_of). Device i of t is devices.(i). rails id is this machine's end of the rail id (Rig_remote_abi.rail), and defaults to none. It loads the images in order and writes each Alloc memory's init last, their holes filled. A Submit step's submission borrows the memory the step names on its device, and stores the parameters and geometry that no run changes. On another machine, load checks t here and loads it through the agent (Programs of another machine).

Error why only for what a device refuses:

  • a device refuses an image (Rig.Image.load), or has no function a Launch or an Entry names, or cannot run it (Rig.Image.resolve), why starting with the device's name;
  • a device that names memory cannot borrow it (Rig.Buffer.borrow), or an Address is of memory its driver names by handle only;
  • Rig.Submission.make, a part's constructor or a run (Rig.Submission.Run) refuses a step, why naming the step;
  • on another machine, the agent's load is Error why.

Raises Invalid_argument if t breaks a rule its types or Order state, which load checks before it makes anything, but for a hole's value, which it checks as it fills the hole. Among them:

  • devices are not as many as t's, or a device does not run code of t's arch for it: its Rig.arch differs, and is neither an x86-64 level at or above t's nor an arm64 arch with each of t's features;
  • an index is outside its array, a size is negative, or an init or a view lies outside its memory;
  • a leaf in an image or in memory of one copy names memory of two copies, or an Entry names a later image;
  • an Int or a loop's trip is past the ints, or a loop's flag lies at a negative byte or past its slot where load knows the slot's length: a Memory view's length and 8 * ints for the ints;
  • a ref or a copy names no slot of its submit, a slot is named by no ref or copy, or a slot is named by both an Unordered ref and a Read or Write ref or a copy;
  • a copy's or a move's length is negative, an offset names a Leaf, or an end's offset that reads no Int is past an int, is negative or puts the end past its slot where load knows the slot's length: a Memory view's length and 8 * ints for the ints;
  • a copy's or a move's two ends share a byte of one memory, of the ints or of one input, at offsets that read no Int;
  • Rail memory or a Ready names a rail that rails does not give;
  • on another machine, a device is not an agent's.

Raises Invalid_argument also if devices are of several machines, or are of another machine and rails is given, or, having done nothing, from a callback inside a lock (Domains); Rig.Out_of_memory if memory cannot be made; and Rig.Lost if a device is lost.

Running

A run writes an input if a Submit step passes it in a slot that a Write or an Unordered ref or a copy's dst names, or a Move has it as its destination.

#type frame = {
  1. #inputs : Rig.Buffer.t array;
  2. #ints : int array;
  3. #step_points : Rig.Point.t array;
}

The type for a run's frame: its inputs, by index, the first words of its ints, and where the run reports each step's point. step_points has one element per Submit step of the program, in step order, a loop's body counted once. run stores into each element the point of its step's last submit in the run. A step the run does not reach, such as the body of a loop of zero trips, holds after that run the shared reached point of Rig.host that step_points starts each element at, so no element ever holds an earlier run's point.

#val step_points : loaded -> Rig.Point.t array

step_points p is a fresh array for the step_points of p's frames, each element the same point of Rig.host, which is reached. A caller makes it once, with the frame, and passes it to every run: a run allocates nothing for it.

#val run : ?waits:Rig.Point.t array -> loaded -> frame -> Rig.Point.t array

run ~waits p f runs p's steps once, in order, over the frame f (Order). It is the run's ends, the points its work ends at: for each device that ran a submission or a move, in the order of p's devices, the point of its last one. A move runs on the device of the point Rig.Buffer.submit_copy returns for it. Each device's first submission, and each move, follows the points of waits (defaults to none) and the device's earlier work.

  • It first waits for the work of run n - 2 on the copies this run uses, the run's ints included, then writes f's ints.
  • The work it does on the host, Submit steps on the host, moves the host makes and loops' flags, is done when it returns. A move whose point is of a device outside p's is done at its step, which waits for it.
  • An Int value is read once the run's earlier work that writes the ints is done, and a loop stores its trip once the run's earlier work that uses the ints is done.
  • It reads f's inputs and ints once, before its first step, so a change to them during the run changes nothing. It keeps f's inputs, and their borrows on the other devices that name them, until it returns.

On another machine, the raises at a step fail the job there instead (Programs of another machine).

Raises Invalid_argument:

  • before any step runs, if f has another number of inputs than p, more ints, or step_points another number of elements than its Submit steps, or if an input is not on its device, holds fewer bytes, is read-only memory (Rig.Buffer.access) that the run writes, or cannot be borrowed by a device that names it, if it is called on the thread that runs p, as from a callback inside that run, or from a callback inside a lock (Domains);
  • at a step, after the earlier steps ran, if the run refuses a launch's geometry, shared memory or a parameter's value read from the ints (Rig.Submission.Run), if a copy's or a move's offset is past an int or puts its end outside its slot's buffer, if a loop's flag lies past its input's bytes, and as Rig.submit and Rig.Buffer.submit_copy raise.

Raises Rig.Lost as the devices raise it.

Programs of another machine

load on another machine's devices sends that machine's host the description, with each device's id on that machine, as the binary of an image (Rig.Image.load), and asks for the image's function "run" (Rig.Image.resolve). Each run is then one submission on that host, whose one part is a Rig.Submission.words: the function's value, the frame's inputs, each as its memory's id on the machine, its offset and its length, and the frame's ints. The submission names only the memory of this process that holds these words, and its point is reached once the run's work on that machine is done. run stores that point in every element of the frame's step_points. The machine's agent runs it after the work submitted to the machine before it, and runs the work submitted after it once the run is done (Rig_remote's order section).

run raises here, before the submission, for each refusal that comes before any step. It also raises Invalid_argument here for a launch whose threads per group exceed the most its function may have (Rig.Image.threads), where each of its three sizes is a constant or a word of the frame's ints that no step may change, and at least one is such a word. A step may change the ints where a Submit passes Ints in a slot it writes or a Move writes Ints, and a loop changes the word trip it stores its trips in. The agent loads and runs the program with the two functions below, and a run they refuse or raise on at a step, such as any other launch with too many threads, fails the job (Rig_remote's failure section).

#val load_received : ?rails:(int -> Rig_remote_abi.end_ option) -> string -> device:(int -> Rig.t option) -> (loaded, string) Stdlib.result

load_received ~rails b ~device is load ~rails of the description in b, the binary another process's load sent to this machine's host, on the devices that device gives for the ids b names.

Error why as load, and where load raises Invalid_argument for a rule the description breaks, since another process made b; or if b is malformed or names an id that device does not give.

#val run_received : ?waits:Rig.Point.t array -> string -> program:(int -> loaded option) -> region:(int -> Rig.Buffer.t option) -> (Rig.Point.t array, string) Stdlib.result

run_received ~waits w ~program ~region is run ~waits of the program that program gives for the value w starts with, over the frame w holds. Each input is the view of region id at its offset and length.

Error why if w is malformed, or program or region gives nothing for an id it names. Otherwise it raises as run.