japhba's picture
Single-token-per-step latent-CoT organism: load-bearing + length-generalising
c9629a1 verified
Raw
History Blame Contribute Delete
3.71 kB
"""Block-structured attention masks for Abstract-CoT bottlenecked SFT.
Training sequence s = [X ; C ; Z ; Y]:
X prompt C verbal CoT (teacher rationale)
Z abstract trace Y answer
The paper's bottleneck (Ramji et al., S3.2, eq. around the block mask A) is *exactly*
standard causal masking with ONE edge removed: **answer positions may not attend to the
verbal-CoT positions**. Every bit of C-information reaching Y must route through the
abstract hidden states H_Z, giving the Markov structure C -> H_Z -> Y and the channel
bound I(C;Y|X,Z) <= I(C;H_Z|X,Z) that scales with the abstract length m.
Note Z attending X u C u Z_<=i is *already* causal (C precedes Z), so it is NOT a
deviation -- the only deviation is the forbidden (Y query, C key) block. We encode roles
per token and forbid a configurable set of (query_role, key_role) pairs, then materialise
a 4D additive mask. transformers>=5.8 eager/sdpa attention honors the 4D mask bit-exactly
(verified: tests/test_masking.py).
"""
from __future__ import annotations
import torch
# per-token roles
X, C, Z, Y = 0, 1, 2, 3
PAD = -1
ROLE = {"X": X, "C": C, "Z": Z, "Y": Y, "PAD": PAD}
# the paper's bottleneck: the answer cannot see the verbal CoT
BOTTLENECK_FORBIDDEN = ((Y, C),)
# TIGHT bottleneck: the answer attends ONLY the abstract -- it cannot see the prompt X OR the
# verbal CoT C. Forces a strict prompt -> abstract -> answer Markov chain, so the abstract is
# load-bearing BY CONSTRUCTION (the answer literally has no other path to the question). Use
# with abstract-only answer generation at inference (no prompt in the answer's context).
TIGHT_FORBIDDEN = ((Y, C), (Y, X))
# debug-only: also forbid Z->C, fully isolating C so Y becomes invariant to C (used to
# prove the 4D mask is actually honored end-to-end)
ISOLATE_C_FORBIDDEN = ((Y, C), (Z, C))
def segment_roles(x_len: int, c_len: int, z_len: int, y_len: int, pad_to: int | None = None) -> torch.Tensor:
"""Build a [L] role vector for one example laid out as [X C Z Y] (+ right PAD)."""
roles = [X] * x_len + [C] * c_len + [Z] * z_len + [Y] * y_len
if pad_to is not None:
assert pad_to >= len(roles), f"pad_to={pad_to} < seq len {len(roles)}"
roles += [PAD] * (pad_to - len(roles))
return torch.tensor(roles, dtype=torch.long)
def build_attention_mask(
role_ids: torch.Tensor,
dtype: torch.dtype = torch.float32,
forbidden_pairs=BOTTLENECK_FORBIDDEN,
) -> torch.Tensor:
"""role_ids: LongTensor[B, L] of roles in {X,C,Z,Y,PAD}.
Returns an additive attention mask FloatTensor[B, 1, L, L] (0 = attend, dtype-min =
masked), ready to pass to a Qwen3 model as ``attention_mask`` under eager/sdpa attn.
"""
assert role_ids.dim() == 2, "role_ids must be [B, L]"
B, L = role_ids.shape
dev = role_ids.device
neg = torch.finfo(dtype).min
idx = torch.arange(L, device=dev)
allowed = (idx[None, :] <= idx[:, None])[None].expand(B, L, L).clone() # causal [B,L,L]
q_role = role_ids[:, :, None] # [B,L,1]
k_role = role_ids[:, None, :] # [B,1,L]
for qr, kr in forbidden_pairs:
allowed &= ~((q_role == qr) & (k_role == kr))
allowed &= (k_role != PAD) # never attend to padding keys
add = torch.zeros((B, L, L), dtype=dtype, device=dev)
add.masked_fill_(~allowed, neg)
# PAD *queries* would otherwise be all-masked -> NaN softmax; let them attend self.
pad_q = (role_ids == PAD)
if pad_q.any():
eye = torch.eye(L, dtype=torch.bool, device=dev)[None].expand(B, L, L)
add = torch.where(pad_q[:, :, None] & eye, torch.zeros((), dtype=dtype, device=dev), add)
return add[:, None] # [B,1,L,L]