prim_bitcast_convert() puts the axis holding an element’s pieces first rather than last when the two data types differ in width, so the pieces of one element are adjacent in the order nv_flatten() reads and a narrowing conversion lays the bytes out the way as_raw() writes them.
nv_broadcast_to() and nv_broadcast_arrays() align axes from the first instead of the last: a shorter shape gets size-1 axes appended, so a length-nrow vector broadcasts against a matrix where a length-ncol one no longer does. Write prim_broadcast_in_axes() with an explicit axis mapping for the previous right-aligned behavior.
The @jit roxygen tag was removed; wrap functions in jit() at the definition instead.
nv_top_k() takes axes instead of axis, ranking the elements of several axes together, and axes = NULL (the default) now ranks over every axis where it used to take the last one. Write axes = -1 for the old default.
The type system of {anvl} was changed to avoid the problems reported in issue #373. Specifically, the ambiguity system was replaced with the RData system and a new system of rules for type promotions. With it, also the promotion behavior of various primitives and API functions was improved.
An R value is now built directly at every data type of its own category, narrow and unsigned integers included, so one the data type cannot hold is an error instead of wrapping around: x_ui8 + (-2L) and x_i8 + 300L are refused. Write nv_convert() on an array where the wraparound is what you want.
as_array() and the as.double() / as.integer() / bit64::as.integer64() / as.logical() methods take check = "warn", "err" or FALSE instead of a flag, following {pjrt}, and warn by default about a value R’s type cannot hold. Write check = "err" where you wrote check = TRUE, and check = FALSE to materialize silently.
nv_array() and nv_scalar() no longer take a check argument, following {pjrt}: what happens to an NA is fixed by the dtype it is built at, and the input is always scanned for values the requested dtype cannot hold. Call anyNA() on the data yourself if you want to hear about a missing value the dtype accepts.
common_dtype() now errors for ui64 and a signed integer instead of returning i64, which could not hold every ui64 value. Convert one of them with nv_convert().
There is now exactly one backend used at a time and it is configured via the anvl.backend option. With this change the device_arg parameter was removed from jit() as it is no longer needed.
A Shape is now represented as an integer vector.
The operators &, |, !, as well as the generics sum() and all() now require a boolean input array, improving consistency with base R.
The method for round was removed, as digits is currently not supported.
nv_quantile() and nv_median() now reduce over axes (plural) instead of a single axis, defaulting to every axis like nv_mean() and base R’s quantile() / median(), and gained a drop argument. Write nv_median(x, axes = -1L) for the previous default.
nv_sort() and nv_argsort() now flatten a multi-axis array when axis = NULL, instead of working along the last axis, so sort() on an anvl array agrees with base R. Write axis = -1L for the previous default.
prim_sort() no longer defaults axis to 1L; pass it explicitly, as with every other primitive.
nv_argmax() and nv_argmin() now reduce over axes (plural) instead of a single axis, defaulting to every axis so that they pair with nv_reduce_max() / nv_reduce_min(). Reducing several axes indexes their column-major flattening. Write nv_argmax(x, axes = -1L) for the previous default.
The tensor_to_gval argument of GraphDescriptor() is now called array_to_gval.
Features
New local_default_device() and with_default_device() set the anvl.default_device option, which names the device a call that names none allocates on in place of the first CPU device.
The environment variables ANVL_DEFAULT_DEVICE and ANVL_DEFAULT_DTYPES, read when anvl is loaded, are used when the anvl.default_device and anvl.default_dtypes options are not set.
nv_rng_state() accepts a seed of any signed or unsigned integer data type, bringing it to i32, where it took an i32 only. The state stays ui64[2] whatever the seed and the default integer data type are.
nv_flatten() accepts a scalar, returning a length-1 array, instead of erroring.
nv_sign() accepts an unsigned integer array, returning 0 or 1 like base R’s sign() on a non-negative number; prim_sign() still takes a signed input only.
nv_reverse() gained an axes = NULL default that reverses every axis, matching rev() and numpy.flip(), and returns x unchanged for an empty axes instead of erroring.
nv_array() accepts a raw() vector holding the native byte payload of prod(shape) elements of dtype (both then required); byrow selects row-major element order for the payload. Only supported on the "pjrt" backend; the inverse direction is the existing as_raw().
New nv_scan(): a fixed-length loop in the style of JAX’s lax.scan that threads a carry through a body function and stacks each step’s outputs along a new leading axis. Supports nested carries, multiple xs and out leaves, reverse scans, xs = NULL counted loops, carry-only loops and zero-length scans. Backed by the new prim_scan() primitive, which lowers to a while loop on the pjrt backend.
The default data types for floating point numbers and integers can now be configured via the anvl.default_dtypes field. You can configure this for a specific scope via local_default_dtypes() and with_default_dtypes(). To convert a function to one running at a specified precision, use with_dtypes().
Improved documentation of API functions and primitives.
Printed graphs read as [captures] (inputs) { ... return ... }, show sub-graphs in full, and wrap long lines to the console width; format() takes width and digits arguments.
nv_quantile() and nv_median() select the needed order statistics with top_k instead of a full sort when every requested quantile lies in the same half of the axis. Results are unchanged: the interpolation index is computed at f64, so it agrees with the window the host sizes.
prim_top_k() gained indices; without them the CUDA lowering uses an unstable sort of the values and a slice instead of the CHLO op, which costs no more than a full sort there. nv_top_k(indices = FALSE) and the quantile fast path use it.
A function returned by jit() no longer evaluates its arguments a second time. It used to rebuild the call with match.call() and evaluate the argument expressions again in the caller’s frame, which computed them twice whenever something had evaluated them already – most visibly under S3 dispatch, which evaluates the first argument to choose a method.
The floating-point nv_* functions refuse a boolean, nv_matmul(), nv_solve() and nv_triangular_solve() included – a boolean used to meet a numeric operand at that operand’s data type and pass their data type check.
nv_top_k() checks k before coercing it, so a fractional or logical k is refused rather than silently truncated.
A range that counts down (x[3:1]) now selects in reverse instead of failing.
Coercing a traced array to R inside jit() – as_array(), as.vector(), as.numeric(), as.character() and friends – now aborts with an explanation instead of falling through to the base R generic. Some of those used to fail with a message about lists or dimensions, and as.vector(), as.list() and as.character() silently returned the traced box itself.
as.vector() now works correctly for AnvlArrays that are converted to bit64::integer64. It used to drop that class along with the shape, exposing the raw 64-bit pattern as a double.
The gradient of a conversion into a non-float data type is now zero instead of one. prim_convert() / nv_convert() passed the cotangent through whatever the data types were, so nv_convert(nv_convert(x, "i32"), "f64") reported a gradient of 1 where nv_floor() – the same function on the reals – correctly reported 0. Conversions between floats still pass the gradient through.
prim_scatter() now checks that update_computation returns one value of x’s data type, as prim_reduce() already did for its reductor. A combiner returning something else made type inference declare a data type the call could not produce, and failed in the backend.
prim_reduce()’s reductor no longer has to name its arguments lhs and rhs. They were passed by name, so function(a, b) failed with unused arguments (lhs = ..., rhs = ...); they are now matched positionally, as prim_scatter() already matched its update_computation.
prim_fill() / nv_fill() check that value is something dtype can hold: a whole number for an integer data type, a non-negative one for an unsigned one, a logical or 0 / 1 for bool.
Every data type of the float category counts as a float, so f16 and bf16 pass the checks that used to accept only f32 and f64. nv_pnorm() and nv_qnorm() keep the narrower requirement, as they carry one coefficient set per width.
The gradient of nv_gamma() is now correct for positive whole numbers.
prim_reduce_any() / prim_reduce_all() (and nv_reduce_any() / nv_reduce_all()) now reject a non-boolean input when the call is traced. Type inference declared a bool output whatever the input was, so an integer operand reached the lowering and failed with Data types of inputs and init_values must match.
Printed graphs, arrays and error messages now spell a data type the way anvl does, so bool no longer shows up as its MLIR spelling i1.
Improved the documentation and various error messages.
nv_runif() with min == max returns the state / values pair every other sampler returns, instead of the filled array on its own.
Tests
The environment variables that configure only the test suite are now spelled with an ANVL_TEST prefix: ANVL_TEST_SKIP_QUICKR. ANVL_TEST itself is unchanged.
The suite can be run with ANVL_DEFAULT_DEVICE=cpu:1, which makes anything allocating on the first CPU device rather than following the trace land on a device of its own instead of agreeing with everything else by accident. The default-device workflow runs it that way on the full-test label.
Moved some of pjrt’s dispatcher tests into anvl.
anvl 0.4.0
Breaking changes
Renamed dim/dims to axis/axes throughout the package (an axis is an index, a dimension is a size); ndims() is now naxes().
Renamed the primary array argument of prim_* / nv_* functions from operand to x.
nv_runif()’s lower/upper arguments are now min/max, nv_rnorm()’s mu/sigma are now mean/sd, and nv_rbinom()’s n is now size, matching the corresponding R functions.
Bug fixes
nv_sample_int() (formerly nv_rdunif()) was off by one: the first integer was drawn twice as often as it should have been, and the last integer was never drawn at all.
Features
New nv_sample() samples from an arbitrary population.
New functions for the normal distribution: nv_dnorm(), nv_qnorm(), and nv_pnorm() thanks to Louis Aslett. They are implemented to be accurate far into either tail.
nv_rnorm()’s mean and sd now accept arrayish inputs.
Dimension arguments (dim, dims, dimension, permutation) now accept negative values that count from the end, so -1 refers to the last dimension.
Reshaping functions accept a single -1 in shape indicating a dimension to be inferred.
Added support for 1-3 dimensional convolutions, thanks to Troy Hernandez.
AnvlArray constructors and converters have gained a check argument that opts into scanning for NA values, see the “Gotchas” vignette for more information.
nv_var() and nv_sd() now default to dims = NULL, which reduces over all dimensions and returns a scalar, consistent with the other reductions.
trace_fn() gained an optimize argument controlling which graph optimization passes run on the traced graph. TRUE runs all passes, FALSE (default) runs none, and a character vector (e.g. c("inline_scalars", "remove_unused_constants")) selects a subset. jit() always traces with all passes enabled.
Improved the installation vignette
Performance
Most nv_*() API functions are now JIT-compiled internally (via a new @jit roxygen roclet), speeding up eager-mode execution.
Tracing (trace_fn()) performance has been improved.
StableHLO lowering has been sped up.
Calling jit()ted functions is now significantly faster.
Bug fixes
Reductions now reject dimensions that are out of range for the operand instead of silently ignoring them.
NULL is now treated as an empty node when flattening and unflattening trees.
On CPU, jitted XLA functions now back every non-aliased output with an R-owned RAWSXP. anvl appends a phantom donated input per unaliased output during lowering, allocates pjrt::pjrt_empty() buffers at execute time, and pjrt migrates the keepalive onto the output XPtr. The output’s host bytes are then managed by R’s GC.
Renamed user-facing API functions to match base R names: nv_sine() -> nv_sin(), nv_cosine() -> nv_cos(), nv_ceil() -> nv_ceiling(), nv_cholesky() -> nv_chol(). The corresponding primitives were renamed in step: prim_sine() -> prim_sin(), prim_cosine() -> prim_cos(), prim_cholesky() -> prim_chol().
nv_solve() no longer requires a to be symmetric positive-definite as it uses LU instead of Cholesky decomposition. Because of this, it is no longer differentiable, as the reverse rule for LU is not implemented yet.
nv_chol() / prim_chol() now default to lower = FALSE (upper-triangular factor), matching base R’s chol(). Previously defaulted to lower = TRUE.
New Features
Linear algebra
New matrix-decomposition primitives and corresponding nv_*() functions: qr, lu, svd, eigh. None of them implement a reverse rule yet.
The overloaded %% operator now calls the new nv_mod() to be consistent with base R.
The reverse rule for prim_reduce_prod() no longer produces NaN / Inf gradients when the input contains zeros.
The CI now actually runs the torch-comparison tests.
nv_runif() not properly respects the lower argument.
anvl 0.2.0
Breaking Changes
The package was renamed from anvil to anvl to avoid a conflict with the Bioconductor package AnVIL.
AnvilTensor/nv_tensor were renamed to AnvlArray and nv_array to be more in line with R’s array(). Also, nv_aten() was renamed to nv_aval().
Subsetting with list() (e.g. x[list(1, 3)]) is no longer supported. Use array() to wrap the indices instead, e.g. x[array(c(1L, 3L))]. This mirrors the input convention used everywhere else in the package.
Removed debug mode.
Remove NSE support for nvl_if. It now requires passing 0-argument closures as true and false arguments.
Primitives renamed from nvl_* to prim_*. The underlying primitive object containing the rules and metadata is now part of the JitPrimitive function via the primitive attribute.
New Features
Better composability: jit()ted functions can now be used in other jit()-calls. This is the mechanism underlying the new eager mode.
Eager mode was added: This means, you can now do nv_add(1, nv_array(1:2)) and it will actually perform the computation and not only do type inference.
An experimental {quickr} backend was added It only runs on CPU for now and supports a subset of available operations. You can enable it via the backend argument in jit() and nv_array() or via the anvl.default_backend option.
New primitives:
nvl_cholesky() to compute the Cholesky decomposition of a matrix.
nvl_triangular_solve() to solve a system of linear equations with a triangular matrix.
New API functions (+ corresponding R generic implementations):
nv_diag() to create a diagonal matrix from a 1-D tensor.
nv_cholesky() to compute the Cholesky decomposition of a matrix.
nv_device() constructs a backend-specific device object (e.g. nv_device("cpu")) that can be passed as device to array constructors like nv_fill() or nv_iota().
Many operations are now done asynchronously, which improves performance, especially on GPUs.
Bug Fixes
+-Inf/NaN are correctly created for f64 when inlined into the XLA exectuable (#182). This caused wrong results with e.g. nv_reduce_max() when working with f64.