Dory-Beam-4B / code /dory /fast_generate.py
ProCreations's picture
Add budget-forcing results, EvalPlus coding results and the live ZeroGPU demo
dafe8d2 verified
Raw History Blame Contribute Delete
11.2 kB
"""KV-cached incremental decoding for the Dory port (dory/modeling.py).
Every (physical attention layer, loop pass) owns its own key/value cache, like
vLLM's ``recurrent_kv_cache_mode="per_loop"``: pass 1 of the recurrent block
attends over pass-1 keys of earlier tokens, pass 2 over pass-2 keys, and so on.
Caches are preallocated once per request:
* full-attention layers: ``[B, kv_heads, capacity, head_dim]`` indexed by position;
* sliding-window layers: a ring buffer of ``window`` slots, slot = position % window.
The port's window keeps keys with ``0 <= i - j < window``, i.e. the current token
plus the previous ``window - 1`` tokens, which is exactly what the ring holds.
The decode step always attends over the whole buffer with a boolean validity mask
(slot <= position), so its shapes are static and it can be captured in a CUDA graph.
RoPE is applied to keys before they are cached (as in the port), so no position
bookkeeping is needed at read time. Keys are stored after qk-norm; queries get it too.
Only batch size 1 is exercised, but nothing assumes it except ``sample``.
"""
import math
import torch
import torch.nn.functional as F
from .modeling import DoryForCausalLM, apply_rope_neox, causal_mask, justnorm, rope_cos_sin, slerp_alpha
class KVCache:
"""Per-(layer, loop) caches for one request."""
def __init__(self, model: DoryForCausalLM, capacity: int, n_loops: int, batch: int = 1, device="cpu",
dtype=None):
cfg = model.config
p = next(model.parameters())
dtype = dtype or p.dtype
self.cfg, self.capacity, self.device = cfg, capacity, torch.device(device)
self.window = cfg.sliding_window
self.pos = 0 # number of tokens already cached
self.k, self.v = {}, {}
n_in, n_rec = cfg.n_input_layers, cfg.n_recurrent_layers
for i, lt in enumerate(cfg.layer_types):
if lt == "mlp":
continue
passes = range(n_loops) if n_in <= i < n_in + n_rec else range(1)
cap = self.window if lt == "sliding_attention" else capacity
for lp in passes:
shape = (batch, cfg.num_key_value_heads, cap, cfg.head_dim)
self.k[i, lp] = torch.zeros(shape, device=device, dtype=dtype)
self.v[i, lp] = torch.zeros(shape, device=device, dtype=dtype)
self.slots = torch.arange(max(capacity, self.window), device=device)
# RoPE tables for every position and both thetas: [capacity, head_dim/2]
pos = torch.arange(capacity, device=device)[None]
self.rope = {}
for lt in set(cfg.layer_types) - {"mlp"}:
c, s = rope_cos_sin(pos, cfg.head_dim, cfg.rope_parameters[lt]["rope_theta"], device)
self.rope[lt] = (c[0], s[0])
def slot_of(self, layer_type, positions):
return positions % self.window if layer_type == "sliding_attention" else positions
def _qkv(mixer, x, cos, sin):
b, t, _ = x.shape
h, kv, d = mixer.num_heads, mixer.num_kv_heads, mixer.head_dim
q = mixer.q_proj(x).view(b, t, h, d)
k = mixer.k_proj(x).view(b, t, kv, d)
v = mixer.v_proj(x).view(b, t, kv, d)
gate = mixer.g_proj(x)
q, k = apply_rope_neox(q, cos, sin), apply_rope_neox(k, cos, sin)
sqk = (mixer.sqk.float() * mixer.sqk_factor).view(h, d)
q = (sqk.square() * justnorm(q).float()).to(q.dtype)
k = justnorm(k)
return q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), gate # [B, H|kv, T, D]
def _finish(mixer, out, gate):
b, h, t, d = out.shape
out = out.transpose(1, 2).reshape(b, t, h * d)
sg = (mixer.s_gate.float() * mixer.s_gate_factor).square()
out = (out.float() * torch.sigmoid(gate.float() * sg)).to(out.dtype)
return mixer.o_proj(justnorm(out))
def _attend(q, k, v, mask, d):
return F.scaled_dot_product_attention(q, k, v, attn_mask=mask, scale=1.0 / math.sqrt(d), enable_gqa=True)
def attn_prefill(mixer, x, cache: KVCache, key, mask):
"""x: [B, T, H] for positions 0..T-1 of an empty cache. Writes the cache, returns the mixer output."""
lt = mixer.layer_type
t = x.shape[1]
positions = torch.arange(t, device=x.device)
cos, sin = cache.rope[lt]
q, k, v, gate = _qkv(mixer, x, cos[None, positions], sin[None, positions])
kc, vc = cache.k[key], cache.v[key]
if lt == "sliding_attention" and t > cache.window: # only the newest `window` keys survive
keep = t - cache.window
slots = cache.slot_of(lt, positions[keep:])
kc[:, :, slots], vc[:, :, slots] = k[:, :, keep:], v[:, :, keep:]
else:
slots = cache.slot_of(lt, positions)
kc[:, :, slots], vc[:, :, slots] = k, v
out = _attend(q, k, v, mask, mixer.head_dim)
return _finish(mixer, out, gate)
def attn_decode(mixer, x, cache: KVCache, key, pos, valid):
"""x: [B, 1, H] at scalar-tensor position `pos`; `valid[lt]` is the bool key mask [1,1,1,cap]."""
lt = mixer.layer_type
cos, sin = cache.rope[lt]
idx = pos.view(1) # index_select, not cos[pos]: a 0-d tensor index syncs and breaks graph capture
q, k, v, gate = _qkv(mixer, x, cos.index_select(0, idx)[None], sin.index_select(0, idx)[None])
slot = cache.slot_of(lt, pos)
kc, vc = cache.k[key], cache.v[key]
kc.index_copy_(2, slot.view(1), k)
vc.index_copy_(2, slot.view(1), v)
out = _attend(q, kc, vc, valid[lt], mixer.head_dim)
return _finish(mixer, out, gate)
def _backbone(model, h, n_loops, run_layer):
"""Shared layer schedule: input layers, n_loops recurrent passes, output layers."""
cfg = model.config
layers = model.backbone.layers
n_in, n_rec = cfg.n_input_layers, cfg.n_recurrent_layers
for i in range(n_in):
h = run_layer(i, 0, h)
input_h = h
state = input_h
for lp in range(n_loops):
h = justnorm(state + input_h)
for i in range(n_in, n_in + n_rec):
h = run_layer(i, lp, h)
state = justnorm(h)
h = state
for i in range(n_in + n_rec, len(layers)):
h = run_layer(i, 0, h)
return h
def _layer(model, i, lp, h, attn):
layer = model.backbone.layers[i]
if layer.is_attention:
out = attn(layer.mixer, h, (i, lp))
return slerp_alpha(h, out, layer.attn_alpha, layer.alpha_scale)
return slerp_alpha(h, layer.mixer(h), layer.mlp_alpha, layer.alpha_scale)
@torch.no_grad()
def prefill(model, input_ids, cache: KVCache, n_loops):
"""Run the prompt (positions 0..T-1), fill the caches, return logits of the last token [B, V]."""
assert cache.pos == 0
t = input_ids.shape[1]
dev = input_ids.device
masks = {lt: causal_mask(t, model.config.sliding_window if lt == "sliding_attention" else None, dev)
for lt in set(model.config.layer_types) - {"mlp"}}
def attn(mixer, h, key):
return attn_prefill(mixer, h, cache, key, masks[mixer.layer_type])
h = model.backbone.embeddings(input_ids)
h = _backbone(model, h, n_loops, lambda i, lp, h: _layer(model, i, lp, h, attn))
cache.pos = t
return model.logits(h[:, -1])
@torch.no_grad()
def decode_step(model, token, cache: KVCache, n_loops, pos):
"""One token [B, 1] at position `pos` (0-d long tensor). Returns logits [B, V]."""
slots = cache.slots
valid = {"full_attention": (slots[: cache.capacity] <= pos)[None, None, None],
"sliding_attention": (slots[: cache.window] <= pos)[None, None, None]}
def attn(mixer, h, key):
return attn_decode(mixer, h, cache, key, pos, valid)
h = model.backbone.embeddings(token)
h = _backbone(model, h, n_loops, lambda i, lp, h: _layer(model, i, lp, h, attn))
return model.logits(h[:, -1])
def sample(logits, temperature=1.0, top_p=0.95, generator=None):
"""Nucleus sampling on [1, V] float logits; temperature 0 = greedy."""
if temperature <= 0:
return logits.argmax(-1, keepdim=True)
probs = torch.softmax(logits / temperature, dim=-1)
if top_p < 1.0:
sp, si = probs.sort(descending=True, dim=-1)
cum = sp.cumsum(-1)
sp[cum - sp > top_p] = 0.0 # keep the smallest prefix whose mass >= top_p
choice = torch.multinomial(sp / sp.sum(-1, keepdim=True), 1, generator=generator)
return si.gather(-1, choice)
return torch.multinomial(probs, 1, generator=generator)
class Decoder:
"""Optional CUDA-graph wrapper around decode_step with static input/position buffers."""
def __init__(self, model, cache, n_loops, use_graph=False):
self.model, self.cache, self.n_loops = model, cache, n_loops
dev = cache.device
self.tok = torch.zeros(1, 1, dtype=torch.long, device=dev)
self.pos = torch.zeros((), dtype=torch.long, device=dev)
self.graph = None
self.use_graph = use_graph and dev.type == "cuda"
def __call__(self, token, pos):
self.tok.fill_(int(token))
self.pos.fill_(int(pos))
if not self.use_graph:
return decode_step(self.model, self.tok, self.cache, self.n_loops, self.pos)
if self.graph is None:
try:
self._capture()
except Exception as e: # pragma: no cover - depends on the CUDA runtime
print(f"CUDA graph capture failed ({type(e).__name__}: {e}); decoding eagerly")
self.use_graph = False
return decode_step(self.model, self.tok, self.cache, self.n_loops, self.pos)
self.graph.replay()
return self.out
def _capture(self):
# Warm up on a side stream, then capture. The cache write at `pos` is idempotent, so
# running the same step several times is harmless.
s = torch.cuda.Stream()
s.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(s):
for _ in range(2):
decode_step(self.model, self.tok, self.cache, self.n_loops, self.pos)
torch.cuda.current_stream().wait_stream(s)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
self.out = decode_step(self.model, self.tok, self.cache, self.n_loops, self.pos)
self.graph = graph
@torch.no_grad()
def generate_stream(model, input_ids, max_new_tokens, n_loops=2, temperature=1.0, top_p=0.95,
eos_ids=(2, 11), seed=None, use_graph=False, return_logits=False):
"""Yield one generated token id (int) at a time; stops after an EOS id (which is yielded).
With return_logits=True yields (token, logits[1, V]) pairs instead (for tests)."""
dev = input_ids.device
t = input_ids.shape[1]
cache = KVCache(model, capacity=t + max_new_tokens, n_loops=n_loops, device=dev)
gen = None
if seed is not None:
gen = torch.Generator(device=dev).manual_seed(seed)
logits = prefill(model, input_ids, cache, n_loops)
step = Decoder(model, cache, n_loops, use_graph=use_graph)
for i in range(max_new_tokens):
nxt = sample(logits, temperature, top_p, gen)
tok = int(nxt[0, 0])
yield (tok, logits.clone()) if return_logits else tok
if tok in eos_ids:
break
if i + 1 < max_new_tokens:
logits = step(tok, t + i)