Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function main()
"man/performance.md",
"man/compatibility.md",
"man/debugging.md",
"man/julia_differences.md",
"man/comparison.md",
],
"API reference" => [
Expand Down
2 changes: 2 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ CUDA.jl's documentation for that shared functionality.
flow.
- [Comparison with cuTile Python](man/comparison.md) — if you are porting from
`cuda.tile`, start here.
- [Differences from Julia](man/julia_differences.md) — where kernel semantics
diverge from host Julia.
- [Debugging](man/debugging.md) — inspecting generated Tile IR.

The [Tile IR specification](https://docs.nvidia.com/cuda/tile-ir/) is the
Expand Down
4 changes: 4 additions & 0 deletions docs/src/man/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,10 @@ in cuTile Python's operation reference.
dimension arguments are 1-based, and `step` is cuTile.jl's name for Python's
`traversal_steps`.

Both APIs keep tile loads and stores checked by default: partial loads use their
requested padding and partial stores clip to the array. In both, an explicit
`check_bounds=false` is the unsafe promise that the whole tile is in bounds.

### Factory

| cuTile Python | cuTile.jl |
Expand Down
2 changes: 1 addition & 1 deletion docs/src/man/element_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,4 @@ v13.3 or newer.

Float-to-integer conversions do not throw in kernels; they truncate toward
zero rather than raising `InexactError`. See [Differences from
Julia](kernels.md#Differences-from-Julia).
Julia](julia_differences.md#Some-operations-are-non-throwing).
35 changes: 35 additions & 0 deletions docs/src/man/julia_differences.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Differences from Julia

cuTile kernels are ordinary Julia functions, and cuTile.jl uses Julia semantics
where Tile IR can express them. This page covers the important exceptions.


## Exceptions become traps

Tile IR has no exceptions or stack unwinding. A conditional `throw`, `error`,
or failing `Base.@assert` becomes a device-side assertion; an unconditional
one becomes a compile-time diagnostic. `try`/`catch` is not supported. Use
`ct.@assert` for explicit runtime checks.


## Some operations are non-throwing

Float-to-integer conversions such as `Int32(x)`, `trunc(Int32, x)`, and
`round(Int32, x, RoundToZero)` truncate toward zero rather than throwing
`InexactError`. They behave like Julia's explicit `unsafe_trunc` primitive.


## Tile bounds handling is not `BoundsError`

Julia bounds checks are protective: a correct access has the same result with
or without its check. Tile IR's checked memory operations instead define
partial edge tiles: loads pad missing elements and stores clip them. A tile
whose origin is valid may still extend past the array, so that padding or
clipping is part of a correct kernel's result.

Consequently, `@inbounds` does not alter `ct.load` or `ct.store` bounds
handling. `@inbounds` and `--check-bounds` still control ordinary Julia
`@boundscheck` blocks in a kernel, such as checks in Base range indexing. For
tile memory, use `check_bounds=false` only as an explicit promise that the
whole tile lies within its array. It drops padding, selects Tile IR's unchecked
encoding, and requires bytecode v13.4 or newer.
14 changes: 3 additions & 11 deletions docs/src/man/kernels.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,6 @@ operations:

## Differences from Julia

### Some operations are non-throwing

cuTile kernels cannot throw Julia exceptions. Operations that would throw in standard Julia
silently produce truncated or wrapped results instead:

- **Float-to-integer conversions:** `Int32(x)`, `trunc(Int32, x)`, and
`round(Int32, x, RoundToZero)` silently truncate toward zero rather than throwing
`InexactError` for non-integer or out-of-range values. Use `unsafe_trunc` for the explicit
non-throwing primitive.

Use `ct.@assert` to add runtime checks in kernels; see [Debugging](debugging.md).
Kernels compile with Julia semantics wherever Tile IR can express them, but
exceptions, some conversions, and bounds handling need different machinery.
See [Differences from Julia](julia_differences.md).
15 changes: 15 additions & 0 deletions docs/src/man/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ requirements mentioned on this page are collected in
[Compatibility](compatibility.md).


## Bounds checking

`ct.load` and `ct.store` are bounds-checked by default: a tile that partially
extends past the array is padded on load (per `padding_mode`) and clipped on
store. Set `check_bounds=false` only when every element of the tile is in the
underlying array. That is an unsafe full-tile promise: it drops load padding,
emits Tile IR's `inbounds` attribute, and requires bytecode v13.4 or newer.

Tile memory bounds handling is not Julia's protective bounds checking.
`@inbounds` and `--check-bounds=yes|no` affect real Julia `@boundscheck`
blocks, but do not change the padding, clipping, masks, or Tile IR attributes
of `ct.load`, `ct.store`, `eachtile`, sparse views, gathers, scatters, or
atomics. Use the explicit keyword when the stronger full-tile promise holds.


## Automatic rank matching

`ct.load` and `ct.store` automatically match the tile rank to that of the target:
Expand Down
14 changes: 8 additions & 6 deletions src/compiler/transform/canonicalize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ const INTRINSIC_RULES = RewriteRule[
@rewrite Core.Intrinsics.or_int(~x, ~y) => Intrinsics.ori(~x, ~y)
@rewrite Core.Intrinsics.xor_int(~x, ~y) => Intrinsics.xori(~x, ~y)

# not_int: xori with all-ones constant (type-dependent)
@rewrite Core.Intrinsics.not_int(~x::Tile{Bool}) => Intrinsics.xori(~x, $(true))
@rewrite Core.Intrinsics.not_int(~x::Tile{Int32}) => Intrinsics.xori(~x, $(Int32(-1)))
@rewrite Core.Intrinsics.not_int(~x::Tile{Int64}) => Intrinsics.xori(~x, $(Int64(-1)))
@rewrite Core.Intrinsics.not_int(~x::Tile{UInt32}) => Intrinsics.xori(~x, $(~UInt32(0)))
@rewrite Core.Intrinsics.not_int(~x::Tile{UInt64}) => Intrinsics.xori(~x, $(~UInt64(0)))
# not_int: xori with all-ones constant (type-dependent). Unlike the binary
# bitwise ops above, the constant makes these rules type-directed, so each
# one has to admit the scalar form too (`!` on a Bool, `~` on an integer).
@rewrite Core.Intrinsics.not_int(~x::Union{Bool, Tile{Bool}}) => Intrinsics.xori(~x, $(true))
@rewrite Core.Intrinsics.not_int(~x::Union{Int32, Tile{Int32}}) => Intrinsics.xori(~x, $(Int32(-1)))
@rewrite Core.Intrinsics.not_int(~x::Union{Int64, Tile{Int64}}) => Intrinsics.xori(~x, $(Int64(-1)))
@rewrite Core.Intrinsics.not_int(~x::Union{UInt32, Tile{UInt32}}) => Intrinsics.xori(~x, $(~UInt32(0)))
@rewrite Core.Intrinsics.not_int(~x::Union{UInt64, Tile{UInt64}}) => Intrinsics.xori(~x, $(~UInt64(0)))

# Float arithmetic
@rewrite Core.Intrinsics.add_float(~x, ~y) => Intrinsics.addf(~x, ~y)
Expand Down
26 changes: 16 additions & 10 deletions src/language/operations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -498,17 +498,17 @@ given padding mode on loads and clipped stores.
build_tiled_view(a, Val(tile_shape), Val(step), Val(padding_mode), Val(order))
end

@inline function make_tile_view(tiles::TiledView{A, RequestedShape, Shape, Shape, Padding}) where
{A, RequestedShape, Shape, Padding}
@inline function make_tile_view(tiles::TiledView{A, RequestedShape, Shape, Shape, Padding},
padding=Padding) where {A, RequestedShape, Shape, Padding}
parent = tiles.parent
tv = Intrinsics.make_tensor_view(typeof(parent), parent.ptr, parent.sizes, parent.strides)
Intrinsics.make_partition_view(tv, tiled_view_shape(tiles), Padding, tiled_view_order(tiles))
Intrinsics.make_partition_view(tv, tiled_view_shape(tiles), padding, tiled_view_order(tiles))
end
@inline function make_tile_view(tiles::TiledView{A, RequestedShape, Shape, Step, Padding}) where
{A, RequestedShape, Shape, Step, Padding}
@inline function make_tile_view(tiles::TiledView{A, RequestedShape, Shape, Step, Padding},
padding=Padding) where {A, RequestedShape, Shape, Step, Padding}
parent = tiles.parent
tv = Intrinsics.make_tensor_view(typeof(parent), parent.ptr, parent.sizes, parent.strides)
Intrinsics.make_strided_view(tv, tiled_view_shape(tiles), tiled_view_step(tiles), Padding,
Intrinsics.make_strided_view(tv, tiled_view_shape(tiles), tiled_view_step(tiles), padding,
tiled_view_order(tiles))
end

Expand All @@ -526,7 +526,8 @@ end
latency::Union{Int, Nothing}=nothing,
allow_tma::Union{Bool, Nothing}=nothing) where {A, N}
N == ndims(tiles) || throw(ArgumentError("eachtile: expected $(ndims(tiles)) tile indices, got $N"))
view = make_tile_view(tiles)
view = check_bounds ? make_tile_view(tiles) :
make_tile_view(tiles, PaddingMode.Undetermined)
tile = load_tile_view(view, latency, allow_tma, promote(index...) .- One(), check_bounds)
reshape(tile, tiled_view_requested_shape(tiles))
end
Expand Down Expand Up @@ -631,7 +632,9 @@ tile = ct.load(arr, (bidy, bidx), (TN, TM); order=(2, 1))
allow_tma::Union{Bool, Nothing}=nothing)
matched = _match_shape(Val(shape), Val(ndims(arr)))
tv = Intrinsics.make_tensor_view(typeof(arr), arr.ptr, arr.sizes, arr.strides)
pv = Intrinsics.make_partition_view(tv, matched, padding_mode, order)
pv = check_bounds ?
Intrinsics.make_partition_view(tv, matched, padding_mode, order) :
Intrinsics.make_partition_view(tv, matched, PaddingMode.Undetermined, order)
tile = Intrinsics.load_partition_view(
pv, latency, allow_tma, promote(index...) .- One(), check_bounds)
reshape(tile, shape)
Expand Down Expand Up @@ -709,8 +712,11 @@ The sparse tile index and dense range starts are one-based at this boundary.
parent_array = parent(view)
tensor_view = Intrinsics.make_tensor_view(typeof(parent_array), parent_array.ptr,
parent_array.sizes, parent_array.strides)
gather_view = Intrinsics.make_gather_scatter_view(
tensor_view, shape, gather_scatter_sparse_dim(view), padding_mode)
sparse_dim = gather_scatter_sparse_dim(view)
gather_view = check_bounds ?
Intrinsics.make_gather_scatter_view(tensor_view, shape, sparse_dim, padding_mode) :
Intrinsics.make_gather_scatter_view(tensor_view, shape, sparse_dim,
PaddingMode.Undetermined)
Intrinsics.load_gather_scatter_view(
gather_view, latency, allow_tma, _gather_scatter_indices(view), check_bounds)
end
Expand Down
107 changes: 107 additions & 0 deletions test/codegen/boundscheck_views.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Tile memory bounds handling is distinct from Julia's protective bounds checks:
# `@inbounds` must not discard a partial tile's padding or store clipping.

const view_spec = ct.ArraySpec{1}(64, true)
const ViewArray = ct.TileArray{Float32, 1, view_spec}
const view2_spec = ct.ArraySpec{2}(64, true)
const ViewArray2 = ct.TileArray{Float32, 2, view2_spec}

direct_plain(a) = (ct.store(a, ct.bid(1),
ct.load(a, ct.bid(1), (16,); padding_mode=ct.PaddingMode.Zero)); return)
direct_inbounds(a) = (@inbounds ct.store(a, ct.bid(1),
ct.load(a, ct.bid(1), (16,); padding_mode=ct.PaddingMode.Zero)); return)
direct_explicit_checked(a) = (@inbounds ct.store(a, ct.bid(1),
ct.load(a, ct.bid(1), (16,);
padding_mode=ct.PaddingMode.Zero,
check_bounds=true)); return)
direct_explicit_unchecked(a) = (ct.store(a, ct.bid(1),
ct.load(a, ct.bid(1), (16,);
padding_mode=ct.PaddingMode.Zero,
check_bounds=false);
check_bounds=false); return)

function tiled_plain(a)
tiles = ct.eachtile(a, (16,); padding_mode=ct.PaddingMode.Zero)
tiles[ct.bid(1)] = tiles[ct.bid(1)]
return
end
function tiled_inbounds(a)
@inbounds begin
tiles = ct.eachtile(a, (16,); padding_mode=ct.PaddingMode.Zero)
tiles[ct.bid(1)] = tiles[ct.bid(1)]
end
return
end
function strided_inbounds(a)
@inbounds begin
tiles = ct.eachtile(a, (16,); step=(32,), padding_mode=ct.PaddingMode.Zero)
tiles[ct.bid(1)] = tiles[ct.bid(1)]
end
return
end

function gather_scatter_plain(a)
rows = ct.arange(4)
view = @view a[rows, Int32(1):Int32(4)]
tile = ct.load(view, (4, 4); padding_mode=ct.PaddingMode.Zero)
ct.store(view, tile)
return
end
function gather_scatter_inbounds(a)
@inbounds begin
rows = ct.arange(4)
view = @view a[rows, Int32(1):Int32(4)]
tile = ct.load(view, (4, 4); padding_mode=ct.PaddingMode.Zero)
ct.store(view, tile)
end
return
end

scalar_plain(a) = (a[1] = a[2]; return)
scalar_inbounds(a) = (@inbounds (a[1] = a[2]); return)

function view_ir(@nospecialize(f), @nospecialize(argtypes); bytecode_version)
sprint(io -> code_tiled(io, f, argtypes; bytecode_version))
end

unchecked_view_ops(f, argtypes; bytecode_version) =
count(line -> occursin("inbounds = [true", line),
split(view_ir(f, argtypes; bytecode_version), '\n'))

has_padding(f, argtypes; bytecode_version) =
occursin("padding_value = zero", view_ir(f, argtypes; bytecode_version))

@testset "Tile views do not inherit @inbounds" begin
one_dim = Tuple{ViewArray}
two_dim = Tuple{ViewArray2}

# v13.3 cannot encode unchecked views, but `@inbounds` is irrelevant to
# Tile memory operations: all of these must stay checked and padded.
for (f, argtypes) in ((direct_plain, one_dim), (direct_inbounds, one_dim),
(direct_explicit_checked, one_dim),
(tiled_plain, one_dim), (tiled_inbounds, one_dim),
(strided_inbounds, one_dim),
(gather_scatter_plain, two_dim),
(gather_scatter_inbounds, two_dim),
(scalar_plain, one_dim), (scalar_inbounds, one_dim))
@test unchecked_view_ops(f, argtypes; bytecode_version=v"13.3") == 0
end

for f in (direct_plain, direct_inbounds, direct_explicit_checked,
tiled_plain, tiled_inbounds, strided_inbounds)
@test has_padding(f, one_dim; bytecode_version=v"13.3")
end
for f in (gather_scatter_plain, gather_scatter_inbounds)
@test has_padding(f, two_dim; bytecode_version=v"13.3")
end

if ct.bytecode_version() >= v"13.4"
# An explicit false is the only way to select the v13.4 `inbounds`
# encoding. It also drops padding, which Tile IR rejects on an
# unchecked view.
@test unchecked_view_ops(direct_explicit_unchecked, one_dim;
bytecode_version=ct.bytecode_version()) == 2
@test !has_padding(direct_explicit_unchecked, one_dim;
bytecode_version=ct.bytecode_version())
end
end
40 changes: 40 additions & 0 deletions test/device/hints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,31 @@ end
@test Array(b) ≈ Array(a)
end

@testset "tile bounds do not inherit @inbounds" begin
function ragged_tiles(a::ct.TileArray{Float32,1}, b::ct.TileArray{Float32,1})
pid = ct.bid(1)
tile = ct.load(a, pid, (16,); padding_mode=ct.PaddingMode.Zero)
ct.store(b, pid, sum(tile; dims=1))
return nothing
end
function ragged_tiles_inbounds(a::ct.TileArray{Float32,1}, b::ct.TileArray{Float32,1})
pid = ct.bid(1)
@inbounds begin
tile = ct.load(a, pid, (16,); padding_mode=ct.PaddingMode.Zero)
ct.store(b, pid, sum(tile; dims=1))
end
return nothing
end

a = CUDA.rand(Float32, 17)
expected = Float32[sum(Array(a)[1:16]), Array(a)[17]]
for kernel in (ragged_tiles, ragged_tiles_inbounds)
b = CUDA.zeros(Float32, 2)
@cuda backend=cuTile blocks=2 kernel(a, b)
@test Array(b) ≈ expected
end
end

if cuTile.bytecode_version() >= v"13.4"
@testset "load/store with check_bounds=false" begin
function vadd_unchecked(a::ct.TileArray{Float32,1},
Expand Down Expand Up @@ -303,6 +328,21 @@ end

@test Array(b) ≈ Array(a)
end

@testset "unchecked load ignores padding_mode" begin
function padded_unchecked(a::ct.TileArray{Float32,1}, b::ct.TileArray{Float32,1})
pid = ct.bid(1)
tile = ct.load(a, pid, (16,); padding_mode=ct.PaddingMode.Zero,
check_bounds=false)
ct.store(b, pid, tile; check_bounds=false)
return nothing
end

a = CUDA.rand(Float32, 1024)
b = CUDA.zeros(Float32, 1024)
@cuda backend=cuTile blocks=64 padded_unchecked(a, b)
@test Array(b) ≈ Array(a)
end
end

end
22 changes: 22 additions & 0 deletions test/device/math.jl
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@ end
@test Array(c) == Array(a) .⊻ Array(b)
end

@testset "not_int on scalars" begin
# `!` on a Bool and `~` on an integer both lower through `not_int`, which
# needs a per-type all-ones constant — so unlike the binary ops above, its
# rules are type-directed and have to cover the scalar form explicitly.
function scalar_not_kernel(a::ct.TileArray{Int32,1}, b::ct.TileArray{Int32,1})
pid = ct.bid(1)
flag = pid > Int32(1)
value = a[pid]
ct.store(b, pid, (!flag ? Int32(1) : Int32(0)) + ~value)
return
end

n = 64
a = CuArray(rand(Int32(0):Int32(0x7fff_ffff), n))
b = CUDA.zeros(Int32, n)

@cuda backend=cuTile blocks=n scalar_not_kernel(a, b)

expected = (.~Array(a)) .+ Int32[i == 1 for i in 1:n]
@test Array(b) == expected
end

@testset "shli (shift left)" begin
function shift_left_kernel(a::ct.TileArray{Int32,1}, b::ct.TileArray{Int32,1})
pid = ct.bid(1)
Expand Down