YangXu1990uiuc opened a new pull request, #20078:
URL: https://github.com/apache/tvm/pull/20078
# [Relax][cuDNN] Do not offload causal / non-fp16 attention, and fix the
default softmax scale
## Summary
The cuDNN BYOC backend offloads any `relax.nn.attention` that 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)` default
softmax 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 attention
op 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 by `if (value)`
and only ever *adds*
`set_diagonal_alignment(TOP_LEFT)` + `set_diagonal_band_right_bound(0)`
(graph_properties.h,
`set_causal_mask`). Passing `false` leaves the graph in its default state —
no diagonal band bound
at all, i.e. every query attends to every key.
Failure scenario: `R.nn.attention(q, k, v, causal_mask="TopLeft")` inside
the stacked-QKV pattern
is partitioned into `fused_..._cudnn`, and the compiled model returns
bidirectional attention
output. Decoder models produce wrong (future-leaking) results with no
diagnostic.
Fix (conservative option): reject the offload at partition time.
`_check_stacked_attention` now
returns `False` when the attention op carries `causal_mask` or
`window_size`. To make the op's
attributes visible to the check, the attention call is added to the pattern
annotations
(`annotations["attention"]`), the same idiom already used for `split` /
`q_transpose` and for
`root` in `make_conv2d_pattern`. A defensive `ICHECK` was also added in the
JSON runtime so that
any future pattern cannot re-introduce the silent-wrong-answer path.
I deliberately did **not** plumb the attribute through to `attention.cc`.
Doing it correctly means
mapping `"TopLeft"`/`"BottomRight"` onto
`set_diagonal_alignment(DiagonalAlignment_t::TOP_LEFT /
BOTTOM_RIGHT)` + `set_diagonal_band_right_bound(0)`, and — for `window_size`
— onto
`set_diagonal_band_left_bound()`; note `set_sliding_window_length()` in the
cuDNN frontend is a
pure alias for `set_diagonal_band_left_bound()` and sets only the *left*
bound, so a sliding-window
implementation 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::scale` is `Optional<FloatImm>`. When it is `None` the JSON
serializer writes an
empty **string** for the attribute
(src/relax/backend/contrib/codegen_json/codegen_json.h:188-191,
and the `Optional` overloads at :76-90). The runtime then does
(src/runtime/extra/contrib/cudnn/cudnn_json_runtime.cc:218-221):
```cpp
double scale = 1 / std::sqrt(head_size);
if (node.HasAttr("scale")) {
scale = node.GetAttr<double>("scale");
}
```
`HasAttr("scale")` is therefore *always* true — the `1/sqrt(head_size)`
default is dead code — and
`GetAttr<double>` (src/runtime/extra/contrib/json/json_node.h:254-258) casts
an `ffi::String` to
`double`, which fails. Either way the documented default scale never reaches
cuDNN.
Failure scenario: any `R.nn.attention(q, k, v)` written without an explicit
`scale` — the common
case, and two of the four parametrizations of the existing
`test_stacked_attention_split_offload`.
Fix: added `JSONGraphNode::GetAttrOpt<T>()`, which returns `std::nullopt`
for an absent attribute
*or* for one that does not hold the requested type, and used it for `scale`.
This keeps the
"`None` is an empty string" serialization convention that every other 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 the
runtime 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 `float16` on the stacked QKV input, following the
`_is_supported_dtype` idiom already
used 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 still
partitioned 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.py` sets a
module-level `pytestmark = [pytest.mark.gpu, skipif(not env.has_cudnn())]`,
so they are skipped in
environments without cuDNN, as is the existing
`test_cudnn_partition_conv2d_without_bias`.
`get_relax_stacked_attention_module` gained an optional `causal_mask`
argument to build the
negative 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.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]