[QNN EP] Add support for Attention Op - #651
Conversation
|
@qti-mbadnara, can we map onnx Attention onto QNN_OP_GROUP_QUERY_ATTENTION for the GPU backend?
HTP may not support this, but GPU can. And perf on GPU using the QNN GQA op will be better than the decomposed sequence. |
|
I agree that the onnx Attention need to map to QNN_OP_GROUP_QUERY_ATTENTION in the normal case. It can resort to this splitting only for the special HTP case temporarily because it would be beneficial for HTP also to implement GQA eventually. I guess the onnx document specifies splitting Attention op to Scale + MatMul + Softmax + MatMul only as a temporary measure until implementations can catch up with the full Attention op kernels. Afaik the GQA is as generic as the new ONNX Attention op. Still we can also add the new ONNX Attention op to QNN if desired. Please let us know your thoughts. |
qti-mattsinc
left a comment
There was a problem hiding this comment.
Please fix decomposition ordering with softcap
Co-authored-by: qti-mattsinc <mattsinc@qti.qualcomm.com> Signed-off-by: qti-mbadnara <mbadnara@qti.qualcomm.com>
qti-yuduo
left a comment
There was a problem hiding this comment.
Reviewed against the ONNX Attention spec and the QNN SDK headers. Architecture and the two-path split look right, and the SDK 2.12 guard is load-bearing (local QnnOpDef.h is 2.4.0 with no GROUP_QUERY symbols).
The softcap-before-mask order is correct — worth noting the vendored cmake/external/onnx is pinned at v1.20.1, which is pre-1.23-errata and has the opposite order. I initially flagged this as a bug against the submodule before checking the published spec. See the comment on L1282.
Two blocking issues (L1336 dtype buffer, L1370 bool mask). Everything else is minor.
Also: IsOpSupported re-runs the decomposition with do_op_validation=true, so most shape mismatches become a clean CPU fallback rather than a bad compile. The bool-mask issue is the exception — it builds a valid graph that computes the wrong thing.
|
Hi @qti-yuduo thanks for the detailed review! Fixed the blockers. |
Description
Adds
AttentionOpBuilderto the QNN EP, enabling the standard ONNXai.onnx::Attentionoperator (opset 23 and 24) to run on the Hexagon HTP and Adreno GPU backends. The builder decomposes the fused Attention node into elementary QNN ops for HTP, and for the GPU backend routes to the nativeQNN_OP_GROUP_QUERY_ATTENTIONkernel when possible.Motivation & Context
The QNN EP had no registration for
ai.onnx::Attention. Every Attention node fell back to CPU EP; withsession.disable_cpu_ep_fallback=1session creation failed outright:Routing
The builder uses a two-track strategy based on backend and node attributes:
n_q % n_kv == 0+is_causal=1+ nosoftcap/attn_mask/qk_output+present_keydeclaredQNN_OP_GROUP_QUERY_ATTENTIONThe native path covers both MHA (
n_q == n_kv) and GQA/MQA (n_q > n_kv). Both 3D BSH and 4D BNSH inputs are supported: for 4D,Transpose+Reshapenodes are inserted before Q/K/V and aReshape+Transposeis inserted after Y to bridge the layout.Why decomposition for HTP: QNN SDK ≤ 2.49 has no MHA primitive for HTP.
QNN_OP_GROUP_QUERY_ATTENTIONis GPU-only (Adreno). The ONNXAttentionspec defines a reference body ofScale + MatMul + Softmax + MatMul; this PR implements that body as QNN node sequences.Why decomposition on GPU for some cases: The native
QNN_OP_GROUP_QUERY_ATTENTIONdoes not supportsoftcapor an additiveattn_maskinput, and is always causal. Cases that need these features fall to the decomposition path, which runs on GPU using individual QNN ops.GPU Native Path (
QNN_OP_GROUP_QUERY_ATTENTION)A single node is emitted with:
NUM_HEADS,KV_NUM_HEADS,SCALE,DO_ROTARY=0seqlens_k(INT32[B]=S_past + S_k − 1), synthetictotal_seq_len(INT32 0D scalar =S_past + S_k), key, value, past_key/past_value (4D BNSH, passed directly)seqlens_kandtotal_seq_lenare synthesized from static input shapes; they are not present in the ONNXai.onnx::Attentionsignature but required by QNN GQA.For 4D BNSH inputs, the builder transparently inserts
Transpose(0,2,1,3)+Reshapebefore Q/K/V (converting[B,n,S,hs]→[B,S,n*hs]) andReshape+Transpose(0,2,1,3)after Y (converting[B,S,n*hs]→[B,n,S,hs]). past_key/past_value are already BNSH and are passed straight through.Decomposition
3D inputs
[B, S, n·hs](BSH layout)sqrt(scale)scalarElementWiseMultiplyQ·√s[B, S_q, n_q·hs]ElementWiseMultiplyK·√s (no KV cache)[B, S_k, n_kv·hs]Reshape+Transpose(0,2,1,3)Q → BNSH[B, n_q, S_q, hs]Reshape+Transpose(0,2,1,3)K → BNSH[B, n_kv, S_k, hs]Concat(past_key, K, axis=2)(cache only)[B, n_kv, S_total, hs]Reshape→Tile→Transpose→Reshape(GQA only)[B, n_q, S_k, hs]ElementWiseMultiplyK·√s (KV cache path only, after concat+expand)[B, n_q, S_total, hs]MatMul(Q, K^T, transpose_in1=true)[B, n_q, S_q, S_k]ElementWiseDivide→Tanh→ElementWiseMultiply(if softcap!=0)[B, n_q, S_q, S_k]ElementWiseAdd(is_causal=1)[B, n_q, S_q, S_k]attn_mask ElementWiseAdd(if provided)[B, n_q, S_q, S_k]Softmax(axis=3)[B, n_q, S_q, S_k]Reshape+Transpose(0,2,1,3)V → BNSH[B, n_kv, S_k, v_hs]Concat(past_value, V, axis=2)(cache only)[B, n_kv, S_total, v_hs][B, n_q, S_k, v_hs]MatMul(attn_weights, V)[B, n_q, S_q, v_hs]Transpose(0,2,1,3)+ReshapeY[B, S_q, n_q·v_hs]The
qk_matmul_output_modecapture points (per published ONNX 1.23 spec — note thatcmake/external/onnxis pinned at v1.20.1 pre-errata which has the opposite ordering):For 4D inputs
[B, n, S, hs](BNSH layout): steps 4–7 and 21–22 are skipped — inputs are already in BNSH format.Operation order — softcap before masks (per ONNX 1.23 spec): Step 12 applies softcap to the raw QK scores before masks. Applying masks before softcap would clamp
-1e9causal mask values to-softcap, making masked positions visible to softmax.GQA head expansion (floor-division semantics matching the ONNX function body):
K[b, kv*ratio+r, s, h] = K_orig[b, kv, s, h]→[K0,K0,...,K1,K1,...]Reshape[B,1,n_kv,S*hs]→Tile[1,ratio,1,1]→Transpose(0,2,1,3)→Reshape[B,n_q,S,hs]present_key/present_valueretainkv_num_headsper the ONNX spec.Causal mask (
is_causal=1): Static[1, 1, S_q, S_k]lower-triangular float tensor (broadcast over[B, n_q, S_q, S_k]in ADD), values0.0forj ≤ i+offset,-1e9otherwise.-infis avoided because HTP V73 HVX does not reliably propagate IEEE-754-infthrough ADD. Offset =S_pastwhen KV cache is active,0otherwise.Softcap (
softcap != 0):ElementWiseDivide(scores, sc) → QNN_OP_TANH → ElementWiseMultiply(result, sc).KV cache ordering:
present_key = Concat(past_key, K_raw)atkv_num_headsheads, then GQA-expanded ton_qheads, then scaled by √s. This ensures past and current keys are treated uniformly, andpresent_key/present_valueoutputs carrykv_num_headsas the spec requires.Supported Configuration
n_q == n_kv,is_causal=1)QNN_OP_GROUP_QUERY_ATTENTION✓n_q > n_kv,is_causal=1)QNN_OP_GROUP_QUERY_ATTENTION✓is_causal=0scaleattn_mask(float additive bias)softcapQNN_OP_TANH✓QNN_OP_TANH(decomposition) ✓past_key/present_key)qk_matmul_outputmodes 0–3Unsupported / Rejected
scale <= 0nonpad_kv_seqlen— padding-mask computation not implemented; rejected outrightsoftmax_precision— cross-dtype softmax accumulation not implemented; rejected outrightpast_keywithoutpresent_key(or vice versa)attn_mask— spec requiresWhere(mask, 0, -inf)conversion; builder emits raw ADD, so bool masks are rejected. Pre-convert to a float additive bias before passing to this EP.doubleandbfloat16dtypes — only float32 and float16 are supported; other types would silently corrupt scalar buffer helpers (causal mask, sqrt_scale, softcap)