File size: 11,192 Bytes
dafe8d2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | """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)
|