Debugging

Printing and assertions

OperationDescription
print(args...)Print values
println(args...)Print values with newline
ct.@assert cond [msg]Abort kernel if condition is false

Standard Julia print/println work inside kernels. String constants and tiles can be mixed freely; format specifiers are inferred from element types at compile time. String interpolation is supported.

println("Block ", ct.bid(1), ": tile=", tile)
println("result=$result")  # string interpolation
ct.@assert idx <= n "index out of bounds"

These are debugging aids and are not optimized for performance.

Inspecting generated Tile IR

The generated Tile IR can be inspected with ct.code_tiled:

ct.code_tiled(vadd, Tuple{ct.TileArray{Float32, 1, Int32, ct.ArraySpec{1}(128, true, (0,), (32,))},
                          ct.TileArray{Float32, 1, Int32, ct.ArraySpec{1}(128, true, (0,), (32,))},
                          ct.TileArray{Float32, 1, Int32, ct.ArraySpec{1}(128, true, (0,), (32,))},
                          ct.Constant{Int64, 16}})

Spelling out those types is only worth it when you have no GPU, since code_tiled does not need CUDA.jl. Otherwise let the launch site derive them for you with ct.@device_code_tiled, described next.

By default code_tiled compiles for the active CUDA device with the toolchain's bytecode version, so its output matches what a launch emits, including architecture-dependent ct.@compiler_options hints. Pass sm_arch to target another architecture, or to inspect code without a CUDA device, and bytecode_version to emit an older version. Combinations the bytecode does not support are rejected, so an older bytecode_version may need an explicit sm_arch on older devices.

To inspect several stages with the same compilation options, create a job:

job = ct.tile_job(vadd, argtypes; sm_arch=v"10.0", num_ctas=2)
ct.code_typed(job)
ct.code_structured(job)
ct.code_tiled(job)

With a CUDA 13.4 or newer tileiras, pass remarks=true to compile the Tile IR and print optimization diagnostics after it:

ct.code_tiled(vadd, argtypes; remarks=true, sm_arch=v"10.0")

The remarks report successful and failed optimizations, including tensor-core selection and memory alignment issues. Remarks are generated afresh for reflection and are not read from or written to the compilation cache.

Inspecting PTX and SASS

The output of tileiras can be inspected with ct.code_ptx and ct.code_sass, taking the same signature as ct.code_tiled:

ct.code_ptx(vadd, argtypes; sm_arch=v"10.0")
ct.code_sass(vadd, argtypes; sm_arch=v"10.0")

code_ptx shows the thread-level SIMT program the tile-level kernel is lowered to, with every compiler decision (thread mapping, CTA size, pipelining, synchronization) already made; code_sass shows the final machine code, disassembled with nvdisasm. Both compile the Tile IR with tileiras but do not need a GPU.

PTX reflection is unstable

PTX is an implementation detail of tileiras, not an interface, and may stop being produced or recorded at any time. code_ptx reads it from an undocumented debug section of the CUBIN and will be removed with it.

Note that tileiras always generates architecture-specific code: targeting sm_arch=v"10.0" produces sm_100a PTX and SASS, which only runs on that exact architecture. Launches therefore always compile for the active device, and reject an explicit sm_arch that differs from it.

Intercepting a launch

@device_code_* macros intercept compilation during a kernel launch, deriving the argument types from the actual CuArrays:

julia> ct.@device_code_tiled @cuda backend=cuTile blocks=cld(vector_size, tile_size) vadd(a, b, c, ct.Constant(tile_size))
// vadd(cuTile.TileArray{Float32, 1, Int32, cuTile.ArraySpec{1, 128, true, (0,), (16,), false, (false,)}()}, …)

cuda_tile.module @kernels {
  entry @vadd(%arg0: tile<ptr<f32>>, …) {
    ...
    return
  }
}

Pass remarks=true to include tileiras optimization remarks for every intercepted kernel:

ct.@device_code_tiled remarks=true @cuda backend=cuTile blocks=grid kernel(args...)

The following macros are available:

MacroOutput
ct.@device_code_warntypeTyped Julia IR with type-instability highlighting
ct.@device_code_typedTyped Julia IR after overlay resolution, returned per job
ct.@device_code_structuredStructured IR (after control-flow structurization)
ct.@device_code_tiledFinal Tile IR (MLIR textual format)
ct.@device_code_ptxPTX generated by tileiras (unstable, as above)

The first two are shared with GPUCompiler: cuTile reports its compilations through the same hook as the LLVM backends, so CUDA.@device_code_typed and CUDA.@device_code_warntype cover cuTile launches as well, including expressions that launch kernels of both kinds. @device_code_typed returns a dictionary from job to typed code rather than printing. Both Julia-level views use cuTile's optimized inferred code, including constant argument specializations. CUDA.@device_code_ptx also includes cuTile launches. The cuTile-specific stage macros only inspect cuTile jobs and ignore other backends; they report an error when the expression contains no cuTile compilations.

The macros use the launch's actual target, constant arguments, and compilation hints, including when the compiled kernel is already cached. Each distinct job is inspected once per macro invocation. Hooks are scoped to the current task and inherited by child tasks; nested macros restore the enclosing hook.

For the machine code, CUDA.@device_code_sass works for cuTile kernels: it intercepts module loads at the driver level (via CUPTI), showing the binary that was actually loaded — complete with Julia source locations, since tileiras compiles with line info. Under Nsight or another active profiler, which conflicts with CUPTI, use ct.code_sass on the signature instead.

Dumping bytecode

Setting JULIA_CUTILE_DUMP_BYTECODE writes the emitted Tile IR bytecode to disk:

❯ JULIA_CUTILE_DUMP_BYTECODE=/tmp/julia_tiles julia --project -e 'using cuTile; ...'
Dumping TILEIR bytecode to file: /tmp/julia_tiles/example.ln42.cutile

The resulting files can be disassembled with NVIDIA's tileirdisasm:

❯ tileirdisasm /tmp/julia_tiles/example.ln42.cutile

CUDA toolkits older than 13.4 ship cuda-tile-translate instead, which needs an explicit flag and cannot read bytecode newer than its own version:

❯ cuda-tile-translate --cudatilebc-to-mlir /tmp/julia_tiles/example.ln42.cutile

This is the same mechanism cuTile Python exposes through CUDA_TILE_DUMP_BYTECODE, so bytecode from both can be compared directly.