Skip to content

Commit ac0ecb8

Browse files
maleadtclaude
andauthored
Store tile shapes in row-major order (#142)
Tile IR is natively row-major (Python cuTile passes shapes verbatim). We were passing Julia's column-major shapes through as-is, then compensating with per-operation fixups: reshape emitted a permute-reshape-permute sandwich, and batched matmul emitted 4 permute ops to convert trailing batch dims to MmaFOp's leading batch convention. Instead, reverse all shapes at the Julia↔Tile IR boundary: Julia (M, K, B) → Tile IR (B, K, M). CGVal.shape now stores Tile IR (row-major) order. Conversion happens in three functions: _tile_type_for_julia!, tile_type_and_shape_for_julia!, and extract_tile_shape. This eliminates the reshape double-permute (now a direct ReshapeOp) and all matmul permutes (operands swapped: mmaf(b, a, acc) computes (N,K)@(K,M)=(N,M) → Julia (M,N), which is correct). Axes for reduce/scan/cat are flipped (tileir_axis = ndim-1-julia_axis), indices for load/store are reversed, and tensor view sizes/strides are reversed to match. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent caf0027 commit ac0ecb8

14 files changed

Lines changed: 269 additions & 207 deletions

File tree

src/compiler/codegen/kernel.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ function emit_kernel!(writer::BytecodeWriter, func_buf::Vector{UInt8},
119119
# Scalar: emit ConstantOp
120120
bytes = constant_to_bytes(val, T)
121121
v = encode_ConstantOp!(ctx.cb, type_id, bytes)
122-
tv = CGVal(v, type_id, T, Int[], nothing, Some(val), nothing)
122+
tv = CGVal(v, type_id, T, ScalarShape(), nothing, Some(val), nothing)
123123
else
124124
# Non-primitive (tuple etc.): ghost with constant
125125
tv = ghost_value(T, val)

src/compiler/codegen/utils.jl

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,64 @@
22
#
33
# Core types (CGVal, CGCtx) and helper functions for Tile IR code generation.
44

5+
#=============================================================================
6+
Type-safe shape wrappers: Julia (column-major) ↔ Tile IR (row-major)
7+
=============================================================================#
8+
9+
# Tile IR is natively row-major: shapes are stored with the slowest-varying dimension first.
10+
# Julia is column-major: shapes are stored with the fastest-varying dimension first.
11+
# Converting between them is a simple reversal. The Shape{O} wrapper ensures we don't
12+
# accidentally mix up conventions — IR operations accept only RowMajorShape, while
13+
# user-facing shapes from Julia are ColMajorShape.
14+
15+
abstract type StorageOrder end
16+
struct RowMajor <: StorageOrder end
17+
struct ColMajor <: StorageOrder end
18+
19+
struct Shape{O<:StorageOrder}
20+
dims::Vector{Int}
21+
end
22+
23+
const RowMajorShape = Shape{RowMajor}
24+
const ColMajorShape = Shape{ColMajor}
25+
26+
# Conversion constructors
27+
RowMajorShape(s::RowMajorShape) = s
28+
RowMajorShape(s::ColMajorShape) = RowMajorShape(reverse(s.dims))
29+
RowMajorShape(t::Tuple) = RowMajorShape(ColMajorShape(collect(Int, t)))
30+
31+
ColMajorShape(s::ColMajorShape) = s
32+
ColMajorShape(s::RowMajorShape) = ColMajorShape(reverse(s.dims))
33+
ColMajorShape(t::Tuple) = ColMajorShape(collect(Int, t))
34+
35+
# Forward common operations to .dims
36+
Base.length(s::Shape) = length(s.dims)
37+
Base.isempty(s::Shape) = isempty(s.dims)
38+
Base.getindex(s::Shape, i) = s.dims[i]
39+
Base.setindex!(s::Shape, v, i) = (s.dims[i] = v; s)
40+
Base.copy(s::Shape{O}) where O = Shape{O}(copy(s.dims))
41+
Base.:(==)(a::Shape{O}, b::Shape{O}) where O = a.dims == b.dims
42+
Base.iterate(s::Shape, state...) = iterate(s.dims, state...)
43+
Base.eachindex(s::Shape) = eachindex(s.dims)
44+
Base.collect(s::Shape) = s.dims
45+
TupleType(s::Shape) = Tuple{s.dims...}
46+
47+
# Scalar (0-D) shape — storage order is irrelevant for zero dimensions
48+
struct ScalarShape end
49+
Base.length(::ScalarShape) = 0
50+
Base.isempty(::ScalarShape) = true
51+
Base.collect(::ScalarShape) = Int[]
52+
TupleType(::ScalarShape) = Tuple{}
53+
54+
Base.:(==)(::ScalarShape, ::ScalarShape) = true
55+
Base.:(==)(::ScalarShape, ::Shape) = false
56+
Base.:(==)(::Shape, ::ScalarShape) = false
57+
58+
# Cross-type conversions (must be after ScalarShape definition)
59+
ColMajorShape(::ScalarShape) = ColMajorShape(Int[])
60+
61+
const TileShape = Union{RowMajorShape, ScalarShape}
62+
563
#=============================================================================
664
IRError: Exception type for IR compilation errors
765
=============================================================================#
@@ -69,7 +127,7 @@ struct CGVal
69127
v::Union{Value, Vector{Value}, Nothing} # Single value, multi-value, or nothing
70128
type_id::Union{TypeId, Nothing} # Tile IR type (nothing for lazy refs or multi-value)
71129
jltype::Any # Original Julia type
72-
shape::Vector{Int} # Tile shape (empty for scalars)
130+
shape::TileShape # Tile shape (ScalarShape for scalars)
73131
# Lazy argument reference: (arg_idx, [field_indices...])
74132
# e.g., (1, [2, 1]) means "argument 1, field 2, sub-field 1"
75133
arg_ref::Union{Tuple{Int, Vector{Int}}, Nothing}
@@ -79,18 +137,18 @@ end
79137

80138
# Convenience constructors for concrete values
81139
CGVal(v::Value, type_id::TypeId, @nospecialize(jltype)) =
82-
CGVal(v, type_id, jltype, Int[], nothing, nothing, nothing)
140+
CGVal(v, type_id, jltype, ScalarShape(), nothing, nothing, nothing)
83141

84-
CGVal(v::Value, type_id::TypeId, @nospecialize(jltype), shape::Vector{Int}) =
142+
CGVal(v::Value, type_id::TypeId, @nospecialize(jltype), shape::TileShape) =
85143
CGVal(v, type_id, jltype, shape, nothing, nothing, nothing)
86144

87145
# Constructor for multi-value results (from loops, ifs)
88146
CGVal(v::Vector{Value}, @nospecialize(jltype)) =
89-
CGVal(v, nothing, jltype, Int[], nothing, nothing, nothing)
147+
CGVal(v, nothing, jltype, ScalarShape(), nothing, nothing, nothing)
90148

91149
# Constructor for lazy argument references
92150
function arg_ref_value(arg_idx::Int, chain::Vector{Int}, @nospecialize(jltype))
93-
CGVal(nothing, nothing, jltype, Int[], (arg_idx, chain), nothing, nothing)
151+
CGVal(nothing, nothing, jltype, ScalarShape(), (arg_idx, chain), nothing, nothing)
94152
end
95153

96154
"""
@@ -99,8 +157,8 @@ end
99157
Create a ghost value (zero-size singleton with no runtime representation).
100158
Optionally stores a compile-time constant value.
101159
"""
102-
ghost_value(@nospecialize(jltype)) = CGVal(nothing, TypeId(-1), jltype, Int[], nothing, nothing, nothing)
103-
ghost_value(@nospecialize(jltype), constant) = CGVal(nothing, TypeId(-1), jltype, Int[], nothing, Some(constant), nothing)
160+
ghost_value(@nospecialize(jltype)) = CGVal(nothing, TypeId(-1), jltype, ScalarShape(), nothing, nothing, nothing)
161+
ghost_value(@nospecialize(jltype), constant) = CGVal(nothing, TypeId(-1), jltype, ScalarShape(), nothing, Some(constant), nothing)
104162

105163
"""
106164
tuple_value(jltype, component_refs, component_constants) -> CGVal
@@ -115,7 +173,7 @@ function tuple_value(@nospecialize(jltype), component_refs::Vector{Any}, compone
115173
else
116174
nothing
117175
end
118-
CGVal(nothing, TypeId(-1), jltype, Int[], nothing, constant, component_refs)
176+
CGVal(nothing, TypeId(-1), jltype, ScalarShape(), nothing, constant, component_refs)
119177
end
120178

121179
"""
@@ -382,27 +440,27 @@ function _tile_type_for_julia!(tt::TypeTable, @nospecialize(T::Type))
382440
throw(IRError("Tile shape must be a tuple, got: $shape_param"))
383441
end
384442
elem_dtype = julia_to_tile_dtype!(tt, eltype(T))
385-
shape = collect(Int, shape_param)
386-
return tile_type!(tt, elem_dtype, shape)
443+
shape = RowMajorShape(shape_param)
444+
return tile_type!(tt, elem_dtype, collect(shape))
387445
end
388446

389447
return nothing
390448
end
391449

392450
"""
393-
tile_type_and_shape_for_julia!(ctx, T) -> (TypeId, Vector{Int})
451+
tile_type_and_shape_for_julia!(ctx, T) -> (TypeId, RowMajorShape)
394452
395453
Get the Tile IR type and shape for a Julia type.
396454
"""
397455
function tile_type_and_shape_for_julia!(ctx::CGCtx, @nospecialize(T))
398456
actual_type = CC.widenconst(T)
399457
type_id = tile_type_for_julia!(ctx, actual_type)
400458

401-
# Extract shape from Tile types
459+
# Extract shape from Tile types (in Tile IR row-major order)
402460
shape = if actual_type <: Tile
403-
collect(Int, size(actual_type))
461+
RowMajorShape(size(actual_type))
404462
else
405-
Int[]
463+
ScalarShape()
406464
end
407465

408466
return (type_id, shape)
@@ -489,14 +547,15 @@ end
489547
#-----------------------------------------------------------------------------
490548

491549
"""
492-
extract_tile_shape(T) -> Vector{Int}
550+
extract_tile_shape(T) -> RowMajorShape
493551
494-
Extract shape from a Tile{T, Shape} type, returning Int[] if not a Tile type.
552+
Extract shape from a Tile{T, Shape} type in Tile IR (row-major) order.
553+
Returns empty shape if not a Tile type.
495554
"""
496555
function extract_tile_shape(@nospecialize(T))
497556
T = CC.widenconst(T)
498557
if T <: Tile
499-
return collect(Int, size(T))
558+
return RowMajorShape(size(T))
500559
end
501-
Int[]
560+
ScalarShape()
502561
end

src/compiler/codegen/values.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ function emit_value!(ctx::CGCtx, val::Integer)
1919
type_id = tile_type_for_julia!(ctx, jltype)
2020
bytes = reinterpret(UInt8, [jltype(val)])
2121
v = encode_ConstantOp!(ctx.cb, type_id, collect(bytes))
22-
CGVal(v, type_id, jltype, Int[], nothing, Some(val), nothing)
22+
CGVal(v, type_id, jltype, ScalarShape(), nothing, Some(val), nothing)
2323
end
2424

2525
function emit_value!(ctx::CGCtx, val::AbstractFloat)
2626
jltype = typeof(val)
2727
type_id = tile_type_for_julia!(ctx, jltype)
2828
bytes = reinterpret(UInt8, [jltype(val)])
2929
v = encode_ConstantOp!(ctx.cb, type_id, collect(bytes))
30-
CGVal(v, type_id, jltype, Int[], nothing, Some(val), nothing)
30+
CGVal(v, type_id, jltype, ScalarShape(), nothing, Some(val), nothing)
3131
end
3232

3333
function emit_value!(ctx::CGCtx, node::QuoteNode)
@@ -67,7 +67,7 @@ function emit_value!(ctx::CGCtx, ref::GlobalRef)
6767
if type_id !== nothing
6868
bytes = constant_to_bytes(val, T)
6969
v = encode_ConstantOp!(ctx.cb, type_id, bytes)
70-
return CGVal(v, type_id, T, Int[], nothing, Some(val), nothing)
70+
return CGVal(v, type_id, T, ScalarShape(), nothing, Some(val), nothing)
7171
end
7272
end
7373
ghost_value(T, val)

src/compiler/intrinsics/arithmetic.jl

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,23 @@ function emit_binop!(ctx::CGCtx, args, encoder::Function; kwargs...)
5252
dtype = julia_to_tile_dtype!(tt, elem_type)
5353
if isempty(lhs_tv.shape)
5454
bv = broadcast_tile_to_shape!(cb, tt, lhs_tv, result_shape, dtype)
55-
lhs_tv = CGVal(bv, tile_type!(tt, dtype, result_shape), elem_type,
55+
lhs_tv = CGVal(bv, tile_type!(tt, dtype, collect(result_shape)), elem_type,
5656
result_shape, nothing, lhs_tv.constant, nothing)
5757
elseif isempty(rhs_tv.shape)
5858
bv = broadcast_tile_to_shape!(cb, tt, rhs_tv, result_shape, dtype)
59-
rhs_tv = CGVal(bv, tile_type!(tt, dtype, result_shape), elem_type,
59+
rhs_tv = CGVal(bv, tile_type!(tt, dtype, collect(result_shape)), elem_type,
6060
result_shape, nothing, rhs_tv.constant, nothing)
6161
end
6262
else
63-
result_shape = Int[]
63+
result_shape = ScalarShape()
6464
end
6565
result_jltype = lhs_tv.jltype
6666
else
6767
throw(IRError("Mixed tile/scalar operations should be handled at intrinsic level via Tile() and broadcast_to()"))
6868
end
6969

7070
dtype = julia_to_tile_dtype!(tt, elem_type)
71-
result_type_id = tile_type!(tt, dtype, result_shape)
71+
result_type_id = tile_type!(tt, dtype, collect(result_shape))
7272

7373
result_v = encoder(cb, result_type_id, lhs_tv.v, rhs_tv.v; kwargs...)
7474

@@ -88,7 +88,7 @@ function emit_unop!(ctx::CGCtx, args, encoder::Function; kwargs...)
8888
result_jltype = source.jltype
8989

9090
dtype = julia_to_tile_dtype!(tt, elem_type)
91-
result_type_id = tile_type!(tt, dtype, result_shape)
91+
result_type_id = tile_type!(tt, dtype, collect(result_shape))
9292

9393
result_v = encoder(cb, result_type_id, source.v; kwargs...)
9494

@@ -149,7 +149,7 @@ function emit_intrinsic!(ctx::CGCtx, ::typeof(Intrinsics.cmpi), args)
149149
result_shape = lhs.shape
150150

151151
bool_dtype = I1(tt)
152-
result_type_id = tile_type!(tt, bool_dtype, result_shape)
152+
result_type_id = tile_type!(tt, bool_dtype, collect(result_shape))
153153

154154
result_v = encode_CmpIOp!(cb, result_type_id, lhs.v, rhs.v; predicate, signedness)
155155
lhs_type = CC.widenconst(lhs.jltype)
@@ -295,7 +295,7 @@ function emit_intrinsic!(ctx::CGCtx, ::typeof(Intrinsics.cmpf), args)
295295
result_shape = lhs.shape
296296

297297
bool_dtype = I1(tt)
298-
result_type_id = tile_type!(tt, bool_dtype, result_shape)
298+
result_type_id = tile_type!(tt, bool_dtype, collect(result_shape))
299299

300300
result_v = encode_CmpFOp!(cb, result_type_id, lhs.v, rhs.v; predicate)
301301
lhs_type = CC.widenconst(lhs.jltype)

src/compiler/intrinsics/atomics.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ function emit_intrinsic!(ctx::CGCtx, ::typeof(Intrinsics.atomic_cas), args)
7272
end
7373
ctx.token = new_token
7474

75-
CGVal(old_val, result_tile_type, Tile{elem_type, Tuple{shape...}}, collect(shape))
75+
julia_shape = ColMajorShape(shape)
76+
CGVal(old_val, result_tile_type, Tile{elem_type, TupleType(julia_shape)}, shape)
7677
end
7778

7879
# cuda_tile.atomic_rmw_tko (shared helper for atomic RMW operations)
@@ -129,7 +130,8 @@ function emit_atomic_rmw!(ctx::CGCtx, args::AbstractVector, mode::AtomicRMWMode.
129130
end
130131
ctx.token = new_token
131132

132-
CGVal(old_val, result_tile_type, Tile{elem_type, Tuple{shape...}}, collect(shape))
133+
julia_shape = ColMajorShape(shape)
134+
CGVal(old_val, result_tile_type, Tile{elem_type, TupleType(julia_shape)}, shape)
133135
end
134136

135137
# cuda_tile.atomic_rmw_tko variants

src/compiler/intrinsics/conversions.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function emit_intrinsic!(ctx::CGCtx, ::typeof(Intrinsics.exti), args)
1919
signedness = @something get_constant(ctx, args[3]) throw(IRError("exti: requires compile-time signedness"))
2020

2121
dtype = julia_to_tile_dtype!(tt, target_type)
22-
result_type_id = tile_type!(tt, dtype, source.shape)
22+
result_type_id = tile_type!(tt, dtype, collect(source.shape))
2323

2424
result_v = encode_ExtIOp!(cb, result_type_id, source.v; signedness)
2525
src_type = CC.widenconst(source.jltype)
@@ -43,7 +43,7 @@ function emit_intrinsic!(ctx::CGCtx, ::typeof(Intrinsics.ftof), args)
4343
target_type = @something get_constant(ctx, args[2]) throw(IRError("ftof: requires compile-time target type"))
4444

4545
dtype = julia_to_tile_dtype!(tt, target_type)
46-
result_type_id = tile_type!(tt, dtype, source.shape)
46+
result_type_id = tile_type!(tt, dtype, collect(source.shape))
4747

4848
result_v = encode_FToFOp!(cb, result_type_id, source.v)
4949
src_type = CC.widenconst(source.jltype)
@@ -68,7 +68,7 @@ function emit_intrinsic!(ctx::CGCtx, ::typeof(Intrinsics.ftoi), args)
6868
signedness = @something get_constant(ctx, args[3]) throw(IRError("ftoi: requires compile-time signedness"))
6969

7070
dtype = julia_to_tile_dtype!(tt, target_type)
71-
result_type_id = tile_type!(tt, dtype, source.shape)
71+
result_type_id = tile_type!(tt, dtype, collect(source.shape))
7272

7373
result_v = encode_FToIOp!(cb, result_type_id, source.v; signedness)
7474
src_type = CC.widenconst(source.jltype)
@@ -93,7 +93,7 @@ function emit_intrinsic!(ctx::CGCtx, ::typeof(Intrinsics.itof), args)
9393
signedness = @something get_constant(ctx, args[3]) throw(IRError("itof: requires compile-time signedness"))
9494

9595
dtype = julia_to_tile_dtype!(tt, target_type)
96-
result_type_id = tile_type!(tt, dtype, source.shape)
96+
result_type_id = tile_type!(tt, dtype, collect(source.shape))
9797

9898
result_v = encode_IToFOp!(cb, result_type_id, source.v; signedness)
9999
src_type = CC.widenconst(source.jltype)
@@ -117,7 +117,7 @@ function emit_intrinsic!(ctx::CGCtx, ::typeof(Intrinsics.trunci), args)
117117
target_type = @something get_constant(ctx, args[2]) throw(IRError("trunci: requires compile-time target type"))
118118

119119
dtype = julia_to_tile_dtype!(tt, target_type)
120-
result_type_id = tile_type!(tt, dtype, source.shape)
120+
result_type_id = tile_type!(tt, dtype, collect(source.shape))
121121

122122
result_v = encode_TruncIOp!(cb, result_type_id, source.v)
123123
src_type = CC.widenconst(source.jltype)

0 commit comments

Comments
 (0)