diagnostics(sae): SparseAutoencoder hook + top-K feature extraction - #316
Draft
jamesburton wants to merge 3 commits into
Draft
diagnostics(sae): SparseAutoencoder hook + top-K feature extraction#316jamesburton wants to merge 3 commits into
jamesburton wants to merge 3 commits into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #311.
ROADMAP Phase 7 Step 50 — SAE integration. Implements the SAE inference primitive +
PostLayerhook on top of the #32 hook system and the #33 logit-lens precedent.Stack
Depends on:
IInferenceHook,HookRegistry, 8-pointTransformerModel.Forwardintegration).This PR is opened as a DRAFT because it stacks on the above. Will rebase to
mainonce #32 and #33 merge.What lands
ISparseAutoencoder(non-breaking extension): addsHiddenSize+ XML doc comments on the existing 3-member placeholder. OriginalEncode/Decode/FeatureCountcontract preserved.SparseAutoencoder— concrete implementation holdingW_enc[d_in, d_sae],b_enc[d_sae],W_dec[d_sae, d_in],b_dec[d_in]as 64-byte-alignedNativeMemory.AlignedAllocbuffers per project memory rules. Implements the canonical SAE forward documented indocs/DIAGNOSTICS.md:SaeHook : IInferenceHookatHookPoint.PostLayer. MirrorsLogitLensHook's defer pattern:OnActivationclones the hidden state on the hot path and returnsContinue; encode + top-K + L2 reconstruction error compute once inGetResults(). Read-only — never returnsReplace(steering is an explicit follow-up).SaeConfig— layer / position selection (reusesLogitLensLayerSelector) +TopK.SaeResult— per-(layer, position) feature indices, magnitudes, reconstruction error, pre-truncation active count.SaeLoader— minimal in-DotLLM.Diagnosticssafetensors reader (F32 only) +SaeCfg.ParseJsonfor SAELens-stylecfg.json(d_in,d_sae,apply_b_dec_to_input,hook_name). Kept inside Diagnostics to avoid expanding the project's dependency surface — the publicLoad/LoadFromBytesAPI is the retarget seam when a shared safetensors reader lands.SaeMath(internal) —TopK+L2Distancepure 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:recon = b_dec + sum(value * W_dec[index]).b_dec/ activation.sqrt(sum-of-squared-dropped-magnitudes).ReconstructionError == 0).SaeHookthroughHookRegistrywith a syntheticPostLayeractivation, verifySaeResultpopulates with indices / magnitudes / active count.Continue.SaeLoader.LoadFromBytes, verifyEncodematches the span-constructed SAE.cfg.jsonapply_b_dec_to_inputflag honored; cfg/header shape mismatch fatal;F16dtype rejected with a clearNotSupportedException.EncodethrowsObjectDisposedException.Full
DotLLM.Tests.Unitsuite: 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.csgets an optional--sae <path>flag. When provided, anSaeHookis 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:
HookResult.ReplaceEleutherAI/sae-llama-3-8b)