Skip to content

Saving and loading

Compiling a problem traces its functions, finds the sparsity of block terms, lowers the programs of every term and compiles them. The disk cache shortens this on a machine that has done it before. A file shortens it everywhere. save writes a compiled problem to a file, and load reads it in another process, on another machine, or later, without tracing, detecting sparsity or compiling anything.

Saving

import tempfile
from pathlib import Path

import jax.numpy as jnp
import numpy as np

import jaxcip as jcp

problem = jcp.Problem(
    variables={"position": jcp.Variable(lower=-2.0, upper=2.0, shape=(2,))},
    objective=lambda x, params: (
        jnp.sum(x["position"] ** 4 - 2 * x["position"] ** 2)
        + jnp.dot(x["position"], params["target"])
    ),
    constraints=(jcp.Constraint(lambda x, params: jnp.sum(x["position"]), upper=1.0),),
)
params = {"target": np.array([1.0, 1.0])}
compiled = jcp.compile(problem, params)

path = Path(tempfile.mkdtemp()) / "problem.jcp"
jcp.save(compiled, path)

The file holds the programs of every term and of the affine remainders with their StableHLO, the sparsity patterns of block terms, the bounds and the compile options. .jcp is the usual extension. dumps returns the same data as bytes, for sending it to a worker process. Terms that appear more than once in a problem share their programs in the file, just as they do in memory.

Loading

The file does not hold the functions of the problem or the trees of its variables and parameters. load takes the trees, so that a loaded problem works like a compiled one: the solution uses the structure of variables, and solve takes parameters in the structure of params.

loaded = jcp.load(path, problem.variables, params)
solution = jcp.solve(loaded, params, options=jcp.SolveOptions(time_limit=60.0))
print(solution.x, solution.objective)
assert loaded.problem is None
assert solution.status is jcp.Status.OPTIMAL
assert loaded.statistics.compiled == 0

The leaves of variables are Variables or arrays, and the leaves of params are arrays or jax.ShapeDtypeStructs, as for compile. Only their paths and shapes matter. They are checked against the file, and a tree with another leaf or shape raises an error that names the first difference. The bounds and domains come from the file, so the Variables of the tree may carry any bounds, or the tree may hold plain arrays. Any pytree that JAX can flatten works, including named tuples, dataclasses and Equinox modules. The class of a node only has to exist in the loading process, since nothing about it is stored.

Other machines

A file stores one executable per program, the one of the machine that saved it, together with the StableHLO of every program. A machine with the same operating system, architecture and plugin version loads the executables directly, on as many threads as threads says, or one per CPU core. On any other machine the plugin cannot use them, and load compiles the StableHLO instead. That costs about as long as the compilation of a problem without the lowering and the sparsity detection. Pass on_mismatch="raise" to get a cyxla.TargetMismatchError in that case.

The programs always run on the CPU, so a file made on a machine without a GPU loads on a machine with one and still runs on the CPU.

What a loaded problem skips

Loading does not import asdex or numba, does not call the functions of the problem and does not trace anything. Terms that the process compiled or loaded before are reused, so loading a second file with the same terms is faster. With warm_up=True, which is the default, each program runs once, so that the first solve is as fast as the later ones. Importing JAX-CIP and JAX is the remaining cost of a new process.

A loaded problem has no jaxprs, so problem is None, and it cannot be recompiled with other options. Compile the problem again for that.

Trust

A file contains machine code, which the plugin runs when it loads the executables. Load only files from a source that you trust. The file protects against damage with checksums, and it does not protect against someone who writes one on purpose.

A compiled problem cannot be pickled, because the trees are not part of the file. Send the bytes of dumps to a worker and call loads there with the trees.