Tile Operands and Execution#
A CUDA tile operation repeats a chosen computation over a tensor region.
T.cuda.tile.apply uses the PTX instruction you specify, while
T.cuda.tile.ewise applies a scalar TIRx function to each element. The
operand views describe which values each thread handles and how many times
it repeats that computation.
Reading an Operand’s Shape#
Suppose each of 64 threads processes twelve float32 values. A
ld.global.v4.b32 instruction loads four 32-bit words at a time, so
each thread needs three loads. We can describe this work with shape
(2, 32, 3, 4):
(2, 32, 3, 4)
└───┘ │ └─ atom: four words used by one instruction
group └──── repeat: three instructions per thread
Part |
Meaning in this example |
|---|---|
Group |
|
Repeat |
|
Atom |
|
The instruction fixes the atom shape. The leading axes describe its
repetitions and the threads that execute them. The register layout below
maps the first two coordinates to warpid and laneid, leaving
3 * 4 = 12 local positions per thread.
A Load, Compute, and Store Example#
The kernel uses this layout to load the values, add one to each, and
store the results. scope="cta" tells the tile calls to use the
leading (2, 32) axes for the threads in the block:
from tvm_tirx.script import tirx as T
from tvm_tirx.tirx.layout import S, TileLayout
from tvm_tirx.backend.cuda.axis import laneid, warpid
register_layout = TileLayout(S[(2, 32, 3, 4) : (1 @ warpid, 1 @ laneid, 4, 1)])
@T.function(private=True)
def plus_one(value: T.float32) -> T.float32:
return value + T.float32(1)
@T.function
def add_one_tile(
A: T.Tensor((2, 32, 3, 4), "float32", align=16),
B: T.Tensor((2, 32, 3, 4), "float32", align=16),
):
T.device_entry(launch=T.cuda.LaunchConfig(grid=1, block=64))
registers = T.alloc_local((2, 32, 3, 4), "float32", layout=register_layout)
T.cuda.tile.apply(T.ptx["ld.global.v4.b32"], registers, A, scope="cta")
T.cuda.tile.ewise(plus_one, registers, registers, scope="cta")
T.cuda.tile.apply(T.ptx["st.global.v4.b32"], B, registers, scope="cta")
The register layout assigns each thread twelve storage positions: three
groups of four. The load and store each transfer four 32-bit words per
instruction, so each thread issues three loads and three stores. Between
them, ewise applies plus_one to all twelve scalar values.
We can compile and check the result:
import torch
import tvm_tirx
compiled = tvm_tirx.compile(add_one_tile, target="cuda")
a = torch.arange(768, dtype=torch.float32, device="cuda").reshape(2, 32, 3, 4)
b = torch.empty_like(a)
compiled(a, b)
torch.testing.assert_close(b, a + 1)
Execution Scope and Scalar Functions#
The kernel’s LaunchConfig launches 64 threads; scope="cta"
describes how the tile operands map to those threads. It does not launch
additional threads or insert a barrier. At scope="thread", an operand
has no group prefix. Other scopes and warp-collective instructions use
the group shapes listed in the coordinate reference.
ewise uses one scalar value per invocation, so it has no trailing
instruction atom. For the same register view, (2, 32) still selects a
thread, while (3, 4) now specifies twelve scalar invocations.
Preparing Operand Views#
The load’s source and destination expose the same group and repeat coordinates. Within each atom, the four global words must be consecutive and 16-byte aligned; the destination words must belong to the selected thread. These requirements come from the chosen instruction, and lowering checks that the input and output views meet them.
An existing tensor may need a view or rearrange to expose these axes,
or a sub to select a region. These operations reuse its storage. They
can change the coordinates used to access values, but a transfer between
threads or memory spaces still requires an explicit load, store, or other
communication instruction. See Types, Tensors, and Memory for tensor views and
Data Layout for their physical placement.
The dtype describes storage elements. In the example, one element is one
32-bit word. An instruction that packs two float16 values into one 32-bit
register instead needs an appropriate packed container; apply does not
pack values automatically. The instruction reference lists the required operand order,
representation, alignment, and optional parameters for each family.
Synchronization Belongs to the Caller#
The example uses thread-private registers, so no thread waits for another.
If a thread instead reads shared memory written by other threads, the
kernel must synchronize those accesses. apply and ewise do not add
that synchronization.
Asynchronous instructions also require their own completion protocol.
cp.async uses copy groups and waits; TMA loads use transaction barriers;
tcgen05 operations have their own commit and wait instructions. The
corresponding instruction guides show these calls
alongside the data movement. Selecting an execution scope does not perform
thread election or complete an asynchronous transfer.