Skip to content

Drop the masked logsumexp reduction in mixture log_prob - #2250

Merged
Qazalbash merged 1 commit into
pyro-ppl:masterfrom
kyo219:mixture-logprob-unmasked-logsumexp
Aug 23, 2026
Merged

Drop the masked logsumexp reduction in mixture log_prob#2250
Qazalbash merged 1 commit into
pyro-ppl:masterfrom
kyo219:mixture-logprob-unmasked-logsumexp

Conversation

@kyo219

@kyo219 kyo219 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Changes made

Values and gradients are unchanged (verified identical for regular values, at zero mixing weights, and for rows where every component is -inf).

Benchmarks

Apple M2 (24 GB), CPU, jax 0.10.2, Python 3.11. Jitted steady-state per-call time, best of 300 calls after warmup, 10,000 values, 5 components (3 for MixtureGeneral with StudentT).

workload master this PR speedup
MixtureSameFamily(Categorical, Normal) log_prob 0.379 ms 0.128 ms 2.96×
same, grad of summed log_prob 0.265 ms 0.211 ms 1.26×
MixtureGeneral (5 × Normal) log_prob 0.14 ms 0.11 ms 1.25×
MixtureGeneral (2 × Normal + StudentT) log_prob 0.11 ms 0.13 ms 0.85×

The big win is MixtureSameFamily, where the component log-probs come from one broadcast producer and the masked reduction forced it to be rematerialized per pass. The last row shows a small absolute regression (~0.02 ms) for one composition where the component log_prob dominates; it appears to be an XLA fusion-choice artifact rather than a systematic cost.

Links to related issues/PRs

#1874 introduced the -inf gradient protection this PR preserves; #2032 incidentally added the where= kwarg this PR removes.

Tests

  • New test_mixture_log_prob_grad_at_zero_weights and test_mixture_log_prob_grad_all_components_neg_inf in test/test_distributions_mixture.py — lock in the gh-1870: Refactor log_prob method in _MixtureBase class to handle -jnp.inf #1874 semantics (no NaN gradients at zero mixing weights and for all--inf rows). Neither case was previously covered by a test.
  • pytest test/test_distributions_mixture.py — 47 passed
  • ruff check / ruff format --check clean; ty check reports the same 10 pre-existing diagnostics as master.

Dependencies

None.

The where= kwarg of jax.nn.logsumexp in _MixtureBase.log_prob forces a
masked reduction that defeats XLA's fused reduction path (3x slower for
MixtureSameFamily log_prob at 10k values on CPU). The mask is redundant:
the value-preserving jnp.where introduced in pyro-ppl#1874 already yields the
same values and stops gradients into -inf component log-probs; the
where= kwarg was added incidentally in the type-hints PR pyro-ppl#2032. Values
and gradients are unchanged, including at zero mixing weights and for
all-components -inf rows; add regression tests for both cases.
@github-actions github-actions Bot added the awaiting review Awaiting review from maintainers label Aug 23, 2026
@Qazalbash

Copy link
Copy Markdown
Collaborator

Thanks @kyo219!

Can you share the code used for benchmarks.

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown

Benchmark report

this PR mixture-logprob-unmasked-logsumexp at 91d46a6e vs baseline master at 999d8d1f

+ run time:     1 faster
  compile time: unchanged across 32 benchmarks

Significant changes (1)

                                 ──────── run time ───────     ────── compile time ─────
  benchmark                      baseline  this PR       Δ     baseline  this PR       Δ
────────────────────────────────────────────────────────────────────────────────────────
+ mixture_same_family_log_prob     2.1 ms   981 µs  -52.6%     102.6 ms  81.3 ms  -20.8%

Red is slower, green is faster; a row is coloured by the worse of its two columns. A delta in parentheses cleared the threshold on a measurement below the resolution floor, so it is shown without being called a change. † marks a benchmark that could not be compared — see below.

Full results

distributions

                                 ──────── run time ────────     ────── compile time ──────
  benchmark                      baseline  this PR        Δ     baseline   this PR       Δ
──────────────────────────────────────────────────────────────────────────────────────────
  biject_to_constraints            4.1 ms   4.0 ms    -2.4%     359.7 ms  356.7 ms   -0.8%
  categorical_log_prob             2.1 ms   2.1 ms    -4.2%      74.9 ms   73.8 ms   -1.5%
  dirichlet_log_prob               638 µs   643 µs    +0.8%     409.7 ms  403.1 ms   -1.6%
  dirichlet_sample                45.5 ms  45.6 ms    +0.3%     856.0 ms  848.7 ms   -0.8%
  gamma_log_prob                   2.2 ms   2.1 ms    -3.4%       2.09 s    2.08 s   -0.7%
  gamma_sample                    20.6 ms  20.8 ms    +1.1%     778.2 ms  795.8 ms   +2.3%
  lkj_cholesky_sample              5.6 ms   5.5 ms    -1.7%       1.17 s    1.17 s   -0.3%
+ mixture_same_family_log_prob     2.1 ms   981 µs   -52.6%     102.6 ms   81.3 ms  -20.8%
  multivariate_normal_log_prob     286 µs   270 µs  (-5.6%)     167.8 ms  167.0 ms   -0.5%
  normal_log_prob                  616 µs   664 µs  (+7.7%)      54.4 ms   52.7 ms   -3.0%
  normal_sample                   20.8 ms  21.1 ms    +1.3%     221.5 ms  204.0 ms   -7.9%
  stick_breaking_transform         6.4 ms   6.2 ms    -3.2%     204.7 ms  205.3 ms   +0.3%
  student_t_log_prob               3.1 ms   3.1 ms    +0.1%      76.7 ms   74.7 ms   -2.6%
  truncated_normal_log_prob        695 µs   706 µs    +1.5%      52.9 ms   53.1 ms   +0.3%

handlers

                                  ──────── run time ───────     ────── compile time ─────
  benchmark                       baseline   this PR      Δ     baseline   this PR      Δ
─────────────────────────────────────────────────────────────────────────────────────────
  initialize_model_hierarchical    42.7 ms   43.6 ms  +2.1%       3.51 s    3.49 s  -0.6%
  log_density_hierarchical          3.7 ms    3.7 ms  +1.9%       1.08 s    1.10 s  +1.5%
  nested_handler_stack              1.4 ms    1.4 ms  +0.5%       452 µs    472 µs  +4.3%
  potential_energy_and_grad          22 µs     23 µs  +3.3%     105.5 ms   99.5 ms  -5.7%
  predictive_forward_sampling     717.4 ms  733.5 ms  +2.2%     176.8 ms  188.5 ms  +6.6%
  trace_seeded_model                814 µs    842 µs  +3.4%     514.3 ms  533.0 ms  +3.7%

mcmc

                             ──────── run time ───────     ───── compile time ─────
  benchmark                  baseline   this PR      Δ     baseline  this PR      Δ
───────────────────────────────────────────────────────────────────────────────────
  hmc_logistic_regression    729.2 ms  729.0 ms  -0.0%       3.09 s   3.10 s  +0.3%
  nuts_dense_mass_funnel       1.16 s    1.15 s  -0.7%       2.35 s   2.37 s  +0.8%
  nuts_eight_schools           1.14 s    1.14 s  -0.1%       2.29 s   2.30 s  +0.5%
  nuts_hierarchical_glm        4.80 s    4.80 s  -0.1%       4.58 s   4.53 s  -1.0%
  nuts_logistic_regression     1.08 s    1.09 s  +0.8%       3.10 s   3.15 s  +1.5%
  nuts_vectorized_chains       2.51 s    2.52 s  +0.3%       2.75 s   2.84 s  +3.2%

svi

                                             ──────── run time ───────     ───── compile time ─────
  benchmark                                  baseline   this PR      Δ     baseline  this PR      Δ
───────────────────────────────────────────────────────────────────────────────────────────────────
  svi_autodelta_map_logistic                 314.1 ms  314.6 ms  +0.2%       3.03 s   3.04 s  +0.3%
  svi_autodiagonalnormal_hierarchical          1.03 s    1.03 s  -0.6%       4.60 s   4.67 s  +1.5%
  svi_automultivariatenormal_eight_schools   769.4 ms  769.6 ms  +0.0%       3.63 s   3.64 s  +0.1%
  svi_autonormal_logistic                    761.8 ms  767.9 ms  +0.8%       3.23 s   3.25 s  +0.7%
  svi_multi_particle_elbo                      1.51 s    1.50 s  -0.4%       3.33 s   3.28 s  -1.6%
  svi_trace_mean_field_elbo                    1.34 s    1.34 s  -0.5%       4.86 s   4.89 s  +0.6%
Methodology and environment

Each benchmark is set up untimed, then called once with the JAX caches cleared and several more times warm. Run is the fastest warm call; compile is the first call minus that, i.e. the tracing, lowering and XLA compilation the warm calls did not have to pay for.

Both refs were measured on the same runner over 2 interleaved round(s), taking the best observation per benchmark. A result is called neutral when it moves less than ±5% (run) or ±25% (compile), or when the measurement itself is under 1 ms (run) / 50 ms (compile) — a shared CI runner cannot resolve changes below that. Compile time gets the looser band because it is measured once per round rather than best-of-N, and swings by roughly 20% even between two runs of identical code. A delta shown in parentheses did clear its threshold, but on a measurement below the resolution floor, so it is reported without being called a change.

baseline this PR
ref master mixture-logprob-unmasked-logsumexp
commit 999d8d1f 91d46a6e
numpyro 0.21.0 0.21.0
jax 0.11.1 0.11.1
backend cpu cpu
python 3.14.7 3.14.7

Runner: Linux-6.17.0-1022-azure-x86_64-with-glibc2.39, 4 CPUs.

Produced by this benchmark run.

@kyo219

kyo219 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Sure! Benchmark script below (Apple M2, CPU, jax 0.10.2; jitted steady-state, best of 300 calls after warmup). The same setup was used to check the value/gradient agreement mentioned in the PR description (log_prob values, grads, zero-mixing-weight grads, and all-components -inf rows compared across both branches).

import time
import jax, jax.numpy as jnp
import numpyro.distributions as dist

N = 10_000
mix = dist.Categorical(probs=jnp.array([0.2, 0.3, 0.1, 0.25, 0.15]))
comp = dist.Normal(jnp.arange(5.0), jnp.ones(5))
m = dist.MixtureSameFamily(mix, comp)
v = jax.random.normal(jax.random.PRNGKey(0), (N,)) * 3

lp = jax.jit(m.log_prob)
g = jax.jit(jax.grad(lambda v: m.log_prob(v).sum()))

def bench(f, x, n=300):
    jax.block_until_ready(f(x))
    t0 = time.perf_counter()
    for _ in range(n):
        r = f(x)
    jax.block_until_ready(r)
    return (time.perf_counter() - t0) / n

print(f"log_prob: {bench(lp, v)*1e3:.3f} ms  grad: {bench(g, v)*1e3:.3f} ms")

On the CI benchmark report: the mixture_same_family_log_prob -51.8% line matches this. The flagged regressions (normal_sample, initialize_model_hierarchical, the SVI ELBOs) don't exercise mixtures.py at all, so I believe those are runner noise.

@Qazalbash Qazalbash left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kyo219

@Qazalbash
Qazalbash merged commit 39bfdfb into pyro-ppl:master Aug 23, 2026
12 checks passed
@Qazalbash Qazalbash removed the awaiting review Awaiting review from maintainers label Aug 23, 2026
@Qazalbash Qazalbash self-assigned this Aug 23, 2026
@kyo219
kyo219 deleted the mixture-logprob-unmasked-logsumexp branch August 24, 2026 01:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants