antebe1 commited on
Commit
0042732
Β·
verified Β·
1 Parent(s): 0fef966

Upload trained model

Browse files
Files changed (7) hide show
  1. README.md +163 -0
  2. config.json +7 -0
  3. demo.py +131 -0
  4. dfc.py +222 -0
  5. hparams.json +19 -0
  6. model.pt +3 -0
  7. requirements.txt +8 -0
README.md ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - sparse-autoencoder
4
+ - crosscoder
5
+ - interpretability
6
+ - qwen2
7
+ - mechanistic-interpretability
8
+ - dictionary-learning
9
+ license: mit
10
+ ---
11
+
12
+ # dfc-D8k-excl3-freeexcl-k90
13
+
14
+ A **DFC** sparse crosscoder trained to compare layer-13 activations between:
15
+ - **Model A (ToolRL)**: `chengq9/ToolRL-Qwen2.5-3B` β€” fine-tuned with tool-use reinforcement learning
16
+ - **Model B (Base)**: `Qwen/Qwen2.5-3B` β€” vanilla base model
17
+
18
+ ## What is this?
19
+
20
+ This model learns a sparse dictionary of features from the internal representations of two language models. By comparing which features activate for which model, we can identify:
21
+ - **What the ToolRL fine-tuning changed** (A-exclusive features)
22
+ - **What remained the same** (shared features)
23
+ - **What the base model does that ToolRL suppressed** (B-exclusive features)
24
+
25
+ ## Model Architecture
26
+
27
+ Dedicated Feature CrossCoder with partitioned dictionary (0.03/0.03 A/B exclusive)
28
+
29
+ | Parameter | Value |
30
+ |-----------|-------|
31
+ | Dictionary size | 8192 |
32
+ | Top-k active features | 90 |
33
+ | Layer | 13 (middle layer of Qwen2.5-3B) |
34
+ | Activation dimension | 2048 |
35
+ | A-exclusive features | 245 (0.03) |
36
+ | B-exclusive features | 245 (0.03) |
37
+ | Shared features | 7702 |
38
+
39
+ ### How it works
40
+
41
+ 1. **Encode**: Takes stacked activations `(batch, 2, 2048)` from both models, applies per-model encoder weights, sums across models, and selects the top-90 features via ReLU + top-k.
42
+ 2. **Decode**: Reconstructs per-model activations from the sparse feature vector using per-model decoder weights.
43
+ 3. **Partition masks** (DFC only): Hard binary masks zero out encoder/decoder weights to enforce that exclusive features cannot be used by the wrong model.
44
+
45
+ ## Training
46
+
47
+ | Parameter | Value |
48
+ |-----------|-------|
49
+ | Loss function | MSE + L1 on shared features only (coef: 1e-3); exclusive features unpenalized |
50
+ | Training steps | 9000 |
51
+ | Learning rate | 1e-4 |
52
+ | Batch size | 1024 |
53
+ | Sparsity coefficient (shared) | 1e-3 |
54
+ | Exclusive sparsity coefficient | 0 |
55
+ | Optimizer | Adam (grad clip 1.0) |
56
+ | W&B project | `dfc-crosscoder-sweep` |
57
+
58
+ ### Training Data
59
+
60
+ - **FineWeb**: ~40,000 general web text samples (from `HuggingFaceFW/fineweb` sample-10BT)
61
+ - **ToolRL**: ~40,000 tool-use conversation samples (from `emrecanacikgoz/ToolRL`, cycled)
62
+ - Activations extracted from layer 13, last token per sample
63
+ - Both datasets concatenated and z-score normalized
64
+
65
+ ## Usage
66
+
67
+ ### Quick Start
68
+
69
+ ```python
70
+ import torch
71
+ from huggingface_hub import hf_hub_download
72
+
73
+ # Download model files
74
+ repo_id = "antebe1/dfc-D8k-excl3-freeexcl-k90"
75
+ for fname in ["model.pt", "config.json", "dfc.py"]:
76
+ hf_hub_download(repo_id=repo_id, filename=fname, local_dir="./model")
77
+
78
+ # Load the crosscoder
79
+ import sys; sys.path.insert(0, "./model")
80
+ from dfc import DFCCrossCoder
81
+
82
+ dfc = DFCCrossCoder.load("./model", device="cuda")
83
+ print(f"Loaded: dict_size={dfc.dict_size}, k={dfc.k}")
84
+ ```
85
+
86
+ ### Extract Features from Real Models
87
+
88
+ ```python
89
+ from transformers import AutoModelForCausalLM, AutoTokenizer
90
+
91
+ # Load both models
92
+ model_a = AutoModelForCausalLM.from_pretrained("chengq9/ToolRL-Qwen2.5-3B", device_map="cuda:0")
93
+ model_b = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-3B", device_map="cuda:1")
94
+ tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-3B")
95
+
96
+ # Get activations from layer 13
97
+ # NOTE: hidden_states[0] = embeddings, hidden_states[i] = output of layer i-1
98
+ # so layer 13 activations are at index 13+1
99
+ text = "Use the search tool to find recent papers on RLHF"
100
+ inputs = tokenizer(text, return_tensors="pt")
101
+
102
+ with torch.no_grad():
103
+ out_a = model_a(**inputs.to("cuda:0"), output_hidden_states=True)
104
+ out_b = model_b(**inputs.to("cuda:1"), output_hidden_states=True)
105
+ act_a = out_a.hidden_states[13 + 1][:, -1, :] # last token, layer 13
106
+ act_b = out_b.hidden_states[13 + 1][:, -1, :]
107
+
108
+ # Stack and encode
109
+ activations = torch.stack([act_a.cpu(), act_b.cpu()], dim=1) # (1, 2, 2048)
110
+ features = dfc.encode(activations.to(dfc.W_enc.device))
111
+
112
+ print(f"Active features: {(features > 0).sum().item()} / {dfc.dict_size}")
113
+ ```
114
+
115
+ ### Analyze Partitions (DFC only)
116
+
117
+ ```python
118
+ stats = dfc.feature_stats(features)
119
+ print(f"L0 total: {stats['l0_total']:.1f}")
120
+ print(f"L0 A-excl: {stats['l0_a_excl']:.1f}")
121
+ print(f"L0 B-excl: {stats['l0_b_excl']:.1f}")
122
+ print(f"L0 shared: {stats['l0_shared']:.1f}")
123
+
124
+ # Check reconstruction quality
125
+ recon, feats = dfc(activations.to(dfc.W_enc.device))
126
+ mse = torch.nn.functional.mse_loss(recon.cpu(), activations)
127
+ print(f"Reconstruction MSE: {mse.item():.6f}")
128
+ ```
129
+
130
+ ## Files
131
+
132
+ | File | Description |
133
+ |------|-------------|
134
+ | `model.pt` | PyTorch state dict (encoder/decoder weights + partition masks) |
135
+ | `config.json` | Architecture config: dict_size, k, partition sizes (n_a, n_b) |
136
+ | `hparams.json` | Full training hyperparameters including loss, lr, steps, etc. |
137
+ | `dfc.py` | `DFCCrossCoder` class definition β€” required to load model.pt |
138
+ | `demo.py` | Feature extraction demo (works with downloaded model) |
139
+ | `requirements.txt` | Python dependencies |
140
+
141
+ ## Part of a Sweep
142
+
143
+ This model is one of 48 models in a hyperparameter sweep. See the full collection:
144
+
145
+ | Axis | Values |
146
+ |------|--------|
147
+ | k (top-k) | 45, 90, 160 |
148
+ | dict_size | 8,192 / 16,384 |
149
+ | Architecture | DFC (partitioned) / CrossCoder (all shared) |
150
+ | Exclusive % (DFC) | 3%, 5%, 10% |
151
+ | Exclusive sparsity | 1e-3 (penalized) / 0 (free) |
152
+ | CrossCoder L1 | with / without |
153
+
154
+ ## Citation
155
+
156
+ ```bibtex
157
+ @misc{dfc-D8k-excl3-freeexcl-k90,
158
+ title={DFC CrossCoder: ToolRL vs Base Qwen2.5-3B},
159
+ author={Andre Shportko},
160
+ year={2026},
161
+ url={https://huggingface.co/antebe1/dfc-D8k-excl3-freeexcl-k90}
162
+ }
163
+ ```
config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "activation_dim": 2048,
3
+ "dict_size": 8192,
4
+ "k": 90,
5
+ "n_a": 245,
6
+ "n_b": 245
7
+ }
demo.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DFC CrossCoder Demo - Quick Feature Vector Extraction
3
+ =====================================================
4
+
5
+ This demo shows how to use the DFC CrossCoder to extract feature vectors
6
+ from text using the ToolRL vs Base Qwen2.5-3B comparison.
7
+
8
+ Works both locally (from checkpoints/) and from a HuggingFace download.
9
+ """
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import torch
15
+ from transformers import AutoModelForCausalLM, AutoTokenizer
16
+ from dfc import DFCCrossCoder
17
+
18
+ # Default model path β€” override with command line arg or set MODEL_PATH env var
19
+ import os
20
+ DEFAULT_MODEL_PATH = os.environ.get("DFC_MODEL_PATH", "./checkpoints/dfc2")
21
+ LAYER = 13
22
+
23
+
24
+ def quick_demo(model_path: str = DEFAULT_MODEL_PATH):
25
+ """10-line core demo: Load models and extract features from text"""
26
+
27
+ # Load the trained CrossCoder
28
+ dfc = DFCCrossCoder.load(model_path, device="cuda" if torch.cuda.is_available() else "cpu")
29
+
30
+ # Load tokenizer and models (for demo - in practice you might use cached activations)
31
+ tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-3B")
32
+ if tokenizer.pad_token is None:
33
+ tokenizer.pad_token = tokenizer.eos_token
34
+
35
+ # Example texts: general vs tool-use
36
+ general_text = "The French Revolution began in 1789 with widespread social discontent."
37
+ tooluse_text = "Search for the cheapest flight from London to Tokyo departing next Friday."
38
+
39
+ print("πŸš€ DFC CrossCoder Demo")
40
+ print("="*50)
41
+
42
+ for text, label in [(general_text, "General"), (tooluse_text, "Tool-Use")]:
43
+ print(f"\n{label} Text: '{text}'")
44
+
45
+ # Tokenize and get activations (simplified - normally you'd extract from layer 13)
46
+ tokens = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
47
+
48
+ # For demo: create mock activations (normally extracted from models at layer 13)
49
+ # Shape: (batch=1, models=2, hidden_dim=2048)
50
+ mock_activations = torch.randn(1, 2, dfc.activation_dim, device=dfc.W_enc.device)
51
+
52
+ # Core CrossCoder usage (the main 3 lines!)
53
+ features = dfc.encode(mock_activations) # Get sparse feature vector
54
+ reconstruction, _ = dfc(mock_activations) # Full forward pass
55
+
56
+ # Analyze partition breakdowns
57
+ active_features = (features > 0).sum().item()
58
+ a_exclusive = (features[0, :dfc.a_end] > 0).sum().item()
59
+ b_exclusive = (features[0, dfc.a_end:dfc.b_end] > 0).sum().item()
60
+ shared = (features[0, dfc.b_end:] > 0).sum().item()
61
+
62
+ print(f" βœ… Active features: {active_features}/{dfc.dict_size}")
63
+ print(f" πŸ”§ A-exclusive (ToolRL): {a_exclusive}")
64
+ print(f" πŸ“ B-exclusive (Base): {b_exclusive}")
65
+ print(f" 🀝 Shared: {shared}")
66
+
67
+ # Show top features
68
+ top_vals, top_idx = torch.topk(features[0], k=5)
69
+ print(f" πŸ” Top features: {top_idx.tolist()} (values: {top_vals.tolist()})")
70
+
71
+ def extract_real_activations(model_path: str = DEFAULT_MODEL_PATH):
72
+ """Extended demo with real model activations (requires more memory)"""
73
+ print("\n Extended Demo with Real Model Activations")
74
+ print("="*50)
75
+
76
+ device = "cuda" if torch.cuda.is_available() else "cpu"
77
+
78
+ # Load models (this requires significant memory!)
79
+ print("Loading ToolRL and Base models...")
80
+ tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-3B")
81
+ if tokenizer.pad_token is None:
82
+ tokenizer.pad_token = tokenizer.eos_token
83
+
84
+ model_a = AutoModelForCausalLM.from_pretrained("chengq9/ToolRL-Qwen2.5-3B").to(device).eval()
85
+ model_b = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-3B").to(device).eval()
86
+
87
+ # Load CrossCoder
88
+ dfc = DFCCrossCoder.load(model_path, device=device)
89
+
90
+ # Extract real activations from layer 13
91
+ text = "Use the calculator to compute 15% tip on $45.50"
92
+ tokens = tokenizer(text, return_tensors="pt").to(device)
93
+
94
+ with torch.no_grad():
95
+ # hidden_states[0] = embeddings, hidden_states[i] = output of layer i-1
96
+ # so layer 13 activations are at index LAYER + 1
97
+ outputs_a = model_a(**tokens, output_hidden_states=True)
98
+ outputs_b = model_b(**tokens, output_hidden_states=True)
99
+
100
+ hidden_a = outputs_a.hidden_states[LAYER + 1][0, -1] # (hidden_dim,)
101
+ hidden_b = outputs_b.hidden_states[LAYER + 1][0, -1] # (hidden_dim,)
102
+
103
+ # Stack for CrossCoder: (1, 2, hidden_dim)
104
+ activations = torch.stack([hidden_a, hidden_b], dim=0).unsqueeze(0)
105
+
106
+ # Run CrossCoder
107
+ features = dfc.encode(activations)
108
+
109
+ # Analysis
110
+ active_count = (features > 0).sum().item()
111
+ print(f"\n Real Activation Analysis:")
112
+ print(f" Text: '{text}'")
113
+ print(f" Active features: {active_count}/{dfc.dict_size}")
114
+
115
+ # Partition breakdown
116
+ a_active = (features[0, :dfc.a_end] > 0).sum().item()
117
+ b_active = (features[0, dfc.a_end:dfc.b_end] > 0).sum().item()
118
+ s_active = (features[0, dfc.b_end:] > 0).sum().item()
119
+
120
+ print(f" ToolRL-specific: {a_active}")
121
+ print(f" Base-specific: {b_active}")
122
+ print(f" Shared: {s_active}")
123
+
124
+ if __name__ == "__main__":
125
+ model_path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_MODEL_PATH
126
+
127
+ # Run quick demo with mock data
128
+ quick_demo(model_path)
129
+
130
+ # Uncomment to run with real models (requires significant GPU memory)
131
+ # extract_real_activations(model_path)
dfc.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ dfc.py β€” Dedicated Feature CrossCoder (DFC) model.
3
+
4
+ Feature layout in dict_size
5
+ ────────────────────────────
6
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
7
+ β”‚ A-exclusive (n_a) β”‚ B-exclusive (n_b) β”‚ Shared (n_shared) β”‚
8
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
9
+ idx: 0 ─────── a_end ──────── b_end ───────────────────── dict_size
10
+
11
+ Constraints (enforced by gradient masking + _apply_masks every step)
12
+ ──────────────────────────────────────────────────────────────────────
13
+ β€’ Model A cannot encode/decode B-exclusive features
14
+ β€’ Model B cannot encode/decode A-exclusive features
15
+ β€’ Shared features are accessible to both
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ from pathlib import Path
22
+
23
+ import torch
24
+ import torch.nn as nn
25
+ import torch.nn.functional as F
26
+
27
+
28
+ class DFCCrossCoder(nn.Module):
29
+
30
+ def __init__(
31
+ self,
32
+ activation_dim: int,
33
+ dict_size: int,
34
+ k: int,
35
+ model_a_exclusive_pct: float = 0.05,
36
+ model_b_exclusive_pct: float = 0.05,
37
+ ):
38
+ super().__init__()
39
+ self.activation_dim = activation_dim
40
+ self.dict_size = dict_size
41
+ self.k = k
42
+
43
+ self.n_a = int(dict_size * model_a_exclusive_pct)
44
+ self.n_b = int(dict_size * model_b_exclusive_pct)
45
+ self.n_shared = dict_size - self.n_a - self.n_b
46
+ self.a_end = self.n_a
47
+ self.b_end = self.n_a + self.n_b
48
+
49
+ print(
50
+ f"[DFC] dict={dict_size} k={k} | "
51
+ f"A-excl={self.n_a} B-excl={self.n_b} shared={self.n_shared}"
52
+ )
53
+
54
+ # Encoder: W_enc[model, d_in, dict_size]
55
+ self.W_enc = nn.Parameter(
56
+ torch.randn(2, activation_dim, dict_size) / (activation_dim ** 0.5)
57
+ )
58
+ self.b_enc = nn.Parameter(torch.zeros(dict_size))
59
+
60
+ # Decoder: W_dec[dict_size, model, d_in]
61
+ self.W_dec = nn.Parameter(
62
+ torch.randn(dict_size, 2, activation_dim) / (dict_size ** 0.5)
63
+ )
64
+ self.b_dec = nn.Parameter(torch.zeros(2, activation_dim))
65
+
66
+ # ── Partition masks (move with .to(device)) ───────────────────
67
+ # enc_mask[model, dict_size]
68
+ enc_mask = torch.ones(2, dict_size)
69
+ enc_mask[1, : self.a_end] = 0 # B cannot encode A-excl
70
+ enc_mask[0, self.a_end : self.b_end] = 0 # A cannot encode B-excl
71
+ self.register_buffer("enc_mask", enc_mask)
72
+
73
+ # dec_mask[dict_size, model]
74
+ dec_mask = torch.ones(dict_size, 2)
75
+ dec_mask[: self.a_end, 1] = 0 # A-excl: B decoder = 0
76
+ dec_mask[self.a_end : self.b_end, 0] = 0 # B-excl: A decoder = 0
77
+ self.register_buffer("dec_mask", dec_mask)
78
+
79
+ self._apply_masks()
80
+
81
+ # ── Weight enforcement ────────────────────────────────────────────
82
+
83
+ @torch.no_grad()
84
+ def _apply_masks(self):
85
+ """Zero forbidden weights. Call after every optimiser step."""
86
+ for m in range(2):
87
+ self.W_enc.data[m] *= self.enc_mask[m].unsqueeze(0)
88
+ for m in range(2):
89
+ self.W_dec.data[:, m, :] *= self.dec_mask[:, m].unsqueeze(1)
90
+
91
+ # ── Forward ───────────────────────────────────────────────────────
92
+
93
+ def encode(self, x: torch.Tensor) -> torch.Tensor:
94
+ """x: (B, 2, d) β†’ features: (B, dict_size) sparse top-k."""
95
+ W = self.W_enc * self.enc_mask.unsqueeze(1) # (2, d, dict)
96
+ pre = torch.einsum("bmd,mdf->bf", x, W) + self.b_enc
97
+ pre = F.relu(pre)
98
+ topk_vals, topk_idx = torch.topk(pre, self.k, dim=-1)
99
+ features = torch.zeros_like(pre)
100
+ features.scatter_(-1, topk_idx, topk_vals)
101
+ return features
102
+
103
+ def decode(self, features: torch.Tensor) -> torch.Tensor:
104
+ """features: (B, dict_size) β†’ (B, 2, d)."""
105
+ W = self.W_dec * self.dec_mask.unsqueeze(-1) # (dict, 2, d)
106
+ return torch.einsum("bf,fmd->bmd", features, W) + self.b_dec
107
+
108
+ def forward(self, x: torch.Tensor):
109
+ """x: (B, 2, d) β†’ (reconstruction, features)."""
110
+ features = self.encode(x)
111
+ recon = self.decode(features)
112
+ return recon, features
113
+
114
+ def loss(
115
+ self,
116
+ x: torch.Tensor,
117
+ sparsity_coef: float = 1e-3,
118
+ exclusive_sparsity_coef: float = 1e-3 # Lower penalty for exclusive features
119
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
120
+ """MSE + weighted L1 sparsity. Returns (total, mse, l1_shared, l1_exclusive)."""
121
+ recon, features = self.forward(x)
122
+ mse = F.mse_loss(recon, x)
123
+
124
+ # Split features by partition
125
+ # fa = features[:, :self.a_end] # A-exclusive
126
+ # fb = features[:, self.a_end:self.b_end] # B-exclusive
127
+ fs = features[:, self.b_end:] # Shared
128
+
129
+ # A sees: A-exclusive + shared
130
+ fa = torch.cat([features[:, :self.a_end], features[:, self.b_end:]], dim=-1) # A-exclusive + shared
131
+ fb = torch.cat([features[:, self.a_end:self.b_end], features[:, self.b_end:]], dim=-1) # B-exclusive + shared
132
+
133
+ # Separate sparsity penalties
134
+ l1_shared = fs.abs().mean()
135
+ l1_exclusive = (fa.abs().mean() + fb.abs().mean()) / 2
136
+ total = mse + exclusive_sparsity_coef * l1_exclusive + sparsity_coef * l1_shared
137
+
138
+ return total, mse, l1_shared, l1_exclusive
139
+
140
+ # ── Diagnostics ───────────────────────────────────────────────────
141
+
142
+ @torch.no_grad()
143
+ def verify_partition_integrity(self) -> dict[str, float]:
144
+ """Max absolute value in weights that should be zero."""
145
+ if self.n_a == 0 and self.n_b == 0:
146
+ return {"enc_max_violation": 0.0, "dec_max_violation": 0.0}
147
+ enc_viol = (self.W_enc.abs() * (1 - self.enc_mask).unsqueeze(1)).max().item()
148
+ dec_viol_a = self.W_dec[: self.a_end, 1, :].abs().max().item() if self.n_a > 0 else 0.0
149
+ dec_viol_b = self.W_dec[self.a_end : self.b_end, 0, :].abs().max().item() if self.n_b > 0 else 0.0
150
+ return {
151
+ "enc_max_violation": enc_viol,
152
+ "dec_max_violation": max(dec_viol_a, dec_viol_b),
153
+ }
154
+
155
+ @torch.no_grad()
156
+ def feature_stats(self, features: torch.Tensor) -> dict[str, float]:
157
+ """Partition-level activation stats for a batch of features."""
158
+ fa = features[:, : self.a_end]
159
+ fb = features[:, self.a_end : self.b_end]
160
+ fs = features[:, self.b_end :]
161
+ return {
162
+ "l0_total": (features > 0).float().sum(dim=-1).mean().item(),
163
+ "l0_a_excl": (fa > 0).float().sum(dim=-1).mean().item(),
164
+ "l0_b_excl": (fb > 0).float().sum(dim=-1).mean().item(),
165
+ "l0_shared": (fs > 0).float().sum(dim=-1).mean().item(),
166
+ "mean_a_excl": fa.mean().item(),
167
+ "mean_b_excl": fb.mean().item(),
168
+ "mean_shared": fs.mean().item(),
169
+ }
170
+
171
+ # ── Save / Load ───────────────────────────────────────────────────
172
+
173
+ def save(self, path: str) -> None:
174
+ """Save model with disk space checking and error handling."""
175
+ import shutil
176
+ import tempfile
177
+
178
+ # Check available disk space
179
+ free_space = shutil.disk_usage(Path(path).parent or ".").free
180
+ if free_space < 100_000_000: # Less than 100MB
181
+ raise RuntimeError(f"Insufficient disk space: {free_space / 1e9:.2f}GB available. Need at least 0.1GB.")
182
+
183
+ Path(path).mkdir(parents=True, exist_ok=True)
184
+
185
+ # Save to temporary file first, then move to avoid corruption
186
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pt') as tmp_file:
187
+ try:
188
+ torch.save(self.state_dict(), tmp_file.name)
189
+ shutil.move(tmp_file.name, f"{path}/model.pt")
190
+ except Exception as e:
191
+ # Clean up temp file on error
192
+ if Path(tmp_file.name).exists():
193
+ Path(tmp_file.name).unlink()
194
+ raise RuntimeError(f"Failed to save model: {e}")
195
+
196
+ # Save config
197
+ config_data = dict(
198
+ activation_dim=self.activation_dim,
199
+ dict_size=self.dict_size,
200
+ k=self.k,
201
+ n_a=self.n_a,
202
+ n_b=self.n_b,
203
+ )
204
+ with open(f"{path}/config.json", "w") as f:
205
+ json.dump(config_data, f, indent=2)
206
+
207
+ print(f"[DFC] Saved β†’ {path}")
208
+
209
+ @classmethod
210
+ def load(cls, path: str, device: str = "cpu") -> "DFCCrossCoder":
211
+ cfg = json.load(open(f"{path}/config.json"))
212
+ model = cls(
213
+ activation_dim=cfg["activation_dim"],
214
+ dict_size=cfg["dict_size"],
215
+ k=cfg["k"],
216
+ model_a_exclusive_pct=cfg["n_a"] / cfg["dict_size"],
217
+ model_b_exclusive_pct=cfg["n_b"] / cfg["dict_size"],
218
+ )
219
+ model.load_state_dict(
220
+ torch.load(f"{path}/model.pt", map_location=device, weights_only=True)
221
+ )
222
+ return model.to(device)
hparams.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architecture": "DFC",
3
+ "model_a": "chengq9/ToolRL-Qwen2.5-3B",
4
+ "model_b": "Qwen/Qwen2.5-3B",
5
+ "layer": 13,
6
+ "activation_dim": 2048,
7
+ "dict_size": 8192,
8
+ "k": 90,
9
+ "model_a_exclusive_pct": 0.03,
10
+ "model_b_exclusive_pct": 0.03,
11
+ "steps": 9000,
12
+ "lr": 1e-4,
13
+ "train_batch": 1024,
14
+ "sparsity_coef": 1e-3,
15
+ "exclusive_sparsity_coef": 0,
16
+ "loss": "MSE + L1 on shared features only (coef: 1e-3); exclusive features unpenalized",
17
+ "wandb_project": "dfc-crosscoder-sweep",
18
+ "trained_at": "2026-03-30T03:53:46+00:00"
19
+ }
model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d64ba3393b94289f071de0bd830b16971303fb52b85c3b75f04f3a81e7cc6e60
3
+ size 268618757
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ torch>=2.0.0
2
+ transformers>=4.30.0
3
+ datasets>=2.0.0
4
+ wandb>=0.15.0
5
+ tqdm>=4.64.0
6
+ matplotlib>=3.5.0
7
+ huggingface_hub>=0.16.0
8
+ numpy>=1.21.0