A native fast path for repeatedly executing a compiled program on inputs of the same signature, intended to be used in anvl. It owns an executable cache keyed on the inputs' structure and abstract values, calling back into R to compile only on a cache miss.
dispatcher() creates a dispatcher for one program family;
dispatch() runs a call through it and returns the call's result.
Usage
dispatcher(
capacity,
compile,
static = character(),
backend = "pjrt",
move_inputs = FALSE,
default_device = NULL,
extractor = NULL,
context = NULL
)Arguments
- capacity
(
integer(1))
Maximum number of compiled executables to cache.- compile
(
function)
Cache-miss callback, called ascompile(info).infocarries what the dispatch already derived from the call, so that the callback need not classify the inputs a second time:args: the dispatched argument list,in_tree: itsRTree(seebuild_tree),leaves: the flat leaf list (seeflatten),is_static: alogical()mask overleaves,avals: per leaf,NULLif static, else thelist(kind, dtype, shape)the cache key was built from.kindis"array"for one of the backend's arrays and"rdata"for a bare R literal or array – the two are different cache keys, because bare R data has no dtype of its own and the caller may compile it into a different program.dtypeis a canonical dtype string ("f32","i64", ...) for an"array"leaf, and for an"rdata"leaf its R storage type instead – itstypeof(), so"double","integer"or"logical". That is what the value is, not a dtype it is not yet:"double"is not"f64", and what the leaf is uploaded at isinput_dtypes.shapeis aninteger(), empty for a scalar,default_device: the device this call resolved because no array input named one – the device the cache key was built on, socompilemust compile for it rather than resolve a default of its own.NULLwhen an array named the device, or undermove_inputs.context: what thecontextresolver returned for this call – the vector the cache key was built on, socompilemust compile under it rather than resolve its own.NULLwhen the dispatcher has nocontext.
For
backend = "pjrt"it must return a named list with:exec: apjrt_compiled executable,client,device: thepjrt_clientand the device the entry is compiled for,out_tree: theRTreeof the outputs (seebuild_tree),out_avals: one aval per output leaf ofout_tree, each alist(dtype = <string>, shape = <integer>). The outputs are wrapped from these.const_arrays(optional): buffers prepended to the inputs,phantom_specs(optional): a list oflist(dtype = <string>, shape = <integer>)donation-output buffers to allocate fresh per call.
Either kind of result may additionally carry:
input_dtypes: acharacter()with one entry per dynamic leaf, in order, naming the dtype that input is supplied at.NAleaves an input alone, and is the only valid entry for an array input: an array is supplied as it is, so declaring a dtype for one is an error rather than a no-op.With
backend = "pjrt"every bare R leaf must name a dtype: bare R data has no dtype of its own, and only the compiled program knows what it is used as, so the engine uploads it at the dtype declared here and never guesses one. It is what lets a program that consumes an R double asf64get the exact value rather than one rounded throughf32first. The"rdata"aval'sdtypeis the leaf's R storage type, so it is never an answer to this: the callback names a real dtype. The field may be omitted only for a call whose inputs are all arrays.Not every R storage type uploads at every dtype, and a pair that cannot be uploaded is rejected here rather than at execute time: a
"double"input takes any dtype, an"integer"input any but"bool", and a"logical"input only"bool".Any other
backenduploads nothing:r_fungets each R value itself, so no entry could take effect and every one of them must beNA. A declared dtype is rejected there rather than accepted and ignored, for the same reason it is at an array input. The field is still length-checked, and may still be omitted entirely.
For any other
backendit must return a named list with:r_fun: a function called with the list of the call's dynamic leaves, in order and with an array leaf contributing its$data, returning the call's finished value. Static leaves are not passed: they are constants of the closurecompilejust built, and a cache hit already proves the call's statics areidentical()to the ones it was built from.
- static
(
character())
Names of top-level arguments that are static (not arrays). Static values are part of the cache key and are excluded from execution. Defaults to none. They are compared withidentical(num.eq = FALSE), i.e. numbers compare bitwise, which is what keeps abit64::integer64NA– stored as the bit pattern of-0– from sharing a cache entry with0.- backend
(
character(1))
The$backendtag every"AnvlArray"input must carry, and the tag stamped on wrapped outputs. It also selects the execution engine (see Backends); anvl's quickr backend passes"quickr".- move_inputs
(
logical(1))
IfTRUE, each cache entry has a target device – thedeviceitscompilecall returned – and the engine places every input on it at execute time; the cache key then carries no device, so inputs may arrive from any device. DefaultFALSE: the first array's device is the call's device, and a conflicting input is an error.It is a policy flag, not a device: which device an entry targets is the
compilecallback's business, and need not be the same for every entry (anvl'sjit(device = )fixes one up front, itsdevice_arg()derives one per static argument value).Placing an input is the engine's business, since only it knows what
$dataholds. Withbackend = "pjrt"an input living elsewhere is copied to the entry's device. With any other backend pjrt does nothing, sor_funmust place its own inputs – it receives only their$data, not their$device, so the placing has to be idempotent.- default_device
(
function|NULL)
Called with no arguments to get the backend's current default device, whenever a call has no array input to read a device from. Its result is part of the cache key, so an entry compiled under one default device is never served after the default changes. Required unlessmove_inputs = TRUE, which fixes the device per entry.Devices are compared by object identity first and by
identical()as a fallback, so equal-but-distinct device objects count as one device. Interning them (one object per device, alive for the session, asas_pjrt_device()does) is therefore not required, but is recommended: an interned device resolves in one pointer comparison, and every distinct object a backend hands out stays alive for the dispatcher's lifetime.- extractor
(
function|NULL)
Reads a non-"pjrt"array's metadata via the backend's accessors, called asextractor(leaf)and returninglist(aval = list(dtype, shape), device, backend)–dtypea tengenDataType,shapeaninteger(). The aval's kind is not the extractor's to say: whatever it returns is an array leaf. Required for any backend other than"pjrt"; ignored for"pjrt"(see Backends).- context
(
function|NULL)
Called with no arguments on every dispatch to get whatever the compiled program depends on beyond its inputs – anvl passes the backend's current default dtypes. Must return acharacter()withoutNAs; its value is part of the cache key, so an entry compiled under one context is never served under another, and it reachescompileasinfo$context. Unlikedefault_device, which is consulted only when no array names a device, this is resolved for every call: any entry may depend on it.NULL(default) keys on the inputs alone.
Details
Each dispatch() call flattens the inputs and builds a cache key: a
dynamic leaf contributes its abstract value – its kind (one of the backend's
arrays, or bare R data), its dtype and its shape – a static leaf
its value (compared with identical()). On a hit the cached executable runs
immediately; on a miss compile is called to produce a new cache entry.
Inputs are validated before the cache is probed, and a rejection names the
offending argument by its path in the argument tree. An input must be an
"AnvlArray" of the dispatcher's backend, a length-1 atomic scalar, or an
is.array() value. compile is therefore never asked to validate: it is
called only on a cache miss, and only for inputs already known to be
executable.
Backends
The backend selects the execution engine, and everything backend-specific
sits behind it:
backend = "pjrt"executes a compiled PJRT executable natively: array inputs contribute their$databuffer, bare R literals and arrays are uploaded at the dtype the entry'sinput_dtypesdeclared for them, and the outputs are wrapped back into"AnvlArray"s – lists of$data,$dtype,$shape,$deviceand$backend– and re-nested viaout_tree, all without leaving C++.any other
backendcalls the compiled R closurecompilereturned, which returns the call's finished value. Execution, output wrapping and input placement therefore stay under the backend's control. This is the path for any non-PJRT backend (e.g. anvl's"quickr").
Of an array input, only $data is ever assumed: for "pjrt" its dtype, shape
and device are read off the PJRTBuffer directly, and for any other backend
they come from extractor. A backend is free to store them as fields or to
compute them on demand.