Skip to content

[BUG]: tileiras 13.4 ignores the occupancy hint on sm_120 when ct.gather is inside a runtime-trip-count loop #117

Open

Description

@05yuki

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-03

Full 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions