Skip to content

perf(utils): precompute r-independent wild-bootstrap inversion pieces (~7x, outputs unchanged)#647

Open
igerber wants to merge 2 commits into
mainfrom
perf/wild-bootstrap-tstar-chunking
Open

perf(utils): precompute r-independent wild-bootstrap inversion pieces (~7x, outputs unchanged)#647
igerber wants to merge 2 commits into
mainfrom
perf/wild-bootstrap-tstar-chunking

Conversation

@igerber

@igerber igerber commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • The WCR bootstrap DGP is linear in the candidate null r (y*(r) = A + r·B with r-independent A, B), so beta_j*(r) is affine and the per-cluster score variance is the PSD quadratic qa + r·qb + r²·qc — five precomputable (B,) vectors. The CI inversion's ~O(100) _t_star(r) evaluations, which each materialized fresh (B×n) bootstrap-outcome, (k×B) refit, and (n×B) residual arrays, now cost O(B) arithmetic each. This lands BOTH options the TODO row offered: the one-time precompute pass is chunked over draws (module byte-budget constant _WILD_PRECOMPUTE_CHUNK_BYTES, sized with a conservative ×8 live-temporary multiplier and monkeypatchable), so peak memory is bounded for large n/B, and no full (B, n) observation-level weight matrix is ever materialized.
  • Numbers: verified against a pristine origin/main worktree on a 5-seed few-cluster grid with the backend pinned — SE, p-value, and inverted CI endpoints all bit-identical; 6.8x end-to-end (0.177s → 0.026s for 5×999-draw inversions). The boottest parity suite (61 tests incl. pinned values) passes unchanged. The quadratic-form evaluation is a ~1-ULP reassociation of the per-call sum(scores²); the strict-inequality tie guard absorbs sub-1e-9 shifts by design. A new TestPrecomputeChunking forces hundreds of draw-chunks and pins chunk-count invariance.
  • Side finding recorded as a new TODO row (Medium): while validating, discovered the Rust-backend solve_ols(return_vcov=True) is run-to-run nondeterministic (~1e-14 rel; 3 distinct clustered-vcov values in 8 identical calls on Apple Silicon; pure-Python backend is bit-stable). Not introduced by this PR (it studentizes with the pre-existing analytical CR1) — tracked for investigation with a repro recipe.
  • Also parks the CS RC pscore (g,t)-dedup row with findings: the RC propensity is fit on the pooled two-period sample whose period-t blocks change with every t, so no two cells share a logit — caching would change the estimator away from DRDID::*_rc.

Methodology references (required if estimator / math changes)

  • Method name(s): Cameron-Gelbach-Miller (2008) WCR wild cluster bootstrap / Roodman et al. (2019) boottest conventions — algebraic reorganization only; restricted-residual linearity in r is the documented basis of the inversion
  • Paper / source link(s): REGISTRY.md § wild cluster bootstrap (WCR contract)
  • Any intentional deviations from the source (and why): None — outputs bit-identical on the verification grid; the reported SE remains the analytical CR1.

Validation

  • Tests added/updated: tests/test_wild_bootstrap.py::TestPrecomputeChunking (chunk-count invariance; tolerances documented against the ambient rust-vcov wobble). Full wild suite 61 passed; tests/test_utils.py, tests/test_methodology_did.py, tests/test_estimators.py pass.
  • Evidence: A/B script vs origin/main worktree (backend pinned): outputs identical, 6.8x.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Generated with Claude Code

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Overall Assessment

✅ Looks good. I found no unmitigated P0/P1 issues.

Executive Summary

  • Affected method: WCR wild cluster bootstrap in diff_diff.utils.wild_bootstrap_se, used by DiD/TWFE.
  • The precompute is an algebraic reorganization of the registry-documented WCR inversion: restricted residuals linear in r, fixed bootstrap weights, CR1 studentization, strict tail counts.
  • NaN/zero-SE handling remains all-or-nothing for public inference and guarded for bootstrap draws.
  • Deferred Rust backend nondeterminism is tracked in TODO.md, so it is not a blocker for this PR.
  • I could not run tests in this sandbox because Python cannot import numpy.

Methodology

No findings.

Cross-check: docs/methodology/REGISTRY.md documents WCR restricted residuals linear in r, bootstrap refit under y*, CR1 studentization, strict exceedance p-values, and CI inversion. The changed code at diff_diff/utils.py:L878-L938 preserves those contracts by precomputing the affine coefficient pieces and PSD quadratic score variance. This is an implementation choice, not an undocumented methodology deviation.

Code Quality

No findings.

The chunk sizing and precompute loop are localized and keep the existing result surface unchanged: diff_diff/utils.py:L900-L926.

Performance

No findings.

The change removes repeated (B, n) materialization inside _t_star(r) and chunks the one-time pass by _WILD_PRECOMPUTE_CHUNK_BYTES: diff_diff/utils.py:L639-L642, diff_diff/utils.py:L911-L926.

Maintainability

No findings.

The explanatory comments are long but useful here because this is math-sensitive code.

Tech Debt

  • Severity: P3 informational, tracked in TODO.md
    Impact: Rust backend clustered VCV nondeterminism is documented as pre-existing follow-up work, not introduced by this PR.
    Concrete fix: No PR-blocking fix required; follow the tracked TODO investigation.
    Location: TODO.md:L31-L33

Security

No findings.

Documentation/Tests

  • Severity: P3
    Impact: CHANGELOG.md says the wild bootstrap suite has 60 tests, while this PR adds a new test and the PR body claims 61. This is only a stale validation count.
    Concrete fix: Update the count to 61 or remove the exact count.
    Location: CHANGELOG.md:L447-L448

Test coverage added for chunk-count invariance at tests/test_wild_bootstrap.py:L1485-L1517; existing brute-force/R parity tests remain in scope for WCR correctness.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Jul 7, 2026
@igerber igerber force-pushed the perf/wild-bootstrap-tstar-chunking branch 3 times, most recently from d382c4e to 8027a33 Compare July 8, 2026 00:16
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 8027a33dac022236e0616a0280808e0559530c3b


Overall Assessment

✅ Looks good. No unmitigated P0/P1 findings.

Executive Summary

  • Affected method: WCR wild cluster bootstrap in diff_diff.utils.wild_bootstrap_se, used by DifferenceInDifferences and inherited by TwoWayFixedEffects.
  • The precompute preserves the registry contract: restricted residuals linear in r, fixed cluster weights during inversion, full-design WCR statistic, CR1 studentization, strict tail counts, and CI inversion.
  • The new chunking is an algebraic/numerical implementation choice, not an undocumented methodology deviation.
  • Prior P3 changelog count issue is still present.
  • I could not run tests in this sandbox: pytest and numpy are unavailable.

Methodology

No findings.

Cross-check: docs/methodology/REGISTRY.md:L94-L146 documents WCR as imposing the null via restricted residuals M_-j y - r M_-j x_j, refitting on y*, using bootstrap CR1 se*, strict exceedance p-values, and CI inversion. The new precompute at diff_diff/utils.py:L878-L938 preserves those objects by caching the affine coefficient and cluster-score pieces and evaluating the same PSD quadratic score variance. This is a valid algebraic reorganization, not a methodology mismatch.

Code Quality

No findings.

The chunk-budget constant and chunk loop are localized at diff_diff/utils.py:L639-L642 and diff_diff/utils.py:L900-L926.

Performance

No findings.

The PR removes repeated observation-level (B, n) materialization inside _t_star(r) and builds only per-chunk (Bc, n) arrays during precompute: diff_diff/utils.py:L911-L926.

Maintainability

No findings.

The explanatory comments are long, but appropriate for math-sensitive inference code.

Tech Debt

  • Severity: P3 informational, tracked in TODO.md
    Impact: Rust backend clustered VCV nondeterminism is documented as pre-existing follow-up work, not introduced by this PR.
    Concrete fix: No PR-blocking fix required; follow the tracked investigation.
    Location: TODO.md:L31-L33

Security

No findings.

Documentation/Tests

  • Severity: P3
    Impact: CHANGELOG.md still says the wild bootstrap parity suite has 60 tests, while this PR adds TestPrecomputeChunking. This is stale validation prose only.
    Concrete fix: Update the count or remove the exact count.
    Location: CHANGELOG.md:L446-L448

  • Severity: P3
    Impact: The new test docstring/name say “bit-for-bit,” but the assertions use tolerances for se and CI endpoints. The intended reason is explained in comments, so this is only wording drift.
    Concrete fix: Rename the test/docstring to “chunk-count invariant” or make the exactness claim only for p_value.
    Location: tests/test_wild_bootstrap.py:L1485-L1517

igerber added a commit that referenced this pull request Jul 8, 2026
…gregation

The cluster-score aggregation in compute_robust_vcov (also reached via
solve_ols(return_vcov=True)) built its (G,k) cluster-scores matrix in
HashMap iteration order, which is SipHash-randomized per call:
mathematically identical, but the GEMM accumulation order changed on
every invocation, wobbling the clustered vcov at ~1e-14 (3 distinct
values across 8 identical calls; the Python backend is bit-stable).
Rows now accumulate in first-appearance order — ascending for the
factorized 0..G-1 ids the Python dispatcher passes, matching NumPy's
groupby order. Verified 1 distinct value across 50 identical calls;
NumPy parity unchanged (~1e-14 cross-backend GEMM tolerance).
Bit-identity regression tests cover contiguous and non-contiguous
unsorted cluster ids. Resolves the TODO row currently in flight on
PR #647 (row deletion follows once both merge).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@igerber igerber force-pushed the perf/wild-bootstrap-tstar-chunking branch from 8027a33 to 5b3f686 Compare July 8, 2026 10:01
igerber and others added 2 commits July 8, 2026 07:13
…st inversion (~7x, bit-identical outputs)

The WCR bootstrap DGP is linear in the candidate null r
(y*(r) = A + r*B), so beta_j*(r) is affine and the per-cluster score
variance is the PSD quadratic qa + r*qb + r^2*qc — five precomputable
(B,) vectors. The CI inversion's ~O(100) _t_star(r) evaluations (each
previously materializing fresh (B,n)/(k,B)/(n,B) arrays) become O(B)
arithmetic; the one-time precompute pass is chunked over draws (~256MB
cap) so peak memory is bounded for large n/B — resolving both options
the TODO row offered at once.

Verified vs origin/main on a 5-seed few-cluster grid (backend pinned):
SE, p-value, and inverted CI endpoints all bit-identical; 6.8x
end-to-end. boottest parity suite (60 tests, pinned values) unchanged.

Also parks the CS RC pscore (g,t)-dedup TODO row with findings: the RC
propensity is fit on the pooled two-period sample whose period-t blocks
change with every t, so no two cells share a logit — caching would
change the estimator away from DRDID::*_rc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@igerber igerber force-pushed the perf/wild-bootstrap-tstar-chunking branch from 5b3f686 to e7a866b Compare July 8, 2026 11:13
igerber added a commit that referenced this pull request Jul 8, 2026
…gregation

The cluster-score aggregation in compute_robust_vcov (also reached via
solve_ols(return_vcov=True)) built its (G,k) cluster-scores matrix in
HashMap iteration order, which is SipHash-randomized per call:
mathematically identical, but the GEMM accumulation order changed on
every invocation, wobbling the clustered vcov at ~1e-14 (3 distinct
values across 8 identical calls; the Python backend is bit-stable).
Rows now accumulate in first-appearance order — ascending for the
factorized 0..G-1 ids the Python dispatcher passes, matching NumPy's
groupby order. Verified 1 distinct value across 50 identical calls;
NumPy parity unchanged (~1e-14 cross-backend GEMM tolerance).
Bit-identity regression tests cover contiguous and non-contiguous
unsorted cluster ids. Resolves the TODO row currently in flight on
PR #647 (row deletion follows once both merge).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: e7a866b2a04233b2b0eeb7bd2bcbe7c319e75399


Overall Assessment

✅ Looks good. No unmitigated P0/P1 findings.

Executive Summary

  • Affected method: WCR wild cluster bootstrap in diff_diff.utils.wild_bootstrap_se, used by DifferenceInDifferences and inherited by TwoWayFixedEffects.
  • The precompute preserves the registry contract: restricted residuals linear in r, fixed bootstrap weights during inversion, full-design WCR statistic, analytical CR1 reported SE, strict tail counts, and CI inversion.
  • The prior P3 changelog/test wording issues are resolved.
  • New Rust backend nondeterminism work is tracked in TODO.md, so it is informational only.
  • I could not run tests locally: this sandbox lacks both pytest and numpy.

Methodology

No findings.

Cross-check: docs/methodology/REGISTRY.md:L94-L146 documents WCR restricted residuals M_-j y - r M_-j x_j, bootstrap refit on y*, CR1 studentization, strict exceedance p-values, and inversion-based CIs. The new cached affine pieces and PSD quadratic variance at diff_diff/utils.py:L878-L938 are an algebraic reorganization of the same statistic, not an undocumented methodology deviation.

Code Quality

No findings.

The chunk-budget constant and chunked precompute are localized at diff_diff/utils.py:L639-L642 and diff_diff/utils.py:L900-L926.

Performance

No findings.

The change removes repeated (B, n) materialization inside _t_star(r) and builds observation-level draw arrays only per chunk: diff_diff/utils.py:L911-L926.

Maintainability

No findings.

The explanatory comments are long, but appropriate for math-sensitive inference code.

Tech Debt

  • Severity: P3 informational, tracked in TODO.md
    Impact: Rust backend clustered VCV nondeterminism is documented as pre-existing follow-up work, not introduced by this PR.
    Concrete fix: No PR-blocking fix required; follow the tracked investigation.
    Location: TODO.md:L33

Security

No findings.

Documentation/Tests

No findings.

Prior P3s are addressed: CHANGELOG.md:L492-L506 no longer claims the stale 60-test count, and tests/test_wild_bootstrap.py:L1485-L1518 now describes chunk-count invariance rather than bit-for-bit equality. Local validation was not possible because pytest and numpy are unavailable in this environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant