juiceb0xc0de commited on
Commit
eca4e43
·
verified ·
1 Parent(s): 1fcda97

Build interactive BF16 BoL operating room

Browse files
README.md CHANGED
@@ -1,13 +1,48 @@
1
  ---
2
- title: 0perating Soon
3
- emoji: ⚡
4
  colorFrom: green
5
- colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.26.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: 0perating-soon
3
+ emoji: 🧬
4
  colorFrom: green
5
+ colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.26.0
8
+ python_version: "3.12"
9
  app_file: app.py
10
+ short_description: Open a language model. Inspect, intervene, compare.
11
+ startup_duration_timeout: 1h
12
+ preload_from_hub:
13
+ - HuggingFaceTB/SmolLM2-1.7B-Instruct
14
  ---
15
 
16
+ # 0perating-soon
17
+
18
+ An interactive operating room for language models, built from Rick Holmberg's BoL tools.
19
+
20
+ Choose a Hugging Face checkpoint, inspect its representations and weights, and measure what happens when selected parts are changed. The default model is **HuggingFaceTB/SmolLM2-1.7B-Instruct**. Execution uses **BF16**, with **FP32 numerical summaries**. Each report records the exact model revision, settings, elapsed GPU-function time, and peak allocated VRAM.
21
+
22
+ ## Instruments
23
+
24
+ - Weight fingerprint, semantic silhouette, semantic clusters, layer CKA, attention maps, and gradient sensitivity.
25
+ - Layer and component ablations, zone cuts, layer keep patterns, and controlled noise sensitivity.
26
+ - **Layer Duets:** measure the interaction between paired ablations.
27
+ - **Butterfly Effect:** compare hidden representations at aligned downstream tokens after a prompt edit.
28
+ - **Context Tug-of-War:** compare candidate continuation scores under competing contexts.
29
+
30
+ Outputs include interactive charts, measurement tables, JSON, and a self-contained printable HTML report. This app exports reports, not modified model weights.
31
+
32
+ ## Model support
33
+
34
+ Use complete, unquantized Safetensors checkpoints supported by Transformers' `AutoModelForCausalLM`. Remote model code and pickle weights are not executed. Private or gated models require your own read token and model access. Intervention tools detect supported decoder layer layouts and fail explicitly when a requested structure is unavailable. Some tools require attention weights or hidden states that not every architecture provides.
35
+
36
+ The app uses ZeroGPU's default 48 GB allocation. A model-size ceiling has not been established: the selected experiment, sequence length, and architecture all affect memory. Check the measured memory in your report. The default checkpoint is preloaded; other checkpoints download before acquiring a GPU and load temporarily inside the GPU worker. Custom-model cold loads therefore take longer. GPU time remains subject to the visitor's Hugging Face quota.
37
+
38
+ ## Interpretation
39
+
40
+ Measurements describe the chosen checkpoint, probes, precision, and interventions. Semantic separation and attention patterns do not establish comprehension. CKA similarity alone does not establish that layers are removable. Gradient norms measure local sensitivity. Noise sensitivity is not an actual INT4/INT8 quantization evaluation. Ablations zero weight matrices while preserving norms and biases; they do not physically remove modules. Changed weights are restored, and failed restoration aborts the experiment.
41
+
42
+ ## Data contributions
43
+
44
+ Research retention is opt-in through a separate contribution button after a report is displayed. Consented data is stored in a **private** Hugging Face dataset for BoL model-analysis research. By default a contribution includes model metadata, numerical settings, and numerical measurements. Custom text, token labels, and outputs require a second explicit permission. Read tokens are never included in reports or contributions. Contributions identify the run rather than the visitor's Hugging Face account.
45
+
46
+ Uncontributed report files expire after about an hour. Checkpoints are cached for serving. Hosting providers may retain their own operational logs according to their policies.
47
+
48
+ Maintainer configuration: set `BOL_CONTRIBUTION_REPO` to a private dataset and `BOL_CONTRIBUTION_TOKEN` as a Space secret with write access. These credentials are used only for consented contributions, never for visitor-selected model access.
app.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """0perating-soon: a browser operating room for language models."""
2
+ import os
3
+ import tempfile
4
+
5
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
6
+ os.environ.setdefault("GRADIO_TEMP_DIR", os.path.join(tempfile.gettempdir(), "bol-gradio"))
7
+ import spaces # Initialize ZeroGPU before any CUDA-touching imports.
8
+ import gradio as gr
9
+
10
+ from bol_space.runtime import DEFAULT_MODEL, DEFAULT_TICKET, analyze_gpu, model_summary, prepare_checkpoint, validate_options
11
+ from bol_space.presentation import create_report, render_result
12
+ from bol_space.contributions import save_contribution
13
+
14
+ TOOLS = [
15
+ ("Weight fingerprint", "fingerprint"),
16
+ ("Semantic silhouette", "silhouette"),
17
+ ("Semantic probe", "semantic"),
18
+ ("Layer similarity · CKA", "cka"),
19
+ ("Attention map", "attention"),
20
+ ("Gradient sensitivity", "gradient"),
21
+ ("Layer sweep", "layer_sweep"),
22
+ ("Component ablation", "component"),
23
+ ("Trisect · layer zones", "trisect"),
24
+ ("Wormhole · layer patterns", "wormhole"),
25
+ ("Noise sensitivity", "noise"),
26
+ ("Layer Duets", "duets"),
27
+ ("Butterfly Effect", "butterfly"),
28
+ ("Context Tug-of-War", "tugofwar"),
29
+ ]
30
+ DESCRIPTIONS = {
31
+ "fingerprint": "Inspect the distribution and scale of weight matrices throughout the model. Statistics describe the checkpoint loaded in BF16.",
32
+ "silhouette": "Follow the gap between related and unrelated word representations through the layers. Use the built-in groups or enter one comma-separated group per line, with at least two words in each of two groups.",
33
+ "semantic": "Explore which word groups sit together in the model's hidden representations.",
34
+ "cka": "Compare how sampled layers represent the same probes. Similar representations can still have different roles.",
35
+ "attention": "See how individual heads distribute attention over your text. Use a sentence or paragraph so heads have something to attend to.",
36
+ "gradient": "Measure how sensitive your text's next-token loss is to each layer's weights.",
37
+ "layer_sweep": "Zero one sampled layer's weight matrices at a time and measure the change in loss. Norms and biases are preserved; weights are restored after each intervention.",
38
+ "component": "Zero a projection type across selected layers and compare the result with the original model. Choose All available to discover the components this architecture exposes.",
39
+ "trisect": "Compare the model's inner, outer, and half-depth zones by zeroing the other zones' weight matrices.",
40
+ "wormhole": "Try alternating layers, outer layers, and other keep patterns. This zeros matrices in the unwanted layers; it does not physically remove the layers.",
41
+ "noise": "Add controlled random noise to sampled layers and measure sensitivity. This is a noise experiment, not a prediction of INT4 or INT8 quantization quality.",
42
+ "duets": "Find layer partnerships: compare individual ablations with paired ablations. Positive interaction means the pair caused more loss damage than the sum of the individual effects. Up to six layers are paired.",
43
+ "butterfly": "Change a small piece of text and watch the representations diverge at matching downstream tokens. Keep a shared ending in both texts for alignment.",
44
+ "tugofwar": "Choose two access codes. The experiment states each code as a fact, then tests both orders and repeats each fact in turn. Your text is a shared preamble before these four scenarios. Scores cover each complete candidate continuation.",
45
+ }
46
+ CSS = """
47
+ .gradio-container {max-width:1440px !important; background:#f7f5ef; color:#22343e;}
48
+ body {background:#f7f5ef;}
49
+ #masthead {border-bottom:1px solid #cbd3ca; padding:20px 2px 26px; margin-bottom:18px;}
50
+ #masthead .eyebrow {font:600 12px monospace; color:#56786a; letter-spacing:.18em;}
51
+ #masthead h1 {font-size:clamp(35px,5vw,64px); line-height:1.02; letter-spacing:-.055em; color:#173a3b; margin:12px 0;}
52
+ #masthead p {font-size:17px; color:#5b6869; max-width:680px; margin:0;}
53
+ #run-button {background:#176d62 !important; border-color:#176d62 !important; color:white !important; min-height:48px;}
54
+ #instrument-note {border-left:3px solid #d69a61; padding-left:14px; margin:10px 0 18px;}
55
+ .block {border-radius:10px !important;}
56
+ #results-panel {min-height:550px;}
57
+ .quiet-note {font-size:12px; color:#687477;}
58
+ footer {display:none !important;}
59
+ """
60
+
61
+
62
+ def begin_run():
63
+ """Invalidate the prior result and permission before preparing another run."""
64
+ return (None, None, None, False, False, "", None,
65
+ "**Preparing your experiment…**", None, [], "", None, None,
66
+ gr.update(interactive=False), gr.update(interactive=False),
67
+ "Preparing the selected checkpoint…")
68
+
69
+
70
+ def prepare_request(repo_id, revision, access_token, tool, text, compare_text, prompt, words,
71
+ max_tokens, max_new_tokens, sample_layers, layer_start, layer_end,
72
+ component, noise_scale, seed, choice_a, choice_b):
73
+ """Validate and download before GPU allocation; clear consent for API callers too."""
74
+ # The browser clears immediately through begin_run. Yielding a reset here
75
+ # also invalidates old server state when /prepare is called directly by API.
76
+ yield None, None, None, False, False, "", gr.update(interactive=False), "Preparing the selected checkpoint…"
77
+ try:
78
+ options = validate_options(tool, {"text": text, "compare_text": compare_text, "prompt": prompt,
79
+ "words": words, "max_tokens": max_tokens, "max_new_tokens": max_new_tokens,
80
+ "sample_layers": sample_layers, "layer_start": layer_start, "layer_end": layer_end,
81
+ "component": component, "noise_scale": noise_scale, "seed": seed,
82
+ "choice_a": choice_a, "choice_b": choice_b})
83
+ ticket = prepare_checkpoint(repo_id, revision, access_token)
84
+ yield ({"ticket": ticket, "tool": tool, "options": options}, None, None,
85
+ False, False, "", gr.update(interactive=False), model_summary(ticket))
86
+ except Exception as exc:
87
+ raise gr.Error(str(exc)) from exc
88
+
89
+
90
+ def finish_report(result):
91
+ """Render measured output and create private, temporary report downloads."""
92
+ if not result or not result.get("run", {}).get("id"):
93
+ raise gr.Error("Run an experiment before creating a report. This session has no completed measurements.")
94
+ summary, figure, frame, comparisons = render_result(result)
95
+ json_path, html_path = create_report(result)
96
+ return (result, summary, figure, frame, comparisons, json_path, html_path,
97
+ result, False, False, "", gr.update(interactive=True), gr.update(interactive=True))
98
+
99
+
100
+ def failed_run():
101
+ """Keep contributions unavailable after any failed stage and allow a retry."""
102
+ return (None, None, None, False, False, "", gr.update(interactive=False),
103
+ gr.update(interactive=True), "**The experiment did not finish.** Adjust its settings and try again.")
104
+
105
+
106
+ def contribute(result, consent, include_text):
107
+ """Retain a completed report only after explicit permission."""
108
+ try:
109
+ return save_contribution(result, consent, include_text), False, False
110
+ except Exception as exc:
111
+ raise gr.Error(str(exc)) from exc
112
+
113
+
114
+ def describe_tool(tool):
115
+ semantic = tool in {"silhouette", "semantic", "cka"}
116
+ return (DESCRIPTIONS[tool], gr.update(visible=tool == "butterfly"),
117
+ gr.update(visible=tool == "tugofwar"), gr.update(visible=semantic),
118
+ gr.update(visible=not semantic and tool != "fingerprint",
119
+ label="Context preamble" if tool == "tugofwar" else "Text to analyze",
120
+ info="Added before each access-code scenario." if tool == "tugofwar" else None))
121
+
122
+
123
+ theme = gr.themes.Base(primary_hue="teal", secondary_hue="orange", neutral_hue="stone",
124
+ font=[gr.themes.GoogleFont("DM Sans"), "Arial", "sans-serif"])
125
+ with gr.Blocks(title="0perating-soon", delete_cache=(900, 3600)) as demo:
126
+ request_state = gr.State(value=None, time_to_live=3600)
127
+ computed_state = gr.State(value=None, time_to_live=3600)
128
+ report_state = gr.State(value=None, time_to_live=3600)
129
+ gr.HTML("""<div id="masthead"><div class="eyebrow">BOL TOOLS / MODEL OPERATING ROOM</div>
130
+ <h1>0perating-soon<span style="color:#d59a61">.</span></h1>
131
+ <p>Open a language model. Inspect its structure. Find out what changes when you intervene.</p></div>""")
132
+ with gr.Row(equal_height=False):
133
+ with gr.Column(scale=4, min_width=310):
134
+ gr.Markdown("### 01 / Admit a model")
135
+ repo = gr.Textbox(value=DEFAULT_MODEL, label="Hugging Face checkpoint", placeholder="organization/model-name")
136
+ with gr.Accordion("Revision & private model access", open=False):
137
+ revision = gr.Textbox(value="main", label="Revision", info="A branch, tag, or commit. Reports record the resolved commit.")
138
+ access_token = gr.Textbox(value="", type="password", label="Your Hugging Face read token (optional)", info="For private or gated models you can access. Used to download the checkpoint; excluded from reports and contributions.")
139
+ model_status = gr.Markdown(model_summary(DEFAULT_TICKET))
140
+ gr.Markdown("### 02 / Choose an experiment")
141
+ tool = gr.Dropdown(choices=TOOLS, value="silhouette", label="Instrument", filterable=True)
142
+ note = gr.Markdown(DESCRIPTIONS["silhouette"], elem_id="instrument-note")
143
+ text = gr.Textbox(value="The capital of France is Paris. Water freezes at zero degrees Celsius. The cat sat beside the window.", label="Text to analyze", lines=4, visible=False, max_length=10000)
144
+ compare_text = gr.Textbox(value="The capital of France is Lyon. Water freezes at zero degrees Celsius. The cat sat beside the window.", label="Comparison text", lines=4, visible=False, max_length=10000)
145
+ with gr.Row(visible=False) as candidates:
146
+ choice_a = gr.Textbox(value=" MAPLE", label="Candidate A", max_length=300)
147
+ choice_b = gr.Textbox(value=" RAVEN", label="Candidate B", max_length=300)
148
+ words = gr.Textbox(value="", label="Word probes (optional)", info="One comma-separated group per line. Silhouette needs at least two groups with two words each. Leave empty for the built-in groups.", lines=3, visible=True, max_length=10000)
149
+ with gr.Accordion("Experiment settings", open=False):
150
+ max_tokens = gr.Slider(32, 256, value=128, step=16, label="Input token limit")
151
+ sample_layers = gr.Slider(2, 16, value=8, step=1, label="Sampled layers", info="Duets uses at most six. Zone and pattern experiments use the full selected range.")
152
+ with gr.Row():
153
+ layer_start = gr.Number(value=0, precision=0, minimum=0, label="First layer")
154
+ layer_end = gr.Number(value=-1, precision=0, minimum=-1, label="Last layer (-1 = final)")
155
+ component = gr.Textbox(value="All available", label="Component", info="Run All available first; then paste a component name from its report.")
156
+ noise_scale = gr.Slider(0, 0.5, value=0.01, step=0.005, label="Noise strength")
157
+ seed = gr.Number(value=42, precision=0, minimum=0, label="Random seed")
158
+ prompt = gr.Textbox(value="The capital of France is", label="Generation probe", max_length=10000)
159
+ max_new_tokens = gr.Slider(1, 40, value=16, step=1, label="Generated tokens")
160
+ run_button = gr.Button("Run experiment →", variant="primary", elem_id="run-button")
161
+ gr.Markdown("BF16 checkpoints · FP32 statistics · on-demand ZeroGPU\n\nThe first run of a new model includes its download. GPU time and memory are measured in every report.", elem_classes=["quiet-note"])
162
+
163
+ with gr.Column(scale=7, min_width=480, elem_id="results-panel"):
164
+ gr.Markdown("### 03 / Read the evidence")
165
+ summary = gr.Markdown("**Your model, under the instruments.**\n\nStart with Semantic silhouette for a quick look through its layers, or choose Layer Duets to test how layers work together.")
166
+ chart = gr.Plot(label="Measured structure")
167
+ with gr.Tabs():
168
+ with gr.Tab("Measurements"):
169
+ table = gr.Dataframe(interactive=False, label="Results", wrap=True)
170
+ with gr.Tab("Before / after"):
171
+ comparisons = gr.Markdown("Text comparisons appear here for experiments that generate or score continuations.")
172
+ with gr.Tab("Raw report"):
173
+ result_json = gr.JSON(label="Complete measurement record")
174
+ gr.Markdown("### 04 / Take your report")
175
+ with gr.Row():
176
+ json_report = gr.File(label="Download JSON", interactive=False)
177
+ html_report = gr.File(label="Download printable HTML", interactive=False)
178
+ gr.Markdown("The HTML report opens offline and can be printed or saved as a PDF from your browser.", elem_classes=["quiet-note"])
179
+ with gr.Accordion("Contribute this run to BoL", open=False):
180
+ gr.Markdown("Optional: help compare model structure across checkpoints. Contributions are stored **privately for BoL research**. Your report is available whether you contribute or not.")
181
+ consent = gr.Checkbox(value=False, label="Allow BoL to keep this run's checkpoint ID and revision, settings, and numerical measurements (including private checkpoint IDs).")
182
+ include_text = gr.Checkbox(value=False, label="Also include my custom text, token labels, and generated outputs.")
183
+ gr.Markdown("Without the second permission, text fields and token labels are excluded. Uncontributed reports are temporary and expire after about one hour.", elem_classes=["quiet-note"])
184
+ save = gr.Button("Contribute this report", variant="secondary", interactive=False)
185
+ contribution_status = gr.Markdown()
186
+
187
+ controls = [repo, revision, access_token, tool, text, compare_text, prompt, words, max_tokens,
188
+ max_new_tokens, sample_layers, layer_start, layer_end, component, noise_scale, seed, choice_a, choice_b]
189
+ tool.change(describe_tool, inputs=tool, outputs=[note, compare_text, candidates, words, text], queue=False, api_visibility="private")
190
+ started = run_button.click(begin_run, inputs=None,
191
+ outputs=[request_state, computed_state, report_state, consent, include_text, contribution_status,
192
+ result_json, summary, chart, table, comparisons, json_report, html_report, save, run_button, model_status],
193
+ queue=False, api_visibility="private")
194
+ prepared = started.success(prepare_request, inputs=controls,
195
+ outputs=[request_state, computed_state, report_state, consent, include_text, contribution_status, save, model_status],
196
+ api_name="prepare", concurrency_id="model_experiment", concurrency_limit=1)
197
+ computed = prepared.success(analyze_gpu, inputs=request_state, outputs=computed_state,
198
+ api_name="analyze", concurrency_id="model_experiment", concurrency_limit=1)
199
+ reported = computed.success(finish_report, inputs=computed_state,
200
+ outputs=[result_json, summary, chart, table, comparisons, json_report, html_report, report_state, consent, include_text, contribution_status, save, run_button],
201
+ api_name="report", concurrency_id="model_experiment", concurrency_limit=1)
202
+ for stage in (prepared, computed, reported):
203
+ stage.failure(failed_run, inputs=None,
204
+ outputs=[request_state, computed_state, report_state, consent, include_text, contribution_status, save, run_button, summary],
205
+ queue=False, api_visibility="private")
206
+ save.click(contribute, inputs=[report_state, consent, include_text], outputs=[contribution_status, consent, include_text],
207
+ api_name="contribute", concurrency_limit=1)
208
+ demo.queue(max_size=24, default_concurrency_limit=1)
209
+
210
+ if __name__ == "__main__":
211
+ demo.launch(theme=theme, css=CSS, show_error=True)
bol_space/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Interactive BoL diagnostics for Hugging Face Spaces."""
bol_space/checkpoints.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared checkpoint loading for the Space and standalone BoL commands.
2
+
3
+ Importing this module does not load a checkpoint. The caller initializes spaces
4
+ before importing it when running in ZeroGPU.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import torch
9
+ from huggingface_hub import HfApi, snapshot_download
10
+ from huggingface_hub.utils import validate_repo_id
11
+ from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
12
+
13
+ DEFAULT_MODEL = "HuggingFaceTB/SmolLM2-1.7B-Instruct"
14
+
15
+
16
+ def prepare_checkpoint(repo_id: str, revision: str = "main", access_token: str = "") -> dict:
17
+ """Resolve access and download only data files before entering the GPU queue."""
18
+ repo_id, revision = repo_id.strip(), revision.strip() or "main"
19
+ validate_repo_id(repo_id)
20
+ token = access_token.strip() or False
21
+ try:
22
+ info = HfApi(token=token).model_info(repo_id, revision=revision, files_metadata=True)
23
+ except Exception as exc:
24
+ raise ValueError("Cannot access that checkpoint. Check the model ID/revision and, for gated or private models, your read token and model access.") from exc
25
+ filenames = [item.rfilename for item in info.siblings if "/" not in item.rfilename]
26
+ weights = [name for name in filenames if name.endswith(".safetensors") and not name.startswith(("optimizer", "adapter_"))]
27
+ if not weights:
28
+ raise ValueError("Choose a complete, unquantized Hugging Face checkpoint with Safetensors weights. GGUF, adapter-only, and pickle checkpoints are not supported.")
29
+ # Reject incompatible formats before downloading their weights.
30
+ config = AutoConfig.from_pretrained(repo_id, revision=info.sha, token=token, trust_remote_code=False)
31
+ if getattr(config, "quantization_config", None):
32
+ raise ValueError("This checkpoint is quantized. Use its original BF16/FP16 Hugging Face checkpoint for weight interventions and gradients.")
33
+ data_files = [name for name in filenames if name.endswith((".json", ".model", ".tiktoken")) or name in {"merges.txt", "vocab.txt"}]
34
+ # Pin all files to the resolved revision, including tokenizer and config.
35
+ path = snapshot_download(repo_id, revision=info.sha, token=token, allow_patterns=weights + data_files)
36
+ count = getattr(info.safetensors, "total", None) if info.safetensors else None
37
+ return {"repo_id": repo_id, "revision": info.sha, "path": path, "architecture": config.model_type,
38
+ "parameters": count, "dtype": "bfloat16", "private": bool(info.private)}
39
+
40
+
41
+ def load_checkpoint(ticket: dict):
42
+ """Use native floating-point modules so inspection and surgery remain available."""
43
+ tokenizer = AutoTokenizer.from_pretrained(ticket["path"], trust_remote_code=False, local_files_only=True)
44
+ if tokenizer.pad_token_id is None:
45
+ if tokenizer.eos_token_id is None:
46
+ raise ValueError("This tokenizer needs a padding or EOS token for batched analysis.")
47
+ tokenizer.pad_token = tokenizer.eos_token
48
+ model = AutoModelForCausalLM.from_pretrained(
49
+ ticket["path"], dtype=torch.bfloat16, trust_remote_code=False,
50
+ use_safetensors=True, local_files_only=True, attn_implementation="eager",
51
+ ).eval()
52
+ device = "cuda" if torch.cuda.is_available() else "cpu"
53
+ return model.to(device), tokenizer
54
+
55
+
bol_space/contributions.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Explicitly consented research contributions, kept in a private Hub dataset."""
2
+ from datetime import datetime, timezone
3
+ import json
4
+ import os
5
+ import uuid
6
+
7
+ from huggingface_hub import HfApi
8
+ from bol_space.presentation import contribution_payload
9
+
10
+
11
+ def save_contribution(result: dict | None, consent: bool, include_text: bool) -> str:
12
+ """Save a completed run only after the user explicitly consents."""
13
+ if consent is not True:
14
+ raise ValueError("Check the contribution permission box first. Your report remains available without contributing.")
15
+ if not result or not result.get("run", {}).get("id"):
16
+ raise ValueError("Run an experiment before contributing a report.")
17
+ repo = os.environ.get("BOL_CONTRIBUTION_REPO", "")
18
+ token = os.environ.get("BOL_CONTRIBUTION_TOKEN", "")
19
+ if not repo or not token:
20
+ raise ValueError("Contributions are not configured yet. You can still download your report.")
21
+ api = HfApi(token=token)
22
+ if not api.dataset_info(repo).private:
23
+ raise ValueError("Contribution storage is not private. No report was saved.")
24
+ payload = contribution_payload(result, include_text=include_text is True)
25
+ payload["consent"] = {"version": "1", "measurements": True, "include_text": include_text is True,
26
+ "timestamp": datetime.now(timezone.utc).isoformat(), "purpose": "Private BoL model-analysis research"}
27
+ content = json.dumps(payload, allow_nan=False, ensure_ascii=False).encode("utf-8")
28
+ contribution_id = uuid.uuid4().hex
29
+ api.upload_file(path_or_fileobj=content, path_in_repo=f"runs/{contribution_id}.json", repo_id=repo, repo_type="dataset", commit_message="Add a consented BoL research contribution")
30
+ return f"Saved privately for BoL research. Contribution ID: {contribution_id}. " + ("Your custom text and outputs were included." if include_text else "Custom text, token labels, and generated outputs were excluded.")
bol_space/inspection.py ADDED
@@ -0,0 +1,578 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Request-local BoL measurements on an already loaded causal language model.
2
+
3
+ No checkpoint loading, persistence, generation, or global model state lives here.
4
+ The caller owns device placement, BF16 loading, request serialization and eval mode.
5
+ Statistics use FP32; casting BF16 values does not recover an FP32 checkpoint.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from difflib import SequenceMatcher
10
+ import math
11
+ import re
12
+
13
+ import torch
14
+ import torch.nn.functional as F
15
+
16
+
17
+ DEFAULT_TEXT = (
18
+ "The quick brown fox crossed the river and stopped beside an old oak tree. "
19
+ "The water reflected the evening sky. A small bird watched from a branch."
20
+ )
21
+ DEFAULT_GROUPS = (
22
+ ("canines", ("dog", "wolf", "puppy")),
23
+ ("felines", ("cat", "kitten", "tiger")),
24
+ ("vehicles", ("car", "truck", "vehicle")),
25
+ ("food", ("banana", "apple", "fruit")),
26
+ ("emotions", ("happy", "joy", "sadness")),
27
+ ("tech", ("code", "python", "programming")),
28
+ ("abstract", ("silence", "peace", "war")),
29
+ )
30
+ RELATED_PAIRS = (
31
+ ("dog", "wolf"), ("cat", "lion"), ("river", "ocean"),
32
+ ("happy", "joyful"), ("cold", "frozen"), ("king", "queen"),
33
+ ("python", "code"), ("car", "vehicle"), ("run", "sprint"),
34
+ )
35
+ UNRELATED_PAIRS = (
36
+ ("dog", "democracy"), ("cat", "algebra"), ("river", "justice"),
37
+ ("happy", "concrete"), ("cold", "philosophy"), ("king", "equation"),
38
+ ("python", "banana"), ("car", "silence"), ("run", "orange"),
39
+ )
40
+
41
+
42
+ def _number(value):
43
+ value = float(value)
44
+ return value if math.isfinite(value) else None
45
+
46
+
47
+ def _device(model):
48
+ return model.get_input_embeddings().weight.device
49
+
50
+
51
+ def _decoder_layers(model):
52
+ for path in ("model.layers", "transformer.h", "gpt_neox.layers", "model.decoder.layers",
53
+ "transformer.blocks", "model.transformer.h"):
54
+ module = model
55
+ for name in path.split("."):
56
+ module = getattr(module, name, None)
57
+ if module is None:
58
+ break
59
+ if isinstance(module, torch.nn.ModuleList) and len(module):
60
+ return module
61
+ raise ValueError(
62
+ "This tool needs a recognized decoder layer list. Supported layouts include "
63
+ "Llama/SmolLM, GPT-2, GPT-NeoX and OPT. This model's layout is not supported yet."
64
+ )
65
+
66
+
67
+ def _selection(model, options):
68
+ layers = _decoder_layers(model)
69
+ start = int(options.get("layer_start") or 0)
70
+ end_value = options.get("layer_end")
71
+ end = len(layers) - 1 if end_value is None or int(end_value) < 0 else int(end_value)
72
+ if start < 0 or start >= len(layers) or end < start or end >= len(layers):
73
+ raise ValueError(f"Choose decoder layers between 0 and {len(layers) - 1} (inclusive).")
74
+ count = min(16, max(1, int(options.get("sample_layers", 8))), end - start + 1)
75
+ indices = [start] if count == 1 else [round(start + i * (end - start) / (count - 1))
76
+ for i in range(count)]
77
+ return layers, indices
78
+
79
+
80
+ def _limit(options):
81
+ return min(256, max(8, int(options.get("max_tokens", 128))))
82
+
83
+
84
+ def _inputs(model, tokenizer, text, limit):
85
+ if not str(text).strip():
86
+ raise ValueError("Enter some text to inspect.")
87
+ encoded = tokenizer(str(text), return_tensors="pt", truncation=True, max_length=limit)
88
+ result = {k: v.to(_device(model)) for k, v in encoded.items()
89
+ if k in ("input_ids", "attention_mask")}
90
+ if "attention_mask" not in result:
91
+ result["attention_mask"] = torch.ones_like(result["input_ids"])
92
+ if not result["input_ids"].numel():
93
+ raise ValueError("The tokenizer returned no tokens for this input.")
94
+ return result
95
+
96
+
97
+ def _forward(model, inputs, **kwargs):
98
+ return model(**inputs, use_cache=False, return_dict=True, **kwargs)
99
+
100
+
101
+ def _states(output, selected, count):
102
+ hidden = getattr(output, "hidden_states", None)
103
+ if hidden is None or len(hidden) != count + 1:
104
+ raise ValueError("This model did not return one hidden state per decoder layer plus embeddings.")
105
+ return {i: hidden[i + 1] for i in selected}
106
+
107
+
108
+ def _plot(kind, title, x_label, y_label, **data):
109
+ return {"kind": kind, "title": title, "x_label": x_label, "y_label": y_label, **data}
110
+
111
+
112
+ def _result(title, summary, rows, plot, details=None, warnings=None, text_outputs=None):
113
+ return {"title": title, "summary": summary, "rows": rows, "plot": plot,
114
+ "details": details or {}, "warnings": warnings or [], "text_outputs": text_outputs or []}
115
+
116
+
117
+ def _line(title, rows, metrics, x="layer"):
118
+ return _plot("line", title, "Decoder layer (zero based)", "Measurement", series=[
119
+ {"name": name, "x": [row[x] for row in rows], "y": [row[metric] for row in rows]}
120
+ for name, metric in metrics
121
+ ])
122
+
123
+
124
+ def _fingerprint(model, tokenizer, options):
125
+ layers, selected = _selection(model, options)
126
+ rows = []
127
+ layer_rows = []
128
+ seed = int(options.get("seed", 42))
129
+ generator = torch.Generator(device="cpu").manual_seed(seed)
130
+ with torch.no_grad():
131
+ for index in selected:
132
+ total_squared = 0.
133
+ elements = 0
134
+ for name, parameter in layers[index].named_parameters():
135
+ if parameter.ndim != 2:
136
+ continue
137
+ values = parameter.detach().float()
138
+ norm = values.norm().item()
139
+ # Bounded power iteration: an estimate, never an exact SVD claim.
140
+ vector = torch.randn(values.shape[1], generator=generator).to(values.device)
141
+ vector = F.normalize(vector, dim=0)
142
+ for _ in range(8):
143
+ left = F.normalize(values @ vector, dim=0)
144
+ vector = F.normalize(values.T @ left, dim=0)
145
+ spectral = (values @ vector).norm().item()
146
+ rows.append({
147
+ "layer": index, "tensor": name, "shape": list(parameter.shape),
148
+ "mean": _number(values.mean()), "std": _number(values.std(unbiased=False)),
149
+ "abs_mean": _number(values.abs().mean()), "minimum": _number(values.min()),
150
+ "maximum": _number(values.max()), "frobenius": _number(norm),
151
+ "sparsity": _number((values.abs() < 1e-6).float().mean()),
152
+ "spectral_estimate": _number(spectral),
153
+ })
154
+ total_squared += norm * norm
155
+ elements += values.numel()
156
+ del values, vector, left
157
+ if elements:
158
+ layer_rows.append({"layer": index, "weight_rms": math.sqrt(total_squared / elements)})
159
+ if not rows:
160
+ raise ValueError("No dense 2D weight matrices were found in the selected decoder layers.")
161
+ return _result(
162
+ "Weight fingerprint", f"Measured {len(rows)} matrices across {len(selected)} selected decoder layers.",
163
+ rows, _line("Weight scale through the model", layer_rows, [("Weight RMS", "weight_rms")]),
164
+ {"selected_layers": selected, "layer_rms": layer_rows, "spectral_power_iterations": 8,
165
+ "sparsity_threshold": 1e-6, "checkpoint_values": "Loaded model values, converted to FP32 for statistics"},
166
+ ["Spectral norms use eight power iterations and are estimates. Condition numbers are not estimated.",
167
+ "A BF16 fingerprint describes rounded BF16 weights, including when the source checkpoint was FP32."],
168
+ )
169
+
170
+
171
+ def _groups(options):
172
+ raw = str(options.get("words") or "").strip()
173
+ if not raw:
174
+ return [(name, list(words)) for name, words in DEFAULT_GROUPS], False
175
+ lines = [line.strip() for line in raw.splitlines() if line.strip()]
176
+ groups = []
177
+ if len(lines) >= 2 and all("," in line for line in lines):
178
+ for index, line in enumerate(lines):
179
+ groups.append((f"Group {index + 1}", [w.strip() for w in line.split(",") if w.strip()]))
180
+ else:
181
+ groups = [("Custom words", [w.strip() for w in re.split(r"[,\n]", raw) if w.strip()])]
182
+ words = [word for _, group in groups for word in group]
183
+ if len(words) < 3 or len(words) > 32:
184
+ raise ValueError("Use 3–32 words or short phrases. For groups, put comma-separated words on separate lines.")
185
+ if len(set(words)) != len(words):
186
+ raise ValueError("Each probe word or phrase must appear only once.")
187
+ if any(len(word) > 160 for word in words):
188
+ raise ValueError("Keep each probe word or phrase below 160 characters.")
189
+ return groups, True
190
+
191
+
192
+ def _word_vectors(model, tokenizer, options, probe_words=None):
193
+ layers, selected = _selection(model, options)
194
+ if probe_words is None:
195
+ groups, custom = _groups(options)
196
+ words = [word for _, group in groups for word in group]
197
+ else:
198
+ groups, custom, words = [], False, list(probe_words)
199
+ vectors = {index: [] for index in selected}
200
+ with torch.no_grad():
201
+ for word in words:
202
+ inputs = _inputs(model, tokenizer, word, min(32, _limit(options)))
203
+ out = _forward(model, inputs, output_hidden_states=True)
204
+ last = int(inputs["attention_mask"][0].nonzero()[-1])
205
+ for index, state in _states(out, selected, len(layers)).items():
206
+ vectors[index].append(state[0, last].detach().float().cpu())
207
+ del out
208
+ return {index: torch.stack(items) for index, items in vectors.items()}, groups, words, custom
209
+
210
+
211
+ def _paired_silhouette(model, tokenizer, options):
212
+ """Preserve the original BoL silhouette's explicitly specified word pairs."""
213
+ words = list(dict.fromkeys(word for pair in RELATED_PAIRS + UNRELATED_PAIRS for word in pair))
214
+ vectors, _, words, _ = _word_vectors(model, tokenizer, options, probe_words=words)
215
+ positions = {word: index for index, word in enumerate(words)}
216
+ rows = []
217
+ for layer, states in vectors.items():
218
+ normalized = F.normalize(states, dim=-1)
219
+ means = []
220
+ for pairs in (RELATED_PAIRS, UNRELATED_PAIRS):
221
+ similarities = [(normalized[positions[a]] * normalized[positions[b]]).sum().item()
222
+ for a, b in pairs]
223
+ means.append(sum(similarities) / len(similarities))
224
+ rows.append({"layer": layer, "within_cosine": means[0], "cross_cosine": means[1],
225
+ "separation": means[0] - means[1]})
226
+ return _result(
227
+ "Semantic silhouette",
228
+ f"Compared the original nine related and nine unrelated word pairs at {len(vectors)} selected layers.",
229
+ rows, _line("Related versus unrelated word-pair geometry", rows,
230
+ [("Related", "within_cosine"), ("Unrelated", "cross_cosine"), ("Gap", "separation")]),
231
+ {"selected_layers": list(vectors), "custom_probes": False, "words": words,
232
+ "probe_contract": "Original BoL silhouette: mean cosine of nine specified related pairs minus mean cosine of nine specified unrelated pairs",
233
+ "related_pairs": [list(pair) for pair in RELATED_PAIRS],
234
+ "unrelated_pairs": [list(pair) for pair in UNRELATED_PAIRS],
235
+ "pooling": "Last non-padding token; each word processed in isolation",
236
+ "row_metrics": {"within_cosine": "Mean specified related-pair cosine", "cross_cosine": "Mean specified unrelated-pair cosine"}},
237
+ ["This preserves the original word-pair measurement with BF16 model execution and FP32 statistics.",
238
+ "Cosine separation is a descriptive gap, not a standard silhouette coefficient, comprehension test or causal importance score.",
239
+ "Probe pairs share words and are not independent. No significance test is reported.",
240
+ "The last returned hidden state may include the model's final normalization."],
241
+ )
242
+
243
+
244
+ def _semantic(model, tokenizer, options, silhouette=False):
245
+ if silhouette and not str(options.get("words") or "").strip():
246
+ return _paired_silhouette(model, tokenizer, options)
247
+ vectors, groups, words, custom = _word_vectors(model, tokenizer, options)
248
+ membership = [name for name, group in groups for _ in group]
249
+ comparable = len(groups) >= 2 and all(len(group) >= 2 for _, group in groups)
250
+ if silhouette and not comparable:
251
+ raise ValueError("Silhouette needs at least two groups of two words. Put each comma-separated group on its own line.")
252
+ matrices = {index: F.normalize(values, dim=-1) @ F.normalize(values, dim=-1).T
253
+ for index, values in vectors.items()}
254
+ rows = []
255
+ for index, similarity in matrices.items():
256
+ if comparable:
257
+ within, cross = [], []
258
+ for a in range(len(words)):
259
+ for b in range(a + 1, len(words)):
260
+ (within if membership[a] == membership[b] else cross).append(float(similarity[a, b]))
261
+ w, c = sum(within) / len(within), sum(cross) / len(cross)
262
+ rows.append({"layer": index, "within_cosine": w, "cross_cosine": c, "separation": w - c})
263
+ display = max(matrices)
264
+ if not comparable:
265
+ rows = [{"layer": display, "word_a": words[a], "word_b": words[b],
266
+ "cosine": float(matrices[display][a, b])}
267
+ for a in range(len(words)) for b in range(a + 1, len(words))]
268
+ plot = (_line("Related versus cross-group geometry", rows,
269
+ [("Within group", "within_cosine"), ("Across groups", "cross_cosine"), ("Gap", "separation")])
270
+ if silhouette else _plot("heatmap", f"Word cosine similarity · layer {display}", "Word", "Word",
271
+ x=words, y=words, z=matrices[display].tolist()))
272
+ return _result(
273
+ "Semantic silhouette" if silhouette else "Semantic probe",
274
+ f"Compared final-token representations of {len(words)} isolated probes at {len(vectors)} layers.",
275
+ rows, plot,
276
+ {"selected_layers": list(vectors), "groups": dict(groups), "display_layer": display,
277
+ "custom_probes": custom, "pooling": "Last non-padding token; each phrase processed in isolation",
278
+ "probe_contract": "Mean within-group cosine minus mean cross-group cosine" if comparable else "Ungrouped pairwise cosine similarities",
279
+ "similarity_matrix": matrices[display].tolist()},
280
+ ["Cosine separation is a descriptive gap, not a standard silhouette coefficient, comprehension test or causal importance score.",
281
+ "Probe pairs share words and are not independent. No significance test is reported.",
282
+ "The last returned hidden state may include the model's final normalization."],
283
+ )
284
+
285
+
286
+ def _linear_cka(x, y):
287
+ """Biased linear CKA on aligned samples; None for degenerate representations."""
288
+ if x.shape[0] != y.shape[0] or x.shape[0] < 3:
289
+ raise ValueError("CKA needs at least three aligned samples.")
290
+ x, y = x.float(), y.float()
291
+ x, y = x - x.mean(0, keepdim=True), y - y.mean(0, keepdim=True)
292
+ # Remove arbitrary activation scale before Gram products. An absolute
293
+ # denominator cutoff would incorrectly call small but varying states null.
294
+ scale_x, scale_y = x.abs().max(), y.abs().max()
295
+ if scale_x.item() == 0 or scale_y.item() == 0:
296
+ return None
297
+ x, y = x / scale_x, y / scale_y
298
+ k, l = x @ x.T, y @ y.T
299
+ denom = k.norm() * l.norm()
300
+ if not torch.isfinite(denom) or denom.item() == 0:
301
+ return None
302
+ return min(1., max(0., float((k * l).sum() / denom)))
303
+
304
+
305
+ def _cka(model, tokenizer, options):
306
+ vectors, groups, words, custom = _word_vectors(model, tokenizer, options)
307
+ indices = list(vectors)
308
+ matrix = [[_linear_cka(vectors[a], vectors[b]) for b in indices] for a in indices]
309
+ return _result(
310
+ "Layer CKA", f"Compared representation geometry across {len(indices)} layers using {len(words)} aligned probes.",
311
+ [{"layer_a": a, "layer_b": b, "cka": matrix[i][j]}
312
+ for i, a in enumerate(indices) for j, b in enumerate(indices)],
313
+ _plot("heatmap", "Linear centered kernel alignment", "Decoder layer", "Decoder layer",
314
+ x=indices, y=indices, z=matrix),
315
+ {"words": words, "selected_layers": indices, "custom_probes": custom,
316
+ "estimator": "Biased linear CKA of centered sample Gram matrices", "sample_count": len(words)},
317
+ ["CKA depends on this small probe set; it does not show whether a layer is dispensable.",
318
+ "Null entries mean a representation has no measurable centered variation.",
319
+ "All selected layer representations are extracted from the same forward pass for each probe."],
320
+ )
321
+
322
+
323
+ def _attention(model, tokenizer, options):
324
+ layers, selected = _selection(model, options)
325
+ inputs = _inputs(model, tokenizer, options.get("text") or DEFAULT_TEXT, _limit(options))
326
+ length = inputs["input_ids"].shape[1]
327
+ if length < 3:
328
+ raise ValueError("Attention needs a phrase or sentence with at least three tokens.")
329
+ with torch.no_grad():
330
+ out = _forward(model, inputs, output_attentions=True)
331
+ attentions = getattr(out, "attentions", None)
332
+ if attentions is None or len(attentions) != len(layers):
333
+ raise ValueError("This model did not expose layer attention weights. Use an eager-attention transformer checkpoint.")
334
+ rows, heatmap, last_maps = [], [], {}
335
+ for index in selected:
336
+ if attentions[index] is None:
337
+ raise ValueError("This layer does not expose attention weights; hybrid/SSM attention is not supported here.")
338
+ p = attentions[index][0].detach().float()
339
+ if p.shape[-2:] != (length, length):
340
+ raise ValueError("This model returned a nonstandard attention layout.")
341
+ p = p / p.sum(-1, keepdim=True).clamp_min(1e-12)
342
+ entropies = -(p * p.clamp_min(1e-12).log()).sum(-1)
343
+ valid = inputs["attention_mask"][0].bool()
344
+ # Each query has its own count of causal, non-padding key positions.
345
+ available = valid.long().cumsum(0).float()
346
+ normalizers = available.clamp_min(2).log()
347
+ use_queries = valid & (available > 1)
348
+ normalized = entropies[:, use_queries] / normalizers[use_queries]
349
+ last = int(valid.nonzero()[-1])
350
+ patterns = p[:, last, :]
351
+ cosines = F.normalize(patterns, dim=-1) @ F.normalize(patterns, dim=-1).T
352
+ offdiag = ~torch.eye(p.shape[0], dtype=torch.bool, device=p.device)
353
+ diversity = float((1 - cosines[offdiag]).mean()) if p.shape[0] > 1 else 0.
354
+ head_values = []
355
+ for head in range(p.shape[0]):
356
+ value = float(normalized[head].mean())
357
+ rows.append({"layer": index, "head": head, "entropy": float(entropies[head, use_queries].mean()),
358
+ "normalized_entropy": value, "head_diversity": diversity,
359
+ "last_token_entropy": float(entropies[head, last]),
360
+ "last_token_normalized_entropy": float(entropies[head, last] / normalizers[last])})
361
+ head_values.append(value)
362
+ heatmap.append(head_values)
363
+ last_maps[str(index)] = patterns.cpu().tolist()
364
+ return _result(
365
+ "Attention map", f"Measured {len(selected)} layers over {length} tokens, with a separate distribution for every head.",
366
+ rows, _plot("heatmap", "Normalized causal attention entropy", "Head", "Decoder layer",
367
+ x=list(range(len(heatmap[0]))), y=selected, z=heatmap),
368
+ {"selected_layers": selected, "token_count": length,
369
+ "tokens": tokenizer.convert_ids_to_tokens(inputs["input_ids"][0].tolist()),
370
+ "last_token_attention": last_maps,
371
+ "normalization": "Entropy / log(causal non-padding key count), averaged over queries with >1 key"},
372
+ ["Low entropy means concentrated attention; it does not establish understanding or head importance.",
373
+ "Head diversity is the mean pairwise cosine distance of last-token attention distributions.",
374
+ "For sliding-window or other restricted masks, the denominator is the full causal history, so entropy is relative to that history."],
375
+ )
376
+
377
+
378
+ def _gradient(model, tokenizer, options):
379
+ layers, selected = _selection(model, options)
380
+ inputs = _inputs(model, tokenizer, options.get("text") or DEFAULT_TEXT, _limit(options))
381
+ if int(inputs["attention_mask"].sum()) < 2:
382
+ raise ValueError("Gradient sensitivity requires at least two tokens.")
383
+ labels = inputs["input_ids"].clone()
384
+ labels[inputs["attention_mask"] == 0] = -100
385
+ parameters = list(model.parameters())
386
+ flags = [p.requires_grad for p in parameters]
387
+ chosen = [(index, name, p) for index in selected for name, p in layers[index].named_parameters()
388
+ if p.ndim == 2 and p.is_floating_point()]
389
+ if not chosen:
390
+ raise ValueError("No dense floating-point matrix parameters were found in these layers.")
391
+ chosen_ids = {id(p) for _, _, p in chosen}
392
+ try:
393
+ for p in parameters:
394
+ p.requires_grad_(id(p) in chosen_ids)
395
+ with torch.inference_mode(False), torch.enable_grad():
396
+ # Rebuild tensors in case an outer caller created them in inference mode.
397
+ grad_inputs = {k: value.clone() for k, value in inputs.items()}
398
+ outputs = _forward(model, grad_inputs, labels=labels.clone())
399
+ loss = outputs.loss
400
+ if loss is None or not torch.isfinite(loss):
401
+ raise ValueError("The model did not produce a finite causal language-model loss.")
402
+ gradients = torch.autograd.grad(loss, [p for _, _, p in chosen], allow_unused=True)
403
+ loss_value = float(loss.detach())
404
+ aggregates = {index: [0., 0., 0., 0] for index in selected}
405
+ components = []
406
+ for (index, name, p), grad in zip(chosen, gradients):
407
+ if grad is None:
408
+ continue
409
+ g = grad.detach().float()
410
+ grad_sq, param_sq = float(g.square().sum()), float(p.detach().float().square().sum())
411
+ state = aggregates[index]
412
+ state[0] += grad_sq
413
+ state[1] += param_sq
414
+ state[2] += float(g.abs().sum())
415
+ state[3] += g.numel()
416
+ components.append({"layer": index, "tensor": name, "gradient_norm": math.sqrt(grad_sq)})
417
+ del gradients, outputs, loss
418
+ rows = []
419
+ for index, (g2, p2, abs_sum, count) in aggregates.items():
420
+ if count:
421
+ rows.append({"layer": index, "gradient_norm": math.sqrt(g2), "gradient_mean": abs_sum / count,
422
+ "parameter_norm": math.sqrt(p2),
423
+ "gradient_to_param_ratio": math.sqrt(g2) / max(math.sqrt(p2), 1e-12)})
424
+ if not rows:
425
+ raise ValueError("The selected matrices were not connected to this model's returned loss.")
426
+ return _result(
427
+ "Gradient sensitivity", f"Measured local parameter sensitivity at a text loss of {loss_value:.4f} nats per token.",
428
+ rows, _line("Gradient relative to weight scale", rows, [("Gradient / parameter norm", "gradient_to_param_ratio")]),
429
+ {"loss": loss_value, "selected_layers": selected, "components": components,
430
+ "objective": "Mean next-token cross entropy; padding excluded by attention_mask"},
431
+ ["Gradients describe this text and parameterization. They are not causal ablation scores or Fisher information.",
432
+ "Only selected layers' 2D parameters are differentiated; BF16 backward values are summarized in FP32."],
433
+ )
434
+ finally:
435
+ for p, flag in zip(parameters, flags):
436
+ p.requires_grad_(flag)
437
+
438
+
439
+ def _butterfly(model, tokenizer, options):
440
+ layers, selected = _selection(model, options)
441
+ a = str(options.get("text") or DEFAULT_TEXT)
442
+ b = str(options.get("compare_text") or a.replace("fox", "wolf", 1))
443
+ inputs_a = _inputs(model, tokenizer, a, _limit(options))
444
+ inputs_b = _inputs(model, tokenizer, b, _limit(options))
445
+ ids_a, ids_b = inputs_a["input_ids"][0].tolist(), inputs_b["input_ids"][0].tolist()
446
+ prefix = 0
447
+ while prefix < min(len(ids_a), len(ids_b)) and ids_a[prefix] == ids_b[prefix]:
448
+ prefix += 1
449
+ identical = ids_a == ids_b
450
+ pairs = [(block.a + offset, block.b + offset)
451
+ for block in SequenceMatcher(a=ids_a, b=ids_b, autojunk=False).get_matching_blocks()
452
+ for offset in range(block.size)
453
+ if identical or (block.a + offset >= prefix and block.b + offset >= prefix)]
454
+ if not pairs:
455
+ raise ValueError("The tokenized prompts have no matching tokens after the edit. Keep a shared suffix after the changed word or phrase.")
456
+ positions_a, positions_b = zip(*pairs)
457
+ with torch.no_grad():
458
+ out_a = _forward(model, inputs_a, output_hidden_states=True)
459
+ saved = {index: state[0, list(positions_a)].detach().float().cpu()
460
+ for index, state in _states(out_a, selected, len(layers)).items()}
461
+ logits_a = out_a.logits[0, -1].detach().float().cpu()
462
+ del out_a
463
+ out_b = _forward(model, inputs_b, output_hidden_states=True)
464
+ rows = []
465
+ for index, state in _states(out_b, selected, len(layers)).items():
466
+ va, vb = saved[index], state[0, list(positions_b)].detach().float().cpu()
467
+ distance = (1 - F.cosine_similarity(va, vb, dim=-1)).clamp_min(0)
468
+ relative = (vb - va).norm(dim=-1) / va.norm(dim=-1).clamp_min(1e-12)
469
+ rows.append({"layer": index, "cosine_distance": float(distance.mean()), "relative_l2": float(relative.mean())})
470
+ logits_b = out_b.logits[0, -1].detach().float().cpu()
471
+ pa, pb = logits_a.softmax(-1), logits_b.softmax(-1)
472
+ mixture = (pa + pb) / 2
473
+ js = float(((pa * (pa.clamp_min(1e-30).log() - mixture.clamp_min(1e-30).log())).sum()
474
+ + (pb * (pb.clamp_min(1e-30).log() - mixture.clamp_min(1e-30).log())).sum()) / 2)
475
+ top = []
476
+ for token_id in torch.topk(mixture, min(8, mixture.numel())).indices.tolist():
477
+ top.append({"token": tokenizer.decode([token_id]), "probability_a": float(pa[token_id]),
478
+ "probability_b": float(pb[token_id])})
479
+ return _result(
480
+ "Butterfly effect", f"Tracked {len(pairs)} matching token positions through {len(selected)} layers after the prompt edit.",
481
+ rows, _line("How the prompt edit changes downstream states", rows,
482
+ [("Cosine distance", "cosine_distance"), ("Relative L2 change", "relative_l2")]),
483
+ {"selected_layers": selected, "aligned_token_count": len(pairs),
484
+ "alignment": [{"position_a": i, "position_b": j, "token_a": ids_a[i], "token_b": ids_b[j]}
485
+ for i, j in pairs], "next_token_js_divergence_nats": max(0., js),
486
+ "next_token_probabilities": top},
487
+ (["The tokenized inputs are identical after truncation; this is a zero-change control."] if identical else []) +
488
+ ["Matched token IDs after the first difference are compared. Insertions may also change absolute positions.",
489
+ "Distances describe representations, not the model's thoughts. Final hidden states may include normalization."],
490
+ [{"label": "Prompt A", "text": a}, {"label": "Prompt B", "text": b}],
491
+ )
492
+
493
+
494
+ def _candidate_score(model, prompt_ids, continuation):
495
+ if not prompt_ids:
496
+ raise ValueError("The tokenizer returned no prompt tokens.")
497
+ all_ids = torch.tensor([prompt_ids + continuation], dtype=torch.long, device=_device(model))
498
+ with torch.no_grad():
499
+ out = _forward(model, {"input_ids": all_ids, "attention_mask": torch.ones_like(all_ids)})
500
+ start = len(prompt_ids) - 1
501
+ logits = out.logits[0, start:start + len(continuation)].float()
502
+ targets = torch.tensor(continuation, dtype=torch.long, device=logits.device)
503
+ scores = logits.log_softmax(-1).gather(-1, targets[:, None]).squeeze(-1)
504
+ return float(scores.mean()), len(continuation)
505
+
506
+
507
+ def _tugofwar(model, tokenizer, options):
508
+ answer_a = str(options.get("choice_a") or "MAPLE").strip()
509
+ answer_b = str(options.get("choice_b") or "RAVEN").strip()
510
+ if answer_a == answer_b or not answer_a or not answer_b:
511
+ raise ValueError("Choose two different nonempty candidate answers.")
512
+ a, b = " " + answer_a, " " + answer_b
513
+ # Explicit token boundary keeps every candidate token scored. Both candidates
514
+ # receive exactly the same prompt IDs, even when their token counts differ.
515
+ continuation_a = tokenizer(a, add_special_tokens=False)["input_ids"]
516
+ continuation_b = tokenizer(b, add_special_tokens=False)["input_ids"]
517
+ if any(not ids or len(ids) > 32 or len(ids) >= _limit(options)
518
+ for ids in (continuation_a, continuation_b)):
519
+ raise ValueError("Choose nonempty candidate continuations of at most 32 tokens, shorter than the input budget.")
520
+ prompt_budget = _limit(options) - max(len(continuation_a), len(continuation_b))
521
+ fact_a, fact_b = f"The access code is {a.strip()}.", f"The access code is {b.strip()}."
522
+ variants = (("a_then_b", [fact_a, fact_b]), ("b_then_a", [fact_b, fact_a]),
523
+ ("repeat_a", [fact_a, fact_a, fact_b]), ("repeat_b", [fact_a, fact_b, fact_b]))
524
+ preamble = str(options.get("text") or "").strip()
525
+ prepared = []
526
+ for name, facts in variants:
527
+ core = "\n".join(facts) + "\nThe access code is"
528
+ core_ids = tokenizer(core, add_special_tokens=True)["input_ids"]
529
+ if len(core_ids) > prompt_budget:
530
+ raise ValueError(
531
+ "The complete conflicting facts and answer candidates do not fit the token budget. "
532
+ "Increase input tokens or shorten the two candidate answers."
533
+ )
534
+ prompt = ((preamble + "\n\n") if preamble else "") + core
535
+ prompt_ids = tokenizer(prompt, add_special_tokens=True)["input_ids"]
536
+ prepared.append((name, core, core_ids, prompt, prompt_ids))
537
+ # Keep the background identical across variants: omit it from every variant
538
+ # if the largest fact block cannot fit alongside it.
539
+ omitted_preamble = any(len(item[4]) > prompt_budget for item in prepared)
540
+ rows, prompts, contexts = [], [], []
541
+ for name, core, core_ids, prompt, prompt_ids in prepared:
542
+ if omitted_preamble:
543
+ # Preserve the actual intervention: losing a repeated fact would
544
+ # make a 'repeat' label describe an experiment that never ran.
545
+ prompt, prompt_ids = core, core_ids
546
+ score_a, count_a = _candidate_score(model, prompt_ids, continuation_a)
547
+ score_b, count_b = _candidate_score(model, prompt_ids, continuation_b)
548
+ rows.append({"variant": name, "a_mean_logprob": score_a, "b_mean_logprob": score_b,
549
+ "a_minus_b": score_a - score_b, "candidate_a_tokens": count_a, "candidate_b_tokens": count_b})
550
+ prompts.append({"label": name, "text": prompt})
551
+ contexts.append({"variant": name, "prompt_token_ids": prompt_ids,
552
+ "background_was_omitted": omitted_preamble})
553
+ return _result(
554
+ "Context tug-of-war", "Compared two complete candidate continuations under reversed and repeated conflicting facts.",
555
+ rows, _plot("bar", "Which continuation is preferred?", "Context arrangement", "Mean log-probability A minus B",
556
+ series=[{"name": "A − B (positive favors A)", "x": [r["variant"] for r in rows],
557
+ "y": [r["a_minus_b"] for r in rows]}]),
558
+ {"candidate_a": a, "candidate_b": b, "scoring": "Mean token log-probability of the full candidate continuation",
559
+ "token_boundary": "Candidate tokenized separately without special tokens and appended to prompt IDs",
560
+ "answer_whitespace": "Candidate answers are trimmed and prefixed with one space after 'is'",
561
+ "contexts": contexts,
562
+ "truncation": "Omit optional background if necessary; preserve complete fact block and identical prompt IDs for both candidates"},
563
+ ["Scores are length-normalized log probabilities, not calibrated answer probabilities.",
564
+ "This probe tests order and repetition sensitivity for these statements; it is not a general instruction-following benchmark."] +
565
+ (["The optional background was omitted from all variants to fit the token budget. Every experimental fact was preserved."]
566
+ if any(context["background_was_omitted"] for context in contexts) else []),
567
+ prompts,
568
+ )
569
+
570
+
571
+ def run_inspection(model, tokenizer, tool: str, options: dict) -> dict:
572
+ """Run one bounded tool without loading models or retaining request data."""
573
+ handlers = {"fingerprint": _fingerprint, "silhouette": lambda m, t, o: _semantic(m, t, o, True),
574
+ "semantic": _semantic, "cka": _cka, "attention": _attention, "gradient": _gradient,
575
+ "butterfly": _butterfly, "tugofwar": _tugofwar}
576
+ if tool not in handlers:
577
+ raise ValueError(f"Unknown inspection tool: {tool}")
578
+ return handlers[tool](model, tokenizer, dict(options or {}))
bol_space/presentation.py ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Browser presentation, offline reports, and consent-scoped research exports.
2
+
3
+ This module never uploads data. ``contribution_payload`` is deliberately separate
4
+ from the user's complete, local report so a consent choice controls retention.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import html
10
+ import json
11
+ import math
12
+ import numbers
13
+ import os
14
+ import re
15
+ import shutil
16
+ import tempfile
17
+ import uuid
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import pandas as pd
23
+ import plotly.graph_objects as go
24
+ import plotly.io as pio
25
+
26
+
27
+ MEASUREMENT_NOTE = (
28
+ "These measurements describe this checkpoint, precision, input, and selected "
29
+ "settings. A change in this experiment is not a general measure of model "
30
+ "quality or proof that a layer has a particular function. BF16 execution and "
31
+ "sampling can affect small differences; compare runs using matching settings."
32
+ )
33
+
34
+ _PALETTE = ["#167c80", "#d88759", "#25394c", "#7d73a4", "#8c9e6c", "#b85e72"]
35
+ _RESULT_KEYS = {
36
+ "title", "summary", "rows", "plot", "details", "warnings", "text_outputs",
37
+ "model", "run",
38
+ }
39
+ _MODEL_KEYS = {"repo_id", "revision", "architecture", "dtype", "parameters"}
40
+ _RUN_KEYS = {"id", "created_at", "tool", "seconds", "peak_vram_gb", "gpu", "options"}
41
+ _TOOL_IDS = {
42
+ "fingerprint", "silhouette", "semantic", "cka", "attention", "gradient",
43
+ "butterfly", "tugofwar", "layer_sweep", "component", "trisect", "wormhole",
44
+ "noise", "duets",
45
+ }
46
+ _NUMERIC_OPTIONS = {
47
+ "max_tokens", "max_new_tokens", "sample_layers", "layer_start", "layer_end",
48
+ "noise_scale", "seed",
49
+ }
50
+ # Every retained metric name is controlled by the application, never by a token,
51
+ # prompt, candidate answer, custom label, or user-supplied dictionary key.
52
+ _NUMERIC_ROW_KEYS = {
53
+ "layer", "head", "layer_a", "layer_b", "mean", "std", "abs_mean",
54
+ "minimum", "maximum", "frobenius", "sparsity", "spectral_estimate",
55
+ "within_cosine", "cross_cosine", "separation", "cosine", "cka", "entropy",
56
+ "normalized_entropy", "head_diversity", "last_token_entropy",
57
+ "last_token_normalized_entropy", "gradient_norm", "gradient_mean",
58
+ "parameter_norm", "gradient_to_param_ratio", "cosine_distance", "relative_l2",
59
+ "a_mean_logprob", "b_mean_logprob", "a_minus_b", "loss", "damage",
60
+ "modified_tensors", "single_damage_a", "single_damage_b", "pair_damage",
61
+ "interaction", "noise_scale", "changed_fraction", "relative_write_norm",
62
+ "kept_layers", "zeroed_layers", "shape", "weight_rms",
63
+ }
64
+ _SECRET_KEYS = {
65
+ "api_key", "hf_token", "access_token", "auth_token", "password", "secret",
66
+ "authorization", "credentials", "tokenizer_token", "hub_token",
67
+ }
68
+
69
+
70
+ def _json_value(value: Any) -> Any:
71
+ """Copy JSON-compatible values and reject non-finite measurements explicitly."""
72
+ if value is None or isinstance(value, (str, bool)):
73
+ return value
74
+ if isinstance(value, numbers.Integral):
75
+ return int(value)
76
+ if isinstance(value, numbers.Real):
77
+ number = float(value)
78
+ if not math.isfinite(number):
79
+ raise ValueError("Reports require finite measurements; found NaN or infinity.")
80
+ return number
81
+ if isinstance(value, dict):
82
+ if not all(isinstance(key, str) for key in value):
83
+ raise ValueError("Report object keys must be strings.")
84
+ return {key: _json_value(item) for key, item in value.items()}
85
+ if isinstance(value, (list, tuple)):
86
+ return [_json_value(item) for item in value]
87
+ raise TypeError(f"Report values must be JSON-compatible, received {type(value).__name__}.")
88
+
89
+
90
+ def _without_secrets(value: Any) -> Any:
91
+ if isinstance(value, dict):
92
+ return {
93
+ key: _without_secrets(item)
94
+ for key, item in value.items()
95
+ if key.casefold() not in _SECRET_KEYS
96
+ }
97
+ if isinstance(value, list):
98
+ return [_without_secrets(item) for item in value]
99
+ return value
100
+
101
+
102
+ def _result_copy(result: dict) -> dict:
103
+ if not isinstance(result, dict):
104
+ raise TypeError("A report result must be a dictionary.")
105
+ copied = _json_value({key: value for key, value in result.items() if key in _RESULT_KEYS})
106
+ for key, allowed in (("model", _MODEL_KEYS), ("run", _RUN_KEYS)):
107
+ metadata = copied.get(key, {})
108
+ if not isinstance(metadata, dict):
109
+ raise ValueError(f"Report {key} metadata must be a dictionary.")
110
+ copied[key] = {name: value for name, value in metadata.items() if name in allowed}
111
+ return _without_secrets(copied)
112
+
113
+
114
+ def _text(value: Any) -> str:
115
+ if value is None:
116
+ return ""
117
+ if isinstance(value, (dict, list)):
118
+ return json.dumps(value, ensure_ascii=False, allow_nan=False)
119
+ return str(value)
120
+
121
+
122
+ def _escaped(value: Any) -> str:
123
+ return html.escape(_text(value), quote=True)
124
+
125
+
126
+ def _markdown(value: Any) -> str:
127
+ return re.sub(r"([\\`*_{}\[\]()#!|])", r"\\\1", _escaped(value))
128
+
129
+
130
+ def _plot_values(values: Any) -> list:
131
+ # Plotly supports a subset of HTML in labels. Treat all model/user text as
132
+ # literal text, including closing script tags in an offline report.
133
+ if not isinstance(values, list):
134
+ return []
135
+ return [_escaped(value) if isinstance(value, str) else value for value in values]
136
+
137
+
138
+ def _figure(result: dict) -> go.Figure:
139
+ spec = result.get("plot") or {}
140
+ if not isinstance(spec, dict):
141
+ spec = {}
142
+ kind = spec.get("kind", "line")
143
+ figure = go.Figure()
144
+ if kind == "heatmap" and spec.get("z"):
145
+ figure.add_trace(go.Heatmap(
146
+ x=_plot_values(spec.get("x", [])),
147
+ y=_plot_values(spec.get("y", [])),
148
+ z=spec["z"],
149
+ colorscale=[[0, "#f0e8dc"], [0.5, "#79bbb4"], [1, "#174957"]],
150
+ colorbar={"thickness": 14, "outlinewidth": 0},
151
+ hoverongaps=False,
152
+ ))
153
+ else:
154
+ series = spec.get("series") or []
155
+ for index, item in enumerate(series):
156
+ if not isinstance(item, dict):
157
+ continue
158
+ common = {
159
+ "x": _plot_values(item.get("x", [])),
160
+ "y": _plot_values(item.get("y", [])),
161
+ "name": _escaped(item.get("name", f"Series {index + 1}")),
162
+ }
163
+ color = _PALETTE[index % len(_PALETTE)]
164
+ if kind == "bar":
165
+ figure.add_trace(go.Bar(**common, marker_color=color))
166
+ else:
167
+ figure.add_trace(go.Scatter(
168
+ **common, mode="lines+markers", line={"color": color, "width": 2},
169
+ marker={"size": 6}, connectgaps=False,
170
+ ))
171
+ if not figure.data:
172
+ figure.add_annotation(
173
+ text="Measurements are available in the table below.", showarrow=False,
174
+ x=0.5, y=0.5, xref="paper", yref="paper",
175
+ )
176
+ figure.update_layout(
177
+ title={"text": _escaped(spec.get("title", "Measurements")), "x": 0.025},
178
+ xaxis_title=_escaped(spec.get("x_label", "")),
179
+ yaxis_title=_escaped(spec.get("y_label", "")),
180
+ template="plotly_white", paper_bgcolor="#fbf8f1", plot_bgcolor="#fbf8f1",
181
+ font={"family": "Arial, sans-serif", "color": "#25394c", "size": 13},
182
+ margin={"l": 65, "r": 30, "t": 75, "b": 65}, height=500,
183
+ legend={"orientation": "h", "y": -0.2, "x": 0},
184
+ hovermode="closest", barmode="group",
185
+ )
186
+ figure.update_xaxes(gridcolor="#e5e0d6", zerolinecolor="#c4c1b7")
187
+ figure.update_yaxes(gridcolor="#e5e0d6", zerolinecolor="#c4c1b7")
188
+ return figure
189
+
190
+
191
+ def render_result(result: dict) -> tuple[str, go.Figure, pd.DataFrame, str]:
192
+ """Return Gradio-ready summary, chart, measurement table, and text comparison."""
193
+ data = _result_copy(result)
194
+ summary = f"### {_markdown(data.get('title', 'Experiment report'))}\n\n"
195
+ summary += _markdown(data.get("summary", ""))
196
+ model = data.get("model", {})
197
+ if model.get("repo_id"):
198
+ summary += f"\n\n**Model:** {_markdown(model['repo_id'])}"
199
+ if model.get("dtype"):
200
+ summary += f" · {_markdown(model['dtype'])}"
201
+ warnings = data.get("warnings") or []
202
+ if warnings:
203
+ summary += "\n\n" + "\n\n".join(f"**Note:** {_markdown(item)}" for item in warnings)
204
+ text_blocks = []
205
+ for item in data.get("text_outputs") or []:
206
+ if isinstance(item, dict):
207
+ text_blocks.append(
208
+ f"#### {_markdown(item.get('label', 'Output'))}\n\n"
209
+ f"<pre>{_escaped(item.get('text', ''))}</pre>"
210
+ )
211
+ return summary, _figure(data), pd.DataFrame(data.get("rows") or []), "\n\n".join(text_blocks)
212
+
213
+
214
+ _STYLE = """
215
+ :root{color-scheme:light;--ink:#25394c;--muted:#66737b;--paper:#fbf8f1;--line:#dedbd1;--teal:#167c80}
216
+ *{box-sizing:border-box}body{margin:0;background:#eae8df;color:var(--ink);font:15px/1.65 Arial,sans-serif}
217
+ main{max-width:1180px;margin:32px auto;padding:48px;background:var(--paper);border-top:6px solid var(--teal)}
218
+ header{border-bottom:1px solid var(--line);padding-bottom:24px;margin-bottom:24px}.brand{color:var(--teal);font:700 13px/1.5 monospace;letter-spacing:.13em}
219
+ h1{font-size:38px;letter-spacing:-.04em;line-height:1.15;margin:16px 0}h2{font-size:22px;margin-top:34px}h3{font-size:16px}
220
+ .subtitle,.muted{color:var(--muted)}.facts{display:flex;flex-wrap:wrap;gap:12px;margin:20px 0}.fact{border:1px solid var(--line);border-radius:8px;padding:10px 16px;min-width:130px}
221
+ .fact span{display:block;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.07em}.fact strong{display:block;font-size:16px;overflow-wrap:anywhere}
222
+ .summary{font-size:18px;white-space:pre-wrap}.note{background:#eaf1ed;border-left:3px solid var(--teal);padding:14px 18px;margin:18px 0}.warning{border-left-color:#d88759;background:#f6ecdf}
223
+ .table-wrap{overflow:auto}table{border-collapse:collapse;width:100%;font-size:13px}th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--line);vertical-align:top}
224
+ th{font-weight:700;background:#eeede5}tbody tr:nth-child(even){background:#f5f3ec}pre{white-space:pre-wrap;overflow-wrap:anywhere;padding:18px;background:#f0eee6;border:1px solid var(--line);border-radius:8px;font:13px/1.6 monospace}
225
+ .outputs{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px}.output{min-width:0}.output pre{height:calc(100% - 48px)}
226
+ details{margin:22px 0}summary{cursor:pointer;font-weight:700}footer{margin-top:38px;border-top:1px solid var(--line);padding-top:16px;font-size:12px;color:var(--muted)}
227
+ .print{border:1px solid var(--teal);border-radius:6px;color:var(--teal);background:transparent;padding:9px 14px;cursor:pointer;float:right}
228
+ @media(max-width:700px){main{margin:0;padding:24px}h1{font-size:29px}.print{float:none;margin-bottom:14px}}
229
+ @media print{@page{margin:14mm}body{background:white;font-size:11px}main{margin:0;padding:0;max-width:none;background:white;border-top-width:3px}.print{display:none}.modebar{display:none!important}h1{font-size:28px}h2,h3{break-after:avoid}pre,.fact,.note,.output{break-inside:avoid}.table-wrap{overflow:visible}td,th{padding:6px}footer{margin-top:20px}.js-plotly-plot{break-inside:avoid}}
230
+ """
231
+
232
+
233
+ def create_report(result: dict) -> tuple[str, str]:
234
+ """Write a complete JSON result and self-contained printable HTML to unique files."""
235
+ data = _result_copy(result)
236
+ serialized = json.dumps(data, ensure_ascii=False, indent=2, allow_nan=False)
237
+ _, figure, table, _ = render_result(data)
238
+ chart = pio.to_html(
239
+ figure, include_plotlyjs=True, include_mathjax=False, full_html=False,
240
+ div_id=f"measurements-{uuid.uuid4().hex}", default_width="100%",
241
+ config={"responsive": True, "displaylogo": False, "toImageButtonOptions": {"format": "svg"}},
242
+ )
243
+ model, run = data.get("model", {}), data.get("run", {})
244
+ facts = [("Checkpoint", model.get("repo_id", "Unspecified")), ("Precision", model.get("dtype", "Unspecified"))]
245
+ if isinstance(run.get("seconds"), (int, float)):
246
+ facts.append(("Run time", f"{run['seconds']:.2f} s"))
247
+ if isinstance(run.get("peak_vram_gb"), (int, float)):
248
+ facts.append(("Peak GPU memory", f"{run['peak_vram_gb']:.2f} GB"))
249
+ fact_html = "".join(f'<div class="fact"><span>{_escaped(label)}</span><strong>{_escaped(value)}</strong></div>' for label, value in facts)
250
+ warnings = "".join(f'<div class="note warning">{_escaped(item)}</div>' for item in data.get("warnings") or [])
251
+ table_html = table.to_html(index=False, escape=True, border=0, na_rep="—") if not table.empty else '<p class="muted">No tabular measurements for this run.</p>'
252
+ outputs = "".join(
253
+ f'<article class="output"><h3>{_escaped(item.get("label", "Output"))}</h3><pre>{_escaped(item.get("text", ""))}</pre></article>'
254
+ for item in data.get("text_outputs") or [] if isinstance(item, dict)
255
+ )
256
+ output_section = f'<section><h2>Text comparisons</h2><div class="outputs">{outputs}</div></section>' if outputs else ""
257
+ metadata = {"model": model, "run": run, "details": data.get("details", {})}
258
+ metadata_html = _escaped(json.dumps(metadata, ensure_ascii=False, indent=2, allow_nan=False))
259
+ title = _escaped(data.get("title", "Experiment report"))
260
+ document = f'''<!doctype html>
261
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
262
+ <title>{title} · 0perating-soon</title><style>{_STYLE}</style></head>
263
+ <body><main><header><button class="print" onclick="window.print()">Print / save PDF</button>
264
+ <div class="brand">0PERATING-SOON / BoL</div><h1>{title}</h1>
265
+ <p class="subtitle">Model internals, opened for inspection.</p><div class="facts">{fact_html}</div></header>
266
+ <p class="summary">{_escaped(data.get("summary", ""))}</p>{warnings}
267
+ <section aria-label="Interactive measurements">{chart}</section>
268
+ <section><h2>Measurements</h2><div class="table-wrap">{table_html}</div></section>{output_section}
269
+ <section><h2>Reading this result</h2><div class="note">{_escaped(MEASUREMENT_NOTE)}</div></section>
270
+ <details open><summary>Model, settings, and experiment details</summary><pre>{metadata_html}</pre></details>
271
+ <footer>This report is self-contained and works offline. Downloading a report does not contribute its data; research contributions require a separate consent choice.</footer>
272
+ </main></body></html>'''
273
+ # Files inside Gradio's cache are served in place and registered for its
274
+ # expiry sweep. Writing elsewhere would leave an untracked original behind.
275
+ cache_root = Path(os.environ.get("GRADIO_TEMP_DIR", os.path.join(tempfile.gettempdir(), "bol-gradio")))
276
+ cache_root.mkdir(parents=True, exist_ok=True)
277
+ directory = Path(tempfile.mkdtemp(prefix="bol-report-", dir=cache_root))
278
+ json_path, html_path = directory / "experiment.json", directory / "experiment.html"
279
+ try:
280
+ json_path.write_text(serialized, encoding="utf-8")
281
+ html_path.write_text(document, encoding="utf-8")
282
+ except Exception:
283
+ shutil.rmtree(directory, ignore_errors=True)
284
+ raise
285
+ return str(json_path), str(html_path)
286
+
287
+
288
+ def _numeric(value: Any) -> bool:
289
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
290
+
291
+
292
+ def _numeric_value(value: Any) -> bool:
293
+ return _numeric(value) or (
294
+ isinstance(value, list) and all(_numeric_value(item) for item in value)
295
+ )
296
+
297
+
298
+ def _numeric_axis(values: Any) -> list:
299
+ if not isinstance(values, list):
300
+ return []
301
+ # Categorical axes may contain tokens or candidate text. Preserve their
302
+ # positional relationship to numeric measurements, without their labels.
303
+ if all(_numeric(item) or item is None for item in values):
304
+ return values
305
+ return list(range(len(values)))
306
+
307
+
308
+ def _numeric_plot(spec: Any) -> dict:
309
+ if not isinstance(spec, dict):
310
+ return {}
311
+ kind = spec.get("kind")
312
+ if kind not in {"line", "bar", "heatmap"}:
313
+ return {}
314
+ output: dict = {"kind": kind}
315
+ if kind == "heatmap":
316
+ matrix = spec.get("z", [])
317
+ if not isinstance(matrix, list) or not all(
318
+ isinstance(row, list) and all(_numeric(value) or value is None for value in row)
319
+ for row in matrix
320
+ ):
321
+ return {}
322
+ output.update(x=_numeric_axis(spec.get("x")), y=_numeric_axis(spec.get("y")), z=matrix)
323
+ else:
324
+ output["series"] = []
325
+ for index, item in enumerate(spec.get("series") or []):
326
+ if not isinstance(item, dict):
327
+ continue
328
+ values = item.get("y", [])
329
+ if not isinstance(values, list) or not all(_numeric(value) or value is None for value in values):
330
+ continue
331
+ output["series"].append({
332
+ "name": f"series_{index + 1}", "x": _numeric_axis(item.get("x")), "y": values,
333
+ })
334
+ return output
335
+
336
+
337
+ def contribution_payload(result: dict, include_text: bool) -> dict:
338
+ """Build the exact contribution, using no custom text without text consent.
339
+
340
+ Text-free exports intentionally omit summaries, details, tensor/component
341
+ labels, token IDs, candidate answers, arbitrary options, and plot labels.
342
+ Their row order and generic plot series IDs preserve numeric comparisons.
343
+ Model identifiers are retained as the provenance the user agrees to share.
344
+ """
345
+ if not isinstance(include_text, bool):
346
+ raise TypeError("include_text must be an explicit boolean consent choice.")
347
+ result = _result_copy(result)
348
+ if include_text:
349
+ data = result
350
+ else:
351
+ model, run = result.get("model", {}), result.get("run", {})
352
+ safe_model = {
353
+ key: model[key] for key in ("repo_id", "revision", "architecture", "dtype")
354
+ if isinstance(model.get(key), str)
355
+ }
356
+ if _numeric(model.get("parameters")):
357
+ safe_model["parameters"] = model["parameters"]
358
+ safe_run = {
359
+ key: run[key] for key in ("seconds", "peak_vram_gb")
360
+ if _numeric(run.get(key))
361
+ }
362
+ if run.get("tool") in _TOOL_IDS:
363
+ safe_run["tool"] = run["tool"]
364
+ run_id = run.get("id")
365
+ if isinstance(run_id, str) and re.fullmatch(r"[a-fA-F0-9-]{16,36}", run_id):
366
+ safe_run["id"] = run_id
367
+ created_at = run.get("created_at")
368
+ if isinstance(created_at, str):
369
+ try:
370
+ datetime.fromisoformat(created_at.replace("Z", "+00:00"))
371
+ except ValueError:
372
+ pass
373
+ else:
374
+ safe_run["created_at"] = created_at
375
+ options = run.get("options") or {}
376
+ safe_run["options"] = {
377
+ key: value for key, value in options.items()
378
+ if key in _NUMERIC_OPTIONS and (_numeric_value(value) or isinstance(value, bool))
379
+ } if isinstance(options, dict) else {}
380
+ rows = []
381
+ for row in result.get("rows") or []:
382
+ if isinstance(row, dict):
383
+ rows.append({
384
+ key: value for key, value in row.items()
385
+ if key in _NUMERIC_ROW_KEYS and (_numeric_value(value) or value is None)
386
+ })
387
+ data = {"model": safe_model, "run": safe_run, "rows": rows, "plot": _numeric_plot(result.get("plot"))}
388
+ payload = {"schema_version": "1", "consent_version": "1", "includes_text": include_text, "data": data}
389
+ # Keep this promise local: upload code receives already-valid strict JSON.
390
+ json.dumps(payload, ensure_ascii=False, allow_nan=False)
391
+ return payload
bol_space/runtime.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Checkpoint preparation and short, isolated GPU experiments."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime, timezone
5
+ import gc
6
+ import math
7
+ import time
8
+ import uuid
9
+
10
+ import spaces # Must precede torch and transformers.
11
+ import torch
12
+ from bol_space.checkpoints import DEFAULT_MODEL, prepare_checkpoint, load_checkpoint
13
+ INSPECTION_TOOLS = {"fingerprint", "silhouette", "semantic", "cka", "attention", "gradient", "butterfly", "tugofwar"}
14
+ SURGERY_TOOLS = {"layer_sweep", "component", "trisect", "wormhole", "noise", "duets"}
15
+ ALL_TOOLS = INSPECTION_TOOLS | SURGERY_TOOLS
16
+
17
+
18
+ def validate_options(tool: str, options: dict) -> dict:
19
+ if tool not in ALL_TOOLS:
20
+ raise ValueError("Choose an available experiment.")
21
+ result = dict(options)
22
+ limits = {"max_tokens": (32, 256), "max_new_tokens": (1, 40), "sample_layers": (2, 16), "layer_start": (0, 10000), "layer_end": (-1, 10000), "seed": (0, 2147483647)}
23
+ for key, (low, high) in limits.items():
24
+ value = result.get(key)
25
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or int(value) != value or not low <= value <= high:
26
+ raise ValueError(f"{key} must be an integer from {low} to {high}.")
27
+ result[key] = int(value)
28
+ scale = float(result.get("noise_scale", 0.01))
29
+ if not math.isfinite(scale) or not 0 <= scale <= 0.5:
30
+ raise ValueError("Noise strength must be between 0 and 0.5.")
31
+ result["noise_scale"] = scale
32
+ for key in ("text", "compare_text", "prompt", "words", "component", "choice_a", "choice_b"):
33
+ value = result.get(key, "")
34
+ if not isinstance(value, str) or len(value) > 10000:
35
+ raise ValueError(f"{key} must contain no more than 10,000 characters.")
36
+ if not result.get("text", "").strip():
37
+ raise ValueError("Enter some text to analyze.")
38
+ return result
39
+
40
+
41
+ def model_summary(ticket: dict) -> str:
42
+ count = ticket.get("parameters")
43
+ size = f" · {count / 1e9:.2f}B parameters · approximately {count * 2 / 1e9:.2f} GB of BF16 weights" if count else ""
44
+ return f"**{ticket['repo_id']}** · {ticket['architecture']}{size}\n\nCheckpoint `{ticket['revision'][:12]}` is ready. Run an experiment to measure its actual GPU memory use."
45
+
46
+
47
+ # ZeroGPU registers the default model's CUDA placement at startup. User-selected
48
+ # checkpoints cannot be preloaded before they are known; their download is kept
49
+ # outside the GPU call and their temporary model is released after each request.
50
+ print("Preparing default SmolLM2 checkpoint...", flush=True)
51
+ DEFAULT_TICKET = prepare_checkpoint(DEFAULT_MODEL)
52
+ DEFAULT_INSTANCE, DEFAULT_TOKENIZER = load_checkpoint(DEFAULT_TICKET)
53
+ print("Default checkpoint registered in BF16; application ready to start.", flush=True)
54
+
55
+
56
+ def duration_for_request(request: dict | None) -> int:
57
+ if not request:
58
+ return 60
59
+ tool = request.get("tool")
60
+ duration = 90 if tool in {"fingerprint", "gradient", "duets", "component", "trisect", "wormhole", "noise", "layer_sweep"} else 60
61
+ if request.get("ticket", {}).get("revision") != DEFAULT_TICKET["revision"]:
62
+ duration += 60
63
+ return duration
64
+
65
+
66
+ @spaces.GPU(duration=duration_for_request)
67
+ def analyze_gpu(request: dict) -> dict:
68
+ """Run one BoL experiment in BF16 and return CPU measurements and resource use."""
69
+ from bol_space.inspection import run_inspection
70
+ from bol_space.surgery import run_surgery
71
+
72
+ if not request or "ticket" not in request:
73
+ raise ValueError("Prepare a checkpoint before starting an experiment.")
74
+ ticket, tool = request["ticket"], request["tool"]
75
+ options = validate_options(tool, request["options"])
76
+ started = time.perf_counter()
77
+ on_gpu = torch.cuda.is_available()
78
+ if on_gpu:
79
+ torch.cuda.reset_peak_memory_stats()
80
+ is_default = ticket["repo_id"] == DEFAULT_TICKET["repo_id"] and ticket["revision"] == DEFAULT_TICKET["revision"] and not getattr(DEFAULT_INSTANCE, "_bol_integrity_failed", False)
81
+ model = tokenizer = None
82
+ try:
83
+ if is_default:
84
+ model, tokenizer = DEFAULT_INSTANCE, DEFAULT_TOKENIZER
85
+ else:
86
+ model, tokenizer = load_checkpoint(ticket)
87
+ count = sum(p.numel() for p in model.parameters())
88
+ fn = run_inspection if tool in INSPECTION_TOOLS else run_surgery
89
+ with torch.random.fork_rng(devices=[torch.cuda.current_device()] if on_gpu else []):
90
+ torch.manual_seed(options["seed"])
91
+ result = fn(model, tokenizer, tool, options)
92
+ if on_gpu:
93
+ torch.cuda.synchronize()
94
+ result["model"] = {k: ticket[k] for k in ("repo_id", "revision", "architecture", "dtype")}
95
+ result["model"]["parameters"] = count
96
+ result["run"] = {"id": uuid.uuid4().hex, "created_at": datetime.now(timezone.utc).isoformat(), "tool": tool,
97
+ "seconds": round(time.perf_counter() - started, 3), "peak_vram_gb": round(torch.cuda.max_memory_allocated() / 1e9, 3) if on_gpu else 0,
98
+ "gpu": torch.cuda.get_device_name() if on_gpu else "CPU", "options": options}
99
+ result.setdefault("warnings", []).append("BF16 model execution; numerical summaries use FP32. Results describe these probes and this checkpoint.")
100
+ return result
101
+ except torch.cuda.OutOfMemoryError as exc:
102
+ raise ValueError("This experiment exceeded the available GPU memory. Try fewer sampled layers, shorter text, or a smaller checkpoint. No model-size ceiling has been established yet.") from exc
103
+ except RuntimeError as exc:
104
+ if is_default and "Weight restoration failed" in str(exc):
105
+ # A reused worker must never serve measurements from a failed restore.
106
+ # Future calls on this worker load an independent pristine checkpoint.
107
+ DEFAULT_INSTANCE._bol_integrity_failed = True
108
+ raise
109
+ finally:
110
+ if model is not None:
111
+ model.zero_grad(set_to_none=True)
112
+ if not is_default:
113
+ del model, tokenizer
114
+ gc.collect()
115
+ if on_gpu:
116
+ torch.cuda.empty_cache()
bol_space/surgery.py ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Bounded, reversible BoL experiments on an already loaded causal model.
2
+
3
+ The caller owns model loading, request serialization, and disposal after errors.
4
+ These experiments zero matrices rather than deleting decoder blocks: residual
5
+ paths, normalization parameters, and biases remain in place.
6
+ """
7
+
8
+ from contextlib import contextmanager
9
+ from itertools import combinations
10
+ import math
11
+
12
+ import torch
13
+ import torch.nn.functional as F
14
+ from torch import nn
15
+
16
+
17
+ def _layer_list(model):
18
+ paths = (
19
+ "model.layers", "transformer.h", "model.decoder.layers",
20
+ "gpt_neox.layers", "transformer.layers", "transformer.blocks",
21
+ "model.transformer.h", "model.transformer.blocks", "backbone.layers",
22
+ "decoder.layers", "layers",
23
+ )
24
+ for path in paths:
25
+ try:
26
+ layers = model.get_submodule(path)
27
+ except (AttributeError, KeyError):
28
+ continue
29
+ if isinstance(layers, nn.ModuleList) and len(layers):
30
+ return path, layers
31
+ raise ValueError(
32
+ "No supported decoder layer list found. This architecture cannot run "
33
+ "weight surgery yet; use a model with a standard decoder ModuleList."
34
+ )
35
+
36
+
37
+ def _matrices(layers, indices):
38
+ """Group eligible parameters by layer, protecting normalization and biases."""
39
+ result = {}
40
+ for index in indices:
41
+ layer = layers[index]
42
+ modules = dict(layer.named_modules())
43
+ parameters = []
44
+ for name, parameter in layer.named_parameters():
45
+ if parameter.ndim < 2 or not parameter.is_floating_point():
46
+ continue
47
+ if name.rsplit(".", 1)[-1] == "bias":
48
+ continue
49
+ parts = name.split(".")[:-1]
50
+ ancestors = ["."] + [".".join(parts[:i]) for i in range(1, len(parts) + 1)]
51
+ if "norm" in type(layer).__name__.lower() or any(
52
+ "norm" in type(modules[prefix]).__name__.lower()
53
+ for prefix in ancestors if prefix in modules
54
+ ):
55
+ continue
56
+ parameters.append((name, parameter))
57
+ if not parameters:
58
+ raise ValueError(f"Layer {index} has no eligible floating-point weight matrices.")
59
+ result[index] = parameters
60
+ return result
61
+
62
+
63
+ def _unique(parameters):
64
+ seen = set()
65
+ output = []
66
+ for name, parameter in parameters:
67
+ if id(parameter) not in seen:
68
+ output.append((name, parameter))
69
+ seen.add(id(parameter))
70
+ return output
71
+
72
+
73
+ def _collect(by_layer, indices):
74
+ return _unique(
75
+ (f"{index}.{name}", parameter)
76
+ for index in indices for name, parameter in by_layer[index]
77
+ )
78
+
79
+
80
+ @contextmanager
81
+ def _temporary_edit(parameters, *, noise_scale=None, seed=42):
82
+ """Back up only edited tensors; restore even if editing/forward/generation fails."""
83
+ parameters = _unique(parameters)
84
+ if not parameters:
85
+ raise ValueError("The intervention selected no weight matrices.")
86
+ originals = []
87
+ info = {"modified_tensors": len(parameters), "integrity_verified": False}
88
+ generators = {}
89
+ changed, count, delta_squared, original_squared = 0, 0, 0.0, 0.0
90
+ try:
91
+ with torch.no_grad():
92
+ for name, parameter in parameters:
93
+ original = parameter.detach().clone()
94
+ # Record the backup before the first mutation, including partial failures.
95
+ originals.append((name, parameter, original))
96
+ if noise_scale is None:
97
+ parameter.zero_()
98
+ else:
99
+ device = str(parameter.device)
100
+ if device not in generators:
101
+ generators[device] = torch.Generator(device=parameter.device).manual_seed(seed)
102
+ source = original.float()
103
+ scale = source.abs().mean() * noise_scale
104
+ noise = torch.randn(
105
+ parameter.shape, device=parameter.device, dtype=torch.float32,
106
+ generator=generators[device],
107
+ )
108
+ parameter.copy_(source + noise * scale)
109
+ realized = parameter.detach().float() - source
110
+ changed += int(torch.count_nonzero(realized).item())
111
+ count += parameter.numel()
112
+ delta_squared += float(realized.square().sum().item())
113
+ original_squared += float(source.square().sum().item())
114
+ del source, noise, realized
115
+ if noise_scale is not None:
116
+ info["changed_fraction"] = changed / count if count else 0.0
117
+ info["relative_write_norm"] = (
118
+ math.sqrt(delta_squared / original_squared) if original_squared else 0.0
119
+ )
120
+ yield info
121
+ finally:
122
+ # Attempt every restoration before raising an integrity error.
123
+ failed = []
124
+ with torch.no_grad():
125
+ for name, parameter, original in originals:
126
+ try:
127
+ parameter.copy_(original)
128
+ if not torch.equal(parameter.detach(), original):
129
+ failed.append(name)
130
+ except Exception:
131
+ failed.append(name)
132
+ if failed:
133
+ raise RuntimeError(
134
+ "Weight restoration failed; discard this model instance. "
135
+ f"Affected tensors: {', '.join(failed[:4])}"
136
+ )
137
+ info["integrity_verified"] = True
138
+
139
+
140
+ def _integer(options, key, default, lower, upper):
141
+ value = options.get(key, default)
142
+ try:
143
+ numeric = float(value)
144
+ integer = int(numeric)
145
+ except (TypeError, ValueError, OverflowError) as exc:
146
+ raise ValueError(f"{key} must be an integer from {lower} to {upper}.") from exc
147
+ if not math.isfinite(numeric) or integer != numeric or not lower <= integer <= upper:
148
+ raise ValueError(f"{key} must be an integer from {lower} to {upper}.")
149
+ return integer
150
+
151
+
152
+ def _sample(indices, count):
153
+ if len(indices) <= count:
154
+ return list(indices)
155
+ if count == 1:
156
+ return [indices[len(indices) // 2]]
157
+ return [indices[round(i * (len(indices) - 1) / (count - 1))] for i in range(count)]
158
+
159
+
160
+ def _prepare_batch(model, tokenizer, text, max_tokens):
161
+ texts = [line.strip() for line in text.splitlines() if line.strip()]
162
+ if not texts:
163
+ raise ValueError("Enter evaluation text containing at least two tokens.")
164
+ if len(texts) > 8:
165
+ raise ValueError("Use at most eight nonempty evaluation lines per experiment.")
166
+ batch = tokenizer(
167
+ texts, return_tensors="pt", padding=True,
168
+ truncation=True, max_length=max_tokens,
169
+ )
170
+ device = next(model.parameters()).device
171
+ batch = {key: value.to(device) for key, value in batch.items()
172
+ if key in ("input_ids", "attention_mask")}
173
+ if "attention_mask" not in batch:
174
+ raise ValueError("Tokenizer did not return an attention mask.")
175
+ targets = batch["input_ids"][:, 1:].clone()
176
+ valid = batch["attention_mask"][:, 1:].bool() & batch["attention_mask"][:, :-1].bool()
177
+ targets.masked_fill_(~valid, -100)
178
+ scored_tokens = int(valid.sum().item())
179
+ if not scored_tokens:
180
+ raise ValueError("Evaluation text must have at least two unpadded tokens in a line.")
181
+ return batch, targets, scored_tokens
182
+
183
+
184
+ def _loss(model, batch, targets):
185
+ with torch.no_grad():
186
+ logits = model(**batch, use_cache=False).logits
187
+ # Explicit FP32 scoring, even when the model forward pass uses BF16.
188
+ shifted = logits[:, :-1, :].float().contiguous()
189
+ value = F.cross_entropy(shifted.view(-1, shifted.shape[-1]), targets.reshape(-1))
190
+ numeric = float(value.item())
191
+ if not math.isfinite(numeric):
192
+ raise FloatingPointError("The model produced a non-finite evaluation loss; the experiment was restored.")
193
+ return numeric
194
+
195
+
196
+ def _generate(model, tokenizer, prompt, max_tokens, max_new_tokens):
197
+ device = next(model.parameters()).device
198
+ inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=max_tokens)
199
+ inputs = {key: value.to(device) for key, value in inputs.items()
200
+ if key in ("input_ids", "attention_mask")}
201
+ kwargs = {"max_new_tokens": max_new_tokens, "do_sample": False, "use_cache": True}
202
+ if tokenizer.pad_token_id is not None:
203
+ kwargs["pad_token_id"] = tokenizer.pad_token_id
204
+ with torch.no_grad():
205
+ output = model.generate(**inputs, **kwargs)
206
+ continuation = output[0, inputs["input_ids"].shape[1]:]
207
+ return tokenizer.decode(continuation, skip_special_tokens=True)
208
+
209
+
210
+ def _zones(indices, tool):
211
+ n = len(indices)
212
+ q1, q2, q3 = n // 4, n // 2, n - n // 4
213
+ if tool == "trisect":
214
+ keeps = [
215
+ ("Inner core", indices[q1:q3]),
216
+ ("Outer shell", indices[:q1] + indices[q3:]),
217
+ ("Early half", indices[:q2]),
218
+ ("Late half", indices[q2:]),
219
+ ("Early inner quarter", indices[q1:q2]),
220
+ ("Late inner quarter", indices[q2:q3]),
221
+ ("Early outer + late inner", indices[:q1] + indices[q2:q3]),
222
+ ("Early inner + late outer", indices[q1:q2] + indices[q3:]),
223
+ ]
224
+ else:
225
+ keeps = [
226
+ ("Outer quarters", indices[:q1] + indices[q3:]),
227
+ ("Early outer + late inner", indices[:q1] + indices[q2:q3]),
228
+ ("Early inner + late outer", indices[q1:q2] + indices[q3:]),
229
+ ("Inner half", indices[q1:q3]),
230
+ ("Every even layer", [index for index in indices if index % 2 == 0]),
231
+ ("Every odd layer", [index for index in indices if index % 2 == 1]),
232
+ ("First + last", [indices[0], indices[-1]]),
233
+ ("First 3 + last 3", indices[:3] + indices[-3:]),
234
+ ("First 5 + last 5", indices[:5] + indices[-5:]),
235
+ ("Every third layer", indices[::3]),
236
+ ]
237
+ result, seen = [], set()
238
+ for label, keep in keeps:
239
+ keep = sorted(set(keep))
240
+ zero = tuple(index for index in indices if index not in keep)
241
+ # Small ranges produce duplicate/no-op patterns; do not call them findings.
242
+ if not keep or not zero or zero in seen:
243
+ continue
244
+ seen.add(zero)
245
+ result.append((label, keep, list(zero)))
246
+ return result
247
+
248
+
249
+ def run_surgery(model, tokenizer, tool: str, options: dict) -> dict:
250
+ """Run one experiment and return a JSON-native report with restored weights.
251
+
252
+ Layer bounds are inclusive; -1 means the final layer. Sweep/noise sample at
253
+ most 16 layers; duets sample at most six. Component and zone cuts use every
254
+ layer inside the selected range. All scores are token-weighted causal NLL
255
+ on the supplied evaluation text, not a general capability benchmark.
256
+ """
257
+ supported = {"layer_sweep", "component", "trisect", "wormhole", "noise", "duets"}
258
+ if tool not in supported:
259
+ raise ValueError(f"Unknown surgery tool: {tool}")
260
+ path, layers = _layer_list(model)
261
+ total = len(layers)
262
+ try:
263
+ start = _integer(options, "layer_start", 0, 0, total - 1)
264
+ end = _integer(options, "layer_end", -1, -1, total - 1)
265
+ end = total - 1 if end == -1 else end
266
+ if end < start:
267
+ raise ValueError("end precedes start")
268
+ except ValueError as exc:
269
+ raise ValueError(f"Invalid layer range; use 0 through {total - 1}, with end >= start.") from exc
270
+ max_tokens = _integer(options, "max_tokens", 128, 2, 256)
271
+ max_new_tokens = _integer(options, "max_new_tokens", 16, 1, 40)
272
+ sample_count = _integer(options, "sample_layers", 8, 1, 16)
273
+ seed = _integer(options, "seed", 42, 0, 2**32 - 1)
274
+ text = str(options.get("text", ""))
275
+ prompt = str(options.get("prompt", "")).strip()
276
+ indices = list(range(start, end + 1))
277
+ selected = _sample(indices, min(sample_count, 6) if tool == "duets" else sample_count)
278
+ if tool in {"component", "trisect", "wormhole"}:
279
+ selected = indices
280
+ if tool == "duets" and len(selected) < 2:
281
+ raise ValueError("Layer Duets needs at least two selected layers.")
282
+ by_layer = _matrices(layers, selected)
283
+ warnings = [
284
+ "Results describe this evaluation text and this model precision; they do not establish general layer importance.",
285
+ "Matrix weights are edited in place; normalization, biases, residual routes, and block execution are retained.",
286
+ ]
287
+ if len(selected) < len(indices):
288
+ warnings.append(f"Sampled {len(selected)} of {len(indices)} layers in the requested range.")
289
+ selected_parameter_ids = {id(parameter) for values in by_layer.values() for _, parameter in values}
290
+ parameter_owners = {}
291
+ # Check the full layer list, including layers excluded by range/sampling.
292
+ # An alias outside the sample would otherwise receive an unreported cut.
293
+ for index, layer in enumerate(layers):
294
+ for parameter in layer.parameters():
295
+ if id(parameter) in selected_parameter_ids:
296
+ parameter_owners.setdefault(id(parameter), set()).add(index)
297
+ if any(len(owners) > 1 for owners in parameter_owners.values()):
298
+ raise ValueError("Selected layers share parameters; independent layer surgery is unsupported for this architecture.")
299
+
300
+ details = {
301
+ "layer_path": path, "total_layers": total, "selected_layers": selected,
302
+ "requested_range": [start, end], "sampled": len(selected) < len(indices),
303
+ "integrity_verified": False,
304
+ "scope": "2D-or-higher floating weight tensors in selected decoder layers; normalization and biases protected",
305
+ "loss_definition": "Mean next-token negative log likelihood over unpadded targets; computed in FP32",
306
+ "generation_format": "Raw prompt continuation, greedy decoding",
307
+ }
308
+ experiments = []
309
+ noise_scale = None
310
+ if tool in {"layer_sweep", "noise"}:
311
+ if tool == "noise":
312
+ try:
313
+ noise_scale = float(options.get("noise_scale", 0.01))
314
+ except (TypeError, ValueError) as exc:
315
+ raise ValueError("noise_scale must be a finite number between 0 and 1.") from exc
316
+ if not math.isfinite(noise_scale) or not 0 <= noise_scale <= 1:
317
+ raise ValueError("noise_scale must be a finite number between 0 and 1.")
318
+ details.update(noise_scale=noise_scale, seed=seed)
319
+ warnings.append("Gaussian weight noise is not quantization and cannot predict an INT4/INT8 bit budget.")
320
+ for index in selected:
321
+ experiments.append((f"Layer {index}", _collect(by_layer, [index]), {"layer": index}))
322
+ elif tool == "component":
323
+ components = {}
324
+ for index, entries in by_layer.items():
325
+ for name, parameter in entries:
326
+ component = name.rsplit(".", 1)[0] if name.endswith(".weight") else name
327
+ components.setdefault(component, []).append((f"{index}.{name}", parameter))
328
+ available = sorted(components)
329
+ requested = str(options.get("component", "All available")).strip()
330
+ if requested.casefold() in {"", "all", "all available"}:
331
+ chosen = available
332
+ elif requested in components:
333
+ chosen = [requested]
334
+ else:
335
+ chosen = [name for name in available if requested.casefold() in name.casefold()]
336
+ if not chosen:
337
+ raise ValueError(f"No matrix component matches {requested!r}. Available: {', '.join(available[:24])}")
338
+ if len(chosen) > 16:
339
+ warnings.append(f"Sampled 16 of {len(chosen)} matching component paths; refine the component field for a specific module.")
340
+ chosen = _sample(chosen, 16)
341
+ details["available_components"] = available[:128]
342
+ details["tested_components"] = chosen
343
+ for name in chosen:
344
+ experiments.append((name, _unique(components[name]), {"component": name}))
345
+ elif tool in {"trisect", "wormhole"}:
346
+ patterns = _zones(selected, tool)
347
+ requested = str(options.get("pattern", "")).strip()
348
+ if requested.casefold() not in {"", "all", "all patterns"}:
349
+ patterns = [entry for entry in patterns if requested.casefold() in entry[0].casefold()]
350
+ if not patterns:
351
+ raise ValueError("No nonempty cut pattern matches this range and pattern; choose a wider layer range or clear the pattern.")
352
+ for label, kept, zeroed in patterns:
353
+ experiments.append((label, _collect(by_layer, zeroed), {"kept_layers": kept, "zeroed_layers": zeroed}))
354
+ details["scope"] += "; layers outside the requested range stay intact; pattern names describe retained matrices"
355
+ else:
356
+ for index in selected:
357
+ experiments.append((f"Layer {index}", _collect(by_layer, [index]), {"layer": index}))
358
+ for a, b in combinations(selected, 2):
359
+ experiments.append((f"Layers {a} + {b}", _collect(by_layer, [a, b]), {"layer_a": a, "layer_b": b}))
360
+
361
+ modes = [(module, module.training) for module in model.modules()]
362
+ model.eval()
363
+ try:
364
+ batch, targets, scored_tokens = _prepare_batch(model, tokenizer, text, max_tokens)
365
+ baseline = _loss(model, batch, targets)
366
+ details.update(baseline_loss=baseline, scored_tokens=scored_tokens)
367
+ text_outputs = []
368
+ if prompt:
369
+ text_outputs.append({"label": "Original model", "text": _generate(model, tokenizer, prompt, max_tokens, max_new_tokens)})
370
+ measured = []
371
+ for label, parameters, fields in experiments:
372
+ with _temporary_edit(parameters, noise_scale=noise_scale, seed=seed) as edit:
373
+ loss = _loss(model, batch, targets)
374
+ row = {"name": label, **fields, "loss": loss, "damage": loss - baseline,
375
+ "modified_tensors": edit["modified_tensors"]}
376
+ if noise_scale is not None:
377
+ row.update(noise_scale=noise_scale, changed_fraction=edit["changed_fraction"],
378
+ relative_write_norm=edit["relative_write_norm"])
379
+ measured.append(row)
380
+ worst = max(range(len(measured)), key=lambda index: measured[index]["damage"])
381
+ if prompt:
382
+ label, parameters, _ = experiments[worst]
383
+ with _temporary_edit(parameters, noise_scale=noise_scale, seed=seed):
384
+ altered = _generate(model, tokenizer, prompt, max_tokens, max_new_tokens)
385
+ text_outputs.append({"label": f"Largest measured loss change: {label}", "text": altered})
386
+ details["integrity_verified"] = True
387
+ details["restoration_verification"] = "Exact tensor equality immediately after every intervention and generation comparison"
388
+ details["evaluation_forward_passes"] = 1 + len(experiments)
389
+ details["generation_comparisons"] = 2 if prompt else 0
390
+
391
+ titles = {
392
+ "layer_sweep": "Layer sweep", "component": "Component ablation",
393
+ "trisect": "Trisect: core and shell", "wormhole": "Wormhole patterns",
394
+ "noise": "Noise sensitivity", "duets": "Layer Duets",
395
+ }
396
+ if tool == "duets":
397
+ singles = {row["layer"]: row for row in measured if "layer" in row}
398
+ rows = []
399
+ matrix = [[None for _ in selected] for _ in selected]
400
+ for row in measured:
401
+ if "layer_a" not in row:
402
+ continue
403
+ a, b = row["layer_a"], row["layer_b"]
404
+ da, db = singles[a]["damage"], singles[b]["damage"]
405
+ interaction = row["damage"] - da - db
406
+ rows.append({**row, "single_damage_a": da, "single_damage_b": db,
407
+ "pair_damage": row["damage"], "interaction": interaction})
408
+ ai, bi = selected.index(a), selected.index(b)
409
+ matrix[ai][bi] = matrix[bi][ai] = interaction
410
+ details["singles"] = list(singles.values())
411
+ details["interaction_definition"] = "interaction = (pair loss - baseline) - (single A loss - baseline) - (single B loss - baseline)"
412
+ plot = {"kind": "heatmap", "title": "Excess pair damage (positive: more than additive)",
413
+ "x_label": "Layer", "y_label": "Layer", "x": [f"L{i}" for i in selected],
414
+ "y": [f"L{i}" for i in selected], "z": matrix}
415
+ summary = (
416
+ f"Tested {len(singles)} single cuts and {len(rows)} pairs. Positive interaction means "
417
+ "the pair raises loss more than the sum of its individual cuts; negative means less. "
418
+ "This measures non-additivity on the chosen text, not a proven functional partnership."
419
+ )
420
+ else:
421
+ rows = measured
422
+ x_values = [row["layer"] if "layer" in row else row["name"] for row in rows]
423
+ plot = {"kind": "line" if tool in {"layer_sweep", "noise"} else "bar",
424
+ "title": "Loss change from original model", "x_label": "Layer" if tool in {"layer_sweep", "noise"} else "Intervention",
425
+ "y_label": "Change in mean NLL", "series": [{"name": "Loss change", "x": x_values, "y": [row["damage"] for row in rows]}]}
426
+ summary = (
427
+ f"Original loss: {baseline:.4f}. Tested {len(rows)} interventions; "
428
+ f"largest measured loss change: {measured[worst]['damage']:+.4f} "
429
+ f"({measured[worst]['name']}). Every modified tensor was restored and checked."
430
+ )
431
+ if noise_scale is not None and any(row["changed_fraction"] == 0 for row in rows):
432
+ warnings.append("At least one noise edit changed no stored values; inspect changed_fraction before interpreting zero loss change.")
433
+ return {"title": titles[tool], "summary": summary, "rows": rows, "plot": plot,
434
+ "details": details, "warnings": warnings, "text_outputs": text_outputs}
435
+ finally:
436
+ for module, training in modes:
437
+ module.training = training
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.11.0
2
+ transformers==5.16.1
3
+ accelerate>=1.12.0
4
+ numpy>=2.0,<3
5
+ scipy==1.18.1
6
+ pandas==3.0.5
7
+ plotly==7.0.0
8
+ safetensors>=0.6.2
9
+ sentencepiece>=0.2.0
tests/test_app_flow.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exercise app callbacks and state bindings without downloading a checkpoint.
2
+
3
+ The UI module starts its default model at import. Extracting its real callback
4
+ definitions lets these regression tests run independently of that GPU startup;
5
+ the deployed Gradio client smoke test covers the actual component integration.
6
+ """
7
+
8
+ import ast
9
+ from pathlib import Path
10
+ from types import SimpleNamespace
11
+ from unittest.mock import Mock
12
+
13
+ import pytest
14
+
15
+
16
+ SOURCE = Path(__file__).resolve().parents[1] / "app.py"
17
+ TREE = ast.parse(SOURCE.read_text())
18
+
19
+
20
+ def binding(callback):
21
+ for node in ast.walk(TREE):
22
+ if (isinstance(node, ast.Call) and node.args
23
+ and isinstance(node.args[0], ast.Name) and node.args[0].id == callback):
24
+ return {keyword.arg: keyword.value for keyword in node.keywords}
25
+ raise AssertionError(f"No event is bound to {callback}")
26
+
27
+
28
+ def names(node):
29
+ return [node.id] if isinstance(node, ast.Name) else [item.id for item in node.elts]
30
+
31
+
32
+ @pytest.fixture
33
+ def callbacks():
34
+ definitions = [node for node in TREE.body if isinstance(node, ast.FunctionDef)
35
+ or (isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == "DESCRIPTIONS"
36
+ for target in node.targets))]
37
+ namespace = {
38
+ "gr": SimpleNamespace(update=lambda **kwargs: kwargs, Error=ValueError),
39
+ "validate_options": Mock(return_value={"seed": 42}),
40
+ "prepare_checkpoint": Mock(return_value={"repo_id": "test/model", "revision": "abc"}),
41
+ "model_summary": Mock(return_value="Ready"),
42
+ "render_result": Mock(return_value=("Summary", "Figure", "Table", "Comparison")),
43
+ "create_report": Mock(return_value=("experiment.json", "experiment.html")),
44
+ "save_contribution": Mock(return_value="Saved"),
45
+ }
46
+ exec(compile(ast.Module(body=definitions, type_ignores=[]), str(SOURCE), "exec"), namespace)
47
+ return namespace
48
+
49
+
50
+ def prepare_arguments():
51
+ return ("test/model", "main", "VISITOR_TOKEN", "silhouette", "Main text", "Comparison",
52
+ "Generation", "", 128, 16, 8, 0, -1, "All available", 0.01, 42, " MAPLE", " RAVEN")
53
+
54
+
55
+ def test_begin_run_clears_prior_state_permissions_and_visible_results(callbacks):
56
+ values = dict(zip(names(binding("begin_run")["outputs"]), callbacks["begin_run"](), strict=True))
57
+ for key in ("request_state", "computed_state", "report_state", "result_json", "chart", "json_report", "html_report"):
58
+ assert values[key] is None
59
+ assert values["table"] == []
60
+ assert values["comparisons"] == ""
61
+ assert values["consent"] is False
62
+ assert values["include_text"] is False
63
+ assert values["save"]["interactive"] is False
64
+ assert values["run_button"]["interactive"] is False
65
+ assert ast.literal_eval(binding("begin_run")["queue"]) is False
66
+
67
+
68
+ def test_api_prepare_invalidates_prior_result_before_download_or_failure(callbacks):
69
+ callbacks["prepare_checkpoint"].side_effect = ValueError("No access")
70
+ generator = callbacks["prepare_request"](*prepare_arguments())
71
+ initial = dict(zip(names(binding("prepare_request")["outputs"]), next(generator), strict=True))
72
+ callbacks["prepare_checkpoint"].assert_not_called()
73
+ assert all(initial[key] is None for key in ("request_state", "computed_state", "report_state"))
74
+ assert initial["consent"] is False
75
+ assert initial["include_text"] is False
76
+ assert initial["save"]["interactive"] is False
77
+ with pytest.raises(ValueError, match="No access"):
78
+ next(generator)
79
+
80
+
81
+ def test_prepare_keeps_18_controls_and_excludes_visitor_token_from_state(callbacks):
82
+ result = list(callbacks["prepare_request"](*prepare_arguments()))[-1]
83
+ values = dict(zip(names(binding("prepare_request")["outputs"]), result, strict=True))
84
+ assert len(names(binding("prepare_request")["inputs"])) == 1 # The named controls list.
85
+ controls = next(node.value for node in ast.walk(TREE) if isinstance(node, ast.Assign)
86
+ and any(isinstance(target, ast.Name) and target.id == "controls" for target in node.targets))
87
+ assert len(controls.elts) == 18
88
+ assert "VISITOR_TOKEN" not in repr(values)
89
+ assert values["request_state"]["tool"] == "silhouette"
90
+ assert values["report_state"] is None
91
+
92
+
93
+ def test_report_can_only_consume_server_computed_state():
94
+ assert names(binding("analyze_gpu")["inputs"]) == ["request_state"]
95
+ assert names(binding("analyze_gpu")["outputs"]) == ["computed_state"]
96
+ assert names(binding("finish_report")["inputs"]) == ["computed_state"]
97
+ assert "result_json" not in names(binding("finish_report")["inputs"])
98
+
99
+
100
+ def test_report_reenables_contributions_only_for_completed_measurements(callbacks):
101
+ result = {"run": {"id": "completed-run"}, "title": "Measured"}
102
+ values = dict(zip(names(binding("finish_report")["outputs"]), callbacks["finish_report"](result), strict=True))
103
+ assert values["result_json"] == result
104
+ assert values["report_state"] == result
105
+ assert values["consent"] is False
106
+ assert values["include_text"] is False
107
+ assert values["save"]["interactive"] is True
108
+ assert values["run_button"]["interactive"] is True
109
+ with pytest.raises(ValueError, match="no completed measurements"):
110
+ callbacks["finish_report"](None)
111
+
112
+
113
+ def test_failed_stage_clears_every_result_state_and_requires_fresh_consent(callbacks):
114
+ values = dict(zip(names(binding("failed_run")["outputs"]), callbacks["failed_run"](), strict=True))
115
+ assert all(values[key] is None for key in ("request_state", "computed_state", "report_state"))
116
+ assert values["consent"] is False
117
+ assert values["include_text"] is False
118
+ assert values["save"]["interactive"] is False
119
+ assert values["run_button"]["interactive"] is True
120
+
121
+
122
+ def test_tool_controls_match_the_inputs_actually_used(callbacks):
123
+ silhouette = callbacks["describe_tool"]("silhouette")
124
+ assert silhouette[3]["visible"] is True # grouped words
125
+ assert silhouette[4]["visible"] is False # unused main text
126
+ tug = callbacks["describe_tool"]("tugofwar")
127
+ assert tug[1]["visible"] is False # engine does not use comparison text
128
+ assert tug[2]["visible"] is True
129
+ assert tug[4]["label"] == "Context preamble"
130
+ assert callbacks["describe_tool"]("butterfly")[1]["visible"] is True
tests/test_contributions.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from types import SimpleNamespace
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from bol_space.contributions import save_contribution
8
+
9
+
10
+ def result():
11
+ return {"title": "Test", "summary": "PRIVATE_TEXT_CANARY", "model": {"repo_id": "owner/model", "revision": "abc", "dtype": "bfloat16", "architecture": "llama", "parameters": 10},
12
+ "run": {"id": "fixture", "tool": "gradient", "options": {"text": "PRIVATE_TEXT_CANARY", "max_tokens": 128}},
13
+ "rows": [{"layer": 0, "gradient_norm": 0.5}], "plot": {}, "text_outputs": [{"label": "prompt", "text": "PRIVATE_TEXT_CANARY"}]}
14
+
15
+
16
+ def test_no_consent_never_connects_to_hub():
17
+ with patch("bol_space.contributions.HfApi") as api:
18
+ with pytest.raises(ValueError, match="permission"):
19
+ save_contribution(result(), False, True)
20
+ api.assert_not_called()
21
+
22
+
23
+ def test_public_dataset_never_receives_contribution():
24
+ api = MagicMock()
25
+ api.dataset_info.return_value = SimpleNamespace(private=False)
26
+ with patch.dict("os.environ", {"BOL_CONTRIBUTION_REPO": "owner/private", "BOL_CONTRIBUTION_TOKEN": "unit-test-placeholder"}), patch("bol_space.contributions.HfApi", return_value=api):
27
+ with pytest.raises(ValueError, match="not private"):
28
+ save_contribution(result(), True, False)
29
+ api.upload_file.assert_not_called()
30
+
31
+
32
+ def test_consented_measurements_exclude_text_and_record_scope():
33
+ api = MagicMock()
34
+ api.dataset_info.return_value = SimpleNamespace(private=True)
35
+ with patch.dict("os.environ", {"BOL_CONTRIBUTION_REPO": "owner/private", "BOL_CONTRIBUTION_TOKEN": "unit-test-placeholder"}), patch("bol_space.contributions.HfApi", return_value=api):
36
+ status = save_contribution(result(), True, False)
37
+ kwargs = api.upload_file.call_args.kwargs
38
+ assert kwargs["repo_type"] == "dataset"
39
+ assert b"PRIVATE_TEXT_CANARY" not in kwargs["path_or_fileobj"]
40
+ payload = json.loads(kwargs["path_or_fileobj"])
41
+ assert payload["consent"]["measurements"] is True
42
+ assert payload["consent"]["include_text"] is False
43
+ assert payload["data"]["rows"] == [{"layer": 0, "gradient_norm": 0.5}]
44
+ assert "Saved privately" in status
tests/test_inspection.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Numerical and state-restoration checks using a tiny local causal network."""
2
+ import json
3
+ import importlib.util
4
+ import types
5
+ import unittest
6
+
7
+ import torch
8
+ from torch import nn
9
+
10
+ from bol_space.inspection import _linear_cka, run_inspection
11
+
12
+
13
+ class CharacterTokenizer:
14
+ def __call__(self, text, return_tensors=None, truncation=False, max_length=None,
15
+ add_special_tokens=True, **kwargs):
16
+ ids = ([1] if add_special_tokens else []) + [2 + ord(c) % 61 for c in text]
17
+ if truncation and max_length:
18
+ ids = ids[:max_length]
19
+ if return_tensors == "pt":
20
+ ids = torch.tensor([ids], dtype=torch.long)
21
+ return {"input_ids": ids, "attention_mask": torch.ones_like(ids)}
22
+ return {"input_ids": ids}
23
+
24
+ def decode(self, ids, **kwargs):
25
+ return " ".join(str(int(i)) for i in ids)
26
+
27
+ def convert_ids_to_tokens(self, ids):
28
+ return [str(i) for i in ids]
29
+
30
+
31
+ class TinyCausalModel(nn.Module):
32
+ def __init__(self):
33
+ super().__init__()
34
+ self.embeddings = nn.Embedding(64, 8)
35
+ self.model = nn.Module()
36
+ self.model.layers = nn.ModuleList([nn.Linear(8, 8), nn.Linear(8, 8)])
37
+ self.lm_head = nn.Linear(8, 64)
38
+ self.config = types.SimpleNamespace(num_hidden_layers=2)
39
+ self.fail = False
40
+
41
+ def get_input_embeddings(self):
42
+ return self.embeddings
43
+
44
+ def forward(self, input_ids, attention_mask=None, output_hidden_states=False,
45
+ output_attentions=False, use_cache=False, labels=None, **kwargs):
46
+ if self.fail:
47
+ raise RuntimeError("injected failure")
48
+ h = self.embeddings(input_ids)
49
+ states = [h]
50
+ n = h.shape[1]
51
+ denom = torch.arange(1, n + 1, device=h.device)[None, :, None]
52
+ for layer in self.model.layers:
53
+ h = h + torch.tanh(layer(h.cumsum(1) / denom))
54
+ states.append(h)
55
+ logits = self.lm_head(h)
56
+ loss = None
57
+ if labels is not None:
58
+ loss = torch.nn.functional.cross_entropy(
59
+ logits[:, :-1].float().reshape(-1, 64), labels[:, 1:].reshape(-1),
60
+ ignore_index=-100,
61
+ )
62
+ attn = torch.ones(n, n, device=h.device).tril()
63
+ attn /= attn.sum(-1, keepdim=True)
64
+ return types.SimpleNamespace(
65
+ hidden_states=tuple(states) if output_hidden_states else None,
66
+ attentions=tuple(attn[None, None].repeat(1, 2, 1, 1) for _ in range(2))
67
+ if output_attentions else None,
68
+ logits=logits, loss=loss,
69
+ )
70
+
71
+
72
+ class InspectionTests(unittest.TestCase):
73
+ def setUp(self):
74
+ with torch.random.fork_rng():
75
+ torch.manual_seed(7)
76
+ self.model = TinyCausalModel().eval()
77
+ self.tokenizer = CharacterTokenizer()
78
+
79
+ def test_cka_handles_geometry_and_degeneracy(self):
80
+ x = torch.tensor([[1., 0.], [0., 2.], [-2., 1.], [1., -1.]])
81
+ rotated = torch.stack([-x[:, 1], x[:, 0]], dim=1) * 4 + 12
82
+ self.assertAlmostEqual(_linear_cka(x, rotated), 1., places=5)
83
+ self.assertAlmostEqual(_linear_cka(x, x * 1e-8), 1., places=5)
84
+ self.assertIsNone(_linear_cka(torch.ones(4, 2), x))
85
+
86
+ def test_default_silhouette_uses_original_related_and_unrelated_pairs(self):
87
+ related = [("dog", "wolf"), ("cat", "lion"), ("river", "ocean"),
88
+ ("happy", "joyful"), ("cold", "frozen"), ("king", "queen"),
89
+ ("python", "code"), ("car", "vehicle"), ("run", "sprint")]
90
+ unrelated = [("dog", "democracy"), ("cat", "algebra"), ("river", "justice"),
91
+ ("happy", "concrete"), ("cold", "philosophy"), ("king", "equation"),
92
+ ("python", "banana"), ("car", "silence"), ("run", "orange")]
93
+ # Compute the expected measurement independently from direct model
94
+ # outputs, so substituting generic cluster means breaks this contract.
95
+ vectors = {}
96
+ with torch.no_grad():
97
+ for word in set(w for pair in related + unrelated for w in pair):
98
+ out = self.model(**self.tokenizer(word, return_tensors="pt"), output_hidden_states=True)
99
+ vectors[word] = [state[0, -1].float() for state in out.hidden_states[1:]]
100
+ result = run_inspection(self.model, self.tokenizer, "silhouette", {})
101
+ for row in result["rows"]:
102
+ layer = row["layer"]
103
+ means = [sum(float(torch.nn.functional.cosine_similarity(
104
+ vectors[a][layer], vectors[b][layer], dim=0)) for a, b in pairs) / 9
105
+ for pairs in (related, unrelated)]
106
+ self.assertAlmostEqual(row["within_cosine"], means[0], places=6)
107
+ self.assertAlmostEqual(row["cross_cosine"], means[1], places=6)
108
+ self.assertAlmostEqual(row["separation"], means[0] - means[1], places=6)
109
+ self.assertEqual(result["details"]["related_pairs"], [list(pair) for pair in related])
110
+ self.assertEqual(result["details"]["unrelated_pairs"], [list(pair) for pair in unrelated])
111
+
112
+ def test_custom_silhouette_still_uses_explicit_groups(self):
113
+ result = run_inspection(self.model, self.tokenizer, "silhouette", {"words": "dog, wolf\ncar, truck"})
114
+ self.assertTrue(result["details"]["custom_probes"])
115
+ self.assertEqual(result["details"]["groups"], {"Group 1": ["dog", "wolf"], "Group 2": ["car", "truck"]})
116
+
117
+ def test_attention_uniform_causal_rows_have_unit_normalized_entropy(self):
118
+ r = run_inspection(self.model, self.tokenizer, "attention", {"text": "abcdef"})
119
+ for row in r["rows"]:
120
+ self.assertAlmostEqual(row["normalized_entropy"], 1., places=5)
121
+ self.assertAlmostEqual(row["head_diversity"], 0., places=5)
122
+
123
+ def test_gradient_does_not_change_weights_flags_or_existing_grads(self):
124
+ params = list(self.model.parameters())
125
+ params[0].requires_grad_(False)
126
+ params[-1].grad = torch.ones_like(params[-1])
127
+ flags = [p.requires_grad for p in params]
128
+ weights = [p.detach().clone() for p in params]
129
+ original_grad = params[-1].grad
130
+ with torch.no_grad():
131
+ r = run_inspection(self.model, self.tokenizer, "gradient", {"text": "abcdef"})
132
+ self.assertTrue(any(row["gradient_norm"] > 0 for row in r["rows"]))
133
+ self.assertEqual(flags, [p.requires_grad for p in params])
134
+ self.assertIs(params[-1].grad, original_grad)
135
+ for old, p in zip(weights, params):
136
+ self.assertTrue(torch.equal(old, p))
137
+ self.assertTrue(all(p.grad is None for p in params[:-1]))
138
+
139
+ def test_gradient_restores_flags_on_failure(self):
140
+ params = list(self.model.parameters())
141
+ params[0].requires_grad_(False)
142
+ flags = [p.requires_grad for p in params]
143
+ self.model.fail = True
144
+ with self.assertRaisesRegex(RuntimeError, "injected"):
145
+ run_inspection(self.model, self.tokenizer, "gradient", {"text": "abcdef"})
146
+ self.assertEqual(flags, [p.requires_grad for p in params])
147
+
148
+ def test_butterfly_compares_matching_tokens_after_edit(self):
149
+ r = run_inspection(self.model, self.tokenizer, "butterfly", {
150
+ "text": "cat walks home", "compare_text": "dog walks home", "max_tokens": 40,
151
+ })
152
+ self.assertGreater(r["details"]["aligned_token_count"], 0)
153
+ self.assertTrue(any(row["cosine_distance"] > 0 for row in r["rows"]))
154
+ for pair in r["details"]["alignment"]:
155
+ self.assertEqual(pair["token_a"], pair["token_b"])
156
+ self.assertGreater(pair["position_a"], 1)
157
+
158
+ def test_identical_butterfly_inputs_measure_zero(self):
159
+ r = run_inspection(self.model, self.tokenizer, "butterfly", {
160
+ "text": "cat walks home", "compare_text": "cat walks home",
161
+ })
162
+ self.assertTrue(all(abs(row["cosine_distance"]) < 1e-6 for row in r["rows"]))
163
+
164
+ def test_tugofwar_scores_entire_multitoken_candidates(self):
165
+ r = run_inspection(self.model, self.tokenizer, "tugofwar", {
166
+ "choice_a": " MAPLE", "choice_b": " RAVEN", "max_tokens": 256,
167
+ })
168
+ self.assertEqual(len(r["rows"]), 4)
169
+ for row in r["rows"]:
170
+ self.assertEqual(row["candidate_a_tokens"], 6)
171
+ self.assertEqual(row["candidate_b_tokens"], 6)
172
+ self.assertLess(row["a_mean_logprob"], 0)
173
+
174
+ def test_tugofwar_candidates_share_context_when_lengths_differ(self):
175
+ class RecordingModel(TinyCausalModel):
176
+ def __init__(self):
177
+ super().__init__()
178
+ self.seen = []
179
+
180
+ def forward(self, input_ids, **kwargs):
181
+ self.seen.append(input_ids[0].tolist())
182
+ return super().forward(input_ids=input_ids, **kwargs)
183
+
184
+ model = RecordingModel().eval()
185
+ run_inspection(model, self.tokenizer, "tugofwar", {
186
+ "choice_a": "A", "choice_b": "BANANA", "text": "background " * 60, "max_tokens": 128,
187
+ })
188
+ for a, b in zip(model.seen[::2], model.seen[1::2]):
189
+ self.assertEqual(a[:-2], b[:-7])
190
+ self.assertLessEqual(len(a), 128)
191
+ self.assertLessEqual(len(b), 128)
192
+
193
+ def test_tugofwar_refuses_to_truncate_experimental_facts(self):
194
+ with self.assertRaisesRegex(ValueError, "complete conflicting facts"):
195
+ run_inspection(self.model, self.tokenizer, "tugofwar", {"max_tokens": 32})
196
+
197
+ def test_gradient_masks_padding_by_mask_and_preserves_real_eos_tokens(self):
198
+ class PaddedTokenizer(CharacterTokenizer):
199
+ pad_token_id = 1
200
+
201
+ def __call__(self, text, **kwargs):
202
+ return {"input_ids": torch.tensor([[1, 4, 1, 5, 1]]),
203
+ "attention_mask": torch.tensor([[1, 1, 1, 1, 0]])}
204
+
205
+ tokenizer = PaddedTokenizer()
206
+ inputs = tokenizer("unused")
207
+ with torch.no_grad():
208
+ expected = float(self.model(**inputs, labels=torch.tensor([[1, 4, 1, 5, -100]])).loss)
209
+ result = run_inspection(self.model, tokenizer, "gradient", {"text": "unused"})
210
+ self.assertAlmostEqual(result["details"]["loss"], expected, places=6)
211
+
212
+ def test_tools_return_finite_serializable_results(self):
213
+ for tool in ("fingerprint", "silhouette", "semantic", "cka", "attention", "gradient"):
214
+ with self.subTest(tool=tool):
215
+ r = run_inspection(self.model, self.tokenizer, tool, {"text": "abcdef"})
216
+ self.assertTrue(r["rows"])
217
+ json.dumps(r, allow_nan=False)
218
+
219
+ @unittest.skipUnless(importlib.util.find_spec("transformers"), "Transformers is optional for local unit checks")
220
+ def test_real_bf16_llama_contract_without_download(self):
221
+ from transformers import LlamaConfig, LlamaForCausalLM
222
+ with torch.random.fork_rng():
223
+ torch.manual_seed(42)
224
+ model = LlamaForCausalLM(LlamaConfig(
225
+ vocab_size=64, hidden_size=16, intermediate_size=24,
226
+ num_hidden_layers=2, num_attention_heads=2, num_key_value_heads=1,
227
+ max_position_embeddings=256, attn_implementation="eager",
228
+ )).to(torch.bfloat16).eval()
229
+ for tool in ("fingerprint", "silhouette", "semantic", "cka", "attention", "gradient", "butterfly", "tugofwar"):
230
+ with self.subTest(tool=tool):
231
+ r = run_inspection(model, self.tokenizer, tool, {
232
+ "text": "cat walks home", "compare_text": "dog walks home", "max_tokens": 256,
233
+ })
234
+ self.assertTrue(r["rows"])
235
+ json.dumps(r, allow_nan=False)
236
+ self.assertTrue(all(p.grad is None for p in model.parameters()))
237
+
238
+
239
+ if __name__ == "__main__":
240
+ unittest.main()
tests/test_presentation.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Regression checks for report execution safety and consent boundaries."""
2
+
3
+ import copy
4
+ import json
5
+ import math
6
+ import os
7
+ import shutil
8
+ import tempfile
9
+ import unittest
10
+ from pathlib import Path
11
+ from unittest.mock import patch
12
+
13
+ from bol_space.presentation import contribution_payload, create_report, render_result
14
+
15
+
16
+ def example_result():
17
+ return {
18
+ "title": "Layer sweep", "summary": "Compare selected layer interventions.",
19
+ "rows": [{"layer": 1, "loss": 2.2, "damage": 0.15}],
20
+ "plot": {"kind": "line", "title": "Loss by layer", "x_label": "Layer", "y_label": "Loss",
21
+ "series": [{"name": "Loss", "x": [1, 2], "y": [2.2, 2.3]}]},
22
+ "details": {"baseline_loss": 2.05}, "warnings": [],
23
+ "text_outputs": [{"label": "Prompt", "text": "The sky is"}],
24
+ "model": {"repo_id": "HuggingFaceTB/SmolLM2-1.7B-Instruct", "revision": "a" * 40,
25
+ "architecture": "LlamaForCausalLM", "dtype": "bfloat16", "parameters": 1710000000},
26
+ "run": {"id": "0a234567-abcd-abcd-abcd-0123456789ab", "created_at": "2026-09-06T12:00:00+00:00",
27
+ "tool": "layer_sweep", "seconds": 2.7, "peak_vram_gb": 4.2,
28
+ "options": {"seed": 42, "max_tokens": 128, "sample_layers": 4}},
29
+ }
30
+
31
+
32
+ class PresentationTests(unittest.TestCase):
33
+ def setUp(self):
34
+ self.directories = set()
35
+
36
+ def tearDown(self):
37
+ for directory in self.directories:
38
+ shutil.rmtree(directory)
39
+
40
+ def report(self, result):
41
+ paths = create_report(result)
42
+ self.directories.add(Path(paths[0]).parent)
43
+ return paths
44
+
45
+ def test_report_escapes_html_and_script_terminators(self):
46
+ result = example_result()
47
+ attack = '</script><script>window.HACKED="yes"</script><img src=x onerror=alert(1)>'
48
+ result["title"] = attack
49
+ result["summary"] = attack
50
+ result["rows"][0][attack] = attack
51
+ result["plot"]["series"][0]["name"] = attack
52
+ result["plot"]["series"][0]["x"] = [attack, attack]
53
+ result["plot"]["title"] = attack
54
+ result["details"][attack] = attack
55
+ result["text_outputs"] = [{"label": attack, "text": attack}]
56
+ json_path, html_path = self.report(result)
57
+ document = Path(html_path).read_text()
58
+ self.assertNotIn(attack, document)
59
+ self.assertNotIn('<script>window.HACKED=', document)
60
+ self.assertNotIn('<img src=x', document)
61
+ self.assertIn('&lt;/script&gt;', document)
62
+ self.assertNotRegex(document, r'<script\s+[^>]*src\s*=')
63
+ self.assertIn('Plotly.newPlot', document)
64
+ self.assertIn('@media print', document)
65
+ self.assertEqual(json.loads(Path(json_path).read_text())["title"], attack)
66
+ markdown, figure, frame, texts = render_result(result)
67
+ self.assertNotIn('<img src=x', markdown + texts)
68
+ self.assertNotIn('</script>', figure.to_json())
69
+ self.assertEqual(len(frame), 1)
70
+
71
+ def test_text_free_payload_has_no_custom_text_through_any_label(self):
72
+ result = example_result()
73
+ canary = "CUSTOM_CANARY_829740"
74
+ result["title"] = canary
75
+ result["summary"] = canary
76
+ result["details"] = {canary: [{"nested": canary}]}
77
+ result["warnings"] = [canary]
78
+ result["rows"][0].update({canary: 2, "component": canary, "name": canary, "loss": canary, "tokens": [5, 42]})
79
+ result["plot"].update({"title": canary, "x_label": canary, "y_label": canary})
80
+ result["plot"]["series"][0].update({"name": canary, "x": [canary, canary]})
81
+ result["text_outputs"] = [{"label": canary, "text": canary}]
82
+ result["run"]["options"].update({canary: 4, "prompt": canary, "component": canary, "layer_start": canary})
83
+ original = copy.deepcopy(result)
84
+ payload = contribution_payload(result, False)
85
+ encoded = json.dumps(payload, allow_nan=False)
86
+ self.assertNotIn(canary, encoded)
87
+ self.assertNotIn('"tokens"', encoded)
88
+ self.assertFalse(payload["includes_text"])
89
+ self.assertEqual(payload["consent_version"], "1")
90
+ self.assertEqual(payload["data"]["run"]["options"]["seed"], 42)
91
+ self.assertEqual(payload["data"]["rows"][0]["damage"], 0.15)
92
+ self.assertEqual(payload["data"]["plot"]["series"][0]["x"], [0, 1])
93
+ self.assertEqual(result, original)
94
+
95
+ def test_text_free_heatmap_keeps_measurements_without_labels(self):
96
+ result = example_result()
97
+ result["plot"] = {"kind": "heatmap", "x": ["SECRET_X"], "y": ["SECRET_Y"], "z": [[0.3]], "title": "SECRET_TITLE"}
98
+ plot = contribution_payload(result, False)["data"]["plot"]
99
+ self.assertEqual(plot, {"kind": "heatmap", "x": [0], "y": [0], "z": [[0.3]]})
100
+ self.assertEqual(len(render_result(result)[1].data), 1)
101
+
102
+ def test_text_consent_keeps_text_but_not_unknown_top_level_or_credentials(self):
103
+ result = example_result()
104
+ result["private_metadata"] = "UNEXPECTED_SECRET"
105
+ result["run"]["options"]["hf_token"] = "NEVER_EXPORT"
106
+ result["run"]["options"]["prompt"] = "An explicitly shared prompt"
107
+ result["model"]["authorization"] = "NEVER_EXPORT"
108
+ payload = contribution_payload(result, True)
109
+ self.assertTrue(payload["includes_text"])
110
+ self.assertEqual(payload["data"]["text_outputs"], result["text_outputs"])
111
+ self.assertIn("An explicitly shared prompt", json.dumps(payload))
112
+ self.assertNotIn("NEVER_EXPORT", json.dumps(payload))
113
+ self.assertNotIn("UNEXPECTED_SECRET", json.dumps(payload))
114
+
115
+ def test_unique_files_and_strict_json(self):
116
+ result = example_result()
117
+ first = self.report(result)
118
+ second = self.report(result)
119
+ self.assertTrue(set(first).isdisjoint(second))
120
+ for filename in (first[0], second[0]):
121
+ decoded = json.loads(Path(filename).read_text(), parse_constant=lambda value: self.fail(value))
122
+ self.assertEqual(decoded, result)
123
+
124
+ def test_original_reports_live_inside_the_configured_gradio_cache(self):
125
+ with tempfile.TemporaryDirectory() as cache_root:
126
+ with patch.dict(os.environ, {"GRADIO_TEMP_DIR": cache_root}):
127
+ paths = create_report(example_result())
128
+ for path in paths:
129
+ self.assertTrue(Path(path).is_relative_to(cache_root))
130
+ self.assertTrue(Path(path).is_file())
131
+
132
+ def test_partial_report_files_are_removed_when_writing_fails(self):
133
+ original_write = Path.write_text
134
+
135
+ def write_or_fail(path, *args, **kwargs):
136
+ if path.suffix == ".html":
137
+ raise OSError("Simulated full disk")
138
+ return original_write(path, *args, **kwargs)
139
+
140
+ with tempfile.TemporaryDirectory() as cache_root:
141
+ with patch.dict(os.environ, {"GRADIO_TEMP_DIR": cache_root}), patch.object(Path, "write_text", write_or_fail):
142
+ with self.assertRaisesRegex(OSError, "full disk"):
143
+ create_report(example_result())
144
+ self.assertEqual(list(Path(cache_root).iterdir()), [])
145
+
146
+ def test_nonfinite_values_are_rejected(self):
147
+ for bad in (math.nan, math.inf, -math.inf):
148
+ for function in (render_result, create_report, lambda data: contribution_payload(data, False)):
149
+ with self.subTest(bad=bad, function=function):
150
+ result = example_result()
151
+ result["rows"][0]["loss"] = bad
152
+ with self.assertRaisesRegex(ValueError, "finite"):
153
+ function(result)
154
+
155
+ def test_missing_plot_and_rows_still_render(self):
156
+ summary, figure, frame, text = render_result({"title": "A result", "summary": "Complete"})
157
+ self.assertIn("A result", summary)
158
+ self.assertEqual(len(figure.layout.annotations), 1)
159
+ self.assertTrue(frame.empty)
160
+ self.assertEqual(text, "")
161
+
162
+ def test_consent_requires_actual_boolean(self):
163
+ with self.assertRaises(TypeError):
164
+ contribution_payload(example_result(), "false")
165
+
166
+
167
+ if __name__ == "__main__":
168
+ unittest.main()
tests/test_surgery.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPU contract tests; no checkpoint downloads are required."""
2
+
3
+ import json
4
+ import math
5
+ from types import SimpleNamespace
6
+
7
+ import pytest
8
+ import torch
9
+ from torch import nn
10
+
11
+ from bol_space.surgery import run_surgery
12
+
13
+
14
+ class TinyTokenizer:
15
+ # EOS and padding share an ID, as on many causal-model tokenizers.
16
+ pad_token_id = 0
17
+ eos_token_id = 0
18
+
19
+ def __call__(self, text, *, return_tensors, truncation, max_length, padding=False):
20
+ texts = [text] if isinstance(text, str) else text
21
+ lengths = [min(len(item.split()), max_length) for item in texts]
22
+ width = max(lengths)
23
+ return {
24
+ "input_ids": torch.zeros((len(texts), width), dtype=torch.long),
25
+ "attention_mask": torch.tensor(
26
+ [[1] * length + [0] * (width - length) for length in lengths]
27
+ ),
28
+ }
29
+
30
+ def decode(self, tokens, skip_special_tokens=True):
31
+ return "generated text"
32
+
33
+
34
+ class TinyModel(nn.Module):
35
+ def __init__(self, count=4, fail_when_edited=False):
36
+ super().__init__()
37
+ self.model = nn.Module()
38
+ self.model.layers = nn.ModuleList()
39
+ self.fail_when_edited = fail_when_edited
40
+ for _ in range(count):
41
+ layer = nn.Module()
42
+ layer.proj = nn.Linear(2, 2)
43
+ layer.norm = nn.LayerNorm(2)
44
+ nn.init.ones_(layer.proj.weight)
45
+ self.model.layers.append(layer)
46
+ self.bfloat16()
47
+
48
+ def forward(self, input_ids, attention_mask, use_cache=False):
49
+ if self.fail_when_edited and any(
50
+ not bool(layer.proj.weight.count_nonzero()) for layer in self.model.layers
51
+ ):
52
+ raise RuntimeError("injected forward failure")
53
+ score = sum(layer.proj.weight.float().mean() for layer in self.model.layers)
54
+ logits = torch.stack([score, torch.zeros_like(score)])
55
+ return SimpleNamespace(logits=logits.expand(*input_ids.shape, 2))
56
+
57
+ def generate(self, **kwargs):
58
+ return kwargs["input_ids"]
59
+
60
+
61
+ def options(**kwargs):
62
+ return {"text": "one two three\none two", "prompt": "", **kwargs}
63
+
64
+
65
+ def snapshot(model):
66
+ return {name: tensor.detach().clone() for name, tensor in model.named_parameters()}
67
+
68
+
69
+ def assert_restored(model, before):
70
+ for name, tensor in model.named_parameters():
71
+ assert torch.equal(tensor, before[name]), name
72
+
73
+
74
+ def test_forward_error_restores_modified_weights_and_training_state():
75
+ model = TinyModel(fail_when_edited=True)
76
+ model.train()
77
+ before = snapshot(model)
78
+ with pytest.raises(RuntimeError, match="injected forward failure"):
79
+ run_surgery(model, TinyTokenizer(), "layer_sweep", options(sample_layers=2))
80
+ assert_restored(model, before)
81
+ assert model.training
82
+
83
+
84
+ @pytest.mark.parametrize("tool", ["layer_sweep", "component", "trisect", "wormhole", "noise", "duets"])
85
+ def test_every_tool_returns_finite_json_and_restores_weights(tool):
86
+ model = TinyModel()
87
+ before = snapshot(model)
88
+ result = run_surgery(model, TinyTokenizer(), tool, options(sample_layers=2))
89
+ assert result["rows"]
90
+ assert result["details"]["integrity_verified"] is True
91
+ assert_restored(model, before)
92
+ json.dumps(result, allow_nan=False)
93
+
94
+
95
+ def test_eos_tokens_are_scored_even_when_eos_is_the_padding_token():
96
+ # Every real target token is EOS=0. Masking by token ID would erase them all.
97
+ result = run_surgery(TinyModel(), TinyTokenizer(), "layer_sweep", options(sample_layers=1))
98
+ assert result["details"]["baseline_loss"] == pytest.approx(math.log1p(math.exp(-4)), abs=1e-6)
99
+ assert result["details"]["scored_tokens"] == 3
100
+
101
+
102
+ def test_duets_measure_excess_pair_damage_with_correct_sign():
103
+ result = run_surgery(
104
+ TinyModel(), TinyTokenizer(), "duets",
105
+ options(sample_layers=2, layer_start=0, layer_end=1),
106
+ )
107
+ # Logit for target class is 4 intact, 3 after one cut, 2 after both cuts.
108
+ baseline = math.log1p(math.exp(-4))
109
+ single = math.log1p(math.exp(-3)) - baseline
110
+ pair = math.log1p(math.exp(-2)) - baseline
111
+ row = result["rows"][0]
112
+ assert (row["layer_a"], row["layer_b"]) == (0, 1)
113
+ assert row["interaction"] == pytest.approx(pair - 2 * single, abs=1e-6)
114
+ assert row["interaction"] > 0
115
+ assert result["plot"]["z"][0][1] == result["plot"]["z"][1][0]
116
+
117
+
118
+ def test_unknown_component_fails_without_changing_weights():
119
+ model = TinyModel()
120
+ before = snapshot(model)
121
+ with pytest.raises(ValueError, match="No matrix component"):
122
+ run_surgery(model, TinyTokenizer(), "component", options(component="does_not_exist"))
123
+ assert_restored(model, before)
124
+
125
+
126
+ @pytest.mark.parametrize("start,end", [(-1, 2), (3, 1), (0, 4)])
127
+ def test_invalid_layer_ranges_are_rejected(start, end):
128
+ with pytest.raises(ValueError, match="layer range"):
129
+ run_surgery(TinyModel(), TinyTokenizer(), "layer_sweep", options(layer_start=start, layer_end=end))
130
+
131
+
132
+ def test_noise_is_reproducible_without_changing_global_rng():
133
+ model = TinyModel()
134
+ rng = torch.random.get_rng_state().clone()
135
+ first = run_surgery(model, TinyTokenizer(), "noise", options(sample_layers=2, noise_scale=0.1))
136
+ second = run_surgery(model, TinyTokenizer(), "noise", options(sample_layers=2, noise_scale=0.1))
137
+ assert first["rows"] == second["rows"]
138
+ assert torch.equal(rng, torch.random.get_rng_state())
139
+
140
+
141
+ def test_models_without_a_recognized_layer_list_fail_explicitly():
142
+ with pytest.raises(ValueError, match="layer list"):
143
+ run_surgery(nn.Linear(2, 2), TinyTokenizer(), "layer_sweep", options())
144
+
145
+
146
+ def test_shared_weights_in_an_unsampled_layer_are_rejected():
147
+ model = TinyModel()
148
+ model.model.layers[3].proj.weight = model.model.layers[0].proj.weight
149
+ with pytest.raises(ValueError, match="share parameters"):
150
+ run_surgery(model, TinyTokenizer(), "layer_sweep", options(layer_start=0, layer_end=0))
151
+
152
+
153
+ def test_generation_error_after_a_cut_restores_weights():
154
+ class GenerationFailure(TinyModel):
155
+ def generate(self, **kwargs):
156
+ if any(not bool(layer.proj.weight.count_nonzero()) for layer in self.model.layers):
157
+ raise RuntimeError("injected generation failure")
158
+ return super().generate(**kwargs)
159
+
160
+ model = GenerationFailure()
161
+ before = snapshot(model)
162
+ with pytest.raises(RuntimeError, match="injected generation failure"):
163
+ run_surgery(model, TinyTokenizer(), "layer_sweep", options(sample_layers=2, prompt="one two"))
164
+ assert_restored(model, before)
165
+
166
+
167
+ def test_nonfinite_intervention_loss_raises_and_restores():
168
+ class NonfiniteModel(TinyModel):
169
+ def forward(self, **kwargs):
170
+ output = super().forward(**kwargs)
171
+ if any(not bool(layer.proj.weight.count_nonzero()) for layer in self.model.layers):
172
+ output.logits = output.logits * float("nan")
173
+ return output
174
+
175
+ model = NonfiniteModel()
176
+ before = snapshot(model)
177
+ with pytest.raises(FloatingPointError, match="non-finite"):
178
+ run_surgery(model, TinyTokenizer(), "layer_sweep", options(sample_layers=2))
179
+ assert_restored(model, before)