Skip to content

Commit d51eab0

Browse files
committed
Add check_bounds option to speed up ct.load/store
Signed-off-by: Ziheng Deng <zihengd@nvidia.com>
1 parent bfff83e commit d51eab0

5 files changed

Lines changed: 169 additions & 13 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- Added a ``check_bounds`` option to ``ct.load()``, ``ct.store()`` and the
2+
``TiledView.load()`` / ``TiledView.store()`` methods. It defaults to ``True``.
3+
when set to ``False``, it declares all elements in the tile is guaranteed to
4+
stay within the array bounds and skips the out-of-bounds check to speed up the
5+
load/store. Setting it to ``False`` requires tileiras 13.4+.

src/cuda/tile/_ir/ops.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -873,12 +873,25 @@ def _uniform_tuple(val: Any, *, rank: int):
873873
return (val,) * rank
874874

875875

876+
def _check_bounds_to_inbounds(check_bounds: Var, rank: int) -> tuple[bool, ...]:
877+
check = require_constant_bool(check_bounds)
878+
inbounds = _uniform_tuple(not check, rank=rank)
879+
if not check:
880+
cur_version = Builder.get_current().ir_ctx.tileiras_version
881+
if cur_version < BytecodeVersion.V_13_4:
882+
raise TileUnsupportedFeatureError(
883+
f"'check_bounds=False' requires tileiras {BytecodeVersion.V_13_4.as_string()}"
884+
f" or later. Current version is {cur_version.as_string()}.")
885+
return inbounds
886+
887+
876888
@dataclass(eq=False)
877889
class TileLoad(Operation, opcode="tile_load", memory_effect=MemoryEffect.LOAD):
878890
latency: Optional[int] = attribute()
879891
allow_tma: Optional[bool] = attribute()
880892
memory_order: MemoryOrder = attribute(default=MemoryOrder.WEAK)
881893
memory_scope: MemoryScope = attribute(default=MemoryScope.NONE)
894+
inbounds: tuple[bool, ...] = attribute(default=())
882895
view: Var = operand()
883896
index: tuple[Var, ...] = operand()
884897
token: Optional[Var] = operand(default=None)
@@ -910,14 +923,15 @@ def generate_bytecode(self, ctx: BytecodeContext) -> tuple[bc.Value, bc.Value]:
910923
memory_ordering_semantics=memory_order_to_bytecode[self.memory_order],
911924
memory_scope=memory_scope_to_bytecode[self.memory_scope],
912925
optimization_hints=ctx.load_store_hints(self.latency, self.allow_tma),
913-
inbounds=_uniform_tuple(False, rank=len(self.index)),
926+
inbounds=self.inbounds or _uniform_tuple(False, rank=len(self.index)),
914927
)
915928
return res, res_token
916929

917930

918931
def _tile_load_impl_inner(array: Var, index_items: tuple[Var, ...], shape: Sequence[int],
919932
order: Sequence[int], padding_mode: PaddingMode,
920933
latency: Var, allow_tma: Var,
934+
inbounds: tuple[bool, ...] = (),
921935
traversal_steps: Optional[tuple[int, ...]] = None,
922936
memory_order: MemoryOrder = MemoryOrder.WEAK,
923937
memory_scope: MemoryScope = MemoryScope.NONE) -> Var:
@@ -938,7 +952,7 @@ def _tile_load_impl_inner(array: Var, index_items: tuple[Var, ...], shape: Seque
938952
result, _token = add_operation_variadic(TileLoad, (res_ty, TokenTy()),
939953
view=view, index=index_items, latency=latency,
940954
allow_tma=allow_tma, memory_order=memory_order,
941-
memory_scope=memory_scope)
955+
memory_scope=memory_scope, inbounds=inbounds)
942956
return reshape(result, shape)
943957

944958

@@ -1009,7 +1023,7 @@ def raw_array_memory_store_offset_impl(self: Var, offset: Var, value: Var,
10091023

10101024
@impl(ct.load)
10111025
def tile_load_impl(array: Var, index: Var, shape: Var, order: Var,
1012-
padding_mode: Var, latency: Var, allow_tma: Var,
1026+
padding_mode: Var, check_bounds: Var, latency: Var, allow_tma: Var,
10131027
memory_order: Var, memory_scope: Var) -> Var:
10141028
array_ty = require_array_type(array)
10151029
index_ty = require_index_or_index_tuple_type(index)
@@ -1022,11 +1036,12 @@ def tile_load_impl(array: Var, index: Var, shape: Var, order: Var,
10221036
allow_0d_shape=True)
10231037
order = require_constant_axis_order(order, array_ty.ndim)
10241038
padding_mode = require_constant_enum(padding_mode, PaddingMode)
1039+
inbounds = _check_bounds_to_inbounds(check_bounds, array_ty.ndim)
10251040
mem_order = require_constant_enum(memory_order, MemoryOrder)
10261041
mem_scope = require_constant_enum(memory_scope, MemoryScope)
10271042
validate_memory_order_and_scope(mem_order, mem_scope, TileLoad)
10281043
return _tile_load_impl_inner(array, index_items, shape, order, padding_mode, latency, allow_tma,
1029-
memory_order=mem_order, memory_scope=mem_scope)
1044+
inbounds=inbounds, memory_order=mem_order, memory_scope=mem_scope)
10301045

10311046

10321047
@dataclass(eq=False)
@@ -1035,6 +1050,7 @@ class TileStore(Operation, opcode="tile_store", memory_effect=MemoryEffect.STORE
10351050
allow_tma: Optional[bool] = attribute()
10361051
memory_order: MemoryOrder = attribute(default=MemoryOrder.WEAK)
10371052
memory_scope: MemoryScope = attribute(default=MemoryScope.NONE)
1053+
inbounds: tuple[bool, ...] = attribute(default=())
10381054
view: Var = operand()
10391055
index: tuple[Var, ...] = operand()
10401056
tile: Var = operand()
@@ -1066,12 +1082,13 @@ def generate_bytecode(self, ctx: BytecodeContext) -> bc.Value:
10661082
memory_ordering_semantics=memory_order_to_bytecode[self.memory_order],
10671083
memory_scope=memory_scope_to_bytecode[self.memory_scope],
10681084
optimization_hints=ctx.load_store_hints(self.latency, self.allow_tma),
1069-
inbounds=_uniform_tuple(False, rank=len(self.index))
1085+
inbounds=self.inbounds or _uniform_tuple(False, rank=len(self.index))
10701086
)
10711087

10721088

10731089
def _tile_store_impl_inner(array: Var, index_items: tuple[Var, ...], tile: Var,
10741090
order: Sequence[int], latency: Var, allow_tma: Var,
1091+
inbounds: tuple[bool, ...] = (),
10751092
traversal_steps: Optional[tuple[int, ...]] = None,
10761093
memory_order: MemoryOrder = MemoryOrder.WEAK,
10771094
memory_scope: MemoryScope = MemoryScope.NONE):
@@ -1092,12 +1109,12 @@ def _tile_store_impl_inner(array: Var, index_items: tuple[Var, ...], tile: Var,
10921109
traversal_steps)
10931110
add_operation(TileStore, TokenTy(), view=view, index=index_items, tile=tile,
10941111
latency=latency, allow_tma=allow_tma, memory_order=memory_order,
1095-
memory_scope=memory_scope)
1112+
memory_scope=memory_scope, inbounds=inbounds)
10961113

10971114

10981115
@impl(ct.store)
10991116
def tile_store_impl(array: Var, index: Var, tile: Var, order: Var,
1100-
latency: Var, allow_tma: Var,
1117+
check_bounds: Var, latency: Var, allow_tma: Var,
11011118
memory_order: Var, memory_scope: Var):
11021119
array_ty = require_array_type(array)
11031120
index_ty = require_index_or_index_tuple_type(index)
@@ -1108,11 +1125,12 @@ def tile_store_impl(array: Var, index: Var, tile: Var, order: Var,
11081125

11091126
tile = implicit_cast(tile, array_ty.dtype, "Stored tile is incompatible with array's dtype")
11101127
order = require_constant_axis_order(order, array_ty.ndim)
1128+
inbounds = _check_bounds_to_inbounds(check_bounds, array_ty.ndim)
11111129
mem_order = require_constant_enum(memory_order, MemoryOrder)
11121130
mem_scope = require_constant_enum(memory_scope, MemoryScope)
11131131
validate_memory_order_and_scope(mem_order, mem_scope, TileStore)
11141132
_tile_store_impl_inner(array, index_items, tile, order, latency, allow_tma,
1115-
memory_order=mem_order, memory_scope=mem_scope)
1133+
inbounds=inbounds, memory_order=mem_order, memory_scope=mem_scope)
11161134

11171135

11181136
@dataclass(eq=False)
@@ -3158,23 +3176,27 @@ def tiled_view_num_tiles(self: Var, axis: Var) -> Var:
31583176

31593177

31603178
@impl(ct.TiledView.load)
3161-
def tiled_view_load_impl(self: Var, index: Var, latency: Var, allow_tma: Var) -> Var:
3179+
def tiled_view_load_impl(self: Var, index: Var, check_bounds: Var, latency: Var,
3180+
allow_tma: Var) -> Var:
31623181
view_ty = require_tiled_view_type(self)
31633182
index_ty = require_index_or_index_tuple_type(index)
31643183
index_items = index.get_aggregate().items if isinstance(index_ty, TupleTy) else (index,)
31653184
if view_ty.ndim != len(index_items):
31663185
raise TileTypeError(f"Index size {len(index_items)}"
31673186
f" does not match the tiled view rank {view_ty.ndim}")
31683187

3188+
inbounds = _check_bounds_to_inbounds(check_bounds, view_ty.ndim)
31693189
[array] = self.get_aggregate().as_tuple()
31703190
order = get_default_order(view_ty.ndim)
31713191
return _tile_load_impl_inner(array, index_items, view_ty.tile_shape, order,
31723192
view_ty.padding_mode, latency, allow_tma,
3193+
inbounds=inbounds,
31733194
traversal_steps=view_ty.traversal_steps)
31743195

31753196

31763197
@impl(ct.TiledView.store)
3177-
def tiled_view_store_impl(self: Var, index: Var, tile: Var, latency: Var, allow_tma: Var):
3198+
def tiled_view_store_impl(self: Var, index: Var, tile: Var, check_bounds: Var, latency: Var,
3199+
allow_tma: Var):
31783200
view_ty = require_tiled_view_type(self)
31793201
index_ty = require_index_or_index_tuple_type(index)
31803202
index_items = index.get_aggregate().items if isinstance(index_ty, TupleTy) else (index,)
@@ -3187,12 +3209,14 @@ def tiled_view_store_impl(self: Var, index: Var, tile: Var, latency: Var, allow_
31873209
raise TileTypeError(f"Tile shape {tile_ty.shape} is not broadcastable"
31883210
f" to the tiled view's tile shape {view_ty.tile_shape}")
31893211

3212+
inbounds = _check_bounds_to_inbounds(check_bounds, view_ty.ndim)
31903213
tile = broadcast_to(tile, view_ty.tile_shape)
31913214
tile = implicit_cast(tile, view_ty.dtype,
31923215
"Stored tile is incompatible with tiled view's dtype")
31933216
[array] = self.get_aggregate().as_tuple()
31943217
order = get_default_order(view_ty.ndim)
31953218
_tile_store_impl_inner(array, index_items, tile, order, latency, allow_tma,
3219+
inbounds=inbounds,
31963220
traversal_steps=view_ty.traversal_steps)
31973221

31983222

src/cuda/tile/_stub.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ def traversal_steps(self) -> tuple[int, ...]:
816816

817817
@stub
818818
def load(self, index: Shape, *,
819+
check_bounds: Constant[bool] = True,
819820
latency: Optional[int] = None,
820821
allow_tma: Optional[bool] = None) -> Tile:
821822
"""Loads a tile from the |tiled view| at the given tile `index`.
@@ -828,6 +829,11 @@ def load(self, index: Shape, *,
828829
829830
Args:
830831
index (tuple[int,...]): An index in the |tiled view|'s tile space.
832+
check_bounds (const bool): Whether to bounds-check the tile against the view
833+
boundaries. When ``False``, the tile is assumed to stay fully within bounds and the
834+
out-of-bounds check is skipped, producing a faster load; violating this assumption
835+
is undefined behavior. Defaults to ``True``. Setting it to ``False`` requires
836+
tileiras 13.4+.
831837
latency (const int): A hint indicating how heavy DRAM traffic will be. It shall be an
832838
integer between 1 (low) and 10 (high). By default, the compiler will infer the
833839
latency.
@@ -858,6 +864,7 @@ def kernel(x):
858864

859865
@stub
860866
def store(self, index: Shape, tile: Tile, *,
867+
check_bounds: Constant[bool] = True,
861868
latency: Optional[int] = None,
862869
allow_tma: Optional[bool] = None) -> None:
863870
"""Stores a `tile` into the |tiled view| at the given tile `index`.
@@ -872,6 +879,11 @@ def store(self, index: Shape, tile: Tile, *,
872879
Args:
873880
index (tuple[int,...]): An index in the |tiled view|'s tile space.
874881
tile (Tile): The tile to store.
882+
check_bounds (const bool): Whether to bounds-check the tile against the view
883+
boundaries. When ``False``, the tile is assumed to stay fully within bounds and the
884+
out-of-bounds check is skipped, producing a faster store; violating this assumption
885+
is undefined behavior. Defaults to ``True``. Setting it to ``False`` requires
886+
tileiras 13.4+.
875887
latency (const int): A hint indicating how heavy DRAM traffic will be. It shall be an
876888
integer between 1 (low) and 10 (high). By default, the compiler will infer the
877889
latency.
@@ -1255,6 +1267,7 @@ def load(array: Array, /,
12551267
shape: Constant[Shape], *,
12561268
order: Constant[Order] = "C",
12571269
padding_mode: PaddingMode = PaddingMode.UNDETERMINED,
1270+
check_bounds: Constant[bool] = True,
12581271
latency: Optional[int] = None,
12591272
allow_tma: Optional[bool] = None,
12601273
memory_order: MemoryOrder = MemoryOrder.WEAK,
@@ -1302,6 +1315,11 @@ def load(array: Array, /,
13021315
13031316
padding_mode (PaddingMode): The value used to pad the tile when it extends beyond the array
13041317
boundaries. By default, the padding value is undetermined.
1318+
check_bounds (const bool): Whether to bounds-check the tile against the array boundaries.
1319+
When ``False``, the tile is assumed to stay fully within bounds and the out-of-bounds
1320+
check is skipped, producing a faster load; violating this assumption is undefined
1321+
behavior. Since no out-of-bound elements can occur in this case, ``padding_mode`` is
1322+
ignored. Defaults to ``True``. Setting it to ``False`` requires tileiras 13.4+.
13051323
latency (const int): A hint indicating how heavy DRAM traffic will be. It shall be an
13061324
integer between 1 (low) and 10 (high). By default, the compiler will infer the latency.
13071325
allow_tma (const bool): If False, the load will not use TMA. By default, TMA is allowed.
@@ -1408,6 +1426,7 @@ def store(array: Array, /,
14081426
index: Shape,
14091427
tile: TileOrScalar, *,
14101428
order: Constant[Order] = "C",
1429+
check_bounds: Constant[bool] = True,
14111430
latency: Optional[int] = None,
14121431
allow_tma: Optional[bool] = None,
14131432
memory_order: MemoryOrder = MemoryOrder.WEAK,
@@ -1437,6 +1456,10 @@ def store(array: Array, /,
14371456
tile (Tile): The |tile| to store. The rank of the tile must match rank of the array,
14381457
unless it is a scalar or 0d tile.
14391458
order ("C" or "F", or tuple[const int,...]): Order of axis mapping. See :py:func:`load`.
1459+
check_bounds (const bool): Whether to bounds-check the tile against the array boundaries.
1460+
When ``False``, the tile is assumed to stay fully within bounds and the out-of-bounds
1461+
check is skipped, producing a faster store; violating this assumption is undefined
1462+
behavior. Defaults to ``True``. Setting it to ``False`` requires tileiras 13.4+.
14401463
latency (int, optional): A hint indicating how heavy DRAM traffic will be. It shall be an
14411464
integer between 1 (low) and 10 (high). By default, the compiler will infer the latency.
14421465
allow_tma (bool, optional): If False, the store will not use TMA.

test/test_load_store.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import pytest
88

99
from math import ceil
10-
from conftest import float_dtypes, bool_dtypes, get_tileiras_version, int_dtypes, dtype_id
11-
from cuda.tile import TileTypeError
10+
from conftest import (float_dtypes, bool_dtypes, get_tileiras_version, int_dtypes, dtype_id,
11+
requires_tileiras)
12+
from cuda.tile import TileTypeError, TileUnsupportedFeatureError
1213
from cuda.tile._bytecode.version import BytecodeVersion
1314
from cuda.tile._ir.cast_ops import _is_implicit_cast_ok
1415
from cuda.tile._ir.typing_support import to_dtype
@@ -214,3 +215,51 @@ def kern(x):
214215
match="Axis order must be a permutation, but axis 1 is used at least twice"):
215216
x = torch.zeros((64, 64), device="cuda")
216217
ct.launch(torch.cuda.current_stream(), (1,), kern, (x,))
218+
219+
220+
@ct.kernel
221+
def copy_2d_no_check_bounds(x, y, TILE_X: ct.Constant[int], TILE_Y: ct.Constant[int]):
222+
bidx = ct.bid(0)
223+
bidy = ct.bid(1)
224+
tx = ct.load(x, index=(bidx, bidy), shape=(TILE_X, TILE_Y), check_bounds=False)
225+
ct.store(y, index=(bidx, bidy), tile=tx, check_bounds=False)
226+
227+
228+
@pytest.mark.use_mlir
229+
@requires_tileiras(BytecodeVersion.V_13_4)
230+
def test_load_store_check_bounds():
231+
# check_bounds=False lowers to an all-true `inbounds` attribute on every dimension.
232+
shape = (64, 64)
233+
tile = (32, 32)
234+
x = make_tensor(shape, dtype=torch.float32, device="cuda")
235+
y = torch.zeros_like(x)
236+
grid = (shape[0] // tile[0], shape[1] // tile[1], 1)
237+
bytecode = get_bytecode(copy_2d_no_check_bounds, (x, y, tile[0], tile[1]))
238+
wildcard = "{{.*}}"
239+
filecheck(bytecode, "\n".join([
240+
f"// CHECK: load_view_tko{wildcard}inbounds = [true, true]",
241+
f"// CHECK: store_view_tko{wildcard}inbounds = [true, true]",
242+
]))
243+
ct.launch(torch.cuda.current_stream(), grid, copy_2d_no_check_bounds, (x, y, tile[0], tile[1]))
244+
assert_equal(y, x)
245+
246+
247+
@pytest.mark.use_mlir
248+
def test_load_store_check_bounds_default():
249+
# The default check_bounds=True emits no `inbounds` attribute.
250+
x = make_tensor((32,), dtype=torch.float16, device="cuda")
251+
y = torch.zeros_like(x)
252+
bytecode = get_bytecode(array_copy_1d, (x, y, 16))
253+
filecheck(bytecode, "\n".join([
254+
"// CHECK: load_view_tko",
255+
"// CHECK-NOT: inbounds",
256+
]))
257+
258+
259+
@pytest.mark.skipif(get_tileiras_version() >= BytecodeVersion.V_13_4,
260+
reason="check_bounds=False is supported on tileiras 13.4+")
261+
def test_check_bounds_requires_13_4():
262+
x = make_tensor((64, 64), dtype=torch.float16, device="cuda")
263+
y = torch.zeros_like(x)
264+
with pytest.raises(TileUnsupportedFeatureError, match="check_bounds=False.*requires tileiras"):
265+
get_bytecode(copy_2d_no_check_bounds, (x, y, 32, 32))

0 commit comments

Comments
 (0)