Skip to content

docs(demos): add SwiGLU gate-sign demo for double-negative echoes - #1936

Merged
jlarson4 merged 3 commits into
TransformerLensOrg:devfrom
emerardd:docs/swiglu-gate-sign-demo
Oct 22, 2026
Merged

jlarson4 merged 3 commits into
TransformerLensOrg:devfrom
emerardd:docs/swiglu-gate-sign-demo

Conversation

@emerardd

Copy link
Copy Markdown
Contributor

Description

Adds demos/SwiGLU_Gate_Sign_Demo.ipynb, the demo approved in #1933.

A gated MLP neuron outputs act(gate) * value. When the gate is closed, act(gate) is a small negative number, so a negative value produces a positive "echo" (Goodfire's double-negative regime). Using only the existing gated-MLP hook aliases (mlp.hook_pre, mlp.hook_pre_linear, mlp.hook_post), the notebook:

  • tags each neuron's top-k activations by sign regime in one streaming pass;
  • finds neurons whose top-k is made of echoes, and shows a typical one next to its gate-open negative pole;
  • compares each neuron's echo peak with its gate-open peak;
  • removes the echo with a forward hook and compares the loss change against two controls: closed-gate negatives, and a layer-matched random set of neurons.

It runs SmolLM2-135M on CPU in float32 with a small token budget (about 2 minutes on 4 threads). The last section has a static table of full-size results for SmolLM2-135M, Qwen3-0.6B and Qwen3-1.7B, including the layer-1 / never-open-gate split promised in #1933. No helper function is added, per the approval comment.

Registration, one line each:

  • notebook CI matrix (checks.yml) and makefile notebook-test;
  • docs/make_docs.py copy_demos, the docs toctree (index.md), and tests/unit/test_make_docs.py;
  • a "See also" entry in hook_system.md.

Fixes #1933

Type of change

  • New feature (non-breaking change which adds functionality)

Checklist:

  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have not rewritten tests relating to key interfaces which would affect backward compatibility

On the unchecked box: no library code changes, so I ran only the affected tests rather than the whole unit suite.

  • pytest --nbval-sanitize-with demos/doc_sanitize.cfg demos/SwiGLU_Gate_Sign_Demo.ipynb passed 11/11, repeated across thread counts (4 and 32) and two torch builds.
  • tests/unit/test_make_docs.py passed.

The notebook was only validated on Windows, so the Linux notebook job in CI is the first check there.

Adds demos/SwiGLU_Gate_Sign_Demo.ipynb (proposal TransformerLensOrg#1933). Using only the
gated-MLP hook aliases (mlp.hook_pre, hook_pre_linear, hook_post), it tags
each neuron's top-k activations by sign regime, finds neurons whose top-k
is made of gate-closed echoes, compares echo and gate-open peaks, and
removes the echo with a forward hook against two controls. It runs
SmolLM2-135M on CPU float32 with a small token budget and lists
full-size results for SmolLM2-135M, Qwen3-0.6B and Qwen3-1.7B.

Registers the notebook in the notebook CI matrix, the makefile, the docs
build and its unit test, and links it from the hook system page.
0 * inf is NaN, so masking ineligible neurons by adding (~mask) * inf
turned every eligible score into NaN and argmin returned the first
all-echo neuron. Mask with torch.where instead, and rewrite the example
note for the neuron that is now selected.
@jlarson4

Copy link
Copy Markdown
Collaborator

Good timing on this! Reviewing now while CI completes so I can squeeze it into 4.3.0 before I put out the release this afternoon

@jlarson4 jlarson4 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a couple detail comments, let me know if you have any questions

Comment thread demos/SwiGLU_Gate_Sign_Demo.ipynb Outdated
"for layer in range(N_LAYERS):\n",
" pool = (~echo_dominated[layer]).nonzero().squeeze(-1)\n",
" n = int(echo_dominated[layer].sum())\n",
" random_neurons[layer, pool[torch.randperm(len(pool), generator=generator)[:n]]] = True\n",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The random set and the selected set have the same neuron count and these neurons carry much less echo. So the gap to the selected set mixes which neurons were edited with how much was zeroed. Can the control be matched on removed echo magnitude, so it tests the selection on its own?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. The random control now draws neurons per layer until they carry as much echo on the test rows as the selected set (1.01× in the demo, within 2% for the full-size runs). The selected neurons still cost 10–100× more: +0.334 vs +0.033 live, and in the full-size table +0.21/+1.31/+0.53 vs +0.018/+0.013/+0.030. I also added a note that a count-matched control overstates the selection effect.

Comment thread demos/SwiGLU_Gate_Sign_Demo.ipynb Outdated
"| Δ loss, echo removed, echo-dominated neurons | +0.21 | +1.31 | +0.53 |\n",
"| Δ loss, echo removed, random neurons | +0.002 | +0.049 | +0.039 |\n",
"\n",
"In the Qwen3 models, layer 1 holds many echo-dominated neurons, and some neurons never open their gate. Splitting the echo-dominated set shows the removal cost does not come from either group. Each cell gives Δ loss, then the same-size random control in parentheses, then the number of neurons. The first two groups overlap (layer 1 contains some never-open neurons), and \"Neither\" excludes both:\n",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Qwen3-0.6B column doesn't support this sentence: the 108 never-open neurons cost +0.366, more than the 1,899 neurons in "Neither". Could this be reworded to match what the split shows for each model?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that sentence overclaimed. I replaced it with a per-model description. In 0.6B the never-open neurons carry the largest cost, because their whole positive side is echo. In SmolLM2 and 1.7B the cost sits mostly in neither group. The split also uses the matched controls now.

The random control matched the echo-dominated set on neuron count, but
those neurons carry much less echo, so the gap mixed which neurons were
edited with how much was zeroed. Draw random neurons per layer until
they carry as much echo on the test rows as the selected set (1.01x
here), and use the same matching for the full-size tables.

The split paragraph claimed the removal cost came from neither layer 1
nor never-open neurons, which Qwen3-0.6B contradicts (its 108 never-open
neurons cost more than the 1,899 in neither group). Describe the split
per model instead.
@jlarson4
jlarson4 merged commit 7d4f463 into TransformerLensOrg:dev Oct 22, 2026
29 checks passed
@jlarson4

Copy link
Copy Markdown
Collaborator

Looks great! Merged and including in the next release

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