Skip to content

Least-squares solve¤

jaxmg.least_squares solves \(\min_X\lVert AX-B\rVert_2\) for an \(M\times N\) matrix with \(M\geq N\) using distributed QR factorization.

Common setup¤

This example uses four Python processes and a \(4\times1\) process grid. The dimensions are chosen so the local matrix shards and the returned solution are evenly divisible by the process grid and tile size.

import jax

jax.config.update("jax_enable_x64", True)
jax.distributed.initialize()

import jax.numpy as jnp
from jax.sharding import NamedSharding, PartitionSpec as P
from jaxmg import least_squares


mesh = jax.make_mesh((4, 1), ("pr", "pc"))
jax.set_mesh(mesh)
matrix_specs = P("pr", "pc")
matrix_sharding = NamedSharding(mesh, matrix_specs)
solve_sharding = NamedSharding(mesh, P("pr", None))

T_A = 128
M, N = 2048, 1024
dtype = jnp.float64
expected_x = jnp.ones((N,), dtype=dtype)


@jax.jit
def make_problem():
    diagonal = jnp.arange(1, N + 1, dtype=dtype)
    a = jnp.zeros((M, N), dtype=dtype)
    a = a.at[jnp.arange(N), jnp.arange(N)].set(diagonal)
    a = jax.reshard(a, matrix_sharding)
    b = jax.reshard(a @ expected_x, solve_sharding)
    return a, b

Solve with least_squares¤

Pass the sharded matrix and solve input directly to least_squares:

a, b = make_problem()

x = least_squares(
    a,
    b,
    T_A=T_A,
)
x.block_until_ready()

correct = jnp.allclose(x, expected_x)
correct.block_until_ready()

if jax.process_index() == 0:
    print(correct)

A matrix solve input of shape (M, K) returns a solution of shape (N, K). Each process-grid column must own at least one block-cyclic tile of the solve input, so a vector solve input requires a process grid with one column.

Warning

The public wrapper donates a and b to the compiled solve. Do not use those arrays after the call. Use donate=False to preserve them at the cost of retaining the original inputs alongside the native work buffers.

There is no need to apply jax.jit or specify donate_argnums: least_squares uses an internally cached jitted wrapper. Use the context interface below when the solve must be embedded in a larger compiled calculation.

Advanced: control the outer jax.jit¤

least_squares_shardmap_ctx runs the same distributed pipeline but leaves the outer jax.jit boundary to the caller. It returns (a_work, b_work, x, status). The two work outputs provide shape-compatible alias targets because cuSOLVERMp overwrites both inputs.

The advanced examples additionally use:

from functools import partial

from jaxmg import least_squares_shardmap_ctx

Case 1: a and b are arguments of the jitted function¤

Donate both arguments and retain both work outputs in the returned pytree:

@partial(jax.jit, donate_argnums=(0, 1))
def compiled_solve(a, b):
    a_work, b_work, x, status = least_squares_shardmap_ctx(
        a,
        b,
        T_A=T_A,
    )
    return a_work, b_work, 2.0 * x, status


a, b = make_problem()
a_work, b_work, scaled_x, status = compiled_solve(a, b)
scaled_x.block_until_ready()

a_work and b_work are opaque overwritten solver storage. They should not be interpreted as the original inputs.

Case 2: a and b are created inside the jitted function¤

When the outer function constructs both inputs, XLA controls their lifetime and the work outputs can remain internal:

@jax.jit
def build_and_solve():
    diagonal = jnp.arange(1, N + 1, dtype=dtype)
    a = jnp.zeros((M, N), dtype=dtype)
    a = a.at[jnp.arange(N), jnp.arange(N)].set(diagonal)
    a = jax.reshard(a, matrix_sharding)
    b = jax.reshard(a @ expected_x, solve_sharding)

    _, _, x, status = least_squares_shardmap_ctx(
        a,
        b,
        T_A=T_A,
    )
    return x, status


x, status = build_and_solve()
x.block_until_ready()

See the least_squares API reference for the complete argument and return-value documentation.