@nnx.jit
def cached_forward(model, ks, vs, X, t):
"""Logits for the last of the new tokens X at positions t, t+1, ...;
writes their keys and values into the cache slots at those positions."""
B, T = X.shape
H = model.token_emb(X)
if model.pos == 'learned':
H = H + model.pos_emb(t + jnp.arange(T))
mask = (jnp.arange(ks.shape[2])[None, :]
<= (t + jnp.arange(T))[:, None])[None, None]
for i, blk in enumerate(model.blks): # pre-norm arrangement
attn, Y = blk.attention, blk.norm1(H)
q, k, v = jnp.split(attn.W_qkv(Y), 3, axis=-1)
q, k, v = (u.reshape(B, T, attn.num_heads, -1) for u in (q, k, v))
if attn.rope:
q, k = rope(q, t), rope(k, t)
# Callers must keep t + T <= max_len: an out-of-range write does
# not fail here -- dynamic_update_slice clamps its start index
# silently. (t is traced under jit, so generate_cached asserts.)
ks = jax.lax.dynamic_update_slice(ks, k[None], (i, 0, t, 0, 0))
vs = jax.lax.dynamic_update_slice(vs, v[None], (i, 0, t, 0, 0))
Y = jax.nn.dot_product_attention(q, ks[i], vs[i], mask=mask)
H = H + attn.W_o(Y.reshape(B, T, -1))
H = H + blk.ffn(blk.norm2(H))
return model.token_emb.attend(model.norm(H))[:, -1], ks, vs
def generate_cached(self, prefix, num_tokens, seed=0, temperature=1.0,
top_k=None):
"""Sample as generate does, but never recompute the prefix."""
assert len(prefix) + num_tokens <= self.max_len, 'cache overflow'
ks, vs = init_cache(self, 1, self.max_len)
logits, ks, vs = cached_forward(self, ks, vs,
jnp.asarray(prefix)[None], jnp.array(0))
ids, key = list(prefix), jax.random.key(seed)
for t in range(len(prefix), len(prefix) + num_tokens):
l = logits[0] / temperature
if top_k is not None:
l = jnp.where(l < jnp.sort(l)[-top_k], -jnp.inf, l)
key, sub = jax.random.split(key)
ids.append(int(jax.random.categorical(sub, l)))
logits, ks, vs = cached_forward(self, ks, vs,
jnp.asarray([[ids[-1]]]),
jnp.array(t))
return ids