mixle.inference.leaf_hotswap module¶
Leaf hot-swap / analytic roll-up – swap a plateaued gradient leaf for a closed-form surrogate, with a retained path back on misfit (workstream D4).
Frame (see the ConditionalJIT track, D1-D6): the estimator tree is an IR. D1
(mixle.inference.node_report) instruments every node with a per-round residual/Q-gain
report and an update_kind classification – in particular "gradient" for a
GradLeaf node, whose M-step is m_steps iterations of SGD/Adam
rather than a closed-form update. D2 (mixle.inference.freeze_rollup) and D3
(mixle.inference.block_em) both spend a converged/near-zero D1 Q-gain on SCHEDULING
decisions (skip an E-step recompute, skip a turn in this round’s M-step) while leaving the node’s
own model object untouched. D4 goes one step further for gradient leaves specifically: once a
gradient leaf’s own Q-gain has plateaued (gradient descent has stopped making meaningful progress,
so the remaining M-step compute it would otherwise spend is close to wasted), swap it for a
closed-form surrogate – a moment-matched
MultivariateGaussianDistribution fit
against the SAME (responsibility-weighted) data the gradient leaf was trained on – so every later
round’s cost for that node collapses from O(param_count * _GRADIENT_STEPS) (D1’s own gradient
M-step cost proxy) to O(param_count) (a closed-form MLE).
Correctness backbone (unchanged from the rest of the D-track): this is a SCHEDULING/specialization
optimization only. Swapping a node’s model object for an approximation is more aggressive than
D2/D3’s “leave the object alone, just skip recomputing/re-fitting it” story, so D4 earns back the
Neal-Hinton guarantee two ways instead of one: (1) the swap itself is gated on a genuine, locally
computed misfit RECEIPT (misfit_receipt()) – not merely assumed to be a good approximation
– and (2) the ORIGINAL gradient leaf is never discarded (SwapRecord.original), so if the
receipt later shows the surrogate drifting away from the real held-out data (e.g. the underlying
regime shifts after the swap), swap_back() restores the exact retained object and gradient
fitting resumes exactly where it left off – “never truly forget”, the same policy D2’s freeze/
roll-up commits to for frozen mixture components. F itself (the real Neal-Hinton free energy) is
still the audit receipt: run_em_with_hotswap() gates every round’s proposal – the swap-in
itself AND the following M-step (gradient OR closed-form), as one atomic unit – behind the same
accept/reject monotone-F test D2/D3 already use, so a bad swap can cost speed (a rejected-and-
reverted round, or a later swap-back once already committed) but never correctness. This is the
gate that actually makes the mechanism safe: moment_matched_surrogate() on its own is a
plain Gaussian MLE fit with no guarantee of matching an arbitrary (e.g. multi-modal) gradient
leaf’s held-out density – see that function’s own docstring for a worked adversarial example and
mixle.tests.leaf_hotswap_test.MonotoneObjectiveGateCatchesBadSwapTestCase for the regression
test proving the gate rejects and fully reverts (not merely skips the M-step of) exactly that
case. Earlier revisions of this module applied a plateau-triggered swap to the working tree
unconditionally, before the round’s accept/reject check, and only skipped model = candidate on
rejection – so a rejected round still silently returned the corrupted surrogate; the gate now
rolls the swap itself back too when a round is rejected.
Scope: like D2/D3, this targets one gradient leaf embedded as a component of a
MixtureDistribution (mixed freely with classical families,
per mixle.models.grad_leaf’s whole point) – the “tree” in swap_leaf(tree, leaf_path,
surrogate) is that mixture, and leaf_path is the component index. A single gradient leaf not
embedded in any combinator (tree is the leaf itself) is also supported directly (leaf_path
is ignored) since it is strictly the num_components=1 degenerate case of the same operation –
useful for isolating the swap/misfit/swap-back mechanics from mixture E-step bookkeeping in tests.
Generalizing past MixtureDistribution to arbitrary composite/sequence trees is the same “later
items are expected to widen it” carve-out D2 documents for its own combinator scope.
Moment-matching machinery: G1’s moment_propagation.py (origin/moment-propagation,
unmerged into this branch’s D1/D2/D3 chain – see this module’s PR description for how it was
read via git show rather than a cross-branch merge) propagates a GAUSSIAN LAW through the
specific layer types of mixle.models.transformer.CausalLM (Linear/LayerNorm/GELU/Attention),
which is a different, narrower object than “moment-match an arbitrary gradient leaf’s behavior
against arbitrary data” – it has no entry point that takes a generic GradLeaf and a data
sample. Reusing it here would mean either constraining D4 to transformer-shaped leaves only (out
of scope – GradLeaf wraps ANY torch density module) or re-deriving a generic version of its
per-layer-law machinery, which is its own multi-week item. moment_matched_surrogate()
therefore uses straightforward closed-form moment matching instead: draw/collect the SAME
(possibly responsibility-weighted) data the gradient leaf was trained on and fit a
MultivariateGaussianDistribution to it via
the existing, real closed-form MLE machinery
(MultivariateGaussianAccumulator /
MultivariateGaussianEstimator) – an
honest, fully-real “G1 machinery” scope per the roadmap item’s own explicit fallback clause.
- class PlateauMonitor(*, q_gain_tol=_DEFAULT_PLATEAU_Q_GAIN_TOL, patience=_DEFAULT_PLATEAU_PATIENCE, n_mc=_DEFAULT_PLATEAU_N_MC)[source]
Bases:
objectTracks, per leaf path, how many consecutive rounds a D1
NodeReporthas reported a near-zero Q-gain for a"gradient"-update-kind node – the “gradient descent has stopped making meaningful progress” signalrun_em_with_hotswap()swaps on.Deliberately keyed and reset exactly like
mixle.inference.freeze_rollup. FreezeRollupCache’s own_frozen_streak: a report that stops looking plateaued (the node moved again, or was swapped back to the original – seereset()) resets the streak to 0 rather than latching a stale verdict.- reset(path)[source]
Clear the tracked history for
path(e.g. after a swap or a swap-back).- Parameters:
path (Any)
- Return type:
None
- is_plateaued(path, leaf, *, nobs=None, seed=_SCORE_SEED)[source]
Return whether
leaf(identified bypath) has plateaued this round.Only a D1
update_kind == "gradient"node can plateau in this module’s sense (a closed-form/conjugate/frozen/em node has no “wasted SGD compute” to reclaim by swapping); anything else always returnsFalseand resets the streak, so a leaf that has already been swapped for its (closed-form) surrogate is correctly reported as “not plateaued” going forward – there is nothing left to swap.D1’s own
residualis a MONTE-CARLO estimate (-mean(log_density(x))over the node’s own self-samples, seemixle.inference.node_report’s module docstring) – for a genuinely converged gradient leaf, round-to-round Q-gain is dominated by MC sampling noise rather than any real drift in the fit, so this class uses a much largern_mcthan D1’s own default (_DEFAULT_PLATEAU_N_MCvs D1’s_DEFAULT_MC_SAMPLES=64) to push that noise floor down, and a correspondingly looserq_gain_tolthan D2/D3’s exact-residual default (their residual is deterministic given unchanged parameters; this one never is).
- class SwapRecord(leaf_path, original, surrogate, swap_round, baseline_misfit, misfit_history=<factory>, swapped_back=False, swap_back_round=None)[source]
Bases:
objectThe retrievable receipt of one leaf hot-swap – “never truly forget” (D2’s own phrase for its freeze/roll-up cache) applied to a swapped-out gradient leaf:
originalis the exactGradLeafobject that was in the tree before the swap, always retrievable, never discarded, soswap_back()can restore it byte-for-byte.
- class LeafHotswapStats(round_index, n_components, n_frozen, n_swapped, n_log_density_evals, n_gradient_m_steps, n_closed_form_m_steps, objective, accepted=True, swapped_this_round=(), swapped_back_this_round=())[source]
Bases:
objectOne round’s accounting for the hot-swap EM driver – mirrors
mixle.inference.freeze_rollup.FreezeRollupStats/mixle.inference.block_em. BlockEMStats(samen_log_density_evalswall-clock proxy and real Neal-Hintonobjective), plus the D4-specificn_gradient_m_steps/n_closed_form_m_stepssplit that is literally what “faster to same F” is measured against: a swapped component’s per-round M-step cost drops from D1’sparam_count * _GRADIENT_STEPSproxy toparam_count.swapped_this_roundis which components a plateau proposed a swap for this round – it is recorded even whenacceptedis False (a rejected round rolls the swap itself back out of the returned model/swap_records, but the attempt still happened and is worth a receipt); usen_swapped(or checkidx in swap_records) for which swaps are actually COMMITTED as of this round.
- moment_matched_surrogate(gradient_leaf, data, weights=None)[source]
Fit a closed-form
MultivariateGaussianDistributionthat moment-matchesdata– the SAME (optionally responsibility-weights-ed) datagradient_leafwas trained on – via the real closed-form MLE machinery (weighted mean/covariance), not a re-derived formula.This is a genuine fit, not a placeholder: it reads no attribute off
gradient_leafat all (a torch module has no portable closed-form “current mean/covariance” to read off directly – scoring/sampling is the only generic contract), so “moment-matched against the gradient leaf’s current behavior” means “against the data its current fit was scoring/trained on”, exactly the module-docstring’s documented (and roadmap-sanctioned) fallback to plain closed-form moment matching in lieu of G1’s transformer-specific law-propagation machinery.gradient_leafis accepted (rather than a bare module) purely as a type/documentation signal of intent – see the module docstring’s “why not G1” note – it is not otherwise used.IMPORTANT, honest limitation – read before trusting this function’s output alone: a single Gaussian can only ever be as good an approximation as the true fitted density IS Gaussian. Against a unimodal, roughly-Gaussian-shaped leaf (the common case for a single mixture component pulling its own well-separated slice of responsibility-weighted data) this is an excellent approximation. Against a leaf whose OWN fitted density is multi-modal, heavy-tailed, or otherwise non-Gaussian, this function will silently produce a POOR approximation with no warning – e.g. on a genuinely bimodal
GradLeaf(two well-separated modes,mixle.tests.leaf_hotswap_test.BimodalGauss) the moment-matched surrogate collapses both modes into one wide Gaussian sitting between them, degrading held-out NLL by roughly 80% relative to the original leaf (seemixle.tests.leaf_hotswap_test.MonotoneObjectiveGateCatchesBadSwapTestCase). This function provides NO guarantee, on its own, that the surrogate’s held-out density tracks the original leaf’s – that guarantee, to the extent one exists, is earned entirely byrun_em_with_hotswap()’s per-round monotone-F accept/reject gate (see that function’s own docstring): a swap-plus-refit round that does not improve the real Neal-Hinton objective is rejected and reverted, INCLUDING the swap itself, not merely the following M-step. Do not callmoment_matched_surrogate()outside that gated driver (or an equivalent one) and assume the result is a safe stand-in forgradient_leaf– verify with a real misfit receipt (misfit_receipt()) against genuinely held-out data first.
- misfit_receipt(surrogate, holdout_data)[source]
A genuine, locally-computed misfit scalar for
surrogateon REAL held-out data: its own negative log-likelihood,-mean(log_density(x)). Not a placeholder – recomputed from real samples every call, exactly the same style of receipt D1’sresidualand G1’sclosure_errorboth are (see the respective module docstrings). Lower is better; compared againstSwapRecord.baseline_misfit(the receipt measured right after the swap) byshould_swap_back()to decide whether the surrogate has since drifted away from the real data it was swapped in to approximate.
- swap_leaf(tree, leaf_path, surrogate, *, round_index=0)[source]
Replace the plateaued gradient leaf at
leaf_pathintreewithsurrogate.Returns
(new_tree, swap_record):new_treehas the surrogate in place (every generic D1/D2/D3 mechanism –node_report(),detect_frozen(),seq_log_density– sees an ordinaryMultivariateGaussianDistributionnode from here on, no special-casing required upstream);swap_record.originalretains the exactGradLeafthat was swapped out, per this module’s “never discard capacity” policy (seeSwapRecord).
- swap_back(tree, swap_record, *, round_index=0)[source]
Restore
swap_record.original(the retained gradient leaf) intotreeatswap_record.leaf_path, undoingswap_leaf(). Marksswap_recordas swapped back (in place) so callers/tests can assert the misfit receipt actually fired.
- run_em_with_hotswap(enc_data, estimator, initial_model, *, holdout_data=None, max_its=10, delta=1.0e-9, cache=None, monitor=None, freeze_q_gain_tol=1.0e-6, plateau_q_gain_tol=_DEFAULT_PLATEAU_Q_GAIN_TOL, plateau_patience=_DEFAULT_PLATEAU_PATIENCE, plateau_n_mc=_DEFAULT_PLATEAU_N_MC, misfit_tol=_DEFAULT_MISFIT_TOL, accept_tolerance=_DEFAULT_ACCEPT_TOLERANCE)[source]
Run EM over a
MixtureDistributionwith D4 leaf hot-swap: any component D1 reports as a plateaued gradient leaf (seePlateauMonitor) is swapped for a moment-matched closed-form surrogate (moment_matched_surrogate(), fit against that round’s responsibility-weighted data), which is then updated by a closed-form re-fit every round instead of gradient descent – and swapped back to the retained original the instant a real misfit receipt (misfit_receipt()onholdout_data, when supplied) shows it has drifted (seeshould_swap_back()).Reuses D2’s
FreezeRollupCache/detect_frozenfor ordinary converged-component freezing (composes with D4 exactly like D3 does: a frozen component is excluded from both the swap check and the M-step) and the same per-round objective accept/reject gate D2/D3 use, sohistoryis a real monotone-F receipt: swapping in a surrogate, or swapping back out of one, can only ever be accepted if the round’s real Neal-Hinton objective does not decrease.holdout_data, if given, is treated as belonging to whichever component the CURRENT model would assign it to most responsibly at swap time (a single fixed sample scored against every swapped component’s surrogate) – a simplification documented here rather than a full per-component responsibility-weighted holdout split, mirroring D2/D3’s own explicit single-combinator scope carve-outs (see this module’s docstring).Returns
(final_model, history, swap_records)whereswap_recordsmaps component index to everySwapRecordcreated during the run (including ones later swapped back), so a caller/test can retrieve the retained original gradient leaf and inspectmisfit_history.- Parameters:
enc_data (Any)
estimator (MixtureEstimator)
initial_model (MixtureDistribution)
holdout_data (Any)
max_its (int)
delta (float | None)
cache (FreezeRollupCache | None)
monitor (PlateauMonitor | None)
freeze_q_gain_tol (float)
plateau_q_gain_tol (float)
plateau_patience (int)
plateau_n_mc (int)
misfit_tol (float)
accept_tolerance (float)
- Return type:
tuple[MixtureDistribution, list[LeafHotswapStats], dict[int, SwapRecord]]