Automatic vectorisation

Automatic vectorisation#

This notebook introduces auto_vmap, the conservative automatic vectorisation helper. The intended pattern is to write one clear single-sample function and then declare the sample ranks of the arguments that may carry a leading batch axis.

The helper implements paired batching only. If several checked arguments are batched, their leading dimensions must agree; unbatched checked arguments are broadcast with in_axes=None.

import jax.numpy as jnp
import numpy as np

from stringjax_tools.auto_vectorise import auto_vmap

Rank-only batching#

The declarations below mean that moduli and fluxes are vector-valued single samples, while tau is scalar-valued. A shape such as (N, h) for moduli is therefore interpreted as a leading-axis batch of N samples.

@auto_vmap(moduli=1, tau=0, fluxes=1, jit=False)
def observable(moduli, tau, fluxes, scale=1.0):
    return scale * (jnp.sum(moduli) + tau + jnp.sum(fluxes))

single = observable(jnp.array([1.0, 2.0]), jnp.array(3.0), jnp.arange(4.0))
paired = observable(jnp.ones((3, 2)), jnp.arange(3.0), jnp.ones((3, 4)))
broadcast = observable(jnp.ones((3, 2)), jnp.array(3.0), jnp.ones(4))

print('single:', single)
print('paired:', paired)
print('broadcast:', broadcast)
print('axes:', observable.auto_vmap_axes(jnp.ones((3, 2)), jnp.array(3.0), jnp.ones(4)))

Exact shape validation#

Ranks decide whether an argument is batched. Exact trailing dimensions are an optional diagnostic layer. String dimensions such as "h12" are resolved from self or another bound object; derived dimensions should be callables.

class ToyModel:
    def __init__(self, h12, n_fluxes):
        self.h12 = h12
        self.n_fluxes = n_fluxes

    @auto_vmap(
        sample_ranks={'moduli': 1, 'fluxes': 1},
        sample_shapes={
            'moduli': 'h12',
            'fluxes': lambda bound: (2 * bound['self'].n_fluxes,),
        },
        validate_shapes=True,
        jit=False,
    )
    def diagnostic(self, moduli, fluxes):
        return jnp.sum(jnp.abs(moduli) ** 2) + 0.01 * jnp.sum(fluxes**2)

model = ToyModel(h12=3, n_fluxes=2)
values = model.diagnostic(jnp.ones((4, 3)), jnp.ones((4, 4)))
print(values)

Shape errors are raised before JAX traces the function, which makes them easier to interpret than downstream tracing errors.

try:
    model.diagnostic(jnp.ones((4, 2)), jnp.ones((4, 4)))
except ValueError as exc:
    print(type(exc).__name__ + ':', exc)