[Relax][cuDNN] Do not offload causal / non-fp16 attention, and fix the default softmax scale - #20078
Open
YangXu1990uiuc wants to merge 1 commit into
Open
[Relax][cuDNN] Do not offload causal / non-fp16 attention, and fix the default softmax scale#20078YangXu1990uiuc wants to merge 1 commit into
YangXu1990uiuc wants to merge 1 commit into
Conversation
The cuDNN BYOC backend matched relax.nn.attention on the op node alone, so attention carrying a causal mask was offloaded to a cuDNN SDPA graph built with .set_causal_mask(false) and silently computed bidirectionally. - python/tvm/relax/backend/patterns.py:339: annotate the attention call so the partition check can read the op attributes. - python/tvm/relax/backend/cuda/cudnn.py:89-97: do not offload when causal_mask or window_size is set, or when the input is not float16 (the runtime only builds a half-precision graph, attention.cc:42). - src/runtime/extra/contrib/json/json_node.h:261-274: add HasAttrValue, since a None attribute is serialized as an empty string and HasAttr cannot tell. It is paired with GetAttr<T>, which throws on a type mismatch, so an attribute we cannot read is loud rather than silently replaced by the default. - src/runtime/extra/contrib/cudnn/cudnn_json_runtime.cc:218-231: use it so the 1/sqrt(head_size) default scale is actually applied, and reject masked attention that reaches the runtime. - python/tvm/relax/testing/attention.py:79: optional causal_mask argument. - tests/python/relax/test_codegen_cudnn.py:302-329: partition tests for the causal and fp32 cases, plus a positive control.
Contributor
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
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.
[Relax][cuDNN] Do not offload causal / non-fp16 attention, and fix the default softmax scale
Summary
The cuDNN BYOC backend offloads any
relax.nn.attentionthat matches the stacked-QKV pattern,without ever looking at the op's attributes. A model that asks for causal attention is therefore
compiled into a cuDNN SDPA graph that computes full bidirectional attention — silently, with
no error and no warning. The same blind spot lets fp32/bf16 attention be partitioned to a runtime
that only builds a half-precision graph, and makes the documented
1/sqrt(head_dim)defaultsoftmax scale unreachable. This PR rejects the workloads cuDNN is not actually being asked to
compute, fixes the default-scale path, and adds partition-level regression tests.
1. Causal attention is offloaded and computed non-causally
make_stacked_attention_pattern(python/tvm/relax/backend/patterns.py:338) matches the attentionop node only, with no constraint on its attributes, and
_check_stacked_attention(python/tvm/relax/backend/cuda/cudnn.py:69-89) validates only ndim and the split axis. The runtime
then builds the SDPA graph with
.set_causal_mask(false)(src/runtime/extra/contrib/cudnn/cudnn_frontend/attention.cc:88-93).
On the cuDNN side that call is a no-op: in the cuDNN frontend,
SDPA_attributes::set_causal_mask(bool value)is guarded byif (value)and only ever addsset_diagonal_alignment(TOP_LEFT)+set_diagonal_band_right_bound(0)(graph_properties.h,set_causal_mask). Passingfalseleaves the graph in its default state — no diagonal band boundat all, i.e. every query attends to every key.
Failure scenario:
R.nn.attention(q, k, v, causal_mask="TopLeft")inside the stacked-QKV patternis partitioned into
fused_..._cudnn, and the compiled model returns bidirectional attentionoutput. Decoder models produce wrong (future-leaking) results with no diagnostic.
Fix (conservative option): reject the offload at partition time.
_check_stacked_attentionnowreturns
Falsewhen the attention op carriescausal_maskorwindow_size. To make the op'sattributes visible to the check, the attention call is added to the pattern annotations
(
annotations["attention"]), the same idiom already used forsplit/q_transposeand forrootinmake_conv2d_pattern. A defensiveICHECKwas also added in the JSON runtime so thatany future pattern cannot re-introduce the silent-wrong-answer path.
I deliberately did not plumb the attribute through to
attention.cc. Doing it correctly meansmapping
"TopLeft"/"BottomRight"ontoset_diagonal_alignment(DiagonalAlignment_t::TOP_LEFT / BOTTOM_RIGHT)+set_diagonal_band_right_bound(0), and — forwindow_size— ontoset_diagonal_band_left_bound(); noteset_sliding_window_length()in the cuDNN frontend is apure alias for
set_diagonal_band_left_bound()and sets only the left bound, so a sliding-windowimplementation must set both bounds explicitly. That is a feature addition that needs numerical
validation on a GPU, which this audit could not run. Rejecting the pattern falls back to the
non-offloaded path, which is correct.
2. The default softmax scale never reaches the runtime
AttentionAttrs::scaleisOptional<FloatImm>. When it isNonethe JSON serializer writes anempty string for the attribute (src/relax/backend/contrib/codegen_json/codegen_json.h:188-191,
and the
Optionaloverloads at :76-90). The runtime then does(src/runtime/extra/contrib/cudnn/cudnn_json_runtime.cc:218-221):
HasAttr("scale")is therefore always true — the1/sqrt(head_size)default is dead code — andGetAttr<double>(src/runtime/extra/contrib/json/json_node.h:254-258) casts anffi::Stringtodouble, which fails. Either way the documented default scale never reaches cuDNN.Failure scenario: any
R.nn.attention(q, k, v)written without an explicitscale— the commoncase, and two of the four parametrizations of the existing
test_stacked_attention_split_offload.Fix: added
JSONGraphNode::HasAttrValue(), which reports whether an attribute is present and isnot the empty-string
Nonesentinel, and paired it with the existingGetAttr<T>()forscale.GetAttr<T>throws on a type mismatch, so an attribute we cannot read is loud rather thansilently replaced by the default -- which would be the same class of silent wrong answer this PR
exists to fix. This keeps the "
Noneis an empty string" serialization convention that everyother BYOC runtime already relies on, instead of changing the shared serializer.
3. No dtype guard at partition time
_check_stacked_attention(python/tvm/relax/backend/cuda/cudnn.py:69) has no dtype check, but theruntime only ever builds a half-precision graph and hard-fails on
TVM_FFI_ICHECK(data_type.code == kDLFloat && data_type.bits == 16) << "Only float16 is supported"(src/runtime/extra/contrib/cudnn/cudnn_frontend/attention.cc:42-43).
Failure scenario: an fp32 (or bf16) attention module is happily partitioned to cuDNN and then
aborts at module init with "Only float16 is supported", instead of falling back to a working
non-offloaded implementation.
Fix: require
float16on the stacked QKV input, following the_is_supported_dtypeidiom alreadyused by
_check_conv2d.Evidence
Confirmed by reading the full chain (pattern -> offload check -> runtime). No runtime repro was
performed (TVM was not built), so no measured numbers are claimed for this repo.
Testing
Added to
tests/python/relax/test_codegen_cudnn.py:test_stacked_attention_partition— positive control: fp16 unmasked attention is stillpartitioned to cuDNN (guards against over-rejection).
test_stacked_attention_causal_not_partitioned[TopLeft|BottomRight]— fails before this change(the causal graph is offloaded), passes after.
test_stacked_attention_fp32_not_partitioned— fails before this change, passes after.These are partition-only tests and need no GPU, but note that
test_codegen_cudnn.pysets amodule-level
pytestmark = [pytest.mark.gpu, skipif(not env.has_cudnn())], so they are skipped inenvironments without cuDNN, as is the existing
test_cudnn_partition_conv2d_without_bias.get_relax_stacked_attention_modulegained an optionalcausal_maskargument to build thenegative cases.
None of this was run locally: TVM is not built in the audit environment, so the new tests are
unverified. The changed Python files were byte-compiled and checked with the repo's formatting
settings (100-column), and the changed C++ files are clean under the repo
.clang-format.Also worth flagging for maintainers: the only end-to-end cuDNN SDPA test,
test_stacked_attention_split_offload, is unconditionally skipped(
@pytest.mark.skip(reason="require cudnn frontend"), tests/python/relax/test_codegen_cudnn.py:302).Nothing in CI exercises this runtime, which is how findings 1 and 2 survived.
Found by an integration audit of cuDNN SDPA consumers by the NVIDIA cuDNN team.