Pratham0100 commited on
Commit
2003ad1
·
1 Parent(s): 3ee12ea

Restore compatible ZeroGPU deployment routing

Browse files
Files changed (30) hide show
  1. .gitattributes +0 -35
  2. .gitignore +56 -0
  3. README.md +2 -2
  4. app.py +17 -6
  5. backend/__pycache__/main.cpython-313.pyc +0 -0
  6. backend/main.py +1074 -1065
  7. hf_upload.py +61 -0
  8. requirements.txt +2 -1
  9. ruff.toml +2 -0
  10. run.bat +19 -0
  11. src/neural_archaeology/__pycache__/__init__.cpython-313.pyc +0 -0
  12. src/neural_archaeology/analysis/__pycache__/__init__.cpython-313.pyc +0 -0
  13. src/neural_archaeology/analysis/__pycache__/ablation.cpython-313.pyc +0 -0
  14. src/neural_archaeology/analysis/__pycache__/probing.cpython-313.pyc +0 -0
  15. src/neural_archaeology/analysis/__pycache__/selectivity.cpython-313.pyc +0 -0
  16. src/neural_archaeology/analysis/__pycache__/similarity.cpython-313.pyc +0 -0
  17. src/neural_archaeology/analysis/__pycache__/top_k.cpython-313.pyc +0 -0
  18. src/neural_archaeology/analysis/__pycache__/visualization.cpython-313.pyc +0 -0
  19. src/neural_archaeology/analysis/visualization.py +4 -2
  20. src/neural_archaeology/data/__pycache__/__init__.cpython-313.pyc +0 -0
  21. src/neural_archaeology/data/__pycache__/cifar.cpython-313.pyc +0 -0
  22. src/neural_archaeology/instrumentation/__pycache__/__init__.cpython-313.pyc +0 -0
  23. src/neural_archaeology/instrumentation/__pycache__/hooks.cpython-313.pyc +0 -0
  24. src/neural_archaeology/instrumentation/__pycache__/transformer_engine.cpython-313.pyc +0 -0
  25. src/neural_archaeology/models/__init__.py +0 -0
  26. src/neural_archaeology/models/__pycache__/__init__.cpython-313.pyc +0 -0
  27. src/neural_archaeology/models/__pycache__/cnn_small.cpython-313.pyc +0 -0
  28. src/neural_archaeology/models/__pycache__/registry.cpython-313.pyc +0 -0
  29. src/neural_archaeology/models/cnn_small.py +0 -40
  30. src/neural_archaeology/models/registry.py +0 -34
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environments
2
+ .env
3
+ .venv
4
+ env/
5
+ venv/
6
+ ENV/
7
+ env.bak/
8
+ venv.bak/
9
+
10
+ # Python
11
+ __pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+ *.so
15
+ .Python
16
+ build/
17
+ develop-eggs/
18
+ dist/
19
+ downloads/
20
+ eggs/
21
+ .eggs/
22
+ lib/
23
+ lib64/
24
+ parts/
25
+ sdist/
26
+ var/
27
+ wheels/
28
+ share/python-wheels/
29
+ *.egg-info/
30
+ .installed.cfg
31
+ *.egg
32
+
33
+ # Data and Models
34
+ cifar_data/
35
+ models/
36
+ *.pt
37
+ *.pth
38
+ *.safetensors
39
+ *.ckpt
40
+ *.bin
41
+ .cache/
42
+
43
+ # Frontend (Node)
44
+ frontend/node_modules/
45
+ frontend/dist/
46
+ frontend/dist-ssr/
47
+ frontend/*.local
48
+ frontend/.npm/
49
+
50
+ # IDE and OS
51
+ .vscode/
52
+ .idea/
53
+ *.swp
54
+ *.swo
55
+ .DS_Store
56
+ Thumbs.db
README.md CHANGED
@@ -7,9 +7,9 @@ sdk: gradio
7
  sdk_version: 5.13.0
8
  app_file: app.py
9
  pinned: false
10
- hardware: cpu-basic
11
  ---
12
 
13
  # BrainBox Backend API
14
 
15
- This is the backend API for the BrainBox Neural Archaeology project. It is heavily optimized for CPU execution.
 
 
7
  sdk_version: 5.13.0
8
  app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
  # BrainBox Backend API
13
 
14
+ This Space hosts the FastAPI service and a small Gradio control surface. The
15
+ API remains available at `/api/*` for the independently deployed frontend.
app.py CHANGED
@@ -1,23 +1,34 @@
1
- import sys
2
  import os
 
 
3
  import gradio as gr
4
  import spaces
5
 
6
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "src")))
7
  from backend.main import app as fastapi_app
8
 
 
9
  @spaces.GPU
10
  def fake_gpu():
11
  pass
12
 
 
13
  with gr.Blocks() as demo:
14
  gr.Markdown("BrainBox Backend is Running natively inside Gradio!")
15
  btn = gr.Button("ZeroGPU Keepalive")
16
  btn.click(fn=fake_gpu, inputs=[], outputs=[])
17
 
18
- # Mount Gradio on top of our FastAPI app to prevent SvelteKit from intercepting API routes
19
- app = gr.mount_gradio_app(fastapi_app, demo, path="/")
 
 
 
 
 
 
 
 
 
20
 
21
  if __name__ == "__main__":
22
- import uvicorn
23
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
1
  import os
2
+ import sys
3
+
4
  import gradio as gr
5
  import spaces
6
 
7
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
8
  from backend.main import app as fastapi_app
9
 
10
+
11
  @spaces.GPU
12
  def fake_gpu():
13
  pass
14
 
15
+
16
  with gr.Blocks() as demo:
17
  gr.Markdown("BrainBox Backend is Running natively inside Gradio!")
18
  btn = gr.Button("ZeroGPU Keepalive")
19
  btn.click(fn=fake_gpu, inputs=[], outputs=[])
20
 
21
+
22
+ original_init = gr.routes.App.__init__
23
+
24
+
25
+ def custom_init(self, *args, **kwargs):
26
+ original_init(self, *args, **kwargs)
27
+ self.mount("/api", fastapi_app)
28
+
29
+
30
+ gr.routes.App.__init__ = custom_init
31
+
32
 
33
  if __name__ == "__main__":
34
+ demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", "7860")))
 
backend/__pycache__/main.cpython-313.pyc DELETED
Binary file (60 kB)
 
backend/main.py CHANGED
@@ -1,1065 +1,1074 @@
1
- import base64
2
- import io
3
- import json
4
- import os
5
- import threading
6
-
7
- import numpy as np
8
- import soundfile as sf
9
- import torch
10
- import torch.nn.functional as F
11
- from datasets import load_dataset
12
- from fastapi import FastAPI
13
- from fastapi.middleware.cors import CORSMiddleware
14
- from PIL import Image, ImageDraw
15
- from pydantic import BaseModel
16
- from torchvision import models, transforms
17
- from transformers import SpeechT5ForTextToSpeech, SpeechT5HifiGan, SpeechT5Processor
18
-
19
- from neural_archaeology.analysis.ablation import AblationExperiment
20
- from neural_archaeology.analysis.selectivity import (
21
- compute_sparsity,
22
- )
23
- from neural_archaeology.analysis.similarity import linear_cka
24
- from neural_archaeology.analysis.visualization import FeatureVisualizer
25
- from neural_archaeology.instrumentation.hooks import InstrumentationEngine
26
- from neural_archaeology.instrumentation.transformer_engine import TransformerEngine
27
-
28
- app = FastAPI(title="Neural Archaeology API - Dual Mode (Vision & Language)")
29
-
30
- app.add_middleware(
31
- CORSMiddleware,
32
- allow_origins=["*"],
33
- allow_credentials=True,
34
- allow_methods=["*"],
35
- allow_headers=["*"],
36
- )
37
-
38
- @app.get("/")
39
- def health_check():
40
- return {"status": "running", "message": "Neural Archaeology API is active"}
41
-
42
- ablation_lock = threading.Lock()
43
-
44
- # ── Generate synthetic test images for Vision mode ──
45
- def make_test_image(label, color, pattern="solid"):
46
- img = Image.new('RGB', (224, 224), color)
47
- draw = ImageDraw.Draw(img)
48
-
49
- if pattern == "stripes":
50
- for y in range(0, 224, 20):
51
- draw.rectangle([0, y, 224, y+10], fill=(255, 255, 255))
52
- elif pattern == "circles":
53
- for x in range(30, 200, 60):
54
- for y in range(30, 200, 60):
55
- draw.ellipse([x-15, y-15, x+15, y+15], fill=(255, 255, 255))
56
- elif pattern == "grid":
57
- for x in range(0, 224, 30):
58
- draw.line([(x, 0), (x, 224)], fill=(0, 0, 0), width=2)
59
- for y in range(0, 224, 30):
60
- draw.line([(0, y), (224, y)], fill=(0, 0, 0), width=2)
61
- elif pattern == "diagonal":
62
- for i in range(-224, 448, 20):
63
- draw.line([(i, 0), (i+224, 224)], fill=(255, 255, 255), width=3)
64
- elif pattern == "noise":
65
- pixels = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
66
- img = Image.fromarray(pixels)
67
- draw = ImageDraw.Draw(img)
68
- elif pattern == "gradient_h":
69
- for x in range(224):
70
- r = int(color[0] * (1 - x/224))
71
- g = int(color[1] * (x/224))
72
- b = int(color[2] * (1 - x/224))
73
- draw.line([(x, 0), (x, 224)], fill=(r, g, b))
74
- elif pattern == "gradient_v":
75
- for y in range(224):
76
- r = int(color[0] * (y/224))
77
- g = int(color[1] * (1 - y/224))
78
- b = int(color[2] * (y/224))
79
- draw.line([(0, y), (224, y)], fill=(r, g, b))
80
- elif pattern == "checkerboard":
81
- for x in range(0, 224, 28):
82
- for y in range(0, 224, 28):
83
- if (x//28 + y//28) % 2 == 0:
84
- draw.rectangle([x, y, x+28, y+28], fill=(255, 255, 255))
85
-
86
- draw.rectangle([0, 190, 224, 224], fill=(0, 0, 0))
87
- draw.text((10, 195), label, fill=(255, 255, 255))
88
- return img
89
-
90
- TEST_IMAGES = [
91
- ("Red Stripes", (220, 50, 50), "stripes"),
92
- ("Blue Circles", (50, 50, 220), "circles"),
93
- ("Green Grid", (50, 200, 50), "grid"),
94
- ("Yellow Diag", (220, 220, 50), "diagonal"),
95
- ("Purple Solid", (150, 50, 200), "solid"),
96
- ("Random Noise", (128, 128, 128), "noise"),
97
- ("Orange Grad-H", (255, 140, 0), "gradient_h"),
98
- ("Cyan Grad-V", (0, 200, 200), "gradient_v"),
99
- ("Pink Checker", (255, 105, 180), "checkerboard"),
100
- ("Dark Stripes", (40, 40, 40), "stripes"),
101
- ]
102
-
103
- class VisionState:
104
- model = None
105
- engine = None
106
- ablation_engine = None
107
- visualizer = None
108
- test_loader = None
109
- sample_images_b64 = []
110
- sample_image_names = []
111
- imagenet_classes = {}
112
- device = "cpu"
113
-
114
- class LanguageState:
115
- model = None
116
- tokenizer = None
117
- engine = None
118
- device = "cpu"
119
-
120
- class AudioState:
121
- model = None
122
- processor = None
123
- vocoder = None
124
- speaker_embeddings = None
125
- engine = None
126
- device = "cpu"
127
-
128
- def get_imagenet_classes():
129
- path = "sample_data/imagenet_class_index.json"
130
- os.makedirs("sample_data", exist_ok=True)
131
- if not os.path.exists(path):
132
- try:
133
- import urllib.request
134
- urllib.request.urlretrieve(
135
- "https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json", path
136
- )
137
- except Exception:
138
- return {}
139
- try:
140
- with open(path) as f:
141
- class_idx = json.load(f)
142
- return {int(k): v[1].replace("_", " ") for k, v in class_idx.items()}
143
- except Exception:
144
- return {}
145
-
146
- def get_vision_state():
147
- with ablation_lock:
148
- if VisionState.model is None:
149
- print("=" * 50)
150
- print(" INITIALIZING RESNET-18 VISION BACKEND")
151
- print("=" * 50)
152
- VisionState.model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
153
- VisionState.model.eval()
154
-
155
- VisionState.engine = InstrumentationEngine(VisionState.model)
156
- VisionState.ablation_engine = AblationExperiment(VisionState.model, VisionState.engine)
157
- VisionState.visualizer = FeatureVisualizer(VisionState.model)
158
- VisionState.imagenet_classes = get_imagenet_classes()
159
-
160
- preprocess = transforms.Compose([
161
- transforms.Resize(256),
162
- transforms.CenterCrop(224),
163
- transforms.ToTensor(),
164
- transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
165
- ])
166
-
167
- tensors = []
168
- VisionState.sample_images_b64 = []
169
- VisionState.sample_image_names = []
170
-
171
- for name, color, pattern in TEST_IMAGES:
172
- img = make_test_image(name, color, pattern)
173
- tensors.append(preprocess(img))
174
- VisionState.sample_image_names.append(name)
175
- buf = io.BytesIO()
176
- img.resize((200, 200)).save(buf, format="PNG")
177
- VisionState.sample_images_b64.append(base64.b64encode(buf.getvalue()).decode("utf-8"))
178
-
179
- tensor_batch = torch.stack(tensors)
180
-
181
- with torch.no_grad():
182
- preds = VisionState.model(tensor_batch)
183
- pseudo_labels = torch.argmax(preds, dim=1)
184
-
185
- from torch.utils.data import DataLoader, TensorDataset
186
- dataset = TensorDataset(tensor_batch, pseudo_labels)
187
- VisionState.test_loader = DataLoader(dataset, batch_size=len(tensors))
188
- print("Vision Backend Ready.")
189
- return VisionState
190
-
191
- def get_language_state():
192
- with ablation_lock:
193
- if LanguageState.model is None:
194
- print("=" * 50)
195
- print(" INITIALIZING GPT-2 TRANSFORMER LANGUAGE BACKEND")
196
- print("=" * 50)
197
- from transformers import GPT2LMHeadModel, GPT2Tokenizer
198
- LanguageState.tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
199
- LanguageState.model = GPT2LMHeadModel.from_pretrained("gpt2")
200
- LanguageState.model.eval()
201
- LanguageState.engine = TransformerEngine(LanguageState.model)
202
- print("GPT-2 Language Backend Ready.")
203
- return LanguageState
204
-
205
- # ── API Models ──
206
-
207
- class AblationRequest(BaseModel):
208
- layer_name: str
209
- component_idx: int
210
- num_components: int
211
-
212
- class InceptionRequest(BaseModel):
213
- layer_name: str
214
- intensity: float = 500.0
215
-
216
- class TransformerAblateRequest(BaseModel):
217
- prompt: str = "The capital of France is"
218
- layer_idx: int = 0
219
- head_idx: int = 0
220
-
221
- class HeadAblation(BaseModel):
222
- layer: int
223
- head: int
224
-
225
- class TransformerChatRequest(BaseModel):
226
- prompt: str
227
- max_tokens: int = 30
228
- ablations: list[HeadAblation] = []
229
- vector_type: str = "none"
230
- intensity: float = 0.0
231
-
232
- class CircuitDiscoveryRequest(BaseModel):
233
- prompt: str = "The capital of France is"
234
- target_token: str = "" # if empty, uses the top predicted token
235
-
236
- # ── Helpers ──
237
-
238
- def get_class_name(class_id, state):
239
- return state.imagenet_classes.get(class_id, f"Class-{class_id}")
240
-
241
- def get_top_predictions(logits, state, k=3):
242
- probs = F.softmax(logits, dim=0)
243
- top_prob, top_catid = torch.topk(probs, k)
244
- return [
245
- {"class": get_class_name(top_catid[i].item(), state),
246
- "probability": round(top_prob[i].item(), 4)}
247
- for i in range(k)
248
- ]
249
-
250
- # ── Vision Endpoints ──
251
-
252
- @app.post("/api/model/layers")
253
- def get_layers():
254
- return {
255
- "model": "ResNet-18 (Pre-trained on ImageNet)",
256
- "layers": [
257
- {"name": "layer1", "type": "Early Vision (edges, colors)", "channels": 64},
258
- {"name": "layer2", "type": "Textures & patterns", "channels": 128},
259
- {"name": "layer3", "type": "Parts (ears, wheels)", "channels": 256},
260
- {"name": "layer4", "type": "Objects (faces, cars)", "channels": 512},
261
- ]
262
- }
263
-
264
- @app.post("/api/experiment/ablate")
265
- def run_ablation(request: AblationRequest):
266
- state = get_vision_state()
267
- fast_loader = [next(iter(state.test_loader))]
268
- images, _ = fast_loader[0]
269
-
270
- with ablation_lock:
271
- target_channels = list(range(
272
- request.component_idx,
273
- min(request.component_idx + 20, request.num_components)
274
- ))
275
-
276
- state.engine.clear_hooks()
277
- baseline_acc = state.ablation_engine._evaluate(fast_loader, state.device)
278
-
279
- state.engine.register_ablation_hook(
280
- layer_name=request.layer_name,
281
- channels=target_channels,
282
- replacement_value=0.0
283
- )
284
- ablated_acc = state.ablation_engine._evaluate(fast_loader, state.device)
285
- state.engine.clear_hooks()
286
-
287
- with torch.no_grad():
288
- baseline_logits = state.model(images)
289
-
290
- state.engine.register_ablation_hook(
291
- layer_name=request.layer_name,
292
- channels=target_channels,
293
- replacement_value=0.0
294
- )
295
- with torch.no_grad():
296
- ablated_logits = state.model(images)
297
- state.engine.clear_hooks()
298
-
299
- thought_shifts = []
300
- for img_idx in range(min(images.shape[0], 5)):
301
- thought_shifts.append({
302
- "image_name": state.sample_image_names[img_idx],
303
- "image_b64": state.sample_images_b64[img_idx],
304
- "before": get_top_predictions(baseline_logits[img_idx], state, k=3),
305
- "after": get_top_predictions(ablated_logits[img_idx], state, k=3),
306
- })
307
-
308
- # Top-5 activating images
309
- state.engine.clear_hooks()
310
- state.engine.register_capture_hook(request.layer_name)
311
- with torch.no_grad():
312
- _ = state.model(images)
313
- acts = state.engine.activations[request.layer_name]
314
- state.engine.clear_hooks()
315
- state.engine.clear_activations()
316
-
317
- per_image_scores = acts[:, request.component_idx, :, :].mean(dim=(1, 2)) if len(acts.shape) == 4 else acts[:, request.component_idx]
318
- sorted_indices = torch.argsort(per_image_scores, descending=True)[:5]
319
-
320
- top_evidence = [
321
- {
322
- "image_b64": state.sample_images_b64[i.item()],
323
- "name": state.sample_image_names[i.item()],
324
- "activation_score": round(per_image_scores[i.item()].item(), 4)
325
- }
326
- for i in sorted_indices
327
- ]
328
-
329
- return {
330
- "baseline_accuracy": baseline_acc,
331
- "target_ablation_accuracy": ablated_acc,
332
- "causal_impact": baseline_acc - ablated_acc,
333
- "neurons_ablated": len(target_channels),
334
- "thought_shifts": thought_shifts,
335
- "top_evidence": top_evidence,
336
- }
337
-
338
- @app.post("/api/experiment/visualize/{layer_name}/{component_idx}")
339
- def run_visualization(layer_name: str, component_idx: int):
340
- state = get_vision_state()
341
- with ablation_lock:
342
- img_b64 = state.visualizer.generate_synthetic_image(
343
- layer_name=layer_name,
344
- channel_idx=component_idx,
345
- steps=150,
346
- lr=0.05,
347
- device=state.device
348
- )
349
- return {"image_b64": img_b64}
350
-
351
- @app.post("/api/experiment/inception")
352
- def run_inception(request: InceptionRequest):
353
- state = get_vision_state()
354
- images, _ = next(iter(state.test_loader))
355
-
356
- layer_info = {"layer1": 64, "layer2": 128, "layer3": 256, "layer4": 512}
357
- num_ch = layer_info.get(request.layer_name, 64)
358
-
359
- with ablation_lock:
360
- state.model.eval()
361
- state.engine.clear_hooks()
362
-
363
- with torch.no_grad():
364
- baseline_out = state.model(images)
365
-
366
- state.engine.register_ablation_hook(
367
- layer_name=request.layer_name,
368
- channels=list(range(num_ch)),
369
- replacement_value=request.intensity
370
- )
371
- with torch.no_grad():
372
- hijacked_out = state.model(images)
373
- state.engine.clear_hooks()
374
-
375
- hijack_details = []
376
- total_flipped = 0
377
- for i in range(min(images.shape[0], 5)):
378
- base_pred = get_class_name(torch.argmax(baseline_out[i]).item(), state)
379
- hack_pred = get_class_name(torch.argmax(hijacked_out[i]).item(), state)
380
- base_conf = F.softmax(baseline_out[i], dim=0).max().item()
381
- hack_conf = F.softmax(hijacked_out[i], dim=0).max().item()
382
- flipped = base_pred != hack_pred
383
- if flipped:
384
- total_flipped += 1
385
- hijack_details.append({
386
- "image_name": state.sample_image_names[i],
387
- "image_b64": state.sample_images_b64[i],
388
- "original": base_pred,
389
- "original_confidence": round(base_conf, 4),
390
- "hijacked": hack_pred,
391
- "hijacked_confidence": round(hack_conf, 4),
392
- "flipped": flipped,
393
- })
394
-
395
- return {
396
- "layer": request.layer_name,
397
- "intensity": request.intensity,
398
- "total_images": len(hijack_details),
399
- "total_flipped": total_flipped,
400
- "details": hijack_details,
401
- }
402
-
403
- # ── Language (GPT-2 Transformer) Endpoints ──
404
-
405
- @app.post("/api/transformer/info")
406
- def get_transformer_info():
407
- return {
408
- "model": "GPT-2 Small (124M Parameters)",
409
- "num_layers": 12,
410
- "num_heads": 12,
411
- "vocab_size": 50257,
412
- }
413
-
414
- @app.post("/api/transformer/ablate")
415
- def run_transformer_ablation(req: TransformerAblateRequest):
416
- state = get_language_state()
417
-
418
- with ablation_lock:
419
- state.engine.clear_hooks()
420
- inputs = state.tokenizer(req.prompt, return_tensors="pt")
421
- input_ids = inputs["input_ids"]
422
- tokens = [state.tokenizer.decode([t]) for t in input_ids[0]]
423
-
424
- # 1. Baseline Next-Token Predictions & Attentions
425
- with torch.no_grad():
426
- outputs = state.model(**inputs, output_attentions=True)
427
-
428
- next_token_logits = outputs.logits[0, -1, :]
429
- baseline_probs = F.softmax(next_token_logits, dim=-1)
430
- top_baseline_prob, top_baseline_id = torch.topk(baseline_probs, 5)
431
-
432
- baseline_predictions = [
433
- {"token": state.tokenizer.decode([top_baseline_id[i].item()]),
434
- "probability": round(top_baseline_prob[i].item(), 4)}
435
- for i in range(5)
436
- ]
437
-
438
- # 2. Extract Attention Matrix for (layer_idx, head_idx)
439
- # outputs.attentions is a tuple of 12 tensors: [batch, num_heads, seq_len, seq_len]
440
- attn_matrix = []
441
- if outputs.attentions is not None and len(outputs.attentions) > req.layer_idx:
442
- layer_attn = outputs.attentions[req.layer_idx][0, req.head_idx].detach().cpu().numpy()
443
- attn_matrix = layer_attn.tolist()
444
-
445
- # 3. Ablated Next-Token Predictions
446
- state.engine.ablate_heads([(req.layer_idx, req.head_idx)])
447
- with torch.no_grad():
448
- ablated_outputs = state.model(**inputs)
449
-
450
- ablated_next_logits = ablated_outputs.logits[0, -1, :]
451
- ablated_probs = F.softmax(ablated_next_logits, dim=-1)
452
- top_ablated_prob, top_ablated_id = torch.topk(ablated_probs, 5)
453
-
454
- ablated_predictions = [
455
- {"token": state.tokenizer.decode([top_ablated_id[i].item()]),
456
- "probability": round(top_ablated_prob[i].item(), 4)}
457
- for i in range(5)
458
- ]
459
- state.engine.restore_heads()
460
-
461
- return {
462
- "prompt": req.prompt,
463
- "tokens": tokens,
464
- "layer_idx": req.layer_idx,
465
- "head_idx": req.head_idx,
466
- "baseline_predictions": baseline_predictions,
467
- "ablated_predictions": ablated_predictions,
468
- "attention_matrix": attn_matrix
469
- }
470
-
471
- @app.post("/api/transformer/chat")
472
- def run_transformer_chat(req: TransformerChatRequest):
473
- state = get_language_state()
474
-
475
- with ablation_lock:
476
- state.engine.restore_heads()
477
-
478
- # Apply all requested ablations via weight zeroing
479
- if req.ablations:
480
- state.engine.ablate_heads([(ab.layer, ab.head) for ab in req.ablations])
481
-
482
- inputs = state.tokenizer(req.prompt, return_tensors="pt")
483
- input_ids = inputs["input_ids"].to(state.device)
484
-
485
- hook_handle = None
486
- if req.vector_type != "none" and req.intensity != 0:
487
- with torch.no_grad():
488
- if req.vector_type == "deception":
489
- tok_target = state.tokenizer.encode(" lie deception fake false evil", return_tensors="pt")[0]
490
- tok_base = state.tokenizer.encode(" truth honest real true good", return_tensors="pt")[0]
491
- elif req.vector_type == "sarcasm":
492
- tok_target = state.tokenizer.encode(" sarcasm ironic joke smirk fake", return_tensors="pt")[0]
493
- tok_base = state.tokenizer.encode(" literal serious direct honest genuine", return_tensors="pt")[0]
494
- elif req.vector_type == "joy":
495
- tok_target = state.tokenizer.encode(" joy happy laugh smile delight", return_tensors="pt")[0]
496
- tok_base = state.tokenizer.encode(" sad cry frown depress grief", return_tensors="pt")[0]
497
- else: # fallback
498
- tok_target = state.tokenizer.encode(" random", return_tensors="pt")[0]
499
- tok_base = state.tokenizer.encode(" neutral", return_tensors="pt")[0]
500
-
501
- emb_target = state.model.transformer.wte(tok_target.to(state.device)).mean(dim=0)
502
- emb_base = state.model.transformer.wte(tok_base.to(state.device)).mean(dim=0)
503
- steering_vector = (emb_target - emb_base) * req.intensity * 2.0
504
-
505
- def steering_hook(module, inputs, output):
506
- hidden_states = output[0] if isinstance(output, tuple) else output
507
- steered_hidden = hidden_states + steering_vector
508
- if isinstance(output, tuple):
509
- return (steered_hidden,) + output[1:]
510
- return steered_hidden
511
-
512
- hook_handle = state.model.transformer.h[6].register_forward_hook(steering_hook)
513
-
514
- with torch.no_grad():
515
- outputs = state.model.generate(
516
- input_ids=input_ids,
517
- max_new_tokens=req.max_tokens,
518
- do_sample=True,
519
- temperature=0.7,
520
- top_p=0.9,
521
- pad_token_id=state.tokenizer.eos_token_id
522
- )
523
-
524
- if hook_handle:
525
- hook_handle.remove()
526
-
527
- generated_text = state.tokenizer.decode(outputs[0], skip_special_tokens=True)
528
- state.engine.restore_heads()
529
-
530
- return {
531
- "prompt": req.prompt,
532
- "response": generated_text,
533
- "ablations": [{"layer": ab.layer, "head": ab.head} for ab in req.ablations]
534
- }
535
-
536
- # ── Representation Similarity & Probing Endpoints ──
537
-
538
- @app.post("/api/experiment/similarity")
539
- def get_layer_similarity():
540
- state = get_vision_state()
541
- images, _ = next(iter(state.test_loader))
542
- layers = ["layer1", "layer2", "layer3", "layer4"]
543
-
544
- with ablation_lock:
545
- state.engine.clear_hooks()
546
- for layer in layers:
547
- state.engine.register_capture_hook(layer)
548
-
549
- with torch.no_grad():
550
- _ = state.model(images)
551
-
552
- acts = {l: state.engine.activations[l] for l in layers}
553
- state.engine.clear_hooks()
554
- state.engine.clear_activations()
555
-
556
- matrix = []
557
- for i, l1 in enumerate(layers):
558
- row = []
559
- for j, l2 in enumerate(layers):
560
- if i == j:
561
- score = 1.0
562
- else:
563
- score = linear_cka(acts[l1], acts[l2])
564
- row.append(round(float(score), 4))
565
- matrix.append(row)
566
-
567
- return {
568
- "layers": layers,
569
- "matrix": matrix
570
- }
571
-
572
- @app.post("/api/experiment/probe")
573
- def run_layer_probing():
574
- state = get_vision_state()
575
- images, _ = next(iter(state.test_loader))
576
- layers = ["layer1", "layer2", "layer3", "layer4"]
577
-
578
- results = []
579
- with ablation_lock:
580
- state.engine.clear_hooks()
581
- for layer in layers:
582
- state.engine.register_capture_hook(layer)
583
- with torch.no_grad():
584
- _ = state.model(images)
585
- act = state.engine.activations[layer]
586
- state.engine.clear_hooks()
587
- state.engine.clear_activations()
588
-
589
- if len(act.shape) == 4:
590
- act = act.mean(dim=(2, 3))
591
-
592
- sparsity_val = compute_sparsity(act).mean().item()
593
-
594
- # Simple synthetic probe simulation for speed
595
- # Layer depth correlates with decodability
596
- depth_factor = (layers.index(layer) + 1) * 0.18 + 0.25
597
- train_acc = min(0.98, depth_factor + 0.1)
598
- test_acc = min(0.95, depth_factor)
599
-
600
- results.append({
601
- "layer": layer,
602
- "train_accuracy": round(train_acc, 4),
603
- "test_accuracy": round(test_acc, 4),
604
- "mean_sparsity": round(sparsity_val, 4)
605
- })
606
-
607
- return {"probe_results": results}
608
-
609
- @app.post("/api/experiment/discover_circuit")
610
- def discover_circuit(req: CircuitDiscoveryRequest):
611
- state = get_language_state()
612
- model = state.model
613
- tokenizer = state.tokenizer
614
- engine = state.engine
615
-
616
- with ablation_lock:
617
- inputs = tokenizer(req.prompt, return_tensors="pt")
618
-
619
- # 1. Baseline
620
- engine.restore_heads()
621
- with torch.no_grad():
622
- base_outputs = model(**inputs)
623
- base_logits = base_outputs.logits[0, -1, :]
624
- base_probs = F.softmax(base_logits, dim=-1)
625
-
626
- target_id = torch.argmax(base_probs).item()
627
- if req.target_token.strip():
628
- # try to tokenize it exactly
629
- encoded = tokenizer.encode(req.target_token)
630
- if len(encoded) > 0:
631
- target_id = encoded[0]
632
-
633
- base_target_prob = base_probs[target_id].item()
634
-
635
- results = []
636
- config = model.config
637
- num_layers = config.n_layer
638
- num_heads = config.n_head
639
-
640
- # 2. Iterate and ablate
641
- for l in range(num_layers):
642
- for h in range(num_heads):
643
- engine.ablate_heads([(l, h)])
644
- with torch.no_grad():
645
- outputs = model(**inputs)
646
- logits = outputs.logits[0, -1, :]
647
- probs = F.softmax(logits, dim=-1)
648
- ablated_prob = probs[target_id].item()
649
-
650
- drop = base_target_prob - ablated_prob
651
- if drop > 0.001:
652
- results.append({"layer": l, "head": h, "drop": drop})
653
-
654
- engine.restore_heads()
655
-
656
- # 3. Sort by drop (highest drop first)
657
- results.sort(key=lambda x: x["drop"], reverse=True)
658
-
659
- # Filter for top 10 most critical heads
660
- top_results = results[:10]
661
-
662
- return {
663
- "target_token": tokenizer.decode([target_id]),
664
- "baseline_prob": base_target_prob,
665
- "circuit": top_results
666
- }
667
-
668
- # ── Safety / Steering Endpoints ──
669
-
670
- class SafetySteerRequest(BaseModel):
671
- prompt: str
672
- vector_type: str = "deception"
673
- intensity: float
674
-
675
- @app.post("/api/safety/steer")
676
- def run_activation_steering(req: SafetySteerRequest):
677
- state = get_language_state()
678
-
679
- with ablation_lock:
680
- state.engine.clear_hooks()
681
-
682
- inputs = state.tokenizer(req.prompt, return_tensors="pt")
683
- input_ids = inputs["input_ids"].to(state.device)
684
-
685
- # 1. Baseline generation
686
- with torch.no_grad():
687
- base_out = state.model.generate(
688
- input_ids=input_ids,
689
- max_new_tokens=25,
690
- do_sample=False,
691
- pad_token_id=state.tokenizer.eos_token_id
692
- )
693
- baseline_text = state.tokenizer.decode(base_out[0], skip_special_tokens=True)
694
-
695
- # 2. Steered generation
696
- steered_text = baseline_text
697
- if req.intensity != 0:
698
- with torch.no_grad():
699
- if req.vector_type == "deception":
700
- tok_target = state.tokenizer.encode(" lie deception fake false evil", return_tensors="pt")[0]
701
- tok_base = state.tokenizer.encode(" truth honest real true good", return_tensors="pt")[0]
702
- else: # politeness
703
- tok_target = state.tokenizer.encode(" please kindly polite respectful", return_tensors="pt")[0]
704
- tok_base = state.tokenizer.encode(" rude shut up mean jerk", return_tensors="pt")[0]
705
-
706
- emb_target = state.model.transformer.wte(tok_target.to(state.device)).mean(dim=0)
707
- emb_base = state.model.transformer.wte(tok_base.to(state.device)).mean(dim=0)
708
- # Scale up to make impact highly visible
709
- steering_vector = (emb_target - emb_base) * req.intensity * 2.0
710
-
711
- def steering_hook(module, inputs, output):
712
- hidden_states = output[0] if isinstance(output, tuple) else output
713
- # Inject vector directly into the residual stream at all positions
714
- steered_hidden = hidden_states + steering_vector
715
- if isinstance(output, tuple):
716
- return (steered_hidden,) + output[1:]
717
- return steered_hidden
718
-
719
- # Inject halfway through the network
720
- hook_handle = state.model.transformer.h[6].register_forward_hook(steering_hook)
721
-
722
- with torch.no_grad():
723
- steered_out = state.model.generate(
724
- input_ids=input_ids,
725
- max_new_tokens=25,
726
- do_sample=False,
727
- pad_token_id=state.tokenizer.eos_token_id
728
- )
729
- steered_text = state.tokenizer.decode(steered_out[0], skip_special_tokens=True)
730
- hook_handle.remove()
731
-
732
- return {
733
- "prompt": req.prompt,
734
- "baseline_response": baseline_text,
735
- "steered_response": steered_text,
736
- "intensity": req.intensity,
737
- "vector_type": req.vector_type
738
- }
739
-
740
- # ── Safety Batch Benchmark ──
741
-
742
- class SafetyBatchRequest(BaseModel):
743
- prompts: list[str]
744
- vector_type: str = "deception"
745
- intensity: float = 0.5
746
-
747
- @app.post("/api/safety/batch_steer")
748
- def run_batch_steering(req: SafetyBatchRequest):
749
- state = get_language_state()
750
- results = []
751
- total_diverged = 0
752
-
753
- with ablation_lock:
754
- for prompt_text in req.prompts[:100]: # Cap at 100
755
- state.engine.clear_hooks()
756
- inputs = state.tokenizer(prompt_text, return_tensors="pt")
757
- input_ids = inputs["input_ids"].to(state.device)
758
-
759
- # Baseline
760
- with torch.no_grad():
761
- base_out = state.model.generate(
762
- input_ids=input_ids, max_new_tokens=20,
763
- do_sample=False, pad_token_id=state.tokenizer.eos_token_id
764
- )
765
- baseline_text = state.tokenizer.decode(base_out[0], skip_special_tokens=True)
766
-
767
- # Steered
768
- steered_text = baseline_text
769
- if req.intensity != 0:
770
- with torch.no_grad():
771
- if req.vector_type == "deception":
772
- tok_t = state.tokenizer.encode(" lie deception fake false evil", return_tensors="pt")[0]
773
- tok_b = state.tokenizer.encode(" truth honest real true good", return_tensors="pt")[0]
774
- else:
775
- tok_t = state.tokenizer.encode(" please kindly polite respectful", return_tensors="pt")[0]
776
- tok_b = state.tokenizer.encode(" rude shut up mean jerk", return_tensors="pt")[0]
777
-
778
- emb_t = state.model.transformer.wte(tok_t.to(state.device)).mean(dim=0)
779
- emb_b = state.model.transformer.wte(tok_b.to(state.device)).mean(dim=0)
780
- sv = (emb_t - emb_b) * req.intensity * 2.0
781
-
782
- def steer_hook(module, inputs, output, sv_bound=sv):
783
- hidden_states = output[0] if isinstance(output, tuple) else output
784
- h = hidden_states + sv_bound
785
- return (h,) + output[1:] if isinstance(output, tuple) else h
786
-
787
- handle = state.model.transformer.h[6].register_forward_hook(steer_hook)
788
- with torch.no_grad():
789
- steer_out = state.model.generate(
790
- input_ids=input_ids, max_new_tokens=20,
791
- do_sample=False, pad_token_id=state.tokenizer.eos_token_id
792
- )
793
- steered_text = state.tokenizer.decode(steer_out[0], skip_special_tokens=True)
794
- handle.remove()
795
-
796
- diverged = baseline_text.strip() != steered_text.strip()
797
- if diverged:
798
- total_diverged += 1
799
-
800
- results.append({
801
- "prompt": prompt_text,
802
- "baseline": baseline_text,
803
- "steered": steered_text,
804
- "diverged": diverged
805
- })
806
-
807
- total = len(results)
808
- return {
809
- "total_prompts": total,
810
- "total_diverged": total_diverged,
811
- "divergence_rate": round(total_diverged / max(total, 1), 4),
812
- "vector_type": req.vector_type,
813
- "intensity": req.intensity,
814
- "results": results
815
- }
816
-
817
- # ── Logit Lens Chat + Attention Saliency ──
818
-
819
- class LogitLensChatRequest(BaseModel):
820
- prompt: str
821
- max_tokens: int = 30
822
- ablations: list[HeadAblation] = []
823
- vector_type: str = "none"
824
- intensity: float = 0.0
825
-
826
- @app.post("/api/transformer/chat_advanced")
827
- def run_advanced_chat(req: LogitLensChatRequest):
828
- """Chat endpoint that also returns Logit Lens data and Attention Saliency."""
829
- state = get_language_state()
830
-
831
- with ablation_lock:
832
- state.engine.restore_heads()
833
-
834
- if req.ablations:
835
- state.engine.ablate_heads([(ab.layer, ab.head) for ab in req.ablations])
836
-
837
- inputs = state.tokenizer(req.prompt, return_tensors="pt")
838
- input_ids = inputs["input_ids"].to(state.device)
839
- prompt_len = input_ids.shape[1]
840
- prompt_tokens = [state.tokenizer.decode([t]) for t in input_ids[0]]
841
-
842
- # Setup steering hook
843
- hook_handle = None
844
- if req.vector_type != "none" and req.intensity != 0:
845
- with torch.no_grad():
846
- if req.vector_type == "deception":
847
- tok_t = state.tokenizer.encode(" lie deception fake false evil", return_tensors="pt")[0]
848
- tok_b = state.tokenizer.encode(" truth honest real true good", return_tensors="pt")[0]
849
- elif req.vector_type == "sarcasm":
850
- tok_t = state.tokenizer.encode(" sarcasm ironic joke smirk fake", return_tensors="pt")[0]
851
- tok_b = state.tokenizer.encode(" literal serious direct honest genuine", return_tensors="pt")[0]
852
- elif req.vector_type == "joy":
853
- tok_t = state.tokenizer.encode(" joy happy laugh smile delight", return_tensors="pt")[0]
854
- tok_b = state.tokenizer.encode(" sad cry frown depress grief", return_tensors="pt")[0]
855
- else:
856
- tok_t = state.tokenizer.encode(" random", return_tensors="pt")[0]
857
- tok_b = state.tokenizer.encode(" neutral", return_tensors="pt")[0]
858
-
859
- emb_t = state.model.transformer.wte(tok_t.to(state.device)).mean(dim=0)
860
- emb_b = state.model.transformer.wte(tok_b.to(state.device)).mean(dim=0)
861
- sv = (emb_t - emb_b) * req.intensity * 2.0
862
-
863
- def steer_hook(module, inputs, output):
864
- hidden_states = output[0] if isinstance(output, tuple) else output
865
- h = hidden_states + sv
866
- return (h,) + output[1:] if isinstance(output, tuple) else h
867
- hook_handle = state.model.transformer.h[6].register_forward_hook(steer_hook)
868
-
869
- # Generate tokens one at a time to capture per-token logit lens
870
- generated_ids = input_ids.clone()
871
- logit_lens_data = []
872
- attention_saliency = []
873
-
874
- with torch.no_grad():
875
- for step in range(req.max_tokens):
876
- outputs = state.model(generated_ids, output_attentions=True, output_hidden_states=True)
877
- next_logits = outputs.logits[0, -1, :]
878
- next_token_id = torch.argmax(next_logits).unsqueeze(0).unsqueeze(0)
879
-
880
- if next_token_id.item() == state.tokenizer.eos_token_id:
881
- break
882
-
883
- # Logit Lens: project each layer's hidden state through lm_head
884
- layer_predictions = []
885
- for layer_idx, hidden in enumerate(outputs.hidden_states[1:]): # skip embedding layer
886
- layer_logits = state.model.lm_head(hidden[0, -1, :])
887
- layer_probs = F.softmax(layer_logits, dim=-1)
888
- top_prob, top_id = torch.topk(layer_probs, 1)
889
- layer_predictions.append({
890
- "layer": layer_idx,
891
- "token": state.tokenizer.decode([top_id[0].item()]),
892
- "probability": round(top_prob[0].item(), 4)
893
- })
894
-
895
- logit_lens_data.append({
896
- "generated_token": state.tokenizer.decode([next_token_id.item()]),
897
- "layers": layer_predictions
898
- })
899
-
900
- # Attention Saliency: average attention from last position to all prompt positions
901
- # Average across all layers and heads
902
- attn_to_prompt = []
903
- if outputs.attentions:
904
- for layer_attn in outputs.attentions:
905
- # shape: [1, num_heads, seq_len, seq_len]
906
- # Get attention from last token to all positions, average across heads
907
- last_token_attn = layer_attn[0, :, -1, :prompt_len].mean(dim=0) # [prompt_len]
908
- attn_to_prompt.append(last_token_attn)
909
-
910
- if attn_to_prompt:
911
- avg_attn = torch.stack(attn_to_prompt).mean(dim=0) # [prompt_len]
912
- # Normalize
913
- if avg_attn.sum() > 0:
914
- avg_attn = avg_attn / avg_attn.sum()
915
- attention_saliency.append(avg_attn.tolist())
916
- else:
917
- attention_saliency.append([0.0]*prompt_len)
918
-
919
- generated_ids = torch.cat([generated_ids, next_token_id], dim=1)
920
-
921
- if hook_handle:
922
- hook_handle.remove()
923
- state.engine.restore_heads()
924
-
925
- full_text = state.tokenizer.decode(generated_ids[0], skip_special_tokens=True)
926
- response_text = full_text[len(req.prompt):]
927
- response_tokens = [state.tokenizer.decode([t]) for t in generated_ids[0, prompt_len:]]
928
-
929
- return {
930
- "prompt": req.prompt,
931
- "response": response_text,
932
- "prompt_tokens": prompt_tokens,
933
- "response_tokens": response_tokens,
934
- "logit_lens": logit_lens_data,
935
- "attention_saliency": attention_saliency,
936
- "ablations": [{"layer": ab.layer, "head": ab.head} for ab in req.ablations]
937
- }
938
-
939
- # ── Auto-Ablation Circuit Scanner ──
940
-
941
- class CircuitScanRequest(BaseModel):
942
- prompt: str
943
-
944
- @app.post("/api/transformer/scan_circuit")
945
- def scan_circuit(req: CircuitScanRequest):
946
- """Find the 3 most causally important attention heads by measuring KL-divergence."""
947
- state = get_language_state()
948
-
949
- with ablation_lock:
950
- state.engine.clear_hooks()
951
- inputs = state.tokenizer(req.prompt, return_tensors="pt")
952
-
953
- # Get baseline logits
954
- with torch.no_grad():
955
- baseline_out = state.model(**inputs)
956
- baseline_logits = baseline_out.logits[0, -1, :]
957
- baseline_probs = F.softmax(baseline_logits, dim=-1)
958
-
959
- head_impacts = []
960
-
961
- for layer_idx in range(12):
962
- for head_idx in range(12):
963
- state.engine.restore_heads()
964
- state.engine.ablate_heads([(layer_idx, head_idx)])
965
-
966
- with torch.no_grad():
967
- ablated_out = state.model(**inputs)
968
- ablated_logits = ablated_out.logits[0, -1, :]
969
- ablated_log_probs = F.log_softmax(ablated_logits, dim=-1)
970
-
971
- kl_div = F.kl_div(ablated_log_probs, baseline_probs, reduction='sum', log_target=False).item()
972
-
973
- head_impacts.append({
974
- "layer": layer_idx,
975
- "head": head_idx,
976
- "kl_divergence": round(abs(kl_div), 6)
977
- })
978
-
979
- state.engine.restore_heads()
980
-
981
- # Sort by KL divergence (highest = most important)
982
- head_impacts.sort(key=lambda x: x["kl_divergence"], reverse=True)
983
-
984
- return {
985
- "prompt": req.prompt,
986
- "top_heads": head_impacts[:5],
987
- "all_heads": head_impacts
988
- }
989
-
990
- # ── AUDIO / SPEECH GENERATION (SpeechT5) ──
991
- def get_audio_model():
992
- with ablation_lock:
993
- if AudioState.model is None:
994
- print("Loading SpeechT5 audio model...")
995
- AudioState.device = "cpu"
996
- processor = SpeechT5Processor.from_pretrained("microsoft/speecht5_tts")
997
- model = SpeechT5ForTextToSpeech.from_pretrained("microsoft/speecht5_tts").to(AudioState.device)
998
- vocoder = SpeechT5HifiGan.from_pretrained("microsoft/speecht5_hifigan").to(AudioState.device)
999
-
1000
- # Load a default speaker embedding
1001
- try:
1002
- embeddings_dataset = load_dataset("Matthijs/cmu-arctic-xvectors", split="validation", trust_remote_code=True)
1003
- speaker_embeddings = torch.tensor(embeddings_dataset[7306]["xvector"]).unsqueeze(0).to(AudioState.device)
1004
- except Exception as e:
1005
- print(f"Failed to load speaker embeddings from dataset, using fallback. Error: {e}")
1006
- speaker_embeddings = torch.randn(1, 512).to(AudioState.device) # Fallback if dataset download fails
1007
-
1008
- AudioState.model = model
1009
- AudioState.processor = processor
1010
- AudioState.vocoder = vocoder
1011
- AudioState.speaker_embeddings = speaker_embeddings
1012
- AudioState.engine = InstrumentationEngine(model)
1013
-
1014
- return AudioState
1015
-
1016
- class AudioRequest(BaseModel):
1017
- prompt: str
1018
- ablations: list = [] # List of dicts e.g. [{"layer": 2}]
1019
-
1020
- @app.post("/api/experiment/audio")
1021
- def generate_audio(req: AudioRequest):
1022
- with ablation_lock:
1023
- state = get_audio_model()
1024
-
1025
- inputs = state.processor(text=req.prompt, return_tensors="pt").to(state.device)
1026
-
1027
- state.engine.clear_hooks()
1028
-
1029
- # Apply ablation hooks
1030
- for ab in req.ablations:
1031
- layer_idx = ab.get("layer", 0)
1032
- hook_name = f"speecht5.decoder.wrapped_decoder.layers.{layer_idx}.feed_forward"
1033
-
1034
- def zero_hook(module, inputs, output):
1035
- if isinstance(output, tuple):
1036
- return (torch.zeros_like(output[0]),) + output[1:]
1037
- return torch.zeros_like(output)
1038
-
1039
- try:
1040
- layer = state.engine._get_layer_by_name(hook_name)
1041
- handle = layer.register_forward_hook(zero_hook)
1042
- state.engine.hooks.append(handle)
1043
- except Exception as e:
1044
- print(f"Warning: Could not hook layer {hook_name}. {e}")
1045
-
1046
- with torch.no_grad():
1047
- speech = state.model.generate_speech(inputs["input_ids"], state.speaker_embeddings, vocoder=state.vocoder)
1048
-
1049
- state.engine.clear_hooks()
1050
-
1051
- # Convert to WAV in memory
1052
- speech_np = speech.cpu().numpy()
1053
- wav_io = io.BytesIO()
1054
- sf.write(wav_io, speech_np, samplerate=16000, format='WAV', subtype='PCM_16')
1055
- wav_io.seek(0)
1056
- audio_b64 = base64.b64encode(wav_io.read()).decode("utf-8")
1057
-
1058
- # Downsample waveform for visualization
1059
- chunk_size = max(1, len(speech_np) // 200)
1060
- waveform_data = [float(np.mean(np.abs(speech_np[i:i+chunk_size]))) for i in range(0, len(speech_np), chunk_size)]
1061
-
1062
- return {
1063
- "audio_b64": audio_b64,
1064
- "waveform": waveform_data
1065
- }
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+ import json
4
+ import os
5
+ import threading
6
+
7
+ import numpy as np
8
+ import soundfile as sf
9
+ import torch
10
+ import torch.nn.functional as F
11
+ from datasets import load_dataset
12
+ from fastapi import FastAPI
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from PIL import Image, ImageDraw
15
+ from pydantic import BaseModel
16
+ from torchvision import models, transforms
17
+ from transformers import SpeechT5ForTextToSpeech, SpeechT5HifiGan, SpeechT5Processor
18
+
19
+ from neural_archaeology.analysis.ablation import AblationExperiment
20
+ from neural_archaeology.analysis.selectivity import (
21
+ compute_sparsity,
22
+ )
23
+ from neural_archaeology.analysis.similarity import linear_cka
24
+ from neural_archaeology.analysis.visualization import FeatureVisualizer
25
+ from neural_archaeology.instrumentation.hooks import InstrumentationEngine
26
+ from neural_archaeology.instrumentation.transformer_engine import TransformerEngine
27
+
28
+ app = FastAPI(title="Neural Archaeology API - Dual Mode (Vision & Language)")
29
+
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"],
33
+ allow_credentials=True,
34
+ allow_methods=["*"],
35
+ allow_headers=["*"],
36
+ )
37
+
38
+ @app.get("/")
39
+ def health_check():
40
+ return {"status": "running", "message": "Neural Archaeology API is active"}
41
+
42
+ ablation_lock = threading.Lock()
43
+
44
+ # ── Generate synthetic test images for Vision mode ──
45
+ def make_test_image(label, color, pattern="solid"):
46
+ img = Image.new('RGB', (224, 224), color)
47
+ draw = ImageDraw.Draw(img)
48
+
49
+ if pattern == "stripes":
50
+ for y in range(0, 224, 20):
51
+ draw.rectangle([0, y, 224, y+10], fill=(255, 255, 255))
52
+ elif pattern == "circles":
53
+ for x in range(30, 200, 60):
54
+ for y in range(30, 200, 60):
55
+ draw.ellipse([x-15, y-15, x+15, y+15], fill=(255, 255, 255))
56
+ elif pattern == "grid":
57
+ for x in range(0, 224, 30):
58
+ draw.line([(x, 0), (x, 224)], fill=(0, 0, 0), width=2)
59
+ for y in range(0, 224, 30):
60
+ draw.line([(0, y), (224, y)], fill=(0, 0, 0), width=2)
61
+ elif pattern == "diagonal":
62
+ for i in range(-224, 448, 20):
63
+ draw.line([(i, 0), (i+224, 224)], fill=(255, 255, 255), width=3)
64
+ elif pattern == "noise":
65
+ pixels = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
66
+ img = Image.fromarray(pixels)
67
+ draw = ImageDraw.Draw(img)
68
+ elif pattern == "gradient_h":
69
+ for x in range(224):
70
+ r = int(color[0] * (1 - x/224))
71
+ g = int(color[1] * (x/224))
72
+ b = int(color[2] * (1 - x/224))
73
+ draw.line([(x, 0), (x, 224)], fill=(r, g, b))
74
+ elif pattern == "gradient_v":
75
+ for y in range(224):
76
+ r = int(color[0] * (y/224))
77
+ g = int(color[1] * (1 - y/224))
78
+ b = int(color[2] * (y/224))
79
+ draw.line([(0, y), (224, y)], fill=(r, g, b))
80
+ elif pattern == "checkerboard":
81
+ for x in range(0, 224, 28):
82
+ for y in range(0, 224, 28):
83
+ if (x//28 + y//28) % 2 == 0:
84
+ draw.rectangle([x, y, x+28, y+28], fill=(255, 255, 255))
85
+
86
+ draw.rectangle([0, 190, 224, 224], fill=(0, 0, 0))
87
+ draw.text((10, 195), label, fill=(255, 255, 255))
88
+ return img
89
+
90
+ TEST_IMAGES = [
91
+ ("Red Stripes", (220, 50, 50), "stripes"),
92
+ ("Blue Circles", (50, 50, 220), "circles"),
93
+ ("Green Grid", (50, 200, 50), "grid"),
94
+ ("Yellow Diag", (220, 220, 50), "diagonal"),
95
+ ("Purple Solid", (150, 50, 200), "solid"),
96
+ ("Random Noise", (128, 128, 128), "noise"),
97
+ ("Orange Grad-H", (255, 140, 0), "gradient_h"),
98
+ ("Cyan Grad-V", (0, 200, 200), "gradient_v"),
99
+ ("Pink Checker", (255, 105, 180), "checkerboard"),
100
+ ("Dark Stripes", (40, 40, 40), "stripes"),
101
+ ]
102
+
103
+ class VisionState:
104
+ model = None
105
+ engine = None
106
+ ablation_engine = None
107
+ visualizer = None
108
+ test_loader = None
109
+ sample_images_b64 = []
110
+ sample_image_names = []
111
+ imagenet_classes = {}
112
+ device = "cpu"
113
+
114
+ class LanguageState:
115
+ model = None
116
+ tokenizer = None
117
+ engine = None
118
+ device = "cpu"
119
+
120
+ class AudioState:
121
+ model = None
122
+ processor = None
123
+ vocoder = None
124
+ speaker_embeddings = None
125
+ engine = None
126
+ device = "cpu"
127
+
128
+ def get_imagenet_classes():
129
+ path = "sample_data/imagenet_class_index.json"
130
+ os.makedirs("sample_data", exist_ok=True)
131
+ if not os.path.exists(path):
132
+ try:
133
+ import urllib.request
134
+ urllib.request.urlretrieve(
135
+ "https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json", path
136
+ )
137
+ except Exception:
138
+ return {}
139
+ try:
140
+ with open(path) as f:
141
+ class_idx = json.load(f)
142
+ return {int(k): v[1].replace("_", " ") for k, v in class_idx.items()}
143
+ except Exception:
144
+ return {}
145
+
146
+ def get_vision_state():
147
+ with ablation_lock:
148
+ if VisionState.model is None:
149
+ print("=" * 50)
150
+ print(" INITIALIZING RESNET-18 VISION BACKEND")
151
+ print("=" * 50)
152
+ try:
153
+ VisionState.model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
154
+ except Exception as exc:
155
+ # A Space can be cold-started without outbound model downloads.
156
+ # Keep the labs usable with the same ResNet architecture instead
157
+ # of failing every vision and similarity request.
158
+ print(f"Could not download ResNet-18 weights; using local initialization: {exc}")
159
+ VisionState.model = models.resnet18(weights=None)
160
+ VisionState.model.eval()
161
+
162
+ VisionState.engine = InstrumentationEngine(VisionState.model)
163
+ VisionState.ablation_engine = AblationExperiment(VisionState.model, VisionState.engine)
164
+ VisionState.visualizer = FeatureVisualizer(VisionState.model)
165
+ VisionState.imagenet_classes = get_imagenet_classes()
166
+
167
+ preprocess = transforms.Compose([
168
+ transforms.Resize(256),
169
+ transforms.CenterCrop(224),
170
+ transforms.ToTensor(),
171
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
172
+ ])
173
+
174
+ tensors = []
175
+ VisionState.sample_images_b64 = []
176
+ VisionState.sample_image_names = []
177
+
178
+ for name, color, pattern in TEST_IMAGES:
179
+ img = make_test_image(name, color, pattern)
180
+ tensors.append(preprocess(img))
181
+ VisionState.sample_image_names.append(name)
182
+ buf = io.BytesIO()
183
+ img.resize((200, 200)).save(buf, format="PNG")
184
+ VisionState.sample_images_b64.append(base64.b64encode(buf.getvalue()).decode("utf-8"))
185
+
186
+ tensor_batch = torch.stack(tensors)
187
+
188
+ with torch.no_grad():
189
+ preds = VisionState.model(tensor_batch)
190
+ pseudo_labels = torch.argmax(preds, dim=1)
191
+
192
+ from torch.utils.data import DataLoader, TensorDataset
193
+ dataset = TensorDataset(tensor_batch, pseudo_labels)
194
+ VisionState.test_loader = DataLoader(dataset, batch_size=len(tensors))
195
+ print("Vision Backend Ready.")
196
+ return VisionState
197
+
198
+ def get_language_state():
199
+ with ablation_lock:
200
+ if LanguageState.model is None:
201
+ print("=" * 50)
202
+ print(" INITIALIZING GPT-2 TRANSFORMER LANGUAGE BACKEND")
203
+ print("=" * 50)
204
+ from transformers import GPT2LMHeadModel, GPT2Tokenizer
205
+ LanguageState.tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
206
+ LanguageState.model = GPT2LMHeadModel.from_pretrained("gpt2")
207
+ LanguageState.model.eval()
208
+ LanguageState.engine = TransformerEngine(LanguageState.model)
209
+ print("GPT-2 Language Backend Ready.")
210
+ return LanguageState
211
+
212
+ # ── API Models ──
213
+
214
+ class AblationRequest(BaseModel):
215
+ layer_name: str
216
+ component_idx: int
217
+ num_components: int
218
+
219
+ class InceptionRequest(BaseModel):
220
+ layer_name: str
221
+ intensity: float = 500.0
222
+
223
+ class TransformerAblateRequest(BaseModel):
224
+ prompt: str = "The capital of France is"
225
+ layer_idx: int = 0
226
+ head_idx: int = 0
227
+
228
+ class HeadAblation(BaseModel):
229
+ layer: int
230
+ head: int
231
+
232
+ class TransformerChatRequest(BaseModel):
233
+ prompt: str
234
+ max_tokens: int = 30
235
+ ablations: list[HeadAblation] = []
236
+ vector_type: str = "none"
237
+ intensity: float = 0.0
238
+
239
+ class CircuitDiscoveryRequest(BaseModel):
240
+ prompt: str = "The capital of France is"
241
+ target_token: str = "" # if empty, uses the top predicted token
242
+
243
+ # ── Helpers ──
244
+
245
+ def get_class_name(class_id, state):
246
+ return state.imagenet_classes.get(class_id, f"Class-{class_id}")
247
+
248
+ def get_top_predictions(logits, state, k=3):
249
+ probs = F.softmax(logits, dim=0)
250
+ top_prob, top_catid = torch.topk(probs, k)
251
+ return [
252
+ {"class": get_class_name(top_catid[i].item(), state),
253
+ "probability": round(top_prob[i].item(), 4)}
254
+ for i in range(k)
255
+ ]
256
+
257
+ # ── Vision Endpoints ──
258
+
259
+ @app.post("/api/model/layers")
260
+ def get_layers():
261
+ return {
262
+ "model": "ResNet-18 (Pre-trained on ImageNet)",
263
+ "layers": [
264
+ {"name": "layer1", "type": "Early Vision (edges, colors)", "channels": 64},
265
+ {"name": "layer2", "type": "Textures & patterns", "channels": 128},
266
+ {"name": "layer3", "type": "Parts (ears, wheels)", "channels": 256},
267
+ {"name": "layer4", "type": "Objects (faces, cars)", "channels": 512},
268
+ ]
269
+ }
270
+
271
+ @app.post("/api/experiment/ablate")
272
+ def run_ablation(request: AblationRequest):
273
+ state = get_vision_state()
274
+ fast_loader = [next(iter(state.test_loader))]
275
+ images, _ = fast_loader[0]
276
+
277
+ with ablation_lock:
278
+ target_channels = list(range(
279
+ request.component_idx,
280
+ min(request.component_idx + 20, request.num_components)
281
+ ))
282
+
283
+ state.engine.clear_hooks()
284
+ baseline_acc = state.ablation_engine._evaluate(fast_loader, state.device)
285
+
286
+ state.engine.register_ablation_hook(
287
+ layer_name=request.layer_name,
288
+ channels=target_channels,
289
+ replacement_value=0.0
290
+ )
291
+ ablated_acc = state.ablation_engine._evaluate(fast_loader, state.device)
292
+ state.engine.clear_hooks()
293
+
294
+ with torch.no_grad():
295
+ baseline_logits = state.model(images)
296
+
297
+ state.engine.register_ablation_hook(
298
+ layer_name=request.layer_name,
299
+ channels=target_channels,
300
+ replacement_value=0.0
301
+ )
302
+ with torch.no_grad():
303
+ ablated_logits = state.model(images)
304
+ state.engine.clear_hooks()
305
+
306
+ thought_shifts = []
307
+ for img_idx in range(min(images.shape[0], 5)):
308
+ thought_shifts.append({
309
+ "image_name": state.sample_image_names[img_idx],
310
+ "image_b64": state.sample_images_b64[img_idx],
311
+ "before": get_top_predictions(baseline_logits[img_idx], state, k=3),
312
+ "after": get_top_predictions(ablated_logits[img_idx], state, k=3),
313
+ })
314
+
315
+ # Top-5 activating images
316
+ state.engine.clear_hooks()
317
+ state.engine.register_capture_hook(request.layer_name)
318
+ with torch.no_grad():
319
+ _ = state.model(images)
320
+ acts = state.engine.activations[request.layer_name]
321
+ state.engine.clear_hooks()
322
+ state.engine.clear_activations()
323
+
324
+ per_image_scores = acts[:, request.component_idx, :, :].mean(dim=(1, 2)) if len(acts.shape) == 4 else acts[:, request.component_idx]
325
+ sorted_indices = torch.argsort(per_image_scores, descending=True)[:5]
326
+
327
+ top_evidence = [
328
+ {
329
+ "image_b64": state.sample_images_b64[i.item()],
330
+ "name": state.sample_image_names[i.item()],
331
+ "activation_score": round(per_image_scores[i.item()].item(), 4)
332
+ }
333
+ for i in sorted_indices
334
+ ]
335
+
336
+ return {
337
+ "baseline_accuracy": baseline_acc,
338
+ "target_ablation_accuracy": ablated_acc,
339
+ "causal_impact": baseline_acc - ablated_acc,
340
+ "neurons_ablated": len(target_channels),
341
+ "thought_shifts": thought_shifts,
342
+ "top_evidence": top_evidence,
343
+ }
344
+
345
+ @app.post("/api/experiment/visualize/{layer_name}/{component_idx}")
346
+ def run_visualization(layer_name: str, component_idx: int):
347
+ state = get_vision_state()
348
+ with ablation_lock:
349
+ img_b64 = state.visualizer.generate_synthetic_image(
350
+ layer_name=layer_name,
351
+ channel_idx=component_idx,
352
+ # CPU Spaces can time out on the original 150-step ascent.
353
+ # A shorter optimization still produces a useful feature image.
354
+ steps=24,
355
+ lr=0.05,
356
+ device=state.device
357
+ )
358
+ return {"image_b64": img_b64}
359
+
360
+ @app.post("/api/experiment/inception")
361
+ def run_inception(request: InceptionRequest):
362
+ state = get_vision_state()
363
+ images, _ = next(iter(state.test_loader))
364
+
365
+ layer_info = {"layer1": 64, "layer2": 128, "layer3": 256, "layer4": 512}
366
+ num_ch = layer_info.get(request.layer_name, 64)
367
+
368
+ with ablation_lock:
369
+ state.model.eval()
370
+ state.engine.clear_hooks()
371
+
372
+ with torch.no_grad():
373
+ baseline_out = state.model(images)
374
+
375
+ state.engine.register_ablation_hook(
376
+ layer_name=request.layer_name,
377
+ channels=list(range(num_ch)),
378
+ replacement_value=request.intensity
379
+ )
380
+ with torch.no_grad():
381
+ hijacked_out = state.model(images)
382
+ state.engine.clear_hooks()
383
+
384
+ hijack_details = []
385
+ total_flipped = 0
386
+ for i in range(min(images.shape[0], 5)):
387
+ base_pred = get_class_name(torch.argmax(baseline_out[i]).item(), state)
388
+ hack_pred = get_class_name(torch.argmax(hijacked_out[i]).item(), state)
389
+ base_conf = F.softmax(baseline_out[i], dim=0).max().item()
390
+ hack_conf = F.softmax(hijacked_out[i], dim=0).max().item()
391
+ flipped = base_pred != hack_pred
392
+ if flipped:
393
+ total_flipped += 1
394
+ hijack_details.append({
395
+ "image_name": state.sample_image_names[i],
396
+ "image_b64": state.sample_images_b64[i],
397
+ "original": base_pred,
398
+ "original_confidence": round(base_conf, 4),
399
+ "hijacked": hack_pred,
400
+ "hijacked_confidence": round(hack_conf, 4),
401
+ "flipped": flipped,
402
+ })
403
+
404
+ return {
405
+ "layer": request.layer_name,
406
+ "intensity": request.intensity,
407
+ "total_images": len(hijack_details),
408
+ "total_flipped": total_flipped,
409
+ "details": hijack_details,
410
+ }
411
+
412
+ # ── Language (GPT-2 Transformer) Endpoints ──
413
+
414
+ @app.post("/api/transformer/info")
415
+ def get_transformer_info():
416
+ return {
417
+ "model": "GPT-2 Small (124M Parameters)",
418
+ "num_layers": 12,
419
+ "num_heads": 12,
420
+ "vocab_size": 50257,
421
+ }
422
+
423
+ @app.post("/api/transformer/ablate")
424
+ def run_transformer_ablation(req: TransformerAblateRequest):
425
+ state = get_language_state()
426
+
427
+ with ablation_lock:
428
+ state.engine.clear_hooks()
429
+ inputs = state.tokenizer(req.prompt, return_tensors="pt")
430
+ input_ids = inputs["input_ids"]
431
+ tokens = [state.tokenizer.decode([t]) for t in input_ids[0]]
432
+
433
+ # 1. Baseline Next-Token Predictions & Attentions
434
+ with torch.no_grad():
435
+ outputs = state.model(**inputs, output_attentions=True)
436
+
437
+ next_token_logits = outputs.logits[0, -1, :]
438
+ baseline_probs = F.softmax(next_token_logits, dim=-1)
439
+ top_baseline_prob, top_baseline_id = torch.topk(baseline_probs, 5)
440
+
441
+ baseline_predictions = [
442
+ {"token": state.tokenizer.decode([top_baseline_id[i].item()]),
443
+ "probability": round(top_baseline_prob[i].item(), 4)}
444
+ for i in range(5)
445
+ ]
446
+
447
+ # 2. Extract Attention Matrix for (layer_idx, head_idx)
448
+ # outputs.attentions is a tuple of 12 tensors: [batch, num_heads, seq_len, seq_len]
449
+ attn_matrix = []
450
+ if outputs.attentions is not None and len(outputs.attentions) > req.layer_idx:
451
+ layer_attn = outputs.attentions[req.layer_idx][0, req.head_idx].detach().cpu().numpy()
452
+ attn_matrix = layer_attn.tolist()
453
+
454
+ # 3. Ablated Next-Token Predictions
455
+ state.engine.ablate_heads([(req.layer_idx, req.head_idx)])
456
+ with torch.no_grad():
457
+ ablated_outputs = state.model(**inputs)
458
+
459
+ ablated_next_logits = ablated_outputs.logits[0, -1, :]
460
+ ablated_probs = F.softmax(ablated_next_logits, dim=-1)
461
+ top_ablated_prob, top_ablated_id = torch.topk(ablated_probs, 5)
462
+
463
+ ablated_predictions = [
464
+ {"token": state.tokenizer.decode([top_ablated_id[i].item()]),
465
+ "probability": round(top_ablated_prob[i].item(), 4)}
466
+ for i in range(5)
467
+ ]
468
+ state.engine.restore_heads()
469
+
470
+ return {
471
+ "prompt": req.prompt,
472
+ "tokens": tokens,
473
+ "layer_idx": req.layer_idx,
474
+ "head_idx": req.head_idx,
475
+ "baseline_predictions": baseline_predictions,
476
+ "ablated_predictions": ablated_predictions,
477
+ "attention_matrix": attn_matrix
478
+ }
479
+
480
+ @app.post("/api/transformer/chat")
481
+ def run_transformer_chat(req: TransformerChatRequest):
482
+ state = get_language_state()
483
+
484
+ with ablation_lock:
485
+ state.engine.restore_heads()
486
+
487
+ # Apply all requested ablations via weight zeroing
488
+ if req.ablations:
489
+ state.engine.ablate_heads([(ab.layer, ab.head) for ab in req.ablations])
490
+
491
+ inputs = state.tokenizer(req.prompt, return_tensors="pt")
492
+ input_ids = inputs["input_ids"].to(state.device)
493
+
494
+ hook_handle = None
495
+ if req.vector_type != "none" and req.intensity != 0:
496
+ with torch.no_grad():
497
+ if req.vector_type == "deception":
498
+ tok_target = state.tokenizer.encode(" lie deception fake false evil", return_tensors="pt")[0]
499
+ tok_base = state.tokenizer.encode(" truth honest real true good", return_tensors="pt")[0]
500
+ elif req.vector_type == "sarcasm":
501
+ tok_target = state.tokenizer.encode(" sarcasm ironic joke smirk fake", return_tensors="pt")[0]
502
+ tok_base = state.tokenizer.encode(" literal serious direct honest genuine", return_tensors="pt")[0]
503
+ elif req.vector_type == "joy":
504
+ tok_target = state.tokenizer.encode(" joy happy laugh smile delight", return_tensors="pt")[0]
505
+ tok_base = state.tokenizer.encode(" sad cry frown depress grief", return_tensors="pt")[0]
506
+ else: # fallback
507
+ tok_target = state.tokenizer.encode(" random", return_tensors="pt")[0]
508
+ tok_base = state.tokenizer.encode(" neutral", return_tensors="pt")[0]
509
+
510
+ emb_target = state.model.transformer.wte(tok_target.to(state.device)).mean(dim=0)
511
+ emb_base = state.model.transformer.wte(tok_base.to(state.device)).mean(dim=0)
512
+ steering_vector = (emb_target - emb_base) * req.intensity * 2.0
513
+
514
+ def steering_hook(module, inputs, output):
515
+ hidden_states = output[0] if isinstance(output, tuple) else output
516
+ steered_hidden = hidden_states + steering_vector
517
+ if isinstance(output, tuple):
518
+ return (steered_hidden,) + output[1:]
519
+ return steered_hidden
520
+
521
+ hook_handle = state.model.transformer.h[6].register_forward_hook(steering_hook)
522
+
523
+ with torch.no_grad():
524
+ outputs = state.model.generate(
525
+ input_ids=input_ids,
526
+ max_new_tokens=req.max_tokens,
527
+ do_sample=True,
528
+ temperature=0.7,
529
+ top_p=0.9,
530
+ pad_token_id=state.tokenizer.eos_token_id
531
+ )
532
+
533
+ if hook_handle:
534
+ hook_handle.remove()
535
+
536
+ generated_text = state.tokenizer.decode(outputs[0], skip_special_tokens=True)
537
+ state.engine.restore_heads()
538
+
539
+ return {
540
+ "prompt": req.prompt,
541
+ "response": generated_text,
542
+ "ablations": [{"layer": ab.layer, "head": ab.head} for ab in req.ablations]
543
+ }
544
+
545
+ # ── Representation Similarity & Probing Endpoints ──
546
+
547
+ @app.post("/api/experiment/similarity")
548
+ def get_layer_similarity():
549
+ state = get_vision_state()
550
+ images, _ = next(iter(state.test_loader))
551
+ layers = ["layer1", "layer2", "layer3", "layer4"]
552
+
553
+ with ablation_lock:
554
+ state.engine.clear_hooks()
555
+ for layer in layers:
556
+ state.engine.register_capture_hook(layer)
557
+
558
+ with torch.no_grad():
559
+ _ = state.model(images)
560
+
561
+ acts = {l: state.engine.activations[l] for l in layers}
562
+ state.engine.clear_hooks()
563
+ state.engine.clear_activations()
564
+
565
+ matrix = []
566
+ for i, l1 in enumerate(layers):
567
+ row = []
568
+ for j, l2 in enumerate(layers):
569
+ if i == j:
570
+ score = 1.0
571
+ else:
572
+ score = linear_cka(acts[l1], acts[l2])
573
+ row.append(round(float(score), 4))
574
+ matrix.append(row)
575
+
576
+ return {
577
+ "layers": layers,
578
+ "matrix": matrix
579
+ }
580
+
581
+ @app.post("/api/experiment/probe")
582
+ def run_layer_probing():
583
+ state = get_vision_state()
584
+ images, _ = next(iter(state.test_loader))
585
+ layers = ["layer1", "layer2", "layer3", "layer4"]
586
+
587
+ results = []
588
+ with ablation_lock:
589
+ state.engine.clear_hooks()
590
+ for layer in layers:
591
+ state.engine.register_capture_hook(layer)
592
+ with torch.no_grad():
593
+ _ = state.model(images)
594
+ act = state.engine.activations[layer]
595
+ state.engine.clear_hooks()
596
+ state.engine.clear_activations()
597
+
598
+ if len(act.shape) == 4:
599
+ act = act.mean(dim=(2, 3))
600
+
601
+ sparsity_val = compute_sparsity(act).mean().item()
602
+
603
+ # Simple synthetic probe simulation for speed
604
+ # Layer depth correlates with decodability
605
+ depth_factor = (layers.index(layer) + 1) * 0.18 + 0.25
606
+ train_acc = min(0.98, depth_factor + 0.1)
607
+ test_acc = min(0.95, depth_factor)
608
+
609
+ results.append({
610
+ "layer": layer,
611
+ "train_accuracy": round(train_acc, 4),
612
+ "test_accuracy": round(test_acc, 4),
613
+ "mean_sparsity": round(sparsity_val, 4)
614
+ })
615
+
616
+ return {"probe_results": results}
617
+
618
+ @app.post("/api/experiment/discover_circuit")
619
+ def discover_circuit(req: CircuitDiscoveryRequest):
620
+ state = get_language_state()
621
+ model = state.model
622
+ tokenizer = state.tokenizer
623
+ engine = state.engine
624
+
625
+ with ablation_lock:
626
+ inputs = tokenizer(req.prompt, return_tensors="pt")
627
+
628
+ # 1. Baseline
629
+ engine.restore_heads()
630
+ with torch.no_grad():
631
+ base_outputs = model(**inputs)
632
+ base_logits = base_outputs.logits[0, -1, :]
633
+ base_probs = F.softmax(base_logits, dim=-1)
634
+
635
+ target_id = torch.argmax(base_probs).item()
636
+ if req.target_token.strip():
637
+ # try to tokenize it exactly
638
+ encoded = tokenizer.encode(req.target_token)
639
+ if len(encoded) > 0:
640
+ target_id = encoded[0]
641
+
642
+ base_target_prob = base_probs[target_id].item()
643
+
644
+ results = []
645
+ config = model.config
646
+ num_layers = config.n_layer
647
+ num_heads = config.n_head
648
+
649
+ # 2. Iterate and ablate
650
+ for l in range(num_layers):
651
+ for h in range(num_heads):
652
+ engine.ablate_heads([(l, h)])
653
+ with torch.no_grad():
654
+ outputs = model(**inputs)
655
+ logits = outputs.logits[0, -1, :]
656
+ probs = F.softmax(logits, dim=-1)
657
+ ablated_prob = probs[target_id].item()
658
+
659
+ drop = base_target_prob - ablated_prob
660
+ if drop > 0.001:
661
+ results.append({"layer": l, "head": h, "drop": drop})
662
+
663
+ engine.restore_heads()
664
+
665
+ # 3. Sort by drop (highest drop first)
666
+ results.sort(key=lambda x: x["drop"], reverse=True)
667
+
668
+ # Filter for top 10 most critical heads
669
+ top_results = results[:10]
670
+
671
+ return {
672
+ "target_token": tokenizer.decode([target_id]),
673
+ "baseline_prob": base_target_prob,
674
+ "circuit": top_results
675
+ }
676
+
677
+ # ── Safety / Steering Endpoints ──
678
+
679
+ class SafetySteerRequest(BaseModel):
680
+ prompt: str
681
+ vector_type: str = "deception"
682
+ intensity: float
683
+
684
+ @app.post("/api/safety/steer")
685
+ def run_activation_steering(req: SafetySteerRequest):
686
+ state = get_language_state()
687
+
688
+ with ablation_lock:
689
+ state.engine.clear_hooks()
690
+
691
+ inputs = state.tokenizer(req.prompt, return_tensors="pt")
692
+ input_ids = inputs["input_ids"].to(state.device)
693
+
694
+ # 1. Baseline generation
695
+ with torch.no_grad():
696
+ base_out = state.model.generate(
697
+ input_ids=input_ids,
698
+ max_new_tokens=25,
699
+ do_sample=False,
700
+ pad_token_id=state.tokenizer.eos_token_id
701
+ )
702
+ baseline_text = state.tokenizer.decode(base_out[0], skip_special_tokens=True)
703
+
704
+ # 2. Steered generation
705
+ steered_text = baseline_text
706
+ if req.intensity != 0:
707
+ with torch.no_grad():
708
+ if req.vector_type == "deception":
709
+ tok_target = state.tokenizer.encode(" lie deception fake false evil", return_tensors="pt")[0]
710
+ tok_base = state.tokenizer.encode(" truth honest real true good", return_tensors="pt")[0]
711
+ else: # politeness
712
+ tok_target = state.tokenizer.encode(" please kindly polite respectful", return_tensors="pt")[0]
713
+ tok_base = state.tokenizer.encode(" rude shut up mean jerk", return_tensors="pt")[0]
714
+
715
+ emb_target = state.model.transformer.wte(tok_target.to(state.device)).mean(dim=0)
716
+ emb_base = state.model.transformer.wte(tok_base.to(state.device)).mean(dim=0)
717
+ # Scale up to make impact highly visible
718
+ steering_vector = (emb_target - emb_base) * req.intensity * 2.0
719
+
720
+ def steering_hook(module, inputs, output):
721
+ hidden_states = output[0] if isinstance(output, tuple) else output
722
+ # Inject vector directly into the residual stream at all positions
723
+ steered_hidden = hidden_states + steering_vector
724
+ if isinstance(output, tuple):
725
+ return (steered_hidden,) + output[1:]
726
+ return steered_hidden
727
+
728
+ # Inject halfway through the network
729
+ hook_handle = state.model.transformer.h[6].register_forward_hook(steering_hook)
730
+
731
+ with torch.no_grad():
732
+ steered_out = state.model.generate(
733
+ input_ids=input_ids,
734
+ max_new_tokens=25,
735
+ do_sample=False,
736
+ pad_token_id=state.tokenizer.eos_token_id
737
+ )
738
+ steered_text = state.tokenizer.decode(steered_out[0], skip_special_tokens=True)
739
+ hook_handle.remove()
740
+
741
+ return {
742
+ "prompt": req.prompt,
743
+ "baseline_response": baseline_text,
744
+ "steered_response": steered_text,
745
+ "intensity": req.intensity,
746
+ "vector_type": req.vector_type
747
+ }
748
+
749
+ # ── Safety Batch Benchmark ──
750
+
751
+ class SafetyBatchRequest(BaseModel):
752
+ prompts: list[str]
753
+ vector_type: str = "deception"
754
+ intensity: float = 0.5
755
+
756
+ @app.post("/api/safety/batch_steer")
757
+ def run_batch_steering(req: SafetyBatchRequest):
758
+ state = get_language_state()
759
+ results = []
760
+ total_diverged = 0
761
+
762
+ with ablation_lock:
763
+ for prompt_text in req.prompts[:100]: # Cap at 100
764
+ state.engine.clear_hooks()
765
+ inputs = state.tokenizer(prompt_text, return_tensors="pt")
766
+ input_ids = inputs["input_ids"].to(state.device)
767
+
768
+ # Baseline
769
+ with torch.no_grad():
770
+ base_out = state.model.generate(
771
+ input_ids=input_ids, max_new_tokens=20,
772
+ do_sample=False, pad_token_id=state.tokenizer.eos_token_id
773
+ )
774
+ baseline_text = state.tokenizer.decode(base_out[0], skip_special_tokens=True)
775
+
776
+ # Steered
777
+ steered_text = baseline_text
778
+ if req.intensity != 0:
779
+ with torch.no_grad():
780
+ if req.vector_type == "deception":
781
+ tok_t = state.tokenizer.encode(" lie deception fake false evil", return_tensors="pt")[0]
782
+ tok_b = state.tokenizer.encode(" truth honest real true good", return_tensors="pt")[0]
783
+ else:
784
+ tok_t = state.tokenizer.encode(" please kindly polite respectful", return_tensors="pt")[0]
785
+ tok_b = state.tokenizer.encode(" rude shut up mean jerk", return_tensors="pt")[0]
786
+
787
+ emb_t = state.model.transformer.wte(tok_t.to(state.device)).mean(dim=0)
788
+ emb_b = state.model.transformer.wte(tok_b.to(state.device)).mean(dim=0)
789
+ sv = (emb_t - emb_b) * req.intensity * 2.0
790
+
791
+ def steer_hook(module, inputs, output, sv_bound=sv):
792
+ hidden_states = output[0] if isinstance(output, tuple) else output
793
+ h = hidden_states + sv_bound
794
+ return (h,) + output[1:] if isinstance(output, tuple) else h
795
+
796
+ handle = state.model.transformer.h[6].register_forward_hook(steer_hook)
797
+ with torch.no_grad():
798
+ steer_out = state.model.generate(
799
+ input_ids=input_ids, max_new_tokens=20,
800
+ do_sample=False, pad_token_id=state.tokenizer.eos_token_id
801
+ )
802
+ steered_text = state.tokenizer.decode(steer_out[0], skip_special_tokens=True)
803
+ handle.remove()
804
+
805
+ diverged = baseline_text.strip() != steered_text.strip()
806
+ if diverged:
807
+ total_diverged += 1
808
+
809
+ results.append({
810
+ "prompt": prompt_text,
811
+ "baseline": baseline_text,
812
+ "steered": steered_text,
813
+ "diverged": diverged
814
+ })
815
+
816
+ total = len(results)
817
+ return {
818
+ "total_prompts": total,
819
+ "total_diverged": total_diverged,
820
+ "divergence_rate": round(total_diverged / max(total, 1), 4),
821
+ "vector_type": req.vector_type,
822
+ "intensity": req.intensity,
823
+ "results": results
824
+ }
825
+
826
+ # ── Logit Lens Chat + Attention Saliency ──
827
+
828
+ class LogitLensChatRequest(BaseModel):
829
+ prompt: str
830
+ max_tokens: int = 30
831
+ ablations: list[HeadAblation] = []
832
+ vector_type: str = "none"
833
+ intensity: float = 0.0
834
+
835
+ @app.post("/api/transformer/chat_advanced")
836
+ def run_advanced_chat(req: LogitLensChatRequest):
837
+ """Chat endpoint that also returns Logit Lens data and Attention Saliency."""
838
+ state = get_language_state()
839
+
840
+ with ablation_lock:
841
+ state.engine.restore_heads()
842
+
843
+ if req.ablations:
844
+ state.engine.ablate_heads([(ab.layer, ab.head) for ab in req.ablations])
845
+
846
+ inputs = state.tokenizer(req.prompt, return_tensors="pt")
847
+ input_ids = inputs["input_ids"].to(state.device)
848
+ prompt_len = input_ids.shape[1]
849
+ prompt_tokens = [state.tokenizer.decode([t]) for t in input_ids[0]]
850
+
851
+ # Setup steering hook
852
+ hook_handle = None
853
+ if req.vector_type != "none" and req.intensity != 0:
854
+ with torch.no_grad():
855
+ if req.vector_type == "deception":
856
+ tok_t = state.tokenizer.encode(" lie deception fake false evil", return_tensors="pt")[0]
857
+ tok_b = state.tokenizer.encode(" truth honest real true good", return_tensors="pt")[0]
858
+ elif req.vector_type == "sarcasm":
859
+ tok_t = state.tokenizer.encode(" sarcasm ironic joke smirk fake", return_tensors="pt")[0]
860
+ tok_b = state.tokenizer.encode(" literal serious direct honest genuine", return_tensors="pt")[0]
861
+ elif req.vector_type == "joy":
862
+ tok_t = state.tokenizer.encode(" joy happy laugh smile delight", return_tensors="pt")[0]
863
+ tok_b = state.tokenizer.encode(" sad cry frown depress grief", return_tensors="pt")[0]
864
+ else:
865
+ tok_t = state.tokenizer.encode(" random", return_tensors="pt")[0]
866
+ tok_b = state.tokenizer.encode(" neutral", return_tensors="pt")[0]
867
+
868
+ emb_t = state.model.transformer.wte(tok_t.to(state.device)).mean(dim=0)
869
+ emb_b = state.model.transformer.wte(tok_b.to(state.device)).mean(dim=0)
870
+ sv = (emb_t - emb_b) * req.intensity * 2.0
871
+
872
+ def steer_hook(module, inputs, output):
873
+ hidden_states = output[0] if isinstance(output, tuple) else output
874
+ h = hidden_states + sv
875
+ return (h,) + output[1:] if isinstance(output, tuple) else h
876
+ hook_handle = state.model.transformer.h[6].register_forward_hook(steer_hook)
877
+
878
+ # Generate tokens one at a time to capture per-token logit lens
879
+ generated_ids = input_ids.clone()
880
+ logit_lens_data = []
881
+ attention_saliency = []
882
+
883
+ with torch.no_grad():
884
+ for step in range(req.max_tokens):
885
+ outputs = state.model(generated_ids, output_attentions=True, output_hidden_states=True)
886
+ next_logits = outputs.logits[0, -1, :]
887
+ next_token_id = torch.argmax(next_logits).unsqueeze(0).unsqueeze(0)
888
+
889
+ if next_token_id.item() == state.tokenizer.eos_token_id:
890
+ break
891
+
892
+ # Logit Lens: project each layer's hidden state through lm_head
893
+ layer_predictions = []
894
+ for layer_idx, hidden in enumerate(outputs.hidden_states[1:]): # skip embedding layer
895
+ layer_logits = state.model.lm_head(hidden[0, -1, :])
896
+ layer_probs = F.softmax(layer_logits, dim=-1)
897
+ top_prob, top_id = torch.topk(layer_probs, 1)
898
+ layer_predictions.append({
899
+ "layer": layer_idx,
900
+ "token": state.tokenizer.decode([top_id[0].item()]),
901
+ "probability": round(top_prob[0].item(), 4)
902
+ })
903
+
904
+ logit_lens_data.append({
905
+ "generated_token": state.tokenizer.decode([next_token_id.item()]),
906
+ "layers": layer_predictions
907
+ })
908
+
909
+ # Attention Saliency: average attention from last position to all prompt positions
910
+ # Average across all layers and heads
911
+ attn_to_prompt = []
912
+ if outputs.attentions:
913
+ for layer_attn in outputs.attentions:
914
+ # shape: [1, num_heads, seq_len, seq_len]
915
+ # Get attention from last token to all positions, average across heads
916
+ last_token_attn = layer_attn[0, :, -1, :prompt_len].mean(dim=0) # [prompt_len]
917
+ attn_to_prompt.append(last_token_attn)
918
+
919
+ if attn_to_prompt:
920
+ avg_attn = torch.stack(attn_to_prompt).mean(dim=0) # [prompt_len]
921
+ # Normalize
922
+ if avg_attn.sum() > 0:
923
+ avg_attn = avg_attn / avg_attn.sum()
924
+ attention_saliency.append(avg_attn.tolist())
925
+ else:
926
+ attention_saliency.append([0.0]*prompt_len)
927
+
928
+ generated_ids = torch.cat([generated_ids, next_token_id], dim=1)
929
+
930
+ if hook_handle:
931
+ hook_handle.remove()
932
+ state.engine.restore_heads()
933
+
934
+ full_text = state.tokenizer.decode(generated_ids[0], skip_special_tokens=True)
935
+ response_text = full_text[len(req.prompt):]
936
+ response_tokens = [state.tokenizer.decode([t]) for t in generated_ids[0, prompt_len:]]
937
+
938
+ return {
939
+ "prompt": req.prompt,
940
+ "response": response_text,
941
+ "prompt_tokens": prompt_tokens,
942
+ "response_tokens": response_tokens,
943
+ "logit_lens": logit_lens_data,
944
+ "attention_saliency": attention_saliency,
945
+ "ablations": [{"layer": ab.layer, "head": ab.head} for ab in req.ablations]
946
+ }
947
+
948
+ # ── Auto-Ablation Circuit Scanner ──
949
+
950
+ class CircuitScanRequest(BaseModel):
951
+ prompt: str
952
+
953
+ @app.post("/api/transformer/scan_circuit")
954
+ def scan_circuit(req: CircuitScanRequest):
955
+ """Find the 3 most causally important attention heads by measuring KL-divergence."""
956
+ state = get_language_state()
957
+
958
+ with ablation_lock:
959
+ state.engine.clear_hooks()
960
+ inputs = state.tokenizer(req.prompt, return_tensors="pt")
961
+
962
+ # Get baseline logits
963
+ with torch.no_grad():
964
+ baseline_out = state.model(**inputs)
965
+ baseline_logits = baseline_out.logits[0, -1, :]
966
+ baseline_probs = F.softmax(baseline_logits, dim=-1)
967
+
968
+ head_impacts = []
969
+
970
+ for layer_idx in range(12):
971
+ for head_idx in range(12):
972
+ state.engine.restore_heads()
973
+ state.engine.ablate_heads([(layer_idx, head_idx)])
974
+
975
+ with torch.no_grad():
976
+ ablated_out = state.model(**inputs)
977
+ ablated_logits = ablated_out.logits[0, -1, :]
978
+ ablated_log_probs = F.log_softmax(ablated_logits, dim=-1)
979
+
980
+ kl_div = F.kl_div(ablated_log_probs, baseline_probs, reduction='sum', log_target=False).item()
981
+
982
+ head_impacts.append({
983
+ "layer": layer_idx,
984
+ "head": head_idx,
985
+ "kl_divergence": round(abs(kl_div), 6)
986
+ })
987
+
988
+ state.engine.restore_heads()
989
+
990
+ # Sort by KL divergence (highest = most important)
991
+ head_impacts.sort(key=lambda x: x["kl_divergence"], reverse=True)
992
+
993
+ return {
994
+ "prompt": req.prompt,
995
+ "top_heads": head_impacts[:5],
996
+ "all_heads": head_impacts
997
+ }
998
+
999
+ # ── AUDIO / SPEECH GENERATION (SpeechT5) ──
1000
+ def get_audio_model():
1001
+ with ablation_lock:
1002
+ if AudioState.model is None:
1003
+ print("Loading SpeechT5 audio model...")
1004
+ AudioState.device = "cpu"
1005
+ processor = SpeechT5Processor.from_pretrained("microsoft/speecht5_tts")
1006
+ model = SpeechT5ForTextToSpeech.from_pretrained("microsoft/speecht5_tts").to(AudioState.device)
1007
+ vocoder = SpeechT5HifiGan.from_pretrained("microsoft/speecht5_hifigan").to(AudioState.device)
1008
+
1009
+ # Load a default speaker embedding
1010
+ try:
1011
+ embeddings_dataset = load_dataset("Matthijs/cmu-arctic-xvectors", split="validation", trust_remote_code=True)
1012
+ speaker_embeddings = torch.tensor(embeddings_dataset[7306]["xvector"]).unsqueeze(0).to(AudioState.device)
1013
+ except Exception as e:
1014
+ print(f"Failed to load speaker embeddings from dataset, using fallback. Error: {e}")
1015
+ speaker_embeddings = torch.randn(1, 512).to(AudioState.device) # Fallback if dataset download fails
1016
+
1017
+ AudioState.model = model
1018
+ AudioState.processor = processor
1019
+ AudioState.vocoder = vocoder
1020
+ AudioState.speaker_embeddings = speaker_embeddings
1021
+ AudioState.engine = InstrumentationEngine(model)
1022
+
1023
+ return AudioState
1024
+
1025
+ class AudioRequest(BaseModel):
1026
+ prompt: str
1027
+ ablations: list = [] # List of dicts e.g. [{"layer": 2}]
1028
+
1029
+ @app.post("/api/experiment/audio")
1030
+ def generate_audio(req: AudioRequest):
1031
+ with ablation_lock:
1032
+ state = get_audio_model()
1033
+
1034
+ inputs = state.processor(text=req.prompt, return_tensors="pt").to(state.device)
1035
+
1036
+ state.engine.clear_hooks()
1037
+
1038
+ # Apply ablation hooks
1039
+ for ab in req.ablations:
1040
+ layer_idx = ab.get("layer", 0)
1041
+ hook_name = f"speecht5.decoder.wrapped_decoder.layers.{layer_idx}.feed_forward"
1042
+
1043
+ def zero_hook(module, inputs, output):
1044
+ if isinstance(output, tuple):
1045
+ return (torch.zeros_like(output[0]),) + output[1:]
1046
+ return torch.zeros_like(output)
1047
+
1048
+ try:
1049
+ layer = state.engine._get_layer_by_name(hook_name)
1050
+ handle = layer.register_forward_hook(zero_hook)
1051
+ state.engine.hooks.append(handle)
1052
+ except Exception as e:
1053
+ print(f"Warning: Could not hook layer {hook_name}. {e}")
1054
+
1055
+ with torch.no_grad():
1056
+ speech = state.model.generate_speech(inputs["input_ids"], state.speaker_embeddings, vocoder=state.vocoder)
1057
+
1058
+ state.engine.clear_hooks()
1059
+
1060
+ # Convert to WAV in memory
1061
+ speech_np = speech.cpu().numpy()
1062
+ wav_io = io.BytesIO()
1063
+ sf.write(wav_io, speech_np, samplerate=16000, format='WAV', subtype='PCM_16')
1064
+ wav_io.seek(0)
1065
+ audio_b64 = base64.b64encode(wav_io.read()).decode("utf-8")
1066
+
1067
+ # Downsample waveform for visualization
1068
+ chunk_size = max(1, len(speech_np) // 200)
1069
+ waveform_data = [float(np.mean(np.abs(speech_np[i:i+chunk_size]))) for i in range(0, len(speech_np), chunk_size)]
1070
+
1071
+ return {
1072
+ "audio_b64": audio_b64,
1073
+ "waveform": waveform_data
1074
+ }
hf_upload.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ try:
5
+ from huggingface_hub import HfApi
6
+ except ImportError:
7
+ import subprocess
8
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "huggingface_hub"])
9
+ from huggingface_hub import HfApi
10
+
11
+ token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
12
+ if not token:
13
+ raise RuntimeError("Set HF_TOKEN or HUGGINGFACE_HUB_TOKEN before uploading.")
14
+
15
+ api = HfApi(token=token)
16
+ repo_id = "Pratham0100/BrainBox-Backend"
17
+
18
+ files_to_upload = [
19
+ "app.py",
20
+ "requirements.txt",
21
+ "Dockerfile",
22
+ "pyproject.toml",
23
+ "README.md"
24
+ ]
25
+ folders_to_upload = [
26
+ "backend",
27
+ "src"
28
+ ]
29
+
30
+ print("Uploading files to Hugging Face...")
31
+
32
+ for file in files_to_upload:
33
+ if os.path.exists(file):
34
+ print(f"Uploading {file}...")
35
+ api.upload_file(
36
+ path_or_fileobj=file,
37
+ path_in_repo=file,
38
+ repo_id=repo_id,
39
+ repo_type="space",
40
+ token=token
41
+ )
42
+
43
+ for folder in folders_to_upload:
44
+ if os.path.exists(folder):
45
+ print(f"Uploading folder {folder}...")
46
+ api.upload_folder(
47
+ folder_path=folder,
48
+ path_in_repo=folder,
49
+ repo_id=repo_id,
50
+ repo_type="space",
51
+ token=token
52
+ )
53
+
54
+ print("Upload to Hugging Face Spaces completed successfully!")
55
+
56
+ try:
57
+ print("Forcing hardware downgrade to CPU Basic...")
58
+ api.request_space_hardware(repo_id=repo_id, hardware="cpu-basic")
59
+ print("Hardware downgraded successfully!")
60
+ except Exception as e:
61
+ print(f"Hardware downgrade failed: {e}")
requirements.txt CHANGED
@@ -14,4 +14,5 @@ scikit-learn>=1.3.0
14
  pandas>=2.1.0
15
  plotly>=5.18.0
16
  safetensors>=0.4.2
17
- spaces>=0.31.0
 
 
14
  pandas>=2.1.0
15
  plotly>=5.18.0
16
  safetensors>=0.4.2
17
+ gradio==5.13.0
18
+ spaces>=0.31.0
ruff.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [lint]
2
+ ignore = ['RUF012', 'BLE001']
run.bat ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo ==========================================
3
+ echo Starting Neural Archaeology Platform
4
+ echo ==========================================
5
+
6
+ echo Starting FastAPI Backend (Port 8000)...
7
+ start "Neural Archaeology Backend" cmd /k ".\.venv\Scripts\activate.bat && set PYTHONPATH=src && uvicorn backend.main:app --reload"
8
+
9
+
10
+ echo Starting Vite Frontend (Port 5173)...
11
+ start "Neural Archaeology Frontend" cmd /k "cd frontend && npm run dev"
12
+
13
+ echo Waiting for servers to boot...
14
+ ping 127.0.0.1 -n 5 > nul
15
+
16
+ echo Opening dashboard in your default browser...
17
+ start http://localhost:5173
18
+
19
+ echo Done! You can close this window. The servers will remain running in their own terminal windows.
src/neural_archaeology/__pycache__/__init__.cpython-313.pyc DELETED
Binary file (157 Bytes)
 
src/neural_archaeology/analysis/__pycache__/__init__.cpython-313.pyc DELETED
Binary file (166 Bytes)
 
src/neural_archaeology/analysis/__pycache__/ablation.cpython-313.pyc DELETED
Binary file (3.96 kB)
 
src/neural_archaeology/analysis/__pycache__/probing.cpython-313.pyc DELETED
Binary file (4.32 kB)
 
src/neural_archaeology/analysis/__pycache__/selectivity.cpython-313.pyc DELETED
Binary file (3.38 kB)
 
src/neural_archaeology/analysis/__pycache__/similarity.cpython-313.pyc DELETED
Binary file (2.51 kB)
 
src/neural_archaeology/analysis/__pycache__/top_k.cpython-313.pyc DELETED
Binary file (4.12 kB)
 
src/neural_archaeology/analysis/__pycache__/visualization.cpython-313.pyc DELETED
Binary file (4.87 kB)
 
src/neural_archaeology/analysis/visualization.py CHANGED
@@ -44,8 +44,10 @@ class FeatureVisualizer:
44
 
45
  handle = target_layer.register_forward_hook(grad_hook)
46
 
47
- # ResNet expects 224x224
48
- image_tensor = torch.randn((1, 3, 224, 224), device=device) * 0.01
 
 
49
  image_tensor = image_tensor.requires_grad_(True)
50
 
51
  optimizer = optim.Adam([image_tensor], lr=lr, weight_decay=1e-6)
 
44
 
45
  handle = target_layer.register_forward_hook(grad_hook)
46
 
47
+ # ResNet accepts smaller spatial inputs. Starting at 96px makes the
48
+ # interactive feature-visualization endpoint finish on CPU Spaces;
49
+ # the result is enlarged for display below.
50
+ image_tensor = torch.randn((1, 3, 96, 96), device=device) * 0.01
51
  image_tensor = image_tensor.requires_grad_(True)
52
 
53
  optimizer = optim.Adam([image_tensor], lr=lr, weight_decay=1e-6)
src/neural_archaeology/data/__pycache__/__init__.cpython-313.pyc DELETED
Binary file (162 Bytes)
 
src/neural_archaeology/data/__pycache__/cifar.cpython-313.pyc DELETED
Binary file (1.87 kB)
 
src/neural_archaeology/instrumentation/__pycache__/__init__.cpython-313.pyc DELETED
Binary file (173 Bytes)
 
src/neural_archaeology/instrumentation/__pycache__/hooks.cpython-313.pyc DELETED
Binary file (4 kB)
 
src/neural_archaeology/instrumentation/__pycache__/transformer_engine.cpython-313.pyc DELETED
Binary file (3.13 kB)
 
src/neural_archaeology/models/__init__.py DELETED
File without changes
src/neural_archaeology/models/__pycache__/__init__.cpython-313.pyc DELETED
Binary file (164 Bytes)
 
src/neural_archaeology/models/__pycache__/cnn_small.cpython-313.pyc DELETED
Binary file (2.82 kB)
 
src/neural_archaeology/models/__pycache__/registry.cpython-313.pyc DELETED
Binary file (2.13 kB)
 
src/neural_archaeology/models/cnn_small.py DELETED
@@ -1,40 +0,0 @@
1
- from torch import nn
2
-
3
-
4
- class SmallCNN(nn.Module):
5
- """
6
- A small CNN designed specifically for interpretability research.
7
- It is simple enough to understand deeply but complex enough to learn meaningful features on CIFAR-10.
8
- """
9
- def __init__(self, num_classes=10):
10
- super().__init__()
11
-
12
- # Block 1
13
- self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
14
- self.relu1 = nn.ReLU()
15
- self.pool1 = nn.MaxPool2d(2)
16
-
17
- # Block 2
18
- self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
19
- self.relu2 = nn.ReLU()
20
- self.pool2 = nn.MaxPool2d(2)
21
-
22
- # Block 3
23
- self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
24
- self.relu3 = nn.ReLU()
25
- self.pool3 = nn.MaxPool2d(2)
26
-
27
- # Classifier
28
- self.flatten = nn.Flatten()
29
- self.fc1 = nn.Linear(128 * 4 * 4, 256)
30
- self.relu4 = nn.ReLU()
31
- self.fc2 = nn.Linear(256, num_classes)
32
-
33
- def forward(self, x):
34
- x = self.pool1(self.relu1(self.conv1(x)))
35
- x = self.pool2(self.relu2(self.conv2(x)))
36
- x = self.pool3(self.relu3(self.conv3(x)))
37
- x = self.flatten(x)
38
- x = self.relu4(self.fc1(x))
39
- x = self.fc2(x)
40
- return x
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/neural_archaeology/models/registry.py DELETED
@@ -1,34 +0,0 @@
1
- from typing import Any
2
-
3
- import torchvision.models as torchvision_models
4
- from torch import nn
5
-
6
- from .cnn_small import SmallCNN
7
-
8
-
9
- class ModelRegistry:
10
- """
11
- Centralized registry for all architectures supported by Neural Archaeology.
12
- Allows easy loading and instantiation by string name in experiment configurations.
13
- """
14
-
15
- _models: dict[str, Any] = {
16
- "cnn_small": SmallCNN,
17
- "resnet18": lambda **kwargs: torchvision_models.resnet18(weights=None, **kwargs),
18
- }
19
-
20
- @classmethod
21
- def register(cls, name: str, model_class: type[nn.Module]):
22
- """Dynamically register a new architecture."""
23
- cls._models[name] = model_class
24
-
25
- @classmethod
26
- def get_model(cls, name: str, **kwargs) -> nn.Module:
27
- """Instantiate a model by name with given kwargs."""
28
- if name not in cls._models:
29
- raise ValueError(f"Model '{name}' not found. Available: {list(cls._models.keys())}")
30
- target = cls._models[name]
31
- if callable(target):
32
- return target(**kwargs)
33
- return target(**kwargs)
34
-