Repository navigation
fix: VI quality control retrains once, at the caller's observation and settings - #2149
Conversation
…fails When the quality control of `VIPosterior.train` caught an error, it retrained by calling `train` again with only a smaller learning rate. That retry dropped the caller's `x`, iteration limits and optimizer settings, and it ran quality control again, so a check that kept failing retrained over and over until Python's recursion limit. It also caught every exception, not only a failed check. The basic checks now raise `QualityControlError` instead of `assert`, and `train` catches only that. A failed check retrains from scratch once, with the caller's settings and a 10 times smaller learning rate, and warns; if the check still fails, or `q` was passed as an already-built distribution that cannot be rebuilt, it warns that the posterior may be unreliable.
The basic checks sampled and evaluated `q` at the default observation. After `train(x=...)` with another observation, sampling there raised, and the old catch-all handler then retrained `q` on the default observation instead of the caller's. The checks now use the observation `q` was trained on.
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2149 +/- ##
==========================================
+ Coverage 91.15% 91.17% +0.01%
==========================================
Files 142 142
Lines 14884 14896 +12
==========================================
+ Hits 13568 13581 +13
+ Misses 1316 1315 -1
Flags with carried forward coverage won't be shown. Click here to find out more.
|
manuelgloeckler
left a comment
There was a problem hiding this comment.
Approving — this is a clean fix of a real bug.
I checked that the three new tests fail on main and pass here, and that the diagnosis is right: evaluate() never forwards an observation, so basic_checks was sampling at the default x while q had been trained at the caller's. Passing _trained_on through fixes exactly that.
The retry half is the more important fix. The old recursive train() call without quality_control=False would re-run the quality control on every retry, so a persistently failing metric recursed; passing the caller's settings and disabling quality control on the single retry is correct.
Two non-blocking notes, no action needed for this PR:
- There is no test for a retry that succeeds (first check fails, second passes). That is the branch the PR exists to make work, so it would be nice to have eventually.
basic_checksstill uses a bareassertfor the_prior is not Nonecheck on line 71 while everything else now raisesQualityControlError. That looks deliberate rather than an oversight — a missing_prioris a programming error, not a quality failure — so I would not change it, but the asymmetry is worth a comment in the code.
I also checked the untrained-posterior path (_trained_on is None): it passes x=None into sample(), which then hits the existing "was not fit on the specified observation" ValueError. Same as the old behaviour, and it correctly propagates rather than being swallowed by _quality_control_error, which only catches QualityControlError.
The tests covered a check that keeps failing and an already-built q, but not the case the single retry exists for: the first check fails, the retrained q passes, and train() only warns that it is retraining.
VIPosterior.__init__ raises a ValueError when it finds no prior, and set_q and to() keep a prior in place, so the assert could not fire. A prior set to None by hand still fails with an error that skips the retry, since only QualityControlError triggers one.
|
Thanks for the review! I did both notes in this PR:
I also merged |
I found this while benchmarking the VI flows for smaller defaults.
VIPosterior.trainchecks the fitted posterior at the end (quality control). When the check failed, the retry it started had three problems:qat the default observation, not at thexpassed totrain. Sotrain(x=x_o)on a posterior with another default x failed the check, and the retry then trainedqon the default x. The caller got a posterior for the wrong observation, silently.x, iteration limits, particles, optimizer arguments) and ran the check again. A check that kept failing retrained until the recursion limit.Summary
The basic checks now raise
QualityControlErrorinstead ofassert, so they also work underpython -O. Aqpassed as an already-built distribution cannot be rebuilt, so a failed check only warns there. Other errors from the quality metrics now reach the caller.Evidence
train(x=x_o), default x differsqtrained on the default xqtrained onx_o, check passesqfails the checkq" from inside the handlerThree new tests in
tests/vi_test.pycover these cases. Each fails onmain.Merge Danger
Door: two-way
Blast Radius: small. It affects only VI training with quality control on, which is the default. Messages are now warnings instead of prints.