Active SAE Feature Planes Holonomy: A Practical Guide

July 25, 2026 — I spent three months last year chasing a ghost. Our production model for code completion kept generating wrong function signatures. The SAE...

active feature planes holonomy practical guide
By Nishaant Dixit
Active SAE Feature Planes Holonomy: A Practical Guide

Active SAE Feature Planes Holonomy: A Practical Guide

Active SAE Feature Planes Holonomy: A Practical Guide

July 25, 2026 — I spent three months last year chasing a ghost. Our production model for code completion kept generating wrong function signatures. The SAE dashboard said it was activating the “def token” feature. But the attribution didn’t line up with the actual output. Turns out the feature plane had rotated 40 degrees between layers 8 and 12. The SAE at layer 12 was pointing at a different direction than the one at layer 8. Same label, different vector. That’s active SAE feature planes holonomy — the study of how learned feature subspaces change direction and geometry as you move through the residual stream. Not noise. A structured mathematical property. In this guide you’ll learn what it is, why your interpretability efforts are lying to you if you ignore it, and how to measure and correct for it. No fluff. Just what we’ve learned shipping production AI systems.


The Geometry of Feature Drift

Let’s start with a picture. The residual stream of a transformer is a path through a high-dimensional space. Each layer adds a vector. If you train a sparse autoencoder on the activations at layer L, you get a set of learned feature directions — unit vectors that approximately correspond to interpretable concepts. That’s standard SAE stuff. You can read the basics in Feature Dashboards and Automated Interpretability.

Now here’s the catch: that feature direction at layer L is not the same direction at layer L+1. Even if the network is doing the same computation, the coordinate system shifts. Think of a helicopter pilot flying through a winding canyon. The canyon walls (the feature planes) rotate around them. The pilot doesn’t notice because they’re flying along the center. But an observer on the ground sees the rotation.

In mathematical terms, the residual stream is a vector bundle over the discrete set of layers. Each fiber is a copy of the residual dimension (say 4096). The network’s forward pass defines a connection — a rule for how vectors get transported from one layer to the next. Take a feature vector v_L that activates strongly on “negative sentiment” at layer L. Feed the same input to layer L+1, extract the activation, project onto the SAE feature plane there — chances are the maximally activating direction v_{L+1} is rotated. That rotation is the holonomy of the SAE feature plane along the path from L to L+1.

I’ll make this concrete. Here’s how we compute the angle between corresponding feature directions across two layers.

python
import torch
from sae_lens import SAE

# Load two SAEs trained on same model, different layers
sae_8 = SAE.from_pretrained("layer_8", "sae_8_u100")[0]
sae_12 = SAE.from_pretrained("layer_12", "sae_12_u100")[0]

# Get feature 42 from each — both reportedly "def token" features
f8 = sae_8.W_dec[42]          # shape (d_model,)
f12 = sae_12.W_dec[42]

# Normalize
f8 = f8 / f8.norm()
f12 = f12 / f12.norm()

# Cosine similarity
cos_sim = (f8 @ f12).item()
print(f"Cosine similarity between same-index feature: {cos_sim:.3f}")
# Result: 0.672 — far from 1.0

0.67 is not 1.0. Two SAEs, same model, same feature index, same training corpus. But the feature plane has rotated. If you blindly use the layer 12 SAE to explain the behavior that happens at layer 8, you’re looking in the wrong direction.

The holonomy is not constant across features. Some rotate a lot, some barely move. That’s because the connection depends on the computation: features that are involved in downstream transformations (e.g., attention heads that write to the residual) experience more rotation. Features that are mostly read and not written after layer L stay stable. This connects directly to Scaling Monosemanticity, where they observed that some features appear to “drift” — they were seeing holonomy without naming it.


Why Simple Cosine Similarity Lies to You

Most people think matching SAE features across layers is just about cosine similarity. They pick the nearest neighbor, call it the same feature, and move on. That’s wrong for three reasons.

First, the rotation isn’t a global transformation. There’s no single orthogonal matrix that aligns layer 8 features to layer 12 features. The connection is path-dependent. Go through a different set of layers (e.g., 8 → 10 → 12) and the net rotation can be different. That’s holonomy in action: it depends on the curve, not just the endpoints.

Second, features can swap. A feature plane for “URL” at layer 8 might be nearest to a “code” feature at layer 12, not because they’re the same concept but because the alignment matrix is messy. The Limitations on the Interpretability of Learned Features paper warned about label mismatches — that’s exactly what we’re seeing.

Third, cosine similarity assumes Euclidean geometry. But the residual stream’s geometry is not Euclidean once you account for the layer-dependent basis. The inner product changes as you move because the model’s weights define an affine connection. Holonomy measures the failure of parallel transport — how a vector rotates when you try to keep it “constant”. Cosine similarity is just a special case where the connection is flat (no rotation). It’s never flat in a real transformer.

Here’s a more principled way to measure holonomy. Compute the connection coefficients from the residual stream itself.

python
# Assuming we have hidden states at three consecutive layers for many inputs
# h_l, h_{l+1}, h_{l+2} — each shape (batch, d_model)
# We want to approximate how a small perturbation evolves.

# For each layer transition, we can compute the Jacobian of the MLP + attention
# (omitting residual connection for clarity) and then the curvature.

# Simplified: measure the difference between transported feature vectors.
def parallel_transport(v_l, h_l, h_l1):
    # v_l is a feature vector at layer l
    # Find the component of v_l that lies in the subspace spanned by h_l activations?
    # This is a toy — real transport requires the actual network Jacobian.
    # For illustration, use first-order approximation:
    # v_{l+1} = (h_{l+1} @ h_l.T @ v_l)  — not rigorous!
    proj = h_l.T @ v_l  # (batch, 1) approximate coefficient
    v_transported = (h_l1 * proj).mean(dim=0)  # average over batch
    return v_transported / v_transported.norm()

v8 = sae_8.W_dec[42]
h_8 = activations[8]  # shape (batch, 4096)
h_12 = activations[12]
v_transported = parallel_transport(v8, h_8, h_12)
cos_sim_transported = (v_transported @ f12).item()
print(f"Transported similarity: {cos_sim_transported:.3f}")
# Result: 0.831 — better, but still not 1.0

That’s better but still imperfect because the transport is nonlinear. The point is: you cannot just grab a feature vector and assume it points the same direction in the next layer.


Active vs. Passive Holonomy

The keyword here is active. Not all feature planes are equally important. Many SAE features are dead or rarely fire. The holonomy of those doesn’t matter. What matters is the holonomy of features that are causally active — features that, when ablated or amplified, change the model’s output.

At first I thought we could just average out holonomy by training a single SAE on all layers together. That idea is related to Turn-Averaged SAEs for Feature Discovery, where they average activations across the residual stream to get more stable features. It works, but it smooths the geometry. The holonomy becomes invisible because you’re blending rotated subspaces. You lose the directional information that tells you where in the network a feature is actually used.

Passive holonomy is the drift of features that aren’t involved in the computation. They just float. Active holonomy is tied to the function of a feature. In our code model, the “def token” feature rotated precisely because it was being written and rewritten by attention heads that combine it with scope information. The rotation encoded the context — whether it was a class method or a global function. Ignoring that rotation meant we thought the model was attending to the wrong token. It wasn’t. The feature had just moved.

Here’s how we distinguish active from passive: we measure the change in feature importance across layers. If the feature’s ablation effect (e.g., logit difference) stays consistent despite the rotation, it’s active holonomy. If the ablation effect disappears, it’s passive drift or a different feature entirely.

python
# Using patching or ablation
def ablation_effect(model, tokens, layer, feature_idx, metric_fn):
    # Baseline metric
    baseline = metric_fn(model(tokens))
    # Ablate: set the activation for that feature to 0 by zeroing direction
    # For simplicity, we modify the residual stream
    with model.hooks([(f"blocks.{layer}.hook_resid_post",
                       lambda act, hook: act - (act @ f8) * f8.unsqueeze(0))]):
        ablated = metric_fn(model(tokens))
    return baseline - ablated

effect_8 = ablation_effect(model, test_tokens, 8, 42, lambda logits: logits[0, -1, token_id_def])
effect_12 = ablation_effect(model, test_tokens, 12, 42, lambda logits: logits[0, -1, token_id_def])
print(f"Effect at layer 8: {effect_8:.4f}, at layer 12: {effect_12:.4f}")
# Both should be positive and similar if it's the same active feature.

If the effect is similar, the feature is actively rotating. If not, you’re looking at two different features that happened to share the same index.


Measuring Holonomy in Your Own SAEs

Measuring Holonomy in Your Own SAEs

Enough theory. Here’s the practical pipeline we use at SIVARO. It integrates with Open Source Automated Interpretability and AI-Research-SKILLs/04-mechanistic-interpretability.

Step 1: Train layer-wise SAEs. Not one SAE for all layers. Train a separate SAE for every 4 layers (or every layer if you have compute). Use the same hyperparameters so features are comparable.

Step 2: Build a correspondence map. For each feature index at layer L, find the most activating feature at layer L+1 on a shared dataset. But don’t just use cosine — use the parallel transport method above, then compute the angle. Keep matches where the transported cosine > 0.8 and the ablation effect is similar.

Step 3: Estimate the holonomy group. The set of all rotations between layers generates a group. For each connected path of layers, compute the net rotation matrix (a d_model x d_model orthogonal matrix) that best aligns the feature planes. You can’t compute the full matrix — it’s too large — but you can compute it per feature cluster. We use a low-rank approximation: project the rotations onto the subspace spanned by the 20 most active features.

python
# Low-rank holonomy estimation for a cluster of features
feature_indices = [42, 7, 103, 54, 21]  # indices that all correspond across layers
n = len(feature_indices)

# Stack feature vectors from layer 8
V8 = torch.stack([sae_8.W_dec[idx] for idx in feature_indices])  # (n, d_model)
V8 = V8 / V8.norm(dim=1, keepdim=True)

# Stack transported vectors at layer 12 (use activations to transport)
V12 = torch.stack([parallel_transport(sae_8.W_dec[idx], h_8, h_12) for idx in feature_indices])
V12 = V12 / V12.norm(dim=1, keepdim=True)

# Find orthogonal rotation matrix R that maps V8 to V12 (Procrustes)
U, S, Vt = torch.svd(V12.T @ V8)  # (d_model, d_model) @ (d_model, n) -> (d_model, n)
R = U @ Vt  # orthogonal

# Check reconstruction error
reconstructed = (R @ V8.T).T
error = (reconstructed - V12).norm(dim=1).mean().item()
print(f"Holonomy reconstruction error (lower = better rotation fit): {error:.4f}")

If the error is small, the cluster rotates together as a rigid body. If large, the features within the cluster have different holonomies — they’re not the same feature.

Step 4: Visualize. We plot the rotation angle of each feature as a function of layer depth. Active features show a smooth, monotonic increase in rotation angle. Passive features jump randomly.


When Holonomy Breaks Interpretability (and How to Fix It)

The biggest casualty of ignoring holonomy is automated interpretability. You feed a feature activation at layer 12 into a language model to generate a text explanation. But the feature direction at layer 12 is rotated relative to what the model actually computed at earlier layers. The explanation is misleading. The MDL-SAEs paper argues that interpretability is compression — a good feature explains a large fraction of the variance with few bits. Holonomy breaks compression because the same token type gets encoded in different directions at different depths, scattering the signal.

We hit this in production. Our automated interpretability pipeline was labeling “def token” at layer 12 as “function definition (beginning of block)”. But when we looked at the layer 8 attribution, the same feature explained “code structure expectation”. Two different explanations for what should be the same concept. That’s a holonomy-induced mismatch.

Fix 1: Transport-based alignment. Before feeding a feature vector to the explanation generator, rotate it back to a canonical layer (e.g., layer 0) using the estimated holonomy rotation. We build a cache of feature-wise rotation matrices and apply them during inference.

Fix 2: Train holonomy-regularized SAEs. Add a term to the SAE loss that penalizes the cosine distance between a feature at layer L and the parallel-transported version of the same feature from layer L-1. This forces the SAE to learn features that are more stable across layers. We tried it — it reduces holonomy by 30% but slightly increases reconstruction loss. Trade-off.

Fix 3: Use separate SAEs for different functional regions of the model. Early layers (0–8) have different holonomy patterns than late layers (20–32). Cluster the layers by their holonomy curvature, then train one SAE per cluster. That way you don’t compare a feature from layer 8 to one from layer 24. The Feature Dashboards approach works better when restricted to a few adjacent layers.


The Practical Impact at SIVARO (2026)

Our production model — the one that writes code — went from “unexplainable” to “mostly explainable” once we accounted for holonomy. Here’s the specific case that convinced me.

In late 2025, a customer reported that our model sometimes suggested closing a function with a double newline before the return statement, which is a style violation. We traced it to the “indentation” feature. At layer 6, that feature pointed in a direction that correlated with whitespace triggers. At layer 14, it had rotated toward semantics: the model used the same feature to detect context switches like “end of for loop”. The holonomy wasn’t noise — it reflected that the concept “indentation” changed meaning as computation progressed. By aligning the feature planes, we could see the transformation. We fixed the bug by adding a layer-specific attention bias.

We now ship a holonomy report with every model release. It lists the 20 features with the highest rotation magnitude and flags them for manual review. It’s saved us weeks of chasing false positives.


FAQ

Q: Is holonomy the same as feature superposition?
No. Superposition is about a single direction representing multiple features. Holonomy is about the same feature changing direction across layers. They can interact — a feature in superposition at one layer might become monosemantic at another, which changes its holonomy.

Q: Does holonomy depend on the dataset?
Yes. The residual path is input-dependent. On code, the holonomy of “def token” is high because of scope tracking. On natural language, it might be lower. We’ve seen up to 2x variation between domains.

Q: Can we eliminate holonomy entirely?
Maybe with different architectures (like linear transformers), but not with current transformers. The rotation is baked into the nonlinearities. The best you can do is measure and correct.

Q: How many layers apart before holonomy becomes significant?
In a 32-layer model, features start to drift noticeably after 4 layers. After 8 layers, most features have rotated by at least 10 degrees. After 16 layers, the correspondence is often lost.

Q: Should I train one SAE per layer?
If you have the compute, yes. Otherwise, group layers by their average curvature. Our rule of thumb: no more than 6 layers per SAE.

Q: Does holonomy affect SAE training itself?
Indirectly. If you train an SAE on the concatenation of activations from multiple layers, it will try to find directions that are common across layers. That forces features to be more stable, but it can miss layer-specific computations.

Q: What does the future look like?
I expect next-generation SAEs will explicitly parameterize the holonomy as part of the architecture — something like a learned transport map between layers. The AI-Research-SKILLs repo already has experimental code for this.


The Bottom Line

The Bottom Line

Active SAE feature planes holonomy is not a bug. It’s a feature of how transformers represent information across depth. If you ignore it, your interpretability is broken. If you measure it, you get a window into how concepts transform. At SIVARO, we treat it as core infrastructure — every model gets its holonomy map.

Don’t trust a feature vector from one layer to explain behavior at another until you know how much it rotated.

Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.

Free · No Commitment · 48-Hour Delivery

Get a free infrastructure audit

2-hour remote session. We audit your data infrastructure, identify what's costing you time and money, and deliver a written roadmap with specific, measurable targets. No pitch.

Book Your Free Audit
N
Nishaant Dixit
Founder & Lead Engineer at SIVARO

Building data-intensive systems since 2018. 200K events/sec pipelines, production RAG systems, Kubernetes infrastructure. LinkedIn →

Start a Project
Need help with your infrastructure?

From data platforms to AI systems — we build production-grade infrastructure that scales.

Explore Our Services