diff --git a/README.md b/README.md index ac8b866..8ceebbd 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The protocol uses no words or alphabet, but four meaning-free local symbols. For See [`docs/results-ecp6.md`](docs/results-ecp6.md) for the conclusion, [`docs/protocol-specification-ecp6.md`](docs/protocol-specification-ecp6.md) for the wire format and [`evidence/ecp6/report.md`](evidence/ecp6/report.md) for the compact confirmatory evidence. ECP-7 development now tests how much of that result depends on the explicit -one-factor-per-slot architecture. Its first twenty-five sealed batches are valid +one-factor-per-slot architecture. Its first twenty-six sealed batches are valid development results but none passes the full gate. Batch 15 established the strong position-aware base: at 30,000 optimization steps it reaches 83.46% train exactness, 82.59% validation and 83.37% translator validation while using @@ -55,7 +55,11 @@ worst-link and injectivity gates still failed. Batch 25 extended that catch-up to 45,000 steps and improved again to 85.26% train, 85.24% validation and 85.13% translator validation. Receiver performance now nearly reaches the mathematical ceiling of the non-injective sender -codebooks, so another horizon-only extension is not justified. See +codebooks, so another horizon-only extension is not justified. Batch 26 added +one sender/receiver route cycle and reached 85.38% validation, but did not add +a single net unique code: shared errors became link-specific errors instead. +The replay-routing family is therefore closed; future work must target sender +capacity directly. See [`docs/development-log-ecp7.md`](docs/development-log-ecp7.md). ## Build on this work @@ -121,6 +125,7 @@ Do not translate those artifacts in place. New experiment configurations and all - [`config/ecp7-b23-development.yaml`](config/ecp7-b23-development.yaml) — Batch 22 sender-only replay through step 20,000 followed by restored joint replay gradients. - [`config/ecp7-b24-development.yaml`](config/ecp7-b24-development.yaml) — Batch 23 routing with sender messages detached for receiver-only replay after step 20,000. - [`config/ecp7-b25-development.yaml`](config/ecp7-b25-development.yaml) — Batch 24 receiver-only catch-up with an isolated 45,000-step maximum horizon. +- [`config/ecp7-b26-development.yaml`](config/ecp7-b26-development.yaml) — Batch 25 with one preregistered sender/receiver replay-route cycle. - [`config/ecp7-positive-control-development.yaml`](config/ecp7-positive-control-development.yaml) — ECP-6 positive control on the ECP-7 split. - [`config/ecp0.yaml`](config/ecp0.yaml) — machine-readable configuration from step 1. - [`config/ecp1.yaml`](config/ecp1.yaml) — frozen configuration of the population trial. diff --git a/config/ecp7-b26-development.yaml b/config/ecp7-b26-development.yaml new file mode 100644 index 0000000..85c026a --- /dev/null +++ b/config/ecp7-b26-development.yaml @@ -0,0 +1,25 @@ +extends: ecp7-b25-development.yaml + +experiment: + title: ECP-7 coordinate-descent hard-replay cycle + arm: coordinate_descent_hard_replay_cycle + objective: >- + Test whether a second sender-only separation pulse against saturated + receivers can break remaining code collisions before a final receiver-only + catch-up phase. + +training: + global_hard_meaning_replay: + gradient_route_phases: + - end_step: 20000 + sender_message_gradients: true + receiver_parameter_gradients: false + - end_step: 30000 + sender_message_gradients: false + receiver_parameter_gradients: true + - end_step: 35000 + sender_message_gradients: true + receiver_parameter_gradients: false + - end_step: 45000 + sender_message_gradients: false + receiver_parameter_gradients: true diff --git a/docs/AI_AGENT_START.md b/docs/AI_AGENT_START.md index cbb60cd..a3fd820 100644 --- a/docs/AI_AGENT_START.md +++ b/docs/AI_AGENT_START.md @@ -41,7 +41,7 @@ python3.12 -m venv .venv .venv/bin/ecp6 --config config/ecp6.yaml validate ``` -Expected baseline: 122 passing tests and split sizes `14336/1024/1024`. +Expected baseline: 125 passing tests and split sizes `14336/1024/1024`. ## 4. Reproduce ECP-6 @@ -71,7 +71,7 @@ This reruns a known experiment. It must not be presented as a new independent co ## 5. Continue ECP-7 scientifically -Do not modify ECP-0 through ECP-6 or the completed ECP-7 Batch 1 through Batch 25 +Do not modify ECP-0 through ECP-6 or the completed ECP-7 Batch 1 through Batch 26 configs. ECP7-B1-I collapsed to 130–139 hard messages. ECP7-B2-I improved its soft objective but collapsed further to 85–104 hard messages. ECP7-B3-I applied that loss to straight-through hard messages and improved to 585–972 messages, @@ -149,11 +149,18 @@ ECP7-B25-I extended only that catch-up horizon to 45,000 steps. It improved to 85.13% translator validation. The worst train link exactly matches its 82.81% unique-code ceiling, so sender injectivity remains the structural blocker and the confirmatory ECP-7 test remains sealed. +ECP7-B26-I inserted a second sender-only pulse followed by receiver-only +catch-up. It reached small new bests of 85.37% mean train and 85.38% validation +and translator validation, but its unique-message counts were effectively +unchanged. Shared failures fell while any-link failures rose, closing the +replay-routing family as an error-redistribution mechanism rather than an +injectivity solution. The confirmatory ECP-7 test remains sealed. Continue with this sequence: 1. Read `docs/research-design-ecp7.md` and `docs/development-log-ecp7.md`. -2. Define exactly one ECP7-B26 intervention and its failure criterion. +2. Define exactly one ECP7-B27 direct sender-capacity intervention and its + failure criterion. 3. Register the variant and immutable configuration hashes before training. 4. Add tests for every new invariant. 5. Use only `smoke` and `develop`; keep the ECP-7 test split sealed. @@ -166,29 +173,20 @@ Continue with this sequence: ## 6. Recommended next experiment -Batch 25 confirms that extra receiver-only catch-up time improves decoder fit -but leaves the sender collision structure almost unchanged. The observed worst -train link exactly reaches its sender's unique-code ceiling, so another horizon -extension cannot solve the gate. A clean ECP7-B26 coordinate-descent progression -is: - -- inherit the complete Batch 25 implementation and 45,000-step ceiling; -- keep sender-only replay through step 20,000 and receiver-only replay through - step 30,000 exactly as in Batch 25; -- route the additional replay branch sender-only again for steps 30,001–35,000, - then restore receiver-only replay from step 35,001; -- reuse the registered 5,000-step sender-separation duration; -- keep ordinary base-task updates to both senders and receivers unchanged; -- keep weight `0.25`, the 64-meaning replay batch, 200-step refresh, - seed-derived sampler, predicate and temperature unchanged; -- continue to mine and replay training meanings only; -- measure shared and any-link pool sizes, train exactness, injectivity, - validation and new-reader induction, and rerun the frozen ECP-6 control. - -Do not combine the routing cycle with another predicate, coefficient, graded -sampling, collision pressure, a new architecture, factor-specific weights, -optimizer changes, a horizon change, variable-length messages or -negotiation. +Batch 26 shows that another one-sided replay cycle cannot increase sender code +capacity. Receiver-only phases fit the existing codebook ceiling; sender-only +ordinary-task replay converts shared errors into link-specific errors. Do not +try another route boundary, route cycle or horizon extension. + +For ECP7-B27, preregister exactly one direct sender-capacity intervention on the +strongest stable base. A defensible candidate is a late, bounded pulse that +explicitly separates globally mined hard-message collisions while preserving +the ordinary reconstruction task. Reuse an already tested loss and coefficient +where possible, activate it only after a shared unchanged trajectory, and +define success primarily as a reproducible increase in unique messages without +loss of worst-link validation. Do not combine this with a new architecture, +new predicate, optimizer change, factor-specific supervision, variable-length +messages or negotiation in the same batch. ## 7. Definition of done for any contribution diff --git a/docs/development-log-ecp7.md b/docs/development-log-ecp7.md index 2a44476..f7c9bf5 100644 --- a/docs/development-log-ecp7.md +++ b/docs/development-log-ecp7.md @@ -33,6 +33,7 @@ this log. | ECP7-B23-I | Intervention | B22 sender-only replay warmup followed by restored joint replay gradients | Position-aware MLP | valid negative; joint phase recreates shared errors without improving accuracy | | ECP7-B24-I | Intervention | B23 sender-only warmup followed by receiver-only replay catch-up | Position-aware MLP | valid negative; new-best means but link-specific errors and injectivity fail | | ECP7-B25-I | Intervention | B24 receiver-only catch-up with a 45,000-step maximum horizon | Position-aware MLP | valid negative; new-best means reach the non-injective code-cap | +| ECP7-B26-I | Intervention | B25 with a second sender-only pulse and final receiver-only catch-up | Position-aware MLP | valid negative; route cycle changes error sharing but not code capacity | ## Batch 1 preregistration @@ -2088,3 +2089,98 @@ receiver-only through 30,000, a second sender-only separation pulse through registered 5,000-step duration and directly tests whether saturated receivers provide a better fixed target for breaking remaining sender collisions. No other setting may change. + +## Batch 26 preregistration + +Preregistered: July 18, 2026 at 17:46:02 UTC
+Seed: `11`
+Maximum population steps: `45,000`
+Minimum selection steps: `15,000`
+Early-stopping patience: `5,000` steps
+Hard-meaning predicate: all `16` current population links have an incorrect factor
+Replay route through step 20,000: **sender-only**
+Replay route steps 20,001–30,000: **receiver-only**
+Replay route steps 30,001–35,000: **sender-only**
+Replay route steps 35,001–45,000: **receiver-only**
+Ordinary-task sender and receiver gradients: **enabled throughout**
+Replay weight: `0` through step `15,000`, linear to `0.25` at step `20,000`, then hold
+Replay batch size: `64` meanings, uniform with replacement
+Training-only pool refresh interval: `200` steps
+Replay sampler seed offset: `+1211`
+Raw configuration SHA-256: `9721d3af452b72788568f7fc5262056aa770e443c0a3bc35112183a08c2badb7`
+Effective configuration SHA-256: `be1310f79b4b7a72134393292480244eeba123d4828b3c94dbf32787dd6a4e3b`
+Split-SHA-256: `4947058c75ab07cb43a87eb82776b12cb2a7e2eeba7114de110d3b852cbc64cd`
+Test unsealed: **no** + +ECP7-B26-I inherits Batch 25 and changes only the replay gradient-route +schedule. The complete trajectory through step 30,000 remains identical. A +second 5,000-step sender-only pulse then uses the saturated receivers as fixed +targets, followed by 10,000 steps of receiver-only catch-up. The ordinary task +remains joint throughout. Exact phase-boundary and one-sided-gradient +invariants are tested. The positive control is rerun, and no alternative cycle +or accompanying intervention is admitted into this batch. + +## Batch 26 results + +Positive-control run: `runs/ecp7-batch26-control-development/20260718T174733Z-ecp7-development`
+Intervention run: `runs/ecp7-batch26-intervention-development/20260718T174733Z-ecp7-development`
+Test unsealed: **no** + +| Metric | Positive control | ECP7-B26-I | +|---|---:|---:| +| Population train, mean | 100% | **85.3725%** | +| Population train, worst link | 100% | 82.7427% | +| Population validation, mean | 100% | **85.3760%** | +| Population validation, worst link | 100% | 82.7148% | +| Universal translator, validation | 100% | **85.3760%** | +| Exact sender-message agreement | 100% | 92.38% | +| Unique messages per sender | 15,360 | 12,720–13,440 | +| Collision meanings per sender | 0 | 1,920–2,640 | +| Message entropy | 13.91 bits | 13.54–13.64 bits | +| Development gate | pass | **fail** | + +The positive control passed every gate at 100%. ECP7-B26-I sets small new +ECP-7 bests for mean train, mean validation, translator validation and sender +agreement. It still fails both train thresholds and sender injectivity; the +worst validation link merely ties Batch 25. The joint gate fails and +confirmatory access remains unauthorized. + +All 151 optimization records through step 30,000 match Batch 25 exactly. At the +second sender-only boundary the all-link pool drops from 1,710 at step 30,000 +to 1,264 at step 30,200 and reaches its minimum of 1,209 at step 30,800. The +selected checkpoint occurs at step 34,200, during this sender-only pulse, with +1,330 shared failures. The final receiver-only phase begins with 1,648 shared +failures at step 35,200 and early stopping terminates at step 39,200, 5,000 +steps after selection. Receiver catch-up therefore does not improve on the +sender-only checkpoint. + +The selected diagnostic finds 1,330 meanings failed by all links and 3,262 +failed by at least one link, with 10.29 mean failed links per erroneous +meaning. Relative to Batch 25, the extra sender pulse removes 347 shared +failures but creates 295 additional any-link failures. This reproduces the +sender-only tradeoff observed in Batch 22: common errors become link-specific +rather than disappearing. + +Selected sender codebooks contain 13,438, 12,960, 12,720 and 13,440 unique +messages, with 1,922, 2,400, 2,640 and 1,920 collision meanings. Batch 25 used +13,440, 12,960, 12,720 and 13,440 unique messages. The second sender pulse thus +adds no code capacity and regresses sender 0 by two meanings. Population and +universal-translator validation factor accuracies are both +`[100%, 100%, 98.44%, 86.94%]`. The stable size and texture collisions remain +the limiting structure. + +Both sealed analyses report 65 matching artifact hashes, 16,384 +validation-only episodes, no confirmatory-test keys, valid local alphabets, +and exactly 14 declared bits for every logged message. + +## Batch 26 decision + +ECP7-B26-I is a valid negative result. Its very small mean improvements come +from redistributing reader errors, not from making the sender codebooks more +injective. Repeating or extending one-sided ordinary-task replay is therefore +not justified: receiver-only training is already code-capacity-limited, while +sender-only training reduces shared errors by fragmenting them across links. + +The ECP-7 confirmatory split remains sealed. Any future batch must change the +sender collision objective or sender representation directly, rather than add +another route phase, route boundary or horizon extension. diff --git a/docs/research-design-ecp7.md b/docs/research-design-ecp7.md index 795ea6b..a4f2f4b 100644 --- a/docs/research-design-ecp7.md +++ b/docs/research-design-ecp7.md @@ -1091,3 +1091,59 @@ sealed-test requirement fails. No alternative horizon, gradient route, predicate, coefficient, replay batch, refresh, architecture, base loss, optimizer, data, translator or seed may be tried inside Batch 25. Failure is recorded without confirmatory test access. + +## Batch 26 preregistration — Coordinate-descent replay cycle + +Status: **completed sealed development; valid negative**
+Preregistered: July 18, 2026 at 17:46:02 UTC
+Configuration: `config/ecp7-b26-development.yaml`
+Raw configuration SHA-256: `9721d3af452b72788568f7fc5262056aa770e443c0a3bc35112183a08c2badb7`
+Effective configuration SHA-256: `be1310f79b4b7a72134393292480244eeba123d4828b3c94dbf32787dd6a4e3b` + +Batch 25 shows that receiver-only catch-up nearly saturates the mathematical +capacity of the learned sender codebooks. The worst train link exactly matches +its sender's 82.81% unique-code ceiling. Batch 26 tests whether one additional +sender-only separation pulse against those saturated receivers can break +collisions before a final decoder catch-up phase. + +ECP7-B26-I inherits the complete Batch 25 implementation, 45,000-step ceiling, +hard-meaning predicate, replay mechanism, architecture, ordinary task batch, +utilization objective, optimizer, data, temperature schedule, coefficient, +replay batch, sampler, refresh, warmup, selection, translator and thresholds. +The sole change is the explicit one-sided replay schedule: + +1. sender-only through step 20,000; +2. receiver-only through step 30,000; +3. sender-only through step 35,000; +4. receiver-only through step 45,000. + +Each boundary is inclusive, so the next route begins on the following step. +The ordinary base task updates both senders and receivers at every step. Mining +still uses hard training predictions only. The complete optimization trajectory +through step 30,000 must match Batch 25 and Batch 24 exactly. + +The batch contains exactly two seed-11 runs: + +1. the unchanged ECP-6 positive control; +2. ECP7-B26-I with the coordinate-descent replay cycle. + +All existing validity and development gates remain unchanged. The intervention +fails if any train, validation, translator, injectivity, channel-integrity or +sealed-test requirement fails. No alternative phase boundary, gradient route, +predicate, coefficient, replay batch, refresh, architecture, base loss, +optimizer, horizon, data, translator or seed may be tried inside Batch 26. +Failure is recorded without confirmatory test access. + +### Batch 26 outcome + +The registered seed-11 intervention reached 85.37% mean train, 82.74% +worst-link train, 85.38% mean validation, 82.71% worst-link validation and +85.38% universal-translator validation. Its sender codebooks used 13,438, +12,960, 12,720 and 13,440 unique messages, effectively unchanged from Batch +25. The second sender-only pulse reduced shared errors but increased meanings +failed by at least one population link. It therefore failed the unchanged +train and injectivity gates without accessing the confirmatory split. + +This completes the registered replay-routing family. Further route cycling or +horizon extension is disallowed as an unsupported repetition; any later +development batch must preregister a direct sender-capacity intervention. diff --git a/evidence/ecp7-development/README.md b/evidence/ecp7-development/README.md index 9fe0a32..94f2a7f 100644 --- a/evidence/ecp7-development/README.md +++ b/evidence/ecp7-development/README.md @@ -143,6 +143,14 @@ validation, but the observed worst train link exactly reached its sender's receiver training time. Exact identities and hashes are in `batch25-manifest.json`. +Batch 26 inserted one second sender-only pulse followed by receiver-only +catch-up. It reached small new-best means of 85.37% train and 85.38% validation +and translator validation, but its unique-message counts were effectively +unchanged. Shared failures fell from 1,677 to 1,330 while any-link failures +rose from 2,967 to 3,262, confirming that route cycling redistributes errors +without fixing sender collisions. Exact identities and hashes are in +`batch26-manifest.json`. + Reproduce the development arms with: ```bash diff --git a/evidence/ecp7-development/batch26-manifest.json b/evidence/ecp7-development/batch26-manifest.json new file mode 100644 index 0000000..aa8018b --- /dev/null +++ b/evidence/ecp7-development/batch26-manifest.json @@ -0,0 +1,63 @@ +{ + "batch": "ECP7-B26", + "confirmatory_test_unsealed": false, + "config_effective_sha256": "be1310f79b4b7a72134393292480244eeba123d4828b3c94dbf32787dd6a4e3b", + "config_raw_sha256": "9721d3af452b72788568f7fc5262056aa770e443c0a3bc35112183a08c2badb7", + "control": { + "artifact_hashes": { + "effective_config.yaml": "4bfb976293f117d3ae35f790e4fd6371628be95d91febb334bc275dc5d6b66d4", + "posthoc-analysis.json": "8146bbb5c0bedd194b256ee1f0dc84d9786d44a2d77163bbc1e80225e7b0fb35", + "seed-11/metrics.json": "fa21529cfcb56003047961b491d1ebd84280e678fab05fda2f5e8023e9d8ff96", + "summary.json": "aaf459e85329fa65046e9ebf102307f540a1dae068f6f2cb3306561ce1b07409" + }, + "development_gate_passed": true, + "population_train_mean_exact_match": 1.0, + "population_validation_mean_exact_match": 1.0, + "run_id": "20260718T174733Z-ecp7-development", + "universal_translator_validation_exact_match": 1.0, + "unique_messages_per_sender": [15360, 15360, 15360, 15360] + }, + "intervention": { + "artifact_hashes": { + "effective_config.yaml": "82ec815bbcf240c8a0e18787f8907df2874a161fac919ca97208d0daf892b2e1", + "posthoc-analysis.json": "b09d0be6b3a4d1071ec34f6ce3e8a5ff302a80f311a8543d6768465151b805f4", + "seed-11/metrics.json": "2ca9c25920dd54338aaff43167ba13a7c70c38885af7f3589444acd7fecf3385", + "summary.json": "34b2615e96c67ec48f2a7ec1f5b1d447cda56489ad995383a1c91a5b48f83780" + }, + "best_code_utilization_loss": -0.7653568983078003, + "best_global_hard_meaning_replay_loss": 0.22441774606704712, + "best_global_hard_meaning_replay_pool_size": 1330, + "best_global_hard_meaning_replay_weight": 0.25, + "best_step": 34200, + "collision_meanings_per_sender": [1922, 2400, 2640, 1920], + "development_gate_passed": false, + "exact_sender_message_agreement": 0.923849826388889, + "final_hard_meaning_replay_pool_size": 1649, + "final_step": 39200, + "gradient_route_phases": [ + {"end_step": 20000, "receiver_parameter_gradients": false, "sender_message_gradients": true}, + {"end_step": 30000, "receiver_parameter_gradients": true, "sender_message_gradients": false}, + {"end_step": 35000, "receiver_parameter_gradients": false, "sender_message_gradients": true}, + {"end_step": 45000, "receiver_parameter_gradients": true, "sender_message_gradients": false} + ], + "initial_hard_meaning_replay_pool_size": 1745, + "message_entropy_bits_per_sender": [13.638937503109894, 13.57669791977656, 13.539550361165908, 13.63919791977656], + "minimum_hard_meaning_replay_pool_size": 1209, + "minimum_hard_meaning_replay_pool_step": 30800, + "population_train_mean_exact_match": 0.8537248883928572, + "population_train_worst_link_exact_match": 0.8274274553571429, + "population_validation_factor_accuracy": [1.0, 1.0, 0.984375, 0.869384765625], + "population_validation_mean_exact_match": 0.853759765625, + "population_validation_worst_link_exact_match": 0.8271484375, + "run_id": "20260718T174733Z-ecp7-development", + "selected_all_links_wrong_meaning_count": 1330, + "selected_any_link_wrong_meaning_count": 3262, + "selected_mean_incorrect_links_per_hard_meaning": 10.285714285714286, + "universal_translator_validation_exact_match": 0.853759765625, + "unique_message_capacity_mean": 0.8554361979166667, + "unique_messages_per_sender": [13438, 12960, 12720, 13440] + }, + "seed": 11, + "split_artifact_sha256": "676195997ee0c77d5c9565bae045dbb938b432a2e4e74b666233b0a41f924718", + "split_sha256": "4947058c75ab07cb43a87eb82776b12cb2a7e2eeba7114de110d3b852cbc64cd" +} diff --git a/src/ai_taal/config.py b/src/ai_taal/config.py index 5b0b9c2..1e8f0bf 100644 --- a/src/ai_taal/config.py +++ b/src/ai_taal/config.py @@ -300,6 +300,7 @@ def validate_config(config: dict[str, Any]) -> None: sender_gradient_switch_step = hard_replay.get( "disable_sender_message_gradients_after_step" ) + gradient_route_phases = hard_replay.get("gradient_route_phases") if not isinstance(receiver_parameter_gradients, bool): raise ConfigError( "Global hard-meaning replay receiver parameter gradients must be boolean." @@ -367,6 +368,54 @@ def validate_config(config: dict[str, Any]) -> None: raise ConfigError( "Global hard-meaning replay sender and receiver gradient switches must match." ) + if gradient_route_phases is not None: + if ( + not isinstance(gradient_route_phases, list) + or not gradient_route_phases + ): + raise ConfigError( + "Global hard-meaning replay gradient-route phases must be a non-empty list." + ) + previous_end = -1 + for phase in gradient_route_phases: + if not isinstance(phase, dict): + raise ConfigError( + "Global hard-meaning replay gradient-route phases must be objects." + ) + phase_end = phase.get("end_step") + sender_gradients = phase.get("sender_message_gradients") + receiver_gradients = phase.get("receiver_parameter_gradients") + if isinstance(phase_end, bool) or not isinstance(phase_end, int): + raise ConfigError( + "Global hard-meaning replay gradient-route phase ends must be integers." + ) + if not isinstance(sender_gradients, bool) or not isinstance( + receiver_gradients, bool + ): + raise ConfigError( + "Global hard-meaning replay gradient-route flags must be boolean." + ) + if sender_gradients == receiver_gradients: + raise ConfigError( + "Global hard-meaning replay gradient-route phases must enable exactly one side." + ) + if phase_end <= previous_end: + raise ConfigError( + "Global hard-meaning replay gradient-route phase ends must increase." + ) + if phase_end % training["evaluation_interval"] != 0: + raise ConfigError( + "Global hard-meaning replay gradient-route phase ends must align with evaluation boundaries." + ) + previous_end = phase_end + if gradient_route_phases[0]["end_step"] != start_step + warmup_steps: + raise ConfigError( + "Global hard-meaning replay gradient-route phases must start with the registered warmup boundary." + ) + if gradient_route_phases[-1]["end_step"] != training["max_steps"]: + raise ConfigError( + "Global hard-meaning replay gradient-route phases must cover max_steps." + ) if batch_size < 1: raise ConfigError( "Global hard-meaning replay batch size must be positive." diff --git a/src/ai_taal/training.py b/src/ai_taal/training.py index 8973548..20f3a4b 100644 --- a/src/ai_taal/training.py +++ b/src/ai_taal/training.py @@ -1398,6 +1398,11 @@ def _hard_replay_receiver_parameter_gradients( hard_replay_config: dict[str, Any], step: int ) -> bool: """Return whether the replay branch updates receiver parameters at this step.""" + phased_route = _hard_replay_phased_gradient_route( + hard_replay_config, step + ) + if phased_route is not None: + return phased_route[1] initially_enabled = bool( hard_replay_config.get("receiver_parameter_gradients", True) ) @@ -1413,7 +1418,32 @@ def _hard_replay_sender_message_gradients( hard_replay_config: dict[str, Any], step: int ) -> bool: """Return whether the replay branch backpropagates into sender messages.""" + phased_route = _hard_replay_phased_gradient_route( + hard_replay_config, step + ) + if phased_route is not None: + return phased_route[0] switch_step = hard_replay_config.get( "disable_sender_message_gradients_after_step" ) return switch_step is None or step <= int(switch_step) + + +def _hard_replay_phased_gradient_route( + hard_replay_config: dict[str, Any], step: int +) -> tuple[bool, bool] | None: + """Return an explicit sender/receiver route when phases are configured.""" + phases = hard_replay_config.get("gradient_route_phases") + if not phases: + return None + for phase in phases: + if step <= int(phase["end_step"]): + return ( + bool(phase["sender_message_gradients"]), + bool(phase["receiver_parameter_gradients"]), + ) + final_phase = phases[-1] + return ( + bool(final_phase["sender_message_gradients"]), + bool(final_phase["receiver_parameter_gradients"]), + ) diff --git a/tests/conftest.py b/tests/conftest.py index 0f35505..efceb99 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -168,3 +168,8 @@ def ecp7_b24_config(): @pytest.fixture(scope="session") def ecp7_b25_config(): return load_config(PROJECT_ROOT / "config" / "ecp7-b25-development.yaml") + + +@pytest.fixture(scope="session") +def ecp7_b26_config(): + return load_config(PROJECT_ROOT / "config" / "ecp7-b26-development.yaml") diff --git a/tests/test_config.py b/tests/test_config.py index f318d5f..854b7f3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -875,3 +875,71 @@ def test_ecp7_b25_changes_only_the_training_horizon( assert b24_training.pop("max_steps") == 30000 assert b25_training.pop("max_steps") == 45000 assert b25_training == b24_training + + +def test_ecp7_b26_changes_only_the_replay_gradient_route_phases( + ecp7_b25_config, ecp7_b26_config +): + assert ecp7_b26_config["world"] == ecp7_b25_config["world"] + assert ecp7_b26_config["dataset"] == ecp7_b25_config["dataset"] + assert ecp7_b26_config["channel"] == ecp7_b25_config["channel"] + assert ecp7_b26_config["agents"] == ecp7_b25_config["agents"] + b25_training = deepcopy(ecp7_b25_config["training"]) + b26_training = deepcopy(ecp7_b26_config["training"]) + assert b26_training["global_hard_meaning_replay"].pop( + "gradient_route_phases" + ) == [ + { + "end_step": 20000, + "sender_message_gradients": True, + "receiver_parameter_gradients": False, + }, + { + "end_step": 30000, + "sender_message_gradients": False, + "receiver_parameter_gradients": True, + }, + { + "end_step": 35000, + "sender_message_gradients": True, + "receiver_parameter_gradients": False, + }, + { + "end_step": 45000, + "sender_message_gradients": False, + "receiver_parameter_gradients": True, + }, + ] + assert b26_training == b25_training + + +def test_hard_replay_gradient_route_phases_are_bounded_and_one_sided( + ecp7_b26_config, +): + invalid_order = deepcopy(ecp7_b26_config) + invalid_order["training"]["global_hard_meaning_replay"][ + "gradient_route_phases" + ][2]["end_step"] = 30000 + with pytest.raises(ConfigError, match="phase ends must increase"): + validate_config(invalid_order) + + invalid_route = deepcopy(ecp7_b26_config) + invalid_route["training"]["global_hard_meaning_replay"][ + "gradient_route_phases" + ][2]["receiver_parameter_gradients"] = True + with pytest.raises(ConfigError, match="must enable exactly one side"): + validate_config(invalid_route) + + invalid_boundary = deepcopy(ecp7_b26_config) + invalid_boundary["training"]["global_hard_meaning_replay"][ + "gradient_route_phases" + ][2]["end_step"] = 35100 + with pytest.raises(ConfigError, match="evaluation boundaries"): + validate_config(invalid_boundary) + + invalid_final = deepcopy(ecp7_b26_config) + invalid_final["training"]["global_hard_meaning_replay"][ + "gradient_route_phases" + ][-1]["end_step"] = 44800 + with pytest.raises(ConfigError, match="must cover max_steps"): + validate_config(invalid_final) diff --git a/tests/test_training.py b/tests/test_training.py index 9a4885b..37602f3 100644 --- a/tests/test_training.py +++ b/tests/test_training.py @@ -705,6 +705,32 @@ def test_hard_replay_sender_gradients_switch_after_registered_step( assert not _hard_replay_sender_message_gradients(schedule, 30000) +def test_hard_replay_coordinate_route_follows_registered_phases( + ecp7_b26_config, +): + schedule = ecp7_b26_config["training"]["global_hard_meaning_replay"] + expected = { + 15000: (True, False), + 20000: (True, False), + 20001: (False, True), + 30000: (False, True), + 30001: (True, False), + 35000: (True, False), + 35001: (False, True), + 45000: (False, True), + } + + for step, (sender_gradients, receiver_gradients) in expected.items(): + assert ( + _hard_replay_sender_message_gradients(schedule, step) + is sender_gradients + ) + assert ( + _hard_replay_receiver_parameter_gradients(schedule, step) + is receiver_gradients + ) + + def test_receiver_binding_calibration_recovers_exact_permutation(ecp4_config): receiver = FactorizedPermutationSlotReceiver(ModelSpec.from_config(ecp4_config)) meanings = torch.tensor(