Skip to content

feat(coordinator): add the R9 role-ablation policies (no_thinker / no_trirole) - #425

Merged
James-CUDA merged 1 commit into
James-CUDA:mainfrom
minion1227:minion_role_ablations
Jul 23, 2026
Merged

feat(coordinator): add the R9 role-ablation policies (no_thinker / no_trirole)#425
James-CUDA merged 1 commit into
James-CUDA:mainfrom
minion1227:minion_role_ablations

Conversation

@minion1227

Copy link
Copy Markdown
Contributor

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 across src/, configs/ and scripts/ and they appear only inside docstrings and the example JSON at scripts/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_token are a different concern (parameter packing and hidden-state extraction respectively) and are deliberately left out — see Scope below.

What's here

coordinator/ablations.py adds AblatedPolicy, which satisfies the session's existing Policy protocol (decide(transcript_text, *, sample, rng) -> (agent_idx, Role)), so it drops into run_trajectory with session.py, policy.py and slm.py untouched.

variant surviving roles
no_thinker Worker, Verifier
no_trirole Worker only (single role → no role differentiation)

Agent (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

  • Only the role half of R9. This produces no_thinker and no_trirole. It does not produce no_svf or last_token.
  • [OUR CHOICE] no_trirole collapses 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.
  • This ships the producer, not the numbers. Actually filling in R9's accuracies still means running the benchmark; nothing here claims to have reproduced the paper's table.

Why it's low-risk

  • Purely additive. No existing caller changes behaviour; nothing is wired into the submission or training path. The wrapper never mutates the policy it wraps (pinned by a test).
  • Not a fitness-semantics change. It restricts decisions at eval time; θ keeps its full width and configure forwards it unchanged, so an ablated run stays directly comparable with the full model's.
  • Reuses the shipped selection rule rather than forking it. The wrapper's argmax/sample rule is pinned against the real LinearHead.select on random hidden states (test_torch_role_ablation_parity.py), so the two cannot drift and the ablated numbers stay comparable.
  • Torch-free at import. import trinity.coordinator.ablations pulls no torch; it is imported lazily inside decide(), as policy.CoordinatorPolicy.decide already 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_imported guards

Several modules ship an in-process test_no_torch_imported, which is order-fragile: any earlier test that imports torch fails it. test_union_oracle.py solves the same problem in a clean subprocess and is immune. Both new torch checks here follow the subprocess pattern, and they pass PYTHONPATH for the checkout under test rather than relying on whichever trinity happens to be installed.

Verification

Run against a locally-resolved main (see below), compared A/B with the same tree minus these files:

base with this PR
passed 1,724 1,772 (+48)
failed 11 11 — byte-identical list

The 11 are pre-existing test_report_script_* subprocess failures, untouched by this branch.

  • ruff check src/ scripts/repo_governance/ → clean; mypy on the new module → clean

Note on CI: main has unresolved merge-conflict markers in src/trinity/analysis/__init__.py (introduced by dc15f97), so the package does not import and this PR is red on the merge commit. This branch touches no file in analysis/.

The fix is not contested: #402, #405, #407, #408 and #403 all rewrite that file to the byte-identical result (every one is index 9916618..6865bc2), so merging any one of them repairs main and the rest become no-ops on that file. #407 / #408 / #403 are already green — they touch that file for their own feature and resolved it in passing. Only PRs that don't touch it (this one, #415, #416, #417, #418, #412) are red.

🤖 Generated with Claude Code

…_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
James-CUDA force-pushed the minion_role_ablations branch from 88e0133 to 1f86fd8 Compare July 23, 2026 11:36
@James-CUDA
James-CUDA merged commit e60c360 into James-CUDA:main Jul 23, 2026
@minion1227
minion1227 deleted the minion_role_ablations branch July 23, 2026 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants