Repository navigation
[BUG][CuTeDSL] cute.copy(..., tma_desc_ptr=TensorMap.get_ptr()) is silently ignored on tiled TMA loads #3670
Description
Which component has the problem?
CuTe DSL
Bug Report
Describe the bug
Passing the pointer from cutlass.experimental.cuda.TensorMap.get_ptr() as tma_desc_ptr= to cute.copy on a tiled TMA load compiles and runs, but the copy keeps using the descriptor that make_tiled_tma_atom baked into the atom. The same address, re-typed as a cute pointer with TensorMapManager.get_tensormap_ptr(..., AddressSpace.generic), is honoured.
Every TMA trait's unpack that takes tma_desc_ptr (cute/nvgpu/cpasync/copy.py, 4.8.0 lines 266, 484, 750, 886, 1005, 1184, 1580) sets the tma_descriptor_ptr field only if isinstance(tma_desc_ptr, Pointer), where Pointer is cutlass.cute.typing.Pointer. TensorMap.get_ptr() returns cutlass.Pointer as documented, i.e. cutlass.base_dsl.typing.Pointer, which is not a subclass of cute.typing.Pointer. Anything else is skipped in Python and never reaches the IR.
Steps/Code to reproduce bug
One CTA TMA-loads a 64x64 fp32 tile of x into y. The kernel also gets a same-box TensorMap based at z, so an honoured override gives y == z.
import hashlib, re, sys
import torch, cutlass, cutlass.cute as cute
from cutlass.cute.nvgpu import cpasync
from cutlass.cute.runtime import from_dlpack
from cutlass.experimental.cuda import TensorMap, create_tensor_map_tiled_from_view
from cutlass.utils import SmemAllocator, TensorMapManager, TensorMapUpdateMode
from cutlass._mlir import ir
from cutlass._mlir.dialects import llvm
M, N = 64, 64
@cute.kernel
def load_tile(atom: cute.CopyAtom, mX: cute.Tensor, y: cute.Tensor,
desc: cutlass.GridConstant[TensorMap], mode: cutlass.Constexpr):
tidx, _, _ = cute.arch.thread_idx()
smem = SmemAllocator()
sX = smem.allocate_tensor(cutlass.Float32, cute.make_layout((M, N, 1), stride=(N, 1, M * N)), 128)
bar = smem.allocate_array(cutlass.Int64, 1)
gX = cute.local_tile(mX, (M, N), (None, None))
tXsX, tXgX = cpasync.tma_partition(atom, 0, cute.make_layout(1),
cute.group_modes(sX, 0, 2), cute.group_modes(gX, 0, 2))
if tidx == 0:
cute.arch.mbarrier_init(bar, 1)
cute.arch.mbarrier_init_fence()
cute.arch.sync_threads()
if tidx == 0:
cute.arch.mbarrier_arrive_and_expect_tx(bar, M * N * 4)
if tidx < 32: # whole warp: the TMA copy elects one lane itself (elect.sync)
if cutlass.const_expr(mode == "baked"):
cute.copy(atom, tXgX[(None, 0, 0)], tXsX[(None, 0)], tma_bar_ptr=bar)
elif cutlass.const_expr(mode == "get_ptr"):
p = desc.get_ptr()
print(f" get_ptr() -> {type(p).__module__}.{type(p).__qualname__}, "
f"isinstance(p, cute.Pointer) = {isinstance(p, cute.Pointer)}")
cute.copy(atom, tXgX[(None, 0, 0)], tXsX[(None, 0)], tma_bar_ptr=bar, tma_desc_ptr=p)
else:
# same address as get_ptr(), re-typed as a generic cute pointer to a tensormap; the manager
# asserts its second argument as the pointer alignment, and the descriptor param is .align 64 in PTX
addr = cutlass.Int64(llvm.ptrtoint(ir.IntegerType.get_signless(64), desc.get_ptr()))
p = TensorMapManager(TensorMapUpdateMode.GMEM, 64).get_tensormap_ptr(
cute.make_ptr(cutlass.Int64, addr, cute.AddressSpace.gmem, assumed_align=64),
cute.AddressSpace.generic)
cute.copy(atom, tXgX[(None, 0, 0)], tXsX[(None, 0)], tma_bar_ptr=bar, tma_desc_ptr=p)
cute.arch.mbarrier_wait(bar, 0)
for m in cutlass.range_constexpr(M):
y[m, tidx] = sX[m, tidx, 0]
@cute.jit
def run(x: cute.Tensor, z: cute.Tensor, y: cute.Tensor, mode: cutlass.Constexpr):
atom, mX = cpasync.make_tiled_tma_atom(
cpasync.CopyBulkTensorTileG2SOp(), x, cute.make_layout((M, N), stride=(N, 1)), (M, N))
desc = create_tensor_map_tiled_from_view(z, box_dims=(M, N)) # same box as the atom, base = z
load_tile(atom, mX, y, desc, mode).launch(grid=(1, 1, 1), block=(N, 1, 1), smem=M * N * 4 + 1024)
def tma_lines(ptx):
return [l.strip() for l in ptx.splitlines() if "cp.async.bulk.tensor" in l]
print("cutlass", cutlass.__version__, "|", torch.cuda.get_device_name(), "| torch", torch.__version__)
x = torch.arange(M * N, dtype=torch.float32, device="cuda").reshape(M, N)
z = -x - 1
fx, fz = from_dlpack(x, assumed_align=16), from_dlpack(z, assumed_align=16)
bodies = {}
for mode in ("baked", "get_ptr", "retyped"):
print(f"[{mode}]")
y = torch.zeros_like(x)
fy = from_dlpack(y, assumed_align=16)
compiled = cute.compile(run, fx, fz, fy, mode, options="--keep-ptx")
compiled(fx, fz, fy)
torch.cuda.synchronize()
ptx = compiled.__ptx__ or ""
body = re.sub(r"\w*load_tile\w*", "K", ptx)
bodies[mode] = hashlib.md5(body.encode()).hexdigest()[:12]
for l in tma_lines(ptx):
print(" ptx:", l)
print(f" y == x: {torch.equal(y, x)} y == z: {torch.equal(y, z)} ptx md5 (name-normalised): {bodies[mode]}")
print("get_ptr PTX identical to baked PTX:", bodies["get_ptr"] == bodies["baked"])stdout on 4.8.0; three deprecation warnings omitted (4.7.1 differs only in the version and PTX hashes):
cutlass 4.8.0 | NVIDIA H100 80GB HBM3 | torch 2.12.0a0+5aff3928d8.nv26.05
[baked]
ptx: cp.async.bulk.tensor.2d.shared::cta.global.tile.mbarrier::complete_tx::bytes.L2::cache_hint [%r1], [%rd4, {%r7, %r7}], [%r8], %rd5;
y == x: True y == z: False ptx md5 (name-normalised): 9f27b4d01fb2
[get_ptr]
get_ptr() -> cutlass.base_dsl.typing.Pointer, isinstance(p, cute.Pointer) = False
ptx: cp.async.bulk.tensor.2d.shared::cta.global.tile.mbarrier::complete_tx::bytes.L2::cache_hint [%r1], [%rd4, {%r7, %r7}], [%r8], %rd5;
y == x: True y == z: False ptx md5 (name-normalised): 9f27b4d01fb2
[retyped]
ptx: cp.async.bulk.tensor.2d.shared::cta.global.tile.mbarrier::complete_tx::bytes.L2::cache_hint [%r1], [%rd4, {%r7, %r7}], [%r8], %rd5;
y == x: False y == z: True ptx md5 (name-normalised): 6b927178b420
get_ptr PTX identical to baked PTX: True
Expected behavior
Either honour the pointer (return a cute pointer from get_ptr(), or accept base_dsl.typing.Pointer in unpack), or raise in unpack when tma_desc_ptr has an unsupported type.
Environment details (please complete the following information):
- Environment location: container (NGC PyTorch 26.05 image)
- nvidia-cutlass-dsl 4.7.1 and 4.8.0, H100 80GB HBM3 (sm_90), driver 590.44.01, CUDA 13.2, Python 3.12, torch 2.12
Additional context
copy.py and experimental/cuda/tensor_map.py on main@0b55a2f are identical to 4.8.0. The new CopyBulkTensorTileG2SOverrideOp (address/dims/strides override) takes no descriptor pointer.
Activity
kvnloo commentedon Sep 27, 2026
This looks like a particularly dangerous API failure because the override is silently ignored.
If a caller supplies:
tma_desc_ptr=...then I think only two outcomes should be valid:
Falling back to the descriptor baked into the atom makes the kernel run successfully with the wrong source, which is much harder to detect.
The underlying mismatch seems to be between:
cutlass.Pointerand:
cute.typing.Pointerin the current
isinstance()gate.A focused regression could use two TensorMaps pointing at different source tensors and assert:
That directly tests descriptor ownership rather than only inspecting Python types.