Commit
·
7dac8c1
1
Parent(s):
d407fda
fix
Browse files
cognitive_mapping_probe/concepts.py
CHANGED
|
@@ -18,18 +18,8 @@ def _get_last_token_hidden_state(llm: LLM, prompt: str) -> torch.Tensor:
|
|
| 18 |
outputs = llm.model(**inputs, output_hidden_states=True)
|
| 19 |
last_hidden_state = outputs.hidden_states[-1][0, -1, :].cpu()
|
| 20 |
|
| 21 |
-
# KORREKTUR:
|
| 22 |
-
|
| 23 |
-
# gegenüber API-Änderungen in `transformers`.
|
| 24 |
-
try:
|
| 25 |
-
# Versuche, die Größe über die Einbettungsschicht zu erhalten, was am stabilsten ist.
|
| 26 |
-
expected_size = llm.model.get_input_embeddings().weight.shape[1]
|
| 27 |
-
except AttributeError:
|
| 28 |
-
# Fallback, falls die Methode nicht existiert, auf den wahrscheinlichen Namen.
|
| 29 |
-
# Gemma3Config hat 'hidden_size', aber andere könnten es anders nennen.
|
| 30 |
-
expected_size = getattr(llm.config, 'hidden_size', getattr(llm.config, 'd_model', 0))
|
| 31 |
-
assert expected_size > 0, "Could not determine hidden size from model config."
|
| 32 |
-
|
| 33 |
|
| 34 |
assert last_hidden_state.shape == (expected_size,), \
|
| 35 |
f"Hidden state shape mismatch. Expected {(expected_size,)}, got {last_hidden_state.shape}"
|
|
@@ -44,7 +34,7 @@ def get_concept_vector(llm: LLM, concept: str, baseline_words: List[str] = BASEL
|
|
| 44 |
target_hs = _get_last_token_hidden_state(llm, prompt_template.format(concept))
|
| 45 |
baseline_hss = []
|
| 46 |
for word in tqdm(baseline_words, desc=f" - Calculating baseline for '{concept}'", leave=False, bar_format="{l_bar}{bar:10}{r_bar}"):
|
| 47 |
-
baseline_hss.append(_get_last_token_hidden_state(llm, prompt_template.format(word)))
|
| 48 |
assert all(hs.shape == target_hs.shape for hs in baseline_hss)
|
| 49 |
mean_baseline_hs = torch.stack(baseline_hss).mean(dim=0)
|
| 50 |
dbg(f" - Mean baseline vector computed with norm {torch.norm(mean_baseline_hs).item():.2f}")
|
|
|
|
| 18 |
outputs = llm.model(**inputs, output_hidden_states=True)
|
| 19 |
last_hidden_state = outputs.hidden_states[-1][0, -1, :].cpu()
|
| 20 |
|
| 21 |
+
# KORREKTUR: Greife auf die stabile, abstrahierte Konfiguration zu.
|
| 22 |
+
expected_size = llm.stable_config.hidden_dim
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
assert last_hidden_state.shape == (expected_size,), \
|
| 25 |
f"Hidden state shape mismatch. Expected {(expected_size,)}, got {last_hidden_state.shape}"
|
|
|
|
| 34 |
target_hs = _get_last_token_hidden_state(llm, prompt_template.format(concept))
|
| 35 |
baseline_hss = []
|
| 36 |
for word in tqdm(baseline_words, desc=f" - Calculating baseline for '{concept}'", leave=False, bar_format="{l_bar}{bar:10}{r_bar}"):
|
| 37 |
+
baseline_hss.append(_get_last_token_hidden_state(llm, prompt_template.format(word)))
|
| 38 |
assert all(hs.shape == target_hs.shape for hs in baseline_hss)
|
| 39 |
mean_baseline_hs = torch.stack(baseline_hss).mean(dim=0)
|
| 40 |
dbg(f" - Mean baseline vector computed with norm {torch.norm(mean_baseline_hs).item():.2f}")
|
cognitive_mapping_probe/llm_iface.py
CHANGED
|
@@ -4,12 +4,22 @@ import random
|
|
| 4 |
import numpy as np
|
| 5 |
from transformers import AutoModelForCausalLM, AutoTokenizer, set_seed
|
| 6 |
from typing import Optional
|
|
|
|
| 7 |
|
| 8 |
from .utils import dbg
|
| 9 |
|
| 10 |
# Ensure deterministic CuBLAS operations for reproducibility on GPU
|
| 11 |
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
class LLM:
|
| 14 |
"""
|
| 15 |
Eine robuste, bereinigte Schnittstelle zum Laden und Interagieren mit einem Sprachmodell.
|
|
@@ -39,9 +49,33 @@ class LLM:
|
|
| 39 |
print(f"[WARN] Could not set 'eager' attention: {e}.", flush=True)
|
| 40 |
|
| 41 |
self.model.eval()
|
| 42 |
-
self.config = self.model.config
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
print(f"[INFO] Model '{model_id}' loaded on device: {self.model.device}", flush=True)
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
def set_all_seeds(self, seed: int):
|
| 46 |
"""Setzt alle relevanten Seeds für maximale Reproduzierbarkeit."""
|
| 47 |
os.environ['PYTHONHASHSEED'] = str(seed)
|
|
|
|
| 4 |
import numpy as np
|
| 5 |
from transformers import AutoModelForCausalLM, AutoTokenizer, set_seed
|
| 6 |
from typing import Optional
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
|
| 9 |
from .utils import dbg
|
| 10 |
|
| 11 |
# Ensure deterministic CuBLAS operations for reproducibility on GPU
|
| 12 |
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
|
| 13 |
|
| 14 |
+
@dataclass
|
| 15 |
+
class StableLLMConfig:
|
| 16 |
+
"""
|
| 17 |
+
Eine stabile, interne Abstraktionsschicht für Modell-Konfigurationen.
|
| 18 |
+
Macht unseren Code unabhängig von den sich ändernden Attributnamen in `transformers`.
|
| 19 |
+
"""
|
| 20 |
+
hidden_dim: int
|
| 21 |
+
num_layers: int
|
| 22 |
+
|
| 23 |
class LLM:
|
| 24 |
"""
|
| 25 |
Eine robuste, bereinigte Schnittstelle zum Laden und Interagieren mit einem Sprachmodell.
|
|
|
|
| 49 |
print(f"[WARN] Could not set 'eager' attention: {e}.", flush=True)
|
| 50 |
|
| 51 |
self.model.eval()
|
| 52 |
+
self.config = self.model.config # Behalte den Zugriff auf die Originalkonfiguration
|
| 53 |
+
|
| 54 |
+
# --- NEU: Befülle die stabile Konfigurations-Abstraktion ---
|
| 55 |
+
self.stable_config = self._populate_stable_config()
|
| 56 |
+
|
| 57 |
print(f"[INFO] Model '{model_id}' loaded on device: {self.model.device}", flush=True)
|
| 58 |
|
| 59 |
+
def _populate_stable_config(self) -> StableLLMConfig:
|
| 60 |
+
"""
|
| 61 |
+
Liest die volatile `transformers`-Konfiguration aus und befüllt unsere stabile Datenklasse.
|
| 62 |
+
"""
|
| 63 |
+
# Robuste Methode für hidden_dim
|
| 64 |
+
try:
|
| 65 |
+
hidden_dim = self.model.get_input_embeddings().weight.shape[1]
|
| 66 |
+
except AttributeError:
|
| 67 |
+
hidden_dim = getattr(self.config, 'hidden_size', getattr(self.config, 'd_model', 0))
|
| 68 |
+
|
| 69 |
+
# Robuste Methode für num_layers
|
| 70 |
+
num_layers = getattr(self.config, 'num_hidden_layers', getattr(self.config, 'num_layers', 0))
|
| 71 |
+
|
| 72 |
+
# Assertions zur Sicherstellung der wissenschaftlichen Validität
|
| 73 |
+
assert hidden_dim > 0, "Could not determine hidden dimension from model config."
|
| 74 |
+
assert num_layers > 0, "Could not determine number of layers from model config."
|
| 75 |
+
|
| 76 |
+
dbg(f"Populated stable config: hidden_dim={hidden_dim}, num_layers={num_layers}")
|
| 77 |
+
return StableLLMConfig(hidden_dim=hidden_dim, num_layers=num_layers)
|
| 78 |
+
|
| 79 |
def set_all_seeds(self, seed: int):
|
| 80 |
"""Setzt alle relevanten Seeds für maximale Reproduzierbarkeit."""
|
| 81 |
os.environ['PYTHONHASHSEED'] = str(seed)
|
cognitive_mapping_probe/resonance_seismograph.py
CHANGED
|
@@ -36,25 +36,38 @@ def run_silent_cogitation_seismic(
|
|
| 36 |
if injection_vector is not None and injection_strength > 0:
|
| 37 |
injection_vector = injection_vector.to(device=llm.model.device, dtype=llm.model.dtype)
|
| 38 |
if injection_layer is None:
|
| 39 |
-
|
|
|
|
| 40 |
|
| 41 |
dbg(f"Injection enabled: Layer {injection_layer}, Strength {injection_strength:.2f}")
|
| 42 |
|
| 43 |
def injection_hook(module, layer_input):
|
| 44 |
# Der Hook operiert auf dem Input, der bereits 3D ist [batch, seq_len, hidden_dim]
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
| 46 |
modified_hidden_states = layer_input[0] + (injection_3d * injection_strength)
|
| 47 |
return (modified_hidden_states,) + layer_input[1:]
|
| 48 |
|
| 49 |
for i in tqdm(range(num_steps), desc=f"Recording Dynamics (Temp {temperature:.2f})", leave=False, bar_format="{l_bar}{bar:10}{r_bar}"):
|
| 50 |
next_token_logits = llm.model.lm_head(hidden_state_2d)
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
try:
|
| 56 |
# Aktiviere den Hook vor dem forward-Pass
|
| 57 |
if injection_vector is not None and injection_strength > 0:
|
|
|
|
|
|
|
| 58 |
target_layer = llm.model.model.layers[injection_layer]
|
| 59 |
hook_handle = target_layer.register_forward_pre_hook(injection_hook)
|
| 60 |
|
|
|
|
| 36 |
if injection_vector is not None and injection_strength > 0:
|
| 37 |
injection_vector = injection_vector.to(device=llm.model.device, dtype=llm.model.dtype)
|
| 38 |
if injection_layer is None:
|
| 39 |
+
# KORREKTUR: Greife auf die stabile, abstrahierte Konfiguration zu.
|
| 40 |
+
injection_layer = llm.stable_config.num_layers // 2
|
| 41 |
|
| 42 |
dbg(f"Injection enabled: Layer {injection_layer}, Strength {injection_strength:.2f}")
|
| 43 |
|
| 44 |
def injection_hook(module, layer_input):
|
| 45 |
# Der Hook operiert auf dem Input, der bereits 3D ist [batch, seq_len, hidden_dim]
|
| 46 |
+
# Stelle sicher, dass der Vektor korrekt auf die Sequenzlänge des Inputs gebroadcastet wird.
|
| 47 |
+
# Normalerweise ist die seq_len hier 1.
|
| 48 |
+
seq_len = layer_input[0].shape[1]
|
| 49 |
+
injection_3d = injection_vector.unsqueeze(0).expand(1, seq_len, -1)
|
| 50 |
modified_hidden_states = layer_input[0] + (injection_3d * injection_strength)
|
| 51 |
return (modified_hidden_states,) + layer_input[1:]
|
| 52 |
|
| 53 |
for i in tqdm(range(num_steps), desc=f"Recording Dynamics (Temp {temperature:.2f})", leave=False, bar_format="{l_bar}{bar:10}{r_bar}"):
|
| 54 |
next_token_logits = llm.model.lm_head(hidden_state_2d)
|
| 55 |
|
| 56 |
+
# Verwende eine minimale Temperatur, um deterministisches Verhalten bei temp=0 zu gewährleisten
|
| 57 |
+
temp_to_use = temperature if temperature > 0.0 else 1.0
|
| 58 |
+
probabilities = torch.nn.functional.softmax(next_token_logits / temp_to_use, dim=-1)
|
| 59 |
+
|
| 60 |
+
if temperature > 0.0:
|
| 61 |
+
next_token_id = torch.multinomial(probabilities, num_samples=1)
|
| 62 |
+
else:
|
| 63 |
+
next_token_id = torch.argmax(probabilities, dim=-1).unsqueeze(-1)
|
| 64 |
+
|
| 65 |
|
| 66 |
try:
|
| 67 |
# Aktiviere den Hook vor dem forward-Pass
|
| 68 |
if injection_vector is not None and injection_strength > 0:
|
| 69 |
+
# Stelle sicher, dass der Layer-Index gültig ist.
|
| 70 |
+
assert 0 <= injection_layer < llm.stable_config.num_layers, f"Injection layer {injection_layer} is out of bounds."
|
| 71 |
target_layer = llm.model.model.layers[injection_layer]
|
| 72 |
hook_handle = target_layer.register_forward_pre_hook(injection_hook)
|
| 73 |
|
tests/conftest.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
| 1 |
import pytest
|
| 2 |
import torch
|
| 3 |
from types import SimpleNamespace
|
| 4 |
-
from cognitive_mapping_probe.llm_iface import LLM
|
| 5 |
|
| 6 |
@pytest.fixture(scope="session")
|
| 7 |
def mock_llm_config():
|
| 8 |
"""Stellt eine minimale, Schein-Konfiguration für das LLM bereit."""
|
|
|
|
| 9 |
return SimpleNamespace(
|
| 10 |
hidden_size=128,
|
| 11 |
num_hidden_layers=2,
|
|
@@ -16,16 +17,14 @@ def mock_llm_config():
|
|
| 16 |
def mock_llm(mocker, mock_llm_config):
|
| 17 |
"""
|
| 18 |
Erstellt einen robusten "Mock-LLM" für Unit-Tests.
|
| 19 |
-
FINAL KORRIGIERT:
|
| 20 |
-
Import-Pfade in den zu testenden Modulen ausgerichtet.
|
| 21 |
"""
|
| 22 |
mock_tokenizer = mocker.MagicMock()
|
| 23 |
mock_tokenizer.eos_token_id = 1
|
| 24 |
mock_tokenizer.decode.return_value = "mocked text"
|
| 25 |
|
| 26 |
-
# Definiere eine stabile Mock-Funktion für die Eingabe-Embeddings
|
| 27 |
mock_embedding_layer = mocker.MagicMock()
|
| 28 |
-
mock_embedding_layer.weight.shape = (32000, mock_llm_config.hidden_size)
|
| 29 |
|
| 30 |
def mock_model_forward(*args, **kwargs):
|
| 31 |
batch_size = 1
|
|
@@ -42,13 +41,12 @@ def mock_llm(mocker, mock_llm_config):
|
|
| 42 |
}
|
| 43 |
return SimpleNamespace(**mock_outputs)
|
| 44 |
|
| 45 |
-
llm_instance = LLM.__new__(LLM)
|
| 46 |
|
| 47 |
llm_instance.model = mocker.MagicMock(side_effect=mock_model_forward)
|
| 48 |
llm_instance.model.config = mock_llm_config
|
| 49 |
llm_instance.model.device = 'cpu'
|
| 50 |
llm_instance.model.dtype = torch.float32
|
| 51 |
-
# Füge die gemockte Embedding-Funktion hinzu, um den Test in `concepts.py` zu bestehen
|
| 52 |
llm_instance.model.get_input_embeddings.return_value = mock_embedding_layer
|
| 53 |
|
| 54 |
mock_layer = mocker.MagicMock()
|
|
@@ -57,18 +55,22 @@ def mock_llm(mocker, mock_llm_config):
|
|
| 57 |
llm_instance.model.lm_head = mocker.MagicMock(return_value=torch.randn(1, 32000))
|
| 58 |
|
| 59 |
llm_instance.tokenizer = mock_tokenizer
|
| 60 |
-
llm_instance.config = mock_llm_config #
|
| 61 |
llm_instance.seed = 42
|
| 62 |
llm_instance.set_all_seeds = mocker.MagicMock()
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
# Patch an allen Stellen, an denen das Modell tatsächlich geladen wird.
|
| 65 |
-
# Dies stellt sicher, dass kein Test versucht, ein echtes Modell herunterzuladen.
|
| 66 |
mocker.patch('cognitive_mapping_probe.llm_iface.get_or_load_model', return_value=llm_instance)
|
| 67 |
mocker.patch('cognitive_mapping_probe.orchestrator_seismograph.get_or_load_model', return_value=llm_instance)
|
| 68 |
mocker.patch('cognitive_mapping_probe.auto_experiment.get_or_load_model', return_value=llm_instance)
|
| 69 |
|
| 70 |
-
# Mocke `get_concept_vector`, um zu verhindern, dass es im Orchestrator-Test ausgeführt wird
|
| 71 |
-
|
| 72 |
-
mocker.patch('cognitive_mapping_probe.concepts.get_concept_vector', return_value=torch.randn(mock_llm_config.hidden_size))
|
| 73 |
|
| 74 |
return llm_instance
|
|
|
|
| 1 |
import pytest
|
| 2 |
import torch
|
| 3 |
from types import SimpleNamespace
|
| 4 |
+
from cognitive_mapping_probe.llm_iface import LLM, StableLLMConfig
|
| 5 |
|
| 6 |
@pytest.fixture(scope="session")
|
| 7 |
def mock_llm_config():
|
| 8 |
"""Stellt eine minimale, Schein-Konfiguration für das LLM bereit."""
|
| 9 |
+
# Diese Fixture repräsentiert die *volatile* transformers-Konfiguration
|
| 10 |
return SimpleNamespace(
|
| 11 |
hidden_size=128,
|
| 12 |
num_hidden_layers=2,
|
|
|
|
| 17 |
def mock_llm(mocker, mock_llm_config):
|
| 18 |
"""
|
| 19 |
Erstellt einen robusten "Mock-LLM" für Unit-Tests.
|
| 20 |
+
FINAL KORRIGIERT: Simuliert nun auch die `stable_config`-Abstraktionsschicht.
|
|
|
|
| 21 |
"""
|
| 22 |
mock_tokenizer = mocker.MagicMock()
|
| 23 |
mock_tokenizer.eos_token_id = 1
|
| 24 |
mock_tokenizer.decode.return_value = "mocked text"
|
| 25 |
|
|
|
|
| 26 |
mock_embedding_layer = mocker.MagicMock()
|
| 27 |
+
mock_embedding_layer.weight.shape = (32000, mock_llm_config.hidden_size)
|
| 28 |
|
| 29 |
def mock_model_forward(*args, **kwargs):
|
| 30 |
batch_size = 1
|
|
|
|
| 41 |
}
|
| 42 |
return SimpleNamespace(**mock_outputs)
|
| 43 |
|
| 44 |
+
llm_instance = LLM.__new__(LLM)
|
| 45 |
|
| 46 |
llm_instance.model = mocker.MagicMock(side_effect=mock_model_forward)
|
| 47 |
llm_instance.model.config = mock_llm_config
|
| 48 |
llm_instance.model.device = 'cpu'
|
| 49 |
llm_instance.model.dtype = torch.float32
|
|
|
|
| 50 |
llm_instance.model.get_input_embeddings.return_value = mock_embedding_layer
|
| 51 |
|
| 52 |
mock_layer = mocker.MagicMock()
|
|
|
|
| 55 |
llm_instance.model.lm_head = mocker.MagicMock(return_value=torch.randn(1, 32000))
|
| 56 |
|
| 57 |
llm_instance.tokenizer = mock_tokenizer
|
| 58 |
+
llm_instance.config = mock_llm_config # Die originale, volatile config
|
| 59 |
llm_instance.seed = 42
|
| 60 |
llm_instance.set_all_seeds = mocker.MagicMock()
|
| 61 |
|
| 62 |
+
# KORREKTUR: Erzeuge die stabile Konfiguration, die die Tests nun erwarten.
|
| 63 |
+
llm_instance.stable_config = StableLLMConfig(
|
| 64 |
+
hidden_dim=mock_llm_config.hidden_size,
|
| 65 |
+
num_layers=mock_llm_config.num_hidden_layers
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
# Patch an allen Stellen, an denen das Modell tatsächlich geladen wird.
|
|
|
|
| 69 |
mocker.patch('cognitive_mapping_probe.llm_iface.get_or_load_model', return_value=llm_instance)
|
| 70 |
mocker.patch('cognitive_mapping_probe.orchestrator_seismograph.get_or_load_model', return_value=llm_instance)
|
| 71 |
mocker.patch('cognitive_mapping_probe.auto_experiment.get_or_load_model', return_value=llm_instance)
|
| 72 |
|
| 73 |
+
# Mocke `get_concept_vector`, um zu verhindern, dass es im Orchestrator-Test ausgeführt wird.
|
| 74 |
+
mocker.patch('cognitive_mapping_probe.orchestrator_seismograph.get_concept_vector', return_value=torch.randn(mock_llm_config.hidden_size))
|
|
|
|
| 75 |
|
| 76 |
return llm_instance
|