IRONWOOD

IRONWOOD

Copenhagen, Denmark

A short talk on annotation-driven vectorization for jaxtyped JAX dataclasses.
IRONWOODNoah SyrkisSeptember 22, 20261 |Vectorizing structured JAX code2 |Annotations define vectorization3 |PyTrees and broadcasting4 |Controlling what “scalar” means5 |From one device to a mesh6 |The Ironwood programming model1 |Vectorizing structured JAX codeConsider an array function, mapping an x to y. Vectorizing across the leftmost dimension means applying f independently to each row of x.2 of 181 |Vectorizing structured JAX codeimport jax.numpy as jnpfrom jax import Arraydef f(x: Array) -> Array: return x * xListing 1: Libraries are important and a function to be vectorized is defined# [0, 1, 2, ..., 9]x = jnp.arange(10)# [0, 1, 4, ..., 81]y = jax.vmap(f)(x)Listing 2: We declare an array for each entry of which we want to call f.3 of 181 |Vectorizing structured JAX codex[0]x[1]x[2]fy[0]y[1]y[2]4 of 181 |Vectorizing structured JAX codeBroadcasting shares an input across mapped calls.def add(x, bias): return x + biasy = jax.vmap(add, in_axes=(0, None))(x, bias)x varies.bias is reused.x[i]biasaddy[i]This is the first bookkeeping step: deciding which leaves are mapped and which leaves are shared.5 of 181 |Vectorizing structured JAX codejnp.vectorize generalizes the idea.It lets scalar signatures describe how arrays should broadcast and map.@jnp.vectorize(signature="(d),(d)->()")def dot(x, y): return x @ yThe signature says d is part of one scalar call.Other leading dimensions broadcast and vector­ize.x.shape # [batch, d]y.shape # [d]dot(x, y).shape # [batch]6 of 181 |Vectorizing structured JAX codeIronwood applies that idea to PyTrees.Real JAX code is often dataclass-shaped, not just array-shaped.@dataclassclass Example: x: Shaped[Array, "*batch feature"] mask: Shaped[Array, "*batch"] table: Shaped[Array, "kind feature"]The annotations say what jnp.vectorize signa­tures say for arrays.They also work through structured JAX values.7 of 182 |Annotations define vectorizationIronwood reads jaxtyping annotations.x: Shaped[Array, "*batch feature"]y: Shaped[Array, "*batch"]z: Shaped[Array, "feature"]*batch means: these leading axes are mapped.[b, n]*batch = bfeature = nEverything after *batch belongs to the scalar function.8 of 182 |Annotations define vectorizationThe function stays ordinary.from ironwood import vectorize@vectorizedef normalize(x: Shaped[Array, "*batch feature"]): return x / jnp.linalg.norm(x)The scalar body sees one feature vector.The caller may pass one vector, a batch of vec­tors, or a rectangular batch of vectors.The output gets the same batch prefix back.9 of 183 |PyTrees and broadcastingDataclasses are PyTrees.Ironwood descends through them, finds anno­tated leaves, and reconstructs the same structure on return.@dataclassclass Pair: left: Shaped[Array, "*batch dim"] right: Shaped[Array, "*batch dim"]@vectorizedef distance(pair: Pair) -> Array: return jnp.linalg.norm(pair.left - pair.right)10 of 183 |PyTrees and broadcastingLeaves without a batch prefix broadcast.@dataclassclass Batch: x: Shaped[Array, "*batch dim"] center: Shaped[Array, "dim"]x[b, d]center[d]scalar callout[b]One value can be shared by every mapped case without spelling out an axes policy.11 of 184 |Controlling what “scalar” meansScalar does not mean rank zero.It means one outer case.points: Shaped[Array, "*batch point xy"]point and xy remain inside the scalar function.@vectorizedef centroid( points: Shaped[Array, "*batch point xy"],) -> Shaped[Array, "xy"]: return points.mean(axis=0)The body computes one centroid. The wrapper computes many.12 of 184 |Controlling what “scalar” meansExtend keeps a named inner axis on the result.from ironwood import Extend@vectorizedef classify( x: Shaped[Array, "*batch item feature"],) -> Extend[Shaped[Array, ""], "item"]: return model(x)*batchitemfeaturescalar body*batch itemThe annotation says which axes are outer batch and which axes are retained.13 of 185 |From one device to a meshThe same annotated boundary can be sharded.@vectorize(shard_axis="data")def score(x: Example) -> Result: return scalar_score(x)batchflattenshard_mapvmaprestoreThe scalar body is still local code.14 of 185 |From one device to a meshThe mesh is explicit JAX state.mesh = jax.make_mesh((4,), ("data",))with jax.set_mesh(mesh): out = jax.jit(score)(examples)Ironwood shards the mapped batch.Non-batched leaves are replicated.The batch size must divide cleanly across the mesh axis.15 of 186 |The Ironwood programming modelIronwood is a small adapter:▶read jaxtyping annotations▶flatten PyTrees▶broadcast scalar leaves▶call vmap▶optionally call shard_map▶rebuild the outputjaxtyped dataclassIronwoodvmapshard_mapsame shape16 of 186 |The Ironwood programming modelThe rule:Write the scalar program.Put batch structure in the annotations.Keep data structure in dataclasses.Let JAX compile the transformed function.@vectorize(shard_axis="data")def f(x: Example, key: Shaped[Array, "*batch key"]): return scalar_f(x, key)Ironwood is for code that is already pure, typed, and structured.17 of 18References