mixle.stats.compute.fused_codegen module¶
Source-generated fused numba kernels for composite / mixture trees.
The generated-numba kernels in declarations lower one leaf at a time: each leaf materializes its
sufficient statistic in numpy and runs its own row loop, so a composite or mixture pays a Python<->C
boundary crossing and an intermediate allocation per factor and per component. For a deep model of
cheap leaves that overhead dominates – numpy itself is multi-pass for the same reason.
This module instead emits a SINGLE @njit function for the whole model structure: one pass over the
rows with the composite sum and the mixture log-sum-exp in nopython registers, no per-factor allocation.
Two leaf flavours, each using the right primitive (numba supports BOTH a SIMD-vectorized scalar loop and
BLAS via @/np.dot in nopython):
scalar leaves (Gaussian, Exponential, Poisson, Gamma, Geometric, Bernoulli): univariate, O(1) per element. Their scalar log-density is inlined into the row loop – numba SIMD-vectorizes it, and fusing every factor into one pass with no intermediate arrays beats numpy’s multi-pass evaluation. A leaf may have
arity > 1data arrays (e.g. Poisson carries(x, lgamma(x+1)), Gamma(x, log x)).matrix leaves (MultivariateGaussian, …): the quadratic form
(x-mu)' P (x-mu)and the E-step’s weighted GramX' (r .* X)are matmuls – emitted as numba@(BLAS). Fusing the score, the responsibility soft-max and the BLAS accumulation into one njit beats numpy’s per-component dispatch (~5x on a GMM E-step). A scalar triple-loop here would be ~15x SLOWER – never hand-roll a matmul.
When a model mixes the two, the matrix quad-forms are precomputed via BLAS before the row loop, the row loop scores + forms responsibilities (storing them only when a matrix leaf needs them downstream), and the matrix sufficient statistics are accumulated via BLAS after it. Pure-scalar models never materialize the responsibility matrix, so their fast path is byte-for-byte the original one.
A discrete leaf whose value-keyed category counts are the sufficient statistic (Categorical) fuses too – the categorical kind scores from a precomputed (K, C) log-prob table and accumulates a (K, C) weighted count histogram. The general bar for fusion is a map-reducible sufficient statistic (a sum/count, a min/max, or a histogram – not necessarily additive): that is what lets Binomial (n from max x) and NegativeBinomial (iterative dispersion over the count histogram) fuse despite non-additive/iterative MLEs.
Generated kernels are compiled once and disk-cached (see _njit()), so the compile cost is paid once
per structure ever, not per process. A leaf with no template (e.g. Laplace, whose weighted-median MLE
keeps the raw observations) -> fusible() is False -> numpy.
- class LeafTemplate(name, matches, data, params, to_value=None, arity=1, kind='scalar', expr=None, acc_names=(), acc_stmt=None, mat_precompute=None, mat_row=None, mat_accumulate=None, vec_row=None, vec_accumulate=None, tab_table=None, tab_hist=False, tab_to_value=None, wants_minmax=False, to_value_g=None, cat_table=None, cat_to_value=None, dtype='float64')[source]
Bases:
objectHow to fuse one leaf family.
kind="scalar"leaves useexpr/acc_stmtoverarity1-D data arrays (the row values are passed as a listvalsof numba expressions).kind="matrix"leaves use themat_*hooks (a single 2-D data array + BLAS precompute/accumulate).paramsstacks the leaf’s parameters across the K mixture components;to_valuepacks a component’s accumulated statistics into the estimator’svalue()tuple.- Parameters:
name (str)
arity (int)
kind (str)
acc_stmt (Callable[[list[str], dict[str, str], str], str] | None)
mat_precompute (Callable[[int, dict[str, str]], list[str]] | None)
vec_accumulate (Callable[[int, dict[str, str]], list[str]] | None)
tab_hist (bool)
tab_to_value (Callable[[float, float, ndarray | None, int, int], tuple] | None)
wants_minmax (bool)
to_value_g (Callable[[tuple, float, float, float], tuple] | None)
dtype (str)
- name: str
- arity: int = 1
- kind: str = 'scalar'
- tab_hist: bool = False
- wants_minmax: bool = False
- dtype: str = 'float64'
- register_leaf_template(t)[source]
- Parameters:
t (LeafTemplate)
- Return type:
None
- analyze(model)[source]
Return a
FusedPlanifmodelis a fusible mixture/composite of templated leaves, else None.Handles: a single leaf, a Composite of leaves, a Mixture of leaves, and a Mixture of Composites (every component sharing the same leaf-type structure). Anything else -> None (fall back to numpy).
The structural nodes are matched by EXACT type, not duck-typing: mixture-flavoured relatives (SemiSupervised / Joint / Hierarchical mixtures, Select, …) carry
components/log_wbut have different E-step semantics, so they must keep their own kernels.- Parameters:
model (Any)
- Return type:
FusedPlan | None
- fused_seq_log_density(model, enc, compute_dtype=None)[source]
Per-row log densities of
modelover encodingencvia one fused numba pass.compute_dtype(e.g.np.float32) runs the row arithmetic in reduced precision while the log-sum-exp accumulator and output stay float64;Nonekeeps the byte-identical float64 path.Raises
ValueErrorifmodelis not fusible – callers should checkfusible()first.
- fused_accumulate(model, enc, weights, return_ll=False, compute_dtype=None)[source]
Run one fused E-step and return the sufficient statistic in the estimator’s
value()format.The whole E-step – component scoring, responsibility soft-max, and per-leaf weighted-statistic accumulation (scalar inline, matrix via BLAS) – runs in a single nopython pass, then is packed into the exact tuple shape
estimate(nobs, suff_stat)expects. Withreturn_llthe weighted data log-likelihood (the posterior normalizer, computed for free in the same pass) is also returned as(suff_stat, ll)so the EM loop can skip a separate scoring pass. RaisesValueErrorif not fusible.
- class FusedKernel(dist, engine, estimator=None)[source]
Bases:
objectA duck-typed
Kernelbacked by the source-generated fused scorer and E-step.- Parameters:
dist (Any)
engine (Any)
estimator (Any)
- class FusedKernelFactory(fallback=None)[source]
Bases:
objectBuild a
FusedKernelon a numba-capable engine for fusible models, else delegate.Scoring needs only
fusible(); estimation also needs the fused E-step (fusible_estep()), so when an estimator is present we require the stronger check. Everything else falls through to the declaration/numba/generic factory (the previous behaviour), so registering this is never a regression.- Parameters:
fallback (Any)
- class FusedPlan(num_components, is_mixture, leaf_templates, signature, component_is_composite=False)[source]
Bases:
objectA fusible model structure: K mixture components (1 if not a mixture), each a list of leaf factors.
- Parameters:
- num_components: int
- is_mixture: bool
- leaf_templates: tuple[LeafTemplate, ...]
- signature: tuple
- component_is_composite: bool = False
- property has_matrix: bool