[LLVMCPU] Use native bf16 converts when the target supports them (#24759)

## Summary

The bf16 arithmetic is getting promoted to f32 in codegen, which is
correct for every CPU (there's no CPU with native bf16 arithmetic yet,
RISC-V `Zvfbfmin` provides conversions only). The conversions around
that promotion, however, are always expanded in software. That prevents
targets that do have bf16 <-> f32 conversion instructions from selecting
them, costing ~6 integer ops per narrow and 2 per widen on every bf16
load and store.

Introduce another LLVMCPULoweringPipeline option: it's set to true on
RISC-V targets with both `Zfbfmin` and `Zvfbfmin` extensions. Both are
required, since with only the vector extension scalar bf16 residue
lowers to `__truncsfbf2` libcalls. bf16 loads/stores and the promotion's
extf/truncf then select as `vfwcvtbf16/vfncvtbf16` (vector) and
`fcvt.s.bf16/fcvt.bf16.s` (scalar). This option is passed to
`ConvertUnsupportedFloatToIntBuffers` and `arith-expand` passes.

## Testing

Measured on a SpaceMiT K3 (X100 cores) (yay, i have access to one now).

Tested on kernels: 1M-element bf16 kernels, native vs current expansion,
outputs bit-identical in every case:

| kernel | native | expanded | speedup |
| :-- | --: | --: | --: |
| elementwise add | 0.800 ms | 1.45 ms | 1.81× |
| f32 to bf16 cast | 0.650 ms | 1.01 ms | 1.55× |
| sigmoid-weighted elementwise chain | 7.83 ms | 8.31 ms | 1.06× |
| bf16 to f32 cast | 0.494 ms | 0.538 ms | 1.09× |

Also tested on whole models: a bf16 `whisper-tiny-en` encoder-decoder
runs in **3.580 s vs 3.936 s (1.10×)** with bit-identical output against
PyTorch reference. A small CLIP-style bf16 model compiles to a 7.5%
smaller vmfb.

---------

Signed-off-by: Zmicier Prybysh <zprybysh@baylibre.com>
diff --git a/compiler/plugins/target/LLVMCPU/LLVMCPUTarget.cpp b/compiler/plugins/target/LLVMCPU/LLVMCPUTarget.cpp
index fda3035..9a8ec2e 100644
--- a/compiler/plugins/target/LLVMCPU/LLVMCPUTarget.cpp
+++ b/compiler/plugins/target/LLVMCPU/LLVMCPUTarget.cpp
@@ -259,10 +259,14 @@
 
   void buildTranslationPassPipeline(IREE::HAL::ExecutableTargetAttr targetAttr,
                                     OpPassManager &passManager) final {
-    bool enableAArch64SME = isAArch64(targetAttr.getConfiguration()) &&
-                            hasSMEFeature(targetAttr.getConfiguration());
-    buildLLVMCPUCodegenPassPipeline(passManager.nest<ModuleOp>(),
-                                    codegenOptions_, enableAArch64SME);
+    DictionaryAttr config = targetAttr.getConfiguration();
+    LLVMCPUPipelineOptions pipelineOpts;
+    pipelineOpts.cpuOpts = codegenOptions_;
+    pipelineOpts.enableAArch64SME = isAArch64(config) && hasSMEFeature(config);
+    pipelineOpts.enableNativeBf16Converts = isRISCV(config) &&
+                                            hasZfbfminFeature(config) &&
+                                            hasZvfbfminFeature(config);
+    buildLLVMCPUCodegenPassPipeline(passManager.nest<ModuleOp>(), pipelineOpts);
     buildCodegenTranslationPostProcessingPassPipeline(passManager);
   }
 
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPULowerExecutableTarget.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPULowerExecutableTarget.cpp
index edb73bd..d445c8a 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPULowerExecutableTarget.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPULowerExecutableTarget.cpp
@@ -168,6 +168,9 @@
       isAArch64(targetConfig) && hasSMEFeature(targetConfig);
   pipelineOpts.enableAArch64I8mm =
       isAArch64(targetConfig) && hasI8mmFeature(targetConfig);
+  pipelineOpts.enableNativeBf16Converts = isRISCV(targetConfig) &&
+                                          hasZfbfminFeature(targetConfig) &&
+                                          hasZvfbfminFeature(targetConfig);
   pipelineOpts.enablePeeling = isOptEnabled(funcOp, getEnableLoopPeelingStr());
 
   LoweringConfigAttrInterface loweringConfig = getRootLoweringConfig(funcOp);
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.cpp
index 91fa67f..9984e0c 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.cpp
@@ -483,8 +483,8 @@
 }
 
 static void addLowerToLLVMPasses(OpPassManager &modulePassManager,
-                                 bool enableAArch64SME,
-                                 const CPUCodegenOptions &cpuOpts) {
+                                 const LLVMCPUPipelineOptions &pipelineOpts) {
+  const CPUCodegenOptions &cpuOpts = pipelineOpts.cpuOpts;
   // TODO: Remove the following pass and plumb support for #hal.descriptor_type
   // memory space through the stack.
   FunctionLikeNest(modulePassManager)
@@ -502,12 +502,13 @@
                          createLLVMCPUEmitVectorizationRemarksPass)
       .addPass(createConvertLinalgToLoopsPass)
       .addPass(createConvertBf16ArithToF32Pass)
-      .addPass([]() {
-        // Convert bf16 buffers to i16. LLVM IR supports fp8 types
-        // natively, so we don't need to convert them here.
+      .addPass([&]() {
+        // Convert bf16 buffers to i16, unless the target has native bf16
+        // converts. LLVM IR supports fp8 types natively, so we don't need
+        // to convert them here.
         return createConvertUnsupportedFloatToIntBuffersPass(
             ConvertUnsupportedFloatToIntBuffersPassOptions{
-                /*includeBf16=*/true,
+                /*includeBf16=*/!pipelineOpts.enableNativeBf16Converts,
                 /*includeF8E5M2=*/false,
                 /*includeF8E4M3FN=*/false,
                 /*includeF8E5M2FNUZ=*/false,
@@ -532,7 +533,7 @@
       .addPredicatedPass(cpuOpts.useFastMinMaxOps,
                          createReplaceSlowMinMaxOpsPass);
 
-  if (enableAArch64SME) {
+  if (pipelineOpts.enableAArch64SME) {
     modulePassManager.addPass(mlir::arm_sme::createVectorLegalizationPass());
     FunctionLikeNest(modulePassManager)
         .addPredicatedPass(
@@ -563,7 +564,7 @@
   }
 
   VectorTransferLoweringPassOptions transferLoweringOptions;
-  if (!enableAArch64SME) {
+  if (!pipelineOpts.enableAArch64SME) {
     // The ArmSME dialect has its own (more specific) lowerings for scalable
     // vectors that occur later in the pipeline, so only enable the general
     // lowerings if SME is not available.
@@ -598,7 +599,9 @@
       .addPass(createIREEAffineExpandIndexOpsPass)
       .addPass([&]() {
         arith::ArithExpandOpsPassOptions options;
-        options.includeBf16 = true;
+        // Keep bf16 extf/truncf intact when the target lowers them to native
+        // conversion instructions.
+        options.includeBf16 = !pipelineOpts.enableNativeBf16Converts;
         options.includeF8E8M0 = true;
         return arith::createArithExpandOpsPass(options);
       })
@@ -609,7 +612,7 @@
       .addPredicatedPass(cpuOpts.instrumentMemoryAccesses,
                          createInstrumentMemoryAccessesPass);
 
-  if (enableAArch64SME) {
+  if (pipelineOpts.enableAArch64SME) {
     FunctionLikeNest(modulePassManager).addPass([&] {
       return createConvertArmSMEToLLVMPass();
     });
@@ -682,14 +685,13 @@
 }
 
 void buildLLVMCPUCodegenPassPipeline(OpPassManager &modulePassManager,
-                                     const CPUCodegenOptions &cpuOpts,
-                                     bool enableAArch64SME,
+                                     const LLVMCPUPipelineOptions &pipelineOpts,
                                      bool includeLLVMLowering) {
   modulePassManager.addPass(createLowerExecutableUsingTransformDialectPass());
   FunctionLikeNest(modulePassManager)
       .addPass([&]() {
         return createLLVMCPULowerExecutableTargetPass(
-            LLVMCPULowerExecutableTargetPassOptions{cpuOpts});
+            LLVMCPULowerExecutableTargetPassOptions{pipelineOpts.cpuOpts});
       })
       .addPass(createVerifyWorkgroupDistributionPass);
   if (clPatchFuncOps) {
@@ -702,7 +704,7 @@
   modulePassManager.addPass(IREE::Util::createDropCompilerHintsPass());
 
   if (includeLLVMLowering) {
-    addLowerToLLVMPasses(modulePassManager, enableAArch64SME, cpuOpts);
+    addLowerToLLVMPasses(modulePassManager, pipelineOpts);
   }
   LLVM_DEBUG({
     llvm::dbgs() << "LLVMCPU codegen pass pipeline:\n";
@@ -847,6 +849,11 @@
     Option<bool> enableArmSME{
         *this, "enable-arm-sme",
         llvm::cl::desc("Enable the ArmSME lowering pipeline.")};
+    Option<bool> enableNativeBf16Converts{
+        *this, "enable-native-bf16-converts",
+        llvm::cl::desc("Assume the target has hardware bf16 <-> f32 converts "
+                       "and keep bf16 in the IR instead of emulating it."),
+        llvm::cl::init(false)};
     Option<bool> includeLLVMLowering{
         *this, "include-llvm-lowering",
         llvm::cl::desc("Include the lowering to LLVM dialect."),
@@ -859,10 +866,13 @@
           "Runs the LLVMCPU lowering pipeline",
           [](OpPassManager &modulePassManager,
              LLVMCPULoweringPipelineOptions const &options) {
-            CPUCodegenOptions cpuOpts =
+            LLVMCPUPipelineOptions pipelineOpts;
+            pipelineOpts.cpuOpts =
                 getCPUCodegenOptionsForTextualPipeline(options);
-            buildLLVMCPUCodegenPassPipeline(modulePassManager, cpuOpts,
-                                            options.enableArmSME,
+            pipelineOpts.enableAArch64SME = options.enableArmSME;
+            pipelineOpts.enableNativeBf16Converts =
+                options.enableNativeBf16Converts;
+            buildLLVMCPUCodegenPassPipeline(modulePassManager, pipelineOpts,
                                             options.includeLLVMLowering);
           });
 
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.h b/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.h
index 5e71246..e08c1a9 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.h
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.h
@@ -85,6 +85,7 @@
   bool enableVectorMasking = false;
   bool enableAArch64SME = false;
   bool enableAArch64I8mm = false;
+  bool enableNativeBf16Converts = false;
   bool lowerToAVX2 = false;
 };
 
@@ -146,8 +147,7 @@
 /// to LLVM dialect via the structured ops path. The `modulePassManager`
 /// should operate on the module within the IREE::HAL::ExecutableOp.
 void buildLLVMCPUCodegenPassPipeline(OpPassManager &modulePassManager,
-                                     const CPUCodegenOptions &codegenOptions,
-                                     bool enableAArch64SME = false,
+                                     const LLVMCPUPipelineOptions &pipelineOpts,
                                      bool includeLLVMLowering = true);
 
 //----------------------------------------------------------------------------//
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/Utils.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/Utils.cpp
index 7d1c3b2..d76f7c7 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/Utils.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/Utils.cpp
@@ -89,6 +89,14 @@
          hasFeature(targetConfig, "+zve64d");
 }
 
+bool hasZfbfminFeature(DictionaryAttr targetConfig) {
+  return hasFeature(targetConfig, "+zfbfmin");
+}
+
+bool hasZvfbfminFeature(DictionaryAttr targetConfig) {
+  return hasFeature(targetConfig, "+zvfbfmin");
+}
+
 bool hasSMEFeature(DictionaryAttr targetConfig) {
   return hasFeature(targetConfig, "+sme");
 }
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/Utils.h b/compiler/src/iree/compiler/Codegen/LLVMCPU/Utils.h
index 2840d0d..7cbdd63 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/Utils.h
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/Utils.h
@@ -49,6 +49,12 @@
 /// cpu features.
 bool hasAnyVFeature(DictionaryAttr targetConfig);
 
+/// Returns true if the 'targetAttr' contains '+zfbfmin' in its cpu features.
+bool hasZfbfminFeature(DictionaryAttr targetConfig);
+
+/// Returns true if the 'targetAttr' contains '+zvfbfmin' in its cpu features.
+bool hasZvfbfminFeature(DictionaryAttr targetConfig);
+
 /// Returns true if the 'targetAttr' contains '+sme' in its cpu features.
 bool hasSMEFeature(DictionaryAttr targetConfig);
 
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/BUILD.bazel b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/BUILD.bazel
index 7bbe656..837115d 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/BUILD.bazel
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/BUILD.bazel
@@ -40,6 +40,7 @@
             "lowering_strategy_from_tuning_spec.mlir",
             "peel.mlir",
             "pipeline_arm_sme_streaming_mode_tests.mlir",
+            "pipeline_bf16_native_converts_tests.mlir",
             "pipeline_disable_distribution_tests.mlir",
             "pipeline_full_smoketests.mlir",
             "pipeline_gather_tests.mlir",
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/CMakeLists.txt b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/CMakeLists.txt
index 25afeb8..8d75ee9 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/CMakeLists.txt
@@ -35,6 +35,7 @@
     "lowering_strategy_from_tuning_spec.mlir"
     "peel.mlir"
     "pipeline_arm_sme_streaming_mode_tests.mlir"
+    "pipeline_bf16_native_converts_tests.mlir"
     "pipeline_disable_distribution_tests.mlir"
     "pipeline_full_smoketests.mlir"
     "pipeline_gather_tests.mlir"
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/pipeline_bf16_native_converts_tests.mlir b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/pipeline_bf16_native_converts_tests.mlir
new file mode 100644
index 0000000..25aeb5a
--- /dev/null
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/pipeline_bf16_native_converts_tests.mlir
@@ -0,0 +1,96 @@
+// RUN: iree-opt --iree-codegen-llvmcpu-configuration-pipeline --iree-codegen-llvmcpu-lowering-pipeline='enable-native-bf16-converts=true' --split-input-file %s | FileCheck %s --check-prefixes=COMMON,NATIVE
+// RUN: iree-opt --iree-codegen-llvmcpu-configuration-pipeline --iree-codegen-llvmcpu-lowering-pipeline --split-input-file %s | FileCheck %s --check-prefixes=COMMON,EMULATED
+
+// Verifies the enable-native-bf16-converts pipeline option. The target backend
+// derives this option from the target's cpu features (Zfbfmin + Zvfbfmin on
+// RISC-V). Arithmetic is promoted to f32 either way, there is no non-widening
+// bf16 arithmetic (the only bf16-input arithmetic instruction, Zvfbfwma's
+// `vfwmaccbf16`, widens to f32). What the option controls is the conversions
+// around the promotion. When it's set, bf16 storage is kept and the promotion's
+// extf/truncf survive to the LLVM dialect as fpext/fptrunc, which select as
+// native conversion instructions, otherwise bf16 storage becomes i16 and the
+// conversions are expanded into shift/round-bias integer sequences.
+
+#pipeline_layout = #hal.pipeline.layout<bindings = [
+  #hal.pipeline.binding<storage_buffer>,
+  #hal.pipeline.binding<storage_buffer>,
+  #hal.pipeline.binding<storage_buffer>
+]>
+
+#executable_target_embedded_elf_riscv_64_ = #hal.executable.target<"llvm-cpu", "embedded-elf-riscv_64", {cpu_features = "+m,+a,+f,+d,+c,+v,+zfbfmin,+zvfbfmin", data_layout = "e-m:e-p:64:64-i64:64-i128:128-n32:64-S128", native_vector_size = 16 : index, target_triple = "riscv64-unknown-unknown-eabi-elf"}>
+builtin.module {
+  func.func @bf16_add() attributes {hal.executable.target = #executable_target_embedded_elf_riscv_64_} {
+    %0 = hal.interface.binding.subspan layout(#pipeline_layout) binding(0) : !iree_tensor_ext.dispatch.tensor<readonly:tensor<1024xbf16>>
+    %1 = hal.interface.binding.subspan layout(#pipeline_layout) binding(1) : !iree_tensor_ext.dispatch.tensor<readonly:tensor<1024xbf16>>
+    %2 = hal.interface.binding.subspan layout(#pipeline_layout) binding(2) : !iree_tensor_ext.dispatch.tensor<writeonly:tensor<1024xbf16>>
+    %lhs = iree_tensor_ext.dispatch.tensor.load %0, offsets = [0], sizes = [1024], strides = [1] : !iree_tensor_ext.dispatch.tensor<readonly:tensor<1024xbf16>> -> tensor<1024xbf16>
+    %rhs = iree_tensor_ext.dispatch.tensor.load %1, offsets = [0], sizes = [1024], strides = [1] : !iree_tensor_ext.dispatch.tensor<readonly:tensor<1024xbf16>> -> tensor<1024xbf16>
+    %init = tensor.empty() : tensor<1024xbf16>
+    %add = linalg.add ins(%lhs, %rhs : tensor<1024xbf16>, tensor<1024xbf16>) outs(%init : tensor<1024xbf16>) -> tensor<1024xbf16>
+    iree_tensor_ext.dispatch.tensor.store %add, %2, offsets = [0], sizes = [1024], strides = [1] : tensor<1024xbf16> -> !iree_tensor_ext.dispatch.tensor<writeonly:tensor<1024xbf16>>
+    return
+  }
+}
+// Native bf16 add: bf16 loads, fpext/fptrunc around the add, then bf16 store.
+// Emulated: the add is performed in f32 on values reconstructed from i16
+// storage, and the result is rounded back with the shift/bias sequence.
+// COMMON-LABEL: llvm.func @bf16_add
+//      NATIVE:    %[[LHS:.+]] = llvm.load {{.*}} -> bf16
+//      NATIVE:    %[[LHSF:.+]] = llvm.fpext %[[LHS]] : bf16 to f32
+//      NATIVE:    %[[RHS:.+]] = llvm.load {{.*}} -> bf16
+//      NATIVE:    %[[RHSF:.+]] = llvm.fpext %[[RHS]] : bf16 to f32
+//    EMULATED:    %[[LHS:.+]] = llvm.load {{.*}} -> i16
+//    EMULATED:    %[[LHSZ:.+]] = llvm.zext %[[LHS]] : i16 to i32
+//    EMULATED:    %[[LHSS:.+]] = llvm.shl %[[LHSZ]], {{.*}} : i32
+//    EMULATED:    %[[LHSF:.+]] = llvm.bitcast %[[LHSS]] : i32 to f32
+//    EMULATED:    %[[RHS:.+]] = llvm.load {{.*}} -> i16
+//    EMULATED:    %[[RHSZ:.+]] = llvm.zext %[[RHS]] : i16 to i32
+//    EMULATED:    %[[RHSS:.+]] = llvm.shl %[[RHSZ]], {{.*}} : i32
+//    EMULATED:    %[[RHSF:.+]] = llvm.bitcast %[[RHSS]] : i32 to f32
+//      COMMON:    %[[SUM:.+]] = llvm.fadd %[[LHSF]], %[[RHSF]] {{.*}} : f32
+//      NATIVE:    %[[RES:.+]] = llvm.fptrunc %[[SUM]] : f32 to bf16
+//      NATIVE:    llvm.store %[[RES]], {{.*}} : bf16
+//  NATIVE-NOT:    llvm.lshr
+//  NATIVE-NOT:    llvm.shl
+//    EMULATED:    llvm.fcmp "une" %[[SUM]], %[[SUM]]
+//    EMULATED:    llvm.lshr
+//    EMULATED:    llvm.store {{.*}} i16
+
+// -----
+
+#pipeline_layout = #hal.pipeline.layout<bindings = [
+  #hal.pipeline.binding<storage_buffer>,
+  #hal.pipeline.binding<storage_buffer>
+]>
+
+#executable_target_embedded_elf_riscv_64_ = #hal.executable.target<"llvm-cpu", "embedded-elf-riscv_64", {cpu_features = "+m,+a,+f,+d,+c,+v,+zfbfmin,+zvfbfmin", data_layout = "e-m:e-p:64:64-i64:64-i128:128-n32:64-S128", native_vector_size = 16 : index, target_triple = "riscv64-unknown-unknown-eabi-elf"}>
+builtin.module {
+  func.func @bf16_truncf() attributes {hal.executable.target = #executable_target_embedded_elf_riscv_64_} {
+    %0 = hal.interface.binding.subspan layout(#pipeline_layout) binding(0) : !iree_tensor_ext.dispatch.tensor<readonly:tensor<1024xf32>>
+    %1 = hal.interface.binding.subspan layout(#pipeline_layout) binding(1) : !iree_tensor_ext.dispatch.tensor<writeonly:tensor<1024xbf16>>
+    %in = iree_tensor_ext.dispatch.tensor.load %0, offsets = [0], sizes = [1024], strides = [1] : !iree_tensor_ext.dispatch.tensor<readonly:tensor<1024xf32>> -> tensor<1024xf32>
+    %init = tensor.empty() : tensor<1024xbf16>
+    %trunc = linalg.generic {
+        indexing_maps = [affine_map<(d0) -> (d0)>, affine_map<(d0) -> (d0)>],
+        iterator_types = ["parallel"]}
+        ins(%in : tensor<1024xf32>) outs(%init : tensor<1024xbf16>) {
+      ^bb0(%a: f32, %out: bf16):
+        %t = arith.truncf %a : f32 to bf16
+        linalg.yield %t : bf16
+    } -> tensor<1024xbf16>
+    iree_tensor_ext.dispatch.tensor.store %trunc, %1, offsets = [0], sizes = [1024], strides = [1] : tensor<1024xbf16> -> !iree_tensor_ext.dispatch.tensor<writeonly:tensor<1024xbf16>>
+    return
+  }
+}
+// Both paths load the f32 input the same way. Native: the cast stays a single
+// fptrunc (selectable as one narrowing convert) and stores bf16. Emulated: the
+// round-to-nearest-even shift/bias sequence, storing i16.
+// COMMON-LABEL: llvm.func @bf16_truncf
+//        COMMON:   %[[IN:.+]] = llvm.load {{.*}} -> vector<{{[0-9]+}}xf32>
+//        NATIVE:   %[[RES:.+]] = llvm.fptrunc %[[IN]] : vector<{{[0-9]+}}xf32> to vector<{{[0-9]+}}xbf16>
+//        NATIVE:   llvm.store %[[RES]], {{.*}} : vector<{{[0-9]+}}xbf16>
+//    NATIVE-NOT:   llvm.lshr
+//  EMULATED-NOT:   llvm.fptrunc {{.*}} to vector<{{[0-9]+}}xbf16>
+//      EMULATED:   llvm.fcmp "une" %[[IN]], %[[IN]]
+//      EMULATED:   llvm.lshr
+//      EMULATED:   llvm.store {{.*}} vector<{{[0-9]+}}xi16>