Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **FE-absorption benchmark suite** (`benchmarks/speed_review/bench_fe_absorption.py`, scenarios
7-13 in `docs/performance-scenarios.md`): seven realistic workloads (county policy event study,
firm panel with churn, scanner store-week, 5M-order geo experiment, survey BRR replicates,
correlated-FE stress, small-panel guard) timing the MAP-demeaning hot path
(`demean_by_groups` / `within_transform`) with subprocess isolation, multi-run CV reporting, and
ATT/SE identity capture so optimization PRs can prove estimates are unchanged
(`--check-estimates`). Includes an optional pyfixest yardstick lane
(`bench_fe_absorption_pyfixest.py`, guarded on import - never a dependency) asserting < 1e-6
coefficient parity on the exact-estimand scenarios, and committed BEFORE baselines
(`baselines/fe_absorption_before.json`). Measurement-only: no library behavior change.
- **`HeterogeneousAdoptionDiD` cluster-robust SE on the continuous paths** (Phase 2a). `cluster=`
is now threaded into `bias_corrected_local_linear` on the `continuous_at_zero` /
`continuous_near_d_lower` designs, so the CCT-2014 robust variance becomes cluster-robust and the
Expand Down
35 changes: 35 additions & 0 deletions benchmarks/speed_review/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,16 @@ benchmarks/speed_review/
├── bench_memory_scaling.py # peak-RSS sweep for the memory-scaling
│ # work (B1 #561 / B2 #563 / C #567);
│ # subprocess-isolated ru_maxrss, median
├── bench_fe_absorption.py # Scenarios 7-13: MAP-demeaning hot path
│ # (subprocess-isolated, multi-run CV,
│ # ATT/SE identity capture + gate)
├── bench_fe_absorption_pyfixest.py # optional external yardstick (guarded
│ # on `import pyfixest`; never a dep)
├── fe_absorption_datagen.py # seeded DGPs shared by both FE lanes
└── baselines/ # this effort's output
├── memory_scaling_{before,after}.json # peak RSS pre-#561 vs current
├── fe_absorption_{before,after}.json # FE-absorption timings + identity
├── fe_absorption_pyfixest.json # yardstick timings + parity (optional)
├── <scenario>_<backend>.json # phase-level wall-clock + peak RSS
├── mem_profile_brfss_large_<backend>.txt # tracemalloc top-N sites
└── profiles/ # flame HTMLs (gitignored)
Expand Down Expand Up @@ -76,6 +84,33 @@ Multi-scale scenarios write per-scale outputs
(e.g. `dose_response_rust.json`). Full runtime for all scales × both
backends is ~90 seconds on Apple Silicon M4.

### FE-absorption suite (scenarios 7-13)

Standalone like `bench_memory_scaling.py` (not part of `run_all.py` - the 5M-row
scenarios are too heavy for the routine sweep):

```bash
# Full suite (~20-40 min on M4; strictly sequential subprocesses by design)
python benchmarks/speed_review/bench_fe_absorption.py \
--out benchmarks/speed_review/baselines/fe_absorption_before.json

# Smoke test / one scenario
python benchmarks/speed_review/bench_fe_absorption.py --quick
python benchmarks/speed_review/bench_fe_absorption.py --only geo_experiment

# After an optimization: regenerate + prove estimates did not move
python benchmarks/speed_review/bench_fe_absorption.py \
--out benchmarks/speed_review/baselines/fe_absorption_after.json \
--check-estimates benchmarks/speed_review/baselines/fe_absorption_before.json

# Optional external yardstick (skipped cleanly if pyfixest is absent)
pip install pyfixest
python benchmarks/speed_review/bench_fe_absorption_pyfixest.py
```

Do not run anything else on the machine during a baseline run - the committed
JSONs carry a CV field per scenario and the driver flags CV > 10% as unusable.

## Where to look for findings

[`docs/performance-plan.md`](../../docs/performance-plan.md) - "Practitioner
Expand Down
149 changes: 149 additions & 0 deletions benchmarks/speed_review/baselines/fe_absorption_before.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
{
"suite": "fe_absorption",
"platform": "macOS-26.5.1-arm64-arm-64bit-Mach-O",
"python": "3.14.4",
"versions": {
"diff_diff": "3.6.1",
"numpy": "2.5.0",
"pandas": "3.0.3"
},
"backend_requested": "python",
"repeats": 3,
"quick": false,
"tolerances": {
"att_atol": 1e-09,
"se_rtol": 1e-07,
"se_rtol_survey": 1e-06,
"gate_exempt": [
"tail_stress"
]
},
"results": [
{
"scenario": "county_policy",
"n_obs": 177289,
"checksum": 6315.071127371084,
"att": 0.28037854139903007,
"se": 0.031244864610216142,
"fit_median_s": 3.8655,
"fit_min_s": 3.7841,
"fit_max_s": 4.0366,
"fit_cv": 0.0253,
"noisy": false,
"n_timed_fits": 9,
"datagen_s": 0.005,
"peak_rss_mb": 3547.5,
"backend_resolved": "python",
"warnings": []
},
{
"scenario": "firm_churn",
"n_obs": 2400000,
"checksum": 240997.06223418942,
"att": 0.3144327184497805,
"se": 0.005837281701254032,
"fit_median_s": 92.9567,
"fit_min_s": 91.5167,
"fit_max_s": 93.0237,
"fit_cv": 0.0092,
"noisy": false,
"n_timed_fits": 3,
"datagen_s": 0.075,
"peak_rss_mb": 11550.3,
"backend_resolved": "python",
"warnings": []
},
{
"scenario": "scanner_twfe",
"n_obs": 3255000,
"checksum": 76832.22571739106,
"att": 0.2511641314956207,
"se": 0.0024149387421607484,
"fit_median_s": 1.5491,
"fit_min_s": 1.5475,
"fit_max_s": 1.5551,
"fit_cv": 0.0026,
"noisy": false,
"n_timed_fits": 3,
"datagen_s": 0.08,
"peak_rss_mb": 980.1,
"backend_resolved": "python",
"warnings": []
},
{
"scenario": "geo_experiment",
"n_obs": 5000000,
"checksum": 443696.3834000407,
"att": 0.2519437259991462,
"se": 0.0017886316514778143,
"fit_median_s": 2.6299,
"fit_min_s": 2.6156,
"fit_max_s": 2.6465,
"fit_cv": 0.0059,
"noisy": false,
"n_timed_fits": 3,
"datagen_s": 0.062,
"peak_rss_mb": 1631.5,
"backend_resolved": "python",
"warnings": [
"Rank-deficient design matrix: dropping 1 of 4 columns (column 1). Coefficients for these columns are set to NA"
]
},
{
"scenario": "survey_absorb",
"n_obs": 500000,
"checksum": -176482.6014001448,
"att": 0.24553586050769946,
"se": 0.007662904200749773,
"fit_median_s": 7.3845,
"fit_min_s": 7.3301,
"fit_max_s": 8.5032,
"fit_cv": 0.0856,
"noisy": false,
"n_timed_fits": 3,
"datagen_s": 0.109,
"peak_rss_mb": 3621.1,
"backend_resolved": "python",
"warnings": [
"Rank-deficient design matrix: dropping 1 of 4 columns (column 1). Coefficients for these columns are set to NA"
]
},
{
"scenario": "tail_stress",
"n_obs": 5000000,
"checksum": 376703.7248141762,
"att": 0.25020825600819235,
"se": 0.004392831033439421,
"fit_median_s": 26.2715,
"fit_min_s": 26.125,
"fit_max_s": 26.3224,
"fit_cv": 0.0039,
"noisy": false,
"n_timed_fits": 3,
"datagen_s": 0.072,
"peak_rss_mb": 1632.3,
"backend_resolved": "python",
"warnings": [
"Rank-deficient design matrix: dropping 1 of 4 columns (column 1). Coefficients for these columns are set to NA",
"demean_by_groups alternating projection (variables: ['y', 'post', '_treat_time']) did not converge in 100 iter"
]
},
{
"scenario": "guard_small",
"n_obs": 20000,
"checksum": -571.6190664591859,
"att": 0.2515087011451766,
"se": 0.028726073333820948,
"fit_median_s": 0.0046,
"fit_min_s": 0.0044,
"fit_max_s": 0.0051,
"fit_cv": 0.0414,
"noisy": false,
"n_timed_fits": 21,
"datagen_s": 0.001,
"peak_rss_mb": 145.1,
"backend_resolved": "python",
"warnings": []
}
]
}
69 changes: 69 additions & 0 deletions benchmarks/speed_review/baselines/fe_absorption_pyfixest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"suite": "fe_absorption_pyfixest",
"pyfixest": "0.60.0",
"platform": "macOS-26.5.1-arm64-arm-64bit-Mach-O",
"repeats": 3,
"quick": false,
"results": [
{
"scenario": "county_policy",
"n_obs": 177289,
"checksum": 6315.071127371084,
"formula": "y ~ i(rel_time, ref=-1) | unit + time",
"coef": null,
"fit_median_s": 0.1904,
"fit_cv": 0.0401,
"parity_ok": null
},
{
"scenario": "firm_churn",
"n_obs": 2400000,
"checksum": 240997.06223418942,
"formula": "y ~ i(rel_time, ref=-1) | unit + time",
"coef": null,
"fit_median_s": 1.9116,
"fit_cv": 0.2027,
"parity_ok": null
},
{
"scenario": "scanner_twfe",
"n_obs": 3255000,
"checksum": 76832.22571739106,
"formula": "y ~ d | unit + post",
"coef": 0.2511641314950137,
"fit_median_s": 0.6441,
"fit_cv": 0.1229,
"parity_ok": true
},
{
"scenario": "geo_experiment",
"n_obs": 5000000,
"checksum": 443696.3834000407,
"formula": "y ~ d | store + week",
"coef": 0.2519437259991667,
"fit_median_s": 0.6228,
"fit_cv": 0.078,
"parity_ok": true
},
{
"scenario": "tail_stress",
"n_obs": 5000000,
"checksum": 376703.7248141762,
"formula": "y ~ d | store + week",
"coef": 0.2502082587524758,
"fit_median_s": 3.0748,
"fit_cv": 0.0149,
"parity_ok": true
},
{
"scenario": "guard_small",
"n_obs": 20000,
"checksum": -571.6190664591859,
"formula": "y ~ d | unit + post",
"coef": 0.2515087011451808,
"fit_median_s": 0.0066,
"fit_cv": 0.0222,
"parity_ok": true
}
]
}
Loading
Loading