test150m / cubic_v11_long_context_32k.py
Asilarkness's picture
Publish CubicV11 checkpoint at step 6500
b071478 verified
Raw
History Blame Contribute Delete
15.6 kB
"""CubicV11 long-context architecture and 32k training-memory benchmark.
This is the optimization gate before distillation. It replaces V7's dense
N x N cosine top-k matrix with fused block-sparse local attention, adds causal
global block summaries, RoPE, GQA, detached depth memory, SwiGLU,
tied embeddings and per-block activation checkpointing.
Run:
python cubic_v11_long_context_32k.py
The script executes a real forward + backward + Muon step at 32,768 tokens and
falls back to 16,384 only if the full training step cannot fit.
"""
from __future__ import annotations
import gc
import importlib.util
import math
import sys
import time
from pathlib import Path
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.checkpoint import checkpoint
try:
from torch.nn.attention.flex_attention import create_block_mask, flex_attention
except ImportError as exc:
raise RuntimeError("CubicV11 long context requires PyTorch FlexAttention") from exc
ROOT = Path(__file__).parent
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
VOCAB_SIZE = 8_192
DIM = 896
DEPTH = 14
QUERY_HEADS = 14
KV_HEADS = 7
HEAD_DIM = DIM // QUERY_HEADS
DEPTH_RANK = 224
DEPTH_HEADS = 7
LOCAL_WINDOW = 4_096
SUMMARY_BLOCK = 256
ROPE_BASE = 500_000.0
TARGET_LENGTHS = (32_768, 16_384)
SEED = 20260720
def load_base():
source = ROOT / "cubic_v5_muon_benchmark.py"
spec = importlib.util.spec_from_file_location("v11_muon_base", source)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
# Compile only the sparse attention operator. Compiling the entire 14-layer
# checkpointed graph is slower and substantially more fragile at 32k.
compiled_flex_attention = torch.compile(flex_attention, mode="default", dynamic=False)
class RotaryEmbedding(nn.Module):
def __init__(self, head_dim: int, base: float):
super().__init__()
inv = 1.0 / (base ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim))
self.register_buffer("inv_freq", inv, persistent=False)
def forward(self, seq_len: int, device, dtype):
positions = torch.arange(seq_len, device=device, dtype=torch.float32)
angles = torch.outer(positions, self.inv_freq.to(device))
cos = torch.repeat_interleave(angles.cos(), 2, dim=-1).to(dtype)
sin = torch.repeat_interleave(angles.sin(), 2, dim=-1).to(dtype)
return cos.view(1, 1, seq_len, -1), sin.view(1, 1, seq_len, -1)
def rotate_half(x):
even = x[..., 0::2]
odd = x[..., 1::2]
return torch.stack((-odd, even), dim=-1).flatten(-2)
def apply_rope(x, cos, sin):
return x * cos + rotate_half(x) * sin
class SwiGLU(nn.Module):
def __init__(self, dim: int):
super().__init__()
hidden = 2_432 # close to 8/3 * DIM and divisible by 128
self.gate_up = nn.Linear(dim, 2 * hidden, bias=False)
self.down = nn.Linear(hidden, dim, bias=False)
def forward(self, x):
gate, value = self.gate_up(x).chunk(2, dim=-1)
return self.down(F.silu(gate) * value)
class CompressedDepthMemory(nn.Module):
def __init__(self):
super().__init__()
self.norm = nn.RMSNorm(DIM)
self.kv = nn.Linear(DIM, 2 * DEPTH_RANK, bias=False)
def forward(self, x):
# The residual sequence path still carries full gradients. Detaching
# only the historical side-path avoids retaining every prior block's
# full activation graph at 32k.
batch, seq, _ = x.shape
kv = self.kv(self.norm(x.detach())).view(
batch, seq, 2, DEPTH_HEADS, DEPTH_RANK // DEPTH_HEADS
)
return kv[:, :, 0], kv[:, :, 1]
class LongContextAttention(nn.Module):
def __init__(self, layer_idx: int):
super().__init__()
self.layer_idx = layer_idx
self.q_proj = nn.Linear(DIM, QUERY_HEADS * HEAD_DIM, bias=False)
self.kv_proj = nn.Linear(DIM, 2 * KV_HEADS * HEAD_DIM, bias=False)
self.out_proj = nn.Linear(DIM, DIM, bias=False)
# True pooled global summaries. This branch is linear in sequence
# length because there are only N / SUMMARY_BLOCK summary vectors.
self.global_q = nn.Linear(DIM, DEPTH_RANK, bias=False)
self.global_kv = nn.Linear(DIM, 2 * DEPTH_RANK, bias=False)
self.global_out = nn.Linear(DEPTH_RANK, DIM, bias=False)
self.null_summary = nn.Parameter(torch.zeros(1, 1, DIM))
# Names intentionally contain mix_logit/content_gate so the existing
# Cubic Muon builder assigns the validated gate learning-rate schedule.
self.global_mix_logit = nn.Parameter(torch.full((DIM,), -2.0))
self.has_depth = layer_idx > 0
if self.has_depth:
self.depth_q = nn.Linear(DIM, DEPTH_RANK, bias=False)
self.depth_up = nn.Linear(DEPTH_RANK, DIM, bias=False)
self.depth_mix_logit = nn.Parameter(torch.full((DIM,), math.atanh(0.15)))
self.depth_content_gate = nn.Linear(DIM, 1)
nn.init.zeros_(self.depth_content_gate.weight)
nn.init.zeros_(self.depth_content_gate.bias)
self.local_mask = None
self.global_mask = None
def set_masks(self, local_mask, global_mask):
self.local_mask = local_mask
self.global_mask = global_mask
def local_branch(self, x, cos, sin):
batch, seq, _ = x.shape
q = self.q_proj(x).view(batch, seq, QUERY_HEADS, HEAD_DIM).transpose(1, 2)
kv = self.kv_proj(x).view(batch, seq, 2, KV_HEADS, HEAD_DIM)
k, v = kv.unbind(2)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
q = apply_rope(q, cos, sin)
k_rope = apply_rope(k, cos, sin)
local = compiled_flex_attention(
q.contiguous(), k_rope.contiguous(), v.contiguous(),
block_mask=self.local_mask, enable_gqa=True,
)
return local.transpose(1, 2).reshape(batch, seq, DIM)
def global_branch(self, x):
batch, seq, _ = x.shape
blocks = seq // SUMMARY_BLOCK
summaries = x.view(batch, blocks, SUMMARY_BLOCK, DIM).mean(dim=2)
summaries = torch.cat((self.null_summary.expand(batch, -1, -1), summaries), dim=1)
q = self.global_q(x).view(batch, seq, DEPTH_HEADS, DEPTH_RANK // DEPTH_HEADS).transpose(1, 2)
kv = self.global_kv(summaries).view(
batch, blocks + 1, 2, DEPTH_HEADS, DEPTH_RANK // DEPTH_HEADS
)
k, v = kv.unbind(2)
global_out = compiled_flex_attention(
q.contiguous(), k.transpose(1, 2).contiguous(), v.transpose(1, 2).contiguous(),
block_mask=self.global_mask,
)
global_out = global_out.transpose(1, 2).reshape(batch, seq, DEPTH_RANK)
return self.global_out(global_out)
def depth_branch(self, x, history_k, history_v):
batch, seq, _ = x.shape
layers = len(history_k)
dim = DEPTH_RANK // DEPTH_HEADS
q = self.depth_q(x).view(batch * seq, DEPTH_HEADS, 1, dim)
k = torch.stack(history_k, dim=2).permute(0, 1, 3, 2, 4).reshape(
batch * seq, DEPTH_HEADS, layers, dim
)
v = torch.stack(history_v, dim=2).permute(0, 1, 3, 2, 4).reshape(
batch * seq, DEPTH_HEADS, layers, dim
)
depth = F.scaled_dot_product_attention(q, k, v, dropout_p=0.0)
depth = depth.reshape(batch, seq, DEPTH_RANK)
depth = self.depth_up(depth)
gate = 2.0 * torch.sigmoid(self.depth_content_gate(x))
return gate * self.depth_mix_logit.tanh() * depth
def forward(self, x, cos, sin, history_k, history_v):
local = self.local_branch(x, cos, sin)
global_out = self.global_branch(x)
out = local + torch.sigmoid(self.global_mix_logit) * global_out
if self.has_depth:
out = out + self.depth_branch(x, history_k, history_v)
return self.out_proj(out)
class LongContextBlock(nn.Module):
def __init__(self, layer_idx: int):
super().__init__()
self.norm1 = nn.RMSNorm(DIM)
self.attn = LongContextAttention(layer_idx)
self.norm2 = nn.RMSNorm(DIM)
self.mlp = SwiGLU(DIM)
self.ls1 = nn.Parameter(torch.ones(DIM))
self.ls2 = nn.Parameter(torch.ones(DIM))
def forward(self, x, cos, sin, history_k, history_v):
x = x + self.ls1 * self.attn(self.norm1(x), cos, sin, history_k, history_v)
return x + self.ls2 * self.mlp(self.norm2(x))
class CubicV11LongContext(nn.Module):
def __init__(self, seq_len: int, use_checkpoint: bool = True, vocab_size: int = VOCAB_SIZE):
super().__init__()
if seq_len % SUMMARY_BLOCK:
raise ValueError(f"seq_len must be divisible by {SUMMARY_BLOCK}")
self.seq_len = seq_len
self.vocab_size = vocab_size
self.use_checkpoint = use_checkpoint
self.embed = nn.Embedding(vocab_size, DIM)
self.rope = RotaryEmbedding(HEAD_DIM, ROPE_BASE)
self.depth_memory = CompressedDepthMemory()
self.blocks = nn.ModuleList([LongContextBlock(index) for index in range(DEPTH)])
self.norm = nn.RMSNorm(DIM)
self.head = nn.Linear(DIM, vocab_size, bias=False)
self.apply(self._init_weights)
residual_std = 0.02 / math.sqrt(2 * DEPTH)
for block in self.blocks:
nn.init.normal_(block.attn.out_proj.weight, mean=0.0, std=residual_std)
nn.init.normal_(block.mlp.down.weight, mean=0.0, std=residual_std)
if block.attn.has_depth:
nn.init.zeros_(block.attn.depth_content_gate.weight)
nn.init.zeros_(block.attn.depth_content_gate.bias)
self.head.weight = self.embed.weight
@staticmethod
def _init_weights(module):
if isinstance(module, nn.Linear):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
if module.bias is not None:
nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
def set_masks(self, local_mask, global_mask):
for block in self.blocks:
block.attn.set_masks(local_mask, global_mask)
def run_block(self, block, x, cos, sin, history_k, history_v):
if not self.use_checkpoint or not self.training:
return block(x, cos, sin, history_k, history_v)
layer_count = len(history_k)
def inner(x_value, cos_value, sin_value, *memory):
keys = memory[:layer_count]
values = memory[layer_count:]
return block(x_value, cos_value, sin_value, keys, values)
return checkpoint(
inner, x, cos, sin, *history_k, *history_v,
use_reentrant=False, preserve_rng_state=False,
)
def forward(self, tokens):
batch, seq = tokens.shape
if seq != self.seq_len:
raise ValueError(f"Expected fixed sequence length {self.seq_len}, got {seq}")
x = self.embed(tokens)
cos, sin = self.rope(seq, x.device, x.dtype)
history_k, history_v = [], []
for index, block in enumerate(self.blocks):
if index < DEPTH - 1:
new_k, new_v = self.depth_memory(x)
x = self.run_block(block, x, cos, sin, history_k, history_v)
if index < DEPTH - 1:
history_k.append(new_k)
history_v.append(new_v)
return self.head(self.norm(x))
def make_masks(seq_len: int, device):
def local_causal_mask(batch, head, q_idx, kv_idx):
return (q_idx >= kv_idx) & ((q_idx - kv_idx) < LOCAL_WINDOW)
# kv_idx=0 is a learned null summary. kv_idx=1 summarizes tokens
# [0, SUMMARY_BLOCK), and becomes visible starting at q=SUMMARY_BLOCK.
def global_summary_mask(batch, head, q_idx, kv_idx):
return (kv_idx == 0) | ((kv_idx * SUMMARY_BLOCK) <= q_idx)
print("Creating FlexAttention block masks ...")
local = create_block_mask(
local_causal_mask, B=None, H=None, Q_LEN=seq_len, KV_LEN=seq_len,
device=device, BLOCK_SIZE=128,
)
global_mask = create_block_mask(
global_summary_mask, B=None, H=None, Q_LEN=seq_len,
KV_LEN=seq_len // SUMMARY_BLOCK + 1, device=device,
BLOCK_SIZE=128,
)
return local, global_mask
def configure_muon(base, seq_len: int):
base.SEQ_LEN = seq_len
base.BATCH_SIZE = 1
base.STEPS = 100
base.DIM = DIM
base.DEPTH = DEPTH
base.HEADS = QUERY_HEADS
base.DEPTH_RANK = DEPTH_RANK
base.VOCAB_SIZE = VOCAB_SIZE
def benchmark(seq_len: int):
if DEVICE != "cuda":
raise RuntimeError("The 16k/32k training benchmark requires CUDA")
base = load_base()
configure_muon(base, seq_len)
torch.manual_seed(SEED)
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats()
local_mask, global_mask = make_masks(seq_len, DEVICE)
model = CubicV11LongContext(seq_len, use_checkpoint=True).to(DEVICE)
model.set_masks(local_mask, global_mask)
optimizer = base.build_optimizer(model, f"CUBIC-V11-{seq_len // 1024}K")
params = sum(parameter.numel() for parameter in model.parameters())
tokens = torch.randint(0, VOCAB_SIZE, (1, seq_len + 1), device=DEVICE)
x, targets = tokens[:, :-1], tokens[:, 1:]
print("=" * 108)
print(f"CUBIC V11 LONG CONTEXT | seq={seq_len:,} | params={params:,} | bf16 | checkpointing=on")
print(f"local window={LOCAL_WINDOW} | summary block={SUMMARY_BLOCK} | GQA={QUERY_HEADS}:{KV_HEADS} | depth rank={DEPTH_RANK}")
print("=" * 108)
model.train()
optimizer.zero_grad(set_to_none=True)
started = time.perf_counter()
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
logits = model(x)
loss = F.cross_entropy(logits.reshape(-1, VOCAB_SIZE), targets.reshape(-1))
forward_seconds = time.perf_counter() - started
started_backward = time.perf_counter()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0, foreach=True)
optimizer.step()
torch.cuda.synchronize()
backward_seconds = time.perf_counter() - started_backward
peak = torch.cuda.max_memory_allocated() / 1024**3
reserved = torch.cuda.max_memory_reserved() / 1024**3
total = forward_seconds + backward_seconds
print(f"loss={loss.detach().float().item():.4f} (random-token target ~= {math.log(VOCAB_SIZE):.4f})")
print(f"forward={forward_seconds:.2f}s | backward+Muon={backward_seconds:.2f}s | tokens/s={seq_len/total:,.0f}")
print(f"peak allocated={peak:.2f} GiB | peak reserved={reserved:.2f} GiB")
print("32K_TRAINING_FITS=YES" if seq_len == 32_768 else "16K_TRAINING_FITS=YES")
return peak
def main():
if DEVICE == "cuda":
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
torch.set_float32_matmul_precision("high")
try:
torch._inductor.config.triton.cudagraphs = False
except (AttributeError, ImportError):
pass
last_error = None
for length in TARGET_LENGTHS:
try:
benchmark(length)
return
except torch.cuda.OutOfMemoryError as exc:
last_error = exc
print(f"{length:,} OOM; clearing cache and trying the next target.")
gc.collect()
torch.cuda.empty_cache()
raise RuntimeError("Neither 32k nor 16k training step fit") from last_error
if __name__ == "__main__":
main()