Skip to content

Commit d5a2d61

Browse files
authored
Fix invalid Triton code for mixed scalar/block indexing in store operations when block dimension has size 1 (#1258)
1 parent 5055c54 commit d5a2d61

7 files changed

Lines changed: 191 additions & 2 deletions

File tree

helion/_compiler/indexing_strategy.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,70 @@ def codegen_store(
179179
) -> ast.AST:
180180
indexing = SubscriptIndexing.create(state, fake_tensor, subscript, extra_mask)
181181
name = state.device_function.tensor_arg(fake_tensor).name
182+
183+
# Check if the pointer is effectively scalar but the value has dimensions.
184+
# This happens when all block-indexed dimensions have size 1 in the target tensor.
185+
# In this case, we need to reshape the value to scalar to match the pointer.
186+
env = CompileEnvironment.current()
187+
output_size = SubscriptIndexing.compute_shape(fake_tensor, subscript, state)
188+
189+
# Determine if pointer has any block dimensions by checking if any block index
190+
# targets a non-size-1 tensor dimension. We need to match the logic in
191+
# SubscriptIndexing.create which skips dimensions where fake_tensor.size(i) == 1.
192+
pointer_has_block_dims = False
193+
tensor_dim = 0
194+
k_index = 0
195+
for k in subscript:
196+
if k is None:
197+
# None adds a dimension to output, not from tensor
198+
pass
199+
elif isinstance(k, int):
200+
# Scalar int index - consumes tensor dim but adds scalar to pointer
201+
tensor_dim += 1
202+
elif _get_tile_with_offset_info(
203+
k, state, k_index
204+
) is not None or isinstance(k, torch.Tensor):
205+
# Tensor index (tile.index + offset or regular tensor) - block index
206+
if not env.known_equal(fake_tensor.size(tensor_dim), 1):
207+
pointer_has_block_dims = True
208+
tensor_dim += 1
209+
k_index += 1
210+
elif isinstance(k, torch.SymInt):
211+
# SymInt can be block index (with BlockSizeOrigin) or scalar
212+
symbol = k._sympy_()
213+
origin = None
214+
if isinstance(symbol, sympy.Symbol):
215+
origin = HostFunction.current().expr_to_origin.get(symbol)
216+
if origin and isinstance(origin.origin, BlockSizeOrigin):
217+
# Block index
218+
if not env.known_equal(fake_tensor.size(tensor_dim), 1):
219+
pointer_has_block_dims = True
220+
# Both block and scalar SymInt consume a tensor dimension
221+
tensor_dim += 1
222+
k_index += 1
223+
elif isinstance(k, slice):
224+
# Slice - adds block dimension if slice_size > 1
225+
size = fake_tensor.size(tensor_dim)
226+
slice_size = compute_slice_size(k, size)
227+
if not env.known_equal(slice_size, 1):
228+
if not env.known_equal(fake_tensor.size(tensor_dim), 1):
229+
pointer_has_block_dims = True
230+
tensor_dim += 1
231+
k_index += 1
232+
233+
# If pointer is scalar but output_size has dimensions, reshape value to scalar.
234+
# Skip reshaping for scalar constants which don't have shape.
235+
if (
236+
not pointer_has_block_dims
237+
and output_size
238+
and not isinstance(value, ast.Constant)
239+
):
240+
# Pointer is scalar but value may have shape - squeeze to scalar
241+
value = expr_from_string(
242+
"tl.reshape({value}, [])",
243+
value=value,
244+
)
245+
182246
return expr_from_string(
183247
f"tl.store({name} + {{offset}}, {{value}}, {{mask}})",
184248
value=value,

test/test_associative_scan.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ def _helion_test_single_element(x, result):
753753
row_data = tl.load(x + tl.zeros([1, 1], tl.int32), None)
754754
# src[test_associative_scan.py:N]: result[i, :] = hl.associative_scan(add_combine_fn, row_data, dim=1)
755755
_associative_scan = tl.associative_scan(row_data, 1, add_combine_fn_0)
756-
tl.store(result + tl.zeros([1, 1], tl.int32), _associative_scan, None)
756+
tl.store(result + tl.zeros([1, 1], tl.int32), tl.reshape(_associative_scan, []), None)
757757

758758
def test_single_element(x: torch.Tensor, *, _launcher=_default_launcher):
759759
# src[test_associative_scan.py:N]: result = torch.empty_like(x)

test/test_associative_scan.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from helion._testing import RefEagerTestBase
1010
from helion._testing import TestCase
1111
from helion._testing import code_and_output
12+
from helion._testing import skipIfCpu
1213
from helion._testing import skipIfRefEager
1314
import helion.language as hl
1415

@@ -381,6 +382,7 @@ def test_reverse_kernel(x: torch.Tensor) -> torch.Tensor:
381382
# Verify reverse parameter is in generated code
382383
self.assertIn("reverse=True", code)
383384

385+
@skipIfCpu("")
384386
def test_associative_scan_edge_cases(self):
385387
"""Test associative_scan edge cases."""
386388

test/test_indexing.expected

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,75 @@ def masked_store(x: torch.Tensor, *, _launcher=_default_launcher):
759759
# src[test_indexing.py:N]: return out
760760
return out
761761

762+
--- assertExpectedJournal(TestIndexing.test_mixed_scalar_block_store_size1_dim)
763+
from __future__ import annotations
764+
765+
import torch
766+
import helion.language as hl
767+
import triton
768+
import triton.language as tl
769+
from torch._inductor.runtime import triton_helpers
770+
from torch._inductor.runtime.triton_helpers import math as tl_math
771+
from helion.runtime import default_launcher as _default_launcher
772+
773+
@triton.jit
774+
def _helion_kernel_with_mixed_store(x_data, out, scales, _BLOCK_SIZE_0: tl.constexpr, _BLOCK_SIZE_2: tl.constexpr):
775+
# src[test_indexing.py:N]: for m_tile, n_tile in hl.tile([m, n], block_size=[None, n_block]):
776+
num_blocks_0 = 1
777+
pid_1 = tl.program_id(0) // num_blocks_0
778+
offset_0 = pid_1 * _BLOCK_SIZE_0
779+
# src[test_indexing.py:N]: n_tile.begin, n_tile.end, block_size=BLOCK_SIZE
780+
tile_end = offset_0 + _BLOCK_SIZE_0
781+
# src[test_indexing.py:N]: for n_tile_local in hl.tile(
782+
# src[test_indexing.py:N]: n_tile.begin, n_tile.end, block_size=BLOCK_SIZE
783+
# src[test_indexing.py:N]: ):
784+
# src[test_indexing.py:N-N]: ...
785+
for offset_2 in tl.range(offset_0.to(tl.int32), tile_end.to(tl.int32), _BLOCK_SIZE_2):
786+
indices_2 = offset_2 + tl.arange(0, _BLOCK_SIZE_2).to(tl.int32)
787+
mask_2 = indices_2 < tile_end
788+
# src[test_indexing.py:N]: x_block = x_data[m_tile, n_tile_local]
789+
x_block = tl.load(x_data + indices_2[None, :] * 1, mask_2[None, :], other=0)
790+
# src[test_indexing.py:N]: row_max = x_block.abs().amax(dim=1)
791+
v_0 = tl_math.abs(x_block)
792+
_mask_to = tl.where(tl.broadcast_to(mask_2[None, :], [1, _BLOCK_SIZE_2]), v_0, tl.full([], float('-inf'), tl.float32))
793+
row_max = tl.cast(tl.max(_mask_to, 1), tl.float32)
794+
# src[test_indexing.py:N]: row_value = row_max.to(torch.uint8)
795+
v_1 = tl.cast(row_max, tl.uint8)
796+
# src[test_indexing.py:N]: out[m_tile, n_tile_local] = x_block * 2.0
797+
v_2 = 2.0
798+
v_3 = x_block * v_2
799+
tl.store(out + indices_2[None, :] * 1, v_3, mask_2[None, :])
800+
# src[test_indexing.py:N]: scale_col_idx = n_tile_local.begin // BLOCK_SIZE # scalar
801+
floordiv = triton_helpers.div_floor_integer(offset_2, 32)
802+
# src[test_indexing.py:N]: scales[m_tile, scale_col_idx] = row_value # row_value is block
803+
tl.store(scales + floordiv * 1, tl.reshape(v_1, []), None)
804+
805+
def kernel_with_mixed_store(x_data: torch.Tensor, BLOCK_SIZE: hl.constexpr, *, _launcher=_default_launcher):
806+
# src[test_indexing.py:N]: m, n = x_data.shape
807+
m, n = x_data.shape
808+
# src[test_indexing.py:N]: n = hl.specialize(n)
809+
n = 64
810+
# src[test_indexing.py:N]: n_scale_cols = (n + BLOCK_SIZE - 1) // BLOCK_SIZE
811+
n_scale_cols = (n + 32 - 1) // 32
812+
# src[test_indexing.py:N]: scales = x_data.new_empty((m, n_scale_cols), dtype=torch.uint8)
813+
scales = x_data.new_empty((m, n_scale_cols), dtype=torch.uint8)
814+
# src[test_indexing.py:N]: out = x_data.new_empty(x_data.shape, dtype=torch.float32)
815+
out = x_data.new_empty(x_data.shape, dtype=torch.float32)
816+
# src[test_indexing.py:N]: for m_tile, n_tile in hl.tile([m, n], block_size=[None, n_block]):
817+
_BLOCK_SIZE_0 = 32
818+
# src[test_indexing.py:N]: for n_tile_local in hl.tile(
819+
# src[test_indexing.py:N]: n_tile.begin, n_tile.end, block_size=BLOCK_SIZE
820+
# src[test_indexing.py:N]: ):
821+
# src[test_indexing.py:N-N]: ...
822+
_BLOCK_SIZE_2 = 32
823+
# src[test_indexing.py:N]: for m_tile, n_tile in hl.tile([m, n], block_size=[None, n_block]):
824+
# src[test_indexing.py:N]: for n_tile_local in hl.tile(
825+
# src[test_indexing.py:N]: n_tile.begin, n_tile.end, block_size=BLOCK_SIZE
826+
# src[test_indexing.py:N-N]: ...
827+
_launcher(_helion_kernel_with_mixed_store, (1 * triton.cdiv(64, _BLOCK_SIZE_0),), x_data, out, scales, _BLOCK_SIZE_0, _BLOCK_SIZE_2, num_warps=4, num_stages=1)
828+
# src[test_indexing.py:N]: return out, scales
829+
return (out, scales)
830+
762831
--- assertExpectedJournal(TestIndexing.test_non_consecutive_tensor_indexers_no_broadcast)
763832
from __future__ import annotations
764833

test/test_indexing.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,7 @@ def test_reduction_tensor_descriptor_indexing_reduction_loop(self):
972972
torch.testing.assert_close(result, expected)
973973
self.assertExpectedJournal(code)
974974

975+
@skipIfCpu("")
975976
def test_2d_slice_index(self):
976977
"""Test both setter from scalar and getter for [:,i]"""
977978

@@ -2161,6 +2162,57 @@ def store_with_mixed_indices(
21612162
torch.testing.assert_close(result, expected)
21622163
self.assertExpectedJournal(code)
21632164

2165+
@skipIfCpu("")
2166+
def test_mixed_scalar_block_store_size1_dim(self):
2167+
"""Test store with mixed scalar/block indexing when block dimension has size 1.
2168+
2169+
This tests a bug fix where storing a block value with:
2170+
- One index being a tile/block (e.g., m_tile) over a size-1 dimension
2171+
- Another index being a scalar (e.g., computed from tile.begin)
2172+
would generate invalid Triton code because the pointer became scalar
2173+
but the value was still a block.
2174+
"""
2175+
2176+
@helion.kernel(autotune_effort="none")
2177+
def kernel_with_mixed_store(
2178+
x_data: torch.Tensor, BLOCK_SIZE: hl.constexpr
2179+
) -> tuple[torch.Tensor, torch.Tensor]:
2180+
m, n = x_data.shape
2181+
n = hl.specialize(n)
2182+
n_scale_cols = (n + BLOCK_SIZE - 1) // BLOCK_SIZE
2183+
scales = x_data.new_empty((m, n_scale_cols), dtype=torch.uint8)
2184+
out = x_data.new_empty(x_data.shape, dtype=torch.float32)
2185+
2186+
n_block = hl.register_block_size(BLOCK_SIZE, n)
2187+
2188+
for m_tile, n_tile in hl.tile([m, n], block_size=[None, n_block]):
2189+
for n_tile_local in hl.tile(
2190+
n_tile.begin, n_tile.end, block_size=BLOCK_SIZE
2191+
):
2192+
x_block = x_data[m_tile, n_tile_local]
2193+
2194+
# Compute one value per row in m_tile
2195+
row_max = x_block.abs().amax(dim=1)
2196+
row_value = row_max.to(torch.uint8)
2197+
2198+
out[m_tile, n_tile_local] = x_block * 2.0
2199+
2200+
# Mixed indexing: block row index + scalar column index
2201+
scale_col_idx = n_tile_local.begin // BLOCK_SIZE # scalar
2202+
scales[m_tile, scale_col_idx] = row_value # row_value is block
2203+
2204+
return out, scales
2205+
2206+
# Test with m=1 (single row - this was the failing case before the fix)
2207+
# The fix ensures tl.reshape is applied to squeeze the value to scalar
2208+
# when the pointer is scalar due to size-1 dimensions being dropped.
2209+
x1 = torch.randn(1, 64, device=DEVICE, dtype=torch.float32)
2210+
code, (out1, scales1) = code_and_output(kernel_with_mixed_store, (x1, 32))
2211+
expected_out1 = x1 * 2.0
2212+
torch.testing.assert_close(out1, expected_out1)
2213+
self.assertEqual(scales1.shape, (1, 2))
2214+
self.assertExpectedJournal(code)
2215+
21642216

21652217
if __name__ == "__main__":
21662218
unittest.main()

test/test_reduce.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def _helion_test_reduce_codegen_kernel(x, result, _RDIM_SIZE_1: tl.constexpr):
198198
row_data = tl.load(x + indices_1[None, :] * 1, mask_1[None, :], other=0)
199199
# src[test_reduce.py:N]: result[i] = hl.reduce(add_combine_fn, row_data, dim=1)
200200
_reduce = tl.reduce(row_data, 1, add_combine_fn_0)
201-
tl.store(result + tl.zeros([1], tl.int32), _reduce, None)
201+
tl.store(result + tl.zeros([1], tl.int32), tl.reshape(_reduce, []), None)
202202

203203
def test_reduce_codegen_kernel(x: torch.Tensor, *, _launcher=_default_launcher):
204204
# src[test_reduce.py:N]: result = torch.empty([x.size(0)], dtype=x.dtype, device=x.device)

test/test_reduce.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from helion._testing import RefEagerTestBase
1010
from helion._testing import TestCase
1111
from helion._testing import code_and_output
12+
from helion._testing import skipIfCpu
1213
import helion.language as hl
1314

1415

@@ -500,6 +501,7 @@ def test_argmax_negative_kernel(
500501
self.assertIn("tl.reduce", code)
501502
self.assertIn("argmax_combine_fn_", code)
502503

504+
@skipIfCpu("")
503505
def test_reduce_code_generation(self):
504506
"""Test that reduce generates correct Triton code."""
505507

0 commit comments

Comments
 (0)