Skip to content

Commit a911cec

Browse files
JakeStevensfacebook-github-bot
authored andcommitted
Support bf16 delegation for fully-connected (#21493)
Summary: A reland of D113977357/#21409 Adds XNNPACK delegation support for bf16 fully-connected, including bf16 dynamic-quant (8da4w). This lets bf16 models (e.g. google/gemma-3-1b-it exported with --dtype bfloat16) lower their linear layers to XNNPACK instead of falling back to portable. Key addition for the reland: an enable_bf16 flag in the xnnpack_config, which is default false. This fixes the internal failures, where the XNNPACK pin is too old. Differential Revision: D114220918
1 parent d632341 commit a911cec

5 files changed

Lines changed: 144 additions & 13 deletions

File tree

backends/xnnpack/operators/node_visitor.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -265,12 +265,15 @@ def get_per_channel_dtype(
265265
)
266266
else:
267267
node_dtype = get_node_dtype(node)
268-
if node_dtype is not None and node_dtype == torch.float16:
269-
dtype = (
270-
XNNDatatype.xnn_datatype_fp32
271-
if force_fp32
272-
else XNNDatatype.xnn_datatype_fp16
273-
)
268+
# fp16/bf16 tensors keep their datatype unless we've been asked to
269+
# force fp32 (e.g. biases for dynamic-quant or bf16 fully-connected),
270+
# in which case they fall back to the default fp32.
271+
float_dtype_map = {
272+
torch.float16: XNNDatatype.xnn_datatype_fp16,
273+
torch.bfloat16: XNNDatatype.xnn_datatype_bf16,
274+
}
275+
if not force_fp32:
276+
dtype = float_dtype_map.get(node_dtype, dtype)
274277

275278
return dtype
276279

@@ -591,7 +594,7 @@ def get_serialized_buffer_index(
591594
# Quantize buffer if static data is indeed quantized
592595
if quant_params is not None and not quant_params.is_dynamic:
593596
const_val = quant_params.quantize_tensor(const_val).contiguous()
594-
elif const_val.dtype != torch.float16 or force_fp32:
597+
elif const_val.dtype not in (torch.float16, torch.bfloat16) or force_fp32:
595598
# ensure that the const is fp32
596599
const_val = const_val.to(dtype=torch.float32).contiguous()
597600

@@ -712,12 +715,19 @@ def define_nodes_tensor_inputs_outputs(
712715
bias_quant_params = QuantParams.from_bias(
713716
bias_node, weight_quant_params, input_quant_params
714717
)
718+
# XNNPACK's bf16 fully-connected (bf16_bf16_f32) takes a bf16
719+
# activation/weight but an fp32 bias, so force the bias to fp32.
720+
weight_val = weight_node.meta.get("val", None)
721+
bias_force_fp32 = (
722+
weight_val is not None and weight_val.dtype == torch.bfloat16
723+
)
715724
self.define_tensor(
716725
bias_node,
717726
xnn_graph,
718727
vals_to_ids,
719728
quant_params=bias_quant_params,
720729
convert_to_nhwc=False, # Bias is generally 1d and can not be in NHWC
730+
force_fp32=bias_force_fp32,
721731
)
722732

723733
def define_node(

backends/xnnpack/operators/op_linear.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ def define_node(
7373
force_fp32 = False
7474
if input_quant_params is not None and input_quant_params.is_dynamic:
7575
force_fp32 = True
76+
# XNNPACK's bf16 fully-connected (bf16_bf16_f32) takes a bf16
77+
# activation/weight but an fp32 bias, so force the bias to fp32.
78+
weight_val = weight_node.meta.get("val", None)
79+
if weight_val is not None and weight_val.dtype == torch.bfloat16:
80+
force_fp32 = True
7681

7782
self.define_tensor(
7883
get_input_node(node, 2),

backends/xnnpack/partition/config/xnnpack_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ def __init__(self, **kwargs):
5050
self.force_non_static_weights_for_f32_linear = kwargs.get(
5151
"force_non_static_weights_for_f32_linear", False
5252
)
53+
# Opt-in flag for bf16 delegation (e.g. bf16 fully-connected). XNNPACK
54+
# only supports bf16 fully-connected on new enough revisions, so this
55+
# stays off by default and bf16 nodes fall back to the portable op
56+
# unless the caller explicitly enables it.
57+
self.enable_bf16 = kwargs.get("enable_bf16", False)
5358

5459
def get_partition(
5560
self, node: torch.fx.Node, ep: ExportedProgram
@@ -229,6 +234,8 @@ def _check_node_has_valid_dtype(self, node):
229234
torch.float32,
230235
torch.float16,
231236
}
237+
if self.enable_bf16:
238+
valid_dtypes.add(torch.bfloat16)
232239
# Only allow int8 and quant dtypes for quant operations
233240
if is_quant(node) or is_dequant(node) or is_qparam(node):
234241
valid_dtypes.update(

backends/xnnpack/runtime/XNNCompiler.cpp

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,32 @@ Error defineConvertNode(
791791

792792
return Error::Ok;
793793
};
794+
/*
795+
Look up a serialized tensor value (plain or quantized wrapper) by its
796+
output id. Returns nullptr if not found.
797+
*/
798+
const fb_xnnpack::XNNTensorValue* getSerializedTensorValue(
799+
const fb_xnnpack::XNNGraph* graph,
800+
uint32_t id) noexcept {
801+
if (graph == nullptr || graph->xvalues() == nullptr) {
802+
return nullptr;
803+
}
804+
for (auto value : *graph->xvalues()) {
805+
const fb_xnnpack::XNNTensorValue* tv = nullptr;
806+
if (value->xvalue_union_type() == fb_xnnpack::XValueUnion::XNNTensorValue) {
807+
tv = value->xvalue_union_as_XNNTensorValue();
808+
} else if (
809+
value->xvalue_union_type() ==
810+
fb_xnnpack::XValueUnion::XNNQuantizedTensorValue) {
811+
tv = value->xvalue_union_as_XNNQuantizedTensorValue()->tensor_value();
812+
}
813+
if (tv != nullptr && tv->id_out() == id) {
814+
return tv;
815+
}
816+
}
817+
return nullptr;
818+
}
819+
794820
/*
795821
Define serialized linear(fully-connected) node into the subgraph using
796822
the remapped ids to map the serialized ids, to the new ids generated
@@ -810,14 +836,52 @@ Error defineFullyConnectedNode(
810836
REMAP_ID(remapped_ids, graph_node->bias_id(), fc_bias);
811837
REMAP_ID(remapped_ids, graph_node->output_id(), fc_output);
812838

839+
// XNNPACK only provides a bf16 fully-connected of type bf16_bf16_f32:
840+
// bf16 activation x bf16 weight -> fp32 output. When the serialized graph
841+
// asks for a bf16 output (e.g. a fully bf16 model), define the FC with an
842+
// fp32 intermediate output and append a convert (fp32 -> bf16) so the
843+
// delegate boundary stays bf16.
844+
const auto* in_tv = getSerializedTensorValue(graph, graph_node->input1_id());
845+
const auto* filt_tv =
846+
getSerializedTensorValue(graph, graph_node->filter_id());
847+
const auto* out_tv = getSerializedTensorValue(graph, graph_node->output_id());
848+
const bool needs_bf16_output_convert = in_tv != nullptr &&
849+
filt_tv != nullptr && out_tv != nullptr &&
850+
in_tv->datatype() == DataType::xnn_datatype_bf16 &&
851+
filt_tv->datatype() == DataType::xnn_datatype_bf16 &&
852+
out_tv->datatype() == DataType::xnn_datatype_bf16;
853+
854+
uint32_t fc_compute_output = fc_output;
855+
if (needs_bf16_output_convert) {
856+
std::vector<size_t> out_dims =
857+
flatbufferDimsToVector<size_t>(out_tv->dims());
858+
uint32_t intermediate_id = XNN_INVALID_VALUE_ID;
859+
xnn_status ts = xnn_define_tensor_value(
860+
subgraph_ptr,
861+
xnn_datatype_fp32,
862+
out_dims.size(),
863+
out_dims.data(),
864+
/*data=*/nullptr,
865+
/*external_id=*/XNN_INVALID_VALUE_ID,
866+
/*flags=*/0,
867+
&intermediate_id);
868+
ET_CHECK_OR_RETURN_ERROR(
869+
ts == xnn_status_success,
870+
Internal,
871+
"Failed to define fp32 intermediate for bf16 linear node %i: %s",
872+
node->debug_handle(),
873+
xnn_status_to_string(ts));
874+
fc_compute_output = intermediate_id;
875+
}
876+
813877
xnn_status status = xnn_define_fully_connected(
814878
subgraph_ptr,
815879
min_max.first,
816880
min_max.second,
817881
fc_input1,
818882
fc_filter,
819883
fc_bias,
820-
fc_output,
884+
fc_compute_output,
821885
graph_node->flags());
822886
ET_CHECK_OR_RETURN_ERROR(
823887
status == xnn_status_success,
@@ -826,6 +890,17 @@ Error defineFullyConnectedNode(
826890
node->debug_handle(),
827891
xnn_status_to_string(status));
828892

893+
if (needs_bf16_output_convert) {
894+
xnn_status cs = xnn_define_convert(
895+
subgraph_ptr, fc_compute_output, fc_output, /*flags=*/0);
896+
ET_CHECK_OR_RETURN_ERROR(
897+
cs == xnn_status_success,
898+
Internal,
899+
"Failed to define bf16 output convert for linear node %i: %s",
900+
node->debug_handle(),
901+
xnn_status_to_string(cs));
902+
}
903+
829904
return Error::Ok;
830905
};
831906

backends/xnnpack/test/ops/test_linear.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@
4646
torchao_installed = False
4747

4848

49+
def is_fbcode() -> bool:
50+
# torch.version.git_version is only set in OSS PyTorch; the internal
51+
# Buck-built torch omits it.
52+
return not hasattr(torch.version, "git_version")
53+
54+
4955
# Pytorch Modules Used for Testing
5056
class BaseLinear(torch.nn.Module):
5157
def __init__(
@@ -64,7 +70,11 @@ def __init__(
6470
self.ic = input_channels
6571
self.oc = output_channels
6672

67-
assert dtype in [torch.float, torch.half], "Unsupported op dtype"
73+
assert dtype in [
74+
torch.float,
75+
torch.half,
76+
torch.bfloat16,
77+
], "Unsupported op dtype"
6878
self.op_dtype = dtype
6979
self.in_size = in_size
7080

@@ -388,6 +398,7 @@ def _test_groupwise_dq_linear(
388398
num_linears: int = 1,
389399
atol: float = 5e-3, # TODO(T212995726): Investigate right atol for rand[n] inputs
390400
rtol: float = 5e-3, # TODO(T212995726): Investigate right rtol for rand[n] inputs
401+
enable_bf16: bool = False,
391402
):
392403
"""
393404
Helper function to test groupwise dynamic quantized linear op with different configurations.
@@ -404,6 +415,7 @@ def _test_groupwise_dq_linear(
404415
DynamicallyQuantizedPartitioner = XnnpackPartitioner(
405416
config_precisions=ConfigPrecisionType.DYNAMIC_QUANT,
406417
per_op_mode=True,
418+
enable_bf16=enable_bf16,
407419
)
408420
tester = (
409421
Tester(mod, inputs)
@@ -706,11 +718,22 @@ def _test_qd8_per_token_weight_per_channel_group_int4(
706718
# Mean: 0.2373046875, 0.237060546875
707719
# Max: 1.0078125, 1.0078125
708720
# Min: -0.08465576171875, -0.08441162109375
709-
atol = (
710-
1e-2 if dtype == torch.half else 5e-3
711-
) # TODO(T212995726): Investigate right atol for rand[n] inputs
721+
# bf16 has ~8x coarser mantissa than fp16, so it needs a
722+
# looser atol.
723+
# TODO(T212995726): Investigate right atol for rand[n] inputs
724+
if dtype == torch.bfloat16:
725+
atol = 8e-2
726+
elif dtype == torch.half:
727+
atol = 1e-2
728+
else:
729+
atol = 5e-3
712730
self._test_groupwise_dq_linear(
713-
lin_mod, inputs, group_size=bl, use_bias=use_bias, atol=atol
731+
lin_mod,
732+
inputs,
733+
group_size=bl,
734+
use_bias=use_bias,
735+
atol=atol,
736+
enable_bf16=dtype == torch.bfloat16,
714737
)
715738

716739
def test_fp16_linear(self):
@@ -839,6 +862,17 @@ def test_linear_qd8_f16_per_token_weight_per_channel_group_int4(self):
839862
def test_linear_qd8_f32_per_token_weight_per_channel_group_int4(self):
840863
self._test_qd8_per_token_weight_per_channel_group_int4(dtype=torch.float)
841864

865+
# Tests for q[dp]8-bf16-qb4w
866+
@unittest.skipIf(
867+
not torchao_installed, "Per Channel Group Quantization Required TorchAO"
868+
)
869+
@unittest.skipIf(
870+
is_fbcode(),
871+
"wait for XNNPACK pin bump to enable",
872+
)
873+
def test_linear_qd8_bf16_per_token_weight_per_channel_group_int4(self):
874+
self._test_qd8_per_token_weight_per_channel_group_int4(dtype=torch.bfloat16)
875+
842876
@unittest.skipIf(
843877
not torchao_installed, "Per Channel Group Quantization Required TorchAO"
844878
)

0 commit comments

Comments
 (0)