Skip to content

Commit 722743c

Browse files
Add MHA fusion for Nemotron speech conformer encoder (#27764)
### Description This PR updates the pattern matchings to perform multi-head attention fusion for the conformer encoder inside [Nemotron speech](https://huggingface.co/nvidia/nemotron-speech-streaming-en-0.6b). <img width="550" height="976" alt="image" src="https://github.com/user-attachments/assets/a194308e-ce69-4128-9389-aae2a64b312f" /> ### Motivation and Context These changes allow the `MultiHeadAttention` op to appear in the encoder ONNX model. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 1c05b13 commit 722743c

3 files changed

Lines changed: 601 additions & 19 deletions

File tree

onnxruntime/python/tools/transformers/fusion_conformer_attention.py

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node):
3232
[1, None, 0, 0, 0],
3333
)
3434
if qkv_nodes is None:
35-
logger.debug("fuse_conformer_attention: failed to match qkv path")
36-
return
35+
qkv_nodes = self.model.match_parent_path(
36+
normalize_node,
37+
["MatMul", "Reshape", "Transpose", "MatMul"],
38+
[1, 0, 0, 0],
39+
)
40+
if qkv_nodes is None:
41+
logger.debug("fuse_conformer_attention: failed to match qkv path")
42+
return
3743

3844
reshape_qkv, transpose_qkv, matmul_qkv = qkv_nodes[-3], qkv_nodes[-2], qkv_nodes[-1]
3945

@@ -50,33 +56,50 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node):
5056
[1, 0, 0, 0],
5157
)
5258
if v_nodes is None:
53-
logger.debug("fuse_conformer_attention: failed to match v path")
54-
return
59+
v_nodes = self.model.match_parent_path(
60+
matmul_qkv,
61+
["Transpose", "Reshape", "MatMul"],
62+
[1, 0, 0],
63+
)
64+
if v_nodes is None:
65+
logger.debug("fuse_conformer_attention: failed to match v path")
66+
return
5567
else:
5668
concat_v = v_nodes[0]
5769
concat_parent = self.model.get_parent(concat_v, 0, None)
5870
present_v = concat_v.output[0]
5971
past_v = concat_parent.output[0]
6072

61-
add_v, matmul_v = v_nodes[-2], v_nodes[-1]
73+
add_v = v_nodes[-2] if len(v_nodes) >= 2 and v_nodes[-2].op_type == "Add" else None
74+
matmul_v = v_nodes[-1]
6275

6376
attn_mask = ""
6477
qk_nodes = self.model.match_parent_path(
6578
matmul_qkv,
6679
["Softmax", "Add", "MatMul"],
6780
[0, 0, 0],
6881
)
82+
where_qk = None
6983
if qk_nodes is None:
7084
qk_nodes = self.model.match_parent_path(
7185
matmul_qkv,
7286
["Where", "Softmax", "Where", "Add", "MatMul"],
7387
[0, 2, 0, 2, 0],
7488
)
7589
if qk_nodes is None:
76-
logger.debug("fuse_conformer_attention: failed to match qk path")
77-
return
90+
qk_nodes = self.model.match_parent_path(
91+
matmul_qkv,
92+
["Where", "Softmax", "Where", "Div", "Add", "MatMul"],
93+
[0, 2, 0, 2, 0, 0],
94+
)
95+
if qk_nodes is None:
96+
logger.debug("fuse_conformer_attention: failed to match qk path")
97+
return
98+
where_qk = qk_nodes[2]
99+
else:
100+
where_qk = qk_nodes[2]
78101

79-
where_qk = qk_nodes[2]
102+
if where_qk is not None:
80103
mask_nodes = self.model.match_parent_path(
81104
where_qk,
82105
["Equal", "Unsqueeze", "Cast"],
@@ -99,20 +122,46 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node):
99122
[0, 0, 0, 0, 0],
100123
)
101124
if q_nodes is None:
102-
logger.debug("fuse_conformer_attention: failed to match q path")
103-
return
125+
q_nodes = self.model.match_parent_path(
126+
matmul_qk,
127+
["Transpose", "Add", "Reshape", "MatMul"],
128+
[0, 0, 0, 1],
129+
)
130+
if q_nodes is None:
131+
q_nodes = self.model.match_parent_path(
132+
matmul_qk,
133+
["Transpose", "Add", "Reshape", "MatMul"],
134+
[0, 0, 0, 0],
135+
)
136+
if q_nodes is None:
137+
logger.debug("fuse_conformer_attention: failed to match q path")
138+
return
104139

105-
reshape_q, add_q, matmul_q = q_nodes[-3], q_nodes[-2], q_nodes[-1]
140+
reshape_q = next((node for node in q_nodes if node.op_type == "Reshape"), None)
141+
add_q = next((node for node in q_nodes if node.op_type == "Add"), None)
142+
matmul_q = next((node for node in reversed(q_nodes) if node.op_type == "MatMul"), None)
143+
if reshape_q is None or add_q is None or matmul_q is None:
144+
logger.debug("fuse_conformer_attention: failed to identify q reshape/add/matmul nodes")
145+
return
106146

107147
extra_q_nodes = self.model.match_parent_path(
108148
add_qk,
109149
["Reshape", "Transpose", "MatMul", "Transpose", "Reshape", "Div"],
110150
[1, 0, 0, 0, 0, 0],
111151
)
112-
if extra_q_nodes is not None and q_nodes[0] != extra_q_nodes[-1]:
152+
if extra_q_nodes is not None and q_nodes[0].op_type in ["Div", "Mul"] and q_nodes[0] != extra_q_nodes[-1]:
113153
logger.debug("fuse_conformer_attention: failed to match extra q path")
114154
return
115155

156+
if extra_q_nodes is None:
157+
nemotron_extra_q_nodes = self.model.match_parent_path(
158+
add_qk,
159+
["Slice", "Reshape", "Slice", "Reshape", "Pad", "MatMul", "Transpose", "Add"],
160+
[1, 0, 0, 0, 0, 0, 0, 0],
161+
)
162+
if nemotron_extra_q_nodes is not None:
163+
extra_q_nodes = nemotron_extra_q_nodes
164+
116165
past_k, present_k = "", ""
117166
k_nodes = self.model.match_parent_path(
118167
matmul_qk,
@@ -132,24 +181,50 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node):
132181
[1, 0, 0, 0],
133182
)
134183
if k_nodes is None:
135-
logger.debug("fuse_conformer_attention: failed to match k path")
136-
return
184+
k_nodes = self.model.match_parent_path(
185+
matmul_qk,
186+
["Transpose", "Reshape", "MatMul"],
187+
[1, 0, 0],
188+
)
189+
if k_nodes is None:
190+
logger.debug("fuse_conformer_attention: failed to match k path")
191+
return
137192
else:
138193
concat_k = k_nodes[1]
139194
concat_parent = self.model.get_parent(concat_k, 0, None)
140195
past_k = concat_parent.output[0]
141196
present_k = concat_k.output[0]
142197

143-
add_k, matmul_k = k_nodes[-2], k_nodes[-1]
198+
add_k = k_nodes[-2] if len(k_nodes) >= 2 and k_nodes[-2].op_type == "Add" else None
199+
matmul_k = k_nodes[-1]
144200

145201
num_heads, hidden_size = self.get_num_heads_and_hidden_size(reshape_q)
146202
if num_heads <= 0 or hidden_size <= 0 or (hidden_size % num_heads) != 0:
147203
logger.debug("fuse_conformer_attention: failed to detect num_heads or hidden_size")
148204
return
149205

206+
# Validate attention_bias: the Attention and MultiHeadAttention kernels require a 4-D
207+
# tensor with shape [batch_size or 1, num_heads or 1, sequence_length, total_sequence_length].
208+
# Scalar or 1-D initializers (e.g. a plain QK scaling constant) must not be forwarded as
209+
# attention_bias. Non-initializer values (computed positional-bias outputs) are kept as-is.
210+
attention_bias = add_qk.input[1]
211+
bias_init = self.model.get_initializer(attention_bias)
212+
if bias_init is not None and len(bias_init.dims) != 4:
213+
logger.debug(
214+
"fuse_conformer_attention: skipping attention_bias %s with dims %s (expected 4-D)",
215+
attention_bias,
216+
list(bias_init.dims),
217+
)
218+
attention_bias = ""
219+
150220
new_node = None
151221
use_packed_attention_op = (
152-
matmul_q.input[0] == matmul_k.input[0] and matmul_k.input[0] == matmul_v.input[0] and extra_q_nodes is None
222+
matmul_q.input[0] == matmul_k.input[0]
223+
and matmul_k.input[0] == matmul_v.input[0]
224+
and extra_q_nodes is None
225+
and add_q is not None
226+
and add_k is not None
227+
and add_v is not None
153228
)
154229
if use_packed_attention_op:
155230
# Self-attention, use Attention op
@@ -165,7 +240,7 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node):
165240
hidden_size=hidden_size,
166241
first_input=matmul_q.input[0],
167242
output=reshape_qkv.output[0],
168-
add_qk_str=add_qk.input[1],
243+
add_qk_str=attention_bias,
169244
past_k=past_k,
170245
past_v=past_v,
171246
present_k=present_k,
@@ -183,7 +258,7 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node):
183258
hidden_size=hidden_size,
184259
output=reshape_qkv.output[0],
185260
key_padding_mask=attn_mask,
186-
add_qk=add_qk.input[1],
261+
add_qk=attention_bias,
187262
past_k=past_k,
188263
past_v=past_v,
189264
present_k=present_k,

0 commit comments

Comments
 (0)