hunterbown commited on
Commit
1768b31
·
verified ·
1 Parent(s): f361393

Add Colab-ready SCU demo notebook

Browse files
Files changed (1) hide show
  1. notebooks/SCU_Demo.ipynb +554 -0
notebooks/SCU_Demo.ipynb ADDED
@@ -0,0 +1,554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Shannon Control Unit — Dial-in LLM regularization (Colab demo)\n\n",
8
+ "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/hmbown/shannon-control-unit/blob/main/notebooks/SCU_Demo.ipynb)\n\n",
9
+ "Held-out: Base 3.920 BPT (ppl 15.14) → SCU 3.676 (ppl 12.78), Δ −0.244 BPT ≈ −15.6% ppl.\n\n",
10
+ "Adapters inherit Meta Llama 3.2 license; SCU code Apache-2.0. U.S. patent pending (provisional filed Sep 2025).\n"
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "metadata": {},
17
+ "outputs": [],
18
+ "source": [
19
+ "# 1) Setup\n",
20
+ "import os, sys, subprocess, random, json\n",
21
+ "from pathlib import Path\n",
22
+ "\n",
23
+ "# Minimal deps; bitsandbytes only on CUDA\n",
24
+ "os.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\"\n",
25
+ "def _pip_install(pkgs):\n",
26
+ " cmd = [sys.executable, '-m', 'pip', 'install', '-q'] + list(pkgs)\n",
27
+ " return subprocess.call(cmd)\n",
28
+ "\n",
29
+ "# Ensure core libs; rely on preinstalled torch\n",
30
+ "_pip_install(['transformers', 'peft', 'accelerate', 'huggingface_hub', 'matplotlib', 'numpy', 'pandas'])\n",
31
+ "\n",
32
+ "# Optional: install bitsandbytes only if CUDA is available\n",
33
+ "cuda_avail = False\n",
34
+ "try:\n",
35
+ " import torch\n",
36
+ " cuda_avail = torch.cuda.is_available()\n",
37
+ "except Exception:\n",
38
+ " pass\n",
39
+ "if cuda_avail:\n",
40
+ " _ = _pip_install(['bitsandbytes'])\n",
41
+ "\n",
42
+ "# Optional: login to Hugging Face to access gated models (accept Llama 3.2 terms).\n",
43
+ "# from huggingface_hub import login\n",
44
+ "# login() # Ensure you've accepted https://huggingface.co/meta-llama/Llama-3.2-1B and 3B\n",
45
+ "\n",
46
+ "# Seed everything deterministically\n",
47
+ "import numpy as np\n",
48
+ "random.seed(42)\n",
49
+ "np.random.seed(42)\n",
50
+ "try:\n",
51
+ " import torch\n",
52
+ " torch.manual_seed(42)\n",
53
+ " if torch.cuda.is_available():\n",
54
+ " torch.cuda.manual_seed_all(42)\n",
55
+ "except Exception:\n",
56
+ " pass\n",
57
+ "\n",
58
+ "print('Setup complete.')\n"
59
+ ]
60
+ },
61
+ {
62
+ "cell_type": "code",
63
+ "execution_count": null,
64
+ "metadata": {},
65
+ "outputs": [],
66
+ "source": [
67
+ "# 2) Device & precision detection\n",
68
+ "import torch\n",
69
+ "from pathlib import Path\n",
70
+ "device = 'cuda' if torch.cuda.is_available() else ('mps' if torch.backends.mps.is_available() else 'cpu')\n",
71
+ "print('Device:', device, '| Torch:', torch.__version__, '| CUDA:', torch.version.cuda)\n",
72
+ "\n",
73
+ "bnb_config = None\n",
74
+ "if device == 'cuda':\n",
75
+ " try:\n",
76
+ " from transformers import BitsAndBytesConfig\n",
77
+ " bnb_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16)\n",
78
+ " four_bit_active = True\n",
79
+ " except Exception as e:\n",
80
+ " print('bitsandbytes not available; falling back to fp16/fp32.')\n",
81
+ " bnb_config = None\n",
82
+ " four_bit_active = False\n",
83
+ "else:\n",
84
+ " four_bit_active = False\n",
85
+ "\n",
86
+ "IS_CUDA = device == 'cuda'\n",
87
+ "IS_MPS = device == 'mps'\n",
88
+ "IS_CPU = device == 'cpu'\n",
89
+ "print('4-bit active:' , four_bit_active)\n",
90
+ "if IS_MPS:\n",
91
+ " print('Running fp32 on Apple Silicon (MPS).')\n",
92
+ "if IS_CPU:\n",
93
+ " print('WARNING: Using CPU; training is disabled and steps reduced.')\n"
94
+ ]
95
+ },
96
+ {
97
+ "cell_type": "code",
98
+ "execution_count": null,
99
+ "metadata": {},
100
+ "outputs": [],
101
+ "source": [
102
+ "# 3) Config\n",
103
+ "MODEL_SIZE = '1B' # '1B' or '3B'\n",
104
+ "TARGET_S = 0.01\n",
105
+ "STEPS = 250 if IS_CUDA else (120 if IS_MPS else 40)\n",
106
+ "BLOCK_SIZE = 1024\n",
107
+ "BATCH_SIZE = 1\n",
108
+ "GRAD_ACCUM = 4\n",
109
+ "PRIOR_SIGMA = 0.01\n",
110
+ "\n",
111
+ "root = Path.cwd()\n",
112
+ "out_dir = Path('outputs/PI/demo_run')\n",
113
+ "fig_dir = Path('assets/figures')\n",
114
+ "out_dir.mkdir(parents=True, exist_ok=True)\n",
115
+ "fig_dir.mkdir(parents=True, exist_ok=True)\n",
116
+ "print('Outputs ->', out_dir.resolve())\n",
117
+ "print('Figures ->', fig_dir.resolve())\n"
118
+ ]
119
+ },
120
+ {
121
+ "cell_type": "code",
122
+ "execution_count": null,
123
+ "metadata": {},
124
+ "outputs": [],
125
+ "source": [
126
+ "# 4) Load base model + optional adapter\n",
127
+ "from transformers import AutoModelForCausalLM, AutoTokenizer\n",
128
+ "base_id = 'meta-llama/Llama-3.2-1B' if MODEL_SIZE == '1B' else 'meta-llama/Llama-3.2-3B'\n",
129
+ "if MODEL_SIZE == '3B':\n",
130
+ " print('Note: 3B may OOM on Colab T4; prefer 1B for demo.')\n",
131
+ "\n",
132
+ "try:\n",
133
+ " tok = AutoTokenizer.from_pretrained(base_id, use_fast=True)\n",
134
+ " if tok.pad_token is None:\n",
135
+ " tok.pad_token = tok.eos_token\n",
136
+ " if IS_CUDA and bnb_config is not None:\n",
137
+ " model = AutoModelForCausalLM.from_pretrained(\n",
138
+ " base_id, quantization_config=bnb_config, device_map='auto'\n",
139
+ " )\n",
140
+ " else:\n",
141
+ " model = AutoModelForCausalLM.from_pretrained(\n",
142
+ " base_id, torch_dtype=torch.float32, device_map='auto' if not IS_CPU else None\n",
143
+ " )\n",
144
+ " model.config.pad_token_id = tok.pad_token_id\n",
145
+ " try:\n",
146
+ " model.config.use_cache = False\n",
147
+ " except Exception:\n",
148
+ " pass\n",
149
+ " model.eval()\n",
150
+ " total_params = sum(p.numel() for p in model.parameters())/1e6\n",
151
+ " print(f'Loaded base: {base_id} | params: {total_params:.1f}M')\n",
152
+ " print('LoRA adapters: none loaded')\n",
153
+ "except Exception as e:\n",
154
+ " print('ERROR: Could not load base model/tokenizer.\n\\n'\n",
155
+ " 'This model is gated. Ensure you are logged in to Hugging Face '\n",
156
+ " 'and have accepted the license terms for Llama 3.2.\n\\n'\n",
157
+ " f'Visit: https://huggingface.co/{base_id}', sep='')\n",
158
+ " print('Original error:', repr(e))\n",
159
+ " model = None\n",
160
+ " tok = None\n"
161
+ ]
162
+ },
163
+ {
164
+ "cell_type": "code",
165
+ "execution_count": null,
166
+ "metadata": {},
167
+ "outputs": [],
168
+ "source": [
169
+ "# 5) Quick generation sanity\n",
170
+ "def generate_text(prompt, max_new_tokens=64):\n",
171
+ " if model is None or tok is None:\n",
172
+ " return '[model not available]'\n",
173
+ " inputs = tok(prompt, return_tensors='pt').to(model.device)\n",
174
+ " with torch.no_grad():\n",
175
+ " out = model.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False, \n",
176
+ " pad_token_id=tok.pad_token_id, eos_token_id=tok.eos_token_id)\n",
177
+ " return tok.decode(out[0], skip_special_tokens=True)\n",
178
+ "\n",
179
+ "for p in [\n",
180
+ " 'Explain Shannon Control Unit (SCU) in one paragraph.',\n",
181
+ " 'Write a haiku about control loops in AI.',\n",
182
+ " 'List three practical uses of LoRA adapters.'\n",
183
+ "]:\n",
184
+ " print('\n--- Prompt ---')\n",
185
+ " print(p)\n",
186
+ " print('\n--- Output ---')\n",
187
+ " print(generate_text(p))\n"
188
+ ]
189
+ },
190
+ {
191
+ "cell_type": "code",
192
+ "execution_count": null,
193
+ "metadata": {},
194
+ "outputs": [],
195
+ "source": [
196
+ "# 6) Metrics utilities\n",
197
+ "import math\n",
198
+ "\n",
199
+ "def calculate_bpt(model, text, tok, max_len=512):\n",
200
+ " enc = tok(text, return_tensors='pt', truncation=True, max_length=max_len)\n",
201
+ " enc = {k: v.to(model.device) for k, v in enc.items()}\n",
202
+ " labels = enc['input_ids'].clone()\n",
203
+ " with torch.no_grad():\n",
204
+ " out = model(**enc, labels=labels)\n",
205
+ " return out.loss.item() / math.log(2) # bits per token\n",
206
+ "\n",
207
+ "def param_bpt_lora(model, prior_sigma=0.01, tokens_norm=512_000):\n",
208
+ " quad = 0.0\n",
209
+ " for name, p in model.named_parameters():\n",
210
+ " if p.requires_grad and ('lora' in name.lower() or 'lora_' in name.lower()):\n",
211
+ " quad += (p.float() ** 2).sum().item()\n",
212
+ " nats = quad / (2.0 * (prior_sigma ** 2))\n",
213
+ " bits = nats / math.log(2)\n",
214
+ " return bits / max(tokens_norm, 1)\n",
215
+ "\n",
216
+ "def compute_S(data_bpt, param_bpt):\n",
217
+ " return param_bpt / max(data_bpt + param_bpt, 1e-12)\n",
218
+ "\n",
219
+ "def bpt_to_ppl(bpt):\n",
220
+ " return 2.0 ** bpt\n"
221
+ ]
222
+ },
223
+ {
224
+ "cell_type": "code",
225
+ "execution_count": null,
226
+ "metadata": {},
227
+ "outputs": [],
228
+ "source": [
229
+ "# 7) Reproduce validation (Base vs SCU)\n",
230
+ "from peft import PeftModel\n",
231
+ "import pandas as pd\n",
232
+ "\n",
233
+ "def load_val_texts():\n",
234
+ " # Prefer data/val.txt if present, else small built-in list\n",
235
+ " path = Path('data/val.txt')\n",
236
+ " if path.exists():\n",
237
+ " return [line.strip() for line in path.read_text().splitlines() if line.strip()][:25]\n",
238
+ " return [\n",
239
+ " 'Quantum error correction protects information from decoherence and noise.',\n",
240
+ " 'The SCU adjusts regularization strength to track a target parameter ratio.',\n",
241
+ " 'LoRA adapters enable efficient fine-tuning of large language models.',\n",
242
+ " 'Perplexity is an exponential function of bits per token.',\n",
243
+ " 'PI control uses proportional and integral action to reduce steady-state error.',\n",
244
+ " 'Evaluation on held-out documents ensures generalization beyond training.'\n",
245
+ " ]\n",
246
+ "\n",
247
+ "def try_load_adapter_into(model):\n",
248
+ " # 1) Local demo adapter if exists\n",
249
+ " local = out_dir\n",
250
+ " if (local / 'adapter_config.json').exists():\n",
251
+ " print(f'Loading local adapter: {local}')\n",
252
+ " return PeftModel.from_pretrained(model, local, is_trainable=False)\n",
253
+ " # 2) Published adapter (if available)\n",
254
+ " for repo_id in ['hunterbown/shannon-control-unit']:\n",
255
+ " try:\n",
256
+ " print(f'Trying to load adapter from HF: {repo_id}')\n",
257
+ " return PeftModel.from_pretrained(model, repo_id, is_trainable=False)\n",
258
+ " except Exception as e:\n",
259
+ " print(f'Could not load {repo_id}:', repr(e))\n",
260
+ " return None\n",
261
+ "\n",
262
+ "val_texts = load_val_texts()\n",
263
+ "print(f'Validation texts: {len(val_texts)}')\n",
264
+ "\n",
265
+ "base_bpts = []\n",
266
+ "if model is not None and tok is not None:\n",
267
+ " for t in val_texts:\n",
268
+ " try:\n",
269
+ " base_bpts.append(calculate_bpt(model, t, tok))\n",
270
+ " except Exception as e:\n",
271
+ " print('Eval error on base model:', repr(e))\n",
272
+ " break\n",
273
+ "\n",
274
+ "adapter = None\n",
275
+ "scu_bpts = []\n",
276
+ "param_bpt = None\n",
277
+ "if model is not None and tok is not None:\n",
278
+ " try:\n",
279
+ " adapter = try_load_adapter_into(model)\n",
280
+ " except Exception as e:\n",
281
+ " print('Adapter load error:', repr(e))\n",
282
+ "\n",
283
+ " if adapter is not None:\n",
284
+ " adapter.eval()\n",
285
+ " # Evaluate with adapter\n",
286
+ " for t in val_texts:\n",
287
+ " try:\n",
288
+ " scu_bpts.append(calculate_bpt(adapter, t, tok))\n",
289
+ " except Exception as e:\n",
290
+ " print('Eval error with adapter:', repr(e))\n",
291
+ " break\n",
292
+ " # ParamBPT for LoRA\n",
293
+ " try:\n",
294
+ " param_bpt = param_bpt_lora(adapter, prior_sigma=PRIOR_SIGMA, tokens_norm=512_000)\n",
295
+ " except Exception as e:\n",
296
+ " print('ParamBPT error:', repr(e))\n",
297
+ "\n",
298
+ "def summarize_rows(base_bpts, scu_bpts, param_bpt):\n",
299
+ " rows = []\n",
300
+ " if base_bpts:\n",
301
+ " bbpt = float(np.mean(base_bpts))\n",
302
+ " rows.append(['Base', bbpt, np.nan, 0.0, bpt_to_ppl(bbpt)])\n",
303
+ " if scu_bpts:\n",
304
+ " sbpt = float(np.mean(scu_bpts))\n",
305
+ " pb = float(param_bpt) if param_bpt is not None else np.nan\n",
306
+ " S = compute_S(sbpt, pb) if pb == pb else np.nan\n",
307
+ " rows.append(['SCU', sbpt, pb, S, bpt_to_ppl(sbpt)])\n",
308
+ " return pd.DataFrame(rows, columns=['Model', 'DataBPT', 'ParamBPT', 'S', 'PPL'])\n",
309
+ "\n",
310
+ "df_val = summarize_rows(base_bpts, scu_bpts, param_bpt)\n",
311
+ "if not df_val.empty:\n",
312
+ " with pd.option_context('display.precision', 4):\n",
313
+ " print(df_val)\n",
314
+ " if len(df_val) == 2:\n",
315
+ " delta_bpt = df_val.loc[0, 'DataBPT'] - df_val.loc[1, 'DataBPT']\n",
316
+ " base_ppl = df_val.loc[0, 'PPL']\n",
317
+ " scu_ppl = df_val.loc[1, 'PPL']\n",
318
+ " ppl_drop_pct = 100.0 * (base_ppl - scu_ppl) / max(base_ppl, 1e-9)\n",
319
+ " print(f"ΔBPT = {delta_bpt:.3f} | PPL drop ≈ {ppl_drop_pct:.1f}%")\n",
320
+ "else:\n",
321
+ " print('Validation skipped (model or adapter unavailable).')\n"
322
+ ]
323
+ },
324
+ {
325
+ "cell_type": "code",
326
+ "execution_count": null,
327
+ "metadata": {},
328
+ "outputs": [],
329
+ "source": [
330
+ "# 8) Control demonstration (training run)\n",
331
+ "import shlex, platform, time\n",
332
+ "\n",
333
+ "def find_upwards(rel_path, max_up=3):\n",
334
+ " p = Path(rel_path)\n",
335
+ " if p.exists():\n",
336
+ " return p\n",
337
+ " cur = Path.cwd()\n",
338
+ " for _ in range(max_up):\n",
339
+ " cand = cur / rel_path\n",
340
+ " if cand.exists():\n",
341
+ " return cand\n",
342
+ " cur = cur.parent\n",
343
+ " return None\n",
344
+ "\n",
345
+ "script_path = find_upwards('scripts/train_scu.py')\n",
346
+ "print('Trainer script:', script_path)\n",
347
+ "\n",
348
+ "log_csv = out_dir / 'train_log.csv'\n",
349
+ "metadata_json = out_dir / 'metadata.json'\n",
350
+ "\n",
351
+ "should_train = IS_CUDA or IS_MPS\n",
352
+ "if not should_train:\n",
353
+ " print('CPU detected: skipping training. Will simulate control log if needed.')\n",
354
+ "\n",
355
+ "if should_train and script_path and script_path.exists():\n",
356
+ " base_flag = 'meta-llama/Llama-3.2-1B' if MODEL_SIZE == '1B' else 'meta-llama/Llama-3.2-3B'\n",
357
+ " cmd = f\"{sys.executable} {script_path} --base_model {base_flag} --adapter_out {out_dir} \\\n",
358
+ " --steps {STEPS} --batch_size {BATCH_SIZE} --gradient_accumulation_steps {GRAD_ACCUM} \\\n",
359
+ " --block_size {BLOCK_SIZE} --prior_sigma {PRIOR_SIGMA} \\\n",
360
+ " --target_s {TARGET_S} --kp 0.8 --ki 0.15 --log_csv {log_csv} --train_data data/train.txt\"\n",
361
+ " print('Launching trainer:')\n",
362
+ " print(cmd)\n",
363
+ " try:\n",
364
+ " subprocess.run(shlex.split(cmd), check=True)\n",
365
+ " except subprocess.CalledProcessError as e:\n",
366
+ " print('Training failed:', e)\n",
367
+ " print('Falling back to simulated log.')\n",
368
+ "\n",
369
+ "# If log missing (CPU or failure), create a toy control log so plots exist\n",
370
+ "if not log_csv.exists():\n",
371
+ " import pandas as pd\n",
372
+ " import numpy as np\n",
373
+ " steps = 120 if IS_MPS else (250 if IS_CUDA else 80)\n",
374
+ " xs = np.arange(steps)\n",
375
+ " # Toy S(t): first-order approach to TARGET_S with small noise\n",
376
+ " S = TARGET_S + 0.3*TARGET_S*np.exp(-xs/25.0) * np.cos(xs/10.0) + 0.02*TARGET_S*np.random.default_rng(42).normal(size=steps)\n",
377
+ " lam = np.clip(1.0 + 2.0*np.exp(-xs/35.0), 1e-4, 10.0)\n",
378
+ " data_bpt = 3.9 - 0.0015*xs + 0.02*np.random.default_rng(0).normal(size=steps)\n",
379
+ " param_bpt = S * np.maximum(data_bpt + 1e-6, 1e-6) / np.maximum(1 - S, 1e-6)\n",
380
+ " df_sim = pd.DataFrame({\n",
381
+ " 'step': xs, 'data_bpt': data_bpt, 'param_bpt': param_bpt,\n",
382
+ " 'S': S, 'lambda': lam, 'I': np.cumsum(S - TARGET_S)*0.001, 'wall_time_s': xs * 0.5\n",
383
+ " })\n",
384
+ " out_dir.mkdir(parents=True, exist_ok=True)\n",
385
+ " df_sim.to_csv(log_csv, index=False)\n",
386
+ " with open(metadata_json, 'w') as f:\n",
387
+ " json.dump({'target_s': float(TARGET_S)}, f)\n",
388
+ "\n",
389
+ "# Show the tail of the log\n",
390
+ "import pandas as pd\n",
391
+ "if log_csv.exists():\n",
392
+ " df_tail = pd.read_csv(log_csv).tail(8)\n",
393
+ " print(df_tail)\n",
394
+ "else:\n",
395
+ " print('No training log found at', log_csv)\n"
396
+ ]
397
+ },
398
+ {
399
+ "cell_type": "code",
400
+ "execution_count": null,
401
+ "metadata": {},
402
+ "outputs": [],
403
+ "source": [
404
+ "# 9) Plot S(t) & λ(t)\n",
405
+ "import pandas as pd, matplotlib.pyplot as plt, numpy as np\n",
406
+ "from pathlib import Path\n",
407
+ "log_path = Path(out_dir) / 'train_log.csv'\n",
408
+ "df = pd.read_csv(log_path)\n",
409
+ "meta_path = Path(out_dir) / 'metadata.json'\n",
410
+ "if meta_path.exists():\n",
411
+ " metadata = json.loads(meta_path.read_text())\n",
412
+ " S_target = 100.0 * float(metadata.get('target_s', TARGET_S))\n",
413
+ "else:\n",
414
+ " S_target = 100.0 * float(TARGET_S)\n",
415
+ "\n",
416
+ "# S(t) with target band\n",
417
+ "plt.figure(figsize=(10,6), dpi=200)\n",
418
+ "plt.plot(df['step'], 100.0*df['S'], label='S(t)')\n",
419
+ "band = 0.2\n",
420
+ "plt.axhspan(S_target - band, S_target + band, alpha=0.15, color='tab:blue')\n",
421
+ "plt.xlabel('Step'); plt.ylabel('S (%)'); plt.title('S(t) tracking')\n",
422
+ "plt.legend(loc='best')\n",
423
+ "fig_dir.mkdir(parents=True, exist_ok=True)\n",
424
+ "plt.tight_layout(); plt.savefig(fig_dir / 's_curve.png')\n",
425
+ "plt.show()\n",
426
+ "\n",
427
+ "# λ(t) log-y\n",
428
+ "plt.figure(figsize=(10,6), dpi=200)\n",
429
+ "plt.semilogy(df['step'], df['lambda'], label='λ(t)')\n",
430
+ "plt.xlabel('Step'); plt.ylabel('λ'); plt.title('λ(t) bounded (log scale)')\n",
431
+ "plt.legend(loc='best')\n",
432
+ "plt.tight_layout(); plt.savefig(fig_dir / 'lambda_curve.png')\n",
433
+ "plt.show()\n",
434
+ "\n",
435
+ "# Settling time and steady-state error\n",
436
+ "S_pct = 100.0 * df['S'].values\n",
437
+ "steps = df['step'].values.astype(int)\n",
438
+ "lower, upper = S_target - band, S_target + band\n",
439
+ "settle_idx = None\n",
440
+ "window = 25\n",
441
+ "for i in range(len(S_pct) - window):\n",
442
+ " seg = S_pct[i:i+window]\n",
443
+ " if np.all((seg >= lower) & (seg <= upper)):\n",
444
+ " settle_idx = int(steps[i])\n",
445
+ " break\n",
446
+ "if settle_idx is None:\n",
447
+ " print('Settling time: not settled within band')\n",
448
+ "else:\n",
449
+ " print('Settling time (first in-band for ≥25 steps):', settle_idx)\n",
450
+ "# Steady-state error over last 20%\n",
451
+ "cut = int(0.8 * len(S_pct))\n",
452
+ "ss_err = float(np.mean(np.abs(S_pct[cut:] - S_target)))\n",
453
+ "print(f'Steady-state |S−S*| over last 20%: {ss_err:.3f} pp')\n",
454
+ "print('Saved figures:', fig_dir / 's_curve.png', '|', fig_dir / 'lambda_curve.png')\n"
455
+ ]
456
+ },
457
+ {
458
+ "cell_type": "code",
459
+ "execution_count": null,
460
+ "metadata": {},
461
+ "outputs": [],
462
+ "source": [
463
+ "# 10) Minimal ablations (optional/short)\n",
464
+ "import shlex\n",
465
+ "abl_script = Path('scripts/run_ablation.py')\n",
466
+ "if abl_script.exists():\n",
467
+ " small_steps = 80 if IS_CUDA else (40 if IS_MPS else 10)\n",
468
+ " base_flag = 'meta-llama/Llama-3.2-1B' if MODEL_SIZE == '1B' else 'meta-llama/Llama-3.2-3B'\n",
469
+ " try:\n",
470
+ " print('Running fixed-λ ablation (short) ...')\n",
471
+ " cmd = f\"{sys.executable} {abl_script} --mode fixed-lambda --steps {small_steps} --batch_size 1 --base_model {base_flag} --output figures/ablations_fixed_lambda.md\"\n",
472
+ " subprocess.run(shlex.split(cmd), check=True)\n",
473
+ " print('Running target-sweep ablation (short) ...')\n",
474
+ " cmd = f\"{sys.executable} {abl_script} --mode target-sweep --steps {small_steps} --batch_size 1 --base_model {base_flag} --output figures/ablations_target_sweep.md\"\n",
475
+ " subprocess.run(shlex.split(cmd), check=True)\n",
476
+ " # Summarize a couple results if present\n",
477
+ " summ_rows = []\n",
478
+ " for p in Path('ablations').rglob('eval_results.json'):\n",
479
+ " try:\n",
480
+ " d = json.loads(p.read_text())\n",
481
+ " summ_rows.append({'path': str(p.parent), 'scu_bpt': d.get('scu_bpt'), 'delta_bpt': d.get('delta_bpt')})\n",
482
+ " except Exception:\n",
483
+ " pass\n",
484
+ " if summ_rows:\n",
485
+ " df_summ = pd.DataFrame(summ_rows)\n",
486
+ " print(df_summ.head(10))\n",
487
+ " else:\n",
488
+ " print('Ablation summaries not found (may be gated or skipped).')\n",
489
+ " except Exception as e:\n",
490
+ " print('Ablations skipped:', repr(e))\n",
491
+ " print('Hint: try running locally with more memory if needed.')\n",
492
+ "else:\n",
493
+ " print('No ablation script found; skipping.')\n"
494
+ ]
495
+ },
496
+ {
497
+ "cell_type": "markdown",
498
+ "metadata": {},
499
+ "source": [
500
+ "## 11) Export figures & links\n",
501
+ "- Saved: `assets/figures/s_curve.png`\n",
502
+ "- Saved: `assets/figures/lambda_curve.png`\n",
503
+ "- Site URLs (if hosting the repo website):\n",
504
+ " - `/assets/figures/s_curve.png`\n",
505
+ " - `/assets/figures/lambda_curve.png`\n"
506
+ ]
507
+ },
508
+ {
509
+ "cell_type": "code",
510
+ "execution_count": null,
511
+ "metadata": {},
512
+ "outputs": [],
513
+ "source": [
514
+ "# Zip and download figures if on Colab\n",
515
+ "import os, shutil\n",
516
+ "if 'COLAB_RELEASE_TAGS' in os.environ or 'COLAB_GPU' in os.environ:\n",
517
+ " shutil.make_archive('figures', 'zip', root_dir='assets', base_dir='figures')\n",
518
+ " try:\n",
519
+ " from google.colab import files\n",
520
+ " files.download('figures.zip')\n",
521
+ " except Exception:\n",
522
+ " print('Zip created at figures.zip')\n",
523
+ "else:\n",
524
+ " print('Not running on Colab; skipping download.')\n"
525
+ ]
526
+ },
527
+ {
528
+ "cell_type": "markdown",
529
+ "metadata": {},
530
+ "source": [
531
+ "## 12) Troubleshooting\n",
532
+ "- MPS OOM: use `batch_size=1`, `gradient_accumulation_steps=4`, `block_size=1024`, enable gradient checkpointing, and set `model.config.use_cache=False`.\n",
533
+ "- CUDA path: ensure `bitsandbytes` installed; on A100/V100 you can try fp16 instead of 4-bit if memory allows.\n",
534
+ "- HF access: accept the Meta Llama 3.2 license and login via `huggingface_hub.login()`.\n",
535
+ "- CPU mode: training is disabled; the notebook will still evaluate (if models load) and will simulate control logs to emit figures.\n",
536
+ "\n",
537
+ "Note: Adapters inherit Meta Llama 3.2 license; SCU code Apache-2.0. U.S. patent pending (provisional filed Sep 2025).\n"
538
+ ]
539
+ }
540
+ ],
541
+ "metadata": {
542
+ "kernelspec": {
543
+ "display_name": "Python 3",
544
+ "language": "python",
545
+ "name": "python3"
546
+ },
547
+ "language_info": {
548
+ "name": "python",
549
+ "version": "3.x"
550
+ }
551
+ },
552
+ "nbformat": 4,
553
+ "nbformat_minor": 5
554
+ }