github ↗
docs menu

Module Nx_array

Dtypes, layouts and arrays over device buffers.

An array is three facts over a Rig.Buffer.t, whose bytes it reads:

  • a dtype (Dtype), what one element is: its storage format and the OCaml type its values read as;
  • a layout (Layout), where elements lie: a map from an index to an element position in the buffer;
  • the buffer, which places the array on a device (device).

A movement (Move) changes a layout without moving an element, and move applies one to an array. v makes an array from its parts and checks that its layout reaches only bits of its buffer, which every movement keeps; create and of_array make fresh ones.

Host kernels are C and read arrays only through the door of nx_array.h, which claims every operand of a call or none, waits under the claims for unfinished device work on them, and answers an answer. A kernel's caller hands a refusal to refused, which raises. The door may run OCaml code while it waits, so a kernel's external is never [@@noalloc]:

ocaml
external add_kernel :
  ('v, 's) Nx_array.t -> ('v, 's) Nx_array.t -> ('v, 's) Nx_array.t ->
  Nx_array.answer = "nx_cpu_add"

let add z x y =
  match add_kernel z x y with
  | Done -> z
  | Declined -> invalid_arg "Nx.add: nx_cpu_add does not compute this case"
  | refusal -> Nx_array.refused "Nx.add" refusal [ Any z; Any x; Any y ]

Kernels that submit device work from OCaml claim their arrays through door, which answers the same refusals. nx_dtype.h holds the dtypes' codes, their facts and every conversion into them for C, CUDA, HIP and Metal sources.

Dtypes

#module Dtype : sig ... end

Element formats.

Layouts

#module Move : sig ... end

Movements.

#module Layout : sig ... end

Layouts.

Arrays

#type ('v, 's) t = private {
  1. #dtype : ('v, 's) Dtype.t;
  2. #layout : Layout.t;
  3. #buffer : Rig.Buffer.t;
}

The type for arrays of elements of storage format 's read as 'v. An array's layout reaches only bits of its buffer, at non-negative positions, and its first element lies on a multiple of its storage's alignment: its width for dtypes of a byte or more, one byte for narrower ones, one component's for complex ones. The record is private. Only this module's functions make an array, so every array keeps these facts.

#type any =
  1. #| Any : ('v, 's) t -> any

The type for arrays whose dtype is chosen at run time. expect recovers the static type. Any a is a at run time. Packing allocates nothing.

#val v : ('v, 's) Dtype.t -> Layout.t -> Rig.Buffer.t -> ('v, 's) t

v dt l b is the array of dt elements laid out by l over b's bytes.

Raises Invalid_argument if b is dead, l reaches a bit past b's bytes, or l has an element and its first element's byte offset into b's memory, or for host memory its address, is not a multiple of dt's alignment.

#val create : ?memory:Rig.Buffer.kind -> Rig.t -> ('v, 's) Dtype.t -> int iarray -> ('v, 's) t

create d dt s is a fresh C-contiguous array of shape s in d's memory of kind memory (defaults to Device), at offset 0, with unspecified elements. Its buffer holds Dtype.bytes dt n bytes for n elements; the bits of a sub-byte array's last byte past its last element are zero.

Raises Invalid_argument as Layout.contiguous does, and what Rig.Buffer.create raises and, for a sub-byte array, what Rig.Buffer.submit_fill raises.

#val host : ('v, 's) Dtype.t -> int iarray -> ('v, 's) t

host dt s is create Rig.host dt s: a fresh C-contiguous array of shape s in the host's memory, which write fills.

Raises Invalid_argument as create does.

#val dtype : ('v, 's) t -> ('v, 's) Dtype.t

dtype a is a's dtype.

#val layout : ('v, 's) t -> Layout.t

layout a is a's layout.

#val buffer : ('v, 's) t -> Rig.Buffer.t

buffer a is a's buffer.

#val device : ('v, 's) t -> Rig.t

device a is the device of a's buffer, the device a lives on.

#val expect : ('w, 'r) Dtype.t -> any -> ('w, 'r) t

expect dt (Any a) is a at type dt if its dtype is dt.

Raises Invalid_argument naming both dtypes otherwise.

Views

move and bitcast make arrays over their argument's buffer.

#val move : Move.t -> ('v, 's) t -> ('v, 's) t option

move m a is the array over a's buffer laid out by Layout.move m (layout a), or None where that is None.

Raises Invalid_argument as Move.shape does.

#val bitcast : ('w, 'r) Dtype.t -> ('v, 's) t -> ('w, 'r) t option

bitcast dt a is a's bits read as elements of dt, over a's buffer. With r the ratio of the two widths:

  • equal widths keep the layout;
  • to a narrower dtype, a trailing axis of extent r and stride 1 is appended, and the other strides and the offset are multiplied by r;
  • to a wider dtype, a needs a trailing axis of extent r and stride 1, and an offset and other strides that are multiples of r; the axis is removed and they are divided by r. An array with no element needs only the trailing axis of extent r.

It is None where widening's conditions fail or the result's first element is not on a multiple of dt's alignment.

Raises Invalid_argument if a's buffer is dead, and on a narrowing of an array of rank Layout.max_rank.

Elements

A function that reads or writes bytes on the host claims the buffer's memory, waits for the device work the access must follow (Rig.Buffer.wait), and holds the claim while it runs. It raises Invalid_argument if the buffer is dead, its memory is held exclusive, or the host does not address its memory, and Rig.Lost as Rig.Buffer.wait does.

#val get : ('v, 's) t -> int iarray -> 'v

get a i is a's element at index i.

Raises Invalid_argument unless i has Layout.rank (layout a) entries, each in [0, Layout.dim (layout a) j) for entry j.

#val set : ('v, 's) t -> int iarray -> 'v -> unit

set a i x stores x at a's index i. Stores to the other elements of a sub-byte element's byte, from any domain, are kept.

Raises Invalid_argument as get does, if a reaches an element twice (Layout.is_distinct), if a's memory is Read, and if x is an int below Dtype.min_value or above Dtype.max_value of a's dtype.

#val to_array : ('v, 's) t -> 'v array

to_array a is a's elements in C order of indices. float elements fill a flat float array; int32, int64 and Complex.t elements are boxed.

#val of_array : ('v, 's) Dtype.t -> int iarray -> 'v array -> ('v, 's) t

of_array dt s xs is a fresh C-contiguous array of shape s on Rig.host holding xs in C order of indices, each stored as Dtype.of_float says.

Raises Invalid_argument as Layout.contiguous does, if xs does not have one value per index of s, or if a value is an int outside its dtype's range.

Placement

to_device, transfer and copy store an array's elements bit for bit, NaN payloads included, on any devices, and run no kernel. Where the host addresses the memory of both arrays it moves the bytes, and they return once the bytes are there, as they do when the host gathers an array narrower than a byte from memory it does not address. Otherwise a device copies, and they return as Rig.Buffer.submit_copy returns: every later access of either array's memory, by the host or a device, follows the copy (Order). Memory outside the claims, such as a bigarray's (of_bigarray), is read or written until the copy is done, so a program orders its own access through such a bigarray after a Rig.Buffer.wait. borrow shares an array's memory with another device instead, where that device maps it. to_device and transfer also raise what Rig.Buffer.create, Rig.Buffer.submit_copy and Rig.Buffer.submit_fill raise.

#val to_device : Rig.t -> ('v, 's) t -> ('v, 's) t

to_device d a is a's elements on d, also when a is on d, in a fresh buffer of at most Dtype.bytes of one element per index of a, an axis of stride 0 counted once. Where the bytes from a's first to its last position are no more than those, the copy keeps a's layout shifted to it, and a sub-byte copy's first and last bytes keep the source's bits beside the elements, each as found before or after a store another domain makes to it meanwhile. Otherwise the copy's axes go by decreasing magnitude of a's stride, ties in axis order, the last at stride 1 and each other at the product of the extents after it. Each takes the sign of a's stride, an axis of stride 0 keeps 0, and the least position is at 0. Shape [3; 2] at strides [2; 2] gets strides [2; 1].

Raises Invalid_argument if a's buffer is dead or its memory is held exclusive.

#val transfer : src:('v, 's) t -> dst:('v, 's) t -> unit

transfer ~src ~dst stores src's elements into dst, of the same shape. It writes dst's elements and no other bit of its buffer, a sub-byte neighbour included.

Raises Invalid_argument if the shapes differ, a buffer is dead or its memory is held exclusive, dst reaches an element twice (Layout.is_distinct), dst's elements are narrower than a byte and the host does not address its memory, dst's memory is Read, or dst shares a byte with src without being identical to it (Kernels).

#val copy : ('v, 's) t -> ('v, 's) t

copy a is a fresh C-contiguous array on a's device holding a's elements, the array create makes there filled by transfer.

Raises what create and transfer raise.

#val borrow : Rig.t -> ('v, 's) t -> ('v, 's) t option

borrow d a is Some a', a's elements on d without a copy: a's dtype and layout over a borrow of its buffer (Rig.Buffer.borrow), which shares a's memory, so writes through either show in the other. It is Some a for a on d, and None where d cannot map a's memory. On another device, a' holds d's mapping of the memory while it lives, and a copy between a' and d's memory runs on d directly (Copying).

Raises Invalid_argument if a's buffer is dead, and what Rig.Buffer.borrow raises: Rig.Lost for a lost device.

Bigarrays

bigarray, read, write and of_bigarray share bytes with the value they take, without a copy. The bigarray that bigarray, read and write give is of the kind a's dtype stores (Dtype.to_bigarray_kind), which its type fixes. They raise Invalid_argument unless the array a they take is C-contiguous (Layout.is_contiguous) and of rank at most 16, the most axes a Bigarray has, if a's dtype has no Bigarray kind, and if a's buffer is dead or its memory is held exclusive. They raise Rig.Lost as Rig.Buffer.wait does.

#val bigarray : ('v, 's) t -> ('v, 's, Stdlib.Bigarray.c_layout) Stdlib.Bigarray.Genarray.t

bigarray a is a bigarray of a's shape over a's own bytes. Writes through it write a, and are allowed only if a's memory admits them (Rig.Buffer.access). It waits once for the work on a's memory, and from then on that memory is never held exclusive again (Rig.Buffer.export). Access through the bigarray follows later device work only after Rig.Buffer.wait. A format Bigarray lacks is bitcast first to one of its width (bitcast).

Reads and writes through the bigarray are Bigarray's: an int out of range keeps its low bits, and a float stored into a Float16 bigarray is rounded through binary32 first, so it can differ from Dtype.of_float by one unit in the last place.

Raises Invalid_argument naming Nx_array.bigarray, and unless a is on Rig.host.

#val read : ('v, 's) t -> (('v, 's, Stdlib.Bigarray.c_layout) Stdlib.Bigarray.Genarray.t -> 'a) -> 'a

read a f is f b, where b is a bigarray of a's shape over a's own bytes; an a with no element gives an empty b, whatever its memory. It claims a's memory for reading, waits for the device work a read must follow (Rig.Buffer.wait), and ends the claim when f returns or raises. The memory stays inside the claims, so a donation can hold it exclusive once f returns, which bigarray prevents for good.

f must not write through b, since work that reads the memory may still run, and must not keep b after it returns, since a donation or Rig.Buffer.free can then give the memory to another array. The claim orders b only against donations. The program must order b against work another domain submits while f runs.

Raises Invalid_argument naming Nx_array.read, and if a has an element and the host does not address its memory.

#val write : ('v, 's) t -> (('v, 's, Stdlib.Bigarray.c_layout) Stdlib.Bigarray.Genarray.t -> 'a) -> 'a

write a f is f b, where b is a bigarray of a's shape over a's own bytes: what f stores through b is a's elements. An a with no element gives an empty b, whatever its memory. It claims a's memory for writing, waits for the device work a write must follow (Rig.Buffer.wait), and ends the claim when f returns or raises, as read does.

f must not keep b after it returns, since a donation or Rig.Buffer.free can then give the memory to another array. The program must order b against work another domain submits while f runs.

Stores through b are Bigarray's, as bigarray says.

Raises Invalid_argument naming Nx_array.write, and if a's memory admits no write (Rig.Buffer.access), and if a has an element and the host does not address its memory.

#val of_bigarray : ('v, 's) Dtype.t -> ('v, 's, Stdlib.Bigarray.c_layout) Stdlib.Bigarray.Genarray.t -> ('v, 's) t

of_bigarray dt b is the C-contiguous array of dt elements over b's bytes, of b's shape. The types tie dt to b's kind: a kind no dtype stores, as Char, Int or Nativeint, is a type error.

Raises Invalid_argument as v does if b's data is not aligned for its elements.

Kernels

Two arrays are identical when they have one element width and place every index at the same bits, which two memories may hold, as an io memory and a bigarray over its pages do (Rig.Buffer.overlaps). A written array may share bytes with read arrays identical to it, and with no other.

The door and the kernels that read through it keep other domains waiting to collect for a bounded time, which does not grow with the number of arrays they claim or release. Past max_operands arrays, other threads of the calling domain may run OCaml code while the door claims or releases them.

#val max_operands : int

max_operands is 18, nx_array.h's NX_MAX_OPERANDS: the length of the runs in which the door claims and releases arrays.

#type answer =
  1. #| Done
  2. #| Declined
  3. #| Wrong_dtype
    (*

    An operand's dtype is not the one the kernel loads.

    *)
  4. #| Dead_buffer
    (*

    An operand's buffer is dead.

    *)
  5. #| Off_host
    (*

    The host does not address an operand's memory.

    *)
  6. #| Held_exclusive
    (*

    An operand's memory is held exclusive.

    *)
  7. #| Read_only
    (*

    A written operand's memory is Read.

    *)
  8. #| Repeated_elements
    (*

    A written operand reaches an element twice.

    *)
  9. #| Overlapping
    (*

    A written operand shares a byte with another written operand, or with a read operand not identical to it (Kernels).

    *)
  10. #| Shape_mismatch
    (*

    The operands do not fit the operation: their number, or their shapes. Operands of one loop of different shapes, shapes its descriptor refuses, or other destinations than an operation names, as a reduction given other than its one or two.

    *)

The type for what a kernel answers, and the door. A kernel answers Done once its work is done on the host or queued on the device's timeline, Declined for a case it does not compute, and otherwise, before any write, a refusal: what is wrong with an operand. Dead_buffer, Held_exclusive and Read_only, which claiming an operand answers, come before Off_host, Repeated_elements and Overlapping, so a dead operand is Dead_buffer whatever else is wrong with it. C answers Val_int of nx_array.h's code, whose enum lists the constructors in this order.

#val door : written:any iarray -> read:any iarray -> ('a -> unit) -> 'a -> answer

door ~written ~read f x claims the memory of written for writing and of read for reading, every claim or none, runs f x, and releases the claims when f returns or raises, before an exception of f propagates. It waits for no device work. It answers Done once f returns, and otherwise, without running f or holding a claim, Dead_buffer or Held_exclusive for an array, or Read_only, Repeated_elements or Overlapping for a written one.

f must touch the arrays' elements only through work it submits with Rig.submit, naming each array of written in its writes and each of read in its reads or writes. The door allocates nothing.

Raises Rig.Lost, without running f or holding a claim, if an array's memory is a lost device's or must follow work a lost device did not finish, and as f's exception for a device lost while f runs.

#val refused : string -> answer -> any list -> 'a

refused name r operands raises Invalid_argument for the refusal r that a kernel answered for operands, naming name, the reason r gives, and each operand's dtype and shape. name is the function the user called, as "Nx.add". For Dead_buffer, and for Held_exclusive once the holder consumed the memory, the reason names each operand whose buffer is dead, in operands' order, as <dtype> <shape> was <why>, why the reason its consumer gave (Rig.Buffer.dead): Nx.add: float32 [3] was donated to Nx.mul (float32 [3], …). For Done and Declined it raises Invalid_argument saying they refuse nothing.