sumitk's picture
Upload folder using huggingface_hub
4e70c09 verified
Raw History Blame Contribute Delete
32.9 kB
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.checkpoint import checkpoint
from conveyer.config import ModelConfig
class RMSNorm(nn.Module):
def __init__(self, d_model: int, eps: float = 1e-6) -> None:
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(d_model))
def forward(self, x: torch.Tensor) -> torch.Tensor:
norm = x.float().pow(2).mean(dim=-1, keepdim=True)
x_normed = x.float() * torch.rsqrt(norm + self.eps)
return (self.weight * x_normed).type_as(x)
class RotaryEmbedding(nn.Module):
def __init__(self, head_dim: int, max_seq_len: int = 2048, theta: float = 10000.0) -> None:
super().__init__()
assert head_dim % 2 == 0, "head_dim must be even for RoPE"
self.head_dim = head_dim
self.max_seq_len = max_seq_len
self.theta = theta
inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2).float() / head_dim))
self.register_buffer("inv_freq", inv_freq, persistent=False)
self._build_cache(max_seq_len)
def _build_cache(self, seq_len: int) -> None:
self.max_seq_len = max(self.max_seq_len, seq_len)
t = torch.arange(self.max_seq_len, device=self.inv_freq.device, dtype=torch.float32)
freqs = torch.outer(t, self.inv_freq) # (seq_len, head_dim // 2)
cos = torch.cos(freqs)
sin = torch.sin(freqs)
self.register_buffer("cos_cached", cos, persistent=False)
self.register_buffer("sin_cached", sin, persistent=False)
def get_cos_sin(self, positions: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""positions: (L,) or (N, L) integer positions."""
max_pos = int(positions.max().item()) + 1 if positions.numel() > 0 else 1
if max_pos > self.cos_cached.size(0):
self._build_cache(max_pos + 256)
cos = self.cos_cached[positions]
sin = self.sin_cached[positions]
return cos, sin
def apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
"""
x: (N, H, L, D)
cos, sin: (L, D//2) or (N, L, D//2)
"""
if cos.dim() == 2:
cos = cos.unsqueeze(0).unsqueeze(0) # (1, 1, L, D//2)
sin = sin.unsqueeze(0).unsqueeze(0)
elif cos.dim() == 3:
cos = cos.unsqueeze(1) # (N, 1, L, D//2)
sin = sin.unsqueeze(1)
d2 = x.shape[-1] // 2
x1 = x[..., :d2]
x2 = x[..., d2:]
out1 = x1 * cos - x2 * sin
out2 = x1 * sin + x2 * cos
return torch.cat([out1, out2], dim=-1).type_as(x)
class SwiGLUFFN(nn.Module):
def __init__(self, d_model: int, d_ff: int, dropout: float = 0.0) -> None:
super().__init__()
self.w_gate = nn.Linear(d_model, d_ff, bias=False)
self.w_up = nn.Linear(d_model, d_ff, bias=False)
self.w_down = nn.Linear(d_ff, d_model, bias=False)
self.drop = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.drop(self.w_down(F.silu(self.w_gate(x)) * self.w_up(x)))
@dataclass
class PrefixKVCache:
"""Stores committed prefix Keys and Values per layer: list of (K, V) each (N, H, P, D_h)."""
k_cache: list[torch.Tensor]
v_cache: list[torch.Tensor]
seq_len: int
@classmethod
def empty(cls, n_layers: int, batch_size: int, n_heads: int, head_dim: int, device: torch.device, dtype: torch.dtype) -> "PrefixKVCache":
k_cache = [torch.empty((batch_size, n_heads, 0, head_dim), device=device, dtype=dtype) for _ in range(n_layers)]
v_cache = [torch.empty((batch_size, n_heads, 0, head_dim), device=device, dtype=dtype) for _ in range(n_layers)]
return cls(k_cache=k_cache, v_cache=v_cache, seq_len=0)
def clone(self) -> "PrefixKVCache":
return PrefixKVCache(
k_cache=[k.clone() for k in self.k_cache],
v_cache=[v.clone() for v in self.v_cache],
seq_len=self.seq_len,
)
def detach(self) -> "PrefixKVCache":
return PrefixKVCache(
k_cache=[k.detach() for k in self.k_cache],
v_cache=[v.detach() for v in self.v_cache],
seq_len=self.seq_len,
)
@dataclass
class BeltState:
"""Holds the 64-slot Dual-Channel + Elastic Planning dough state on the conveyor belt."""
e_soft: torch.Tensor # (N, B, D) lexical anchor soft embeddings
m_thought: torch.Tensor # (N, B, D) continuous thought highway vectors
tick: int # current absolute sequence position of Slot 0
probs: Optional[torch.Tensor] = None # (N, B, V) last probability distributions
logits: Optional[torch.Tensor] = None # (N, B, V) last raw logits
gates: Optional[torch.Tensor] = None # (N, B, D) last update gate activations
shift_probs: Optional[torch.Tensor] = None # (N, B) elastic shift vs. pause probability s_k
def detach(self) -> "BeltState":
return BeltState(
e_soft=self.e_soft.detach(),
m_thought=self.m_thought.detach(),
tick=self.tick,
probs=self.probs.detach() if self.probs is not None else None,
logits=self.logits.detach() if self.logits is not None else None,
gates=self.gates.detach() if self.gates is not None else None,
shift_probs=self.shift_probs.detach() if self.shift_probs is not None else None,
)
@dataclass
class BeltStepOutput:
logits: torch.Tensor # (N, B, V) raw vocabulary logits at all 64 slots
probs: torch.Tensor # (N, B, V) temperature-scaled probabilities p_k
e_soft: torch.Tensor # (N, B, D) expected vocab embeddings p_k^T E_vocab
cand_thought: torch.Tensor # (N, B, D) candidate thought vectors c_k
gate: torch.Tensor # (N, B, D) update gate g_k
m_updated: torch.Tensor # (N, B, D) updated thought vectors before index shift
shift_prob: torch.Tensor # (N, B) elastic shift probability s_k (1.0 in front conveyor, <1 in back 75% planning)
expand_cand: torch.Tensor # (N, D) mid-belt expansion thought vector for forming new sentence clauses
slot0_logits: torch.Tensor # (N, V) crisp logits at Position 0
class ConveyerAttention(nn.Module):
def __init__(self, cfg: ModelConfig) -> None:
super().__init__()
assert cfg.d_model % cfg.n_heads == 0
self.n_heads = cfg.n_heads
self.head_dim = cfg.d_model // cfg.n_heads
self.d_model = cfg.d_model
self.qkv = nn.Linear(cfg.d_model, 3 * cfg.d_model, bias=False)
self.proj = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
self.dropout = cfg.dropout
def forward(
self,
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
attn_mask: Optional[torch.Tensor] = None,
layer_kv: Optional[tuple[torch.Tensor, torch.Tensor]] = None,
return_new_kv: bool = False,
is_causal: bool = False,
) -> tuple[torch.Tensor, Optional[tuple[torch.Tensor, torch.Tensor]]]:
"""
x: (N, L, D)
layer_kv: optional (K_prev, V_prev) each (N, H, P, D_h)
"""
bsz, seq_len, _ = x.shape
qkv = self.qkv(x)
q, k, v = qkv.chunk(3, dim=-1)
q = q.view(bsz, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
k = k.view(bsz, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
v = v.view(bsz, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
q = apply_rope(q, cos, sin)
k = apply_rope(k, cos, sin)
new_kv = (k, v) if return_new_kv else None
if layer_kv is not None and layer_kv[0].size(2) > 0:
k = torch.cat([layer_kv[0].type_as(k), k], dim=2)
v = torch.cat([layer_kv[1].type_as(v), v], dim=2)
out = F.scaled_dot_product_attention(
q,
k,
v,
attn_mask=None if is_causal else attn_mask,
dropout_p=self.dropout if self.training else 0.0,
is_causal=is_causal,
)
out = out.transpose(1, 2).contiguous().view(bsz, seq_len, self.d_model)
return self.proj(out), new_kv
class ConveyerBlock(nn.Module):
def __init__(self, cfg: ModelConfig) -> None:
super().__init__()
self.norm1 = RMSNorm(cfg.d_model)
self.attn = ConveyerAttention(cfg)
self.norm2 = RMSNorm(cfg.d_model)
self.ffn = SwiGLUFFN(cfg.d_model, cfg.d_ff, cfg.dropout)
def forward(
self,
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
attn_mask: Optional[torch.Tensor] = None,
layer_kv: Optional[tuple[torch.Tensor, torch.Tensor]] = None,
return_new_kv: bool = False,
is_causal: bool = False,
) -> tuple[torch.Tensor, Optional[tuple[torch.Tensor, torch.Tensor]]]:
attn_out, new_kv = self.attn(
self.norm1(x),
cos=cos,
sin=sin,
attn_mask=attn_mask,
layer_kv=layer_kv,
return_new_kv=return_new_kv,
is_causal=is_causal,
)
x = x + attn_out
x = x + self.ffn(self.norm2(x))
return x, new_kv
class ConveyerDiffusionLM(nn.Module):
"""
64-Slot Dual-Channel Elastic Conveyor-Belt Diffusion Language Model.
Input / Output Contract at every tick t:
- Input : [All Committed Text Prefix x_{0..t-1}] + [64-Slot Conveyor/Planning Belt]
- Output: [Popped-out Confirmed Token x_t at Slot 0] + [Updated 64-Slot Conveyor/Planning Belt]
Two-Zone Human-Style Thinking & Speaking Belt:
- Front 25% (Slots 0 .. F-1, e.g. 0..15): Immediate Articulation Stream.
Shifts left by 1 slot every tick (s_k = 1.0), polishing immediate grammar toward Slot 0 (alpha_0 = 0).
- Back 75% (Slots F .. B-1, e.g. 16..63): Elastic Sentence-Planning Horizon.
Can pause in-place (s_k < 1.0) to hold long-range sentence goals in mind while new sentence clauses
bud and expand in the middle of the belt (around Slot F).
"""
def __init__(self, cfg: ModelConfig) -> None:
super().__init__()
self.cfg = cfg
self.belt_size = cfg.belt_size
self.front_slots = min(cfg.front_conveyer_slots, cfg.belt_size)
self.d_model = cfg.d_model
self.vocab_size = cfg.vocab_size
# Token embedding & weight-tied LM head
self.tok_emb = nn.Embedding(cfg.vocab_size, cfg.d_model)
self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
self.lm_head.weight = self.tok_emb.weight # weight tying
# Learned oven slot embeddings for positions k = 0 .. B-1 on the conveyor belt
self.slot_emb = nn.Embedding(cfg.belt_size, cfg.d_model)
# Learned initial thought vector for newly spawned dough at Slot B-1
self.m_init = nn.Parameter(torch.zeros(cfg.d_model))
# Dough input RMSNorm
self.dough_norm = RMSNorm(cfg.d_model)
# Transformer trunk
self.rope = RotaryEmbedding(
head_dim=cfg.d_model // cfg.n_heads,
max_seq_len=cfg.max_seq_len,
theta=cfg.rope_theta,
)
self.blocks = nn.ModuleList([ConveyerBlock(cfg) for _ in range(cfg.n_layers)])
self.final_norm = RMSNorm(cfg.d_model)
# Channel 2: Continuous Thought Highway heads (RecurrentCubeLM style)
self.thought_cand = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
self.thought_gate = nn.Linear(cfg.d_model, cfg.d_model, bias=True)
# Elastic Planning & Mid-Belt Sentence Expansion Heads
# shift_head predicts whether each back-75% planning slot shifts left or pauses in place
self.shift_head = nn.Linear(cfg.d_model, 1, bias=True)
# expand_head synthesizes a new phrase/clause embedding at the boundary between the
# paused 75% planning zone and the moving 25% front conveyor
self.expand_head = nn.Linear(2 * cfg.d_model, cfg.d_model, bias=False)
# Precompute positional temperature schedule tau_k, thought mixing alpha_k, and target horizon offsets
B = cfg.belt_size
F_slots = self.front_slots
k_idx = torch.arange(B, dtype=torch.float32)
ratio = k_idx / max(B - 1, 1)
tau_schedule = cfg.tau_min + (ratio ** cfg.tau_gamma) * (cfg.tau_max - cfg.tau_min)
alpha_schedule = ratio.clone() # alpha_0 = 0.0, alpha_{B-1} = 1.0
# Target sequence offsets delta_k from current tick t:
# - Front slots k = 0 .. F-1 map 1-to-1 to immediate tokens t + k
# - Back 75% planning slots k = F .. B-1 span a wider lookahead horizon (stride >= 1)
stride = max(1, cfg.plan_horizon_stride) if cfg.use_elastic_planning else 1
target_offsets = torch.arange(B, dtype=torch.long)
if B > F_slots and stride > 1:
back_idx = torch.arange(B - F_slots, dtype=torch.long)
target_offsets[F_slots:] = F_slots + back_idx * stride
# Prior bias for shift_head: front slots always shift (+6.0), back 75% slots default to ~0.35 shift (65% pause)
shift_prior = torch.zeros(B, dtype=torch.float32)
shift_prior[:F_slots] = 6.0
if B > F_slots:
back_ratio = torch.linspace(0.0, 1.0, B - F_slots)
shift_prior[F_slots:] = -0.3 - 1.2 * back_ratio # sigmoid(-0.3..-1.5) ~ 0.42 down to 0.18
self.register_buffer("tau_schedule", tau_schedule, persistent=False)
self.register_buffer("alpha_schedule", alpha_schedule, persistent=False)
self.register_buffer("target_offsets", target_offsets, persistent=False)
self.register_buffer("shift_prior", shift_prior, persistent=False)
self._init_weights()
def _init_weights(self) -> None:
nn.init.normal_(self.tok_emb.weight, mean=0.0, std=0.02)
nn.init.normal_(self.slot_emb.weight, mean=0.0, std=0.02)
nn.init.normal_(self.m_init, mean=0.0, std=0.02)
for m in self.modules():
if isinstance(m, nn.Linear) and m is not self.lm_head:
nn.init.normal_(m.weight, mean=0.0, std=0.02)
if m.bias is not None:
nn.init.zeros_(m.bias)
nn.init.constant_(self.thought_gate.bias, self.cfg.gate_bias_init)
nn.init.zeros_(self.shift_head.weight)
nn.init.zeros_(self.shift_head.bias)
def num_parameters(self, trainable_only: bool = True) -> int:
return sum(p.numel() for p in self.parameters() if (p.requires_grad or not trainable_only))
def get_slot_target_offsets(self, active_slots: Optional[int] = None) -> torch.Tensor:
B = active_slots if active_slots is not None else self.belt_size
return self.target_offsets[:B]
def init_prefix_cache(self, batch_size: int, device: torch.device, dtype: Optional[torch.dtype] = None) -> PrefixKVCache:
if dtype is None:
dtype = self.tok_emb.weight.dtype
return PrefixKVCache.empty(
n_layers=self.cfg.n_layers,
batch_size=batch_size,
n_heads=self.cfg.n_heads,
head_dim=self.cfg.d_model // self.cfg.n_heads,
device=device,
dtype=dtype,
)
def prefill_prefix(
self,
prefix_tokens: torch.Tensor,
kv_cache: Optional[PrefixKVCache] = None,
) -> PrefixKVCache:
"""
Encodes committed prefix tokens x_{t_start .. t_end} into the causal PrefixKVCache.
prefix_tokens: (N, P) long tensor.
"""
bsz, plen = prefix_tokens.shape
device = prefix_tokens.device
if kv_cache is None:
kv_cache = self.init_prefix_cache(bsz, device)
if plen == 0:
return kv_cache
start_pos = kv_cache.seq_len
positions = torch.arange(start_pos, start_pos + plen, device=device, dtype=torch.long)
cos, sin = self.rope.get_cos_sin(positions)
slot0_bias = self.slot_emb.weight[0].view(1, 1, -1)
x = self.dough_norm(self.tok_emb(prefix_tokens) + slot0_bias)
total_k = start_pos + plen
use_is_causal = start_pos == 0 and plen > 1
if use_is_causal or plen == 1:
attn_mask = None
else:
causal_mask = torch.ones((plen, total_k), device=device, dtype=torch.bool).tril(diagonal=start_pos)
attn_mask = causal_mask.unsqueeze(0).unsqueeze(0)
new_k_list: list[torch.Tensor] = []
new_v_list: list[torch.Tensor] = []
for i, block in enumerate(self.blocks):
layer_kv = (kv_cache.k_cache[i], kv_cache.v_cache[i])
x, new_kv = block(
x,
cos=cos,
sin=sin,
attn_mask=attn_mask,
layer_kv=layer_kv,
return_new_kv=True,
is_causal=use_is_causal,
)
assert new_kv is not None
new_k_list.append(torch.cat([kv_cache.k_cache[i].type_as(new_kv[0]), new_kv[0]], dim=2))
new_v_list.append(torch.cat([kv_cache.v_cache[i].type_as(new_kv[1]), new_kv[1]], dim=2))
return PrefixKVCache(k_cache=new_k_list, v_cache=new_v_list, seq_len=total_k)
def forward_causal_and_cache(
self,
seq_tokens: torch.Tensor,
prefix_len: int,
) -> tuple[torch.Tensor, PrefixKVCache]:
"""
Runs a full causal pass over `seq_tokens` (N, L) through the shared transformer blocks:
1. Computes next-token logits `(N, L, V)` at every position so all ~160 tokens per story
receive direct causal language-modeling supervision (10x more signal per step).
2. Extracts the exact causal KV cache for `seq_tokens[:, :prefix_len]` (invariant to tokens >= prefix_len)
to seed the conveyor belt unroll without running a duplicate prefill pass.
"""
bsz, seq_len = seq_tokens.shape
device = seq_tokens.device
positions = torch.arange(0, seq_len, device=device, dtype=torch.long)
cos, sin = self.rope.get_cos_sin(positions)
slot0_bias = self.slot_emb.weight[0].view(1, 1, -1)
x = self.dough_norm(self.tok_emb(seq_tokens) + slot0_bias)
k_list: list[torch.Tensor] = []
v_list: list[torch.Tensor] = []
for block in self.blocks:
x, new_kv = block(
x,
cos=cos,
sin=sin,
attn_mask=None,
layer_kv=None,
return_new_kv=True,
is_causal=(seq_len > 1),
)
assert new_kv is not None
k_list.append(new_kv[0][:, :, :prefix_len, :].contiguous())
v_list.append(new_kv[1][:, :, :prefix_len, :].contiguous())
h = self.final_norm(x)
causal_logits = self.lm_head(h)
kv_cache = PrefixKVCache(k_cache=k_list, v_cache=v_list, seq_len=prefix_len)
return causal_logits, kv_cache
def append_committed_token(
self,
token_ids: torch.Tensor,
kv_cache: PrefixKVCache,
) -> PrefixKVCache:
if token_ids.dim() == 1:
token_ids = token_ids.unsqueeze(1)
return self.prefill_prefix(token_ids, kv_cache=kv_cache)
def spawn_raw_dough(
self,
batch_size: int,
num_slots: int,
device: torch.device,
deterministic: bool = False,
) -> tuple[torch.Tensor, torch.Tensor]:
mean_emb = self.tok_emb.weight.mean(dim=0, keepdim=True).unsqueeze(0)
m_base = self.m_init.view(1, 1, -1)
if deterministic or self.cfg.spawn_noise_std <= 0.0:
e_new = mean_emb.expand(batch_size, num_slots, self.d_model).clone()
m_new = m_base.expand(batch_size, num_slots, self.d_model).clone()
else:
eps_e = torch.randn((batch_size, num_slots, self.d_model), device=device, dtype=mean_emb.dtype)
eps_m = torch.randn((batch_size, num_slots, self.d_model), device=device, dtype=mean_emb.dtype)
e_new = mean_emb + self.cfg.spawn_noise_std * 0.02 * eps_e
m_new = m_base + self.cfg.thought_noise_std * 0.02 * eps_m
return e_new, m_new
def init_empty_belt(
self,
batch_size: int,
tick: int,
device: torch.device,
deterministic: bool = False,
) -> BeltState:
e_soft, m_thought = self.spawn_raw_dough(
batch_size=batch_size,
num_slots=self.belt_size,
device=device,
deterministic=deterministic,
)
return BeltState(e_soft=e_soft, m_thought=m_thought, tick=tick)
def compose_dough_inputs(self, belt: BeltState, active_slots: Optional[int] = None) -> torch.Tensor:
B = active_slots if active_slots is not None else self.belt_size
e_soft = belt.e_soft[:, :B, :]
m_thought = belt.m_thought[:, :B, :]
slots = torch.arange(B, device=e_soft.device, dtype=torch.long)
s_emb = self.slot_emb(slots).unsqueeze(0)
if self.cfg.use_thought_highway:
alpha = self.alpha_schedule[:B].view(1, B, 1) # alpha_0 == 0.0
fused = e_soft + alpha * m_thought + s_emb
else:
fused = e_soft + s_emb
return self.dough_norm(fused)
def _build_belt_mask(
self,
belt_len: int,
prefix_len: int,
device: torch.device,
) -> Optional[torch.Tensor]:
total_k = prefix_len + belt_len
if not self.cfg.causal_belt_only:
return torch.ones((1, 1, belt_len, total_k), device=device, dtype=torch.bool)
mask = torch.ones((belt_len, total_k), device=device, dtype=torch.bool).tril(diagonal=prefix_len)
return mask.unsqueeze(0).unsqueeze(0)
def forward_belt(
self,
belt: BeltState,
kv_cache: Optional[PrefixKVCache] = None,
active_slots: Optional[int] = None,
) -> BeltStepOutput:
"""
Single forward pass across all active slots on the belt:
Input : [Committed Prefix KV Cache x_{0..t-1}] + [Belt Dough 0..B-1]
Output: [Slot 0 Token Logits + Updated Belt State (Lexical + Thought + Elastic Shift Gate)]
"""
B = active_slots if active_slots is not None else self.belt_size
dough = self.compose_dough_inputs(belt, active_slots=B)
device = dough.device
prefix_len = kv_cache.seq_len if kv_cache is not None else 0
offsets = self.get_slot_target_offsets(active_slots=B)
positions = belt.tick + offsets
cos, sin = self.rope.get_cos_sin(positions)
attn_mask = (
None
if not self.cfg.causal_belt_only
else self._build_belt_mask(belt_len=B, prefix_len=prefix_len, device=device)
)
h = dough
use_ckpt = self.training and self.cfg.grad_checkpointing
for i, block in enumerate(self.blocks):
layer_kv = (kv_cache.k_cache[i], kv_cache.v_cache[i]) if kv_cache is not None else None
if use_ckpt:
def _run_block(inp: torch.Tensor, blk: ConveyerBlock = block, lkv: Optional[tuple[torch.Tensor, torch.Tensor]] = layer_kv) -> torch.Tensor:
out_h, _ = blk(inp, cos=cos, sin=sin, attn_mask=attn_mask, layer_kv=lkv, return_new_kv=False)
return out_h
h = checkpoint(_run_block, h, use_reentrant=False)
else:
h, _ = block(
h,
cos=cos,
sin=sin,
attn_mask=attn_mask,
layer_kv=layer_kv,
return_new_kv=False,
)
h = self.final_norm(h) # (N, B, D)
# Channel 1: Lexical Anchor (Soft Probability Projection)
logits = self.lm_head(h) # (N, B, V)
tau = self.tau_schedule[:B].view(1, B, 1).type_as(logits)
probs = F.softmax((logits / tau).float(), dim=-1).type_as(self.tok_emb.weight)
e_soft = torch.matmul(probs, self.tok_emb.weight) # (N, B, D)
# Channel 2: Continuous Thought Highway (Gated Residual Update)
cand = self.thought_cand(h)
gate = torch.sigmoid(self.thought_gate(h))
m_prev = belt.m_thought[:, :B, :]
if self.cfg.use_thought_highway:
m_updated = gate * m_prev + (1.0 - gate) * cand
else:
m_updated = torch.zeros_like(m_prev)
# Elastic Planning Shift/Pause Gate & Mid-Belt Sentence Expansion
F_slots = min(self.front_slots, B)
if self.cfg.use_elastic_planning and B > F_slots:
prior = self.shift_prior[:B].view(1, B).type_as(h)
raw_shift = self.shift_head(h).squeeze(-1) + prior # (N, B)
shift_prob = torch.sigmoid(raw_shift)
# Front conveyor slots (0 .. F-1) always shift left at full speed (1.0)
front_ones = torch.ones((h.size(0), F_slots), device=device, dtype=shift_prob.dtype)
shift_prob = torch.cat([front_ones, shift_prob[:, F_slots:]], dim=1)
# Mid-belt expansion thought synthesized from the boundary between Front (F-1) and Planning (F)
boundary_pair = torch.cat([h[:, F_slots - 1, :], h[:, F_slots, :]], dim=-1)
expand_cand = self.expand_head(boundary_pair) # (N, D)
else:
shift_prob = torch.ones((h.size(0), B), device=device, dtype=h.dtype)
expand_cand = h[:, -1, :]
return BeltStepOutput(
logits=logits,
probs=probs,
e_soft=e_soft,
cand_thought=cand,
gate=gate,
m_updated=m_updated,
shift_prob=shift_prob,
expand_cand=expand_cand,
slot0_logits=logits[:, 0, :],
)
def shift_belt_left(
self,
step_out: BeltStepOutput,
current_tick: int,
deterministic_spawn: bool = False,
clamp_tokens: Optional[torch.Tensor] = None,
clamp_mask: Optional[torch.Tensor] = None,
) -> BeltState:
"""
Updates the belt for tick t + 1:
1. Front Conveyor Zone (Slots 0 .. F-2):
Always shifts left by 1 slot (`1..F-1 -> 0..F-2`) so fully-baked tokens flow smoothly out of Slot 0.
2. Mid-Belt Sentence Expansion Zone (Slot F-1, e.g. Slot 15):
Blends the advancing plan from Slot F with the newly expanded clause (`expand_cand`),
allowing new sentences and words to form in the middle while the back 75% pauses!
3. Back 75% Elastic Planning Zone (Slots F .. B-2, e.g. 16..62):
Each slot k updates elastically using its learned shift probability `s_k`:
e'_{k} = s_k * e_{k+1} + (1 - s_k) * e_k
m'_{k} = s_k * m_{k+1} + (1 - s_k) * m_k
When `s_k` is low (~0.25), the back 75% of the belt pauses in-place holding the long-range
sentence plan steady while the front conveyor articulates the current sentence!
4. Slot B-1 (Slot 63):
Blends fresh raw dough into the tail of the planning belt.
"""
bsz, B, _ = step_out.e_soft.shape
device = step_out.e_soft.device
F_slots = min(self.front_slots, B)
e_spawn, m_spawn = self.spawn_raw_dough(
batch_size=bsz,
num_slots=1,
device=device,
deterministic=deterministic_spawn,
)
if B <= 1:
e_next = e_spawn
m_next = m_spawn
probs_next = torch.full((bsz, 1, self.vocab_size), 1.0 / self.vocab_size, device=device, dtype=step_out.probs.dtype)
logits_next = torch.zeros((bsz, 1, self.vocab_size), device=device, dtype=step_out.logits.dtype)
gates_next = torch.ones((bsz, 1, self.d_model), device=device, dtype=step_out.gate.dtype)
elif (not self.cfg.use_elastic_planning) or B <= F_slots:
# Strict 1-by-1 shift across all B slots
e_next = torch.cat([step_out.e_soft[:, 1:, :], e_spawn], dim=1)
m_next = torch.cat([step_out.m_updated[:, 1:, :], m_spawn], dim=1)
probs_next = torch.cat(
[
step_out.probs[:, 1:, :],
torch.full((bsz, 1, self.vocab_size), 1.0 / self.vocab_size, device=device, dtype=step_out.probs.dtype),
],
dim=1,
)
logits_next = torch.cat(
[
step_out.logits[:, 1:, :],
torch.zeros((bsz, 1, self.vocab_size), device=device, dtype=step_out.logits.dtype),
],
dim=1,
)
gates_next = torch.cat(
[
step_out.gate[:, 1:, :],
torch.ones((bsz, 1, self.d_model), device=device, dtype=step_out.gate.dtype),
],
dim=1,
)
else:
# Elastic Two-Zone Update:
# (a) Front slots 0 .. F-2 shift strictly from 1 .. F-1
e_front = step_out.e_soft[:, 1:F_slots, :]
m_front = step_out.m_updated[:, 1:F_slots, :]
# (b) Mid-Belt Expansion Slot (F-1): where paused back-belt plans expand into new active words
s_mid = step_out.shift_prob[:, F_slots : F_slots + 1].unsqueeze(-1) # (N, 1, 1)
e_FromPlan = step_out.e_soft[:, F_slots : F_slots + 1, :]
m_FromPlan = step_out.m_updated[:, F_slots : F_slots + 1, :]
m_expand = step_out.expand_cand.unsqueeze(1) # (N, 1, D)
e_mid = e_FromPlan
m_mid = s_mid * m_FromPlan + (1.0 - s_mid) * m_expand
# (c) Back 75% Planning Slots (F .. B-2): elastic interpolation between staying (k) and shifting (k+1)
s_back = step_out.shift_prob[:, F_slots : B - 1].unsqueeze(-1) # (N, B - 1 - F, 1)
e_stay = step_out.e_soft[:, F_slots : B - 1, :]
e_adv = step_out.e_soft[:, F_slots + 1 : B, :]
e_back = s_back * e_adv + (1.0 - s_back) * e_stay
m_stay = step_out.m_updated[:, F_slots : B - 1, :]
m_adv = step_out.m_updated[:, F_slots + 1 : B, :]
m_back = s_back * m_adv + (1.0 - s_back) * m_stay
# (d) Slot B-1 (Tail of planning belt): elastic blend between holding Slot B-1 and fresh raw dough
s_tail = step_out.shift_prob[:, B - 1 : B].unsqueeze(-1)
e_tail = s_tail * e_spawn + (1.0 - s_tail) * step_out.e_soft[:, B - 1 : B, :]
m_tail = s_tail * m_spawn + (1.0 - s_tail) * step_out.m_updated[:, B - 1 : B, :]
e_next = torch.cat([e_front, e_mid, e_back, e_tail], dim=1)
m_next = torch.cat([m_front, m_mid, m_back, m_tail], dim=1)
probs_next = torch.cat(
[
step_out.probs[:, 1:, :],
torch.full((bsz, 1, self.vocab_size), 1.0 / self.vocab_size, device=device, dtype=step_out.probs.dtype),
],
dim=1,
)
logits_next = torch.cat(
[
step_out.logits[:, 1:, :],
torch.zeros((bsz, 1, self.vocab_size), device=device, dtype=step_out.logits.dtype),
],
dim=1,
)
gates_next = torch.cat(
[
step_out.gate[:, 1:, :],
torch.ones((bsz, 1, self.d_model), device=device, dtype=step_out.gate.dtype),
],
dim=1,
)
if clamp_tokens is not None and clamp_mask is not None:
clamped_emb = self.tok_emb(clamp_tokens)
mask_exp = clamp_mask.unsqueeze(-1)
e_next = torch.where(mask_exp, clamped_emb, e_next)
return BeltState(
e_soft=e_next,
m_thought=m_next,
tick=current_tick + 1,
probs=probs_next,
logits=logits_next,
gates=gates_next,
shift_probs=step_out.shift_prob,
)