Diff Features
Stop guessing which features matter — let the diff between two prompts tell you
- conditioning_a
- conditioning_b
- VISUALIZATION
- DIFF_REPORT
- A_FEATURES
- B_FEATURES
The pack's own docs call this its most powerful research tool, and they're not wrong. Instead of fishing through Feature Map hoping to spot which features matter, you hand Diff Features two conditionings - say "cinematic photo" and "snapshot photo" - and it runs both through your SAE, finds the features that fire differently, and hands you a ready-to-paste list for Feature Gate or Feature Probe. It's a hypothesis generator: it tells you what's actually different between two concepts in your encoder's feature space, before you spend any GPU on probing.
How it works
Both conditionings get encoded through the SAE to sparse feature activations. The node then diffs them - per feature, abs(activation_a - activation_b) - keeps the biggest differences above a threshold, and reports them per side. Features that light up in A but not B are "what A has that B doesn't," and vice versa. pool_mode controls whether activations are averaged across tokens (default mean) or max-pooled.
The clever bit is save_lens. Enable it and the differential direction gets saved as a .pt lens in lenses/ - which means the difference between two concepts becomes a steerable lens. That's a genuinely neat workflow: "cinematic minus snapshot" as a reusable direction you can apply, attenuate, or invert with the regular Concept Steer node. The brief even names the pattern: instead of guessing features, let the diff tell you what makes "oil painting" different from "watercolor."
The inputs that matter
- conditioning_a / conditioning_b - the two conditionings to compare (e.g. "cinematic photo" vs "snapshot photo"). Encode each with a CLIP Text Encode.
- sae_path - the SAE both sides decompose through. Must be the same one used in Feature Map/Gate.
- top_k - top differential features per side (15 default, 3–50).
- min_diff - minimum absolute difference to include a feature. Raise to filter noise; lower to see subtle differences. This is the dial you'll actually tune.
- sae_expansion / transcoder_repo - match your SAE, or point at a pretrained transcoder repo.
- pool_mode -
meanrecommended per the tooltip. - save_lens / lens_name / protect_existing - save the discovered difference as a lens (auto-renames
_v2etc. so it won't clobber).
Outputs: VISUALIZATION (IMAGE), DIFF_REPORT (STRING), plus A_FEATURES and B_FEATURES - the paste-ready feature lists per side.
Installing
Standard pack install - ComfyUI Manager search "Concept Steer", or git clone https://github.com/Nynxz/ComfyUI-ConceptSteer into custom_nodes/ and restart. Needs a trained SAE (or transcoder repo) and transformers/safetensors. Note the conditioning inputs come from your text encoder at the same layer the SAE targets, so keep layer/expansion consistent.
Common gotchas
- min_diff is the noise filter and it's easy to forget. At the 0.01 default you'll see a long tail of near-zero diffs. If the report is noisy, raise it.
- The diff is only as meaningful as the SAE. A weak SAE (synthetic data, bad L1) produces diffs between noise, not concepts. Fineweb-trained or pretrained transcoders give far cleaner answers.
- Same SAE everywhere. Decompose both conditionings and your downstream gates with one consistent SAE or the indices drift into meaninglessness.
- Prompt phrasing biases the diff. "Cinematic photo" vs "snapshot photo" differs mostly in style because the content is held constant - that's the intended setup. If your two prompts also differ in subject, the diff mixes style and content. Keep content fixed when you want a style-only answer.
Inputs (12)
| Name | Type | Default | Description |
|---|---|---|---|
| conditioning_a | CONDITIONING | First conditioning (e.g. 'cinematic photo') | |
| conditioning_b | CONDITIONING | Second conditioning (e.g. 'snapshot photo') | |
| sae_path | STRING | Absolute path to SAE weights (.pt file). Must be the same SAE used in Feature Map/Gate. | |
| top_k | INT | 153–50 | Number of top differential features to show per side |
| min_diff | FLOAT | 0.0100–1 | Minimum absolute difference to include a feature. Raise this to filter noise. Lower to see subtle differences. |
| sae_expansion | INT | 82–128 | SAE expansion factor (auto-detected for transcoders). 8x for trained SAEs, 64x for pretrained transcoders. |
| transcoder_repo | STRING | HuggingFace repo for pretrained transcoders (e.g. 'mwhanna/qwen3-4b-transcoders'). Auto-downloads on first use. Leave empty to use sae_path instead. | |
| pool_mode | COMBO | mean | 'mean' = average across tokens (recommended). 'max' = max activation per feature across tokens. |
| save_lens | BOOLEAN | false | Save the differential direction as a .pt lens file in the lenses/ directory. Use with Concept Steer to apply the discovered concept difference. |
| lens_name | STRING | diff_discovery | Name for the saved lens file (only used when save_lens is enabled). |
| dict_path | STRING | Path to feature dictionary JSON (from Feature Dictionary node). When provided, differential features are labeled. | |
| protect_existing | BOOLEAN | true | If the lens file already exists (save_lens=True), save as _v2, _v3, … instead of overwriting. |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| VISUALIZATION | IMAGE | — |
| DIFF_REPORT | STRING | — |
| A_FEATURES | STRING | — |
| B_FEATURES | STRING | — |