garyboon Claude Opus 4.7 (1M context) commited on
Commit
019492b
·
1 Parent(s): e11c277

Emit per-layer top-K SwiGLU intermediate activations for RQ1 Step 5

Browse files

Adds 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>

Files changed (1) hide show
  1. backend/model_service.py +87 -0
backend/model_service.py CHANGED
@@ -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)