All skills
nvidia avatar

/tilegym-converting-cutile-to-julia

@2bf003b
by NVIDIA Corporationnvidia/tilegym821 stars
89

Converts cuTile Python GPU kernels (@ct.kernel) to cuTile.jl Julia equivalents. Handles kernel syntax translation, 0-indexed to 1-indexed conversion, broadcasting differences, memory layout (row-major to column-major), type system mapping, and launch API differences. Use when converting, porting, or translating cuTile Python kernels to Julia cuTile.jl, or debugging/optimizing existing Julia cuTile translations.

  • 17 files
  • 115 KB
  • CC-BY-4
  • Updated 4 months ago
  • GitHub

Use this Skill: https://skilld.dev/gh/nvidia/tilegym/tilegym-converting-cutile-to-julia

This session only. Nothing lands on disk.

referencesapi-mapping.md

≈4.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

cuTile Python ↔ cuTile.jl (Julia) API Mapping

Import & Setup

Python Julia Notes
import cuda.tile as ct import cuTile as ct Different package name
import cupy as cp using CUDA GPU array library
import numpy as np (stdlib) Julia has built-in arrays
from math import ceil (builtin cld) Ceiling division

Kernel Definition

Python Julia Notes
@ct.kernel (none) No decorator needed
def kernel(a, b, c): function kernel(a::ct.TileArray{T,N}, ...) where {T} Typed parameters
param: ct.Constant[int] param::Int (+ ct.Constant(val) at launch) Constant at launch, not signature
param: ct.Constant[float] param::Float32 (+ ct.Constant(val) at launch) Same pattern
(implicit return) return or return nothing Must be explicit

Kernel Launch

Python Julia Notes
ct.launch(stream, grid, kernel, (a, b, c, val)) ct.launch(kernel, grid, a, b, c, ct.Constant(val)) No stream; args splatted; constants wrapped
@ct.kernel(occupancy=N) ct.@compiler_options occupancy=N (in kernel body) Replaces launch kwargs
grid = (M, N, 1) grid = (M, N) or grid = (M, N, K) Trailing 1s optional
cp.cuda.get_current_stream() (implicit) Julia uses task-bound stream
cp.cuda.runtime.deviceSynchronize() CUDA.synchronize() Explicit sync

Grid & Block IDs

Python Julia Notes
ct.bid(0) ct.bid(1) 1-indexed
ct.bid(1) ct.bid(2) 1-indexed
ct.bid(2) ct.bid(3) 1-indexed
ct.num_blocks(0) ct.num_blocks(1) 1-indexed
ct.cdiv(a, b) cld(a, b) Julia builtin

Memory Operations

Python Julia Notes
ct.load(arr, index=(i,j), shape=(m,n)) ct.load(arr; index=(i,j), shape=(m,n)) Keyword preferred
ct.load(arr, index=(i,j), shape=(m,n), padding_mode=ct.PaddingMode.ZERO) ct.load(arr; index=(i,j), shape=(m,n), padding_mode=ct.PaddingMode.Zero) Semicolon kwargs; Zero not ZERO
ct.load(arr, index=(b,h,0,j), shape=(1,1,D,N), order=(0,1,3,2)) ct.load(arr; index=(b,h,j,1), shape=(N,D,1,1), order=(2,1,3,4)) ⚠️ order remaps BOTH shape AND index positions — see Critical Rule 16
ct.store(arr, index=(i,j), tile=t) ct.store(arr; index=(i,j), tile=t) Keyword preferred
ct.gather(arr, indices) ct.gather(arr, indices) Same
ct.scatter(arr, indices, tile) ct.scatter(arr, indices, tile) Same
ct.load(arr, index=bid, shape=()) arr[bid] 0-D tile → scalar indexing
ct.num_tiles(A, axis=1, shape=(m,n)) ct.num_tiles(A, 2, (m,n)) Axis 1-indexed
A.shape[0] size(A, 1) 1-indexed
A.shape[1] size(A, 2) 1-indexed
A.dtype eltype(A) or T (from where clause) Julia type system

Padding Modes

Python Julia
ct.PaddingMode.ZERO ct.PaddingMode.Zero
ct.PaddingMode.NAN ct.PaddingMode.Nan
ct.PaddingMode.POS_INF ct.PaddingMode.PosInf
ct.PaddingMode.NEG_INF ct.PaddingMode.NegInf
ct.PaddingMode.NEG_ZERO ct.PaddingMode.NegZero

Tile Construction

Python Julia Notes
ct.full((m,n), 0, dtype=ct.float32) fill(0.0f0, (m, n)) Base.fill overlay
ct.zeros((m,n), dtype=ct.float32) zeros(Float32, m, n) Base.zeros overlay
ct.arange(N, dtype=ct.int32) ct.arange(N) Returns 1-indexed [1,...,N], Int32
ct.ones((m,n), dtype=ct.float32) ones(Float32, m, n) Base.ones overlay

Type Conversion

Python Julia Notes
tile.astype(ct.float32) convert(ct.Tile{Float32}, tile) —
ct.astype(tile, ct.float32) convert(ct.Tile{Float32}, tile) —
tile.astype(ct.tfloat32) convert(ct.Tile{ct.TFloat32}, tile) TFloat32 type
ct.astype(acc, C.dtype) convert(ct.Tile{T}, acc) Use type parameter

Type Names

Python Julia
ct.float16 Float16
ct.float32 Float32
ct.float64 Float64
ct.bfloat16 BFloat16
ct.tfloat32 ct.TFloat32
ct.int8 Int8
ct.int16 Int16
ct.int32 Int32
ct.int64 Int64
ct.uint8 UInt8
ct.uint16 UInt16
ct.uint32 UInt32
ct.uint64 UInt64
ct.bool_ / bool Bool

Arithmetic & Element-wise

Python Julia Notes
a + b (same shape) a + b Same
a + b (different shape) a .+ b Must use broadcast dot
a - b (same shape) a - b Same
a * scalar a * scalar Same
a / scalar a / scalar Same
a * b (element-wise) a .* b Broadcast; a * b is matmul!
a / b (element-wise) a ./ b Broadcast
a ** 2 a .^ 2 or a .^ 2.0f0 Broadcast
-tile .-tile or broadcast neg —

Comparisons & Logic

Python Julia
a < b a .< b
a > b a .> b
a <= b a .<= b
a >= b a .>= b
a == b a .== b
a != b a .!= b

Math Functions

Python Julia Notes
ct.exp(tile) exp.(tile) Broadcast syntax
ct.exp2(tile) exp2.(tile) —
ct.log(tile) log.(tile) —
ct.log2(tile) log2.(tile) —
ct.sqrt(tile) sqrt.(tile) Base function — safe everywhere
ct.rsqrt(tile) rsqrt.(tile) cuTile.jl exports rsqrt — broadcast dot works. map(ct.rsqrt, tile) also works.
ct.abs(tile) abs.(tile) Base function — safe everywhere
ct.sin(tile) sin.(tile) —
ct.cos(tile) cos.(tile) —
ct.fma(a, b, c) fma.(a, b, c) —
ct.negative(tile) (-).(tile) or .-(tile) Negate
ct.maximum(a, b) (element-wise) max.(a, b) Element-wise max
ct.minimum(a, b) (element-wise) min.(a, b) Element-wise min

Reductions

Python Julia Notes
ct.sum(tile, axis=0) sum(tile; dims=1) Axis +1; keeps dim
ct.sum(tile, axis=1) sum(tile; dims=2) Axis +1; keeps dim
ct.max(tile, axis=0) maximum(tile; dims=1) max → maximum
ct.min(tile, axis=0) minimum(tile; dims=1) min → minimum
ct.sum(tile, axis=0, keepdims=True) sum(tile; dims=1) Always keeps dims
ct.sum(tile, axis=0, keepdims=False) dropdims(sum(tile; dims=1); dims=1) Explicit dropdims
ct.argmax(tile, axis=0) argmax(tile; dims=1) 1-indexed result
ct.argmin(tile, axis=0) argmin(tile; dims=1) 1-indexed result

Scans (Prefix Operations)

Python Julia
ct.cumsum(tile, axis=0) cumsum(tile; dims=1)
ct.cumprod(tile, axis=0) cumprod(tile; dims=1)

Shape Operations

Python Julia Notes
ct.reshape(tile, shape) reshape(tile, shape) —
ct.permute(tile, (0,2,1)) permutedims(tile, (1,3,2)) Each axis +1
ct.transpose(tile) transpose(tile) 2D only
ct.broadcast_to(tile, shape) ct.broadcast_to(tile, shape) Same
ct.extract(tile, index=(i,j), shape=(m,n)) ct.extract(tile, (i+1,j+1), (m,n)) Index 1-based
ct.cat((a, b), axis=0) ct.cat((a, b), 1) Axis +1
ct.cat((a, b), axis=-1) ct.cat((a, b), -1) Negative OK

Matrix Operations

Python Julia Notes
ct.mma(a, b, acc=acc) muladd(a, b, acc) No keyword for acc
ct.mma(a, b) a * b No accumulator
ct.matmul(W, X) W * X * is matmul for 2D/3D tiles
(manual TF32 check) if A.dtype == ct.float32: a = ct.astype(a, ct.tfloat32) if T === Float32; a = convert(ct.Tile{ct.TFloat32}, a); end === for type comparison

Conditional / Selection

Python Julia Notes
ct.where(mask, x, y) ifelse.(mask, x, y) Broadcast ifelse
ct.where(mask, tile, 0) ifelse.(mask, tile, 0.0f0) Scalar must match type

Atomic Operations

Python Julia
ct.atomic_cas(arr, idx, expected, desired, memory_order=ct.MemoryOrder.ACQUIRE) ct.atomic_cas(arr, idx, expected, desired; memory_order=ct.MemoryOrder.Acquire)
ct.atomic_xchg(arr, idx, val, memory_order=ct.MemoryOrder.RELEASE) ct.atomic_xchg(arr, idx, val; memory_order=ct.MemoryOrder.Release)
ct.atomic_add(arr, idx, val) ct.atomic_add(arr, idx, val)
ct.MemoryOrder.ACQUIRE ct.MemoryOrder.Acquire
ct.MemoryOrder.RELEASE ct.MemoryOrder.Release
ct.MemoryOrder.RELAXED ct.MemoryOrder.Relaxed

Control Flow

Python Julia Notes
for k in range(n): for k in Int32(1):n cuTile 0.2 supports native for loops; use 1-based when k is a tile index for ct.load/ct.store
for k in range(0, n): for k in Int32(0):n - Int32(1) Use 0-based when k is used in arithmetic (e.g., k * TILE_SIZE + offset)
if cond: if cond Same structure
if A.dtype == ct.float32: if T === Float32 Use === for type check

Host Harness

Python Julia Notes
cp.random.rand(M, N).astype(np.float32) CUDA.rand(Float32, M, N) —
cp.random.randn(M, N).astype(np.float32) CUDA.randn(Float32, M, N) —
cp.empty((M, N), dtype=np.float32) CuArray{Float32}(undef, M, N) —
cp.zeros((M, N), dtype=np.float32) CUDA.zeros(Float32, M, N) —
cp.empty_like(a) similar(a) —
cp.asnumpy(arr) Array(arr) GPU → CPU
np.allclose(a, b, rtol=..., atol=...) isapprox(a, b; rtol=..., atol=...) —
assert ... @assert ... —
CUDA event timing CUDA.@elapsed ct.launch(...) Returns seconds
ceil(M / tile) cld(M, tile) Ceiling division
data["key"] data.key Named tuple access
{"key": val} (; key=val) Named tuple literal

Memory Layout Considerations

Python uses row-major (C-order), Julia uses column-major (Fortran-order).

For 2D arrays, this is largely transparent since cuTile handles it via strides.

For batched operations (3D+), consider reordering dimensions:

  • Python: (Batch, M, K) — batch is first (outermost in row-major)
  • Julia: (M, K, Batch) — batch is last (outermost in column-major)

This gives optimal memory access patterns in each language.

When converting, either:

  1. Transpose the layout (recommended for performance): change array shapes and adjust kernel indexing
  2. Keep the layout and accept potentially suboptimal memory access patterns

Bitwise Operations

Python cuTile Julia cuTile.jl
ct.bitwise_xor(a, b) a .⊻ b
ct.bitwise_rshift(a, n) a .>> n
ct.bitwise_lshift(a, n) a .<< n
ct.bitwise_and(a, mask) a .& mask
ct.bitwise_or(a, b) a .| b
ct.bitwise_not(a) .~a

1D Element-wise Pattern (TMA load/store)

For simple 1D element-wise ops (dropout, activations), use the TMA ct.load/ct.store pattern. No to_col_major/from_col_major needed — 1D arrays have no row/col distinction.

# 1D kernel using TMA block indexing
function my_1d_kernel(x::ct.TileArray{T,1}, output::ct.TileArray{T,1},
                      BLOCK_SIZE::Int) where {T}
    bid = ct.bid(1)
    x_tile = ct.load(x; index=bid, shape=(BLOCK_SIZE,))
    # ... process ...
    ct.store(output; index=bid, tile=result_tile)
    return nothing
end

Host harness: flatten input, pad to BLOCK_SIZE multiple, launch kernel, trim output.

2D Persistent Scheduling Pattern (RoPE, etc.)

For row-per-block kernels with many rows, use persistent scheduling with ct.Constant for tile sizes:

function my_kernel(data::ct.TileArray{T,2}, TILE_HD::Int) where {T}
    ct.@compiler_options occupancy=2
    bid = ct.bid(1)
    num_programs = ct.num_blocks(1)
    n_rows = size(data, 2)
    row_idx = bid
    while row_idx <= n_rows
        tile = ct.load(data; index=(Int32(1), row_idx), shape=(TILE_HD, 1))
        # ... process row ...
        ct.store(data; index=(Int32(1), row_idx), tile=result)
        row_idx += num_programs
    end
    return
end

# Launch with ct.Constant for tile size
ct.launch(my_kernel, num_blocks, data_cu, ct.Constant(tile_hd))

Kernel Patterns for Large Tensors

When a single ct.load of the entire data exceeds hardware limits, use one of these patterns:

Pattern A: Column-loop with ct.load/ct.store (Online Algorithm)

Best for TMA-based kernels where each chunk is a contiguous tile along columns. Uses for loops for column iteration and ct.num_tiles for tile count.

function online_kernel(output::ct.TileArray{T, 2}, input::ct.TileArray{T, 2},
                       TILE_SIZE::Int) where {T}
    row_idx = ct.bid(1)
    num_col_tiles = ct.num_tiles(input, 2, (1, TILE_SIZE))

    m_prev = fill(-Inf32, (1, 1))
    l_prev = zeros(Float32, 1, 1)

    for col_idx in Int32(1):num_col_tiles
        tile = ct.load(input; index=(row_idx, col_idx), shape=(1, TILE_SIZE),
                      padding_mode=ct.PaddingMode.NegInf)
        tile = convert(ct.Tile{Float32}, tile)
        tile_max = maximum(tile; dims=2)
        m_curr = max.(tile_max, m_prev)
        l_prev = l_prev .* exp.(m_prev .- m_curr)
        l_prev = sum(exp.(tile .- m_curr); dims=2) .+ l_prev
        m_prev = m_curr
    end

    for col_idx in Int32(1):num_col_tiles
        tile = ct.load(input; index=(row_idx, col_idx), shape=(1, TILE_SIZE),
                      padding_mode=ct.PaddingMode.NegInf)
        tile = convert(ct.Tile{Float32}, tile)
        result = exp.(tile .- m_prev) ./ l_prev
        ct.store(output; index=(row_idx, col_idx), tile=convert(ct.Tile{T}, result))
    end
    return
end

Pattern B: Chunked with ct.gather/ct.scatter and ct.Constant (Preferred)

Use when you need multiple passes over column chunks. Pass tile sizes as ct.Constant at launch — no @eval needed.

function chunked_kernel(output::ct.TileArray{T, 2}, input::ct.TileArray{T, 2},
                        n_cols::Int, TILE_SIZE::Int) where {T}
    ct.@compiler_options occupancy=4
    row_idx = ct.bid(1)
    num_chunks = (n_cols + TILE_SIZE - Int32(1)) ÷ Int32(TILE_SIZE)
    col_offsets_base = ct.arange(TILE_SIZE)
    row_tile = ct.Tile(row_idx)

    row_max = fill(-Inf32, (1,))
    denominator = zeros(Float32, TILE_SIZE)

    for chunk_idx in Int32(0):num_chunks - Int32(1)
        col_indices = ct.broadcast_to(ct.Tile(chunk_idx * Int32(TILE_SIZE)), (TILE_SIZE,)) .+ col_offsets_base
        chunk = ct.gather(input, (row_tile, col_indices); check_bounds=true, padding_value=T(-Inf))
        chunk = convert(ct.Tile{Float32}, chunk)
        row_max = max.(row_max, ct.Tile(maximum(chunk)))
    end
    # ... pass 2 and 3 similarly ...
    return
end

function julia_chunked_softmax(output::CuMatrix{T}, input::CuMatrix{T};
                               tile_size::Int=1024) where {T}
    M, N = size(input)
    ct.launch(chunked_kernel, M, output, input, ct.Constant(N), ct.Constant(tile_size))
    CUDA.synchronize()
    return
end

Quick Conversion Reference

Python cuTile Julia cuTile.jl
@ct.kernel function ... end
ct.bid(0) ct.bid(1)
ct.num_blocks(0) ct.num_blocks(1)
ct.num_tiles(A, axis=1, shape=s) ct.num_tiles(A, 2, s)
A.shape[0] size(A, 1)
ct.load(arr, index=i, shape=s) ct.load(arr; index=i, shape=s)
ct.store(arr, index=i, tile=t) ct.store(arr; index=i, tile=t)
.astype(ct.float32) convert(ct.Tile{Float32}, tile)
ct.mma(a, b, acc=acc) muladd(a, b, acc)
ct.where(m, x, y) ifelse.(m, x, y)
ct.sum(t, axis=0) sum(t; dims=1)
ct.maximum(a, b) max.(a, b)
ct.exp(t) exp.(t)
ct.rsqrt(t) rsqrt.(t) (cuTile.jl exports rsqrt; map(ct.rsqrt, t) also works)
for k in range(n): for k in Int32(1):n
ct.launch(stream, grid, kernel, (args)) ct.launch(kernel, grid, args...)
ct.Constant[int] in sig ::Int in sig, ct.Constant(val) at launch
ct.cdiv(a, b) cld(a, b)
ct.PaddingMode.ZERO ct.PaddingMode.Zero
a * b (element-wise) a .* b
ct.bitwise_xor/and/or/rshift/lshift a .⊻ b / a .& mask / a .| b / a .>> n / a .<< n
floor(x) element-wise floor.(tile) — works on float tiles

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at 2bf003b. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 4 days ago.

Activeupdated 4 months ago
Other metadata
metadata
{
  "author": "TileGym Team <TileGym@nvidia.com>",
  "tags": [
    "cutile",
    "julia",
    "conversion",
    "gpu",
    "kernel"
  ]
}

README badge

README badge for nvidia/tilegym/tilegym-converting-cutile-to-julia