@@ -362,17 +362,53 @@ def __call__(
362362 return None , None , None
363363
364364 # 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 )
369- # Compute head weights: project from input, [b, t, embed_dim] -> [b, t, h]
370- weights = self .weights_proj (inputs_q )
371- # Weights scaling affect indexer_score, but does not affect topk_indices. Keep scaling for numerical stability.
372- # https://github.com/deepseek-ai/DeepSeek-V3.2-Exp/blob/87e509a2e5a100d221c97df52c6e8be7835f0057/inference/model.py#L478-L480
373- 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]
365+ # When qk_head_chunk_size > 0, compute Index Scores by chunking the 'heads' dimension to reduce memory
366+ # The naive evaluation materializes [b, t, s, h].
367+ # We use jax.lax.scan to compute the score iteratively over head chunks.
368+ b , t , h , d = q .shape
369+ # Control the HBM footprint of QK tensor: [batch, q_len, s_len, heads]
370+ # If set to 0 (defaults), it falls back to native materialization.
371+ head_chunk_size = getattr (self .config , "qk_head_chunk_size" , 0 )
372+ if head_chunk_size > 0 :
373+ if head_chunk_size > h or h % head_chunk_size != 0 :
374+ raise ValueError (
375+ f"qk_head_chunk_size ({ head_chunk_size } ) must be <= number of heads ({ h } ) "
376+ f"and divide it evenly."
377+ )
378+ num_chunks = h // head_chunk_size
379+ # q: [b, t, h, d] -> [h, b, t, d] -> [num_chunks, head_chunk_size, b, t, d]
380+ q_h = q .transpose (2 , 0 , 1 , 3 ).reshape (num_chunks , head_chunk_size , b , t , d )
381+ # weights: [b, t, h] -> [h, b, t] -> [num_chunks, head_chunk_size, b, t]
382+ w_h = weights .transpose (2 , 0 , 1 ).reshape (num_chunks , head_chunk_size , b , t )
383+
384+ def scan_body_indexer (carry , xs ):
385+ q_c = xs ["q" ] # [h_chunk, b, t, d]
386+ w_c = xs ["w" ] # [h_chunk, b, t]
387+
388+ # Transpose back for einsum:
389+ q_c = q_c .transpose (1 , 2 , 0 , 3 ) # [b, t, h_chunk, d]
390+ w_c = w_c .transpose (1 , 2 , 0 ) # [b, t, h_chunk]
391+
392+ logits = jnp .einsum ("bthd, bsd -> btsh" , q_c , k , precision = self .config .matmul_precision )
393+ logits = jax .nn .relu (logits )
394+
395+ score_chunk = jnp .einsum (
396+ "btsh, bth -> bts" ,
397+ logits ,
398+ w_c ,
399+ precision = self .config .matmul_precision ,
400+ )
401+ return carry + score_chunk .astype (jnp .float32 ), None
402+
403+ init_score = jnp .zeros ((b , t , k .shape [1 ]), dtype = jnp .float32 )
404+ indexer_score , _ = jax .lax .scan (jax .checkpoint (scan_body_indexer ), init_score , {"q" : q_h , "w" : w_h })
405+ indexer_score = indexer_score .astype (q .dtype )
406+
407+ else :
408+ # Aggregate head-wise logits: logits @ weights natively
409+ logits = jnp .einsum ("bthd, bsd -> btsh" , q , k , precision = self .config .matmul_precision )
410+ logits = jax .nn .relu (logits )
411+ indexer_score = jnp .einsum ("btsh, bth -> bts" , logits , weights , precision = self .config .matmul_precision )
376412
377413 internal_padding_mask = None
378414 if cached_s is not None :
@@ -1086,25 +1122,72 @@ def calculate_indexer_loss(
10861122 query = jax .lax .stop_gradient (query )
10871123 key = jax .lax .stop_gradient (key )
10881124
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-
1125+ # Ensure indexer_score updates identically in all branches
10921126 if sparse_loss :
1093- # indexer_mask is already pre-filtered with the attention_mask if any
1094- attention_scores = attention_scores + indexer_mask [:, None , :, :]
10951127 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 )
11021128 indexer_probs = jax .nn .softmax (indexer_score .astype (jnp .float32 ), axis = - 1 )
11031129
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 )
1130+ batch , q_len , heads , dim = query .shape
1131+
1132+ # Chunk across the 'heads' dimension manually using jax.lax.scan
1133+ # Control the HBM footprint of QK tensor: [batch, q_len, s_len, heads]
1134+ # If set to 0, it falls back to native implementation.
1135+ head_chunk_size = getattr (self .config , "qk_head_chunk_size" , 0 )
1136+ if head_chunk_size > 0 :
1137+ if head_chunk_size > heads or heads % head_chunk_size != 0 :
1138+ raise ValueError (
1139+ f"qk_head_chunk_size ({ head_chunk_size } ) must be <= number of heads ({ heads } ) "
1140+ f"and divide it evenly."
1141+ )
1142+ num_chunks = heads // head_chunk_size
1143+
1144+ # Transpose and reshape to put chunk dimension first for jax.lax.scan
1145+ # query: [b, t, h, d] -> [h, b, t, d] -> [num_chunks, head_chunk_size, b, t, d]
1146+ q_h = query .transpose (2 , 0 , 1 , 3 ).reshape (num_chunks , head_chunk_size , batch , q_len , dim )
1147+ k_h = key .transpose (2 , 0 , 1 , 3 ).reshape (num_chunks , head_chunk_size , batch , key .shape [1 ], dim )
1148+
1149+ def scan_body_heads (carry , xs ):
1150+ q_c = xs ["q" ] # [h_chunk, b, t, d]
1151+ k_c = xs ["k" ] # [h_chunk, b, s, d]
1152+
1153+ # Transpose back: [h_chunk, b, t, d] -> [b, t, h_chunk, d]
1154+ q_c = q_c .transpose (1 , 2 , 0 , 3 )
1155+ k_c = k_c .transpose (1 , 2 , 0 , 3 )
1156+
1157+ attn_chunk = jnp .einsum (
1158+ "bthd, bshd -> 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