echo-1 / ml /data.py
lupodevelop's picture
echo-1 Stage 0 explainer: diffusion vs autoregressive, execution-verified
3afc977 verified
Raw
History Blame Contribute Delete
8.16 kB
"""Dataset and sequence encoding for block infilling.
The masked target is a contiguous, line-aligned BLOCK of the program body (~half
of it), not a single short line. This stresses global structure (def-use across
the gap, table/segment coherence) — the regime where the thesis expects the
diffusion substrate to matter. Both models see the SAME block:
diffusion (in-place, exact): [bos] prefix <block> suffix [eos] [pad...]
the block positions are the denoise region; context is always visible. No gap
padding — the block occupies exactly its real length.
AR-FIM (causal): [bos][pre] prefix [suf] suffix [mid] block [eos] [pad...]
loss only on the block+eos span, matching diffusion's region-only loss.
`prefix + block + suffix == source` exactly (char-offset slicing), so a correct
fill reconstructs the program and verification by execution is well-defined.
"""
from __future__ import annotations
import json
import numpy as np
import torch
from torch.utils.data import Dataset
from .config import TaskConfig
from .tokenizer import Tokenizer
def load_records(path: str) -> list[dict]:
with open(path) as f:
return [json.loads(line) for line in f if line.strip()]
def make_block(source: str, frac: float, rng: np.random.RandomState):
"""Slice the source into (prefix, block, suffix) where block is a contiguous,
line-aligned run of body lines totalling ~frac of the body. Returns None if
there is no usable body. prefix+block+suffix == source exactly."""
nl = source.find("\n")
if nl < 0:
return None
body_start = nl + 1
stripped = source.rstrip("\n")
end_pos = stripped.rfind("\nend")
if end_pos < 0:
return None
body_end = end_pos + 1 # position of the final 'end'
body = source[body_start:body_end]
if not body.strip():
return None
# Line-start offsets within the body (each body line keeps its trailing \n).
starts = [0] + [i + 1 for i, ch in enumerate(body) if ch == "\n" and i + 1 < len(body)]
n = len(starts)
target = max(1, int(len(body) * frac))
s = rng.randint(0, n)
acc = 0
e = s
while e < n and acc < target:
seg_end = starts[e + 1] if e + 1 < n else len(body)
acc += seg_end - starts[e]
e += 1
block_start = body_start + starts[s]
block_end = body_start + (starts[e] if e < n else len(body))
prefix = source[:block_start]
block = source[block_start:block_end]
suffix = source[block_end:]
if not block.strip():
return None
return prefix, block, suffix
def encode_diffusion(tok: Tokenizer, prefix, block, suffix, cfg: TaskConfig):
"""(ids, region_mask, block_id, attn_mask) of length T, or None if it doesn't
fit. block_id is the generation-block index for region positions, -1 elsewhere
(used by the block-causal mask)."""
pre = tok.encode(prefix)
mid = tok.encode(block)
suf = tok.encode(suffix)
body = [tok.bos_id] + pre + mid + suf + [tok.eos_id]
if len(body) > cfg.seq_len:
return None
ids = np.full(cfg.seq_len, tok.pad_id, dtype=np.int64)
ids[: len(body)] = body
region = np.zeros(cfg.seq_len, dtype=bool)
start = 1 + len(pre)
region[start : start + len(mid)] = True
block_id = np.full(cfg.seq_len, -1, dtype=np.int64)
for r in range(len(mid)):
block_id[start + r] = r // cfg.block_len
attn = ids != tok.pad_id
return ids, region, block_id, attn
def encode_ar(tok: Tokenizer, prefix, block, suffix, cfg: TaskConfig):
"""(ids, loss_mask) of length T, or None if it doesn't fit."""
pre = tok.encode(prefix)
mid = tok.encode(block)
suf = tok.encode(suffix)
head = [tok.bos_id, tok.pre_id] + pre + [tok.suf_id] + suf + [tok.mid_id]
tail = mid + [tok.eos_id]
body = head + tail
if len(body) > cfg.seq_len:
return None
ids = np.full(cfg.seq_len, tok.pad_id, dtype=np.int64)
ids[: len(body)] = body
loss_mask = np.zeros(cfg.seq_len, dtype=bool)
loss_mask[len(head) : len(head) + len(tail)] = True
return ids, loss_mask
def block_for_eval(source: str, frac: float, idx: int):
"""Deterministic block for a record (seeded by index), so diffusion and AR are
evaluated on the IDENTICAL masked block."""
return make_block(source, frac, np.random.RandomState(idx + 1))
# ---- token-level (lua mode): everything in token-id space ----
# Whitespace is dropped, so blocks are contiguous lexeme runs and reconstruction
# is tok.decode(prefix + block + suffix) (space-joined, executes identically).
def make_block_lua(source: str, frac: float, rng: np.random.RandomState, tok: Tokenizer):
"""Split the lexeme-id list into (pre, block, suf) id lists, block ~frac of the
body (tokens between the signature's ')' and the final 'end'). Returns None if
no usable body."""
ids = tok.encode(source)
rp = tok.stoi.get(")")
endt = tok.stoi.get("end")
if rp is None or endt is None or rp not in ids or endt not in ids:
return None
h = ids.index(rp) # end of the parameter list
be = max(i for i, t in enumerate(ids) if t == endt) # final 'end'
b0, b1 = h + 1, be
if b1 <= b0:
return None
n = b1 - b0
target = max(1, int(n * frac))
s = rng.randint(0, n)
e = min(s + target, n)
a, c = b0 + s, b0 + e
return ids[:a], ids[a:c], ids[c:]
def _ids_canvas(tok, pre, blk, suf, cfg, ar):
if ar:
head = [tok.bos_id, tok.pre_id] + pre + [tok.suf_id] + suf + [tok.mid_id]
tail = blk + [tok.eos_id]
body = head + tail
if len(body) > cfg.seq_len:
return None
arr = np.full(cfg.seq_len, tok.pad_id, dtype=np.int64)
arr[: len(body)] = body
loss = np.zeros(cfg.seq_len, dtype=bool)
loss[len(head): len(head) + len(tail)] = True
return arr, loss
body = [tok.bos_id] + pre + blk + suf + [tok.eos_id]
if len(body) > cfg.seq_len:
return None
arr = np.full(cfg.seq_len, tok.pad_id, dtype=np.int64)
arr[: len(body)] = body
region = np.zeros(cfg.seq_len, dtype=bool)
start = 1 + len(pre)
region[start: start + len(blk)] = True
bid = np.full(cfg.seq_len, -1, dtype=np.int64)
for r in range(len(blk)):
bid[start + r] = r // cfg.block_len
attn = arr != tok.pad_id
return arr, region, bid, attn
class InfillDataset(Dataset):
"""Block-infilling dataset. Each record gets a deterministic block (seeded by
index) so runs are reproducible and the two modes train on matching blocks."""
def __init__(self, records: list[dict], tok: Tokenizer, cfg: TaskConfig, mode: str):
assert mode in ("diffusion", "ar")
self.tok = tok
self.cfg = cfg
self.mode = mode
self.items = []
self.skipped = 0
ar = mode == "ar"
lua = tok.mode == "lua"
for i, rec in enumerate(records):
# Per-record block fraction sampled in [frac_lo, frac_hi] so one model
# handles any masking ratio; eval then sweeps the ratio for free.
rng = np.random.RandomState(i + 1)
frac = cfg.frac_lo + (cfg.frac_hi - cfg.frac_lo) * rng.rand()
if lua:
blk = make_block_lua(rec["source"], frac, rng, tok)
e = None if blk is None else _ids_canvas(tok, blk[0], blk[1], blk[2], cfg, ar)
else:
blk = make_block(rec["source"], frac, rng)
e = None if blk is None else (
encode_ar(tok, *blk, cfg) if ar else encode_diffusion(tok, *blk, cfg))
if blk is None or e is None:
self.skipped += 1
else:
self.items.append(e)
def __len__(self):
return len(self.items)
def __getitem__(self, idx):
if self.mode == "diffusion":
ids, region, block_id, attn = self.items[idx]
return (torch.from_numpy(ids), torch.from_numpy(region),
torch.from_numpy(block_id), torch.from_numpy(attn))
ids, loss_mask = self.items[idx]
return torch.from_numpy(ids), torch.from_numpy(loss_mask)