@@ -362,17 +362,55 @@ 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+ # Compute Index Scores safely by chunking the 'heads' dimension to prevent OOM
366+ # The naive evaluation materializes [b, t, s, h] which explodes HBM.
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] or [batch, heads, q_len, s_len]
370+ # If set to 0, 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+ # Upcast the chunk to fp32 directly inside the block so we precisely match MXU
396+ # accumulator numerical stability without bfloat16 degradation over the loop boundary
397+ score_chunk = jnp .einsum (
398+ "btsh, bth -> bts" ,
399+ logits ,
400+ w_c ,
401+ precision = self .config .matmul_precision ,
402+ )
403+ return carry + score_chunk .astype (jnp .float32 ), None
404+
405+ init_score = jnp .zeros ((b , t , k .shape [1 ]), dtype = jnp .float32 )
406+ indexer_score , _ = jax .lax .scan (jax .checkpoint (scan_body_indexer ), init_score , {"q" : q_h , "w" : w_h })
407+ indexer_score = indexer_score .astype (q .dtype )
408+
409+ else :
410+ # Aggregate head-wise logits: logits @ weights natively
411+ logits = jnp .einsum ("bthd, bsd -> btsh" , q , k , precision = self .config .matmul_precision )
412+ logits = jax .nn .relu (logits )
413+ indexer_score = jnp .einsum ("btsh, bth -> bts" , logits , weights , precision = self .config .matmul_precision )
376414
377415 internal_padding_mask = None
378416 if cached_s is not None :
@@ -1086,25 +1124,72 @@ def calculate_indexer_loss(
10861124 query = jax .lax .stop_gradient (query )
10871125 key = jax .lax .stop_gradient (key )
10881126
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-
1127+ # Ensure indexer_score updates identically in all branches
10921128 if sparse_loss :
1093- # indexer_mask is already pre-filtered with the attention_mask if any
1094- attention_scores = attention_scores + indexer_mask [:, None , :, :]
10951129 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 )
11021130 indexer_probs = jax .nn .softmax (indexer_score .astype (jnp .float32 ), axis = - 1 )
11031131
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 )
1132+ batch , q_len , heads , dim = query .shape
1133+
1134+ # Chunk across the 'heads' dimension manually using jax.lax.scan
1135+ # Control the HBM footprint of QK tensor: [batch, q_len, s_len, heads]
1136+ # If set to 0, it falls back to native implementation.
1137+ head_chunk_size = getattr (self .config , "qk_head_chunk_size" , 0 )
1138+ if head_chunk_size > 0 :
1139+ if head_chunk_size > heads or heads % head_chunk_size != 0 :
1140+ raise ValueError (
1141+ f"qk_head_chunk_size ({ head_chunk_size } ) must be <= number of heads ({ heads } ) "
1142+ f"and divide it evenly."
1143+ )
1144+ num_chunks = heads // head_chunk_size
1145+
1146+ # Transpose and reshape to put chunk dimension first for jax.lax.scan
1147+ # query: [b, t, h, d] -> [h, b, t, d] -> [num_chunks, head_chunk_size, b, t, d]
1148+ q_h = query .transpose (2 , 0 , 1 , 3 ).reshape (num_chunks , head_chunk_size , batch , q_len , dim )
1149+ k_h = key .transpose (2 , 0 , 1 , 3 ).reshape (num_chunks , head_chunk_size , batch , key .shape [1 ], dim )
1150+
1151+ def scan_body_heads (carry , xs ):
1152+ q_c = xs ["q" ] # [h_chunk, b, t, d]
1153+ k_c = xs ["k" ] # [h_chunk, b, s, d]
1154+
1155+ # Transpose back: [h_chunk, b, t, d] -> [b, t, h_chunk, d]
1156+ q_c = q_c .transpose (1 , 2 , 0 , 3 )
1157+ k_c = k_c .transpose (1 , 2 , 0 , 3 )
1158+
1159+ attn_chunk = jnp .einsum (
1160+ "bthd, bshd -> bhts" ,
1161+ q_c ,
1162+ k_c ,
1163+ precision = self .config .matmul_precision ,
1164+ )
1165+
1166+ if sparse_loss :
1167+ attn_chunk = attn_chunk + indexer_mask [:, None , :, :]
1168+ elif attention_mask is not None :
1169+ attn_chunk = attn_chunk + attention_mask [:, None , :, :]
1170+
1171+ probs_chunk = jax .nn .softmax (attn_chunk .astype (jnp .float32 ), axis = - 1 )
1172+ probs_chunk_sum = jnp .sum (probs_chunk , axis = 1 ) # [b, t, s]
1173+
1174+ return carry + probs_chunk_sum , None
1175+
1176+ init_probs = jnp .zeros ((batch , q_len , key .shape [1 ]), dtype = jnp .float32 )
1177+ attention_probs , _ = jax .lax .scan (jax .checkpoint (scan_body_heads ), init_probs , {"q" : q_h , "k" : k_h })
1178+
1179+ else :
1180+ # Natively evaluate if chunking is disabled or head sizing does not allow clean chunks
1181+ attention_scores = jnp .einsum (
1182+ "bthd, bshd -> bhts" ,
1183+ query ,
1184+ key ,
1185+ precision = self .config .matmul_precision ,
1186+ )
1187+ if sparse_loss :
1188+ attention_scores = attention_scores + indexer_mask [:, None , :, :]
1189+ elif attention_mask is not None :
1190+ attention_scores = attention_scores + attention_mask [:, None , :, :]
1191+ attention_probs = jnp .sum (jax .nn .softmax (attention_scores .astype (jnp .float32 ), axis = - 1 ), axis = 1 )
1192+ attention_probs = jax .lax .optimization_barrier (attention_probs )
11081193 # L1 normalize aggregated target distribution
11091194 attention_probs = attention_probs / (jnp .sum (attention_probs , axis = - 1 , keepdims = True ) + EPS )
11101195
0 commit comments