[BUG] Explicit 2D block indexing breaks predicate simplification compared to implicit 1D indexing
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 10.5k
- Forks
- 2.1k
- Avg merge
- 3d 11h
- Merged PRs (30d)
- 7
Description
Which component has the problem?
CuTe DSL
Bug Report
Describe the bug
CuteDSL fails to simplify and consolidate symbolic expressions for out-of-bound predicates when a 2D block tensor is explicitly indexed via a tuple coordinate (bidy, bidx).
-
Using a 1D coordinate
bidx(after transposing the thread block mapping in row-major order) allows the compiler to successfully consolidate predicates. -
Even though the explicit 2D coordinate
(bidy, bidx)is semantically and mathematically identical to the 1D variant, it generates scattered, element-wise predicates instead.
This will lead to branch divergence and performance degradation.
Steps/Code to reproduce bug
import cutlass
import cutlass.cute as cute
from cutlass.cute import KeepPTX
# =====================================================================
# Variant 1: 1D Indexing
# =====================================================================
@cute.kernel
def add_one_copy_atom_pred_kernel_1d(
x: cute.Tensor, crd: cute.Tensor, shape: cute.Shape,
thr_layout: cute.Layout, val_layout: cute.Layout
):
tidx, _, _ = cute.arch.thread_idx()
bidx, _, _ = cute.arch.block_idx()
blkx = x[(None, None), bidx]
blkCrd = crd[(None, None), bidx]
copy_atom = cute.make_copy_atom(
cute.nvgpu.CopyUniversalOp(), x.element_type
)
tile_copy: cute.TiledCopy = cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
thr_copy = tile_copy.get_slice(tidx)
thrx = thr_copy.partition_S(blkx)
thrCrd = thr_copy.partition_S(blkCrd)
frgPred = cute.make_rmem_tensor(thrCrd.shape, cutlass.Boolean)
for i in range(cute.size(frgPred)):
val = cute.elem_less(thrCrd[i], shape)
frgPred[i] = val
frgx = cute.make_fragment_like(thrx)
cute.copy(copy_atom, thrx, frgx, pred=frgPred)
result = frgx.load() + cutlass.BFloat16(1)
frgx.store(result)
cute.copy(copy_atom, frgx, thrx, pred=frgPred)
@cute.jit
def add_one_copy_atom_pred_1d(x: cute.Tensor):
tile_m = 8
tile_n = 256
num_threads_per_block = 256
elems_per_load = 128 // x.element_type.width
threads_per_row = tile_n // elems_per_load
rows_per_load = num_threads_per_block // threads_per_row
iters = tile_m // rows_per_load
thr_layout = cute.make_ordered_layout((rows_per_load, threads_per_row), order=(1, 0))
val_layout = cute.make_ordered_layout((iters, elems_per_load), order=(1, 0))
tiler_mn, _ = cute.make_layout_tv(thr_layout, val_layout)
gx = cute.zipped_divide(x, tiler_mn)
crd = cute.make_identity_tensor(x.shape)
gcrd = cute.zipped_divide(crd, tiler_mn)
remap_block = cute.make_ordered_layout(
cute.select(gx.shape[1], mode=[1, 0]), order=(1, 0)
)
gx = cute.composition(gx, (None, remap_block))
gcrd = cute.composition(gcrd, (None, remap_block))
add_one_copy_atom_pred_kernel_1d(gx, gcrd, x.shape, thr_layout, val_layout).launch(
grid=(cute.size(gx, mode=[1]), 1, 1),
block=(num_threads_per_block, 1, 1)
)
# =====================================================================
# Variant 2: Explicit 2D Indexing
# =====================================================================
@cute.kernel
def add_one_copy_atom_pred_kernel_2d(
x: cute.Tensor, crd: cute.Tensor, shape: cute.Shape,
thr_layout: cute.Layout, val_layout: cute.Layout
):
tidx, _, _ = cute.arch.thread_idx()
bidx, bidy, _ = cute.arch.block_idx()
blkx = x[(None, None), (bidy, bidx)]
blkCrd = crd[(None, None), (bidy, bidx)]
copy_atom = cute.make_copy_atom(
cute.nvgpu.CopyUniversalOp(), x.element_type
)
tile_copy: cute.TiledCopy = cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
thr_copy = tile_copy.get_slice(tidx)
thrx = thr_copy.partition_S(blkx)
thrCrd = thr_copy.partition_S(blkCrd)
frgPred = cute.make_rmem_tensor(thrCrd.shape, cutlass.Boolean)
for i in range(cute.size(frgPred)):
val = cute.elem_less(thrCrd[i], shape)
frgPred[i] = val
frgx = cute.make_fragment_like(thrx)
cute.copy(copy_atom, thrx, frgx, pred=frgPred)
result = frgx.load() + cutlass.BFloat16(1)
frgx.store(result)
cute.copy(copy_atom, frgx, thrx, pred=frgPred)
@cute.jit
def add_one_copy_atom_pred_2d(x: cute.Tensor):
tile_m = 8
tile_n = 256
num_threads_per_block = 256
elems_per_load = 128 // x.element_type.width
threads_per_row = tile_n // elems_per_load
rows_per_load = num_threads_per_block // threads_per_row
iters = tile_m // rows_per_load
thr_layout = cute.make_ordered_layout((rows_per_load, threads_per_row), order=(1, 0))
val_layout = cute.make_ordered_layout((iters, elems_per_load), order=(1, 0))
tiler_mn, _ = cute.make_layout_tv(thr_layout, val_layout)
gx = cute.zipped_divide(x, tiler_mn)
crd = cute.make_identity_tensor(x.shape)
gcrd = cute.zipped_divide(crd, tiler_mn)
add_one_copy_atom_pred_kernel_2d(gx, gcrd, x.shape, thr_layout, val_layout).launch(
grid=(gx.shape[1][1], gx.shape[1][0], 1),
block=(num_threads_per_block, 1, 1)
)
import torch
from cutlass.cute.runtime import from_dlpack
x = torch.randn(16384, 16384, device="cuda", dtype=torch.bfloat16)
x_ = from_dlpack(x, assumed_align=16)
cute.compile[KeepPTX](add_one_copy_atom_pred_1d, x_)
cute.compile[KeepPTX](add_one_copy_atom_pred_2d, x_)
The issue can be clearly illustrated by the generated PTX codes, or by benchmarking two kernel. It turns out that the 2D variant is significantly slower.
Expected behavior
Both indexing variants should produce equivalent, fully simplified predicates for out-of-bound checks since their block level layouts are identical.
Environment details (please complete the following information):
- GPU: RTX 5090
- CUTLASS: 4.5.2
- CUDA: 13.1.2
Additional context
Here is the comparison of the generated PTX. Note how the 1D variant successfully consolidates predicates, while the 2D variant generates divergent instructions.
1D variant (all predications are identical)
...
setp.gt.u32 %p1, %r1, 16383;
@%p1 bra $L__BB0_2;
ld.global.b16 %rs1, [%rd1];
mov.b32 %r89, {%rs1, %rs2};
$L__BB0_2:
setp.gt.u32 %p2, %r1, 16383;
@%p2 bra $L__BB0_4;
ld.global.b16 %rs3, [%rd1+2];
{ .reg .b16 tmp; mov.b32 {%rs4, tmp}, %r89; }
mov.b32 %r89, {%rs4, %rs3};
$L__BB0_4:
setp.gt.u32 %p3, %r1, 16383;
@%p3 bra $L__BB0_6;
ld.global.b16 %rs5, [%rd1+4];
{ .reg .b16 tmp; mov.b32 {tmp, %rs6}, %r15; }
mov.b32 %r93, {%rs5, %rs6};
...
2D variant (predications differ, leading to warp divergence)
...
setp.gt.u32 %p7, %r81, 16383;
setp.lt.u32 %p8, %r81, 16384;
setp.gt.s32 %p9, %r82, 16383;
setp.lt.s32 %p10, %r82, 16383;
and.pred %p1, %p8, %p10;
setp.lt.s32 %p11, %r82, 16382;
and.pred %p2, %p8, %p11;
setp.lt.s32 %p12, %r82, 16380;
and.pred %p3, %p8, %p12;
setp.lt.s32 %p13, %r82, 16379;
and.pred %p4, %p8, %p13;
setp.lt.s32 %p14, %r82, 16378;
and.pred %p5, %p8, %p14;
setp.lt.s32 %p15, %r82, 16377;
and.pred %p6, %p8, %p15;
or.pred %p16, %p7, %p9;
@%p16 bra $L__BB0_2;
ld.global.b16 %rs9, [%rd1];
mov.b32 %r119, {%rs9, %rs10};
$L__BB0_2:
not.pred %p17, %p1;
@%p17 bra $L__BB0_4;
ld.global.b16 %rs11, [%rd1+2];
{ .reg .b16 tmp; mov.b32 {%rs12, tmp}, %r119; }
mov.b32 %r119, {%rs12, %rs11};
$L__BB0_4:
not.pred %p18, %p2;
@%p18 bra $L__BB0_6;
ld.global.b16 %rs13, [%rd1+4];
{ .reg .b16 tmp; mov.b32 {tmp, %rs14}, %r14; }
mov.b32 %r123, {%rs13, %rs14};
...
Similar kernels of the two variants, only without predication, will generate identical PTX codes as expected, proving the issue lies strictly in the predication logic.
@cute.kernel
def add_one_copy_atom_kernel_1d(
x: cute.Tensor,
thr_layout: cute.Layout, val_layout: cute.Layout
):
tidx, _, _ = cute.arch.thread_idx()
bidx, _, _ = cute.arch.block_idx()
blkx = x[(None, None), bidx]
copy_atom = cute.make_copy_atom(
cute.nvgpu.CopyUniversalOp(), x.element_type
)
tile_copy: cute.TiledCopy = cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
thr_copy = tile_copy.get_slice(tidx)
thrx = thr_copy.partition_S(blkx)
frgx = cute.make_fragment_like(thrx)
cute.copy(copy_atom, thrx, frgx)
result = frgx.load() + cutlass.BFloat16(1)
frgx.store(result)
cute.copy(copy_atom, frgx, thrx)
@cute.jit
def add_one_copy_atom_1d(x: cute.Tensor):
# ... (same as previous kernel)
gx = cute.zipped_divide(x, tiler_mn)
remap_block = cute.make_ordered_layout(
cute.select(gx.shape[1], mode=[1, 0]), order=(1, 0)
)
gx = cute.composition(gx, (None, remap_block))
add_one_copy_atom_kernel_1d(gx, thr_layout, val_layout).launch(
grid=(cute.size(gx, mode=[1]), 1, 1),
block=(num_threads_per_block, 1, 1)
)
@cute.kernel
def add_one_copy_atom_kernel_2d(
x: cute.Tensor,
thr_layout: cute.Layout, val_layout: cute.Layout
):
tidx, _, _ = cute.arch.thread_idx()
bidx, bidy, _ = cute.arch.block_idx()
blkx = x[(None, None), (bidy, bidx)]
copy_atom = cute.make_copy_atom(
cute.nvgpu.CopyUniversalOp(), x.element_type
)
tile_copy: cute.TiledCopy = cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
thr_copy = tile_copy.get_slice(tidx)
thrx = thr_copy.partition_S(blkx)
frgx = cute.make_fragment_like(thrx)
cute.copy(copy_atom, thrx, frgx)
result = frgx.load() + cutlass.BFloat16(1)
frgx.store(result)
cute.copy(copy_atom, frgx, thrx)
@cute.jit
def add_one_copy_atom_2d(x: cute.Tensor):
# ... (same as previous kernel)
gx = cute.zipped_divide(x, tiler_mn)
add_one_copy_atom_kernel_2d(gx, thr_layout, val_layout).launch(
grid=(gx.shape[1][1], gx.shape[1][0], 1),
block=(num_threads_per_block, 1, 1)
)
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by compiling add_one_copy_atom_pred_1d and add_one_copy_atom_pred_2d with KeepPTX, then compare the generated predicates shown in the issue. Trace the CuTe DSL predicate simplification and layout-indexing paths involved in the two kernel entry points. Done means equivalent consolidated predicates for both variants, with the reproduced performance difference removed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, python
- Domain
- compilers, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100