File size: 23,164 Bytes
4e70c09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
from __future__ import annotations

import argparse
from collections import Counter
import json
import math
from pathlib import Path
from typing import Any
import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader

from conveyer.checkpoint import load_checkpoint
from conveyer.config import ConveyerConfig
from conveyer.data import build_dataset_and_tokenizer, collate_conveyer_batch
from conveyer.infer import generate_conveyer
from conveyer.losses import compute_unrolled_conveyer_loss
from conveyer.model import ConveyerDiffusionLM


# ---------------------------------------------------------------------------
# Benchmark 3: Zero-Shot Commonsense / Story-Coherence Multiple Choice (2-way)
# ---------------------------------------------------------------------------
# Model scores log P(option | context) on unseen story pairs testing:
#   - Physical commonsense (heavy vs light, hot vs cold, broken vs whole)
#   - Emotional / social cause-and-effect (lost toy -> sad, gift -> happy)
#   - Narrative entity & object permanence (tracking who holds what, where they are)
COMMONSENSE_CLOZE_PAIRS: list[tuple[str, str, str, str]] = [
    (
        "emotion_cause",
        "Once upon a time, Tom lost his favorite red ball in the dark woods. Because he lost his ball, Tom felt very",
        " sad and started to cry.",
        " happy and started to laugh.",
    ),
    (
        "emotion_cause",
        "Lily worked all morning to bake a sweet cake for her mom. When her mom smiled and hugged her, Lily felt very",
        " proud and happy.",
        " angry and mean.",
    ),
    (
        "physical_weight",
        "The little mouse tried to lift the huge heavy rock, but the rock was much too",
        " heavy for the tiny mouse to move.",
        " light and floated into the sky.",
    ),
    (
        "physical_temperature",
        "Ben went outside in the snow without his coat or gloves. Soon his hands felt very",
        " cold and freezing.",
        " hot and burning.",
    ),
    (
        "physical_temperature",
        "The soup had just come off the stove and was boiling. Mom told Mia to wait because the soup was too",
        " hot to eat right now.",
        " cold and frozen like ice.",
    ),
    (
        "physical_fragile",
        "The glass cup fell off the high table onto the hard stone floor. With a loud crash, the glass",
        " broke into many small pieces.",
        " bounced high like a rubber ball.",
    ),
    (
        "object_use",
        "The wooden door was locked tight. To unlock the door, Anna took out her small brass",
        " key and turned it in the lock.",
        " spoon and stirred the soup.",
    ),
    (
        "object_use",
        "It started to rain hard outside. Before walking to school, Max opened his big",
        " umbrella to stay dry.",
        " pillow to go to sleep.",
    ),
    (
        "biological_need",
        "The little puppy had been running in the park all day and had not eaten anything. His tummy rumbled because he was very",
        " hungry and wanted some food.",
        " sleepy and wanted a haircut.",
    ),
    (
        "biological_need",
        "After playing all day until night came and the stars shone in the sky, the tired children went to bed to",
        " sleep until morning.",
        " swim in the cold ocean.",
    ),
    (
        "entity_tracking",
        "Sam had a blue car and Lucy had a yellow doll. Sam gave his blue car to Lucy. Now Lucy had the yellow doll and the",
        " blue car.",
        " green bicycle.",
    ),
    (
        "entity_tracking",
        " Lily put her shiny coin inside the small red box and closed the lid. When she opened the red box later, she saw her",
        " shiny coin inside.",
        " big brown horse inside.",
    ),
    (
        "danger_reaction",
        "Lily and a little bunny walked into the green forest. Suddenly a big fierce lion jumped out and roared loudly. The little bunny was",
        " scared and hid behind a tree.",
        " bored and read a newspaper.",
    ),
    (
        "social_repair",
        "Tom accidentally knocked over Mia's tall block tower. Tom felt bad about the accident, so he said,",
        ' "I am sorry, Mia. Let me help you build it again."',
        ' "Thank you for the delicious birthday cake."',
    ),
    (
        "spatial_logic",
        "The bird wanted to reach the nest at the very top of the tall tree, so it flapped its wings and",
        " flew up high into the branches.",
        " dug a deep hole under the mud.",
    ),
    (
        "weather_visibility",
        "At midnight all the lights went out in the room. Because it was pitch dark, Ben could not",
        " see anything around him.",
        " hear the loud thunder outside.",
    ),
]


NOVEL_STORY_PROMPTS: list[tuple[str, list[str]]] = [
    (
        "Once upon a time, Lily and a little bunny walked into the green forest to find the golden key near the locked treasure chest. Suddenly a big lion",
        ["lion", "Lily", "bunny", "key", "chest", "forest", "scared", "roar", "said"],
    ),
    (
        "Once upon a time, there was a little robot named Beep who lived in a noisy workshop. One rainy morning, Beep found a tiny lost kitten by the door. Beep",
        ["Beep", "kitten", "robot", "door", "rain", "warm", "milk", "help"],
    ),
    (
        "Sam and his sister Mia built a huge sandcastle on the beach near the waves. Suddenly, a big wave splashed toward their castle. Sam quickly",
        ["Sam", "Mia", "sand", "castle", "wave", "beach", "water", "wall"],
    ),
    (
        "In a quiet village, an old baker dropped his round loaf of bread and it rolled out the door down the hill. A hungry puppy saw the bread and",
        ["bread", "puppy", "baker", "hill", "chased", "ran", "ate", "mouth"],
    ),
    (
        "Lucy had a box of bright crayons, but she could not find the blue crayon to color the sky. She looked under her bed and",
        ["Lucy", "crayon", "blue", "bed", "sky", "found", "draw", "paper"],
    ),
    (
        "One winter day, Max made a snowman in the yard with a carrot nose. When the warm sun came out in the afternoon, the snowman began to",
        ["Max", "snowman", "sun", "melt", "carrot", "snow", "warm", "water"],
    ),
]


@torch.no_grad()
def evaluate_zero_shot_commonsense(

    model: ConveyerDiffusionLM,

    tokenizer: Any,

    device: torch.device,

) -> dict[str, Any]:
    """

    Evaluates zero-shot 2-way multiple-choice accuracy (BLiMP / StoryCloze style) using both:

      1. Causal LM log-probability per token over the continuation.

      2. Conveyor Slot-0 first-token preference on the continuation.

    """
    model.eval()
    correct_lm = 0
    correct_s0 = 0
    margin_sum = 0.0
    details: list[dict[str, Any]] = []

    for category, ctx, good_cont, bad_cont in COMMONSENSE_CLOZE_PAIRS:
        ctx_ids = tokenizer.encode(ctx.strip())
        good_ids = tokenizer.encode(ctx.strip() + good_cont)
        bad_ids = tokenizer.encode(ctx.strip() + bad_cont)
        plen = len(ctx_ids)

        def _score_seq(full_ids: list[int]) -> float:
            inp = torch.tensor([full_ids], dtype=torch.long, device=device)
            logits, _ = model.forward_causal_and_cache(inp, prefix_len=len(full_ids))
            log_probs = F.log_softmax(logits[0, :-1, :].float(), dim=-1)
            targets = inp[0, 1:]
            # Mean log-prob over the continuation tokens [plen-1 : len-1]
            start_idx = max(0, plen - 1)
            cont_lp = log_probs[start_idx:, :].gather(1, targets[start_idx:].unsqueeze(1)).squeeze(1)
            return float(cont_lp.mean().item())

        score_good = _score_seq(good_ids)
        score_bad = _score_seq(bad_ids)
        is_ok = score_good > score_bad
        correct_lm += int(is_ok)
        margin_sum += score_good - score_bad

        # Also check Conveyor Slot-0 log-prob on the first continuation token
        if len(good_ids) > plen and len(bad_ids) > plen:
            p_tensor = torch.tensor([ctx_ids], dtype=torch.long, device=device)
            kv = model.prefill_prefix(p_tensor)
            belt = model.init_empty_belt(1, tick=plen, device=device, deterministic=True)
            out = model.forward_belt(belt, kv_cache=kv)
            s0_lp = F.log_softmax(out.slot0_logits[0].float(), dim=-1)
            s0_good = float(s0_lp[good_ids[plen]].item())
            s0_bad = float(s0_lp[bad_ids[plen]].item())
            correct_s0 += int(s0_good > s0_bad)

        details.append(
            {
                "category": category,
                "correct": is_ok,
                "logprob_good": round(score_good, 3),
                "logprob_bad": round(score_bad, 3),
            }
        )

    n = max(1, len(COMMONSENSE_CLOZE_PAIRS))
    return {
        "commonsense_lm_acc": correct_lm / n,
        "commonsense_slot0_acc": correct_s0 / n,
        "mean_logprob_margin": margin_sum / n,
        "num_pairs": len(COMMONSENSE_CLOZE_PAIRS),
        "details": details,
    }


@torch.no_grad()
def evaluate_prefix_sensitivity_and_generation_quality(

    model: ConveyerDiffusionLM,

    tokenizer: Any,

    device: torch.device,

    max_new_tokens: int = 80,

) -> dict[str, Any]:
    """

    Measures:

      1. Prefix Sensitivity KL(P(slot0 | prompt) || P(slot0 | shuffled_prompt)):

         In the broken template model this was 0.03 nats (ignored word order).

         A real language model should have KL >> 0.5 nats.

      2. Open-Ended Generation Quality on 6 unseen prompts:

         - Distinct-2 / Distinct-3 n-gram diversity (1.0 = no repetition, <0.5 = looping)

         - 4-gram repetition rate (0.0 = clean prose)

         - Prompt entity/topic retention rate (% of continuations that reference the prompt's new entities/events)

    """
    model.eval()
    kl_list: list[float] = []
    dist2_list: list[float] = []
    dist3_list: list[float] = []
    rep4_list: list[float] = []
    entity_hits = 0
    samples: list[dict[str, str]] = []

    g = torch.Generator().manual_seed(123)
    for prompt_text, expected_keywords in NOVEL_STORY_PROMPTS:
        ids = tokenizer.encode(prompt_text.strip())
        p_true = torch.tensor([ids], dtype=torch.long, device=device)
        kv_true = model.prefill_prefix(p_true)
        belt_true = model.init_empty_belt(1, tick=len(ids), device=device, deterministic=True)
        d_true = F.softmax(model.forward_belt(belt_true, kv_cache=kv_true).slot0_logits[0].float(), dim=-1)

        perm = torch.randperm(len(ids) - 1, generator=g) + 1
        shuf_ids = [ids[0]] + [ids[i] for i in perm.tolist()]
        p_shuf = torch.tensor([shuf_ids], dtype=torch.long, device=device)
        kv_shuf = model.prefill_prefix(p_shuf)
        belt_shuf = model.init_empty_belt(1, tick=len(ids), device=device, deterministic=True)
        d_shuf = F.softmax(model.forward_belt(belt_shuf, kv_cache=kv_shuf).slot0_logits[0].float(), dim=-1)

        kl = float((d_true * (d_true.clamp_min(1e-9).log() - d_shuf.clamp_min(1e-9).log())).sum().item())
        kl_list.append(kl)

        out_ids, _ = generate_conveyer(
            model=model,
            prompt_tokens=p_true,
            max_new_tokens=max_new_tokens,
            prompt_overlap=10,
            temperature=0.0,
            eos_token_id=tokenizer.eos_id,
            deterministic_spawn=True,
        )
        gen_toks = out_ids[0, len(ids) :].tolist()
        gen_text = tokenizer.decode(gen_toks).strip()

        words = [w.strip(".,!?;:\"'()").lower() for w in gen_text.split() if w.strip()]
        if len(words) >= 4:
            bigrams = list(zip(words[:-1], words[1:]))
            trigrams = list(zip(words[:-2], words[1:-1], words[2:]))
            fourgrams = list(zip(words[:-3], words[1:-2], words[2:-1], words[3:]))
            dist2_list.append(len(set(bigrams)) / max(1, len(bigrams)))
            dist3_list.append(len(set(trigrams)) / max(1, len(trigrams)))
            c4 = Counter(fourgrams)
            repeated_4 = sum(v - 1 for v in c4.values() if v > 1)
            rep4_list.append(repeated_4 / max(1, len(fourgrams)))

        low_gen = gen_text.lower()
        hit = any(kw.lower() in low_gen for kw in expected_keywords[:4])
        entity_hits += int(hit)
        samples.append({"prompt": prompt_text, "continuation": gen_text})

    return {
        "prefix_word_order_kl": sum(kl_list) / max(1, len(kl_list)),
        "distinct_2": sum(dist2_list) / max(1, len(dist2_list)),
        "distinct_3": sum(dist3_list) / max(1, len(dist3_list)),
        "repeat_4gram_rate": sum(rep4_list) / max(1, len(rep4_list)),
        "prompt_entity_retention": entity_hits / max(1, len(NOVEL_STORY_PROMPTS)),
        "samples": samples,
    }


@torch.no_grad()
def evaluate_crystallization_curve(

    model: ConveyerDiffusionLM,

    cfg: ConveyerConfig,

    val_loader: DataLoader[dict[str, torch.Tensor]],

    device: torch.device,

    pad_id: int = 0,

    max_batches: int = 20,

) -> dict[str, Any]:
    """

    Computes the Per-Slot Accuracy, Entropy, and Pause Rate across the 64-slot oven belt

    plus held-out real TinyStories Perplexity (Causal LM & Conveyor Slot-0).

    """
    model.eval()
    B = model.belt_size
    acc_sum = torch.zeros(B, device=device)
    ent_sum = torch.zeros(B, device=device)
    s0_loss_sum = 0.0
    lm_loss_sum = 0.0
    lm_acc_sum = 0.0
    pause_sum = 0.0
    count = 0

    for i, batch in enumerate(val_loader):
        if i >= max_batches:
            break
        seq = batch["input_ids"].to(device)
        plen = batch["prompt_lengths"].to(device)
        use_amp = device.type == "cuda" and torch.cuda.is_bf16_supported()
        with torch.autocast(device_type=device.type, dtype=torch.bfloat16 if use_amp else torch.float32, enabled=use_amp):
            m = compute_unrolled_conveyer_loss(
                model=model,
                seq_tokens=seq,
                cfg=cfg,
                prompt_lengths=plen,
                tick_start=int(plen.min().item()),
                pad_token_id=pad_id,
            )
        acc_sum += m.per_slot_acc
        ent_sum += m.per_slot_entropy
        s0_loss_sum += m.slot0_loss
        lm_loss_sum += m.prefix_lm_loss
        lm_acc_sum += m.prefix_lm_acc
        pause_sum += m.mean_plan_pause
        count += 1

    denom = max(count, 1)
    acc_curve = (acc_sum / denom).cpu().tolist()
    ent_curve = (ent_sum / denom).cpu().tolist()
    s0_ce = s0_loss_sum / denom
    lm_ce = lm_loss_sum / denom
    return {
        "slot0_ce": s0_ce,
        "slot0_ppl": math.exp(min(s0_ce, 20.0)),
        "slot0_acc": acc_curve[0],
        "causal_lm_ce": lm_ce,
        "causal_lm_ppl": math.exp(min(lm_ce, 20.0)),
        "causal_lm_acc": lm_acc_sum / denom,
        "mean_plan_pause": pause_sum / denom,
        "per_slot_acc": acc_curve,
        "per_slot_entropy": ent_curve,
    }


@torch.no_grad()
def evaluate_closed_loop_generation(

    model: ConveyerDiffusionLM,

    val_ds: Any,

    tokenizer: Any,

    device: torch.device,

    num_episodes: int = 32,

    prompt_overlap: int = 12,

    bake_steps: int = 64,

    max_new_tokens: int = 24,

) -> dict[str, float]:
    model.eval()
    n_eval = min(num_episodes, len(val_ds))
    first_tok_correct = 0
    total_tok_correct = 0
    total_tok_count = 0
    exact_match = 0

    for i in range(n_eval):
        item = val_ds[i]
        seq = item["input_ids"].to(device)
        plen = int(item["prompt_len"])
        prompt = seq[:plen].unsqueeze(0)

        gt_full = seq[plen:].tolist()
        gt_target: list[int] = []
        for tid in gt_full:
            if tid == tokenizer.pad_id:
                break
            gt_target.append(tid)
            if tid == tokenizer.eos_id:
                break

        if not gt_target:
            continue

        gen_len = min(max_new_tokens, len(gt_target))
        full_out, _ = generate_conveyer(
            model=model,
            prompt_tokens=prompt,
            max_new_tokens=gen_len,
            prompt_overlap=prompt_overlap,
            bake_steps=bake_steps,
            temperature=0.0,
            eos_token_id=tokenizer.eos_id,
            deterministic_spawn=True,
        )
        pred_ids = full_out[0, plen : plen + gen_len].tolist()
        gt_slice = gt_target[:gen_len]

        if len(pred_ids) > 0 and pred_ids[0] == gt_slice[0]:
            first_tok_correct += 1

        matches = sum(1 for p, g in zip(pred_ids, gt_slice) if p == g)
        total_tok_correct += matches
        total_tok_count += len(gt_slice)
        if pred_ids == gt_slice:
            exact_match += 1

    return {
        "first_token_acc": first_tok_correct / max(1, n_eval),
        "token_acc": total_tok_correct / max(1, total_tok_count),
        "exact_seq_acc": exact_match / max(1, n_eval),
    }


def main() -> None:
    parser = argparse.ArgumentParser(description="Comprehensive Benchmark Suite for Conveyor-Belt Diffusion LLM")
    parser.add_argument("--ckpt", type=str, default="runs/tinystories/best.pt", help="Path to checkpoint (.pt)")
    parser.add_argument("--device", type=str, default="auto", help="'cpu', 'cuda', or 'auto'")
    parser.add_argument("--episodes", type=int, default=32, help="Number of closed-loop episodes")
    parser.add_argument("--bake-steps", type=int, nargs="+", default=[16, 64], help="Bake depths K to evaluate")
    parser.add_argument("--overlaps", type=int, nargs="+", default=[0, 10], help="Prompt-overlap windows W_pre")
    parser.add_argument("--out-json", type=str, default=None, help="Optional path to save evaluation JSON")
    args = parser.parse_args()

    if args.device == "auto":
        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    else:
        device = torch.device(args.device)

    model, cfg, ckpt_meta = load_checkpoint(args.ckpt, device=device)
    _, val_ds, tokenizer = build_dataset_and_tokenizer(cfg)
    val_loader = DataLoader(
        val_ds,
        batch_size=cfg.train.batch_size,
        shuffle=False,
        collate_fn=lambda b: collate_conveyer_batch(b, pad_id=tokenizer.pad_id),
    )

    step_num = ckpt_meta.get("step", "?")
    elapsed_h = float(ckpt_meta.get("elapsed_sec", 0.0)) / 3600.0

    cryst = evaluate_crystallization_curve(model, cfg, val_loader, device, pad_id=tokenizer.pad_id)
    acc_c = cryst["per_slot_acc"]
    ent_c = cryst["per_slot_entropy"]
    B = len(acc_c)

    print("=" * 92)
    print(
        f"CONVEYOR-BELT DIFFUSION LLM BENCHMARK SUITE | Ckpt: {args.ckpt} (step {step_num}, {elapsed_h:.2f}h) "
        f"| Params: {model.num_parameters()/1e6:.2f}M | Device: {device}"
    )
    print("=" * 92)
    print(
        f"[1] Held-Out Real TinyStories LM Metrics (10,000 unseen stories, 0% templates):\n"
        f"    - Conveyor Slot-0 Perplexity : {cryst['slot0_ppl']:6.2f}  | Top-1 Next-Token Acc: {cryst['slot0_acc']*100:5.2f}%\n"
        f"    - Causal Prefix LM Perplexity: {cryst['causal_lm_ppl']:6.2f}  | Top-1 Next-Token Acc: {cryst['causal_lm_acc']*100:5.2f}%\n"
        f"    - Back-75% Planning Pause    : {cryst['mean_plan_pause']*100:5.1f}%"
    )
    print("-" * 92)
    print("[2] Per-Slot Crystallization Curve along the 64-Slot Oven Belt (Back k=63 -> Front k=0):")
    sample_slots = [B - 1, (3 * B) // 4, B // 2, B // 4, min(8, B - 1), min(3, B - 1), 0]
    seen: set[int] = set()
    for k in sample_slots:
        if k in seen:
            continue
        seen.add(k)
        zone = "Back (75% Plan)   " if k >= 16 else "Front (25% Stream)"
        print(f"    Slot {k:2d} [{zone}] | Top-1 Acc: {acc_c[k]*100:6.2f}% | Entropy: {ent_c[k]:.3f} nats")

    if cfg.data.dataset == "tinystories":
        cs = evaluate_zero_shot_commonsense(model, tokenizer, device)
        gen_q = evaluate_prefix_sensitivity_and_generation_quality(model, tokenizer, device)
        print("-" * 92)
        print(
            f"[3] Zero-Shot StoryCloze / Commonsense 2-Way Multiple Choice ({cs['num_pairs']} unseen pairs, random=50%):\n"
            f"    - Causal LM Choice Accuracy  : {cs['commonsense_lm_acc']*100:6.2f}% (mean log-prob margin: {cs['mean_logprob_margin']:+.3f} nats)\n"
            f"    - Conveyor Slot-0 Choice Acc : {cs['commonsense_slot0_acc']*100:6.2f}%"
        )
        print("-" * 92)
        print(
            f"[4] Prompt Responsiveness & Open-Ended Generation Quality (6 novel story prompts):\n"
            f"    - Word-Order Sensitivity KL  : {gen_q['prefix_word_order_kl']:6.3f} nats (broken model was 0.032; >0.50 = strong)\n"
            f"    - Prompt Entity Retention    : {gen_q['prompt_entity_retention']*100:6.1f}%\n"
            f"    - Lexical Diversity          : Distinct-2 = {gen_q['distinct_2']*100:5.1f}% | Distinct-3 = {gen_q['distinct_3']*100:5.1f}%\n"
            f"    - 4-gram Repetition Rate     : {gen_q['repeat_4gram_rate']*100:5.2f}% (lower is better; 0% = zero loops)"
        )
        print("-" * 92)
        print("[5] Live Novel Prompt Continutations:")
        for s in gen_q["samples"][:3]:
            print(f"    PROMPT : {s['prompt']}")
            print(f"    OUTPUT : {s['continuation']}\n")
    else:
        cs = {}
        gen_q = {}

    print("-" * 92)
    print(f"{'Bake Depth K':<15} | {'Prompt Overlap W':<18} | {'Pos-0 Acc':<12} | {'Token Acc':<12} | {'Exact Seq Acc':<14}")
    print("-" * 92)

    sweep_results: list[dict[str, Any]] = []
    for k_bake in args.bake_steps:
        for w_ov in args.overlaps:
            res = evaluate_closed_loop_generation(
                model=model,
                val_ds=val_ds,
                tokenizer=tokenizer,
                device=device,
                num_episodes=args.episodes,
                prompt_overlap=w_ov,
                bake_steps=k_bake,
            )
            print(
                f"K = {k_bake:<11d} | W_pre = {w_ov:<10d} | "
                f"{res['first_token_acc']*100:9.2f}%  | {res['token_acc']*100:9.2f}%  | {res['exact_seq_acc']*100:10.2f}%"
            )
            sweep_results.append({"bake_steps": k_bake, "prompt_overlap": w_ov, **res})

    if args.out_json:
        Path(args.out_json).parent.mkdir(parents=True, exist_ok=True)
        Path(args.out_json).write_text(
            json.dumps(
                {
                    "step": step_num,
                    "crystallization": cryst,
                    "commonsense": cs,
                    "generation_quality": gen_q,
                    "closed_loop": sweep_results,
                },
                indent=2,
            ),
            encoding="utf-8",
        )


if __name__ == "__main__":
    main()