spectral-trust 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spectral_trust-0.1.0/LICENSE +21 -0
- spectral_trust-0.1.0/PKG-INFO +87 -0
- spectral_trust-0.1.0/README.md +62 -0
- spectral_trust-0.1.0/setup.cfg +4 -0
- spectral_trust-0.1.0/setup.py +33 -0
- spectral_trust-0.1.0/src/spectral_trust/__init__.py +14 -0
- spectral_trust-0.1.0/src/spectral_trust/__main__.py +4 -0
- spectral_trust-0.1.0/src/spectral_trust/cli.py +425 -0
- spectral_trust-0.1.0/src/spectral_trust/config.py +40 -0
- spectral_trust-0.1.0/src/spectral_trust/framework.py +246 -0
- spectral_trust-0.1.0/src/spectral_trust/graph.py +121 -0
- spectral_trust-0.1.0/src/spectral_trust/instrumentation.py +169 -0
- spectral_trust-0.1.0/src/spectral_trust/spectral.py +249 -0
- spectral_trust-0.1.0/src/spectral_trust.egg-info/PKG-INFO +87 -0
- spectral_trust-0.1.0/src/spectral_trust.egg-info/SOURCES.txt +17 -0
- spectral_trust-0.1.0/src/spectral_trust.egg-info/dependency_links.txt +1 -0
- spectral_trust-0.1.0/src/spectral_trust.egg-info/entry_points.txt +2 -0
- spectral_trust-0.1.0/src/spectral_trust.egg-info/requires.txt +9 -0
- spectral_trust-0.1.0/src/spectral_trust.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Valentin Noël
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spectral_trust
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Spectral diagnostics for trust in LLMs
|
|
5
|
+
Author: Google DeepMind (simulated)
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: pandas
|
|
11
|
+
Requires-Dist: matplotlib
|
|
12
|
+
Requires-Dist: seaborn
|
|
13
|
+
Requires-Dist: scipy
|
|
14
|
+
Requires-Dist: torch>=2.0.0
|
|
15
|
+
Requires-Dist: transformers>=4.30.0
|
|
16
|
+
Requires-Dist: tqdm
|
|
17
|
+
Requires-Dist: accelerate
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
# Spectral Trust Framework
|
|
27
|
+
|
|
28
|
+
**A Graph Signal Processing (GSP) framework for measuring the trustworthiness of LLM internal representations.**
|
|
29
|
+
|
|
30
|
+
[`spectral_trust`](https://github.com/your-username/spectral-trust) constructs dynamic graphs from attention patterns and applies spectral analysis (eigenvalues, Dirichlet energy) to detect hallucinations, quantify uncertainty, and map the "smoothness" of reasoning flows.
|
|
31
|
+
|
|
32
|
+
## What is it?
|
|
33
|
+
By treating the transformer's attention mechanism as a **graph** and the hidden states as **signals** on that graph, we can calculate rigorous mathematical metrics:
|
|
34
|
+
* **Dirichlet Energy**: How much the signal varies across connected tokens (proxy for conflict/uncertainty).
|
|
35
|
+
* **Smoothness Index**: Normalized energy indicating how well the representation aligns with the attention structure.
|
|
36
|
+
* **Fiedler Value**: Algebraic connectivity of the attention graph.
|
|
37
|
+
* **HFER (High-Frequency Energy Ratio)**: Energy concentration in high-frequency spectral components.
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
- **Plug-and-Play**: Works out-of-the-box with `Llama-3`, `Mistral`, `Qwen`, `Gemma`, and `Phi`.
|
|
41
|
+
- **Offline Ready**: `--offline` mode to use cached models without internet access.
|
|
42
|
+
- **Spectral Metrics**: Automatically computes Energy, Entropy, Fiedler Value, HFER, and Smoothness.
|
|
43
|
+
- **Robustness Tools**: Includes hooks for head ablation and residual patching.
|
|
44
|
+
|
|
45
|
+
## Structure
|
|
46
|
+
- `src/spectral_trust/`: Core package source code.
|
|
47
|
+
- `notebooks/`: Jupyter notebooks for demonstration.
|
|
48
|
+
- `examples/`: Minimal example scripts.
|
|
49
|
+
- `dist/`: Wheel and source distributions.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install spectral_trust
|
|
55
|
+
# OR install from source
|
|
56
|
+
pip install -e .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
### CLI Power Tool
|
|
62
|
+
|
|
63
|
+
**Analyze a sentence** (uses `cuda` if available):
|
|
64
|
+
```bash
|
|
65
|
+
gsp-cli analyze --text "The capital of France is Paris." --model llama-3.1-8b
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Offline Mode** (no internet required):
|
|
69
|
+
```bash
|
|
70
|
+
gsp-cli analyze --text "Refactoring is fun." --model llama-3.2-1b --offline
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Python API
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from spectral_trust import GSPDiagnosticsFramework, GSPConfig
|
|
77
|
+
|
|
78
|
+
config = GSPConfig(model_name="llama-3.2-1b", device="cuda", local_files_only=True)
|
|
79
|
+
with GSPDiagnosticsFramework(config) as framework:
|
|
80
|
+
framework.instrumenter.load_model("meta-llama/Llama-3.2-1B")
|
|
81
|
+
results = framework.analyze_text("The capital of France is Paris.")
|
|
82
|
+
|
|
83
|
+
print(f"Smoothness: {results['layer_diagnostics'][-1].smoothness_index:.4f}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
MIT
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Spectral Trust Framework
|
|
2
|
+
|
|
3
|
+
**A Graph Signal Processing (GSP) framework for measuring the trustworthiness of LLM internal representations.**
|
|
4
|
+
|
|
5
|
+
[`spectral_trust`](https://github.com/your-username/spectral-trust) constructs dynamic graphs from attention patterns and applies spectral analysis (eigenvalues, Dirichlet energy) to detect hallucinations, quantify uncertainty, and map the "smoothness" of reasoning flows.
|
|
6
|
+
|
|
7
|
+
## What is it?
|
|
8
|
+
By treating the transformer's attention mechanism as a **graph** and the hidden states as **signals** on that graph, we can calculate rigorous mathematical metrics:
|
|
9
|
+
* **Dirichlet Energy**: How much the signal varies across connected tokens (proxy for conflict/uncertainty).
|
|
10
|
+
* **Smoothness Index**: Normalized energy indicating how well the representation aligns with the attention structure.
|
|
11
|
+
* **Fiedler Value**: Algebraic connectivity of the attention graph.
|
|
12
|
+
* **HFER (High-Frequency Energy Ratio)**: Energy concentration in high-frequency spectral components.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
- **Plug-and-Play**: Works out-of-the-box with `Llama-3`, `Mistral`, `Qwen`, `Gemma`, and `Phi`.
|
|
16
|
+
- **Offline Ready**: `--offline` mode to use cached models without internet access.
|
|
17
|
+
- **Spectral Metrics**: Automatically computes Energy, Entropy, Fiedler Value, HFER, and Smoothness.
|
|
18
|
+
- **Robustness Tools**: Includes hooks for head ablation and residual patching.
|
|
19
|
+
|
|
20
|
+
## Structure
|
|
21
|
+
- `src/spectral_trust/`: Core package source code.
|
|
22
|
+
- `notebooks/`: Jupyter notebooks for demonstration.
|
|
23
|
+
- `examples/`: Minimal example scripts.
|
|
24
|
+
- `dist/`: Wheel and source distributions.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install spectral_trust
|
|
30
|
+
# OR install from source
|
|
31
|
+
pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### CLI Power Tool
|
|
37
|
+
|
|
38
|
+
**Analyze a sentence** (uses `cuda` if available):
|
|
39
|
+
```bash
|
|
40
|
+
gsp-cli analyze --text "The capital of France is Paris." --model llama-3.1-8b
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Offline Mode** (no internet required):
|
|
44
|
+
```bash
|
|
45
|
+
gsp-cli analyze --text "Refactoring is fun." --model llama-3.2-1b --offline
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Python API
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from spectral_trust import GSPDiagnosticsFramework, GSPConfig
|
|
52
|
+
|
|
53
|
+
config = GSPConfig(model_name="llama-3.2-1b", device="cuda", local_files_only=True)
|
|
54
|
+
with GSPDiagnosticsFramework(config) as framework:
|
|
55
|
+
framework.instrumenter.load_model("meta-llama/Llama-3.2-1B")
|
|
56
|
+
results = framework.analyze_text("The capital of France is Paris.")
|
|
57
|
+
|
|
58
|
+
print(f"Smoothness: {results['layer_diagnostics'][-1].smoothness_index:.4f}")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
import pathlib
|
|
3
|
+
|
|
4
|
+
here = pathlib.Path(__file__).parent.resolve()
|
|
5
|
+
long_description = (here / "README.md").read_text(encoding="utf-8")
|
|
6
|
+
|
|
7
|
+
setup(
|
|
8
|
+
name="spectral_trust",
|
|
9
|
+
version="0.1.0",
|
|
10
|
+
package_dir={"": "src"},
|
|
11
|
+
packages=find_packages(where="src"),
|
|
12
|
+
install_requires=[
|
|
13
|
+
"numpy",
|
|
14
|
+
"pandas",
|
|
15
|
+
"matplotlib",
|
|
16
|
+
"seaborn",
|
|
17
|
+
"scipy",
|
|
18
|
+
"torch>=2.0.0",
|
|
19
|
+
"transformers>=4.30.0",
|
|
20
|
+
"tqdm",
|
|
21
|
+
"accelerate"
|
|
22
|
+
],
|
|
23
|
+
entry_points={
|
|
24
|
+
"console_scripts": [
|
|
25
|
+
"gsp-cli=spectral_trust.cli:main",
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
author="Google DeepMind (simulated)",
|
|
29
|
+
description="Spectral diagnostics for trust in LLMs",
|
|
30
|
+
long_description=long_description,
|
|
31
|
+
long_description_content_type="text/markdown",
|
|
32
|
+
python_requires=">=3.8",
|
|
33
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .config import GSPConfig
|
|
2
|
+
from .framework import GSPDiagnosticsFramework
|
|
3
|
+
from .spectral import SpectralDiagnostics, SpectralAnalyzer
|
|
4
|
+
from .graph import GraphConstructor
|
|
5
|
+
from .instrumentation import LLMInstrumenter
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"GSPConfig",
|
|
9
|
+
"GSPDiagnosticsFramework",
|
|
10
|
+
"SpectralDiagnostics",
|
|
11
|
+
"SpectralAnalyzer",
|
|
12
|
+
"GraphConstructor",
|
|
13
|
+
"LLMInstrumenter"
|
|
14
|
+
]
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Command Line Interface for Spectral Trust Framework (formerly GSP LLM Diagnostics)
|
|
4
|
+
Provides easy-to-use CLI for running various analysis tasks with modern language models
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
import pandas as pd
|
|
16
|
+
import torch
|
|
17
|
+
|
|
18
|
+
from .framework import GSPDiagnosticsFramework
|
|
19
|
+
from .config import GSPConfig
|
|
20
|
+
from .spectral import SpectralDiagnostics
|
|
21
|
+
|
|
22
|
+
# --- UTF-8 console safety on Windows ---
|
|
23
|
+
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
|
24
|
+
try:
|
|
25
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
# Setup logging
|
|
30
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
# Modern model configurations with optimal settings
|
|
34
|
+
MODERN_MODELS = {
|
|
35
|
+
# Llama models
|
|
36
|
+
'llama-3.2-1b': {
|
|
37
|
+
'name': 'meta-llama/Llama-3.2-1B',
|
|
38
|
+
'max_length': 2048,
|
|
39
|
+
'trust_remote_code': True,
|
|
40
|
+
'torch_dtype': 'float16'
|
|
41
|
+
},
|
|
42
|
+
'llama-3.2-3b': {
|
|
43
|
+
'name': 'meta-llama/Llama-3.2-3B',
|
|
44
|
+
'max_length': 2048,
|
|
45
|
+
'trust_remote_code': True,
|
|
46
|
+
'torch_dtype': 'float16'
|
|
47
|
+
},
|
|
48
|
+
'llama-3.2-3b-instruct': {
|
|
49
|
+
'name': 'meta-llama/Llama-3.2-3B-Instruct',
|
|
50
|
+
'max_length': 2048,
|
|
51
|
+
'trust_remote_code': True,
|
|
52
|
+
'torch_dtype': 'float16'
|
|
53
|
+
},
|
|
54
|
+
'llama-3.1-8b': {
|
|
55
|
+
'name': 'meta-llama/Meta-Llama-3.1-8B',
|
|
56
|
+
'max_length': 4096,
|
|
57
|
+
'trust_remote_code': True,
|
|
58
|
+
'torch_dtype': 'float16'
|
|
59
|
+
},
|
|
60
|
+
'llama-3.1-8b-instruct': {
|
|
61
|
+
'name': 'meta-llama/Meta-Llama-3.1-8B-Instruct',
|
|
62
|
+
'max_length': 4096,
|
|
63
|
+
'trust_remote_code': True,
|
|
64
|
+
'torch_dtype': 'float16'
|
|
65
|
+
},
|
|
66
|
+
'llama-3.1-70b': {
|
|
67
|
+
'name': 'meta-llama/Meta-Llama-3.1-70B',
|
|
68
|
+
'max_length': 4096,
|
|
69
|
+
'trust_remote_code': True,
|
|
70
|
+
'torch_dtype': 'float16',
|
|
71
|
+
'device_map': 'auto'
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
# Mistral models
|
|
75
|
+
'mistral-7b': {
|
|
76
|
+
'name': 'mistralai/Mistral-7B-v0.1',
|
|
77
|
+
'max_length': 8192,
|
|
78
|
+
'torch_dtype': 'float16'
|
|
79
|
+
},
|
|
80
|
+
'mistral-7b-instruct': {
|
|
81
|
+
'name': 'mistralai/Mistral-7B-Instruct-v0.3',
|
|
82
|
+
'max_length': 8192,
|
|
83
|
+
'torch_dtype': 'float16'
|
|
84
|
+
},
|
|
85
|
+
'mixtral-8x7b': {
|
|
86
|
+
'name': 'mistralai/Mixtral-8x7B-v0.1',
|
|
87
|
+
'max_length': 8192,
|
|
88
|
+
'torch_dtype': 'float16',
|
|
89
|
+
'device_map': 'auto'
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
# Qwen models
|
|
93
|
+
'qwen2-7b': {
|
|
94
|
+
'name': 'Qwen/Qwen2-7B',
|
|
95
|
+
'max_length': 4096,
|
|
96
|
+
'trust_remote_code': True,
|
|
97
|
+
'torch_dtype': 'float16'
|
|
98
|
+
},
|
|
99
|
+
'qwen2.5-7b': {
|
|
100
|
+
'name': 'Qwen/Qwen2.5-7B',
|
|
101
|
+
'max_length': 8192,
|
|
102
|
+
'trust_remote_code': True,
|
|
103
|
+
'torch_dtype': 'float16'
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
# Gemma models
|
|
107
|
+
'gemma-2b': {
|
|
108
|
+
'name': 'google/gemma-2b',
|
|
109
|
+
'max_length': 2048,
|
|
110
|
+
'torch_dtype': 'float16'
|
|
111
|
+
},
|
|
112
|
+
'gemma-7b': {
|
|
113
|
+
'name': 'google/gemma-7b',
|
|
114
|
+
'max_length': 2048,
|
|
115
|
+
'torch_dtype': 'float16'
|
|
116
|
+
},
|
|
117
|
+
'gemma2-9b': {
|
|
118
|
+
'name': 'google/gemma-2-9b',
|
|
119
|
+
'max_length': 4096,
|
|
120
|
+
'torch_dtype': 'float16'
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
# Phi models
|
|
124
|
+
'phi-3-mini': {
|
|
125
|
+
'name': 'microsoft/Phi-3-mini-4k-instruct',
|
|
126
|
+
'max_length': 4096,
|
|
127
|
+
'trust_remote_code': True,
|
|
128
|
+
'torch_dtype': 'float16'
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
# Legacy models
|
|
132
|
+
'gpt2': {
|
|
133
|
+
'name': 'gpt2',
|
|
134
|
+
'max_length': 1024,
|
|
135
|
+
'torch_dtype': 'float32'
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
def get_model_info(model_key: str) -> Dict[str, Any]:
|
|
140
|
+
"""Get model configuration by key or return custom config for unknown models"""
|
|
141
|
+
if model_key in MODERN_MODELS:
|
|
142
|
+
return MODERN_MODELS[model_key]
|
|
143
|
+
else:
|
|
144
|
+
# Assume it's a HuggingFace model path
|
|
145
|
+
return {
|
|
146
|
+
'name': model_key,
|
|
147
|
+
'max_length': 2048,
|
|
148
|
+
'torch_dtype': 'float16',
|
|
149
|
+
'trust_remote_code': False
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class HeadSpec:
|
|
154
|
+
layer: int
|
|
155
|
+
heads: List[int]
|
|
156
|
+
|
|
157
|
+
class PatchResidualAt:
|
|
158
|
+
"""
|
|
159
|
+
Forward hook to patch residual slice at a layer and token window.
|
|
160
|
+
Assumes transformer blocks are at model.model.layers[idx].
|
|
161
|
+
"""
|
|
162
|
+
def __init__(self, t_lo: int, t_hi: int, donor_hidden: torch.Tensor):
|
|
163
|
+
self.t_lo = t_lo
|
|
164
|
+
self.t_hi = t_hi
|
|
165
|
+
self.donor_hidden = donor_hidden # (1, seq, dim)
|
|
166
|
+
|
|
167
|
+
def __call__(self, module, inp, out):
|
|
168
|
+
if isinstance(out, tuple):
|
|
169
|
+
hs = out[0]; rest = out[1:]
|
|
170
|
+
else:
|
|
171
|
+
hs = out; rest = None
|
|
172
|
+
L = hs.shape[1]
|
|
173
|
+
a = max(0, min(self.t_lo, L))
|
|
174
|
+
b = max(0, min(self.t_hi, L))
|
|
175
|
+
if a < b:
|
|
176
|
+
hs = hs.clone()
|
|
177
|
+
hs[:, a:b, :] = self.donor_hidden[:, a:b, :].to(hs.device)
|
|
178
|
+
return (hs, *rest) if rest is not None else hs
|
|
179
|
+
|
|
180
|
+
def parse_heads(spec: Optional[str]) -> List[HeadSpec]:
|
|
181
|
+
if not spec or not spec.strip():
|
|
182
|
+
return []
|
|
183
|
+
out: List[HeadSpec] = []
|
|
184
|
+
for chunk in spec.split(";"):
|
|
185
|
+
chunk = chunk.strip()
|
|
186
|
+
if not chunk:
|
|
187
|
+
continue
|
|
188
|
+
L_str, hs = chunk.split(":")
|
|
189
|
+
L = int(L_str.strip())
|
|
190
|
+
idxs = []
|
|
191
|
+
for tok in hs.split(","):
|
|
192
|
+
tok = tok.strip()
|
|
193
|
+
if "-" in tok:
|
|
194
|
+
a, b = tok.split("-"); a, b = int(a), int(b)
|
|
195
|
+
idxs.extend(range(a, b + 1))
|
|
196
|
+
else:
|
|
197
|
+
idxs.append(int(tok))
|
|
198
|
+
out.append(HeadSpec(L, sorted(set(idxs))))
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
def _attn_module(hf_model, layer_idx: int):
|
|
202
|
+
"""
|
|
203
|
+
Return the attention module for a decoder block across families.
|
|
204
|
+
"""
|
|
205
|
+
block_container = getattr(hf_model, "model", hf_model)
|
|
206
|
+
block = block_container.layers[layer_idx]
|
|
207
|
+
for name in ("self_attn", "attention", "attn"):
|
|
208
|
+
if hasattr(block, name):
|
|
209
|
+
return getattr(block, name)
|
|
210
|
+
raise RuntimeError(f"Layer {layer_idx} has no attention module")
|
|
211
|
+
|
|
212
|
+
def _attach_attn_output_head_zero_hook(hf_model, specs: List[HeadSpec]) -> List:
|
|
213
|
+
"""
|
|
214
|
+
Forward-hook on the ATTENTION MODULE OUTPUT to zero selected heads.
|
|
215
|
+
"""
|
|
216
|
+
handles = []
|
|
217
|
+
nH = hf_model.config.num_attention_heads
|
|
218
|
+
Hd = hf_model.config.hidden_size // nH
|
|
219
|
+
|
|
220
|
+
by_layer = {}
|
|
221
|
+
for s in specs:
|
|
222
|
+
valid = [h for h in s.heads if 0 <= h < nH]
|
|
223
|
+
if valid:
|
|
224
|
+
by_layer.setdefault(s.layer, sorted(set(valid)))
|
|
225
|
+
|
|
226
|
+
if not by_layer:
|
|
227
|
+
return handles
|
|
228
|
+
|
|
229
|
+
for layer_idx, heads in by_layer.items():
|
|
230
|
+
attn = _attn_module(hf_model, layer_idx)
|
|
231
|
+
head_slices = [(h * Hd, (h + 1) * Hd) for h in heads]
|
|
232
|
+
|
|
233
|
+
def hook(module, inputs, output, head_slices=head_slices, Hd=Hd):
|
|
234
|
+
y = output
|
|
235
|
+
if isinstance(y, tuple):
|
|
236
|
+
y = y[0]
|
|
237
|
+
if y is None:
|
|
238
|
+
return output
|
|
239
|
+
for a, b in head_slices:
|
|
240
|
+
y[..., a:b] = 0
|
|
241
|
+
return y
|
|
242
|
+
|
|
243
|
+
handles.append(attn.register_forward_hook(hook))
|
|
244
|
+
return handles
|
|
245
|
+
|
|
246
|
+
def setup_model_for_analysis(hf_model):
|
|
247
|
+
"""
|
|
248
|
+
Setup model configuration for better compatibility with GSP analysis.
|
|
249
|
+
"""
|
|
250
|
+
if hasattr(hf_model, "config"):
|
|
251
|
+
hf_model.config.output_attentions = True
|
|
252
|
+
hf_model.config.output_hidden_states = True
|
|
253
|
+
try:
|
|
254
|
+
hf_model.config.attn_implementation = "eager"
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
return hf_model
|
|
258
|
+
|
|
259
|
+
def safe_tokenize_with_fallback(tokenizer, text: str, **kwargs):
|
|
260
|
+
"""
|
|
261
|
+
Safely tokenize text with fallbacks.
|
|
262
|
+
"""
|
|
263
|
+
enc = tokenizer(text, **kwargs)
|
|
264
|
+
if (enc.get("input_ids", None) is None or
|
|
265
|
+
enc["input_ids"].numel() == 0 or
|
|
266
|
+
enc["input_ids"].shape[1] == 0):
|
|
267
|
+
|
|
268
|
+
candidates = [text.strip(), " " + text.strip(), text.strip() + "."]
|
|
269
|
+
for candidate in candidates:
|
|
270
|
+
if not candidate: continue
|
|
271
|
+
try:
|
|
272
|
+
enc = tokenizer(candidate, **kwargs)
|
|
273
|
+
if (enc.get("input_ids", None) is not None and enc["input_ids"].numel() > 0):
|
|
274
|
+
return enc, candidate
|
|
275
|
+
except Exception:
|
|
276
|
+
continue
|
|
277
|
+
raise RuntimeError("Failed to tokenize any variant of the input text")
|
|
278
|
+
return enc, text
|
|
279
|
+
|
|
280
|
+
def generate_response(hf_model, hf_tokenizer, prompt_text, max_new_tokens=20):
|
|
281
|
+
"""Generate response with minimal risk of CUDA assertion errors"""
|
|
282
|
+
try:
|
|
283
|
+
inputs = hf_tokenizer(prompt_text, return_tensors="pt", max_length=512, truncation=True)
|
|
284
|
+
model_device = next(hf_model.parameters()).device
|
|
285
|
+
inputs = {k: v.to(model_device) for k, v in inputs.items()}
|
|
286
|
+
|
|
287
|
+
with torch.no_grad():
|
|
288
|
+
outputs = hf_model.generate(
|
|
289
|
+
inputs['input_ids'],
|
|
290
|
+
max_new_tokens=max_new_tokens,
|
|
291
|
+
do_sample=False,
|
|
292
|
+
pad_token_id=hf_tokenizer.eos_token_id,
|
|
293
|
+
use_cache=False
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
new_tokens = outputs[0][inputs['input_ids'].shape[1]:]
|
|
297
|
+
response = hf_tokenizer.decode(new_tokens, skip_special_tokens=True)
|
|
298
|
+
return response.strip()
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.warning(f"Generation failed: {e}")
|
|
301
|
+
return ""
|
|
302
|
+
|
|
303
|
+
def extract_answer(text: str) -> str:
|
|
304
|
+
if not text: return "unknown"
|
|
305
|
+
return text.strip() # Simplified for brevity, original regex logic can be added if needed
|
|
306
|
+
|
|
307
|
+
def create_model_config(model_key: str, args) -> GSPConfig:
|
|
308
|
+
model_info = get_model_info(model_key)
|
|
309
|
+
if hasattr(args, 'device') and args.device != 'auto':
|
|
310
|
+
device = args.device
|
|
311
|
+
else:
|
|
312
|
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
|
313
|
+
|
|
314
|
+
return GSPConfig(
|
|
315
|
+
model_name=model_info['name'],
|
|
316
|
+
max_length=getattr(args, 'max_length', model_info['max_length']),
|
|
317
|
+
device=device,
|
|
318
|
+
trust_remote_code=model_info.get('trust_remote_code', False),
|
|
319
|
+
torch_dtype=model_info.get('torch_dtype', 'float16'),
|
|
320
|
+
output_dir=getattr(args, 'output_dir', './gsp_results'),
|
|
321
|
+
save_plots=getattr(args, 'save_plots', False),
|
|
322
|
+
verbose=getattr(args, 'verbose', False),
|
|
323
|
+
num_layers_analyze=getattr(args, 'num_layers', None),
|
|
324
|
+
num_eigenvalues=getattr(args, 'num_eigenvalues', 100),
|
|
325
|
+
eigen_solver=getattr(args, 'eigen_solver', 'sparse'),
|
|
326
|
+
head_aggregation=getattr(args, 'head_aggregation', 'uniform'),
|
|
327
|
+
symmetrization=getattr(args, 'symmetrization', 'symmetric'),
|
|
328
|
+
normalization=getattr(args, 'normalization', 'rw'),
|
|
329
|
+
hfer_cutoff_ratio=getattr(args, 'hfer_cutoff', 0.1),
|
|
330
|
+
local_files_only=getattr(args, 'offline', False)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def cmd_analyze(args):
|
|
334
|
+
"""Analyze (single or batch), generate a model answer, score vs gold, and log per-layer metrics."""
|
|
335
|
+
logger.info(f"Starting analysis with model: {args.model}")
|
|
336
|
+
config = create_model_config(args.model, args)
|
|
337
|
+
|
|
338
|
+
text = args.text
|
|
339
|
+
if args.text_file:
|
|
340
|
+
text = Path(args.text_file).read_text(encoding='utf-8')
|
|
341
|
+
|
|
342
|
+
if not text:
|
|
343
|
+
logger.error("No text provided")
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
with GSPDiagnosticsFramework(config) as framework:
|
|
348
|
+
framework.instrumenter.load_model(config.model_name)
|
|
349
|
+
framework.instrumenter.register_hooks()
|
|
350
|
+
|
|
351
|
+
hf_model = setup_model_for_analysis(framework.instrumenter.model)
|
|
352
|
+
hf_tokenizer = framework.instrumenter.tokenizer
|
|
353
|
+
|
|
354
|
+
# Head ablation hooks
|
|
355
|
+
handles = []
|
|
356
|
+
if args.ablate_heads:
|
|
357
|
+
specs = parse_heads(args.ablate_heads)
|
|
358
|
+
handles = _attach_attn_output_head_zero_hook(hf_model, specs)
|
|
359
|
+
|
|
360
|
+
# Analyze
|
|
361
|
+
results = framework.analyze_text(text)
|
|
362
|
+
|
|
363
|
+
# Show output
|
|
364
|
+
if args.emit_text:
|
|
365
|
+
resp = generate_response(hf_model, hf_tokenizer, text)
|
|
366
|
+
logger.info(f"Response: {resp}")
|
|
367
|
+
|
|
368
|
+
# Print Summary
|
|
369
|
+
if results and 'layer_diagnostics' in results:
|
|
370
|
+
print("\nResults:")
|
|
371
|
+
headers = f"{'Layer':>5} {'Energy':>10} {'HFER':>10} {'Entropy':>10} {'Fiedler':>10} {'Smoothness':>10}"
|
|
372
|
+
print(headers)
|
|
373
|
+
print("-" * len(headers))
|
|
374
|
+
for d in results['layer_diagnostics']:
|
|
375
|
+
print(f"{d.layer:5d} {d.energy:10.4f} {d.hfer:10.4f} {d.spectral_entropy:10.4f} {d.fiedler_value:10.4f} {d.smoothness_index:10.4f}")
|
|
376
|
+
|
|
377
|
+
for h in handles: h.remove()
|
|
378
|
+
|
|
379
|
+
except Exception as e:
|
|
380
|
+
logger.error(f"Analysis failed: {e}")
|
|
381
|
+
import traceback
|
|
382
|
+
traceback.print_exc()
|
|
383
|
+
|
|
384
|
+
def cmd_list_models(args):
|
|
385
|
+
print("Available Models:")
|
|
386
|
+
for k, v in MODERN_MODELS.items():
|
|
387
|
+
print(f" {k:20} -> {v['name']}")
|
|
388
|
+
|
|
389
|
+
def main():
|
|
390
|
+
parser = argparse.ArgumentParser(description="Spectral Trust CLI")
|
|
391
|
+
subparsers = parser.add_subparsers(dest='command', required=True)
|
|
392
|
+
|
|
393
|
+
# Analyze
|
|
394
|
+
p_analyze = subparsers.add_parser('analyze')
|
|
395
|
+
p_analyze.add_argument('--text', type=str)
|
|
396
|
+
p_analyze.add_argument('--text_file', type=str)
|
|
397
|
+
p_analyze.add_argument('--model', type=str, default='llama-3.2-1b')
|
|
398
|
+
p_analyze.add_argument('--device', type=str, default='auto')
|
|
399
|
+
p_analyze.add_argument('--output_dir', type=str, default='./results')
|
|
400
|
+
p_analyze.add_argument('--save_plots', action='store_true')
|
|
401
|
+
p_analyze.add_argument('--verbose', action='store_true')
|
|
402
|
+
p_analyze.add_argument('--emit_text', action='store_true')
|
|
403
|
+
p_analyze.add_argument('--ablate_heads', type=str)
|
|
404
|
+
|
|
405
|
+
# GSP params
|
|
406
|
+
p_analyze.add_argument('--head_aggregation', type=str, default='uniform')
|
|
407
|
+
p_analyze.add_argument('--symmetrization', type=str, default='symmetric')
|
|
408
|
+
p_analyze.add_argument('--normalization', type=str, default='rw')
|
|
409
|
+
p_analyze.add_argument('--hfer_cutoff', type=float, default=0.1)
|
|
410
|
+
p_analyze.add_argument('--offline', action='store_true', help='Use local files only (no download)')
|
|
411
|
+
|
|
412
|
+
# List models
|
|
413
|
+
subparsers.add_parser('list-models')
|
|
414
|
+
|
|
415
|
+
args = parser.parse_args()
|
|
416
|
+
|
|
417
|
+
if args.command == 'analyze':
|
|
418
|
+
cmd_analyze(args)
|
|
419
|
+
elif args.command == 'list-models':
|
|
420
|
+
cmd_list_models(args)
|
|
421
|
+
else:
|
|
422
|
+
parser.print_help()
|
|
423
|
+
|
|
424
|
+
if __name__ == "__main__":
|
|
425
|
+
main()
|