Module type Rig_edge.Driver
Drivers.
rig counts the calls in flight on each device that is not lost, and calls Driver.stop once, when none is. Every call of the driver's OCaml functions is counted except Driver.facts, Driver.locate, Driver.peer and Driver.stop. rig calls locate and peer at any time, and they must call nothing that may block or fault. Calls of the C functions are counted where the device declares may_block. After stop, rig calls only Driver.free, Driver.unload and Driver.signaled, and lets go of the values the device's submissions keep (Rig.Submission.make).
A Driver.Fault from a counted call, and a failed submit or commit of the driver, lose the device.
rig calls a driver's functions but Driver.locate, Driver.peer and Driver.signaled on a thread that holds no lock of rig's, of a driver's or of an io library's. A call of rig's made inside a lock raises Invalid_argument as it starts (Rig's domains section). A driver must count each lock it holds while OCaml code runs, or while a stub runs pending signal handlers, among the locks its thread holds, from before the take to after the give (rig_hold in rig_edge.h). Its functions that may take a lock and that rig calls without its own check, those of its capability, its opener's make and its own public functions, must start the same way: each must raise Invalid_argument, having changed nothing, while rig_holds () is not 0. Driver.locate, Driver.peer and Driver.signaled check nothing and must take no lock, since rig calls signaled while it holds a lock of its own and the other two at any time. A function that takes no lock may be called from a callback at any point.
Warning. Another caller must call the functions of this signature only as rig does, holding no lock. Nothing checks it before the call takes a lock. A take of a lock by a thread that holds that lock, or of one of rig's by a thread that holds any, raises Invalid_argument, having taken nothing, but what the call did before stays done, and a take of another driver lock out of the driver's order can make two threads wait for each other for good.
Another exception that escapes Driver.alloc, Driver.free, Driver.map_peer, Driver.map_host, Driver.dma, Driver.image, the function of a Place, Driver.entry, Driver.grow or Driver.unload on a device that is not lost also loses it, with the exception's text, since the call may have left the driver's state half changed. An Invalid_argument from entry loses nothing. Neither does any exception from Driver.facts, Driver.locate or Driver.peer, or one other than Fault from Driver.prepare, Driver.sleep or Driver.signaled.
An asynchronous exception (Rig's Errors section) can end any OCaml code of the driver, and rig calls Driver.stop, Driver.free and Driver.unload after it. A driver must therefore take each lock it holds while OCaml code runs immediately before the handler that gives it back, with no allocation or call in between, and must give it back first in both the return and the raise, as Mutex.protect does.
rig calls the driver's functions but Driver.locate, Driver.peer and Driver.signaled with the calling thread's signals blocked, all but those a fault raises (rig_mask in rig_edge.h), and gives the thread its mask back once it has recorded the call's outcome. A signal's handler therefore runs in a driver only where the driver unblocks signals: inside its waits, each of which calls rig_unblock right before its blocking step and rig_block right after it, whether the step returns or raises, and whose step releases the OCaml runtime while it blocks, as a blocking stub does. rig_block runs the handlers of the signals that came during the wait first, so an exception a handler raises ends the wait, and the call as an asynchronous one does. A signal that comes elsewhere in the call runs its handler at the call's next wait or once the call has returned. The driver's functions that start with the check of rig_holds () above block signals the same way for their whole run. Its finalisers and exit handlers block them with rig_shield, waits included, so that no signal's exception escapes them into the code they interrupt or skips the exit handlers after them. A thread the driver starts blocks them for its life, so that a signal sent to the process runs its handler on a thread of the program's. On Windows, where OCaml has no signal masks, none of these block a signal: its handler runs at any allocation or poll point of a driver's function, finaliser or exit handler. A driver's OCaml code counts its locks and blocks signals through Rig_edge_primitives.
rig guarantees these properties, so that a driver checks none of them:
- it frees each region at most once, on its device, and never again after a
freeof it raised; - it frees a region only after every
Driver.map_peerregion over it; - it calls
Driver.peerandDriver.map_peerwith two distinct devices of the driver; - it calls
Driver.dmaonly with a regionDriver.allocmade, on a device that is not lost; - it unloads each image once, and calls
Driver.prepareandDriver.entryonly on a loaded image; - it allocates
Pinnedmemory whereallocofMappedreturnsNone, orDevicememory on a device that declareshost_addresses; - it bounds hangs itself (
hang_ms), soDriver.sleepnever times out work.
type tThe type for open devices of the driver.
type regionThe type for memory of a device.
type imageThe type for loaded code.
exception Fault of stringFault why reports that the device faulted. Any counted call may raise it.
key identifies the driver. rig asks map_peer only of two devices with the same key.
Memory
alloc d m n is n bytes of d's memory m, or None if d has none. n is positive and a multiple of 4, Rig.word, so the region holds a buffer's bytes past its length up to a whole word. Counted.
free d r gives back r, which is a region alloc made, a mapping map_peer or map_host made, or d's word of the driver's own (Rig_edge.facts). rig frees r once no work of d that uses it can run, and may free it after stop.
peer d d' is true iff map_peer d d' maps Device memory of d'. It must return its result from what d and d' learned when they opened, also once either is lost.
map_peer d d' r maps for d the memory of r, any memory of d'. d's GPU may already map r's memory, through r or another region. Each region must keep the memory mapped until rig frees it. Counted.
map_host d p n maps for d the n bytes of host memory at p. p starts a page and n is positive. rig calls it only where d declares maps_host. The region must map the whole pages that hold the n bytes, so that work reaches through it each word of the allocation's last bytes (Rig's Words section), and they must stay mapped until the region is freed (free). Counted.
dma d r ~fn is how the PCI function at the bus address fn of d's machine, such as "0000:41:00.0", reaches all of r's bytes by DMA, or Error why if it cannot, why naming the cause, such as a memory BAR smaller than the GPU's memory. It is Error why, why naming fn, if fn is not a bus address or names no function of the machine. rig calls it only with a region alloc made. Counted.
The driver decides whether fn reaches the memory where it lies, and must return Error rather than move the memory where fn reaches it. It must make the memory visible to fn after each value's work that wrote it, and visible to the device's later work after fn wrote it and a call of rig's submits that work.
For Bus, the driver must keep the memory where it is, out of any other allocation, and keep d's function from any other open, until the process exits, whatever rig frees or stops meanwhile.
Code
image d b takes the binary b, or is Error why if d refuses it. Counted.
val entry : image -> string -> entry_answerentry i f is Entry e with what the driver needs to launch i's function f, or Absent if i has no function f. It must make what a launch of f needs, such as a compiled pipeline or f's scratch memory, so that the driver's submit allocates nothing to launch f. A second call for f must make nothing new. The first call may block. Counted.
It is No_memory n, having made nothing, if the device has no memory for the n bytes f's launches need. rig then runs the rounds of reclamation an allocation does, asks again after each, and raises Rig.Out_of_memory after the last.
Raises Invalid_argument with the driver's reason if f needs more than the device offers, where the driver finds that only here.
prepare i f may start making what entry i f makes and return without waiting, so that the device makes the entries of several functions concurrently: it is entry's asynchronous start. It must report nothing, failures included: the next entry i f must wait for what it started and answer as if it made the entry itself. A prepare of an f that has an entry or a started prepare must start nothing, and a prepare that raises must have changed nothing. Doing nothing is a valid prepare, as on a device whose entry makes nothing slow. Counted.
Queues
grow d makes what d's queues need to hold the parts of the submission whose room last answered RIG_GROW on the calling thread (The C functions). It changes nothing the C functions read: a later room puts what it made into the queues. Once it has answered Grown, room must not answer RIG_GROW for those parts again, whatever other submissions' growths answer.
It is Short n if d has no memory for n bytes the growth takes. What it made stays for a later room. rig then runs a round of the reclamation an allocation runs, for d's budget and the host's, and asks room again, which answers RIG_GROW anew for parts that still need a growth.
rig calls it on the thread whose room answered RIG_GROW, after that answer and before that thread calls room again, while the C functions may run on other threads for other submissions. A submission asks at most four times, as an allocation tries: after the last, a Short n raises Rig.Out_of_memory with n. A driver whose room never answers RIG_GROW is never asked. Counted.
Timeline
signaled d is the value in d's word, read with acquire order. rig calls it only for a word of the driver's own (Rig_edge.facts), also after stop, until it frees that word. Counted until stop. After it, rig drops its faults. rig may call it while it holds a lock of its own, so it must not block or call rig.
sleep d ~seen ~still_ms blocks on the device's events until the word differs from seen, and must return, or raise Fault, at the latest after still_ms milliseconds. It must return immediately if the word already differs. It may return earlier, on an event of other work or a timer of the driver's own, since rig reads the word again after every return and sleeps again while it is unmoved. rig may call it while the driver's submit or commit runs. Counted.
An exception other than Fault that escapes sleep loses nothing (Drivers). The driver must record each event it consumes, such as a fault's report, before it consumes it, so that the next call of sleep still raises Fault for it.
Raises Fault once the device has faulted.
Stopping
stop d ~fault stops the lost d and must not wait for its work. rig calls it only at d's close or at an open of its name, with no counted call of d in flight. fault is Some why if rig lost d for a Fault of a counted call or for its hang bound (hang_ms), and None otherwise.
- The driver must write the last value given to its
submitinto the word, with release order, also when thatsubmitfailed, and only once no work ofdruns, since rig then frees the memory that work uses. If no work runs whenstopis called, the driver writes it beforestopreturns. Otherwise the queues' releases write it, or the driver does once that work runs no more. - The driver must drop the work
dencoded that runs only once committed, such as an open command buffer. stopmust release nothing rig made. rig frees each region and unloads each image itself, also afterstop.stopmust release what the driver made to run work, such as its queues, and must not wait for that work. A word of the driver's own, and whatsignaledneeds to read it, must stay until rig frees the word.- Once the word shows the last value given to
submitandstophas returned, no write ofdor of its work to the word may land, since the word then passes to a later device.
After stop, rig calls free and unload only once the word reads that last value, also after a stop that raised, such as one an asynchronous exception ended before it ended the device's work. It calls them uncounted and drops their faults. They may come after an open of the same hardware has made a new device of this driver, and must touch nothing of the new device.