Skip to content

diagnostics(sae): SparseAutoencoder hook + top-K feature extraction - #316

Draft
jamesburton wants to merge 3 commits into
kkokosa:mainfrom
jamesburton:issue/311-diagnostics-sae-integration
Draft

diagnostics(sae): SparseAutoencoder hook + top-K feature extraction#316
jamesburton wants to merge 3 commits into
kkokosa:mainfrom
jamesburton:issue/311-diagnostics-sae-integration

Conversation

@jamesburton

Copy link
Copy Markdown

Closes #311.

ROADMAP Phase 7 Step 50SAE integration. Implements the SAE inference primitive + PostLayer hook on top of the #32 hook system and the #33 logit-lens precedent.

Stack

Depends on:

This PR is opened as a DRAFT because it stacks on the above. Will rebase to main once #32 and #33 merge.

What lands

  • ISparseAutoencoder (non-breaking extension): adds HiddenSize + XML doc comments on the existing 3-member placeholder. Original Encode / Decode / FeatureCount contract preserved.
  • SparseAutoencoder — concrete implementation holding W_enc [d_in, d_sae], b_enc [d_sae], W_dec [d_sae, d_in], b_dec [d_in] as 64-byte-aligned NativeMemory.AlignedAlloc buffers per project memory rules. Implements the canonical SAE forward documented in docs/DIAGNOSTICS.md:
    pre = activation - b_dec        (when ApplyBDecToInput; SAELens convention)
    features = relu(pre @ W_enc + b_enc)
    active = top-K features by magnitude
    reconstruction = sum(active @ W_dec_rows) + b_dec
    
  • SaeHook : IInferenceHook at HookPoint.PostLayer. Mirrors LogitLensHook's defer pattern: OnActivation clones the hidden state on the hot path and returns Continue; encode + top-K + L2 reconstruction error compute once in GetResults(). Read-only — never returns Replace (steering is an explicit follow-up).
  • SaeConfig — layer / position selection (reuses LogitLensLayerSelector) + TopK.
  • SaeResult — per-(layer, position) feature indices, magnitudes, reconstruction error, pre-truncation active count.
  • SaeLoader — minimal in-DotLLM.Diagnostics safetensors reader (F32 only) + SaeCfg.ParseJson for SAELens-style cfg.json (d_in, d_sae, apply_b_dec_to_input, hook_name). Kept inside Diagnostics to avoid expanding the project's dependency surface — the public Load / LoadFromBytes API is the retarget seam when a shared safetensors reader lands.
  • SaeMath (internal) — TopK + L2Distance pure functions, testable without weights.

Tests

28 new unit tests in tests/DotLLM.Tests.Unit/Diagnostics/SaeHookTests.cs, all model-free and using synthetic SAEs with hand-computable expected values:

  • Pure math: TopK descending order + K-clamping; L2Distance symmetry + zero-on-equality.
  • Synthetic SAE forward: hand-computed Encode/Decode against a 2×3 SAE — pre-ReLU output, ReLU clamp on negative inputs, decode formula recon = b_dec + sum(value * W_dec[index]).
  • ApplyBDecToInput: SAELens pre-subtraction verified against the same hand-computed test with shifted b_dec / activation.
  • Top-K truncation: deliberately sparsity-inducing weights, K=2 drops the two smallest features; reconstruction error matches the closed-form sqrt(sum-of-squared-dropped-magnitudes).
  • Identity SAE round-trip: encode → decode reconstructs non-negative input bit-exact (ReconstructionError == 0).
  • Hook integration: fire SaeHook through HookRegistry with a synthetic PostLayer activation, verify SaeResult populates with indices / magnitudes / active count.
  • Hook filtering: layer-selector + position-selector restrict captures as expected; activation length mismatch throws; hook always returns Continue.
  • Loader round-trip: build a synthetic safetensors buffer in-memory (no binary fixtures), load via SaeLoader.LoadFromBytes, verify Encode matches the span-constructed SAE. cfg.json apply_b_dec_to_input flag honored; cfg/header shape mismatch fatal; F16 dtype rejected with a clear NotSupportedException.
  • Dispose: idempotent; post-Dispose Encode throws ObjectDisposedException.

Full DotLLM.Tests.Unit suite: 1163/1163 non-CUDA tests pass (CUDA tests fail in this environment for lack of a CUDA device — pre-existing and unrelated).

Sample

samples/DotLLM.Sample.Interpretability/Program.cs gets an optional --sae <path> flag. When provided, an SaeHook is registered at the middle layer for the final prompt position alongside the existing logit lens, and top-K features + reconstruction error are printed. When omitted the original logit-lens-only workflow is unchanged.

Out of scope (follow-ups)

Documented in #311 — left as separate issues to keep this PR reviewable:

  • Steering / ablation via HookResult.Replace
  • F16 / BF16 weight dtypes in the SAE loader
  • Curated SAE registry JSON (EleutherAI / Goodfire / Llama Scope)
  • Neuronpedia feature-label client
  • Serve / Chat UI panels
  • Real-checkpoint integration test (e.g. EleutherAI/sae-llama-3-8b)

jamesburton and others added 3 commits June 8, 2026 16:08
…nsformerModel forward integration (#32)

Implements the inference hook system described in upstream issue #32: a zero-cost
diagnostic hook surface for activation inspection and intervention across the
transformer forward pass.

What lands:

- HookRegistry (DotLLM.Core.Diagnostics): thread-safe register/unregister with
  copy-on-write per-point hook lists. HasHookAt is an O(1) flag lookup; Fire is
  lock-free and threads ReplaceResult outputs back into the activation buffer for
  downstream hooks and downstream computation. Length-mismatch on Replace throws.
- HookResult.Replace(ReadOnlySpan<float>) overload added — satisfies the issue's
  spec literally for span-holding callers. The original float[] overload is
  preserved.
- CaptureHook (DotLLM.Diagnostics): built-in IInferenceHook that snapshots
  activations into an indexed dictionary, optionally filtered by layer set and
  token-position set. Captures are independent copies (not aliasing the source).
- TransformerModel.Forward: fire calls inserted at all 8 HookPoint locations —
  PostEmbedding, PreAttention/PostAttention/PreFfn/PostFfn/PostLayer (per layer),
  PreLmHead, PostLmHead. Each site is guarded by `Hooks is not null &&
  Hooks.HasHookAt(point)`. Fired per token with the token's position threaded into
  HookContext.
- Fused-decode path preserved when hooks are off: PreAttention/PreFfn only
  materialize normOut when their hook is registered. With Hooks=null the hot path
  is byte-identical to today.
- TransformerModel exposes settable Hooks/SequenceId/CurrentStep properties for
  callers performing per-request tracing.

Tests (25 new, all passing):

- HookRegistryTests: registration/unregistration round-trips, Continue
  pass-through, Replace mutation, in-order multi-hook fire, replacement threading,
  length-mismatch guard, and two zero-allocation tests verifying GC.GetAllocatedBytesForCurrentThread
  delta == 0 for the null and empty-registry guarded patterns over 10K iterations.
- CaptureHookTests: filter intersection, independent-copy semantics, registry
  round-trip, non-layer point keying.

Pre-existing Models tests (79) all pass — no regressions on the forward path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ction + sample (#33)

Adds the logit-lens interpretability technique on top of the #32 hook
system: capture residual-stream hidden states at PostLayer for selected
layers, project each through the model's final RMSNorm + LM head, and
extract per-layer softmax distributions with top-K, entropy, and
convergence analysis.

- ILogitsProjector (Core/Diagnostics): minimal hidden→logits abstraction
  implemented by TransformerModel.ProjectToLogits, which reuses the same
  final-norm + LM-head GemmInterleaved path Forward takes at seqLen == 1.
  Guarantees a bit-exact match between final-layer lens output and the
  model's logits row (verified by a dedicated test).
- LogitLensHook: PostLayer hook that clones hidden states on the hot path
  and defers projection/softmax/top-K to GetResults().
- LogitLensConfig + LogitLensLayerSelector: all-layers / every-Nth /
  specific-list selection, configurable top-K, optional full-distribution
  storage, optional token-position filter.
- LogitLensAnalysis: stateless ConvergenceLayer, ConfidenceAcrossLayers,
  and RankOf helpers operating over results.
- LogitLensMath (internal): numerically stable softmax, Shannon entropy,
  top-K extraction — pure functions, testable without a model.
- Sample: samples/DotLLM.Sample.Interpretability/Program.cs loads a GGUF
  model, runs a prompt, prints a per-layer top-5 table plus convergence
  layer and confidence trajectory.
- Tests: 16 LogitLens tests — pure-math correctness (softmax sum-to-one,
  numerical stability, entropy, top-K), hook plumbing (layer/position
  filters, every-Nth selector, top-K clamping, valid distributions),
  analysis helpers (convergence, confidence trajectory, rank), and a
  SkippableFact discriminating test that asserts bit-exact equality
  between the model's final logits row and the lens projection of the
  final layer's PostLayer capture on SmolLM-135M Q8_0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…raction (#311)

Implements the SAE integration described in upstream issue #311 (ROADMAP Phase 7
Step 50): a sparse-autoencoder inference primitive plus PostLayer hook surface
on top of the #32 hook system.

What lands:

- ISparseAutoencoder (extended, non-breaking): the existing 3-member interface
  on the dev branch was a placeholder; this PR adds a HiddenSize property and
  fills in the XML doc comments. The Encode/Decode/FeatureCount contract is
  preserved.
- SparseAutoencoder: concrete implementation holding W_enc, b_enc, W_dec, b_dec
  as 64-byte-aligned NativeMemory.AlignedAlloc buffers (per project memory
  rules). Implements the canonical SAE forward documented in
  docs/DIAGNOSTICS.md: features = relu((activation - b_dec? ) @ W_enc + b_enc);
  reconstruction = top-K features @ W_dec + b_dec. Optional ApplyBDecToInput
  (SAELens convention) honored via constructor flag.
- SaeHook : IInferenceHook at HookPoint.PostLayer. Mirrors LogitLensHook's
  defer pattern — OnActivation clones the hidden state and returns Continue;
  encode + top-K + L2 reconstruction error are computed once in GetResults().
  Read-only contract: never returns Replace (steering is an explicit follow-up).
- SaeConfig: layer/position selection (reuses LogitLensLayerSelector) and TopK.
- SaeResult: per-(layer, position) result with feature indices, magnitudes,
  reconstruction error, and pre-truncation active-feature count.
- SaeLoader: minimal in-Diagnostics safetensors reader (F32 only) plus
  SaeCfg.ParseJson for SAELens-style cfg.json (d_in, d_sae,
  apply_b_dec_to_input, hook_name). Kept inside DotLLM.Diagnostics to avoid
  expanding the project's dependency surface to DotLLM.Models — the
  public Load / LoadFromBytes surface is the retarget seam if Diagnostics
  ever depends on a shared safetensors reader.
- SaeMath (internal): TopK + L2Distance helpers — pure functions, testable
  without an SAE.

Tests (28 new, all passing):

- Pure-math: TopK descending order, K-clamping; L2Distance symmetry / zero-on-equality.
- Synthetic SAE forward: hand-computed Encode/Decode against a 2x3 SAE with
  known weights — pre-ReLU output, ReLU clamp on negative inputs, decode
  formula recon = b_dec + sum(value * W_dec[index]). ApplyBDecToInput
  pre-subtraction verified against the same hand-computed test with shifted
  b_dec / activation.
- Top-K truncation: deliberately sparsity-inducing weights, K=2 drops the
  two smallest features and the reconstruction-error matches the closed-form
  sqrt(sum-of-squared-dropped-magnitudes).
- Identity SAE round-trip: encode + decode reconstructs non-negative input
  bit-exact, ReconstructionError == 0.
- Hook integration: fire SaeHook through HookRegistry with a synthetic
  PostLayer activation, verify SaeResult populates with indices / magnitudes
  / active count.
- Hook filtering: layer-selector and position-selector restrict captures as
  expected; activation length mismatch throws (catches misconfigured layer
  bindings); hook always returns Continue.
- Loader round-trip: build a synthetic safetensors buffer in-memory (no
  binary fixtures), load via SaeLoader.LoadFromBytes, verify Encode matches
  the span-constructed SAE. cfg.json apply_b_dec_to_input flag honored;
  cfg/header shape mismatch fatal; F16 dtype rejected with a NotSupportedException
  rather than silent misinterpretation.
- Dispose: idempotent; post-Dispose Encode throws ObjectDisposedException.

Sample: samples/DotLLM.Sample.Interpretability/Program.cs gets an optional
`--sae <path>` flag — when provided, an SaeHook is registered at the middle
layer for the final prompt position alongside the existing logit lens, and
top-K features + reconstruction error are printed. When omitted the original
logit-lens-only workflow is unchanged.

Out of scope (explicit follow-ups, documented in #311):

- Steering / ablation via HookResult.Replace
- F16 / BF16 weight dtypes in the SAE loader
- Curated SAE registry JSON
- Neuronpedia feature-label client
- Serve / Chat UI panels
- Real-checkpoint integration test

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

diagnostics(sae): SparseAutoencoder hook + offline-trained SAE loader (ROADMAP Phase 7 Step 50)

1 participant