theapemachine commited on
Commit
91d8295
·
verified ·
1 Parent(s): a204781

Add cortex/core.py — CortexModule, CortexSurgeon, CortexConfig

Browse files
Files changed (1) hide show
  1. cortex/core.py +493 -0
cortex/core.py ADDED
@@ -0,0 +1,493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core abstractions for Cortex layer surgery.
3
+
4
+ CortexModule: Protocol for all plug-in modules.
5
+ CortexSurgeon: Orchestrator that injects/removes modules into a pretrained model.
6
+ CortexConfig: Configuration for which modules go where.
7
+ """
8
+
9
+ import torch
10
+ import torch.nn as nn
11
+ from abc import ABC, abstractmethod
12
+ from dataclasses import dataclass, field
13
+ from typing import Dict, List, Optional, Tuple, Any, Callable, Union
14
+ from enum import Enum
15
+ import copy
16
+ import logging
17
+
18
+ logger = logging.getLogger("cortex")
19
+
20
+
21
+ class InjectionPoint(Enum):
22
+ """Where in the transformer block a module can be injected."""
23
+ PRE_ATTENTION = "pre_attention" # Before self-attention
24
+ POST_ATTENTION = "post_attention" # After self-attention, before FFN
25
+ PRE_FFN = "pre_ffn" # Before FFN (same as post_attention for standard transformers)
26
+ POST_FFN = "post_ffn" # After FFN, after residual add
27
+ RESIDUAL_STREAM = "residual_stream" # Direct access to the residual stream (wraps entire block)
28
+ FULL_BLOCK_WRAP = "full_block_wrap" # Wraps the entire transformer block
29
+
30
+
31
+ class CortexModule(ABC, nn.Module):
32
+ """
33
+ Base class for all Cortex plug-in modules.
34
+
35
+ Each module must declare:
36
+ - injection_point: Where in the transformer block it operates
37
+ - target_layers: Which layers to inject into (list of ints or "all")
38
+
39
+ And implement:
40
+ - forward(): Takes hidden states + optional kwargs, returns modified hidden states
41
+ - on_inject(): Called once when the module is injected (for initialization from activations)
42
+ - on_remove(): Called when the module is removed
43
+ """
44
+
45
+ def __init__(self, injection_point: InjectionPoint, target_layers: Union[List[int], str] = "all"):
46
+ super().__init__()
47
+ self.injection_point = injection_point
48
+ self.target_layers = target_layers
49
+ self._original_modules: Dict[str, nn.Module] = {}
50
+ self._hooks: List[torch.utils.hooks.RemovableHook] = []
51
+ self._active = True
52
+
53
+ @abstractmethod
54
+ def forward(
55
+ self,
56
+ hidden_states: torch.Tensor,
57
+ layer_idx: int,
58
+ attention_mask: Optional[torch.Tensor] = None,
59
+ position_ids: Optional[torch.Tensor] = None,
60
+ past_key_values: Optional[Any] = None,
61
+ **kwargs
62
+ ) -> torch.Tensor:
63
+ """
64
+ Process hidden states.
65
+
66
+ Args:
67
+ hidden_states: [batch, seq_len, hidden_dim]
68
+ layer_idx: Which transformer layer this is being called from
69
+ attention_mask: Optional attention mask
70
+ position_ids: Optional position ids
71
+ past_key_values: Optional KV cache
72
+ **kwargs: Additional model-specific arguments
73
+
74
+ Returns:
75
+ Modified hidden_states [batch, seq_len, hidden_dim]
76
+ """
77
+ ...
78
+
79
+ def on_inject(self, model: nn.Module, layer_idx: int, hidden_dim: int):
80
+ """Called once when the module is injected. Use for calibration-based initialization."""
81
+ pass
82
+
83
+ def on_remove(self, model: nn.Module, layer_idx: int):
84
+ """Called when the module is removed. Clean up hooks, restore originals."""
85
+ for hook in self._hooks:
86
+ hook.remove()
87
+ self._hooks.clear()
88
+
89
+ def enable(self):
90
+ self._active = True
91
+
92
+ def disable(self):
93
+ self._active = False
94
+
95
+ @property
96
+ def is_active(self):
97
+ return self._active
98
+
99
+ def extra_repr(self):
100
+ return f"injection={self.injection_point.value}, layers={self.target_layers}, active={self._active}"
101
+
102
+
103
+ @dataclass
104
+ class CortexConfig:
105
+ """Configuration for a Cortex surgery session."""
106
+ modules: Dict[str, Dict] = field(default_factory=dict)
107
+ # Each entry: {"module_class": "MemoryBank", "params": {...}, "target_layers": [0,1,2,...]}
108
+ freeze_base_model: bool = True
109
+ gradient_checkpointing: bool = False
110
+
111
+ def add_module(self, name: str, module_class: str, target_layers: Union[List[int], str] = "all", **params):
112
+ self.modules[name] = {
113
+ "module_class": module_class,
114
+ "params": params,
115
+ "target_layers": target_layers,
116
+ }
117
+ return self
118
+
119
+
120
+ class TransformerBlockWrapper(nn.Module):
121
+ """
122
+ Wraps a transformer block to intercept activations at specific injection points.
123
+ Multiple CortexModules can be attached at different injection points.
124
+ """
125
+
126
+ def __init__(self, original_block: nn.Module, layer_idx: int, model_type: str = "llama"):
127
+ super().__init__()
128
+ self.original_block = original_block
129
+ self.layer_idx = layer_idx
130
+ self.model_type = model_type
131
+
132
+ # Modules organized by injection point
133
+ self.pre_attention_modules: nn.ModuleList = nn.ModuleList()
134
+ self.post_attention_modules: nn.ModuleList = nn.ModuleList()
135
+ self.pre_ffn_modules: nn.ModuleList = nn.ModuleList()
136
+ self.post_ffn_modules: nn.ModuleList = nn.ModuleList()
137
+ self.residual_modules: nn.ModuleList = nn.ModuleList()
138
+
139
+ def add_module_at(self, module: CortexModule):
140
+ """Add a CortexModule at its declared injection point."""
141
+ point = module.injection_point
142
+ if point == InjectionPoint.PRE_ATTENTION:
143
+ self.pre_attention_modules.append(module)
144
+ elif point == InjectionPoint.POST_ATTENTION:
145
+ self.post_attention_modules.append(module)
146
+ elif point == InjectionPoint.PRE_FFN:
147
+ self.pre_ffn_modules.append(module)
148
+ elif point == InjectionPoint.POST_FFN:
149
+ self.post_ffn_modules.append(module)
150
+ elif point in (InjectionPoint.RESIDUAL_STREAM, InjectionPoint.FULL_BLOCK_WRAP):
151
+ self.residual_modules.append(module)
152
+
153
+ def forward(self, hidden_states, **kwargs):
154
+ """
155
+ Execute the transformer block with all injected modules.
156
+
157
+ Supports LLaMA/Mistral/Qwen/Gemma-style blocks where:
158
+ - self_attn is the attention sublayer
159
+ - mlp is the FFN sublayer
160
+ - input_layernorm / post_attention_layernorm are the norms
161
+ """
162
+ # === RESIDUAL STREAM MODULES (operate on the whole block) ===
163
+ for mod in self.residual_modules:
164
+ if mod.is_active:
165
+ hidden_states = mod(hidden_states, layer_idx=self.layer_idx, **kwargs)
166
+
167
+ # === PRE-ATTENTION ===
168
+ for mod in self.pre_attention_modules:
169
+ if mod.is_active:
170
+ hidden_states = mod(hidden_states, layer_idx=self.layer_idx, **kwargs)
171
+
172
+ # === ORIGINAL BLOCK FORWARD ===
173
+ outputs = self.original_block(hidden_states, **kwargs)
174
+
175
+ # outputs is typically (hidden_states, ...) — extract hidden states
176
+ if isinstance(outputs, tuple):
177
+ hidden_states_out = outputs[0]
178
+ rest = outputs[1:]
179
+ else:
180
+ hidden_states_out = outputs
181
+ rest = ()
182
+
183
+ # === POST-FFN ===
184
+ for mod in self.post_ffn_modules:
185
+ if mod.is_active:
186
+ hidden_states_out = mod(hidden_states_out, layer_idx=self.layer_idx, **kwargs)
187
+
188
+ if rest:
189
+ return (hidden_states_out,) + rest
190
+ return hidden_states_out
191
+
192
+
193
+ class CortexSurgeon:
194
+ """
195
+ Main orchestrator for performing layer surgery on pretrained models.
196
+
197
+ Usage:
198
+ model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.2-1B")
199
+ surgeon = CortexSurgeon(model)
200
+
201
+ # Add modules
202
+ surgeon.add_module("memory", MemoryBank(hidden_dim=2048, num_slots=64), target_layers=[8,9,10,11])
203
+ surgeon.add_module("halluc_gate", HallucinationGate(hidden_dim=2048), target_layers="all")
204
+
205
+ # Perform surgery
206
+ surgeon.operate()
207
+
208
+ # Model now has enhanced capabilities
209
+ outputs = model(**inputs)
210
+ """
211
+
212
+ def __init__(self, model: nn.Module, model_type: str = "auto"):
213
+ self.model = model
214
+ self.model_type = self._detect_model_type(model) if model_type == "auto" else model_type
215
+ self.modules: Dict[str, Tuple[CortexModule, Union[List[int], str]]] = {}
216
+ self._wrappers: Dict[int, TransformerBlockWrapper] = {}
217
+ self._hooks: List[torch.utils.hooks.RemovableHook] = []
218
+ self._operated = False
219
+
220
+ # Detect model architecture
221
+ self.hidden_dim = self._get_hidden_dim()
222
+ self.num_layers = self._get_num_layers()
223
+ self.layers_accessor = self._get_layers_accessor()
224
+
225
+ logger.info(f"CortexSurgeon initialized: model_type={self.model_type}, "
226
+ f"hidden_dim={self.hidden_dim}, num_layers={self.num_layers}")
227
+
228
+ def _detect_model_type(self, model: nn.Module) -> str:
229
+ """Auto-detect the model architecture."""
230
+ class_name = model.__class__.__name__.lower()
231
+ if "llama" in class_name:
232
+ return "llama"
233
+ elif "mistral" in class_name:
234
+ return "mistral"
235
+ elif "qwen" in class_name:
236
+ return "qwen2"
237
+ elif "gemma" in class_name:
238
+ return "gemma"
239
+ elif "gpt2" in class_name or "gptneo" in class_name:
240
+ return "gpt2"
241
+ elif "phi" in class_name:
242
+ return "phi"
243
+ else:
244
+ if hasattr(model, "config"):
245
+ mt = getattr(model.config, "model_type", "").lower()
246
+ if mt:
247
+ return mt
248
+ return "llama"
249
+
250
+ def _get_hidden_dim(self) -> int:
251
+ """Get the hidden dimension of the model."""
252
+ if hasattr(self.model, "config"):
253
+ return self.model.config.hidden_size
254
+ raise ValueError("Cannot detect hidden_dim — model has no config")
255
+
256
+ def _get_num_layers(self) -> int:
257
+ """Get the number of transformer layers."""
258
+ if hasattr(self.model, "config"):
259
+ return self.model.config.num_hidden_layers
260
+ raise ValueError("Cannot detect num_layers — model has no config")
261
+
262
+ def _get_layers_accessor(self) -> str:
263
+ """Get the attribute path to the list of transformer layers."""
264
+ for path in ["model.layers", "transformer.h", "gpt_neox.layers", "model.decoder.layers"]:
265
+ obj = self.model
266
+ try:
267
+ for attr in path.split("."):
268
+ obj = getattr(obj, attr)
269
+ if hasattr(obj, "__len__") and len(obj) > 0:
270
+ return path
271
+ except AttributeError:
272
+ continue
273
+ raise ValueError("Cannot find transformer layers in model")
274
+
275
+ def _get_layers(self) -> nn.ModuleList:
276
+ """Access the transformer layers."""
277
+ obj = self.model
278
+ for attr in self.layers_accessor.split("."):
279
+ obj = getattr(obj, attr)
280
+ return obj
281
+
282
+ def _resolve_target_layers(self, target: Union[List[int], str]) -> List[int]:
283
+ """Resolve 'all' to a list of layer indices, or pass through a list."""
284
+ if target == "all":
285
+ return list(range(self.num_layers))
286
+ elif isinstance(target, str) and target == "middle":
287
+ start = self.num_layers // 3
288
+ end = 2 * self.num_layers // 3
289
+ return list(range(start, end))
290
+ elif isinstance(target, str) and target == "deep":
291
+ start = 2 * self.num_layers // 3
292
+ return list(range(start, self.num_layers))
293
+ return list(target)
294
+
295
+ def add_module(
296
+ self,
297
+ name: str,
298
+ module: CortexModule,
299
+ target_layers: Optional[Union[List[int], str]] = None
300
+ ):
301
+ """
302
+ Register a CortexModule for injection.
303
+
304
+ Args:
305
+ name: Unique name for this module instance
306
+ module: The CortexModule to inject
307
+ target_layers: Override the module's target_layers if provided
308
+ """
309
+ if target_layers is not None:
310
+ module.target_layers = target_layers
311
+ self.modules[name] = module
312
+ logger.info(f"Registered module '{name}': {module.__class__.__name__} "
313
+ f"at {module.injection_point.value} for layers {module.target_layers}")
314
+
315
+ def operate(self, freeze_base: bool = True):
316
+ """
317
+ Perform the surgery — inject all registered modules into the model.
318
+
319
+ Args:
320
+ freeze_base: If True, freeze all original model parameters (only cortex modules train)
321
+ """
322
+ if self._operated:
323
+ raise RuntimeError("Surgery already performed. Create a new CortexSurgeon to modify further.")
324
+
325
+ layers = self._get_layers()
326
+
327
+ if freeze_base:
328
+ for param in self.model.parameters():
329
+ param.requires_grad = False
330
+ logger.info("Froze all base model parameters")
331
+
332
+ for name, module in self.modules.items():
333
+ target_layer_idxs = self._resolve_target_layers(module.target_layers)
334
+
335
+ for layer_idx in target_layer_idxs:
336
+ if layer_idx >= len(layers):
337
+ logger.warning(f"Layer {layer_idx} out of range (model has {len(layers)} layers), skipping")
338
+ continue
339
+
340
+ layer = layers[layer_idx]
341
+ module.on_inject(self.model, layer_idx, self.hidden_dim)
342
+ hook = self._create_hook(module, layer, layer_idx)
343
+ if hook is not None:
344
+ self._hooks.append(hook)
345
+
346
+ device = next(self.model.parameters()).device
347
+ module.to(device)
348
+
349
+ for param in module.parameters():
350
+ param.requires_grad = True
351
+
352
+ logger.info(f"Injected '{name}' into layers {target_layer_idxs}")
353
+
354
+ self._operated = True
355
+
356
+ total_params = sum(p.numel() for p in self.model.parameters())
357
+ cortex_params = sum(
358
+ sum(p.numel() for p in m.parameters())
359
+ for m in self.modules.values()
360
+ )
361
+ logger.info(f"Surgery complete: {cortex_params:,} Cortex params / {total_params:,} total "
362
+ f"({cortex_params/total_params*100:.2f}% overhead)")
363
+
364
+ return self
365
+
366
+ def _create_hook(self, module: CortexModule, layer: nn.Module, layer_idx: int):
367
+ """Create and register the appropriate hook for a module's injection point."""
368
+ point = module.injection_point
369
+
370
+ if point == InjectionPoint.POST_FFN:
371
+ def post_ffn_hook(mod, inp, output, _module=module, _layer_idx=layer_idx):
372
+ if not _module.is_active:
373
+ return output
374
+ if isinstance(output, tuple):
375
+ hidden = output[0]
376
+ hidden = _module(hidden, layer_idx=_layer_idx)
377
+ return (hidden,) + output[1:]
378
+ return _module(output, layer_idx=_layer_idx)
379
+ return layer.register_forward_hook(post_ffn_hook)
380
+
381
+ elif point == InjectionPoint.PRE_ATTENTION:
382
+ def pre_attn_hook(mod, inp, _module=module, _layer_idx=layer_idx):
383
+ if not _module.is_active:
384
+ return inp
385
+ if isinstance(inp, tuple):
386
+ hidden = inp[0]
387
+ hidden = _module(hidden, layer_idx=_layer_idx)
388
+ return (hidden,) + inp[1:]
389
+ return (_module(inp, layer_idx=_layer_idx),)
390
+ return layer.register_forward_pre_hook(pre_attn_hook)
391
+
392
+ elif point == InjectionPoint.POST_ATTENTION:
393
+ attn = self._get_attention_sublayer(layer)
394
+ if attn is not None:
395
+ def post_attn_hook(mod, inp, output, _module=module, _layer_idx=layer_idx):
396
+ if not _module.is_active:
397
+ return output
398
+ if isinstance(output, tuple):
399
+ hidden = output[0]
400
+ hidden = _module(hidden, layer_idx=_layer_idx)
401
+ return (hidden,) + output[1:]
402
+ return _module(output, layer_idx=_layer_idx)
403
+ return attn.register_forward_hook(post_attn_hook)
404
+
405
+ elif point == InjectionPoint.PRE_FFN:
406
+ ffn = self._get_ffn_sublayer(layer)
407
+ if ffn is not None:
408
+ def pre_ffn_hook(mod, inp, _module=module, _layer_idx=layer_idx):
409
+ if not _module.is_active:
410
+ return inp
411
+ if isinstance(inp, tuple):
412
+ hidden = inp[0]
413
+ hidden = _module(hidden, layer_idx=_layer_idx)
414
+ return (hidden,) + inp[1:]
415
+ return (_module(inp, layer_idx=_layer_idx),)
416
+ return ffn.register_forward_pre_hook(pre_ffn_hook)
417
+
418
+ elif point == InjectionPoint.RESIDUAL_STREAM:
419
+ def residual_hook(mod, inp, output, _module=module, _layer_idx=layer_idx):
420
+ if not _module.is_active:
421
+ return output
422
+ if isinstance(output, tuple):
423
+ hidden = output[0]
424
+ hidden = _module(hidden, layer_idx=_layer_idx)
425
+ return (hidden,) + output[1:]
426
+ return _module(output, layer_idx=_layer_idx)
427
+ return layer.register_forward_hook(residual_hook)
428
+
429
+ return None
430
+
431
+ def _get_attention_sublayer(self, layer: nn.Module) -> Optional[nn.Module]:
432
+ """Get the attention sublayer of a transformer block."""
433
+ for name in ["self_attn", "attn", "attention"]:
434
+ if hasattr(layer, name):
435
+ return getattr(layer, name)
436
+ return None
437
+
438
+ def _get_ffn_sublayer(self, layer: nn.Module) -> Optional[nn.Module]:
439
+ """Get the FFN sublayer of a transformer block."""
440
+ for name in ["mlp", "feed_forward", "ffn"]:
441
+ if hasattr(layer, name):
442
+ return getattr(layer, name)
443
+ return None
444
+
445
+ def remove_all(self):
446
+ """Remove all injected modules and restore the original model."""
447
+ for hook in self._hooks:
448
+ hook.remove()
449
+ self._hooks.clear()
450
+
451
+ for name, module in self.modules.items():
452
+ target_layers = self._resolve_target_layers(module.target_layers)
453
+ for layer_idx in target_layers:
454
+ module.on_remove(self.model, layer_idx)
455
+
456
+ self.modules.clear()
457
+ self._operated = False
458
+ logger.info("All Cortex modules removed, model restored")
459
+
460
+ def get_trainable_parameters(self):
461
+ """Yield only the Cortex module parameters (for optimizer)."""
462
+ for module in self.modules.values():
463
+ yield from module.parameters()
464
+
465
+ def get_parameter_report(self) -> Dict:
466
+ """Get a detailed report of parameter counts per module."""
467
+ report = {}
468
+ for name, module in self.modules.items():
469
+ params = sum(p.numel() for p in module.parameters())
470
+ trainable = sum(p.numel() for p in module.parameters() if p.requires_grad)
471
+ report[name] = {"total": params, "trainable": trainable}
472
+ return report
473
+
474
+ def save_cortex_modules(self, path: str):
475
+ """Save only the Cortex module weights."""
476
+ state = {}
477
+ for name, module in self.modules.items():
478
+ state[name] = {
479
+ "class": module.__class__.__name__,
480
+ "state_dict": module.state_dict(),
481
+ "injection_point": module.injection_point.value,
482
+ "target_layers": module.target_layers,
483
+ }
484
+ torch.save(state, path)
485
+ logger.info(f"Saved Cortex modules to {path}")
486
+
487
+ def load_cortex_modules(self, path: str):
488
+ """Load Cortex module weights."""
489
+ state = torch.load(path, map_location="cpu")
490
+ for name, module in self.modules.items():
491
+ if name in state:
492
+ module.load_state_dict(state[name]["state_dict"])
493
+ logger.info(f"Loaded weights for '{name}'")