@@ -361,18 +361,57 @@ 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+ if head_chunk_size > 0 :
379+ if head_chunk_size > h or h % head_chunk_size != 0 :
380+ raise ValueError (
381+ f"qk_head_chunk_size ({ head_chunk_size } ) must be <= number of heads ({ h } ) "
382+ f"and divide it evenly."
383+ )
384+ num_chunks = h // head_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 , head_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 , head_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 = jnp .einsum ("hbtd, bsd -> btsh" , q_c , k , precision = self .config .matmul_precision )
396+ logits = jax .nn .relu (logits )
397+
398+ score_chunk = jnp .einsum (
399+ "btsh, hbt -> bts" ,
400+ logits ,
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+ indexer_score , _ = jax .lax .scan (jax .checkpoint (scan_body_indexer ), init_score , {"q" : q_h , "w" : w_h })
408+ indexer_score = indexer_score .astype (q .dtype )
409+
410+ else :
411+ # Aggregate head-wise logits: logits @ weights natively
412+ logits = jnp .einsum ("bthd, bsd -> btsh" , q , k , precision = self .config .matmul_precision )
413+ logits = jax .nn .relu (logits )
414+ indexer_score = jnp .einsum ("btsh, bth -> bts" , logits , weights , precision = self .config .matmul_precision )
376415
377416 internal_padding_mask = None
378417 if cached_s is not None :
@@ -1086,25 +1125,69 @@ def calculate_indexer_loss(
10861125 query = jax .lax .stop_gradient (query )
10871126 key = jax .lax .stop_gradient (key )
10881127
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-
1128+ # Ensure indexer_score updates identically in all branches
10921129 if sparse_loss :
1093- # indexer_mask is already pre-filtered with the attention_mask if any
1094- attention_scores = attention_scores + indexer_mask [:, None , :, :]
10951130 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 )
11021131 indexer_probs = jax .nn .softmax (indexer_score .astype (jnp .float32 ), axis = - 1 )
11031132
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 )
1133+ batch , q_len , heads , dim = query .shape
1134+
1135+ # Chunk across the 'heads' dimension manually using jax.lax.scan
1136+ # Control the HBM footprint of QK tensor: [batch, q_len, s_len, heads]
1137+ # If set to 0, it falls back to native implementation.
1138+ head_chunk_size = getattr (self .config , "qk_head_chunk_size" , 0 )
1139+ if head_chunk_size > 0 :
1140+ if head_chunk_size > heads or heads % head_chunk_size != 0 :
1141+ raise ValueError (
1142+ f"qk_head_chunk_size ({ head_chunk_size } ) must be <= number of heads ({ heads } ) "
1143+ f"and divide it evenly."
1144+ )
1145+ num_chunks = heads // head_chunk_size
1146+
1147+ # Transpose and reshape to put chunk dimension first for jax.lax.scan
1148+ # query: [b, t, h, d] -> [h, b, t, d] -> [num_chunks, head_chunk_size, b, t, d]
1149+ q_h = query .transpose (2 , 0 , 1 , 3 ).reshape (num_chunks , head_chunk_size , batch , q_len , dim )
1150+ k_h = key .transpose (2 , 0 , 1 , 3 ).reshape (num_chunks , head_chunk_size , batch , key .shape [1 ], dim )
1151+
1152+ def scan_body_heads (carry , xs ):
1153+ q_c = xs ["q" ] # [h_chunk, b, t, d]
1154+ k_c = xs ["k" ] # [h_chunk, b, s, d]
1155+
1156+ # Directly use the chunked shapes in einsum to avoid transposes inside the loop
1157+ attn_chunk = jnp .einsum (
1158+ "hbtd, hbsd -> bhts" ,
1159+ q_c ,
1160+ k_c ,
1161+ precision = self .config .matmul_precision ,
1162+ )
1163+
1164+ if sparse_loss :
1165+ attn_chunk = attn_chunk + indexer_mask [:, None , :, :]
1166+ elif attention_mask is not None :
1167+ attn_chunk = attn_chunk + attention_mask [:, None , :, :]
1168+
1169+ probs_chunk = jax .nn .softmax (attn_chunk .astype (jnp .float32 ), axis = - 1 )
1170+ probs_chunk_sum = jnp .sum (probs_chunk , axis = 1 ) # [b, t, s]
1171+
1172+ return carry + probs_chunk_sum , None
1173+
1174+ init_probs = jnp .zeros ((batch , q_len , key .shape [1 ]), dtype = jnp .float32 )
1175+ attention_probs , _ = jax .lax .scan (jax .checkpoint (scan_body_heads ), init_probs , {"q" : q_h , "k" : k_h })
1176+
1177+ else :
1178+ # Native implementation (default) if chunking is disabled
1179+ attention_scores = jnp .einsum (
1180+ "bthd, bshd -> bhts" ,
1181+ query ,
1182+ key ,
1183+ precision = self .config .matmul_precision ,
1184+ )
1185+ if sparse_loss :
1186+ attention_scores = attention_scores + indexer_mask [:, None , :, :]
1187+ elif attention_mask is not None :
1188+ attention_scores = attention_scores + attention_mask [:, None , :, :]
1189+ attention_probs = jnp .sum (jax .nn .softmax (attention_scores .astype (jnp .float32 ), axis = - 1 ), axis = 1 )
1190+ attention_probs = jax .lax .optimization_barrier (attention_probs )
11081191 # L1 normalize aggregated target distribution
11091192 attention_probs = attention_probs / (jnp .sum (attention_probs , axis = - 1 , keepdims = True ) + EPS )
11101193
0 commit comments