Skip to content

Commit 285287f

Browse files
mvafinCopilot
andcommitted
[PT FE] Address review feedback on aten::_transformer_encoder_layer_fwd PR
- Restrict test_transformer_encoder_layer_module's norm_first parametrization to False: PyTorch disables the fused fast path when norm_first=True, so aten::_transformer_encoder_layer_fwd is never traced in that case (already covered by test_transformer_encoder_layer_fwd, which calls the op directly). - Add test_native_multi_head_attention_weights to cover the attention weights (second output) of aten::_native_multi_head_attention, both averaged and non-averaged, confirming build_multi_head_attention() produces post-softmax weights matching PyTorch (verified directly against torch.ops.aten ._native_multi_head_attention). - Extend the same test to non-boolean (additive) masks, confirming aten::_native_multi_head_attention accepts them like PyTorch does. This also surfaced a pre-existing CPU plugin defect (unrelated to the PyTorch FE conversion) when a key-padding mask has as many elements as the QK^T MatMul's last dimension; the corresponding case is marked xfail with an explanation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c7836fa commit 285287f

2 files changed

Lines changed: 67 additions & 8 deletions

File tree

tests/layer_tests/pytorch_tests/test_native_multi_head_attention.py

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
NO_MASK, ATTN_MASK, KEY_PAD_MASK, MERGED_MASK = -1, 0, 1, 2
1717

1818
class aten_native_multi_head_attention(torch.nn.Module):
19-
def __init__(self, mask, need_weights, average_attn_weights, mask_data=None) -> None:
19+
def __init__(self, mask, need_weights, average_attn_weights, mask_data=None, mask_dtype="bool") -> None:
2020
super().__init__()
2121
self.qkv = torch.nn.Linear(EMBED_DIM, 3 * EMBED_DIM, dtype = torch.float32)
2222
self.qkv.requires_grad_(False)
@@ -28,17 +28,21 @@ def __init__(self, mask, need_weights, average_attn_weights, mask_data=None) ->
2828
self.need_weights = need_weights
2929
self.average_attn_weights = average_attn_weights
3030

31-
# Currently only int masks are working correctly, they are converted to bool.
32-
# Float masks raise a warning in PyTorch and are (incorrectly) converted to bool,
33-
# which later returns NaNs as MHA's output
31+
def make_mask(data):
32+
bool_mask = torch.from_numpy(data.astype("bool"))
33+
if mask_dtype == "float":
34+
# A non-boolean mask is additive: masked positions carry -inf, unmasked ones 0.
35+
return torch.zeros(bool_mask.shape, dtype=torch.float32).masked_fill(bool_mask, float("-inf"))
36+
return bool_mask
37+
3438
if mask == ATTN_MASK:
35-
self.mask = torch.from_numpy(mask_data.astype("bool")) if mask_data is not None else None
39+
self.mask = make_mask(mask_data) if mask_data is not None else None
3640
self.mask_type = ATTN_MASK
3741
elif mask == KEY_PAD_MASK:
38-
self.mask = torch.from_numpy(mask_data.astype("bool")) if mask_data is not None else None
42+
self.mask = make_mask(mask_data) if mask_data is not None else None
3943
self.mask_type = KEY_PAD_MASK
4044
elif mask == MERGED_MASK:
41-
self.mask = torch.from_numpy(mask_data.astype("bool")) if mask_data is not None else None
45+
self.mask = make_mask(mask_data) if mask_data is not None else None
4246
self.mask_type = MERGED_MASK
4347
else:
4448
self.mask = None
@@ -55,6 +59,23 @@ def forward(self, query, key, value):
5559
mask_type = self.mask_type
5660
)[0]
5761

62+
63+
class aten_native_multi_head_attention_with_weights(aten_native_multi_head_attention):
64+
"""Like aten_native_multi_head_attention, but also returns the attention weights output.
65+
Only meant to be used with need_weights=True, where the second output is a Tensor (not None),
66+
so the traced/scripted graph has a single, well defined output type."""
67+
68+
def forward(self, query, key, value):
69+
return torch.ops.aten._native_multi_head_attention(
70+
query, key, value,
71+
embed_dim=self.embed_dim, num_head=self.num_heads,
72+
qkv_weight=self.qkv.weight, qkv_bias=self.qkv.bias,
73+
proj_weight=self.proj.weight, proj_bias=self.proj.bias,
74+
mask = self.mask, need_weights=self.need_weights,
75+
average_attn_weights = self.average_attn_weights,
76+
mask_type = self.mask_type
77+
)
78+
5879
class TestNativeMultiHeadAttention(PytorchLayerTest):
5980
def _prepare_input(self):
6081
# NativeMHA is self-attention
@@ -90,3 +111,37 @@ def _get_mask_data(self, mask):
90111
def test_native_multi_head_attention(self, ie_device, precision, ir_version, mask, need_weights, average_attn_weights):
91112
mask_data = self._get_mask_data(mask)
92113
self._test(aten_native_multi_head_attention(mask, need_weights, average_attn_weights, mask_data), "aten::_native_multi_head_attention", ie_device, precision, ir_version)
114+
115+
@pytest.mark.nightly
116+
@pytest.mark.precommit
117+
@pytest.mark.parametrize(
118+
"mask",
119+
[NO_MASK, ATTN_MASK, KEY_PAD_MASK, MERGED_MASK]
120+
)
121+
@pytest.mark.parametrize("average_attn_weights", [False, True])
122+
@pytest.mark.parametrize("mask_dtype", ["bool", "float"])
123+
@pytest.mark.xfail(condition=platform.system() in ('Darwin', 'Linux') and platform.machine() in ('arm', 'armv7l',
124+
'aarch64',
125+
'arm64', 'ARM64'),
126+
reason='Ticket - 122715')
127+
def test_native_multi_head_attention_weights(self, ie_device, precision, ir_version, mask, average_attn_weights,
128+
mask_dtype):
129+
# The attention weights (second output) are only produced/checked here, both averaged and
130+
# non-averaged. This also covers non-boolean (additive) masks: aten::_native_multi_head_attention
131+
# accepts them the same way as aten::_transformer_encoder_layer_fwd does.
132+
if ie_device == "CPU" and mask == KEY_PAD_MASK and mask_dtype == "float":
133+
# A key-padding mask, once unsqueezed to [batch, 1, 1, seq], is additive and has exactly
134+
# `seq` elements, i.e. the same size as the last dimension of the preceding QK^T MatMul.
135+
# The CPU plugin's post-ops fusion (DnnlPostOpsComposer::appendBinary) misidentifies this
136+
# Add as a fusable per-output-channel bias and fails with
137+
# "Check 'data.size() == OC' failed ... data size: 6 OC: 0" at graph compilation time,
138+
# because MatMul (unlike Convolution/FullyConnected) has no well-defined output-channel
139+
# axis for this fusion. This is a CPU plugin defect, not a PyTorch FE conversion issue -
140+
# the produced graph is mathematically correct and matches PyTorch's output.
141+
pytest.xfail("CPU plugin fails to compile Add(MatMul_output, mask) when the additive mask "
142+
"has as many elements as the MatMul's last dimension "
143+
"(dnnl_postops_composer.cpp:483, 'data.size() == OC' assertion)")
144+
mask_data = self._get_mask_data(mask)
145+
self._test(aten_native_multi_head_attention_with_weights(mask, True, average_attn_weights, mask_data,
146+
mask_dtype=mask_dtype),
147+
"aten::_native_multi_head_attention", ie_device, precision, ir_version)

tests/layer_tests/pytorch_tests/test_transformer_encoder_layer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,11 @@ def test_transformer_encoder_layer_fwd(self, norm_first, use_gelu, mask_type,
112112

113113
@pytest.mark.nightly
114114
@pytest.mark.precommit
115-
@pytest.mark.parametrize("norm_first", [False, True])
115+
# norm_first=True disables PyTorch's fused fast path (why_not_sparsity_fast_path =
116+
# "norm_first was True"), so the traced graph decomposes into elementary ops and
117+
# aten::_transformer_encoder_layer_fwd is never emitted. That case is already covered
118+
# by test_transformer_encoder_layer_fwd, which calls the fast-path op directly.
119+
@pytest.mark.parametrize("norm_first", [False])
116120
@pytest.mark.parametrize("activation", ["relu", "gelu"])
117121
@pytest.mark.skipif(PytorchLayerTest.use_torch_export(),
118122
reason="TransformerEncoderLayer fast path is not used by torch.export")

0 commit comments

Comments
 (0)