HardKimi K3

KDA Recurrence

Kimi K3

Hard

Problem

Kimi Delta Attention updates one matrix state per batch item and head after every token. Queries and keys have shape (B,S,H,D_k), values and output-gate logits have shape (B,S,H,D_v), and the state has shape (B,H,D_k,D_v). The write strength supplies one scalar per token and head, while the decay logits have the same shape as the keys.

For retention vector \alpha_t, write strength \beta_t, key k_t, value v_t, and previous state S_{t-1}, update the state in sequence order with

S_t=\left(I-\beta_tk_tk_t^{\mathsf T}\right)\operatorname{Diag}(\alpha_t)S_{t-1}+\beta_tk_tv_t^{\mathsf T}.

For lower log-decay bound g_{\min}<0 and decay logit z_t, compute

\alpha_t=\exp\left(g_{\min}\operatorname{sigmoid}(z_t)\right).

Read from the updated state, not the previous state:

\widetilde{o}_t=S_t^{\mathsf T}q_t.

Apply RMS normalization independently over each head's D_v values, multiply by the sigmoid output gate, concatenate the H heads, and apply the supplied projection from width HD_v to model width. Return a dictionary with outputs and final_state. Their shapes are (B,S,D_{model}) and (B,H,D_k,D_v), and both preserve the input floating-point dtype and device.

Theory

Kimi Delta Attention, or KDA, reads a sequence through a running memory matrix. Instead of keeping every earlier key and value, it updates one fixed-size state after each token. The important idea in this problem is that the state can forget old information, correct information associated with a key, and write new information before the current query reads from it.

What the state remembers

Think of the state as a small associative memory. A key describes where information belongs, a value is the information being stored, and a query asks the memory what it currently knows. The matrix state connects key features to value features, so multiplying the updated state by a query produces a value-like output.

KDA processes tokens in sequence order because every update depends on the state left by the previous token. For the current token, the update is

S_t = \left(I-\beta_t k_t k_t^{\mathsf T}\right)\operatorname{Diag}(\alpha_t)S_{t-1} + \beta_t k_t v_t^{\mathsf T}

This formula has three understandable parts:

The erase and write terms use the same strength \beta_t. A small value makes the update cautious, while a value near one makes the new token change the memory strongly.

Why KDA decays channels separately

A single scalar forget gate would retain or fade every key feature by the same amount. KDA instead computes one retention value for each key channel. This allows some parts of the memory to remain stable while others change quickly.

The decay logits are converted to retention values with

\alpha_t = \exp\left(g_{\min}\operatorname{sigmoid}(z_t)\right)

Since g_{\min} is negative and sigmoid returns a value between zero and one, every retention value lies between \exp(g_{\min}) and one. In Kimi K3, the fixed lower log-decay bound prevents the decay from becoming arbitrarily extreme. For this exercise, the practical consequence is simple: apply sigmoid first, multiply by the negative bound, then exponentiate element by element.

Read after writing

The current output is read from S_t, not from S_{t-1}:

\widetilde{o}_t = S_t^{\mathsf T}q_t

That ordering means a token can contribute to its own result. If the implementation reads first and updates afterward, every output is shifted by one step and the first token cannot use its own value.

The raw readout is normalized independently within each head. RMS normalization divides a head vector by the square root of its mean squared value plus a small epsilon. This controls scale without subtracting the mean. A sigmoid gate then decides, channel by channel, how much of the normalized readout should pass. Finally, the heads are joined and the supplied output projection mixes their channels into model width.

A one-dimensional example

Use a single head with one key channel and one value channel. Let the previous state be 2, retention be 0.5, key be 1, value be 4, and write strength be 0.25.

First decay the old state:

0.5 \times 2 = 1

The erase factor is 1-0.25\times1\times1=0.75, so the retained part becomes 0.75. The new write is 0.25\times1\times4=1. The updated state is therefore 1.75.

If the query is 2, the raw readout is 1.75\times2=3.5. This example shows why the update is more than ordinary accumulation: part of the old association is deliberately erased before the new one is written.

Implementation order

Common mistakes to avoid

Examples

Example 1

Input
query = [[[[1]]]], key = [[[[1]]]], value = [[[[2]]]], decay_logits = [[[[0]]]], write_strength = [[[[0.5]]]], output_gate_logits = [[[[0]]]], output_projection = [[1]], initial_state = [[[[0]]]], g_min = -5, eps = 1e-06
Output
{"outputs": tensor([[[0.4999997500001875]]]), "final_state": tensor([[[[1.0]]]])}
Explanation
The write creates a state value of 1. RMS normalization keeps the scalar readout near 1, and the sigmoid gate scales it by 0.5.

Example 2

Input
query/key/decay logits shape (1, 2, 1, 2), value shape (1, 2, 1, 1), initial state shape (1, 1, 2, 1), output projection shape (2, 1)
Output
{"outputs": tensor of shape (1, 2, 2), "final_state": tensor of shape (1, 1, 2, 1)}

Example 3

Input
query/key/decay logits shape (1, 2, 2, 1), value shape (1, 2, 2, 1), initial state shape (1, 2, 1, 1), output projection shape (2, 2)
Output
{"outputs": tensor of shape (1, 2, 2), "final_state": tensor of shape (1, 2, 1, 1)}

Hints

  1. Broadcast each retention vector across the value dimension before applying it to the state.
  2. Use outer products for both the erase term and the key-value write term.
  3. Read from the updated state, normalize each head over its value width, then concatenate the heads.

Requirements

Constraints

Starter Code

import torch

def kda_recurrence(query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, decay_logits: torch.Tensor, write_strength: torch.Tensor, output_gate_logits: torch.Tensor, output_projection: torch.Tensor, initial_state: torch.Tensor, g_min: float = -5.0, eps: float = 1e-6) -> dict[str, torch.Tensor]:
    """
    Returns a dictionary containing sequence outputs and the final recurrent state.
    """
    pass

Test Cases

CaseMatches
One token write and readpublic
Repeated key erases stale valuepublic
Two heads with a nonzero statepublic