Repository navigation
Description
Version
1.6.0rc5
Version (CUDA Toolkit)
13.4 (nvidia-cuda-tileiras 13.4.92); compared with 13.3 (nvidia-cuda-tileiras 13.3.36)
Which installation method(s) does this occur on?
Pip
Describe the bug.
On sm_120 (RTX 5070 Ti), tileiras 13.4.92 ignores the occupancy hint for a kernel whose ct.gather sits
inside a loop with a runtime trip count. With occupancy=1, 2 and 4 it emits the same cubin: 384 threads,
168 registers and 100,484 B of shared memory. At that size one CTA fits per SM.
tileiras 13.3.36 compiles the same kernel with occupancy=2 to 256 threads, 128 registers and 44 KB, so two CTAs
fit per SM. The 13.4 kernel is 1.65x slower (22.09 ms vs 13.36 ms).
The other three cases (no gather, no runtime loop, or neither) honour occupancy=2 on both versions. The trip
count in the reproducer is 1 at run time, so the loop's presence alone changes the result.
I expected 13.4 to honour occupancy=2 here as 13.3 does, or to report that it cannot meet the hint.
Where we hit it: FlashInfer 0.7.2's cuTile NVFP4 MoE. _grouped_gemm_w4a16 (flashinfer/fused_moe/cutile/fp4.py)
gathers activation rows inside for iteration in range(num_iterations) and launches with occupancy=2. On a
MoE layer with H 6144, I 3072, 32 experts and top-4, at 16,384 tokens:
_grouped_gemm_w4a16 |
13.3.36 | 13.4.92 | 13.4.92, loop compiled to one iteration |
|---|---|---|---|
| threads / regs / smem | 256 / 128 / 43,180 B | 128 / 196 / 74,796 B | 256 / 128 / 42,028 B |
| GEMM1 (N 6144, K 6144) | 56.9 ms | 134.1 ms | 53.5 ms |
| GEMM2 (N 6144, K 3072) | 28.5 ms | 61.7 ms | 27.1 ms |
Changing occupancy (1, 2, 3, 4 or none) does not change the 13.4 cubin. The bytecode that cuda-tile emits for
13.3 also compiles to 128 / 196 / 74,796 B under 13.4, so the change is in tileiras itself.
The last column compiles the M-block loop to a single iteration. Its outputs are byte-identical to the original
kernel's. That is valid for these launches because each M block already has its own CTA (K is above
_PERSISTENT_MAX_K). _grouped_gemm_w4a4 has the same loop: on 13.4 it compiles to 384 threads and 96,388 B and
takes 11.4 ms per call (N 6144, K 3072, 16,384 tokens). With the loop compiled to one iteration it takes 8.2 ms
at 256 threads and 43,140 B.
Minimum reproducible example
"""tileiras ignores the occupancy hint for a W4A16 GEMM whose activation gather sits inside a runtime loop.
One kernel: FP4 (E2M1) weights with E4M3 block scales per 16, BF16 activations, tile 64x128x128. The activation
is read with a tile load or a row gather (GATHER), and the M-block loop runs a runtime trip count or once (LOOP).
For each case and occupancy hint the script prints the compiled CTA size, registers and shared memory, and the
kernel time.
usage: CUDA_TILE_TEMP_DIR=$(mktemp -d) python occupancy_repro.py (cuobjdump on PATH)
"""
import glob
import os
import re
import statistics
import subprocess
import cuda.tile as ct
import torch
ConstInt = ct.Constant[int]
ConstBool = ct.Constant[bool]
TM, TN, TK, SB = 64, 128, 128, 16
@ct.kernel
def w4a16_gemm(X, ROWS, W, S, OUT, num_iterations, grid_m, K: ConstInt, GATHER: ConstBool, LOOP: ConstBool):
nb = ct.bid(1)
k_off = ct.arange(TK, dtype=ct.int32)
for it in range(num_iterations if LOOP else 1):
mb = ct.bid(0) + it * grid_m
rows = ct.gather(ROWS, (mb * TM + ct.arange(TM, dtype=ct.int32),), padding_value=0)
acc = ct.zeros((TM, TN), dtype=ct.float32)
for kt in range(K // TK):
if GATHER:
idx = ct.reshape(rows, (TM, 1)) * K + ct.reshape(kt * TK + k_off, (1, TK))
a = ct.gather(X, (idx,), padding_value=0, check_bounds=True, latency=3)
else:
a = ct.load(X, index=(mb, kt), shape=(TM, TK), latency=3)
wb = ct.load(W, index=(nb, kt), shape=(TN, TK // 2), latency=3)
w = ct.reshape(ct.unpack_from_bytes(ct.reshape(wb, (-1,)), ct.float4_e2m1fn), (TN, TK))
s = ct.load(S, index=(nb, kt), shape=(TN, TK // SB), latency=3)
wd = ct.reshape(ct.astype(w, ct.bfloat16), (TN, TK // SB, SB)) * ct.expand_dims(
ct.astype(s, ct.bfloat16), axis=2)
acc = ct.mma(a, ct.permute(ct.reshape(wd, (TN, TK)), (1, 0)), acc)
ct.store(OUT, index=(mb, nb), tile=ct.astype(acc, ct.bfloat16))
def cubin_resources(path):
elf = subprocess.run(["cuobjdump", "-elf", path], capture_output=True, text=True).stdout
ru = subprocess.run(["cuobjdump", "-res-usage", path], capture_output=True, text=True).stdout
nt = re.search(r"EIATTR_REQNTID\s.*?Value:\s*(0x[0-9a-f]+)", elf, re.S)
r = re.search(r"REG:(\d+) STACK:\d+ SHARED:(\d+)", ru)
threads = int(nt.group(1), 16) if nt else "?"
return f"threads {threads:>3} regs {r.group(1):>3} smem {int(r.group(2)):>6} B" if r else "?"
M, N, K = 16384, 6144, 6144
dev = torch.device("cuda")
g = torch.Generator(device=dev).manual_seed(0)
X = torch.randn(M, K, device=dev, generator=g).to(torch.bfloat16)
W = torch.randint(0, 256, (N, K // 2), device=dev, generator=g, dtype=torch.uint8)
S = (torch.rand(N, K // SB, device=dev, generator=g) * 0.01 + 0.001).to(torch.float8_e4m3fn)
E2M1 = torch.tensor([0, .5, 1, 1.5, 2, 3, 4, 6, -0., -.5, -1, -1.5, -2, -3, -4, -6], device=dev)
Wf = torch.stack((E2M1[(W & 15).long()], E2M1[(W >> 4).long()]), -1).reshape(N, K)
Wf = Wf * S.float().repeat_interleave(SB, dim=1)
ROWS = torch.randperm(M, device=dev, generator=g).to(torch.int32)
ref_gather = X[ROWS.long()].float() @ Wf.T
ref_load = X.float() @ Wf.T
tmp = os.environ["CUDA_TILE_TEMP_DIR"]
num_mb = M // TM
from cuda.tile import _compile # noqa: E402
print(f"tileiras {_compile._find_compiler_bin().path}")
print(f"GPU {torch.cuda.get_device_name(dev)} sm_{''.join(map(str, torch.cuda.get_device_capability(dev)))} "
f"cuda-tile {ct.__version__ if hasattr(ct, '__version__') else '?'} torch {torch.__version__}")
stream = torch.cuda.current_stream()
for gather in (False, True):
for loop in (False, True):
for occ in (1, 2, 4):
kern = w4a16_gemm.replace_hints(occupancy=occ)
out = torch.empty(M, N, device=dev, dtype=torch.bfloat16)
before = set(glob.glob(os.path.join(tmp, "w4a16_gemm*.cubin")))
def run():
# one CTA per M block in both cases; LOOP only changes how the kernel is written
ct.launch(stream, (num_mb, N // TN), kern,
(X.reshape(-1) if gather else X, ROWS, W, S, out, 1, num_mb, K, gather, loop))
run()
torch.cuda.synchronize()
ref = ref_gather if gather else ref_load
err = float((out.float() - ref).norm() / ref.norm())
ts = []
for _ in range(10):
a, b = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True)
a.record()
run()
b.record()
b.synchronize()
ts.append(a.elapsed_time(b))
new = sorted(set(glob.glob(os.path.join(tmp, "w4a16_gemm*.cubin"))) - before)
res = cubin_resources(new[-1]) if new else "(no new cubin)"
print(f"gather={gather!s:5} runtime_loop={loop!s:5} occupancy={occ}: {res} "
f"{statistics.median(ts):7.2f} ms rel err {err:.1e}", flush=True)Relevant log output
=== nvidia-cuda-tileiras 13.4.92
GPU NVIDIA GeForce RTX 5070 Ti sm_120 cuda-tile 1.6.0rc5 torch 2.14.1+cu130
gather=False runtime_loop=False occupancy=1: threads 256 regs 255 smem 43092 B 16.34 ms rel err 1.7e-03
gather=False runtime_loop=False occupancy=2: threads 256 regs 128 smem 43084 B 13.83 ms rel err 1.7e-03
gather=False runtime_loop=False occupancy=4: threads 256 regs 255 smem 43092 B 16.34 ms rel err 1.7e-03
gather=False runtime_loop=True occupancy=1: threads 256 regs 255 smem 43092 B 15.50 ms rel err 1.7e-03
gather=False runtime_loop=True occupancy=2: threads 256 regs 128 smem 43084 B 13.90 ms rel err 1.7e-03
gather=False runtime_loop=True occupancy=4: threads 256 regs 255 smem 43092 B 15.51 ms rel err 1.7e-03
gather=True runtime_loop=False occupancy=1: threads 384 regs 168 smem 43092 B 24.22 ms rel err 1.7e-03
gather=True runtime_loop=False occupancy=2: threads 256 regs 128 smem 43076 B 15.50 ms rel err 1.7e-03
gather=True runtime_loop=False occupancy=4: threads 384 regs 168 smem 43092 B 24.22 ms rel err 1.7e-03
gather=True runtime_loop=True occupancy=1: threads 384 regs 168 smem 100484 B 22.09 ms rel err 1.7e-03
gather=True runtime_loop=True occupancy=2: threads 384 regs 168 smem 100484 B 22.09 ms rel err 1.7e-03
gather=True runtime_loop=True occupancy=4: threads 384 regs 168 smem 100484 B 22.09 ms rel err 1.7e-03
=== nvidia-cuda-tileiras 13.3.36
GPU NVIDIA GeForce RTX 5070 Ti sm_120 cuda-tile 1.6.0rc5 torch 2.14.1+cu130
gather=False runtime_loop=False occupancy=1: threads 384 regs 168 smem 44124 B 20.00 ms rel err 1.7e-03
gather=False runtime_loop=False occupancy=2: threads 256 regs 128 smem 44100 B 12.98 ms rel err 1.7e-03
gather=False runtime_loop=False occupancy=4: threads 384 regs 168 smem 44124 B 20.00 ms rel err 1.7e-03
gather=False runtime_loop=True occupancy=1: threads 384 regs 168 smem 44108 B 20.83 ms rel err 1.7e-03
gather=False runtime_loop=True occupancy=2: threads 256 regs 128 smem 44068 B 13.02 ms rel err 1.7e-03
gather=False runtime_loop=True occupancy=4: threads 384 regs 168 smem 44108 B 20.83 ms rel err 1.7e-03
gather=True runtime_loop=False occupancy=1: threads 256 regs 255 smem 44100 B 15.84 ms rel err 1.7e-03
gather=True runtime_loop=False occupancy=2: threads 256 regs 128 smem 44100 B 13.24 ms rel err 1.7e-03
gather=True runtime_loop=False occupancy=4: threads 256 regs 255 smem 44100 B 15.84 ms rel err 1.7e-03
gather=True runtime_loop=True occupancy=1: threads 384 regs 168 smem 44092 B 17.64 ms rel err 1.7e-03
gather=True runtime_loop=True occupancy=2: threads 256 regs 128 smem 44068 B 13.36 ms rel err 1.7e-03
gather=True runtime_loop=True occupancy=4: threads 384 regs 168 smem 44092 B 17.64 ms rel err 1.7e-03Full env printout
OS Ubuntu 26.04.1 LTS, kernel 7.0.0-38-generic
GPU NVIDIA GeForce RTX 5070 Ti (sm_120), driver 595.91.07
Python 3.12.14
cuda-tile 1.6.0rc5
cuda-python 13.4.4
torch 2.14.1+cu130
nvidia-cuda-nvcc 13.4.92
nvidia-nvvm 13.4.92
nvidia-cuda-tileiras 13.4.92 (and 13.3.36 for the comparison)
flashinfer-python 0.7.2 (for the FlashInfer numbers only)Other/Misc.
Both runs use the same cuda-tile, Python and torch, so only tileiras differs. cuda-tile takes the pip tileiras only
when its minor version matches nvidia-cuda-nvcc and nvidia-nvvm. For the 13.3.36 run we put that tileiras on
PATH and skipped the pip lookup (cuda.tile._compile._find_pip_tileiras = lambda: None).
occupancy=4 comes out the same as occupancy=1 on both versions; we assume the hint cannot be met there.
Activity