Skip to content

v0.1.0

Choose a tag to compare

@ehsanmok ehsanmok released this 04 Sep 23:43
· 408 commits to main since this release

First release of νMAX, a numerical computing library built on MAX.

One kernel, several meanings

Every kernel in the library is written once, against the FloatLike trait.
The conformer you instantiate it with decides what the call returns:

Type What a call gives you
Plain[dtype, width] Ordinary SIMD at hardware speed
Dual[Inner] f(x) and f'(x) from one call, forward-mode
Gradient[Inner, n_vars] Every ∂f/∂xᵢ from one call
Compensated[dtype, width] Roughly double dtype's precision, same algorithm
Decimal Exact base-10 fixed point
Complex[Inner] Complex results
Interval[Inner] Rigorous enclosures

They nest. Gradient[Dual[Plain]] is a Hessian, Complex[Dual[Plain]]
differentiates holomorphically, Dual[Compensated] takes a derivative in
double-double. Nobody implemented autodiff for the incomplete beta function;
the chain rule lives in Dual's arithmetic rather than in any kernel, so
differentiating one costs nothing extra.

from numax import Dual, FloatLike, Plain, f32

def g[T: FloatLike](x: T) -> T:              # written once, no `dtype`
    return (-(x * x)).exp()

def main():
    print(g(Plain[f32](0.5)).v)              # 0.7788008
    var d = g(Dual[Plain[f32]].seed(0.5))
    print(d.value.v, d.deriv.v)              # 0.7788008 -0.7788008

One tensor, every device

Tensor owns a MAX DeviceBuffer, so the DeviceContext handed to a
factory decides host or device memory. Nothing else in the program changes,
and .view() yields the TileTensor every MAX kernel takes. The tensor
drivers (map, reduce, reduce_axis, broadcast_op_rows,
map_threaded) take one gpu: Bool compile-time parameter instead of
shipping two implementations, and map[gpu=True] launches through
DeviceContext.enqueue_function on Metal and CUDA with no per-backend
branch anywhere in the library.

The two-tier rule is what pays for that. Tier-1 kernels run a fixed
iteration count with no per-lane branching, doing selection arithmetically
instead of with an if, which is what makes them GPU-launchable unmodified.
Tier 2 is Plain-only and may loop to a tolerance. Every module and every
function declares which tier it is in, so a reader at a call site never has
to audit a body.

What is in it

Subpackage Contents
numax.core FloatLike and its conformers, Tensor, elementwise math, comparisons, sorting, the tensor engine
numax.special Γ and B, erf, Bessel J/Y, Lambert W, elliptic K/E, orthogonal polynomials, activations
numax.linalg cholesky, lu, qr, eigh, svd, solve, inverse, pinv, det, cond, norms, matmul
numax.optimize newton/halley/bisection at fixed iterations; newton_tol, brentq, bfgs to a tolerance
numax.integrate Gauss-Legendre/Simpson/trapezoid, rk4/dopri5; quad, quad_vec, solve_ivp
numax.interpolate Horner, cubic splines, Chebyshev fits
numax.fft fft/ifft, rfft/irfft, fft2, fftfreq, fftshift
numax.signal convolve, correlate, Hann/Hamming/Blackman windows
numax.stats Descriptive statistics, nine scipy.stats-shaped distribution namespaces, sampling
numax.io .npy read/write, byte-identical to numpy.save, plus numax's own NMX1 format

Subpackages carry NumPy and SciPy names, so an import has an obvious
counterpart. from numax.prelude import * is the common surface in one
line. It leaves out every name that would shadow a Mojo builtin, because a
module-level definition replaces the builtin rather than overloading it.

The .npy support is small but it removes a conversion step: a program
ported from NumPy can read the files it already has and hand results back
the same way.

Performance

Every number below was measured, and CPU and GPU results are never combined
into one comparison. On an A10G's GPU, numax reaches 60,955 M elem/s against
torch.compile's 53,607, about 83% of the card's bandwidth spec. On the same
machine's CPU, map over a Plain kernel runs at 0.998x a hand-written
raw-SIMD loop and 4.5x NumPy. Every row in docs/performance.md compares
implementations on one processor.

Accuracy

Every approximation documents an error bound, and pixi run accuracy checks
each one against mpmath references computed at 50 digits and checked into
the repository.

Limits worth knowing before you start

Shapes are compile-time parameters. There is no broadcasting, no strided
views, no slicing and no fancy indexing. Those wait on a runtime-shape
owner, because a view over a slice is not row-major and every driver in
numax.core.tensor requires that it is.

Above the tensor drivers the surface is not uniformly rank-generic yet.
numax.stats reduces over every element with no axis=; sorting flattens;
transpose is 2-D; concatenate, split and stack are rank-1. The
SciPy-shaped algorithms are fixed-size Array kernels, so linalg works on
matrices and fft2 on a square transform, while quad, solve_ivp, the
splines and the distributions are 1-D.

Anything whose length depends on a value rather than a parameter has to say
so in its signature. unique returns a full-length tensor plus a count
rather than a right-sized array, and its docstring tells you to read only
the first count entries.

.npz is out of scope. It is a zip container, and while stored-only entries
would be easy, deflate is not.

Running the GPU examples needs a real device. CI compiles them against a
pinned target so their call sites stay honest, but execution is a local
check, verified by hand on both Metal and CUDA.

This is a young library and the APIs may change.

Install

[workspace]
channels = ["https://conda.modular.com/max", "conda-forge"]
preview = ["pixi-build"]

[dependencies]
numax = { git = "https://github.com/ehsanmok/numax.git", tag = "v0.1.0" }

Requires pixi. mojo ==1.0.0 and max ==26.5 come in
transitively. numax is distributed as source rather than a .mojopkg,
because a package embeds the compiler version it was built with and refuses
to load against any other.

Verified before release on linux-64: the git dependency resolves from this
tag, builds, and runs the example above with no import path flag. The
manifest declares linux-64, linux-aarch64 and osx-arm64; CI runs on linux-64
and osx-arm64.

Verification

44 test suites and 664 tests, 14 CPU examples, the accuracy harness against
the mpmath references, the CPU benchmark, mojo doc over the whole public
generic surface, and a formatting check. All green on the tagged commit, on
both ubuntu-latest and macos-15.

Full documentation: features ·
architecture · parity ·
performance