github ↗
docs menu

Module Rig_amd

The driver of AMD GPUs.

A device of this driver is one AMD GPU. Paths find and take GPUs, and the driver runs them. A path is a library that reaches a GPU in one way and gives the driver what it found there: the GPU's description, and functions that allocate the GPU's memory, make its hardware queues, wait for its interrupt and stop it (path). Rig_amd_amdgpu reaches a GPU through Linux's amdgpu kernel driver, and Rig_amd_pci drives the GPU's PCI function itself, with no kernel driver. Each offers an opener whose make fills a path and calls make.

From any path, make builds the same device. Two queues take rig's work. "COMPUTE:0" is a ring of PM4 or AQL packets that the GPU's compute engine reads, and "COPY:0" a ring of SDMA packets that a copy engine reads. Both rings lie in Pinned memory, so the driver's submit places packets and rings the queues' doorbells with plain stores. The argument segment is Pinned memory into which submit copies each launch's arguments. The last packet of a value's work is its release, which writes the value into the timeline word, the eight bytes of host memory that the opener's make is given, once the work has completed, and raises the path's interrupt, on which sleep blocks. What differs between paths, such as a bound on how long work runs or whether the GPU maps host memory, is a field of the path, which the device's facts carry (Facts). The sections below hold on every path.

rig --room, submit--> COMPUTE:0 ring, PM4 or AQL --+
                      COPY:0 ring, SDMA -----------+--> GPU
                                                         |
                          release of value v, interrupt  |
rig <--signaled, sleep-- timeline word, host memory <----+

path: the GPU's memory, its hardware queues, its interrupt, its stop

A device's life goes like this:

  1. A path's opener reaches the GPU and fills a path.
  2. make makes the device's rings and argument segment, and maps its timeline word, through the path. Rig.open_ runs these two steps through the opener's make.
  3. rig gives each submission to the driver's room and submit (C functions). The GPU runs its parts and releases its value.
  4. Once rig loses or closes the device, stop ends it (Stop).

Unless a value says otherwise, a function that calls the path returns the path's refusal of its arguments as None or Error, and raises Fault for any other failure of the GPU or the path. signaled, free and stop never raise it. Any domain may call any value concurrently with others, except the C functions and stop, which rig calls as Rig_edge.Driver states. sleep may run beside the C functions. The scratch and trace of the device's capability, and an opener's make, called from a callback inside a lock (Rig's domains section), raise Invalid_argument, having changed nothing (Drivers).

A program names a GPU through a path's opener:

ocaml
match Rig.open_ (Rig_amd_amdgpu.gpu 0) with
| Error why -> prerr_endline why
| Ok d -> print_endline (Rig.arch d) (* such as "gfx1201" *)

The C functions states how rig hands a driver work for its queues, and Rig_edge what its timeline word shows. Rig_pci's section on what an open needs states what an open of a GPU with no kernel driver needs.

GPUs

An AMD GPU has two version numbers. Its processor (Rig_amd_abi.Gpu.processor), such as gfx942, names the code objects it runs. Its GC version, such as 9.4.3, names the block that runs its compute queues, whose registers and packets follow it. The Instinct MI300 series is processor gfx942 with GC 9.4.3. This driver names GPUs by processor, and Rig_amd_pci, which programs the GC, by GC version.

The driver drives gfx942, gfx950 (the MI350 series), and the processors of GFX11 and GFX12. On a GPU of one die its compute queue reads PM4 packets. On a GPU of several, such as the MI300X, it reads AQL packets, which every die runs (Rig_amd_abi.Capability.compute). The copy queue reads SDMA packets.

On a GPU of several dies, the AQL queue hands every kernel the device's scratch memory.

The driver has run on gfx1201 alone. Its AQL queue, with the dispatch, scratch and room of GPUs of several dies, has never run on hardware.

Order

"COMPUTE:0" runs words (Rig.Submission.words) and launches, and "COPY:0", the copy engine, runs words and copies. The parts of one queue run in array order. A part's after indices, which rig derives (Rig_edge.queue), order it after parts of the other queue, and two parts of different queues that after does not order may run in parallel.

A copy runs on the copy engine or on the compute engine, and keeps its place in the order of "COPY:0" whichever engine runs it, as does a copy of no bytes.

Value v's work starts once the work of v - 1 has completed, on both queues. A submission of no parts releases its value once v - 1 is reached.

A wait for another device's point holds the submission's work back until the aligned 64-bit word at the wait's address holds at least the point's value, compared as unsigned integers. The compute queue makes such waits only on a GPU whose GC and compute firmware (mec of path) this library knows to compare 64-bit words. On other GPUs the driver's waits lists neither stores nor hosts (Rig_edge.waits), and rig waits on the host.

Caches. The first part of a submission on the compute engine, and any part after a wait for the other queue, follows an invalidation of every cache of the GPU, its L2 included, after the L2 is written back. Any other launch follows at least an invalidation of the caches above the L2. A launch therefore reads what the host, the copy queue, other devices and the launches before it wrote. A value's release writes the L2 back, so the host and other devices read what its work wrote once the value is reached.

Memory

  • Device memory is GPU memory, which the host does not address.
  • Pinned memory is host memory that the GPU addresses, which the host reads as fast as other memory.
  • Mapped memory is GPU memory that the host addresses through the GPU's memory BAR, write-combined, so the host writes it fast and reads it slowly. The host's writes reach the work submitted after them. alloc returns None for it on a path without such memory or whose hdp is None (path).

alloc returns None if the GPU or the host lacks the memory. alloc and map_host raise Invalid_argument for fewer than one byte. Freeing a mapping ends the mapping alone, and the memory it maps stays.

locate returns the GPU address of a region's first byte, and that address again as the handle, since the GPU names memory by address. The host address is None iff the region is Device memory or a map_peer view of Device memory.

peer and map_peer return true and a view at the same GPU address iff paths of one key opened both devices and this device's path reaches the other GPU's memory (reaches of path). A device flushes the host writes of at most 7 other GPUs, so map_peer returns None for Mapped memory of an eighth. A view of Mapped memory flushes the other GPU's host writes before this device's later work until the other device stops, and map_peer of a stopped device's Mapped memory is None. Peer access between two GPUs, with the flushes of each other's host writes, is untested, and so is a path whose hdp is None.

dma is the path's answer (dma of path), or Error where the path gives no register that flushes the GPU's host data path (hdp of path), since another function's writes through the memory BAR pass it as the host's do. Once a region is given, each submission flushes that path before the GPU reads, for the rest of the process, so the device's later work reads what the function wrote. The function reads what a value's work wrote once the value is reached, since its release writes the L2 back, and the host data path of the GPUs this driver drives keeps no read cache to invalidate.

map_host returns a region the GPU addresses, or Never if the path maps no host memory, and otherwise the path's refusal of the pages (map_host). Both paths map host pages for writing, so it answers Never for pages mapped read-only, such as an io device's pages over a read-only file. Rig_amd_amdgpu's kernel driver faults the pages in for writing, and Rig_amd_pci pins them for writing through VFIO.

Images

image returns Place (n, lay) for a code object, whose image takes n bytes (Rig_amd_abi.Code_object.size) of the GPU's own memory, from which its code runs. It returns Error msg:

  • if the binary is not a code object, with Rig_amd_abi.Code_object.of_string's message;
  • if it is for another processor, as "a code object for gfx90a; the GPU is gfx1201";
  • if a kernel takes more local data share than the GPU has, naming the kernel and both sizes.

entry returns the address of the kernel's descriptor, the symbol f ^ ".kd", as code, which a dispatch names, the kernel's explicit_size as params and its max_threads as threads (Rig_amd_abi.Code_object.kernel). It makes the kernel's launch and grows the device's scratch memory to the kernel's private segment. It answers No_memory n, having made nothing, if the GPU has no memory for the n bytes of that scratch. It raises Invalid_argument for a kernel:

  • that reads an implicit argument that is a runtime's service (Other of Rig_amd_abi.Code_object.hidden);
  • that reads its dispatch packet, on a GPU of one die.

unload frees the launches.

Driver

The sections above state what this driver adds to each value's contract.

include Rig_edge.Driver
#type t

The type for open devices of the driver.

#type region

The type for memory of a device.

#type image

The type for loaded code.

#exception Fault of string

Fault why reports that the device faulted. Any counted call may raise it.

#val key : t Stdlib.Type.Id.t

key identifies the driver. rig asks map_peer only of two devices with the same key.

#val facts : t -> region Rig_edge.facts

facts d is d's facts.

Memory

#val alloc : t -> Rig_edge.memory -> int -> region option

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.

#val free : t -> region -> unit

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.

#val locate : region -> Rig_edge.location

locate r is where r lies.

#val peer : t -> t -> bool

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.

#val map_peer : t -> t -> region -> region Rig_edge.mapping

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.

#val map_host : t -> int -> int -> region Rig_edge.mapping

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.

#val dma : t -> region -> fn:string -> (Rig_edge.dma, string) Stdlib.result

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

#val image : t -> string -> ((region, image) Rig_edge.code, string) Stdlib.result

image d b takes the binary b, or is Error why if d refuses it. Counted.

#val entry : image -> string -> Rig_edge.entry_answer

entry 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.

#val prepare : image -> string -> unit

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.

#val unload : t -> image -> unit

unload d i releases what image, prepare and entry made for i. rig calls it once no work of d that runs i can run. The region of a Place is not i's, and rig frees it after. Counted.

Queues

#val grow : t -> Rig_edge.grown

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

#val signaled : t -> int

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.

#val sleep : t -> seen:int -> still_ms:int -> unit

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

#val stop : t -> fault:string option -> unit

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 submit into the word, with release order, also when that submit failed, and only once no work of d runs, since rig then frees the memory that work uses. If no work runs when stop is called, the driver writes it before stop returns. Otherwise the queues' releases write it, or the driver does once that work runs no more.
  • The driver must drop the work d encoded that runs only once committed, such as an open command buffer.
  • stop must release nothing rig made. rig frees each region and unloads each image itself, also after stop.
  • stop must 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 what signaled needs to read it, must stay until rig frees the word.
  • Once the word shows the last value given to submit and stop has returned, no write of d or 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.

#val capability : t -> Rig_amd_abi.Capability.t

capability g is the record g's facts carry as capability (facts).

This driver

Facts

The device's facts (Rig_edge.facts) are:

  • arch, the GPU's processor (Rig_amd_abi.Gpu.processor), such as "gfx1201";
  • budget, the bytes of the GPU's own memory;
  • queues, "COMPUTE:0", which runs Words and Launch, and "COPY:0", which runs Words and Copy, index 0 and 1 in C;
  • copy_engine, Some 1, "COPY:0";
  • completion, Store, since the queue that releases a value writes the word;
  • waits, stores and hosts iff the compute queue compares 64-bit words (Order);
  • may_block, false, since the C functions write memory and call no system function;
  • hang_ms, the path's hang_ms, None on a path whose kernel driver bounds how long work runs;
  • maps_host, true iff the path maps host memory (map_host of path);
  • host_addresses, false, since the host does not address Device memory;
  • alignment, 4096, since both paths allocate whole 4 KiB pages of the GPU, at addresses that start on one;
  • cache_bytes, the path's cache_bytes: the L2's and the MALL's bytes as the kernel driver states them on the amdgpu path, None where it states no L2, and None on the PCI path, which reads neither;
  • capability, capability, declared with Rig_amd_abi.declare;
  • word, the device's timeline word, as Word. below states;
  • bus, the path's bus, the GPU's PCI function.

Capability. The record holds the GPU, its clock, its compute queue's packets, its work-group processors that run work, and its trace buffers, made at the first trace. On an AQL queue its scratch grows the device's scratch memory, which the queue hands every kernel, and returns Error once stop has begun. On a PM4 queue the dispatch of each launch names the scratch.

Word. The device maps rig's whole array of words through the path (map_host of path). The path's first map of the array on a GPU makes the mapping, and every later one counts against it: a GPU maps the array once, at the host's addresses, while a device of it lives. On a path that maps no host memory, or when the word make was given lies outside the array, the word is eight bytes of the device's own Pinned memory, started at the given word's value, which rig reads through signaled. The device's values start after that value. The word holds an unsigned 64-bit integer in the host's byte order. A release writes it after its work's writes reached memory. A release on "COMPUTE:0" writes all 64 bits, and one on "COPY:0" the low 32 bits, so the driver releases on "COMPUTE:0" every value whose low 32 bits are 0, the only values whose high 32 bits change.

Sleep. sleep blocks on the path's interrupt, which every release raises, and lets other domains run while it waits. It may return on the interrupt of an earlier release or of other work of the GPU. The path reports a fault of the device's work, such as a page fault or a reset of the GPU, and sleep raises it as Fault with the path's report.

C functions

edge's room, submit and settle are rig_amd_room, rig_amd_submit and rig_amd_settle, declared in rig_amd.h. Its commit does nothing, since submit places each value's release and returns RIG_COMMITTED. A part is:

  • words, whole packets of the queue's kind: PM4, or AQL in multiples of 16 words, on "COMPUTE:0", and SDMA on "COPY:0";
  • on "COMPUTE:0", a launch. submit writes the kernel's arguments into the argument segment: the launch's parameters, each ref's address added, then the implicit arguments the kernel reads (Rig_amd_abi.Code_object.hidden) for a grid of whole groups that starts at work-item 0. A group takes the kernel's group segment plus the launch's shared memory of local data share;
  • on "COPY:0", a copy between two GPU addresses, its handles, whose ranges do not overlap. A box of two rows or more whose pitches are multiples of 4 bytes is sub-window copies (Rig_amd_abi.Sdma.window) of up to most_rows rows each, in the largest element of up to 16 bytes that divides its width, its pitches and each side's address modulo 4, if its width and pitches fit the packet in those elements and each side's address modulo 4 plus its width is at most its pitch; any other box is linear copies of each row. A fill is a CONST_FILL of its whole dwords, as many bytes a packet as a linear copy moves, and a linear copy of each unaligned end, at most three bytes, from four bytes of the argument segment that hold its byte. It reads no other memory. The compute engine runs only copies of one row.

A submission takes fewer words than the compute ring holds, at most half the copy ring and at most half the argument segment, since its SDMA packets and argument bytes never wrap. The segment also holds 8 bytes per part, the word through which a part of one queue orders a part of the other. room returns RIG_NEVER for parts that exceed the largest rings the path makes (most_ring of path: 2 GiB through Rig_amd_amdgpu; through Rig_amd_pci 16 GiB for the compute queue and 8 GiB for the copy queue, but 16 MiB on SDMA 4, whose one queue that path programs keeps the ring the device opens with), or that the device does not run, and RIG_GROW for parts that exceed its rings or its segment. On SDMA 4 over Rig_amd_pci, a submission of copies past half that ring, about 170,000 linear copies of one packet each, never fits, and Rig.Buffer.submit_copy then submits a box of that many rows that sub-windows do not copy in several submissions.

grow then makes, of Pinned memory of the first power of two from 4096 bytes that holds twice what the parts need and at most most_ring, a segment, or for each ring they exceed a new queue of the path's on a new ring, with read and write positions of its own at 0 (queue of path). The next room that needs them installs them once the device reached every value submitted, and every room answers RIG_LATER until then, so the device's work ends while other threads submit: later work goes to the new queue, whose doorbell is another, and the replaced queue, which read every packet, runs beside it until the next growth or stop destroys it (retire of path) and gives back its ring. A growth whose parts need more than a queue a growth made and no room installed replaces that queue, which ran no work. It makes the new queue first and then destroys the one it replaces. Where the path answers Full for the new one, as over Rig_amd_pci, it destroys the old one first and makes the new one in its slot, so over Rig_amd_pci a path holds at most two queues of a kind. Segments and rings never shrink. grow answers Short n if the path lacks n bytes of memory, or another resource for a queue, having made nothing that stays and kept what a growth handed, and raises Fault if the path refuses a queue for another reason. A copy in linear copies takes a packet for each 1 GiB of each row it moves (4 MiB on SDMA engines before 4.4.2 and on 5.0 and 5.1). The device does not run:

  • a copy of one byte or more on "COMPUTE:0";
  • words on an AQL queue that are not whole packets;
  • a launch whose parameters end before its kernel's explicit arguments do (explicit_size of Rig_amd_abi.Code_object.kernel);
  • a launch whose grid or group has an empty axis, whose groups have more work-items than its kernel's max_threads, or whose shared memory exceeds the GPU's local data share less the kernel's group segment;
  • on a GPU of several dies, a launch of 2{^32} work-items or more along an axis;
  • a part with an after index not below its own.

On PM4 and SDMA rings, the queues read none of a submission's packets before all of them are placed, since submit moves the write position and rings the doorbell last. An AQL queue may read a packet as soon as its header is placed, and submit places each header after the rest of its packet. rig gives submit no waits where the compute queue cannot wait, as the device's facts state (Rig_edge.facts).

Times. While a profile is taken, each queue a value's work enters writes the GPU's clock after the value's waits, and the queue that releases the value writes it before the release: on "COMPUTE:0" once the dispatches before it have completed (PM4's COPY_DATA of the clock), on "COPY:0" after a fence has waited for its copies (SDMA's TIMESTAMP). settle, or a later submit, converts the earliest start and the end to the host's clock, in value order, with each no earlier than the end of the value converted before it, since a value's work starts once every earlier value's work has completed. A stamp of a completed value that the work never wrote makes the next settle fail, which loses the device.

A device measures the clock against the host's clock as the path's clock says, and converts a stamp along the line through its first measure, at open, and its latest, once they lie a second apart, and at clock_hz from the latest before. A measure through a Source takes the time of a host clock between two readings of it. A source may count another counter of the clock's rate, a constant apart from the clock, as KFD's does on GFX 12: the device takes the constant once, at open, from stamps its compute queue writes between two readings of the host's clock, the closest pair of eight, against a read of the source, and adds it to every read. That read is the first of sixteen before the stamps within 10 µs, else the first of sixteen after them within 10 µs, else the narrowest; it is also the measure at open, which, if wider than 10 µs, the first re-measure within 10 µs replaces in the line. A re-measure whose readings lie more than 10 µs apart is dropped. A time is thus within its measure's deviation plus the line's rate error over the time since the latest measure, at most 62.5 ms. A measure's deviation is half its interval, 5 µs once a measure within the bound is taken. The rate error is at most the two measures' deviations over their distance, 10 µs over a second (10 ppm) for deviations of 5 µs, and the clock's own until they lie a second apart. Through KFD, KFD samples its host clock after it reads the GPU's counter, and the device maps that sample to the host's clock with a pair of readings of its own. A measure's deviation is then half its interval plus KFD's gap between the counter read and its host sample. That gap is one-sided, usually one register read, and unbounded under preemption. Through a Source a time also holds the constant's error: half the stamps' interval plus the deviation of the read it was taken against, for the device's life. The constant stays fixed for that life, and a counter that moved against the clock would shift every time by the move: a conversion that puts a value's start before its submit, or its end after the conversion, by more than 1 ms leaves both its times 0, as for work the device cannot time, so a profile records no Work for it (Rig.Profile.event). A late read of the source, as KFD's under preemption, does the same. With Round_trip the device measures at open only, and its times drift from the host's clock at the clock's rate error since open.

Stop

stop passes fault to the path's stop, which destroys the device's queues. Once none runs, it writes the last value given to submit into the word and gives back the device's own memory. If the path could not destroy every queue, the device keeps its memory, and the word reaches that value only if the queues still run and complete their work.

Either way the stop frees the C state the device ran work with, its rings' and timed values' records, which the queues never read. The edge that reads the word, 24 bytes, stays for the process. A word of the device's own goes back when rig frees it (Rig_edge.facts).

Once the word shows the last value and stop has returned, no work of the device writes it: the queues release values in order, and the last value's release is the last write; the stop writes the word itself only once the path destroyed every queue.

Measures

A device made with measure (make) counts measure.counters around each launch of a value a profile times. It takes the GPU's stable power state (stable_power of path) and holds it as its path states, so every process's work on the GPU runs at its stable clocks meanwhile. Of the 8 SQ counters a GFX12 SQ counts at once, it counts at most 4, "SQ_WAVES" one of them, which it counts whether listed or not.

A measured launch starts once the work before it has completed, and the work after it on the compute queue starts once its counters are stored. Its submission's Work covers both. A copy part of the same value runs on the copy queue and waits only for what its after names, so it may run during the launch, and a counter of cycles or waits may then include its contention for memory. Work submitted while no profile is taken is not measured.

Its Counters event has the launch's own start and stop and each counter's values as Rig_amd_abi.Counter.t lays them out. The values are the launch's alone and exact unless another process counts on the GPU, or waves started under another process's counting still run. A launch records no Counters, and counts in an Unmeasured event, if "SQ_WAVES" differs from its grid's waves, if a value reads 2{^32} - 1, if the host had no memory for its records, or once the device's VMID is seen to have moved, until a count with no launch reads nothing under the filter of the launch after it: the GPU may then have counted another process's waves as the device's.

The device's facts list the counters, "SQ_WAVES" first, each with one value per place Rig_amd_abi.Counter.t counts it in.

#type measure = {
  1. #counters : string list;
}

The type for the counters a device measures, by their names in Rig_amd_abi.Counter.names.

Paths

Only path libraries use these values. A path reaches a GPU in one way, reads what the device needs, and gives it all to make as a path. What the driver does when a path refuses or fails a call, such as a stop that answers `Unknown or a free that raises, is untested.

#type 'm memory = {
  1. #address : int;
    (*

    The GPU address of its first byte.

    *)
  2. #host : int option;
    (*

    The host address of its first byte, if the host addresses it.

    *)
  3. #data : 'm;
    (*

    The path's own data for it.

    *)
}

The type for memory a path gives a device.

#type clock =
  1. #| Source of int
    (*

    The address of the path's struct rig_amd_clock (rig_amd.h), which the path keeps while a device may read it. A device reads it at open and again in rig_amd_settle and in a rig_amd_submit that converts an earlier value's stamps, at most every 62.5 ms, and takes at open the constant between its counter and the clock (C functions).

    *)
  2. #| Round_trip
    (*

    The device measures at open only, with a stamp its compute queue writes between two readings of the host's clock, the closest of eight.

    *)

The type for how a device reads the GPU's clock, the clock of its stamps, against the host's clock (C functions).

#type refusal =
  1. #| Lacking of string
    (*

    The path, the kernel driver or the GPU lacked memory or another resource for it, as the reason says.

    *)
  2. #| Full of string
    (*

    The queues of the kind already hold every slot the path has for them, as the reason says; a retire frees one.

    *)
  3. #| Refused of string
    (*

    Any other refusal, as the reason says.

    *)

The type for a path's refusal of a call.

#type 'm path = {
  1. #key : 'm Stdlib.Type.Id.t;
    (*

    The path's key: devices whose paths share key are of one machine, and map each other's memory with map_peer.

    *)
  2. #index : int;
    (*

    The GPU's number among the machine's AMD GPUs in bus order (is_gpu).

    *)
  3. #gpu : Rig_amd_abi.Gpu.t;
    (*

    The GPU, as its formats depend on it.

    *)
  4. #waves : int;
    (*

    The most waves a compute unit runs concurrently.

    *)
  5. #lds : int;
    (*

    The local data share of a workgroup, in bytes.

    *)
  6. #clock_hz : int;
    (*

    The frequency of the GPU's clock, in hertz.

    *)
  7. #clock : clock;
    (*

    How a device reads the GPU's clock against the host's clock.

    *)
  8. #mec : int;
    (*

    The version of the firmware of its compute queues.

    *)
  9. #wgps : int array array;
    (*

    The work-group processors that run work: wgps.(e).(a) has bit w set iff processor w of shader array a of shader engine e does, engines numbered across dies. A die whose processors the path cannot read has every processor of its arrays set.

    *)
  10. #budget : int;
    (*

    The bytes of the GPU's own memory.

    *)
  11. #cache_bytes : int option;
    (*

    Some n with n the bytes of the GPU's L2 and its MALL (Infinity Cache) together, or None if the path does not know the L2's.

    *)
  12. #alloc : Rig_edge.memory -> int -> 'm memory option;
    (*

    alloc k n is n new bytes of memory k:

    • Device, GPU memory the host does not address;
    • Mapped, GPU memory the host addresses through the GPU's memory BAR;
    • Pinned, host memory the GPU addresses, cached for the host and snooped by the GPU. The path owns it, so that no unmap of the process's memory takes it from the GPU.

    It is None if the memory of k is exhausted, or if Mapped memory does not exist.

    *)
  13. #map_host : (int -> int -> 'm memory option Rig_edge.mapping) option;
    (*

    Some map if the GPU maps host memory. map a n maps for the GPU the pages that hold the n bytes of host memory at a and must answer Maps (Some m). m's address is where the GPU reaches the byte at a, and its host is Some a. Where the path refuses the pages, it is No_room if the path lacked the room it maps them in, and Never otherwise, such as for read-only pages. map_host is None if the GPU maps no host memory.

    *)
  14. #reaches : int -> bool;
    (*

    reaches j is true iff map_peer maps the GPU memory the path gives a device of GPU j, numbered as index. It is true for this GPU.

    *)
  15. #map_peer : 'm memory -> 'm memory option Rig_edge.mapping;
    (*

    map_peer m maps for the GPU the memory m that the path gave another of its devices, at the same address, and must answer Maps (Some _). It is Never if the GPU does not reach that device's GPU's memory, and No_room if the path lacked the room to map it. Memory of a device of the same GPU is in its address space already, so the view maps nothing, and free of it gives nothing back.

    *)
  16. #free : 'm memory -> unit;
    (*

    free m gives back what alloc, map_host or map_peer gave.

    *)
  17. #bus : string option;
    (*

    The bus address of the GPU's PCI function on its machine.

    *)
  18. #dma : 'm memory -> fn:string -> (Rig_edge.dma, string) Stdlib.result;
    (*

    dma m ~fn is how the function at fn reaches the memory m that alloc gave, as Rig_edge.Driver.dma states, or Error with the path's reason. The path keeps the memory where it lies: it refuses where fn would reach GPU memory only once the memory moved.

    *)
  19. #queue : [ `Pm4 | `Aql | `Sdma ] -> ring:int -> bytes:int -> read:int -> write:int -> (int, refusal) Stdlib.result;
    (*

    queue k ~ring ~bytes ~read ~write makes a hardware queue that reads packets of k from the bytes bytes of Pinned memory at ring, a power of two of at most most_ring k, writes its read position to the 64-bit word at read and reads its write position from the word at write, all GPU addresses. Its read and write positions start at 0, and write holds 0. It is the host address of the queue's 64-bit doorbell, or Error r. The device makes one queue of each kind at open, and one more of a kind for each growth of that queue's ring (C functions), which runs beside the queue it replaces until that one goes (retire). A path whose slots for the kind all hold queues answers Error (Full _), and needs no memory for a queue in a slot a retire freed; a path that never answers Full holds three queues of a kind while a growth replaces a queue no work ran on. A queue that raises may have made the queue, which stop must then destroy.

    *)
  20. #retire : int -> unit;
    (*

    retire bell destroys the queue queue made whose doorbell is at bell, which has read every packet up to its write position. The device then gives back its ring and positions. It raises Fault if the path could not destroy it.

    *)
  21. #most_ring : [ `Pm4 | `Aql | `Sdma ] -> int;
    (*

    most_ring k is the most bytes of a ring of a queue of kind k, a power of two: the bound of the path's interface or of the queue's registers.

    *)
  22. #hdp : int option;
    (*

    The host address of the 32-bit register whose store flushes the GPU's host data path, after which host writes through the memory BAR are in the GPU's memory; None if the host does not reach it. The register must stay mapped until stop is called. Devices that view the GPU's Mapped memory (map_peer) store to it too, until stop, which waits for their stores before it calls stop.

    *)
  23. #interrupt : int;
    (*

    The nonzero context that the interrupt of a value's release carries, which wakes sleep.

    *)
  24. #hang_ms : int option;
    (*

    Some n if work whose word has not moved for n milliseconds hung, where nothing else bounds the GPU's work; None where the path's kernel driver does. It is the hang_ms of the device's facts (Rig_edge.facts).

    *)
  25. #interrupts : unit -> int;
    (*

    interrupts () is the path's count of the interrupts its sleeps took and the faults they recorded, as sleep compares it.

    *)
  26. #sleep : ms:int -> after:int -> unit;
    (*

    sleep ~ms ~after returns once the GPU interrupts, at once if a sleep of the path, in any domain, took an interrupt or recorded a fault since interrupts () was after, or after ms milliseconds. sleep reads after before it reads the word it sleeps on, so a release after that read wakes the sleep even when another domain's sleep took its interrupt. A path that cannot keep the count must say which sleeps may wait out ms instead. It raises Fault with the path's report of a fault of the GPU's work.

    *)
  27. #stable_power : unit -> (unit, string) Stdlib.result;
    (*

    stable_power () holds the GPU's clocks and shader engines steady, as tracing and measures need, for as long as the path states.

    *)
  28. #stop : fault:string option -> [ `Stopped | `Unknown ];
    (*

    stop ~fault destroys the queues queue made, one whose queue call raised included. It returns `Stopped once none of them runs or none can write memory outside the GPU, and `Unknown if one may. fault is the reason rig lost the device for, if it lost it for a fault or a hang (Rig_edge.Driver.stop). A path that keeps the GPU's state for later opens records it; a path that keeps none ignores it.

    *)
}

The type for what a path gives a device. A path fills it with functions over its own state, which this library never names.

  • Each function returns the GPU's refusal as None or Error, and raises Fault for any other failure of the GPU or of the path.
  • Any domain may call them at the same time, except stop, which the device calls once. After stop the device calls only free and reaches. It keeps free until it has given back all the memory the path gave, and reaches for the life of the process.
  • A sleep that another domain began before stop may still be waiting when stop runs. It returns without reading the GPU once stop has begun.
  • A function that holds a lock across OCaml code must take it with Lock.protect (A path's locks).
#val make : ?measure:measure -> word:nativeint -> 'm path -> (t, string) Stdlib.result

make ~word p is a device of the GPU p reaches, whose timeline word is the 8 bytes at the host address word (Rig_edge.opener). It makes, through p, the device's queues and argument segment, maps its word, and loads the kernel the device copies with; with measure, it measures those counters (Measures), and loads the kernel that reads its VMID. A failed make gives back what it took once p's stop returns `Stopped, and keeps it otherwise. It calls stop only if it made a queue, or if p's queue raised, which may have made one.

The result is Error msg:

  • if this library does not drive the GPU's processor, or knows no registers for its GC, naming the processor;
  • if p refuses a queue, with p's message;
  • if p lacks the memory the device needs, naming what it was for;
  • if p's Pinned memory has no host address;
  • if this library's copy kernel for the GPU's processor does not load, naming the kernel and the processor and saying why;
  • if p.clock cannot read the GPU's clock, or, with Round_trip, the GPU writes no stamp of its clock within a second;
  • with measure, if the GPU is not a GFX12 GPU of one die, if a counter is unknown, listed twice or not an SQ counter, or if they are more than 4 with "SQ_WAVES", naming why, or with stable_power's message.

It takes its first measure of the GPU's clock as p.clock says.

Raises Invalid_argument if p.interrupt is 0, if p.clock_hz is below 1, or if a count of p.gpu (Rig_amd_abi.Gpu.t) is below 1.

#val is_gpu : vendor:int -> class_:int -> bool

is_gpu ~vendor ~class_ is true iff a PCI function of vendor vendor and 24-bit class code class_ is an AMD GPU: AMD's vendor 0x1002 and a display controller or a processing accelerator, base class 0x03 or 0x12. Every path numbers a machine's GPUs 0, 1, … in bus order among such functions.

A path's locks

For a path's implementation. A path that holds a lock across OCaml code must take it with Lock.protect, which counts it among its thread's locks (rig_hold in rig_edge.h), so that a call of rig's or of the driver's from a callback inside it is refused as the call starts and does not wait for good.

Warning. The path must take its locks in the order Lock.create states. Nothing checks it, and two threads that take two locks in opposite orders wait for each other for good. Only a take by the thread that holds the lock raises. A path's entries must run their whole body inside Lock.guarded; one that does not may raise Invalid_argument from a lock it takes later, with what it changed before left changed, and a signal's handler may run anywhere in it.

#module Lock : sig ... end

References