Repository navigation
feat(unroll): add #[nounroll] to keep loops rolled - #1552
Open
praveenperera wants to merge 1 commit into
Open
praveenperera wants to merge 1 commit into
praveenperera wants to merge 1 commit into
Conversation
ptxas fully unrolls small constant-trip-count loops even when the PTX keeps them rolled, and #[unroll(N)] cannot ask for a factor of one. #[nounroll] on a loop in a #[kernel] or #[device] function now keeps it rolled through LLVM, libNVVM and ptxas. The macro plants a marker that the importer turns into a mir.nounroll_hint. The hint survives MIR preparation and is copied with any loop that #[unroll] clones. Just before lowering, each hint's loop gets a single latch, and the exporter attaches llvm.loop.unroll.disable to that latch's branch, so full-debug builds get it too. WGMMA counted-loop fusion accepts a hint inside the K loop it replaces and starts the fused loop with .pragma "nounroll". The nounroll example covers nested loops, the main and remainder loops of a partial unroll, range for loops and device functions, and its shape check runs in the compile-only smoketest. Closes NVIDIA#1468 Signed-off-by: Praveen Perera <me@praveenperera.com>
praveenperera
marked this pull request as ready for review
October 19, 2026 21:27
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1468.
What this adds
#[nounroll]on a loop in a#[kernel]or#[device]function keeps that loop rolled through LLVM, libNVVM and ptxas. ptxas otherwise fully unrolls small constant-trip-count loops even when the PTX keeps them rolled.The loop's latch branch carries
!llvm.loopmetadata withllvm.loop.unroll.disable. The NVPTX backend prints.pragma "nounroll";at the loop header, and libNVVM reads the same metadata.How it works
cuda-macrosstrips#[nounroll]fromfor,whileandloopand inserts a__nounroll_config()marker at the top of the body, as it does for#[unroll]. It rejects arguments and a loop carrying both#[unroll]and#[nounroll]. A literal#[unroll(1)]now points to#[nounroll].mir-importeremits amir.nounroll_hintop. The op has noSideEffectsimpl, so DCE keeps it, and the unroll pass ignores it: unrolling an enclosing loop copies the hint along with the inner loop.mir_transforms::nounroll::resolve_nounroll_hintsruns at the start oflower_mir_to_llvm, after WGMMA fusion, the last transform that reshapes loops. For each hint it finds the innermost loop, joins its back-edges through one latch with the existingmerge_backedges, removes the hint, and returns the latch'smir.goto. Because it runs in lowering rather than MIR preparation, full-debug builds get it too.mir-lowermarks those gotos, and the branch conversion carries the mark tollvm.br., !llvm.loop !Nto a marked branch and emits onedistinctnode per loop plus one shared!{!"llvm.loop.unroll.disable"}node, from the module's single metadata ID counter.The single latch matters because LLVM reads loop metadata from the latch terminator and ignores it when several latches disagree. A branch that ends an inner loop and also jumps to the outer header would otherwise apply the request to both loops.
Interactions asked about in #1468
#[unroll(4)]around a#[nounroll]loop leaves four inner copies in the main loop and one in the remainder loop, and all five are marked. The main and remainder loops themselves are not..pragma "nounroll";per annotated source loop.forloops #1346 rangeforloops:#[nounroll]works on a rangeforloop, including one inside a fully unrolled loop.#[nounroll]on an F16 m64n64 K loop failed to compile, since F16 has no pointer fallback, and a BF16 one quietly lost the fused value-carrier form. The fused loop is one inline-PTX operation with no LLVM loop to mark, so fusion moves the request onto that operation, and its template starts the loop header with.pragma "nounroll";. This covers both the counted loop and the counted pipeline.#[unroll(N)]leaves behind, because that loop carries no metadata. Any#[nounroll]loop inside it is cloned and every clone stays rolled. The same latch metadata could close [BUG]: #[unroll(N)] emits no loop metadata, so the NVVM backend unrolls the loops a second time #1375 by marking the main and remainder loops; that changes existing#[unroll(N)]codegen, so it is left for a follow-up.Testing
mir-transforms/tests/nounroll.rs: latch placement for an inner and an outer loop of a nest, a loop with twocontinuepaths, a#[nounroll]loop inside a full and a partial#[unroll](3 and 5 marked copies, main and remainder unmarked), and a hint outside any loop. Each test checks the returned branch is the marked loop's only latch.mir-lower/tests/lowering_test/loops.rs: three sibling loops, two hinted, go throughlower_mir_to_llvmand both the PTX and legacy NVVM exporters. Exactly two marked latches come out, each with its owndistinctself-referencing node, one shared property node, and no metadata ID defined twice next to!nvvmir.version.wgmma_lowering.rs: the F16 counted K-loop still fuses with a hint in its latch, and the fused template's loop header starts with.pragma "nounroll";only when the loop was hinted. Without the fusion change, the hinted loop failed with "invalid input program".wgmma.rsunit test: both counted templates put the pragma right after their loop label when asked, and omit it otherwise.collector.rsunit test: the marker check matches onlycuda_device's__unroll_configand__nounroll_config, not a function like__nounroll_config_helperor one with the same name in another crate.cuda-macrosunit tests: the marker goes first inwhile,forandloopbodies; arguments and#[unroll]+#[nounroll]are rejected.nounrollexample, withverify-code-shape.shfor the compile-only smoketest. It checks the exact count of marked latches per function in the pre-opt.ll, and at least that many pragmas in the PTX, with none in the control kernel. LLVM may clone a rolled loop, as in the [BUG]: #[unroll(N)] emits no loop metadata, so the NVVM backend unrolls the loops a second time #1375 case above.--device-debug. An sm_80 libNVVM cubin build (legacy LLVM 7 dialect) also compiles.taps_default(control)taps_rolledrolled_k_unrolled_inner(K loop rolled, inner#[unroll])rolled_inside_full_unrollrolled_inside_partial_unroll(libNVVM: 5 inner, plus the main and remainder loops)rolled_in_device_fn(loopin an#[inline(never)] #[device]fn)just checkandscripts/sync-example-locks.sh --checkon Linux with CUDA 13.1 and LLVM 21.cargo clippy --all-targets -D warningsandcargo fmt --checkfor the new example;scripts/smoketest.shGPU,--compile-onlyand--full-debuglanes fornounroll,unroll_smokeandpolicy_config, and the GPU and--compile-onlylanes forwgmma_mma_bf16;just book. (wgmma_mma_bf16fails its--full-debugbuild with "invalid input program" on unmodifiedmainata25a5857as well.)The example's annotated loops all load from memory. With an arithmetic-only body and a constant trip count, LLVM or ptxas computes the result outright and deletes the loop, which leaves nothing to keep rolled.
Limits
Like
#[unroll], the hint is mapped to the innermost loop around it. A loop whose body never repeats, such asloop { ...; break; }, produces no loop in MIR, so inside another loop the request applies to the enclosing loop. The book says so. Telling the two apart would need a second frontend marker that survives cloning, for both annotations.Not in this PR
#[unroll(N)]on a loop that contains a rangeforloop warns and does not unroll onmaintoday. The inner loop'sOptionmatch has an unreachable arm, which partial unrolling counts as a second exit. The example's partial-unroll kernel uses an innerwhileloop for that reason.