@@ -59,6 +59,8 @@ def _flash_attention_kernel(
5959 l_scratch_ref ,
6060 o_scratch_ref ,
6161 o_ref ,
62+ l_ring_ref = None ,
63+ m_ring_ref = None ,
6264 * ,
6365 mask_value : float ,
6466 grid_width : int ,
@@ -70,6 +72,7 @@ def _flash_attention_kernel(
7072 q_seq_len : int ,
7173 kv_seq_len : int ,
7274 use_base2_exp : bool = True ,
75+ fuse_reciprocal : bool = True ,
7376):
7477 float32 = jnp .float32
7578 head_dim_v_repeats , rem = divmod (head_dim_v , NUM_SUBLANES )
@@ -192,8 +195,18 @@ def last_body():
192195 @pl .when (j == grid_width - 1 )
193196 def end ():
194197 l = l_scratch_ref [...]
195- l_inv = jnp .tile (1.0 / l , (head_dim_v_repeats , 1 ))
196- o_ref [...] = (o_scratch_ref [...] * l_inv ).astype (o_ref .dtype )
198+ if fuse_reciprocal :
199+ l_inv = jnp .tile (1.0 / l , (head_dim_v_repeats , 1 ))
200+ o_ref [...] = (o_scratch_ref [...] * l_inv ).astype (o_ref .dtype )
201+ else :
202+ # Ring path: emit the un-normalized numerator plus the running softmax
203+ # stats (max logit `m` and linear denominator `l`) so the outer ring loop
204+ # can merge shard contributions and normalize only once at the very end.
205+ o_ref [...] = o_scratch_ref [...].astype (o_ref .dtype )
206+ if l_ring_ref is not None :
207+ l_ring_ref [...] = l .astype (l_ring_ref .dtype )
208+ if m_ring_ref is not None :
209+ m_ring_ref [...] = m_scratch_ref [...].astype (m_ring_ref .dtype )
197210
198211
199212def _flash_attention_kernel_mhpt (
@@ -437,6 +450,116 @@ def v_index_map(h, i, j, *_):
437450 return all_out [- 1 ]
438451
439452
453+ def _splash_attention_forward_ring (
454+ q : jax .Array ,
455+ k : jax .Array ,
456+ v : jax .Array ,
457+ block_sizes : _BlockSizes ,
458+ bkv_compute_in : int ,
459+ q_seq_len : int | None = None ,
460+ kv_seq_len : int | None = None ,
461+ use_base2_exp : bool = True ,
462+ use_experimental_scheduler : bool = False ,
463+ vmem_limit_bytes : int | None = None ,
464+ ):
465+ """Ring-specific forward path that returns pre-reciprocal fp32 accumulators.
466+
467+ Mirrors `_splash_attention_forward`, but instead of normalizing the output by
468+ the softmax denominator inside the kernel, it returns the un-normalized
469+ numerator (`out`) together with the per-row max logit (`m`) and linear softmax
470+ denominator (`l`). The outer ring loop merges these shard contributions and
471+ normalizes only once at the very end (see
472+ `ring_attention_kernel._custom_ring_attention_forward`).
473+
474+ Returns:
475+ A tuple `(out, m, l)` where
476+ - `out` has shape `(num_q_heads, q_seq_len, head_dim_v)` (fp32, un-normalized),
477+ - `m` and `l` have shape `(num_q_heads, q_seq_len)` (fp32).
478+ """
479+ num_q_heads , padded_q_seq_len , head_dim_qk = q .shape
480+ head_dim_v = v .shape [- 1 ]
481+ bq , bkv = block_sizes .block_q , block_sizes .block_kv
482+ bkv_compute = block_sizes .block_kv_compute
483+ num_kv_heads = k .shape [0 ]
484+ padded_kv_seq_len = k .shape [1 ]
485+
486+ actual_q_seq_len = q_seq_len if q_seq_len is not None else padded_q_seq_len
487+ actual_kv_seq_len = kv_seq_len if kv_seq_len is not None else padded_kv_seq_len
488+ q_heads_per_kv_head = num_q_heads // num_kv_heads
489+
490+ def q_index_map (h , i , j , * _ ):
491+ return (h , i , 0 )
492+
493+ def out_index_map (h , i , j , * _ ):
494+ return h , 0 , i
495+
496+ def k_index_map (h , i , j , * _ ):
497+ return (h // q_heads_per_kv_head , j , 0 )
498+
499+ def v_index_map (h , i , j , * _ ):
500+ return (h // q_heads_per_kv_head , j , 0 )
501+
502+ in_specs = [
503+ pl .BlockSpec ((None , bq , head_dim_qk ), q_index_map ),
504+ pl .BlockSpec ((None , bkv , head_dim_qk ), k_index_map ),
505+ pl .BlockSpec ((None , bkv , head_dim_v ), v_index_map ),
506+ ]
507+ out_shapes = [
508+ jax .ShapeDtypeStruct ((NUM_SUBLANES , bq ), jnp .float32 ),
509+ jax .ShapeDtypeStruct ((NUM_SUBLANES , bq ), jnp .float32 ),
510+ jax .ShapeDtypeStruct ((head_dim_v , bq ), jnp .float32 ),
511+ jax .ShapeDtypeStruct ((num_q_heads , head_dim_v , actual_q_seq_len ), jnp .float32 ),
512+ jax .ShapeDtypeStruct ((num_q_heads , NUM_SUBLANES , actual_q_seq_len ), jnp .float32 ),
513+ jax .ShapeDtypeStruct ((num_q_heads , NUM_SUBLANES , actual_q_seq_len ), jnp .float32 ),
514+ ]
515+ out_specs = [
516+ pl .BlockSpec ((NUM_SUBLANES , bq ), lambda * _ : (0 , 0 )),
517+ pl .BlockSpec ((NUM_SUBLANES , bq ), lambda * _ : (0 , 0 )),
518+ pl .BlockSpec ((head_dim_v , bq ), lambda * _ : (0 , 0 )),
519+ pl .BlockSpec ((None , head_dim_v , bq ), out_index_map ),
520+ pl .BlockSpec ((None , NUM_SUBLANES , bq ), out_index_map ),
521+ pl .BlockSpec ((None , NUM_SUBLANES , bq ), out_index_map ),
522+ ]
523+ grid_width = (actual_kv_seq_len + bkv - 1 ) // bkv
524+ grid_height = (actual_q_seq_len + bq - 1 ) // bq
525+ grid = (num_q_heads , grid_height , grid_width )
526+
527+ all_out = pl .pallas_call (
528+ functools .partial (
529+ _flash_attention_kernel ,
530+ mask_value = DEFAULT_MASK_VALUE ,
531+ grid_width = grid_width ,
532+ bq = bq ,
533+ bkv = bkv ,
534+ bkv_compute = bkv_compute ,
535+ bkv_compute_in = bkv_compute_in ,
536+ head_dim_v = head_dim_v ,
537+ q_seq_len = actual_q_seq_len ,
538+ kv_seq_len = actual_kv_seq_len ,
539+ use_base2_exp = use_base2_exp ,
540+ fuse_reciprocal = False ,
541+ ),
542+ grid_spec = pltpu .PrefetchScalarGridSpec (
543+ num_scalar_prefetch = 0 ,
544+ in_specs = in_specs ,
545+ out_specs = out_specs ,
546+ grid = grid ,
547+ ),
548+ compiler_params = pltpu .CompilerParams (
549+ dimension_semantics = ("parallel" , "arbitrary" , "arbitrary" ),
550+ flags = {"XLA_TPU_FORCE_LP_LLO_SCHEDULER" : use_experimental_scheduler },
551+ disable_bounds_checks = True ,
552+ skip_device_barrier = True ,
553+ vmem_limit_bytes = vmem_limit_bytes ,
554+ ),
555+ out_shape = out_shapes ,
556+ )(q , k , v )
557+ out = jnp .swapaxes (all_out [3 ], 1 , 2 ) # (h, head_dim_v, s) -> (h, s, head_dim_v)
558+ l = all_out [4 ][:, 0 , :] # (h, s)
559+ m = all_out [5 ][:, 0 , :] # (h, s)
560+ return out , m , l
561+
562+
440563def _splash_attention_forward_mhpt (
441564 q : jax .Array ,
442565 k : jax .Array ,
0 commit comments