Skip to content

← Modern Techniques step 7 of 10

Hard Research

KV Cache for Autoregressive Decoding

Implement kv_cache_step for one attention head and one new token. Reuse the stored key and value vectors, append the new token's key and value, and compute attention for its query.

Caching saves the work of projecting past tokens again. The new query still attends to the entire available prefix, including the new token itself. The projections are supplied here; you do not compute embeddings or apply query, key, value or output-projection matrices.

Inputs

  • cached_K: (L, D), keys for the previous L tokens in sequence order.
  • cached_V: (L, D), values for those same tokens in the same order.
  • new_q: (1, D), the new token's query.
  • new_k: (1, D), the new token's key.
  • new_v: (1, D), the new token's value.

L may be zero; D is positive. All arrays are finite, share a floating-point dtype and device, and have the stated shapes. Keep the row dimension even for the single new token. Leave every input unchanged.

Append, then attend

Append new_k after the cached keys, and new_v after the cached values:

full_K = concatenate([cached_K, new_k], axis=0)
full_V = concatenate([cached_V, new_v], axis=0)
scores = new_q @ full_K.T / sqrt(D)
weights = softmax(scores, axis=-1)
output = weights @ full_V

Both caches now have shape (L+1, D). Scores and weights have shape (1, L+1). Normalize over all L+1 keys together. Use a stable softmax, subtracting the maximum score before exponentiation; a framework softmax helper is allowed.

There are no future tokens in these caches, so every key is allowed. Do not apply a triangular mask to the 1-by-(L+1) score row: its row index is zero, but the query's sequence position is L. Masking by that local row index would incorrectly hide most of the prefix. Do not recompute outputs for old queries or add the query vector to either cache.

Return value

Return a dictionary with exactly these entries, preserving dtype and device:

  • "output": (1, D), the attention result for the new token.
  • "updated_K": (L+1, D), the cached keys followed by new_k.
  • "updated_V": (L+1, D), the cached values followed by new_v.

For the next token, pass updated_K and updated_V back as the caches, along with that next token's new query, key and value. Preserve matching key/value row order as the sequence grows.

Boundary and example

With an empty cache, the only key is new_k. Its attention weight is one, so output equals new_v for any query.

If new_q is all zeros, every score is zero and output is the average of all cached values plus new_v. For cached_V=[[1,0]] and new_v=[[0,1]], that gives output=[[0.5,0.5]]. Keys still appear in updated_K in their original order even when the attention weights are equal.

In the causal panel below, this operation corresponds to the last query row: it can use every key up to and including its own position.

Loading visualization…