[BUG] CuTe DSL 4.7.1: the same MLIR compiles to different PTX from run to run (address-dependent codegen in the embedded libNVVM 13.3); not reproduced in 4.8.1
Maintainers usually reply within 4 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
Research direction
Start with the supplied repro.py and run it against CuTe DSL 4.7.1 as described; it saves the input MLIR and pipeline and reports PTX variants. Compare results with 4.8.1 and inspect the recorded environment and libNVVM version details. Done means establishing whether libNVVM 13.3 nondeterminism is known or guaranteed, and identifying whether 13.4 fixes it or merely avoids this reproduction.
Written by the indexing model from the issue text.
Description
Which component has the problem?
CuTe DSL
Bug Report
Describe the bug
Compiling the same kernel with CuTe DSL 4.7.1 does not always give the same PTX/SASS. In our builds about 2.5% of the kernels (161 of 6491, every target from sm_80 to sm_121a) came out in two or more variants over three cold builds of the same tree. Which kernels vary changes from build to build. The variants differ in instruction order and register numbering, and sometimes in register count or a folded constant.
The input to the backend does not vary. The MLIR the DSL hands to its pass pipeline is byte-identical in every run, and so is the LLVM-dialect module after the DSL's MLIR passes (cute-to-nvvm{... llvm-only=true}). The variation is in the libNVVM step, and it depends on where the compiler's allocations land:
- The same input gives different PTX across processes, and also when the same module goes through the pipeline again in one process.
- With ASLR disabled (
setarch -R) and no extra threads in the process (OPENBLAS_NUM_THREADS=1; numpy's OpenBLAS starts its pool at import), 32 of 32 runs gave the same PTX. Either one alone was not enough. - Our kernels' LLVM IR, fed as text to the public
nvidia-nvvm==13.3.73libNVVM through the C API (nvvmCompileProgram, no DSL involved), gives the same PTX variants. It varies at-opt=2/-opt=3and not at-opt=0/-opt=1. Withnvidia-nvvm13.4.59 or 13.4.92, every run gives the same PTX. We cannot share that IR; the text IR of the CUTLASS example below is rejected by libNVVM 13.3's parser ("parse expected type" in the kernel's parameter list), so the repro below goes through the DSL's pipeline.
With CuTe DSL 4.8.1 (embedded libNVVM 13.4.59) we see no variation: 0 of 6491 kernels over five cold builds, and 180 of 180 runs of the repro below.
Steps/Code to reproduce bug
Public code only: the Blackwell FMHA example of this repository at v4.7.1 (causal, head_dim 128, non-persistent), compiled for sm_100a with fake tensors. No GPU and no PyTorch needed.
pip install "nvidia-cutlass-dsl[cu13]==4.7.1"
git clone --depth 1 -b v4.7.1 https://github.com/NVIDIA/cutlass
python repro.py cutlass 120
The script:
- Calls
cute.compileonce and saves the module the DSL hands to its pass pipeline (generic form) and the pipeline string. - Runs that module through that pipeline N times in one process: a new
ir.Context,ir.Module.parse,PassManager.parse(pipeline).run(...), then a hash of the dumped PTX. - Between runs, mallocs blocks of seeded random sizes and frees every other one, so each run sees another heap layout.
It prints the variants and exits 1 if there is more than one.
repro.py
"""CuTe DSL 4.7.1: the same MLIR, run through the DSL's own cute-to-nvvm pipeline again in the same process, gives
different PTX. The kernel is the Blackwell FMHA example of the CUTLASS repository (v4.7.1; causal, head_dim 128,
non-persistent), compiled for sm_100a with fake tensors: no GPU and no PyTorch needed.
pip install "nvidia-cutlass-dsl[cu13]==4.7.1"
git clone --depth 1 -b v4.7.1 https://github.com/NVIDIA/cutlass
python repro.py cutlass [N]
1. cute.compile of the kernel, once, saving the module the DSL hands to its pass pipeline (generic form) and the
pipeline string: ./input.mlir, ./pipeline.txt.
2. N times (default 120): a new ir.Context, parse ./input.mlir, PassManager.parse(pipeline).run(...), hash the PTX the
pipeline dumps. Before each run the script mallocs blocks of seeded random sizes and frees every other one, so the
runs see different heap layouts.
It prints how many runs gave each PTX, keeps one PTX per variant in ./variants/ and exits 1 if there is more than one.
"""
import collections, ctypes, hashlib, os, random, re, shutil, sys, tempfile, types
from pathlib import Path
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "")
os.environ.setdefault("CUTE_DSL_ARCH", "sm_100a")
cutlass_dir = Path(sys.argv[1])
count = int(sys.argv[2]) if len(sys.argv) > 2 else 120
# the example imports torch at module level but uses it only in its run(): a stand-in is enough to compile
class _Stub(types.ModuleType):
def __getattr__(self, name):
if name.startswith("__"):
raise AttributeError(name)
return _Stub(f"{self.__name__}.{name}")
def __call__(self, *args, **kwargs):
return _Stub("call")
try:
import torch # noqa: F401
except ImportError:
sys.modules["torch"] = _Stub("torch")
examples = cutlass_dir / "examples/python/CuTeDSL"
sys.path[:0] = [str(examples), str(examples / "cute/blackwell/kernel/attention/fmha")]
import cutlass
import cutlass.cute as cute
from cutlass._mlir import ir, passmanager
from cutlass.base_dsl import compiler
from cutlass.cute.typing import Float32, Int32
import fmha
from helpers import fmha_helpers
# 1. the module and pipeline of one compile
compile_module = compiler.Compiler.compile
def keep_input(self, module, pipeline, *args, **kwargs):
if "cute-to-nvvm" in pipeline:
Path("input.mlir").write_text(module.operation.get_asm(print_generic_op_form=True))
Path("pipeline.txt").write_text(re.sub(r"dump-ptx-path='[^']*'", "", pipeline))
return compile_module(self, module, pipeline, *args, **kwargs)
compiler.Compiler.compile = keep_input
def fake(rank):
shape = tuple(cute.sym_int() for _ in range(rank - 1)) + (128,)
stride = tuple(cute.sym_int64(divisibility=8) for _ in range(rank - 1)) + (1,)
return cute.runtime.make_fake_tensor(cutlass.Float16, shape, stride=stride, assumed_align=32)
kernel = fmha.BlackwellFusedMultiHeadAttentionForward(
Float32, Float32, (128, 128), 128, False, fmha_helpers.MaskEnum.WINDOW_MASK_INFERENCE, False, False
)
q, k, v, o = (fake(5) for _ in range(4))
cute.compile(
kernel, q, k, v, o, tuple(Int32(1) for _ in range(8)), None, None, None, None,
Float32(1.0), Float32(1.0), Float32(1.0), None, None, Int32(0), None, None, cute.runtime.make_fake_stream(), False,
options="--gpu-arch=sm_100a --opt-level=2",
)
compiler.Compiler.compile = compile_module
text = Path("input.mlir").read_text()
pipeline = Path("pipeline.txt").read_text()
# 2. the same module through the same pipeline, again and again
libc = ctypes.CDLL(None)
libc.malloc.restype = ctypes.c_void_p
libc.free.argtypes = [ctypes.c_void_p]
rng = random.Random(0)
work = Path(tempfile.mkdtemp(prefix="cutedsl_repro_"))
out = Path("variants")
shutil.rmtree(out, ignore_errors=True)
out.mkdir()
seen = collections.Counter()
for i in range(count):
blocks = [libc.malloc(rng.choice((16, 24, 40, 72, 136, 264, 520, 1032, 4104, 65544))) for _ in range(rng.randrange(1, 3000))]
for block in blocks[::2]:
libc.free(block)
dump = work / f"run{i}"
with ir.Context() as context:
context.enable_multithreading(False)
module = ir.Module.parse(text)
passmanager.PassManager.parse(pipeline.rstrip("})") + f" dump-ptx-path='{dump}'}})").run(module.operation)
(ptx,) = work.glob(f"run{i}.*ptx")
digest = hashlib.sha256(ptx.read_bytes()).hexdigest()[:12]
seen[digest] += 1
if not (out / f"{digest}.ptx").exists():
shutil.copy(ptx, out / f"{digest}.ptx")
print(f"run {i}: {digest}", flush=True)
shutil.rmtree(work)
print(f"cutlass-dsl {cutlass.__version__}: {count} runs of one module, {len(seen)} PTX variants: {dict(seen)}")
sys.exit(1 if len(seen) > 1 else 0)
Observed with 4.7.1: two PTX variants. Six processes of 60 runs each gave 55/5, 60/0, 53/7, 58/2, 55/5 and 57/3, so 22 of 360 runs produced the second variant. Diff excerpt (the rest is register renumbering):
- .reg .b64 %rd<3000>;
+ .reg .b64 %rd<2999>;
@@
- cvta.param.u64 %rd399, %rd4;
+ cvta.param.u64 %rd398, %rd4;
+ prefetch.tensormap [%rd398];
+ cvta.param.u64 %rd399, %rd3;
...
- cvta.param.u64 %rd402, %rd1;
- prefetch.tensormap [%rd402];
With nvidia-cutlass-dsl[cu13]==4.8.1 the same script gives one PTX in every run.
Expected behavior
The same input, compiler and options always give the same PTX and cubin.
Environment details (please complete the following information):
- Environment location: Docker container on bare metal, Linux x86_64 (Ubuntu 24.04.3, glibc 2.39, kernel 5.10). Compile only (
CUTE_DSL_ARCH=sm_100a,CUDA_VISIBLE_DEVICES=), no GPU used. - nvidia-cutlass-dsl 4.7.1 (
nvidia-cutlass-dsl-libs-cu134.7.1; embedded libNVVM "CL-37800683, Cuda compilation tools, release 13.3, V13.3.27"). Compared with 4.8.1 (embedded libNVVM 13.4, V13.4.59). - Standalone libNVVM checks:
nvidia-nvvm13.3.73 varies; 13.4.59 and 13.4.92 do not. - Python 3.13.14.
Additional context
Ruled out:
- The Python frontend: the pipeline input is byte-identical in every run.
PYTHONHASHSEED: fixing it does not help.- Dump paths and the pid: with ASLR off and one thread, runs with different paths and pids gave the same PTX.
- MLIR multithreading: disabled on every context in the repro.
- #3564 (
link-librariesorder): our pipelines link no libraries.
Questions:
- Is this a known libNVVM 13.3 issue that was fixed in 13.4? The 4.8.x and CUDA 13.4 release notes do not mention it, and we could not tell a fix from a change that no longer triggers on these kernels.
- Is deterministic code generation (same input and options give the same PTX and cubin) something CuTe DSL and libNVVM guarantee and test? We rely on it to cache and compare compiled kernels.
- Dominant language
- C++
- Stars
- 10.7k
- Forks
- 2.2k
- Avg merge
- 3d 20h
- Merged PRs (30d)
- 4
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
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.
More from NVIDIA/cutlass
-
[Bug][CuTeDSL][Fuzz] `utils.gemm.sm100.epilogue` releases the accumulator before `tcgen05.ld` completes: no `tcgen05.wait::ld` before the arrive that lets the next MMA overwrite TMEMPossibly taken @0z5a claimed this 3 days ago. Openbug CuTe DSL
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 4 days
-
? - Needs Triage question
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
NVIDIA/cutlass#3796 · 1 comment · 1 reaction ·
Maintainers usually reply within 4 days
-
[DOC] IKET quick start omits the GEMM GPU requirement and references a missing examplePossibly taken @0z5a claimed this 14 days ago. Open
Difficulty 1/5 Under an hour Newbie friendliness 82/100
NVIDIA/cutlass#3761 · 1 comment ·
Maintainers usually reply within 4 days
-
[BUG] CuTe DSL from_dlpack rejects float8_e4m3fn tensors (dtype_map lists Float8E5M2 only)Possibly taken @Ammar-Alnagar claimed this 23 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
NVIDIA/cutlass#3716 · 1 comment ·
Maintainers usually reply within 4 days
-
EllGemm ColumnMajor-output workspace sizing uses the unswapped problem shape and undersizes split-K semaphoresPossibly taken @XFDG claimed this 52 days ago. OpenCUTLASS C++
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
NVIDIA/cutlass#3539 · 2 comments ·
Maintainers usually reply within 4 days
Similar issues
-
Code: Performance
Difficulty 1/5 Under an hour Newbie friendliness 88/100
CrimsonCrossBunker/Cataclysm-Cleanwater-Bomb#1167 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
meshcore-dev/MeshCore#3760 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
deepseek-ai/DeepGEMM-Ascend#29 ·
Maintainers usually reply within 1 day
-
ci Windows
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
aethersdr/AetherSDR#7003 · 1 comment ·
Maintainers usually reply within 1 day