Streaming variational inference: Trainer for minibatch ADVI - #710
Open
YichengYang-Ethan wants to merge 9 commits into
Open
Streaming variational inference: Trainer for minibatch ADVI#710YichengYang-Ethan wants to merge 9 commits into
YichengYang-Ethan wants to merge 9 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #710 +/- ##
===========================================
+ Coverage 51.60% 91.86% +40.26%
===========================================
Files 73 101 +28
Lines 8003 9159 +1156
===========================================
+ Hits 4130 8414 +4284
+ Misses 3873 745 -3128
🚀 New features to boost your workflow:
|
Contributor
|
Is this ready for review? |
YichengYang-Ethan
force-pushed
the
streaming-trainer
branch
3 times, most recently
from
July 20, 2026 11:35
aeabc44 to
e1f4660
Compare
YichengYang-Ethan
force-pushed
the
streaming-trainer
branch
2 times, most recently
from
August 2, 2026 14:55
7965c76 to
3584ff4
Compare
Trainer(method=..., dataloader=...).fit(n) owns the loop: it seeds the model's pm.Data placeholder before step 0 and streams a batch into it after every step, so the user writes no callbacks. Every step advances, including the last. Skipping the final advance made fit(n) pull exactly n batches, but it left the batch fit had just trained in the placeholder, so Inference.refine -- which steps before replaying callbacks -- retrained it, and the closure counter that implemented the skip stayed live inside refine after an early stop and stranded it on a stale batch. The loader already reads one batch ahead for its pass-size check, so uniform advancing costs nothing a re-readable source did not already pay. User callbacks run before the advance, so one inspecting the placeholder sees the batch that produced the latest loss rather than its successor, and a StopIteration from one ends the fit without pulling again. An Inference instance bound to a different model than the one being trained is now refused instead of silently optimizing a model that never receives a batch, and a model whose observed variables declare no total_size, or one that disagrees with the loader's N, warns rather than returning a quietly misweighted posterior.
Decodes which batch each gradient step trained from the loss fingerprint and compares it to the sequence a user gets by iterating the loader -- across a single step, a fit ending exactly at an epoch seam, and fits wrapping the loader once and several times. The previous assertions checked that n distinct values were seen, which a stream off by one still satisfies. Also pins that the scaling warning fires on an absent or mismatched total_size and stays quiet on a correct one, over two N/batch pairs, and that the bound-model check accepts an instance built under the model as readily as it rejects one built elsewhere.
Remove the cross-module `_is_positive_int` import: it is private to dataloader.py, where it validates row counts, and `n` is a step count. Inline the check instead of promoting a one-line predicate into shared code. Remove the `_stream` generator nested inside `fit`, hoisting it to a module-level `_cycle`. Epoch cycling is the loop rule that has to hold, and inside a method body it could only be exercised through a full ADVI fit; two direct tests now pin it. Remove the Notes block from the class docstring: it described a refactor of the per-step set_data as still pending, which is PR-description material rather than API documentation. Co-Authored-By: Claude <noreply@anthropic.com>
Mutation testing found two behaviours of fit() with nothing asserting them: dropping the isinstance(n, bool) clause and dropping the progressbar setdefault both left the suite green. Co-Authored-By: Claude <noreply@anthropic.com>
DataLoader validates its sizes with numbers.Integral, so fit now does the same: np.int64(4) was accepted for batch_size and refused for n. The two spin-detector fixtures now raise on a third pass, so a _cycle that loses its guard fails those tests instead of hanging them. Co-Authored-By: Claude <noreply@anthropic.com>
…batch count The merged pymc-devs#698 makes len(DataLoader) the batch count, matching torch, with the dataset size N on the .total_size property. Every total_size=len(loader) in the tests silently declared N to be the batch count under the new semantics. The pass-boundary warning test pinned a loader check that the merge deleted, so it goes too. Co-Authored-By: Claude <noreply@anthropic.com>
The DataLoader isinstance check and the data_name lookup both protected failures that announce themselves: a wrong loader dies on .total_size before anything is consumed, and a wrong data_name dies on the first set_data with a KeyError naming it, one batch in. The Inference-bound-to-another-model check stays because that failure is silent. Their tests go with them, and the class docstring loses the lineage paragraph. Co-Authored-By: Claude <noreply@anthropic.com>
…open The mutation audit re-added the DataLoader isinstance guard and no test failed: nothing asserted that an iterable with a total_size attribute is enough. This test trains through a plain sized iterable and kills that mutation. Co-Authored-By: Claude <noreply@anthropic.com>
The module and class docstrings both read as 'user callbacks are unsupported' while fit() documents, supports, and tests callbacks=; the phrase now says what was meant, once: the user writes no hand-written streaming callbacks. test_fit_trains_one_batch_per_step and test_user_callbacks_see_the_batch_that_produced_the_loss assert strict subsets (installed only / seen only) of what the parametrized test_steps_consume_the_loaders_own_batch_sequence asserts across four n/blocks configurations; the default-optimizer sequencing path stays pinned by test_refine_after_fit_continues_without_repeating_a_batch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
YichengYang-Ethan
force-pushed
the
streaming-trainer
branch
from
August 8, 2026 17:13
3584ff4 to
b736877
Compare
YichengYang-Ethan
marked this pull request as ready for review
August 14, 2026 08:29
Contributor
Author
|
Marking this ready, with a short reading guide.
Tests pin the batch-to-step alignment against the loader itself, the duck-typed loader contract (what the deleted isinstance guard used to cover), and both mismatch warnings. Validated end to end on 30.7M rows of tick data against a closed-form posterior streamed through the same loader. |
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.
Streaming variational inference: Trainer for minibatch ADVI
Stacks on #698 (the streaming
DataLoader). This ports theTrainerfrompymc-devs/pymc#8333 into extras, so both halves of the streaming-VI work —
out-of-core data plus a callback-free fit loop — are here now.
What it does
Trainer(method="advi", dataloader=loader, data_name="batch").fit(n)owns thefitting loop and streams each minibatch into the model's
pm.Dataplaceholderwith
set_data, so the user writes no callbacks. TheDataLoaderowns batching(
len(dataloader)is the dataset sizeN), the model owns the math.fit(n)feeds exactlynminibatches (the first seeds the placeholder beforestep 0; the advance after the final step is skipped). User callbacks compose with
the internal advance instead of colliding on the keyword, and an
Inferenceinstance is forwarded to
pm.fitunchanged.Relationship to #635 (the new ADVI API)
#635 already reworks ADVI around its own
Trainer(
pymc_extras/inference/advi/training.py), so this is not meant to land asecond, competing
Trainer. It's the interim, out-of-core path that runs ontoday's
pm.fit— useful now, and a concrete home for the streamingDataLoaderwhile #635 is in review. The end state is to feed the
DataLoaderinto #635'sTrainerthrough a small adapter, at which point this interimTrainerretires.Opening it as a draft so the two can be reconciled rather than duplicated; happy
to fold it into #635's direction once that settles.
Where the scaling lives (interim)
The
N / batch_sizerescaling stays in the model, viatotal_size=len(loader),reusing the existing
create_minibatch_rvmachinery, so it runs unchanged ontoday's
pm.fit. Folding the scaling into the inference step — so it is derivedfrom
len(dataloader)and drops out of the model body — is the cleaner end stateand lines up with #635; the
TrainerNotesdocument this.Tests
tests/variational/test_streaming_trainer.py: end-to-end equivalence to in-RAMpm.MinibatchADVI, exact batch accounting (fit(n)consumesnbatches),placeholder seeding/streaming,
refineresuming the stream, the pass-boundarytotal_sizecheck, user-callback composition, and the input guards.Refs
Ports pymc-devs/pymc#8333 · stacks on #698 · original data layer
pymc-devs/pymc#8325 · relates to #635.