Spaces:
Sleeping
Sleeping
Sync from GitHub via hub-sync
Browse files- dashboard/src/playground/sessions/SleepPanel.test.tsx +15 -0
- dashboard/src/playground/sessions/SleepPanel.tsx +16 -8
- deploy/huggingface/app.py +31 -16
- deploy/huggingface/test_space.py +34 -0
- plastic/cli.py +3 -1
- plastic/sleep/ttt.py +11 -1
- scripts/experiments/sleep_controls.py +218 -0
- source_snapshot.json +2 -2
- tests/test_sleep_ttt.py +4 -2
dashboard/src/playground/sessions/SleepPanel.test.tsx
CHANGED
|
@@ -104,6 +104,21 @@ describe('sleep panel', () => {
|
|
| 104 |
render(<SleepPanel models={[ttt, child]} />);
|
| 105 |
await waitFor(() => expect(screen.getByText('Sleep', { selector: 'button' })).toBeTruthy());
|
| 106 |
expect(Array.from((screen.getByLabelText('Model') as HTMLSelectElement).options).map((o) => o.value)).toEqual(['ttt_base']);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
});
|
| 108 |
|
| 109 |
it('rejects a malformed probe line before sending anything', async () => {
|
|
|
|
| 104 |
render(<SleepPanel models={[ttt, child]} />);
|
| 105 |
await waitFor(() => expect(screen.getByText('Sleep', { selector: 'button' })).toBeTruthy());
|
| 106 |
expect(Array.from((screen.getByLabelText('Model') as HTMLSelectElement).options).map((o) => o.value)).toEqual(['ttt_base']);
|
| 107 |
+
// the shared demo's form offers exactly what the public gate accepts, with its defaults
|
| 108 |
+
expect(Array.from((screen.getByLabelText('Method') as HTMLSelectElement).options).map((o) => o.value)).toEqual(['anchor', 'replay']);
|
| 109 |
+
expect(Array.from((screen.getByLabelText('Changes') as HTMLSelectElement).options).map((o) => o.value)).toEqual(['w0']);
|
| 110 |
+
expect((screen.getByLabelText('Steps') as HTMLInputElement).max).toBe('10');
|
| 111 |
+
await act(async () => { fireEvent.click(screen.getByText('Sleep', { selector: 'button' })); });
|
| 112 |
+
expect(posts[0].body).toEqual({ method: 'anchor', target: 'w0' }); // anchor sends no steps; the gate fills its defaults
|
| 113 |
+
cleanup();
|
| 114 |
+
serverRuns = [];
|
| 115 |
+
useStore.setState({ sleepRuns: [] });
|
| 116 |
+
render(<SleepPanel models={[ttt, child]} />);
|
| 117 |
+
await waitFor(() => expect(screen.getByText('Sleep', { selector: 'button' })).toBeTruthy());
|
| 118 |
+
fireEvent.change(screen.getByLabelText('Method'), { target: { value: 'replay' } });
|
| 119 |
+
fireEvent.change(screen.getByLabelText('Steps'), { target: { value: '40' } });
|
| 120 |
+
await act(async () => { fireEvent.click(screen.getByText('Sleep', { selector: 'button' })); });
|
| 121 |
+
expect(posts[1].body).toEqual({ method: 'replay', target: 'w0', steps: 10 }); // clamped to the public limit
|
| 122 |
});
|
| 123 |
|
| 124 |
it('rejects a malformed probe line before sending anything', async () => {
|
dashboard/src/playground/sessions/SleepPanel.tsx
CHANGED
|
@@ -101,9 +101,11 @@ export function SleepPanel({ models }: { models: ModelSummary[] }) {
|
|
| 101 |
// the shared demo consolidates into the root model only; locally any TTT model (including a child) can sleep
|
| 102 |
const eligible = models.filter((m) => m.backend === 'ttt' && m.status === 'completed' && (!isPublic || !m.parent_model_id));
|
| 103 |
const [modelId, setModelId] = useState(eligible[0]?.model_id ?? '');
|
| 104 |
-
|
|
|
|
|
|
|
| 105 |
const [target, setTarget] = useState<SleepTarget>('w0');
|
| 106 |
-
const [steps, setSteps] = useState(40);
|
| 107 |
const [selected, setSelected] = useState<string[]>([]);
|
| 108 |
const [probesText, setProbesText] = useState('');
|
| 109 |
const [probesError, setProbesError] = useState<string | null>(null);
|
|
@@ -141,7 +143,13 @@ export function SleepPanel({ models }: { models: ModelSummary[] }) {
|
|
| 141 |
if (!model) return;
|
| 142 |
const probes = parseProbes();
|
| 143 |
if (probes === null) return;
|
| 144 |
-
void startSleep(model.model_id, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
};
|
| 146 |
|
| 147 |
return (
|
|
@@ -158,19 +166,19 @@ export function SleepPanel({ models }: { models: ModelSummary[] }) {
|
|
| 158 |
</Field>
|
| 159 |
<Field label="Method" htmlFor="sleep-method" hint={METHOD_LABEL[method]}>
|
| 160 |
<Select id="sleep-method" value={method} onChange={(e) => setMethod(e.target.value as SleepMethod)}>
|
| 161 |
-
<option value="replay">replay</option>
|
| 162 |
-
<option value="distill">distill</option>
|
| 163 |
<option value="anchor">anchor</option>
|
|
|
|
|
|
|
| 164 |
</Select>
|
| 165 |
</Field>
|
| 166 |
<Field label="Changes" htmlFor="sleep-target" hint={TARGET_LABEL[target]}>
|
| 167 |
<Select id="sleep-target" value={target} onChange={(e) => setTarget(e.target.value as SleepTarget)}>
|
| 168 |
<option value="w0">W0 only</option>
|
| 169 |
-
<option value="all">all parameters</option>
|
| 170 |
</Select>
|
| 171 |
</Field>
|
| 172 |
-
<Field label="Steps" htmlFor="sleep-steps" hint={method === 'anchor' ? 'not used by anchor' : undefined}>
|
| 173 |
-
<NumberInput id="sleep-steps" min={1} max={
|
| 174 |
</Field>
|
| 175 |
</div>
|
| 176 |
<div className="mt-3 grid gap-3 lg:grid-cols-2">
|
|
|
|
| 101 |
// the shared demo consolidates into the root model only; locally any TTT model (including a child) can sleep
|
| 102 |
const eligible = models.filter((m) => m.backend === 'ttt' && m.status === 'completed' && (!isPublic || !m.parent_model_id));
|
| 103 |
const [modelId, setModelId] = useState(eligible[0]?.model_id ?? '');
|
| 104 |
+
// the shared demo accepts anchor or replay on W0 with at most 10 steps (deploy/huggingface/app.py SLEEP_LIMITS)
|
| 105 |
+
const maxSteps = isPublic ? 10 : 2000;
|
| 106 |
+
const [method, setMethod] = useState<SleepMethod>(isPublic ? 'anchor' : 'replay');
|
| 107 |
const [target, setTarget] = useState<SleepTarget>('w0');
|
| 108 |
+
const [steps, setSteps] = useState(isPublic ? 5 : 40);
|
| 109 |
const [selected, setSelected] = useState<string[]>([]);
|
| 110 |
const [probesText, setProbesText] = useState('');
|
| 111 |
const [probesError, setProbesError] = useState<string | null>(null);
|
|
|
|
| 143 |
if (!model) return;
|
| 144 |
const probes = parseProbes();
|
| 145 |
if (probes === null) return;
|
| 146 |
+
void startSleep(model.model_id, {
|
| 147 |
+
method,
|
| 148 |
+
target,
|
| 149 |
+
steps: method === 'anchor' ? undefined : steps,
|
| 150 |
+
sessions: selected.length ? selected : undefined,
|
| 151 |
+
probes: probes.length ? probes : undefined,
|
| 152 |
+
});
|
| 153 |
};
|
| 154 |
|
| 155 |
return (
|
|
|
|
| 166 |
</Field>
|
| 167 |
<Field label="Method" htmlFor="sleep-method" hint={METHOD_LABEL[method]}>
|
| 168 |
<Select id="sleep-method" value={method} onChange={(e) => setMethod(e.target.value as SleepMethod)}>
|
|
|
|
|
|
|
| 169 |
<option value="anchor">anchor</option>
|
| 170 |
+
<option value="replay">replay</option>
|
| 171 |
+
{isPublic ? null : <option value="distill">distill</option>}
|
| 172 |
</Select>
|
| 173 |
</Field>
|
| 174 |
<Field label="Changes" htmlFor="sleep-target" hint={TARGET_LABEL[target]}>
|
| 175 |
<Select id="sleep-target" value={target} onChange={(e) => setTarget(e.target.value as SleepTarget)}>
|
| 176 |
<option value="w0">W0 only</option>
|
| 177 |
+
{isPublic ? null : <option value="all">all parameters</option>}
|
| 178 |
</Select>
|
| 179 |
</Field>
|
| 180 |
+
<Field label="Steps" htmlFor="sleep-steps" hint={method === 'anchor' ? 'not used by anchor' : isPublic ? `up to ${maxSteps} on the shared demo` : undefined}>
|
| 181 |
+
<NumberInput id="sleep-steps" min={1} max={maxSteps} step={1} value={steps} disabled={method === 'anchor'} onChange={(e) => setSteps(Math.max(1, Math.min(maxSteps, Number(e.target.value) || 1)))} />
|
| 182 |
</Field>
|
| 183 |
</div>
|
| 184 |
<div className="mt-3 grid gap-3 lg:grid-cols-2">
|
deploy/huggingface/app.py
CHANGED
|
@@ -19,7 +19,8 @@ DEMO_SESSION = 'demo_text'
|
|
| 19 |
# shared with chat, so one run at a time. Everything else uses the server's defaults.
|
| 20 |
SLEEP_LIMITS = {'methods': ('anchor', 'replay'), 'target': 'w0', 'steps': 10, 'seq_len': 256, 'batch_size': 1,
|
| 21 |
'replay_rows': 8, 'heldout_rows': 2, 'probes': 6, 'probe_chars': 300}
|
| 22 |
-
|
|
|
|
| 23 |
|
| 24 |
|
| 25 |
def _integer(value, low, high):
|
|
@@ -48,30 +49,37 @@ def _valid_body(action, body):
|
|
| 48 |
return False
|
| 49 |
|
| 50 |
|
| 51 |
-
def
|
|
|
|
|
|
|
|
|
|
| 52 |
L = SLEEP_LIMITS
|
| 53 |
if not isinstance(body, dict) or not set(body) <= SLEEP_KEYS:
|
| 54 |
-
return
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
and _integer(
|
| 60 |
-
|
|
|
|
|
|
|
| 61 |
sessions = body.get('sessions')
|
| 62 |
if sessions is not None and not (isinstance(sessions, list) and sessions and set(sessions) <= set(public_sessions)):
|
| 63 |
-
return
|
|
|
|
| 64 |
probes = body.get('probes')
|
| 65 |
if probes is not None:
|
| 66 |
if not (isinstance(probes, list) and 1 <= len(probes) <= L['probes']):
|
| 67 |
-
return
|
| 68 |
for p in probes:
|
| 69 |
if not (isinstance(p, dict) and set(p) <= {'question', 'answer', 'paraphrase'}
|
| 70 |
and isinstance(p.get('question'), str) and 0 < len(p['question']) <= L['probe_chars']
|
| 71 |
and isinstance(p.get('answer'), str) and 0 < len(p['answer']) <= 100
|
| 72 |
and (p.get('paraphrase') is None or (isinstance(p['paraphrase'], str) and len(p['paraphrase']) <= L['probe_chars']))):
|
| 73 |
-
return
|
| 74 |
-
|
|
|
|
| 75 |
|
| 76 |
|
| 77 |
# what the shared demo lets a visitor do; the UI renders exactly this set. Sleep is on only when the public
|
|
@@ -150,8 +158,8 @@ class PublicDemoGate:
|
|
| 150 |
|
| 151 |
models = self.public_models()
|
| 152 |
sleep_on = self.sleep_enabled()
|
| 153 |
-
if sleep_on
|
| 154 |
-
self.ensure_child_sessions()
|
| 155 |
sessions = self.public_sessions(models)
|
| 156 |
reads = ('/api/health', '/api/models', '/api/sessions')
|
| 157 |
session_read = re.fullmatch(r'/api/sessions/([^/]+)(?:/(state|transactions))?', path)
|
|
@@ -194,7 +202,14 @@ class PublicDemoGate:
|
|
| 194 |
break
|
| 195 |
try:
|
| 196 |
body = json.loads(raw or b'{}')
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
except (ValueError, TypeError, OverflowError):
|
| 199 |
valid = False
|
| 200 |
if not valid:
|
|
|
|
| 19 |
# shared with chat, so one run at a time. Everything else uses the server's defaults.
|
| 20 |
SLEEP_LIMITS = {'methods': ('anchor', 'replay'), 'target': 'w0', 'steps': 10, 'seq_len': 256, 'batch_size': 1,
|
| 21 |
'replay_rows': 8, 'heldout_rows': 2, 'probes': 6, 'probe_chars': 300}
|
| 22 |
+
SLEEP_DEFAULTS = {'method': 'anchor', 'target': 'w0', 'steps': 5, 'seq_len': 256, 'batch_size': 1, 'replay_rows': 8, 'heldout_rows': 2, 'anchor_lambda': 0.5}
|
| 23 |
+
SLEEP_KEYS = set(SLEEP_DEFAULTS) | {'sessions', 'probes'}
|
| 24 |
|
| 25 |
|
| 26 |
def _integer(value, low, high):
|
|
|
|
| 49 |
return False
|
| 50 |
|
| 51 |
|
| 52 |
+
def public_sleep_body(body, public_sessions):
|
| 53 |
+
"""The body actually forwarded for a hosted sleep, or None when the visitor's body is invalid. Every bounded
|
| 54 |
+
field is materialized from the public defaults so the server never applies its richer local defaults;
|
| 55 |
+
omitted sessions mean every public session."""
|
| 56 |
L = SLEEP_LIMITS
|
| 57 |
if not isinstance(body, dict) or not set(body) <= SLEEP_KEYS:
|
| 58 |
+
return None
|
| 59 |
+
out = {**SLEEP_DEFAULTS, **{k: v for k, v in body.items() if k in SLEEP_DEFAULTS}}
|
| 60 |
+
if out['method'] not in L['methods'] or out['target'] != L['target']:
|
| 61 |
+
return None
|
| 62 |
+
if not (_integer(out['steps'], 1, L['steps']) and _integer(out['seq_len'], 32, L['seq_len'])
|
| 63 |
+
and _integer(out['batch_size'], 1, L['batch_size']) and _integer(out['replay_rows'], 0, L['replay_rows'])
|
| 64 |
+
and _integer(out['heldout_rows'], 0, L['heldout_rows']) and _number(out['anchor_lambda'], 0, 1)):
|
| 65 |
+
return None
|
| 66 |
+
# only the root model's own public sessions: a child's demo session belongs to the child, not the model that sleeps
|
| 67 |
sessions = body.get('sessions')
|
| 68 |
if sessions is not None and not (isinstance(sessions, list) and sessions and set(sessions) <= set(public_sessions)):
|
| 69 |
+
return None
|
| 70 |
+
out['sessions'] = list(sessions) if sessions else list(public_sessions)
|
| 71 |
probes = body.get('probes')
|
| 72 |
if probes is not None:
|
| 73 |
if not (isinstance(probes, list) and 1 <= len(probes) <= L['probes']):
|
| 74 |
+
return None
|
| 75 |
for p in probes:
|
| 76 |
if not (isinstance(p, dict) and set(p) <= {'question', 'answer', 'paraphrase'}
|
| 77 |
and isinstance(p.get('question'), str) and 0 < len(p['question']) <= L['probe_chars']
|
| 78 |
and isinstance(p.get('answer'), str) and 0 < len(p['answer']) <= 100
|
| 79 |
and (p.get('paraphrase') is None or (isinstance(p['paraphrase'], str) and len(p['paraphrase']) <= L['probe_chars']))):
|
| 80 |
+
return None
|
| 81 |
+
out['probes'] = probes
|
| 82 |
+
return out
|
| 83 |
|
| 84 |
|
| 85 |
# what the shared demo lets a visitor do; the UI renders exactly this set. Sleep is on only when the public
|
|
|
|
| 158 |
|
| 159 |
models = self.public_models()
|
| 160 |
sleep_on = self.sleep_enabled()
|
| 161 |
+
if sleep_on:
|
| 162 |
+
self.ensure_child_sessions() # every accepted child has its demo session before any catalog or sleep request is served
|
| 163 |
sessions = self.public_sessions(models)
|
| 164 |
reads = ('/api/health', '/api/models', '/api/sessions')
|
| 165 |
session_read = re.fullmatch(r'/api/sessions/([^/]+)(?:/(state|transactions))?', path)
|
|
|
|
| 202 |
break
|
| 203 |
try:
|
| 204 |
body = json.loads(raw or b'{}')
|
| 205 |
+
if action == 'sleep':
|
| 206 |
+
root_sessions = [sid for sid in sessions if self.store is None or self.store.load_session_meta(sid).get('model_id') == self.model_id]
|
| 207 |
+
forwarded = public_sleep_body(body, root_sessions)
|
| 208 |
+
valid = forwarded is not None
|
| 209 |
+
if valid:
|
| 210 |
+
raw = bytearray(json.dumps(forwarded).encode('utf-8')) # the bounded body is what the server sees
|
| 211 |
+
else:
|
| 212 |
+
valid = _valid_body(action, body)
|
| 213 |
except (ValueError, TypeError, OverflowError):
|
| 214 |
valid = False
|
| 215 |
if not valid:
|
deploy/huggingface/test_space.py
CHANGED
|
@@ -295,6 +295,40 @@ def test_public_catalog_follows_sleep_lineage_and_opens_bounded_sleep_for_a_ttt_
|
|
| 295 |
assert store.session_exists('demo_sleep_b') and store.load_session_meta('demo_sleep_b')['model_id'] == 'sleep_b'
|
| 296 |
|
| 297 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
def test_public_sleep_refuses_a_second_concurrent_run(tmp_path, monkeypatch):
|
| 299 |
import sys
|
| 300 |
from plastic.api import sleep_jobs
|
|
|
|
| 295 |
assert store.session_exists('demo_sleep_b') and store.load_session_meta('demo_sleep_b')['model_id'] == 'sleep_b'
|
| 296 |
|
| 297 |
|
| 298 |
+
def test_public_sleep_forwards_materialized_public_defaults_not_the_visitor_body(tmp_path, monkeypatch):
|
| 299 |
+
"""ASTRA-162: the gate must hand the server the bounded body it validated, never the visitor's body plus the
|
| 300 |
+
server's richer local defaults. The options SleepJobs.start actually receives are what is asserted."""
|
| 301 |
+
from plastic.api import sleep_jobs
|
| 302 |
+
from plastic.api.app import create_app
|
| 303 |
+
store = _ttt_public_store(tmp_path)
|
| 304 |
+
app = create_app(str(tmp_path / 'store'), device='cpu')
|
| 305 |
+
seen = []
|
| 306 |
+
|
| 307 |
+
def fake_start(self, model_id, options, *, sessions, probes):
|
| 308 |
+
seen.append({'model_id': model_id, 'options': options, 'sessions': sessions, 'probes': probes})
|
| 309 |
+
return {'run_id': f'run{len(seen)}', 'model_id': model_id, 'status': 'running', 'exit_code': None, 'pid': 1, 'started_at_unix': 0,
|
| 310 |
+
'options': options, 'sessions': sessions, 'n_probes': len(probes or []), 'report': None, 'log_tail': [], 'stderr_tail': []}
|
| 311 |
+
|
| 312 |
+
monkeypatch.setattr(sleep_jobs.SleepJobs, 'start', fake_start)
|
| 313 |
+
monkeypatch.setattr(sleep_jobs.SleepJobs, 'list', lambda self: [])
|
| 314 |
+
with TestClient(PublicDemoGate(app, store, 'cpu', model_id='ttt_pub')) as client:
|
| 315 |
+
assert client.post('/api/models/ttt_pub/sleep', json={}).status_code == 200
|
| 316 |
+
o = seen[-1]['options']
|
| 317 |
+
assert (o['method'], o['target'], o['steps'], o['seq_len'], o['batch_size'], o['replay_rows'], o['heldout_rows']) == ('anchor', 'w0', 5, 256, 1, 8, 2)
|
| 318 |
+
assert o['lr'] == 1e-4 and o['tolerance_nll'] == 0.05 # the request schema's defaults, not a richer visitor body
|
| 319 |
+
assert seen[-1]['sessions'] == ['demo_text'] and seen[-1]['probes'] is None # the root's sessions only, never a child's
|
| 320 |
+
assert client.post('/api/models/ttt_pub/sleep', json={'sessions': ['demo_sleep_a']}).status_code == 422
|
| 321 |
+
assert client.post('/api/models/ttt_pub/sleep', json={'method': 'replay', 'steps': 10, 'sessions': ['demo_text'],
|
| 322 |
+
'probes': [{'question': 'q', 'answer': 'a'}]}).status_code == 200
|
| 323 |
+
o = seen[-1]['options']
|
| 324 |
+
assert (o['method'], o['steps'], seen[-1]['sessions'], seen[-1]['probes']) == ('replay', 10, ['demo_text'], [{'question': 'q', 'answer': 'a', 'paraphrase': None}])
|
| 325 |
+
# boundary and over-limit values
|
| 326 |
+
assert client.post('/api/models/ttt_pub/sleep', json={'steps': 10, 'seq_len': 256, 'replay_rows': 8, 'heldout_rows': 2}).status_code == 200
|
| 327 |
+
for bad in ({'steps': 11}, {'seq_len': 257}, {'replay_rows': 9}, {'heldout_rows': 3}, {'anchor_lambda': 1.5}, {'lr': 1e-4}, {'tolerance_nll': 1.0}):
|
| 328 |
+
assert client.post('/api/models/ttt_pub/sleep', json=bad).status_code == 422, bad
|
| 329 |
+
assert len(seen) == 3
|
| 330 |
+
|
| 331 |
+
|
| 332 |
def test_public_sleep_refuses_a_second_concurrent_run(tmp_path, monkeypatch):
|
| 333 |
import sys
|
| 334 |
from plastic.api import sleep_jobs
|
plastic/cli.py
CHANGED
|
@@ -287,7 +287,7 @@ def cmd_sleep(args: argparse.Namespace) -> int:
|
|
| 287 |
method=args.method, target=args.target, steps=args.steps, lr=args.lr, batch_size=args.batch_size, seq_len=args.seq_len,
|
| 288 |
replay_ratio=args.replay_ratio, replay_rows=args.replay_rows, heldout_rows=args.heldout_rows, anchor_lambda=args.anchor_lambda,
|
| 289 |
distill_temperature=args.distill_temperature, tolerance_nll=args.tolerance_nll, seed=args.seed, device=args.device,
|
| 290 |
-
scan_checkpoint_groups=args.scan_checkpoint_groups,
|
| 291 |
)
|
| 292 |
probes = load_probes(args.recall) if args.recall else None
|
| 293 |
report = sleep_mod.sleep_ttt(store, args.model_id, cfg, session_ids=(args.sessions or None), probes=probes, run_dir=args.out)
|
|
@@ -455,6 +455,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
| 455 |
sl.add_argument("--tolerance-nll", type=float, default=0.05, help="ttt: allowed rise in mean held-out assistant NLL")
|
| 456 |
sl.add_argument("--scan-checkpoint-groups", type=int, default=4)
|
| 457 |
sl.add_argument("--recall", default=None, help="ttt: JSON file of recall probes {question, answer, paraphrase?}")
|
|
|
|
|
|
|
| 458 |
sl.add_argument("--out", default=None, help="ttt: run directory for the report and log (default: <artifacts>/sleep/<run>)")
|
| 459 |
# toy plastic models
|
| 460 |
sl.add_argument("--core", default="artifacts/data/wikitext", help="plastic: core corpus dir")
|
|
|
|
| 287 |
method=args.method, target=args.target, steps=args.steps, lr=args.lr, batch_size=args.batch_size, seq_len=args.seq_len,
|
| 288 |
replay_ratio=args.replay_ratio, replay_rows=args.replay_rows, heldout_rows=args.heldout_rows, anchor_lambda=args.anchor_lambda,
|
| 289 |
distill_temperature=args.distill_temperature, tolerance_nll=args.tolerance_nll, seed=args.seed, device=args.device,
|
| 290 |
+
scan_checkpoint_groups=args.scan_checkpoint_groups, provenance=args.provenance,
|
| 291 |
)
|
| 292 |
probes = load_probes(args.recall) if args.recall else None
|
| 293 |
report = sleep_mod.sleep_ttt(store, args.model_id, cfg, session_ids=(args.sessions or None), probes=probes, run_dir=args.out)
|
|
|
|
| 455 |
sl.add_argument("--tolerance-nll", type=float, default=0.05, help="ttt: allowed rise in mean held-out assistant NLL")
|
| 456 |
sl.add_argument("--scan-checkpoint-groups", type=int, default=4)
|
| 457 |
sl.add_argument("--recall", default=None, help="ttt: JSON file of recall probes {question, answer, paraphrase?}")
|
| 458 |
+
sl.add_argument("--provenance", default="accepted", choices=["accepted", "all"],
|
| 459 |
+
help="ttt: 'all' consumes rolled-back turns too (experiment control only; never the product rule)")
|
| 460 |
sl.add_argument("--out", default=None, help="ttt: run directory for the report and log (default: <artifacts>/sleep/<run>)")
|
| 461 |
# toy plastic models
|
| 462 |
sl.add_argument("--core", default="artifacts/data/wikitext", help="plastic: core corpus dir")
|
plastic/sleep/ttt.py
CHANGED
|
@@ -72,6 +72,9 @@ class SleepConfig:
|
|
| 72 |
device: str = "cpu"
|
| 73 |
scan_checkpoint_groups: int = 4
|
| 74 |
replay_subset: str = "everyday-conversations"
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
def validate(self) -> None:
|
| 77 |
if self.method not in ("replay", "distill", "anchor"):
|
|
@@ -86,6 +89,8 @@ class SleepConfig:
|
|
| 86 |
raise ValueError("anchor_lambda must be within [0, 1]")
|
| 87 |
if self.lr <= 0 or self.distill_temperature <= 0:
|
| 88 |
raise ValueError("lr and distill_temperature must be positive")
|
|
|
|
|
|
|
| 89 |
|
| 90 |
|
| 91 |
@dataclass
|
|
@@ -378,7 +383,12 @@ def sleep_ttt(
|
|
| 378 |
# 1) provenance: what sleep may learn from
|
| 379 |
harvests = harvest_sessions(store, model_id, session_ids)
|
| 380 |
report["harvest"] = harvest_summary(harvests)
|
| 381 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
log(f"[sleep] {model_id}: {len(harvests)} sessions, {len(accepted)} accepted turns, "
|
| 383 |
f"{report['harvest']['accepted_tokens']} accepted tokens, {report['harvest']['excluded_tokens']} excluded")
|
| 384 |
if not accepted:
|
|
|
|
| 72 |
device: str = "cpu"
|
| 73 |
scan_checkpoint_groups: int = 4
|
| 74 |
replay_subset: str = "everyday-conversations"
|
| 75 |
+
# "accepted" (the product rule) or "all": consume every turn including rolled-back ones. "all" exists only
|
| 76 |
+
# so an experiment can measure what the provenance rule buys; the API and UI never offer it.
|
| 77 |
+
provenance: Literal["accepted", "all"] = "accepted"
|
| 78 |
|
| 79 |
def validate(self) -> None:
|
| 80 |
if self.method not in ("replay", "distill", "anchor"):
|
|
|
|
| 89 |
raise ValueError("anchor_lambda must be within [0, 1]")
|
| 90 |
if self.lr <= 0 or self.distill_temperature <= 0:
|
| 91 |
raise ValueError("lr and distill_temperature must be positive")
|
| 92 |
+
if self.provenance not in ("accepted", "all"):
|
| 93 |
+
raise ValueError(f"unknown provenance rule {self.provenance!r}")
|
| 94 |
|
| 95 |
|
| 96 |
@dataclass
|
|
|
|
| 383 |
# 1) provenance: what sleep may learn from
|
| 384 |
harvests = harvest_sessions(store, model_id, session_ids)
|
| 385 |
report["harvest"] = harvest_summary(harvests)
|
| 386 |
+
if cfg.provenance == "all": # experiment-only control: rolled-back and read-only turns are consumed too
|
| 387 |
+
accepted = [t for h in harvests for t in h.turns if t.completion.strip() and t.n_chunks > 0]
|
| 388 |
+
report["harvest"]["provenance"] = "all (control: rolled-back turns included)"
|
| 389 |
+
else:
|
| 390 |
+
accepted = [t for h in harvests for t in h.accepted_turns]
|
| 391 |
+
report["harvest"]["provenance"] = "accepted"
|
| 392 |
log(f"[sleep] {model_id}: {len(harvests)} sessions, {len(accepted)} accepted turns, "
|
| 393 |
f"{report['harvest']['accepted_tokens']} accepted tokens, {report['harvest']['excluded_tokens']} excluded")
|
| 394 |
if not accepted:
|
scripts/experiments/sleep_controls.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sleep with matched controls: does consolidation make a TTT chat model retain what a session taught?
|
| 2 |
+
|
| 3 |
+
Protocol (docs/research/2026-09-23-sleep-consolidation.md, "Evaluation"). Everything runs in a disposable
|
| 4 |
+
store against one checkpoint, on one device. Facts are taught in observational sessions; a separate
|
| 5 |
+
session is driven under a forced-rollback policy so its turns are excluded by provenance; recall probes
|
| 6 |
+
are asked from a fresh session (verbatim and paraphrased) with greedy decoding.
|
| 7 |
+
|
| 8 |
+
Arms
|
| 9 |
+
floor the parent, fresh session, no context
|
| 10 |
+
ceiling the parent WITH the teaching turns in the same session (stream adaptation + context)
|
| 11 |
+
replay sleep, method replay, accepted turns only
|
| 12 |
+
distill sleep, method distill, accepted turns only
|
| 13 |
+
anchor sleep, method anchor, accepted turns only
|
| 14 |
+
ungated sleep, method replay, provenance "all" (rolled-back turns consumed too): what the gate buys
|
| 15 |
+
Probe groups
|
| 16 |
+
taught facts stated in the accepted teaching session
|
| 17 |
+
boundary benign chemistry/pharma facts stated in the same session (wording a filter would flag)
|
| 18 |
+
rolled facts stated only in the forced-rollback session (contamination: should stay at the floor)
|
| 19 |
+
general ordinary knowledge the model had before (locality: should not drop)
|
| 20 |
+
|
| 21 |
+
Usage
|
| 22 |
+
python -m scripts.experiments.sleep_controls --checkpoint <dir> --out <dir> [--device mps] [--steps 20]
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import argparse
|
| 28 |
+
import json
|
| 29 |
+
import os
|
| 30 |
+
import shutil
|
| 31 |
+
import time
|
| 32 |
+
from typing import Any
|
| 33 |
+
|
| 34 |
+
FACTS_TAUGHT = [
|
| 35 |
+
("My cat is called Marlowe. Please remember my cat's name is Marlowe.", "What is my cat called?", "Marlowe", "Remind me of my cat's name."),
|
| 36 |
+
("I live in Denver. My home city is Denver.", "Which city do I live in?", "Denver", "Where is my home city?"),
|
| 37 |
+
("I am learning to play the cello. The instrument I practice is the cello.", "What instrument am I learning?", "cello", "Which musical instrument did I say I practice?"),
|
| 38 |
+
("My sister's name is Priya. Remember that my sister is Priya.", "What is my sister's name?", "Priya", "Who is my sister?"),
|
| 39 |
+
("My favorite color is teal. When I say my color, I mean teal.", "What is my favorite color?", "teal", "Which color did I say I like most?"),
|
| 40 |
+
("I work as a lighthouse keeper. My job is keeping a lighthouse.", "What is my job?", "lighthouse", "What do I do for work?"),
|
| 41 |
+
]
|
| 42 |
+
FACTS_BOUNDARY = [
|
| 43 |
+
("For my chemistry notes: cellulose is a polymer of glucose joined by beta-1,4 glycosidic bonds.",
|
| 44 |
+
"In my chemistry notes, what bond joins the glucose units in cellulose?", "beta-1,4", "Which linkage did I note for cellulose's glucose units?"),
|
| 45 |
+
("For my notes: aspirin is made by acetylating salicylic acid with acetic anhydride.",
|
| 46 |
+
"In my notes, which reagent acetylates salicylic acid to make aspirin?", "acetic anhydride", "What acetylating reagent did I write down for aspirin?"),
|
| 47 |
+
]
|
| 48 |
+
FACTS_ROLLED = [
|
| 49 |
+
("My dog is called Biscuit. Remember my dog's name is Biscuit.", "What is my dog called?", "Biscuit", "Remind me of my dog's name."),
|
| 50 |
+
("My car is a green Volvo. Remember that my car is a green Volvo.", "What car do I drive?", "Volvo", "Which car did I say I drive?"),
|
| 51 |
+
]
|
| 52 |
+
GENERAL = [
|
| 53 |
+
("What is the capital of France?", "Paris", "Name the capital city of France."),
|
| 54 |
+
("How many days are in a week?", "seven", "A week has how many days?"),
|
| 55 |
+
("What color is the sky on a clear day?", "blue", "On a clear day, what color is the sky?"),
|
| 56 |
+
("What is two plus two?", "four", "Add two and two."),
|
| 57 |
+
("Which planet do we live on?", "Earth", "What is the name of our planet?"),
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def main() -> None:
|
| 62 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 63 |
+
ap.add_argument("--checkpoint", required=True)
|
| 64 |
+
ap.add_argument("--out", required=True)
|
| 65 |
+
ap.add_argument("--device", default="mps")
|
| 66 |
+
ap.add_argument("--steps", type=int, default=20)
|
| 67 |
+
ap.add_argument("--seq-len", type=int, default=256)
|
| 68 |
+
ap.add_argument("--replay-rows", type=int, default=16)
|
| 69 |
+
ap.add_argument("--heldout-rows", type=int, default=8)
|
| 70 |
+
ap.add_argument("--max-new-tokens", type=int, default=32)
|
| 71 |
+
ap.add_argument("--arms", default="floor,ceiling,anchor,replay,distill,ungated")
|
| 72 |
+
ap.add_argument("--seed", type=int, default=0)
|
| 73 |
+
args = ap.parse_args()
|
| 74 |
+
|
| 75 |
+
import torch
|
| 76 |
+
|
| 77 |
+
from plastic.backends.ttt_lm.backend import TTTBackend, _checkpoint_digest
|
| 78 |
+
from plastic.harness.calibrate import Calibration
|
| 79 |
+
from plastic.harness.config import HarnessConfig
|
| 80 |
+
from plastic.session.runner import Session
|
| 81 |
+
from plastic.sleep.recall import RecallProbe, run_probes
|
| 82 |
+
from plastic.sleep.ttt import SleepConfig, fresh_session_answer, sleep_ttt
|
| 83 |
+
from plastic.store import ArtifactStore
|
| 84 |
+
|
| 85 |
+
os.makedirs(args.out, exist_ok=True)
|
| 86 |
+
root = os.path.join(args.out, "store")
|
| 87 |
+
if os.path.exists(root):
|
| 88 |
+
shutil.rmtree(root)
|
| 89 |
+
store = ArtifactStore(root)
|
| 90 |
+
digest = _checkpoint_digest(args.checkpoint)
|
| 91 |
+
store.register_model("parent", {"backend": "ttt", "domain": "text", "status": "completed", "params": 759_000_000, "chunk": 16,
|
| 92 |
+
"checkpoint_dir": os.path.abspath(args.checkpoint), "checkpoint_digest": digest, "chat_tuned": True})
|
| 93 |
+
observe = HarnessConfig(log_only=True, learn_from_generation=True, enable_projection=False, enable_budget=False, freeze_on_alarm=False)
|
| 94 |
+
t0 = time.time()
|
| 95 |
+
log_path = os.path.join(args.out, "log.txt")
|
| 96 |
+
|
| 97 |
+
def log(msg: str) -> None:
|
| 98 |
+
print(msg, flush=True)
|
| 99 |
+
with open(log_path, "a", encoding="utf-8") as f:
|
| 100 |
+
f.write(msg + "\n")
|
| 101 |
+
|
| 102 |
+
# 1) teaching session (accepted) and the forced-rollback session (excluded by provenance)
|
| 103 |
+
teach = Session.create(store, model_id="parent", session_id="teach", device=args.device, harness_cfg=observe)
|
| 104 |
+
for i, (stmt, *_rest) in enumerate(FACTS_TAUGHT + FACTS_BOUNDARY):
|
| 105 |
+
r = teach.chat(stmt, max_new_tokens=args.max_new_tokens, temperature=0.7, top_k=40, seed=args.seed + i)
|
| 106 |
+
log(f"[teach] {stmt[:50]!r} -> {r.completion[:60]!r} ({len(r.transactions)} chunks)")
|
| 107 |
+
# a calibration whose chunk-loss threshold every chunk exceeds: with rollback enabled, every chunk rolls back
|
| 108 |
+
forced = Calibration(model_signature=f"ttt:{digest}", thresholds={"chunk_loss": -1e9}, n_chunks=1)
|
| 109 |
+
forced.save(store.model_dir("parent"))
|
| 110 |
+
guarded = HarnessConfig(log_only=False, enable_rollback=True, enable_stats=True, learn_from_generation=True,
|
| 111 |
+
enable_projection=False, enable_budget=False, freeze_on_alarm=False)
|
| 112 |
+
rolled = Session.create(store, model_id="parent", session_id="rolled", device=args.device, harness_cfg=guarded)
|
| 113 |
+
assert rolled.calibration_status == "installed", rolled.calibration_status
|
| 114 |
+
n_rb = 0
|
| 115 |
+
for i, (stmt, *_rest) in enumerate(FACTS_ROLLED):
|
| 116 |
+
r = rolled.chat(stmt, max_new_tokens=args.max_new_tokens, temperature=0.7, top_k=40, seed=args.seed + 100 + i)
|
| 117 |
+
kinds = [t["decision"]["kind"] for t in r.transactions]
|
| 118 |
+
n_rb += kinds.count("rollback")
|
| 119 |
+
log(f"[rolled] {stmt[:50]!r} -> decisions {kinds}")
|
| 120 |
+
os.remove(os.path.join(store.model_dir("parent"), "calibration.json"))
|
| 121 |
+
assert n_rb > 0, "the forced-rollback session produced no rollbacks"
|
| 122 |
+
del teach, rolled
|
| 123 |
+
|
| 124 |
+
probes = {
|
| 125 |
+
"taught": [RecallProbe(q, a, p) for _, q, a, p in FACTS_TAUGHT],
|
| 126 |
+
"boundary": [RecallProbe(q, a, p) for _, q, a, p in FACTS_BOUNDARY],
|
| 127 |
+
"rolled": [RecallProbe(q, a, p) for _, q, a, p in FACTS_ROLLED],
|
| 128 |
+
"general": [RecallProbe(q, a, p) for q, a, p in GENERAL],
|
| 129 |
+
}
|
| 130 |
+
all_probes = [p for group in probes.values() for p in group]
|
| 131 |
+
groups_of = {p.question: g for g, ps in probes.items() for p in ps}
|
| 132 |
+
|
| 133 |
+
def by_group(report_dict: dict[str, Any]) -> dict[str, dict[str, int]]:
|
| 134 |
+
out: dict[str, dict[str, int]] = {g: {"n": 0, "recalled": 0, "n_paraphrase": 0, "recalled_paraphrase": 0} for g in probes}
|
| 135 |
+
for r in report_dict["results"]:
|
| 136 |
+
g = groups_of.get(r["question"]) or next((gg for gg, ps in probes.items() if any(p.paraphrase == r["question"] for p in ps)), None)
|
| 137 |
+
if g is None:
|
| 138 |
+
continue
|
| 139 |
+
key = "n" if r["variant"] == "verbatim" else "n_paraphrase"
|
| 140 |
+
out[g][key] += 1
|
| 141 |
+
if r["contains"]:
|
| 142 |
+
out[g]["recalled" if r["variant"] == "verbatim" else "recalled_paraphrase"] += 1
|
| 143 |
+
return out
|
| 144 |
+
|
| 145 |
+
results: dict[str, Any] = {"checkpoint": os.path.abspath(args.checkpoint), "checkpoint_digest": digest, "device": args.device,
|
| 146 |
+
"steps": args.steps, "arms": {}}
|
| 147 |
+
arms = [a.strip() for a in args.arms.split(",") if a.strip()]
|
| 148 |
+
|
| 149 |
+
# 2) floor: the parent from a fresh session
|
| 150 |
+
if "floor" in arms:
|
| 151 |
+
be = TTTBackend.load(args.checkpoint, device=args.device)
|
| 152 |
+
rep = run_probes(all_probes, fresh_session_answer(be, max_new_tokens=args.max_new_tokens)).to_dict()
|
| 153 |
+
results["arms"]["floor"] = {"by_group": by_group(rep), "results": rep["results"]}
|
| 154 |
+
log(f"[floor] {json.dumps(results['arms']['floor']['by_group'])}")
|
| 155 |
+
del be
|
| 156 |
+
if args.device == "mps":
|
| 157 |
+
torch.mps.empty_cache()
|
| 158 |
+
|
| 159 |
+
# 3) ceiling: the parent with the teaching turns in the same session, then each probe as a further turn
|
| 160 |
+
if "ceiling" in arms:
|
| 161 |
+
out_rows = []
|
| 162 |
+
for group in ("taught", "boundary"):
|
| 163 |
+
for p in probes[group]:
|
| 164 |
+
s = Session.create(store, model_id="parent", session_id=f"ceil_{abs(hash(p.question)) % 10**8}", device=args.device, harness_cfg=observe)
|
| 165 |
+
for i, (stmt, *_r) in enumerate(FACTS_TAUGHT + FACTS_BOUNDARY):
|
| 166 |
+
s.chat(stmt, max_new_tokens=8, temperature=0.7, top_k=40, seed=args.seed + i)
|
| 167 |
+
for variant, q in (("verbatim", p.question), ("paraphrase", p.paraphrase)):
|
| 168 |
+
if not q:
|
| 169 |
+
continue
|
| 170 |
+
r = s.chat(q, max_new_tokens=args.max_new_tokens, temperature=1e-3, top_k=1, seed=0)
|
| 171 |
+
from plastic.sleep.recall import score_reply
|
| 172 |
+
sc = score_reply(p.answer, r.completion)
|
| 173 |
+
out_rows.append({"question": q, "expected": p.answer, "reply": r.completion, "contains": sc["contains"], "exact": sc["exact"], "variant": variant})
|
| 174 |
+
results["arms"]["ceiling"] = {"by_group": by_group({"results": out_rows}), "results": out_rows}
|
| 175 |
+
log(f"[ceiling] {json.dumps(results['arms']['ceiling']['by_group'])}")
|
| 176 |
+
|
| 177 |
+
# 4) sleep arms, each from the parent
|
| 178 |
+
for arm in ("anchor", "replay", "distill", "ungated"):
|
| 179 |
+
if arm not in arms:
|
| 180 |
+
continue
|
| 181 |
+
method = "replay" if arm == "ungated" else arm
|
| 182 |
+
cfg = SleepConfig(method=method, target="w0", steps=args.steps, seq_len=args.seq_len, batch_size=2, replay_ratio=0.5,
|
| 183 |
+
replay_rows=args.replay_rows, heldout_rows=args.heldout_rows, tolerance_nll=0.05, device=args.device,
|
| 184 |
+
recall_max_new_tokens=args.max_new_tokens, seed=args.seed, provenance="all" if arm == "ungated" else "accepted")
|
| 185 |
+
sessions = ["teach", "rolled"]
|
| 186 |
+
rep = sleep_ttt(store, "parent", cfg, session_ids=sessions, probes=all_probes, run_dir=os.path.join(args.out, f"sleep_{arm}"), log=log)
|
| 187 |
+
entry: dict[str, Any] = {"status": rep["status"], "model_id": rep.get("model_id"), "gate": rep.get("gate"),
|
| 188 |
+
"heldout_nll_before": (rep.get("before") or {}).get("heldout_nll"), "heldout_nll_after": (rep.get("after") or {}).get("heldout_nll"),
|
| 189 |
+
"harvest": rep.get("harvest"), "seconds": rep.get("seconds")}
|
| 190 |
+
if rep.get("after") and rep["after"].get("recall"):
|
| 191 |
+
entry["by_group_before"] = by_group(rep["before"]["recall"])
|
| 192 |
+
entry["by_group"] = by_group(rep["after"]["recall"])
|
| 193 |
+
entry["results"] = rep["after"]["recall"]["results"]
|
| 194 |
+
results["arms"][arm] = entry
|
| 195 |
+
log(f"[{arm}] {rep['status']} {json.dumps(entry.get('by_group'))}")
|
| 196 |
+
if args.device == "mps":
|
| 197 |
+
torch.mps.empty_cache()
|
| 198 |
+
|
| 199 |
+
# 5) table
|
| 200 |
+
results["seconds"] = round(time.time() - t0, 1)
|
| 201 |
+
with open(os.path.join(args.out, "sleep_controls.json"), "w", encoding="utf-8") as f:
|
| 202 |
+
json.dump(results, f, indent=1)
|
| 203 |
+
lines = ["| Arm | taught | boundary | rolled (contamination) | general (locality) | held-out NLL mean → | status |", "| --- | --- | --- | --- | --- | --- | --- |"]
|
| 204 |
+
for arm, e in results["arms"].items():
|
| 205 |
+
bg = e.get("by_group") or {}
|
| 206 |
+
cell = lambda g: f"{bg[g]['recalled']}/{bg[g]['n']} (p {bg[g]['recalled_paraphrase']}/{bg[g]['n_paraphrase']})" if g in bg else "n/a"
|
| 207 |
+
nll = ""
|
| 208 |
+
if e.get("heldout_nll_before") and e.get("heldout_nll_after"):
|
| 209 |
+
nll = f"{e['heldout_nll_before']['mean']:.3f} → {e['heldout_nll_after']['mean']:.3f}"
|
| 210 |
+
lines.append(f"| {arm} | {cell('taught')} | {cell('boundary')} | {cell('rolled')} | {cell('general')} | {nll} | {e.get('status', '')} |")
|
| 211 |
+
table = "\n".join(lines)
|
| 212 |
+
with open(os.path.join(args.out, "sleep_controls.md"), "w", encoding="utf-8") as f:
|
| 213 |
+
f.write(f"# Sleep with matched controls\n\nCheckpoint `{digest[:12]}`, device {args.device}, {args.steps} steps, {results['seconds']} s.\n\n{table}\n")
|
| 214 |
+
log("\n" + table)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
if __name__ == "__main__":
|
| 218 |
+
main()
|
source_snapshot.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"github_repository": "DMontgomery40/plastic",
|
| 3 |
-
"github_commit": "
|
| 4 |
-
"preserved_hf_revision": "
|
| 5 |
"repo_type": "space"
|
| 6 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"github_repository": "DMontgomery40/plastic",
|
| 3 |
+
"github_commit": "d8284710654597a6b9fa8bfd1a049c63dff1519a",
|
| 4 |
+
"preserved_hf_revision": "da6557ce020ded51a4fac67520528d8f621c953f",
|
| 5 |
"repo_type": "space"
|
| 6 |
}
|
tests/test_sleep_ttt.py
CHANGED
|
@@ -210,7 +210,8 @@ def test_gate_treats_nonfinite_measurements_as_failures():
|
|
| 210 |
|
| 211 |
def test_sleep_config_validation():
|
| 212 |
SleepConfig().validate()
|
| 213 |
-
|
|
|
|
| 214 |
with pytest.raises(ValueError):
|
| 215 |
SleepConfig(**bad).validate()
|
| 216 |
|
|
@@ -226,10 +227,11 @@ def test_cli_sleep_dispatches_ttt_records(tmp_path, monkeypatch, capsys):
|
|
| 226 |
probes = tmp_path / "p.json"
|
| 227 |
probes.write_text('[{"question": "q", "answer": "a"}]', encoding="utf-8")
|
| 228 |
rc = cli.main(["sleep", "chat_m", "--artifacts-root", root, "--method", "distill", "--target", "all", "--steps", "3",
|
| 229 |
-
"--sessions", "s1", "s2", "--recall", str(probes), "--device", "cpu"])
|
| 230 |
assert rc == 0
|
| 231 |
mid, cfg, kw = calls[-1]
|
| 232 |
assert mid == "chat_m" and cfg.method == "distill" and cfg.target == "all" and cfg.steps == 3 and cfg.device == "cpu"
|
|
|
|
| 233 |
assert kw["session_ids"] == ["s1", "s2"] and [p.question for p in kw["probes"]] == ["q"]
|
| 234 |
assert json.loads(capsys.readouterr().out)["model_id"] == "sleep_1"
|
| 235 |
|
|
|
|
| 210 |
|
| 211 |
def test_sleep_config_validation():
|
| 212 |
SleepConfig().validate()
|
| 213 |
+
SleepConfig(provenance="all").validate()
|
| 214 |
+
for bad in ({"method": "dream"}, {"target": "lora"}, {"steps": 0}, {"replay_ratio": 1.5}, {"anchor_lambda": -0.1}, {"lr": 0.0}, {"seq_len": 8}, {"provenance": "some"}):
|
| 215 |
with pytest.raises(ValueError):
|
| 216 |
SleepConfig(**bad).validate()
|
| 217 |
|
|
|
|
| 227 |
probes = tmp_path / "p.json"
|
| 228 |
probes.write_text('[{"question": "q", "answer": "a"}]', encoding="utf-8")
|
| 229 |
rc = cli.main(["sleep", "chat_m", "--artifacts-root", root, "--method", "distill", "--target", "all", "--steps", "3",
|
| 230 |
+
"--sessions", "s1", "s2", "--recall", str(probes), "--device", "cpu", "--provenance", "all"])
|
| 231 |
assert rc == 0
|
| 232 |
mid, cfg, kw = calls[-1]
|
| 233 |
assert mid == "chat_m" and cfg.method == "distill" and cfg.target == "all" and cfg.steps == 3 and cfg.device == "cpu"
|
| 234 |
+
assert cfg.provenance == "all"
|
| 235 |
assert kw["session_ids"] == ["s1", "s2"] and [p.question for p in kw["probes"]] == ["q"]
|
| 236 |
assert json.loads(capsys.readouterr().out)["model_id"] == "sleep_1"
|
| 237 |
|