Skip to content

Commit 327a68d

Browse files
committed
feat(attention): update CSA/HCA compressors and Lightning Indexer query expansion parameters for Trillium parity
1 parent c98a34e commit 327a68d

1 file changed

Lines changed: 173 additions & 91 deletions

File tree

src/maxtext/layers/attention_compressed.py

Lines changed: 173 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
from typing import Any
1818
import jax
1919
import jax.numpy as jnp
20+
from jax.sharding import Mesh
2021
from flax import nnx
2122
from maxtext.layers.embeddings import DeepSeekV4RotaryEmbedding, apply_rotary_pos_emb
2223
from maxtext.layers.normalizations import DeepSeekV4RMSNorm, DeepSeekV4UnweightedRMSNorm
2324
from maxtext.layers.linears import DeepSeekGroupedLinear
25+
from maxtext.layers.attention_op import AttentionOp
26+
from maxtext.common.common_types import MODEL_MODE_TRAIN, AttentionType
2427

2528

2629
class HCACompressor(nnx.Module):
@@ -105,7 +108,7 @@ def __init__(
105108
# Interleaved rotary embeddings applied to the trailing slice
106109
self.rotary_emb = DeepSeekV4RotaryEmbedding(
107110
head_dim=head_dim,
108-
partial_rotary_factor=64.0 / 512.0,
111+
partial_rotary_factor=config.qk_rope_head_dim / config.head_dim,
109112
rope_theta=rope_theta,
110113
)
111114

@@ -328,7 +331,7 @@ def __init__(
328331
# Interleaved rotary embedding aligning query/key pos representations
329332
self.rotary_emb = DeepSeekV4RotaryEmbedding(
330333
head_dim=self.head_dim,
331-
partial_rotary_factor=(config.head_dim * (64.0 / 512.0)) / self.head_dim,
334+
partial_rotary_factor=config.qk_rope_head_dim / self.head_dim,
332335
rope_theta=rope_theta,
333336
)
334337

@@ -582,7 +585,7 @@ def __init__(
582585
# Interleaved rotary embeddings for compressed sequences
583586
self.rotary_emb = DeepSeekV4RotaryEmbedding(
584587
head_dim=head_dim,
585-
partial_rotary_factor=64.0 / 512.0,
588+
partial_rotary_factor=config.qk_rope_head_dim / config.head_dim,
586589
rope_theta=rope_theta,
587590
)
588591

@@ -761,9 +764,11 @@ def __init__(
761764
num_heads: int,
762765
config: Any,
763766
layer_idx: int,
767+
mesh: Mesh | None = None,
764768
eps: float = 1e-6,
765769
weight_dtype: Any = jnp.float32,
766770
dtype: Any = jnp.float32,
771+
attention_type: str = "compressed_sparse_attention",
767772
*,
768773
rngs: nnx.Rngs,
769774
):
@@ -779,12 +784,13 @@ def __init__(
779784
eps: Tiny additive variance limit for RMS normalization stability.
780785
weight_dtype: The parameter weights numerical data type.
781786
dtype: The mathematical execution numerical data type.
787+
attention_type: The type of compressed attention being instantiated.
782788
rngs: The Flax NNX random number generator collection.
783789
"""
784790
super().__init__()
785791
self.config = config
786792
self.layer_idx = layer_idx
787-
self.layer_type = config.layer_types[layer_idx]
793+
self.attention_type = attention_type
788794
self.num_heads = num_heads
789795
self.head_dim = head_dim
790796
self.sliding_window = config.sliding_window
@@ -858,7 +864,7 @@ def __init__(
858864
self.sinks = nnx.Param(jax.nn.initializers.zeros(rngs.params(), (num_heads,), weight_dtype))
859865

860866
# Layer specific compressor allocation
861-
if self.layer_type == "heavily_compressed_attention":
867+
if self.attention_type == "heavily_compressed_attention":
862868
self.compressor = HCACompressor(
863869
hidden_size=hidden_size,
864870
head_dim=head_dim,
@@ -869,7 +875,7 @@ def __init__(
869875
dtype=dtype,
870876
rngs=rngs,
871877
)
872-
elif self.layer_type == "compressed_sparse_attention":
878+
elif self.attention_type == "compressed_sparse_attention":
873879
self.compressor = CSACompressor(
874880
hidden_size=hidden_size,
875881
q_lora_rank=q_lora_rank,
@@ -884,117 +890,193 @@ def __init__(
884890
else:
885891
self.compressor = None
886892

893+
# Compute partial rotary factor dynamically from config to prevent dimension mismatches.
894+
# DeepSeek-V4 pairs consecutive channels to apply partial RoPE on qk_rope_head_dim channels,
895+
# requiring dynamic scaling: partial_rotary_factor = qk_rope_head_dim / head_dim.
896+
self.partial_rotary_factor = self.config.qk_rope_head_dim / self.config.head_dim
897+
898+
self.rope_theta = (
899+
self.config.rope_max_timescale if self.attention_type == "sliding_attention" else self.config.compress_rope_theta
900+
)
901+
902+
# Local rotary embedding block matching standard MaxText (Gemma/Llama2) paradigms.
903+
self.rotary_embedding = DeepSeekV4RotaryEmbedding(
904+
head_dim=self.head_dim,
905+
partial_rotary_factor=self.partial_rotary_factor,
906+
rope_theta=self.rope_theta,
907+
)
908+
909+
# Scaling factor applied to query representations to match standard MaxText attention scaling.
910+
# MaxText's AttentionOp core expects queries to be pre-scaled by 1 / sqrt(head_dim).
911+
self.scaling = self.head_dim**-0.5
912+
913+
self.attention_op = AttentionOp(
914+
config=self.config,
915+
mesh=mesh,
916+
attention_kernel=self.config.attention,
917+
max_target_length=self.config.max_target_length,
918+
num_query_heads=self.num_heads,
919+
num_kv_heads=1,
920+
dtype=self.dtype,
921+
compute_axis_order=(0, 1, 2, 3),
922+
attention_type=AttentionType.FULL,
923+
rngs=rngs,
924+
)
925+
887926
def __call__(
888927
self,
889-
hidden_states: jnp.ndarray,
890-
cos: jnp.ndarray,
891-
sin: jnp.ndarray,
892-
position_ids: jnp.ndarray,
928+
hidden_states: jnp.ndarray | None = None,
929+
position_ids: jnp.ndarray | None = None,
893930
attention_mask: jnp.ndarray | None = None,
931+
inputs_q: jnp.ndarray | None = None,
932+
inputs_kv: jnp.ndarray | None = None,
933+
**kwargs,
894934
) -> tuple[jnp.ndarray, jnp.ndarray]:
895-
"""Executes DeepSeek-V4 compressed multi-head attention.
935+
"""Executes the main coordination attention pass over sequence inputs.
896936
897-
This method projects input states to query representations, applies low-rank
898-
LoRA, normalizes and applies positional RoPE, creates shared multi-query key-value
899-
configurations, expands local context windows using structural compressors (if present),
900-
calculates multi-head attention logits, applies structural masks and learnable
901-
sinks, computes stable attention probabilities, and mixes final attention outputs
902-
using parallel block-diagonal grouped projections.
937+
This method coordinates multi-head attention augmented with query-compression LoRA
938+
projections, unweighted key/value normalizations, long-range context compressor
939+
integrations, learnable attention sinks, and parallelized grouped output mixing.
903940
904941
Args:
905-
hidden_states: The input hidden representation sequence of shape [B, S, D_model].
906-
cos: Positional RoPE cosine frequencies array of shape [B, S, D_rope].
907-
sin: Positional RoPE sine frequencies array of shape [B, S, D_rope].
908-
position_ids: Absolute sequence position identifiers of shape [B, S].
909-
attention_mask: Optional attention mask of shape [B, 1, S, S_kv].
942+
hidden_states: Input sequence representations of shape [B, S, D_model].
943+
position_ids: Sequence absolute position identifiers of shape [B, S].
944+
attention_mask: Optional attention mask preventing invalid token attendance.
945+
inputs_q: Optional query input override for decoupled execution.
946+
inputs_kv: Optional key/value input override for decoupled execution.
947+
**kwargs: Additional runtime execution configurations (e.g., decoder_segment_ids).
910948
911949
Returns:
912-
A tuple containing:
913-
- The projected mixed output sequence tensor of shape [B, S, D_model].
914-
- The final multi-head attention weights of shape [B, H, S, S_kv].
950+
Tuple containing the projected output representations of shape [B, S, D_model]
951+
and an empty caching intermediate placeholder.
915952
"""
916-
# hidden_states shape: [B, S, D_model]
917-
# cos, sin shape: [B, S, D_rope]
918-
# position_ids shape: [B, S]
919-
# attention_mask shape: [B, 1, S_q, S_kv]
953+
# Resolve input representations from standard hidden states or override inputs.
954+
# hidden_states: [B, S, D_model]
955+
if hidden_states is None:
956+
hidden_states = inputs_q
920957
batch, seq_len, _ = hidden_states.shape
921-
h_shape = (batch, seq_len, self.num_heads, self.head_dim)
922958

923-
# Project inputs to query representations
924-
# q_residual: [B, S, D_rank]
959+
# Generate absolute position identifiers if not provided at runtime.
960+
# position_ids: [B, S]
961+
if position_ids is None:
962+
position_ids = jnp.broadcast_to(jnp.arange(seq_len, dtype=jnp.int32)[None], (batch, seq_len))
963+
964+
# Resolve rotary position embedding sinusoids from runtime keyword arguments or compute local sinusoids.
965+
# Utilizing pre-computed sinusoids avoids redundant computation across decoder layers during forward passes.
966+
# cos: [B, S, D_rope/2]
967+
# sin: [B, S, D_rope/2]
968+
cos = kwargs.get("cos", None)
969+
sin = kwargs.get("sin", None)
970+
if cos is None or sin is None:
971+
cos, sin = self.rotary_embedding(hidden_states, position_ids)
972+
973+
# Project input features to low-rank query residuals and apply RMS normalization.
974+
# # [B, S, D_model] -> [B, S, D_rank]
925975
q_residual = self.q_a_norm(self.q_a_proj(hidden_states))
926-
# q: [B, H, S, D_head]
927-
q = self.q_b_proj(q_residual).reshape(h_shape).transpose(0, 2, 1, 3)
928-
q = self.q_b_norm(q)
929-
# q: [B, H, S, D_head]
930-
q = apply_rotary_pos_emb(q, cos, sin, unsqueeze_dim=1)
931-
932-
# Project inputs to key/value representation
933-
# kv: [B, 1, S, D_head]
934-
kv = self.kv_norm(self.kv_proj(hidden_states)).reshape(batch, seq_len, 1, self.head_dim).transpose(0, 2, 1, 3)
935-
# kv: [B, 1, S, D_head]
936-
kv = apply_rotary_pos_emb(kv, cos, sin, unsqueeze_dim=1)
937976

977+
# Project low-rank residuals to multi-head query dimensions and reshape.
978+
# # [B, S, D_rank] -> [B, S, H, D_head]
979+
q = self.q_b_proj(q_residual).reshape(batch, seq_len, self.num_heads, self.head_dim)
980+
981+
# Apply scale-free unweighted RMS normalization across multi-head queries and scale by attention scaling factor.
982+
# Unweighted normalization stabilizes query variance without introducing learnable scaling parameters.
983+
# MaxText's AttentionOp core assumes pre-scaled query tensors, requiring explicit scaling here.
984+
# # [B, S, H, D_head] -> [B, S, H, D_head]
985+
q = self.q_b_norm(q) * self.scaling
986+
987+
# Apply Rotary Position Embedding (RoPE) to query representations.
988+
# # [B, S, H, D_head] -> [B, S, H, D_head]
989+
q = apply_rotary_pos_emb(q, cos, sin, unsqueeze_dim=2)
990+
991+
# Project input representations to shared key/value features and apply RMS normalization.
992+
# # [B, S, D_model] -> [B, S, 1, D_head]
993+
kv = self.kv_norm(self.kv_proj(hidden_states)).reshape(batch, seq_len, 1, self.head_dim)
994+
995+
# Apply Rotary Position Embedding (RoPE) to shared key/value representations.
996+
# # [B, S, 1, D_head] -> [B, S, 1, D_head]
997+
kv = apply_rotary_pos_emb(kv, cos, sin, unsqueeze_dim=2)
998+
999+
# Integrate long-range context compressor representations if configured.
9381000
block_bias = None
939-
# Apply compressed attention key-value generation path
9401001
if self.compressor is not None:
941-
# compressed_kv: [B, 1, W, D_head] or [B, 1, S*k, D_head]
942-
# block_bias: [B, 1, S, W] or [B, 1, S, S*k]
1002+
# Execute compressor pass to extract compressed key/value blocks and structural block bias masks.
1003+
# compressed_kv: [B, 1, W, D_head] or [B, W, 1, D_head]
1004+
# block_bias: [B, 1, S, W] or [B, S, W]
9431005
compressed_kv, block_bias = self.compressor(hidden_states, q_residual, position_ids)
944-
# kv: [B, 1, S + W, D_head] or [B, 1, S + S*k, D_head]
945-
kv = jnp.concatenate([kv, compressed_kv], axis=2)
9461006

947-
# Broadcast key/value configurations to all heads
948-
# k: [B, H, S_kv, D_head]
949-
k = jnp.repeat(kv, self.num_heads, axis=1)
950-
# v: [B, H, S_kv, D_head]
951-
v = jnp.repeat(kv, self.num_heads, axis=1)
1007+
# Standardize compressed key/value layout to match multi-head sequence formats.
1008+
# # [B, 1, W, D_head] -> [B, W, 1, D_head]
1009+
if compressed_kv.shape[1] == 1:
1010+
compressed_kv = compressed_kv.transpose(0, 2, 1, 3)
9521011

953-
# Compute attention logits
954-
# logits: [B, H, S, S_kv]
955-
logits = jnp.einsum("bhsd, bhkd -> bhsk", q, k, precision=self.config.matmul_precision) * self.scaling
1012+
# Concatenate local sequence keys with compressed long-range cache blocks along sequence dimension.
1013+
# # [B, S, 1, D_head] + [B, W, 1, D_head] -> [B, S + W, 1, D_head]
1014+
kv = jnp.concatenate([kv, compressed_kv], axis=1)
9561015

957-
# Apply attention mask addition and block bias concatenation
1016+
# Reconcile structural block bias masks with runtime attention masks.
9581017
if attention_mask is not None:
9591018
if block_bias is not None:
1019+
# Concatenate block bias mask to attention mask along trailing sequence dimension.
1020+
# # [B, 1, S, S] + [B, 1, S, W] -> [B, 1, S, S + W]
9601021
attention_mask = jnp.concatenate([attention_mask, block_bias.astype(attention_mask.dtype)], axis=-1)
961-
elif kv.shape[2] > attention_mask.shape[-1]:
962-
pad_width = kv.shape[2] - attention_mask.shape[-1]
1022+
elif kv.shape[1] > attention_mask.shape[-1]:
1023+
# Pad attention mask with zero-value allowed elements to match extended key/value sequence length.
1024+
# # [B, 1, S, S] -> [B, 1, S, S + W]
1025+
pad_width = kv.shape[1] - attention_mask.shape[-1]
9631026
attention_mask = jnp.pad(attention_mask, ((0, 0), (0, 0), (0, 0), (0, pad_width)), constant_values=0.0)
964-
logits = logits + attention_mask
965-
966-
# Concatenate learnable attention sinks
967-
# sinks: [1, H, 1, 1] -> [B, H, S, 1]
968-
sinks = self.sinks.value.reshape(1, -1, 1, 1)
969-
sinks = jnp.broadcast_to(sinks, (batch, self.num_heads, seq_len, 1))
970-
# combined_logits: [B, H, S, S_kv + 1]
971-
combined_logits = jnp.concatenate([logits, sinks], axis=-1)
972-
973-
# Stable Softmax projection
974-
combined_logits = combined_logits - jnp.max(combined_logits, axis=-1, keepdims=True)
975-
probs = jax.nn.softmax(combined_logits, axis=-1)
976-
# Drop sinks representation column
977-
# attn_weights: [B, H, S, S_kv]
978-
attn_weights = probs[..., :-1]
979-
980-
# Project attention weights onto values
981-
# attn_output: [B, H, S, D_head]
982-
attn_output = jnp.einsum("bhsk, bhkd -> bhsd", attn_weights, v, precision=self.config.matmul_precision)
983-
984-
# Apply conjugate RoPE transformation to restore position invariants
985-
attn_output = apply_rotary_pos_emb(attn_output, cos, -sin, unsqueeze_dim=1)
986-
987-
# Map outputs to grouped linear configuration
988-
# grouped: [B, S, o_groups, (H / o_groups) * D_head]
989-
grouped = attn_output.transpose(0, 2, 1, 3).reshape(batch, seq_len, self.config.o_groups, -1)
990-
# grouped: [B, S, o_groups, o_lora_rank]
1027+
# Ensure key/value sequence length is perfectly divisible by the Splash attention block size (sa_block_kv).
1028+
# Hardware Matrix Multiply Units (MXUs) and XLA Pallas kernels enforce strict memory layout alignment grids.
1029+
# When Splash Flash Attention is active, the runtime key/value sequence dimension must perfectly divide by sa_block_kv.
1030+
# Because long-range cache compressors append dynamic auxiliary tokens (e.g. +32 tokens), the resulting combined length
1031+
# may break hardware divisibility constraints (e.g. 4128 % 512 != 0).
1032+
# This dynamic padding forces exact MXU grid alignment.
1033+
# # [B, S + W, 1, D_head] -> [B, align(S + W, sa_block_kv), 1, D_head]
1034+
if self.config.sa_block_kv > 0 and kv.shape[1] % self.config.sa_block_kv != 0:
1035+
pad_len = self.config.sa_block_kv - (kv.shape[1] % self.config.sa_block_kv)
1036+
kv = jnp.pad(kv, ((0, 0), (0, pad_len), (0, 0), (0, 0)), constant_values=0.0)
1037+
if attention_mask is not None:
1038+
# Pad 4D attention mask along trailing key/value sequence axis.
1039+
# # [B, 1, Q, S + W] -> [B, 1, Q, align(S + W, sa_block_kv)]
1040+
attention_mask = jnp.pad(attention_mask, ((0, 0), (0, 0), (0, 0), (0, pad_len)), constant_values=0.0)
1041+
1042+
# Squeeze redundant head dimension from 4D attention masks to ensure compatibility with AttentionOp core.
1043+
# # [B, 1, S, S + W] -> [B, S, S + W]
1044+
unified_mask = (
1045+
jnp.squeeze(attention_mask, axis=1) if attention_mask is not None and attention_mask.ndim == 4 else attention_mask
1046+
)
1047+
1048+
# Execute core attention operator pass over query and concatenated key/value sequences.
1049+
# # q: [B, S, H, D_head], kv: [B, S + W, 1, D_head] -> [B, S, H, D_head]
1050+
attn_output = self.attention_op(
1051+
query=q,
1052+
key=kv,
1053+
value=kv,
1054+
decoder_segment_ids=kwargs.get("decoder_segment_ids", None),
1055+
inputs_positions=position_ids,
1056+
model_mode=kwargs.get("model_mode", MODEL_MODE_TRAIN),
1057+
indexer_mask=unified_mask,
1058+
sinks=self.sinks,
1059+
)
1060+
1061+
# Apply conjugate RoPE rotation (-sin) to attention outputs to un-rotate representations.
1062+
# Un-rotating aligns output feature spaces prior to multi-head mixing projections.
1063+
# # [B, S, H, D_head] -> [B, S, H, D_head]
1064+
attn_output = apply_rotary_pos_emb(attn_output, cos, -sin, unsqueeze_dim=2)
1065+
1066+
# Reshape attention outputs into block-diagonal output groups.
1067+
# # [B, S, H, D_head] -> [B, S, o_groups, H * D_head / o_groups]
1068+
grouped = attn_output.reshape(batch, seq_len, self.config.o_groups, -1)
1069+
1070+
# Apply block-diagonal grouped linear projections to mix intra-group features.
1071+
# # [B, S, o_groups, H * D_head / o_groups] -> [B, S, o_groups, o_lora_rank]
9911072
grouped = self.o_a_proj(grouped)
992-
# Flatten back to grouped rank representations
993-
# grouped_flat: [B, S, o_groups * o_lora_rank]
1073+
1074+
# Flatten grouped representations into a unified feature vector per sequence position.
1075+
# # [B, S, o_groups, o_lora_rank] -> [B, S, o_groups * o_lora_rank]
9941076
grouped_flat = grouped.reshape(batch, seq_len, -1)
9951077

996-
# Final linear projection to hidden dimension space
997-
# output: [B, S, D_model]
1078+
# Project mixed representations back to global model hidden dimension.
1079+
# # [B, S, o_groups * o_lora_rank] -> [B, S, D_model]
9981080
output = self.o_b_proj(grouped_flat)
9991081

1000-
return output, attn_weights
1082+
return output, None

0 commit comments

Comments
 (0)