Consider the following example.
#lhs = affine_map<(b, m, n, k) -> (b, m, k)>
#rhs = affine_map<(b, m, n, k) -> (b, k, n)>
#out = affine_map<(b, m, n, k) -> (b, m, n)>
func.func @mixed_bmm(%A: tensor<1x8x16xf32>, %B: tensor<1x16x8xf16>,
%C: tensor<1x8x8xf32>) -> tensor<1x8x8xf32> {
%0 = linalg.batch_matmul ins(%A, %B : tensor<1x8x16xf32>, tensor<1x16x8xf16>)
outs(%C : tensor<1x8x8xf32>) -> tensor<1x8x8xf32>
return %0 : tensor<1x8x8xf32>
}
func.func @mixed_generic(%A: tensor<1x8x16xf32>, %B: tensor<1x16x8xf16>,
%C: tensor<1x8x8xf32>) -> tensor<1x8x8xf32> {
%0 = linalg.generic {indexing_maps = [#lhs, #rhs, #out],
iterator_types = ["parallel", "parallel", "parallel", "reduction"]}
ins(%A, %B : tensor<1x8x16xf32>, tensor<1x16x8xf16>)
outs(%C : tensor<1x8x8xf32>) {
^bb0(%a: f32, %b: f16, %acc: f32):
%narrow = arith.truncf %a : f32 to f16
%ae = arith.extf %narrow : f16 to f32
%be = arith.extf %b : f16 to f32
%mul = arith.mulf %ae, %be : f32
%add = arith.addf %acc, %mul : f32
linalg.yield %add : f32
} -> tensor<1x8x8xf32>
return %0 : tensor<1x8x8xf32>
}
module attributes {transform.with_named_sequence} {
transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
%f = transform.structured.match ops{["func.func"]} in %root
: (!transform.any_op) -> !transform.any_op
%vf = transform.structured.vectorize_children_and_apply_patterns %f
{ fold_type_extensions_into_contract } : (!transform.any_op) -> !transform.any_op
transform.yield
}
}
the outputs for two cases are different with fold_type_extensions_into_contract enabled.
func.func @mixed_bmm(%arg0: tensor<1x8x16xf32>, %arg1: tensor<1x16x8xf16>, %arg2: tensor<1x8x8xf32>) -> tensor<1x8x8xf32> {
%0 = ub.poison : f16
%c0 = arith.constant 0 : index
%1 = ub.poison : f32
%2 = vector.transfer_read %arg0[%c0, %c0, %c0], %1 {in_bounds = [true, true, true]} : tensor<1x8x16xf32>, vector<1x8x16xf32>
%3 = vector.transfer_read %arg1[%c0, %c0, %c0], %0 {in_bounds = [true, true, true]} : tensor<1x16x8xf16>, vector<1x16x8xf16>
%4 = vector.transfer_read %arg2[%c0, %c0, %c0], %1 {in_bounds = [true, true, true]} : tensor<1x8x8xf32>, vector<1x8x8xf32>
%5 = arith.extf %3 : vector<1x16x8xf16> to vector<1x16x8xf32>
%6 = vector.contract {indexing_maps = [#map, #map1, #map2], iterator_types = ["parallel", "parallel", "parallel", "reduction"], kind = #vector.kind<add>} %2, %5, %4 : vector<1x8x16xf32>, vector<1x16x8xf32> into vector<1x8x8xf32>
%7 = vector.transfer_write %6, %arg2[%c0, %c0, %c0] {in_bounds = [true, true, true]} : vector<1x8x8xf32>, tensor<1x8x8xf32>
return %7 : tensor<1x8x8xf32>
}
func.func @mixed_generic(%arg0: tensor<1x8x16xf32>, %arg1: tensor<1x16x8xf16>, %arg2: tensor<1x8x8xf32>) -> tensor<1x8x8xf32> {
%0 = ub.poison : f16
%c0 = arith.constant 0 : index
%1 = ub.poison : f32
%2 = vector.transfer_read %arg0[%c0, %c0, %c0], %1 {in_bounds = [true, true, true]} : tensor<1x8x16xf32>, vector<1x8x16xf32>
%3 = vector.transfer_read %arg1[%c0, %c0, %c0], %0 {in_bounds = [true, true, true]} : tensor<1x16x8xf16>, vector<1x16x8xf16>
%4 = vector.transfer_read %arg2[%c0, %c0, %c0], %1 {in_bounds = [true, true, true]} : tensor<1x8x8xf32>, vector<1x8x8xf32>
%5 = arith.truncf %2 : vector<1x8x16xf32> to vector<1x8x16xf16>
%6 = vector.contract {indexing_maps = [#map, #map1, #map2], iterator_types = ["parallel", "parallel", "parallel", "reduction"], kind = #vector.kind<add>} %5, %3, %4 : vector<1x8x16xf16>, vector<1x16x8xf16> into vector<1x8x8xf32>
%7 = vector.transfer_write %6, %arg2[%c0, %c0, %c0] {in_bounds = [true, true, true]} : vector<1x8x8xf32>, tensor<1x8x8xf32>
return %7 : tensor<1x8x8xf32>
}
In BMM case we see upcast but for generic case we see the low-precision contract as expected. I understand that current implementation of fold_type_extensions_into_contract only fires if both A and B are coming from arith.extf, but shouldn’t these two show consistent behavior?
cc @rengolin