Skip to content

Commit 0d630a6

Browse files
committed
[Pallas] Implement is_row_map_axis legality gate for jagged carry
stack-info: PR: pytorch#2718, branch: thcmbs/stack/2
1 parent 3806ec7 commit 0d630a6

2 files changed

Lines changed: 165 additions & 2 deletions

File tree

helion/_compiler/pallas/ordered_carry.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,29 @@ def is_row_map_axis(state: CodegenState, block_id: int) -> bool:
4848
never summed, scattered, or shifted; the over-read and carry are only
4949
correct in that case.
5050
"""
51-
# TODO(implement): the map-axis check above; for now reject every jagged tile.
52-
return False
51+
# TODO(tcombes): conservative. Scans how the row is indexed and only accepts
52+
# a straight store; exercised only on the bmm and elementwise forms, so
53+
# revisit for completeness (aliasing, multiple stores, broadcast/expand).
54+
from helion._compiler.device_ir import ReductionLoopGraphInfo
55+
from helion._compiler.pallas.plan_tiling import TilePattern
56+
from helion.language.memory_ops import store
57+
58+
has_straight_store = False
59+
for ginfo in state.codegen.codegen_graphs:
60+
if isinstance(ginfo, ReductionLoopGraphInfo) and block_id in ginfo.block_ids:
61+
return False # the row is summed away
62+
for node in ginfo.graph.nodes:
63+
patterns = node.meta.get("indexing_patterns")
64+
if not patterns:
65+
continue
66+
for dim, pat in enumerate(patterns):
67+
if getattr(pat, "block_id", None) != block_id:
68+
continue
69+
if not isinstance(pat, TilePattern):
70+
return False # the row is offset or scattered, not straight
71+
if node.target is store and dim == 0:
72+
has_straight_store = True
73+
return has_straight_store
5374

5475

5576
def needs_ordered_carry(state: CodegenState, block_id: int) -> bool:

test/test_pallas_load_store.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from __future__ import annotations
2+
3+
import torch
4+
5+
import helion
6+
from helion import exc
7+
from helion._testing import DEVICE
8+
from helion._testing import TestCase
9+
from helion._testing import code_and_output
10+
from helion._testing import onlyBackends
11+
from helion._testing import skipUnlessPallas
12+
import helion.language as hl
13+
14+
15+
# out[s:e] = jagged[s:e] @ dense[g] for each group g delimited by seq_offsets.
16+
# The row tile hl.tile(s, e) has runtime bounds; unaligned group boundaries make
17+
# adjacent groups share an output row, which the ordered carry stitches.
18+
@helion.kernel(backend="pallas")
19+
def jagged_dense_bmm(
20+
seq_offsets: torch.Tensor, jagged: torch.Tensor, dense: torch.Tensor
21+
) -> torch.Tensor:
22+
L, D = jagged.shape
23+
B, D, K = dense.shape
24+
out = torch.empty((L, K), dtype=jagged.dtype, device=jagged.device)
25+
for g in hl.grid(B):
26+
s = seq_offsets[g]
27+
e = seq_offsets[g + 1]
28+
for st in hl.tile(s, e):
29+
for kt in hl.tile(0, K):
30+
acc = hl.zeros([st, kt], dtype=torch.float32)
31+
for dt in hl.tile(0, D):
32+
acc = acc + torch.matmul(jagged[st, dt], dense[g, dt, kt])
33+
out[st, kt] = acc
34+
return out
35+
36+
37+
@onlyBackends(["pallas"])
38+
@skipUnlessPallas("JAX/Pallas TPU not available")
39+
class 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".
42+
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+
)
56+
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.
60+
@helion.kernel(backend="pallas")
61+
def jagged_scale(
62+
seq_offsets: torch.Tensor, jagged: torch.Tensor
63+
) -> torch.Tensor:
64+
L, D = jagged.shape
65+
B = seq_offsets.shape[0] - 1
66+
out = torch.empty((L, D), dtype=jagged.dtype, device=jagged.device)
67+
for g in hl.grid(B):
68+
s = seq_offsets[g]
69+
e = seq_offsets[g + 1]
70+
for st in hl.tile(s, e):
71+
for dt in hl.tile(0, D):
72+
out[st, dt] = jagged[st, dt] * 2
73+
return out
74+
75+
off = torch.tensor([0, 13, 25], dtype=torch.int32, device=DEVICE)
76+
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+
)
84+
85+
def test_jagged_reduction_over_row_f32(self) -> None:
86+
# Summing the jagged row away to a dense output reduces it (not a map
87+
# axis), so it takes no carry. For f32 the row reads directly (single
88+
# lane tile) and reduces. bf16's aligned-enclosing reduction read is a
89+
# separate, not-yet-supported case (Mosaic E2003), so it is out of scope.
90+
@helion.kernel(backend="pallas")
91+
def jagged_row_sum(
92+
seq_offsets: torch.Tensor, jagged: torch.Tensor
93+
) -> torch.Tensor:
94+
L, D = jagged.shape
95+
B = seq_offsets.shape[0] - 1
96+
out = torch.zeros((B, D), dtype=torch.float32, device=jagged.device)
97+
for g in hl.grid(B):
98+
s = seq_offsets[g]
99+
e = seq_offsets[g + 1]
100+
for dt in hl.tile(0, D):
101+
acc = hl.zeros([dt], dtype=torch.float32)
102+
for st in hl.tile(s, e):
103+
acc = acc + jagged[st, dt].to(torch.float32).sum(0)
104+
out[g, dt] = acc
105+
return out
106+
107+
off = torch.tensor([0, 13, 25], dtype=torch.int32, device=DEVICE)
108+
j = torch.randn((25, 128), dtype=torch.float32, device=DEVICE)
109+
_code, out = code_and_output(
110+
jagged_row_sum,
111+
(off, j),
112+
block_sizes=[128, 16],
113+
pallas_loop_type="emit_pipeline",
114+
)
115+
expected = torch.stack([j[0:13].sum(0), j[13:25].sum(0)])
116+
torch.testing.assert_close(out, expected, rtol=2e-3, atol=2e-3)
117+
118+
def test_non_jagged_emit_pipeline_unaffected(self) -> None:
119+
# Static (compile-time) tile bounds never trip the gate: a plain
120+
# emit_pipeline matmul still lowers and runs correctly.
121+
@helion.kernel(backend="pallas", static_shapes=True)
122+
def static_matmul(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
123+
M, K = a.shape
124+
K, N = b.shape
125+
out = torch.empty((M, N), dtype=a.dtype, device=a.device)
126+
for mt in hl.tile(M):
127+
for nt in hl.tile(N):
128+
acc = hl.zeros([mt, nt], dtype=torch.float32)
129+
for kt in hl.tile(K):
130+
acc = acc + torch.matmul(a[mt, kt], b[kt, nt])
131+
out[mt, nt] = acc.to(a.dtype)
132+
return out
133+
134+
a = torch.randn((64, 128), dtype=torch.bfloat16, device=DEVICE)
135+
b = torch.randn((128, 256), dtype=torch.bfloat16, device=DEVICE)
136+
_code, out = code_and_output(
137+
static_matmul,
138+
(a, b),
139+
block_sizes=[32, 128, 128],
140+
pallas_loop_type="emit_pipeline",
141+
)
142+
torch.testing.assert_close(out, a @ b)

0 commit comments

Comments
 (0)