Aleksei Ustimenko
Restore production sharding and warm-start support
02f6649
Raw
History Blame Contribute Delete
12.6 kB
# Copyright (c) 2026 Simulacra Research Inc.
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import functools
import time
from dataclasses import dataclass
from typing import Callable
import equinox as eqx
import jax
import jax.numpy as jnp
import numpy as np
from jax.sharding import Mesh, NamedSharding, PartitionSpec as P
from hamiltonzero.checkpoint import load_mcmc, load_model, save_model
from hamiltonzero.compiled.model import (
CompiledFinetuneWaveFunction,
compile_finetune_model,
)
from hamiltonzero.config import FineTuneConfig
from hamiltonzero.data import build_context_and_energy, load_system
from hamiltonzero.energy import vmc_energy_custom_lap_finetune
from hamiltonzero.energy.frame import compile_energy_frame
from hamiltonzero.mcmc import (
REState,
adapt_batched,
cold_samples,
init_batched_state,
run_batched,
)
from hamiltonzero.model import build_model
from hamiltonzero.optim import (
KFACBundle,
apply_finetune_kfac_step,
init_finetune_kfac_state,
learning_rate,
process_finetune_targets,
)
from hamiltonzero.router import (
batch_context,
route_context,
route_state,
select_frozen_route,
strip_router,
)
@dataclass(frozen=True, slots=True)
class FineTuneMetric:
step: int
energy: float
energy_std: float
step_walltime: float
walltime: float
@dataclass(frozen=True, slots=True)
class FineTuneResult:
model: CompiledFinetuneWaveFunction
route_perm: jax.Array
mcmc_state: REState
kfac: KFACBundle
last_metric: FineTuneMetric | None
def _adapt(state: REState, config: FineTuneConfig) -> REState:
return adapt_batched(
state,
beta_history_weight=config.mcmc.beta_history_weight,
sigma_target=config.mcmc.langevin_target_acceptance,
sigma_scale=config.mcmc.sigma_scale,
haar_target=config.mcmc.haar_target_acceptance,
)
def _burn_in(
model: CompiledFinetuneWaveFunction,
context,
state: REState,
config: FineTuneConfig,
step_mcmc,
adapt,
) -> REState:
for iteration in range(config.mcmc.burn_in):
state = step_mcmc(
state,
model,
context,
)
if iteration > 0 and iteration % config.mcmc.adapt_every == 0:
state = adapt(state)
return state
def _replicate(value, sharding: NamedSharding):
return jax.device_put(
value,
jax.tree_util.tree_map(lambda _leaf: sharding, value),
)
def _place_state(state: REState, mesh: Mesh) -> REState:
return jax.device_put(state, _state_sharding(mesh))
def _state_specs() -> REState:
walkers = P("batch")
replicated = P()
return REState(
q=walkers,
log_p=walkers,
grad_log_p=walkers,
beta=replicated,
sigma=replicated,
step=replicated,
key=walkers,
n_local_accept=walkers,
n_local=walkers,
n_swap_accept=walkers,
n_swap=walkers,
mask=replicated,
m=replicated,
n_haar_accept=walkers,
n_haar=walkers,
)
def _state_sharding(mesh: Mesh) -> REState:
replicated = NamedSharding(mesh, P())
batched = NamedSharding(mesh, P("batch"))
return REState(
q=batched,
log_p=batched,
grad_log_p=batched,
beta=replicated,
sigma=replicated,
step=replicated,
key=batched,
n_local_accept=batched,
n_local=batched,
n_swap_accept=batched,
n_swap=batched,
mask=replicated,
m=replicated,
n_haar_accept=batched,
n_haar=batched,
)
def _build_mcmc_entry(
mesh: Mesh,
state_sharding,
model_sharding,
context_sharding,
*,
replica_steps: int,
walker_chunk_size: int | None,
):
specs = _state_specs()
def local_step(state, model, context):
out = run_batched(
model,
context,
state,
n_steps=int(replica_steps),
walker_chunk_size=walker_chunk_size,
)
local_count = jnp.asarray(out.q.shape[0], dtype=jnp.int32)
global_count = jax.lax.psum(local_count, "batch")
guard = global_count.astype(out.q.dtype) * jnp.asarray(0.0, out.q.dtype)
return eqx.tree_at(lambda value: value.q, out, out.q + guard)
mapped = jax.shard_map(
local_step,
mesh=mesh,
in_specs=(specs, P(), P()),
out_specs=specs,
check_vma=False,
)
return jax.jit(
mapped,
in_shardings=(state_sharding, model_sharding, context_sharding),
out_shardings=state_sharding,
donate_argnums=(0,),
)
def _build_energy_entry(
mesh: Mesh,
model_sharding,
frame_sharding,
*,
chunk_size: int,
):
q_spec = P("batch", None, None)
output_spec = P("batch")
def local_energy(model, frame, q):
outputs = vmc_energy_custom_lap_finetune(
model,
frame,
q,
chunk_size=int(chunk_size),
)
local_count = jnp.asarray(q.shape[0], dtype=jnp.int32)
global_count = jax.lax.psum(local_count, "batch")
guard = global_count.astype(jnp.float32) * jnp.asarray(0.0, jnp.float32)
return tuple(value + guard.astype(value.dtype) for value in outputs)
mapped = jax.shard_map(
local_energy,
mesh=mesh,
in_specs=(P(), P(), q_spec),
out_specs=(output_spec,) * 4,
check_vma=False,
)
q_sharding = NamedSharding(mesh, q_spec)
output_sharding = NamedSharding(mesh, output_spec)
return jax.jit(
mapped,
in_shardings=(model_sharding, frame_sharding, q_sharding),
out_shardings=(output_sharding,) * 4,
)
def _metric(
step: int,
total,
step_started: float,
run_started: float,
) -> FineTuneMetric:
jax.block_until_ready(total)
return FineTuneMetric(
step=step,
energy=float(jax.device_get(jnp.mean(total.real))),
energy_std=float(jax.device_get(jnp.std(total.real))),
step_walltime=time.perf_counter() - step_started,
walltime=time.perf_counter() - run_started,
)
def run_finetune(
config: FineTuneConfig,
*,
metric_sink: Callable[[FineTuneMetric], None] | None = None,
) -> FineTuneResult:
key = jax.random.PRNGKey(config.seed)
key_model, key_mcmc = jax.random.split(key)
system = load_system(config.system)
context, energy_inputs = build_context_and_energy(
system,
n_max=None,
mu=config.energy.mu,
eps=config.energy.eps,
)
template = build_model(
config.model,
key_model,
n_max=int(context.mask.shape[-1]),
)
eager_model = load_model(config.checkpoint, template)
state = init_batched_state(
key_mcmc,
context,
batch_size=config.mcmc.batch_size,
n_replicas=config.mcmc.replicas,
initial_m=config.mcmc.initial_haar_sites,
initial_sigma=config.mcmc.initial_sigma,
)
if config.mcmc.reuse_mcmc is not None:
state = load_mcmc(config.mcmc.reuse_mcmc, state)
key, _route_key = jax.random.split(key)
freeze_route = eqx.filter_jit(
functools.partial(
select_frozen_route,
tau=config.route_temperature,
)
)
route_perm = freeze_route(
eager_model,
context,
)
energy_frame = compile_energy_frame(
energy_inputs,
context.mask,
context.bmask,
route_perm,
)
context = route_context(context, route_perm)
state = route_state(state, route_perm)
eager_model = strip_router(eager_model)
key, key_expand = jax.random.split(key)
compile_model = eqx.filter_jit(
functools.partial(
compile_finetune_model,
leaf_rank=config.leaf_rank,
merge_rank=config.merge_rank,
)
)
model = compile_model(
eager_model,
context,
physical_perm=route_perm,
key=key_expand,
)
del eager_model, template, _route_key
devices = tuple(jax.devices())
if config.mcmc.batch_size % len(devices):
raise ValueError(
f"batch_size={config.mcmc.batch_size} must be divisible by "
f"the {len(devices)} visible devices"
)
mesh = Mesh(np.asarray(devices, dtype=object), ("batch",))
replicated = NamedSharding(mesh, P())
state_sharding = _state_sharding(mesh)
model_sharding = jax.tree_util.tree_map(lambda _value: replicated, model)
context_sharding = jax.tree_util.tree_map(lambda _value: replicated, context)
frame_sharding = jax.tree_util.tree_map(lambda _value: replicated, energy_frame)
model = _replicate(model, replicated)
context = _replicate(context, replicated)
energy_frame = _replicate(energy_frame, replicated)
state = _place_state(state, mesh)
context_batch = _replicate(batch_context(context), replicated)
q_cold = cold_samples(state)
kfac_data = NamedSharding(mesh, P(None, "batch"))
q_kfac = jax.device_put(q_cold[None], kfac_data)
energy_seed = jax.device_put(
jnp.zeros((1, config.mcmc.batch_size), dtype=jnp.complex64),
kfac_data,
)
mcmc_entries = {}
def mcmc_entry(replica_steps: int):
entry_key = (int(replica_steps), config.mcmc.walker_chunk_size)
entry = mcmc_entries.get(entry_key)
if entry is None:
entry = _build_mcmc_entry(
mesh,
state_sharding,
model_sharding,
context_sharding,
replica_steps=entry_key[0],
walker_chunk_size=entry_key[1],
)
mcmc_entries[entry_key] = entry
return entry
adapt = jax.jit(
functools.partial(_adapt, config=config),
in_shardings=(state_sharding,),
out_shardings=state_sharding,
)
local_energy = _build_energy_entry(
mesh,
model_sharding,
frame_sharding,
chunk_size=config.energy.chunk_size,
)
target_entry = jax.jit(
functools.partial(
process_finetune_targets,
mad_width=config.kfac.mad_clip_width,
),
in_shardings=(kfac_data, replicated),
out_shardings=kfac_data,
)
kfac = init_finetune_kfac_state(
config.kfac,
model,
q_kfac,
energy_seed,
context_batch,
t=0.0,
key=jax.random.fold_in(key, 0xCAFE),
multi_device=mesh.size > 1,
)
state = _burn_in(
model,
context,
state,
config,
mcmc_entry(config.mcmc.burn_in_replica_steps),
adapt,
)
step_mcmc = mcmc_entry(config.mcmc.steps)
run_started = time.perf_counter()
last_metric = None
for step in range(config.steps):
step_started = time.perf_counter()
state = step_mcmc(
state,
model,
context,
)
if step > 0 and step % config.mcmc.adapt_every == 0:
state = adapt(state)
q_cold = cold_samples(state)
total, _exchange, _casimir, _field = local_energy(
model,
energy_frame,
q_cold,
)
target = target_entry(
total[None],
context_batch.s_norm,
)
key, key_kfac = jax.random.split(key)
model, kfac = apply_finetune_kfac_step(
kfac,
model,
jax.device_put(q_cold[None], kfac_data),
jax.device_put(target, kfac_data),
context_batch,
t=0.0,
key=key_kfac,
momentum=config.kfac.momentum,
learning_rate=learning_rate(config.kfac, step),
damping=config.kfac.damping,
)
jax.block_until_ready(model)
last_metric = _metric(step, total, step_started, run_started)
if metric_sink is not None:
metric_sink(last_metric)
save_model(
config.output,
model,
kind="compiled_finetune",
metadata={
"leaf_rank": int(config.leaf_rank),
"merge_rank": int(config.merge_rank),
"n_max": int(context.mask.shape[-1]),
},
)
return FineTuneResult(
model=model,
route_perm=route_perm,
mcmc_state=state,
kfac=kfac,
last_metric=last_metric,
)
__all__ = [
"FineTuneMetric",
"FineTuneResult",
"run_finetune",
]