[CuTeDSL] Add llc{...} compile-option token and NvvmOptions to pass LLVM codegen flags to the NVVM backend - #3498
[CuTeDSL] Add llc{...} compile-option token and NvvmOptions to pass LLVM codegen flags to the NVVM backend#3498zkyue wants to merge 2 commits into
llc{...} compile-option token and NvvmOptions to pass LLVM codegen flags to the NVVM backend#3498Conversation
…LVM codegen flags
There is no supported way to hand an LLVM codegen (llc) flag to the
DSL's JIT pipeline; doing so today requires monkeypatching
CompileOptions.to_str. The pipeline plumbing already exists: the
cute-to-nvvm pass accepts an nvvm-options option, in which -Xllc <flag>
pairs are forwarded to LLVM codegen.
Add the missing user-facing vocabulary:
* NvvmOptions, a registered StringCompileOption serializing as
nvvm-options='...' into the cute-to-nvvm{...} pipeline brace, the
exact analog of PtxasOptions (ptx-options). Because get_module_hash
hashes CompileOptions.to_str(), the option is automatically part of
the compile-cache key. Exported next to PtxasOptions.
* llc{<flag>[,<flag>...]}, a compact token following the existing
brace-token pattern (debug{...}, warnings{...}): each item is
shape-validated ([A-Za-z0-9][\w.-]*(?:=[\w.-]+)?, spelled without the
leading '-') and appended to NvvmOptions as an -Xllc -<flag> pair.
Accepted everywhere compact tokens are: CUTE_DSL_COMPILER_OPT,
cute.compile(..., options=...), pure-compact and mixed legacy paths.
Both entry points validate against pipeline-string injection: the token
grammar admits no quotes/spaces/braces, and NvvmOptions rejects any
value whose quote-stripped core could escape the quoted slot.
NvvmOptions sets _suppress_when_absent so the mixed argparse path does
not clobber token-set values, and --nvvm-options joins --ptxas-options
in the hyphen-value argparse workaround.
Opt-in only: with the option unset, serialization is byte-identical to
before. Malformed syntax fails at parse time with a targeted
ValueError; flags libNVVM rejects surface as CompilerDiagnosticError;
flags it does not recognize are silently ignored (backend behavior,
documented as such).
Motivation: on a large production sparse-attention backward kernel on
B200 (sm_100a, CUDA 13), -Xllc -aggressive-machine-cse=1 removes 3.9%
(stock) / 6.5% (tuned) of static SASS instructions (IMAD/MOV
address-math dedup) and 1.5-2% of pure-kernel runtime, reproduced on
cutlass-dsl 4.6.1 and 4.7.0, with the deterministic output
bitwise-identical flag-on vs flag-off.
Tests: test/python/CuTeDSL/test_compile_options_llc.py covers the
grammar (valid and malformed), accumulation, string-API and
programmatic paths, injection rejection, and end-to-end compilation
including backend rejection surfacing (26 tests, verified on B200
against the 4.7.0 wheel; the token is rejected by stock as expected).
Signed-off-by: zky <kaiyue.zhou@z.ai>
-Xllc flags forwarded via nvvm-options are parsed into LLVM's
process-global cl::opt registry by the in-process backend, and the
registry is only re-parsed by compiles that themselves pass an
nvvm-options token. A compile using llc{...} therefore leaves the flag
in effect for every subsequent compile in the same process that does
not pass its own nvvm-options; the flag does not expire with the
compile that set it. Found while integrating the token into a
production stack: a kernel compiled with no options after a flagged
compile of a different kernel came out byte-identical (PTX, cubin and
SASS) to an explicitly flagged build of itself, and differed from a
never-flagged process.
Two follow-on hazards fall out of this and are documented too:
* Compile caches (in-process jit cache and the persistent file cache)
key on the requested option string via get_module_hash, not on
inherited registry state -- an unflagged cached JIT compilation in a
flagged process can store flag-affected code under the clean cache
key and serve it to later processes. Explicit cute.compile always
recompiles (no_cache) and is unaffected.
* A paired "reset" compile that passes the flag's default value
explicitly restores the flag, but only best-effort: it needs a
default expressible as a value, must actually reach the backend (a
cache-served compile does not re-parse), and cannot undo a flag from
CUTE_DSL_COMPILER_OPT, which is re-applied every compile. Full
isolation is a fresh process.
This is a limitation of the current in-process backend and cannot be
scoped from Python. Automatic reset semantics were considered and
rejected as unsound: the DSL cannot know an arbitrary flag's default
value, and libNVVM exposes no registry-reset entry point. So: disclose
and pin.
* docs: a "Process-global flag state" section in the compilation
options .rst covering the semantics, the cache-keying consequence,
and the best-effort reset recipe; a short form of the warning in the
NvvmOptions docstring.
* test: TestLlcFlagStateIsProcessGlobal compiles a victim kernel with
no options after a flagged compile of a different kernel, one fresh
interpreter per compile ordering (the state under test is process
state), the victim always the process's second compile so backend
warm-state is identical across arms, and asserts the victim's PTX is
byte-identical to an explicitly flagged build and different from a
clean one. The witness flag is -nvptx-sched4reg, which changes
emitted PTX even on trivial kernels (aggressive-machine-cse is a
no-op on small code); the test skips itself if a future libNVVM
stops recognizing the witness, and fails with a targeted message if
the backend ever gains per-compile scoping.
* test hardening: the pre-existing end-to-end compile tests (accepted
flag, programmatic NvvmOptions, backend rejection) now also run in
disposable subprocesses so their accepted flags cannot poison later
compiles in the test process; the child environment drops
CUTE_DSL_COMPILER_OPT so an ambient llc{...} cannot contaminate the
arms, and PYTHONHASHSEED is pinned.
No functional code change; with the option unset, serialization stays
byte-identical to stock.
Signed-off-by: zky <kaiyue.zhou@z.ai>
|
While integrating this into our production stack we found a behavior of the underlying
Two follow-on consequences worth knowing:
I considered making the DSL auto-reset after each flagged compile and rejected it as
No functional code change; the feature stays opt-in and its serialization with the (edited: removed an internal build-log section that was accidentally included below the technical content — no changes above this line) |
TL;DR
There is currently no supported way to hand an LLVM codegen (llc) flag to the DSL's JIT pipeline. This PR adds one, strictly opt-in:
CUTE_DSL_COMPILER_OPT="llc{aggressive-machine-cse=1}"Each brace item is forwarded to LLVM codegen as an
-Xllc -<flag>pair through thenvvm-optionsoption of thecute-to-nvvmpipeline. Nothing is forwarded unless the user asks for it: with nollc{...}token the new option contributes no text to the pipeline (see Limitations for the one-space registry side effect every added option has).Motivation
On a large production sparse-attention backward kernel on B200 (sm_100a, CUDA 13), a single llc flag,
-aggressive-machine-cse=1, gives a measurable, numerics-preserving win:That flag is emphatically not a universal win, which is exactly why this has to be an opt-in knob rather than a pipeline default. A follow-up sweep of the same flag over five other production CuTe DSL kernels (B200, one toolchain pin, nsys pure-GPU medians, n=100/arm, outputs bitwise or within the fp32-atomic run-to-run envelope in all five) found effects spanning −15.2% to +0.33%, with sign flips:
--opt-level 2and the flag still helps on topSo the flag is safe with respect to numerics but must be chosen per kernel with a perf gate behind it — a scoped, per-compile passthrough (what this PR adds), never a default change.
Today the only way to do this is to monkeypatch
CompileOptions.to_strand splicenvvm-options='-Xllc ...'into its return value — fragile, version-dependent, and unsupported. The pipeline plumbing for it already exists (thecute-to-nvvmpass acceptsnvvm-options, verified on the 4.6.1 and 4.7.0 wheels); what is missing is purely the user-facing vocabulary on the Python side.What this adds
NvvmOptions— a registeredStringCompileOptionwith_option_name = "nvvm-options", the exact analog of the existingPtxasOptions(ptx-options). It serializes asnvvm-options='<value>'into thecute-to-nvvm{...}brace built by_get_pipelineand, becauseget_module_hashhashesCompileOptions.to_str(), is automatically part of the compile-cache key. Exported next toPtxasOptions(cute.NvvmOptions).llc{<flag>[,<flag>...]}— a compact token inCompileOptions._apply_opt_string, following the existing brace-token pattern (debug{...},warnings{...},remarks{...}). Each item is validated for shape ([A-Za-z0-9][\w.-]*(?:=[\w.-]+)?, spelled without the leading-) and appended toNvvmOptionsas an-Xllc -<flag>pair. Multiple items, and repeatedllc{...}tokens, accumulate. The token is accepted everywhere compact tokens already are:CUTE_DSL_COMPILER_OPT,cute.compile(..., options=...)(pure-compact and mixed compact+legacy paths), and_apply_opt_string.Small enabling details:
NvvmOptionssets_suppress_when_absent, so an absent--nvvm-optionsargparse flag on the mixed legacy path does not clobber a value set by thellc{...}token (this is exactly what_suppress_when_absentexists for).--nvvm-optionsjoins--ptxas-optionsin the hyphen-value argparse workaround in_parse_compile_options_from_str.CUTE_DSL_COMPILER_OPTis re-applied to the (reused)CompileOptionson every compile, so a non-idempotent append would grow the value — and the compile-cache key — on each compile..rst, and a test: compact tokens are applied before the legacy string API, so an explicit--nvvm-options <value>replaces flags contributed byllc{...}in the same options string, regardless of order.Docs:
media/docs/pythonDSL/cute_dsl_general/dsl_jit_compilation_options.rstgains annvvm-optionstable row, both string-API and Python-type examples next to theptxas-optionsones, and a short section on forwarding llc flags (including the env-var form).Injection safety
A user-supplied string ends up inside a quoted slot of the pass-pipeline spec, so both entry points validate:
llc{...}token accepts only[A-Za-z0-9][\w.-]*(?:=[\w.-]+)?per comma-separated item — no quotes, spaces, braces, or backslashes can get through, and the flag is composed as-Xllc -<flag>by the parser itself.NvvmOptions(constructor and.valuesetter) rejects any value whose quote-stripped core contains',",{,},\, or control whitespace — i.e. anything that could terminate thenvvm-options='...'slot or the pipeline nesting. Surrounding quotes are tolerated becauseserialize()strips one layer before re-quoting (that is how the legacy string path delivers values).Error behavior (documented, matches what libNVVM actually does)
llc,llc=1,llc{},llc{-foo},llc{a b}, unclosed brace, embedded quote) fails at parse time with aValueErrorcarrying a targeted message.CompilerDiagnosticError, "NVVM backend compilation failed") — verified with-time-passes.Tests
test/python/CuTeDSL/test_compile_options_llc.py(unittest, follows sibling files):--nvvm-optionsprecedence; pure-compact and mixed compact+legacy string API; coexistence withwarnings{...}.ValueErrorwith the expected message.NvvmOptionsdirect use, export identity, quote/brace rejection on constructor and assignment, surrounding-quote tolerance.options="llc{aggressive-machine-cse=1}"compiles;cute.compile[NvvmOptions(...)]compiles; a backend-rejected flag surfaces asCompilerDiagnosticErrormentioning NVVM (skipped, not failed, if a future libNVVM accepts that flag).Verified on B200 (sm_100a): 29/29 pass with the patch (backend-touching tests run in
disposable subprocesses because accepted
-Xllcflags are process-global; the newprocess-global-state test skips itself if its witness flag is invisible on a future
libNVVM); on stock,
llc{...}is rejected withoption 'llc' does not take {...} sub-optionsandNvvmOptionsdoes not exist. The env-var route was additionally verified end-to-end in a fresh process, including that three successive compiles produce a stable option string. The three pre-existing test files intest/python/CuTeDSL/behave identically with and without the patch.Limitations
Process-global flag state (measured; documented and pinned by a test).
-Xllcflags are parsed into LLVM's process-global option registry (cl::opt) bythe in-process backend, and the registry is only re-parsed by compiles that
themselves pass an
nvvm-optionstoken. A compile usingllc{...}thereforeleaves the flag in effect for every subsequent
cute.compilein the sameprocess that does not pass its own
nvvm-options— the flag does not expire withthe compile that set it. Verified on B200: a kernel compiled with no options after
a flagged compile of a different kernel is byte-identical (PTX, cubin and SASS)
to an explicitly flagged build of itself, and differs from a never-flagged process
(witness flag
-nvptx-sched4reg, which is PTX-visible even on trivial kernels).Two follow-on hazards are documented with it: (1) compile caches key on the
requested option string (
get_module_hash), not on inherited registry state, soan unflagged cached JIT compilation in a flagged process can store flag-affected
code under the clean key — including in the persistent file cache, from which it
can be served to later processes (explicit
cute.compilealways recompiles and isunaffected); (2) a paired reset compile that passes the flag's default value
explicitly is only best-effort — it needs a value-expressible default, must
actually reach the backend (cache-served compiles do not re-parse), and cannot undo
CUTE_DSL_COMPILER_OPT, which is re-applied every compile; full isolation is afresh process. This is a limitation of the current in-process backend and cannot be
scoped from Python; automatic reset semantics are unimplementable in general (the
DSL cannot know an arbitrary flag's default, and libNVVM exposes no registry-reset
entry point). The
.rstgained a "Process-global flag state" section;TestLlcFlagStateIsProcessGlobalpins the behavior (one fresh interpreter percompile ordering, victim always the second compile so warm-state matches) and the
pre-existing end-to-end compile tests now also run in disposable subprocesses so
accepted flags cannot poison later compiles in a test worker.
llc flags are inherently tied to the LLVM version inside libNVVM; this is a power-user escape hatch (same contract as
ptxas-options/ nvcc-Xptxas), not a stable API. Opt-in only: with nollc{...}token,NvvmOptionsserializes to nothing.Registering a new option adds one separator space to the default serialized option string (
to_strappends a space per option, including empty ones), so default compile-cache keys change once, as they do whenever an option is added — the DSL's own default string went from 88 to 106 bytes between 4.6.1 and 4.7.0. The extra whitespace is inert in the pass-pipeline string.The backend silently ignores unrecognized flags (libNVVM behavior, not controllable from Python), so a typo in a flag name does not error; flags the backend rejects do error.
No SASS-diff regression test: on toy kernels
-aggressive-machine-cse=1is a no-op (small kernels get re-CSEd downstream regardless), so an instruction-count assertion would be fragile. The compile-success and backend-rejection tests pin the passthrough contract instead.