Repository navigation
Conversation
The cache in tools/pinned_tbs.py kept a pin only for the identical system object, and every simulation has its own, so each simulation of a New York or Alabama record rebuilt a 0.55 GB copy of the tax-benefit system. Key the cache on the source parameter tree instead, and give each simulation a cheap system of its own that reads the pinned tree. Each simulation still uses its own variables, including the structural reform it applied at its own start instant, its own entities and its own receipts. An in-place edit of the source tree rebuilds the pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a Hypothesis test that holds the cache to a model through sequences of reads, in-place edits and reforms, differential tests against a fresh clone for each pin, and isolation tests for reforms, tracing and finished simulations. Run them with the other pinned-system tests in core-heavy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deliberate mutation that dropped the same-system check survived: every simulation rebuilds its structural reform's variables, so the variables check caught the ordinary case. Readers with identical variables would have shared one pinned system and its receipts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found that a marker in the at-instant cache cannot show that a shared tree is unchanged: a bracket edit on a loaded tree, add_child and interpolation all pass it by, so a shared pin could go stale and give a wrong credit. A pin now clones gov.irs.credits and the dependent care subtree, about 420 of 143,000 nodes, and reads the rest from its source, so nothing can go stale and each simulation can afford its own pin. It is otherwise clone_spm_system, so variables, entities and receipts match a clone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviews of the path-based pin found that sharing live nodes with the source leaves a pin reading the source's stale at-instant views and lets a later reform of the source reach it. A pin now owns every node, history and cache, as a clone does, and borrows only the ParameterAtInstant entries, which core never edits. Both trees count as shared, so a reform of either clones first. The cache goes back to one entry per pin, held weakly throughout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A review showed the claim that core never edits an entry overreaches, since metadata writes show through entries. Say what holds: core sets an entry's value only when it builds it, and writes metadata only while it constructs a system. Test that a reform's metadata write leaves a built pin alone. Two mutants survived. A shallow copy of a system shares its tree and variables, so only the base check tells it apart. A system of its own has variables that lead back to its simulation, so only that case shows a strong snapshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reform with no supplied system does not clone, so the strong-snapshot mutant survived it. A supplied system with a reform does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The isolation review found a child named `parent` under Medicaid's categories replacing that node's ancestor in a copied tree, which sent every edit below it into a recursion. Set the ancestor last, as a clone does. Copy each node's metadata too. Core's interpolation records its work there, so a shared record stopped a pin interpolating after its source had. Metadata is 114 MB of a clone, not the bulk I had assumed, and a copy of its dictionaries and lists matches a deep copy at a third of the time. Take a full clone from a system with no barrier, which cannot stop its own reforms writing to shared entries, and from one in the middle of a reform, which marking the tree would detach from the tree the reform holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy only metadata's dictionaries. Copying its lists as well raised the 1,500-row peak by 1.1 GB, and core writes to dictionaries alone: the lists are references and breakdown labels, three quarters of metadata's memory. Set a node's at-instant cache after its children, as core's clone does, so a child of that name cannot replace it. Fall back through the system's own clone, as main does. Cloning the armed system directly read its tree, which fired the barrier and left a later borrower sharing what the reform then wrote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dictionaries inside shared metadata lists stay shared too, and a reform that builds a pin of its own system after its barrier fired can reach that pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
WIP until independent review finishes. The measurements below ran at 96f03a8.
Problem
tools/pinned_tbs.pykept a pinned system only for the identical system object. Every simulation has its own system, so each simulation that reached a pinned formula rebuilt a 0.55 GB copy: New York's child credit (2018-2024 and from 2028), New York's EITC (2021), Alabama's federal tax deduction (2021).Fix
_get_pinned_tbskeys its cache on the source parameter tree and the pin name, not the system. Ordinary simulations share one tree, so they share one pinned tree.pin_spm_policygives each simulation a cheap pinned system of its own: a private variable registry, private entities, a barrier and a provider snapshot around the shared pinned tree.This also fixes an existing staleness: a variable-only reform applied after a pin was built never reached that pin. The cache now compares the pinned system's variables with the base system's.
Measurements
Local, macOS, Python 3.12, one run at a time at 2 BLAS threads on a shared host, through
PolicyEngineRunnerfrom policyengine-taxsim 3.2.6. CPU time reads more reliably than wall time here. Main is 6572bce.Three simulations of one New York 2024 household built three copies on main, taking 7.4 to 10.5 s each and peaking at 3.48 GB. With this change they build one, the second and third take 0.3 s, and the peak falls to 1.89 GB.
ny_ctcstays $330.Invariants
These hold for every sequence of reads, new simulations, in-place edits and reforms in a process:
test_pin_matches_its_source_after_any_sequence_of_changesruns this as a Hypothesis state model.test_a_shared_pin_gives_what_a_fresh_clone_giveschecks the differential on real households, for all three pins.test_the_cache_keeps_nothing_of_a_finished_simulation).Tests
test_pinned_tbs_sharing.pyadds 12 tests. Locally they take 190 s and peak at 3.83 GB (macOS, Python 3.12). They joinREST_CORE_HEAVY_TESTS, the core-heavy CI group; Linux CI timings will appear in that group's run.Mutation check: 10 of 10 deliberate breakages fail these tests (no edit detection, ignoring a tree change, keying on the system, no variables check, reusing the last system, a strong reference, an inherited at-instant cache, a dropped provider snapshot, no private registry, no private entities).
Regression runs at 96f03a8:
test_spm_simulation_isolation(28),test_spm_system(76),test_pinned_branch_credit_dependents(3),test_rest_python_groups(62),test_non_refundable_credit_downstream_consumers(1) andtest_al_agi_dependent_invariants(3) all pass locally. I ran the 19 YAML files that reach a pin, each in its own process, under a 4 GB guard: 14 pass, and 5 hit the guard (ny_ctc,ny_ctc_post_2024_eligible,ny_ctc_pre_2024,ny_income_tax,ny_working_families_tax_credit). The first three stop at the same test on main, so that memory does not come from this change; I did not repeat the other two on main.test_spm_policy_familyandtest_federal_credit_limit_orderalso hit that guard. CI covers all of these.Limits
bracket.parent, so an in-place edit to a scale bracket on a freshly loaded tree does not reach the root. The cache cannot see that edit, and ordinary reads at instants already cached go stale the same way. Cloned trees carry the parent, so a reform's scale edits reach it.🤖 Generated with Claude Code