Optimise QIS sampling; compute degeneracy in log space - #66
Merged
Conversation
QIS::sample was rebuilding a std::map on every call (once per person per marginal) purely to track dimension remapping across slices. Replace it with a vector indexed by the original dimension, pass it by const ref rather than by value, reserve the two dims vectors, and skip the array copy when there are no fixed dimensions to slice. pick() re-accumulated the distribution on every call. The sum of a slice is exactly the reduced value the parent already picked from, so thread it down the recursion instead. Only done for QIS, where the marginals are integral and the sums are therefore exact; the equivalent change to QISI perturbs the last bits of the double sums, which changes sampled populations for a given seed without being any faster, so it isn't made there. Measured on solve_m, min of 5 runs, output identical to before in every case: pop=100k, 8^4 138.8ms -> 116.6ms pop=200k, 16^4 571.6ms -> 529.6ms pop=50k, 4^4 48.5ms -> 38.4ms degeneracy() computed n!/prod((a_k+1)!) as a running product of tgamma calls. tgamma overflows to inf for any cell count >= 170, which silently collapsed the whole product to zero: a 1000-cell array with a single count of 500 returned 0 for a value that is astronomically large. Compute it via lgamma and exponentiate once. Representable results are unchanged. chiSq now walks the underlying storage rather than incrementing an Index, which is equivalent for the same-shaped arrays it is called with. QISI::pick takes a pointer and length instead of a vector, which removes a copy of the sliced array in the 1-D base case. No behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #66 +/- ##
==========================================
- Coverage 95.03% 94.68% -0.35%
==========================================
Files 26 26
Lines 1027 1035 +8
==========================================
+ Hits 976 980 +4
- Misses 51 55 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 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.
Four independent changes to the sampling hot path and the summary statistics. Every one was benchmarked and checked for output equivalence against
mainbefore being kept.QIS::sample— drop the per-callstd::mapsampleis called once per person per marginal, and each call constructed astd::map<int64_t, int64_t>to track how dimensions are remapped as the array is sliced, then passed it by value into the recursion. The map is a dense lookup overdims.size()entries, so a vector indexed by the original dimension does the same job with no allocation. Alsoreserves the twodims_to_*vectors and skips the array copy when there are no fixed dimensions to slice.pick— don't re-sum the distributionpickbegan with anaccumulateover the distribution. But the sum of a slice is exactly the reduced value the parent just picked from, so it can be threaded down the recursion instead.This is done for QIS only. The same change in QISI is not made: there the marginals are
double, the reused sum differs from a re-accumulation in the last bits, and that occasionally flips a pick at a boundary — at pop=51200 over 8^4 it moved chiSq from 400.11 to 394.57 and produced a different population for the same seed. It also measured no faster there (938ms vs 942ms), so there is nothing to trade for it. QIS's marginals areint64_tand the sums are exact indouble, so no such drift is possible.QISI does keep the signature change —
picktakes a pointer and length rather than aconst std::vector&, which removes a copy of the sliced array in the 1-D base case. Behaviour there is unchanged.Results
solve_m, min of 5 runs, 4-D problem with three 2-D marginals:Output — population array, chiSq, pValue, degeneracy — is identical to
mainin every case. QISI output is identical tomainfor everyskipsvalue tested (0, 7, 100, 1000).degeneracy— fix a silent zerodegeneracycomputedn!/prod((a_k+1)!)as a running product oftgammacalls.tgammaoverflows toinffor any argument above ~170, i.e. any cell count of 170 or more, and oneinfin the denominator collapses the entire product to zero. A 1000-cell array holding a single count of 500 returned0for a value that is in fact astronomically large.Computing the same expression via
lgammaand exponentiating once removes the intermediate overflow. Results that were representable before are bit-identical (20 cells of 5 each:1.7357e-39both ways).The stale
// not convinced that this is correctcomment is replaced with a statement of what the function actually computes — the formula itself is unchanged, only how it is evaluated.chiSqWalks the underlying storage rather than incrementing an
Index. Equivalent for the same-shaped arrays it is called with, and it is only called once per solve, so this is tidying rather than a speedup.Testing
pytest: 21 passed_unittest: 1094 tests, 0 failuresmainand this branch side by side as standalone harnesses drivingQIS::solveandQISI::solvedirectly.🤖 Generated with Claude Code