feat(coordinator): add the R9 role-ablation policies (no_thinker / no_trirole) - #425
Merged
Merged
Conversation
…_trirole) SPEC R9's verifier is merged (analysis/ablations.py, James-CUDA#376) and consumes {full, no_svf, no_thinker, no_trirole, last_token}, but nothing in src/ could produce those numbers - the variant names appear only in docstrings and the example JSON in scripts/ablations_report.py. This adds the producer for the two role ablations. AblatedPolicy satisfies the session's Policy protocol, so it drops into run_trajectory with no change to session.py, policy.py or slm.py. Masking is applied to the role logits before the softmax, making the result a true restricted categorical rather than a post-hoc remap of the full model's choice - a remap cannot reconstruct the restricted argmax when the full argmax was the ablated role. Agent (pool model) selection is untouched. The module imports no torch at scope; torch is imported lazily inside decide(), as policy.CoordinatorPolicy.decide already does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
James-CUDA
pushed a commit
to minion1227/Gittensor-TinyRouter
that referenced
this pull request
Jul 23, 2026
…oken) SPEC R9's verifier is merged (analysis/ablations.py, James-CUDA#376) and consumes {full, no_svf, no_thinker, no_trirole, last_token}, but nothing in src/ could produce those numbers. James-CUDA#425 added the two role ablations; this adds the two that concern the feature pipeline. no_svf is a pure theta transformation: the SVF block is set to the identity (all scales 1.0), which is exactly what SVFAdapter.identity_scales() already calls 'no adaptation'. It reuses the canonical params.pack/unpack, keeps theta at full width so an ablated run stays comparable with the full one, and changes no code path. last_token reads the appended EOS instead of SPEC 3.2's penultimate token. slm.py gains a token_index parameter defaulting to -2, so the shipped config and every existing caller are byte-for-byte unchanged; the default is also declared at class level so instances built via object.__new__ (as the offline tests do) keep reading the canonical position. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
James-CUDA
force-pushed
the
minion_role_ablations
branch
from
July 23, 2026 11:36
88e0133 to
1f86fd8
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 R9 — "removing SVF / Thinker / tri-role / penultimate-token all hurt" — has a merged verifier and no producer.
analysis/ablations.py(#376) takes{full, no_svf, no_thinker, no_trirole, last_token}and reports each ablation's drop. But grep those four variant names acrosssrc/,configs/andscripts/and they appear only inside docstrings and the example JSON atscripts/ablations_report.py:11. Nothing in the repo could ever emit one of them, so R9 could only ever be graded against numbers typed in by hand from the paper.This PR is the producer for the two role ablations.
no_svf/last_tokenare a different concern (parameter packing and hidden-state extraction respectively) and are deliberately left out — see Scope below.What's here
coordinator/ablations.pyaddsAblatedPolicy, which satisfies the session's existingPolicyprotocol (decide(transcript_text, *, sample, rng) -> (agent_idx, Role)), so it drops intorun_trajectorywithsession.py,policy.pyandslm.pyuntouched.no_thinkerno_triroleAgent (pool-model) selection is passed through unchanged: R9's role ablations remove role structure, not model routing. There's a test asserting the ablated agent choice equals the un-ablated one on every input.
Why the mask goes on the logits, not the output
Masking is applied to the role logits before the softmax, making the result a true restricted categorical. Taking the full model's decision and remapping it afterwards is not the same thing: if the un-ablated argmax was Thinker, a post-hoc rule cannot know whether Worker or Verifier was runner-up, so it cannot reconstruct the restricted argmax. Sampling has the same problem — the restricted distribution renormalizes over the survivors rather than deleting one outcome from the full one.
That distinction is pinned by
test_masked_argmax_is_not_a_post_hoc_remap_of_the_full_argmax, which constructs logits where the two rules disagree.Scope, stated honestly
no_thinkerandno_trirole. It does not produceno_svforlast_token.no_trirolecollapses to Worker. The SPEC names the ablation but not which role survives; Worker is the role whose prompt asks for a direct solution. It's a constructor argument, so the choice is overridable rather than baked in.Why it's low-risk
configureforwards it unchanged, so an ablated run stays directly comparable with the full model's.LinearHead.selecton random hidden states (test_torch_role_ablation_parity.py), so the two cannot drift and the ablated numbers stay comparable.import trinity.coordinator.ablationspulls no torch; it is imported lazily insidedecide(), aspolicy.CoordinatorPolicy.decidealready does — and only after the encoder/head check, so the rejection path doesn't pull it in either.A note on the
test_no_torch_importedguardsSeveral modules ship an in-process
test_no_torch_imported, which is order-fragile: any earlier test that imports torch fails it.test_union_oracle.pysolves the same problem in a clean subprocess and is immune. Both new torch checks here follow the subprocess pattern, and they passPYTHONPATHfor the checkout under test rather than relying on whichevertrinityhappens to be installed.Verification
Run against a locally-resolved
main(see below), compared A/B with the same tree minus these files:The 11 are pre-existing
test_report_script_*subprocess failures, untouched by this branch.ruff check src/ scripts/repo_governance/→ clean;mypyon the new module → clean🤖 Generated with Claude Code