11from __future__ import annotations
22
33import torch
4+ from torch .testing ._internal .common_utils import instantiate_parametrized_tests
5+ from torch .testing ._internal .common_utils import parametrize
46
57import helion
68from helion import exc
@@ -34,29 +36,104 @@ def jagged_dense_bmm(
3436 return out
3537
3638
39+ def _ref (off : torch .Tensor , j : torch .Tensor , d : torch .Tensor ) -> torch .Tensor :
40+ out = torch .empty ((j .shape [0 ], d .shape [2 ]), dtype = j .dtype , device = j .device )
41+ for g in range (d .shape [0 ]):
42+ s , e = int (off [g ]), int (off [g + 1 ])
43+ if e > s :
44+ out [s :e ] = j [s :e ] @ d [g ]
45+ return out
46+
47+
48+ def _inputs (offsets : list [int ], D : int , K : int , dtype : torch .dtype ):
49+ off = torch .tensor (offsets , dtype = torch .int32 , device = DEVICE )
50+ L , B = offsets [- 1 ], len (offsets ) - 1
51+ torch .manual_seed (0 )
52+ j = torch .randn ((L , D ), dtype = dtype , device = DEVICE )
53+ d = torch .randn ((B , D , K ), dtype = dtype , device = DEVICE )
54+ return off , j , d
55+
56+
57+ def _run (off , j , d , block_sizes ):
58+ return code_and_output (
59+ jagged_dense_bmm ,
60+ (off , j , d ),
61+ block_sizes = block_sizes ,
62+ pallas_loop_type = "emit_pipeline" ,
63+ )
64+
65+
66+ # bf16 with the kernel's f32-accumulating MXU matches a single torch matmul of
67+ # the same group bit-for-bit (single D-tile), so the default tolerances hold.
3768@onlyBackends (["pallas" ])
3869@skipUnlessPallas ("JAX/Pallas TPU not available" )
3970class TestPallasJaggedCarry (TestCase ):
40- # The legality gate is in place but the carry store is not, so a legal
41- # map-axis store reaches the unimplemented carry and raises "carry".
71+ # Simple cases that show the logic.
4272
43- def test_bmm_store_reaches_carry (self ) -> None :
44- # Identity store on the jagged row is a map axis: the gate accepts it, so
45- # it reaches the unimplemented carry.
46- off = torch .tensor ([0 , 13 , 25 ], dtype = torch .int32 , device = DEVICE )
47- j = torch .randn ((25 , 128 ), dtype = torch .bfloat16 , device = DEVICE )
48- d = torch .randn ((2 , 128 , 128 ), dtype = torch .bfloat16 , device = DEVICE )
49- with self .assertRaisesRegex (exc .InductorLoweringError , "carry" ):
50- code_and_output (
51- jagged_dense_bmm ,
52- (off , j , d ),
53- block_sizes = [16 , 128 , 128 ],
54- pallas_loop_type = "emit_pipeline" ,
55- )
73+ @parametrize ("dtype" , [torch .float32 , torch .bfloat16 ])
74+ def test_single_group (self , dtype : torch .dtype ) -> None :
75+ # One group: exercises the aligned-enclosing read and the tail mask,
76+ # with no carry between groups.
77+ off , j , d = _inputs ([0 , 20 ], 128 , 128 , dtype )
78+ _code , out = _run (off , j , d , [16 , 128 , 128 ])
79+ torch .testing .assert_close (out , _ref (off , j , d ))
80+
81+ @parametrize ("dtype" , [torch .float32 , torch .bfloat16 ])
82+ def test_aligned_groups_no_carry (self , dtype : torch .dtype ) -> None :
83+ # Sublane-aligned boundaries: no over-read and no shared row, so the
84+ # carry fold/save guards never fire.
85+ off , j , d = _inputs ([0 , 16 , 32 ], 128 , 128 , dtype )
86+ _code , out = _run (off , j , d , [16 , 128 , 128 ])
87+ torch .testing .assert_close (out , _ref (off , j , d ))
88+
89+ def test_carry_keeps_both_groups (self ) -> None :
90+ # Two groups [0, 3) and [3, 16) share the [0, 16) boundary row. With
91+ # identity weights out == jagged, so a clobbered carry would be obvious.
92+ off = torch .tensor ([0 , 3 , 16 ], dtype = torch .int32 , device = DEVICE )
93+ j = (
94+ torch .arange (1 , 17 , dtype = torch .bfloat16 , device = DEVICE )[:, None ]
95+ .expand (16 , 128 )
96+ .contiguous ()
97+ )
98+ eye = torch .eye (128 , dtype = torch .bfloat16 , device = DEVICE )
99+ _code , out = _run (off , j , torch .stack ([eye , eye ]), [16 , 128 , 128 ])
100+ torch .testing .assert_close (out , j )
101+
102+ # The carry across the full configuration matrix.
56103
57- def test_elementwise_store_reaches_carry (self ) -> None :
58- # A non-matmul map-axis store is accepted too, so it also reaches the
59- # unimplemented carry.
104+ @parametrize ("dtype" , [torch .float32 , torch .bfloat16 ])
105+ @parametrize (
106+ "offsets" ,
107+ [
108+ [0 , 13 , 25 ], # shared boundary row
109+ [0 , 20 , 50 ], # multi-row groups
110+ [0 , 3 , 7 , 16 ], # several tiny groups in one row (cumulative carry)
111+ ],
112+ )
113+ def test_bmm_block_eq_sublane (self , dtype : torch .dtype , offsets : list [int ]) -> None :
114+ off , j , d = _inputs (offsets , 128 , 128 , dtype )
115+ code , out = _run (off , j , d , [16 , 128 , 128 ])
116+ self .assertIn ("pltpu.emit_pipeline" , code )
117+ torch .testing .assert_close (out , _ref (off , j , d ))
118+
119+ @parametrize ("dtype" , [torch .float32 , torch .bfloat16 ])
120+ def test_bmm_block_gt_group (self , dtype : torch .dtype ) -> None :
121+ # Two groups (13 rows) are smaller than block_row=32, total L=200 >> block_row.
122+ off , j , d = _inputs ([0 , 13 , 100 , 113 , 200 ], 128 , 128 , dtype )
123+ _code , out = _run (off , j , d , [32 , 128 , 128 ])
124+ torch .testing .assert_close (out , _ref (off , j , d ))
125+
126+ @parametrize ("dtype" , [torch .float32 , torch .bfloat16 ])
127+ def test_bmm_multi_k_tile (self , dtype : torch .dtype ) -> None :
128+ # K=256 with block_col=128 gives two output-column tiles; the carry stacks
129+ # the per-column-tile boundary rows along its scratch row dim.
130+ off , j , d = _inputs ([0 , 17 , 40 , 71 ], 128 , 256 , dtype )
131+ _code , out = _run (off , j , d , [16 , 128 , 128 ])
132+ torch .testing .assert_close (out , _ref (off , j , d ))
133+
134+ def test_elementwise_map_axis (self ) -> None :
135+ # The non-matmul map-axis store from commit 2 now runs: out[st] = 2 *
136+ # jagged[st], carried across the shared boundary like the matmul case.
60137 @helion .kernel (backend = "pallas" )
61138 def jagged_scale (
62139 seq_offsets : torch .Tensor , jagged : torch .Tensor
@@ -74,13 +151,15 @@ def jagged_scale(
74151
75152 off = torch .tensor ([0 , 13 , 25 ], dtype = torch .int32 , device = DEVICE )
76153 j = torch .randn ((25 , 128 ), dtype = torch .bfloat16 , device = DEVICE )
77- with self .assertRaisesRegex (exc .InductorLoweringError , "carry" ):
78- code_and_output (
79- jagged_scale ,
80- (off , j ),
81- block_sizes = [16 , 128 ],
82- pallas_loop_type = "emit_pipeline" ,
83- )
154+ _code , out = code_and_output (
155+ jagged_scale ,
156+ (off , j ),
157+ block_sizes = [16 , 128 ],
158+ pallas_loop_type = "emit_pipeline" ,
159+ )
160+ torch .testing .assert_close (out , j * 2 )
161+
162+ # Rejection paths.
84163
85164 def test_jagged_reduction_over_row_f32 (self ) -> None :
86165 # Summing the jagged row away to a dense output reduces it (not a map
@@ -115,6 +194,45 @@ def jagged_row_sum(
115194 expected = torch .stack ([j [0 :13 ].sum (0 ), j [13 :25 ].sum (0 )])
116195 torch .testing .assert_close (out , expected , rtol = 2e-3 , atol = 2e-3 )
117196
197+ def test_block_not_multiple_of_sublane_raises (self ) -> None :
198+ # bf16 sublane S=16; block_row=8 is not a multiple, so the carry rejects it
199+ # loudly instead of clobbering the boundary with a plain store.
200+ off , j , d = _inputs ([0 , 13 , 25 ], 128 , 128 , torch .bfloat16 )
201+ with self .assertRaisesRegex (
202+ exc .InductorLoweringError , "block_row .* must be a multiple"
203+ ):
204+ _run (off , j , d , [8 , 128 , 128 ])
205+
206+ def test_multi_grid_group_rejected (self ) -> None :
207+ # The fold guard keys off program_id(0), so a second grid dimension is
208+ # refused rather than folding against the wrong program id.
209+ @helion .kernel (backend = "pallas" )
210+ def two_grid_bmm (
211+ seq_offsets : torch .Tensor , jagged : torch .Tensor , dense : torch .Tensor
212+ ) -> torch .Tensor :
213+ L , D = jagged .shape
214+ B , D , K = dense .shape
215+ out = torch .empty ((L , K ), dtype = jagged .dtype , device = jagged .device )
216+ for _b , g in hl .grid ([2 , B ]):
217+ s = seq_offsets [g ]
218+ e = seq_offsets [g + 1 ]
219+ for st in hl .tile (s , e ):
220+ for kt in hl .tile (0 , K ):
221+ acc = hl .zeros ([st , kt ], dtype = torch .float32 )
222+ for dt in hl .tile (0 , D ):
223+ acc = acc + torch .matmul (jagged [st , dt ], dense [g , dt , kt ])
224+ out [st , kt ] = acc
225+ return out
226+
227+ off , j , d = _inputs ([0 , 13 , 25 ], 128 , 128 , torch .bfloat16 )
228+ with self .assertRaisesRegex (exc .InductorLoweringError , "only grid dimension" ):
229+ code_and_output (
230+ two_grid_bmm ,
231+ (off , j , d ),
232+ block_sizes = [16 , 128 , 128 ],
233+ pallas_loop_type = "emit_pipeline" ,
234+ )
235+
118236 def test_non_jagged_emit_pipeline_unaffected (self ) -> None :
119237 # Static (compile-time) tile bounds never trip the gate: a plain
120238 # emit_pipeline matmul still lowers and runs correctly.
@@ -140,3 +258,6 @@ def static_matmul(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
140258 pallas_loop_type = "emit_pipeline" ,
141259 )
142260 torch .testing .assert_close (out , a @ b )
261+
262+
263+ instantiate_parametrized_tests (TestPallasJaggedCarry )
0 commit comments