@@ -361,18 +361,60 @@ def __call__(
361361 if k .shape [1 ] <= self .indexer_topk :
362362 return None , None , None
363363
364- # Compute Index Scores
365- # QK product: relu(q @ k.T), [b, t, s, h]
366- # Similar to MQA, each key is shared by h query head
367- logits = jnp .einsum ("bthd, bsd -> btsh" , q , k , precision = self .config .matmul_precision )
368- logits = jax .nn .relu (logits )
369364 # Compute head weights: project from input, [b, t, embed_dim] -> [b, t, h]
370365 weights = self .weights_proj (inputs_q )
371366 # Weights scaling affect indexer_score, but does not affect topk_indices. Keep scaling for numerical stability.
372367 # https://github.com/deepseek-ai/DeepSeek-V3.2-Exp/blob/87e509a2e5a100d221c97df52c6e8be7835f0057/inference/model.py#L478-L480
373368 weights = weights * (self .n_heads ** - 0.5 ) * self .softmax_scale
374- # Aggregate head-wise logits: logits @ weights
375- indexer_score = jnp .einsum ("btsh, bth -> bts" , logits , weights , precision = self .config .matmul_precision ) # [b, t, s]
369+
370+ # Compute Index Scores
371+ # When qk_head_chunk_size > 0, compute Index Scores by chunking the 'heads' dimension to reduce memory
372+ # The naive evaluation materializes [b, t, s, h].
373+ # We use jax.lax.scan to compute the score iteratively over head chunks.
374+ b , t , h , d = q .shape
375+ # Control the HBM footprint of QK tensor: [batch, q_len, s_len, heads]
376+ # If set to 0 (defaults), it falls back to native materialization.
377+ head_chunk_size = getattr (self .config , "qk_head_chunk_size" , 0 )
378+
379+ def chunked_head_loop (chunk_size ):
380+ if chunk_size > h or h % chunk_size != 0 :
381+ raise ValueError (
382+ f"qk_head_chunk_size ({ chunk_size } ) must be <= number of indexer heads ({ h } ) " "and divide it evenly."
383+ )
384+ num_chunks = h // chunk_size
385+ # q: [b, t, h, d] -> [h, b, t, d] -> [num_chunks, head_chunk_size, b, t, d]
386+ q_h = q .transpose (2 , 0 , 1 , 3 ).reshape (num_chunks , chunk_size , b , t , d )
387+ # weights: [b, t, h] -> [h, b, t] -> [num_chunks, head_chunk_size, b, t]
388+ w_h = weights .transpose (2 , 0 , 1 ).reshape (num_chunks , chunk_size , b , t )
389+
390+ def scan_body_indexer (carry , xs ):
391+ q_c = xs ["q" ] # [h_chunk, b, t, d]
392+ w_c = xs ["w" ] # [h_chunk, b, t]
393+
394+ # Directly use the chunked shapes in einsum to avoid transposes inside the loop
395+ logits_inner = jnp .einsum ("hbtd, bsd -> btsh" , q_c , k , precision = self .config .matmul_precision )
396+ logits_inner = jax .nn .relu (logits_inner )
397+
398+ score_chunk = jnp .einsum (
399+ "btsh, hbt -> bts" ,
400+ logits_inner ,
401+ w_c ,
402+ precision = self .config .matmul_precision ,
403+ )
404+ return carry + score_chunk .astype (jnp .float32 ), None
405+
406+ init_score = jnp .zeros ((b , t , k .shape [1 ]), dtype = jnp .float32 )
407+ idx_score , _ = jax .lax .scan (jax .checkpoint (scan_body_indexer ), init_score , {"q" : q_h , "w" : w_h })
408+ return idx_score .astype (q .dtype )
409+
410+ if head_chunk_size > 0 :
411+ indexer_score = chunked_head_loop (head_chunk_size )
412+
413+ else :
414+ # Aggregate head-wise logits: logits @ weights natively
415+ logits = jnp .einsum ("bthd, bsd -> btsh" , q , k , precision = self .config .matmul_precision )
416+ logits = jax .nn .relu (logits )
417+ indexer_score = jnp .einsum ("btsh, bth -> bts" , logits , weights , precision = self .config .matmul_precision )
376418
377419 internal_padding_mask = None
378420 if cached_s is not None :
@@ -1086,25 +1128,69 @@ def calculate_indexer_loss(
10861128 query = jax .lax .stop_gradient (query )
10871129 key = jax .lax .stop_gradient (key )
10881130
1089- # Compute attention scores: [b, t, h, d] @ [b, s, h, d] -> [b, h, t, s]
1090- attention_scores = jnp .einsum ("bthd, bshd -> bhts" , query , key , precision = self .config .matmul_precision )
1091-
1131+ # Ensure indexer_score updates identically in all branches
10921132 if sparse_loss :
1093- # indexer_mask is already pre-filtered with the attention_mask if any
1094- attention_scores = attention_scores + indexer_mask [:, None , :, :]
10951133 indexer_score = indexer_score + indexer_mask
1096- elif attention_mask is not None :
1097- # indexer_score already applies attention_mask; updating attention_scores only
1098- attention_scores = attention_scores + attention_mask [:, None , :, :]
1099-
1100- # Use float32 for softmax numerical stability.
1101- attention_probs = jax .nn .softmax (attention_scores .astype (jnp .float32 ), axis = - 1 )
11021134 indexer_probs = jax .nn .softmax (indexer_score .astype (jnp .float32 ), axis = - 1 )
11031135
1104- # Aggregate heads: [b, h, t, s] -> [b, t, s]
1105- attention_probs = jnp .sum (attention_probs , axis = 1 )
1106- # Force materialization and prevent fusion across this point to reuse the intermediate tensor
1107- attention_probs = jax .lax .optimization_barrier (attention_probs )
1136+ batch , q_len , heads , dim = query .shape
1137+
1138+ # Chunk across the 'heads' dimension manually using jax.lax.scan
1139+ # Control the HBM footprint of QK tensor: [batch, q_len, s_len, heads]
1140+ # If set to 0, it falls back to native implementation.
1141+ head_chunk_size = getattr (self .config , "qk_head_chunk_size" , 0 )
1142+ if head_chunk_size > 0 :
1143+ if head_chunk_size > heads or heads % head_chunk_size != 0 :
1144+ raise ValueError (
1145+ f"qk_head_chunk_size ({ head_chunk_size } ) must be <= number of attention query heads ({ heads } ) "
1146+ "and divide it evenly."
1147+ )
1148+ num_chunks = heads // head_chunk_size
1149+
1150+ # Transpose and reshape to put chunk dimension first for jax.lax.scan
1151+ # query: [b, t, h, d] -> [h, b, t, d] -> [num_chunks, head_chunk_size, b, t, d]
1152+ q_h = query .transpose (2 , 0 , 1 , 3 ).reshape (num_chunks , head_chunk_size , batch , q_len , dim )
1153+ k_h = key .transpose (2 , 0 , 1 , 3 ).reshape (num_chunks , head_chunk_size , batch , key .shape [1 ], dim )
1154+
1155+ def scan_body_heads (carry , xs ):
1156+ q_c = xs ["q" ] # [h_chunk, b, t, d]
1157+ k_c = xs ["k" ] # [h_chunk, b, s, d]
1158+
1159+ # Directly use the chunked shapes in einsum to avoid transposes inside the loop
1160+ attn_chunk = jnp .einsum (
1161+ "hbtd, hbsd -> bhts" ,
1162+ q_c ,
1163+ k_c ,
1164+ precision = self .config .matmul_precision ,
1165+ )
1166+
1167+ if sparse_loss :
1168+ attn_chunk = attn_chunk + indexer_mask [:, None , :, :]
1169+ elif attention_mask is not None :
1170+ attn_chunk = attn_chunk + attention_mask [:, None , :, :]
1171+
1172+ probs_chunk = jax .nn .softmax (attn_chunk .astype (jnp .float32 ), axis = - 1 )
1173+ probs_chunk_sum = jnp .sum (probs_chunk , axis = 1 ) # [b, t, s]
1174+
1175+ return carry + probs_chunk_sum , None
1176+
1177+ init_probs = jnp .zeros ((batch , q_len , key .shape [1 ]), dtype = jnp .float32 )
1178+ attention_probs , _ = jax .lax .scan (scan_body_heads , init_probs , {"q" : q_h , "k" : k_h })
1179+
1180+ else :
1181+ # Native implementation (default) if chunking is disabled
1182+ attention_scores = jnp .einsum (
1183+ "bthd, bshd -> bhts" ,
1184+ query ,
1185+ key ,
1186+ precision = self .config .matmul_precision ,
1187+ )
1188+ if sparse_loss :
1189+ attention_scores = attention_scores + indexer_mask [:, None , :, :]
1190+ elif attention_mask is not None :
1191+ attention_scores = attention_scores + attention_mask [:, None , :, :]
1192+ attention_probs = jnp .sum (jax .nn .softmax (attention_scores .astype (jnp .float32 ), axis = - 1 ), axis = 1 )
1193+ attention_probs = jax .lax .optimization_barrier (attention_probs )
11081194 # L1 normalize aggregated target distribution
11091195 attention_probs = attention_probs / (jnp .sum (attention_probs , axis = - 1 , keepdims = True ) + EPS )
11101196
0 commit comments