feat(coordinator): add the SPEC §3.5 alternative heads (R10 ablation parity) - #416
Merged
Merged
Conversation
This was referenced Jul 22, 2026
fix(analysis): resolve unresolved merge-conflict markers in __init__ (package failed to import)
#405
Merged
…parity) SPEC §1.3 invariant R10 — "linear head >= all other head variants overall" (Table 3) — is the justification for shipping a plain linear routing head. The *verifier* for R10 is already merged (`analysis/head_variants.py`, James-CUDA#352): hand it a {variant: {benchmark: score}} table and it reports the ranking and the R10 verdict. But nothing in `src/` could ever *produce* a non-linear head, so the table it grades had to be typed in by hand from the paper. This is the producer. Adds low-rank, sparse, block-diagonal-2 and block-diagonal-10 (SPEC §3.5), each mapping h -> z and splitting into the same two logit groups the linear head uses, so a variant is a drop-in replacement at the decision boundary. `param_counts()` feeds the verifier's `params` argument for Table 3's efficiency column. Two SPEC problems surfaced while reproducing Table 3 exactly; both are documented and pinned by tests rather than quietly worked around: - **low-rank is internally inconsistent.** A low-rank head is r*(d_h + n_a) parameters, which is 14,476 at the stated r=14 — not the tabled 20,680, which needs r=20. The two cannot both hold. The stated *hyperparameter* is kept as the default (it is the more direct claim); `rank=20` reproduces the table. - **d_h=1024 is not divisible by 10.** SPEC's headline block-diag-10 ("one block per logit", "exact 10x reduction" to 1,024 params) is unconstructible with equal input chunks, at the paper's own shape. Blocks therefore partition the inputs *near-equally*; since the chunk sizes sum to d_h, the count is exactly (n_a/B)*d_h at any d_h, reproducing both 5,120 and 1,024. Requiring B | d_h would make the paper's own variant impossible to build. Sparse's rho carries no gradient by construction (it sets an integer k through a floor, so dL/drho is structurally zero however the mask is relaxed). It stays a parameter because SPEC's "+2" counts it and the gradient-free optimizers this repo uses for the head can search it; `test_sparse_rho_has_no_gradient_path` asserts this rather than hiding it. `from_config` is the first reader of the `coordinator.head` block: `type`, `hidden_dim`, `include_stop_action` and `factorize` had no consumer anywhere in `src/`, so editing them silently did nothing. They are now honoured or rejected with an explicit error. Feeding it the shipped configs/trinity.yaml reproduces exactly today's linear head. Purely additive: `params.py` and `policy.py` are untouched, with tests pinning `make_spec().n_total == 13312` (the frozen submission dimension, which is linear-only) and that policy still builds LinearHead. No default changes. Tests import torch lazily inside the test bodies and parametrize over a literal variant list, because pytest imports test modules at *collection* time — a module-scope torch import would break `test_shaped_fitness.py::test_no_torch_imported` regardless of filename. Full suite: 1911 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
James-CUDA
force-pushed
the
minion_head_variants
branch
from
July 23, 2026 11:38
e3453a1 to
2882cdd
Compare
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.
The gap
SPEC §1.3 invariant R10 — "linear head ≥ all other head variants overall" (Table 3) — is the justification for shipping a plain linear routing head. The verifier for R10 is already merged (
analysis/head_variants.py, #352): hand it a{variant: {benchmark: score}}table and it reports the overall ranking and the R10 verdict.But nothing in
src/could ever produce a non-linear head, so the table it grades had to be typed in by hand from the paper.coordinator/contains onlyhead.py(LinearHead). This PR is the producer.Four
coordinator.headconfig knobs —type,hidden_dim,include_stop_action,factorize— also had zero readers anywhere insrc/, so editing them silently did nothing.What's here
coordinator/heads.pyadds low-rank, sparse, block-diagonal-2 and block-diagonal-10 per SPEC §3.5. Each mapsh → zand splits into the same two logit groups the linear head uses (z[:n_models]agent,z[n_models:]role, one softmax each), so a variant is a drop-in replacement at the decision boundary.param_counts()produces theparamsmapping the merged R10 verifier already accepts, for Table 3's efficiency column.Two SPEC problems this surfaced
Reproducing Table 3 exactly turned up two inconsistencies. Both are documented and pinned by tests rather than quietly worked around — flagging them is the point, since R10 is graded against these numbers.
1. Low-rank is internally inconsistent. A low-rank head is
U ∈ R^{r×d_h}plusV ∈ R^{n_a×r}(σ is fixed, not a parameter) =r·(d_h + n_a). At the paper'sd_h=1024, n_a=10and the statedr=14, that is 14,476 — not the tabled 20,680, which requiresr=20. The two cannot both hold.This keeps the stated hyperparameter
r=14as the default, on the grounds that a hyperparameter is the more direct claim and silently switching tor=20to make a derived total match would hide the conflict. Passrank=20to reproduce the table. Pinned bytest_lowrank_rank14_contradicts_spec_table3.2.
d_h=1024is not divisible by 10. SPEC's headline variant — block-diag-10, "one block per logit", "exact 10× reduction" to 1,024 params — is unconstructible with equal input chunks at the paper's own shape (1024/10 = 102.4).The resolution:
Bmust dividen_a(output logits split cleanly into blocks) but need not divided_h. Inputs are partitioned intoBcontiguous near-equal chunks summing to exactlyd_h(e.g.103,103,103,103,102,…). Because the chunk sizes sum tod_h, the count is exactly(n_a/B)·d_h— no rounding, at anyd_h— reproducing both 5,120 and 1,024. RequiringB | d_hinstead would make the paper's own headline variant impossible to build.Every other Table 3 count (linear 10,240; sparse 11,266; block-diag-2 5,120; block-diag-10 1,024) is reproduced exactly at the paper's shape.
Sparse's
ρcarries no gradient, stated plainlySPEC defines
k = max(1, ⌊d_h·(1−σ(ρ))⌋). A floor to an integer has zero derivative almost everywhere, so∂L/∂ρis structurally zero however the mask is relaxed — the Gumbel relaxation makes which edges survive differentiable, not how many.ρis kept as a parameter because SPEC's+2counts it and because the gradient-free optimizers this repo actually uses for the head (sep-CMA-ES, Random Search) can search it.test_sparse_rho_has_no_gradient_pathasserts this rather than hiding it. Onlyτis gradient-trainable of the two.Why it's low-risk
params.pyandpolicy.pyare untouched.make_spec()computesn_head = n_a·d_h, which is linear-only and feeds the frozen submission dimension — wiring a variant into that path would changen_totaland break submission, so this deliberately does not. Tests pinmake_spec().n_total == 13312and thatpolicystill buildsLinearHead.from_configon the shippedconfigs/trinity.yamlreproduces exactly today's linear head (test_from_config_builds_the_shipped_config_unchanged).make_head("linear")returnshead.py'sLinearHead, pinned identical bytest_registry_linear_matches_head_py.hidden_dim != 0,include_stop_action, and a non-two_softmaxfactorizeeach raise with an explanation, since no SPEC §3.5 head implements them.Verification
pytest tests/ -q→ 1911 passed, 0 failed (81 new)ruff check src/ scripts/repo_governance/→ clean;mypy src/trinity/coordinator/heads.py→ cleanparam_counts()feeding the mergedanalyze_heads()verifier returns the R10 verdictTests import torch lazily inside the test bodies and parametrize over a literal variant list (pinned against the registry by
test_variants_list_matches_registry), because pytest imports test modules at collection time — a module-scope torch import would breaktest_shaped_fitness.py::test_no_torch_importedregardless of filename.🤖 Generated with Claude Code