NRPT#
The primary interface is autotuning: autotune /
autosample discover the chain count, local-exploration
count, and schedule together, then draw from the target. The building-block
tuners and the core single run are below them.
Autotuning#
hamon.autosample(key: jax.Array, *, n_samples: int, n_warmup: int = 0, steps_per_sample: int = 1, **autotune_kwargs) -> tuple[jax.Array, AutotuneReport]
#
One-shot: :func:autotune then draw — returns (samples, report).
The convenience entry for "give me samples." Forwards all keyword arguments
to :func:autotune (ebm/program or factories, init_factory,
beta_range, device, …), then draws n_samples from the tuned plan.
For repeated draws from one tuned configuration, call :func:autotune once
and reuse :meth:NRPTPlan.sample.
hamon.autotune
#
Full NRPT autotuning: orchestrate chain count, exploration, and schedule.
The one-call front door (autotune / autosample) and its result objects,
pure host orchestration over the tuners in hamon.tuning and the jitted core
in hamon.nrpt.
DeviceLike
module-attribute
#
Represent a PEP 604 union type
E.g. for int | str
__cached__
module-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__doc__
module-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__file__
module-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__name__
module-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__package__
module-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
annotations
module-attribute
#
AutotuneReport
#
Diagnostics from an :func:autotune run.
Attributes:
| Name | Type | Description |
|---|---|---|
n_chains |
discovered chain count N. |
|
gibbs_steps_per_round |
discovered local-exploration count n_expl. |
|
Lambda |
estimated global communication barrier Λ. |
|
betas |
the final tuned β ladder. |
|
device |
the resolved device (string) or |
|
chain_history |
per-probe records from the N search (:func: |
|
exploration |
the :func: |
|
round_trip_diagnostics |
round-trip summary from the final production run. |
|
total_round_trips |
total completed round trips observed during the final
production run (summed across chains), or |
|
production_rounds |
number of rounds the production run used (the window
|
__annotations__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_fields__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_params__
class-attribute
#
__dict__
class-attribute
#
Read-only proxy of a mapping.
__doc__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__
class-attribute
#
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral.
int('0b100', base=0) 4
__match_args__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__static_attributes__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
#
list of weak references to the object
summary() -> str
#
Human-readable multi-line summary.
__eq__(other)
#
__init__(n_chains: int, gibbs_steps_per_round: int, Lambda: float, betas: np.ndarray, device: str | None, chain_history: list, exploration: dict | None, round_trip_diagnostics: dict | None, total_round_trips: int | None = None, production_rounds: int | None = None) -> None
#
__repr__()
#
NRPTPlan
#
A tuned NRPT configuration plus a warm cold-chain state.
Returned by :func:autotune. Holds the discovered hyperparameters (N,
schedule, n_expl) and an equilibrated cold-chain state, so :meth:sample
can draw repeatedly and cheaply — no re-tuning, reusing the compiled loop.
Attributes:
| Name | Type | Description |
|---|---|---|
n_chains |
/ betas / gibbs_steps_per_round / Lambda
|
the tuned config. |
device |
/ betas / gibbs_steps_per_round / Lambda
|
the resolved device (or |
report |
/ betas / gibbs_steps_per_round / Lambda
|
the :class: |
__annotations__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_fields__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_params__
class-attribute
#
__dict__
class-attribute
#
Read-only proxy of a mapping.
__doc__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__
class-attribute
#
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral.
int('0b100', base=0) 4
__match_args__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__static_attributes__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
#
list of weak references to the object
sample(key: jax.Array, n_samples: int, *, n_warmup: int = 0, steps_per_sample: int = 1) -> jax.Array
#
Draw n_samples from the target (cold chain) — cheap and repeatable.
Runs single-chain block Gibbs at the tuned cold β from the stored warm
state (the established post-NRPT draw). Returns a (n_samples,
n_nodes) array; call again with a fresh key for more, with no
re-tuning.
__eq__(other)
#
__init__(n_chains: int, betas: np.ndarray, gibbs_steps_per_round: int, Lambda: float, device: Any, report: AutotuneReport, _cold_program: BlockSamplingProgram, _warm_state: list, _clamp_state: list, _obs_block: Any) -> None
#
__repr__()
#
AbstractEBM
#
Something that has a well-defined energy function (map from a state to a scalar).
__abstractclassvars__
class-attribute
#
Build an immutable unordered collection of unique elements.
__abstractmethods__
class-attribute
#
Build an immutable unordered collection of unique elements.
__abstractvars__
class-attribute
#
Build an immutable unordered collection of unique elements.
__annotations__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_fields__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_params__
class-attribute
#
__doc__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__
class-attribute
#
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral.
int('0b100', base=0) 4
__match_args__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__static_attributes__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__init__() -> None
#
Initialize self. See help(type(self)) for accurate signature.
energy(state: list[PyTree[Shaped[Array, 'nodes ?*state'], _State]], blocks: BlockSpec | list[Block]) -> Float[Array, '']
#
Evaluate the energy function of the EBM given some state information.
Arguments:
state: The state for which to evaluate the energy function. Must be compatible withblocks.blocks: Specifies how the information instateis organized. May be either a pre-builtBlockSpec(fast path — avoids rebuilding the spec) or a plainlist[Block]for convenience when calling from user code.
Returns:
A scalar representing the energy value associated with state.
with_beta(beta: Array) -> AbstractEBM
#
Return a copy of this EBM with a different inverse-temperature β.
Subclasses that want to work with tune_schedule(ebm=..., program=...)
must override this method.
BlockSamplingProgram
#
A PGM block-sampling program.
This class encapsulates everything that is needed to run a PGM block sampling program in hamon.
per_block_interactions and per_block_interaction_active are parallel to the free blocks in gibbs_spec, and
their members are passed directly to a sampler when the state of the corresponding free block is being updated
during a sampling program. per_block_interaction_global_inds and per_block_interaction_global_slices are
also parallel to the free blocks, and are used to slice the global state of the program to produce the
state information required to update the state of each block alongside the static information contained in the
interactions.
Attributes:
gibbs_spec: A division of some PGM into free and clamped blocks.samplers: A sampler to use to update every free block ingibbs_spec.per_block_interactions: All the interactions that touch each free block ingibbs_spec.per_block_interaction_active: indicates which interactions are real and which interactions are not part of the model and have been added to pad data structures so that they can be rectangular.per_block_interaction_global_inds: how to find the information required to update each block within the global state listper_block_interaction_global_slices: how to slice each array in the global state list to find the information required to update each block_block_sd_inds: precomputed sd_index for each free block (avoids recomputing inside scan)_block_positions: precomputed node positions in global state for each free block (avoids recomputing inside scan)_block_output_sds: precomputed output ShapeDtypeStruct pytree for each free block_block_slice_starts: static start index when the block occupies a contiguous range of the global state (always the case for blocks laid out byBlockSpec), orNoneto fall back to a gather-index scatter. A contiguous range lets the write-back lower tolax.dynamic_update_sliceinstead of a scatter, which XLA fuses far better.
__abstractclassvars__
class-attribute
#
Build an immutable unordered collection of unique elements.
__abstractmethods__
class-attribute
#
Build an immutable unordered collection of unique elements.
__abstractvars__
class-attribute
#
Build an immutable unordered collection of unique elements.
__annotations__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_fields__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_params__
class-attribute
#
__doc__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__
class-attribute
#
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral.
int('0b100', base=0) 4
__match_args__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__static_attributes__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__init__(gibbs_spec: BlockGibbsSpec, samplers: list[hamon.conditional_samplers.AbstractConditionalSampler], interaction_groups: list[hamon.interaction.InteractionGroup])
#
Construct a BlockSamplingProgram.
Takes in a set of information that implicitly defines a sampling program and manipulates it into a shape appropriate for vectorized block-sampling. This involves reindexing, slicing, and often padding.
Arguments:
gibbs_spec: A division of some PGM into free and clamped blocks.samplers: The update rule to use for each free block ingibbs_spec.interaction_groups: A list ofInteractionGroupsthat define how the variables in your sampling program affect one another.
with_ebm(ebm) -> BlockSamplingProgram
#
Return a copy of this program rewired to a different EBM.
Subclasses that want to work with tune_schedule(ebm=..., program=...)
must override this method.
autotune(key: jax.Array, *, ebm: AbstractEBM | None = None, program: BlockSamplingProgram | None = None, ebm_factory: Callable | None = None, program_factory: Callable | None = None, init_factory: Callable, clamp_state: list | None = None, sample_nodes: Sequence | None = None, beta_range: tuple[float, float] = (0.0, 1.0), target_acceptance: float = 0.5, min_chains: int = 3, max_chains: int = 128, initial_n: int | None = None, seed_from_energy: bool = True, gibbs_steps_per_round: int | None = None, search_exploration: bool = False, max_exploration_steps: int = 8, cost_model: bool = True, select_by: str = 'cost', target_efficiency: float = 0.9, rounds_per_probe: int = 400, n_tune: int = 4, n_polish: int = 2, n_rounds: int = 1000, compile_cache: bool | str = True, pad_probes: bool | None = None, device: DeviceLike = 'auto') -> NRPTPlan
#
Autotune the full NRPT configuration: N, exploration count, and schedule.
The one-call front door for solving a problem with hamon. Runs the
dependency-ordered, cheap→expensive recipe and returns an :class:NRPTPlan
you draw from with :meth:NRPTPlan.sample:
- N via :func:
tune_chains, probed at the final n_expl when it is already known (pinned or the deterministic device default) so stage 3 reuses stage 1's compiled round loop — the biggest cold-run compile. When the n_expl search is on, probes run at n_expl=1 instead (cheapest; Λ — hence N* — is invariant to n_expl). - n_expl — by default a deterministic device-calibrated count
(accelerator → a fixed mid-range value, CPU → 1): reproducible across runs
and ~free, since the ESS-per-wall-second objective is flat in n_expl on a
dispatch-bound accelerator. Pin it explicitly with
gibbs_steps_per_round(e.g. a value calibrated for your hardware), or passsearch_exploration=Trueto tune it via :func:tune_explorationat the fixed N, reusing the schedule from step 1 (the equi-acceptance schedule is n_expl-invariant, so this needs no re-tuning and never re-discovers N); the"cost"search maximizes ESS per measured wall-second but its pick is not reproducible across runs (it depends on the machine's clock state), so it is best used as a one-time per-hardware calibration. - Schedule polish via :func:
tune_scheduleat the chosen (N, n_expl), which also leaves an equilibrated warm cold-chain state.
The multi-probe search recompiles per chain count and per n_expl, so by
default the persistent compilation cache is enabled (compile_cache)
to amortize those compiles across probes and runs.
Pass either a single template ebm + program (temperature-linear mode)
or per-chain ebm_factory + program_factory, exactly as the individual
tuners accept. init_factory(n_chains, ebms, programs) -> list builds one
initial state per chain at the discovered N.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
PRNG key. |
required |
ebm / program
|
single template objects (temperature-linear mode), or |
required | |
ebm_factory / program_factory
|
per-chain factories. |
required | |
init_factory
|
Callable
|
|
required |
clamp_state
|
list | None
|
clamped block states. |
None
|
sample_nodes
|
Sequence | None
|
nodes defining the column order of drawn samples (must be
free nodes of the program). |
None
|
beta_range
|
tuple[float, float]
|
|
(0.0, 1.0)
|
target_acceptance
|
float
|
per-pair swap acceptance target for the N search. Default 0.5 — the round-trip-optimal r = 1/2 (N ≈ 2Λ; Syed et al.). |
0.5
|
min_chains / max_chains / initial_n
|
N-search bounds / start. |
required | |
seed_from_energy
|
bool
|
seed the chain-count search from a cheap energy-variance
Λ̂ (no PT ladder) so it converges in one probe — but only when local
exploration mixes; a Gelman–Rubin R̂ check falls back to the robust
|
True
|
gibbs_steps_per_round
|
int | None
|
pin n_expl to this value, skipping both the device
default and the search (step 2). For hardware you have already
calibrated. |
None
|
search_exploration
|
bool
|
tune n_expl by a wall-timed search (step 2). Default
|
False
|
max_exploration_steps
|
int
|
ceiling for the n_expl doubling search (when
|
8
|
select_by
|
str
|
for |
'cost'
|
target_efficiency
|
float
|
ELE-adequacy threshold for |
0.9
|
cost_model
|
bool
|
for the |
True
|
rounds_per_probe
|
int
|
rounds per tuning/exploration probe (the cheap search budget). |
400
|
n_tune
|
int
|
schedule-tuning phases per N probe. |
4
|
n_polish
|
int
|
schedule-tuning phases in the final polish. |
2
|
n_rounds
|
int
|
rounds for the final production run — equilibrates the warm
cold state and is the window the reported round-trip rate / efficiency
are measured over. Should be |
1000
|
compile_cache
|
bool | str
|
|
True
|
pad_probes
|
bool | None
|
chain-mask the stage-1 probes — pad every probe's round
loop to |
None
|
device
|
DeviceLike
|
where to run; resolved once and reused across every stage. |
'auto'
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
NRPTPlan
|
class: |
autosample(key: jax.Array, *, n_samples: int, n_warmup: int = 0, steps_per_sample: int = 1, **autotune_kwargs) -> tuple[jax.Array, AutotuneReport]
#
One-shot: :func:autotune then draw — returns (samples, report).
The convenience entry for "give me samples." Forwards all keyword arguments
to :func:autotune (ebm/program or factories, init_factory,
beta_range, device, …), then draws n_samples from the tuned plan.
For repeated draws from one tuned configuration, call :func:autotune once
and reuse :meth:NRPTPlan.sample.
resolve_entry_device(device: DeviceLike, *, n_chains: int, n_nodes: int, arrays: Any = ()) -> JaxDevice | None
#
Entry-point resolution: heuristic score plus a tracer guard.
When any entry array is a tracer the caller is already inside a jit/vmap/grad trace, where opening device contexts or transferring arrays is not meaningful — routing becomes a no-op.
tune_chains(key: jax.Array, ebm_factory: Callable | None = None, program_factory: Callable | None = None, init_factory: Callable | None = None, clamp_state: list | None = None, beta_range: tuple[float, float] = (0.0, 1.0), gibbs_steps_per_round: int = 0, initial_n: int | None = None, seed_from_energy: bool = True, target_acceptance: float = 0.5, rounds_per_probe: int = 200, n_tune_per_probe: int = 4, max_iters: int = 6, min_chains: int = 3, max_chains: int = 128, lambda_rtol: float = 0.05, *, ebm: AbstractEBM | None = None, program: BlockSamplingProgram | None = None, tune_tol: float | None = None, safety_margin: float = 0.05, device: DeviceLike = 'auto', pad_probes: bool = False) -> dict
#
Iteratively discover the right chain count for a given target acceptance.
Follows the N-tuning method of Syed et al. (2021): the global communication barrier Λ is a schedule invariant (Σ rejection_rates ≈ Λ at any chain count), so it is estimated at a single fixed N from a schedule-tuned run rather than searched for by probing many chain counts.
- Estimate Λ̂ = Σ rejection_rates at the current N (each probe runs
tune_schedule, which tunes the schedule toward equi-acceptance). - Recommend N = ceil(Λ̂·(1 + safety_margin) / r_target) + 1 — the round-trip-optimal 2Λ + 1 chains at r = 1/2 (target_acceptance = 0.5).
- Iterate this fixed point (re-estimate Λ̂ at N) until N stops moving.
Because Λ̂ comes from the current probe (not a running maximum), the result is
essentially independent of the starting N — discovery from initial_n=None
and from a reasonable guess converge to the same count. With no initial_n
the first probe runs at a high pilot of max_chains chains: high on
purpose, because a low pilot's rejection rates saturate and bias Λ̂ low,
forcing the fixed point to climb over several probes. An over-resolved pilot
gives an unbiased Λ̂ in one probe, landing n* within ±1 immediately, so
discovery converges in ~2 probes regardless of problem size.
Instead of providing ebm_factory and program_factory, you can pass
a template ebm and program and the factories will be built
internally using ebm.with_beta() and program.with_ebm().
init_factory is still required as initialization varies by use case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
PRNG key |
required |
ebm_factory
|
Callable | None
|
betas_array → list[EBM] |
None
|
program_factory
|
Callable | None
|
list[EBM] → list[Program] |
None
|
init_factory
|
Callable | None
|
(n_chains, list[EBM], list[Program]) → list[init_states]. Receives EBMs and programs so it can extract the correct free_blocks for initialization (block nodes must be the same objects as the EBMs' nodes). |
None
|
clamp_state
|
list | None
|
clamped block states |
None
|
beta_range
|
tuple[float, float]
|
(β_min, β_max) for the temperature range |
(0.0, 1.0)
|
gibbs_steps_per_round
|
int
|
Gibbs sweeps between swap attempts |
0
|
initial_n
|
int | None
|
starting chain count. The default |
None
|
seed_from_energy
|
bool
|
seed the search from a cheap energy-variance Λ̂
(Theorem 2, no PT ladder; see :func: |
True
|
target_acceptance
|
float
|
desired per-pair swap acceptance rate. Default 0.5 — the round-trip-optimal rejection r = 1/2 (N ≈ 2Λ; Syed et al.), not the 0.77 from the reversible-PT literature. |
0.5
|
rounds_per_probe
|
int
|
rounds per probe (and for the final production probe) |
200
|
n_tune_per_probe
|
int
|
schedule tuning iterations for the final probe |
4
|
max_iters
|
int
|
maximum discovery iterations |
6
|
min_chains
|
int
|
floor on chain count |
3
|
max_chains
|
int
|
ceiling on chain count |
128
|
lambda_rtol
|
float
|
relative tolerance for Λ stabilization (default 5%) |
0.05
|
safety_margin
|
float
|
small fractional pad on N* (default 0.05) covering residual barrier bias and ELE-assumption violations; 0.0 gives the bare round-trip-optimal count |
0.05
|
pad_probes
|
bool
|
run every probe's round loop padded to |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
dict with keys:
n_chains: final recommended chain count
betas: optimized schedule at that chain count
Lambda: conservative (max) barrier estimate
Lambda_raw: last raw estimate (may be lower than Lambda)
target_acceptance: the target used
converged_reason: "chain_count" | "lambda_stable" | "max_iters"
barrier_identified: whether the final count's ladder round-tripped, so
the reported Lambda is identified rather than a stalled-conveyor
within-basin artifact ( |
tune_exploration(key: jax.Array, ebm_factory: Callable | None = None, program_factory: Callable | None = None, init_states: Sequence[list] = (), clamp_state: list | None = None, initial_betas: jax.Array | None = None, start_steps: int = 1, max_steps: int = 64, rounds_per_probe: int = 400, n_tune_per_probe: int = 3, improve_tol: float = 0.05, time_rounds: int = 200, time_reps: int = 3, cost_model: bool = True, select_by: str = 'cost', target_efficiency: float = 0.9, *, fixed_schedule: jax.Array | None = None, ebm: AbstractEBM | None = None, program: BlockSamplingProgram | None = None, device: DeviceLike = 'auto') -> dict
#
Discover the local-exploration count gibbs_steps_per_round (n_expl).
n_expl is the only major NRPT knob hamon does not otherwise auto-tune
(tune_chains sets N from Λ; tune_schedule sets the
schedule). The objective maximized here is effective sample size per
measured steady-state wall-second,
objective(n_expl) = ESS_median(cold chain) / (rounds · t_round(n_expl)),
where t_round is the per-round wall time measured on the target device
after warm-up (so XLA compile is excluded). This is the honest endgame and,
crucially, it self-calibrates to the hardware's real cost structure. The
per-round cost is t_round = c₀ + n_expl·c_s: a fixed overhead c₀
(swap pass, energy recompute, host/kernel dispatch, scan bookkeeping) plus
n_expl Gibbs sweeps at c_s each. The Syed et al. compute model assumes
c₀ = 0 (cost ∝ n_expl), which makes n_expl=1 optimal; but cold-chain ESS
grows sub-linearly in n_expl while real per-round cost grows less than
linearly when c₀ is non-trivial — so on a dispatch-bound backend where
c₀ ≳ 1.4·c_s the optimum shifts to n_expl > 1. Measuring t_round
rather than assuming cost ∝ n_expl is what lets the search see this.
Why not the cheaper round-trip proxy τ_obs / n_expl? Empirically it
under-picks n_expl: round trips count excursions but not how decorrelated
each cold sample is, so doubling n_expl can sharply raise cold-chain ESS
while barely moving τ_obs (the r≈0.81 ESS↔round-trip correlation of Syed
et al. breaking down). The round-trip rate is still used as a robust gate
and cross-check — efficiency = τ_obs/τ̄ is the ELE-violation meter, and
the efficiency_limiter from :func:report_nrpt_diagnostics stops the
search when a probe is schedule-limited (an unequalized ladder, where more
local exploration cannot help). rt_per_compute and t_round are
recorded per probe alongside the ESS-per-second objective.
See :func:tune_chains for the N analogue. The chain count is held
fixed at len(initial_betas) — Λ (hence N) is a schedule invariant robust
to n_expl, so the two searches decouple; run tune_chains first
if N is unknown.
Each probe (1) gets a schedule, (2) measures ESS over a cold-chain trace via
an :class:~hamon.NRPTStateObserver, and (3) times the steady-state round
loop (warm-up absorbs the one-time compile; the median of time_reps runs
of time_rounds rounds gives t_round). With fixed_schedule=None
(default) the schedule is re-tuned per probe via :func:tune_schedule; pass
fixed_schedule (a pre-tuned ladder) to reuse it and run each probe as
a single :func:nrpt production call — much cheaper, and sound because the
equi-acceptance schedule is invariant to n_expl. autotune uses this mode.
Instead of ebm_factory / program_factory you may pass a single
template ebm and program (temperature-linear mode).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
PRNG key. |
required |
ebm_factory / program_factory
|
per-chain factories, or use |
required | |
init_states
|
Sequence[list]
|
one initial block-state list per chain (fixed across probes). |
()
|
clamp_state
|
list | None
|
clamped block states. |
None
|
initial_betas
|
Array | None
|
the (fixed) β ladder; its length sets the chain count. |
None
|
start_steps
|
int
|
smallest n_expl to try ( |
1
|
max_steps
|
int
|
largest n_expl before the search stops. |
64
|
rounds_per_probe
|
int
|
production rounds per probe (and the tuning ceiling) — must be large enough for ESS and τ_obs to be low-variance. |
400
|
n_tune_per_probe
|
int
|
schedule-tuning phases per probe. |
3
|
improve_tol
|
float
|
minimum fractional objective gain to keep doubling (guards against chasing Monte-Carlo noise past the peak). |
0.05
|
time_rounds
|
int
|
rounds per timed run when measuring |
200
|
time_reps
|
int
|
number of timed runs to reduce over (noise control). |
3
|
select_by
|
str
|
how to choose n_expl. |
'cost'
|
target_efficiency
|
float
|
ELE-adequacy threshold for |
0.9
|
cost_model
|
bool
|
for |
True
|
fixed_schedule
|
Array | None
|
a pre-tuned β ladder to reuse across all probes (each
probe becomes one production run, no per-probe re-tuning). |
None
|
device
|
DeviceLike
|
where to run; resolved once and reused across probes. Timing is measured on this device, so the chosen n_expl is calibrated to it. |
'auto'
|
Returns:
| Type | Description |
|---|---|
dict
|
dict with keys:
gibbs_steps_per_round: the chosen n_expl.
objective: ESS per measured wall-second at the chosen n_expl.
ess_median / tau_observed / efficiency / rt_per_compute / t_round:
at the choice ( |
tune_schedule(key: jax.Array, ebm_factory: Callable | None = None, program_factory: Callable | None = None, init_states: Sequence[list] = (), clamp_state: list | None = None, n_rounds: int = 0, gibbs_steps_per_round: int = 0, initial_betas: jax.Array | None = None, n_tune: int = 5, rounds_per_tune: int = 200, track_round_trips: bool = True, *, ebm: AbstractEBM | None = None, program: BlockSamplingProgram | None = None, observer: AbstractNRPTObserver | None = None, adaptive_tuning: bool = True, tune_tol: float | None = None, equalize_tol: float = 0.05, phase_patience: int = 2, min_tune_phases: int = 1, round_batch: int = 50, min_rounds_per_tune: int = 50, round_stable_k: int = 2, lambda_rtol: float = 0.05, device: DeviceLike = 'auto', pad_chains_to: int | None = None) -> tuple[list, dict]
#
NRPT with iterative schedule optimization (Algorithm 4).
pad_chains_to enables chain masking in every phase's round loop (see
:func:hamon.nrpt.nrpt): phases at different chain counts padded to the
same length share one compiled executable. Stats and states are sliced
back to the true count before any tuning math sees them, so the schedule
optimization is untouched. Incompatible with observer.
Adapts the β schedule over tuning phases, then runs the final n_rounds
production phase with the optimized schedule. Each phase logs one INFO line
(Λ, mean acceptance, schedule movement) so long runs are not silent.
Instead of providing ebm_factory and program_factory, you can pass
a template ebm and program and the factories will be built
internally using ebm.with_beta() and program.with_ebm().
Convergence-driven tuning (default, adaptive_tuning=True): budgets
are chosen automatically, so callers need not guess them. Each phase runs as
many rounds as needed for the Λ estimate to settle (between
min_rounds_per_tune and the rounds_per_tune ceiling, in
round_batch increments — see _tune_phase_adaptive_rounds), giving a
low-variance rejection-rate estimate. The schedule with the best
(lowest-spread) rejection rates seen across phases is kept for production —
not the last, which can be noisier. Tuning stops once the schedule is
well-equalized (std(rejection_rates) < equalize_tol) OR has settled
(max|Δβ| below the effective tune_tol — its Monte-Carlo floor) for
phase_patience consecutive phases, after at least min_tune_phases,
capped at n_tune. (max|Δβ| alone is not a reliable convergence
signal: it plateaus at a problem-dependent noise floor rather than going to
zero, so the equalization check is what stops already-good schedules.) When
tune_tol is left None it defaults to _DEFAULT_TUNE_TOL here.
Counts are deterministic for a given seed but problem-dependent — do not
assume a fixed round/phase count.
Legacy mode (adaptive_tuning=False): runs exactly n_tune phases
of exactly rounds_per_tune rounds and uses the last schedule.
tune_tol then behaves as the optional early-stop it always was (None
⇒ run all n_tune phases). n_tune and rounds_per_tune act as
safety caps in both modes.
Returns (states, stats) where stats includes tuning history in
stats["tuning_history"] (each entry records max_beta_shift).
States are ordered by ascending β — the cold chain (target
distribution) is states[-1].
device is resolved once here and passed to every tuning and
production phase, so the device never flips mid-run (see hamon.device).
hamon.NRPTPlan
#
A tuned NRPT configuration plus a warm cold-chain state.
Returned by :func:autotune. Holds the discovered hyperparameters (N,
schedule, n_expl) and an equilibrated cold-chain state, so :meth:sample
can draw repeatedly and cheaply — no re-tuning, reusing the compiled loop.
Attributes:
| Name | Type | Description |
|---|---|---|
n_chains |
/ betas / gibbs_steps_per_round / Lambda
|
the tuned config. |
device |
/ betas / gibbs_steps_per_round / Lambda
|
the resolved device (or |
report |
/ betas / gibbs_steps_per_round / Lambda
|
the :class: |
sample(key: jax.Array, n_samples: int, *, n_warmup: int = 0, steps_per_sample: int = 1) -> jax.Array
#
Draw n_samples from the target (cold chain) — cheap and repeatable.
Runs single-chain block Gibbs at the tuned cold β from the stored warm
state (the established post-NRPT draw). Returns a (n_samples,
n_nodes) array; call again with a fresh key for more, with no
re-tuning.
hamon.AutotuneReport
#
Diagnostics from an :func:autotune run.
Attributes:
| Name | Type | Description |
|---|---|---|
n_chains |
discovered chain count N. |
|
gibbs_steps_per_round |
discovered local-exploration count n_expl. |
|
Lambda |
estimated global communication barrier Λ. |
|
betas |
the final tuned β ladder. |
|
device |
the resolved device (string) or |
|
chain_history |
per-probe records from the N search (:func: |
|
exploration |
the :func: |
|
round_trip_diagnostics |
round-trip summary from the final production run. |
|
total_round_trips |
total completed round trips observed during the final
production run (summed across chains), or |
|
production_rounds |
number of rounds the production run used (the window
|
summary() -> str
#
Human-readable multi-line summary.
Building blocks#
hamon.nrpt
#
Non-Reversible Parallel Tempering with vectorized swaps.
Based on Syed et al. (2021), "Non-Reversible Parallel Tempering: a Scalable Highly Parallel MCMC Scheme" (arXiv:1905.02939).
Exploits temperature-linearity (E_β = β·E_base) for single-eval-per-chain swap decisions. Adaptive schedule optimization (Algorithm 4) equalizes rejection rates. Optional energy caching with boundary-only deltas for rectangular block partitions.
DeviceLike
module-attribute
#
Represent a PEP 604 union type
E.g. for int | str
__cached__
module-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__doc__
module-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__file__
module-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__name__
module-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__package__
module-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
annotations
module-attribute
#
logger
module-attribute
#
Instances of the Logger class represent a single logging channel. A "logging channel" indicates an area of an application. Exactly how an "area" is defined is up to the application developer. Since an application can have any number of areas, logging channels are identified by a unique string. Application areas can be nested (e.g. an area of "input processing" might include sub-areas "read CSV files", "read XLS files" and "read Gnumeric files"). To cater for this natural nesting, channel names are organized into a namespace hierarchy where levels are separated by periods, much like the Java or Python package namespace. So in the instance given above, channel names might be "input" for the upper level, and "input.csv", "input.xls" and "input.gnu" for the sub-levels. There is no arbitrary limit to the depth of nesting.
NRPTCarry
#
Scan carry for the NRPT inner loop.
__annotations__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__doc__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__
class-attribute
#
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral.
int('0b100', base=0) 4
__match_args__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__orig_bases__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__slots__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__static_attributes__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
accepted
class-attribute
#
Alias for field number 2
attempted
class-attribute
#
Alias for field number 3
base_E
class-attribute
#
Alias for field number 5
idx_state
class-attribute
#
Alias for field number 4
key
class-attribute
#
Alias for field number 0
obs_carry
class-attribute
#
Alias for field number 6
states
class-attribute
#
Alias for field number 1
AbstractEBM
#
Something that has a well-defined energy function (map from a state to a scalar).
__abstractclassvars__
class-attribute
#
Build an immutable unordered collection of unique elements.
__abstractmethods__
class-attribute
#
Build an immutable unordered collection of unique elements.
__abstractvars__
class-attribute
#
Build an immutable unordered collection of unique elements.
__annotations__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_fields__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_params__
class-attribute
#
__doc__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__
class-attribute
#
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral.
int('0b100', base=0) 4
__match_args__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__static_attributes__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__init__() -> None
#
Initialize self. See help(type(self)) for accurate signature.
energy(state: list[PyTree[Shaped[Array, 'nodes ?*state'], _State]], blocks: BlockSpec | list[Block]) -> Float[Array, '']
#
Evaluate the energy function of the EBM given some state information.
Arguments:
state: The state for which to evaluate the energy function. Must be compatible withblocks.blocks: Specifies how the information instateis organized. May be either a pre-builtBlockSpec(fast path — avoids rebuilding the spec) or a plainlist[Block]for convenience when calling from user code.
Returns:
A scalar representing the energy value associated with state.
with_beta(beta: Array) -> AbstractEBM
#
Return a copy of this EBM with a different inverse-temperature β.
Subclasses that want to work with tune_schedule(ebm=..., program=...)
must override this method.
AbstractNRPTObserver
#
Observer for NRPT rounds, called once per round after Gibbs sweeps and swaps.
Concrete subclasses must implement __call__ and may override init
to provide a non-trivial carry. The observation returned by
__call__ is stacked by lax.scan into a pytree with a leading axis
of size n_rounds. Return None as the observation for
accumulate-only observers that do not need per-round storage.
__abstractclassvars__
class-attribute
#
Build an immutable unordered collection of unique elements.
__abstractmethods__
class-attribute
#
Build an immutable unordered collection of unique elements.
__abstractvars__
class-attribute
#
Build an immutable unordered collection of unique elements.
__annotations__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_fields__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_params__
class-attribute
#
__doc__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__
class-attribute
#
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral.
int('0b100', base=0) 4
__match_args__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__static_attributes__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__call__(stacked_states: list[Array], base_energies: Array, round_idx: Int[Array, ''], carry: ~ObserveCarry) -> tuple[~ObserveCarry, PyTree]
#
Observe one NRPT round.
Arguments:
stacked_states: Per-block arrays, each of shape(n_chains, ...). The cold chain (target) is at index-1.base_energies: Shape(n_chains,)base energies (no β factor), aligned withstacked_states—base_energies[c]is the energy of the state at chain positioncafter this round's swaps.round_idx: Zero-based round counter.carry: Arbitrary pytree state threaded across rounds.
Returns:
(updated_carry, observation) — observation is stacked by
lax.scan; use None for accumulate-only mode.
__init__() -> None
#
Initialize self. See help(type(self)) for accurate signature.
init() -> PyTree
#
Initialize the observer carry. Defaults to None.
BlockSamplingProgram
#
A PGM block-sampling program.
This class encapsulates everything that is needed to run a PGM block sampling program in hamon.
per_block_interactions and per_block_interaction_active are parallel to the free blocks in gibbs_spec, and
their members are passed directly to a sampler when the state of the corresponding free block is being updated
during a sampling program. per_block_interaction_global_inds and per_block_interaction_global_slices are
also parallel to the free blocks, and are used to slice the global state of the program to produce the
state information required to update the state of each block alongside the static information contained in the
interactions.
Attributes:
gibbs_spec: A division of some PGM into free and clamped blocks.samplers: A sampler to use to update every free block ingibbs_spec.per_block_interactions: All the interactions that touch each free block ingibbs_spec.per_block_interaction_active: indicates which interactions are real and which interactions are not part of the model and have been added to pad data structures so that they can be rectangular.per_block_interaction_global_inds: how to find the information required to update each block within the global state listper_block_interaction_global_slices: how to slice each array in the global state list to find the information required to update each block_block_sd_inds: precomputed sd_index for each free block (avoids recomputing inside scan)_block_positions: precomputed node positions in global state for each free block (avoids recomputing inside scan)_block_output_sds: precomputed output ShapeDtypeStruct pytree for each free block_block_slice_starts: static start index when the block occupies a contiguous range of the global state (always the case for blocks laid out byBlockSpec), orNoneto fall back to a gather-index scatter. A contiguous range lets the write-back lower tolax.dynamic_update_sliceinstead of a scatter, which XLA fuses far better.
__abstractclassvars__
class-attribute
#
Build an immutable unordered collection of unique elements.
__abstractmethods__
class-attribute
#
Build an immutable unordered collection of unique elements.
__abstractvars__
class-attribute
#
Build an immutable unordered collection of unique elements.
__annotations__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_fields__
class-attribute
#
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__dataclass_params__
class-attribute
#
__doc__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__
class-attribute
#
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral.
int('0b100', base=0) 4
__match_args__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__
class-attribute
#
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__static_attributes__
class-attribute
#
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__init__(gibbs_spec: BlockGibbsSpec, samplers: list[hamon.conditional_samplers.AbstractConditionalSampler], interaction_groups: list[hamon.interaction.InteractionGroup])
#
Construct a BlockSamplingProgram.
Takes in a set of information that implicitly defines a sampling program and manipulates it into a shape appropriate for vectorized block-sampling. This involves reindexing, slicing, and often padding.
Arguments:
gibbs_spec: A division of some PGM into free and clamped blocks.samplers: The update rule to use for each free block ingibbs_spec.interaction_groups: A list ofInteractionGroupsthat define how the variables in your sampling program affect one another.
with_ebm(ebm) -> BlockSamplingProgram
#
Return a copy of this program rewired to a different EBM.
Subclasses that want to work with tune_schedule(ebm=..., program=...)
must override this method.
optimize_schedule(rejection_rates: jax.Array, betas: jax.Array) -> jax.Array
#
Equalize per-pair rejection rates by redistributing β values.
Estimates the cumulative communication barrier Λ(β_k) = Σ_{i≤k} r_i, then places the new ladder at a regular grid of Λ (the equi-acceptance schedule of Syed et al. 2021, Algorithm 2: β_k = Λ⁻¹(k/N · Λ)). The inverse Λ⁻¹ is taken as a Fritsch–Carlson monotone cubic* of β against the cumulative barrier — smoother than the previous piecewise-linear inverse while staying monotone (no overshoot), which the paper recommends over linear interpolation.
The result keeps the dtype of betas so repeated tuning phases do not
drift to float64 when x64 is enabled.
nrpt(key: jax.Array, ebms: Sequence[AbstractEBM] | AbstractEBM, programs: Sequence[BlockSamplingProgram] | BlockSamplingProgram, init_states: Sequence[list], clamp_state: list, n_rounds: int, gibbs_steps_per_round: int, betas: jax.Array | None = None, track_round_trips: bool = True, energy_delta_fn: Callable | None = None, observer: AbstractNRPTObserver | None = None, device: DeviceLike = 'auto', pad_chains_to: int | None = None, _emit_diagnostics: bool = True, _return_stacked: bool = False) -> tuple[list, dict]
#
Non-Reversible Parallel Tempering with vectorized swaps.
pad_chains_to (≥ n_chains) enables chain masking: the ladder is
padded to that fixed length with copies of the coldest chain and the round
loop runs with the true chain count as traced data — swaps beyond the
live prefix are forced-rejected (identity permutation), so the padding is
fully decoupled from the live ladder. All returned states/stats are sliced
back to the true count, so callers see exactly the shapes and semantics of
an unpadded run. The point: probes at different chain counts padded to
the same length share ONE compiled round loop instead of recompiling per
count (the dominant cold cost of tune_chains), at the price of wasted
Gibbs work on the padding chains (~free on a dispatch-bound accelerator).
Temperature-linear mode only; incompatible with observer and
energy_delta_fn.
Single-pass DEO: one swap parity per round, alternating even/odd. Multi-pass breaks non-reversibility (even∘odd∘odd∘even = identity).
Temperature-linear mode: instead of per-chain sequences, ebms
and programs may each be a single template object (any β; it is
rebased to β = 1 via with_beta()/with_ebm()). betas is then
required and defines the chain count. Interactions are scaled by each
chain's β inside the sampling kernel, which assumes every interaction
array is linear in β — true for the DiscreteEBMFactor family (and
anything built from β-scaled factor weights), and consistent with the
E_β = β·E_base assumption the swap math already makes. This avoids
constructing one program per chain and storing per-chain copies of
every interaction tensor. For models whose interactions are not
linear in β, pass explicit per-chain sequences instead.
Chains are ordered by ascending β: index 0 is the hottest chain
(lowest β, closest to the reference distribution) and index −1 is the
coldest chain (highest β, the target distribution you want to
sample from). The returned states list preserves this ordering,
and betas must be sorted ascending (validated).
init_states may be either a sequence of per-chain block-state
lists, or a single block-state list whose arrays carry a leading
(n_chains, ...) axis — e.g. straight from
hinton_init(key, model, blocks, (n_chains,)) — avoiding the
per-chain list/restack dance.
A hottest chain at exactly β = 0 (sampling the reference distribution)
is supported; base energies are computed from a β = 1 copy of the EBM
when with_beta() is available, falling back to the coldest chain.
.. warning::
To collect samples from the target distribution, always use
states[-1] (the cold chain), not states[0].
device selects where the computation runs: "auto" (default) routes
small workloads to the CPU and large ones to a visible accelerator (see
hamon.device), "cpu"/"gpu" force a platform, a concrete
jax.Device is used as-is, and None leaves placement untouched.
Routing re-commits the entry arrays (program tensors, states, betas) to
the chosen device; outputs come back committed there. Arrays closed over
by energy_delta_fn cannot be moved by routing — keep them uncommitted
or on the target device.
Stats keys
accepted, attempted, acceptance_rate, rejection_rates, betas round_trip_diagnostics (if track_round_trips=True): Lambda, tau_predicted, tau_observed, efficiency, lambda_profile, round_trips_per_chain, restarts_per_chain observations (if observer is not None): Per-round observer output stacked along axis 0. observer_carry (if observer is not None): Final observer carry after all rounds.
free_node_count(program) -> int
#
Total free nodes in a sampling program — O(1) Python metadata.
init_index_state(n_chains: int) -> dict
#
Initialize index process tracking arrays.
machine_to_chain[j] = which chain position machine j's state
currently occupies. Initially machine j is at chain j.
visited_top[j] = whether machine j has reached chain N since
its last round trip completion.
Returns a dict suitable for inclusion in lax.scan carry.
resolve_entry_device(device: DeviceLike, *, n_chains: int, n_nodes: int, arrays: Any = ()) -> JaxDevice | None
#
Entry-point resolution: heuristic score plus a tracer guard.
When any entry array is a tracer the caller is already inside a jit/vmap/grad trace, where opening device contexts or transferring arrays is not meaningful — routing becomes a no-op.
round_trip_summary(index_state: dict, rejection_rates: jax.Array, betas: jax.Array, n_rounds: int) -> dict
#
Compute full diagnostic summary for NRPT run.
Jitted so the handful of reductions below (Λ, τ̄, the local-barrier profile,
the round-trip rate) fuse into a single compiled kernel instead of ~8 eager
op-by-op dispatches, each of which otherwise pays a first-shape XLA compile
when called once per probe at a new chain count. n_rounds is traced (not
static), so the compile is shared across round counts.
Returns dict with
Lambda: global communication barrier estimate tau_predicted: theoretical optimal round trip rate tau_observed: empirical round trip rate efficiency: tau_observed / tau_predicted (closer to 1 = better) lambda_profile: local barrier at each pair midpoint round_trips_per_chain: per-machine round trip counts restarts_per_chain: per-machine restart counts
tree_device_put(tree: _T, device: JaxDevice | None) -> _T
#
Commit every jax.Array leaf of tree to device.
Non-array leaves (blocks, nodes, samplers, specs) pass through with object
identity preserved, so equinox's static partition hashes identically and
jit caches stay warm. If every array leaf is already committed to
device, the original object is returned unchanged — this keeps
repeated calls (e.g. tune_schedule tuning phases) presenting the
literally-same pytree to the jit cache.
hamon.tune_schedule(key: jax.Array, ebm_factory: Callable | None = None, program_factory: Callable | None = None, init_states: Sequence[list] = (), clamp_state: list | None = None, n_rounds: int = 0, gibbs_steps_per_round: int = 0, initial_betas: jax.Array | None = None, n_tune: int = 5, rounds_per_tune: int = 200, track_round_trips: bool = True, *, ebm: AbstractEBM | None = None, program: BlockSamplingProgram | None = None, observer: AbstractNRPTObserver | None = None, adaptive_tuning: bool = True, tune_tol: float | None = None, equalize_tol: float = 0.05, phase_patience: int = 2, min_tune_phases: int = 1, round_batch: int = 50, min_rounds_per_tune: int = 50, round_stable_k: int = 2, lambda_rtol: float = 0.05, device: DeviceLike = 'auto', pad_chains_to: int | None = None) -> tuple[list, dict]
#
NRPT with iterative schedule optimization (Algorithm 4).
pad_chains_to enables chain masking in every phase's round loop (see
:func:hamon.nrpt.nrpt): phases at different chain counts padded to the
same length share one compiled executable. Stats and states are sliced
back to the true count before any tuning math sees them, so the schedule
optimization is untouched. Incompatible with observer.
Adapts the β schedule over tuning phases, then runs the final n_rounds
production phase with the optimized schedule. Each phase logs one INFO line
(Λ, mean acceptance, schedule movement) so long runs are not silent.
Instead of providing ebm_factory and program_factory, you can pass
a template ebm and program and the factories will be built
internally using ebm.with_beta() and program.with_ebm().
Convergence-driven tuning (default, adaptive_tuning=True): budgets
are chosen automatically, so callers need not guess them. Each phase runs as
many rounds as needed for the Λ estimate to settle (between
min_rounds_per_tune and the rounds_per_tune ceiling, in
round_batch increments — see _tune_phase_adaptive_rounds), giving a
low-variance rejection-rate estimate. The schedule with the best
(lowest-spread) rejection rates seen across phases is kept for production —
not the last, which can be noisier. Tuning stops once the schedule is
well-equalized (std(rejection_rates) < equalize_tol) OR has settled
(max|Δβ| below the effective tune_tol — its Monte-Carlo floor) for
phase_patience consecutive phases, after at least min_tune_phases,
capped at n_tune. (max|Δβ| alone is not a reliable convergence
signal: it plateaus at a problem-dependent noise floor rather than going to
zero, so the equalization check is what stops already-good schedules.) When
tune_tol is left None it defaults to _DEFAULT_TUNE_TOL here.
Counts are deterministic for a given seed but problem-dependent — do not
assume a fixed round/phase count.
Legacy mode (adaptive_tuning=False): runs exactly n_tune phases
of exactly rounds_per_tune rounds and uses the last schedule.
tune_tol then behaves as the optional early-stop it always was (None
⇒ run all n_tune phases). n_tune and rounds_per_tune act as
safety caps in both modes.
Returns (states, stats) where stats includes tuning history in
stats["tuning_history"] (each entry records max_beta_shift).
States are ordered by ascending β — the cold chain (target
distribution) is states[-1].
device is resolved once here and passed to every tuning and
production phase, so the device never flips mid-run (see hamon.device).
hamon.optimize_schedule(rejection_rates: jax.Array, betas: jax.Array) -> jax.Array
#
Equalize per-pair rejection rates by redistributing β values.
Estimates the cumulative communication barrier Λ(β_k) = Σ_{i≤k} r_i, then places the new ladder at a regular grid of Λ (the equi-acceptance schedule of Syed et al. 2021, Algorithm 2: β_k = Λ⁻¹(k/N · Λ)). The inverse Λ⁻¹ is taken as a Fritsch–Carlson monotone cubic* of β against the cumulative barrier — smoother than the previous piecewise-linear inverse while staying monotone (no overshoot), which the paper recommends over linear interpolation.
The result keeps the dtype of betas so repeated tuning phases do not
drift to float64 when x64 is enabled.
hamon.tune_chains(key: jax.Array, ebm_factory: Callable | None = None, program_factory: Callable | None = None, init_factory: Callable | None = None, clamp_state: list | None = None, beta_range: tuple[float, float] = (0.0, 1.0), gibbs_steps_per_round: int = 0, initial_n: int | None = None, seed_from_energy: bool = True, target_acceptance: float = 0.5, rounds_per_probe: int = 200, n_tune_per_probe: int = 4, max_iters: int = 6, min_chains: int = 3, max_chains: int = 128, lambda_rtol: float = 0.05, *, ebm: AbstractEBM | None = None, program: BlockSamplingProgram | None = None, tune_tol: float | None = None, safety_margin: float = 0.05, device: DeviceLike = 'auto', pad_probes: bool = False) -> dict
#
Iteratively discover the right chain count for a given target acceptance.
Follows the N-tuning method of Syed et al. (2021): the global communication barrier Λ is a schedule invariant (Σ rejection_rates ≈ Λ at any chain count), so it is estimated at a single fixed N from a schedule-tuned run rather than searched for by probing many chain counts.
- Estimate Λ̂ = Σ rejection_rates at the current N (each probe runs
tune_schedule, which tunes the schedule toward equi-acceptance). - Recommend N = ceil(Λ̂·(1 + safety_margin) / r_target) + 1 — the round-trip-optimal 2Λ + 1 chains at r = 1/2 (target_acceptance = 0.5).
- Iterate this fixed point (re-estimate Λ̂ at N) until N stops moving.
Because Λ̂ comes from the current probe (not a running maximum), the result is
essentially independent of the starting N — discovery from initial_n=None
and from a reasonable guess converge to the same count. With no initial_n
the first probe runs at a high pilot of max_chains chains: high on
purpose, because a low pilot's rejection rates saturate and bias Λ̂ low,
forcing the fixed point to climb over several probes. An over-resolved pilot
gives an unbiased Λ̂ in one probe, landing n* within ±1 immediately, so
discovery converges in ~2 probes regardless of problem size.
Instead of providing ebm_factory and program_factory, you can pass
a template ebm and program and the factories will be built
internally using ebm.with_beta() and program.with_ebm().
init_factory is still required as initialization varies by use case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
PRNG key |
required |
ebm_factory
|
Callable | None
|
betas_array → list[EBM] |
None
|
program_factory
|
Callable | None
|
list[EBM] → list[Program] |
None
|
init_factory
|
Callable | None
|
(n_chains, list[EBM], list[Program]) → list[init_states]. Receives EBMs and programs so it can extract the correct free_blocks for initialization (block nodes must be the same objects as the EBMs' nodes). |
None
|
clamp_state
|
list | None
|
clamped block states |
None
|
beta_range
|
tuple[float, float]
|
(β_min, β_max) for the temperature range |
(0.0, 1.0)
|
gibbs_steps_per_round
|
int
|
Gibbs sweeps between swap attempts |
0
|
initial_n
|
int | None
|
starting chain count. The default |
None
|
seed_from_energy
|
bool
|
seed the search from a cheap energy-variance Λ̂
(Theorem 2, no PT ladder; see :func: |
True
|
target_acceptance
|
float
|
desired per-pair swap acceptance rate. Default 0.5 — the round-trip-optimal rejection r = 1/2 (N ≈ 2Λ; Syed et al.), not the 0.77 from the reversible-PT literature. |
0.5
|
rounds_per_probe
|
int
|
rounds per probe (and for the final production probe) |
200
|
n_tune_per_probe
|
int
|
schedule tuning iterations for the final probe |
4
|
max_iters
|
int
|
maximum discovery iterations |
6
|
min_chains
|
int
|
floor on chain count |
3
|
max_chains
|
int
|
ceiling on chain count |
128
|
lambda_rtol
|
float
|
relative tolerance for Λ stabilization (default 5%) |
0.05
|
safety_margin
|
float
|
small fractional pad on N* (default 0.05) covering residual barrier bias and ELE-assumption violations; 0.0 gives the bare round-trip-optimal count |
0.05
|
pad_probes
|
bool
|
run every probe's round loop padded to |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
dict with keys:
n_chains: final recommended chain count
betas: optimized schedule at that chain count
Lambda: conservative (max) barrier estimate
Lambda_raw: last raw estimate (may be lower than Lambda)
target_acceptance: the target used
converged_reason: "chain_count" | "lambda_stable" | "max_iters"
barrier_identified: whether the final count's ladder round-tripped, so
the reported Lambda is identified rather than a stalled-conveyor
within-basin artifact ( |
hamon.tune_exploration(key: jax.Array, ebm_factory: Callable | None = None, program_factory: Callable | None = None, init_states: Sequence[list] = (), clamp_state: list | None = None, initial_betas: jax.Array | None = None, start_steps: int = 1, max_steps: int = 64, rounds_per_probe: int = 400, n_tune_per_probe: int = 3, improve_tol: float = 0.05, time_rounds: int = 200, time_reps: int = 3, cost_model: bool = True, select_by: str = 'cost', target_efficiency: float = 0.9, *, fixed_schedule: jax.Array | None = None, ebm: AbstractEBM | None = None, program: BlockSamplingProgram | None = None, device: DeviceLike = 'auto') -> dict
#
Discover the local-exploration count gibbs_steps_per_round (n_expl).
n_expl is the only major NRPT knob hamon does not otherwise auto-tune
(tune_chains sets N from Λ; tune_schedule sets the
schedule). The objective maximized here is effective sample size per
measured steady-state wall-second,
objective(n_expl) = ESS_median(cold chain) / (rounds · t_round(n_expl)),
where t_round is the per-round wall time measured on the target device
after warm-up (so XLA compile is excluded). This is the honest endgame and,
crucially, it self-calibrates to the hardware's real cost structure. The
per-round cost is t_round = c₀ + n_expl·c_s: a fixed overhead c₀
(swap pass, energy recompute, host/kernel dispatch, scan bookkeeping) plus
n_expl Gibbs sweeps at c_s each. The Syed et al. compute model assumes
c₀ = 0 (cost ∝ n_expl), which makes n_expl=1 optimal; but cold-chain ESS
grows sub-linearly in n_expl while real per-round cost grows less than
linearly when c₀ is non-trivial — so on a dispatch-bound backend where
c₀ ≳ 1.4·c_s the optimum shifts to n_expl > 1. Measuring t_round
rather than assuming cost ∝ n_expl is what lets the search see this.
Why not the cheaper round-trip proxy τ_obs / n_expl? Empirically it
under-picks n_expl: round trips count excursions but not how decorrelated
each cold sample is, so doubling n_expl can sharply raise cold-chain ESS
while barely moving τ_obs (the r≈0.81 ESS↔round-trip correlation of Syed
et al. breaking down). The round-trip rate is still used as a robust gate
and cross-check — efficiency = τ_obs/τ̄ is the ELE-violation meter, and
the efficiency_limiter from :func:report_nrpt_diagnostics stops the
search when a probe is schedule-limited (an unequalized ladder, where more
local exploration cannot help). rt_per_compute and t_round are
recorded per probe alongside the ESS-per-second objective.
See :func:tune_chains for the N analogue. The chain count is held
fixed at len(initial_betas) — Λ (hence N) is a schedule invariant robust
to n_expl, so the two searches decouple; run tune_chains first
if N is unknown.
Each probe (1) gets a schedule, (2) measures ESS over a cold-chain trace via
an :class:~hamon.NRPTStateObserver, and (3) times the steady-state round
loop (warm-up absorbs the one-time compile; the median of time_reps runs
of time_rounds rounds gives t_round). With fixed_schedule=None
(default) the schedule is re-tuned per probe via :func:tune_schedule; pass
fixed_schedule (a pre-tuned ladder) to reuse it and run each probe as
a single :func:nrpt production call — much cheaper, and sound because the
equi-acceptance schedule is invariant to n_expl. autotune uses this mode.
Instead of ebm_factory / program_factory you may pass a single
template ebm and program (temperature-linear mode).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
PRNG key. |
required |
ebm_factory / program_factory
|
per-chain factories, or use |
required | |
init_states
|
Sequence[list]
|
one initial block-state list per chain (fixed across probes). |
()
|
clamp_state
|
list | None
|
clamped block states. |
None
|
initial_betas
|
Array | None
|
the (fixed) β ladder; its length sets the chain count. |
None
|
start_steps
|
int
|
smallest n_expl to try ( |
1
|
max_steps
|
int
|
largest n_expl before the search stops. |
64
|
rounds_per_probe
|
int
|
production rounds per probe (and the tuning ceiling) — must be large enough for ESS and τ_obs to be low-variance. |
400
|
n_tune_per_probe
|
int
|
schedule-tuning phases per probe. |
3
|
improve_tol
|
float
|
minimum fractional objective gain to keep doubling (guards against chasing Monte-Carlo noise past the peak). |
0.05
|
time_rounds
|
int
|
rounds per timed run when measuring |
200
|
time_reps
|
int
|
number of timed runs to reduce over (noise control). |
3
|
select_by
|
str
|
how to choose n_expl. |
'cost'
|
target_efficiency
|
float
|
ELE-adequacy threshold for |
0.9
|
cost_model
|
bool
|
for |
True
|
fixed_schedule
|
Array | None
|
a pre-tuned β ladder to reuse across all probes (each
probe becomes one production run, no per-probe re-tuning). |
None
|
device
|
DeviceLike
|
where to run; resolved once and reused across probes. Timing is measured on this device, so the chosen n_expl is calibrated to it. |
'auto'
|
Returns:
| Type | Description |
|---|---|
dict
|
dict with keys:
gibbs_steps_per_round: the chosen n_expl.
objective: ESS per measured wall-second at the chosen n_expl.
ess_median / tau_observed / efficiency / rt_per_compute / t_round:
at the choice ( |