@@ -160,5 +160,84 @@ def ring_attn(ring_kernel, q, k, v, segment_ids):
160160 self ._assert_allclose (dv , dv_ref , rtol = 1e-2 , atol = 1e-2 )
161161
162162
163+ class RingAttentionHeadsPerTileTest (test_utils .SplashAttentionTestCase ):
164+ """`heads_per_tile` (multi-head-per-tile) invariance for ring attention.
165+
166+ heads_per_tile is a pure tiling/scheduling choice for the forward kernel: for a
167+ fixed input, running with heads_per_tile=N must produce the same output as
168+ heads_per_tile=1. This guards against the block-index class of bug (a wrong
169+ head-tile mapping compiles and runs but silently returns garbage).
170+ """
171+
172+ def setUp (self ):
173+ if jax .default_backend () != "tpu" :
174+ self .skipTest ("Multi-head-per-tile ring attention runs on TPU." )
175+ super ().setUp ()
176+
177+ @parameterized .product (
178+ heads_per_tile = [2 , 4 ],
179+ head_dim = [128 ],
180+ dtype = [jnp .bfloat16 ],
181+ )
182+ def test_heads_per_tile_matches_single_head (self , heads_per_tile , head_dim , dtype ):
183+ ring_size = 2
184+ num_heads = 4 # MHA (num_q_heads == num_kv_heads); divisible by heads_per_tile.
185+ if len (jax .devices ()) < ring_size :
186+ self .skipTest (f"This test requires { ring_size } devices, but has only { len (jax .devices ())} ." )
187+
188+ ring_axis = "ring"
189+ devices = np .asarray (jax .devices ()[:ring_size ]).reshape (1 , ring_size )
190+ mesh = jax .sharding .Mesh (devices , ("heads" , ring_axis ))
191+ seq_len = 1024 * ring_size
192+
193+ k1 , k2 , k3 = random .split (random .key (0 ), 3 )
194+ scale = head_dim ** - 0.5
195+ q = random .normal (k1 , (num_heads , seq_len , head_dim ), dtype = dtype ) * scale
196+ k = random .normal (k2 , (num_heads , seq_len , head_dim ), dtype = dtype ) * scale
197+ v = random .normal (k3 , (num_heads , seq_len , head_dim ), dtype = dtype ) * scale
198+
199+ # The mhpt fast path supports full MHA + static FullMask + HEAD_DIM_MINOR only.
200+ mask = mask_lib .FullMask (_shape = (seq_len , seq_len ))
201+ q_spec = P (None , ring_axis , None )
202+ kv_spec = q_spec
203+
204+ def run (hpt ):
205+ config = splash .SplashConfig .get_default ()
206+ config = dataclasses .replace (
207+ config ,
208+ use_base2_exp = False ,
209+ fuse_reciprocal = True ,
210+ heads_per_tile = hpt ,
211+ )
212+ ring_kernel = ring_attention_kernel .make_ring_attention (
213+ mask ,
214+ is_mqa = False ,
215+ ring_axis = ring_axis ,
216+ config = config ,
217+ save_residuals = False ,
218+ q_seq_shards = ring_size ,
219+ kv_seq_shards = ring_size ,
220+ )
221+ kernel_spec = ring_kernel .manual_sharding_spec ()
222+
223+ @partial (
224+ jax .shard_map ,
225+ mesh = mesh ,
226+ in_specs = (kernel_spec , q_spec , kv_spec , kv_spec , None ),
227+ out_specs = q_spec ,
228+ check_vma = False ,
229+ )
230+ def ring_attn (ring_kernel , q , k , v , segment_ids ):
231+ return ring_kernel (q , k , v , segment_ids )
232+
233+ return ring_attn (ring_kernel , q , k , v , None )
234+
235+ out_ref = run (1 ) # baseline: single head per tile (flash_attention_kernel)
236+ out_mhpt = run (heads_per_tile ) # multi-head-per-tile (flash_attention_kernel_mhpt)
237+
238+ # Pure tiling => numerically equivalent to the single-head-per-tile baseline.
239+ self ._assert_allclose (out_mhpt , out_ref , rtol = 5e-3 , atol = 5e-3 )
240+
241+
163242if __name__ == "__main__" :
164243 absltest .main ()
0 commit comments