Linear Attention › Causal Linear Attention
Language AI HandbookAll chapters Linear Attention › Causal Linear AttentionOne section · Sections in this chapter8 / 15 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
For autoregressive language models, we need causal attention: each position can only attend to previous positions. Standard causal attention applies a triangular mask before softmax. Linear attention requires a different approach.
The key insight is that the key-value aggregate can be computed incrementally. Instead of summing over all positions, we maintain a running sum up to the current position:
where:
- : the cumulative key-value aggregate up to position , computed by summing outer products for all positions
- : the cumulative key sum up to position , used for normalization
- : retrieves a weighted combination of values based on the query at position
- : computes the normalization factor
This formulation naturally respects causality: position only accesses information from positions through . The running sum structure enables efficient incremental computation during inference.
def causal_linear_attention(Q, K, V, feature_map=None, eps=1e-6):
"""
Compute causal linear attention where each position attends only to previous positions.
Args:
Q: Query matrix of shape (n, d_k)
K: Key matrix of shape (n, d_k)
V: Value matrix of shape (n, d_v)
feature_map: Function to apply to Q and K
eps: Numerical stability constant
Returns:
Output of shape (n, d_v)
"""
if feature_map is None:
feature_map = elu_feature_map
n, d_k = Q.shape
d_v = V.shape[1]
Q_prime = feature_map(Q) # (n, d_k)
K_prime = feature_map(K) # (n, d_k)
# Initialize running sums
S = np.zeros((d_k, d_v), dtype=Q.dtype) # Key-value aggregate
Z = np.zeros(d_k, dtype=Q.dtype) # Key sum for normalization
outputs = np.zeros((n, d_v), dtype=Q.dtype)
for i in range(n):
# Update running sums with current position
S += np.outer(K_prime[i], V[i])
Z += K_prime[i]
# Compute output for position i
numerator = Q_prime[i] @ S
denominator = Q_prime[i] @ Z + eps
outputs[i] = numerator / denominator
return outputsnp.random.seed(42)
n, d = 32, 16
Q = np.random.randn(n, d).astype(np.float32) * 0.5
K = np.random.randn(n, d).astype(np.float32) * 0.5
V = np.random.randn(n, d).astype(np.float32)
output_causal = causal_linear_attention(Q, K, V)
# Compare with non-causal
output_full = linear_attention(Q, K, V, feature_map=elu_feature_map)
# The outputs should differ because causal restricts attention
diff = np.mean(np.abs(output_causal - output_full))Causal vs Non-causal Linear Attention: Mean absolute difference: 0.2188 This difference is expected: causal attention sees less context per position.
The causal formulation is particularly elegant because the running sum structure means that during inference, we can process one token at a time with memory overhead per step (just updating the aggregate). This makes linear attention ideal for autoregressive generation.
