Repository navigation
feat(cuda-oxide): carry inline intent through the device pipeline - #1511
Conversation
…uded Device code only reached LLVM with `alwaysinline`: `#[inline]` hints were dropped and `#[inline(never)]` became an ordinary function that `opt` was free to inline. Code that relies on a call boundary (to keep a helper's floating-point contraction identical across call sites, or to bound code size in a hot loop) could not express it. `InlineIntent` (`MirFuncOp::inline_intent`) records the Rust attribute on the imported function and the lowering maps it to LLVM function attributes: `#[inline]` -> `inlinehint`, `#[inline(always)]` -> `alwaysinline`, `#[inline(never)]` -> `noinline`, rendered through `llvm_func_attrs` like any other function attribute. The MirFuncOp verifier rejects an unknown keyword and any intent on a kernel entry (a `.entry` is never called, so an inline keyword on it has no meaning); a new test covers both rules. No inliner pass is added: `opt` honours the keywords. One `#[inline(never)]` is not carried: the one `#[device]` puts on the prefixed function of a device function with its own type or const parameters, so each monomorphization stays a separate item for the collector. It is a host-side boundary; as `noinline` it would keep every call and stop const-generic arguments from folding into their callers (the `const_generic` example checks that they fold). The macro's attribute comes first, so rustc keeps it and reports an inline attribute the user adds there as unused; on such functions a user's own inline attribute did not reach device code before this change and does not now. The match uses `opt_item_name`, so closures, which may carry `#[inline(never)]`, are handled like any other function. Signed-off-by: Alex Khal <joss13@gmail.com> Signed-off-by: ssoj13 <96214515+ssoj13@users.noreply.github.com>
The `#[inline]` request a source function makes is now a `mir.inline_intent` attribute on `mir.func` (`InlineIntentAttr`: Hint, Always, Never) instead of a string holding an LLVM keyword. The accessors are infallible, the verifier keeps the kernel-entry rule and rejects a value of another attribute type under the key, and the LLVM keyword is chosen only in mir-lower. The enum and its key leave reserved-oxide-symbols, so dialect-mir no longer depends on it; both lockfiles record that. The verifier comment no longer claims LLVM rejects the keywords on a kernel. The collector-boundary doc drops the closure case, which stable syntax cannot reach, and says what a later change to the macro's attribute order must do. Signed-off-by: nihalpasham <nihalp@nvidia.com>
…the libNVVM route A non-kernel function that returns a bare pointer in a non-generic address space now returns a generic pointer on both NVVM dialects. The callee casts to generic right before each `ret`, and every direct call site casts the result back to the original address space, so uses keep their types. libNVVM declares the 32-bit shared-pointer return of such a function as a 64-bit `.param .b64 func_retval0` while storing 32 bits into it, and ptxas rejects every call. The route never met this while such helpers were inlined before reaching libNVVM; a `noinline` helper returning `*mut SharedArray` is a real function. Declarations, kernels, parameters and aggregates are left alone, and a function whose address is taken is reported instead of rewritten. Unit tests cover both dialects, the untouched cases and the rejection; `addressof_sharedarray --emit-nvvm-ir` passes again. Signed-off-by: nihalpasham <nihalp@nvidia.com>
…Zinline-mir=no `#[inline(never)]` now reaches LLVM as `noinline`, so the two helpers it marked became real PTX functions and the volatile loads left the entry the shape check scans. The example tests facts reaching loads inside helpers that LLVM inlines, so MIR inlining is switched off for this one package through profile-rustflags, as math_no_mir_inline does, and the helpers drop the attribute. The shape check passes again unchanged. Signed-off-by: nihalpasham <nihalp@nvidia.com>
One kernel reaches a helper of each inline kind: `#[inline(never)]` on a plain function and on a non-generic `#[device]` function, `#[inline]`, `#[inline(always)]` and a const-generic `#[device]` helper. The shape check asserts the attribute on each definition in the pre-opt LLVM IR, no keyword on the generic `#[device]` instance, `call.uni` to both `noinline` helpers in the entry, no function left for the `alwaysinline` helper and the const-generic factor folded to an immediate. MIR inlining is off for this package so every helper reaches LLVM as its own definition. Signed-off-by: nihalpasham <nihalp@nvidia.com>
The supported-features row lists the three keywords and the generic `#[device]` exception. The cluster shape script explains why the call still vanishes with `noinline` (opt forwards the `returned` argument), and the smoketest rationale for the unoptimized rebuild no longer says the markers survive only unoptimized. Signed-off-by: nihalpasham <nihalp@nvidia.com>
|
Thanks for this, the fix is right and it is going in. One snag: your fork is a fork of ansidium/cuda-rust-windows, not of NVIDIA/cuda-rust, so "allow edits from maintainers" gives push rights to ansidium, not to us, and I could not push my follow-ups onto your branch. I reviewed and validated this on the 5090 and added the follow-ups on top of your commit, on branch |
|
Thanks @nihalpasham! Since GitHub only permits one fork per repository network ( |
…tent (pliron 0.20 Location::clone)
|
/ok to test d0bcc80 |
|
Thanks, this is the right direction and it matches what rustc's own backend does:
At |
A one-page figure and a short subsection on the device-functions page for the inline intent mapping that NVIDIA#1511 adds: #[inline] as inlinehint, #[inline(always)] as alwaysinline, #[inline(never)] as noinline, and the collector-boundary exception for generic #[device] helpers. Every quoted line comes from the inline_intent example built on an RTX 5090. Signed-off-by: nihalpasham <nihalp@nvidia.com>
…ples The lock committed with NVIDIA#1511 predates the NVIDIA#1545 re-alignment, so the first build of the example added itoa, oxide-tile-export, serde_json and zmij and left the tree dirty. A resolve with the existing lock adds the four entries at the versions the other 239 locks carry. Signed-off-by: nihalpasham <nihalp@nvidia.com>
A one-page figure and an "Inlining helpers" subsection on the device-functions page for the inline intent mapping that #1511 landed: #[inline] as inlinehint, #[inline(always)] as alwaysinline, #[inline(never)] as noinline, and the collector-boundary exception for generic #[device] helpers. Every quoted line comes from the inline_intent example built on an RTX 5090. The inline_intent lockfile committed with #1511 predates the #1545 re-alignment, so the first build of the example added four entries and left the tree dirty; a resolve against the existing lock adds them at the versions the other 239 locks carry. Signed-off-by: nihalpasham <nihalp@nvidia.com>
What this adds
Rust lets a function say how it wants to be inlined:
#[inline]as a hint,#[inline(always)]as a demand,#[inline(never)]as a refusal. The device pipeline passed only the demand through to LLVM. The hint was dropped, and the refusal stopped Rust's own inliner but not LLVM's, so a helper that wanted to stay a separate GPU function never did. The three requests now reach LLVM as the matching function attributes, the mapping rustc's own backend uses, and#[inline(never)]means one real PTX function.Contract
mir.funcattribute#[inline]inline_intent = hintinlinehint#[inline(always)]inline_intent = alwaysalwaysinline#[inline(never)]inline_intent = nevernoinline.func, reached withcall.uniunless opt can forward areturnedargument and drop the call, as theclusterexample documents#[inline(never)]that#[device]adds to a helper with its own type or const parameters is not carried. It keeps each monomorphization visible to the host-side collector and nothing else, so const-generic arguments keep folding into their callers. A user's own inline attribute on such a helper still does not reach device code; that is [BUG]:#[device]does not obey#[inline]on a generic function #1471 and stays open.What we fixed
#[inline]and#[inline(never)]were dropped on the way to LLVM; only#[inline(always)]survived, through a string attribute onmir.functhat the exporter matched by name.InlineIntentAttronmir.func, set by the backend fromcodegen_fn_attrs, lowered to the LLVM function attribute in one place in mir-lower, and verified: a request on a kernel entry or a wrongly typed value under the key is an error, not a silent "none".noinlinehelper that returns a shared-memory pointer is now a real function, and libNVVM declares that 32-bit return slot as.param .b64while the body stores 32 bits and every call site reads a 32-bitretval0, so ptxas rejectedaddressof_sharedarrayon the--emit-nvvm-irroute. The legalizer now gives every non-kernel definition that returns a bare non-generic pointer, inline attribute or not, a generic return and restores the address space with anaddrspacecaston both sides of each call. Parameters, aggregates, kernels and declarations are untouched; a function whose address is taken is reported. The libNVVM side is tracked internally (NVBug 6946922); the pass goes once it ships.drop_glueexample (fix(mir-importer): preserve non-terminating drops #1361, landed after this branch was cut) relied on#[inline(never)]being dropped for its store-then-self-loop PTX check; test(drop_glue): keep destructor MIR with -Zinline-mir=no, not #[inline(never)] #1549 moved it to package-scoped-Zinline-mir=noahead of this PR, so the full smoketest is green with every example.kernel_reference_validitykept its helper route inlined with#[inline(never)]and a comment saying LLVM would inline it anyway; it now says what it means with-Zinline-mir=noon its own package, and its two volatile loads stay in the entry.Implementation
rustc-codegen-cuda/src/device_codegen.rs: mapsrustc_attr_ir::InlineAttr(Hint,Always/Force,Never) toInlineIntentAttrbefore entering the stable_mir context;is_generic_device_collector_boundaryrecognizes the macro's own#[inline(never)]on a prefixed device symbol with its own generics and drops it. The test keys on the device symbol name plus own generics, which holds while the macro writes its attribute first; once fix(cuda-macros): obey #[inline] on generic device functions #1472 lets a user attribute replace the macro's, it must key on the attribute's span instead.dialect-mir:InlineIntentAttr(mir.inline_intent, apliron_attrenum) andMirFuncOp::{set_inline_intent, inline_intent}under theinline_intentkey; the verifier rejects the attribute on a kernel and a foreign type under the key.mir-importer:FunctionInput.inlinereplacesis_inline_always;translator/body.rssets the attribute on the newmir.func.mir-lower/src/lowering.rs:lower_inline_intent_attris the only place that namesinlinehint,alwaysinlineandnoinline; it writes them intollvm_func_attrs, which llvm-export already renders. The old by-namealwaysinlinespecial case in the exporter is gone.nvvm-transforms/src/legalize.rs:legalize_pointer_returnsruns for both NVVM dialects before the legacy pass;retype_pointer_returncasts to generic before eachret,cast_call_result_backrebuilds each direct call and casts the result back.reserved-oxide-symbolsgains the device-symbol test the backend uses.inline_intent(new; one helper of each kind, a generic#[device]helper, host and device compute the same values;verify-code-shape.shchecks the keyword on each pre-optdefine, twocall.uniin the entry, noalways_subfunction, and the folded immediate; MIR inlining is off for its package so every helper reaches LLVM),kernel_reference_validity(-Zinline-mir=no),cluster(shape script comment: opt still removes the call because it forwards thereturnedargument).scripts/smoketest.shlistsinline_intentamong the examples rebuilt unoptimized for their shape check.#[device]helper row in the supported-features appendix states the mapping and the exception.Verification
Checked at
d0bcc80f8merged with main5b606fddd, #1509 and #1510 (nightly-2026-10-11, two RTX 5090, CUDA 13.4):cargo test --workspace --all-targetsin cuda-oxide: 5596 passed, 0 failed; the backend crate's own 33 tests pass. Clippy with-D warningson both workspaces, rustdoc,cargo oxide fmt --check, the lock check, the 18 guards and cargo deny pass. All 55 CI checks passed.cargo oxide run inline_intent:SUCCESS: all 256 elements match the host reference, andverify-code-shape.shpasses:noinline,inlinehintandalwaysinlineon the right pre-opt defines, no keyword on the const-generic#[device]helper, twocall.uniin the entry, noalways_subfunction left,scale_by::<4>folded to0f40800000.kernel_reference_validitypasses with its shape check (two volatile loads stay in the entry under-Zinline-mir=no);const_genericandclusterpass on sm_120.cargo oxide run addressof_sharedarray --emit-nvvm-ir --arch sm_120passes with the legalized generic return; main's backend inlines the helper and passes too. memcheck on the libNVVM-route and direct-route binaries: 0 errors.gemm_sol,tcgen05_matmul,wgmma,mma_mxf8f6f4andvecaddbuilt with main's backend and with this tree's are identical after hash normalization.drop_glue's shape check from fix(mir-importer): preserve non-terminating drops #1361, which assumed#[inline(never)]is ignored; with test(drop_glue): keep destructor MIR with -Zinline-mir=no, not #[inline(never)] #1549 applied the lane and its neighbors (cluster,inline_intent,kernel_reference_validity) pass. The ten LTOIR-lane examples ran through libNVVM with--emit-nvvm-irin that smoketest.Caveats
#[inline(never)]on a closure is unreachable on stable Rust, so the closure case named in the original description is not covered.#[device]exception means a user's inline attribute on a helper with its own generics still does not reach device code ([BUG]:#[device]does not obey#[inline]on a generic function #1471).#[inline]family. rustc's own backend also emitsnoinlinefor#[optimize(none)]andinlinehintfor shims and drop glue that require inlining; neither is touched here.#[inline(never)]helpers (cluster_sync, the__cluster_configmarker) now reach the pre-opt IR asnoinlinedefinitions; opt removes them as unreferenced, and the compared examples emit identical PTX on the direct route.inline_intent/Cargo.lockpredates Tile libraries: call Rust device functions from Tile kernels #1545's lock re-alignment, so the first build adds four entries (itoa,oxide-tile-export,serde_json,zmij); the follow-up that adds the book figure re-aligns it.Related: #188, #189, #1471. Port of ansidium#11.