@@ -673,6 +673,138 @@ def generate_attention_mask(
673673
674674 return jnp .where (output_mask , 0.0 , DEFAULT_MASK_VALUE ) if output_mask is not None else None
675675
676+ def calculate_moba_gate_logic (self , q_item , k_item , q_pos_item ):
677+ """Computes the block-level MoBA gating intermediates for one batch item.
678+
679+ Args:
680+ q_item: Query tensor shaped `[q_len, n_q_heads, head_dim]`.
681+ k_item: Key tensor shaped `[kv_len, n_kv_heads, head_dim]`.
682+ q_pos_item: Absolute query positions shaped `[q_len]`, used to derive the
683+ chunk index for each query.
684+ For example, during prefill after 128 tokens
685+ have been processed `q_pos_item` is `jnp.arange(128, 128 + q_len)`,
686+ while in autoregressive decode with a single query token it is
687+ `jnp.array([kv_len - 1])`.
688+
689+ Returns:
690+ `need_attend`, a boolean mask of shape `[n_kv_heads, g, q_len, num_block]`
691+ indicating which key blocks each query should attend to. The additional
692+ values in the returned tuple are debug intermediates used for logging and
693+ diagnostics when inspecting the gating behaviour.
694+ """
695+ q_len , n_q_heads , head_dim = q_item .shape
696+ kv_len , n_kv_heads , _ = k_item .shape
697+ g = n_q_heads // n_kv_heads
698+
699+ q_item_f32 = q_item .astype (jnp .float32 ).reshape (q_len , n_kv_heads , g , head_dim ) # grouped-query attention (GQA)
700+
701+ moba_chunk_size = self .config .moba_chunk_size
702+ moba_topk = self .config .moba_topk
703+
704+ num_block = math .ceil (kv_len / moba_chunk_size )
705+
706+ block_ids = jnp .arange (kv_len , dtype = jnp .int32 ) // moba_chunk_size # chunk index for each key position
707+ # Sum key vectors per chunk so we can later average within each block.
708+ key_gate_weight_sum = jax .ops .segment_sum (
709+ k_item .astype (jnp .float32 ), block_ids , num_segments = num_block
710+ ) # [num_block, n_kv_heads, head_dim]
711+ # Count how many tokens end up in each chunk so we can take the mean.
712+ block_counts = jax .ops .segment_sum (
713+ jnp .ones ((kv_len ,), dtype = jnp .float32 ), block_ids , num_segments = num_block
714+ ) # [num_block]
715+ # Mean Pooling, Avoid division by zero for empty blocks.
716+ key_gate_weight = key_gate_weight_sum / jnp .maximum (
717+ block_counts [:, None , None ], 1
718+ ) # [num_block, n_kv_heads, head_dim]
719+
720+ # Take the dot product between each query and every key chunk to get a score.
721+ gate = jnp .einsum ("skgd,Nkd->kgsN" , q_item_f32 , key_gate_weight ) # [n_kv_heads, g, q_len, num_block]
722+ gate_before_masking = gate
723+
724+ q_block_idx = q_pos_item // moba_chunk_size # chunk id for each query
725+ block_indices = jnp .arange (num_block ) # list every key chunk index
726+
727+ q_block_idx_b = jnp .expand_dims (q_block_idx , axis = - 1 ) # [q_len, 1]
728+ block_indices_b = jnp .expand_dims (block_indices , axis = 0 ) # [1, num_block]
729+
730+ # Block-causal masking: a query can't attend to future key blocks,
731+ # and must attend to its own key block.
732+ mask_future = q_block_idx_b > block_indices_b
733+ gate = jnp .where (mask_future , gate , - float ("inf" ))
734+ mask_diag = q_block_idx_b == block_indices_b
735+ gate = jnp .where (mask_diag , float ("inf" ), gate )
736+ gate_after_masking = gate
737+
738+ k_for_topk = min (moba_topk , num_block )
739+ gate_top_k_val , gate_top_k_idx = jax .lax .top_k (gate , k = k_for_topk ) # [n_kv_heads, g, q_len, k_for_topk]
740+ gate_top_k_val_min = jnp .min (gate_top_k_val , axis = - 1 , keepdims = True ) # [n_kv_heads, g, q_len, 1]
741+ need_attend_threshold_mask = gate >= gate_top_k_val_min # [n_kv_heads, g, q_len, num_block]
742+
743+ # Tie-breaking: if multiple blocks have the same gate value as the k-th
744+ # block, we only select the ones that appear in the top-k indices.
745+ gate_idx_mask = jnp .sum (
746+ jax .nn .one_hot (gate_top_k_idx , num_block , dtype = jnp .bool_ ), axis = - 2
747+ ) # [n_kv_heads, g, q_len, num_block]
748+ need_attend = jnp .logical_and (need_attend_threshold_mask , gate_idx_mask ) # [n_kv_heads, g, q_len, num_block]
749+
750+ return (
751+ key_gate_weight ,
752+ gate_before_masking ,
753+ gate_after_masking ,
754+ gate_top_k_val ,
755+ gate_top_k_idx ,
756+ gate_top_k_val_min ,
757+ need_attend_threshold_mask ,
758+ gate_idx_mask ,
759+ need_attend , # [n_kv_heads, g, q_len, num_block]
760+ )
761+
762+ def generate_moba_mask_single_item (self , q_item , k_item , q_positions ):
763+ """Generates the token-level MoBA additive mask for a single batch item."""
764+ q_len , _ , _ = q_item .shape
765+ kv_len , _ , _ = k_item .shape
766+ moba_chunk_size = self .config .moba_chunk_size
767+
768+ # Run the gating logic to find which key blocks this query cares about.
769+ * _ , need_attend = self .calculate_moba_gate_logic (q_item , k_item , q_positions )
770+
771+ # Expand the block-level `need_attend` mask to a token-level mask.
772+ k_block_indices = jnp .arange (kv_len , dtype = jnp .int32 ) // moba_chunk_size
773+ token_level_need_attend = need_attend [..., k_block_indices ]
774+
775+ # Convert the boolean mask to float mask values.
776+ gate = jnp .where (token_level_need_attend , 0.0 , - float ("inf" ))
777+
778+ # Apply a final per-token causal mask to ensure causality within chunks.
779+ k_indices = jax .lax .broadcasted_iota (jnp .int32 , (q_len , kv_len ), 1 )
780+ q_indices = q_positions [:, None ]
781+ causal_mask = q_indices >= k_indices
782+ gate = jnp .where (causal_mask , gate , - float ("inf" ))
783+
784+ # Return the additive mask for this batch item.
785+ return gate
786+
787+ def _generate_moba_mask (self , query : Array , key : Array , q_positions : Array ) -> Array :
788+ """Builds the token-level MoBA additive mask for the whole batch.
789+
790+ Args:
791+ query: Query tensor shaped `[batch, q_len, n_q_heads, head_dim]`.
792+ key: Key tensor shaped `[batch, kv_len, n_kv_heads, head_dim]`.
793+ q_positions: Absolute query positions shaped `[q_len]`, shared across the
794+ batch, identifying the starting offset of each query token.
795+ For example, in prefill after 128 tokens we pass
796+ `jnp.arange(128, 128 + q_len)`, while in autoregressive decode with a
797+ single new token the vector is `[kv_len - 1]` for each batch element.
798+
799+ Returns:
800+ Additive attention mask with shape
801+ `[batch, n_kv_heads, n_q_heads // n_kv_heads, q_len, kv_len]` containing
802+ `0.` for permitted positions and `-inf` for masked ones.
803+ """
804+ # vmap over the batch dimension of query and key. q_positions is constant across the batch.
805+ moba_mask = jax .vmap (self .generate_moba_mask_single_item , in_axes = (0 , 0 , None ))(query , key , q_positions )
806+ return moba_mask
807+
676808 def apply_attention (
677809 self ,
678810 query : Array ,
@@ -1330,9 +1462,27 @@ def apply_attention_dot(
13301462 # Casting softmaxt computation for float32 for model stability.
13311463 if self .float32_logits :
13321464 attn_weights = attn_weights .astype (jnp .float32 )
1465+
13331466 attn_mask = self .generate_attention_mask (
13341467 query , key , decoder_segment_ids , model_mode , previous_chunk , bidirectional_mask
13351468 )
1469+ if self .config .moba :
1470+ kv_seq_len = key .shape [1 ]
1471+ # This logic for `next_pos` is duplicated from `generate_attention_mask`.
1472+ # It determines the starting position of the query sequence.
1473+ next_pos = 0
1474+ if previous_chunk is not None :
1475+ next_pos = previous_chunk .shape [1 ]
1476+ elif model_mode == MODEL_MODE_AUTOREGRESSIVE and q_seq_len == 1 :
1477+ next_pos = kv_seq_len - 1
1478+ q_positions = jnp .arange (next_pos , next_pos + q_seq_len )
1479+
1480+ # The gate calculation in MoBA uses the unscaled query.
1481+ # With scaled query, the gate values are scaled, but since the top-k selection
1482+ # is scale-invariant, we can use the scaled query directly.
1483+ moba_mask = self ._generate_moba_mask (query , key , q_positions )
1484+ attn_weights += moba_mask
1485+
13361486 if self .is_partition_in_decode (q_seq_len ):
13371487 attn_mask = partitioning .with_sharding_constraint (attn_mask , (KV_LENGTH , HEAD , None , None , None ))
13381488 elif model_mode == MODEL_MODE_PREFILL :
0 commit comments