Twobombs's picture
Duplicate from simulacra-research/HamiltonZero
71a70fb
Raw
History Blame Contribute Delete
9.23 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,
) -> REState:
replica_steps = config.mcmc.burn_in_replica_steps
step_mcmc = eqx.filter_jit(
functools.partial(
run_batched,
n_steps=replica_steps,
walker_chunk_size=config.mcmc.walker_chunk_size,
)
)
adapt = eqx.filter_jit(functools.partial(_adapt, config=config))
for iteration in range(config.mcmc.burn_in):
state = step_mcmc(
model,
context,
state,
)
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:
replicated = NamedSharding(mesh, P())
batched = NamedSharding(mesh, P("batch"))
shardings = 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,
)
return jax.device_put(state, shardings)
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,
)
reused = False
if config.mcmc.reuse_mcmc is not None:
state = load_mcmc(config.mcmc.reuse_mcmc, state)
reused = True
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())
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,
)
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,
)
if not reused:
state = _burn_in(model, context, state, config)
step_mcmc = eqx.filter_jit(
functools.partial(
run_batched,
n_steps=config.mcmc.steps,
walker_chunk_size=config.mcmc.walker_chunk_size,
)
)
adapt = eqx.filter_jit(functools.partial(_adapt, config=config))
local_energy = eqx.filter_jit(
functools.partial(
vmc_energy_custom_lap_finetune,
chunk_size=config.energy.chunk_size,
)
)
run_started = time.perf_counter()
last_metric = None
for step in range(config.steps):
step_started = time.perf_counter()
state = step_mcmc(
model,
context,
state,
)
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 = process_finetune_targets(
total[None],
context_batch.s_norm,
mad_width=config.kfac.mad_clip_width,
)
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",
]