"""echo-1 Stage 0 explainer (Gradio). Hide a block of a real Lua program and let two from-scratch models fill it back in: a block-diffusion model (parallel, refined over steps) and an autoregressive baseline (left to right). Every fill runs through the real Lua interpreter against held-out tests, so the green/red badge is proof it executes, not a guess. Run: .venv/bin/python -m viz.app Public link: ECHO_SHARE=1 .venv/bin/python -m viz.app """ from __future__ import annotations import html import json import os import subprocess import time import numpy as np import torch import torch.nn.functional as F import gradio as gr from ml.ar import build_prompt from ml.config import ModelConfig, TaskConfig from ml.data import _ids_canvas, make_block_lua from ml.model import Transformer from ml.tokenizer import Tokenizer DIFF_DIR = os.environ.get("ECHO_DIFF", "runs/ediff") AR_DIR = os.environ.get("ECHO_AR", "runs/ear") EVAL_PATH = os.environ.get("ECHO_EVAL", "data/easy_eval.jsonl") VERIFIER = os.environ.get("ECHO_VERIFIER", "./target/release/echo-data") DEVICE = "cpu" def load_model(run_dir): ckpt = torch.load(f"{run_dir}/model.pt", map_location=DEVICE, weights_only=False) mcfg = ModelConfig(**ckpt["model_cfg"]) model = Transformer(mcfg, causal=(ckpt["mode"] == "ar")).to(DEVICE) model.load_state_dict(ckpt["model"]) model.eval() tok = Tokenizer.load(f"{run_dir}/tokenizer.json") task = TaskConfig(**ckpt["task_cfg"]) return model, tok, task DMODEL, TOK, TASK = load_model(DIFF_DIR) AMODEL, ATOK, ATASK = load_model(AR_DIR) if TASK.tile_size >= TASK.block_len: TASK.tile_size = max(2, TASK.block_len // 4) def curate_examples(path, n=10): """A small, varied, readable set: short programs spread across features.""" rows = [] try: rows = [json.loads(l) for l in open(path)] except FileNotFoundError: pass picked, seen = [], set() # one pass preferring variety by feature, short and readable def score(r): return len(r["source"].splitlines()) rows = [r for r in rows if 5 <= len(r["source"].splitlines()) <= 12] want = ["recursion", "table_build", "loops", "closure", None] for feat in want * 3: for r in sorted(rows, key=score): key = r["source"] if key in seen: continue f = r.get("features", {}) if feat is None or f.get(feat): picked.append(r) seen.add(key) break if len(picked) >= n: break if not picked: picked = rows[:n] return picked RECORDS = curate_examples(EVAL_PATH) def label(i, r): f = r.get("features", {}) tags = [k for k in ("recursion", "loops", "table_build", "closure") if f.get(k)] return f"#{i+1} ({', '.join(tags) or 'simple'}, {len(r['source'].splitlines())} lines)" CHOICES = [(label(i, r), i) for i, r in enumerate(RECORDS)] def tests_for(idx): return RECORDS[idx].get("tests", []) if 0 <= idx < len(RECORDS) else [] # ---- model passes ---- @torch.no_grad() def diffuse(pre, blk_ids, suf, n_inner): enc = _ids_canvas(TOK, pre, blk_ids, suf, TASK, ar=False) if enc is None: return None, None, 0.0 ids, region, _bid, attn = (torch.from_numpy(x).to(DEVICE) for x in enc) pos = torch.arange(ids.size(0), device=DEVICE) ctx_idx = pos[attn & (~region)] region_idx = pos[region] R = region_idx.numel() tile = TASK.tile_size target = list(blk_ids) t0 = time.perf_counter() caches = DMODEL.encode_context(ids[ctx_idx].unsqueeze(0), ctx_idx.unsqueeze(0)) cur = [TOK.mask_id] * R masked_g = [True] * R frames = [{"ids": list(cur), "masked": list(masked_g), "label": "input (everything hidden)"}] for bi, b0 in enumerate(range(0, R, TASK.block_len)): bp = region_idx[b0:b0 + TASK.block_len] Lb = bp.numel() blk = torch.full((Lb,), TOK.mask_id, dtype=torch.long, device=DEVICE) masked = torch.ones(Lb, dtype=torch.bool, device=DEVICE) for inner in range(n_inner): logits, _ = DMODEL.decode_block(blk.unsqueeze(0), bp.unsqueeze(0), caches) conf, pred = F.softmax(logits[0], -1).max(-1) blk = torch.where(masked, pred, blk) masked = torch.zeros(Lb, dtype=torch.bool, device=DEVICE) for k in range(Lb): cur[b0 + k] = int(blk[k]); masked_g[b0 + k] = False frames.append({"ids": list(cur), "masked": list(masked_g), "label": f"block {bi+1}, refine step {inner+1} of {n_inner}"}) if inner == n_inner - 1: break fm = 1.0 - (inner + 1) / n_inner nt = (Lb + tile - 1) // tile keep = round(nt * fm) if keep <= 0: continue tc = torch.stack([conf[t*tile:(t+1)*tile].mean() for t in range(nt)]) for t in torch.argsort(tc)[:keep].tolist(): lo, hi = t*tile, min((t+1)*tile, Lb) blk[lo:hi] = TOK.mask_id; masked[lo:hi] = True for k in range(lo, hi): masked_g[b0 + k] = True _, caches = DMODEL.decode_block(blk.unsqueeze(0), bp.unsqueeze(0), caches) lat = (time.perf_counter() - t0) * 1000 frames.append({"ids": list(cur), "masked": [False]*R, "label": "final"}) recon = TOK.decode(list(pre) + cur + list(suf)) return frames, {"pre": pre, "suf": suf, "target": target, "recon": recon}, lat @torch.no_grad() def ar_fill(pre, suf): head = build_prompt(ATOK, pre, suf, ATASK) if head is None: return None, 0.0 t0 = time.perf_counter() out = AMODEL.generate(torch.tensor(head, device=DEVICE), max_new=ATASK.max_decode, eos_id=ATOK.eos_id) lat = (time.perf_counter() - t0) * 1000 return out, lat def verify(source, tests): if not tests or not os.path.exists(VERIFIER): return None try: p = subprocess.run([VERIFIER, "verify-batch"], input=json.dumps({"source": source, "tests": tests}), capture_output=True, text=True, timeout=20) for line in p.stdout.splitlines(): if line.strip(): return json.loads(line)["pass"] except Exception: return None return None # ---- readable layout: lay Lua tokens onto indented lines, colour the region ---- PRE = ("font:13px/1.7 ui-monospace,monospace;white-space:pre;background:#1e1e1e;" "color:#9aa;padding:14px;border-radius:8px;overflow-x:auto") STARTERS = {"local", "return", "for", "while", "if", "function", "repeat"} CLOSERS = {"end", "else", "elseif", "until"} def _chip(txt, status): e = html.escape(txt) if status == "ctx": return e if status == "mask": return "__" bg = "#2f9e57" if status == "ok" else "#c0392b" return f"{e}" def layout(items): """items: list of (text, status). status in ctx/ok/wrong/mask (mask has text None). Returns HTML with newlines and indentation.""" lines, cur, indent = [], [], 0 def flush(): if cur: lines.append(" " * indent + " ".join(cur)); cur.clear() for text, status in items: if status == "mask": cur.append(_chip("", "mask")); continue if text in CLOSERS: flush() if text in ("end", "until"): indent = max(0, indent - 1) elif text in STARTERS: flush() cur.append(_chip(text, status)) if text in ("do", "then"): flush(); indent += 1 elif text == "function": indent += 1 elif text == "else": flush() flush() body = "\n".join(lines).strip("\n") return f"