@@ -51,7 +51,21 @@ def __init__(self, block_q: int, block_kv: int, block_kv_compute: int | None = N
5151 self .block_kv_compute = block_kv_compute if block_kv_compute is not None else block_kv
5252
5353
54+ # Fixed-m softmax-bound constants. Instead of tracking the online-softmax
55+ # running max per KV block, eligible heads subtract a precomputed per-query
56+ # upper bound on the logits (Cauchy-Schwarz: max_j q_i.k_j <= ||q_i|| *
57+ # max_j||k_j||). _FIXED_M_RECENTER (C) shifts the exp2 exponents up so the
58+ # largest surviving term stays above the f32 subnormal-flush floor 2^-126:
59+ # with k-smoothing the per-row max is >= 0, so the max term has exponent
60+ # >= -ceil(bound) + C, which stays > -126 while ceil(bound) <=
61+ # _FIXED_M_SAFE_BOUND (= C + 126 - 1 of margin). Heads whose worst-case bound
62+ # exceeds the gate fall back to online softmax (the "sink" heads).
63+ _FIXED_M_RECENTER = 88.0
64+ _FIXED_M_SAFE_BOUND = 213.0
65+
66+
5467def _flash_attention_kernel (
68+ mk_ref ,
5569 q_ref ,
5670 k_ref ,
5771 v_ref ,
@@ -73,34 +87,45 @@ def _flash_attention_kernel(
7387 kv_seq_len : int ,
7488 use_base2_exp : bool = True ,
7589 fuse_reciprocal : bool = True ,
90+ use_fixed_m : bool = False ,
7691):
7792 float32 = jnp .float32
7893 head_dim_v_repeats , rem = divmod (head_dim_v , NUM_SUBLANES )
7994 if rem != 0 :
8095 raise NotImplementedError (f"{ head_dim_v = } should be a multiple of { NUM_SUBLANES } " )
8196
82- _ , _ , j = pl .program_id (0 ), pl .program_id (1 ), pl .program_id (2 )
97+ h , _ , j = pl .program_id (0 ), pl .program_id (1 ), pl .program_id (2 )
8398 exp = jnp .exp2 if use_base2_exp else jnp .exp
99+ sv_dims = (((0 ,), (0 ,)), ((), ()))
100+
101+ # Per-head dispatch: heads inside the no-flush window run fixed-m, the rest
102+ # keep online softmax. Branch once per head (body level), never per step.
103+ is_fixed = (mk_ref [1 , h ] > 0.5 ) if use_fixed_m else False
84104
85105 @pl .when (j == 0 )
86106 def init ():
87107 o_scratch_ref [...] = jnp .zeros_like (o_scratch_ref )
88- m_scratch_ref [...] = jnp .full_like (m_scratch_ref , mask_value )
89108 l_scratch_ref [...] = jnp .zeros_like (l_scratch_ref )
109+ if use_fixed_m :
90110
91- def compute_body (kv_compute_index , _ ):
92- m_prev , l_prev = m_scratch_ref [...], l_scratch_ref [...]
93- q = q_ref [...]
94- o_prev = o_scratch_ref [:]
111+ @pl .when (is_fixed )
112+ def _init_fixed ():
113+ # Per-query Cauchy-Schwarz bound m_i = ceil(||q_i|| * max_j||k_j||) - C.
114+ qf = q_ref [...].astype (float32 )
115+ qn = jnp .sqrt ((qf * qf ).sum (axis = 1 ))[None , :] # (1, bq) per-query norm
116+ bound = qn * mk_ref [0 , h ]
117+ m_fixed = jnp .ceil (bound ) - _FIXED_M_RECENTER
118+ m_scratch_ref [...] = jnp .broadcast_to (m_fixed , m_scratch_ref .shape )
95119
96- base_offset = kv_compute_index * bkv_compute
97- slice_k = pl . ds ( base_offset , bkv_compute )
98- k_chunk = k_ref [ slice_k , :]
120+ @ pl . when ( jnp . logical_not ( is_fixed ))
121+ def _init_online ():
122+ m_scratch_ref [...] = jnp . full_like ( m_scratch_ref , mask_value )
99123
100- qk = lax . dot_general ( k_chunk , q , NT_DIM_NUMBERS , preferred_element_type = float32 )
101- v_chunk = v_ref [ slice_k , :]
124+ else :
125+ m_scratch_ref [...] = jnp . full_like ( m_scratch_ref , mask_value )
102126
103- # --- V1 VPU REGISTER TILING ---
127+ def _online_inner (qk , v_chunk , m_prev , l_prev , o_prev ):
128+ # Standard online-softmax tiling over the VPU register block.
104129 step = bkv_compute_in
105130 for i in range (0 , qk .shape [0 ], step ):
106131 qk_slice = qk [i : i + step ]
@@ -113,84 +138,99 @@ def compute_body(kv_compute_index, _):
113138 alpha = exp (m_prev - m_next )
114139 l_next = l_curr + alpha * l_prev
115140
116- sv_dims = (((0 ,), (0 ,)), ((), ()))
117141 o_curr = lax .dot_general (
118142 v_chunk [i : i + step ],
119143 s_curr .astype (q_ref .dtype ),
120144 sv_dims ,
121145 preferred_element_type = float32 ,
122146 )
123-
124- alpha_o = alpha [0 :1 , ...]
125- o_prev = alpha_o * o_prev + o_curr
126-
147+ o_prev = alpha [0 :1 , ...] * o_prev + o_curr
127148 m_prev , l_prev = m_next , l_next
128- # --- END V1 TILING ---
129-
130- m_scratch_ref [...], l_scratch_ref [...] = m_prev , l_prev
131- o_scratch_ref [:] = o_prev
149+ return m_prev , l_prev , o_prev
132150
133- def last_compute_body (kv_compute_index ):
134- m_prev , l_prev = m_scratch_ref [...], l_scratch_ref [...]
135- q = q_ref [...]
136- o_prev = o_scratch_ref [:]
137-
138- slice_k_len = kv_seq_len % bkv_compute
139- slice_k = pl .ds (kv_compute_index * bkv_compute , slice_k_len )
140- k_chunk = k_ref [slice_k , :]
141-
142- qk = lax .dot_general (k_chunk , q , NT_DIM_NUMBERS , preferred_element_type = float32 )
143- v_chunk = v_ref [slice_k , :]
144-
145- # --- V1 VPU REGISTER TILING ---
151+ def _fixed_inner (qk , v_chunk , m_fix , l_prev , o_prev ):
152+ # Fixed-m fast path: m is constant, so no reduce-max and no alpha rescale.
146153 step = bkv_compute_in
147154 for i in range (0 , qk .shape [0 ], step ):
148155 qk_slice = qk [i : i + step ]
149156
150- m_curr = qk_slice .max (axis = 0 )[None , :]
151- m_next = jnp .maximum (m_prev , m_curr )
152- s_curr = exp (qk_slice - m_next [0 :1 ])
157+ s_curr = exp (qk_slice - m_fix [0 :1 ])
153158 l_curr = s_curr .sum (axis = 0 , keepdims = True )
154159
155- alpha = exp (m_prev - m_next )
156- l_next = l_curr + alpha * l_prev
157-
158- sv_dims = (((0 ,), (0 ,)), ((), ()))
159160 o_curr = lax .dot_general (
160161 v_chunk [i : i + step ],
161162 s_curr .astype (q_ref .dtype ),
162163 sv_dims ,
163164 preferred_element_type = float32 ,
164165 )
166+ o_prev = o_prev + o_curr
167+ l_prev = l_prev + l_curr
168+ return l_prev , o_prev
165169
166- alpha_o = alpha [0 :1 , ...]
167- o_prev = alpha_o * o_prev + o_curr
170+ def compute_body_online (kv_compute_index , _ ):
171+ q = q_ref [...]
172+ base_offset = kv_compute_index * bkv_compute
173+ slice_k = pl .ds (base_offset , bkv_compute )
174+ qk = lax .dot_general (k_ref [slice_k , :], q , NT_DIM_NUMBERS , preferred_element_type = float32 )
175+ v_chunk = v_ref [slice_k , :]
176+ m_prev , l_prev , o_prev = _online_inner (qk , v_chunk , m_scratch_ref [...], l_scratch_ref [...], o_scratch_ref [:])
177+ m_scratch_ref [...], l_scratch_ref [...] = m_prev , l_prev
178+ o_scratch_ref [:] = o_prev
168179
169- m_prev , l_prev = m_next , l_next
170- # --- END V1 TILING ---
180+ def compute_body_fixed (kv_compute_index , _ ):
181+ q = q_ref [...]
182+ base_offset = kv_compute_index * bkv_compute
183+ slice_k = pl .ds (base_offset , bkv_compute )
184+ qk = lax .dot_general (k_ref [slice_k , :], q , NT_DIM_NUMBERS , preferred_element_type = float32 )
185+ v_chunk = v_ref [slice_k , :]
186+ l_prev , o_prev = _fixed_inner (qk , v_chunk , m_scratch_ref [...], l_scratch_ref [...], o_scratch_ref [:])
187+ l_scratch_ref [...] = l_prev
188+ o_scratch_ref [:] = o_prev
171189
190+ def last_compute_body_online (kv_compute_index ):
191+ q = q_ref [...]
192+ slice_k_len = kv_seq_len % bkv_compute
193+ slice_k = pl .ds (kv_compute_index * bkv_compute , slice_k_len )
194+ qk = lax .dot_general (k_ref [slice_k , :], q , NT_DIM_NUMBERS , preferred_element_type = float32 )
195+ v_chunk = v_ref [slice_k , :]
196+ m_prev , l_prev , o_prev = _online_inner (qk , v_chunk , m_scratch_ref [...], l_scratch_ref [...], o_scratch_ref [:])
172197 m_scratch_ref [...], l_scratch_ref [...] = m_prev , l_prev
173198 o_scratch_ref [:] = o_prev
174199
175200 assert bkv % bkv_compute == 0
176201
177- @pl .when (j != grid_width - 1 )
178- def body ():
179- lax .fori_loop (0 , (bkv // bkv_compute ), compute_body , None , unroll = True )
202+ if use_fixed_m :
203+
204+ @pl .when ((j != grid_width - 1 ) & is_fixed )
205+ def _body_fixed ():
206+ lax .fori_loop (0 , (bkv // bkv_compute ), compute_body_fixed , None , unroll = True )
207+
208+ @pl .when ((j != grid_width - 1 ) & jnp .logical_not (is_fixed ))
209+ def _body_online ():
210+ lax .fori_loop (0 , (bkv // bkv_compute ), compute_body_online , None , unroll = True )
211+
212+ else :
213+
214+ @pl .when (j != grid_width - 1 )
215+ def body ():
216+ lax .fori_loop (0 , (bkv // bkv_compute ), compute_body_online , None , unroll = True )
180217
218+ # The final KV block always runs online for every head. Fixed-m heads arrive
219+ # with m_scratch = ceil(bound) - C and o/l at that reference; the online
220+ # alpha = exp2(m_prev - m_next) rescale renormalizes them transparently.
181221 @pl .when (j == grid_width - 1 )
182222 def last_body ():
183223 if kv_seq_len % bkv == 0 :
184224 iter_num = bkv // bkv_compute
185- lax .fori_loop (0 , iter_num , compute_body , None , unroll = True )
225+ lax .fori_loop (0 , iter_num , compute_body_online , None , unroll = True )
186226 else :
187227 remain_kv_seq_len = kv_seq_len % bkv
188228 iter_num = (remain_kv_seq_len + bkv_compute - 1 ) // bkv_compute
189229 if remain_kv_seq_len % bkv_compute == 0 :
190- lax .fori_loop (0 , iter_num , compute_body , None , unroll = True )
230+ lax .fori_loop (0 , iter_num , compute_body_online , None , unroll = True )
191231 else :
192- lax .fori_loop (0 , iter_num - 1 , compute_body , None , unroll = True )
193- last_compute_body (iter_num - 1 )
232+ lax .fori_loop (0 , iter_num - 1 , compute_body_online , None , unroll = True )
233+ last_compute_body_online (iter_num - 1 )
194234
195235 @pl .when (j == grid_width - 1 )
196236 def end ():
@@ -373,9 +413,16 @@ def _splash_attention_forward(
373413 use_base2_exp : bool = True ,
374414 use_experimental_scheduler : bool = False ,
375415 vmem_limit_bytes : int | None = None ,
416+ use_fixed_m : bool = False ,
417+ mk : jax .Array | None = None ,
376418):
377419 num_q_heads , padded_q_seq_len , head_dim_qk = q .shape
378420 head_dim_v = v .shape [- 1 ]
421+ # Scalar-prefetch operand carrying per-head fixed-m data:
422+ # mk[0, h] = max_j||k_j|| (Cauchy-Schwarz factor), mk[1, h] = eligibility.
423+ # A dummy is supplied for online callers; the kernel ignores it.
424+ if mk is None :
425+ mk = jnp .zeros ((2 , num_q_heads ), jnp .float32 )
379426 bq , bkv = block_sizes .block_q , block_sizes .block_kv
380427 bkv_compute = block_sizes .block_kv_compute
381428 num_kv_heads = k .shape [0 ]
@@ -431,9 +478,10 @@ def v_index_map(h, i, j, *_):
431478 q_seq_len = actual_q_seq_len ,
432479 kv_seq_len = actual_kv_seq_len ,
433480 use_base2_exp = use_base2_exp ,
481+ use_fixed_m = use_fixed_m ,
434482 ),
435483 grid_spec = pltpu .PrefetchScalarGridSpec (
436- num_scalar_prefetch = 0 ,
484+ num_scalar_prefetch = 1 ,
437485 in_specs = in_specs ,
438486 out_specs = out_specs ,
439487 grid = grid ,
@@ -446,7 +494,7 @@ def v_index_map(h, i, j, *_):
446494 vmem_limit_bytes = vmem_limit_bytes ,
447495 ),
448496 out_shape = out_shapes ,
449- )(q , k , v )
497+ )(mk , q , k , v )
450498 return all_out [- 1 ]
451499
452500
@@ -461,6 +509,8 @@ def _splash_attention_forward_ring(
461509 use_base2_exp : bool = True ,
462510 use_experimental_scheduler : bool = False ,
463511 vmem_limit_bytes : int | None = None ,
512+ use_fixed_m : bool = False ,
513+ mk : jax .Array | None = None ,
464514):
465515 """Ring-specific forward path that returns pre-reciprocal fp32 accumulators.
466516
@@ -524,6 +574,13 @@ def v_index_map(h, i, j, *_):
524574 grid_height = (actual_q_seq_len + bq - 1 ) // bq
525575 grid = (num_q_heads , grid_height , grid_width )
526576
577+ # Scalar-prefetch operand carrying per-head fixed-m data (same convention as
578+ # `_splash_attention_forward`): mk[0, h] = max_j||k_j|| over ALL ring shards
579+ # (the caller all-reduces this over the ring axis), mk[1, h] = eligibility.
580+ # A dummy is supplied for online callers; the kernel ignores it.
581+ if mk is None :
582+ mk = jnp .zeros ((2 , num_q_heads ), jnp .float32 )
583+
527584 all_out = pl .pallas_call (
528585 functools .partial (
529586 _flash_attention_kernel ,
@@ -538,9 +595,10 @@ def v_index_map(h, i, j, *_):
538595 kv_seq_len = actual_kv_seq_len ,
539596 use_base2_exp = use_base2_exp ,
540597 fuse_reciprocal = False ,
598+ use_fixed_m = use_fixed_m ,
541599 ),
542600 grid_spec = pltpu .PrefetchScalarGridSpec (
543- num_scalar_prefetch = 0 ,
601+ num_scalar_prefetch = 1 ,
544602 in_specs = in_specs ,
545603 out_specs = out_specs ,
546604 grid = grid ,
@@ -553,7 +611,7 @@ def v_index_map(h, i, j, *_):
553611 vmem_limit_bytes = vmem_limit_bytes ,
554612 ),
555613 out_shape = out_shapes ,
556- )(q , k , v )
614+ )(mk , q , k , v )
557615 out = jnp .swapaxes (all_out [3 ], 1 , 2 ) # (h, head_dim_v, s) -> (h, s, head_dim_v)
558616 l = all_out [4 ][:, 0 , :] # (h, s)
559617 m = all_out [5 ][:, 0 , :] # (h, s)
@@ -660,9 +718,12 @@ def make_splash_mha(
660718 use_base2_exp : bool = True ,
661719 use_experimental_scheduler : bool = False ,
662720 vmem_limit_bytes : int | None = None ,
721+ use_fixed_m : bool = False ,
663722):
664- def _splash_attention (q , k , v ):
723+ def _splash_attention (q , k , v , mk = None ):
665724 if heads_per_tile > 1 :
725+ if use_fixed_m :
726+ raise NotImplementedError ("fixed-m is not supported with heads_per_tile > 1" )
666727 return _splash_attention_forward_mhpt (
667728 q ,
668729 k ,
@@ -687,6 +748,8 @@ def _splash_attention(q, k, v):
687748 use_base2_exp = use_base2_exp ,
688749 use_experimental_scheduler = use_experimental_scheduler ,
689750 vmem_limit_bytes = vmem_limit_bytes ,
751+ use_fixed_m = use_fixed_m ,
752+ mk = mk ,
690753 )
691754
692755 return _splash_attention
0 commit comments