Research · 100M programme

August 28, 2026 · 12 min read · Code included

Can a hybrid architecture beat the Transformer?

The Transformer — the design behind nearly every LLM — re-reads everything it has seen for every new word. That works brilliantly, but it gets expensive. We built two alternatives that carry a running memory alongside attention, trained each at 100 million parameters on 2 billion tokens, and raced them against an identical Transformer. Here is what happened, what it means, and every line of code to repeat it.

100M parameters 2B tokens H100 GPU bfloat16 Fully reproducible

⚡ The 2-minute version

The idea in one picture

A standard Transformer answers each new word by re-reading all previous words — accurate, but the cost grows with every token. Our hybrids add a second pathway: a compact world-state, a running notebook the model updates token by token and can consult instantly.

🔁 Classic Transformer LLM

Every layer attends to every past token. Nothing is ever forgotten — but nothing is ever summarized either, so long contexts get slow and expensive.

VS

🧠 TAM v3 / Cortex-S hybrid

Attention runs alongside a learned recurrent memory that compresses the past into a fixed-size state. Less re-reading, cheaper long contexts — if the memory learns to keep the right things.

The two challengers

TAM v3 — Temporal Associative Memory

Each block runs reduced-width causal attention and a diagonal affine scan (the recurrent world-state) in parallel. A learned scalar gate mixes the two:

output = 2 × ((1−g) × attention + g × world_state)

The scan is parallel-associative, so training stays fast. The gate learns to route about 55% through the world-state branch and 45% through attention.

Cortex-S — Safe Recurrent MoE

Sparse mixture-of-experts (8 experts, top-2 routing) with persistent recurrent state (128 values per layer). Full attention runs only every 6th layer — the rest use the recurrent pathway.

Includes a deterministic safety kernel boundary: every inference step is auditable and compute-bounded by construction.

🔬 A fair fight: matched experimental design

The only difference between runs is the architecture itself — same size, same data, same hardware, same starting seed:

101.8M

Parameters — matched within 0.003%
TAM: 101,806,616 · Transformer: 101,803,520

2B

Tokens — identical byte-for-byte
FineWeb-Edu, FineMath, StackV2, Cosmopedia, ArXiv

H100

Hardware — pinned GPU class
bfloat16 precision, torch.compile, AdamW

8100

Same seed — identical initialization
Same optimizer, LR schedule, batch size

Results at 100M / 2B tokens

Lower loss (NLL) and lower perplexity (PPL) mean the model predicts text better. TAM v3 wins both.

14.86TAM v3 Perplexity
15.05Transformer Perplexity
2.698TAM v3 NLL
2.712Transformer NLL

Full pretraining + post-training comparison

MetricTAM v3TransformerWinner
Pretrain NLL ↓2.69842.7116TAM
Pretrain perplexity ↓14.8615.05TAM
SFT assistant NLL ↓1.78391.7947TAM
DPO reward accuracy ↑62.0%62.6%TFM
Final mixture NLL ↓2.84392.8647TAM
Throughput (tok/s) ↑~254.8k~319.9kTFM
Wall-clock time ↓~9,196s~6,917sTFM

NLL = negative log-likelihood (lower is better). PPL = perplexity (lower is better). Both post-trained with SFT on SmolSmolTalk (100K examples) + DPO on UltraFeedback (10K pairs). Context length 512.

Scaling: TAM wins at every size

Before the full run, TAM v3 was screened at 25M and 50M parameters (3 seeds each, 10M tokens). It won on loss every single time — and the gap grew from 25M to 50M.

ScaleTAM v3 NLLTransformer NLLTAM winsThroughput ratio
25M10M tokens · 3 seeds7.1627.2073 / 3~86%
50M10M tokens · 3 seeds6.8897.0643 / 3~82%
100M2B tokens · 1 seed2.6982.7121 / 1~80%

Downstream quizzes: the Transformer strikes back

On standard multiple-choice benchmarks the Transformer wins 4 out of 5 — a known small-scale phenomenon: better text prediction does not always mean better quiz answers yet.

BenchmarkTAM v3TransformerDelta
ARC-Easy31.5%35.5%−4.0 pp
ARC-Challenge26.0%25.5%+0.5 pp
PIQA54.0%58.0%−4.0 pp
HellaSwag27.5%29.5%−2.0 pp
OpenBookQA28.5%29.5%−1.0 pp
GSM8K2.0%2.0%0.0 pp
Five-MCQ mean33.5%35.6%−2.1 pp

Memory probes: where the world-state shines

Four synthetic tasks (delayed recall, associative recall, state tracking, needle retrieval) at seven context lengths, 96 paired trials each. TAM's biggest win lands at 256 tokens — exactly where a running memory helps most.

Context lengthTAM − Transformer95% CISignal
128−2.08 pp−7.03 to +2.60
256+7.81 pp+2.60 to +13.54Significant
384+2.86 pp−2.60 to +8.33
512−0.52 pp−5.73 to +4.69
640−1.04 pp−6.51 to +4.69
768−0.78 pp−6.25 to +4.69
1000+1.04 pp−4.43 to +6.51
Aggregate+1.04 pp−1.00 to +3.05

At 256 tokens, TAM's state tracking was +11.5 pp and needle retrieval +12.5 pp over the Transformer. The edge fades at longer contexts where full attention already captures the dependencies.

Cortex-S confirms it is not a fluke

A completely separate design on the same frozen 2B-token corpus — and it also beats the Transformer on loss.

15.015Cortex-S Perplexity
15.054Transformer Perplexity
MetricCortex-STransformer
Parameters101,778,112101,803,520
Final NLL ↓2.70912.7116
Final perplexity ↓15.01515.054
ArchitectureSparse MoE + recurrentStandard causal attention

Cortex-S: 8 experts (top-2 routing), 24 layers with full attention only every 6th layer, persistent state of size 128 per layer, deterministic safety kernel on every step.

Honest verdict

TAM v3 is not a breakthrough — but the signal is real.

But:

Next step: TAM v4 (persistent cross-chunk state, novelty write gate, dynamic routing) gets tested at small scale before any more GPU budget is committed. Scaling TAM v3 past 100M is not justified on these numbers.

Training protocol

Data

FineWeb-Edu (45%), FineMath (17.5%), StackV2 (15%), Cosmopedia (15%), ArXiv (7.5%) — 2B tokens, GPT-2 tokenizer, context 512

Optimizer

AdamW, beta1 0.9, beta2 0.95, LR 3e-4 cosine, weight decay 0.1, gradient clip 1.0

Batch

Micro-batch 64 by gradient accumulation 2 gives effective batch 128, bfloat16 mixed precision

Post-training

SFT on SmolSmolTalk (100K examples), then DPO on UltraFeedback (10K preference pairs)

Replicate it: the full training code

Everything below is the exact protocol from this paper, written as runnable reference code. Train the hybrid, train the baseline, compare them yourself — on our numbers or your own data. The 2B-token runs used a fused associative-scan kernel; the loop below is mathematically identical and easier to follow.

0Requirements and launch

terminalone H100 · Python 3.11
pip install torch==2.3.* datasets transformers tqdm numpy

python prepare_data.py --out ./shards --tokens 2000000000
python train_hybrid.py --arch tam          --shards ./shards
python train_hybrid.py --arch transformer  --shards ./shards
python compare_runs.py --a ./out/tam --b ./out/transformer

1Build the data shards — stream the five sources with the paper weights, GPT-2 tokenize, pack to context 512 plus one label token.

prepare_data.pyrun once
# Documented mix: FineWeb-Edu 45, FineMath 17.5, StackV2 15,
# Cosmopedia 15, ArXiv 7.5. Point DATASETS at your local mirrors.
import argparse, numpy as np
from datasets import load_dataset, interleave_datasets
from transformers import GPT2TokenizerFast

CTX = 512
MIX = [
    ("fineweb-edu",   0.450),
    ("finemath",      0.175),
    ("stack-v2",      0.150),
    ("cosmopedia",    0.150),
    ("arxiv",         0.075),
]

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--out", required=True)
    ap.add_argument("--tokens", type=int, default=2_000_000_000)
    ap.add_argument("--seed", type=int, default=8100)
    args = ap.parse_args()

    tok = GPT2TokenizerFast.from_pretrained("gpt2")
    streams = [load_dataset(name, split="train", streaming=True)
               for name, _ in MIX]
    probs = [w for _, w in MIX]
    data = interleave_datasets(streams, probabilities=probs, seed=args.seed)

    buf, written, shard = [], 0, 0
    for row in data:
        buf.extend(tok(row["text"])["input_ids"])
        while len(buf) >= CTX + 1:
            seq = np.array(buf[:CTX + 1], dtype=np.uint16)
            seq.tofile(f"{args.out}/shard_{shard:05d}.bin")
            buf = buf[CTX + 1:]
            written += CTX + 1
            shard += 1
        if written >= args.tokens:
            break
    print(f"wrote {written} tokens in {shard} shards")

if __name__ == "__main__":
    main()

2Train TAM v3 or the matched Transformer — same size, same seed, same everything except the block. Effective batch 128 (micro 64 by accumulation 2), AdamW with cosine decay, bfloat16.

train_hybrid.pythe whole run
import argparse, glob, math
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from tqdm import tqdm

D_MODEL, N_LAYERS, N_HEADS, CTX = 768, 12, 12, 512
VOCAB, SEED = 50257, 8100   # GPT-2 vocab, paper seed

class CausalAttention(nn.Module):
    def __init__(self, d, heads):
        super().__init__()
        self.qkv = nn.Linear(d, 3 * d, bias=False)
        self.proj = nn.Linear(d, d, bias=False)
        self.heads = heads
    def forward(self, x):
        B, T, D = x.shape
        q, k, v = self.qkv(x).chunk(3, dim=-1)
        def split(t):
            r = t.view(B, T, self.heads, D // self.heads)
            return r.transpose(1, 2)
        q, k, v = split(q), split(k), split(v)
        y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
        y = y.transpose(1, 2).reshape(B, T, D)
        return self.proj(y)

class TAMBlock(nn.Module):
    """Reduced-width attention in parallel with a diagonal
    affine scan (the world-state), mixed by a learned gate:
    out = 2 * ((1 - g) * attn + g * state)."""
    def __init__(self, d, heads):
        super().__init__()
        self.attn = CausalAttention(d, heads)
        self.decay = nn.Parameter(torch.zeros(d))
        self.in_proj = nn.Linear(d, d, bias=False)
        self.gate = nn.Parameter(torch.zeros(()))
        self.norm = nn.LayerNorm(d)
    def scan(self, u):
        """Diagonal recurrence s = alpha * s + u, one step per token.
        Paper runs use a fused associative-scan kernel; this loop
        is mathematically identical and easier to audit."""
        alpha = torch.exp(-F.softplus(self.decay))  # (d,)
        s = torch.zeros_like(u[:, :1])
        outs = []
        for t in range(u.size(1)):
            s = alpha * s + u[:, t:t + 1]
            outs.append(s)
        return torch.cat(outs, dim=1)
    def forward(self, x):
        h = self.norm(x)
        a = self.attn(h)
        s = self.scan(self.in_proj(h))
        g = torch.sigmoid(self.gate)
        return x + 2 * ((1 - g) * a + g * s)

class TransformerBlock(nn.Module):
    """Matched baseline: full-width causal attention plus MLP."""
    def __init__(self, d, heads):
        super().__init__()
        self.attn = CausalAttention(d, heads)
        self.mlp = nn.Sequential(nn.Linear(d, 4 * d),
                                 nn.GELU(), nn.Linear(4 * d, d))
        self.n1 = nn.LayerNorm(d)
        self.n2 = nn.LayerNorm(d)
    def forward(self, x):
        x = x + self.attn(self.n1(x))
        return x + self.mlp(self.n2(x))

class LM(nn.Module):
    def __init__(self, arch):
        super().__init__()
        Block = TAMBlock if arch == "tam" else TransformerBlock
        self.emb = nn.Embedding(VOCAB, D_MODEL)
        self.pos = nn.Embedding(CTX, D_MODEL)
        self.blocks = nn.ModuleList([Block(D_MODEL, N_HEADS)
                                     for _ in range(N_LAYERS)])
        self.norm = nn.LayerNorm(D_MODEL)
        self.head = nn.Linear(D_MODEL, VOCAB, bias=False)
    def forward(self, idx):
        B, T = idx.shape
        pos = torch.arange(T, device=idx.device).unsqueeze(0)
        x = self.emb(idx) + self.pos(pos)
        for blk in self.blocks:
            x = blk(x)
        return self.head(self.norm(x))

def shard_stream(path):
    for f in sorted(glob.glob(path + "/*.bin")):
        raw = np.fromfile(f, dtype=np.uint16).astype(np.int64)
        for i in range(0, len(raw) - CTX, CTX + 1):
            yield raw[i:i + CTX + 1]

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--arch", choices=["tam", "transformer"])
    ap.add_argument("--shards", required=True)
    ap.add_argument("--out", default=None)
    args = ap.parse_args()
    out = args.out or f"./out/{args.arch}"

    torch.manual_seed(SEED)
    torch.set_float32_matmul_precision("high")
    dev = "cuda" if torch.cuda.is_available() else "cpu"
    model = torch.compile(LM(args.arch).to(dev, dtype=torch.bfloat16))
    opt = torch.optim.AdamW(model.parameters(), lr=3e-4,
                            betas=(0.9, 0.95), weight_decay=0.1)
    MICRO, ACCUM = 64, 2   # effective batch 128
    total_steps = 2_000_000_000 // (MICRO * ACCUM * CTX)
    sched = torch.optim.lr_scheduler.LambdaLR(
        opt, lambda s: 0.5 * (1 + math.cos(math.pi * s / total_steps)))

    stream, step, running = shard_stream(args.shards), 0, 0.0
    model.train()
    for seq in tqdm(stream, total=total_steps * ACCUM):
        idx = torch.tensor(seq, dtype=torch.long, device=dev)
        idx = idx.unsqueeze(0).expand(MICRO, CTX + 1)
        with torch.autocast(dev, dtype=torch.bfloat16):
            loss = F.cross_entropy(
                model(idx[:, :CTX]).reshape(-1, VOCAB),
                idx[:, 1:].reshape(-1)) / ACCUM
        loss.backward()
        running += loss.item()
        if (step + 1) % ACCUM == 0:
            nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            opt.step()
            sched.step()
            opt.zero_grad()
            nll = running
            print(f"opt-step {(step + 1) // ACCUM} NLL {nll:.4f} "
                  f"PPL {math.exp(nll):.2f}", flush=True)
            running = 0.0
        step += 1
    torch.save(model.state_dict(), out + "/final.pt")

if __name__ == "__main__":
    main()

3Cortex-S variant — swap the block for sparse MoE plus persistent state, with full attention only every 6th layer. Same script, same protocol otherwise.

cortex_s.pydrop-in block
import torch
import torch.nn as nn
import torch.nn.functional as F
from train_hybrid import CausalAttention

N_EXPERTS, TOP_K, STATE = 8, 2, 128

class SparseMoE(nn.Module):
    """8 feed-forward experts, top-2 routing per token."""
    def __init__(self, d):
        super().__init__()
        self.gate = nn.Linear(d, N_EXPERTS, bias=False)
        self.experts = nn.ModuleList(
            [nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(),
                           nn.Linear(4 * d, d))
             for _ in range(N_EXPERTS)])
    def forward(self, x):
        w = F.softmax(self.gate(x), dim=-1)
        topw, topi = w.topk(TOP_K, dim=-1)
        topw = topw / topw.sum(dim=-1, keepdim=True)
        out = torch.zeros_like(x)
        for k in range(TOP_K):
            for j, expert in enumerate(self.experts):
                pick = (topi[..., k] == j).unsqueeze(-1)
                if pick.any():
                    out = out + pick * topw[..., k:k + 1] * expert(x)
        return out

class CortexSBlock(nn.Module):
    """MoE plus a persistent recurrent state. Full attention
    only when layer_id % 6 == 0; other layers ride the state.
    The state update is bounded, so every step is auditable."""
    def __init__(self, d, heads, layer_id):
        super().__init__()
        self.layer_id = layer_id
        self.attn = CausalAttention(d, heads) if layer_id % 6 == 0 else None
        self.moe = SparseMoE(d)
        self.to_state = nn.Linear(d, STATE, bias=False)
        self.from_state = nn.Linear(STATE, d, bias=False)
        self.norm = nn.LayerNorm(d)
    def forward(self, x, state):
        """state: (B, STATE) carried across the sequence."""
        h = self.norm(x)
        if self.attn is not None:
            h = h + self.attn(h)
        h = h + self.moe(h)
        # bounded write: tanh keeps the state in a fixed range
        write = torch.tanh(self.to_state(h).mean(dim=1))
        state = 0.9 * state + 0.1 * write
        read = self.from_state(state).unsqueeze(1)
        return x + h + read, state

Post-training follows the same recipe for every run: supervised fine-tuning on 100K assistant examples, then preference optimization on 10K pairs — same tokenizer, same seeds. Evaluate with NLL and perplexity on a held-out slice before trusting any downstream quiz.

Train it, beat it, tell us

The code above reproduces our runs end to end. If your variant beats TAM v3 on loss and downstream — we want to hear about it at hej@belna.se.

Try Arche 1.0 while it trains →

Home · Research · Pricing