This repository was archived by the owner on Jul 2, 2026. It is now read-only.
Commit abd8277
* Extract ChunkedMatrixAssembler and stream CSR assembly (#818)
Phase 1 of issue #818: refactor `UnifiedMatrixBuilder.build_matrix_chunked`
behind a coordinator class so a later commit can parallelize per-chunk
work across Modal workers.
Changes:
- New `policyengine_us_data/calibration/chunked_matrix_assembler.py`
with `SharedBuildState`, `ChunkPlan`, `ChunkResult` dataclasses and
a `ChunkedMatrixAssembler` class exposing `run_chunks`,
`run_single_chunk`, and `assemble_final`. The per-chunk body
(H5 materialization, per-chunk `Microsimulation`, variable
calculation, COO shard write) moves into `run_single_chunk`.
- Replace the list-then-concat final-assembly block with a two-pass
streaming CSR build: pass 1 counts per-row nnz across shards to
compute `indptr`; pass 2 scatters entries into preallocated
`data`/`indices` arrays, avoiding the scipy COO->CSR memory peak.
Measured peak RSS on a 5M-nnz synthetic fixture is 1.22x the final
CSR, vs. the 2-3x peak the old path reintroduced.
- `build_matrix_chunked` becomes a ~80-line facade; target querying,
uprating, constraint extraction, and manifest handling stay on
`UnifiedMatrixBuilder`. Public signature unchanged; all 6 existing
chunked-matrix integration tests pass without edits.
- Lift `_build_entity_relationship` to module scope as
`build_entity_relationship(sim)` so `SharedBuildState` stays
pickle-clean for cross-process dispatch in phase 2. The memoizing
wrapper on `UnifiedMatrixBuilder` remains for non-chunked callers.
- 10 new unit tests in `tests/unit/calibration/test_chunked_matrix_assembler.py`
cover partition correctness, streaming CSR (including a memory
bound), resume-skip, range-mismatch rejection, and dispatcher
routing via `run_chunks`.
The Modal parallel-dispatch function, `build_matrix_chunk_worker`
Modal function, and `--parallel` / `--num-matrix-workers` CLI flags
will land in the next commit (phase 2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Parallelize chunked matrix build across Modal workers (#818)
Phase 2 of issue #818. Builds on the phase-1 coordinator extraction
to fan chunked matrix building out across Modal workers, cutting the
full-CPS build's wall time from the ~14 h serial ceiling into a
wall-time window proportional to one chunk.
Changes:
- `policyengine_us_data/calibration/chunked_matrix_modal.py`: new
`dispatch_chunks_modal` coordinator. Pickles `SharedBuildState` to
`{chunk_root}/chunk_build_state.pkl` on the pipeline volume,
partitions `range(n_chunks)` into contiguous batches, spawns one
`build_matrix_chunk_worker` per batch via `modal.Function.from_name`,
collects results, aggregates per-chunk errors, and streams the
final CSR from shards on the volume. Contiguous batching (not
round-robin) keeps resume-friendly prefixes on disk.
- `modal_app/matrix_chunk_worker.py`: new `build_matrix_chunk_worker`
`@app.function` registered on the existing
`policyengine-us-data-fit-weights` app (same app as
`build_package_remote`). Mirrors `build_areas_worker` resources
(memory=16384, cpu=1.0, timeout=28800, max_containers=50,
nonpreemptible). Worker reads the pickled shared state, constructs
a `ChunkedMatrixAssembler` with `resume=True`, calls `run_chunks`
on its batch, commits the volume, returns nnz + errors.
- `SharedBuildState` grows a `chunk_size` field so a worker can
reconstruct the assembler from the pickle alone (no extra args).
- `build_matrix_chunked` accepts `parallel`, `num_matrix_workers`,
`run_id`; routes to `dispatch_chunks_modal` when `parallel=True`,
preserves the in-process serial path otherwise.
- `unified_calibration` CLI gains `--parallel` (default off) and
`--num-matrix-workers` (default 50). `--parallel` without
`--chunked-matrix` logs an info message and runs the non-chunked
path unchanged. `run_id` flows from env
`POLICYENGINE_US_DATA_RUN_ID`, set by `build_package_remote`.
- `build_package_remote` / `_build_package_impl` take
`chunked_matrix`, `chunk_size`, `parallel_matrix`,
`num_matrix_workers` kwargs and forward them to the
`unified_calibration` subprocess.
Tests:
- 9 unit tests in `tests/unit/calibration/test_chunked_matrix_modal.py`
cover `partition_chunk_ids_contiguous` (exact division, remainder,
more workers than chunks, zero chunks, invalid num_workers) and
`dispatch_chunks_modal` with injected `worker_function` + `volume`
fakes (spawn/assemble happy path, zero-chunks short-circuit, error
aggregation, shared-state pickle side effect).
- New `test_shared_build_state_roundtrips_pickle` in the assembler
unit tests guards the phase-2 boundary: if `SharedBuildState`
stops pickling cleanly, we catch it here rather than in a Modal
worker. 11 assembler unit tests total.
- `tests/integration/test_matrix_chunk_worker_modal.py` is an
env-gated smoke test (MODAL_TOKEN_ID + MODAL_TOKEN_SECRET +
POLICYENGINE_US_DATA_MODAL_SMOKE=1) that validates the deployed
worker is lookupable via `modal.Function.from_name` and that
production-scale batching is sane. Full end-to-end validation
(write shared state, spawn, verify shards on the volume) is a
pre-merge manual step documented in the PR.
Verified:
- 392 calibration unit tests pass (up from 382; 10 new phase-2
tests; 0 regressions), plus the 6 existing chunked-matrix
integration tests still green against the phase-1 facade.
- `ruff check` and `ruff format --check` clean on all changed files.
Validation still pending on real Modal (deploy + one-chunk
benchmark + full-CPS `workflow_dispatch` run); gated on the Modal
venv-activation PR landing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Thread parallel-matrix flags through pipeline.py + workflow_dispatch
Closes the wiring gap between issue #818's CLI flags and the
pipeline orchestrator. `run_pipeline` now accepts `chunked_matrix`,
`chunk_size`, `parallel_matrix`, and `num_matrix_workers` kwargs
and forwards them to `build_package_remote.remote()`. All four
default off/50, so the auto-triggered pipeline run on the next
`Update package version` commit after this PR merges will continue
to use the existing non-chunked path.
`pipeline.yaml`'s `workflow_dispatch` exposes the same four knobs
(defaults also off/50). To trigger the one-off chunked-parallel
validation run after this PR merges:
gh workflow run pipeline.yaml \
-f chunked_matrix=true \
-f parallel_matrix=true \
-f num_matrix_workers=50
Subsequent automatic pipeline runs (on version-bump commits) pick
up the defaults and stay non-chunked until someone dispatches
manually with the flags on again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Register build_matrix_chunk_worker via pipeline.py import
Without this import, ``modal deploy modal_app/pipeline.py`` (run by
``pipeline.yaml`` on dispatch) would skip ``build_matrix_chunk_worker``
because it lives in its own module — ``_calibration_app`` only knows
about functions that have been loaded. Importing the worker here runs
its ``@app.function`` decorator at module load, so
``app.include(_calibration_app)`` picks it up and the worker is
deployed alongside ``build_package_remote``. Without it,
``modal.Function.from_name`` in ``dispatch_chunks_modal`` would fail
at runtime on the first parallel-matrix attempt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix Modal app name used by dispatch_chunks_modal lookup (#818)
``dispatch_chunks_modal._lookup_worker_function`` was looking the
worker up under ``policyengine-us-data-fit-weights`` — the name
declared on ``_calibration_app`` in
``modal_app/remote_calibration_runner.py`` where the worker's
``@app.function`` decorator attaches at the Python level. That is
not the name the function is registered under in Modal's registry.
``modal_app/pipeline.py`` merges the fit-weights sub-app into the
pipeline app via ``app.include(_calibration_app)`` before deploy.
After ``modal deploy modal_app/pipeline.py`` (what both the
``pipeline.yaml`` dispatch step and the ``pr.yaml`` preview step
run), Modal's registry only knows about
``policyengine-us-data-pipeline`` — there is no independent
``policyengine-us-data-fit-weights`` entry. A ``Function.from_name``
call with the sub-app's name would raise at runtime on the first
parallel-matrix invocation.
Caught by inspecting ``modal app list`` on a deploy: the fit-weights
app is absent; only ``policyengine-us-data-pipeline`` (and the
dataset-only ``policyengine-us-data`` ephemeral from
``push.yaml``'s ``modal run modal_app/data_build.py``) appear.
Changes:
- ``MODAL_APP_NAME`` now ``policyengine-us-data-pipeline``, with a
comment pointing at the ``app.include`` hop that makes this
necessary.
- ``matrix_chunk_worker.py`` docstring rewritten to explain the
two-level naming (Python-object name vs registry name after
``app.include``).
- ``test_matrix_chunk_worker_modal.py`` looks up under the pipeline
app name and updates the "Deploy first with" instruction to
``modal deploy modal_app/pipeline.py`` (the correct deploy path —
the previous instruction would have created an orphan fit-weights
registration that production code never looks up).
Unit tests (which inject a fake ``worker_function``) are unaffected
and still pass. The env-gated integration smoke was previously
guaranteed to fail on first real deploy; it can now pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix diagnostics upload script indentation
* Force Modal worker smoke lookup
* Allow Modal smoke test to use profile auth
* Set volume chunk dir for Modal matrix builds
* Release volume handles before Modal matrix reload
* Document Modal matrix runtime fixes
* Harden parallel matrix Modal dispatch
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Max Ghenis <mghenis@gmail.com>
1 parent ceef4ae commit abd8277
17 files changed
Lines changed: 2131 additions & 329 deletions
File tree
- .github
- scripts
- workflows
- changelog.d
- modal_app
- policyengine_us_data/calibration
- tests
- integration
- unit
- calibration
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
18 | 22 | | |
19 | 23 | | |
20 | 24 | | |
| |||
36 | 40 | | |
37 | 41 | | |
38 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
39 | 50 | | |
40 | 51 | | |
41 | 52 | | |
| |||
58 | 69 | | |
59 | 70 | | |
60 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
61 | 76 | | |
62 | 77 | | |
63 | 78 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
42 | 58 | | |
43 | 59 | | |
44 | 60 | | |
| |||
80 | 96 | | |
81 | 97 | | |
82 | 98 | | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
83 | 103 | | |
84 | 104 | | |
85 | 105 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
| 48 | + | |
48 | 49 | | |
49 | 50 | | |
50 | 51 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
58 | 59 | | |
59 | 60 | | |
60 | 61 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
| 118 | + | |
118 | 119 | | |
119 | 120 | | |
120 | 121 | | |
| |||
129 | 130 | | |
130 | 131 | | |
131 | 132 | | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
132 | 158 | | |
133 | 159 | | |
134 | 160 | | |
| |||
207 | 233 | | |
208 | 234 | | |
209 | 235 | | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
210 | 243 | | |
211 | 244 | | |
212 | 245 | | |
| |||
767 | 800 | | |
768 | 801 | | |
769 | 802 | | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
770 | 807 | | |
771 | 808 | | |
772 | 809 | | |
| |||
792 | 829 | | |
793 | 830 | | |
794 | 831 | | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
795 | 841 | | |
796 | 842 | | |
797 | 843 | | |
| |||
1030 | 1076 | | |
1031 | 1077 | | |
1032 | 1078 | | |
1033 | | - | |
1034 | | - | |
1035 | | - | |
1036 | | - | |
1037 | | - | |
1038 | | - | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
1039 | 1089 | | |
1040 | 1090 | | |
1041 | 1091 | | |
| |||
1068 | 1118 | | |
1069 | 1119 | | |
1070 | 1120 | | |
| 1121 | + | |
| 1122 | + | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
| 1126 | + | |
| 1127 | + | |
1071 | 1128 | | |
1072 | 1129 | | |
1073 | 1130 | | |
| |||
0 commit comments