Avoid unnecessary deepcopy in random_*_module and promote_batch_shape#2222
Merged
Merged
Conversation
…_batch_shape `random_flax_module` / `random_nnx_module` / `random_eqx_module` deep-copied the full parameter pytree before replacing entries with prior samples, and `promote_batch_shape` deep-copied distributions before reassigning their `_batch_shape`. In both cases only the container structure needs to be fresh; the array leaves are never mutated in place, so copying their buffers is wasted work on every eager model trace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h_shape Lock in the new guarantees: promote_batch_shape shares parameter arrays with its input and mutates neither the input distribution nor its base_dist, and _copy_structure copies only the dict structure while sharing leaves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
random_*_module and promote_batch_shape
fehiepsi
reviewed
Jul 18, 2026
| ) | ||
|
|
||
|
|
||
| def _copy_structure(params): |
Member
There was a problem hiding this comment.
Looks great! Maybe we can use jax.tree.map(lamda x: x, params) at those copy places? I'm worried that params might not be a dict in some cases.
Contributor
Author
There was a problem hiding this comment.
Good point, thanks! Switched to inline jax.tree.map(lambda x: x, params) at the three call sites in f1042dc, and replaced the helper's unit test with an API-level one (test_random_flax_module_shares_param_leaves) checking that params not covered by the prior are shared rather than copied. Re-ran the benchmarks — the structure-only copy stays ~2µs regardless of parameter size, and the peak-memory savings are unchanged (PR description updated).
Inline jax.tree.map(lambda x: x, params) at the call sites instead of a dict-only helper, so non-dict containers are handled too. Replace the helper's unit test with an API-level test that random_flax_module shares the leaves of parameters not covered by the prior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes made
numpyro/contrib/module.py:random_flax_module/random_nnx_module/random_eqx_moduleusedcopy.deepcopy(params)before replacing entries with prior samples. Since_update_paramsonly reassigns dict entries and never mutates array leaves in place, deep-copying every weight buffer is unnecessary — for large networks this duplicates all parameters on every eager model trace (e.g. duringinitialize_modelor any non-jitted trace). Replaced withjax.tree.map(lambda x: x, params), which copies the container structure while sharing the leaves.numpyro/distributions/batch_util.py:_default_promote_batch_shapedeep-copied the distribution although it only reassigns_batch_shapeafterwards; switched tocopy.copy, consistent with the existingMaskedDistribution/Independentimplementations._promote_batch_shape_expandedlikewise now shallow-copiesdandd.base_distinstead of deep-copying, since the only in-place updates are to the_batch_shapeattributes of those two fresh copies.Under jit these deep copies were already no-ops (tracers deep-copy to themselves), so behavior is unchanged; the win is for eager execution.
Benchmarks
Apple M2 (24 GB), CPU jax 0.10.2, Python 3.11. Each scenario runs in a fresh process; time is the best of 3 repeats after warmup, peak memory is
ru_maxrss.promote_batch_shapeon a vmappedExpandedDistribution(eager):locsizeEager seeded trace of a model with
random_flax_module(3-layer MLP):For the module path the trace time is dominated by flax init and prior sampling, so the speed change is within noise (the eliminated copy itself scales linearly: 0.6 ms at 13 MB, 8.0 ms at 200 MB, vs ~2 µs for the structure-only
jax.tree.mapcopy at any size); the main win there is peak memory, which drops by about the size of the parameters.Links to related issues/PRs
None.
Tests
New tests locking in the intended semantics:
test_promote_batch_shape_shares_data_and_preserves_input/test_promote_batch_shape_expanded_preserves_inputintest/test_distributions.py—promote_batch_shapeshares parameter arrays with its input (would fail with the previousdeepcopy) and mutates neither the input distribution nor itsbase_dist(guards the new shallow copies).test_random_flax_module_shares_param_leavesintest/contrib/test_module.py— parameters not covered by the prior are shared with the module's init params rather than copied, and prior-covered entries are replaced as before.Existing coverage (all green locally):
pytest test/contrib/test_module.py— 37 passedpytest test/test_distributions.py -k "vmap or promote or expand"— 1389 passed, 15 xfailedpytest test/contrib/test_control_flow.py— 8 passed, 1 xfailed (exercisespromote_batch_shapeviascan)ruff check/ruff format --check/ty checkcleanDependencies
None.
🤖 Generated with Claude Code