| |
| |
|
|
| 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", |
| ] |
|
|