40 layers › 2 groups of 6 layers › Source layer, pairs pooled › Compressed sparse attention
learnedself.indexer(x, qr, latent, start_pos, offset) · 735
Choose 512 compressed positions for each query: 32 small heads score one shared key per position.
Why
Choose. A lightweight indexer scores the compressed entries for each query and keeps the best, and the query attends to those together with its window. DeepSeek V4.1 report §2.3
The layers after this one borrow its choice, which spares them an indexer pass of their own. DeepSeek V4.1 report §2.3.1
All 4 steps
- in
- [B batch, T positions, 1280 query latent] [B batch, T positions, 5120 d] [B batch, T/2 compressed positions, 128 index width]
- out
- [B batch, T positions, 512 selected]
- parameters
- 5,406,720 · × 2 groups = 10,813,440
Parameters
5,406,720 parameters per instance, across its learned components
| Component | Calculation | Parameters |
|---|---|---|
| Index query projection | 5,242,880 | |
| Index head weights | 163,840 | |
| Per-instance total | 5,406,720 | |
Across 2 independent groups: parameters.
Derived from the pinned reference implementation and configuration; equal to the element counts of the checkpoint’s own tensors on the text path. The quantisation scales, the vision encoder and the DSpark blocks are not counted.
Explanation
Layers 2, 8, 14, 20, 24, 28, 32, 36 run an indexer; the layers between them use the positions the nearest earlier one chose (model.py 722–737).
Codemodel.py 369–406, 488–580
class Indexer(torch.nn.Module):
"""Keeps the `index_topk` best compressed positions per query.
A small side attention: fp4 query heads against one shared key per compressed position, scores
rectified then combined by `weights_proj`. With a candidate source this is the second of two
levels; `select_candidate_blocks` is the first.
"""
def __init__(self, args: ModelArgs, layer_id: int):
super().__init__()
# the index keys are derived from the compressor's latent, so only a layer that compresses
# its own KV can produce them; every other indexer reads them from that layer's cache
self.owns_k = layer_id in args.kv_source_layers
self.compress_ratio = args.compress_ratios[layer_id]
self.is_candidate_source = layer_id == args.candidate_source_layer
self.uses_candidates = 0 <= args.candidate_source_layer < layer_id
self.candidate_topk_blocks = args.candidate_topk_blocks
self.candidate_block_size = args.candidate_block_size
self.dim = args.dim
self.n_heads = args.index_n_heads
self.n_local_heads = args.index_n_heads // world_size
self.index_head_dim = args.index_head_dim
self.rope_head_dim = args.rope_head_dim
self.index_topk = args.index_topk
self.q_lora_rank = args.q_lora_rank
self.softmax_scale = self.index_head_dim**-0.5
self.wq_b = ColumnParallelLinear(self.q_lora_rank, self.n_heads * self.index_head_dim)
self.weights_proj = ColumnParallelLinear(self.dim, self.n_heads, dtype=torch.bfloat16)
self.freqs_cis: torch.Tensor | None = None
if self.owns_k:
self.wk = Linear(args.head_dim, self.index_head_dim, dtype=torch.bfloat16)
self.k_norm = RMSNorm(self.index_head_dim, args.norm_eps)
self.k_cache: torch.Tensor
self.register_buffer(
"k_cache",
torch.zeros(args.max_batch_size, args.max_seq_len // self.compress_ratio, args.index_head_dim),
persistent=False,
)
def forward(self, x: torch.Tensor, qr: torch.Tensor, latent: torch.Tensor, start_pos: int, offset: int):
"""`latent` is this layer's RoPE-free compressed latent, None when this layer does not
compress or when its current group is still incomplete. An index-key owner turns it into
index keys here, which has to happen before Attention overwrites that same storage with
the RoPE'd, quantized values."""
assert self.freqs_cis is not None
bsz, seqlen, _ = x.size()
ratio, rd, end_pos = self.compress_ratio, self.rope_head_dim, start_pos + seqlen
# latent is None while a group is still filling up, so there is nothing to publish yet
if self.owns_k and latent is not None:
# a latent stands for the first token of its group, so group j takes position j * ratio
freqs = (
self.freqs_cis[: seqlen - seqlen % ratio : ratio]
if start_pos == 0
else self.freqs_cis[start_pos + 1 - ratio].unsqueeze(0)
)
k = self.k_norm(self.wk(latent))
apply_rotary_emb(k[..., -rd:], freqs)
fp4_act_quant(k, fp4_block_size, True)
self.k_cache[:bsz, start_pos // ratio : start_pos // ratio + k.size(1)] = k
shared_attn.index_k = self.k_cache
q = self.wq_b(qr).unflatten(-1, (self.n_local_heads, self.index_head_dim))
apply_rotary_emb(q[..., -rd:], self.freqs_cis[start_pos:end_pos])
fp4_act_quant(q, fp4_block_size, True)
index_k = shared_attn.index_k[:bsz, : end_pos // ratio]
weights = self.weights_proj(x) * (self.softmax_scale * self.n_heads**-0.5)
index_score = torch.einsum("bshd,btd->bsht", q, index_k)
index_score = (index_score.relu_() * weights.unsqueeze(-1)).sum(dim=2)
if world_size > 1:
dist.all_reduce(index_score)
# how many compressed positions each query can see: a block becomes visible once the query
# has passed its last token. One query per decode step, so there it is just a number.
if start_pos == 0:
compress_lens = (torch.arange(1, seqlen + 1, device=x.device) // ratio).unsqueeze(-1)
index_score.masked_fill_(torch.arange(seqlen // ratio, device=x.device) >= compress_lens, -torch.inf)
else:
compress_lens = end_pos // ratio
if self.is_candidate_source:
shared_attn.candidates = select_candidate_blocks(
index_score, compress_lens, self.candidate_topk_blocks, self.candidate_block_size
)
elif self.uses_candidates:
# level two: score with our own weights, but only inside the source's candidate blocks
index_score = index_score.masked_fill(~shared_attn.candidates, -torch.inf)
# top-k by score, re-sorted into position order; unreachable -> -1, rest shifted by offset
topk = min(self.index_topk, end_pos // ratio)
idxs = index_score.topk(topk, dim=-1, sorted=False).indices.sort(dim=-1).values
return torch.where(idxs < compress_lens, idxs + offset, -1).int()def precompute_freqs_cis(dim, seqlen, original_seq_len, base, factor, beta_fast, beta_slow) -> torch.Tensor:
"""Rotary frequencies as complex exponentials, one row per position.
With original_seq_len > 0 this applies YaRN: dimensions whose wavelength already fits inside the
training context keep their frequency, those far beyond it are divided by `factor`, and the
`beta_fast`..`beta_slow` band in between is faded across with a linear ramp.
"""
freqs = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
if original_seq_len > 0:
# the dim whose wavelength completes `rotations` turns over the training context
def corrected_dim(rotations):
return dim * math.log(original_seq_len / (rotations * 2 * math.pi)) / (2 * math.log(base))
low = max(math.floor(corrected_dim(beta_fast)), 0)
high = min(math.ceil(corrected_dim(beta_slow)), dim - 1)
ramp = ((torch.arange(dim // 2, dtype=torch.float32) - low) / max(high - low, 1e-3)).clamp(0, 1)
smooth = 1 - ramp
freqs = freqs / factor * (1 - smooth) + freqs * smooth
freqs = torch.outer(torch.arange(seqlen), freqs)
return torch.polar(torch.ones_like(freqs), freqs)
def apply_rotary_emb(x: torch.Tensor, freqs_cis: torch.Tensor, inverse: bool = False) -> torch.Tensor:
"""Rotate `x` in place, taking adjacent element pairs as complex numbers. Accepts [b, s, d] and
[b, s, h, d]; `inverse` conjugates the rotation, which is how the attention output gets the
query's rotation removed again so the cache can stay in one shared rotated form."""
y = x
x = torch.view_as_complex(x.float().unflatten(-1, (-1, 2)))
if inverse:
freqs_cis = freqs_cis.conj()
if x.ndim == 3:
freqs_cis = freqs_cis.view(1, x.size(1), x.size(-1))
else:
freqs_cis = freqs_cis.view(1, x.size(1), 1, x.size(-1))
x = torch.view_as_real(x * freqs_cis).flatten(-2)
y.copy_(x)
return ySources2
Direct evidence
The indexer scores every compressed position for every query with few small heads against one shared key, masks what the query may not see, and keeps the best 512; the candidate source also publishes its candidate blocks, and later indexers score inside them.
indexer · deepseek-ai/DeepSeek-V4.1-Flash / inference/model.py
Pinned revision 2cba9e42aa02 ↗ · lines 488–580
Read code excerpt
class Indexer(torch.nn.Module):
"""Keeps the `index_topk` best compressed positions per query.
A small side attention: fp4 query heads against one shared key per compressed position, scores
rectified then combined by `weights_proj`. With a candidate source this is the second of two
levels; `select_candidate_blocks` is the first.
"""
def __init__(self, args: ModelArgs, layer_id: int):
super().__init__()
# the index keys are derived from the compressor's latent, so only a layer that compresses
# its own KV can produce them; every other indexer reads them from that layer's cache
self.owns_k = layer_id in args.kv_source_layers
self.compress_ratio = args.compress_ratios[layer_id]
self.is_candidate_source = layer_id == args.candidate_source_layer
self.uses_candidates = 0 <= args.candidate_source_layer < layer_id
self.candidate_topk_blocks = args.candidate_topk_blocks
self.candidate_block_size = args.candidate_block_size
self.dim = args.dim
self.n_heads = args.index_n_heads
self.n_local_heads = args.index_n_heads // world_size
self.index_head_dim = args.index_head_dim
self.rope_head_dim = args.rope_head_dim
self.index_topk = args.index_topk
self.q_lora_rank = args.q_lora_rank
self.softmax_scale = self.index_head_dim**-0.5
self.wq_b = ColumnParallelLinear(self.q_lora_rank, self.n_heads * self.index_head_dim)
self.weights_proj = ColumnParallelLinear(self.dim, self.n_heads, dtype=torch.bfloat16)
self.freqs_cis: torch.Tensor | None = None
if self.owns_k:
self.wk = Linear(args.head_dim, self.index_head_dim, dtype=torch.bfloat16)
self.k_norm = RMSNorm(self.index_head_dim, args.norm_eps)
self.k_cache: torch.Tensor
self.register_buffer(
"k_cache",
torch.zeros(args.max_batch_size, args.max_seq_len // self.compress_ratio, args.index_head_dim),
persistent=False,
)
def forward(self, x: torch.Tensor, qr: torch.Tensor, latent: torch.Tensor, start_pos: int, offset: int):
"""`latent` is this layer's RoPE-free compressed latent, None when this layer does not
compress or when its current group is still incomplete. An index-key owner turns it into
index keys here, which has to happen before Attention overwrites that same storage with
the RoPE'd, quantized values."""
assert self.freqs_cis is not None
bsz, seqlen, _ = x.size()
ratio, rd, end_pos = self.compress_ratio, self.rope_head_dim, start_pos + seqlen
# latent is None while a group is still filling up, so there is nothing to publish yet
if self.owns_k and latent is not None:
# a latent stands for the first token of its group, so group j takes position j * ratio
freqs = (
self.freqs_cis[: seqlen - seqlen % ratio : ratio]
if start_pos == 0
else self.freqs_cis[start_pos + 1 - ratio].unsqueeze(0)
)
k = self.k_norm(self.wk(latent))
apply_rotary_emb(k[..., -rd:], freqs)
fp4_act_quant(k, fp4_block_size, True)
self.k_cache[:bsz, start_pos // ratio : start_pos // ratio + k.size(1)] = k
shared_attn.index_k = self.k_cache
q = self.wq_b(qr).unflatten(-1, (self.n_local_heads, self.index_head_dim))
apply_rotary_emb(q[..., -rd:], self.freqs_cis[start_pos:end_pos])
fp4_act_quant(q, fp4_block_size, True)
index_k = shared_attn.index_k[:bsz, : end_pos // ratio]
weights = self.weights_proj(x) * (self.softmax_scale * self.n_heads**-0.5)
index_score = torch.einsum("bshd,btd->bsht", q, index_k)
index_score = (index_score.relu_() * weights.unsqueeze(-1)).sum(dim=2)
if world_size > 1:
dist.all_reduce(index_score)
# how many compressed positions each query can see: a block becomes visible once the query
# has passed its last token. One query per decode step, so there it is just a number.
if start_pos == 0:
compress_lens = (torch.arange(1, seqlen + 1, device=x.device) // ratio).unsqueeze(-1)
index_score.masked_fill_(torch.arange(seqlen // ratio, device=x.device) >= compress_lens, -torch.inf)
else:
compress_lens = end_pos // ratio
if self.is_candidate_source:
shared_attn.candidates = select_candidate_blocks(
index_score, compress_lens, self.candidate_topk_blocks, self.candidate_block_size
)
elif self.uses_candidates:
# level two: score with our own weights, but only inside the source's candidate blocks
index_score = index_score.masked_fill(~shared_attn.candidates, -torch.inf)
# top-k by score, re-sorted into position order; unreachable -> -1, rest shifted by offset
topk = min(self.index_topk, end_pos // ratio)
idxs = index_score.topk(topk, dim=-1, sorted=False).indices.sort(dim=-1).values
return torch.where(idxs < compress_lens, idxs + offset, -1).int()Direct evidence
Rotary frequencies with the YaRN scaling above the original context, and the rotation of adjacent feature pairs, with an inverse.
rope · deepseek-ai/DeepSeek-V4.1-Flash / inference/model.py
Pinned revision 2cba9e42aa02 ↗ · lines 369–406
Read code excerpt
def precompute_freqs_cis(dim, seqlen, original_seq_len, base, factor, beta_fast, beta_slow) -> torch.Tensor:
"""Rotary frequencies as complex exponentials, one row per position.
With original_seq_len > 0 this applies YaRN: dimensions whose wavelength already fits inside the
training context keep their frequency, those far beyond it are divided by `factor`, and the
`beta_fast`..`beta_slow` band in between is faded across with a linear ramp.
"""
freqs = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
if original_seq_len > 0:
# the dim whose wavelength completes `rotations` turns over the training context
def corrected_dim(rotations):
return dim * math.log(original_seq_len / (rotations * 2 * math.pi)) / (2 * math.log(base))
low = max(math.floor(corrected_dim(beta_fast)), 0)
high = min(math.ceil(corrected_dim(beta_slow)), dim - 1)
ramp = ((torch.arange(dim // 2, dtype=torch.float32) - low) / max(high - low, 1e-3)).clamp(0, 1)
smooth = 1 - ramp
freqs = freqs / factor * (1 - smooth) + freqs * smooth
freqs = torch.outer(torch.arange(seqlen), freqs)
return torch.polar(torch.ones_like(freqs), freqs)
def apply_rotary_emb(x: torch.Tensor, freqs_cis: torch.Tensor, inverse: bool = False) -> torch.Tensor:
"""Rotate `x` in place, taking adjacent element pairs as complex numbers. Accepts [b, s, d] and
[b, s, h, d]; `inverse` conjugates the rotation, which is how the attention output gets the
query's rotation removed again so the cache can stay in one shared rotated form."""
y = x
x = torch.view_as_complex(x.float().unflatten(-1, (-1, 2)))
if inverse:
freqs_cis = freqs_cis.conj()
if x.ndim == 3:
freqs_cis = freqs_cis.view(1, x.size(1), x.size(-1))
else:
freqs_cis = freqs_cis.view(1, x.size(1), 1, x.size(-1))
x = torch.view_as_real(x * freqs_cis).flatten(-2)
y.copy_(x)
return y