Skip to content

JAX

JAX frontend for ZKDV.

Transaction

Transaction(*, batch: Any, params: Any, opt_state: Any, overlap: Policy = UNCHECKED, _proof: Any | None = None)

Declare the pre- and post-update values inside a ZKDV.jit function.

Source code in zkdv/jax/transaction.py
def __init__(
    self,
    *,
    batch: Any,
    params: Any,
    opt_state: Any,
    overlap: overlap_policy.Policy = overlap_policy.UNCHECKED,
    _proof: Any | None = None,
):
    trace = _CURRENT_TRACE.get()
    if trace is None:
        raise RuntimeError(
            "zkdv.jax.Transaction must be created inside zkdv.jax.ZKDV.jit"
        )
    self._trace = trace
    self._attested = trace._attested
    self._updated = False
    trace.start(
        batch=batch,
        params=params,
        opt_state=opt_state,
        overlap_policy=overlap,
        proof=_proof,
    )

ZKDV

ZKDV(path: str | Path, config: ZKDVConfig | None = None, *, max_in_flight: int = 2, replay_snapshot_interval: int = 12)

Bases: Driver

Generate a verification tape around annotated JAX training functions.

Source code in zkdv/driver/jax.py
def __init__(
    self,
    path: str | Path,
    config: ZKDVConfig | None = None,
    *,
    max_in_flight: int = 2,
    replay_snapshot_interval: int = 12,
) -> None:
    super().__init__(
        path,
        config,
        backend="jax",
        annotation=jax.profiler.TraceAnnotation,
        max_in_flight=max_in_flight,
        replay_snapshot_interval=replay_snapshot_interval,
    )
    self._program_in_shardings = UNSPECIFIED
    self._host_mirrors: dict[int, tuple[weakref.ReferenceType[Any], Any]] = {}

attest

attest(function: Callable[..., Any] | None = None, /, *, in_shardings: Any = UNSPECIFIED, out_shardings: Any = UNSPECIFIED, static_argnums: int | Sequence[int] | None = None, static_argnames: str | Iterable[str] | None = None, donate_argnums: int | Sequence[int] | None = None, donate_argnames: str | Iterable[str] | None = None, keep_unused: bool = False, device: Any | None = None, backend: str | None = None, inline: bool = False, compiler_options: dict[str, Any] | None = None) -> Any

Register the exportable sampled-update program.

Source code in zkdv/driver/jax.py
def attest(
    self,
    function: Callable[..., Any] | None = None,
    /,
    *,
    in_shardings: Any = UNSPECIFIED,
    out_shardings: Any = UNSPECIFIED,
    static_argnums: int | Sequence[int] | None = None,
    static_argnames: str | Iterable[str] | None = None,
    donate_argnums: int | Sequence[int] | None = None,
    donate_argnames: str | Iterable[str] | None = None,
    keep_unused: bool = False,
    device: Any | None = None,
    backend: str | None = None,
    inline: bool = False,
    compiler_options: dict[str, Any] | None = None,
) -> Any:
    """Register the exportable sampled-update program."""

    if function is None:
        return lambda value: self.attest(
            value,
            in_shardings=in_shardings,
            out_shardings=out_shardings,
            static_argnums=static_argnums,
            static_argnames=static_argnames,
            donate_argnums=donate_argnums,
            donate_argnames=donate_argnames,
            keep_unused=keep_unused,
            device=device,
            backend=backend,
            inline=inline,
            compiler_options=compiler_options,
        )
    self._check_poison()
    if self._core is None:
        raise RuntimeError("ZKDV not started. Call start() first.")
    if self._program is not None:
        raise RuntimeError("ZKDV already has an attested program")
    if out_shardings is not UNSPECIFIED:
        warnings.warn(
            "ZKDV.attest ignores out_shardings because the protocol fixes its outputs",
            stacklevel=2,
        )
    self._program = jax.jit(
        function,
        in_shardings=in_shardings,
        static_argnums=static_argnums,
        static_argnames=static_argnames,
        donate_argnums=donate_argnums,
        donate_argnames=donate_argnames,
        keep_unused=keep_unused,
        device=device,
        backend=backend,
        inline=inline,
        compiler_options=compiler_options,
    )
    self._program_in_shardings = in_shardings
    return self._program

jit

jit(function: Callable[..., Any] | None = None, /, *, in_shardings: Any = UNSPECIFIED, out_shardings: Any = UNSPECIFIED, static_argnums: int | Sequence[int] | None = None, static_argnames: str | Iterable[str] | None = None, donate_argnums: int | Sequence[int] | None = None, donate_argnames: str | Iterable[str] | None = None, keep_unused: bool = False, device: Any | None = None, backend: str | None = None, inline: bool = False, compiler_options: dict[str, Any] | None = None) -> Any

Compile a transaction-annotated function with jax.jit semantics.

Source code in zkdv/driver/jax.py
def jit(
    self,
    function: Callable[..., Any] | None = None,
    /,
    *,
    in_shardings: Any = UNSPECIFIED,
    out_shardings: Any = UNSPECIFIED,
    static_argnums: int | Sequence[int] | None = None,
    static_argnames: str | Iterable[str] | None = None,
    donate_argnums: int | Sequence[int] | None = None,
    donate_argnames: str | Iterable[str] | None = None,
    keep_unused: bool = False,
    device: Any | None = None,
    backend: str | None = None,
    inline: bool = False,
    compiler_options: dict[str, Any] | None = None,
) -> Any:
    """Compile a transaction-annotated function with ``jax.jit`` semantics."""

    self._check_poison()
    if function is None:
        return lambda value: self.jit(
            value,
            in_shardings=in_shardings,
            out_shardings=out_shardings,
            static_argnums=static_argnums,
            static_argnames=static_argnames,
            donate_argnums=donate_argnums,
            donate_argnames=donate_argnames,
            keep_unused=keep_unused,
            device=device,
            backend=backend,
            inline=inline,
            compiler_options=compiler_options,
        )
    return CompiledFunction(
        self,
        function,
        in_shardings=in_shardings,
        out_shardings=out_shardings,
        static_argnums=static_argnums,
        static_argnames=static_argnames,
        donate_argnums=donate_argnums,
        donate_argnames=donate_argnames,
        keep_unused=keep_unused,
        device=device,
        backend=backend,
        inline=inline,
        compiler_options=compiler_options,
    )

index

index(tree, idx) -> float

Select one scalar from a PyTree's logical flattened leaf order.

Source code in zkdv/_invoked_jax/index.py
@jax.jit
def index(tree, idx) -> float:
    """Select one scalar from a PyTree's logical flattened leaf order."""

    with jax.enable_x64():
        leaves = jax.tree.leaves(tree)
        ends = []
        size = 0
        for value in leaves:
            size += value.size
            ends.append(size)
        ends = jnp.asarray(ends, dtype=idx.dtype)
        starts = jnp.concatenate((jnp.zeros((1,), dtype=idx.dtype), ends[:-1]))
        leaf_index = jnp.searchsorted(ends, idx, side="right")
        local_index = idx - starts[leaf_index]
        branches = tuple(
            lambda position, value=value: value.reshape(-1)[position].astype(
                jnp.float32
            )
            for value in leaves
        )
        return jax.lax.switch(leaf_index, branches, local_index)

contrib

High-level JAX integrations built on the canonical ZKDV pipeline.

TrainState

Bases: TrainState

Flax's ordinary TrainState with an implicit ZKDV update marker.

apply_gradients

apply_gradients(*, grads: Any, **kwargs: Any) -> TrainState

Apply gradients exactly as Flax does and register the transition.

Source code in zkdv/jax/contrib/train_state.py
def apply_gradients(self, *, grads: Any, **kwargs: Any) -> "TrainState":
    """Apply gradients exactly as Flax does and register the transition."""

    if not is_tracing():
        return super().apply_gradients(grads=grads, **kwargs)

    transaction = Transaction(
        batch=AUTOMATIC_BATCH,
        params=meta.unbox(self.params),
        opt_state=self._zkdv_optimizer_state(),
        overlap=self.zkdv_overlap,
        _proof=self.proof,
    )
    overwrite = isinstance(grads, Mapping) and OVERWRITE_WITH_GRADIENT in grads
    if overwrite:
        gradients = grads["params"]
        parameters = self.params["params"]
    else:
        gradients = grads
        parameters = self.params

    updates, optimizer = self.tx.update(gradients, self.opt_state, parameters)
    if overwrite:
        committed_updates = {
            "params": updates,
            OVERWRITE_WITH_GRADIENT: (
                grads[OVERWRITE_WITH_GRADIENT]
                - self.params[OVERWRITE_WITH_GRADIENT]
            ),
        }
    else:
        committed_updates = updates
    if transaction._attested:
        state = self.replace(step=self.step + 1, opt_state=optimizer, **kwargs)
        transaction.update(
            deltas=meta.unbox(committed_updates),
            params=meta.unbox(self.params),
            opt_state=state._zkdv_optimizer_state(),
        )
        return state

    updated_parameters = optax.apply_updates(parameters, updates)
    params = (
        {
            "params": updated_parameters,
            OVERWRITE_WITH_GRADIENT: grads[OVERWRITE_WITH_GRADIENT],
        }
        if overwrite
        else updated_parameters
    )
    state = self.replace(
        step=self.step + 1,
        params=params,
        opt_state=optimizer,
        **kwargs,
    )
    transaction.update(
        deltas=meta.unbox(committed_updates),
        params=meta.unbox(params),
        opt_state=state._zkdv_optimizer_state(),
    )
    return state

train_state

A Flax TrainState whose optimizer transition is committed by ZKDV.

TrainState

Bases: TrainState

Flax's ordinary TrainState with an implicit ZKDV update marker.

apply_gradients

apply_gradients(*, grads: Any, **kwargs: Any) -> TrainState

Apply gradients exactly as Flax does and register the transition.

Source code in zkdv/jax/contrib/train_state.py
def apply_gradients(self, *, grads: Any, **kwargs: Any) -> "TrainState":
    """Apply gradients exactly as Flax does and register the transition."""

    if not is_tracing():
        return super().apply_gradients(grads=grads, **kwargs)

    transaction = Transaction(
        batch=AUTOMATIC_BATCH,
        params=meta.unbox(self.params),
        opt_state=self._zkdv_optimizer_state(),
        overlap=self.zkdv_overlap,
        _proof=self.proof,
    )
    overwrite = isinstance(grads, Mapping) and OVERWRITE_WITH_GRADIENT in grads
    if overwrite:
        gradients = grads["params"]
        parameters = self.params["params"]
    else:
        gradients = grads
        parameters = self.params

    updates, optimizer = self.tx.update(gradients, self.opt_state, parameters)
    if overwrite:
        committed_updates = {
            "params": updates,
            OVERWRITE_WITH_GRADIENT: (
                grads[OVERWRITE_WITH_GRADIENT]
                - self.params[OVERWRITE_WITH_GRADIENT]
            ),
        }
    else:
        committed_updates = updates
    if transaction._attested:
        state = self.replace(step=self.step + 1, opt_state=optimizer, **kwargs)
        transaction.update(
            deltas=meta.unbox(committed_updates),
            params=meta.unbox(self.params),
            opt_state=state._zkdv_optimizer_state(),
        )
        return state

    updated_parameters = optax.apply_updates(parameters, updates)
    params = (
        {
            "params": updated_parameters,
            OVERWRITE_WITH_GRADIENT: grads[OVERWRITE_WITH_GRADIENT],
        }
        if overwrite
        else updated_parameters
    )
    state = self.replace(
        step=self.step + 1,
        params=params,
        opt_state=optimizer,
        **kwargs,
    )
    transaction.update(
        deltas=meta.unbox(committed_updates),
        params=meta.unbox(params),
        opt_state=state._zkdv_optimizer_state(),
    )
    return state

jit

JAX compilation of symbolically annotated training functions.

CompiledFunction

CompiledFunction(driver: Any, function: Callable[..., Any], *, in_shardings: Any = UNSPECIFIED, out_shardings: Any = UNSPECIFIED, static_argnums: int | Sequence[int] | None = None, static_argnames: str | Iterable[str] | None = None, donate_argnums: int | Sequence[int] | None = None, donate_argnames: str | Iterable[str] | None = None, keep_unused: bool = False, device: Any | None = None, backend: str | None = None, inline: bool = False, compiler_options: dict[str, Any] | None = None)

Bases: CompiledFunction

Compile one annotated function and submit its hidden pipeline evidence.

Source code in zkdv/jax/jit.py
def __init__(
    self,
    driver: Any,
    function: Callable[..., Any],
    *,
    in_shardings: Any = UNSPECIFIED,
    out_shardings: Any = UNSPECIFIED,
    static_argnums: int | Sequence[int] | None = None,
    static_argnames: str | Iterable[str] | None = None,
    donate_argnums: int | Sequence[int] | None = None,
    donate_argnames: str | Iterable[str] | None = None,
    keep_unused: bool = False,
    device: Any | None = None,
    backend: str | None = None,
    inline: bool = False,
    compiler_options: dict[str, Any] | None = None,
) -> None:
    super().__init__(driver, function, TREES, "zkdv.jax.ZKDV.jit")
    self._static_argnums = _indices(
        static_argnums, static_argnames, self._signature.names
    )
    self._donate_argnums = _indices(
        donate_argnums, donate_argnames, self._signature.names
    )
    self._in_shardings = self._normalize_inputs(in_shardings)
    self._requested_out_shardings = out_shardings
    self._out_shardings = None
    self._keep_unused = keep_unused
    self._device = device
    self._backend = backend
    self._inline = inline
    self._compiler_options = compiler_options
    self._schema: TransactionSchema | None = None
    self._replay: ReplaySchema | None = None
    self._analyzer = jax.jit(
        self._stage,
        in_shardings=self._in_shardings,
        out_shardings=UNSPECIFIED,
        static_argnums=self._static_argnums,
        keep_unused=True,
        device=device,
        backend=backend,
        inline=inline,
        compiler_options=compiler_options,
    )

lower

lower(*args: Any, **kwargs: Any) -> Any

Lower the fused executable without opening a transcript transaction.

Source code in zkdv/jax/jit.py
def lower(self, *args: Any, **kwargs: Any) -> Any:
    """Lower the fused executable without opening a transcript transaction."""

    values = self._signature.bind(args, kwargs)
    self._prepare(values)
    compiled_values, _ = self._execution(values)
    return self._compiled.lower(*compiled_values)

pipeline

Canonical JAX transaction pipeline above the native protocol boundary.

Pipeline

Pipeline(core: Any, program: Any, placement: Any, program_in_shardings: Any, queue: PipelineQueue, params: Any, opt_state: Any, batch: Any, *, snapshot_interval: int = 12)

Bases: Pipeline

Compile and submit the JAX fused update protocol.

Source code in zkdv/jax/pipeline.py
def __init__(
    self,
    core: Any,
    program: Any,
    placement: Any,
    program_in_shardings: Any,
    queue: PipelineQueue,
    params: Any,
    opt_state: Any,
    batch: Any,
    *,
    snapshot_interval: int = 12,
) -> None:
    if program_in_shardings is UNSPECIFIED:
        program_in_shardings = (placement,) * 3
    program_inputs = jax.device_put(
        (params, opt_state, batch),
        program_in_shardings,
    )
    (
        self._batch_to_host,
        self._start,
        self._check,
        self._register,
        self._abort,
        self._compile,
        self._prepare,
        self._resolve,
    ) = core.pipeline(program, params, opt_state, placement, program_inputs[2])
    core.program(program, *program_inputs)
    self._placement = placement
    self._batch_placement = program_inputs[2].sharding
    checkpoint = ReplayCheckpoint(
        program,
        params,
        opt_state,
        program_inputs[2],
        snapshot_interval=snapshot_interval,
        snapshot_pool_size=queue.capacity,
    )
    super().__init__(core, queue, checkpoint)

proto

Publicly inheritable JAX protocols. Various library functions will ask for these prototypes.

F_u

Bases: Protocol

Parameter Update Function

Parameters:

Name Type Description Default
params

The model parameters.

required
state

The optimizer state or any sidechannel info.

required
Notes

Should be pure AND have all seralizable state elements. In Jax, serializable means jax.export() can pack it into a flatbuffer. In torch, TODO.

We will be hashing the seralized version of this function into the commitment record.

replay

JAX replay schemas and bounded checkpoints.

ArraySpec dataclass

ArraySpec(shape: tuple[int, ...], dtype: Any, byte_count: int, key: bool)

One array's lossless representation in a packed uint32 batch.

ReplayCheckpoint

ReplayCheckpoint(program: Callable[[Any, Any, Array], tuple[Any, Any]], params: Any, optimizer: Any, batch: Array, *, snapshot_interval: int = 12, snapshot_pool_size: int = 2)

Bound replay with asynchronous, donation-safe host snapshots.

Source code in zkdv/jax/replay/checkpoint.py
def __init__(
    self,
    program: Callable[[Any, Any, jax.Array], tuple[Any, Any]],
    params: Any,
    optimizer: Any,
    batch: jax.Array,
    *,
    snapshot_interval: int = 12,
    snapshot_pool_size: int = 2,
) -> None:
    if snapshot_interval < 1:
        raise ValueError("ZKDV replay_snapshot_interval must be positive")
    self.snapshot_interval = snapshot_interval
    self._param_shardings = _device_shardings(params)
    self._optimizer_shardings = _device_shardings(optimizer)
    self._host_param_shardings = _host_shardings(params)
    self._host_optimizer_shardings = _host_shardings(optimizer)
    self._batch_sharding = batch.sharding

    allocate = jax.jit(
        lambda current_params, current_optimizer: (
            current_params,
            current_optimizer,
        ),
        in_shardings=(self._param_shardings, self._optimizer_shardings),
        out_shardings=(
            self._host_param_shardings,
            self._host_optimizer_shardings,
        ),
    ).lower(params, optimizer).compile()
    # A due copy donates one complete host destination and returns its
    # snapshot into that allocation. Prime enough trees for the hard
    # in-flight bound so production never allocates a training-sized tree.
    with jax.profiler.TraceAnnotation("zkdv.snapshot.pool_warmup"):
        warm = [
            allocate(params, optimizer)
            for _ in range(max(2, snapshot_pool_size))
        ]
        jax.block_until_ready(warm)
    restore_params, restore_optimizer = warm[0]
    self._restore = jax.jit(
        lambda host_params, host_optimizer: (host_params, host_optimizer),
        in_shardings=(
            self._host_param_shardings,
            self._host_optimizer_shardings,
        ),
        out_shardings=(self._param_shardings, self._optimizer_shardings),
    ).lower(restore_params, restore_optimizer).compile()
    self._free = [
        _Destination(host_params, host_optimizer)
        for host_params, host_optimizer in warm
    ]
    destination = self._free[0]
    self._copy = jax.jit(
        lambda current_params, current_optimizer, host_params, host_optimizer: (
            current_params,
            current_optimizer,
        ),
        in_shardings=(
            self._param_shardings,
            self._optimizer_shardings,
            self._host_param_shardings,
            self._host_optimizer_shardings,
        ),
        out_shardings=(
            self._host_param_shardings,
            self._host_optimizer_shardings,
        ),
        donate_argnums=(2, 3),
    ).lower(
        params,
        optimizer,
        destination.params,
        destination.optimizer,
    ).compile()
    self._executor = ThreadPoolExecutor(
        max_workers=1, thread_name_prefix="zkdv-replay-copy"
    )

    def advance(current_params, current_optimizer, packed_batch):
        deltas, optimizer_after = program(
            current_params, current_optimizer, packed_batch
        )
        params_after = jax.tree.map(jnp.add, current_params, deltas)
        return params_after, optimizer_after

    self._advance = jax.jit(
        advance,
        in_shardings=(
            self._param_shardings,
            self._optimizer_shardings,
            self._batch_sharding,
        ),
        out_shardings=(self._param_shardings, self._optimizer_shardings),
        donate_argnums=(0, 1),
    ).lower(params, optimizer, batch).compile()
    self._snapshots: dict[int, _Snapshot] = {}
    self._retired: list[Future[tuple[Any, Any]]] = []
    self._active: set[int] = set()
    self._batches: dict[int, Any] = {}
    self._lock = Lock()

host_optimizer_shardings property

host_optimizer_shardings: Any

Compiler-addressable host placement for optimizer snapshots.

host_param_shardings property

host_param_shardings: Any

Compiler-addressable host placement for parameter snapshots.

accept

accept(index: int, params: Any, optimizer: Any) -> None

Record acceptance without copying the replay result back to host.

Source code in zkdv/jax/replay/checkpoint.py
def accept(self, index: int, params: Any, optimizer: Any) -> None:
    """Record acceptance without copying the replay result back to host."""
    del params, optimizer
    with self._lock:
        if index not in self._active:
            raise RuntimeError("ZKDV accepted an inactive replay snapshot")

activate

activate(index: int, params: Any, optimizer: Any) -> None

Open a transaction and asynchronously copy its pre-state when due.

Source code in zkdv/jax/replay/checkpoint.py
def activate(self, index: int, params: Any, optimizer: Any) -> None:
    """Open a transaction and asynchronously copy its pre-state when due."""
    wait_for = None
    while True:
        with self._lock:
            self._reap_locked()
            if index in self._active:
                raise RuntimeError(
                    f"ZKDV replay snapshot {index} is already active"
                )
            due = index not in self._snapshots and (
                not self._snapshots or index % self.snapshot_interval == 0
            )
            if not due or self._free:
                self._active.add(index)
                break
            wait_for = self._retired[0] if self._retired else None
        if wait_for is None:
            raise RuntimeError("ZKDV replay snapshot pool is exhausted")
        # If copies cannot keep up with the selected interval, expose real
        # backpressure rather than silently violating the replay bound.
        wait_for.result()

    if not due:
        return
    with self._lock:
        destination = self._free.pop()
        dispatching = Event()
        future = self._executor.submit(
            self._copy_to_host,
            params,
            optimizer,
            destination,
            dispatching,
        )
        self._snapshots[index] = _Snapshot(index, future)
        self._prune_locked()
    # Wait only until the copy worker owns the GIL immediately before the
    # compiled call. Its input handler therefore registers source usage
    # before this thread can enter the next donated JAX call. This is not a
    # transfer wait: the host-valued result remains on ``future``.
    dispatching.wait()

close

close() -> None

Release the dispatcher after the owning pipeline has joined.

Source code in zkdv/jax/replay/checkpoint.py
def close(self) -> None:
    """Release the dispatcher after the owning pipeline has joined."""
    self.join()
    self._executor.shutdown(wait=True, cancel_futures=False)

join

join() -> None

Drain copies at an explicit pipeline synchronization boundary.

Source code in zkdv/jax/replay/checkpoint.py
def join(self) -> None:
    """Drain copies at an explicit pipeline synchronization boundary."""
    with self._lock:
        futures = tuple(snapshot.future for snapshot in self._snapshots.values())
        futures += tuple(self._retired)
    for future in futures:
        future.result()
    with self._lock:
        self._reap_locked()

pre_state

pre_state(index: int) -> tuple[Any, Any]

Restore the newest eligible snapshot and replay only its short suffix.

Source code in zkdv/jax/replay/checkpoint.py
def pre_state(self, index: int) -> tuple[Any, Any]:
    """Restore the newest eligible snapshot and replay only its short suffix."""
    with self._lock:
        eligible = [current for current in self._snapshots if current <= index]
        if not eligible:
            raise RuntimeError("ZKDV has no replay snapshot for sampled update")
        start = max(eligible)
        snapshot = self._snapshots[start]
        batches = []
        for current in range(start, index):
            if current not in self._batches:
                raise RuntimeError(f"ZKDV checkpoint is missing batch {current}")
            batches.append(self._batches[current])

    # This is intentionally the first wait on a retained snapshot, and
    # this method is called only after Rust selects an update for checking.
    host_params, host_optimizer = snapshot.future.result()
    with jax.profiler.TraceAnnotation("zkdv.snapshot.restore"):
        params, optimizer = self._restore(host_params, host_optimizer)
    for packed_batch in batches:
        batch = jax.device_put(
            jax.device_get(packed_batch),
            self._batch_sharding,
        )
        params, optimizer = self._advance(params, optimizer, batch)
    jax.block_until_ready((params, optimizer))
    return params, optimizer

record

record(index: int, batch: Any) -> None

Retain one independently owned host batch for a possible suffix.

Source code in zkdv/jax/replay/checkpoint.py
def record(self, index: int, batch: Any) -> None:
    """Retain one independently owned host batch for a possible suffix."""
    with self._lock:
        if index in self._batches:
            raise RuntimeError(f"ZKDV checkpoint batch {index} is out of order")
        self._batches[index] = batch
        self._prune_locked()

release

release(index: int) -> None

Release one transaction; never fence a discarded host transfer.

Source code in zkdv/jax/replay/checkpoint.py
def release(self, index: int) -> None:
    """Release one transaction; never fence a discarded host transfer."""
    with self._lock:
        self._active.discard(index)
        self._prune_locked()

ReplaySchema dataclass

ReplaySchema(tree: PyTreeDef, template: tuple[Any | None, ...], sources: tuple[int, ...], specs: tuple[ArraySpec, ...], params: InputBinding, opt_state: InputBinding, batch: InputBinding | None, arguments: tuple[int, ...], leaf_arguments: tuple[int, ...], static_args: tuple[tuple[int, Any], ...])

Rebuild one training call from challenged state and committed inputs.

pack

pack(values: tuple[Any, ...], host_values: tuple[Any, ...] | None = None) -> ndarray

Encode dynamic call inputs without adding work to the JIT executable.

Source code in zkdv/jax/replay/schema.py
def pack(
    self, values: tuple[Any, ...], host_values: tuple[Any, ...] | None = None
) -> np.ndarray:
    """Encode dynamic call inputs without adding work to the JIT executable."""

    leaves = jax.tree.leaves(values)
    if host_values is None:
        host_values = tuple(leaves[position] for position in self.sources)
    host = jax.device_get(host_values)
    encoded = []
    for value, spec in zip(host, self.specs, strict=True):
        if spec.key:
            value = jax.device_get(jax.random.key_data(value))
        array = np.ascontiguousarray(np.asarray(value, dtype=spec.dtype))
        encoded.append(array.view(np.uint8).reshape(-1))
    raw = np.concatenate(encoded)
    padding = (-raw.size) % np.dtype(np.uint32).itemsize
    if padding:
        raw = np.pad(raw, (0, padding))
    return np.ascontiguousarray(raw.view(np.uint32).reshape(1, -1))

program

program(stage: Callable[..., tuple[Any, Any]]) -> Callable[..., Any]

Expose the full transition used by checkpoint replay and checks.

Source code in zkdv/jax/replay/schema.py
def program(self, stage: Callable[..., tuple[Any, Any]]) -> Callable[..., Any]:
    """Expose the full transition used by checkpoint replay and checks."""

    def attested(params, opt_state, batch):
        values = self.rebuild(params, opt_state, batch)
        return stage(*values)

    return attested

training_stage

training_stage(stage: Callable[..., tuple[Any, ...]]) -> Callable[..., tuple[Any, ...]]

Present parameter buffers separately so only they may be donated.

Source code in zkdv/jax/replay/schema.py
def training_stage(
    self, stage: Callable[..., tuple[Any, ...]]
) -> Callable[..., tuple[Any, ...]]:
    """Present parameter buffers separately so only they may be donated."""

    def lowered(params, opt_state, *inputs):
        return stage(*self.rebuild_from_inputs(params, opt_state, inputs))

    return lowered

checkpoint

Asynchronous JAX host snapshots for bounded checked replay.

At the beginning of a periodic update, a background host thread dispatches a JAX copy of the exact parameter and optimizer pre-state into compiler-addressable host memory (pinned where the backend provides it). The training executable is then dispatched immediately on the calling thread. JAX orders the copy before donated input storage can be reused, while the separate dispatcher prevents the host-valued result from stalling training dispatch. Unchecked flow retains the future without reading its destination.

The default interval of twelve copies the complete pre-state periodically and journals the intervening batches, so a check replays at most eleven earlier transitions before executing the challenged transition. An interval of one copies every step and executes exactly the challenged transition.

Snapshots taken from the training path are evidence, not trusted state. A checked replay still recomputes and validates the challenged update's pre/post parameter and optimizer commitments before it can be accepted.

ReplayCheckpoint

ReplayCheckpoint(program: Callable[[Any, Any, Array], tuple[Any, Any]], params: Any, optimizer: Any, batch: Array, *, snapshot_interval: int = 12, snapshot_pool_size: int = 2)

Bound replay with asynchronous, donation-safe host snapshots.

Source code in zkdv/jax/replay/checkpoint.py
def __init__(
    self,
    program: Callable[[Any, Any, jax.Array], tuple[Any, Any]],
    params: Any,
    optimizer: Any,
    batch: jax.Array,
    *,
    snapshot_interval: int = 12,
    snapshot_pool_size: int = 2,
) -> None:
    if snapshot_interval < 1:
        raise ValueError("ZKDV replay_snapshot_interval must be positive")
    self.snapshot_interval = snapshot_interval
    self._param_shardings = _device_shardings(params)
    self._optimizer_shardings = _device_shardings(optimizer)
    self._host_param_shardings = _host_shardings(params)
    self._host_optimizer_shardings = _host_shardings(optimizer)
    self._batch_sharding = batch.sharding

    allocate = jax.jit(
        lambda current_params, current_optimizer: (
            current_params,
            current_optimizer,
        ),
        in_shardings=(self._param_shardings, self._optimizer_shardings),
        out_shardings=(
            self._host_param_shardings,
            self._host_optimizer_shardings,
        ),
    ).lower(params, optimizer).compile()
    # A due copy donates one complete host destination and returns its
    # snapshot into that allocation. Prime enough trees for the hard
    # in-flight bound so production never allocates a training-sized tree.
    with jax.profiler.TraceAnnotation("zkdv.snapshot.pool_warmup"):
        warm = [
            allocate(params, optimizer)
            for _ in range(max(2, snapshot_pool_size))
        ]
        jax.block_until_ready(warm)
    restore_params, restore_optimizer = warm[0]
    self._restore = jax.jit(
        lambda host_params, host_optimizer: (host_params, host_optimizer),
        in_shardings=(
            self._host_param_shardings,
            self._host_optimizer_shardings,
        ),
        out_shardings=(self._param_shardings, self._optimizer_shardings),
    ).lower(restore_params, restore_optimizer).compile()
    self._free = [
        _Destination(host_params, host_optimizer)
        for host_params, host_optimizer in warm
    ]
    destination = self._free[0]
    self._copy = jax.jit(
        lambda current_params, current_optimizer, host_params, host_optimizer: (
            current_params,
            current_optimizer,
        ),
        in_shardings=(
            self._param_shardings,
            self._optimizer_shardings,
            self._host_param_shardings,
            self._host_optimizer_shardings,
        ),
        out_shardings=(
            self._host_param_shardings,
            self._host_optimizer_shardings,
        ),
        donate_argnums=(2, 3),
    ).lower(
        params,
        optimizer,
        destination.params,
        destination.optimizer,
    ).compile()
    self._executor = ThreadPoolExecutor(
        max_workers=1, thread_name_prefix="zkdv-replay-copy"
    )

    def advance(current_params, current_optimizer, packed_batch):
        deltas, optimizer_after = program(
            current_params, current_optimizer, packed_batch
        )
        params_after = jax.tree.map(jnp.add, current_params, deltas)
        return params_after, optimizer_after

    self._advance = jax.jit(
        advance,
        in_shardings=(
            self._param_shardings,
            self._optimizer_shardings,
            self._batch_sharding,
        ),
        out_shardings=(self._param_shardings, self._optimizer_shardings),
        donate_argnums=(0, 1),
    ).lower(params, optimizer, batch).compile()
    self._snapshots: dict[int, _Snapshot] = {}
    self._retired: list[Future[tuple[Any, Any]]] = []
    self._active: set[int] = set()
    self._batches: dict[int, Any] = {}
    self._lock = Lock()

host_optimizer_shardings property

host_optimizer_shardings: Any

Compiler-addressable host placement for optimizer snapshots.

host_param_shardings property

host_param_shardings: Any

Compiler-addressable host placement for parameter snapshots.

accept

accept(index: int, params: Any, optimizer: Any) -> None

Record acceptance without copying the replay result back to host.

Source code in zkdv/jax/replay/checkpoint.py
def accept(self, index: int, params: Any, optimizer: Any) -> None:
    """Record acceptance without copying the replay result back to host."""
    del params, optimizer
    with self._lock:
        if index not in self._active:
            raise RuntimeError("ZKDV accepted an inactive replay snapshot")

activate

activate(index: int, params: Any, optimizer: Any) -> None

Open a transaction and asynchronously copy its pre-state when due.

Source code in zkdv/jax/replay/checkpoint.py
def activate(self, index: int, params: Any, optimizer: Any) -> None:
    """Open a transaction and asynchronously copy its pre-state when due."""
    wait_for = None
    while True:
        with self._lock:
            self._reap_locked()
            if index in self._active:
                raise RuntimeError(
                    f"ZKDV replay snapshot {index} is already active"
                )
            due = index not in self._snapshots and (
                not self._snapshots or index % self.snapshot_interval == 0
            )
            if not due or self._free:
                self._active.add(index)
                break
            wait_for = self._retired[0] if self._retired else None
        if wait_for is None:
            raise RuntimeError("ZKDV replay snapshot pool is exhausted")
        # If copies cannot keep up with the selected interval, expose real
        # backpressure rather than silently violating the replay bound.
        wait_for.result()

    if not due:
        return
    with self._lock:
        destination = self._free.pop()
        dispatching = Event()
        future = self._executor.submit(
            self._copy_to_host,
            params,
            optimizer,
            destination,
            dispatching,
        )
        self._snapshots[index] = _Snapshot(index, future)
        self._prune_locked()
    # Wait only until the copy worker owns the GIL immediately before the
    # compiled call. Its input handler therefore registers source usage
    # before this thread can enter the next donated JAX call. This is not a
    # transfer wait: the host-valued result remains on ``future``.
    dispatching.wait()

close

close() -> None

Release the dispatcher after the owning pipeline has joined.

Source code in zkdv/jax/replay/checkpoint.py
def close(self) -> None:
    """Release the dispatcher after the owning pipeline has joined."""
    self.join()
    self._executor.shutdown(wait=True, cancel_futures=False)

join

join() -> None

Drain copies at an explicit pipeline synchronization boundary.

Source code in zkdv/jax/replay/checkpoint.py
def join(self) -> None:
    """Drain copies at an explicit pipeline synchronization boundary."""
    with self._lock:
        futures = tuple(snapshot.future for snapshot in self._snapshots.values())
        futures += tuple(self._retired)
    for future in futures:
        future.result()
    with self._lock:
        self._reap_locked()

pre_state

pre_state(index: int) -> tuple[Any, Any]

Restore the newest eligible snapshot and replay only its short suffix.

Source code in zkdv/jax/replay/checkpoint.py
def pre_state(self, index: int) -> tuple[Any, Any]:
    """Restore the newest eligible snapshot and replay only its short suffix."""
    with self._lock:
        eligible = [current for current in self._snapshots if current <= index]
        if not eligible:
            raise RuntimeError("ZKDV has no replay snapshot for sampled update")
        start = max(eligible)
        snapshot = self._snapshots[start]
        batches = []
        for current in range(start, index):
            if current not in self._batches:
                raise RuntimeError(f"ZKDV checkpoint is missing batch {current}")
            batches.append(self._batches[current])

    # This is intentionally the first wait on a retained snapshot, and
    # this method is called only after Rust selects an update for checking.
    host_params, host_optimizer = snapshot.future.result()
    with jax.profiler.TraceAnnotation("zkdv.snapshot.restore"):
        params, optimizer = self._restore(host_params, host_optimizer)
    for packed_batch in batches:
        batch = jax.device_put(
            jax.device_get(packed_batch),
            self._batch_sharding,
        )
        params, optimizer = self._advance(params, optimizer, batch)
    jax.block_until_ready((params, optimizer))
    return params, optimizer

record

record(index: int, batch: Any) -> None

Retain one independently owned host batch for a possible suffix.

Source code in zkdv/jax/replay/checkpoint.py
def record(self, index: int, batch: Any) -> None:
    """Retain one independently owned host batch for a possible suffix."""
    with self._lock:
        if index in self._batches:
            raise RuntimeError(f"ZKDV checkpoint batch {index} is out of order")
        self._batches[index] = batch
        self._prune_locked()

release

release(index: int) -> None

Release one transaction; never fence a discarded host transfer.

Source code in zkdv/jax/replay/checkpoint.py
def release(self, index: int) -> None:
    """Release one transaction; never fence a discarded host transfer."""
    with self._lock:
        self._active.discard(index)
        self._prune_locked()

schema

Canonical JAX evidence and replay of a staged training function.

ArraySpec dataclass

ArraySpec(shape: tuple[int, ...], dtype: Any, byte_count: int, key: bool)

One array's lossless representation in a packed uint32 batch.

ReplaySchema dataclass

ReplaySchema(tree: PyTreeDef, template: tuple[Any | None, ...], sources: tuple[int, ...], specs: tuple[ArraySpec, ...], params: InputBinding, opt_state: InputBinding, batch: InputBinding | None, arguments: tuple[int, ...], leaf_arguments: tuple[int, ...], static_args: tuple[tuple[int, Any], ...])

Rebuild one training call from challenged state and committed inputs.

pack

pack(values: tuple[Any, ...], host_values: tuple[Any, ...] | None = None) -> ndarray

Encode dynamic call inputs without adding work to the JIT executable.

Source code in zkdv/jax/replay/schema.py
def pack(
    self, values: tuple[Any, ...], host_values: tuple[Any, ...] | None = None
) -> np.ndarray:
    """Encode dynamic call inputs without adding work to the JIT executable."""

    leaves = jax.tree.leaves(values)
    if host_values is None:
        host_values = tuple(leaves[position] for position in self.sources)
    host = jax.device_get(host_values)
    encoded = []
    for value, spec in zip(host, self.specs, strict=True):
        if spec.key:
            value = jax.device_get(jax.random.key_data(value))
        array = np.ascontiguousarray(np.asarray(value, dtype=spec.dtype))
        encoded.append(array.view(np.uint8).reshape(-1))
    raw = np.concatenate(encoded)
    padding = (-raw.size) % np.dtype(np.uint32).itemsize
    if padding:
        raw = np.pad(raw, (0, padding))
    return np.ascontiguousarray(raw.view(np.uint32).reshape(1, -1))

program

program(stage: Callable[..., tuple[Any, Any]]) -> Callable[..., Any]

Expose the full transition used by checkpoint replay and checks.

Source code in zkdv/jax/replay/schema.py
def program(self, stage: Callable[..., tuple[Any, Any]]) -> Callable[..., Any]:
    """Expose the full transition used by checkpoint replay and checks."""

    def attested(params, opt_state, batch):
        values = self.rebuild(params, opt_state, batch)
        return stage(*values)

    return attested

training_stage

training_stage(stage: Callable[..., tuple[Any, ...]]) -> Callable[..., tuple[Any, ...]]

Present parameter buffers separately so only they may be donated.

Source code in zkdv/jax/replay/schema.py
def training_stage(
    self, stage: Callable[..., tuple[Any, ...]]
) -> Callable[..., tuple[Any, ...]]:
    """Present parameter buffers separately so only they may be donated."""

    def lowered(params, opt_state, *inputs):
        return stage(*self.rebuild_from_inputs(params, opt_state, inputs))

    return lowered

transaction

Symbolic JAX transaction markers traced by :meth:ZKDV.jit.

Transaction

Transaction(*, batch: Any, params: Any, opt_state: Any, overlap: Policy = UNCHECKED, _proof: Any | None = None)

Declare the pre- and post-update values inside a ZKDV.jit function.

Source code in zkdv/jax/transaction.py
def __init__(
    self,
    *,
    batch: Any,
    params: Any,
    opt_state: Any,
    overlap: overlap_policy.Policy = overlap_policy.UNCHECKED,
    _proof: Any | None = None,
):
    trace = _CURRENT_TRACE.get()
    if trace is None:
        raise RuntimeError(
            "zkdv.jax.Transaction must be created inside zkdv.jax.ZKDV.jit"
        )
    self._trace = trace
    self._attested = trace._attested
    self._updated = False
    trace.start(
        batch=batch,
        params=params,
        opt_state=opt_state,
        overlap_policy=overlap,
        proof=_proof,
    )

TransactionSchema dataclass

TransactionSchema(batch: InputBinding | _AutomaticBatch, params: InputBinding, opt_state: InputBinding, overlap: InputBinding | Policy, proof: Any | None)

Locations of the pre-update values in an arbitrary function ABI.

TransactionTrace

TransactionTrace(*, attested: bool = False)

Collect one transaction while JAX traces a decorated function.

Source code in zkdv/jax/transaction.py
def __init__(self, *, attested: bool = False) -> None:
    self._batch = None
    self._params = None
    self._opt_state = None
    self._overlap = None
    self._deltas = None
    self._params_after = None
    self._opt_state_after = None
    self._proof = None
    self._attested = attested
    self._token: Token | None = None

is_tracing

is_tracing() -> bool

Return whether the current call is being staged by :meth:ZKDV.jit.

Source code in zkdv/jax/transaction.py
def is_tracing() -> bool:
    """Return whether the current call is being staged by :meth:`ZKDV.jit`."""

    return _CURRENT_TRACE.get() is not None

trees

JAX pytree operations used by shared ZKDV mechanics.