@torch.no_grad()
def forward_full(tokens):
"""Reference: full causal forward over the whole sequence -> logits (T, vocab)."""
T = tokens.numel()
x = model.embed(tokens)[None]
mask = torch.full((T, T), float("-inf")).triu(1)
for lyr in model.layers:
h = lyr.attn_norm(x)
q = lyr.wq(h).view(1, T, N_HEADS, HEAD_DIM)
k = lyr.wk(h).view(1, T, N_KV, HEAD_DIM)
v = lyr.wv(h).view(1, T, N_KV, HEAD_DIM)
q, k = apply_rope(q, k, model.freqs)
k = k.repeat_interleave(REP, dim=2)
v = v.repeat_interleave(REP, dim=2)
scores = torch.einsum("bqhd,bkhd->bhqk", q, k) / math.sqrt(HEAD_DIM) + mask
att = torch.einsum("bhqk,bkhd->bqhd", scores.softmax(-1), v)
x = x + lyr.wo(att.reshape(1, T, -1))
x = x + lyr.ffn(lyr.ffn_norm(x))
return model.head(model.norm(x))[0]
@torch.no_grad()
def prefill(pool, bm, sid, tokens):
"""Process the prompt once, writing K/V into this sequence's blocks."""
T = tokens.numel()
x = model.embed(tokens)[None]
mask = torch.full((T, T), float("-inf")).triu(1)
slots = [bm.append_slot(sid) for _ in range(T)]
for li, lyr in enumerate(model.layers):
h = lyr.attn_norm(x)
q = lyr.wq(h).view(1, T, N_HEADS, HEAD_DIM)
k = lyr.wk(h).view(1, T, N_KV, HEAD_DIM)
v = lyr.wv(h).view(1, T, N_KV, HEAD_DIM)
q, k = apply_rope(q, k, model.freqs)
for t, (blk, off) in enumerate(slots): # scatter into the pool
pool.k[li, blk, off] = k[0, t]
pool.v[li, blk, off] = v[0, t]
kr = k.repeat_interleave(REP, dim=2)
vr = v.repeat_interleave(REP, dim=2)
scores = torch.einsum("bqhd,bkhd->bhqk", q, kr) / math.sqrt(HEAD_DIM) + mask
att = torch.einsum("bhqk,bkhd->bqhd", scores.softmax(-1), vr)
x = x + lyr.wo(att.reshape(1, T, -1))
x = x + lyr.ffn(lyr.ffn_norm(x))
return model.head(model.norm(x))[0, -1] # logits for the next token
@torch.no_grad()
def decode_step(pool, bm, sids, last_tokens):
"""One new token for every running sequence. Projections and FFN batch
across sequences; only the attention gather is per-sequence (the ragged
part — exactly what vLLM's custom kernel handles)."""
positions = torch.tensor([bm.length[s] for s in sids]) # position of the new token
slots = [bm.append_slot(s) for s in sids]
x = model.embed(last_tokens) # (B, dim)
for li, lyr in enumerate(model.layers):
h = lyr.attn_norm(x)
q = rope_at(lyr.wq(h).view(-1, N_HEADS, HEAD_DIM), positions)
k = rope_at(lyr.wk(h).view(-1, N_KV, HEAD_DIM), positions)
v = lyr.wv(h).view(-1, N_KV, HEAD_DIM)
outs = []
for b, sid in enumerate(sids):
blk, off = slots[b]
pool.k[li, blk, off] = k[b]
pool.v[li, blk, off] = v[b]
kk, vv = gather_kv(pool, bm, sid, li)
outs.append(paged_attention(q[b], kk, vv))
x = x + lyr.wo(torch.stack(outs).reshape(len(sids), -1))
x = x + lyr.ffn(lyr.ffn_norm(x))
return model.head(model.norm(x)) # (B, vocab)
# --- the proof ---
prompts = [torch.randint(0, VOCAB, (n,)) for n in (11, 23, 37)]
NEW = 20
ref_outputs = []
for p in prompts:
toks = p.clone()
for _ in range(NEW):
toks = torch.cat([toks, forward_full(toks)[-1].argmax().view(1)])
ref_outputs.append(toks[len(p):].tolist())
pool, bm = KVPool(64), BlockManager(64)
paged_outputs = []
for i, p in enumerate(prompts):
bm.start(i)
last = prefill(pool, bm, i, p).argmax().view(1)
out = [last.item()]
for _ in range(NEW - 1):
last = decode_step(pool, bm, [i], last)[0].argmax().view(1)
out.append(last.item())
paged_outputs.append(out)
bm.release(i)
assert paged_outputs == ref_outputs
print(f'paged == contiguous for {len(prompts)} sequences x {NEW} tokens ✓')
print('sample:', paged_outputs[0][:8], '...')