Spaces:
Paused
Emit per-layer top-K SwiGLU intermediate activations for RQ1 Step 5
Browse filesAdds a forward hook on Mistral/LLaMA-style FFN modules that captures
the top-K (=32) hidden neurons of `silu(W_gate · x) ⊙ (W_up · x)` at
the predicting (last) position for each generation step. Stored on
each layer entry as `ffn_top_neurons: { k, intermediate_size,
neurons: [{idx, value}] }`.
Memory-bounded: top-32 of 14,336 ≈ 256 bytes per layer per step,
versus the 57KB-per-layer-per-step the full intermediate vector
would require. The top-K is what the RQ1 frontend's "Behind the
SwiGLU" disclosure needs to render an honest per-neuron view rather
than synthesising activations or showing only summary stats.
Only registers on layers exposing both `gate_proj` and `up_proj`
(SwiGLU); other architectures fall back to the existing
`gate_stats` / `mlp_output_norm` / `ffn_contribution` summary
fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- backend/model_service.py +87 -0
|
@@ -2921,6 +2921,16 @@ async def analyze_research_attention_stream(request: Dict[str, Any], authenticat
|
|
| 2921 |
attn_output_norms = {}
|
| 2922 |
mlp_output_norms = {}
|
| 2923 |
gate_activation_stats = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2924 |
|
| 2925 |
def make_attn_output_hook(layer_idx):
|
| 2926 |
def hook(module, input, output):
|
|
@@ -2965,6 +2975,63 @@ async def analyze_research_attention_stream(request: Dict[str, Any], authenticat
|
|
| 2965 |
pass
|
| 2966 |
return hook
|
| 2967 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2968 |
# Cache for decoded token texts (reused across heads within a step)
|
| 2969 |
step_token_texts_cache: Dict[str, Any] = {}
|
| 2970 |
|
|
@@ -2996,6 +3063,14 @@ async def analyze_research_attention_stream(request: Dict[str, Any], authenticat
|
|
| 2996 |
hooks.append(hook)
|
| 2997 |
if layer_idx == 0:
|
| 2998 |
ffn_type = "swiglu"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2999 |
logger.info(f"Registered attn/MLP output hooks for contribution tracking (ffn_type={ffn_type})")
|
| 3000 |
except Exception as hook_error:
|
| 3001 |
logger.warning(f"Could not register attn/MLP hooks: {hook_error}")
|
|
@@ -3017,6 +3092,7 @@ async def analyze_research_attention_stream(request: Dict[str, Any], authenticat
|
|
| 3017 |
attn_output_norms.clear()
|
| 3018 |
mlp_output_norms.clear()
|
| 3019 |
gate_activation_stats.clear()
|
|
|
|
| 3020 |
|
| 3021 |
# Forward pass with full outputs
|
| 3022 |
outputs = manager.model(
|
|
@@ -3585,6 +3661,17 @@ async def analyze_research_attention_stream(request: Dict[str, Any], authenticat
|
|
| 3585 |
layer_entry["ffn_contribution"] = round(mlp_n / total, 4)
|
| 3586 |
if layer_idx in gate_activation_stats:
|
| 3587 |
layer_entry["gate_stats"] = gate_activation_stats[layer_idx]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3588 |
|
| 3589 |
# Phase 5: Logit lens at sampled layers (every 8th layer)
|
| 3590 |
logit_lens_stride = max(1, n_layers // 5)
|
|
|
|
| 2921 |
attn_output_norms = {}
|
| 2922 |
mlp_output_norms = {}
|
| 2923 |
gate_activation_stats = {}
|
| 2924 |
+
# Per-layer top-K SwiGLU intermediate activations at the
|
| 2925 |
+
# predicting position (last token of the current forward
|
| 2926 |
+
# pass). Each entry is a list of {idx, value} for the
|
| 2927 |
+
# top-K neurons by |silu(W_g·x) ⊙ (W_u·x)| at that layer
|
| 2928 |
+
# for that step. Powers RQ1 Step 5's "Behind the SwiGLU"
|
| 2929 |
+
# per-neuron view without storing the full 14,336-d
|
| 2930 |
+
# vector — bounded memory, full transparency for the
|
| 2931 |
+
# most-loaded neurons.
|
| 2932 |
+
ffn_top_neurons = {}
|
| 2933 |
+
FFN_TOPK = 32
|
| 2934 |
|
| 2935 |
def make_attn_output_hook(layer_idx):
|
| 2936 |
def hook(module, input, output):
|
|
|
|
| 2975 |
pass
|
| 2976 |
return hook
|
| 2977 |
|
| 2978 |
+
def make_ffn_top_neurons_hook(layer_idx):
|
| 2979 |
+
"""
|
| 2980 |
+
Capture the top-K of the SwiGLU intermediate activation
|
| 2981 |
+
vector at the predicting (last) position for this layer.
|
| 2982 |
+
|
| 2983 |
+
Computes silu(W_gate · x) ⊙ (W_up · x) — the same
|
| 2984 |
+
14,336-d intermediate that gets fed to W_down. Storing
|
| 2985 |
+
only the top-K (=32) by |value| keeps memory bounded:
|
| 2986 |
+
32 × {idx:int, value:float} ≈ 256 bytes per layer per
|
| 2987 |
+
step, vs 14,336 × 4 bytes = 57KB per layer per step
|
| 2988 |
+
if we stored the full vector. The top-K is what a
|
| 2989 |
+
developer reads anyway (the rest are noise) and the
|
| 2990 |
+
stored neuron index lets the frontend label "neuron
|
| 2991 |
+
8421 dominated this token at L37".
|
| 2992 |
+
|
| 2993 |
+
Only fires for SwiGLU layers (gate_proj + up_proj
|
| 2994 |
+
present); other architectures are skipped at
|
| 2995 |
+
registration time.
|
| 2996 |
+
"""
|
| 2997 |
+
def hook(module, input, output):
|
| 2998 |
+
try:
|
| 2999 |
+
inp = input[0] if isinstance(input, tuple) else input
|
| 3000 |
+
if inp.dim() == 3:
|
| 3001 |
+
inp = inp[0, -1] # last (predicting) token
|
| 3002 |
+
elif inp.dim() == 2:
|
| 3003 |
+
inp = inp[-1]
|
| 3004 |
+
if hasattr(module, 'gate_proj') and hasattr(module, 'up_proj'):
|
| 3005 |
+
gate_out = torch.nn.functional.silu(
|
| 3006 |
+
module.gate_proj(inp)
|
| 3007 |
+
)
|
| 3008 |
+
up_out = module.up_proj(inp)
|
| 3009 |
+
intermediate = gate_out * up_out
|
| 3010 |
+
abs_inter = intermediate.abs()
|
| 3011 |
+
k = min(FFN_TOPK, intermediate.shape[-1])
|
| 3012 |
+
top_vals, top_idx = torch.topk(abs_inter, k=k)
|
| 3013 |
+
# Use the signed values (not abs) so the
|
| 3014 |
+
# frontend can render direction; abs only
|
| 3015 |
+
# drove the ranking.
|
| 3016 |
+
signed = intermediate[top_idx]
|
| 3017 |
+
ffn_top_neurons[layer_idx] = {
|
| 3018 |
+
"k": k,
|
| 3019 |
+
"intermediate_size": int(intermediate.shape[-1]),
|
| 3020 |
+
"neurons": [
|
| 3021 |
+
{
|
| 3022 |
+
"idx": int(i),
|
| 3023 |
+
"value": round(float(v), 4),
|
| 3024 |
+
}
|
| 3025 |
+
for i, v in zip(
|
| 3026 |
+
top_idx.cpu().tolist(),
|
| 3027 |
+
signed.cpu().tolist(),
|
| 3028 |
+
)
|
| 3029 |
+
],
|
| 3030 |
+
}
|
| 3031 |
+
except Exception:
|
| 3032 |
+
pass
|
| 3033 |
+
return hook
|
| 3034 |
+
|
| 3035 |
# Cache for decoded token texts (reused across heads within a step)
|
| 3036 |
step_token_texts_cache: Dict[str, Any] = {}
|
| 3037 |
|
|
|
|
| 3063 |
hooks.append(hook)
|
| 3064 |
if layer_idx == 0:
|
| 3065 |
ffn_type = "swiglu"
|
| 3066 |
+
# FFN top-K intermediate hook — needs both
|
| 3067 |
+
# gate_proj and up_proj to compute the
|
| 3068 |
+
# SwiGLU intermediate vector.
|
| 3069 |
+
if hasattr(layer.mlp, 'gate_proj') and hasattr(layer.mlp, 'up_proj'):
|
| 3070 |
+
hook = layer.mlp.register_forward_hook(
|
| 3071 |
+
make_ffn_top_neurons_hook(layer_idx)
|
| 3072 |
+
)
|
| 3073 |
+
hooks.append(hook)
|
| 3074 |
logger.info(f"Registered attn/MLP output hooks for contribution tracking (ffn_type={ffn_type})")
|
| 3075 |
except Exception as hook_error:
|
| 3076 |
logger.warning(f"Could not register attn/MLP hooks: {hook_error}")
|
|
|
|
| 3092 |
attn_output_norms.clear()
|
| 3093 |
mlp_output_norms.clear()
|
| 3094 |
gate_activation_stats.clear()
|
| 3095 |
+
ffn_top_neurons.clear()
|
| 3096 |
|
| 3097 |
# Forward pass with full outputs
|
| 3098 |
outputs = manager.model(
|
|
|
|
| 3661 |
layer_entry["ffn_contribution"] = round(mlp_n / total, 4)
|
| 3662 |
if layer_idx in gate_activation_stats:
|
| 3663 |
layer_entry["gate_stats"] = gate_activation_stats[layer_idx]
|
| 3664 |
+
if layer_idx in ffn_top_neurons:
|
| 3665 |
+
# `ffn_top_neurons` holds top-K SwiGLU
|
| 3666 |
+
# intermediate activations at the
|
| 3667 |
+
# predicting position — see
|
| 3668 |
+
# make_ffn_top_neurons_hook for the
|
| 3669 |
+
# per-neuron value semantics. The frontend
|
| 3670 |
+
# uses this to render Step 5's per-neuron
|
| 3671 |
+
# bar chart inside "Behind the SwiGLU"
|
| 3672 |
+
# without the backend needing a separate
|
| 3673 |
+
# endpoint or a 14,336-d cache entry.
|
| 3674 |
+
layer_entry["ffn_top_neurons"] = ffn_top_neurons[layer_idx]
|
| 3675 |
|
| 3676 |
# Phase 5: Logit lens at sampled layers (every 8th layer)
|
| 3677 |
logit_lens_stride = max(1, n_layers // 5)
|