Tile Libraries#
A tile library (.tilelib) packages device functions that a
kernel calls like any other function: an FFT of a tile, the Cholesky
factorization of a tile, and so on. Tile C++ calls them through
__experimental_foreign_decl__ declarations and cuTile Python through
cuda.tile_preview.foreign_call; cuTile Rust calls them through
#[cutile::foreign] declarations.
Note
Tile libraries are a public preview of Tile IR 13.5. They need a CUDA 13.5
tileiras, and a tile library built for that same toolkit.
Finding tile libraries#
Start with NVIDIA’s Tile IR supported-library list.
It links to providers as their packages become available. Tile libraries are
released separately from the CUDA Toolkit; installing CUDA 13.5 alone does not
install every provider’s .tilelib files.
Use the provider’s installation instructions to obtain a .tilelib and its
dependencies for your toolkit. Its interface determines the function names,
constant arguments, shapes, types, and architectures you can use. Then declare
the functions as below and pass the installed paths to
TileLibrarySpec. The provider list describes
Tile IR support; cuTile Rust’s current operand limitations
also apply, including the lack of pointer operands in foreign calls.
The solver.tilelib, fft_path, and provider-library paths in this chapter
are illustrative configuration examples. They are not bundled downloads or
guaranteed package filenames. Replace them with the paths and declarations
from the provider’s package.
Rust SIMT functions from CUDA Oxide#
CUDA Oxide can also build tile libraries from Rust SIMT functions: mark them
with #[tile_export], then package them with cargo oxide tilelib. See the
tilelib_relu example
for export definitions, build commands, and current export limitations.
To call these exports from cuTile Rust, write matching
#[cutile::foreign] unsafe fn declarations
and configure the generated .tilelib on the host
with TileLibrarySpec. Match the exported function name, concrete tile shapes,
element types, and input/output access modes. This is device-function interop:
the SIMT function runs inside the Tile kernel.
This uses the CUDA 13.5 preview and requires matching library, caller bytecode,
and tileiras tooling; preview libraries are not portable across arbitrary
toolkit versions. Oxide’s current export restrictions apply in addition to
the cuTile Rust limitations. The recorded Oxide execution
validation used a Tile C++ caller; it does not establish an Oxide-to-cuTile Rust
execution test.
Reusable Rust kernels#
For Rust implementations of attention, normalization, transformer utilities,
fused QKV, and NVFP4 MoE, see the
cutile-kernels catalog
and the
nvfp4_moe example.
The fused QKV guide
describes its correctness example and opt-in benchmark.
These are Rust source kernels launched from the host. A .tilelib supplies
device functions called from inside a kernel through #[cutile::foreign].
Declaring a foreign function#
A foreign function is a body-less function inside a #[cutile::module],
written like the declaration the tile library carries in its Tile IR
interface:
preview$func extern @cholesky<[
Function: !cuda_tile<preview$string>, FillMode: !cuda_tile<preview$string>,
Batch: index, M: index, T: !cuda_tile<preview$Datatype>
]>(
%A: tile<[Batch]x[M]x[M]xpreview$sym_t<T>> {cuda_tile.preview$access = #cuda_tile<preview$access<inout>>},
%info: tile<[Batch]xi32> {cuda_tile.preview$access = #cuda_tile<preview$access<out>>}
) -> (tile<[Batch]x[M]x[M]xpreview$sym_t<T>>, tile<[Batch]xi32>)
#[cutile::module]
pub mod solver {
use cutile::core::*;
/// # Safety
///
/// The declaration matches `solver.tilelib`'s `cholesky`.
#[cutile::foreign]
pub unsafe fn cholesky<const Batch: i32, const M: i32, T: ElementType>(
Function: &str,
FillMode: &str,
#[inout] A: Tile<T, { [Batch, M, M] }>,
) -> (Tile<T, { [Batch, M, M] }>, Tile<i32, { [Batch] }>); // `info` is output-only
}
The declaration names a library function, not a library: the host chooses the libraries a kernel links (see Configuring tile libraries).
name = "..."is the function’s name in the library; it defaults to the Rust name.The constant arguments are the
&strparameters, then the generic parameters, each group in order: aname: &strparameter is a string,const N: i32anindex, andT: ElementTypea datatype (f32,f64, …). They correspond one to one to the constant parameters of the Tile IR declaration, the template parameters of a Tile C++ declaration and theconstant_paramsof a cuTile Pythonforeign_call. When the library orders them differently, list every name in that order withconst_args = "...", e.g.#[cutile::foreign(const_args = "Batch, Function, M, T, FillMode")].Every other parameter is an operand the caller passes, in the declaration’s order: read-only by default, read-write with
#[inout]. Scalars are rank-0 tiles.The results start with the updated
#[inout]operands. Every further result is an output-only operand (infoabove): it is not a parameter, and the call passes it zero-filled after the parameters. This is the form of Tile C++__experimental_foreign_decl__declarations.The declaration may keep the library’s names (
Batch,A); the lints that would flag them are off for foreign declarations.A foreign function is declared
unsafe fn, and every call is in anunsafeblock (see Safety).
The declaration must match the library’s interface exactly: tileiras checks
the call against the declaration the library carries.
Safety#
The compiler sees only the declaration, not the library’s implementation, so
a foreign function is unsafe fn. A declaration that omits unsafe is an
error, and so is a call outside an unsafe block. Document the obligations
in a # Safety section of the declaration, even when there are none beyond
the ones below. The caller of a foreign function guarantees that:
The declaration matches the library’s interface: the name, the constant arguments and their order, the operand types and shapes, which operands are
#[inout], and the results.tileirasrejects some mismatches, not all.The library implements the function for the target architecture and for the constant arguments of the call.
The function reads and writes only its operands and results. Foreign functions take tiles, which are values, so this holds for every library today. It becomes an obligation on the library once pointer operands are supported.
A wrapper that upholds these obligations for every input it accepts can be
a safe device function that calls the declaration in an unsafe block.
String constants#
A string constant is a &str parameter. The call passes a string that the
JIT resolves when it compiles the kernel, as it does for the format string
of print_tko:
a literal:
cholesky::<1, 32, f64>("potrf", "lower", a);a
constitem whose value is a literal, by its name or its path:const FILL: &str = "lower";,consts::FILL;a local or a function parameter bound to one of these, so a wrapper can take
fill: &strand pass it on. A local reassigned inside a runtimeif, a loop, or a{}orunsafe {}block is not constant after it, or inside the loop: passing it there is a compile error;an associated const of a tag type,
Lower::NAME, orF::NAMEfor a generic parameterFbound to a tag type. When a tag type definesNAMEin two traits, name the trait:<F as FillMode>::NAME.
The string becomes part of the call’s specialization: two calls with
different strings are two different library functions. Any other expression
(a value computed at run time, or concat!) is a compile error.
A tag type resolves by its path where it is written, as in Rust, through
imports and pub use re-exports. Two modules can each declare a Lower
whose NAME differs: each call passes the string of the Lower it names.
A tag that a function or method returns is the one its return type names in
that function’s module.
Mapping to Tile C++ and cuTile Python#
Tile C++ |
cuTile Python |
cuTile Rust |
|
|---|---|---|---|
Declaration |
|
none: every call is |
|
Name in the library |
|
|
|
Constant parameters |
template parameters ( |
|
|
Read-write operands |
|
positional |
|
Output-only operands |
implicit, from |
passed explicitly |
implicit: results after the |
Scalars |
rank-0 tiles |
rank-0 tiles |
rank-0 tiles |
Library |
|
|
|
Calling a foreign function#
Calls look like calls to any device function, inside an unsafe block.
Generic arguments the operands determine can be inferred; the others are
passed with a turbofish, where _ lets the compiler infer the rest:
let (factor, info) = unsafe { cholesky::<_, _, _>("potrf", "lower", a_tile) };
A call lowers to a Tile IR preview$call, e.g.
preview$call @cholesky<["potrf", "lower", 1, 32, f64]>(%a, %info0, %token),
where %info0 is the zero-filled output-only operand.
Foreign calls are ordered like memory accesses: a call happens after every earlier load, store, atomic and call, and before every later one, including across loop iterations and in inlined helpers. Accesses with no call between them stay unordered, as in a kernel without foreign calls.
#[inout] describes how the library uses the operand: the updated value is
the call’s result, and the caller’s tile is unchanged, as with any value.
Two modules can each declare a foreign function with the same Rust name; each call reaches the one it names.
Configuring tile libraries#
Obtain a compatible provider package through Finding tile libraries before configuring these paths.
The host chooses the tile libraries that kernels link, so the same kernel source can link a debug or a release build of a library, or one built for each architecture:
use cutile::tile_kernel::{set_default_tile_libraries, CompileOptions, TileLibrarySpec};
// Libraries every kernel links, for the rest of the process.
set_default_tile_libraries([TileLibrarySpec::new("/opt/tilelibs/solver.tilelib")]);
// A library one launch links in addition to the defaults.
let (y,) = kernels::fft_kernel(y)
.compile_options(CompileOptions::new().tile_library(TileLibrarySpec::new(fft_path)))
.sync()?;
The defaults (
set_default_tile_libraries,add_default_tile_library) and a launch’sCompileOptions::tile_librarylist are combined, canonicalized, sorted and de-duplicated beforetileiraslinks them. A relative path is relative to the current directory. Adding a library that is already a default changes nothing.A kernel without foreign calls links nothing. A kernel with foreign calls and no configured library fails to compile with an error that names the library functions it calls.
An implementation-provider library generates code for each call with a shared library it loads at link time. Declare every file it loads and every setting that changes the code it generates, so the caches miss when either changes:
let solver = TileLibrarySpec::new(mathdx.join("lib/solver.tilelib")) .dependency(mathdx.join("lib/libmathdx_tile.so")) .setting("mathdx-version", "26.01");
Limitations#
Operands and results are tiles and scalars; pointer operands are not supported yet.
Constant parameters are
index, string and datatype parameters; shape (tuple) parameters are not.A tile library must have been built for the
tileirasthat links it.A string constant must be a literal, a
constwith a literal value, or an associated const with a literal value. The JIT does not evaluate other constant expressions.Name resolution across modules has its own limits; see Name Resolution.