Skip to content

Commit 2d8cd35

Browse files
authored
XNNPACK: delegate bf16 softmax, computing in fp32 (#24805)
### Summary Related to #24667. That issue reports that evo1's 24 bf16 softmaxes take 20.0 s of its 35.4 s chunk because their XNNPACK config turns bf16 off, leaving them on the single-threaded portable kernels. This PR addresses only that bf16 softmax item, not the other items tracked in that issue. XNNPACK defines softmax only on fp32 and fp16 tensors, so flipping the config alone would fail at load. SoftmaxConfig no longer sets `supports_bf16 = False`, and the runtime now defines fp32 values for bf16 softmax inputs and outputs and converts between them around the fp32 softmax, sharing the fp32-value and convert helpers with the bf16 matmul path, so the delegate boundary stays bf16. Each side is converted only when it is bf16. Without `enable_bf16` the behavior is unchanged, and fp16 and fp32 softmaxes take the same path as before. The bf16 softmax test and the attention-block partitioner expectation are updated accordingly. **Compatibility:** a `.pte` exported with `enable_bf16=True` after this change delegates bf16 softmax, and fails to load on a runtime built before it. The serialization format doesn't change, so nothing in the file flags this. The bf16 matmul change had the same constraint. This PR was authored with Muse Code. ### Test plan `lintrunner` on the four changed files is clean. Partitioner probes confirmed a bf16 softmax delegates with `enable_bf16=True` and stays on the portable kernels without it, and that the attention block still splits into 3 delegates with the softmax inside one. The runtime change was verified with a fresh Release `executor_runner` build of this branch running exported `.pte` programs: a small bf16 softmax matched eager exactly, a 524288-element bf16 softmax matched to 1.2e-07, a bf16 attention block matched exactly, and fp32 and fp16 softmaxes passed. These cases were rerun after the helper refactor with the same results. On the large bf16 shape, 500 executions took about 0.13 s delegated versus about 1.54 s on the portable kernels on the same binary and inputs. The new `Tester`-based bf16 softmax test and the partitioner load-and-compare test were not run locally: this machine's installed Python runtime is an older build that cannot load programs exported from this branch, and an isolated rebuild was blocked by the repository's requirement that the checkout directory be named exactly `executorch`. Those tests need CI.
1 parent 9fffe15 commit 2d8cd35

4 files changed

Lines changed: 129 additions & 57 deletions

File tree

‎backends/xnnpack/partition/config/generic_node_configs.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,6 @@ def check_constraints(self, node: torch.fx.Node, ep: ExportedProgram) -> bool:
408408

409409
class SoftmaxConfig(GenericNodePartitionerConfig):
410410
target_name = "_softmax.default"
411-
supports_bf16 = False
412411

413412
def check_constraints(self, node: torch.fx.Node, ep: ExportedProgram) -> bool:
414413
"""

‎backends/xnnpack/runtime/XNNCompiler.cpp‎

Lines changed: 89 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -819,35 +819,19 @@ const fb_xnnpack::XNNTensorValue* getSerializedTensorValue(
819819
}
820820

821821
/*
822-
XNNPACK multiplies bf16 matrices only into an fp32 result. When the serialized
823-
graph multiplies bf16 matrices into a bf16 output, define an fp32 value for the
824-
op to write instead, which convertBf16MatMulResult() then converts into the
825-
output, so the delegate boundary stays bf16. Otherwise the op writes the output
826-
itself.
822+
Defines an internal fp32 value with the shape of a serialized value, for an op
823+
that XNNPACK computes only in fp32 on bf16 data. The caller converts between
824+
the two values with convertIfDistinct(), so the delegate boundary stays bf16.
827825
*/
828-
Error defineBf16MatMulResult(
826+
Error defineFp32ValueLike(
829827
xnn_subgraph_t subgraph_ptr,
830-
const fb_xnnpack::XNNGraph* graph,
831-
uint32_t input1_id,
832-
uint32_t input2_id,
833-
uint32_t output_id,
834-
uint32_t remapped_output_id,
828+
const fb_xnnpack::XNNTensorValue* value,
835829
const NodePtr node,
836-
uint32_t* result_id) noexcept {
837-
*result_id = remapped_output_id;
838-
const auto* input1 = getSerializedTensorValue(graph, input1_id);
839-
const auto* input2 = getSerializedTensorValue(graph, input2_id);
840-
const auto* output = getSerializedTensorValue(graph, output_id);
841-
if (input1 == nullptr || input2 == nullptr || output == nullptr ||
842-
input1->datatype() != DataType::xnn_datatype_bf16 ||
843-
input2->datatype() != DataType::xnn_datatype_bf16 ||
844-
output->datatype() != DataType::xnn_datatype_bf16) {
845-
return Error::Ok;
846-
}
830+
uint32_t* fp32_id) noexcept {
847831
// Like defineTensor(), treat a null dims vector as rank 0.
848832
std::vector<size_t> dims;
849-
if (output->dims() != nullptr) {
850-
dims = flatbufferDimsToVector<size_t>(output->dims());
833+
if (value->dims() != nullptr) {
834+
dims = flatbufferDimsToVector<size_t>(value->dims());
851835
}
852836
xnn_status status = xnn_define_tensor_value(
853837
subgraph_ptr,
@@ -857,39 +841,63 @@ Error defineBf16MatMulResult(
857841
/*data=*/nullptr,
858842
/*external_id=*/XNN_INVALID_VALUE_ID,
859843
/*flags=*/0,
860-
result_id);
844+
fp32_id);
861845
ET_CHECK_OR_RETURN_ERROR(
862846
status == xnn_status_success,
863847
Internal,
864-
"Failed to define the fp32 result of bf16 node %i: %s",
848+
"Failed to define an fp32 value for bf16 node %i: %s",
865849
node->debug_handle(),
866850
xnn_status_to_string(status));
867851
return Error::Ok;
868852
}
869853

870-
/*
871-
Converts the fp32 result that defineBf16MatMulResult() defined into the bf16
872-
output. Does nothing when the op wrote the output itself.
873-
*/
874-
Error convertBf16MatMulResult(
854+
Error convertIfDistinct(
875855
xnn_subgraph_t subgraph_ptr,
876-
uint32_t result_id,
877-
uint32_t remapped_output_id,
856+
uint32_t from_id,
857+
uint32_t to_id,
878858
const NodePtr node) noexcept {
879-
if (result_id == remapped_output_id) {
859+
if (from_id == to_id) {
880860
return Error::Ok;
881861
}
882-
xnn_status status = xnn_define_convert(
883-
subgraph_ptr, result_id, remapped_output_id, /*flags=*/0);
862+
xnn_status status =
863+
xnn_define_convert(subgraph_ptr, from_id, to_id, /*flags=*/0);
884864
ET_CHECK_OR_RETURN_ERROR(
885865
status == xnn_status_success,
886866
Internal,
887-
"Failed to convert the result of bf16 node %i to bf16: %s",
867+
"Failed to convert a value of bf16 node %i: %s",
888868
node->debug_handle(),
889869
xnn_status_to_string(status));
890870
return Error::Ok;
891871
}
892872

873+
/*
874+
XNNPACK multiplies bf16 matrices only into an fp32 result. When the serialized
875+
graph multiplies bf16 matrices into a bf16 output, define an fp32 value for the
876+
op to write instead, which the caller converts into the output. Otherwise the
877+
op writes the output itself.
878+
*/
879+
Error defineBf16MatMulResult(
880+
xnn_subgraph_t subgraph_ptr,
881+
const fb_xnnpack::XNNGraph* graph,
882+
uint32_t input1_id,
883+
uint32_t input2_id,
884+
uint32_t output_id,
885+
uint32_t remapped_output_id,
886+
const NodePtr node,
887+
uint32_t* result_id) noexcept {
888+
*result_id = remapped_output_id;
889+
const auto* input1 = getSerializedTensorValue(graph, input1_id);
890+
const auto* input2 = getSerializedTensorValue(graph, input2_id);
891+
const auto* output = getSerializedTensorValue(graph, output_id);
892+
if (input1 == nullptr || input2 == nullptr || output == nullptr ||
893+
input1->datatype() != DataType::xnn_datatype_bf16 ||
894+
input2->datatype() != DataType::xnn_datatype_bf16 ||
895+
output->datatype() != DataType::xnn_datatype_bf16) {
896+
return Error::Ok;
897+
}
898+
return defineFp32ValueLike(subgraph_ptr, output, node, result_id);
899+
}
900+
893901
/*
894902
Define serialized linear(fully-connected) node into the subgraph using
895903
the remapped ids to map the serialized ids, to the new ids generated
@@ -934,9 +942,29 @@ Error defineFullyConnectedNode(
934942
node->debug_handle(),
935943
xnn_status_to_string(status));
936944

937-
return convertBf16MatMulResult(subgraph_ptr, fc_result, fc_output, node);
945+
return convertIfDistinct(subgraph_ptr, fc_result, fc_output, node);
938946
};
939947

948+
/*
949+
XNNPACK defines softmax only on fp32 and fp16 tensors. When a serialized
950+
softmax value is bf16, define an fp32 value for the softmax to use instead.
951+
Otherwise the softmax uses the value itself.
952+
*/
953+
Error defineBf16SoftmaxFp32Value(
954+
xnn_subgraph_t subgraph_ptr,
955+
const fb_xnnpack::XNNGraph* graph,
956+
uint32_t serialized_id,
957+
uint32_t remapped_id,
958+
const NodePtr node,
959+
uint32_t* fp32_id) noexcept {
960+
*fp32_id = remapped_id;
961+
const auto* value = getSerializedTensorValue(graph, serialized_id);
962+
if (value == nullptr || value->datatype() != DataType::xnn_datatype_bf16) {
963+
return Error::Ok;
964+
}
965+
return defineFp32ValueLike(subgraph_ptr, value, node, fp32_id);
966+
}
967+
940968
/*
941969
Define serialized softmax node into the subgraph, using the remapped ids
942970
to map the serialized ids, to the new ids generated when defining
@@ -947,22 +975,40 @@ Error defineSoftmaxNode(
947975
const std::unordered_map<uint32_t, uint32_t>& remapped_ids,
948976
const NodePtr node,
949977
const fb_xnnpack::XNNGraph* graph) noexcept {
950-
MAYBE_UNUSED(graph);
951-
952978
auto graph_node = node->xnode_union_as_XNNSoftmax();
953979
REMAP_ID(remapped_ids, graph_node->input_id(), sm_input);
954980
REMAP_ID(remapped_ids, graph_node->output_id(), sm_output);
955981

982+
uint32_t softmax_input = XNN_INVALID_VALUE_ID;
983+
ET_CHECK_OK_OR_RETURN_ERROR(defineBf16SoftmaxFp32Value(
984+
subgraph_ptr,
985+
graph,
986+
graph_node->input_id(),
987+
sm_input,
988+
node,
989+
&softmax_input));
990+
uint32_t softmax_output = XNN_INVALID_VALUE_ID;
991+
ET_CHECK_OK_OR_RETURN_ERROR(defineBf16SoftmaxFp32Value(
992+
subgraph_ptr,
993+
graph,
994+
graph_node->output_id(),
995+
sm_output,
996+
node,
997+
&softmax_output));
998+
999+
ET_CHECK_OK_OR_RETURN_ERROR(
1000+
convertIfDistinct(subgraph_ptr, sm_input, softmax_input, node));
1001+
9561002
xnn_status status = xnn_define_softmax(
957-
subgraph_ptr, sm_input, sm_output, graph_node->flags());
1003+
subgraph_ptr, softmax_input, softmax_output, graph_node->flags());
9581004
ET_CHECK_OR_RETURN_ERROR(
9591005
status == xnn_status_success,
9601006
Internal,
9611007
"Failed to create softmax node %i with code: %s",
9621008
node->debug_handle(),
9631009
xnn_status_to_string(status));
9641010

965-
return Error::Ok;
1011+
return convertIfDistinct(subgraph_ptr, softmax_output, sm_output, node);
9661012
}
9671013

9681014
Error defineGlobalAvgPooling2dNode(
@@ -1793,7 +1839,7 @@ Error defineBatchMatrixMultiplyNode(
17931839
node->debug_handle(),
17941840
xnn_status_to_string(status));
17951841

1796-
return convertBf16MatMulResult(subgraph_ptr, bmm_result, bmm_out, node);
1842+
return convertIfDistinct(subgraph_ptr, bmm_result, bmm_out, node);
17971843
}
17981844

17991845
/*

‎backends/xnnpack/test/ops/test_softmax.py‎

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import unittest
88

99
import torch
10+
from executorch.backends.xnnpack.partition.xnnpack_partitioner import XnnpackPartitioner
1011
from executorch.backends.xnnpack.test.tester import Tester
12+
from executorch.backends.xnnpack.test.tester.tester import ToEdgeTransformAndLower
1113

1214

1315
class TestSoftmax(unittest.TestCase):
@@ -22,7 +24,7 @@ def __init__(self, dim):
2224
def forward(self, x):
2325
return torch.nn.Softmax(dim=self.dim)(x)
2426

25-
def _test_softmax(self, inputs):
27+
def _test_softmax(self, inputs, enable_bf16=False):
2628
# Dim can be either the last dimension index or -1 (last dimension),
2729
# as xnnpack only supports softmax on the last dimension.
2830
valid_dims = [len(inputs[0]) - 1, -1]
@@ -32,19 +34,48 @@ def _test_softmax(self, inputs):
3234
dynamic_shape[i] = torch.export.Dim(f"dynamic_dim{i}", min=1, max=100)
3335
dynamic_shape = (dynamic_shape,)
3436

37+
# XNNPACK computes a bf16 softmax in fp32 and rounds back to bf16,
38+
# so it needs the looser bf16 tolerance.
39+
atol = rtol = 2e-2 if enable_bf16 else 1e-3
40+
3541
for dim in valid_dims:
36-
(
42+
tester = (
3743
Tester(self.Softmax(dim), inputs, dynamic_shapes=dynamic_shape)
3844
.export()
3945
.check_count({"torch.ops.aten.softmax": 1})
40-
.to_edge_transform_and_lower()
41-
.check_count({"torch.ops.higher_order.executorch_call_delegate": 1})
46+
)
47+
if enable_bf16:
48+
tester = tester.to_edge_transform_and_lower(
49+
ToEdgeTransformAndLower(
50+
partitioners=[XnnpackPartitioner(enable_bf16=True)]
51+
)
52+
)
53+
else:
54+
tester = tester.to_edge_transform_and_lower()
55+
(
56+
tester.check_count(
57+
{"torch.ops.higher_order.executorch_call_delegate": 1}
58+
)
4259
.check_not(["executorch_exir_dialects_edge__ops_aten__softmax_default"])
4360
.to_executorch()
4461
.serialize()
45-
.run_method_and_compare_outputs(num_runs=5)
62+
.run_method_and_compare_outputs(num_runs=5, atol=atol, rtol=rtol)
4663
)
4764

65+
def test_bf16_softmax(self):
66+
inputs = (torch.rand((3, 5, 7)).to(torch.bfloat16),)
67+
self._test_softmax(inputs, enable_bf16=True)
68+
69+
def test_bf16_softmax_not_partitioned_without_enable_bf16(self):
70+
inputs = (torch.rand((3, 5, 7)).to(torch.bfloat16),)
71+
(
72+
Tester(self.Softmax(-1), inputs)
73+
.export()
74+
.check_count({"torch.ops.aten.softmax": 1})
75+
.to_edge_transform_and_lower()
76+
.check(["executorch_exir_dialects_edge__ops_aten__softmax_default"])
77+
)
78+
4879
def test_fp16_softmax(self):
4980
inputs = (torch.rand((3, 5, 7)).to(torch.float16),)
5081
self._test_softmax(inputs)

‎backends/xnnpack/test/test_xnnpack_partitioner.py‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,6 @@ class Mean(torch.nn.Module):
182182
def forward(self, x):
183183
return x.mean((-1, -2), keepdim=True)
184184

185-
class Softmax(torch.nn.Module):
186-
def forward(self, x):
187-
return torch.softmax(x, -1)
188-
189185
def lower(module, x):
190186
return to_edge_transform_and_lower(
191187
export(module, (x,)),
@@ -200,7 +196,6 @@ def lower(module, x):
200196
),
201197
"max_pool2d": torch.nn.MaxPool2d(2),
202198
"mean": Mean(),
203-
"softmax": Softmax(),
204199
"upsample_bilinear2d": torch.nn.Upsample(scale_factor=2, mode="bilinear"),
205200
}
206201
x = torch.randn(1, 4, 8, 8)
@@ -270,8 +265,9 @@ def forward(self, x):
270265
)
271266
def test_enable_bf16_runs_an_attention_block(self):
272267
"""
273-
The linear layers and both batch matrix multiplications of a bf16 attention
274-
block run in XNNPACK, and the program loads and matches eager bf16.
268+
The linear layers, both batch matrix multiplications and the softmax
269+
of a bf16 attention block run in XNNPACK, and the program loads and
270+
matches eager bf16.
275271
"""
276272

277273
class Attention(torch.nn.Module):
@@ -294,7 +290,7 @@ def forward(self, x):
294290
ops = _ops_outside_delegates(program)
295291
# The expand copies that matmul adds split the block into 3 delegates.
296292
self.assertEqual(ops.count("executorch_call_delegate"), 3)
297-
self.assertIn("aten._softmax.default", ops)
293+
self.assertNotIn("aten._softmax.default", ops)
298294
matmuls = {
299295
"aten.addmm.default",
300296
"aten.bmm.default",

0 commit comments

Comments
 (0)