| |
| |
|
|
| from __future__ import annotations |
|
|
| import time |
| from dataclasses import replace |
| from typing import Any, Callable |
|
|
| import jax |
| import jax.numpy as jnp |
| import numpy as np |
|
|
| from hamiltonzero.config import EvalConfig |
|
|
| from .backend import EvalBackend, MCMCPopulation |
| from .statistics import EnergyWindow, P01_TWO_SIDED, select_winner_per_physical |
| from .types import ContestCandidate, ContestResult, EvalMetric, EvalResult |
|
|
|
|
| def _permute_q_prefix(q, permutations): |
| index = jnp.broadcast_to(permutations[:, None, None, :, None], q.shape) |
| return jnp.take_along_axis(q, index, axis=3) |
|
|
|
|
| def _collapse_to_winner(q_virtual, permutations, winner_idx, *, P, K): |
| winner_idx = jnp.asarray(winner_idx, dtype=jnp.int32) |
| inverse = jnp.argsort(permutations, axis=-1) |
| q_canonical = _permute_q_prefix(q_virtual, inverse) |
| batch_per_candidate = q_canonical.shape[1] |
| tail = q_canonical.shape[2:] |
| q_physical = q_canonical.reshape((P, K, batch_per_candidate) + tail).reshape( |
| (P, K * batch_per_candidate) + tail |
| ) |
| permutations_pk = permutations.reshape((P, K, -1)) |
| route = jnp.take_along_axis( |
| permutations_pk, |
| winner_idx[:, None, None], |
| axis=1, |
| )[:, 0] |
| return _permute_q_prefix(q_physical, route), route |
|
|
|
|
| def _gather_winner_ladder(values, winner_idx, *, P, K): |
| winner_idx = jnp.asarray(winner_idx, dtype=jnp.int32) |
| values_pk = values.reshape((P, K) + values.shape[1:]) |
| index = winner_idx[(slice(None), None) + (None,) * (values_pk.ndim - 2)] |
| return jnp.take_along_axis(values_pk, index, axis=1)[:, 0] |
|
|
|
|
| def _as_batched_route(permutation): |
| value = jnp.asarray(permutation, dtype=jnp.int32) |
| if value.ndim == 1: |
| value = value[None, :] |
| if value.ndim != 2 or value.shape[0] != 1: |
| raise ValueError("single-system evaluation requires a route with shape [1, N]") |
| return value |
|
|
|
|
| def _compose_walker_route(old_inverse, route): |
| return jnp.take_along_axis( |
| jnp.asarray(old_inverse, dtype=jnp.int32), |
| route, |
| axis=-1, |
| ) |
|
|
|
|
| def _adapt(backend: EvalBackend, state: Any, config: EvalConfig): |
| return backend.adapt_mcmc(state, config.mcmc) |
|
|
|
|
| def _burn_in( |
| backend: EvalBackend, |
| state: Any, |
| model: Any, |
| context: Any, |
| config: EvalConfig, |
| *, |
| iterations: int, |
| replica_steps: int, |
| ): |
| for _ in range(int(iterations)): |
| state = backend.step_mcmc( |
| state, |
| model, |
| context, |
| replica_steps=int(replica_steps), |
| walker_chunk_size=int(config.mcmc.walker_chunk_size), |
| ) |
| backend.block_until_ready(backend.cold_walkers(state)) |
| state = _adapt(backend, state, config) |
| return state |
|
|
|
|
| def _measure( |
| backend: EvalBackend, |
| state: Any, |
| model: Any, |
| context: Any, |
| config: EvalConfig, |
| *, |
| started: float, |
| metric_sink: Callable[[EvalMetric], None] | None, |
| ): |
| window = EnergyWindow( |
| config.measurements, |
| systems=1, |
| batch_size=config.mcmc.batch_size, |
| ) |
| for step in range(config.measurements): |
| step_started = time.perf_counter() |
| state = backend.step_mcmc( |
| state, |
| model, |
| context, |
| replica_steps=int(config.mcmc.steps), |
| walker_chunk_size=int(config.mcmc.walker_chunk_size), |
| ) |
| q_cold = backend.cold_walkers(state) |
| total, exchange, _casimir, field = backend.custom_lap_energy( |
| model, |
| context, |
| q_cold, |
| config.energy, |
| ) |
| backend.block_until_ready(total) |
| window.push(total, exchange, field) |
| state = _adapt(backend, state, config) |
| if metric_sink is not None: |
| energy = np.asarray(total).real |
| metric_sink( |
| EvalMetric( |
| step=step, |
| energy=float(np.mean(energy)), |
| energy_std=float(np.std(energy)), |
| step_walltime=float(time.perf_counter() - step_started), |
| walltime=float(time.perf_counter() - started), |
| ) |
| ) |
| return state, window |
|
|
|
|
| def _ordinary( |
| backend: EvalBackend, |
| model: Any, |
| context: Any, |
| canonical, |
| mcmc_key, |
| config: EvalConfig, |
| ): |
| state = backend.initialize_mcmc(mcmc_key, model, context, config.mcmc) |
| candidates = backend.beam_candidates( |
| model, |
| canonical.context, |
| beam_width=int(config.contest_beam_width), |
| top_k=1, |
| temperature=float(config.route_temperature), |
| ) |
| permutations = jnp.asarray(candidates.permutations, dtype=jnp.int32) |
| if permutations.ndim != 3 or permutations.shape[:2] != (1, 1): |
| raise ValueError("ordinary eval router must return shape [1, 1, N]") |
| route = permutations[:, 0] |
| walker_route = _compose_walker_route(canonical.old_inverse, route) |
| routed_context = backend.route_context( |
| canonical.context, |
| route, |
| compact_custom_lap=False, |
| ) |
| state = backend.route_mcmc(state, walker_route) |
| wavefunction = backend.compile_single(model, routed_context) |
| backend.block_until_ready(wavefunction) |
| logp = float(np.asarray(candidates.log_probabilities)[0, 0]) |
| return wavefunction, routed_context, state, route, logp, None |
|
|
|
|
| def _compiled_finetune_ordinary( |
| backend: EvalBackend, |
| model: Any, |
| context: Any, |
| canonical, |
| embedded_route, |
| mcmc_key, |
| config: EvalConfig, |
| ): |
| state = backend.initialize_mcmc(mcmc_key, model, context, config.mcmc) |
| route = _as_batched_route(embedded_route) |
| if route.shape[-1] != canonical.context.mask.shape[-1]: |
| raise ValueError( |
| "compiled fine-tune route width does not match the evaluation system" |
| ) |
| walker_route = _compose_walker_route(canonical.old_inverse, route) |
| routed_context = backend.route_context( |
| canonical.context, |
| route, |
| compact_custom_lap=False, |
| ) |
| state = backend.route_mcmc(state, walker_route) |
| wavefunction = backend.compile_embedded(model) |
| backend.block_until_ready(wavefunction) |
| return wavefunction, routed_context, state, route, None, None |
|
|
|
|
| def _contest( |
| backend: EvalBackend, |
| model: Any, |
| canonical, |
| root_key, |
| config: EvalConfig, |
| ): |
| K = int(config.contest_candidates) |
| batch_per_candidate = int(config.mcmc.batch_size) // K |
| candidates = backend.beam_candidates( |
| model, |
| canonical.context, |
| beam_width=int(config.contest_beam_width), |
| top_k=K, |
| temperature=float(config.route_temperature), |
| ) |
| beam_permutations = jnp.asarray(candidates.permutations, dtype=jnp.int32) |
| if beam_permutations.ndim != 3 or beam_permutations.shape[:2] != (1, K): |
| raise ValueError(f"contest router must return shape [1, {K}, N]") |
| n_sites = int(beam_permutations.shape[-1]) |
| permutations = beam_permutations.reshape((K, n_sites)) |
| virtual_context = backend.virtual_context(canonical.context, permutations) |
| race_mcmc = replace(config.mcmc, batch_size=batch_per_candidate) |
| state = backend.initialize_mcmc( |
| jax.random.fold_in(root_key, 7411), |
| model, |
| virtual_context, |
| race_mcmc, |
| ) |
| wavefunctions = backend.compile_candidates( |
| model, |
| canonical.context, |
| permutations, |
| ) |
| backend.block_until_ready(wavefunctions) |
| for _ in range(int(config.contest_preburn)): |
| state = backend.step_mcmc( |
| state, |
| wavefunctions, |
| virtual_context, |
| replica_steps=int(config.mcmc.burn_in_replica_steps), |
| walker_chunk_size=int(config.mcmc.walker_chunk_size), |
| ) |
| backend.block_until_ready(backend.cold_walkers(state)) |
| state = backend.adapt_mcmc(state, race_mcmc) |
| race_window = EnergyWindow( |
| config.contest_measurements, |
| systems=K, |
| batch_size=batch_per_candidate, |
| ) |
| for _ in range(int(config.contest_measurements)): |
| state = backend.step_mcmc( |
| state, |
| wavefunctions, |
| virtual_context, |
| replica_steps=int(config.mcmc.steps), |
| walker_chunk_size=int(config.mcmc.walker_chunk_size), |
| ) |
| state = backend.adapt_mcmc(state, race_mcmc) |
| q_cold = backend.cold_walkers(state) |
| total, exchange, _casimir, field = backend.custom_lap_energy( |
| wavefunctions, |
| virtual_context, |
| q_cold, |
| config.energy, |
| ) |
| backend.block_until_ready(total) |
| race_window.push(total, exchange, field) |
| energies = np.asarray( |
| [[race_window.tail_mean("total", candidate) for candidate in range(K)]] |
| ) |
| tailstd = np.asarray( |
| [[race_window.tail_std("total", candidate) for candidate in range(K)]] |
| ) |
| beam_logp = np.asarray(candidates.log_probabilities, dtype=float) |
| winners, ties, reasons, _bands, standard_errors = select_winner_per_physical( |
| energies, |
| tailstd, |
| beam_logp, |
| batch_per_candidate, |
| z=P01_TWO_SIDED, |
| ucb_z=float(config.contest_se_multiplier), |
| ) |
| winner = int(winners[0]) |
| wavefunction = backend.select_candidate(wavefunctions, winner) |
| population = backend.mcmc_population(state) |
| q_final, route = _collapse_to_winner( |
| population.q, |
| permutations, |
| winners, |
| P=1, |
| K=K, |
| ) |
| sigma = _gather_winner_ladder(population.sigma, winners, P=1, K=K) |
| beta = _gather_winner_ladder(population.beta, winners, P=1, K=K) |
| routed_context = backend.route_context( |
| canonical.context, |
| route, |
| compact_custom_lap=False, |
| ) |
| final_state = backend.initialize_mcmc( |
| jax.random.fold_in(root_key, 7919), |
| wavefunction, |
| routed_context, |
| config.mcmc, |
| ) |
| final_state = backend.replace_mcmc_population( |
| final_state, |
| MCMCPopulation(q=q_final, sigma=sigma, beta=beta), |
| ) |
| backend.block_until_ready(backend.cold_walkers(final_state)) |
| contest_candidates = tuple( |
| ContestCandidate( |
| index=index, |
| route_log_probability=float(beam_logp[0, index]), |
| energy=float(energies[0, index]), |
| standard_error=float(standard_errors[0, index]), |
| walker_tail_std=float(tailstd[0, index]), |
| in_tie_set=bool(ties[0, index]), |
| ) |
| for index in range(K) |
| ) |
| contest = ContestResult( |
| winner=winner, |
| reason=reasons[0], |
| candidates=contest_candidates, |
| ) |
| backend.release_context(virtual_context) |
| return ( |
| wavefunction, |
| routed_context, |
| final_state, |
| route, |
| float(beam_logp[0, winner]), |
| contest, |
| ) |
|
|
|
|
| def _large_n( |
| backend: EvalBackend, |
| model: Any, |
| context: Any, |
| canonical, |
| mcmc_key, |
| config: EvalConfig, |
| ): |
| state = backend.initialize_mcmc(mcmc_key, model, context, config.mcmc) |
| compiled = backend.compile_large_n( |
| model, |
| canonical.context, |
| sequence_shards=int(config.large_n_sequence_shards), |
| pair_tile_size=int(config.large_n_pair_tile_size), |
| temperature=float(config.route_temperature), |
| ) |
| route = _as_batched_route(compiled.permutation) |
| walker_route = _compose_walker_route(canonical.old_inverse, route) |
| routed_context = backend.route_context( |
| canonical.context, |
| route, |
| compact_custom_lap=True, |
| ) |
| state = backend.route_mcmc(state, walker_route) |
| backend.block_until_ready(compiled.wavefunction) |
| logp = float(np.asarray(compiled.log_probability)) |
| return compiled.wavefunction, routed_context, state, route, logp, None |
|
|
|
|
| def _validate(config: EvalConfig) -> None: |
| if config.contest and config.large_n: |
| raise ValueError("contest and large_n are mutually exclusive") |
| if int(config.measurements) < 1: |
| raise ValueError("measurements must be positive") |
| if int(config.mcmc.batch_size) < 1: |
| raise ValueError("MCMC batch size must be positive") |
| if int(config.mcmc.replicas) < 2: |
| raise ValueError("MCMC requires at least two replicas") |
| if int(config.mcmc.steps) < 1: |
| raise ValueError("MCMC replica steps must be positive") |
| if int(config.mcmc.burn_in_replica_steps) < 1: |
| raise ValueError("burn-in replica steps must be positive") |
| if int(config.mcmc.walker_chunk_size) < 1: |
| raise ValueError("walker chunk size must be positive") |
| if config.contest: |
| K = int(config.contest_candidates) |
| W = int(config.contest_beam_width) |
| if K < 2 or W < K: |
| raise ValueError("contest requires beam_width >= candidates >= 2") |
| if int(config.mcmc.batch_size) % K: |
| raise ValueError("MCMC batch size must be divisible by candidates") |
| if int(config.mcmc.batch_size) // K < 32: |
| raise ValueError("contest requires at least 32 walkers per candidate") |
| if int(config.contest_preburn) < 0: |
| raise ValueError("contest preburn must be non-negative") |
| if int(config.contest_measurements) < 1: |
| raise ValueError("contest measurements must be positive") |
| if int(config.large_n_sequence_shards) < 0: |
| raise ValueError("large-N sequence shards must be non-negative") |
| if int(config.large_n_pair_tile_size) < 1: |
| raise ValueError("large-N pair tile size must be positive") |
|
|
|
|
| def evaluate( |
| config: EvalConfig, |
| backend: EvalBackend, |
| *, |
| metric_sink: Callable[[EvalMetric], None] | None = None, |
| ) -> EvalResult: |
| _validate(config) |
| started = time.perf_counter() |
| root_key = jax.random.PRNGKey(int(config.seed)) |
| model_key, mcmc_key = jax.random.split(root_key) |
| context = backend.load_system(config.system, config.energy) |
| model = backend.load_model( |
| config.checkpoint, |
| config.model, |
| model_key, |
| context, |
| contextualizer_attention=config.contextualizer_attention, |
| ) |
| canonical = backend.canonicalize_context(context) |
| embedded_route = backend.embedded_route(model) |
| if embedded_route is not None and (config.contest or config.large_n): |
| raise ValueError( |
| "compiled fine-tune checkpoints support ordinary eval only; " |
| "contest and large_n require a router checkpoint" |
| ) |
| if embedded_route is not None: |
| prepared = _compiled_finetune_ordinary( |
| backend, |
| model, |
| context, |
| canonical, |
| embedded_route, |
| mcmc_key, |
| config, |
| ) |
| path = "ordinary" |
| elif config.contest: |
| prepared = _contest( |
| backend, |
| model, |
| canonical, |
| root_key, |
| config, |
| ) |
| path = "contest" |
| elif config.large_n: |
| prepared = _large_n( |
| backend, |
| model, |
| context, |
| canonical, |
| mcmc_key, |
| config, |
| ) |
| path = "large_n" |
| else: |
| prepared = _ordinary( |
| backend, |
| model, |
| context, |
| canonical, |
| mcmc_key, |
| config, |
| ) |
| path = "ordinary" |
| wavefunction, routed_context, state, route, route_logp, contest = prepared |
| del model, context, canonical, embedded_route, prepared |
| wavefunction, routed_context, state = backend.prepare_singular( |
| wavefunction, |
| routed_context, |
| state, |
| ) |
| state = _burn_in( |
| backend, |
| state, |
| wavefunction, |
| routed_context, |
| config, |
| iterations=int(config.mcmc.burn_in), |
| replica_steps=int(config.mcmc.burn_in_replica_steps), |
| ) |
| _state, window = _measure( |
| backend, |
| state, |
| wavefunction, |
| routed_context, |
| config, |
| started=started, |
| metric_sink=metric_sink, |
| ) |
| route_host = np.asarray(route, dtype=np.int32) |
| if route_host.shape[0] != 1: |
| raise ValueError("single-system eval produced more than one route") |
| return EvalResult( |
| path=path, |
| route=tuple(int(value) for value in route_host[0]), |
| route_log_probability=(None if route_logp is None else float(route_logp)), |
| measurements=int(window.count), |
| walltime_seconds=float(time.perf_counter() - started), |
| energy=window.metrics("total"), |
| channels={ |
| "exchange": window.metrics("exchange"), |
| "field": window.metrics("field"), |
| }, |
| contest=contest, |
| ) |
|
|