Upload folder using huggingface_hub
Browse files- Dockerfile +24 -0
- configs/synthetic.yaml +53 -0
- pyproject.toml +37 -0
- src/conveyer/__init__.py +4 -0
- src/conveyer/checkpoint.py +151 -0
- src/conveyer/config.py +222 -0
- src/conveyer/data/__init__.py +54 -0
- src/conveyer/data/synthetic.py +276 -0
- src/conveyer/data/tinystories.py +533 -0
- src/conveyer/eval.py +557 -0
- src/conveyer/infer.py +319 -0
- src/conveyer/live.py +164 -0
- src/conveyer/losses.py +406 -0
- src/conveyer/model.py +739 -0
- src/conveyer/sweep.py +112 -0
- src/conveyer/train.py +472 -0
- src/conveyer/web_sim.py +840 -0
- tests/test_conveyer.py +254 -0
Dockerfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 6 |
+
PYTHONUNBUFFERED=1 \
|
| 7 |
+
PYTHONPATH=/app/src \
|
| 8 |
+
HOST=0.0.0.0 \
|
| 9 |
+
PORT=7860
|
| 10 |
+
|
| 11 |
+
RUN pip install --no-cache-dir \
|
| 12 |
+
torch --index-url https://download.pytorch.org/whl/cpu && \
|
| 13 |
+
pip install --no-cache-dir numpy pyyaml tiktoken
|
| 14 |
+
|
| 15 |
+
COPY pyproject.toml README.md ./
|
| 16 |
+
COPY configs/ ./configs/
|
| 17 |
+
COPY src/ ./src/
|
| 18 |
+
COPY data/tinystories/tiktoken_compact_4096.json ./data/tinystories/tiktoken_compact_4096.json
|
| 19 |
+
COPY data/tinystories/val_sample_256.txt ./data/tinystories/val_sample_256.txt
|
| 20 |
+
COPY runs/tinystories/benchmark_24h.json ./runs/tinystories/benchmark_24h.json
|
| 21 |
+
|
| 22 |
+
EXPOSE 7860
|
| 23 |
+
|
| 24 |
+
CMD ["python", "-m", "conveyer.web_sim", "--host", "0.0.0.0", "--port", "7860"]
|
configs/synthetic.yaml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model:
|
| 2 |
+
vocab_size: 128
|
| 3 |
+
d_model: 256
|
| 4 |
+
n_heads: 4
|
| 5 |
+
n_layers: 6
|
| 6 |
+
d_ff: 688
|
| 7 |
+
max_seq_len: 512
|
| 8 |
+
dropout: 0.0
|
| 9 |
+
belt_size: 64
|
| 10 |
+
tau_min: 0.5
|
| 11 |
+
tau_max: 4.0
|
| 12 |
+
tau_gamma: 1.5
|
| 13 |
+
gate_bias_init: 1.0
|
| 14 |
+
spawn_noise_std: 0.5
|
| 15 |
+
thought_noise_std: 0.25
|
| 16 |
+
use_thought_highway: true
|
| 17 |
+
causal_belt_only: false
|
| 18 |
+
|
| 19 |
+
train:
|
| 20 |
+
out_dir: "runs/synthetic"
|
| 21 |
+
seed: 42
|
| 22 |
+
batch_size: 64
|
| 23 |
+
lr: 0.0005
|
| 24 |
+
weight_decay: 0.01
|
| 25 |
+
grad_clip: 1.0
|
| 26 |
+
max_steps: 1200
|
| 27 |
+
warmup_steps: 80
|
| 28 |
+
eval_every: 100
|
| 29 |
+
log_every: 25
|
| 30 |
+
unroll_steps: 16
|
| 31 |
+
seed_sigma_max: 3.5
|
| 32 |
+
seed_sigma_eta: 1.25
|
| 33 |
+
loss_w_min: 0.05
|
| 34 |
+
loss_w_beta: 1.5
|
| 35 |
+
entropy_reg: 0.001
|
| 36 |
+
no_belt_bptt: false
|
| 37 |
+
no_thought_highway: false
|
| 38 |
+
ar_baseline: false
|
| 39 |
+
|
| 40 |
+
infer:
|
| 41 |
+
prompt_overlap: 16
|
| 42 |
+
bake_steps: 64
|
| 43 |
+
temperature: 0.0
|
| 44 |
+
max_new_tokens: 64
|
| 45 |
+
|
| 46 |
+
data:
|
| 47 |
+
dataset: "synthetic"
|
| 48 |
+
task: "all"
|
| 49 |
+
seq_len: 128
|
| 50 |
+
num_train: 24000
|
| 51 |
+
num_val: 1024
|
| 52 |
+
data_dir: "data/synthetic"
|
| 53 |
+
vocab_size: 128
|
pyproject.toml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=68", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "diffusion-conveyer-llm"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "64-Slot Dual-Channel Conveyor-Belt Diffusion Language Model"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.10"
|
| 11 |
+
dependencies = [
|
| 12 |
+
"torch>=2.4.0",
|
| 13 |
+
"numpy>=1.26.0",
|
| 14 |
+
"pyyaml>=6.0",
|
| 15 |
+
"tqdm>=4.66.0",
|
| 16 |
+
"tokenizers>=0.19.0",
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
[project.optional-dependencies]
|
| 20 |
+
dev = [
|
| 21 |
+
"pytest>=8.0.0",
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
[project.scripts]
|
| 25 |
+
conveyer-train = "conveyer.train:main"
|
| 26 |
+
conveyer-eval = "conveyer.eval:main"
|
| 27 |
+
conveyer-infer = "conveyer.infer:main"
|
| 28 |
+
conveyer-sweep = "conveyer.sweep:main"
|
| 29 |
+
conveyer-live = "conveyer.live:main"
|
| 30 |
+
conveyer-web = "conveyer.web_sim:main"
|
| 31 |
+
|
| 32 |
+
[tool.setuptools.packages.find]
|
| 33 |
+
where = ["src"]
|
| 34 |
+
|
| 35 |
+
[tool.pytest.ini_options]
|
| 36 |
+
testpaths = ["tests"]
|
| 37 |
+
pythonpath = ["src"]
|
src/conveyer/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from conveyer.config import ConveyerConfig, load_config
|
| 2 |
+
from conveyer.model import ConveyerDiffusionLM
|
| 3 |
+
|
| 4 |
+
__all__ = ["ConveyerConfig", "load_config", "ConveyerDiffusionLM"]
|
src/conveyer/checkpoint.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import asdict
|
| 4 |
+
import os
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Optional
|
| 7 |
+
import urllib.request
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from conveyer.config import ConveyerConfig, _merge_dataclass
|
| 11 |
+
from conveyer.model import ConveyerDiffusionLM
|
| 12 |
+
|
| 13 |
+
HF_DEFAULT_CKPT_URL = "https://huggingface.co/sumitk/diffusion-conveyer-llm/resolve/main/best.pt"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _move_to_cpu(obj: Any) -> Any:
|
| 17 |
+
if isinstance(obj, torch.Tensor):
|
| 18 |
+
return obj.detach().to(device="cpu", non_blocking=False)
|
| 19 |
+
if isinstance(obj, dict):
|
| 20 |
+
return {k: _move_to_cpu(v) for k, v in obj.items()}
|
| 21 |
+
if isinstance(obj, list):
|
| 22 |
+
return [_move_to_cpu(v) for v in obj]
|
| 23 |
+
if isinstance(obj, tuple):
|
| 24 |
+
return tuple(_move_to_cpu(v) for v in obj)
|
| 25 |
+
return obj
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _atomic_write_payload(path: Path, payload: dict[str, Any]) -> None:
|
| 29 |
+
"""
|
| 30 |
+
Crash-safe atomic checkpoint writer on Windows NTFS:
|
| 31 |
+
1. If `path` already exists, rotates `path -> path.prev` BEFORE writing so an abrupt BSOD/power loss
|
| 32 |
+
never leaves both the current and previous checkpoint with un-flushed NTFS data blocks.
|
| 33 |
+
2. Writes `payload` to `path.tmp`, calls `f.flush()` + `os.fsync(f.fileno())` to force NTFS data blocks
|
| 34 |
+
to physical NVMe storage BEFORE updating the directory metadata via `os.replace`.
|
| 35 |
+
"""
|
| 36 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 37 |
+
tmp_p = path.with_suffix(path.suffix + ".tmp")
|
| 38 |
+
prev_p = path.with_suffix(path.suffix + ".prev")
|
| 39 |
+
|
| 40 |
+
with open(tmp_p, "wb") as f:
|
| 41 |
+
torch.save(payload, f)
|
| 42 |
+
f.flush()
|
| 43 |
+
os.fsync(f.fileno())
|
| 44 |
+
|
| 45 |
+
if path.exists():
|
| 46 |
+
try:
|
| 47 |
+
os.replace(path, prev_p)
|
| 48 |
+
except OSError:
|
| 49 |
+
pass
|
| 50 |
+
|
| 51 |
+
os.replace(tmp_p, path)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def save_checkpoint(
|
| 55 |
+
path: str | Path,
|
| 56 |
+
model: ConveyerDiffusionLM,
|
| 57 |
+
cfg: ConveyerConfig,
|
| 58 |
+
step: int,
|
| 59 |
+
optimizer: Optional[torch.optim.Optimizer] = None,
|
| 60 |
+
metrics: Optional[dict[str, Any]] = None,
|
| 61 |
+
best_val_loss: Optional[float] = None,
|
| 62 |
+
elapsed_sec: float = 0.0,
|
| 63 |
+
) -> None:
|
| 64 |
+
"""
|
| 65 |
+
Copies `model` and `optimizer` state dicts to CPU RAM first (avoiding 350 MB of synchronous
|
| 66 |
+
WDDM GPU->CPU DMA transfers inside `torch.save` that can trigger `dxgmms2.sys` BSODs),
|
| 67 |
+
then writes atomically with `os.fsync` and `.prev` backup rotation.
|
| 68 |
+
"""
|
| 69 |
+
if torch.cuda.is_available():
|
| 70 |
+
torch.cuda.synchronize()
|
| 71 |
+
|
| 72 |
+
cpu_model_state = _move_to_cpu(model.state_dict())
|
| 73 |
+
payload: dict[str, Any] = {
|
| 74 |
+
"model_state": cpu_model_state,
|
| 75 |
+
"config": asdict(cfg),
|
| 76 |
+
"step": step,
|
| 77 |
+
"metrics": metrics or {},
|
| 78 |
+
"best_val_loss": best_val_loss,
|
| 79 |
+
"elapsed_sec": elapsed_sec,
|
| 80 |
+
}
|
| 81 |
+
if optimizer is not None:
|
| 82 |
+
payload["optimizer_state"] = _move_to_cpu(optimizer.state_dict())
|
| 83 |
+
|
| 84 |
+
_atomic_write_payload(Path(path), payload)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def load_checkpoint_payload(path: str | Path, map_location: Any = "cpu") -> tuple[dict[str, Any], Path]:
|
| 88 |
+
"""
|
| 89 |
+
Loads checkpoint dict from `path`, automatically falling back to `path.prev` or the latest
|
| 90 |
+
valid `ckpt_step_*.pt` in the same directory if `path` was corrupted by a power loss or BSOD.
|
| 91 |
+
"""
|
| 92 |
+
p = Path(path)
|
| 93 |
+
candidates: list[Path] = [p, p.with_suffix(p.suffix + ".prev")]
|
| 94 |
+
if p.parent.exists():
|
| 95 |
+
milestones = sorted(p.parent.glob("ckpt_step_*.pt"), key=lambda x: x.stat().st_mtime, reverse=True)
|
| 96 |
+
for m in milestones:
|
| 97 |
+
if m not in candidates:
|
| 98 |
+
candidates.append(m)
|
| 99 |
+
m_prev = m.with_suffix(m.suffix + ".prev")
|
| 100 |
+
if m_prev.exists() and m_prev not in candidates:
|
| 101 |
+
candidates.append(m_prev)
|
| 102 |
+
|
| 103 |
+
last_err: Optional[Exception] = None
|
| 104 |
+
for cand in candidates:
|
| 105 |
+
if not cand.exists() or cand.stat().st_size == 0:
|
| 106 |
+
continue
|
| 107 |
+
try:
|
| 108 |
+
data = torch.load(cand, map_location=map_location, weights_only=False)
|
| 109 |
+
if isinstance(data, dict) and "model_state" in data:
|
| 110 |
+
return data, cand
|
| 111 |
+
except Exception as e:
|
| 112 |
+
last_err = e
|
| 113 |
+
print(f"[Checkpoint] Warning: skipping unreadable checkpoint {cand.name} ({e})", flush=True)
|
| 114 |
+
|
| 115 |
+
if p.name == "best.pt" and "tinystories" in str(p).replace("\\", "/"):
|
| 116 |
+
try:
|
| 117 |
+
p.parent.mkdir(parents=True, exist_ok=True)
|
| 118 |
+
tmp_dl = p.with_suffix(".pt.dl")
|
| 119 |
+
print(f"[Checkpoint] Downloading pretrained checkpoint from {HF_DEFAULT_CKPT_URL} ...", flush=True)
|
| 120 |
+
req = urllib.request.Request(HF_DEFAULT_CKPT_URL, headers={"User-Agent": "Mozilla/5.0"})
|
| 121 |
+
with urllib.request.urlopen(req, timeout=120) as resp, open(tmp_dl, "wb") as f:
|
| 122 |
+
while True:
|
| 123 |
+
chunk = resp.read(4 * 1024 * 1024)
|
| 124 |
+
if not chunk:
|
| 125 |
+
break
|
| 126 |
+
f.write(chunk)
|
| 127 |
+
os.replace(tmp_dl, p)
|
| 128 |
+
data = torch.load(p, map_location=map_location, weights_only=False)
|
| 129 |
+
if isinstance(data, dict) and "model_state" in data:
|
| 130 |
+
return data, p
|
| 131 |
+
except Exception as e:
|
| 132 |
+
last_err = e
|
| 133 |
+
|
| 134 |
+
raise RuntimeError(f"No valid checkpoint found for {p} (last error: {last_err})")
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def load_checkpoint(
|
| 138 |
+
path: str | Path,
|
| 139 |
+
device: Optional[torch.device] = None,
|
| 140 |
+
) -> tuple[ConveyerDiffusionLM, ConveyerConfig, dict[str, Any]]:
|
| 141 |
+
if device is None:
|
| 142 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 143 |
+
ckpt, used_path = load_checkpoint_payload(path, map_location=device)
|
| 144 |
+
if used_path != Path(path):
|
| 145 |
+
print(f"[Checkpoint] Recovered from fallback checkpoint {used_path}", flush=True)
|
| 146 |
+
cfg = ConveyerConfig()
|
| 147 |
+
if "config" in ckpt:
|
| 148 |
+
_merge_dataclass(cfg, ckpt["config"])
|
| 149 |
+
model = ConveyerDiffusionLM(cfg.model).to(device)
|
| 150 |
+
model.load_state_dict(ckpt["model_state"])
|
| 151 |
+
return model, cfg, ckpt
|
src/conveyer/config.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import asdict, dataclass, field, fields, is_dataclass
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
import yaml # type: ignore
|
| 10 |
+
except ImportError:
|
| 11 |
+
yaml = None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class ModelConfig:
|
| 16 |
+
vocab_size: int = 128
|
| 17 |
+
d_model: int = 256
|
| 18 |
+
n_heads: int = 4
|
| 19 |
+
n_layers: int = 6
|
| 20 |
+
d_ff: int = 688
|
| 21 |
+
max_seq_len: int = 512
|
| 22 |
+
dropout: float = 0.0
|
| 23 |
+
rope_theta: float = 10000.0
|
| 24 |
+
# Conveyor belt parameters
|
| 25 |
+
belt_size: int = 64
|
| 26 |
+
front_conveyer_slots: int = 16 # Slots 0..15 shift left by 1 every tick; Slots 16..63 (75%) are Elastic Planning Zone
|
| 27 |
+
plan_horizon_stride: int = 2 # Back 75% planning slots span 2x longer horizon while sentences expand in middle
|
| 28 |
+
tau_min: float = 0.5
|
| 29 |
+
tau_max: float = 4.0
|
| 30 |
+
tau_gamma: float = 1.5
|
| 31 |
+
gate_bias_init: float = 1.0
|
| 32 |
+
spawn_noise_std: float = 0.5
|
| 33 |
+
thought_noise_std: float = 0.25
|
| 34 |
+
# Ablation & efficiency switches
|
| 35 |
+
use_thought_highway: bool = True
|
| 36 |
+
use_elastic_planning: bool = True # Enables 75% paused/elastic planning belt with mid-belt expansion
|
| 37 |
+
causal_belt_only: bool = False # If True, disables bidirectional belt attention (AR baseline mode)
|
| 38 |
+
grad_checkpointing: bool = False # Enables activation checkpointing for long unroll horizons (U=24..32)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass
|
| 42 |
+
class TrainConfig:
|
| 43 |
+
out_dir: str = "runs/synthetic"
|
| 44 |
+
seed: int = 42
|
| 45 |
+
batch_size: int = 64
|
| 46 |
+
lr: float = 5e-4
|
| 47 |
+
weight_decay: float = 0.01
|
| 48 |
+
grad_clip: float = 1.0
|
| 49 |
+
max_steps: int = 1200
|
| 50 |
+
max_hours: float = 0.0 # Optional wall-clock training budget in hours (0.0 = use max_steps only)
|
| 51 |
+
warmup_steps: int = 80
|
| 52 |
+
eval_every: int = 100
|
| 53 |
+
save_every: int = 500 # Periodic crash-safe checkpoint interval (steps)
|
| 54 |
+
log_every: int = 25
|
| 55 |
+
unroll_steps: int = 16
|
| 56 |
+
prefix_lm_weight: float = 1.0 # Weight on causal next-token LM loss across the prompt/sequence
|
| 57 |
+
step_cooldown_ms: int = 0 # Optional GPU thermal pacing sleep per step in ms
|
| 58 |
+
max_gpu_temp_c: int = 80 # Automatic thermal throttle threshold in Celsius
|
| 59 |
+
# Corrupted-belt seeding & recovery parameters
|
| 60 |
+
seed_sigma_max: float = 3.5
|
| 61 |
+
seed_sigma_eta: float = 1.25
|
| 62 |
+
recovery_prob: float = 0.35 # DAgger / self-conditioned prefix + belt recovery probability
|
| 63 |
+
belt_corrupt_prob: float = 0.20 # Mid-unroll belt perturbation rate to train active belt recovery
|
| 64 |
+
# Horizon loss weights: w_k = w_min + (1 - w_min) * (1 - k / (B - 1))^w_beta
|
| 65 |
+
loss_w_min: float = 0.05
|
| 66 |
+
loss_w_beta: float = 1.5
|
| 67 |
+
entropy_reg: float = 0.001
|
| 68 |
+
# Ablations
|
| 69 |
+
no_belt_bptt: bool = False
|
| 70 |
+
no_thought_highway: bool = False
|
| 71 |
+
ar_baseline: bool = False
|
| 72 |
+
overfit_batches: int = 0
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@dataclass
|
| 76 |
+
class InferConfig:
|
| 77 |
+
prompt_overlap: int = 12
|
| 78 |
+
bake_steps: int = 64
|
| 79 |
+
temperature: float = 0.0 # 0.0 = greedy argmax at slot 0
|
| 80 |
+
max_new_tokens: int = 64
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@dataclass
|
| 84 |
+
class DataConfig:
|
| 85 |
+
dataset: str = "synthetic" # "synthetic" or "tinystories"
|
| 86 |
+
task: str = "all" # "reverse", "path", "constraint", "all", "lm"
|
| 87 |
+
seq_len: int = 192
|
| 88 |
+
num_train: int = 24000
|
| 89 |
+
num_val: int = 1024
|
| 90 |
+
data_dir: str = "data/synthetic"
|
| 91 |
+
vocab_size: int = 128
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@dataclass
|
| 95 |
+
class ConveyerConfig:
|
| 96 |
+
model: ModelConfig = field(default_factory=ModelConfig)
|
| 97 |
+
train: TrainConfig = field(default_factory=TrainConfig)
|
| 98 |
+
infer: InferConfig = field(default_factory=InferConfig)
|
| 99 |
+
data: DataConfig = field(default_factory=DataConfig)
|
| 100 |
+
|
| 101 |
+
def to_dict(self) -> dict[str, Any]:
|
| 102 |
+
return asdict(self)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _parse_scalar(val_str: str) -> Any:
|
| 106 |
+
s = val_str.strip()
|
| 107 |
+
if not s:
|
| 108 |
+
return ""
|
| 109 |
+
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
|
| 110 |
+
return s[1:-1]
|
| 111 |
+
low = s.lower()
|
| 112 |
+
if low == "true":
|
| 113 |
+
return True
|
| 114 |
+
if low == "false":
|
| 115 |
+
return False
|
| 116 |
+
if low in ("null", "none"):
|
| 117 |
+
return None
|
| 118 |
+
try:
|
| 119 |
+
if "." in s or "e" in low:
|
| 120 |
+
return float(s)
|
| 121 |
+
return int(s)
|
| 122 |
+
except ValueError:
|
| 123 |
+
return s
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def _simple_yaml_parse(text: str) -> dict[str, Any]:
|
| 127 |
+
"""Minimal dependency-free YAML parser for 2-level config files."""
|
| 128 |
+
if yaml is not None:
|
| 129 |
+
return yaml.safe_load(text) or {}
|
| 130 |
+
text_stripped = text.strip()
|
| 131 |
+
if text_stripped.startswith("{"):
|
| 132 |
+
return json.loads(text_stripped)
|
| 133 |
+
root: dict[str, Any] = {}
|
| 134 |
+
current_section: dict[str, Any] | None = None
|
| 135 |
+
for raw_line in text.splitlines():
|
| 136 |
+
line = raw_line.split("#", 1)[0].rstrip()
|
| 137 |
+
if not line.strip():
|
| 138 |
+
continue
|
| 139 |
+
indent = len(line) - len(line.lstrip(" "))
|
| 140 |
+
stripped = line.strip()
|
| 141 |
+
if ":" not in stripped:
|
| 142 |
+
continue
|
| 143 |
+
key, val_part = stripped.split(":", 1)
|
| 144 |
+
key = key.strip()
|
| 145 |
+
val_part = val_part.strip()
|
| 146 |
+
if indent == 0:
|
| 147 |
+
if val_part == "":
|
| 148 |
+
current_section = {}
|
| 149 |
+
root[key] = current_section
|
| 150 |
+
else:
|
| 151 |
+
root[key] = _parse_scalar(val_part)
|
| 152 |
+
current_section = None
|
| 153 |
+
else:
|
| 154 |
+
if current_section is None:
|
| 155 |
+
raise ValueError(f"Unexpected indentation in YAML line: {raw_line}")
|
| 156 |
+
current_section[key] = _parse_scalar(val_part)
|
| 157 |
+
return root
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def _merge_dataclass(dc_instance: Any, updates: dict[str, Any]) -> None:
|
| 161 |
+
valid_fields = {f.name: f for f in fields(dc_instance)}
|
| 162 |
+
for k, v in updates.items():
|
| 163 |
+
if k not in valid_fields:
|
| 164 |
+
continue
|
| 165 |
+
current_val = getattr(dc_instance, k)
|
| 166 |
+
if is_dataclass(current_val) and isinstance(v, dict):
|
| 167 |
+
_merge_dataclass(current_val, v)
|
| 168 |
+
else:
|
| 169 |
+
setattr(dc_instance, k, v)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _cast_value(raw: str, target_type: Any) -> Any:
|
| 173 |
+
if target_type is bool or raw.lower() in ("true", "false"):
|
| 174 |
+
return raw.lower() in ("true", "1", "yes")
|
| 175 |
+
val = _parse_scalar(raw)
|
| 176 |
+
if target_type is float and isinstance(val, int):
|
| 177 |
+
return float(val)
|
| 178 |
+
if target_type is int and isinstance(val, float):
|
| 179 |
+
return int(val)
|
| 180 |
+
if target_type is str:
|
| 181 |
+
return str(val)
|
| 182 |
+
return val
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def apply_overrides(cfg: ConveyerConfig, overrides: list[str]) -> ConveyerConfig:
|
| 186 |
+
for item in overrides:
|
| 187 |
+
if "=" not in item:
|
| 188 |
+
raise ValueError(f"Override must be in key=value format, got: {item}")
|
| 189 |
+
key_path, raw_val = item.split("=", 1)
|
| 190 |
+
parts = key_path.strip().split(".")
|
| 191 |
+
obj: Any = cfg
|
| 192 |
+
for p in parts[:-1]:
|
| 193 |
+
if not hasattr(obj, p):
|
| 194 |
+
raise KeyError(f"Unknown config section '{p}' in '{key_path}'")
|
| 195 |
+
obj = getattr(obj, p)
|
| 196 |
+
leaf = parts[-1]
|
| 197 |
+
if not hasattr(obj, leaf):
|
| 198 |
+
raise KeyError(f"Unknown config key '{leaf}' in '{key_path}'")
|
| 199 |
+
cur = getattr(obj, leaf)
|
| 200 |
+
val = _cast_value(raw_val.strip(), type(cur))
|
| 201 |
+
setattr(obj, leaf, val)
|
| 202 |
+
_sync_config(cfg)
|
| 203 |
+
return cfg
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def _sync_config(cfg: ConveyerConfig) -> None:
|
| 207 |
+
cfg.model.vocab_size = cfg.data.vocab_size
|
| 208 |
+
if cfg.train.no_thought_highway:
|
| 209 |
+
cfg.model.use_thought_highway = False
|
| 210 |
+
if cfg.train.ar_baseline:
|
| 211 |
+
cfg.model.causal_belt_only = True
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def load_config(path: str | Path | None = None, overrides: list[str] | None = None) -> ConveyerConfig:
|
| 215 |
+
cfg = ConveyerConfig()
|
| 216 |
+
if path is not None:
|
| 217 |
+
raw = _simple_yaml_parse(Path(path).read_text(encoding="utf-8"))
|
| 218 |
+
_merge_dataclass(cfg, raw)
|
| 219 |
+
if overrides:
|
| 220 |
+
apply_overrides(cfg, overrides)
|
| 221 |
+
_sync_config(cfg)
|
| 222 |
+
return cfg
|
src/conveyer/data/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any, Protocol
|
| 4 |
+
import torch
|
| 5 |
+
from torch.utils.data import Dataset
|
| 6 |
+
|
| 7 |
+
from conveyer.config import ConveyerConfig
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class TokenizerProtocol(Protocol):
|
| 11 |
+
vocab_size: int
|
| 12 |
+
pad_id: int
|
| 13 |
+
bos_id: int
|
| 14 |
+
eos_id: int
|
| 15 |
+
|
| 16 |
+
def encode(self, text: str) -> list[int]: ...
|
| 17 |
+
def decode(self, ids: list[int]) -> str: ...
|
| 18 |
+
def id_to_piece(self, token_id: int) -> str: ...
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def build_dataset_and_tokenizer(
|
| 22 |
+
cfg: ConveyerConfig,
|
| 23 |
+
infer_only: bool = False,
|
| 24 |
+
) -> tuple[Dataset[dict[str, Any]], Dataset[dict[str, Any]], TokenizerProtocol]:
|
| 25 |
+
if cfg.data.dataset == "synthetic":
|
| 26 |
+
from conveyer.data.synthetic import build_synthetic_datasets
|
| 27 |
+
|
| 28 |
+
return build_synthetic_datasets(cfg)
|
| 29 |
+
elif cfg.data.dataset == "tinystories":
|
| 30 |
+
from conveyer.data.tinystories import build_tinystories_datasets
|
| 31 |
+
|
| 32 |
+
return build_tinystories_datasets(cfg, infer_only=infer_only)
|
| 33 |
+
else:
|
| 34 |
+
raise ValueError(f"Unknown dataset '{cfg.data.dataset}'. Choose 'synthetic' or 'tinystories'.")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def collate_conveyer_batch(batch: list[dict[str, Any]], pad_id: int = 0) -> dict[str, torch.Tensor]:
|
| 38 |
+
max_len = max(item["input_ids"].size(0) for item in batch)
|
| 39 |
+
bsz = len(batch)
|
| 40 |
+
input_ids = torch.full((bsz, max_len), pad_id, dtype=torch.long)
|
| 41 |
+
prompt_lengths = torch.zeros(bsz, dtype=torch.long)
|
| 42 |
+
task_ids = torch.zeros(bsz, dtype=torch.long)
|
| 43 |
+
|
| 44 |
+
for i, item in enumerate(batch):
|
| 45 |
+
seq = item["input_ids"]
|
| 46 |
+
input_ids[i, : seq.size(0)] = seq
|
| 47 |
+
prompt_lengths[i] = int(item.get("prompt_len", 1))
|
| 48 |
+
task_ids[i] = int(item.get("task_id", 0))
|
| 49 |
+
|
| 50 |
+
return {
|
| 51 |
+
"input_ids": input_ids,
|
| 52 |
+
"prompt_lengths": prompt_lengths,
|
| 53 |
+
"task_ids": task_ids,
|
| 54 |
+
}
|
src/conveyer/data/synthetic.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from torch.utils.data import Dataset
|
| 7 |
+
|
| 8 |
+
from conveyer.config import ConveyerConfig
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class SyntheticTokenizer:
|
| 12 |
+
"""
|
| 13 |
+
Compact deterministic symbolic + word-piece vocabulary (128 tokens) for synthetic lookahead planning tasks.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
def __init__(self, vocab_size: int = 128) -> None:
|
| 17 |
+
self.vocab_size = vocab_size
|
| 18 |
+
self.pad_id = 0
|
| 19 |
+
self.bos_id = 1
|
| 20 |
+
self.eos_id = 2
|
| 21 |
+
self.sep_id = 3 # '=>' separator between prompt and output
|
| 22 |
+
|
| 23 |
+
special = ["<PAD>", "<BOS>", "<EOS>", "=>", "|", ":", ",", ";", "(", ")", "[", "]", "->", "+", "=", "?"]
|
| 24 |
+
task_tags = ["[PATH]", "[REV]", "[PLAN]", "[STORY]", "START", "GOAL", "EDGES", "SEQ", "SUM", "END", "STEP", "OK"]
|
| 25 |
+
nodes = [f"N{i}" for i in range(32)]
|
| 26 |
+
numbers = [f"{i}" for i in range(32)]
|
| 27 |
+
words = [
|
| 28 |
+
"the", "little", "robot", "walked", "across", "bridge", "to", "find", "golden", "key",
|
| 29 |
+
"locked", "door", "opened", "quietly", "inside", "shiny", "crystal", "glowed", "bright", "blue",
|
| 30 |
+
"cat", "chased", "mouse", "under", "wooden", "table", "until", "both", "fell", "asleep",
|
| 31 |
+
"wise", "owl", "watched", "stars", "shine", "above", "dark", "forest",
|
| 32 |
+
]
|
| 33 |
+
all_pieces = special + task_tags + nodes + numbers + words
|
| 34 |
+
while len(all_pieces) < vocab_size:
|
| 35 |
+
all_pieces.append(f"TOK_{len(all_pieces)}")
|
| 36 |
+
self.pieces = all_pieces[:vocab_size]
|
| 37 |
+
self.piece_to_id = {p: i for i, p in enumerate(self.pieces)}
|
| 38 |
+
|
| 39 |
+
def id_to_piece(self, token_id: int) -> str:
|
| 40 |
+
if 0 <= token_id < len(self.pieces):
|
| 41 |
+
return self.pieces[token_id]
|
| 42 |
+
return f"<{token_id}>"
|
| 43 |
+
|
| 44 |
+
def encode(self, text: str) -> list[int]:
|
| 45 |
+
tokens: list[int] = [self.bos_id]
|
| 46 |
+
for raw in text.strip().split():
|
| 47 |
+
if raw in self.piece_to_id:
|
| 48 |
+
tokens.append(self.piece_to_id[raw])
|
| 49 |
+
else:
|
| 50 |
+
# Fallback hash into word range
|
| 51 |
+
idx = 64 + (hash(raw) % max(1, self.vocab_size - 64))
|
| 52 |
+
tokens.append(idx)
|
| 53 |
+
return tokens
|
| 54 |
+
|
| 55 |
+
def decode(self, ids: list[int]) -> str:
|
| 56 |
+
out: list[str] = []
|
| 57 |
+
for idx in ids:
|
| 58 |
+
if idx in (self.pad_id, self.bos_id):
|
| 59 |
+
continue
|
| 60 |
+
if idx == self.eos_id:
|
| 61 |
+
break
|
| 62 |
+
out.append(self.id_to_piece(idx))
|
| 63 |
+
return " ".join(out)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _make_path_planning_sample(
|
| 67 |
+
rng: np.random.Generator,
|
| 68 |
+
tok: SyntheticTokenizer,
|
| 69 |
+
seq_len: int,
|
| 70 |
+
) -> tuple[torch.Tensor, int, int]:
|
| 71 |
+
"""
|
| 72 |
+
Task 0 (Lookahead Graph Pathfinding - `[PATH]`):
|
| 73 |
+
A fork graph where START branches into two paths of length 6:
|
| 74 |
+
- One branch leads to a DEAD_END.
|
| 75 |
+
- The other branch leads to the GOAL node.
|
| 76 |
+
The edges are listed in random order, and the target trajectory is:
|
| 77 |
+
`=> N_start N_step1 N_step2 N_step3 N_step4 N_step5 N_goal <EOS> ...`
|
| 78 |
+
Crucially, we also require the model to emit the GOAL-conditioned path in reverse-verified
|
| 79 |
+
or lookahead form where choosing `N_step1` at Position 0 requires chaining backwards from `GOAL`
|
| 80 |
+
across the 6-hop chain. On the 64-slot conveyor belt, `N_goal` at the end of the path is anchored
|
| 81 |
+
while `N_step1 .. N_step5` crystallize simultaneously!
|
| 82 |
+
"""
|
| 83 |
+
node_pool = rng.choice(32, size=12, replace=False)
|
| 84 |
+
start_node = int(node_pool[0])
|
| 85 |
+
good_chain = [int(x) for x in node_pool[1:6]] # 5 intermediate hops
|
| 86 |
+
goal_node = int(node_pool[6])
|
| 87 |
+
bad_chain = [int(x) for x in node_pool[7:12]] # 5 dead-end hops
|
| 88 |
+
|
| 89 |
+
edges: list[tuple[int, int]] = []
|
| 90 |
+
# Branch A (reaches goal_node)
|
| 91 |
+
edges.append((start_node, good_chain[0]))
|
| 92 |
+
for i in range(len(good_chain) - 1):
|
| 93 |
+
edges.append((good_chain[i], good_chain[i + 1]))
|
| 94 |
+
edges.append((good_chain[-1], goal_node))
|
| 95 |
+
|
| 96 |
+
# Branch B (dead-end branch from start_node)
|
| 97 |
+
edges.append((start_node, bad_chain[0]))
|
| 98 |
+
for i in range(len(bad_chain) - 1):
|
| 99 |
+
edges.append((bad_chain[i], bad_chain[i + 1]))
|
| 100 |
+
|
| 101 |
+
# Shuffle edge order so local greedy heuristics fail unless the model traces backward from GOAL
|
| 102 |
+
perm = rng.permutation(len(edges))
|
| 103 |
+
shuffled_edges = [edges[i] for i in perm]
|
| 104 |
+
|
| 105 |
+
prompt_pieces = ["<BOS>", "[PATH]", "START", f"N{start_node}", "GOAL", f"N{goal_node}", "EDGES"]
|
| 106 |
+
for u, v in shuffled_edges:
|
| 107 |
+
prompt_pieces.extend([f"N{u}", "->", f"N{v}", ";"])
|
| 108 |
+
prompt_pieces.append("=>")
|
| 109 |
+
|
| 110 |
+
full_path = [start_node] + good_chain + [goal_node]
|
| 111 |
+
# Emit the path twice separated by '|' and ending with GOAL confirmation so the full 64-slot belt is active
|
| 112 |
+
target_pieces: list[str] = []
|
| 113 |
+
for n in full_path:
|
| 114 |
+
target_pieces.append(f"N{n}")
|
| 115 |
+
target_pieces.append("|")
|
| 116 |
+
for n in reversed(full_path):
|
| 117 |
+
target_pieces.append(f"N{n}")
|
| 118 |
+
target_pieces.extend(["OK", f"N{goal_node}", "<EOS>"])
|
| 119 |
+
|
| 120 |
+
prompt_ids = [tok.piece_to_id[p] for p in prompt_pieces]
|
| 121 |
+
target_ids = [tok.piece_to_id[p] for p in target_pieces]
|
| 122 |
+
full_ids = prompt_ids + target_ids
|
| 123 |
+
|
| 124 |
+
out = torch.full((seq_len,), tok.pad_id, dtype=torch.long)
|
| 125 |
+
L = min(seq_len, len(full_ids))
|
| 126 |
+
out[:L] = torch.tensor(full_ids[:L], dtype=torch.long)
|
| 127 |
+
return out, len(prompt_ids), 0
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _make_reverse_plan_sample(
|
| 131 |
+
rng: np.random.Generator,
|
| 132 |
+
tok: SyntheticTokenizer,
|
| 133 |
+
seq_len: int,
|
| 134 |
+
) -> tuple[torch.Tensor, int, int]:
|
| 135 |
+
"""
|
| 136 |
+
Task 1 (Delayed-Constraint / Reverse Block Planning - `[REV]`):
|
| 137 |
+
The prompt gives a sequence of 16 numbers `a_0 .. a_15`.
|
| 138 |
+
The output must emit pairs `(a_i + a_{15-i}) mod 32` followed by the reversed sequence `a_15 .. a_0`.
|
| 139 |
+
"""
|
| 140 |
+
length = 12
|
| 141 |
+
nums = [int(x) for x in rng.integers(0, 16, size=length)]
|
| 142 |
+
prompt_pieces = ["<BOS>", "[REV]", "SEQ"] + [f"{x}" for x in nums] + ["=>"]
|
| 143 |
+
|
| 144 |
+
sums = [(nums[i] + nums[length - 1 - i]) % 32 for i in range(length)]
|
| 145 |
+
rev = list(reversed(nums))
|
| 146 |
+
target_pieces = (
|
| 147 |
+
["SUM"]
|
| 148 |
+
+ [f"{s}" for s in sums]
|
| 149 |
+
+ ["|"]
|
| 150 |
+
+ [f"{r}" for r in rev]
|
| 151 |
+
+ ["OK", "<EOS>"]
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
prompt_ids = [tok.piece_to_id[p] for p in prompt_pieces]
|
| 155 |
+
target_ids = [tok.piece_to_id[p] for p in target_pieces]
|
| 156 |
+
full_ids = prompt_ids + target_ids
|
| 157 |
+
|
| 158 |
+
out = torch.full((seq_len,), tok.pad_id, dtype=torch.long)
|
| 159 |
+
L = min(seq_len, len(full_ids))
|
| 160 |
+
out[:L] = torch.tensor(full_ids[:L], dtype=torch.long)
|
| 161 |
+
return out, len(prompt_ids), 1
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _make_target_anchored_chain_sample(
|
| 165 |
+
rng: np.random.Generator,
|
| 166 |
+
tok: SyntheticTokenizer,
|
| 167 |
+
seq_len: int,
|
| 168 |
+
) -> tuple[torch.Tensor, int, int]:
|
| 169 |
+
"""
|
| 170 |
+
Task 2 (Target-Anchored Arithmetic / Story Planning - `[PLAN]`):
|
| 171 |
+
Prompt specifies a target final value `END = T` and a sequence of deltas `d_1 .. d_10`.
|
| 172 |
+
The model must choose the unique initial value `x_0` at Position 0 such that after applying
|
| 173 |
+
`x_{k} = (x_{k-1} + d_k) mod 32` for 10 steps, the final token at Position 10 equals `T`!
|
| 174 |
+
Then it emits the full trajectory `x_0, x_1, ..., x_10 = T`.
|
| 175 |
+
"""
|
| 176 |
+
steps = 10
|
| 177 |
+
deltas = [int(x) for x in rng.integers(1, 8, size=steps)]
|
| 178 |
+
target_end = int(rng.integers(0, 32))
|
| 179 |
+
total_delta = sum(deltas) % 32
|
| 180 |
+
x0 = (target_end - total_delta) % 32
|
| 181 |
+
|
| 182 |
+
traj = [x0]
|
| 183 |
+
for d in deltas:
|
| 184 |
+
traj.append((traj[-1] + d) % 32)
|
| 185 |
+
assert traj[-1] == target_end
|
| 186 |
+
|
| 187 |
+
prompt_pieces = (
|
| 188 |
+
["<BOS>", "[PLAN]", "END", f"{target_end}", "STEP"]
|
| 189 |
+
+ [f"+ {d}" for d in deltas]
|
| 190 |
+
# flatten "+ d" into two tokens
|
| 191 |
+
)
|
| 192 |
+
flat_prompt: list[str] = ["<BOS>", "[PLAN]", "END", f"{target_end}", "STEP"]
|
| 193 |
+
for d in deltas:
|
| 194 |
+
flat_prompt.extend(["+", f"{d}"])
|
| 195 |
+
flat_prompt.append("=>")
|
| 196 |
+
|
| 197 |
+
target_pieces = [f"{val}" for val in traj] + ["=", f"{target_end}", "OK", "<EOS>"]
|
| 198 |
+
|
| 199 |
+
prompt_ids = [tok.piece_to_id[p] for p in flat_prompt]
|
| 200 |
+
target_ids = [tok.piece_to_id[p] for p in target_pieces]
|
| 201 |
+
full_ids = prompt_ids + target_ids
|
| 202 |
+
|
| 203 |
+
out = torch.full((seq_len,), tok.pad_id, dtype=torch.long)
|
| 204 |
+
L = min(seq_len, len(full_ids))
|
| 205 |
+
out[:L] = torch.tensor(full_ids[:L], dtype=torch.long)
|
| 206 |
+
return out, len(prompt_ids), 2
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class SyntheticLookaheadDataset(Dataset[dict[str, Any]]):
|
| 210 |
+
def __init__(
|
| 211 |
+
self,
|
| 212 |
+
num_samples: int,
|
| 213 |
+
seq_len: int,
|
| 214 |
+
task: str = "all",
|
| 215 |
+
vocab_size: int = 128,
|
| 216 |
+
seed: int = 42,
|
| 217 |
+
) -> None:
|
| 218 |
+
super().__init__()
|
| 219 |
+
self.num_samples = num_samples
|
| 220 |
+
self.seq_len = seq_len
|
| 221 |
+
self.task = task
|
| 222 |
+
self.tokenizer = SyntheticTokenizer(vocab_size=vocab_size)
|
| 223 |
+
|
| 224 |
+
rng = np.random.default_rng(seed)
|
| 225 |
+
self.samples: list[dict[str, Any]] = []
|
| 226 |
+
|
| 227 |
+
for i in range(num_samples):
|
| 228 |
+
if task == "path":
|
| 229 |
+
t_choice = 0
|
| 230 |
+
elif task == "reverse":
|
| 231 |
+
t_choice = 1
|
| 232 |
+
elif task == "constraint":
|
| 233 |
+
t_choice = 2
|
| 234 |
+
else:
|
| 235 |
+
t_choice = i % 3
|
| 236 |
+
|
| 237 |
+
if t_choice == 0:
|
| 238 |
+
seq, plen, tid = _make_path_planning_sample(rng, self.tokenizer, seq_len)
|
| 239 |
+
elif t_choice == 1:
|
| 240 |
+
seq, plen, tid = _make_reverse_plan_sample(rng, self.tokenizer, seq_len)
|
| 241 |
+
else:
|
| 242 |
+
seq, plen, tid = _make_target_anchored_chain_sample(rng, self.tokenizer, seq_len)
|
| 243 |
+
|
| 244 |
+
self.samples.append(
|
| 245 |
+
{
|
| 246 |
+
"input_ids": seq,
|
| 247 |
+
"prompt_len": plen,
|
| 248 |
+
"task_id": tid,
|
| 249 |
+
}
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
def __len__(self) -> int:
|
| 253 |
+
return len(self.samples)
|
| 254 |
+
|
| 255 |
+
def __getitem__(self, idx: int) -> dict[str, Any]:
|
| 256 |
+
return self.samples[idx]
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def build_synthetic_datasets(
|
| 260 |
+
cfg: ConveyerConfig,
|
| 261 |
+
) -> tuple[SyntheticLookaheadDataset, SyntheticLookaheadDataset, SyntheticTokenizer]:
|
| 262 |
+
train_ds = SyntheticLookaheadDataset(
|
| 263 |
+
num_samples=cfg.data.num_train,
|
| 264 |
+
seq_len=cfg.data.seq_len,
|
| 265 |
+
task=cfg.data.task,
|
| 266 |
+
vocab_size=cfg.data.vocab_size,
|
| 267 |
+
seed=cfg.train.seed,
|
| 268 |
+
)
|
| 269 |
+
val_ds = SyntheticLookaheadDataset(
|
| 270 |
+
num_samples=cfg.data.num_val,
|
| 271 |
+
seq_len=cfg.data.seq_len,
|
| 272 |
+
task=cfg.data.task,
|
| 273 |
+
vocab_size=cfg.data.vocab_size,
|
| 274 |
+
seed=cfg.train.seed + 10000,
|
| 275 |
+
)
|
| 276 |
+
return train_ds, val_ds, train_ds.tokenizer
|
src/conveyer/data/tinystories.py
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from collections import Counter
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import time
|
| 8 |
+
from typing import Any, Iterator, Optional
|
| 9 |
+
import urllib.request
|
| 10 |
+
import numpy as np
|
| 11 |
+
import tiktoken
|
| 12 |
+
import torch
|
| 13 |
+
from torch.utils.data import Dataset
|
| 14 |
+
|
| 15 |
+
from conveyer.config import ConveyerConfig
|
| 16 |
+
|
| 17 |
+
TINYSTORIES_VALID_URL = (
|
| 18 |
+
"https://huggingface.co/datasets/roneneldan/TinyStories/resolve/main/TinyStoriesV2-GPT4-valid.txt"
|
| 19 |
+
)
|
| 20 |
+
TINYSTORIES_TRAIN_URL = (
|
| 21 |
+
"https://huggingface.co/datasets/roneneldan/TinyStories/resolve/main/TinyStoriesV2-GPT4-train.txt"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class TiktokenCompactTokenizer:
|
| 26 |
+
"""
|
| 27 |
+
Backed by OpenAI `tiktoken` (`gpt2` BPE encoding), mapped into a compact vocabulary
|
| 28 |
+
of size `vocab_size` (e.g. 4096) covering the most frequent real-TinyStories BPE subwords
|
| 29 |
+
plus all 256 single-byte BPE tokens so ANY arbitrary English string encodes with ZERO `<UNK>`.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(
|
| 33 |
+
self,
|
| 34 |
+
gpt2_to_compact: dict[int, int],
|
| 35 |
+
compact_to_gpt2: list[int],
|
| 36 |
+
source: str = "real_tinystories_v2",
|
| 37 |
+
) -> None:
|
| 38 |
+
self.enc = tiktoken.get_encoding("gpt2")
|
| 39 |
+
self.gpt2_to_compact = gpt2_to_compact
|
| 40 |
+
self.compact_to_gpt2 = compact_to_gpt2
|
| 41 |
+
self.source = source
|
| 42 |
+
self.vocab_size = len(compact_to_gpt2)
|
| 43 |
+
self.pad_id = 0
|
| 44 |
+
self.bos_id = 1
|
| 45 |
+
self.eos_id = 2
|
| 46 |
+
self.unk_id = 3
|
| 47 |
+
|
| 48 |
+
# Prebuild byte-level fallback table so any rare BPE token decomposes into exact byte BPE tokens
|
| 49 |
+
self.byte_to_compact: dict[int, int] = {}
|
| 50 |
+
for b in range(256):
|
| 51 |
+
gid = self.enc.encode_single_token(bytes([b]))
|
| 52 |
+
if gid in self.gpt2_to_compact:
|
| 53 |
+
self.byte_to_compact[b] = self.gpt2_to_compact[gid]
|
| 54 |
+
|
| 55 |
+
# Prebuild fast array lookup for GPT-2 token IDs (0..50256) -> compact ID (-1 if rare)
|
| 56 |
+
self._lut = np.full(self.enc.n_vocab, -1, dtype=np.int32)
|
| 57 |
+
for gid, cid in self.gpt2_to_compact.items():
|
| 58 |
+
if 0 <= gid < self.enc.n_vocab:
|
| 59 |
+
self._lut[gid] = cid
|
| 60 |
+
|
| 61 |
+
@classmethod
|
| 62 |
+
def build_from_texts(cls, texts: list[str], vocab_size: int = 4096) -> "TiktokenCompactTokenizer":
|
| 63 |
+
enc = tiktoken.get_encoding("gpt2")
|
| 64 |
+
# Special tokens occupy 0..3: <PAD>=0, <BOS>=1, <EOS>=2, <UNK>=3
|
| 65 |
+
compact_to_gpt2: list[int] = [-1, -2, -3, -4]
|
| 66 |
+
seen_gpt2: set[int] = set()
|
| 67 |
+
|
| 68 |
+
# 1. Always include all 256 single-byte BPE tokens so 100% of UTF-8 strings are representable
|
| 69 |
+
for b in range(256):
|
| 70 |
+
gid = enc.encode_single_token(bytes([b]))
|
| 71 |
+
if gid not in seen_gpt2 and len(compact_to_gpt2) < vocab_size:
|
| 72 |
+
seen_gpt2.add(gid)
|
| 73 |
+
compact_to_gpt2.append(gid)
|
| 74 |
+
|
| 75 |
+
# 2. Count BPE token frequencies across real TinyStories
|
| 76 |
+
counts: Counter[int] = Counter()
|
| 77 |
+
sample = texts[:50000]
|
| 78 |
+
for batch_list in enc.encode_ordinary_batch(sample, num_threads=8):
|
| 79 |
+
counts.update(batch_list)
|
| 80 |
+
|
| 81 |
+
for gid, _ in counts.most_common():
|
| 82 |
+
if len(compact_to_gpt2) >= vocab_size:
|
| 83 |
+
break
|
| 84 |
+
if gid not in seen_gpt2:
|
| 85 |
+
seen_gpt2.add(gid)
|
| 86 |
+
compact_to_gpt2.append(gid)
|
| 87 |
+
|
| 88 |
+
# Pad up to vocab_size with sequential gpt2 token IDs if needed
|
| 89 |
+
next_gid = 0
|
| 90 |
+
while len(compact_to_gpt2) < vocab_size:
|
| 91 |
+
if next_gid not in seen_gpt2:
|
| 92 |
+
seen_gpt2.add(next_gid)
|
| 93 |
+
compact_to_gpt2.append(next_gid)
|
| 94 |
+
next_gid += 1
|
| 95 |
+
|
| 96 |
+
gpt2_to_compact = {gid: cid for cid, gid in enumerate(compact_to_gpt2) if gid >= 0}
|
| 97 |
+
return cls(
|
| 98 |
+
gpt2_to_compact=gpt2_to_compact,
|
| 99 |
+
compact_to_gpt2=compact_to_gpt2,
|
| 100 |
+
source="real_tinystories_v2",
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
def save(self, path: str | Path) -> None:
|
| 104 |
+
p = Path(path)
|
| 105 |
+
p.parent.mkdir(parents=True, exist_ok=True)
|
| 106 |
+
payload = {
|
| 107 |
+
"source": self.source,
|
| 108 |
+
"compact_to_gpt2": self.compact_to_gpt2,
|
| 109 |
+
}
|
| 110 |
+
p.write_text(json.dumps(payload), encoding="utf-8")
|
| 111 |
+
|
| 112 |
+
@classmethod
|
| 113 |
+
def load(cls, path: str | Path) -> "TiktokenCompactTokenizer":
|
| 114 |
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
| 115 |
+
compact_to_gpt2: list[int] = data["compact_to_gpt2"]
|
| 116 |
+
source: str = data.get("source", "legacy")
|
| 117 |
+
gpt2_to_compact = {gid: cid for cid, gid in enumerate(compact_to_gpt2) if gid >= 0}
|
| 118 |
+
return cls(gpt2_to_compact=gpt2_to_compact, compact_to_gpt2=compact_to_gpt2, source=source)
|
| 119 |
+
|
| 120 |
+
def id_to_piece(self, token_id: int) -> str:
|
| 121 |
+
if token_id == self.pad_id:
|
| 122 |
+
return "<PAD>"
|
| 123 |
+
if token_id == self.bos_id:
|
| 124 |
+
return "<BOS>"
|
| 125 |
+
if token_id == self.eos_id:
|
| 126 |
+
return "<EOS>"
|
| 127 |
+
if token_id == self.unk_id or not (0 <= token_id < len(self.compact_to_gpt2)):
|
| 128 |
+
return "<UNK>"
|
| 129 |
+
gid = self.compact_to_gpt2[token_id]
|
| 130 |
+
if gid < 0:
|
| 131 |
+
return "<SPEC>"
|
| 132 |
+
raw = self.enc.decode_single_token_bytes(gid).decode("utf-8", errors="replace")
|
| 133 |
+
return raw
|
| 134 |
+
|
| 135 |
+
def _map_gids_to_compact(self, gids: list[int], max_tokens: Optional[int] = None) -> list[int]:
|
| 136 |
+
ids: list[int] = [self.bos_id]
|
| 137 |
+
gpt2_map = self.gpt2_to_compact
|
| 138 |
+
byte_map = self.byte_to_compact
|
| 139 |
+
dec_single = self.enc.decode_single_token_bytes
|
| 140 |
+
unk = self.unk_id
|
| 141 |
+
for gid in gids:
|
| 142 |
+
cid = gpt2_map.get(gid)
|
| 143 |
+
if cid is not None:
|
| 144 |
+
ids.append(cid)
|
| 145 |
+
else:
|
| 146 |
+
for b in dec_single(gid):
|
| 147 |
+
ids.append(byte_map.get(b, unk))
|
| 148 |
+
if max_tokens is not None and len(ids) >= max_tokens:
|
| 149 |
+
break
|
| 150 |
+
return ids
|
| 151 |
+
|
| 152 |
+
def encode(self, text: str) -> list[int]:
|
| 153 |
+
gids = self.enc.encode_ordinary(text)
|
| 154 |
+
return self._map_gids_to_compact(gids)
|
| 155 |
+
|
| 156 |
+
def encode_batch_into_matrix(
|
| 157 |
+
self,
|
| 158 |
+
texts: list[str],
|
| 159 |
+
out_matrix: np.ndarray,
|
| 160 |
+
out_lens: np.ndarray,
|
| 161 |
+
start_row: int,
|
| 162 |
+
seq_len: int,
|
| 163 |
+
) -> int:
|
| 164 |
+
"""
|
| 165 |
+
Multi-threaded Rust BPE batch encoding into `out_matrix[start_row : start_row + written]` (uint16).
|
| 166 |
+
Truncates input text to ~1400 chars before BPE since we only need `seq_len` (e.g. 160) tokens.
|
| 167 |
+
"""
|
| 168 |
+
char_cap = max(seq_len * 8, 1024)
|
| 169 |
+
clipped = [t[:char_cap] if len(t) > char_cap else t for t in texts]
|
| 170 |
+
batch_gids = self.enc.encode_ordinary_batch(clipped, num_threads=8)
|
| 171 |
+
|
| 172 |
+
row = start_row
|
| 173 |
+
max_rows = out_matrix.shape[0]
|
| 174 |
+
eos = self.eos_id
|
| 175 |
+
pad = self.pad_id
|
| 176 |
+
max_body = seq_len - 1
|
| 177 |
+
|
| 178 |
+
for gids in batch_gids:
|
| 179 |
+
if row >= max_rows:
|
| 180 |
+
break
|
| 181 |
+
ids = self._map_gids_to_compact(gids, max_tokens=max_body)
|
| 182 |
+
if len(ids) < 16:
|
| 183 |
+
continue
|
| 184 |
+
if len(ids) < seq_len:
|
| 185 |
+
ids.append(eos)
|
| 186 |
+
L = min(seq_len, len(ids))
|
| 187 |
+
out_matrix[row, :L] = ids[:L]
|
| 188 |
+
if L < seq_len:
|
| 189 |
+
out_matrix[row, L:] = pad
|
| 190 |
+
out_lens[row] = L
|
| 191 |
+
row += 1
|
| 192 |
+
return row - start_row
|
| 193 |
+
|
| 194 |
+
def decode(self, ids: list[int]) -> str:
|
| 195 |
+
gids: list[int] = []
|
| 196 |
+
for cid in ids:
|
| 197 |
+
if cid in (self.pad_id, self.bos_id, self.unk_id):
|
| 198 |
+
continue
|
| 199 |
+
if cid == self.eos_id:
|
| 200 |
+
break
|
| 201 |
+
if 0 <= cid < len(self.compact_to_gpt2):
|
| 202 |
+
gid = self.compact_to_gpt2[cid]
|
| 203 |
+
if gid >= 0:
|
| 204 |
+
gids.append(gid)
|
| 205 |
+
return self.enc.decode(gids)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _download_with_resume(url: str, dest: Path, min_expected_bytes: int = 1_000_000) -> Path:
|
| 209 |
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
| 210 |
+
existing = dest.stat().st_size if dest.exists() else 0
|
| 211 |
+
total_size = 0
|
| 212 |
+
try:
|
| 213 |
+
req_head = urllib.request.Request(url, method="HEAD", headers={"User-Agent": "Mozilla/5.0"})
|
| 214 |
+
with urllib.request.urlopen(req_head, timeout=30) as r:
|
| 215 |
+
total_size = int(r.headers.get("Content-Length", 0))
|
| 216 |
+
except Exception:
|
| 217 |
+
total_size = 0
|
| 218 |
+
|
| 219 |
+
if total_size > 0 and existing >= total_size:
|
| 220 |
+
return dest
|
| 221 |
+
if total_size == 0 and existing >= min_expected_bytes:
|
| 222 |
+
return dest
|
| 223 |
+
|
| 224 |
+
headers = {"User-Agent": "Mozilla/5.0"}
|
| 225 |
+
mode = "wb"
|
| 226 |
+
if existing > 0 and total_size > existing:
|
| 227 |
+
headers["Range"] = f"bytes={existing}-"
|
| 228 |
+
mode = "ab"
|
| 229 |
+
else:
|
| 230 |
+
existing = 0
|
| 231 |
+
|
| 232 |
+
print(
|
| 233 |
+
f"[TinyStories] Downloading {dest.name} "
|
| 234 |
+
f"({existing / (1024 * 1024):.1f} / {total_size / (1024 * 1024):.1f} MB)...",
|
| 235 |
+
flush=True,
|
| 236 |
+
)
|
| 237 |
+
req = urllib.request.Request(url, headers=headers)
|
| 238 |
+
t0 = time.time()
|
| 239 |
+
last_log = t0
|
| 240 |
+
downloaded = existing
|
| 241 |
+
with urllib.request.urlopen(req, timeout=120) as r, open(dest, mode) as f:
|
| 242 |
+
while True:
|
| 243 |
+
chunk = r.read(4 * 1024 * 1024)
|
| 244 |
+
if not chunk:
|
| 245 |
+
break
|
| 246 |
+
f.write(chunk)
|
| 247 |
+
downloaded += len(chunk)
|
| 248 |
+
now = time.time()
|
| 249 |
+
if now - last_log >= 5.0:
|
| 250 |
+
dt = now - t0
|
| 251 |
+
speed = (downloaded - existing) / (1024 * 1024) / max(dt, 1e-3)
|
| 252 |
+
pct = 100.0 * downloaded / max(total_size, 1)
|
| 253 |
+
print(
|
| 254 |
+
f" [{dest.name}] {downloaded / (1024 * 1024):.1f} / "
|
| 255 |
+
f"{total_size / (1024 * 1024):.1f} MB ({pct:.1f}%) at {speed:.1f} MB/s",
|
| 256 |
+
flush=True,
|
| 257 |
+
)
|
| 258 |
+
last_log = now
|
| 259 |
+
return dest
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def iter_clean_real_stories(txt_path: Path, max_stories: Optional[int] = None) -> Iterator[str]:
|
| 263 |
+
"""
|
| 264 |
+
Streams clean real TinyStories from a `<|endoftext|>`-separated raw text file without
|
| 265 |
+
loading the entire 2.12 GB string list into memory at once.
|
| 266 |
+
Filters out byte-0 mid-sentence fragments and any synthetic templates.
|
| 267 |
+
"""
|
| 268 |
+
yielded = 0
|
| 269 |
+
buf = ""
|
| 270 |
+
with open(txt_path, "r", encoding="utf-8", errors="ignore") as f:
|
| 271 |
+
while True:
|
| 272 |
+
chunk = f.read(16 * 1024 * 1024)
|
| 273 |
+
if not chunk:
|
| 274 |
+
break
|
| 275 |
+
buf += chunk
|
| 276 |
+
parts = buf.split("<|endoftext|>")
|
| 277 |
+
buf = parts[-1]
|
| 278 |
+
for raw in parts[:-1]:
|
| 279 |
+
s = " ".join(raw.strip().split())
|
| 280 |
+
if len(s) < 80:
|
| 281 |
+
continue
|
| 282 |
+
if not (s[0].isupper() or s[0] in "\"'"):
|
| 283 |
+
continue
|
| 284 |
+
if "walked into the " in s and "Together, they" in s:
|
| 285 |
+
continue
|
| 286 |
+
yield s
|
| 287 |
+
yielded += 1
|
| 288 |
+
if max_stories is not None and yielded >= max_stories:
|
| 289 |
+
return
|
| 290 |
+
s = " ".join(buf.strip().split())
|
| 291 |
+
if len(s) >= 80 and (s[0].isupper() or s[0] in "\"'"):
|
| 292 |
+
if not ("walked into the " in s and "Together, they" in s):
|
| 293 |
+
if max_stories is None or yielded < max_stories:
|
| 294 |
+
yield s
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def ensure_raw_tinystories_files(data_dir: str | Path, need_full_train: bool = True) -> tuple[Path, Path]:
|
| 298 |
+
root = Path(data_dir)
|
| 299 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 300 |
+
val_path = _download_with_resume(TINYSTORIES_VALID_URL, root / "TinyStoriesV2-GPT4-valid.txt", min_expected_bytes=20_000_000)
|
| 301 |
+
train_path = root / "TinyStoriesV2-GPT4-train.txt"
|
| 302 |
+
if need_full_train or not train_path.exists():
|
| 303 |
+
_download_with_resume(TINYSTORIES_TRAIN_URL, train_path, min_expected_bytes=2_000_000_000)
|
| 304 |
+
return train_path, val_path
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def load_or_prepare_tinystories(data_dir: str | Path, max_stories: int = 42000) -> list[str]:
|
| 308 |
+
"""
|
| 309 |
+
Returns up to `max_stories` 100% REAL TinyStories strings (zero templates).
|
| 310 |
+
"""
|
| 311 |
+
root = Path(data_dir)
|
| 312 |
+
train_path, val_path = ensure_raw_tinystories_files(root, need_full_train=(max_stories > 25000))
|
| 313 |
+
source_path = train_path if train_path.exists() and train_path.stat().st_size > 20_000_000 else val_path
|
| 314 |
+
stories = list(iter_clean_real_stories(source_path, max_stories=max_stories))
|
| 315 |
+
return stories
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def _build_or_load_memmap_split(
|
| 319 |
+
split_name: str,
|
| 320 |
+
raw_txt_path: Path,
|
| 321 |
+
tokenizer: TiktokenCompactTokenizer,
|
| 322 |
+
data_dir: Path,
|
| 323 |
+
target_stories: int,
|
| 324 |
+
seq_len: int,
|
| 325 |
+
) -> tuple[np.memmap, np.ndarray]:
|
| 326 |
+
"""
|
| 327 |
+
Pre-tokenizes `target_stories` real TinyStories from `raw_txt_path` into a compact disk-backed
|
| 328 |
+
`uint16` numpy memmap of shape `(N, seq_len)` + `uint16` lengths array `(N,)`.
|
| 329 |
+
Loading an existing memmap takes < 10ms and uses near-zero RAM.
|
| 330 |
+
"""
|
| 331 |
+
prefix = data_dir / f"real_{split_name}_v5_n{target_stories}_s{seq_len}_v{tokenizer.vocab_size}"
|
| 332 |
+
tokens_bin = Path(str(prefix) + ".tokens.bin")
|
| 333 |
+
lens_bin = Path(str(prefix) + ".lens.bin")
|
| 334 |
+
meta_json = Path(str(prefix) + ".meta.json")
|
| 335 |
+
|
| 336 |
+
if tokens_bin.exists() and lens_bin.exists() and meta_json.exists():
|
| 337 |
+
meta = json.loads(meta_json.read_text(encoding="utf-8"))
|
| 338 |
+
n_rows = int(meta["num_rows"])
|
| 339 |
+
if n_rows > 0 and tokens_bin.stat().st_size == n_rows * seq_len * 2:
|
| 340 |
+
mm = np.memmap(tokens_bin, dtype=np.uint16, mode="r", shape=(n_rows, seq_len))
|
| 341 |
+
lens = np.fromfile(lens_bin, dtype=np.uint16)
|
| 342 |
+
return mm, lens
|
| 343 |
+
|
| 344 |
+
print(
|
| 345 |
+
f"[TinyStories] Building binary token cache for '{split_name}' "
|
| 346 |
+
f"(target={target_stories:,} stories, seq_len={seq_len})...",
|
| 347 |
+
flush=True,
|
| 348 |
+
)
|
| 349 |
+
tmp_tokens = Path(str(tokens_bin) + ".tmp")
|
| 350 |
+
tmp_lens = Path(str(lens_bin) + ".tmp")
|
| 351 |
+
|
| 352 |
+
mm_write = np.memmap(tmp_tokens, dtype=np.uint16, mode="w+", shape=(target_stories, seq_len))
|
| 353 |
+
lens_arr = np.zeros(target_stories, dtype=np.uint16)
|
| 354 |
+
|
| 355 |
+
t0 = time.time()
|
| 356 |
+
last_log = t0
|
| 357 |
+
row = 0
|
| 358 |
+
batch_texts: list[str] = []
|
| 359 |
+
batch_chunk_size = 50000
|
| 360 |
+
|
| 361 |
+
for story in iter_clean_real_stories(raw_txt_path, max_stories=int(target_stories * 1.05) + 2000):
|
| 362 |
+
batch_texts.append(story)
|
| 363 |
+
if len(batch_texts) >= batch_chunk_size:
|
| 364 |
+
added = tokenizer.encode_batch_into_matrix(batch_texts, mm_write, lens_arr, row, seq_len)
|
| 365 |
+
row += added
|
| 366 |
+
batch_texts.clear()
|
| 367 |
+
now = time.time()
|
| 368 |
+
if now - last_log >= 4.0:
|
| 369 |
+
dt = now - t0
|
| 370 |
+
rate = row / max(dt, 1e-3)
|
| 371 |
+
print(
|
| 372 |
+
f" [{split_name}] Tokenized {row:,} / {target_stories:,} stories "
|
| 373 |
+
f"({100.0 * row / max(target_stories, 1):.1f}%) at {rate:,.0f} stories/s",
|
| 374 |
+
flush=True,
|
| 375 |
+
)
|
| 376 |
+
last_log = now
|
| 377 |
+
if row >= target_stories:
|
| 378 |
+
break
|
| 379 |
+
|
| 380 |
+
if batch_texts and row < target_stories:
|
| 381 |
+
added = tokenizer.encode_batch_into_matrix(batch_texts, mm_write, lens_arr, row, seq_len)
|
| 382 |
+
row += added
|
| 383 |
+
batch_texts.clear()
|
| 384 |
+
|
| 385 |
+
mm_write.flush()
|
| 386 |
+
del mm_write
|
| 387 |
+
|
| 388 |
+
# If actual valid stories in file < target_stories, truncate file to exact `row * seq_len * 2` bytes
|
| 389 |
+
if row < target_stories:
|
| 390 |
+
with open(tmp_tokens, "r+b") as f:
|
| 391 |
+
f.truncate(row * seq_len * 2)
|
| 392 |
+
lens_arr = lens_arr[:row]
|
| 393 |
+
|
| 394 |
+
lens_arr.tofile(tmp_lens)
|
| 395 |
+
os.replace(tmp_tokens, tokens_bin)
|
| 396 |
+
os.replace(tmp_lens, lens_bin)
|
| 397 |
+
meta_json.write_text(
|
| 398 |
+
json.dumps({"num_rows": int(row), "seq_len": int(seq_len), "vocab_size": int(tokenizer.vocab_size)}),
|
| 399 |
+
encoding="utf-8",
|
| 400 |
+
)
|
| 401 |
+
dt = time.time() - t0
|
| 402 |
+
print(f"[TinyStories] Saved '{split_name}' binary cache: {row:,} real stories in {dt:.1f}s", flush=True)
|
| 403 |
+
|
| 404 |
+
mm = np.memmap(tokens_bin, dtype=np.uint16, mode="r", shape=(row, seq_len))
|
| 405 |
+
return mm, lens_arr
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
class TinyStoriesDataset(Dataset[dict[str, Any]]):
|
| 409 |
+
def __init__(
|
| 410 |
+
self,
|
| 411 |
+
stories: Optional[list[str]] = None,
|
| 412 |
+
tokenizer: Optional[TiktokenCompactTokenizer] = None,
|
| 413 |
+
seq_len: int = 160,
|
| 414 |
+
prompt_len: int = 28,
|
| 415 |
+
randomize_prompt_len: bool = False,
|
| 416 |
+
token_matrix: Optional[np.ndarray] = None,
|
| 417 |
+
valid_lens: Optional[np.ndarray] = None,
|
| 418 |
+
) -> None:
|
| 419 |
+
super().__init__()
|
| 420 |
+
self.tokenizer = tokenizer
|
| 421 |
+
self.seq_len = seq_len
|
| 422 |
+
self.prompt_len = prompt_len
|
| 423 |
+
self.randomize_prompt_len = randomize_prompt_len
|
| 424 |
+
|
| 425 |
+
if token_matrix is not None and valid_lens is not None:
|
| 426 |
+
self.token_matrix = token_matrix
|
| 427 |
+
self.valid_lens = valid_lens
|
| 428 |
+
else:
|
| 429 |
+
assert stories is not None and tokenizer is not None
|
| 430 |
+
n_cap = len(stories)
|
| 431 |
+
mat = np.full((n_cap, seq_len), tokenizer.pad_id, dtype=np.uint16)
|
| 432 |
+
lens = np.zeros(n_cap, dtype=np.uint16)
|
| 433 |
+
written = tokenizer.encode_batch_into_matrix(stories, mat, lens, 0, seq_len)
|
| 434 |
+
self.token_matrix = mat[:written]
|
| 435 |
+
self.valid_lens = lens[:written]
|
| 436 |
+
|
| 437 |
+
def __len__(self) -> int:
|
| 438 |
+
return int(self.token_matrix.shape[0])
|
| 439 |
+
|
| 440 |
+
def __getitem__(self, idx: int) -> dict[str, Any]:
|
| 441 |
+
row = torch.from_numpy(np.asarray(self.token_matrix[idx], dtype=np.int64))
|
| 442 |
+
L = int(self.valid_lens[idx])
|
| 443 |
+
if not self.randomize_prompt_len:
|
| 444 |
+
p_len = max(4, min(self.prompt_len, L - 8))
|
| 445 |
+
else:
|
| 446 |
+
low = 6
|
| 447 |
+
high = max(low + 1, min(64, L - 8))
|
| 448 |
+
p_len = int(torch.randint(low, high, (1,)).item())
|
| 449 |
+
return {
|
| 450 |
+
"input_ids": row,
|
| 451 |
+
"prompt_len": p_len,
|
| 452 |
+
"task_id": 0,
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def build_tinystories_datasets(
|
| 457 |
+
cfg: ConveyerConfig,
|
| 458 |
+
infer_only: bool = False,
|
| 459 |
+
) -> tuple[TinyStoriesDataset, TinyStoriesDataset, TiktokenCompactTokenizer]:
|
| 460 |
+
data_dir = Path(cfg.data.data_dir)
|
| 461 |
+
data_dir.mkdir(parents=True, exist_ok=True)
|
| 462 |
+
tok_path = data_dir / f"tiktoken_compact_{cfg.data.vocab_size}.json"
|
| 463 |
+
val_sample_path = data_dir / "val_sample_256.txt"
|
| 464 |
+
|
| 465 |
+
if infer_only and tok_path.exists():
|
| 466 |
+
tokenizer = TiktokenCompactTokenizer.load(tok_path)
|
| 467 |
+
cfg.data.vocab_size = tokenizer.vocab_size
|
| 468 |
+
cfg.model.vocab_size = tokenizer.vocab_size
|
| 469 |
+
if val_sample_path.exists():
|
| 470 |
+
sample_lines = [l.strip() for l in val_sample_path.read_text(encoding="utf-8").splitlines() if l.strip()]
|
| 471 |
+
else:
|
| 472 |
+
_, val_txt = ensure_raw_tinystories_files(data_dir, need_full_train=False)
|
| 473 |
+
sample_lines = list(iter_clean_real_stories(val_txt, max_stories=256))
|
| 474 |
+
val_ds = TinyStoriesDataset(
|
| 475 |
+
stories=sample_lines,
|
| 476 |
+
tokenizer=tokenizer,
|
| 477 |
+
seq_len=cfg.data.seq_len,
|
| 478 |
+
prompt_len=28,
|
| 479 |
+
randomize_prompt_len=False,
|
| 480 |
+
)
|
| 481 |
+
return val_ds, val_ds, tokenizer
|
| 482 |
+
|
| 483 |
+
train_txt, val_txt = ensure_raw_tinystories_files(data_dir, need_full_train=True)
|
| 484 |
+
|
| 485 |
+
tokenizer_opt: Optional[TiktokenCompactTokenizer] = None
|
| 486 |
+
if tok_path.exists():
|
| 487 |
+
loaded = TiktokenCompactTokenizer.load(tok_path)
|
| 488 |
+
if loaded.source == "real_tinystories_v2":
|
| 489 |
+
tokenizer_opt = loaded
|
| 490 |
+
|
| 491 |
+
if tokenizer_opt is None:
|
| 492 |
+
print("[TinyStories] Building compact 4096 BPE vocabulary from 50,000 real stories...", flush=True)
|
| 493 |
+
sample_stories = list(iter_clean_real_stories(train_txt, max_stories=50000))
|
| 494 |
+
tokenizer_opt = TiktokenCompactTokenizer.build_from_texts(sample_stories, vocab_size=cfg.data.vocab_size)
|
| 495 |
+
tokenizer_opt.save(tok_path)
|
| 496 |
+
|
| 497 |
+
tokenizer = tokenizer_opt
|
| 498 |
+
cfg.data.vocab_size = tokenizer.vocab_size
|
| 499 |
+
cfg.model.vocab_size = tokenizer.vocab_size
|
| 500 |
+
|
| 501 |
+
train_mm, train_lens = _build_or_load_memmap_split(
|
| 502 |
+
split_name="train",
|
| 503 |
+
raw_txt_path=train_txt,
|
| 504 |
+
tokenizer=tokenizer,
|
| 505 |
+
data_dir=data_dir,
|
| 506 |
+
target_stories=cfg.data.num_train,
|
| 507 |
+
seq_len=cfg.data.seq_len,
|
| 508 |
+
)
|
| 509 |
+
val_mm, val_lens = _build_or_load_memmap_split(
|
| 510 |
+
split_name="val",
|
| 511 |
+
raw_txt_path=val_txt,
|
| 512 |
+
tokenizer=tokenizer,
|
| 513 |
+
data_dir=data_dir,
|
| 514 |
+
target_stories=cfg.data.num_val,
|
| 515 |
+
seq_len=cfg.data.seq_len,
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
train_ds = TinyStoriesDataset(
|
| 519 |
+
tokenizer=tokenizer,
|
| 520 |
+
seq_len=cfg.data.seq_len,
|
| 521 |
+
randomize_prompt_len=True,
|
| 522 |
+
token_matrix=train_mm,
|
| 523 |
+
valid_lens=train_lens,
|
| 524 |
+
)
|
| 525 |
+
val_ds = TinyStoriesDataset(
|
| 526 |
+
tokenizer=tokenizer,
|
| 527 |
+
seq_len=cfg.data.seq_len,
|
| 528 |
+
prompt_len=28,
|
| 529 |
+
randomize_prompt_len=False,
|
| 530 |
+
token_matrix=val_mm,
|
| 531 |
+
valid_lens=val_lens,
|
| 532 |
+
)
|
| 533 |
+
return train_ds, val_ds, tokenizer
|
src/conveyer/eval.py
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from collections import Counter
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
from torch.utils.data import DataLoader
|
| 12 |
+
|
| 13 |
+
from conveyer.checkpoint import load_checkpoint
|
| 14 |
+
from conveyer.config import ConveyerConfig
|
| 15 |
+
from conveyer.data import build_dataset_and_tokenizer, collate_conveyer_batch
|
| 16 |
+
from conveyer.infer import generate_conveyer
|
| 17 |
+
from conveyer.losses import compute_unrolled_conveyer_loss
|
| 18 |
+
from conveyer.model import ConveyerDiffusionLM
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# ---------------------------------------------------------------------------
|
| 22 |
+
# Benchmark 3: Zero-Shot Commonsense / Story-Coherence Multiple Choice (2-way)
|
| 23 |
+
# ---------------------------------------------------------------------------
|
| 24 |
+
# Model scores log P(option | context) on unseen story pairs testing:
|
| 25 |
+
# - Physical commonsense (heavy vs light, hot vs cold, broken vs whole)
|
| 26 |
+
# - Emotional / social cause-and-effect (lost toy -> sad, gift -> happy)
|
| 27 |
+
# - Narrative entity & object permanence (tracking who holds what, where they are)
|
| 28 |
+
COMMONSENSE_CLOZE_PAIRS: list[tuple[str, str, str, str]] = [
|
| 29 |
+
(
|
| 30 |
+
"emotion_cause",
|
| 31 |
+
"Once upon a time, Tom lost his favorite red ball in the dark woods. Because he lost his ball, Tom felt very",
|
| 32 |
+
" sad and started to cry.",
|
| 33 |
+
" happy and started to laugh.",
|
| 34 |
+
),
|
| 35 |
+
(
|
| 36 |
+
"emotion_cause",
|
| 37 |
+
"Lily worked all morning to bake a sweet cake for her mom. When her mom smiled and hugged her, Lily felt very",
|
| 38 |
+
" proud and happy.",
|
| 39 |
+
" angry and mean.",
|
| 40 |
+
),
|
| 41 |
+
(
|
| 42 |
+
"physical_weight",
|
| 43 |
+
"The little mouse tried to lift the huge heavy rock, but the rock was much too",
|
| 44 |
+
" heavy for the tiny mouse to move.",
|
| 45 |
+
" light and floated into the sky.",
|
| 46 |
+
),
|
| 47 |
+
(
|
| 48 |
+
"physical_temperature",
|
| 49 |
+
"Ben went outside in the snow without his coat or gloves. Soon his hands felt very",
|
| 50 |
+
" cold and freezing.",
|
| 51 |
+
" hot and burning.",
|
| 52 |
+
),
|
| 53 |
+
(
|
| 54 |
+
"physical_temperature",
|
| 55 |
+
"The soup had just come off the stove and was boiling. Mom told Mia to wait because the soup was too",
|
| 56 |
+
" hot to eat right now.",
|
| 57 |
+
" cold and frozen like ice.",
|
| 58 |
+
),
|
| 59 |
+
(
|
| 60 |
+
"physical_fragile",
|
| 61 |
+
"The glass cup fell off the high table onto the hard stone floor. With a loud crash, the glass",
|
| 62 |
+
" broke into many small pieces.",
|
| 63 |
+
" bounced high like a rubber ball.",
|
| 64 |
+
),
|
| 65 |
+
(
|
| 66 |
+
"object_use",
|
| 67 |
+
"The wooden door was locked tight. To unlock the door, Anna took out her small brass",
|
| 68 |
+
" key and turned it in the lock.",
|
| 69 |
+
" spoon and stirred the soup.",
|
| 70 |
+
),
|
| 71 |
+
(
|
| 72 |
+
"object_use",
|
| 73 |
+
"It started to rain hard outside. Before walking to school, Max opened his big",
|
| 74 |
+
" umbrella to stay dry.",
|
| 75 |
+
" pillow to go to sleep.",
|
| 76 |
+
),
|
| 77 |
+
(
|
| 78 |
+
"biological_need",
|
| 79 |
+
"The little puppy had been running in the park all day and had not eaten anything. His tummy rumbled because he was very",
|
| 80 |
+
" hungry and wanted some food.",
|
| 81 |
+
" sleepy and wanted a haircut.",
|
| 82 |
+
),
|
| 83 |
+
(
|
| 84 |
+
"biological_need",
|
| 85 |
+
"After playing all day until night came and the stars shone in the sky, the tired children went to bed to",
|
| 86 |
+
" sleep until morning.",
|
| 87 |
+
" swim in the cold ocean.",
|
| 88 |
+
),
|
| 89 |
+
(
|
| 90 |
+
"entity_tracking",
|
| 91 |
+
"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",
|
| 92 |
+
" blue car.",
|
| 93 |
+
" green bicycle.",
|
| 94 |
+
),
|
| 95 |
+
(
|
| 96 |
+
"entity_tracking",
|
| 97 |
+
" Lily put her shiny coin inside the small red box and closed the lid. When she opened the red box later, she saw her",
|
| 98 |
+
" shiny coin inside.",
|
| 99 |
+
" big brown horse inside.",
|
| 100 |
+
),
|
| 101 |
+
(
|
| 102 |
+
"danger_reaction",
|
| 103 |
+
"Lily and a little bunny walked into the green forest. Suddenly a big fierce lion jumped out and roared loudly. The little bunny was",
|
| 104 |
+
" scared and hid behind a tree.",
|
| 105 |
+
" bored and read a newspaper.",
|
| 106 |
+
),
|
| 107 |
+
(
|
| 108 |
+
"social_repair",
|
| 109 |
+
"Tom accidentally knocked over Mia's tall block tower. Tom felt bad about the accident, so he said,",
|
| 110 |
+
' "I am sorry, Mia. Let me help you build it again."',
|
| 111 |
+
' "Thank you for the delicious birthday cake."',
|
| 112 |
+
),
|
| 113 |
+
(
|
| 114 |
+
"spatial_logic",
|
| 115 |
+
"The bird wanted to reach the nest at the very top of the tall tree, so it flapped its wings and",
|
| 116 |
+
" flew up high into the branches.",
|
| 117 |
+
" dug a deep hole under the mud.",
|
| 118 |
+
),
|
| 119 |
+
(
|
| 120 |
+
"weather_visibility",
|
| 121 |
+
"At midnight all the lights went out in the room. Because it was pitch dark, Ben could not",
|
| 122 |
+
" see anything around him.",
|
| 123 |
+
" hear the loud thunder outside.",
|
| 124 |
+
),
|
| 125 |
+
]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
NOVEL_STORY_PROMPTS: list[tuple[str, list[str]]] = [
|
| 129 |
+
(
|
| 130 |
+
"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",
|
| 131 |
+
["lion", "Lily", "bunny", "key", "chest", "forest", "scared", "roar", "said"],
|
| 132 |
+
),
|
| 133 |
+
(
|
| 134 |
+
"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",
|
| 135 |
+
["Beep", "kitten", "robot", "door", "rain", "warm", "milk", "help"],
|
| 136 |
+
),
|
| 137 |
+
(
|
| 138 |
+
"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",
|
| 139 |
+
["Sam", "Mia", "sand", "castle", "wave", "beach", "water", "wall"],
|
| 140 |
+
),
|
| 141 |
+
(
|
| 142 |
+
"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",
|
| 143 |
+
["bread", "puppy", "baker", "hill", "chased", "ran", "ate", "mouth"],
|
| 144 |
+
),
|
| 145 |
+
(
|
| 146 |
+
"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",
|
| 147 |
+
["Lucy", "crayon", "blue", "bed", "sky", "found", "draw", "paper"],
|
| 148 |
+
),
|
| 149 |
+
(
|
| 150 |
+
"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",
|
| 151 |
+
["Max", "snowman", "sun", "melt", "carrot", "snow", "warm", "water"],
|
| 152 |
+
),
|
| 153 |
+
]
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@torch.no_grad()
|
| 157 |
+
def evaluate_zero_shot_commonsense(
|
| 158 |
+
model: ConveyerDiffusionLM,
|
| 159 |
+
tokenizer: Any,
|
| 160 |
+
device: torch.device,
|
| 161 |
+
) -> dict[str, Any]:
|
| 162 |
+
"""
|
| 163 |
+
Evaluates zero-shot 2-way multiple-choice accuracy (BLiMP / StoryCloze style) using both:
|
| 164 |
+
1. Causal LM log-probability per token over the continuation.
|
| 165 |
+
2. Conveyor Slot-0 first-token preference on the continuation.
|
| 166 |
+
"""
|
| 167 |
+
model.eval()
|
| 168 |
+
correct_lm = 0
|
| 169 |
+
correct_s0 = 0
|
| 170 |
+
margin_sum = 0.0
|
| 171 |
+
details: list[dict[str, Any]] = []
|
| 172 |
+
|
| 173 |
+
for category, ctx, good_cont, bad_cont in COMMONSENSE_CLOZE_PAIRS:
|
| 174 |
+
ctx_ids = tokenizer.encode(ctx.strip())
|
| 175 |
+
good_ids = tokenizer.encode(ctx.strip() + good_cont)
|
| 176 |
+
bad_ids = tokenizer.encode(ctx.strip() + bad_cont)
|
| 177 |
+
plen = len(ctx_ids)
|
| 178 |
+
|
| 179 |
+
def _score_seq(full_ids: list[int]) -> float:
|
| 180 |
+
inp = torch.tensor([full_ids], dtype=torch.long, device=device)
|
| 181 |
+
logits, _ = model.forward_causal_and_cache(inp, prefix_len=len(full_ids))
|
| 182 |
+
log_probs = F.log_softmax(logits[0, :-1, :].float(), dim=-1)
|
| 183 |
+
targets = inp[0, 1:]
|
| 184 |
+
# Mean log-prob over the continuation tokens [plen-1 : len-1]
|
| 185 |
+
start_idx = max(0, plen - 1)
|
| 186 |
+
cont_lp = log_probs[start_idx:, :].gather(1, targets[start_idx:].unsqueeze(1)).squeeze(1)
|
| 187 |
+
return float(cont_lp.mean().item())
|
| 188 |
+
|
| 189 |
+
score_good = _score_seq(good_ids)
|
| 190 |
+
score_bad = _score_seq(bad_ids)
|
| 191 |
+
is_ok = score_good > score_bad
|
| 192 |
+
correct_lm += int(is_ok)
|
| 193 |
+
margin_sum += score_good - score_bad
|
| 194 |
+
|
| 195 |
+
# Also check Conveyor Slot-0 log-prob on the first continuation token
|
| 196 |
+
if len(good_ids) > plen and len(bad_ids) > plen:
|
| 197 |
+
p_tensor = torch.tensor([ctx_ids], dtype=torch.long, device=device)
|
| 198 |
+
kv = model.prefill_prefix(p_tensor)
|
| 199 |
+
belt = model.init_empty_belt(1, tick=plen, device=device, deterministic=True)
|
| 200 |
+
out = model.forward_belt(belt, kv_cache=kv)
|
| 201 |
+
s0_lp = F.log_softmax(out.slot0_logits[0].float(), dim=-1)
|
| 202 |
+
s0_good = float(s0_lp[good_ids[plen]].item())
|
| 203 |
+
s0_bad = float(s0_lp[bad_ids[plen]].item())
|
| 204 |
+
correct_s0 += int(s0_good > s0_bad)
|
| 205 |
+
|
| 206 |
+
details.append(
|
| 207 |
+
{
|
| 208 |
+
"category": category,
|
| 209 |
+
"correct": is_ok,
|
| 210 |
+
"logprob_good": round(score_good, 3),
|
| 211 |
+
"logprob_bad": round(score_bad, 3),
|
| 212 |
+
}
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
n = max(1, len(COMMONSENSE_CLOZE_PAIRS))
|
| 216 |
+
return {
|
| 217 |
+
"commonsense_lm_acc": correct_lm / n,
|
| 218 |
+
"commonsense_slot0_acc": correct_s0 / n,
|
| 219 |
+
"mean_logprob_margin": margin_sum / n,
|
| 220 |
+
"num_pairs": len(COMMONSENSE_CLOZE_PAIRS),
|
| 221 |
+
"details": details,
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
@torch.no_grad()
|
| 226 |
+
def evaluate_prefix_sensitivity_and_generation_quality(
|
| 227 |
+
model: ConveyerDiffusionLM,
|
| 228 |
+
tokenizer: Any,
|
| 229 |
+
device: torch.device,
|
| 230 |
+
max_new_tokens: int = 80,
|
| 231 |
+
) -> dict[str, Any]:
|
| 232 |
+
"""
|
| 233 |
+
Measures:
|
| 234 |
+
1. Prefix Sensitivity KL(P(slot0 | prompt) || P(slot0 | shuffled_prompt)):
|
| 235 |
+
In the broken template model this was 0.03 nats (ignored word order).
|
| 236 |
+
A real language model should have KL >> 0.5 nats.
|
| 237 |
+
2. Open-Ended Generation Quality on 6 unseen prompts:
|
| 238 |
+
- Distinct-2 / Distinct-3 n-gram diversity (1.0 = no repetition, <0.5 = looping)
|
| 239 |
+
- 4-gram repetition rate (0.0 = clean prose)
|
| 240 |
+
- Prompt entity/topic retention rate (% of continuations that reference the prompt's new entities/events)
|
| 241 |
+
"""
|
| 242 |
+
model.eval()
|
| 243 |
+
kl_list: list[float] = []
|
| 244 |
+
dist2_list: list[float] = []
|
| 245 |
+
dist3_list: list[float] = []
|
| 246 |
+
rep4_list: list[float] = []
|
| 247 |
+
entity_hits = 0
|
| 248 |
+
samples: list[dict[str, str]] = []
|
| 249 |
+
|
| 250 |
+
g = torch.Generator().manual_seed(123)
|
| 251 |
+
for prompt_text, expected_keywords in NOVEL_STORY_PROMPTS:
|
| 252 |
+
ids = tokenizer.encode(prompt_text.strip())
|
| 253 |
+
p_true = torch.tensor([ids], dtype=torch.long, device=device)
|
| 254 |
+
kv_true = model.prefill_prefix(p_true)
|
| 255 |
+
belt_true = model.init_empty_belt(1, tick=len(ids), device=device, deterministic=True)
|
| 256 |
+
d_true = F.softmax(model.forward_belt(belt_true, kv_cache=kv_true).slot0_logits[0].float(), dim=-1)
|
| 257 |
+
|
| 258 |
+
perm = torch.randperm(len(ids) - 1, generator=g) + 1
|
| 259 |
+
shuf_ids = [ids[0]] + [ids[i] for i in perm.tolist()]
|
| 260 |
+
p_shuf = torch.tensor([shuf_ids], dtype=torch.long, device=device)
|
| 261 |
+
kv_shuf = model.prefill_prefix(p_shuf)
|
| 262 |
+
belt_shuf = model.init_empty_belt(1, tick=len(ids), device=device, deterministic=True)
|
| 263 |
+
d_shuf = F.softmax(model.forward_belt(belt_shuf, kv_cache=kv_shuf).slot0_logits[0].float(), dim=-1)
|
| 264 |
+
|
| 265 |
+
kl = float((d_true * (d_true.clamp_min(1e-9).log() - d_shuf.clamp_min(1e-9).log())).sum().item())
|
| 266 |
+
kl_list.append(kl)
|
| 267 |
+
|
| 268 |
+
out_ids, _ = generate_conveyer(
|
| 269 |
+
model=model,
|
| 270 |
+
prompt_tokens=p_true,
|
| 271 |
+
max_new_tokens=max_new_tokens,
|
| 272 |
+
prompt_overlap=10,
|
| 273 |
+
temperature=0.0,
|
| 274 |
+
eos_token_id=tokenizer.eos_id,
|
| 275 |
+
deterministic_spawn=True,
|
| 276 |
+
)
|
| 277 |
+
gen_toks = out_ids[0, len(ids) :].tolist()
|
| 278 |
+
gen_text = tokenizer.decode(gen_toks).strip()
|
| 279 |
+
|
| 280 |
+
words = [w.strip(".,!?;:\"'()").lower() for w in gen_text.split() if w.strip()]
|
| 281 |
+
if len(words) >= 4:
|
| 282 |
+
bigrams = list(zip(words[:-1], words[1:]))
|
| 283 |
+
trigrams = list(zip(words[:-2], words[1:-1], words[2:]))
|
| 284 |
+
fourgrams = list(zip(words[:-3], words[1:-2], words[2:-1], words[3:]))
|
| 285 |
+
dist2_list.append(len(set(bigrams)) / max(1, len(bigrams)))
|
| 286 |
+
dist3_list.append(len(set(trigrams)) / max(1, len(trigrams)))
|
| 287 |
+
c4 = Counter(fourgrams)
|
| 288 |
+
repeated_4 = sum(v - 1 for v in c4.values() if v > 1)
|
| 289 |
+
rep4_list.append(repeated_4 / max(1, len(fourgrams)))
|
| 290 |
+
|
| 291 |
+
low_gen = gen_text.lower()
|
| 292 |
+
hit = any(kw.lower() in low_gen for kw in expected_keywords[:4])
|
| 293 |
+
entity_hits += int(hit)
|
| 294 |
+
samples.append({"prompt": prompt_text, "continuation": gen_text})
|
| 295 |
+
|
| 296 |
+
return {
|
| 297 |
+
"prefix_word_order_kl": sum(kl_list) / max(1, len(kl_list)),
|
| 298 |
+
"distinct_2": sum(dist2_list) / max(1, len(dist2_list)),
|
| 299 |
+
"distinct_3": sum(dist3_list) / max(1, len(dist3_list)),
|
| 300 |
+
"repeat_4gram_rate": sum(rep4_list) / max(1, len(rep4_list)),
|
| 301 |
+
"prompt_entity_retention": entity_hits / max(1, len(NOVEL_STORY_PROMPTS)),
|
| 302 |
+
"samples": samples,
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
@torch.no_grad()
|
| 307 |
+
def evaluate_crystallization_curve(
|
| 308 |
+
model: ConveyerDiffusionLM,
|
| 309 |
+
cfg: ConveyerConfig,
|
| 310 |
+
val_loader: DataLoader[dict[str, torch.Tensor]],
|
| 311 |
+
device: torch.device,
|
| 312 |
+
pad_id: int = 0,
|
| 313 |
+
max_batches: int = 20,
|
| 314 |
+
) -> dict[str, Any]:
|
| 315 |
+
"""
|
| 316 |
+
Computes the Per-Slot Accuracy, Entropy, and Pause Rate across the 64-slot oven belt
|
| 317 |
+
plus held-out real TinyStories Perplexity (Causal LM & Conveyor Slot-0).
|
| 318 |
+
"""
|
| 319 |
+
model.eval()
|
| 320 |
+
B = model.belt_size
|
| 321 |
+
acc_sum = torch.zeros(B, device=device)
|
| 322 |
+
ent_sum = torch.zeros(B, device=device)
|
| 323 |
+
s0_loss_sum = 0.0
|
| 324 |
+
lm_loss_sum = 0.0
|
| 325 |
+
lm_acc_sum = 0.0
|
| 326 |
+
pause_sum = 0.0
|
| 327 |
+
count = 0
|
| 328 |
+
|
| 329 |
+
for i, batch in enumerate(val_loader):
|
| 330 |
+
if i >= max_batches:
|
| 331 |
+
break
|
| 332 |
+
seq = batch["input_ids"].to(device)
|
| 333 |
+
plen = batch["prompt_lengths"].to(device)
|
| 334 |
+
use_amp = device.type == "cuda" and torch.cuda.is_bf16_supported()
|
| 335 |
+
with torch.autocast(device_type=device.type, dtype=torch.bfloat16 if use_amp else torch.float32, enabled=use_amp):
|
| 336 |
+
m = compute_unrolled_conveyer_loss(
|
| 337 |
+
model=model,
|
| 338 |
+
seq_tokens=seq,
|
| 339 |
+
cfg=cfg,
|
| 340 |
+
prompt_lengths=plen,
|
| 341 |
+
tick_start=int(plen.min().item()),
|
| 342 |
+
pad_token_id=pad_id,
|
| 343 |
+
)
|
| 344 |
+
acc_sum += m.per_slot_acc
|
| 345 |
+
ent_sum += m.per_slot_entropy
|
| 346 |
+
s0_loss_sum += m.slot0_loss
|
| 347 |
+
lm_loss_sum += m.prefix_lm_loss
|
| 348 |
+
lm_acc_sum += m.prefix_lm_acc
|
| 349 |
+
pause_sum += m.mean_plan_pause
|
| 350 |
+
count += 1
|
| 351 |
+
|
| 352 |
+
denom = max(count, 1)
|
| 353 |
+
acc_curve = (acc_sum / denom).cpu().tolist()
|
| 354 |
+
ent_curve = (ent_sum / denom).cpu().tolist()
|
| 355 |
+
s0_ce = s0_loss_sum / denom
|
| 356 |
+
lm_ce = lm_loss_sum / denom
|
| 357 |
+
return {
|
| 358 |
+
"slot0_ce": s0_ce,
|
| 359 |
+
"slot0_ppl": math.exp(min(s0_ce, 20.0)),
|
| 360 |
+
"slot0_acc": acc_curve[0],
|
| 361 |
+
"causal_lm_ce": lm_ce,
|
| 362 |
+
"causal_lm_ppl": math.exp(min(lm_ce, 20.0)),
|
| 363 |
+
"causal_lm_acc": lm_acc_sum / denom,
|
| 364 |
+
"mean_plan_pause": pause_sum / denom,
|
| 365 |
+
"per_slot_acc": acc_curve,
|
| 366 |
+
"per_slot_entropy": ent_curve,
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
@torch.no_grad()
|
| 371 |
+
def evaluate_closed_loop_generation(
|
| 372 |
+
model: ConveyerDiffusionLM,
|
| 373 |
+
val_ds: Any,
|
| 374 |
+
tokenizer: Any,
|
| 375 |
+
device: torch.device,
|
| 376 |
+
num_episodes: int = 32,
|
| 377 |
+
prompt_overlap: int = 12,
|
| 378 |
+
bake_steps: int = 64,
|
| 379 |
+
max_new_tokens: int = 24,
|
| 380 |
+
) -> dict[str, float]:
|
| 381 |
+
model.eval()
|
| 382 |
+
n_eval = min(num_episodes, len(val_ds))
|
| 383 |
+
first_tok_correct = 0
|
| 384 |
+
total_tok_correct = 0
|
| 385 |
+
total_tok_count = 0
|
| 386 |
+
exact_match = 0
|
| 387 |
+
|
| 388 |
+
for i in range(n_eval):
|
| 389 |
+
item = val_ds[i]
|
| 390 |
+
seq = item["input_ids"].to(device)
|
| 391 |
+
plen = int(item["prompt_len"])
|
| 392 |
+
prompt = seq[:plen].unsqueeze(0)
|
| 393 |
+
|
| 394 |
+
gt_full = seq[plen:].tolist()
|
| 395 |
+
gt_target: list[int] = []
|
| 396 |
+
for tid in gt_full:
|
| 397 |
+
if tid == tokenizer.pad_id:
|
| 398 |
+
break
|
| 399 |
+
gt_target.append(tid)
|
| 400 |
+
if tid == tokenizer.eos_id:
|
| 401 |
+
break
|
| 402 |
+
|
| 403 |
+
if not gt_target:
|
| 404 |
+
continue
|
| 405 |
+
|
| 406 |
+
gen_len = min(max_new_tokens, len(gt_target))
|
| 407 |
+
full_out, _ = generate_conveyer(
|
| 408 |
+
model=model,
|
| 409 |
+
prompt_tokens=prompt,
|
| 410 |
+
max_new_tokens=gen_len,
|
| 411 |
+
prompt_overlap=prompt_overlap,
|
| 412 |
+
bake_steps=bake_steps,
|
| 413 |
+
temperature=0.0,
|
| 414 |
+
eos_token_id=tokenizer.eos_id,
|
| 415 |
+
deterministic_spawn=True,
|
| 416 |
+
)
|
| 417 |
+
pred_ids = full_out[0, plen : plen + gen_len].tolist()
|
| 418 |
+
gt_slice = gt_target[:gen_len]
|
| 419 |
+
|
| 420 |
+
if len(pred_ids) > 0 and pred_ids[0] == gt_slice[0]:
|
| 421 |
+
first_tok_correct += 1
|
| 422 |
+
|
| 423 |
+
matches = sum(1 for p, g in zip(pred_ids, gt_slice) if p == g)
|
| 424 |
+
total_tok_correct += matches
|
| 425 |
+
total_tok_count += len(gt_slice)
|
| 426 |
+
if pred_ids == gt_slice:
|
| 427 |
+
exact_match += 1
|
| 428 |
+
|
| 429 |
+
return {
|
| 430 |
+
"first_token_acc": first_tok_correct / max(1, n_eval),
|
| 431 |
+
"token_acc": total_tok_correct / max(1, total_tok_count),
|
| 432 |
+
"exact_seq_acc": exact_match / max(1, n_eval),
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def main() -> None:
|
| 437 |
+
parser = argparse.ArgumentParser(description="Comprehensive Benchmark Suite for Conveyor-Belt Diffusion LLM")
|
| 438 |
+
parser.add_argument("--ckpt", type=str, default="runs/tinystories/best.pt", help="Path to checkpoint (.pt)")
|
| 439 |
+
parser.add_argument("--device", type=str, default="auto", help="'cpu', 'cuda', or 'auto'")
|
| 440 |
+
parser.add_argument("--episodes", type=int, default=32, help="Number of closed-loop episodes")
|
| 441 |
+
parser.add_argument("--bake-steps", type=int, nargs="+", default=[16, 64], help="Bake depths K to evaluate")
|
| 442 |
+
parser.add_argument("--overlaps", type=int, nargs="+", default=[0, 10], help="Prompt-overlap windows W_pre")
|
| 443 |
+
parser.add_argument("--out-json", type=str, default=None, help="Optional path to save evaluation JSON")
|
| 444 |
+
args = parser.parse_args()
|
| 445 |
+
|
| 446 |
+
if args.device == "auto":
|
| 447 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 448 |
+
else:
|
| 449 |
+
device = torch.device(args.device)
|
| 450 |
+
|
| 451 |
+
model, cfg, ckpt_meta = load_checkpoint(args.ckpt, device=device)
|
| 452 |
+
_, val_ds, tokenizer = build_dataset_and_tokenizer(cfg)
|
| 453 |
+
val_loader = DataLoader(
|
| 454 |
+
val_ds,
|
| 455 |
+
batch_size=cfg.train.batch_size,
|
| 456 |
+
shuffle=False,
|
| 457 |
+
collate_fn=lambda b: collate_conveyer_batch(b, pad_id=tokenizer.pad_id),
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
step_num = ckpt_meta.get("step", "?")
|
| 461 |
+
elapsed_h = float(ckpt_meta.get("elapsed_sec", 0.0)) / 3600.0
|
| 462 |
+
|
| 463 |
+
cryst = evaluate_crystallization_curve(model, cfg, val_loader, device, pad_id=tokenizer.pad_id)
|
| 464 |
+
acc_c = cryst["per_slot_acc"]
|
| 465 |
+
ent_c = cryst["per_slot_entropy"]
|
| 466 |
+
B = len(acc_c)
|
| 467 |
+
|
| 468 |
+
print("=" * 92)
|
| 469 |
+
print(
|
| 470 |
+
f"CONVEYOR-BELT DIFFUSION LLM BENCHMARK SUITE | Ckpt: {args.ckpt} (step {step_num}, {elapsed_h:.2f}h) "
|
| 471 |
+
f"| Params: {model.num_parameters()/1e6:.2f}M | Device: {device}"
|
| 472 |
+
)
|
| 473 |
+
print("=" * 92)
|
| 474 |
+
print(
|
| 475 |
+
f"[1] Held-Out Real TinyStories LM Metrics (10,000 unseen stories, 0% templates):\n"
|
| 476 |
+
f" - Conveyor Slot-0 Perplexity : {cryst['slot0_ppl']:6.2f} | Top-1 Next-Token Acc: {cryst['slot0_acc']*100:5.2f}%\n"
|
| 477 |
+
f" - Causal Prefix LM Perplexity: {cryst['causal_lm_ppl']:6.2f} | Top-1 Next-Token Acc: {cryst['causal_lm_acc']*100:5.2f}%\n"
|
| 478 |
+
f" - Back-75% Planning Pause : {cryst['mean_plan_pause']*100:5.1f}%"
|
| 479 |
+
)
|
| 480 |
+
print("-" * 92)
|
| 481 |
+
print("[2] Per-Slot Crystallization Curve along the 64-Slot Oven Belt (Back k=63 -> Front k=0):")
|
| 482 |
+
sample_slots = [B - 1, (3 * B) // 4, B // 2, B // 4, min(8, B - 1), min(3, B - 1), 0]
|
| 483 |
+
seen: set[int] = set()
|
| 484 |
+
for k in sample_slots:
|
| 485 |
+
if k in seen:
|
| 486 |
+
continue
|
| 487 |
+
seen.add(k)
|
| 488 |
+
zone = "Back (75% Plan) " if k >= 16 else "Front (25% Stream)"
|
| 489 |
+
print(f" Slot {k:2d} [{zone}] | Top-1 Acc: {acc_c[k]*100:6.2f}% | Entropy: {ent_c[k]:.3f} nats")
|
| 490 |
+
|
| 491 |
+
if cfg.data.dataset == "tinystories":
|
| 492 |
+
cs = evaluate_zero_shot_commonsense(model, tokenizer, device)
|
| 493 |
+
gen_q = evaluate_prefix_sensitivity_and_generation_quality(model, tokenizer, device)
|
| 494 |
+
print("-" * 92)
|
| 495 |
+
print(
|
| 496 |
+
f"[3] Zero-Shot StoryCloze / Commonsense 2-Way Multiple Choice ({cs['num_pairs']} unseen pairs, random=50%):\n"
|
| 497 |
+
f" - Causal LM Choice Accuracy : {cs['commonsense_lm_acc']*100:6.2f}% (mean log-prob margin: {cs['mean_logprob_margin']:+.3f} nats)\n"
|
| 498 |
+
f" - Conveyor Slot-0 Choice Acc : {cs['commonsense_slot0_acc']*100:6.2f}%"
|
| 499 |
+
)
|
| 500 |
+
print("-" * 92)
|
| 501 |
+
print(
|
| 502 |
+
f"[4] Prompt Responsiveness & Open-Ended Generation Quality (6 novel story prompts):\n"
|
| 503 |
+
f" - Word-Order Sensitivity KL : {gen_q['prefix_word_order_kl']:6.3f} nats (broken model was 0.032; >0.50 = strong)\n"
|
| 504 |
+
f" - Prompt Entity Retention : {gen_q['prompt_entity_retention']*100:6.1f}%\n"
|
| 505 |
+
f" - Lexical Diversity : Distinct-2 = {gen_q['distinct_2']*100:5.1f}% | Distinct-3 = {gen_q['distinct_3']*100:5.1f}%\n"
|
| 506 |
+
f" - 4-gram Repetition Rate : {gen_q['repeat_4gram_rate']*100:5.2f}% (lower is better; 0% = zero loops)"
|
| 507 |
+
)
|
| 508 |
+
print("-" * 92)
|
| 509 |
+
print("[5] Live Novel Prompt Continutations:")
|
| 510 |
+
for s in gen_q["samples"][:3]:
|
| 511 |
+
print(f" PROMPT : {s['prompt']}")
|
| 512 |
+
print(f" OUTPUT : {s['continuation']}\n")
|
| 513 |
+
else:
|
| 514 |
+
cs = {}
|
| 515 |
+
gen_q = {}
|
| 516 |
+
|
| 517 |
+
print("-" * 92)
|
| 518 |
+
print(f"{'Bake Depth K':<15} | {'Prompt Overlap W':<18} | {'Pos-0 Acc':<12} | {'Token Acc':<12} | {'Exact Seq Acc':<14}")
|
| 519 |
+
print("-" * 92)
|
| 520 |
+
|
| 521 |
+
sweep_results: list[dict[str, Any]] = []
|
| 522 |
+
for k_bake in args.bake_steps:
|
| 523 |
+
for w_ov in args.overlaps:
|
| 524 |
+
res = evaluate_closed_loop_generation(
|
| 525 |
+
model=model,
|
| 526 |
+
val_ds=val_ds,
|
| 527 |
+
tokenizer=tokenizer,
|
| 528 |
+
device=device,
|
| 529 |
+
num_episodes=args.episodes,
|
| 530 |
+
prompt_overlap=w_ov,
|
| 531 |
+
bake_steps=k_bake,
|
| 532 |
+
)
|
| 533 |
+
print(
|
| 534 |
+
f"K = {k_bake:<11d} | W_pre = {w_ov:<10d} | "
|
| 535 |
+
f"{res['first_token_acc']*100:9.2f}% | {res['token_acc']*100:9.2f}% | {res['exact_seq_acc']*100:10.2f}%"
|
| 536 |
+
)
|
| 537 |
+
sweep_results.append({"bake_steps": k_bake, "prompt_overlap": w_ov, **res})
|
| 538 |
+
|
| 539 |
+
if args.out_json:
|
| 540 |
+
Path(args.out_json).parent.mkdir(parents=True, exist_ok=True)
|
| 541 |
+
Path(args.out_json).write_text(
|
| 542 |
+
json.dumps(
|
| 543 |
+
{
|
| 544 |
+
"step": step_num,
|
| 545 |
+
"crystallization": cryst,
|
| 546 |
+
"commonsense": cs,
|
| 547 |
+
"generation_quality": gen_q,
|
| 548 |
+
"closed_loop": sweep_results,
|
| 549 |
+
},
|
| 550 |
+
indent=2,
|
| 551 |
+
),
|
| 552 |
+
encoding="utf-8",
|
| 553 |
+
)
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
if __name__ == "__main__":
|
| 557 |
+
main()
|
src/conveyer/infer.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Iterator, Optional
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
|
| 10 |
+
from conveyer.model import BeltState, BeltStepOutput, ConveyerDiffusionLM, PrefixKVCache
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class TickSnapshot:
|
| 15 |
+
"""Complete telemetry of a single conveyor-belt oven tick (used for live CLI & Web UI)."""
|
| 16 |
+
tick: int # absolute sequence index at Slot 0
|
| 17 |
+
is_warmup: bool # True during prompt-overlap pre-baking (t < prompt_len)
|
| 18 |
+
emitted_token: int # token popped out at Slot 0 (clamped if is_warmup)
|
| 19 |
+
predicted_slot0_token: int # model's raw top-1 prediction at Slot 0
|
| 20 |
+
slot_top1_tokens: list[int] # (B,) top-1 token ID at each slot k=0..B-1
|
| 21 |
+
slot_top1_probs: list[float] # (B,) top-1 probability at each slot k=0..B-1
|
| 22 |
+
slot_entropies: list[float] # (B,) Shannon entropy at each slot k=0..B-1
|
| 23 |
+
slot_gate_means: list[float] # (B,) mean thought-highway gate activation g_k
|
| 24 |
+
slot_shift_probs: list[float] # (B,) elastic shift probability s_k (1.0 in front, <1 in back 75% planning)
|
| 25 |
+
slot_top3_tokens: list[list[int]] # (B, 3) top-3 candidate token IDs per slot
|
| 26 |
+
slot_top3_probs: list[list[float]]# (B, 3) top-3 candidate probabilities per slot
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@torch.no_grad()
|
| 30 |
+
def sample_slot0_token(
|
| 31 |
+
logits: torch.Tensor,
|
| 32 |
+
temperature: float = 0.0,
|
| 33 |
+
recent_tokens: Optional[list[int]] = None,
|
| 34 |
+
repetition_penalty: float = 1.25,
|
| 35 |
+
) -> torch.Tensor:
|
| 36 |
+
"""
|
| 37 |
+
logits: (N, V)
|
| 38 |
+
Masks out special tokens (<PAD>=0, <BOS>=1, <UNK>=3) and applies mild repetition penalty
|
| 39 |
+
on recently emitted tokens before greedy or top-k sampling.
|
| 40 |
+
"""
|
| 41 |
+
adj_logits = logits.clone()
|
| 42 |
+
if adj_logits.size(-1) > 4:
|
| 43 |
+
adj_logits[:, 0] = -1e9
|
| 44 |
+
adj_logits[:, 1] = -1e9
|
| 45 |
+
adj_logits[:, 3] = -1e9
|
| 46 |
+
|
| 47 |
+
if recent_tokens and repetition_penalty > 1.0:
|
| 48 |
+
for tid in set(recent_tokens[-6:]):
|
| 49 |
+
if 0 <= tid < adj_logits.size(-1):
|
| 50 |
+
val = adj_logits[:, tid]
|
| 51 |
+
adj_logits[:, tid] = torch.where(val > 0, val / repetition_penalty, val * repetition_penalty)
|
| 52 |
+
# Only block 3-in-a-row identical token stammers (so legitimate words after prompt or across clauses are never clipped!)
|
| 53 |
+
if len(recent_tokens) >= 2 and recent_tokens[-1] == recent_tokens[-2]:
|
| 54 |
+
last_tid = recent_tokens[-1]
|
| 55 |
+
if 0 <= last_tid < adj_logits.size(-1):
|
| 56 |
+
adj_logits[:, last_tid] -= 3.0
|
| 57 |
+
|
| 58 |
+
if temperature <= 1e-5:
|
| 59 |
+
return adj_logits.argmax(dim=-1)
|
| 60 |
+
scaled = adj_logits / temperature
|
| 61 |
+
k = min(24, scaled.size(-1))
|
| 62 |
+
top_vals, _ = torch.topk(scaled, k, dim=-1)
|
| 63 |
+
min_top = top_vals[:, -1].unsqueeze(-1)
|
| 64 |
+
scaled = torch.where(scaled < min_top, torch.full_like(scaled, -1e9), scaled)
|
| 65 |
+
probs = F.softmax(scaled, dim=-1)
|
| 66 |
+
return torch.multinomial(probs, num_samples=1).squeeze(-1)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@torch.no_grad()
|
| 70 |
+
def stream_conveyer_generation(
|
| 71 |
+
model: ConveyerDiffusionLM,
|
| 72 |
+
prompt_tokens: torch.Tensor,
|
| 73 |
+
max_new_tokens: int = 64,
|
| 74 |
+
prompt_overlap: int = 12,
|
| 75 |
+
bake_steps: Optional[int] = None,
|
| 76 |
+
temperature: float = 0.0,
|
| 77 |
+
eos_token_id: Optional[int] = None,
|
| 78 |
+
deterministic_spawn: bool = False,
|
| 79 |
+
) -> Iterator[tuple[torch.Tensor, TickSnapshot]]:
|
| 80 |
+
"""
|
| 81 |
+
Streams generation tick-by-tick using the `n - W_pre` Prompt-Overlap Pre-Baking protocol:
|
| 82 |
+
- Input at every tick t : [Committed Prefix x_{0..t-1}] + [64-Slot Conveyor/Planning Belt]
|
| 83 |
+
- Output at every tick t: [Popped-out Confirmed Token x_t at Slot 0] + [Updated 64-Slot Belt]
|
| 84 |
+
"""
|
| 85 |
+
model.eval()
|
| 86 |
+
if prompt_tokens.dim() == 1:
|
| 87 |
+
prompt_tokens = prompt_tokens.unsqueeze(0)
|
| 88 |
+
|
| 89 |
+
bsz, n = prompt_tokens.shape
|
| 90 |
+
device = prompt_tokens.device
|
| 91 |
+
B = min(bake_steps, model.belt_size) if bake_steps is not None else model.belt_size
|
| 92 |
+
B = max(1, B)
|
| 93 |
+
|
| 94 |
+
max_overlap = min(model.front_slots, B - 1) if B > 1 else 0
|
| 95 |
+
w_pre = max(0, min(n - 1 if n > 1 else 0, prompt_overlap, max_overlap))
|
| 96 |
+
t_start = n - w_pre
|
| 97 |
+
|
| 98 |
+
# 1. Prefill prefix KV cache on x_{0 : t_start - 1}
|
| 99 |
+
kv_cache: PrefixKVCache = model.prefill_prefix(prompt_tokens[:, :t_start])
|
| 100 |
+
|
| 101 |
+
# 2. Initialize conveyor belt at t_start
|
| 102 |
+
belt: BeltState = model.init_empty_belt(
|
| 103 |
+
batch_size=bsz,
|
| 104 |
+
tick=t_start,
|
| 105 |
+
device=device,
|
| 106 |
+
deterministic=deterministic_spawn,
|
| 107 |
+
)
|
| 108 |
+
if B < model.belt_size:
|
| 109 |
+
belt = BeltState(
|
| 110 |
+
e_soft=belt.e_soft[:, :B, :],
|
| 111 |
+
m_thought=belt.m_thought[:, :B, :],
|
| 112 |
+
tick=t_start,
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
offsets = model.get_slot_target_offsets(active_slots=B).to(device) # (B,)
|
| 116 |
+
|
| 117 |
+
# Clamp any belt slots at tick t_start that fall inside the known input prompt (t_start + offsets[k] < n)
|
| 118 |
+
if w_pre > 0:
|
| 119 |
+
slot_pos = t_start + offsets # (B,)
|
| 120 |
+
in_prompt = slot_pos < n
|
| 121 |
+
clamped_pos = slot_pos.clamp(min=0, max=n - 1).unsqueeze(0).expand(bsz, -1)
|
| 122 |
+
prompt_gathered = torch.gather(prompt_tokens, dim=1, index=clamped_pos)
|
| 123 |
+
overlap_emb = model.tok_emb(prompt_gathered)
|
| 124 |
+
e_init = torch.where(in_prompt.view(1, B, 1), overlap_emb, belt.e_soft)
|
| 125 |
+
belt = BeltState(e_soft=e_init, m_thought=belt.m_thought, tick=t_start)
|
| 126 |
+
|
| 127 |
+
total_ticks = w_pre + max_new_tokens
|
| 128 |
+
recent_emitted: list[int] = []
|
| 129 |
+
|
| 130 |
+
for step_idx in range(total_ticks):
|
| 131 |
+
t = t_start + step_idx
|
| 132 |
+
is_warmup = t < n
|
| 133 |
+
|
| 134 |
+
# Single forward pass across all active slots on the belt
|
| 135 |
+
step_out: BeltStepOutput = model.forward_belt(belt=belt, kv_cache=kv_cache, active_slots=B)
|
| 136 |
+
|
| 137 |
+
raw_pred_tok = sample_slot0_token(
|
| 138 |
+
step_out.slot0_logits,
|
| 139 |
+
temperature=temperature,
|
| 140 |
+
recent_tokens=recent_emitted if not is_warmup else None,
|
| 141 |
+
repetition_penalty=1.08 if temperature > 1e-5 else 1.0,
|
| 142 |
+
)
|
| 143 |
+
if is_warmup:
|
| 144 |
+
emitted_tok = prompt_tokens[:, t]
|
| 145 |
+
else:
|
| 146 |
+
emitted_tok = raw_pred_tok
|
| 147 |
+
recent_emitted.append(int(emitted_tok[0].item()))
|
| 148 |
+
|
| 149 |
+
probs0 = step_out.probs[0] # (B, V)
|
| 150 |
+
p_clamped = probs0.clamp(min=1e-8)
|
| 151 |
+
entropies = -(p_clamped * p_clamped.log()).sum(dim=-1) # (B,)
|
| 152 |
+
k_top = min(3, model.vocab_size)
|
| 153 |
+
top_probs, top_ids = probs0.topk(k_top, dim=-1)
|
| 154 |
+
gate_means = step_out.gate[0].mean(dim=-1) # (B,)
|
| 155 |
+
shift_probs = step_out.shift_prob[0] # (B,)
|
| 156 |
+
|
| 157 |
+
snapshot = TickSnapshot(
|
| 158 |
+
tick=t,
|
| 159 |
+
is_warmup=is_warmup,
|
| 160 |
+
emitted_token=int(emitted_tok[0].item()),
|
| 161 |
+
predicted_slot0_token=int(raw_pred_tok[0].item()),
|
| 162 |
+
slot_top1_tokens=top_ids[:, 0].tolist(),
|
| 163 |
+
slot_top1_probs=top_probs[:, 0].tolist(),
|
| 164 |
+
slot_entropies=entropies.tolist(),
|
| 165 |
+
slot_gate_means=gate_means.tolist(),
|
| 166 |
+
slot_shift_probs=shift_probs.tolist(),
|
| 167 |
+
slot_top3_tokens=top_ids.tolist(),
|
| 168 |
+
slot_top3_probs=top_probs.tolist(),
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
yield emitted_tok, snapshot
|
| 172 |
+
|
| 173 |
+
if (not is_warmup) and eos_token_id is not None and int(emitted_tok[0].item()) == eos_token_id:
|
| 174 |
+
break
|
| 175 |
+
|
| 176 |
+
# Commit popped-out token x_t to frozen prefix KV cache
|
| 177 |
+
kv_cache = model.append_committed_token(emitted_tok, kv_cache)
|
| 178 |
+
|
| 179 |
+
# Shift belt left (with elastic 75% back planning zone) and clamp any remaining prompt tokens at t + 1
|
| 180 |
+
next_t = t + 1
|
| 181 |
+
if next_t < n:
|
| 182 |
+
next_pos = next_t + offsets # (B,)
|
| 183 |
+
clamp_mask = (next_pos < n).unsqueeze(0).expand(bsz, -1) # (N, B)
|
| 184 |
+
clamped_idx = next_pos.clamp(min=0, max=n - 1).unsqueeze(0).expand(bsz, -1)
|
| 185 |
+
clamp_tokens = torch.gather(prompt_tokens, dim=1, index=clamped_idx)
|
| 186 |
+
else:
|
| 187 |
+
clamp_tokens = None
|
| 188 |
+
clamp_mask = None
|
| 189 |
+
|
| 190 |
+
belt = model.shift_belt_left(
|
| 191 |
+
step_out=step_out,
|
| 192 |
+
current_tick=t,
|
| 193 |
+
deterministic_spawn=deterministic_spawn,
|
| 194 |
+
clamp_tokens=clamp_tokens,
|
| 195 |
+
clamp_mask=clamp_mask,
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
@torch.no_grad()
|
| 200 |
+
def generate_conveyer(
|
| 201 |
+
model: ConveyerDiffusionLM,
|
| 202 |
+
prompt_tokens: torch.Tensor,
|
| 203 |
+
max_new_tokens: int = 64,
|
| 204 |
+
prompt_overlap: int = 12,
|
| 205 |
+
bake_steps: Optional[int] = None,
|
| 206 |
+
temperature: float = 0.0,
|
| 207 |
+
eos_token_id: Optional[int] = None,
|
| 208 |
+
deterministic_spawn: bool = False,
|
| 209 |
+
) -> tuple[torch.Tensor, list[TickSnapshot]]:
|
| 210 |
+
if prompt_tokens.dim() == 1:
|
| 211 |
+
prompt_tokens = prompt_tokens.unsqueeze(0)
|
| 212 |
+
|
| 213 |
+
generated: list[torch.Tensor] = []
|
| 214 |
+
snapshots: list[TickSnapshot] = []
|
| 215 |
+
|
| 216 |
+
for tok, snap in stream_conveyer_generation(
|
| 217 |
+
model=model,
|
| 218 |
+
prompt_tokens=prompt_tokens,
|
| 219 |
+
max_new_tokens=max_new_tokens,
|
| 220 |
+
prompt_overlap=prompt_overlap,
|
| 221 |
+
bake_steps=bake_steps,
|
| 222 |
+
temperature=temperature,
|
| 223 |
+
eos_token_id=eos_token_id,
|
| 224 |
+
deterministic_spawn=deterministic_spawn,
|
| 225 |
+
):
|
| 226 |
+
snapshots.append(snap)
|
| 227 |
+
if not snap.is_warmup:
|
| 228 |
+
generated.append(tok.unsqueeze(1))
|
| 229 |
+
|
| 230 |
+
if len(generated) > 0:
|
| 231 |
+
gen_tensor = torch.cat(generated, dim=1)
|
| 232 |
+
full_seq = torch.cat([prompt_tokens, gen_tensor], dim=1)
|
| 233 |
+
else:
|
| 234 |
+
full_seq = prompt_tokens.clone()
|
| 235 |
+
|
| 236 |
+
return full_seq, snapshots
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def main() -> None:
|
| 240 |
+
parser = argparse.ArgumentParser(description="CLI Test & Tick-by-Tick Trace for Conveyor-Belt Diffusion LLM")
|
| 241 |
+
parser.add_argument("--ckpt", type=str, default=None, help="Path to model checkpoint (.pt)")
|
| 242 |
+
parser.add_argument("--prompt", type=str, default=None, help="Initial text prompt to continue")
|
| 243 |
+
parser.add_argument("--tokens", type=int, default=32, help="Number of new tokens to bake and emit")
|
| 244 |
+
parser.add_argument("--overlap", type=int, default=12, help="Prompt-overlap pre-baking window (n - W)")
|
| 245 |
+
parser.add_argument("--bake-steps", type=int, default=64, help="Active conveyor belt length K (1..64)")
|
| 246 |
+
parser.add_argument("--temp", type=float, default=0.0, help="Sampling temperature at Slot 0 (0 = greedy, 0.7 = creative)")
|
| 247 |
+
args = parser.parse_args()
|
| 248 |
+
|
| 249 |
+
from conveyer.checkpoint import load_checkpoint
|
| 250 |
+
from conveyer.data import build_dataset_and_tokenizer
|
| 251 |
+
|
| 252 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 253 |
+
ckpt_path = args.ckpt
|
| 254 |
+
if ckpt_path is None:
|
| 255 |
+
ckpt_path = (
|
| 256 |
+
"runs/synthetic/best.pt"
|
| 257 |
+
if (not Path("runs/tinystories/best.pt").exists() and Path("runs/synthetic/best.pt").exists())
|
| 258 |
+
else "runs/tinystories/best.pt"
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
model, cfg, _ = load_checkpoint(ckpt_path, device=device)
|
| 262 |
+
_, val_ds, tokenizer = build_dataset_and_tokenizer(cfg, infer_only=True)
|
| 263 |
+
|
| 264 |
+
if args.prompt is not None:
|
| 265 |
+
# A trailing space becomes a lone ' ' BPE token that never ends a prefix in training.
|
| 266 |
+
prompt_ids = tokenizer.encode(args.prompt.strip())
|
| 267 |
+
prompt_tensor = torch.tensor([prompt_ids], dtype=torch.long, device=device)
|
| 268 |
+
else:
|
| 269 |
+
sample = val_ds[0]
|
| 270 |
+
p_len = int(sample["prompt_len"])
|
| 271 |
+
prompt_tensor = sample["input_ids"][:p_len].unsqueeze(0).to(device)
|
| 272 |
+
|
| 273 |
+
prompt_decoded = tokenizer.decode(prompt_tensor[0].tolist())
|
| 274 |
+
print("=" * 108)
|
| 275 |
+
print(f"Checkpoint : {ckpt_path} ({model.num_parameters()/1e6:.2f}M params, device={device})")
|
| 276 |
+
print(f"Input Text : {prompt_decoded}")
|
| 277 |
+
print("-" * 108)
|
| 278 |
+
print(
|
| 279 |
+
f"{'tick':>4} | {'mode':<6} | {'pop(Slot 0)':<14} | {'front conveyor [1..4]':<30} | "
|
| 280 |
+
f"{'mid expand [15..18]':<24} | {'back 75% plan [58..63] (pause%)'}"
|
| 281 |
+
)
|
| 282 |
+
print("-" * 108)
|
| 283 |
+
|
| 284 |
+
emitted_ids: list[int] = []
|
| 285 |
+
for tok, snap in stream_conveyer_generation(
|
| 286 |
+
model=model,
|
| 287 |
+
prompt_tokens=prompt_tensor,
|
| 288 |
+
max_new_tokens=args.tokens,
|
| 289 |
+
prompt_overlap=args.overlap,
|
| 290 |
+
bake_steps=args.bake_steps,
|
| 291 |
+
temperature=args.temp,
|
| 292 |
+
eos_token_id=tokenizer.eos_id,
|
| 293 |
+
):
|
| 294 |
+
if not snap.is_warmup:
|
| 295 |
+
emitted_ids.append(int(tok[0].item()))
|
| 296 |
+
mode_str = "WARMUP" if snap.is_warmup else "EMIT "
|
| 297 |
+
pop_str = repr(tokenizer.id_to_piece(snap.emitted_token).strip() or tokenizer.id_to_piece(snap.emitted_token))
|
| 298 |
+
B = len(snap.slot_top1_tokens)
|
| 299 |
+
front_str = " ".join(tokenizer.id_to_piece(t).strip() for t in snap.slot_top1_tokens[1 : min(5, B)])
|
| 300 |
+
mid_str = " ".join(tokenizer.id_to_piece(t).strip() for t in snap.slot_top1_tokens[min(15, B - 1) : min(19, B)])
|
| 301 |
+
back_str = " ".join(tokenizer.id_to_piece(t).strip() for t in snap.slot_top1_tokens[max(0, B - 5) : B])
|
| 302 |
+
F_slots = min(model.front_slots, B)
|
| 303 |
+
back_pause = (
|
| 304 |
+
100.0 * (1.0 - sum(snap.slot_shift_probs[F_slots:]) / max(1, B - F_slots))
|
| 305 |
+
if B > F_slots
|
| 306 |
+
else 0.0
|
| 307 |
+
)
|
| 308 |
+
print(
|
| 309 |
+
f"{snap.tick:4d} | {mode_str} | {pop_str:<14} | {front_str[:30]:<30} | "
|
| 310 |
+
f"{mid_str[:24]:<24} | {back_str[:22]:<22} ({back_pause:2.0f}% paused)"
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
print("-" * 108)
|
| 314 |
+
print(f"FULL OUTPUT: {prompt_decoded}{tokenizer.decode(emitted_ids)}")
|
| 315 |
+
print("=" * 108)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
if __name__ == "__main__":
|
| 319 |
+
main()
|
src/conveyer/live.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import sys
|
| 5 |
+
import time
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
from conveyer.checkpoint import load_checkpoint
|
| 9 |
+
from conveyer.config import load_config
|
| 10 |
+
from conveyer.data import build_dataset_and_tokenizer
|
| 11 |
+
from conveyer.infer import TickSnapshot, stream_conveyer_generation
|
| 12 |
+
from conveyer.model import ConveyerDiffusionLM
|
| 13 |
+
|
| 14 |
+
RESET = "\033[0m"
|
| 15 |
+
BOLD = "\033[1m"
|
| 16 |
+
DIM = "\033[2m"
|
| 17 |
+
GREEN = "\033[92m"
|
| 18 |
+
CYAN = "\033[96m"
|
| 19 |
+
YELLOW = "\033[93m"
|
| 20 |
+
MAGENTA = "\033[95m"
|
| 21 |
+
GRAY = "\033[90m"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _format_slot_badge(slot_idx: int, piece: str, prob: float) -> str:
|
| 25 |
+
clean = piece.replace("\n", " ")[:9]
|
| 26 |
+
if slot_idx == 0:
|
| 27 |
+
return f"{BOLD}{GREEN}[0:{clean} {prob*100:2.0f}%]{RESET}"
|
| 28 |
+
elif slot_idx < 15:
|
| 29 |
+
return f"{GREEN}{clean}({prob*100:2.0f}%){RESET}"
|
| 30 |
+
elif slot_idx < 40:
|
| 31 |
+
return f"{YELLOW}{clean}({prob*100:2.0f}%){RESET}"
|
| 32 |
+
else:
|
| 33 |
+
return f"{GRAY}{clean}{RESET}"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def render_frame(
|
| 37 |
+
prompt_str: str,
|
| 38 |
+
committed_pieces: list[str],
|
| 39 |
+
snap: TickSnapshot,
|
| 40 |
+
tokenizer: Any,
|
| 41 |
+
clear_screen: bool = True,
|
| 42 |
+
) -> str:
|
| 43 |
+
B = len(snap.slot_top1_tokens)
|
| 44 |
+
lines: list[str] = []
|
| 45 |
+
if clear_screen:
|
| 46 |
+
lines.append("\033[2J\033[H")
|
| 47 |
+
lines.append("=" * 98)
|
| 48 |
+
mode_tag = (
|
| 49 |
+
f"{CYAN}PRE-BAKING WARMUP (Clamping Input -> Preparing Conveyor Belt){RESET}"
|
| 50 |
+
if snap.is_warmup
|
| 51 |
+
else f"{GREEN}LIVE CONVEYOR BAKING (Serving Slot 0 -> Shifting 1..63 -> Spawning 63){RESET}"
|
| 52 |
+
)
|
| 53 |
+
lines.append(f"{BOLD}64-SLOT DUAL-CHANNEL CONVEYOR OVEN LLM{RESET} | Tick t={snap.tick:3d} | {mode_tag}")
|
| 54 |
+
lines.append("-" * 98)
|
| 55 |
+
lines.append(f"{BOLD}Input Prompt :{RESET} {prompt_str}")
|
| 56 |
+
committed_str = " ".join(committed_pieces) if committed_pieces else "(warming up belt...)"
|
| 57 |
+
lines.append(f"{BOLD}Served Output:{RESET} {BOLD}{GREEN}{committed_str}{RESET}")
|
| 58 |
+
lines.append("-" * 98)
|
| 59 |
+
|
| 60 |
+
# Zone 1: Front of Belt (0 .. 14) - Final Grammatical Polish
|
| 61 |
+
front_items = [
|
| 62 |
+
_format_slot_badge(k, tokenizer.id_to_piece(snap.slot_top1_tokens[k]), snap.slot_top1_probs[k])
|
| 63 |
+
for k in range(min(15, B))
|
| 64 |
+
]
|
| 65 |
+
lines.append(f"{BOLD}{GREEN}FRONT [00..14] (Final Polish, alpha->0):{RESET}")
|
| 66 |
+
lines.append(" <- POP " + " | ".join(front_items[:8]))
|
| 67 |
+
if len(front_items) > 8:
|
| 68 |
+
lines.append(" " + " | ".join(front_items[8:15]))
|
| 69 |
+
|
| 70 |
+
# Zone 2: Middle of Belt (15 .. 39) - Firming Up Phrasing
|
| 71 |
+
if B > 15:
|
| 72 |
+
mid_indices = list(range(15, min(40, B), 2))
|
| 73 |
+
mid_items = [
|
| 74 |
+
_format_slot_badge(k, tokenizer.id_to_piece(snap.slot_top1_tokens[k]), snap.slot_top1_probs[k])
|
| 75 |
+
for k in mid_indices[:10]
|
| 76 |
+
]
|
| 77 |
+
lines.append(f"{BOLD}{YELLOW}MIDDLE [15..39] (Firming Phrasing, Dual-Channel):{RESET}")
|
| 78 |
+
lines.append(" " + " | ".join(mid_items))
|
| 79 |
+
|
| 80 |
+
# Zone 3: Back of Belt (40 .. 63) - Rough Ideas & Fresh Raw Dough
|
| 81 |
+
if B > 40:
|
| 82 |
+
back_indices = list(range(40, B, 2))
|
| 83 |
+
if (B - 1) not in back_indices:
|
| 84 |
+
back_indices.append(B - 1)
|
| 85 |
+
back_items = [
|
| 86 |
+
f"[{k}:{_format_slot_badge(k, tokenizer.id_to_piece(snap.slot_top1_tokens[k]), snap.slot_top1_probs[k])}]"
|
| 87 |
+
for k in back_indices[-8:]
|
| 88 |
+
]
|
| 89 |
+
lines.append(f"{BOLD}{MAGENTA}BACK [40..63] (Rough Ideas <- Slot 63 Raw Dough Spawn):{RESET}")
|
| 90 |
+
lines.append(" " + " ".join(back_items) + " <- [NEW DOUGH]")
|
| 91 |
+
|
| 92 |
+
# Telemetry bar: Entropy & Gate Retention
|
| 93 |
+
ent_front = sum(snap.slot_entropies[: min(8, B)]) / max(1, min(8, B))
|
| 94 |
+
ent_mid = sum(snap.slot_entropies[min(15, B - 1) : min(40, B)]) / max(1, min(40, B) - min(15, B - 1))
|
| 95 |
+
ent_back = sum(snap.slot_entropies[max(0, B - 16) : B]) / max(1, B - max(0, B - 16))
|
| 96 |
+
gate_avg = sum(snap.slot_gate_means) / max(1, B)
|
| 97 |
+
lines.append("-" * 98)
|
| 98 |
+
lines.append(
|
| 99 |
+
f"Entropy (Front/Mid/Back): {ent_front:.2f} / {ent_mid:.2f} / {ent_back:.2f} nats | "
|
| 100 |
+
f"Mean Thought Gate g_k: {gate_avg:.3f}"
|
| 101 |
+
)
|
| 102 |
+
lines.append("=" * 98)
|
| 103 |
+
return "\n".join(lines)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def main() -> None:
|
| 107 |
+
parser = argparse.ArgumentParser(description="Live Terminal Visualizer for the 64-Slot Conveyor-Belt Diffusion LLM")
|
| 108 |
+
parser.add_argument("--ckpt", type=str, default=None, help="Checkpoint path (.pt). If omitted, initializes from config")
|
| 109 |
+
parser.add_argument("--config", type=str, default="configs/synthetic.yaml", help="Config path if --ckpt is not provided")
|
| 110 |
+
parser.add_argument("--prompt", type=str, default=None, help="Custom input prompt")
|
| 111 |
+
parser.add_argument("--tokens", type=int, default=24, help="Number of output tokens to bake")
|
| 112 |
+
parser.add_argument("--overlap", type=int, default=12, help="Prompt-overlap pre-baking window (n - W)")
|
| 113 |
+
parser.add_argument("--bake-steps", type=int, default=64, help="Conveyor belt size (1..64)")
|
| 114 |
+
parser.add_argument("--hz", type=float, default=10.0, help="Refresh rate in ticks/sec (use 0 for no delay)")
|
| 115 |
+
parser.add_argument("--no-clear", action="store_true", help="Print frames sequentially without ANSI screen clear")
|
| 116 |
+
args = parser.parse_args()
|
| 117 |
+
|
| 118 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 119 |
+
if args.ckpt is not None:
|
| 120 |
+
model, cfg, _ = load_checkpoint(args.ckpt, device=device)
|
| 121 |
+
else:
|
| 122 |
+
cfg = load_config(args.config)
|
| 123 |
+
model = ConveyerDiffusionLM(cfg.model).to(device)
|
| 124 |
+
|
| 125 |
+
_, val_ds, tokenizer = build_dataset_and_tokenizer(cfg)
|
| 126 |
+
|
| 127 |
+
if args.prompt is not None:
|
| 128 |
+
prompt_ids = tokenizer.encode(args.prompt)
|
| 129 |
+
prompt_tensor = torch.tensor([prompt_ids], dtype=torch.long, device=device)
|
| 130 |
+
else:
|
| 131 |
+
sample = val_ds[0]
|
| 132 |
+
p_len = int(sample["prompt_len"])
|
| 133 |
+
prompt_tensor = sample["input_ids"][:p_len].unsqueeze(0).to(device)
|
| 134 |
+
|
| 135 |
+
prompt_str = tokenizer.decode(prompt_tensor[0].tolist())
|
| 136 |
+
committed: list[str] = []
|
| 137 |
+
delay = (1.0 / args.hz) if args.hz > 0 else 0.0
|
| 138 |
+
|
| 139 |
+
for tok, snap in stream_conveyer_generation(
|
| 140 |
+
model=model,
|
| 141 |
+
prompt_tokens=prompt_tensor,
|
| 142 |
+
max_new_tokens=args.tokens,
|
| 143 |
+
prompt_overlap=args.overlap,
|
| 144 |
+
bake_steps=args.bake_steps,
|
| 145 |
+
temperature=0.0,
|
| 146 |
+
eos_token_id=tokenizer.eos_id,
|
| 147 |
+
):
|
| 148 |
+
if not snap.is_warmup:
|
| 149 |
+
committed.append(tokenizer.id_to_piece(int(tok[0].item())))
|
| 150 |
+
frame = render_frame(
|
| 151 |
+
prompt_str=prompt_str,
|
| 152 |
+
committed_pieces=committed,
|
| 153 |
+
snap=snap,
|
| 154 |
+
tokenizer=tokenizer,
|
| 155 |
+
clear_screen=(not args.no_clear),
|
| 156 |
+
)
|
| 157 |
+
sys.stdout.write(frame + "\n")
|
| 158 |
+
sys.stdout.flush()
|
| 159 |
+
if delay > 0:
|
| 160 |
+
time.sleep(delay)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
if __name__ == "__main__":
|
| 164 |
+
main()
|
src/conveyer/losses.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from typing import Optional
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
|
| 8 |
+
from conveyer.config import ConveyerConfig
|
| 9 |
+
from conveyer.model import BeltState, ConveyerDiffusionLM, PrefixKVCache
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class UnrollMetrics:
|
| 14 |
+
loss: torch.Tensor
|
| 15 |
+
slot0_loss: float
|
| 16 |
+
slot0_acc: float
|
| 17 |
+
belt_mean_loss: float
|
| 18 |
+
belt_mean_acc: float
|
| 19 |
+
per_slot_acc: torch.Tensor # (B,) accuracy at each belt position k=0..B-1
|
| 20 |
+
per_slot_entropy: torch.Tensor # (B,) entropy at each belt position k=0..B-1
|
| 21 |
+
mean_gate: float
|
| 22 |
+
mean_plan_pause: float = 0.0 # Mean pause rate (1 - s_k) on the back 75% planning belt
|
| 23 |
+
prefix_lm_loss: float = 0.0 # Causal next-token CE loss across the prompt/full sequence
|
| 24 |
+
prefix_lm_acc: float = 0.0 # Causal next-token accuracy across the prompt/full sequence
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def build_horizon_weights(
|
| 28 |
+
belt_size: int,
|
| 29 |
+
w_min: float,
|
| 30 |
+
w_beta: float,
|
| 31 |
+
device: torch.device,
|
| 32 |
+
) -> torch.Tensor:
|
| 33 |
+
k = torch.arange(belt_size, device=device, dtype=torch.float32)
|
| 34 |
+
ratio = k / max(belt_size - 1, 1)
|
| 35 |
+
w = w_min + (1.0 - w_min) * ((1.0 - ratio) ** w_beta)
|
| 36 |
+
return w / w.sum()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def gather_belt_targets(
|
| 40 |
+
seq_tokens: torch.Tensor,
|
| 41 |
+
tick: int,
|
| 42 |
+
offsets: torch.Tensor,
|
| 43 |
+
pad_token_id: int = 0,
|
| 44 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 45 |
+
"""
|
| 46 |
+
Gathers target tokens `x_{tick + offsets[k]}` for each belt slot `k = 0..B-1`:
|
| 47 |
+
- Front 25% Conveyor Slots (k = 0 .. F-1): `offsets[k] = k` (immediate token stream `t + k`)
|
| 48 |
+
- Back 75% Planning Slots (k = F .. B-1): `offsets[k] = F + (k - F) * stride` (longer-range sentence plan!)
|
| 49 |
+
Returns `(targets, in_bounds_mask)` of shape `(N, B)`.
|
| 50 |
+
"""
|
| 51 |
+
bsz, seq_len = seq_tokens.shape
|
| 52 |
+
device = seq_tokens.device
|
| 53 |
+
pos = tick + offsets.to(device) # (B,)
|
| 54 |
+
in_bounds = (pos >= 0) & (pos < seq_len) # (B,)
|
| 55 |
+
clamped_pos = pos.clamp(min=0, max=seq_len - 1).unsqueeze(0).expand(bsz, -1) # (N, B)
|
| 56 |
+
gathered = torch.gather(seq_tokens, dim=1, index=clamped_pos)
|
| 57 |
+
pad_tensor = torch.full_like(gathered, pad_token_id)
|
| 58 |
+
targets = torch.where(in_bounds.unsqueeze(0), gathered, pad_tensor)
|
| 59 |
+
return targets, in_bounds.unsqueeze(0).expand(bsz, -1)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def seed_corrupted_belt(
|
| 63 |
+
model: ConveyerDiffusionLM,
|
| 64 |
+
target_tokens: torch.Tensor,
|
| 65 |
+
tick_start: int,
|
| 66 |
+
sigma_max: float = 3.5,
|
| 67 |
+
sigma_eta: float = 1.25,
|
| 68 |
+
cold_drop_prob: float = 0.85,
|
| 69 |
+
prompt_lengths: Optional[torch.Tensor] = None,
|
| 70 |
+
) -> BeltState:
|
| 71 |
+
bsz, B = target_tokens.shape
|
| 72 |
+
device = target_tokens.device
|
| 73 |
+
V = model.vocab_size
|
| 74 |
+
|
| 75 |
+
k = torch.arange(B, device=device, dtype=torch.float32)
|
| 76 |
+
ratio = k / max(B - 1, 1)
|
| 77 |
+
|
| 78 |
+
signal_strength = 3.0 * ((1.0 - ratio) ** (sigma_eta * 2.0))
|
| 79 |
+
signal_strength[-1] = 0.0
|
| 80 |
+
sigma_k = 0.5 + sigma_max * (ratio ** sigma_eta)
|
| 81 |
+
|
| 82 |
+
one_hot = F.one_hot(target_tokens.clamp(min=0, max=V - 1), num_classes=V).float()
|
| 83 |
+
noise = torch.randn((bsz, B, V), device=device, dtype=torch.float32)
|
| 84 |
+
tau = model.tau_schedule[:B].view(1, B, 1)
|
| 85 |
+
|
| 86 |
+
noisy_logits = signal_strength.view(1, B, 1) * one_hot + sigma_k.view(1, B, 1) * noise
|
| 87 |
+
tilde_p = F.softmax(noisy_logits / tau, dim=-1).type_as(model.tok_emb.weight)
|
| 88 |
+
e_soft = torch.matmul(tilde_p, model.tok_emb.weight)
|
| 89 |
+
|
| 90 |
+
_, m_thought = model.spawn_raw_dough(batch_size=bsz, num_slots=B, device=device)
|
| 91 |
+
|
| 92 |
+
effective_cold_prob = 1.0 if (not model.training and cold_drop_prob > 0.0) else cold_drop_prob
|
| 93 |
+
if effective_cold_prob > 0.0:
|
| 94 |
+
if effective_cold_prob >= 1.0:
|
| 95 |
+
cold_mask = torch.ones((bsz, 1, 1), device=device, dtype=torch.bool)
|
| 96 |
+
else:
|
| 97 |
+
cold_mask = torch.rand((bsz, 1, 1), device=device) < effective_cold_prob
|
| 98 |
+
e_cold, m_cold = model.spawn_raw_dough(batch_size=bsz, num_slots=B, device=device)
|
| 99 |
+
e_soft = torch.where(cold_mask, e_cold, e_soft)
|
| 100 |
+
m_thought = torch.where(cold_mask, m_cold, m_thought)
|
| 101 |
+
uniform_p = torch.full_like(tilde_p, 1.0 / V)
|
| 102 |
+
tilde_p = torch.where(cold_mask, uniform_p, tilde_p)
|
| 103 |
+
|
| 104 |
+
if prompt_lengths is not None:
|
| 105 |
+
offsets = model.get_slot_target_offsets(active_slots=B).to(device)
|
| 106 |
+
pos = tick_start + offsets.unsqueeze(0) # (1, B)
|
| 107 |
+
is_prompt = pos < prompt_lengths.unsqueeze(1) # (N, B)
|
| 108 |
+
prompt_emb = model.tok_emb(target_tokens.clamp(min=0, max=V - 1))
|
| 109 |
+
e_soft = torch.where(is_prompt.unsqueeze(-1), prompt_emb, e_soft)
|
| 110 |
+
|
| 111 |
+
return BeltState(
|
| 112 |
+
e_soft=e_soft,
|
| 113 |
+
m_thought=m_thought,
|
| 114 |
+
tick=tick_start,
|
| 115 |
+
probs=tilde_p,
|
| 116 |
+
logits=noisy_logits,
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def apply_belt_recovery_perturbation(
|
| 121 |
+
model: ConveyerDiffusionLM,
|
| 122 |
+
belt: BeltState,
|
| 123 |
+
corrupt_rate: float = 0.20,
|
| 124 |
+
prompt_mask: Optional[torch.Tensor] = None,
|
| 125 |
+
) -> BeltState:
|
| 126 |
+
"""
|
| 127 |
+
Belt Recovery Training:
|
| 128 |
+
Randomly corrupts a subset of slots on the active conveyor belt mid-unroll (replacing their
|
| 129 |
+
soft embeddings with random vocabulary embeddings or noise), forcing the next forward pass
|
| 130 |
+
to actively repair and re-synchronize the belt from surrounding slots and the prefix!
|
| 131 |
+
"""
|
| 132 |
+
if corrupt_rate <= 0.0 or not model.training:
|
| 133 |
+
return belt
|
| 134 |
+
bsz, B, _ = belt.e_soft.shape
|
| 135 |
+
device = belt.e_soft.device
|
| 136 |
+
# Select ~corrupt_rate of slots per batch item to perturb
|
| 137 |
+
slot_drop = torch.rand((bsz, B), device=device) < corrupt_rate
|
| 138 |
+
if prompt_mask is not None:
|
| 139 |
+
slot_drop = slot_drop & (~prompt_mask)
|
| 140 |
+
|
| 141 |
+
rand_toks = torch.randint(0, model.vocab_size, (bsz, B), device=device)
|
| 142 |
+
rand_emb = model.tok_emb(rand_toks)
|
| 143 |
+
e_noise, m_noise = model.spawn_raw_dough(batch_size=bsz, num_slots=B, device=device)
|
| 144 |
+
corrupted_e = 0.5 * rand_emb + 0.5 * e_noise
|
| 145 |
+
|
| 146 |
+
mask_exp = slot_drop.unsqueeze(-1)
|
| 147 |
+
e_repaired = torch.where(mask_exp, corrupted_e, belt.e_soft)
|
| 148 |
+
m_repaired = torch.where(mask_exp, m_noise, belt.m_thought)
|
| 149 |
+
return BeltState(
|
| 150 |
+
e_soft=e_repaired,
|
| 151 |
+
m_thought=m_repaired,
|
| 152 |
+
tick=belt.tick,
|
| 153 |
+
probs=belt.probs,
|
| 154 |
+
logits=belt.logits,
|
| 155 |
+
gates=belt.gates,
|
| 156 |
+
shift_probs=belt.shift_probs,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def compute_unrolled_conveyer_loss(
|
| 161 |
+
model: ConveyerDiffusionLM,
|
| 162 |
+
seq_tokens: torch.Tensor,
|
| 163 |
+
cfg: ConveyerConfig,
|
| 164 |
+
prompt_lengths: Optional[torch.Tensor] = None,
|
| 165 |
+
tick_start: Optional[int] = None,
|
| 166 |
+
pad_token_id: int = 0,
|
| 167 |
+
) -> UnrollMetrics:
|
| 168 |
+
bsz, seq_len = seq_tokens.shape
|
| 169 |
+
device = seq_tokens.device
|
| 170 |
+
B = model.belt_size
|
| 171 |
+
F_slots = min(model.front_slots, B)
|
| 172 |
+
U = min(cfg.train.unroll_steps, max(1, seq_len - F_slots))
|
| 173 |
+
offsets = model.get_slot_target_offsets(active_slots=B)
|
| 174 |
+
|
| 175 |
+
if tick_start is None:
|
| 176 |
+
max_t0 = max(1, seq_len - F_slots - U + 1)
|
| 177 |
+
if prompt_lengths is not None:
|
| 178 |
+
min_prompt = int(prompt_lengths.min().item())
|
| 179 |
+
max_prompt = int(prompt_lengths.max().item())
|
| 180 |
+
low = max(1, min_prompt - min(U - 2, cfg.infer.prompt_overlap))
|
| 181 |
+
if torch.rand((), device=device).item() < 0.5:
|
| 182 |
+
high = max(low + 1, min(max_t0 + 1, max_prompt + 16))
|
| 183 |
+
else:
|
| 184 |
+
high = max(low + 1, max_t0 + 1)
|
| 185 |
+
tick_start = int(torch.randint(low, high, (1,), device=device).item())
|
| 186 |
+
else:
|
| 187 |
+
tick_start = int(torch.randint(1, max_t0 + 1, (1,), device=device).item())
|
| 188 |
+
|
| 189 |
+
# 1. Causal pass over the full sequence + extract causal KV cache
|
| 190 |
+
prefix_lm_weight = getattr(cfg.train, "prefix_lm_weight", 1.0)
|
| 191 |
+
full_kv: Optional[PrefixKVCache] = None
|
| 192 |
+
if prefix_lm_weight > 0.0 and seq_len > 1:
|
| 193 |
+
causal_logits, full_kv = model.forward_causal_and_cache(seq_tokens, prefix_len=seq_len)
|
| 194 |
+
shift_logits = causal_logits[:, :-1, :].contiguous()
|
| 195 |
+
shift_targets = seq_tokens[:, 1:].contiguous()
|
| 196 |
+
prefix_lm_loss = F.cross_entropy(
|
| 197 |
+
shift_logits.view(-1, model.vocab_size),
|
| 198 |
+
shift_targets.view(-1),
|
| 199 |
+
ignore_index=pad_token_id,
|
| 200 |
+
)
|
| 201 |
+
with torch.no_grad():
|
| 202 |
+
valid_tok = shift_targets != pad_token_id
|
| 203 |
+
n_valid = valid_tok.float().sum().clamp(min=1.0)
|
| 204 |
+
pred_tok = shift_logits.argmax(dim=-1)
|
| 205 |
+
prefix_lm_acc_t = ((pred_tok == shift_targets) & valid_tok).float().sum() / n_valid
|
| 206 |
+
kv_cache = PrefixKVCache(
|
| 207 |
+
k_cache=[k[:, :, :tick_start, :] for k in full_kv.k_cache],
|
| 208 |
+
v_cache=[v[:, :, :tick_start, :] for v in full_kv.v_cache],
|
| 209 |
+
seq_len=tick_start,
|
| 210 |
+
)
|
| 211 |
+
else:
|
| 212 |
+
prefix_tokens = seq_tokens[:, :tick_start]
|
| 213 |
+
kv_cache = model.prefill_prefix(prefix_tokens)
|
| 214 |
+
prefix_lm_loss = torch.tensor(0.0, device=device)
|
| 215 |
+
prefix_lm_acc_t = torch.tensor(0.0, device=device)
|
| 216 |
+
|
| 217 |
+
# 2. Seed initial 64-slot belt at tick_start using slot target offsets
|
| 218 |
+
init_targets, _ = gather_belt_targets(seq_tokens, tick_start, offsets, pad_token_id=pad_token_id)
|
| 219 |
+
|
| 220 |
+
belt = seed_corrupted_belt(
|
| 221 |
+
model=model,
|
| 222 |
+
target_tokens=init_targets,
|
| 223 |
+
tick_start=tick_start,
|
| 224 |
+
sigma_max=cfg.train.seed_sigma_max,
|
| 225 |
+
sigma_eta=cfg.train.seed_sigma_eta,
|
| 226 |
+
cold_drop_prob=0.85,
|
| 227 |
+
prompt_lengths=prompt_lengths,
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
horizon_w = build_horizon_weights(
|
| 231 |
+
belt_size=B,
|
| 232 |
+
w_min=cfg.train.loss_w_min,
|
| 233 |
+
w_beta=cfg.train.loss_w_beta,
|
| 234 |
+
device=device,
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
total_loss = torch.tensor(0.0, device=device)
|
| 238 |
+
prev_logits: Optional[torch.Tensor] = None
|
| 239 |
+
slot0_loss_sum_t = torch.tensor(0.0, device=device)
|
| 240 |
+
slot0_acc_sum_t = torch.tensor(0.0, device=device)
|
| 241 |
+
s0_valid_ticks_t = torch.tensor(0.0, device=device)
|
| 242 |
+
belt_loss_sum_t = torch.tensor(0.0, device=device)
|
| 243 |
+
belt_acc_sum_t = torch.tensor(0.0, device=device)
|
| 244 |
+
gate_sum_t = torch.tensor(0.0, device=device)
|
| 245 |
+
pause_sum_t = torch.tensor(0.0, device=device)
|
| 246 |
+
valid_ticks = 0
|
| 247 |
+
dagger_diverged = False
|
| 248 |
+
|
| 249 |
+
per_slot_correct = torch.zeros(B, device=device)
|
| 250 |
+
per_slot_count = torch.zeros(B, device=device)
|
| 251 |
+
per_slot_entropy_sum = torch.zeros(B, device=device)
|
| 252 |
+
|
| 253 |
+
for u in range(U):
|
| 254 |
+
t = tick_start + u
|
| 255 |
+
if t >= seq_len:
|
| 256 |
+
break
|
| 257 |
+
|
| 258 |
+
targets, in_bounds = gather_belt_targets(seq_tokens, t, offsets, pad_token_id=pad_token_id)
|
| 259 |
+
slot_positions = t + offsets.to(device).unsqueeze(0) # (1, B)
|
| 260 |
+
|
| 261 |
+
# Mid-unroll Belt Recovery Training: occasionally inject wrong/noisy thoughts onto the belt
|
| 262 |
+
# so the next forward pass learns to actively repair the belt!
|
| 263 |
+
if u > 0 and model.training and cfg.train.belt_corrupt_prob > 0.0:
|
| 264 |
+
if torch.rand(()) < 0.35:
|
| 265 |
+
p_mask = (slot_positions < prompt_lengths.unsqueeze(1)) if prompt_lengths is not None else None
|
| 266 |
+
belt = apply_belt_recovery_perturbation(
|
| 267 |
+
model=model,
|
| 268 |
+
belt=belt,
|
| 269 |
+
corrupt_rate=cfg.train.belt_corrupt_prob,
|
| 270 |
+
prompt_mask=p_mask,
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
step_out = model.forward_belt(belt=belt, kv_cache=kv_cache)
|
| 274 |
+
|
| 275 |
+
valid_mask = in_bounds & (targets != pad_token_id) # (N, B)
|
| 276 |
+
if prompt_lengths is not None:
|
| 277 |
+
is_gen = slot_positions >= prompt_lengths.unsqueeze(1)
|
| 278 |
+
valid_mask = valid_mask & is_gen
|
| 279 |
+
|
| 280 |
+
if cfg.model.causal_belt_only:
|
| 281 |
+
ar_mask = torch.zeros_like(valid_mask)
|
| 282 |
+
ar_mask[:, 0] = valid_mask[:, 0]
|
| 283 |
+
valid_mask = ar_mask
|
| 284 |
+
|
| 285 |
+
ce_per_token = F.cross_entropy(
|
| 286 |
+
step_out.logits.reshape(-1, model.vocab_size),
|
| 287 |
+
targets.reshape(-1),
|
| 288 |
+
reduction="none",
|
| 289 |
+
).view(bsz, B)
|
| 290 |
+
|
| 291 |
+
w_broadcast = horizon_w.unsqueeze(0) * valid_mask.float()
|
| 292 |
+
w_denom = w_broadcast.sum().clamp(min=1e-6)
|
| 293 |
+
tick_ce_loss = (ce_per_token * w_broadcast).sum() / w_denom
|
| 294 |
+
|
| 295 |
+
probs_clamped = step_out.probs.clamp(min=1e-8)
|
| 296 |
+
entropy = -(probs_clamped * probs_clamped.log()).sum(dim=-1)
|
| 297 |
+
if cfg.train.entropy_reg > 0.0 and not cfg.model.causal_belt_only:
|
| 298 |
+
back_weights = (torch.arange(B, device=device, dtype=torch.float32) / max(B - 1, 1)).unsqueeze(0)
|
| 299 |
+
ent_bonus = (entropy * back_weights * valid_mask.float()).sum() / valid_mask.float().sum().clamp(min=1.0)
|
| 300 |
+
tick_loss = tick_ce_loss - cfg.train.entropy_reg * ent_bonus
|
| 301 |
+
else:
|
| 302 |
+
tick_loss = tick_ce_loss
|
| 303 |
+
|
| 304 |
+
# Shift-Consistency on the Front Conveyor Stream (0 .. F_slots - 2)
|
| 305 |
+
if u > 0 and prev_logits is not None and not cfg.model.causal_belt_only and F_slots > 1:
|
| 306 |
+
p_prev = F.softmax(prev_logits[:, 1:F_slots, :].detach(), dim=-1)
|
| 307 |
+
log_p_curr = F.log_softmax(step_out.logits[:, : F_slots - 1, :], dim=-1)
|
| 308 |
+
kl_shift = F.kl_div(log_p_curr, p_prev, reduction="none").sum(dim=-1)
|
| 309 |
+
cons_mask = valid_mask[:, : F_slots - 1].float()
|
| 310 |
+
cons_loss = (kl_shift * cons_mask).sum() / cons_mask.sum().clamp(min=1.0)
|
| 311 |
+
tick_loss = tick_loss + 0.08 * cons_loss
|
| 312 |
+
|
| 313 |
+
prev_logits = step_out.logits
|
| 314 |
+
total_loss = total_loss + tick_loss
|
| 315 |
+
valid_ticks += 1
|
| 316 |
+
|
| 317 |
+
with torch.no_grad():
|
| 318 |
+
valid_f = valid_mask.float()
|
| 319 |
+
preds = step_out.logits.argmax(dim=-1)
|
| 320 |
+
correct = (preds == targets).float() * valid_f
|
| 321 |
+
|
| 322 |
+
s0_valid = valid_f[:, 0]
|
| 323 |
+
s0_sum = s0_valid.sum()
|
| 324 |
+
has_s0 = (s0_sum > 0.0).float()
|
| 325 |
+
s0_denom = s0_sum.clamp(min=1.0)
|
| 326 |
+
slot0_loss_sum_t = slot0_loss_sum_t + has_s0 * ((ce_per_token[:, 0] * s0_valid).sum() / s0_denom)
|
| 327 |
+
slot0_acc_sum_t = slot0_acc_sum_t + has_s0 * (correct[:, 0].sum() / s0_denom)
|
| 328 |
+
s0_valid_ticks_t = s0_valid_ticks_t + has_s0
|
| 329 |
+
|
| 330 |
+
all_denom = valid_f.sum().clamp(min=1.0)
|
| 331 |
+
belt_loss_sum_t = belt_loss_sum_t + (ce_per_token * valid_f).sum() / all_denom
|
| 332 |
+
belt_acc_sum_t = belt_acc_sum_t + correct.sum() / all_denom
|
| 333 |
+
gate_sum_t = gate_sum_t + step_out.gate.mean()
|
| 334 |
+
if B > F_slots:
|
| 335 |
+
pause_sum_t = pause_sum_t + (1.0 - step_out.shift_prob[:, F_slots:]).mean()
|
| 336 |
+
|
| 337 |
+
per_slot_correct += correct.sum(dim=0)
|
| 338 |
+
per_slot_count += valid_f.sum(dim=0)
|
| 339 |
+
per_slot_entropy_sum += entropy.mean(dim=0)
|
| 340 |
+
|
| 341 |
+
if u < U - 1 and (t + 1) < seq_len:
|
| 342 |
+
# Prefix Recovery (DAgger-style Scheduled Sampling on final ticks, or fast slice from full_kv):
|
| 343 |
+
use_dagger = (
|
| 344 |
+
model.training
|
| 345 |
+
and (u >= max(0, U - 4) or full_kv is None)
|
| 346 |
+
and (torch.rand(()) < cfg.train.recovery_prob)
|
| 347 |
+
)
|
| 348 |
+
if use_dagger:
|
| 349 |
+
pred_s0 = step_out.slot0_logits.argmax(dim=-1)
|
| 350 |
+
if prompt_lengths is not None:
|
| 351 |
+
is_p = t < prompt_lengths
|
| 352 |
+
committed_x_t = torch.where(is_p, seq_tokens[:, t], pred_s0)
|
| 353 |
+
else:
|
| 354 |
+
committed_x_t = pred_s0
|
| 355 |
+
dagger_diverged = True
|
| 356 |
+
|
| 357 |
+
if not dagger_diverged and full_kv is not None:
|
| 358 |
+
next_len = t + 1
|
| 359 |
+
kv_cache = PrefixKVCache(
|
| 360 |
+
k_cache=[k[:, :, :next_len, :] for k in full_kv.k_cache],
|
| 361 |
+
v_cache=[v[:, :, :next_len, :] for v in full_kv.v_cache],
|
| 362 |
+
seq_len=next_len,
|
| 363 |
+
)
|
| 364 |
+
else:
|
| 365 |
+
if not use_dagger:
|
| 366 |
+
committed_x_t = seq_tokens[:, t]
|
| 367 |
+
kv_cache = model.append_committed_token(committed_x_t, kv_cache)
|
| 368 |
+
|
| 369 |
+
next_targets, _ = gather_belt_targets(seq_tokens, t + 1, offsets, pad_token_id=pad_token_id)
|
| 370 |
+
if prompt_lengths is not None:
|
| 371 |
+
next_pos = (t + 1) + offsets.to(device).unsqueeze(0)
|
| 372 |
+
clamp_mask = next_pos < prompt_lengths.unsqueeze(1)
|
| 373 |
+
else:
|
| 374 |
+
clamp_mask = None
|
| 375 |
+
|
| 376 |
+
belt = model.shift_belt_left(
|
| 377 |
+
step_out=step_out,
|
| 378 |
+
current_tick=t,
|
| 379 |
+
deterministic_spawn=False,
|
| 380 |
+
clamp_tokens=next_targets if clamp_mask is not None else None,
|
| 381 |
+
clamp_mask=clamp_mask,
|
| 382 |
+
)
|
| 383 |
+
if cfg.train.no_belt_bptt:
|
| 384 |
+
belt = belt.detach()
|
| 385 |
+
kv_cache = kv_cache.detach()
|
| 386 |
+
|
| 387 |
+
denom = max(valid_ticks, 1)
|
| 388 |
+
s0_denom_ticks = float(s0_valid_ticks_t.item()) if float(s0_valid_ticks_t.item()) > 0.0 else 1.0
|
| 389 |
+
per_slot_acc = per_slot_correct / per_slot_count.clamp(min=1.0)
|
| 390 |
+
per_slot_entropy = per_slot_entropy_sum / denom
|
| 391 |
+
conveyer_loss = total_loss / denom
|
| 392 |
+
combined_loss = conveyer_loss + prefix_lm_weight * prefix_lm_loss
|
| 393 |
+
|
| 394 |
+
return UnrollMetrics(
|
| 395 |
+
loss=combined_loss,
|
| 396 |
+
slot0_loss=float(slot0_loss_sum_t.item()) / s0_denom_ticks,
|
| 397 |
+
slot0_acc=float(slot0_acc_sum_t.item()) / s0_denom_ticks,
|
| 398 |
+
belt_mean_loss=float(belt_loss_sum_t.item()) / denom,
|
| 399 |
+
belt_mean_acc=float(belt_acc_sum_t.item()) / denom,
|
| 400 |
+
per_slot_acc=per_slot_acc,
|
| 401 |
+
per_slot_entropy=per_slot_entropy,
|
| 402 |
+
mean_gate=float(gate_sum_t.item()) / denom,
|
| 403 |
+
mean_plan_pause=float(pause_sum_t.item()) / denom,
|
| 404 |
+
prefix_lm_loss=float(prefix_lm_loss.item()),
|
| 405 |
+
prefix_lm_acc=float(prefix_lm_acc_t.item()),
|
| 406 |
+
)
|
src/conveyer/model.py
ADDED
|
@@ -0,0 +1,739 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from typing import Optional
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from torch.utils.checkpoint import checkpoint
|
| 9 |
+
|
| 10 |
+
from conveyer.config import ModelConfig
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class RMSNorm(nn.Module):
|
| 14 |
+
def __init__(self, d_model: int, eps: float = 1e-6) -> None:
|
| 15 |
+
super().__init__()
|
| 16 |
+
self.eps = eps
|
| 17 |
+
self.weight = nn.Parameter(torch.ones(d_model))
|
| 18 |
+
|
| 19 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 20 |
+
norm = x.float().pow(2).mean(dim=-1, keepdim=True)
|
| 21 |
+
x_normed = x.float() * torch.rsqrt(norm + self.eps)
|
| 22 |
+
return (self.weight * x_normed).type_as(x)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class RotaryEmbedding(nn.Module):
|
| 26 |
+
def __init__(self, head_dim: int, max_seq_len: int = 2048, theta: float = 10000.0) -> None:
|
| 27 |
+
super().__init__()
|
| 28 |
+
assert head_dim % 2 == 0, "head_dim must be even for RoPE"
|
| 29 |
+
self.head_dim = head_dim
|
| 30 |
+
self.max_seq_len = max_seq_len
|
| 31 |
+
self.theta = theta
|
| 32 |
+
inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2).float() / head_dim))
|
| 33 |
+
self.register_buffer("inv_freq", inv_freq, persistent=False)
|
| 34 |
+
self._build_cache(max_seq_len)
|
| 35 |
+
|
| 36 |
+
def _build_cache(self, seq_len: int) -> None:
|
| 37 |
+
self.max_seq_len = max(self.max_seq_len, seq_len)
|
| 38 |
+
t = torch.arange(self.max_seq_len, device=self.inv_freq.device, dtype=torch.float32)
|
| 39 |
+
freqs = torch.outer(t, self.inv_freq) # (seq_len, head_dim // 2)
|
| 40 |
+
cos = torch.cos(freqs)
|
| 41 |
+
sin = torch.sin(freqs)
|
| 42 |
+
self.register_buffer("cos_cached", cos, persistent=False)
|
| 43 |
+
self.register_buffer("sin_cached", sin, persistent=False)
|
| 44 |
+
|
| 45 |
+
def get_cos_sin(self, positions: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 46 |
+
"""positions: (L,) or (N, L) integer positions."""
|
| 47 |
+
max_pos = int(positions.max().item()) + 1 if positions.numel() > 0 else 1
|
| 48 |
+
if max_pos > self.cos_cached.size(0):
|
| 49 |
+
self._build_cache(max_pos + 256)
|
| 50 |
+
cos = self.cos_cached[positions]
|
| 51 |
+
sin = self.sin_cached[positions]
|
| 52 |
+
return cos, sin
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
|
| 56 |
+
"""
|
| 57 |
+
x: (N, H, L, D)
|
| 58 |
+
cos, sin: (L, D//2) or (N, L, D//2)
|
| 59 |
+
"""
|
| 60 |
+
if cos.dim() == 2:
|
| 61 |
+
cos = cos.unsqueeze(0).unsqueeze(0) # (1, 1, L, D//2)
|
| 62 |
+
sin = sin.unsqueeze(0).unsqueeze(0)
|
| 63 |
+
elif cos.dim() == 3:
|
| 64 |
+
cos = cos.unsqueeze(1) # (N, 1, L, D//2)
|
| 65 |
+
sin = sin.unsqueeze(1)
|
| 66 |
+
d2 = x.shape[-1] // 2
|
| 67 |
+
x1 = x[..., :d2]
|
| 68 |
+
x2 = x[..., d2:]
|
| 69 |
+
out1 = x1 * cos - x2 * sin
|
| 70 |
+
out2 = x1 * sin + x2 * cos
|
| 71 |
+
return torch.cat([out1, out2], dim=-1).type_as(x)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class SwiGLUFFN(nn.Module):
|
| 75 |
+
def __init__(self, d_model: int, d_ff: int, dropout: float = 0.0) -> None:
|
| 76 |
+
super().__init__()
|
| 77 |
+
self.w_gate = nn.Linear(d_model, d_ff, bias=False)
|
| 78 |
+
self.w_up = nn.Linear(d_model, d_ff, bias=False)
|
| 79 |
+
self.w_down = nn.Linear(d_ff, d_model, bias=False)
|
| 80 |
+
self.drop = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
| 81 |
+
|
| 82 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 83 |
+
return self.drop(self.w_down(F.silu(self.w_gate(x)) * self.w_up(x)))
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@dataclass
|
| 87 |
+
class PrefixKVCache:
|
| 88 |
+
"""Stores committed prefix Keys and Values per layer: list of (K, V) each (N, H, P, D_h)."""
|
| 89 |
+
k_cache: list[torch.Tensor]
|
| 90 |
+
v_cache: list[torch.Tensor]
|
| 91 |
+
seq_len: int
|
| 92 |
+
|
| 93 |
+
@classmethod
|
| 94 |
+
def empty(cls, n_layers: int, batch_size: int, n_heads: int, head_dim: int, device: torch.device, dtype: torch.dtype) -> "PrefixKVCache":
|
| 95 |
+
k_cache = [torch.empty((batch_size, n_heads, 0, head_dim), device=device, dtype=dtype) for _ in range(n_layers)]
|
| 96 |
+
v_cache = [torch.empty((batch_size, n_heads, 0, head_dim), device=device, dtype=dtype) for _ in range(n_layers)]
|
| 97 |
+
return cls(k_cache=k_cache, v_cache=v_cache, seq_len=0)
|
| 98 |
+
|
| 99 |
+
def clone(self) -> "PrefixKVCache":
|
| 100 |
+
return PrefixKVCache(
|
| 101 |
+
k_cache=[k.clone() for k in self.k_cache],
|
| 102 |
+
v_cache=[v.clone() for v in self.v_cache],
|
| 103 |
+
seq_len=self.seq_len,
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
def detach(self) -> "PrefixKVCache":
|
| 107 |
+
return PrefixKVCache(
|
| 108 |
+
k_cache=[k.detach() for k in self.k_cache],
|
| 109 |
+
v_cache=[v.detach() for v in self.v_cache],
|
| 110 |
+
seq_len=self.seq_len,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
@dataclass
|
| 115 |
+
class BeltState:
|
| 116 |
+
"""Holds the 64-slot Dual-Channel + Elastic Planning dough state on the conveyor belt."""
|
| 117 |
+
e_soft: torch.Tensor # (N, B, D) lexical anchor soft embeddings
|
| 118 |
+
m_thought: torch.Tensor # (N, B, D) continuous thought highway vectors
|
| 119 |
+
tick: int # current absolute sequence position of Slot 0
|
| 120 |
+
probs: Optional[torch.Tensor] = None # (N, B, V) last probability distributions
|
| 121 |
+
logits: Optional[torch.Tensor] = None # (N, B, V) last raw logits
|
| 122 |
+
gates: Optional[torch.Tensor] = None # (N, B, D) last update gate activations
|
| 123 |
+
shift_probs: Optional[torch.Tensor] = None # (N, B) elastic shift vs. pause probability s_k
|
| 124 |
+
|
| 125 |
+
def detach(self) -> "BeltState":
|
| 126 |
+
return BeltState(
|
| 127 |
+
e_soft=self.e_soft.detach(),
|
| 128 |
+
m_thought=self.m_thought.detach(),
|
| 129 |
+
tick=self.tick,
|
| 130 |
+
probs=self.probs.detach() if self.probs is not None else None,
|
| 131 |
+
logits=self.logits.detach() if self.logits is not None else None,
|
| 132 |
+
gates=self.gates.detach() if self.gates is not None else None,
|
| 133 |
+
shift_probs=self.shift_probs.detach() if self.shift_probs is not None else None,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@dataclass
|
| 138 |
+
class BeltStepOutput:
|
| 139 |
+
logits: torch.Tensor # (N, B, V) raw vocabulary logits at all 64 slots
|
| 140 |
+
probs: torch.Tensor # (N, B, V) temperature-scaled probabilities p_k
|
| 141 |
+
e_soft: torch.Tensor # (N, B, D) expected vocab embeddings p_k^T E_vocab
|
| 142 |
+
cand_thought: torch.Tensor # (N, B, D) candidate thought vectors c_k
|
| 143 |
+
gate: torch.Tensor # (N, B, D) update gate g_k
|
| 144 |
+
m_updated: torch.Tensor # (N, B, D) updated thought vectors before index shift
|
| 145 |
+
shift_prob: torch.Tensor # (N, B) elastic shift probability s_k (1.0 in front conveyor, <1 in back 75% planning)
|
| 146 |
+
expand_cand: torch.Tensor # (N, D) mid-belt expansion thought vector for forming new sentence clauses
|
| 147 |
+
slot0_logits: torch.Tensor # (N, V) crisp logits at Position 0
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class ConveyerAttention(nn.Module):
|
| 151 |
+
def __init__(self, cfg: ModelConfig) -> None:
|
| 152 |
+
super().__init__()
|
| 153 |
+
assert cfg.d_model % cfg.n_heads == 0
|
| 154 |
+
self.n_heads = cfg.n_heads
|
| 155 |
+
self.head_dim = cfg.d_model // cfg.n_heads
|
| 156 |
+
self.d_model = cfg.d_model
|
| 157 |
+
self.qkv = nn.Linear(cfg.d_model, 3 * cfg.d_model, bias=False)
|
| 158 |
+
self.proj = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
|
| 159 |
+
self.dropout = cfg.dropout
|
| 160 |
+
|
| 161 |
+
def forward(
|
| 162 |
+
self,
|
| 163 |
+
x: torch.Tensor,
|
| 164 |
+
cos: torch.Tensor,
|
| 165 |
+
sin: torch.Tensor,
|
| 166 |
+
attn_mask: Optional[torch.Tensor] = None,
|
| 167 |
+
layer_kv: Optional[tuple[torch.Tensor, torch.Tensor]] = None,
|
| 168 |
+
return_new_kv: bool = False,
|
| 169 |
+
is_causal: bool = False,
|
| 170 |
+
) -> tuple[torch.Tensor, Optional[tuple[torch.Tensor, torch.Tensor]]]:
|
| 171 |
+
"""
|
| 172 |
+
x: (N, L, D)
|
| 173 |
+
layer_kv: optional (K_prev, V_prev) each (N, H, P, D_h)
|
| 174 |
+
"""
|
| 175 |
+
bsz, seq_len, _ = x.shape
|
| 176 |
+
qkv = self.qkv(x)
|
| 177 |
+
q, k, v = qkv.chunk(3, dim=-1)
|
| 178 |
+
q = q.view(bsz, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
|
| 179 |
+
k = k.view(bsz, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
|
| 180 |
+
v = v.view(bsz, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
|
| 181 |
+
|
| 182 |
+
q = apply_rope(q, cos, sin)
|
| 183 |
+
k = apply_rope(k, cos, sin)
|
| 184 |
+
|
| 185 |
+
new_kv = (k, v) if return_new_kv else None
|
| 186 |
+
|
| 187 |
+
if layer_kv is not None and layer_kv[0].size(2) > 0:
|
| 188 |
+
k = torch.cat([layer_kv[0].type_as(k), k], dim=2)
|
| 189 |
+
v = torch.cat([layer_kv[1].type_as(v), v], dim=2)
|
| 190 |
+
|
| 191 |
+
out = F.scaled_dot_product_attention(
|
| 192 |
+
q,
|
| 193 |
+
k,
|
| 194 |
+
v,
|
| 195 |
+
attn_mask=None if is_causal else attn_mask,
|
| 196 |
+
dropout_p=self.dropout if self.training else 0.0,
|
| 197 |
+
is_causal=is_causal,
|
| 198 |
+
)
|
| 199 |
+
out = out.transpose(1, 2).contiguous().view(bsz, seq_len, self.d_model)
|
| 200 |
+
return self.proj(out), new_kv
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class ConveyerBlock(nn.Module):
|
| 204 |
+
def __init__(self, cfg: ModelConfig) -> None:
|
| 205 |
+
super().__init__()
|
| 206 |
+
self.norm1 = RMSNorm(cfg.d_model)
|
| 207 |
+
self.attn = ConveyerAttention(cfg)
|
| 208 |
+
self.norm2 = RMSNorm(cfg.d_model)
|
| 209 |
+
self.ffn = SwiGLUFFN(cfg.d_model, cfg.d_ff, cfg.dropout)
|
| 210 |
+
|
| 211 |
+
def forward(
|
| 212 |
+
self,
|
| 213 |
+
x: torch.Tensor,
|
| 214 |
+
cos: torch.Tensor,
|
| 215 |
+
sin: torch.Tensor,
|
| 216 |
+
attn_mask: Optional[torch.Tensor] = None,
|
| 217 |
+
layer_kv: Optional[tuple[torch.Tensor, torch.Tensor]] = None,
|
| 218 |
+
return_new_kv: bool = False,
|
| 219 |
+
is_causal: bool = False,
|
| 220 |
+
) -> tuple[torch.Tensor, Optional[tuple[torch.Tensor, torch.Tensor]]]:
|
| 221 |
+
attn_out, new_kv = self.attn(
|
| 222 |
+
self.norm1(x),
|
| 223 |
+
cos=cos,
|
| 224 |
+
sin=sin,
|
| 225 |
+
attn_mask=attn_mask,
|
| 226 |
+
layer_kv=layer_kv,
|
| 227 |
+
return_new_kv=return_new_kv,
|
| 228 |
+
is_causal=is_causal,
|
| 229 |
+
)
|
| 230 |
+
x = x + attn_out
|
| 231 |
+
x = x + self.ffn(self.norm2(x))
|
| 232 |
+
return x, new_kv
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
class ConveyerDiffusionLM(nn.Module):
|
| 236 |
+
"""
|
| 237 |
+
64-Slot Dual-Channel Elastic Conveyor-Belt Diffusion Language Model.
|
| 238 |
+
|
| 239 |
+
Input / Output Contract at every tick t:
|
| 240 |
+
- Input : [All Committed Text Prefix x_{0..t-1}] + [64-Slot Conveyor/Planning Belt]
|
| 241 |
+
- Output: [Popped-out Confirmed Token x_t at Slot 0] + [Updated 64-Slot Conveyor/Planning Belt]
|
| 242 |
+
|
| 243 |
+
Two-Zone Human-Style Thinking & Speaking Belt:
|
| 244 |
+
- Front 25% (Slots 0 .. F-1, e.g. 0..15): Immediate Articulation Stream.
|
| 245 |
+
Shifts left by 1 slot every tick (s_k = 1.0), polishing immediate grammar toward Slot 0 (alpha_0 = 0).
|
| 246 |
+
- Back 75% (Slots F .. B-1, e.g. 16..63): Elastic Sentence-Planning Horizon.
|
| 247 |
+
Can pause in-place (s_k < 1.0) to hold long-range sentence goals in mind while new sentence clauses
|
| 248 |
+
bud and expand in the middle of the belt (around Slot F).
|
| 249 |
+
"""
|
| 250 |
+
|
| 251 |
+
def __init__(self, cfg: ModelConfig) -> None:
|
| 252 |
+
super().__init__()
|
| 253 |
+
self.cfg = cfg
|
| 254 |
+
self.belt_size = cfg.belt_size
|
| 255 |
+
self.front_slots = min(cfg.front_conveyer_slots, cfg.belt_size)
|
| 256 |
+
self.d_model = cfg.d_model
|
| 257 |
+
self.vocab_size = cfg.vocab_size
|
| 258 |
+
|
| 259 |
+
# Token embedding & weight-tied LM head
|
| 260 |
+
self.tok_emb = nn.Embedding(cfg.vocab_size, cfg.d_model)
|
| 261 |
+
self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
|
| 262 |
+
self.lm_head.weight = self.tok_emb.weight # weight tying
|
| 263 |
+
|
| 264 |
+
# Learned oven slot embeddings for positions k = 0 .. B-1 on the conveyor belt
|
| 265 |
+
self.slot_emb = nn.Embedding(cfg.belt_size, cfg.d_model)
|
| 266 |
+
# Learned initial thought vector for newly spawned dough at Slot B-1
|
| 267 |
+
self.m_init = nn.Parameter(torch.zeros(cfg.d_model))
|
| 268 |
+
|
| 269 |
+
# Dough input RMSNorm
|
| 270 |
+
self.dough_norm = RMSNorm(cfg.d_model)
|
| 271 |
+
|
| 272 |
+
# Transformer trunk
|
| 273 |
+
self.rope = RotaryEmbedding(
|
| 274 |
+
head_dim=cfg.d_model // cfg.n_heads,
|
| 275 |
+
max_seq_len=cfg.max_seq_len,
|
| 276 |
+
theta=cfg.rope_theta,
|
| 277 |
+
)
|
| 278 |
+
self.blocks = nn.ModuleList([ConveyerBlock(cfg) for _ in range(cfg.n_layers)])
|
| 279 |
+
self.final_norm = RMSNorm(cfg.d_model)
|
| 280 |
+
|
| 281 |
+
# Channel 2: Continuous Thought Highway heads (RecurrentCubeLM style)
|
| 282 |
+
self.thought_cand = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
|
| 283 |
+
self.thought_gate = nn.Linear(cfg.d_model, cfg.d_model, bias=True)
|
| 284 |
+
|
| 285 |
+
# Elastic Planning & Mid-Belt Sentence Expansion Heads
|
| 286 |
+
# shift_head predicts whether each back-75% planning slot shifts left or pauses in place
|
| 287 |
+
self.shift_head = nn.Linear(cfg.d_model, 1, bias=True)
|
| 288 |
+
# expand_head synthesizes a new phrase/clause embedding at the boundary between the
|
| 289 |
+
# paused 75% planning zone and the moving 25% front conveyor
|
| 290 |
+
self.expand_head = nn.Linear(2 * cfg.d_model, cfg.d_model, bias=False)
|
| 291 |
+
|
| 292 |
+
# Precompute positional temperature schedule tau_k, thought mixing alpha_k, and target horizon offsets
|
| 293 |
+
B = cfg.belt_size
|
| 294 |
+
F_slots = self.front_slots
|
| 295 |
+
k_idx = torch.arange(B, dtype=torch.float32)
|
| 296 |
+
ratio = k_idx / max(B - 1, 1)
|
| 297 |
+
tau_schedule = cfg.tau_min + (ratio ** cfg.tau_gamma) * (cfg.tau_max - cfg.tau_min)
|
| 298 |
+
alpha_schedule = ratio.clone() # alpha_0 = 0.0, alpha_{B-1} = 1.0
|
| 299 |
+
|
| 300 |
+
# Target sequence offsets delta_k from current tick t:
|
| 301 |
+
# - Front slots k = 0 .. F-1 map 1-to-1 to immediate tokens t + k
|
| 302 |
+
# - Back 75% planning slots k = F .. B-1 span a wider lookahead horizon (stride >= 1)
|
| 303 |
+
stride = max(1, cfg.plan_horizon_stride) if cfg.use_elastic_planning else 1
|
| 304 |
+
target_offsets = torch.arange(B, dtype=torch.long)
|
| 305 |
+
if B > F_slots and stride > 1:
|
| 306 |
+
back_idx = torch.arange(B - F_slots, dtype=torch.long)
|
| 307 |
+
target_offsets[F_slots:] = F_slots + back_idx * stride
|
| 308 |
+
|
| 309 |
+
# Prior bias for shift_head: front slots always shift (+6.0), back 75% slots default to ~0.35 shift (65% pause)
|
| 310 |
+
shift_prior = torch.zeros(B, dtype=torch.float32)
|
| 311 |
+
shift_prior[:F_slots] = 6.0
|
| 312 |
+
if B > F_slots:
|
| 313 |
+
back_ratio = torch.linspace(0.0, 1.0, B - F_slots)
|
| 314 |
+
shift_prior[F_slots:] = -0.3 - 1.2 * back_ratio # sigmoid(-0.3..-1.5) ~ 0.42 down to 0.18
|
| 315 |
+
|
| 316 |
+
self.register_buffer("tau_schedule", tau_schedule, persistent=False)
|
| 317 |
+
self.register_buffer("alpha_schedule", alpha_schedule, persistent=False)
|
| 318 |
+
self.register_buffer("target_offsets", target_offsets, persistent=False)
|
| 319 |
+
self.register_buffer("shift_prior", shift_prior, persistent=False)
|
| 320 |
+
|
| 321 |
+
self._init_weights()
|
| 322 |
+
|
| 323 |
+
def _init_weights(self) -> None:
|
| 324 |
+
nn.init.normal_(self.tok_emb.weight, mean=0.0, std=0.02)
|
| 325 |
+
nn.init.normal_(self.slot_emb.weight, mean=0.0, std=0.02)
|
| 326 |
+
nn.init.normal_(self.m_init, mean=0.0, std=0.02)
|
| 327 |
+
for m in self.modules():
|
| 328 |
+
if isinstance(m, nn.Linear) and m is not self.lm_head:
|
| 329 |
+
nn.init.normal_(m.weight, mean=0.0, std=0.02)
|
| 330 |
+
if m.bias is not None:
|
| 331 |
+
nn.init.zeros_(m.bias)
|
| 332 |
+
nn.init.constant_(self.thought_gate.bias, self.cfg.gate_bias_init)
|
| 333 |
+
nn.init.zeros_(self.shift_head.weight)
|
| 334 |
+
nn.init.zeros_(self.shift_head.bias)
|
| 335 |
+
|
| 336 |
+
def num_parameters(self, trainable_only: bool = True) -> int:
|
| 337 |
+
return sum(p.numel() for p in self.parameters() if (p.requires_grad or not trainable_only))
|
| 338 |
+
|
| 339 |
+
def get_slot_target_offsets(self, active_slots: Optional[int] = None) -> torch.Tensor:
|
| 340 |
+
B = active_slots if active_slots is not None else self.belt_size
|
| 341 |
+
return self.target_offsets[:B]
|
| 342 |
+
|
| 343 |
+
def init_prefix_cache(self, batch_size: int, device: torch.device, dtype: Optional[torch.dtype] = None) -> PrefixKVCache:
|
| 344 |
+
if dtype is None:
|
| 345 |
+
dtype = self.tok_emb.weight.dtype
|
| 346 |
+
return PrefixKVCache.empty(
|
| 347 |
+
n_layers=self.cfg.n_layers,
|
| 348 |
+
batch_size=batch_size,
|
| 349 |
+
n_heads=self.cfg.n_heads,
|
| 350 |
+
head_dim=self.cfg.d_model // self.cfg.n_heads,
|
| 351 |
+
device=device,
|
| 352 |
+
dtype=dtype,
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
def prefill_prefix(
|
| 356 |
+
self,
|
| 357 |
+
prefix_tokens: torch.Tensor,
|
| 358 |
+
kv_cache: Optional[PrefixKVCache] = None,
|
| 359 |
+
) -> PrefixKVCache:
|
| 360 |
+
"""
|
| 361 |
+
Encodes committed prefix tokens x_{t_start .. t_end} into the causal PrefixKVCache.
|
| 362 |
+
prefix_tokens: (N, P) long tensor.
|
| 363 |
+
"""
|
| 364 |
+
bsz, plen = prefix_tokens.shape
|
| 365 |
+
device = prefix_tokens.device
|
| 366 |
+
if kv_cache is None:
|
| 367 |
+
kv_cache = self.init_prefix_cache(bsz, device)
|
| 368 |
+
if plen == 0:
|
| 369 |
+
return kv_cache
|
| 370 |
+
|
| 371 |
+
start_pos = kv_cache.seq_len
|
| 372 |
+
positions = torch.arange(start_pos, start_pos + plen, device=device, dtype=torch.long)
|
| 373 |
+
cos, sin = self.rope.get_cos_sin(positions)
|
| 374 |
+
|
| 375 |
+
slot0_bias = self.slot_emb.weight[0].view(1, 1, -1)
|
| 376 |
+
x = self.dough_norm(self.tok_emb(prefix_tokens) + slot0_bias)
|
| 377 |
+
|
| 378 |
+
total_k = start_pos + plen
|
| 379 |
+
use_is_causal = start_pos == 0 and plen > 1
|
| 380 |
+
if use_is_causal or plen == 1:
|
| 381 |
+
attn_mask = None
|
| 382 |
+
else:
|
| 383 |
+
causal_mask = torch.ones((plen, total_k), device=device, dtype=torch.bool).tril(diagonal=start_pos)
|
| 384 |
+
attn_mask = causal_mask.unsqueeze(0).unsqueeze(0)
|
| 385 |
+
|
| 386 |
+
new_k_list: list[torch.Tensor] = []
|
| 387 |
+
new_v_list: list[torch.Tensor] = []
|
| 388 |
+
for i, block in enumerate(self.blocks):
|
| 389 |
+
layer_kv = (kv_cache.k_cache[i], kv_cache.v_cache[i])
|
| 390 |
+
x, new_kv = block(
|
| 391 |
+
x,
|
| 392 |
+
cos=cos,
|
| 393 |
+
sin=sin,
|
| 394 |
+
attn_mask=attn_mask,
|
| 395 |
+
layer_kv=layer_kv,
|
| 396 |
+
return_new_kv=True,
|
| 397 |
+
is_causal=use_is_causal,
|
| 398 |
+
)
|
| 399 |
+
assert new_kv is not None
|
| 400 |
+
new_k_list.append(torch.cat([kv_cache.k_cache[i].type_as(new_kv[0]), new_kv[0]], dim=2))
|
| 401 |
+
new_v_list.append(torch.cat([kv_cache.v_cache[i].type_as(new_kv[1]), new_kv[1]], dim=2))
|
| 402 |
+
|
| 403 |
+
return PrefixKVCache(k_cache=new_k_list, v_cache=new_v_list, seq_len=total_k)
|
| 404 |
+
|
| 405 |
+
def forward_causal_and_cache(
|
| 406 |
+
self,
|
| 407 |
+
seq_tokens: torch.Tensor,
|
| 408 |
+
prefix_len: int,
|
| 409 |
+
) -> tuple[torch.Tensor, PrefixKVCache]:
|
| 410 |
+
"""
|
| 411 |
+
Runs a full causal pass over `seq_tokens` (N, L) through the shared transformer blocks:
|
| 412 |
+
1. Computes next-token logits `(N, L, V)` at every position so all ~160 tokens per story
|
| 413 |
+
receive direct causal language-modeling supervision (10x more signal per step).
|
| 414 |
+
2. Extracts the exact causal KV cache for `seq_tokens[:, :prefix_len]` (invariant to tokens >= prefix_len)
|
| 415 |
+
to seed the conveyor belt unroll without running a duplicate prefill pass.
|
| 416 |
+
"""
|
| 417 |
+
bsz, seq_len = seq_tokens.shape
|
| 418 |
+
device = seq_tokens.device
|
| 419 |
+
positions = torch.arange(0, seq_len, device=device, dtype=torch.long)
|
| 420 |
+
cos, sin = self.rope.get_cos_sin(positions)
|
| 421 |
+
|
| 422 |
+
slot0_bias = self.slot_emb.weight[0].view(1, 1, -1)
|
| 423 |
+
x = self.dough_norm(self.tok_emb(seq_tokens) + slot0_bias)
|
| 424 |
+
|
| 425 |
+
k_list: list[torch.Tensor] = []
|
| 426 |
+
v_list: list[torch.Tensor] = []
|
| 427 |
+
for block in self.blocks:
|
| 428 |
+
x, new_kv = block(
|
| 429 |
+
x,
|
| 430 |
+
cos=cos,
|
| 431 |
+
sin=sin,
|
| 432 |
+
attn_mask=None,
|
| 433 |
+
layer_kv=None,
|
| 434 |
+
return_new_kv=True,
|
| 435 |
+
is_causal=(seq_len > 1),
|
| 436 |
+
)
|
| 437 |
+
assert new_kv is not None
|
| 438 |
+
k_list.append(new_kv[0][:, :, :prefix_len, :].contiguous())
|
| 439 |
+
v_list.append(new_kv[1][:, :, :prefix_len, :].contiguous())
|
| 440 |
+
|
| 441 |
+
h = self.final_norm(x)
|
| 442 |
+
causal_logits = self.lm_head(h)
|
| 443 |
+
kv_cache = PrefixKVCache(k_cache=k_list, v_cache=v_list, seq_len=prefix_len)
|
| 444 |
+
return causal_logits, kv_cache
|
| 445 |
+
|
| 446 |
+
def append_committed_token(
|
| 447 |
+
self,
|
| 448 |
+
token_ids: torch.Tensor,
|
| 449 |
+
kv_cache: PrefixKVCache,
|
| 450 |
+
) -> PrefixKVCache:
|
| 451 |
+
if token_ids.dim() == 1:
|
| 452 |
+
token_ids = token_ids.unsqueeze(1)
|
| 453 |
+
return self.prefill_prefix(token_ids, kv_cache=kv_cache)
|
| 454 |
+
|
| 455 |
+
def spawn_raw_dough(
|
| 456 |
+
self,
|
| 457 |
+
batch_size: int,
|
| 458 |
+
num_slots: int,
|
| 459 |
+
device: torch.device,
|
| 460 |
+
deterministic: bool = False,
|
| 461 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 462 |
+
mean_emb = self.tok_emb.weight.mean(dim=0, keepdim=True).unsqueeze(0)
|
| 463 |
+
m_base = self.m_init.view(1, 1, -1)
|
| 464 |
+
if deterministic or self.cfg.spawn_noise_std <= 0.0:
|
| 465 |
+
e_new = mean_emb.expand(batch_size, num_slots, self.d_model).clone()
|
| 466 |
+
m_new = m_base.expand(batch_size, num_slots, self.d_model).clone()
|
| 467 |
+
else:
|
| 468 |
+
eps_e = torch.randn((batch_size, num_slots, self.d_model), device=device, dtype=mean_emb.dtype)
|
| 469 |
+
eps_m = torch.randn((batch_size, num_slots, self.d_model), device=device, dtype=mean_emb.dtype)
|
| 470 |
+
e_new = mean_emb + self.cfg.spawn_noise_std * 0.02 * eps_e
|
| 471 |
+
m_new = m_base + self.cfg.thought_noise_std * 0.02 * eps_m
|
| 472 |
+
return e_new, m_new
|
| 473 |
+
|
| 474 |
+
def init_empty_belt(
|
| 475 |
+
self,
|
| 476 |
+
batch_size: int,
|
| 477 |
+
tick: int,
|
| 478 |
+
device: torch.device,
|
| 479 |
+
deterministic: bool = False,
|
| 480 |
+
) -> BeltState:
|
| 481 |
+
e_soft, m_thought = self.spawn_raw_dough(
|
| 482 |
+
batch_size=batch_size,
|
| 483 |
+
num_slots=self.belt_size,
|
| 484 |
+
device=device,
|
| 485 |
+
deterministic=deterministic,
|
| 486 |
+
)
|
| 487 |
+
return BeltState(e_soft=e_soft, m_thought=m_thought, tick=tick)
|
| 488 |
+
|
| 489 |
+
def compose_dough_inputs(self, belt: BeltState, active_slots: Optional[int] = None) -> torch.Tensor:
|
| 490 |
+
B = active_slots if active_slots is not None else self.belt_size
|
| 491 |
+
e_soft = belt.e_soft[:, :B, :]
|
| 492 |
+
m_thought = belt.m_thought[:, :B, :]
|
| 493 |
+
slots = torch.arange(B, device=e_soft.device, dtype=torch.long)
|
| 494 |
+
s_emb = self.slot_emb(slots).unsqueeze(0)
|
| 495 |
+
|
| 496 |
+
if self.cfg.use_thought_highway:
|
| 497 |
+
alpha = self.alpha_schedule[:B].view(1, B, 1) # alpha_0 == 0.0
|
| 498 |
+
fused = e_soft + alpha * m_thought + s_emb
|
| 499 |
+
else:
|
| 500 |
+
fused = e_soft + s_emb
|
| 501 |
+
|
| 502 |
+
return self.dough_norm(fused)
|
| 503 |
+
|
| 504 |
+
def _build_belt_mask(
|
| 505 |
+
self,
|
| 506 |
+
belt_len: int,
|
| 507 |
+
prefix_len: int,
|
| 508 |
+
device: torch.device,
|
| 509 |
+
) -> Optional[torch.Tensor]:
|
| 510 |
+
total_k = prefix_len + belt_len
|
| 511 |
+
if not self.cfg.causal_belt_only:
|
| 512 |
+
return torch.ones((1, 1, belt_len, total_k), device=device, dtype=torch.bool)
|
| 513 |
+
mask = torch.ones((belt_len, total_k), device=device, dtype=torch.bool).tril(diagonal=prefix_len)
|
| 514 |
+
return mask.unsqueeze(0).unsqueeze(0)
|
| 515 |
+
|
| 516 |
+
def forward_belt(
|
| 517 |
+
self,
|
| 518 |
+
belt: BeltState,
|
| 519 |
+
kv_cache: Optional[PrefixKVCache] = None,
|
| 520 |
+
active_slots: Optional[int] = None,
|
| 521 |
+
) -> BeltStepOutput:
|
| 522 |
+
"""
|
| 523 |
+
Single forward pass across all active slots on the belt:
|
| 524 |
+
Input : [Committed Prefix KV Cache x_{0..t-1}] + [Belt Dough 0..B-1]
|
| 525 |
+
Output: [Slot 0 Token Logits + Updated Belt State (Lexical + Thought + Elastic Shift Gate)]
|
| 526 |
+
"""
|
| 527 |
+
B = active_slots if active_slots is not None else self.belt_size
|
| 528 |
+
dough = self.compose_dough_inputs(belt, active_slots=B)
|
| 529 |
+
device = dough.device
|
| 530 |
+
|
| 531 |
+
prefix_len = kv_cache.seq_len if kv_cache is not None else 0
|
| 532 |
+
offsets = self.get_slot_target_offsets(active_slots=B)
|
| 533 |
+
positions = belt.tick + offsets
|
| 534 |
+
cos, sin = self.rope.get_cos_sin(positions)
|
| 535 |
+
|
| 536 |
+
attn_mask = (
|
| 537 |
+
None
|
| 538 |
+
if not self.cfg.causal_belt_only
|
| 539 |
+
else self._build_belt_mask(belt_len=B, prefix_len=prefix_len, device=device)
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
h = dough
|
| 543 |
+
use_ckpt = self.training and self.cfg.grad_checkpointing
|
| 544 |
+
for i, block in enumerate(self.blocks):
|
| 545 |
+
layer_kv = (kv_cache.k_cache[i], kv_cache.v_cache[i]) if kv_cache is not None else None
|
| 546 |
+
if use_ckpt:
|
| 547 |
+
def _run_block(inp: torch.Tensor, blk: ConveyerBlock = block, lkv: Optional[tuple[torch.Tensor, torch.Tensor]] = layer_kv) -> torch.Tensor:
|
| 548 |
+
out_h, _ = blk(inp, cos=cos, sin=sin, attn_mask=attn_mask, layer_kv=lkv, return_new_kv=False)
|
| 549 |
+
return out_h
|
| 550 |
+
h = checkpoint(_run_block, h, use_reentrant=False)
|
| 551 |
+
else:
|
| 552 |
+
h, _ = block(
|
| 553 |
+
h,
|
| 554 |
+
cos=cos,
|
| 555 |
+
sin=sin,
|
| 556 |
+
attn_mask=attn_mask,
|
| 557 |
+
layer_kv=layer_kv,
|
| 558 |
+
return_new_kv=False,
|
| 559 |
+
)
|
| 560 |
+
|
| 561 |
+
h = self.final_norm(h) # (N, B, D)
|
| 562 |
+
|
| 563 |
+
# Channel 1: Lexical Anchor (Soft Probability Projection)
|
| 564 |
+
logits = self.lm_head(h) # (N, B, V)
|
| 565 |
+
tau = self.tau_schedule[:B].view(1, B, 1).type_as(logits)
|
| 566 |
+
probs = F.softmax((logits / tau).float(), dim=-1).type_as(self.tok_emb.weight)
|
| 567 |
+
e_soft = torch.matmul(probs, self.tok_emb.weight) # (N, B, D)
|
| 568 |
+
|
| 569 |
+
# Channel 2: Continuous Thought Highway (Gated Residual Update)
|
| 570 |
+
cand = self.thought_cand(h)
|
| 571 |
+
gate = torch.sigmoid(self.thought_gate(h))
|
| 572 |
+
m_prev = belt.m_thought[:, :B, :]
|
| 573 |
+
if self.cfg.use_thought_highway:
|
| 574 |
+
m_updated = gate * m_prev + (1.0 - gate) * cand
|
| 575 |
+
else:
|
| 576 |
+
m_updated = torch.zeros_like(m_prev)
|
| 577 |
+
|
| 578 |
+
# Elastic Planning Shift/Pause Gate & Mid-Belt Sentence Expansion
|
| 579 |
+
F_slots = min(self.front_slots, B)
|
| 580 |
+
if self.cfg.use_elastic_planning and B > F_slots:
|
| 581 |
+
prior = self.shift_prior[:B].view(1, B).type_as(h)
|
| 582 |
+
raw_shift = self.shift_head(h).squeeze(-1) + prior # (N, B)
|
| 583 |
+
shift_prob = torch.sigmoid(raw_shift)
|
| 584 |
+
# Front conveyor slots (0 .. F-1) always shift left at full speed (1.0)
|
| 585 |
+
front_ones = torch.ones((h.size(0), F_slots), device=device, dtype=shift_prob.dtype)
|
| 586 |
+
shift_prob = torch.cat([front_ones, shift_prob[:, F_slots:]], dim=1)
|
| 587 |
+
# Mid-belt expansion thought synthesized from the boundary between Front (F-1) and Planning (F)
|
| 588 |
+
boundary_pair = torch.cat([h[:, F_slots - 1, :], h[:, F_slots, :]], dim=-1)
|
| 589 |
+
expand_cand = self.expand_head(boundary_pair) # (N, D)
|
| 590 |
+
else:
|
| 591 |
+
shift_prob = torch.ones((h.size(0), B), device=device, dtype=h.dtype)
|
| 592 |
+
expand_cand = h[:, -1, :]
|
| 593 |
+
|
| 594 |
+
return BeltStepOutput(
|
| 595 |
+
logits=logits,
|
| 596 |
+
probs=probs,
|
| 597 |
+
e_soft=e_soft,
|
| 598 |
+
cand_thought=cand,
|
| 599 |
+
gate=gate,
|
| 600 |
+
m_updated=m_updated,
|
| 601 |
+
shift_prob=shift_prob,
|
| 602 |
+
expand_cand=expand_cand,
|
| 603 |
+
slot0_logits=logits[:, 0, :],
|
| 604 |
+
)
|
| 605 |
+
|
| 606 |
+
def shift_belt_left(
|
| 607 |
+
self,
|
| 608 |
+
step_out: BeltStepOutput,
|
| 609 |
+
current_tick: int,
|
| 610 |
+
deterministic_spawn: bool = False,
|
| 611 |
+
clamp_tokens: Optional[torch.Tensor] = None,
|
| 612 |
+
clamp_mask: Optional[torch.Tensor] = None,
|
| 613 |
+
) -> BeltState:
|
| 614 |
+
"""
|
| 615 |
+
Updates the belt for tick t + 1:
|
| 616 |
+
1. Front Conveyor Zone (Slots 0 .. F-2):
|
| 617 |
+
Always shifts left by 1 slot (`1..F-1 -> 0..F-2`) so fully-baked tokens flow smoothly out of Slot 0.
|
| 618 |
+
2. Mid-Belt Sentence Expansion Zone (Slot F-1, e.g. Slot 15):
|
| 619 |
+
Blends the advancing plan from Slot F with the newly expanded clause (`expand_cand`),
|
| 620 |
+
allowing new sentences and words to form in the middle while the back 75% pauses!
|
| 621 |
+
3. Back 75% Elastic Planning Zone (Slots F .. B-2, e.g. 16..62):
|
| 622 |
+
Each slot k updates elastically using its learned shift probability `s_k`:
|
| 623 |
+
e'_{k} = s_k * e_{k+1} + (1 - s_k) * e_k
|
| 624 |
+
m'_{k} = s_k * m_{k+1} + (1 - s_k) * m_k
|
| 625 |
+
When `s_k` is low (~0.25), the back 75% of the belt pauses in-place holding the long-range
|
| 626 |
+
sentence plan steady while the front conveyor articulates the current sentence!
|
| 627 |
+
4. Slot B-1 (Slot 63):
|
| 628 |
+
Blends fresh raw dough into the tail of the planning belt.
|
| 629 |
+
"""
|
| 630 |
+
bsz, B, _ = step_out.e_soft.shape
|
| 631 |
+
device = step_out.e_soft.device
|
| 632 |
+
F_slots = min(self.front_slots, B)
|
| 633 |
+
|
| 634 |
+
e_spawn, m_spawn = self.spawn_raw_dough(
|
| 635 |
+
batch_size=bsz,
|
| 636 |
+
num_slots=1,
|
| 637 |
+
device=device,
|
| 638 |
+
deterministic=deterministic_spawn,
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
if B <= 1:
|
| 642 |
+
e_next = e_spawn
|
| 643 |
+
m_next = m_spawn
|
| 644 |
+
probs_next = torch.full((bsz, 1, self.vocab_size), 1.0 / self.vocab_size, device=device, dtype=step_out.probs.dtype)
|
| 645 |
+
logits_next = torch.zeros((bsz, 1, self.vocab_size), device=device, dtype=step_out.logits.dtype)
|
| 646 |
+
gates_next = torch.ones((bsz, 1, self.d_model), device=device, dtype=step_out.gate.dtype)
|
| 647 |
+
elif (not self.cfg.use_elastic_planning) or B <= F_slots:
|
| 648 |
+
# Strict 1-by-1 shift across all B slots
|
| 649 |
+
e_next = torch.cat([step_out.e_soft[:, 1:, :], e_spawn], dim=1)
|
| 650 |
+
m_next = torch.cat([step_out.m_updated[:, 1:, :], m_spawn], dim=1)
|
| 651 |
+
probs_next = torch.cat(
|
| 652 |
+
[
|
| 653 |
+
step_out.probs[:, 1:, :],
|
| 654 |
+
torch.full((bsz, 1, self.vocab_size), 1.0 / self.vocab_size, device=device, dtype=step_out.probs.dtype),
|
| 655 |
+
],
|
| 656 |
+
dim=1,
|
| 657 |
+
)
|
| 658 |
+
logits_next = torch.cat(
|
| 659 |
+
[
|
| 660 |
+
step_out.logits[:, 1:, :],
|
| 661 |
+
torch.zeros((bsz, 1, self.vocab_size), device=device, dtype=step_out.logits.dtype),
|
| 662 |
+
],
|
| 663 |
+
dim=1,
|
| 664 |
+
)
|
| 665 |
+
gates_next = torch.cat(
|
| 666 |
+
[
|
| 667 |
+
step_out.gate[:, 1:, :],
|
| 668 |
+
torch.ones((bsz, 1, self.d_model), device=device, dtype=step_out.gate.dtype),
|
| 669 |
+
],
|
| 670 |
+
dim=1,
|
| 671 |
+
)
|
| 672 |
+
else:
|
| 673 |
+
# Elastic Two-Zone Update:
|
| 674 |
+
# (a) Front slots 0 .. F-2 shift strictly from 1 .. F-1
|
| 675 |
+
e_front = step_out.e_soft[:, 1:F_slots, :]
|
| 676 |
+
m_front = step_out.m_updated[:, 1:F_slots, :]
|
| 677 |
+
|
| 678 |
+
# (b) Mid-Belt Expansion Slot (F-1): where paused back-belt plans expand into new active words
|
| 679 |
+
s_mid = step_out.shift_prob[:, F_slots : F_slots + 1].unsqueeze(-1) # (N, 1, 1)
|
| 680 |
+
e_FromPlan = step_out.e_soft[:, F_slots : F_slots + 1, :]
|
| 681 |
+
m_FromPlan = step_out.m_updated[:, F_slots : F_slots + 1, :]
|
| 682 |
+
m_expand = step_out.expand_cand.unsqueeze(1) # (N, 1, D)
|
| 683 |
+
e_mid = e_FromPlan
|
| 684 |
+
m_mid = s_mid * m_FromPlan + (1.0 - s_mid) * m_expand
|
| 685 |
+
|
| 686 |
+
# (c) Back 75% Planning Slots (F .. B-2): elastic interpolation between staying (k) and shifting (k+1)
|
| 687 |
+
s_back = step_out.shift_prob[:, F_slots : B - 1].unsqueeze(-1) # (N, B - 1 - F, 1)
|
| 688 |
+
e_stay = step_out.e_soft[:, F_slots : B - 1, :]
|
| 689 |
+
e_adv = step_out.e_soft[:, F_slots + 1 : B, :]
|
| 690 |
+
e_back = s_back * e_adv + (1.0 - s_back) * e_stay
|
| 691 |
+
|
| 692 |
+
m_stay = step_out.m_updated[:, F_slots : B - 1, :]
|
| 693 |
+
m_adv = step_out.m_updated[:, F_slots + 1 : B, :]
|
| 694 |
+
m_back = s_back * m_adv + (1.0 - s_back) * m_stay
|
| 695 |
+
|
| 696 |
+
# (d) Slot B-1 (Tail of planning belt): elastic blend between holding Slot B-1 and fresh raw dough
|
| 697 |
+
s_tail = step_out.shift_prob[:, B - 1 : B].unsqueeze(-1)
|
| 698 |
+
e_tail = s_tail * e_spawn + (1.0 - s_tail) * step_out.e_soft[:, B - 1 : B, :]
|
| 699 |
+
m_tail = s_tail * m_spawn + (1.0 - s_tail) * step_out.m_updated[:, B - 1 : B, :]
|
| 700 |
+
|
| 701 |
+
e_next = torch.cat([e_front, e_mid, e_back, e_tail], dim=1)
|
| 702 |
+
m_next = torch.cat([m_front, m_mid, m_back, m_tail], dim=1)
|
| 703 |
+
|
| 704 |
+
probs_next = torch.cat(
|
| 705 |
+
[
|
| 706 |
+
step_out.probs[:, 1:, :],
|
| 707 |
+
torch.full((bsz, 1, self.vocab_size), 1.0 / self.vocab_size, device=device, dtype=step_out.probs.dtype),
|
| 708 |
+
],
|
| 709 |
+
dim=1,
|
| 710 |
+
)
|
| 711 |
+
logits_next = torch.cat(
|
| 712 |
+
[
|
| 713 |
+
step_out.logits[:, 1:, :],
|
| 714 |
+
torch.zeros((bsz, 1, self.vocab_size), device=device, dtype=step_out.logits.dtype),
|
| 715 |
+
],
|
| 716 |
+
dim=1,
|
| 717 |
+
)
|
| 718 |
+
gates_next = torch.cat(
|
| 719 |
+
[
|
| 720 |
+
step_out.gate[:, 1:, :],
|
| 721 |
+
torch.ones((bsz, 1, self.d_model), device=device, dtype=step_out.gate.dtype),
|
| 722 |
+
],
|
| 723 |
+
dim=1,
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
if clamp_tokens is not None and clamp_mask is not None:
|
| 727 |
+
clamped_emb = self.tok_emb(clamp_tokens)
|
| 728 |
+
mask_exp = clamp_mask.unsqueeze(-1)
|
| 729 |
+
e_next = torch.where(mask_exp, clamped_emb, e_next)
|
| 730 |
+
|
| 731 |
+
return BeltState(
|
| 732 |
+
e_soft=e_next,
|
| 733 |
+
m_thought=m_next,
|
| 734 |
+
tick=current_tick + 1,
|
| 735 |
+
probs=probs_next,
|
| 736 |
+
logits=logits_next,
|
| 737 |
+
gates=gates_next,
|
| 738 |
+
shift_probs=step_out.shift_prob,
|
| 739 |
+
)
|
src/conveyer/sweep.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
from conveyer.checkpoint import load_checkpoint
|
| 10 |
+
from conveyer.config import load_config
|
| 11 |
+
from conveyer.data import build_dataset_and_tokenizer
|
| 12 |
+
from conveyer.eval import evaluate_closed_loop_generation
|
| 13 |
+
from conveyer.train import train_model
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def main() -> None:
|
| 17 |
+
parser = argparse.ArgumentParser(
|
| 18 |
+
description="Head-to-Head Benchmark: 1-Pass AR Baseline vs. 64-Pass Conveyor Diffusion LLM & Ablations"
|
| 19 |
+
)
|
| 20 |
+
parser.add_argument("--config", type=str, default="configs/synthetic.yaml", help="Base config file")
|
| 21 |
+
parser.add_argument("--steps", type=int, default=400, help="Training steps per variant")
|
| 22 |
+
parser.add_argument("--episodes", type=int, default=64, help="Closed-loop evaluation episodes")
|
| 23 |
+
parser.add_argument("--out-dir", type=str, default="runs/sweep", help="Output directory for sweep")
|
| 24 |
+
args = parser.parse_args()
|
| 25 |
+
|
| 26 |
+
variants = [
|
| 27 |
+
{
|
| 28 |
+
"name": "1_ar_baseline",
|
| 29 |
+
"label": "1-Pass Causal AR Baseline (B=1)",
|
| 30 |
+
"overrides": ["train.ar_baseline=true", "train.unroll_steps=8"],
|
| 31 |
+
"bake_steps": 1,
|
| 32 |
+
"overlap": 0,
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"name": "2_conveyer_soft_only",
|
| 36 |
+
"label": "64-Slot Conveyor (Soft-Vocab Only, alpha_k=0)",
|
| 37 |
+
"overrides": ["train.no_thought_highway=true"],
|
| 38 |
+
"bake_steps": 64,
|
| 39 |
+
"overlap": 16,
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"name": "3_conveyer_nobptt",
|
| 43 |
+
"label": "64-Slot Conveyor (Detached W=1, No Belt BPTT)",
|
| 44 |
+
"overrides": ["train.no_belt_bptt=true"],
|
| 45 |
+
"bake_steps": 64,
|
| 46 |
+
"overlap": 16,
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"name": "4_conveyer_dual_full",
|
| 50 |
+
"label": "64-Slot Dual-Channel Conveyor (Soft-Vocab + Gated Thought + BPTT)",
|
| 51 |
+
"overrides": [],
|
| 52 |
+
"bake_steps": 64,
|
| 53 |
+
"overlap": 16,
|
| 54 |
+
},
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
summary: list[dict[str, Any]] = []
|
| 58 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 59 |
+
|
| 60 |
+
for var in variants:
|
| 61 |
+
run_dir = Path(args.out_dir) / var["name"]
|
| 62 |
+
overrides = [
|
| 63 |
+
f"train.max_steps={args.steps}",
|
| 64 |
+
f"train.eval_every={max(50, args.steps // 2)}",
|
| 65 |
+
f"train.out_dir={run_dir.as_posix()}",
|
| 66 |
+
] + var["overrides"]
|
| 67 |
+
|
| 68 |
+
print("\n" + "=" * 90)
|
| 69 |
+
print(f"Training Variant: {var['label']}")
|
| 70 |
+
print("=" * 90)
|
| 71 |
+
cfg = load_config(args.config, overrides)
|
| 72 |
+
val_m = train_model(cfg)
|
| 73 |
+
|
| 74 |
+
ckpt_path = run_dir / "best.pt"
|
| 75 |
+
model, loaded_cfg, _ = load_checkpoint(ckpt_path, device=device)
|
| 76 |
+
_, val_ds, tokenizer = build_dataset_and_tokenizer(loaded_cfg)
|
| 77 |
+
|
| 78 |
+
cl_metrics = evaluate_closed_loop_generation(
|
| 79 |
+
model=model,
|
| 80 |
+
val_ds=val_ds,
|
| 81 |
+
tokenizer=tokenizer,
|
| 82 |
+
device=device,
|
| 83 |
+
num_episodes=args.episodes,
|
| 84 |
+
prompt_overlap=var["overlap"],
|
| 85 |
+
bake_steps=var["bake_steps"],
|
| 86 |
+
)
|
| 87 |
+
row = {
|
| 88 |
+
"name": var["name"],
|
| 89 |
+
"label": var["label"],
|
| 90 |
+
"val_slot0_acc": val_m.get("val_slot0_acc", 0.0),
|
| 91 |
+
"val_slot0_ppl": val_m.get("val_slot0_ppl", 0.0),
|
| 92 |
+
**cl_metrics,
|
| 93 |
+
}
|
| 94 |
+
summary.append(row)
|
| 95 |
+
|
| 96 |
+
print("\n" + "=" * 100)
|
| 97 |
+
print(f"{'Model / Ablation Variant':<56} | {'Pos-0 Acc':<10} | {'Token Acc':<10} | {'Exact Seq':<10}")
|
| 98 |
+
print("-" * 100)
|
| 99 |
+
for r in summary:
|
| 100 |
+
print(
|
| 101 |
+
f"{r['label']:<56} | {r['first_token_acc']*100:8.2f}% | "
|
| 102 |
+
f"{r['token_acc']*100:8.2f}% | {r['exact_seq_acc']*100:8.2f}%"
|
| 103 |
+
)
|
| 104 |
+
print("=" * 100)
|
| 105 |
+
|
| 106 |
+
out_path = Path(args.out_dir) / "sweep_summary.json"
|
| 107 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 108 |
+
out_path.write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
if __name__ == "__main__":
|
| 112 |
+
main()
|
src/conveyer/train.py
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import subprocess
|
| 9 |
+
import time
|
| 10 |
+
from typing import Any, Optional
|
| 11 |
+
import torch
|
| 12 |
+
from torch.utils.data import DataLoader, Subset
|
| 13 |
+
|
| 14 |
+
from conveyer.checkpoint import load_checkpoint_payload, save_checkpoint
|
| 15 |
+
from conveyer.config import ConveyerConfig, load_config
|
| 16 |
+
from conveyer.data import TokenizerProtocol, build_dataset_and_tokenizer, collate_conveyer_batch
|
| 17 |
+
from conveyer.infer import generate_conveyer
|
| 18 |
+
from conveyer.losses import compute_unrolled_conveyer_loss
|
| 19 |
+
from conveyer.model import ConveyerDiffusionLM
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _query_gpu_temp_c() -> Optional[int]:
|
| 23 |
+
try:
|
| 24 |
+
out = subprocess.check_output(
|
| 25 |
+
["nvidia-smi", "--query-gpu=temperature.gpu", "--format=csv,noheader,nounits"],
|
| 26 |
+
timeout=2.0,
|
| 27 |
+
stderr=subprocess.DEVNULL,
|
| 28 |
+
)
|
| 29 |
+
return int(out.decode("utf-8", errors="ignore").strip().splitlines()[0])
|
| 30 |
+
except Exception:
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def get_lr(step: int, max_steps: int, base_lr: float, warmup_steps: int) -> float:
|
| 35 |
+
if step < warmup_steps:
|
| 36 |
+
return base_lr * float(step + 1) / float(max(1, warmup_steps))
|
| 37 |
+
progress = float(step - warmup_steps) / float(max(1, max_steps - warmup_steps))
|
| 38 |
+
progress = min(max(progress, 0.0), 1.0)
|
| 39 |
+
return base_lr * (0.1 + 0.9 * 0.5 * (1.0 + math.cos(math.pi * progress)))
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@torch.no_grad()
|
| 43 |
+
def evaluate_validation(
|
| 44 |
+
model: ConveyerDiffusionLM,
|
| 45 |
+
val_loader: DataLoader[dict[str, torch.Tensor]],
|
| 46 |
+
cfg: ConveyerConfig,
|
| 47 |
+
device: torch.device,
|
| 48 |
+
pad_id: int = 0,
|
| 49 |
+
max_batches: int = 25,
|
| 50 |
+
) -> dict[str, Any]:
|
| 51 |
+
model.eval()
|
| 52 |
+
B = model.belt_size
|
| 53 |
+
loss_sum = 0.0
|
| 54 |
+
s0_loss_sum = 0.0
|
| 55 |
+
s0_acc_sum = 0.0
|
| 56 |
+
lm_loss_sum = 0.0
|
| 57 |
+
lm_acc_sum = 0.0
|
| 58 |
+
belt_acc_sum = 0.0
|
| 59 |
+
per_slot_acc_sum = torch.zeros(B, device=device)
|
| 60 |
+
per_slot_ent_sum = torch.zeros(B, device=device)
|
| 61 |
+
count = 0
|
| 62 |
+
|
| 63 |
+
for i, batch in enumerate(val_loader):
|
| 64 |
+
if i >= max_batches:
|
| 65 |
+
break
|
| 66 |
+
seq = batch["input_ids"].to(device)
|
| 67 |
+
plen = batch["prompt_lengths"].to(device)
|
| 68 |
+
t_eval = max(1, int(plen.min().item()) - min(4, cfg.train.unroll_steps // 2))
|
| 69 |
+
use_amp = device.type == "cuda" and torch.cuda.is_bf16_supported()
|
| 70 |
+
with torch.autocast(device_type=device.type, dtype=torch.bfloat16 if use_amp else torch.float32, enabled=use_amp):
|
| 71 |
+
m = compute_unrolled_conveyer_loss(
|
| 72 |
+
model=model,
|
| 73 |
+
seq_tokens=seq,
|
| 74 |
+
cfg=cfg,
|
| 75 |
+
prompt_lengths=plen,
|
| 76 |
+
tick_start=t_eval,
|
| 77 |
+
pad_token_id=pad_id,
|
| 78 |
+
)
|
| 79 |
+
loss_sum += float(m.loss.item())
|
| 80 |
+
s0_loss_sum += m.slot0_loss
|
| 81 |
+
s0_acc_sum += m.slot0_acc
|
| 82 |
+
lm_loss_sum += m.prefix_lm_loss
|
| 83 |
+
lm_acc_sum += m.prefix_lm_acc
|
| 84 |
+
belt_acc_sum += m.belt_mean_acc
|
| 85 |
+
per_slot_acc_sum += m.per_slot_acc
|
| 86 |
+
per_slot_ent_sum += m.per_slot_entropy
|
| 87 |
+
count += 1
|
| 88 |
+
|
| 89 |
+
model.train()
|
| 90 |
+
denom = max(count, 1)
|
| 91 |
+
s0_ce = s0_loss_sum / denom
|
| 92 |
+
lm_ce = lm_loss_sum / denom
|
| 93 |
+
return {
|
| 94 |
+
"val_loss": loss_sum / denom,
|
| 95 |
+
"val_slot0_ce": s0_ce,
|
| 96 |
+
"val_slot0_ppl": math.exp(min(s0_ce, 20.0)),
|
| 97 |
+
"val_slot0_acc": s0_acc_sum / denom,
|
| 98 |
+
"val_lm_ce": lm_ce,
|
| 99 |
+
"val_lm_ppl": math.exp(min(lm_ce, 20.0)),
|
| 100 |
+
"val_lm_acc": lm_acc_sum / denom,
|
| 101 |
+
"val_belt_acc": belt_acc_sum / denom,
|
| 102 |
+
"val_per_slot_acc": (per_slot_acc_sum / denom).cpu().tolist(),
|
| 103 |
+
"val_per_slot_entropy": (per_slot_ent_sum / denom).cpu().tolist(),
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _prune_old_milestones(out_dir: Path, keep_last: int = 8) -> None:
|
| 108 |
+
milestones = sorted(out_dir.glob("ckpt_step_*.pt"), key=lambda p: p.stat().st_mtime)
|
| 109 |
+
if len(milestones) > keep_last:
|
| 110 |
+
for old_p in milestones[: len(milestones) - keep_last]:
|
| 111 |
+
try:
|
| 112 |
+
old_p.unlink()
|
| 113 |
+
prev_p = old_p.with_suffix(old_p.suffix + ".prev")
|
| 114 |
+
if prev_p.exists():
|
| 115 |
+
prev_p.unlink()
|
| 116 |
+
except OSError:
|
| 117 |
+
pass
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@torch.no_grad()
|
| 121 |
+
def _sample_preview(
|
| 122 |
+
model: ConveyerDiffusionLM,
|
| 123 |
+
tokenizer: TokenizerProtocol,
|
| 124 |
+
device: torch.device,
|
| 125 |
+
prompt_text: str,
|
| 126 |
+
max_new_tokens: int = 32,
|
| 127 |
+
) -> str:
|
| 128 |
+
model.eval()
|
| 129 |
+
p_ids = torch.tensor([tokenizer.encode(prompt_text.strip())], dtype=torch.long, device=device)
|
| 130 |
+
out_ids, _ = generate_conveyer(
|
| 131 |
+
model=model,
|
| 132 |
+
prompt_tokens=p_ids,
|
| 133 |
+
max_new_tokens=max_new_tokens,
|
| 134 |
+
prompt_overlap=10,
|
| 135 |
+
temperature=0.0,
|
| 136 |
+
eos_token_id=tokenizer.eos_id,
|
| 137 |
+
)
|
| 138 |
+
gen_part = tokenizer.decode(out_ids[0, p_ids.size(1) :].tolist())
|
| 139 |
+
model.train()
|
| 140 |
+
return gen_part.replace("\n", " ").strip()
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _sanitize_jsonl(log_path: Path) -> None:
|
| 144 |
+
"""Strips any trailing NUL bytes left by an abrupt OS crash so the log remains valid JSONL."""
|
| 145 |
+
if not log_path.exists():
|
| 146 |
+
return
|
| 147 |
+
raw = log_path.read_bytes().replace(b"\x00", b"")
|
| 148 |
+
lines = [ln for ln in raw.splitlines() if ln.strip().startswith(b"{") and ln.strip().endswith(b"}")]
|
| 149 |
+
log_path.write_bytes(b"\n".join(lines) + (b"\n" if lines else b""))
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def train_model(
|
| 153 |
+
cfg: ConveyerConfig,
|
| 154 |
+
init_ckpt: str | Path | None = None,
|
| 155 |
+
resume_ckpt: str | Path | None = None,
|
| 156 |
+
) -> dict[str, Any]:
|
| 157 |
+
torch.manual_seed(cfg.train.seed)
|
| 158 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 159 |
+
|
| 160 |
+
train_ds, val_ds, tokenizer = build_dataset_and_tokenizer(cfg)
|
| 161 |
+
if cfg.train.overfit_batches > 0:
|
| 162 |
+
n_sub = min(len(train_ds), cfg.train.overfit_batches * cfg.train.batch_size)
|
| 163 |
+
train_ds = Subset(train_ds, list(range(n_sub)))
|
| 164 |
+
val_ds = train_ds
|
| 165 |
+
|
| 166 |
+
collate_fn = lambda b: collate_conveyer_batch(b, pad_id=tokenizer.pad_id)
|
| 167 |
+
train_loader = DataLoader(
|
| 168 |
+
train_ds,
|
| 169 |
+
batch_size=cfg.train.batch_size,
|
| 170 |
+
shuffle=(cfg.train.overfit_batches == 0),
|
| 171 |
+
collate_fn=collate_fn,
|
| 172 |
+
drop_last=False,
|
| 173 |
+
)
|
| 174 |
+
val_loader = DataLoader(
|
| 175 |
+
val_ds,
|
| 176 |
+
batch_size=cfg.train.batch_size,
|
| 177 |
+
shuffle=False,
|
| 178 |
+
collate_fn=collate_fn,
|
| 179 |
+
drop_last=False,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
model = ConveyerDiffusionLM(cfg.model).to(device)
|
| 183 |
+
optimizer = torch.optim.AdamW(
|
| 184 |
+
model.parameters(),
|
| 185 |
+
lr=cfg.train.lr,
|
| 186 |
+
weight_decay=cfg.train.weight_decay,
|
| 187 |
+
betas=(0.9, 0.95),
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
out_dir = Path(cfg.train.out_dir)
|
| 191 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 192 |
+
log_jsonl = out_dir / "train_log.jsonl"
|
| 193 |
+
_sanitize_jsonl(log_jsonl)
|
| 194 |
+
|
| 195 |
+
start_step = 1
|
| 196 |
+
best_val_loss = float("inf")
|
| 197 |
+
prior_elapsed = 0.0
|
| 198 |
+
|
| 199 |
+
# Resolve resume vs warm-start initialization (with automatic fallback to .prev or ckpt_step_*.pt)
|
| 200 |
+
resume_target: Optional[Path] = None
|
| 201 |
+
if resume_ckpt is not None:
|
| 202 |
+
if str(resume_ckpt).lower() == "auto":
|
| 203 |
+
resume_target = out_dir / "last.pt"
|
| 204 |
+
else:
|
| 205 |
+
resume_target = Path(resume_ckpt)
|
| 206 |
+
|
| 207 |
+
if resume_target is not None:
|
| 208 |
+
try:
|
| 209 |
+
ckpt_data, used_path = load_checkpoint_payload(resume_target, map_location="cpu")
|
| 210 |
+
model.load_state_dict(ckpt_data["model_state"], strict=False)
|
| 211 |
+
if "optimizer_state" in ckpt_data:
|
| 212 |
+
try:
|
| 213 |
+
optimizer.load_state_dict(ckpt_data["optimizer_state"])
|
| 214 |
+
except Exception as e:
|
| 215 |
+
print(f"[ConveyerTrain] Warning: could not restore optimizer state ({e})", flush=True)
|
| 216 |
+
start_step = int(ckpt_data.get("step", 0)) + 1
|
| 217 |
+
best_val_loss = float(
|
| 218 |
+
ckpt_data.get("best_val_loss") or ckpt_data.get("metrics", {}).get("val_loss", float("inf"))
|
| 219 |
+
)
|
| 220 |
+
prior_elapsed = float(ckpt_data.get("elapsed_sec", 0.0))
|
| 221 |
+
print(
|
| 222 |
+
f"[ConveyerTrain] Resumed from {used_path} at step {start_step} "
|
| 223 |
+
f"(best_val_loss={best_val_loss:.4f}, prior_elapsed={prior_elapsed/3600:.2f}h)",
|
| 224 |
+
flush=True,
|
| 225 |
+
)
|
| 226 |
+
del ckpt_data
|
| 227 |
+
if device.type == "cuda":
|
| 228 |
+
torch.cuda.empty_cache()
|
| 229 |
+
except Exception as e:
|
| 230 |
+
print(f"[ConveyerTrain] No readable resume checkpoint in {out_dir} ({e}); starting from step 1.", flush=True)
|
| 231 |
+
elif init_ckpt is not None and Path(init_ckpt).exists():
|
| 232 |
+
ckpt_data, used_path = load_checkpoint_payload(init_ckpt, map_location="cpu")
|
| 233 |
+
model.load_state_dict(ckpt_data["model_state"], strict=False)
|
| 234 |
+
print(f"[ConveyerTrain] Warm-started weights from {used_path}", flush=True)
|
| 235 |
+
del ckpt_data
|
| 236 |
+
|
| 237 |
+
use_amp = device.type == "cuda" and torch.cuda.is_bf16_supported()
|
| 238 |
+
amp_dtype = torch.bfloat16 if use_amp else torch.float32
|
| 239 |
+
|
| 240 |
+
mode_desc = (
|
| 241 |
+
"1-Pass AR Baseline"
|
| 242 |
+
if cfg.model.causal_belt_only
|
| 243 |
+
else (
|
| 244 |
+
"64-Slot Conveyor (Soft-Vocab Only)"
|
| 245 |
+
if not cfg.model.use_thought_highway
|
| 246 |
+
else "64-Slot Dual-Channel Conveyor (Soft-Vocab + Gated Thought)"
|
| 247 |
+
)
|
| 248 |
+
)
|
| 249 |
+
max_hours = float(getattr(cfg.train, "max_hours", 0.0))
|
| 250 |
+
save_every = max(1, int(getattr(cfg.train, "save_every", 500)))
|
| 251 |
+
milestone_every = save_every * 2
|
| 252 |
+
|
| 253 |
+
print(
|
| 254 |
+
f"[ConveyerTrain] Mode: {mode_desc} | Params: {model.num_parameters()/1e6:.2f}M | "
|
| 255 |
+
f"Device: {device} | Train stories: {len(train_ds):,} | Val stories: {len(val_ds):,} | "
|
| 256 |
+
f"Unroll U={cfg.train.unroll_steps} | Belt B={cfg.model.belt_size} | "
|
| 257 |
+
f"max_steps={cfg.train.max_steps:,} | max_hours={max_hours:.1f}h | save_every={save_every}",
|
| 258 |
+
flush=True,
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
train_iter = iter(train_loader)
|
| 262 |
+
last_val_metrics: dict[str, Any] = {}
|
| 263 |
+
t0 = time.time()
|
| 264 |
+
|
| 265 |
+
model.train()
|
| 266 |
+
for step in range(start_step, cfg.train.max_steps + 1):
|
| 267 |
+
try:
|
| 268 |
+
batch = next(train_iter)
|
| 269 |
+
except StopIteration:
|
| 270 |
+
train_iter = iter(train_loader)
|
| 271 |
+
batch = next(train_iter)
|
| 272 |
+
|
| 273 |
+
lr = get_lr(step - 1, cfg.train.max_steps, cfg.train.lr, cfg.train.warmup_steps)
|
| 274 |
+
for pg in optimizer.param_groups:
|
| 275 |
+
pg["lr"] = lr
|
| 276 |
+
|
| 277 |
+
seq = batch["input_ids"].to(device)
|
| 278 |
+
plen = batch["prompt_lengths"].to(device)
|
| 279 |
+
|
| 280 |
+
optimizer.zero_grad(set_to_none=True)
|
| 281 |
+
with torch.autocast(device_type=device.type, dtype=amp_dtype, enabled=use_amp):
|
| 282 |
+
metrics = compute_unrolled_conveyer_loss(
|
| 283 |
+
model=model,
|
| 284 |
+
seq_tokens=seq,
|
| 285 |
+
cfg=cfg,
|
| 286 |
+
prompt_lengths=plen,
|
| 287 |
+
pad_token_id=tokenizer.pad_id,
|
| 288 |
+
)
|
| 289 |
+
metrics.loss.backward()
|
| 290 |
+
if cfg.train.grad_clip > 0.0:
|
| 291 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.train.grad_clip)
|
| 292 |
+
optimizer.step()
|
| 293 |
+
|
| 294 |
+
cooldown_ms = int(getattr(cfg.train, "step_cooldown_ms", 0))
|
| 295 |
+
if cooldown_ms > 0:
|
| 296 |
+
if device.type == "cuda":
|
| 297 |
+
torch.cuda.synchronize()
|
| 298 |
+
time.sleep(cooldown_ms / 1000.0)
|
| 299 |
+
|
| 300 |
+
total_elapsed = prior_elapsed + (time.time() - t0)
|
| 301 |
+
|
| 302 |
+
if step % cfg.train.log_every == 0 or step == start_step:
|
| 303 |
+
acc_back = float(metrics.per_slot_acc[-1].item())
|
| 304 |
+
acc_mid = float(metrics.per_slot_acc[model.belt_size // 2].item())
|
| 305 |
+
steps_done = max(1, step - start_step + 1)
|
| 306 |
+
sec_per_step = (time.time() - t0) / steps_done
|
| 307 |
+
gpu_temp = _query_gpu_temp_c() if device.type == "cuda" else None
|
| 308 |
+
temp_str = f" | {gpu_temp}C" if gpu_temp is not None else ""
|
| 309 |
+
print(
|
| 310 |
+
f"step {step:6d}/{cfg.train.max_steps} | loss {metrics.loss.item():.4f} | "
|
| 311 |
+
f"lm_ce {metrics.prefix_lm_loss:.3f} (acc {metrics.prefix_lm_acc:.3f}) | "
|
| 312 |
+
f"s0_ce {metrics.slot0_loss:.3f} (acc {metrics.slot0_acc:.3f}) | "
|
| 313 |
+
f"belt(0/m/63) {metrics.slot0_acc:.2f}/{acc_mid:.2f}/{acc_back:.2f} | "
|
| 314 |
+
f"pause {metrics.mean_plan_pause*100:.0f}% | lr {lr:.2e} | "
|
| 315 |
+
f"{sec_per_step:.2f}s/it{temp_str} | {total_elapsed/3600:.2f}h",
|
| 316 |
+
flush=True,
|
| 317 |
+
)
|
| 318 |
+
max_temp = int(getattr(cfg.train, "max_gpu_temp_c", 80))
|
| 319 |
+
if gpu_temp is not None and max_temp > 0 and gpu_temp >= max_temp:
|
| 320 |
+
torch.cuda.synchronize()
|
| 321 |
+
while True:
|
| 322 |
+
print(
|
| 323 |
+
f" [THERMAL GUARD] GPU at {gpu_temp}C (>= {max_temp}C limit); pausing 8s to let fans cool GPU...",
|
| 324 |
+
flush=True,
|
| 325 |
+
)
|
| 326 |
+
time.sleep(8.0)
|
| 327 |
+
gpu_temp = _query_gpu_temp_c()
|
| 328 |
+
if gpu_temp is None or gpu_temp <= max_temp - 5:
|
| 329 |
+
print(f" [THERMAL GUARD] GPU cooled to {gpu_temp}C; resuming training.", flush=True)
|
| 330 |
+
break
|
| 331 |
+
|
| 332 |
+
is_time_up = max_hours > 0.0 and (total_elapsed >= max_hours * 3600.0)
|
| 333 |
+
is_eval_step = (step % cfg.train.eval_every == 0) or (step == cfg.train.max_steps) or is_time_up
|
| 334 |
+
is_save_step = (step % save_every == 0) or is_eval_step
|
| 335 |
+
|
| 336 |
+
if is_eval_step:
|
| 337 |
+
if device.type == "cuda":
|
| 338 |
+
torch.cuda.empty_cache()
|
| 339 |
+
val_m = evaluate_validation(
|
| 340 |
+
model=model,
|
| 341 |
+
val_loader=val_loader,
|
| 342 |
+
cfg=cfg,
|
| 343 |
+
device=device,
|
| 344 |
+
pad_id=tokenizer.pad_id,
|
| 345 |
+
)
|
| 346 |
+
last_val_metrics = val_m
|
| 347 |
+
curve = val_m["val_per_slot_acc"]
|
| 348 |
+
B = len(curve)
|
| 349 |
+
print(
|
| 350 |
+
f" [VAL step {step:6d} | {total_elapsed/3600:.2f}h] "
|
| 351 |
+
f"val_loss {val_m['val_loss']:.4f} | "
|
| 352 |
+
f"real_s0_ppl {val_m['val_slot0_ppl']:.2f} | real_s0_acc {val_m['val_slot0_acc']:.3f} | "
|
| 353 |
+
f"real_lm_ppl {val_m['val_lm_ppl']:.2f} | real_lm_acc {val_m['val_lm_acc']:.3f} | "
|
| 354 |
+
f"crystallization(63->32->8->0): "
|
| 355 |
+
f"{curve[B-1]:.2f}->{curve[B//2]:.2f}->{curve[min(8, B-1)]:.2f}->{curve[0]:.2f}",
|
| 356 |
+
flush=True,
|
| 357 |
+
)
|
| 358 |
+
if cfg.data.dataset == "tinystories":
|
| 359 |
+
sample_p = (
|
| 360 |
+
"Once upon a time, Lily and a little bunny walked into the green forest "
|
| 361 |
+
"to find the golden key near the locked treasure chest. Suddenly a big lion"
|
| 362 |
+
)
|
| 363 |
+
preview = _sample_preview(model, tokenizer, device, sample_p, max_new_tokens=28)
|
| 364 |
+
print(f" [SAMPLE] ...Suddenly a big lion -> {preview}", flush=True)
|
| 365 |
+
else:
|
| 366 |
+
preview = ""
|
| 367 |
+
|
| 368 |
+
if val_m["val_loss"] <= best_val_loss:
|
| 369 |
+
best_val_loss = val_m["val_loss"]
|
| 370 |
+
save_checkpoint(
|
| 371 |
+
out_dir / "best.pt",
|
| 372 |
+
model,
|
| 373 |
+
cfg,
|
| 374 |
+
step=step,
|
| 375 |
+
optimizer=None, # Keep best.pt lightweight (~115 MB) for fast inference & fewer disk writes
|
| 376 |
+
metrics=val_m,
|
| 377 |
+
best_val_loss=best_val_loss,
|
| 378 |
+
elapsed_sec=total_elapsed,
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
log_entry = {
|
| 382 |
+
"step": step,
|
| 383 |
+
"elapsed_hours": round(total_elapsed / 3600.0, 4),
|
| 384 |
+
"val_loss": round(val_m["val_loss"], 4),
|
| 385 |
+
"val_slot0_ce": round(val_m["val_slot0_ce"], 4),
|
| 386 |
+
"val_slot0_ppl": round(val_m["val_slot0_ppl"], 2),
|
| 387 |
+
"val_slot0_acc": round(val_m["val_slot0_acc"], 4),
|
| 388 |
+
"val_lm_ce": round(val_m["val_lm_ce"], 4),
|
| 389 |
+
"val_lm_ppl": round(val_m["val_lm_ppl"], 2),
|
| 390 |
+
"val_lm_acc": round(val_m["val_lm_acc"], 4),
|
| 391 |
+
"val_belt_acc": round(val_m["val_belt_acc"], 4),
|
| 392 |
+
"sample_lion": preview,
|
| 393 |
+
}
|
| 394 |
+
with open(log_jsonl, "a", encoding="utf-8") as lf:
|
| 395 |
+
lf.write(json.dumps(log_entry) + "\n")
|
| 396 |
+
lf.flush()
|
| 397 |
+
os.fsync(lf.fileno())
|
| 398 |
+
|
| 399 |
+
if is_save_step:
|
| 400 |
+
save_checkpoint(
|
| 401 |
+
out_dir / "last.pt",
|
| 402 |
+
model,
|
| 403 |
+
cfg,
|
| 404 |
+
step=step,
|
| 405 |
+
optimizer=optimizer,
|
| 406 |
+
metrics=last_val_metrics,
|
| 407 |
+
best_val_loss=best_val_loss,
|
| 408 |
+
elapsed_sec=total_elapsed,
|
| 409 |
+
)
|
| 410 |
+
if step % milestone_every == 0:
|
| 411 |
+
save_checkpoint(
|
| 412 |
+
out_dir / f"ckpt_step_{step:06d}.pt",
|
| 413 |
+
model,
|
| 414 |
+
cfg,
|
| 415 |
+
step=step,
|
| 416 |
+
optimizer=optimizer,
|
| 417 |
+
metrics=last_val_metrics,
|
| 418 |
+
best_val_loss=best_val_loss,
|
| 419 |
+
elapsed_sec=total_elapsed,
|
| 420 |
+
)
|
| 421 |
+
_prune_old_milestones(out_dir, keep_last=8)
|
| 422 |
+
if device.type == "cuda":
|
| 423 |
+
torch.cuda.empty_cache()
|
| 424 |
+
|
| 425 |
+
if is_time_up:
|
| 426 |
+
print(
|
| 427 |
+
f"[ConveyerTrain] Reached max_hours={max_hours:.2f}h budget at step {step}. "
|
| 428 |
+
f"Checkpoints saved to {out_dir}.",
|
| 429 |
+
flush=True,
|
| 430 |
+
)
|
| 431 |
+
break
|
| 432 |
+
|
| 433 |
+
return last_val_metrics
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def main() -> None:
|
| 437 |
+
parser = argparse.ArgumentParser(description="Train Conveyor-Belt Diffusion LLM")
|
| 438 |
+
parser.add_argument("--config", type=str, default="configs/synthetic.yaml", help="Path to YAML config")
|
| 439 |
+
parser.add_argument("--init", type=str, default=None, help="Optional checkpoint (.pt) to warm-start weights from")
|
| 440 |
+
parser.add_argument(
|
| 441 |
+
"--resume",
|
| 442 |
+
type=str,
|
| 443 |
+
default=None,
|
| 444 |
+
help="Checkpoint path (.pt) or 'auto' to resume full training state (model + optimizer + step) after a crash",
|
| 445 |
+
)
|
| 446 |
+
parser.add_argument("--max_hours", type=float, default=None, help="Wall-clock training budget in hours (e.g. 24)")
|
| 447 |
+
parser.add_argument("--overfit", type=int, default=0, help="Overfit on N fixed batches for verification")
|
| 448 |
+
parser.add_argument("--no_thought_highway", action="store_true", help="Disable continuous thought highway (alpha_k = 0)")
|
| 449 |
+
parser.add_argument("--no_belt_bptt", action="store_true", help="Detach conveyor belt state between unrolled ticks (W=1)")
|
| 450 |
+
parser.add_argument("--ar_baseline", action="store_true", help="Train 1-pass causal Autoregressive baseline")
|
| 451 |
+
parser.add_argument("overrides", nargs="*", help="Config overrides in section.key=value format")
|
| 452 |
+
args = parser.parse_args()
|
| 453 |
+
|
| 454 |
+
cfg = load_config(args.config, args.overrides)
|
| 455 |
+
if args.max_hours is not None:
|
| 456 |
+
cfg.train.max_hours = args.max_hours
|
| 457 |
+
if args.overfit > 0:
|
| 458 |
+
cfg.train.overfit_batches = args.overfit
|
| 459 |
+
if args.no_thought_highway:
|
| 460 |
+
cfg.train.no_thought_highway = True
|
| 461 |
+
cfg.model.use_thought_highway = False
|
| 462 |
+
if args.no_belt_bptt:
|
| 463 |
+
cfg.train.no_belt_bptt = True
|
| 464 |
+
if args.ar_baseline:
|
| 465 |
+
cfg.train.ar_baseline = True
|
| 466 |
+
cfg.model.causal_belt_only = True
|
| 467 |
+
|
| 468 |
+
train_model(cfg, init_ckpt=args.init, resume_ckpt=args.resume)
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
if __name__ == "__main__":
|
| 472 |
+
main()
|
src/conveyer/web_sim.py
ADDED
|
@@ -0,0 +1,840 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
from urllib.parse import parse_qs, urlparse
|
| 10 |
+
import torch
|
| 11 |
+
from torch.utils.data import DataLoader
|
| 12 |
+
|
| 13 |
+
from conveyer.checkpoint import load_checkpoint
|
| 14 |
+
from conveyer.config import load_config
|
| 15 |
+
from conveyer.data import build_dataset_and_tokenizer, collate_conveyer_batch
|
| 16 |
+
from conveyer.eval import evaluate_crystallization_curve
|
| 17 |
+
from conveyer.infer import generate_conveyer
|
| 18 |
+
from conveyer.model import ConveyerDiffusionLM
|
| 19 |
+
|
| 20 |
+
HTML_PAGE = """<!DOCTYPE html>
|
| 21 |
+
<html lang="en">
|
| 22 |
+
<head>
|
| 23 |
+
<meta charset="UTF-8">
|
| 24 |
+
<title>Conveyor-Belt Diffusion LLM — Elastic Planning & Oven Simulator</title>
|
| 25 |
+
<style>
|
| 26 |
+
:root {
|
| 27 |
+
--bg: #0b0f17;
|
| 28 |
+
--panel: #151b26;
|
| 29 |
+
--panel-alt: #1b2332;
|
| 30 |
+
--border: #2a3447;
|
| 31 |
+
--text: #e6edf3;
|
| 32 |
+
--muted: #8b949e;
|
| 33 |
+
--front: #2ea043;
|
| 34 |
+
--front-glow: rgba(46, 160, 67, 0.25);
|
| 35 |
+
--mid: #d29922;
|
| 36 |
+
--back: #8957e5;
|
| 37 |
+
}
|
| 38 |
+
* { box-sizing: border-box; }
|
| 39 |
+
body {
|
| 40 |
+
margin: 0;
|
| 41 |
+
padding: 18px 24px 32px 24px;
|
| 42 |
+
background: var(--bg);
|
| 43 |
+
color: var(--text);
|
| 44 |
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
| 45 |
+
}
|
| 46 |
+
.header-row {
|
| 47 |
+
display: flex;
|
| 48 |
+
justify-content: space-between;
|
| 49 |
+
align-items: flex-start;
|
| 50 |
+
gap: 16px;
|
| 51 |
+
margin-bottom: 14px;
|
| 52 |
+
}
|
| 53 |
+
h1 { margin: 0 0 4px 0; font-size: 22px; letter-spacing: -0.3px; }
|
| 54 |
+
.subtitle { color: var(--muted); font-size: 13px; }
|
| 55 |
+
.badge-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
| 56 |
+
.metric-pill {
|
| 57 |
+
background: var(--panel-alt);
|
| 58 |
+
border: 1px solid var(--border);
|
| 59 |
+
border-radius: 6px;
|
| 60 |
+
padding: 6px 12px;
|
| 61 |
+
font-size: 12px;
|
| 62 |
+
}
|
| 63 |
+
.metric-pill strong { color: #3fb950; font-size: 14px; margin-left: 4px; }
|
| 64 |
+
|
| 65 |
+
.prompt-card {
|
| 66 |
+
background: var(--panel);
|
| 67 |
+
border: 1px solid var(--border);
|
| 68 |
+
border-radius: 10px;
|
| 69 |
+
padding: 14px 16px;
|
| 70 |
+
margin-bottom: 14px;
|
| 71 |
+
}
|
| 72 |
+
.prompt-top {
|
| 73 |
+
display: flex;
|
| 74 |
+
justify-content: space-between;
|
| 75 |
+
align-items: center;
|
| 76 |
+
margin-bottom: 8px;
|
| 77 |
+
flex-wrap: wrap;
|
| 78 |
+
gap: 8px;
|
| 79 |
+
}
|
| 80 |
+
.prompt-top label { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; }
|
| 81 |
+
.presets { display: flex; gap: 6px; flex-wrap: wrap; }
|
| 82 |
+
.preset-btn {
|
| 83 |
+
background: var(--panel-alt);
|
| 84 |
+
color: var(--text);
|
| 85 |
+
border: 1px solid var(--border);
|
| 86 |
+
border-radius: 5px;
|
| 87 |
+
padding: 4px 10px;
|
| 88 |
+
font-size: 12px;
|
| 89 |
+
cursor: pointer;
|
| 90 |
+
}
|
| 91 |
+
.preset-btn:hover { border-color: #58a6ff; }
|
| 92 |
+
.prompt-input-row {
|
| 93 |
+
display: flex;
|
| 94 |
+
gap: 10px;
|
| 95 |
+
align-items: center;
|
| 96 |
+
flex-wrap: wrap;
|
| 97 |
+
}
|
| 98 |
+
#promptInput {
|
| 99 |
+
flex: 1;
|
| 100 |
+
min-width: 320px;
|
| 101 |
+
background: #080b11;
|
| 102 |
+
color: var(--text);
|
| 103 |
+
border: 1px solid var(--border);
|
| 104 |
+
border-radius: 6px;
|
| 105 |
+
padding: 10px 12px;
|
| 106 |
+
font-size: 14px;
|
| 107 |
+
}
|
| 108 |
+
.knob {
|
| 109 |
+
display: flex;
|
| 110 |
+
flex-direction: column;
|
| 111 |
+
font-size: 11px;
|
| 112 |
+
color: var(--muted);
|
| 113 |
+
gap: 3px;
|
| 114 |
+
}
|
| 115 |
+
.knob input {
|
| 116 |
+
width: 72px;
|
| 117 |
+
background: #080b11;
|
| 118 |
+
color: var(--text);
|
| 119 |
+
border: 1px solid var(--border);
|
| 120 |
+
border-radius: 5px;
|
| 121 |
+
padding: 6px 8px;
|
| 122 |
+
font-size: 13px;
|
| 123 |
+
}
|
| 124 |
+
.btn-primary {
|
| 125 |
+
background: #238636;
|
| 126 |
+
color: #fff;
|
| 127 |
+
border: none;
|
| 128 |
+
border-radius: 6px;
|
| 129 |
+
padding: 10px 18px;
|
| 130 |
+
cursor: pointer;
|
| 131 |
+
font-weight: 600;
|
| 132 |
+
font-size: 14px;
|
| 133 |
+
}
|
| 134 |
+
.btn-primary:hover { background: #2ea043; }
|
| 135 |
+
.btn-secondary {
|
| 136 |
+
background: var(--panel-alt);
|
| 137 |
+
color: var(--text);
|
| 138 |
+
border: 1px solid var(--border);
|
| 139 |
+
border-radius: 6px;
|
| 140 |
+
padding: 8px 14px;
|
| 141 |
+
cursor: pointer;
|
| 142 |
+
font-weight: 600;
|
| 143 |
+
font-size: 13px;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.story-grid {
|
| 147 |
+
display: grid;
|
| 148 |
+
grid-template-columns: 2fr 1fr;
|
| 149 |
+
gap: 14px;
|
| 150 |
+
margin-bottom: 14px;
|
| 151 |
+
}
|
| 152 |
+
@media (max-width: 1000px) { .story-grid { grid-template-columns: 1fr; } }
|
| 153 |
+
.card {
|
| 154 |
+
background: var(--panel);
|
| 155 |
+
border: 1px solid var(--border);
|
| 156 |
+
border-radius: 10px;
|
| 157 |
+
padding: 14px 16px;
|
| 158 |
+
}
|
| 159 |
+
.card h3 {
|
| 160 |
+
margin: 0 0 10px 0;
|
| 161 |
+
font-size: 12px;
|
| 162 |
+
color: var(--muted);
|
| 163 |
+
text-transform: uppercase;
|
| 164 |
+
letter-spacing: 0.5px;
|
| 165 |
+
display: flex;
|
| 166 |
+
justify-content: space-between;
|
| 167 |
+
align-items: center;
|
| 168 |
+
}
|
| 169 |
+
.story-box {
|
| 170 |
+
font-size: 17px;
|
| 171 |
+
line-height: 1.65;
|
| 172 |
+
background: #080b11;
|
| 173 |
+
border: 1px solid var(--border);
|
| 174 |
+
border-radius: 8px;
|
| 175 |
+
padding: 14px 16px;
|
| 176 |
+
min-height: 112px;
|
| 177 |
+
}
|
| 178 |
+
.story-prompt { color: #a5d6ff; }
|
| 179 |
+
.story-gen { color: #3fb950; font-weight: 600; }
|
| 180 |
+
.story-latest {
|
| 181 |
+
background: rgba(63, 185, 80, 0.22);
|
| 182 |
+
border-bottom: 2px solid #3fb950;
|
| 183 |
+
padding: 1px 4px;
|
| 184 |
+
border-radius: 4px;
|
| 185 |
+
}
|
| 186 |
+
.story-upcoming { color: #8b949e; font-style: italic; font-size: 14px; }
|
| 187 |
+
|
| 188 |
+
.cryst-row {
|
| 189 |
+
display: flex;
|
| 190 |
+
justify-content: space-between;
|
| 191 |
+
font-size: 12px;
|
| 192 |
+
padding: 5px 0;
|
| 193 |
+
border-bottom: 1px solid #1f2735;
|
| 194 |
+
}
|
| 195 |
+
.cryst-bar-wrap {
|
| 196 |
+
width: 100px;
|
| 197 |
+
height: 8px;
|
| 198 |
+
background: #080b11;
|
| 199 |
+
border-radius: 4px;
|
| 200 |
+
overflow: hidden;
|
| 201 |
+
display: inline-block;
|
| 202 |
+
vertical-align: middle;
|
| 203 |
+
margin-left: 8px;
|
| 204 |
+
}
|
| 205 |
+
.cryst-bar { height: 100%; background: #3fb950; }
|
| 206 |
+
|
| 207 |
+
.oven-card {
|
| 208 |
+
background: var(--panel);
|
| 209 |
+
border: 2px solid var(--border);
|
| 210 |
+
border-radius: 12px;
|
| 211 |
+
padding: 16px;
|
| 212 |
+
}
|
| 213 |
+
.oven-controls {
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 14px;
|
| 217 |
+
margin-bottom: 12px;
|
| 218 |
+
flex-wrap: wrap;
|
| 219 |
+
}
|
| 220 |
+
.conveyor-track-wrapper {
|
| 221 |
+
position: relative;
|
| 222 |
+
background: #070a0f;
|
| 223 |
+
border: 2px solid #283246;
|
| 224 |
+
border-radius: 10px;
|
| 225 |
+
padding: 14px 12px;
|
| 226 |
+
overflow-x: auto;
|
| 227 |
+
}
|
| 228 |
+
.conveyor-belt-row {
|
| 229 |
+
display: flex;
|
| 230 |
+
gap: 8px;
|
| 231 |
+
align-items: stretch;
|
| 232 |
+
min-width: max-content;
|
| 233 |
+
}
|
| 234 |
+
.dough-ball {
|
| 235 |
+
width: 108px;
|
| 236 |
+
flex-shrink: 0;
|
| 237 |
+
background: #101622;
|
| 238 |
+
border: 1px solid var(--border);
|
| 239 |
+
border-radius: 8px;
|
| 240 |
+
padding: 8px;
|
| 241 |
+
}
|
| 242 |
+
.dough-ball.slot-zero {
|
| 243 |
+
width: 128px;
|
| 244 |
+
background: rgba(46, 160, 67, 0.14);
|
| 245 |
+
border: 2px solid #3fb950;
|
| 246 |
+
box-shadow: 0 0 14px var(--front-glow);
|
| 247 |
+
}
|
| 248 |
+
.dough-ball.zone-front { border-top: 3px solid var(--front); }
|
| 249 |
+
.dough-ball.zone-mid { border-top: 3px solid var(--mid); }
|
| 250 |
+
.dough-ball.zone-back { border-top: 3px solid var(--back); }
|
| 251 |
+
|
| 252 |
+
.ball-top {
|
| 253 |
+
display: flex;
|
| 254 |
+
justify-content: space-between;
|
| 255 |
+
font-size: 10px;
|
| 256 |
+
color: var(--muted);
|
| 257 |
+
margin-bottom: 4px;
|
| 258 |
+
}
|
| 259 |
+
.ball-word {
|
| 260 |
+
font-size: 15px;
|
| 261 |
+
font-weight: 700;
|
| 262 |
+
color: var(--text);
|
| 263 |
+
white-space: nowrap;
|
| 264 |
+
overflow: hidden;
|
| 265 |
+
text-overflow: ellipsis;
|
| 266 |
+
margin-bottom: 5px;
|
| 267 |
+
}
|
| 268 |
+
.ball-cand {
|
| 269 |
+
font-size: 10px;
|
| 270 |
+
color: var(--muted);
|
| 271 |
+
white-space: nowrap;
|
| 272 |
+
overflow: hidden;
|
| 273 |
+
text-overflow: ellipsis;
|
| 274 |
+
line-height: 1.35;
|
| 275 |
+
}
|
| 276 |
+
.conf-bar {
|
| 277 |
+
height: 4px;
|
| 278 |
+
background: #1f2735;
|
| 279 |
+
border-radius: 2px;
|
| 280 |
+
margin-top: 6px;
|
| 281 |
+
overflow: hidden;
|
| 282 |
+
}
|
| 283 |
+
.conf-fill { height: 100%; background: #3fb950; }
|
| 284 |
+
.zone-legend {
|
| 285 |
+
display: flex;
|
| 286 |
+
gap: 18px;
|
| 287 |
+
font-size: 12px;
|
| 288 |
+
color: var(--muted);
|
| 289 |
+
margin-top: 10px;
|
| 290 |
+
flex-wrap: wrap;
|
| 291 |
+
}
|
| 292 |
+
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; vertical-align: middle; }
|
| 293 |
+
</style>
|
| 294 |
+
</head>
|
| 295 |
+
<body>
|
| 296 |
+
|
| 297 |
+
<div class="header-row">
|
| 298 |
+
<div>
|
| 299 |
+
<h1>64-Slot Dual-Channel Elastic Conveyor-Belt Diffusion LLM</h1>
|
| 300 |
+
<div class="subtitle" id="modelSubtitle">Loading model info...</div>
|
| 301 |
+
</div>
|
| 302 |
+
<div class="badge-row">
|
| 303 |
+
<div class="metric-pill">Model Params: <strong id="paramBadge">--</strong></div>
|
| 304 |
+
<div class="metric-pill">Slot-0 Val Accuracy: <strong id="s0AccBadge">--</strong></div>
|
| 305 |
+
<div class="metric-pill">Back 75% Planning Pause: <strong id="pauseBadge">--</strong></div>
|
| 306 |
+
<div class="metric-pill">Sample Token Match: <strong id="sampleAccBadge">--</strong></div>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
|
| 310 |
+
<!-- 1. Prompt Input & Controls -->
|
| 311 |
+
<div class="prompt-card">
|
| 312 |
+
<div class="prompt-top">
|
| 313 |
+
<label>1. Enter Any Initial Text Prompt (Press Enter or Click "Bake & Continue Text"):</label>
|
| 314 |
+
<div class="presets">
|
| 315 |
+
<button class="preset-btn" onclick="setPreset(0)">Preset 1: Lion in the Forest</button>
|
| 316 |
+
<button class="preset-btn" onclick="setPreset(1)">Preset 2: Robot & Lost Kitten</button>
|
| 317 |
+
<button class="preset-btn" onclick="setPreset(2)">Preset 3: Melting Snowman</button>
|
| 318 |
+
<button class="preset-btn" onclick="setPreset(3)">Preset 4: Missing Blue Crayon</button>
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
| 321 |
+
<div class="prompt-input-row">
|
| 322 |
+
<input type="text" id="promptInput" onkeydown="if(event.key==='Enter'){runGenerate(-1);}" value="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" />
|
| 323 |
+
<div class="knob">
|
| 324 |
+
<span>Pre-Bake Overlap (W)</span>
|
| 325 |
+
<input type="number" id="overlapInput" value="12" min="0" max="60" />
|
| 326 |
+
</div>
|
| 327 |
+
<div class="knob">
|
| 328 |
+
<span>Oven Slots (B)</span>
|
| 329 |
+
<input type="number" id="bakeInput" value="64" min="1" max="64" />
|
| 330 |
+
</div>
|
| 331 |
+
<div class="knob">
|
| 332 |
+
<span>Generate Tokens</span>
|
| 333 |
+
<input type="number" id="tokensInput" value="32" min="6" max="64" />
|
| 334 |
+
</div>
|
| 335 |
+
<div class="knob">
|
| 336 |
+
<span>Temp (τ)</span>
|
| 337 |
+
<input type="number" id="tempInput" value="0.0" step="0.1" min="0.0" max="1.5" />
|
| 338 |
+
</div>
|
| 339 |
+
<button class="btn-primary" id="bakeBtn" onclick="runGenerate(-1)">Bake & Continue Text</button>
|
| 340 |
+
</div>
|
| 341 |
+
</div>
|
| 342 |
+
|
| 343 |
+
<!-- 2. Live Text Continuation + Accuracy Panel -->
|
| 344 |
+
<div class="story-grid">
|
| 345 |
+
<div class="card">
|
| 346 |
+
<h3>
|
| 347 |
+
<span>2. Live Text Continuation (Blue = Input Prompt • Green = Popped Out of Slot 0)</span>
|
| 348 |
+
<span id="tickModeLabel" style="color:#3fb950;"></span>
|
| 349 |
+
</h3>
|
| 350 |
+
<div class="story-box">
|
| 351 |
+
<span id="storyPromptSpan" class="story-prompt"></span><span id="storyGenSpan" class="story-gen"></span><span id="storyLatestSpan" class="story-latest" style="display:none;"></span>
|
| 352 |
+
<span id="storyPreviewSpan" class="story-upcoming"></span>
|
| 353 |
+
</div>
|
| 354 |
+
<div id="groundTruthBox" style="margin-top:10px; font-size:12px; color:var(--muted);"></div>
|
| 355 |
+
</div>
|
| 356 |
+
|
| 357 |
+
<div class="card">
|
| 358 |
+
<h3>
|
| 359 |
+
<span>Oven Crystallization & Accuracy</span>
|
| 360 |
+
<span style="font-size:11px; color:#58a6ff;">Back (k=63) → Front (k=0)</span>
|
| 361 |
+
</h3>
|
| 362 |
+
<div id="crystTable"></div>
|
| 363 |
+
</div>
|
| 364 |
+
</div>
|
| 365 |
+
|
| 366 |
+
<!-- 3. Bottom Conveyor Belt Oven -->
|
| 367 |
+
<div class="oven-card">
|
| 368 |
+
<div class="oven-controls">
|
| 369 |
+
<strong style="font-size:13px; text-transform:uppercase; color:var(--muted);">3. Live 64-Slot Conveyor & Planning Belt (Bottom):</strong>
|
| 370 |
+
<button class="btn-secondary" onclick="stepTick(-1)">← Prev Tick</button>
|
| 371 |
+
<button class="btn-primary" id="playBtn" onclick="togglePlay()" style="padding:7px 16px;">Pause</button>
|
| 372 |
+
<button class="btn-secondary" onclick="stepTick(1)">Next Tick →</button>
|
| 373 |
+
<label style="font-size:12px; color:var(--muted); display:flex; align-items:center; gap:8px; flex:1; min-width:220px;">
|
| 374 |
+
<span id="scrubberLabel">Tick 0/0</span>
|
| 375 |
+
<input type="range" id="tickSlider" min="0" max="0" value="0" style="flex:1;" oninput="onScrub(this.value)" />
|
| 376 |
+
</label>
|
| 377 |
+
<label style="font-size:12px; color:var(--muted);">
|
| 378 |
+
Speed:
|
| 379 |
+
<select id="speedSelect" onchange="updateSpeed()" style="background:#080b11; color:var(--text); border:1px solid var(--border); padding:4px; border-radius:4px;">
|
| 380 |
+
<option value="450">Slow (2.2 Hz)</option>
|
| 381 |
+
<option value="240" selected>Normal (4 Hz)</option>
|
| 382 |
+
<option value="110">Fast (9 Hz)</option>
|
| 383 |
+
</select>
|
| 384 |
+
</label>
|
| 385 |
+
</div>
|
| 386 |
+
|
| 387 |
+
<div class="conveyor-track-wrapper">
|
| 388 |
+
<div id="conveyorRow" class="conveyor-belt-row"></div>
|
| 389 |
+
</div>
|
| 390 |
+
|
| 391 |
+
<div class="zone-legend">
|
| 392 |
+
<span><i class="dot" style="background:#3fb950;"></i><strong>Slot 0 (Front Exit, α<sub>0</sub> = 0):</strong> Confirmed Token Popping Out</span>
|
| 393 |
+
<span><i class="dot" style="background:#2ea043;"></i><strong>Slots 1..15 (Front 25% Conveyor):</strong> Shifts Left Every Tick (100% Speed)</span>
|
| 394 |
+
<span><i class="dot" style="background:#d29922;"></i><strong>Slot 15..20 (Mid-Belt Expansion):</strong> New Clauses Form Here</span>
|
| 395 |
+
<span><i class="dot" style="background:#8957e5;"></i><strong>Slots 16..63 (Back 75% Elastic Planning):</strong> Pauses in Place Holding Long-Range Sentence Plan</span>
|
| 396 |
+
</div>
|
| 397 |
+
</div>
|
| 398 |
+
|
| 399 |
+
<script>
|
| 400 |
+
const PRESETS = [
|
| 401 |
+
"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",
|
| 402 |
+
"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",
|
| 403 |
+
"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",
|
| 404 |
+
"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"
|
| 405 |
+
];
|
| 406 |
+
|
| 407 |
+
let traceData = null;
|
| 408 |
+
let curFrame = 0;
|
| 409 |
+
let playing = true;
|
| 410 |
+
let intervalMs = 240;
|
| 411 |
+
let timerHandle = null;
|
| 412 |
+
|
| 413 |
+
function setPreset(idx) {
|
| 414 |
+
const inp = document.getElementById('promptInput');
|
| 415 |
+
inp.value = PRESETS[idx];
|
| 416 |
+
runGenerate(-1);
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
async function runGenerate(sampleOverride = -1) {
|
| 420 |
+
const btn = document.getElementById('bakeBtn');
|
| 421 |
+
btn.textContent = "Baking...";
|
| 422 |
+
const promptText = document.getElementById('promptInput').value.trim();
|
| 423 |
+
const ov = document.getElementById('overlapInput').value;
|
| 424 |
+
const bs = document.getElementById('bakeInput').value;
|
| 425 |
+
const mt = document.getElementById('tokensInput').value;
|
| 426 |
+
const tp = document.getElementById('tempInput').value;
|
| 427 |
+
|
| 428 |
+
let url = `/api/trace?overlap=${ov}&bake_steps=${bs}&tokens=${mt}&temp=${tp}&_t=${Date.now()}`;
|
| 429 |
+
if (sampleOverride >= 0 || promptText === "") {
|
| 430 |
+
url += `&sample=${Math.max(0, sampleOverride)}`;
|
| 431 |
+
} else {
|
| 432 |
+
url += `&prompt=${encodeURIComponent(promptText)}`;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
const resp = await fetch(url, { cache: "no-store" });
|
| 436 |
+
traceData = await resp.json();
|
| 437 |
+
btn.textContent = "Bake & Continue Text";
|
| 438 |
+
|
| 439 |
+
if (sampleOverride >= 0 || promptText === "") {
|
| 440 |
+
document.getElementById('promptInput').value = traceData.prompt;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
document.getElementById('modelSubtitle').textContent =
|
| 444 |
+
`Checkpoint: ${traceData.ckpt_name} | Tokenizer: Tiktoken BPE (${traceData.vocab_size} vocab) | Input: [All Prefix + 64 Belt] -> Output: [1 Token + 64 Belt]`;
|
| 445 |
+
document.getElementById('paramBadge').textContent = `${traceData.params_m}M`;
|
| 446 |
+
document.getElementById('s0AccBadge').textContent = `${(traceData.val_slot0_acc * 100).toFixed(1)}%`;
|
| 447 |
+
document.getElementById('sampleAccBadge').textContent =
|
| 448 |
+
traceData.sample_token_acc !== null ? `${(traceData.sample_token_acc * 100).toFixed(1)}%` : "Custom Input";
|
| 449 |
+
|
| 450 |
+
renderCrystallizationTable(traceData.crystallization);
|
| 451 |
+
|
| 452 |
+
// Jump straight to the first live emitted token tick so the user immediately sees their prompt continuation
|
| 453 |
+
let firstEmitIdx = 0;
|
| 454 |
+
for (let i = 0; i < traceData.snapshots.length; i++) {
|
| 455 |
+
if (!traceData.snapshots[i].is_warmup) {
|
| 456 |
+
firstEmitIdx = i;
|
| 457 |
+
break;
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
curFrame = firstEmitIdx;
|
| 461 |
+
const slider = document.getElementById('tickSlider');
|
| 462 |
+
slider.max = Math.max(0, traceData.snapshots.length - 1);
|
| 463 |
+
slider.value = curFrame;
|
| 464 |
+
renderFrame(curFrame);
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
function renderCrystallizationTable(cryst) {
|
| 468 |
+
const container = document.getElementById('crystTable');
|
| 469 |
+
if (!cryst || !cryst.length) {
|
| 470 |
+
container.innerHTML = '<div style="color:var(--muted);font-size:12px;">No crystallization stats available.</div>';
|
| 471 |
+
return;
|
| 472 |
+
}
|
| 473 |
+
container.innerHTML = cryst.map(row => {
|
| 474 |
+
const pct = Math.round(row.acc * 100);
|
| 475 |
+
return `
|
| 476 |
+
<div class="cryst-row">
|
| 477 |
+
<span><strong>Slot ${row.slot}</strong> (${row.zone})</span>
|
| 478 |
+
<span>
|
| 479 |
+
H=${row.entropy.toFixed(2)} | <strong>${pct}%</strong>
|
| 480 |
+
<span class="cryst-bar-wrap"><span class="cryst-bar" style="width:${pct}%"></span></span>
|
| 481 |
+
</span>
|
| 482 |
+
</div>
|
| 483 |
+
`;
|
| 484 |
+
}).join('');
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
function onScrub(val) {
|
| 488 |
+
curFrame = parseInt(val, 10);
|
| 489 |
+
renderFrame(curFrame);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
function stepTick(delta) {
|
| 493 |
+
if (!traceData || !traceData.snapshots.length) return;
|
| 494 |
+
playing = false;
|
| 495 |
+
document.getElementById('playBtn').textContent = 'Play';
|
| 496 |
+
curFrame = (curFrame + delta + traceData.snapshots.length) % traceData.snapshots.length;
|
| 497 |
+
document.getElementById('tickSlider').value = curFrame;
|
| 498 |
+
renderFrame(curFrame);
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
function togglePlay() {
|
| 502 |
+
playing = !playing;
|
| 503 |
+
document.getElementById('playBtn').textContent = playing ? 'Pause' : 'Play';
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
function updateSpeed() {
|
| 507 |
+
intervalMs = parseInt(document.getElementById('speedSelect').value, 10);
|
| 508 |
+
if (timerHandle) clearInterval(timerHandle);
|
| 509 |
+
timerHandle = setInterval(tickLoop, intervalMs);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
function renderFrame(idx) {
|
| 513 |
+
if (!traceData || !traceData.snapshots.length) return;
|
| 514 |
+
const snap = traceData.snapshots[idx];
|
| 515 |
+
document.getElementById('scrubberLabel').textContent =
|
| 516 |
+
`Tick ${idx + 1}/${traceData.snapshots.length} (Pos t=${snap.tick})`;
|
| 517 |
+
document.getElementById('pauseBadge').textContent = `${Math.round(snap.back_pause_pct)}% Paused`;
|
| 518 |
+
|
| 519 |
+
document.getElementById('tickModeLabel').textContent = snap.is_warmup
|
| 520 |
+
? `[PRE-BAKING WARMUP: Clamping Input "${snap.emitted_piece}" while Belt Plans Ahead]`
|
| 521 |
+
: `[LIVE BAKING: Slot 0 Popped "${snap.emitted_piece}" | Back 75% Belt ${Math.round(snap.back_pause_pct)}% Paused]`;
|
| 522 |
+
|
| 523 |
+
document.getElementById('storyPromptSpan').textContent = traceData.prompt;
|
| 524 |
+
|
| 525 |
+
const genPieces = [];
|
| 526 |
+
for (let i = 0; i <= idx; i++) {
|
| 527 |
+
if (!traceData.snapshots[i].is_warmup) {
|
| 528 |
+
genPieces.push(traceData.snapshots[i].emitted_piece);
|
| 529 |
+
}
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
const latestSpan = document.getElementById('storyLatestSpan');
|
| 533 |
+
if (genPieces.length > 0) {
|
| 534 |
+
const prevText = genPieces.slice(0, -1).join('');
|
| 535 |
+
const lastPiece = genPieces[genPieces.length - 1];
|
| 536 |
+
document.getElementById('storyGenSpan').textContent = prevText;
|
| 537 |
+
latestSpan.style.display = 'inline';
|
| 538 |
+
latestSpan.textContent = lastPiece;
|
| 539 |
+
} else {
|
| 540 |
+
document.getElementById('storyGenSpan').textContent = "";
|
| 541 |
+
latestSpan.style.display = 'none';
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
const previewWords = snap.slots.slice(1, 7).map(s => s.piece).join('');
|
| 545 |
+
document.getElementById('storyPreviewSpan').textContent = ` [next on belt: ${previewWords} ...]`;
|
| 546 |
+
|
| 547 |
+
const gtBox = document.getElementById('groundTruthBox');
|
| 548 |
+
if (traceData.ground_truth_continuation) {
|
| 549 |
+
gtBox.innerHTML = `<strong>Reference Continuation:</strong> ${traceData.ground_truth_continuation}`;
|
| 550 |
+
} else {
|
| 551 |
+
gtBox.innerHTML = `<strong>Dynamic Prompt Active:</strong> Continuing "${traceData.prompt.slice(0, 64)}..." in real time`;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
const row = document.getElementById('conveyorRow');
|
| 555 |
+
row.innerHTML = '';
|
| 556 |
+
for (let k = 0; k < snap.slots.length; k++) {
|
| 557 |
+
const s = snap.slots[k];
|
| 558 |
+
const zoneClass = k === 0 ? 'slot-zero zone-front' : (k < 16 ? 'zone-front' : (k < 22 ? 'zone-mid' : 'zone-back'));
|
| 559 |
+
const pct = Math.round(s.prob * 100);
|
| 560 |
+
const pausePct = Math.round((1.0 - s.shift_prob) * 100);
|
| 561 |
+
const div = document.createElement('div');
|
| 562 |
+
div.className = `dough-ball ${zoneClass}`;
|
| 563 |
+
const label = k === 0 ? 'SLOT 0 (POP)' : (k === 15 ? 'SLOT 15 (EXPAND)' : (k >= 16 ? `Plan ${k}` : `Conv ${k}`));
|
| 564 |
+
const top2 = s.top3.slice(1, 3).map(c => `${c.w.trim() || c.w}(${Math.round(c.p*100)}%)`).join(', ');
|
| 565 |
+
const motionTag = k < 16 ? `Shift 100%` : `Pause ${pausePct}%`;
|
| 566 |
+
div.innerHTML = `
|
| 567 |
+
<div class="ball-top"><span>${label}</span><span>${pct}%</span></div>
|
| 568 |
+
<div class="ball-word">${s.piece.trim() || s.piece}</div>
|
| 569 |
+
<div class="ball-cand">2nd: ${top2 || '-'}</div>
|
| 570 |
+
<div class="ball-cand">${motionTag} | H=${s.entropy.toFixed(1)}</div>
|
| 571 |
+
<div class="conf-bar"><div class="conf-fill" style="width:${pct}%"></div></div>
|
| 572 |
+
`;
|
| 573 |
+
row.appendChild(div);
|
| 574 |
+
}
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
function tickLoop() {
|
| 578 |
+
if (playing && traceData && traceData.snapshots.length > 0) {
|
| 579 |
+
curFrame = (curFrame + 1) % traceData.snapshots.length;
|
| 580 |
+
document.getElementById('tickSlider').value = curFrame;
|
| 581 |
+
renderFrame(curFrame);
|
| 582 |
+
}
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
timerHandle = setInterval(tickLoop, intervalMs);
|
| 586 |
+
runGenerate(-1);
|
| 587 |
+
</script>
|
| 588 |
+
</body>
|
| 589 |
+
</html>
|
| 590 |
+
"""
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
class ConveyerWebApp:
|
| 594 |
+
def __init__(
|
| 595 |
+
self,
|
| 596 |
+
model: ConveyerDiffusionLM,
|
| 597 |
+
cfg: Any,
|
| 598 |
+
ckpt_path: str,
|
| 599 |
+
ckpt_meta: dict[str, Any],
|
| 600 |
+
val_ds: Any,
|
| 601 |
+
tokenizer: Any,
|
| 602 |
+
device: torch.device,
|
| 603 |
+
) -> None:
|
| 604 |
+
self.model = model
|
| 605 |
+
self.cfg = cfg
|
| 606 |
+
self.ckpt_path = ckpt_path
|
| 607 |
+
self.ckpt_meta = ckpt_meta
|
| 608 |
+
self.val_ds = val_ds
|
| 609 |
+
self.tokenizer = tokenizer
|
| 610 |
+
self.device = device
|
| 611 |
+
self.cached_cryst = self._compute_initial_stats()
|
| 612 |
+
|
| 613 |
+
def _compute_initial_stats(self) -> list[dict[str, Any]]:
|
| 614 |
+
metrics = self.ckpt_meta.get("metrics", {})
|
| 615 |
+
acc_curve = metrics.get("val_per_slot_acc")
|
| 616 |
+
ent_curve = metrics.get("val_per_slot_entropy")
|
| 617 |
+
if not acc_curve or not ent_curve:
|
| 618 |
+
val_loader = DataLoader(
|
| 619 |
+
self.val_ds,
|
| 620 |
+
batch_size=min(32, len(self.val_ds)),
|
| 621 |
+
shuffle=False,
|
| 622 |
+
collate_fn=lambda b: collate_conveyer_batch(b, pad_id=self.tokenizer.pad_id),
|
| 623 |
+
)
|
| 624 |
+
c = evaluate_crystallization_curve(
|
| 625 |
+
self.model, self.cfg, val_loader, self.device, pad_id=self.tokenizer.pad_id, max_batches=2
|
| 626 |
+
)
|
| 627 |
+
acc_curve = c["per_slot_acc"]
|
| 628 |
+
ent_curve = c["per_slot_entropy"]
|
| 629 |
+
|
| 630 |
+
B = len(acc_curve)
|
| 631 |
+
slots_to_show = [0, 3, 8, 15, 24, 36, 48, B - 1]
|
| 632 |
+
rows: list[dict[str, Any]] = []
|
| 633 |
+
seen: set[int] = set()
|
| 634 |
+
for k in slots_to_show:
|
| 635 |
+
k = min(k, B - 1)
|
| 636 |
+
if k in seen:
|
| 637 |
+
continue
|
| 638 |
+
seen.add(k)
|
| 639 |
+
zone = (
|
| 640 |
+
"Slot 0 Pop"
|
| 641 |
+
if k == 0
|
| 642 |
+
else ("Front Conveyor" if k < 16 else ("Mid Expansion" if k == 15 else "Back 75% Plan"))
|
| 643 |
+
)
|
| 644 |
+
rows.append(
|
| 645 |
+
{
|
| 646 |
+
"slot": k,
|
| 647 |
+
"zone": zone,
|
| 648 |
+
"acc": float(acc_curve[k]),
|
| 649 |
+
"entropy": float(ent_curve[k]),
|
| 650 |
+
}
|
| 651 |
+
)
|
| 652 |
+
return rows
|
| 653 |
+
|
| 654 |
+
def compute_trace(
|
| 655 |
+
self,
|
| 656 |
+
prompt_text: str | None,
|
| 657 |
+
sample_idx: int,
|
| 658 |
+
overlap: int,
|
| 659 |
+
bake_steps: int,
|
| 660 |
+
max_tokens: int,
|
| 661 |
+
temperature: float = 0.0,
|
| 662 |
+
) -> dict[str, Any]:
|
| 663 |
+
gt_continuation: str | None = None
|
| 664 |
+
gt_target_ids: list[int] = []
|
| 665 |
+
|
| 666 |
+
if prompt_text is not None and prompt_text.strip() != "":
|
| 667 |
+
encoded = self.tokenizer.encode(prompt_text.strip())
|
| 668 |
+
prompt_ids = torch.tensor([encoded], dtype=torch.long, device=self.device)
|
| 669 |
+
for i in range(min(300, len(self.val_ds))):
|
| 670 |
+
seq = self.val_ds[i]["input_ids"]
|
| 671 |
+
if seq[: len(encoded)].tolist() == encoded:
|
| 672 |
+
rem = [
|
| 673 |
+
int(t)
|
| 674 |
+
for t in seq[len(encoded) :].tolist()
|
| 675 |
+
if int(t) not in (self.tokenizer.pad_id, self.tokenizer.eos_id)
|
| 676 |
+
]
|
| 677 |
+
gt_target_ids = rem[:max_tokens]
|
| 678 |
+
gt_continuation = self.tokenizer.decode(gt_target_ids)
|
| 679 |
+
break
|
| 680 |
+
else:
|
| 681 |
+
idx = max(0, min(sample_idx, len(self.val_ds) - 1))
|
| 682 |
+
item = self.val_ds[idx]
|
| 683 |
+
plen = int(item["prompt_len"])
|
| 684 |
+
seq = item["input_ids"]
|
| 685 |
+
prompt_ids = seq[:plen].unsqueeze(0).to(self.device)
|
| 686 |
+
rem = [int(t) for t in seq[plen:].tolist() if int(t) not in (self.tokenizer.pad_id, self.tokenizer.eos_id)]
|
| 687 |
+
gt_target_ids = rem[:max_tokens]
|
| 688 |
+
gt_continuation = self.tokenizer.decode(gt_target_ids)
|
| 689 |
+
|
| 690 |
+
full_out, snapshots = generate_conveyer(
|
| 691 |
+
model=self.model,
|
| 692 |
+
prompt_tokens=prompt_ids,
|
| 693 |
+
max_new_tokens=max_tokens,
|
| 694 |
+
prompt_overlap=overlap,
|
| 695 |
+
bake_steps=bake_steps,
|
| 696 |
+
temperature=temperature,
|
| 697 |
+
eos_token_id=self.tokenizer.eos_id,
|
| 698 |
+
deterministic_spawn=(temperature <= 1e-5),
|
| 699 |
+
)
|
| 700 |
+
|
| 701 |
+
plen_actual = prompt_ids.size(1)
|
| 702 |
+
gen_ids = full_out[0, plen_actual:].tolist()
|
| 703 |
+
|
| 704 |
+
sample_token_acc: float | None = None
|
| 705 |
+
if gt_target_ids:
|
| 706 |
+
comp_len = min(len(gen_ids), len(gt_target_ids))
|
| 707 |
+
if comp_len > 0:
|
| 708 |
+
matches = sum(1 for a, b in zip(gen_ids[:comp_len], gt_target_ids[:comp_len]) if a == b)
|
| 709 |
+
sample_token_acc = matches / comp_len
|
| 710 |
+
|
| 711 |
+
F_slots = min(self.model.front_slots, bake_steps)
|
| 712 |
+
serialized_snaps: list[dict[str, Any]] = []
|
| 713 |
+
for s in snapshots:
|
| 714 |
+
slots_info = []
|
| 715 |
+
B = len(s.slot_top1_tokens)
|
| 716 |
+
for k in range(B):
|
| 717 |
+
top3_list = [
|
| 718 |
+
{"w": self.tokenizer.id_to_piece(tid), "p": round(float(pr), 4)}
|
| 719 |
+
for tid, pr in zip(s.slot_top3_tokens[k], s.slot_top3_probs[k])
|
| 720 |
+
]
|
| 721 |
+
slots_info.append(
|
| 722 |
+
{
|
| 723 |
+
"k": k,
|
| 724 |
+
"piece": self.tokenizer.id_to_piece(s.slot_top1_tokens[k]),
|
| 725 |
+
"prob": round(float(s.slot_top1_probs[k]), 4),
|
| 726 |
+
"entropy": round(float(s.slot_entropies[k]), 3),
|
| 727 |
+
"gate": round(float(s.slot_gate_means[k]), 3),
|
| 728 |
+
"shift_prob": round(float(s.slot_shift_probs[k]), 3),
|
| 729 |
+
"top3": top3_list,
|
| 730 |
+
}
|
| 731 |
+
)
|
| 732 |
+
back_pause = (
|
| 733 |
+
100.0 * (1.0 - sum(s.slot_shift_probs[F_slots:]) / max(1, B - F_slots))
|
| 734 |
+
if B > F_slots
|
| 735 |
+
else 0.0
|
| 736 |
+
)
|
| 737 |
+
serialized_snaps.append(
|
| 738 |
+
{
|
| 739 |
+
"tick": s.tick,
|
| 740 |
+
"is_warmup": s.is_warmup,
|
| 741 |
+
"emitted_piece": self.tokenizer.id_to_piece(s.emitted_token),
|
| 742 |
+
"mean_gate": sum(s.slot_gate_means) / max(1, B),
|
| 743 |
+
"back_pause_pct": back_pause,
|
| 744 |
+
"slots": slots_info,
|
| 745 |
+
}
|
| 746 |
+
)
|
| 747 |
+
|
| 748 |
+
metrics = self.ckpt_meta.get("metrics", {})
|
| 749 |
+
val_s0_acc = float(metrics.get("val_slot0_acc", self.cached_cryst[0]["acc"] if self.cached_cryst else 0.0))
|
| 750 |
+
|
| 751 |
+
return {
|
| 752 |
+
"ckpt_name": self.ckpt_path,
|
| 753 |
+
"dataset": self.cfg.data.dataset,
|
| 754 |
+
"vocab_size": self.tokenizer.vocab_size,
|
| 755 |
+
"params_m": f"{self.model.num_parameters() / 1e6:.2f}",
|
| 756 |
+
"val_slot0_acc": val_s0_acc,
|
| 757 |
+
"sample_token_acc": sample_token_acc,
|
| 758 |
+
"prompt": self.tokenizer.decode(prompt_ids[0].tolist()),
|
| 759 |
+
"ground_truth_continuation": gt_continuation,
|
| 760 |
+
"crystallization": self.cached_cryst,
|
| 761 |
+
"snapshots": serialized_snaps,
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
|
| 765 |
+
def main() -> None:
|
| 766 |
+
parser = argparse.ArgumentParser(description="Interactive Web Simulator for the 64-Slot Conveyor-Belt Diffusion LLM")
|
| 767 |
+
parser.add_argument("--ckpt", type=str, default=None, help="Path to checkpoint (.pt)")
|
| 768 |
+
parser.add_argument("--config", type=str, default="configs/tinystories.yaml", help="Fallback config if no checkpoint")
|
| 769 |
+
parser.add_argument("--host", type=str, default=os.environ.get("HOST", "127.0.0.1"))
|
| 770 |
+
parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "8766")))
|
| 771 |
+
args = parser.parse_args()
|
| 772 |
+
|
| 773 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 774 |
+
ckpt_path = args.ckpt
|
| 775 |
+
if ckpt_path is None:
|
| 776 |
+
if Path("runs/tinystories/best.pt").exists():
|
| 777 |
+
ckpt_path = "runs/tinystories/best.pt"
|
| 778 |
+
elif Path("runs/synthetic/best.pt").exists():
|
| 779 |
+
ckpt_path = "runs/synthetic/best.pt"
|
| 780 |
+
else:
|
| 781 |
+
ckpt_path = "runs/tinystories/best.pt"
|
| 782 |
+
|
| 783 |
+
try:
|
| 784 |
+
model, cfg, ckpt_meta = load_checkpoint(ckpt_path, device=device)
|
| 785 |
+
except Exception:
|
| 786 |
+
cfg = load_config(args.config)
|
| 787 |
+
model = ConveyerDiffusionLM(cfg.model).to(device)
|
| 788 |
+
ckpt_path = "untrained"
|
| 789 |
+
ckpt_meta = {}
|
| 790 |
+
|
| 791 |
+
_, val_ds, tokenizer = build_dataset_and_tokenizer(cfg, infer_only=True)
|
| 792 |
+
app = ConveyerWebApp(
|
| 793 |
+
model=model,
|
| 794 |
+
cfg=cfg,
|
| 795 |
+
ckpt_path=ckpt_path,
|
| 796 |
+
ckpt_meta=ckpt_meta,
|
| 797 |
+
val_ds=val_ds,
|
| 798 |
+
tokenizer=tokenizer,
|
| 799 |
+
device=device,
|
| 800 |
+
)
|
| 801 |
+
|
| 802 |
+
class Handler(BaseHTTPRequestHandler):
|
| 803 |
+
def do_GET(self) -> None:
|
| 804 |
+
parsed = urlparse(self.path)
|
| 805 |
+
if parsed.path == "/api/trace":
|
| 806 |
+
qs = parse_qs(parsed.query)
|
| 807 |
+
prompt_text = qs.get("prompt", [None])[0]
|
| 808 |
+
sample_idx = int(qs.get("sample", ["0"])[0])
|
| 809 |
+
overlap = int(qs.get("overlap", ["12"])[0])
|
| 810 |
+
bake_steps = int(qs.get("bake_steps", ["64"])[0])
|
| 811 |
+
tokens = int(qs.get("tokens", ["32"])[0])
|
| 812 |
+
temp = float(qs.get("temp", ["0.0"])[0])
|
| 813 |
+
payload = json.dumps(
|
| 814 |
+
app.compute_trace(prompt_text, sample_idx, overlap, bake_steps, tokens, temperature=temp)
|
| 815 |
+
).encode("utf-8")
|
| 816 |
+
self.send_response(200)
|
| 817 |
+
self.send_header("Content-Type", "application/json")
|
| 818 |
+
self.send_header("Cache-Control", "no-store, no-cache, must-revalidate")
|
| 819 |
+
self.send_header("Content-Length", str(len(payload)))
|
| 820 |
+
self.end_headers()
|
| 821 |
+
self.wfile.write(payload)
|
| 822 |
+
else:
|
| 823 |
+
body = HTML_PAGE.encode("utf-8")
|
| 824 |
+
self.send_response(200)
|
| 825 |
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
| 826 |
+
self.send_header("Cache-Control", "no-store")
|
| 827 |
+
self.send_header("Content-Length", str(len(body)))
|
| 828 |
+
self.end_headers()
|
| 829 |
+
self.wfile.write(body)
|
| 830 |
+
|
| 831 |
+
def log_message(self, format: str, *args: Any) -> None:
|
| 832 |
+
return
|
| 833 |
+
|
| 834 |
+
server = HTTPServer((args.host, args.port), Handler)
|
| 835 |
+
print(f"Conveyor-Belt Diffusion LLM Web Simulator running at http://{args.host}:{args.port}", flush=True)
|
| 836 |
+
server.serve_forever()
|
| 837 |
+
|
| 838 |
+
|
| 839 |
+
if __name__ == "__main__":
|
| 840 |
+
main()
|
tests/test_conveyer.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from conveyer.config import ConveyerConfig, ModelConfig, load_config
|
| 6 |
+
from conveyer.data.synthetic import SyntheticLookaheadDataset
|
| 7 |
+
from conveyer.infer import generate_conveyer, stream_conveyer_generation
|
| 8 |
+
from conveyer.losses import build_horizon_weights, compute_unrolled_conveyer_loss, seed_corrupted_belt
|
| 9 |
+
from conveyer.model import BeltState, ConveyerDiffusionLM
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _small_model_cfg(
|
| 13 |
+
belt_size: int = 64,
|
| 14 |
+
use_thought_highway: bool = True,
|
| 15 |
+
causal_belt_only: bool = False,
|
| 16 |
+
) -> ModelConfig:
|
| 17 |
+
return ModelConfig(
|
| 18 |
+
vocab_size=64,
|
| 19 |
+
d_model=64,
|
| 20 |
+
n_heads=4,
|
| 21 |
+
n_layers=2,
|
| 22 |
+
d_ff=128,
|
| 23 |
+
max_seq_len=256,
|
| 24 |
+
dropout=0.0,
|
| 25 |
+
belt_size=belt_size,
|
| 26 |
+
tau_min=0.5,
|
| 27 |
+
tau_max=4.0,
|
| 28 |
+
tau_gamma=1.5,
|
| 29 |
+
gate_bias_init=1.0,
|
| 30 |
+
use_thought_highway=use_thought_highway,
|
| 31 |
+
causal_belt_only=causal_belt_only,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_prefix_kv_cache_chunked_equivalence() -> None:
|
| 36 |
+
"""
|
| 37 |
+
Verifies that encoding a prefix in one shot (`prefill_prefix(x_{0:P})`) produces the exact same
|
| 38 |
+
KV cache and downstream belt logits as incremental token-by-token `append_committed_token`.
|
| 39 |
+
"""
|
| 40 |
+
torch.manual_seed(123)
|
| 41 |
+
cfg = _small_model_cfg(belt_size=16)
|
| 42 |
+
model = ConveyerDiffusionLM(cfg).eval()
|
| 43 |
+
|
| 44 |
+
prefix = torch.randint(1, 60, (2, 10), dtype=torch.long)
|
| 45 |
+
belt = model.init_empty_belt(batch_size=2, tick=10, device=prefix.device, deterministic=True)
|
| 46 |
+
|
| 47 |
+
# Full prefill in one shot
|
| 48 |
+
kv_full = model.prefill_prefix(prefix)
|
| 49 |
+
out_full = model.forward_belt(belt, kv_cache=kv_full)
|
| 50 |
+
|
| 51 |
+
# Incremental prefill: first 4 tokens, then 1 by 1
|
| 52 |
+
kv_inc = model.prefill_prefix(prefix[:, :4])
|
| 53 |
+
for t in range(4, 10):
|
| 54 |
+
kv_inc = model.append_committed_token(prefix[:, t], kv_inc)
|
| 55 |
+
out_inc = model.forward_belt(belt, kv_cache=kv_inc)
|
| 56 |
+
|
| 57 |
+
assert kv_full.seq_len == kv_inc.seq_len == 10
|
| 58 |
+
for l_idx in range(cfg.n_layers):
|
| 59 |
+
assert torch.allclose(kv_full.k_cache[l_idx], kv_inc.k_cache[l_idx], atol=1e-5)
|
| 60 |
+
assert torch.allclose(kv_full.v_cache[l_idx], kv_inc.v_cache[l_idx], atol=1e-5)
|
| 61 |
+
assert torch.allclose(out_full.logits, out_inc.logits, atol=1e-5)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def test_slot0_lexical_anchoring_alpha_zero() -> None:
|
| 65 |
+
"""
|
| 66 |
+
Verifies that alpha_0 == 0.0 at Position 0:
|
| 67 |
+
Perturbing `m_thought[:, 0, :]` at Slot 0 must have ZERO effect on the composed dough input at Slot 0,
|
| 68 |
+
forcing Slot 0 to rely 100% on its crisp lexical projection `e_soft[:, 0, :]` before popping out.
|
| 69 |
+
Meanwhile, perturbing `m_thought[:, k, :]` for k > 0 MUST change `dough[:, k, :]`.
|
| 70 |
+
"""
|
| 71 |
+
torch.manual_seed(42)
|
| 72 |
+
cfg = _small_model_cfg(belt_size=64, use_thought_highway=True)
|
| 73 |
+
model = ConveyerDiffusionLM(cfg).eval()
|
| 74 |
+
|
| 75 |
+
belt1 = model.init_empty_belt(batch_size=2, tick=5, device=torch.device("cpu"), deterministic=True)
|
| 76 |
+
m_mod = belt1.m_thought.clone()
|
| 77 |
+
m_mod[:, 0, :] += 100.0 # Massive perturbation at Slot 0 thought vector
|
| 78 |
+
belt2 = BeltState(e_soft=belt1.e_soft.clone(), m_thought=m_mod, tick=5)
|
| 79 |
+
|
| 80 |
+
dough1 = model.compose_dough_inputs(belt1)
|
| 81 |
+
dough2 = model.compose_dough_inputs(belt2)
|
| 82 |
+
|
| 83 |
+
assert float(model.alpha_schedule[0].item()) == 0.0
|
| 84 |
+
assert float(model.alpha_schedule[-1].item()) == 1.0
|
| 85 |
+
assert torch.allclose(dough1[:, 0, :], dough2[:, 0, :], atol=1e-6)
|
| 86 |
+
|
| 87 |
+
# Now perturb Slot 63 thought vector -> dough[:, 63, :] must change
|
| 88 |
+
m_mod63 = belt1.m_thought.clone()
|
| 89 |
+
m_mod63[:, 63, :] += 10.0
|
| 90 |
+
belt3 = BeltState(e_soft=belt1.e_soft.clone(), m_thought=m_mod63, tick=5)
|
| 91 |
+
dough3 = model.compose_dough_inputs(belt3)
|
| 92 |
+
assert not torch.allclose(dough1[:, 63, :], dough3[:, 63, :], atol=1e-4)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def test_bidirectional_belt_attention_vs_causal_ar() -> None:
|
| 96 |
+
"""
|
| 97 |
+
Verifies that in Conveyor mode (`causal_belt_only=False`), Slot 0's output logits actively depend on
|
| 98 |
+
downstream dough balls at the back of the belt (e.g., Slot 63), whereas in AR baseline mode
|
| 99 |
+
(`causal_belt_only=True`), Slot 0 is completely blind to Slots 1..63.
|
| 100 |
+
"""
|
| 101 |
+
torch.manual_seed(7)
|
| 102 |
+
# 1. Conveyor bidirectional belt mode
|
| 103 |
+
cfg_bi = _small_model_cfg(belt_size=64, causal_belt_only=False)
|
| 104 |
+
model_bi = ConveyerDiffusionLM(cfg_bi).eval()
|
| 105 |
+
|
| 106 |
+
belt_a = model_bi.init_empty_belt(batch_size=1, tick=4, device=torch.device("cpu"), deterministic=True)
|
| 107 |
+
belt_b = BeltState(e_soft=belt_a.e_soft.clone(), m_thought=belt_a.m_thought.clone(), tick=4)
|
| 108 |
+
belt_b.e_soft[:, 63, :] += 5.0 # Perturb back of the belt (Slot 63)
|
| 109 |
+
|
| 110 |
+
out_bi_a = model_bi.forward_belt(belt_a)
|
| 111 |
+
out_bi_b = model_bi.forward_belt(belt_b)
|
| 112 |
+
# In bidirectional conveyor mode, Slot 0 uses the mental anchor from Slot 63
|
| 113 |
+
assert not torch.allclose(out_bi_a.slot0_logits, out_bi_b.slot0_logits, atol=1e-5)
|
| 114 |
+
|
| 115 |
+
# 2. Causal AR baseline mode
|
| 116 |
+
cfg_ar = _small_model_cfg(belt_size=64, causal_belt_only=True)
|
| 117 |
+
model_ar = ConveyerDiffusionLM(cfg_ar).eval()
|
| 118 |
+
out_ar_a = model_ar.forward_belt(belt_a)
|
| 119 |
+
out_ar_b = model_ar.forward_belt(belt_b)
|
| 120 |
+
# In causal AR mode, Slot 0 cannot see Slot 63
|
| 121 |
+
assert torch.allclose(out_ar_a.slot0_logits, out_ar_b.slot0_logits, atol=1e-6)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def test_left_shift_mechanics_and_elastic_planning_zone() -> None:
|
| 125 |
+
"""
|
| 126 |
+
Verifies both:
|
| 127 |
+
1. Strict 1-by-1 conveyor shift (`use_elastic_planning=False`), where slots 1..63 shift into 0..62.
|
| 128 |
+
2. Elastic Two-Zone Planning Belt (`use_elastic_planning=True`), where the front 25% (`0..14`) shifts
|
| 129 |
+
strictly from `1..15`, Slot 15 blends mid-belt sentence expansion (`expand_cand`), and the back 75%
|
| 130 |
+
(`16..63`) pauses elastically via `shift_prob < 1.0`.
|
| 131 |
+
"""
|
| 132 |
+
torch.manual_seed(99)
|
| 133 |
+
# 1. Strict conveyor mode
|
| 134 |
+
cfg_strict = _small_model_cfg(belt_size=64)
|
| 135 |
+
cfg_strict.use_elastic_planning = False
|
| 136 |
+
model_strict = ConveyerDiffusionLM(cfg_strict).eval()
|
| 137 |
+
|
| 138 |
+
belt = model_strict.init_empty_belt(batch_size=1, tick=0, device=torch.device("cpu"), deterministic=True)
|
| 139 |
+
step_out = model_strict.forward_belt(belt)
|
| 140 |
+
shifted = model_strict.shift_belt_left(step_out, current_tick=0, deterministic_spawn=True)
|
| 141 |
+
|
| 142 |
+
assert shifted.tick == 1
|
| 143 |
+
assert shifted.e_soft.shape == (1, 64, cfg_strict.d_model)
|
| 144 |
+
assert torch.allclose(shifted.e_soft[:, :63, :], step_out.e_soft[:, 1:, :], atol=1e-6)
|
| 145 |
+
assert torch.allclose(shifted.m_thought[:, :63, :], step_out.m_updated[:, 1:, :], atol=1e-6)
|
| 146 |
+
|
| 147 |
+
# 2. Elastic Two-Zone Planning mode (Front 25% = 16 slots, Back 75% = 48 slots)
|
| 148 |
+
cfg_elastic = _small_model_cfg(belt_size=64)
|
| 149 |
+
cfg_elastic.use_elastic_planning = True
|
| 150 |
+
cfg_elastic.front_conveyer_slots = 16
|
| 151 |
+
model_elastic = ConveyerDiffusionLM(cfg_elastic).eval()
|
| 152 |
+
|
| 153 |
+
step_el = model_elastic.forward_belt(belt)
|
| 154 |
+
shifted_el = model_elastic.shift_belt_left(step_el, current_tick=0, deterministic_spawn=True)
|
| 155 |
+
|
| 156 |
+
# Front 15 slots (0..14) shift 100% from (1..15)
|
| 157 |
+
assert torch.allclose(shifted_el.e_soft[:, :15, :], step_el.e_soft[:, 1:16, :], atol=1e-6)
|
| 158 |
+
assert torch.allclose(shifted_el.m_thought[:, :15, :], step_el.m_updated[:, 1:16, :], atol=1e-6)
|
| 159 |
+
# Back 75% planning slots have shift_prob < 0.5 (paused > 50% in-place)
|
| 160 |
+
assert float(step_el.shift_prob[:, 16:].mean().item()) < 0.50
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def test_prompt_overlap_prebaking_clamps_input_and_emits() -> None:
|
| 164 |
+
"""
|
| 165 |
+
Verifies the `n - W_pre` cold-start solution:
|
| 166 |
+
When `prompt_overlap = 6` on a prompt of length `n = 10`:
|
| 167 |
+
- Warm-up runs for ticks `t = 4, 5, 6, 7, 8, 9` (6 warm-up ticks), clamping emitted tokens to `prompt[t]`.
|
| 168 |
+
- Generation runs for `max_new_tokens = 8` ticks (`t = 10..17`), returning a sequence of length `10 + 8 = 18`.
|
| 169 |
+
"""
|
| 170 |
+
torch.manual_seed(11)
|
| 171 |
+
cfg = _small_model_cfg(belt_size=32)
|
| 172 |
+
model = ConveyerDiffusionLM(cfg).eval()
|
| 173 |
+
|
| 174 |
+
prompt = torch.arange(5, 15, dtype=torch.long).unsqueeze(0) # length n = 10
|
| 175 |
+
full_seq, snapshots = generate_conveyer(
|
| 176 |
+
model=model,
|
| 177 |
+
prompt_tokens=prompt,
|
| 178 |
+
max_new_tokens=8,
|
| 179 |
+
prompt_overlap=6,
|
| 180 |
+
bake_steps=32,
|
| 181 |
+
temperature=0.0,
|
| 182 |
+
deterministic_spawn=True,
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
assert full_seq.shape == (1, 18)
|
| 186 |
+
assert torch.equal(full_seq[:, :10], prompt)
|
| 187 |
+
assert len(snapshots) == 6 + 8
|
| 188 |
+
|
| 189 |
+
# First 6 snapshots must be warm-up ticks at t = 4..9 with clamped prompt tokens
|
| 190 |
+
for i in range(6):
|
| 191 |
+
assert snapshots[i].is_warmup is True
|
| 192 |
+
assert snapshots[i].tick == 4 + i
|
| 193 |
+
assert snapshots[i].emitted_token == int(prompt[0, 4 + i].item())
|
| 194 |
+
|
| 195 |
+
# Next 8 snapshots must be live emissions at t = 10..17
|
| 196 |
+
for j in range(8):
|
| 197 |
+
snap = snapshots[6 + j]
|
| 198 |
+
assert snap.is_warmup is False
|
| 199 |
+
assert snap.tick == 10 + j
|
| 200 |
+
assert snap.emitted_token == int(full_seq[0, 10 + j].item())
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def test_unrolled_bptt_gradient_flow_and_slot63_zero_leakage() -> None:
|
| 204 |
+
"""
|
| 205 |
+
Verifies that:
|
| 206 |
+
1. `seed_corrupted_belt` puts zero target signal at Slot 63 (pure noise).
|
| 207 |
+
2. `compute_unrolled_conveyer_loss` backpropagates non-zero gradients into both the lexical embeddings
|
| 208 |
+
and the continuous thought highway (`thought_cand`, `thought_gate`, `slot_emb`, `m_init`).
|
| 209 |
+
"""
|
| 210 |
+
torch.manual_seed(21)
|
| 211 |
+
full_cfg = ConveyerConfig(model=_small_model_cfg(belt_size=16, use_thought_highway=True))
|
| 212 |
+
full_cfg.train.unroll_steps = 4
|
| 213 |
+
model = ConveyerDiffusionLM(full_cfg.model).train()
|
| 214 |
+
|
| 215 |
+
# Check Slot B-1 has zero target signal in seed_corrupted_belt
|
| 216 |
+
torch.manual_seed(55)
|
| 217 |
+
t_a = torch.ones((1, 16), dtype=torch.long) * 5
|
| 218 |
+
b_a = seed_corrupted_belt(model, t_a, tick_start=2, cold_drop_prob=0.0)
|
| 219 |
+
torch.manual_seed(55)
|
| 220 |
+
t_b = torch.ones((1, 16), dtype=torch.long) * 5
|
| 221 |
+
t_b[:, -1] = 42 # Change target at the last slot (B-1)
|
| 222 |
+
b_b = seed_corrupted_belt(model, t_b, tick_start=2, cold_drop_prob=0.0)
|
| 223 |
+
assert torch.allclose(b_a.e_soft[:, -1, :], b_b.e_soft[:, -1, :], atol=1e-6)
|
| 224 |
+
|
| 225 |
+
# Check gradient flow across unrolled BPTT
|
| 226 |
+
seq = torch.randint(4, 50, (3, 40), dtype=torch.long)
|
| 227 |
+
plen = torch.tensor([8, 8, 8], dtype=torch.long)
|
| 228 |
+
metrics = compute_unrolled_conveyer_loss(
|
| 229 |
+
model=model,
|
| 230 |
+
seq_tokens=seq,
|
| 231 |
+
cfg=full_cfg,
|
| 232 |
+
prompt_lengths=plen,
|
| 233 |
+
tick_start=6,
|
| 234 |
+
)
|
| 235 |
+
metrics.loss.backward()
|
| 236 |
+
|
| 237 |
+
assert model.tok_emb.weight.grad is not None and model.tok_emb.weight.grad.abs().sum() > 0
|
| 238 |
+
assert model.slot_emb.weight.grad is not None and model.slot_emb.weight.grad.abs().sum() > 0
|
| 239 |
+
assert model.thought_cand.weight.grad is not None and model.thought_cand.weight.grad.abs().sum() > 0
|
| 240 |
+
assert model.thought_gate.weight.grad is not None and model.thought_gate.weight.grad.abs().sum() > 0
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def test_synthetic_lookahead_dataset_and_config_loader() -> None:
|
| 244 |
+
cfg = load_config("configs/synthetic.yaml", ["train.max_steps=10", "data.num_train=60"])
|
| 245 |
+
assert cfg.train.max_steps == 10
|
| 246 |
+
assert cfg.data.num_train == 60
|
| 247 |
+
|
| 248 |
+
ds = SyntheticLookaheadDataset(num_samples=12, seq_len=96, task="all", vocab_size=128)
|
| 249 |
+
assert len(ds) == 12
|
| 250 |
+
for i in range(6):
|
| 251 |
+
item = ds[i]
|
| 252 |
+
assert item["input_ids"].shape == (96,)
|
| 253 |
+
assert 4 < item["prompt_len"] < 60
|
| 254 |
+
assert item["task_id"] in (0, 1, 2)
|