[stablehlo] Implement product of householder reflectors (#15555)

The stablehlo.custom call for product of householder reflectors can be
directly generated via multiple operations. Implemented the non-blocked
variant as a lowering directly to `linalg`. Testing we have a simplified
lit test as the algorithm is not friendly for lit tests. Instead we use a
numerical validation in stablehlo_ops e2e tests.
diff --git a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/BUILD.bazel b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/BUILD.bazel
index 5da79e9..22fcb46 100644
--- a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/BUILD.bazel
+++ b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/BUILD.bazel
@@ -72,6 +72,7 @@
         "LegalizeToLinalgUtils.cpp",
         "LegalizeToLinalgUtils.h",
         "MapStableHLOToScalarOp.h",
+        "StableHLOCustomCalls.cpp",
         "StableHLOToArith.cpp",
         "StableHLOToIREEInputDialects.cpp",
         "StableHLOToLinalg.cpp",
diff --git a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/CMakeLists.txt b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/CMakeLists.txt
index f615ae2..5cf31a3 100644
--- a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/CMakeLists.txt
+++ b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/CMakeLists.txt
@@ -56,6 +56,7 @@
     "LegalizeToLinalgUtils.cpp"
     "LegalizeToLinalgUtils.h"
     "MapStableHLOToScalarOp.h"
+    "StableHLOCustomCalls.cpp"
     "StableHLOToArith.cpp"
     "StableHLOToIREEInputDialects.cpp"
     "StableHLOToLinalg.cpp"
diff --git a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Passes.cpp b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Passes.cpp
index d735622..34c61da 100644
--- a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Passes.cpp
+++ b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Passes.cpp
@@ -42,6 +42,7 @@
   passManager.addNestedPass<func::FuncOp>(mlir::createCanonicalizerPass());
   passManager.addNestedPass<func::FuncOp>(createStableHLOCanonicalize());
   passManager.addNestedPass<func::FuncOp>(mlir::createCSEPass());
+  passManager.addNestedPass<func::FuncOp>(createLegalizeStableHLOCustomCalls());
   passManager.addNestedPass<func::FuncOp>(
       stablehlo::createLegalizeControlFlow());
 
diff --git a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Passes.td b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Passes.td
index 4f19875..02dc578 100644
--- a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Passes.td
+++ b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Passes.td
@@ -51,6 +51,11 @@
   let summary = "Legalizes from CHLO ops flow to StableHLO and Shape ops";
 }
 
+def LegalizeStableHLOCustomCalls :
+    Pass<"iree-stablehlo-legalize-custom-calls", "func::FuncOp"> {
+  let summary = "Legalizes specialized custom calls to decomposed implementations";
+}
+
 def LegalizeShapeComputations :
     Pass<"iree-stablehlo-legalize-shape-computations", "func::FuncOp"> {
   let summary = "Legalizes StableHLO shape operations to core-mlir operations";
diff --git a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Preprocessing/StableHLOToStableHLO.cpp b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Preprocessing/StableHLOToStableHLO.cpp
index fff4726..7fb89d1 100644
--- a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Preprocessing/StableHLOToStableHLO.cpp
+++ b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/Preprocessing/StableHLOToStableHLO.cpp
@@ -1532,6 +1532,7 @@
   }
 };
 
+// TODO(suderman): Move this to solve code.
 struct CustomCallIsTopK final
     : OpRewritePattern<mlir::stablehlo::CustomCallOp> {
   using OpRewritePattern::OpRewritePattern;
@@ -1751,6 +1752,7 @@
   }
 };
 
+// TODO(suderman): Move this to solve code.
 struct ApproxTopK final : OpRewritePattern<mlir::stablehlo::CustomCallOp> {
   using OpRewritePattern::OpRewritePattern;
 
diff --git a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/StableHLOCustomCalls.cpp b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/StableHLOCustomCalls.cpp
new file mode 100644
index 0000000..3ad03ea
--- /dev/null
+++ b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/StableHLOCustomCalls.cpp
@@ -0,0 +1,247 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// Implements logic for lowering CHLO ops to StableHLO and Shape dialect ops,
+// taking care of CHLO's broadcasting semantics
+
+#include "llvm/ADT/STLExtras.h"
+#include "mlir/Dialect/Arith/IR/Arith.h"
+#include "mlir/Dialect/Func/IR/FuncOps.h"
+#include "mlir/Dialect/Linalg/IR/Linalg.h"
+#include "mlir/Dialect/SCF/IR/SCF.h"
+#include "mlir/Dialect/Tensor/IR/Tensor.h"
+#include "mlir/IR/BuiltinTypes.h"
+#include "mlir/IR/ImplicitLocOpBuilder.h"
+#include "mlir/IR/TypeUtilities.h"
+#include "mlir/Support/LogicalResult.h"
+#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
+#include "stablehlo-iree/Conversion/Passes.h"
+#include "stablehlo-iree/Conversion/Preprocessing/Rewriters.h"
+#include "stablehlo-iree/Conversion/Rewriters.h"
+#include "stablehlo/dialect/BroadcastUtils.h"
+#include "stablehlo/dialect/StablehloOps.h"
+
+#include <algorithm>
+
+namespace mlir::iree_compiler::stablehlo {
+
+#define GEN_PASS_DEF_LEGALIZESTABLEHLOCUSTOMCALLS
+#include "stablehlo-iree/Conversion/Passes.h.inc"
+
+namespace {
+
+// Computes householder using vector `v` and a `tau` matrice
+// with the k-th element. See householder transformation as below:
+// https://en.wikipedia.org/wiki/Householder_transformation
+static Value computeHouseholder(Value v, Value tau, Value k,
+                                ImplicitLocOpBuilder b) {
+  auto vTy = cast<ShapedType>(v.getType());
+
+  SmallVector<int64_t> hShape(vTy.getShape());
+  hShape.push_back(hShape.back());
+
+  auto hTy = RankedTensorType::get(hShape, vTy.getElementType());
+  Value empty = b.create<tensor::EmptyOp>(hShape, vTy.getElementType());
+
+  auto outMap = b.getMultiDimIdentityMap(hShape.size());
+
+  SmallVector<AffineExpr> exprs;
+  for (int i = 0, s = vTy.getRank(); i < s; ++i) {
+    exprs.push_back(b.getAffineDimExpr(i));
+  }
+
+  AffineMap vMap = AffineMap::get(hTy.getRank(), 0, exprs, b.getContext());
+
+  exprs.back() = b.getAffineDimExpr(vTy.getRank());
+  AffineMap vTMap = AffineMap::get(hTy.getRank(), 0, exprs, b.getContext());
+
+  SmallVector<AffineMap> affineMaps = {vMap, vTMap, outMap};
+
+  SmallVector<utils::IteratorType> iterTypes(hShape.size(),
+                                             utils::IteratorType::parallel);
+
+  Value zero = b.create<arith::ConstantOp>(b.getZeroAttr(vTy.getElementType()));
+  Value one =
+      b.create<arith::ConstantOp>(b.getFloatAttr(vTy.getElementType(), 1.0));
+
+  return b
+      .create<linalg::GenericOp>(
+          hTy, ValueRange{v, v}, empty, affineMaps, iterTypes,
+          [&](OpBuilder &bb, Location loc, ValueRange args) {
+            ImplicitLocOpBuilder b(loc, bb);
+            SmallVector<Value> indices;
+            for (int i = 0, s = hTy.getRank(); i < s; ++i) {
+              indices.push_back(b.create<linalg::IndexOp>(loc, i));
+            }
+
+            SmallVector<Value> tauIndices(indices.begin(), indices.end() - 2);
+            tauIndices.push_back(k);
+            Value t = b.create<tensor::ExtractOp>(tau, tauIndices);
+
+            // Generates the lower triangularization of the matrix with
+            // one values on the diagonal.
+            auto tri = [&](Value v, Value i) {
+              Value eq =
+                  b.create<arith::CmpIOp>(arith::CmpIPredicate::eq, i, k);
+              Value lt =
+                  b.create<arith::CmpIOp>(arith::CmpIPredicate::ult, i, k);
+              Value sel = b.create<arith::SelectOp>(eq, one, v);
+              return b.create<arith::SelectOp>(lt, zero, sel);
+            };
+
+            Value v = tri(args[0], indices[indices.size() - 2]);
+            Value vT = tri(args[1], indices[indices.size() - 1]);
+
+            Value h = b.create<arith::MulFOp>(v, vT);
+            h = b.create<arith::MulFOp>(h, t);
+
+            Value isDiag = b.create<arith::CmpIOp>(arith::CmpIPredicate::eq,
+                                                   indices[indices.size() - 2],
+                                                   indices[indices.size() - 1]);
+            Value diag = b.create<arith::SelectOp>(isDiag, one, zero);
+            Value sub = b.create<arith::SubFOp>(diag, h);
+
+            b.create<linalg::YieldOp>(sub);
+          })
+      .getResult(0);
+}
+
+// Slices the k-th column of matrix and computes the householder transformation
+// for using the `tau` value.
+static Value computeHouseholderSlice(Value matrix, Value tau, Value k,
+                                     ImplicitLocOpBuilder b) {
+  auto matrixTy = cast<ShapedType>(matrix.getType());
+  int rank = matrixTy.getRank();
+
+  SmallVector<OpFoldResult> vStrides(rank, b.getIndexAttr(1));
+  SmallVector<int64_t> vShape(matrixTy.getShape());
+  vShape[vShape.size() - 1] = 1;
+
+  SmallVector<OpFoldResult> vOffsets(rank, b.getIndexAttr(0));
+  vOffsets[vOffsets.size() - 1] = k;
+
+  SmallVector<OpFoldResult> vSizes;
+  for (auto v : vShape) {
+    vSizes.push_back(b.getIndexAttr(v));
+  }
+
+  auto sliceTy = RankedTensorType::get(vShape, matrixTy.getElementType());
+  Value v = b.create<tensor::ExtractSliceOp>(sliceTy, matrix, vOffsets, vSizes,
+                                             vStrides);
+
+  SmallVector<ReassociationIndices> reass;
+  for (int i = 0; i < rank - 2; ++i) {
+    reass.push_back({i});
+  }
+  reass.push_back({rank - 2, rank - 1});
+
+  ArrayRef<int64_t> collapseVShape(vShape.begin(), vShape.end() - 1);
+  auto collapseVTy =
+      RankedTensorType::get(collapseVShape, matrixTy.getElementType());
+  Value collapseV = b.create<tensor::CollapseShapeOp>(collapseVTy, v, reass);
+
+  Value householder = computeHouseholder(collapseV, tau, k, b);
+  return householder;
+}
+
+struct HouseholderReflectorRewriter final
+    : OpRewritePattern<mlir::stablehlo::CustomCallOp> {
+  using OpRewritePattern<mlir::stablehlo::CustomCallOp>::OpRewritePattern;
+  using OpAdaptor = mlir::stablehlo::CustomCallOp::Adaptor;
+
+  LogicalResult matchAndRewrite(mlir::stablehlo::CustomCallOp op,
+                                PatternRewriter &rewriter) const final {
+    if (op.getCallTargetName() != "ProductOfElementaryHouseholderReflectors") {
+      return rewriter.notifyMatchFailure(
+          op, "not ProductOfElementaryHouseholderReflectors");
+    }
+
+    ImplicitLocOpBuilder b(op.getLoc(), rewriter);
+    auto matrix = op.getOperand(0);
+    auto tau = op.getOperand(1);
+    auto matrixTy = cast<ShapedType>(matrix.getType());
+    auto tauTy = cast<ShapedType>(tau.getType());
+    auto rank = matrixTy.getRank();
+
+    if (isa<ComplexType>(matrixTy.getElementType())) {
+      return rewriter.notifyMatchFailure(op, "complex types not supported");
+    }
+
+    if (rank < 2) {
+      return rewriter.notifyMatchFailure(op, "requires minimum rank 2 matrix");
+    }
+
+    // Implementation needs to be checked to work with variable dimension
+    // lengths. Should be relatively straightforward.
+    if (!matrixTy.hasStaticShape() || !tauTy.hasStaticShape()) {
+      return rewriter.notifyMatchFailure(op,
+                                         "not supported for dynamic shapes");
+    }
+
+    Value zero = b.create<arith::ConstantIndexOp>(0);
+    Value one = b.create<arith::ConstantIndexOp>(1);
+    Value k = b.create<arith::ConstantIndexOp>(tauTy.getShape().back());
+    Value householder0 = computeHouseholderSlice(matrix, tau, zero, b);
+    auto scf = b.create<scf::ForOp>(
+        one, k, one, ValueRange{householder0},
+        [&](OpBuilder &bb, Location loc, Value iv, ValueRange args) {
+          ImplicitLocOpBuilder b(loc, bb);
+          Value householder = computeHouseholderSlice(matrix, tau, iv, b);
+
+          std::vector<int64_t> batch(rank - 2);
+          for (int i = 0; i < rank - 2; ++i)
+            batch[i] = i;
+          std::vector<int64_t> lhsContract = {rank - 1};
+          std::vector<int64_t> rhsContract = {rank - 2};
+
+          auto dotNums = mlir::stablehlo::DotDimensionNumbersAttr::get(
+              b.getContext(), batch, batch, lhsContract, rhsContract);
+          Value dot = b.create<mlir::stablehlo::DotGeneralOp>(
+              householder0.getType(), args[0], householder, dotNums, nullptr);
+          b.create<scf::YieldOp>(loc, dot);
+        });
+
+    SmallVector<OpFoldResult> vOffsets(rank, b.getIndexAttr(0));
+    SmallVector<OpFoldResult> vStrides(rank, b.getIndexAttr(1));
+    SmallVector<int64_t> vShape(matrixTy.getShape());
+    SmallVector<OpFoldResult> vSizes;
+    for (auto v : vShape) {
+      vSizes.push_back(b.getIndexAttr(v));
+    }
+
+    auto sliceTy = RankedTensorType::get(vShape, matrixTy.getElementType());
+    Value v = b.create<tensor::ExtractSliceOp>(sliceTy, scf.getResult(0),
+                                               vOffsets, vSizes, vStrides);
+
+    rewriter.replaceOp(op, v);
+    return success();
+  }
+};
+
+//===----------------------------------------------------------------------===//
+// Pass Definition.
+//===----------------------------------------------------------------------===//
+
+struct LegalizeStableHLOCustomCalls final
+    : impl::LegalizeStableHLOCustomCallsBase<LegalizeStableHLOCustomCalls> {
+  void getDependentDialects(DialectRegistry &registry) const override {
+    registry.insert<arith::ArithDialect, linalg::LinalgDialect, scf::SCFDialect,
+                    mlir::stablehlo::StablehloDialect, tensor::TensorDialect>();
+  }
+
+  void runOnOperation() override {
+    func::FuncOp f = getOperation();
+    MLIRContext *ctx = f.getContext();
+
+    RewritePatternSet patterns(ctx);
+    patterns.add<HouseholderReflectorRewriter>(ctx);
+    if (failed(applyPatternsAndFoldGreedily(f, std::move(patterns)))) {
+      signalPassFailure();
+    }
+  }
+};
+} // namespace
+} // namespace mlir::iree_compiler::stablehlo
diff --git a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/test/BUILD.bazel b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/test/BUILD.bazel
index c0c0b41..c42d6f8 100644
--- a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/test/BUILD.bazel
+++ b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/test/BUILD.bazel
@@ -23,6 +23,7 @@
             "legalize_chlo_with_broadcast.mlir",
             "legalize_control_flow.mlir",
             "legalize_shape_computations.mlir",
+            "stablehlo_custom_calls.mlir",
             "stablehlo_to_iree_input_dialects.mlir",
             "stablehlo_to_linalg_convolution.mlir",
             "stablehlo_to_linalg_dot_prod.mlir",
diff --git a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/test/CMakeLists.txt b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/test/CMakeLists.txt
index 7400707..3faf84a 100644
--- a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/test/CMakeLists.txt
+++ b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/test/CMakeLists.txt
@@ -21,6 +21,7 @@
     "legalize_chlo_with_broadcast.mlir"
     "legalize_control_flow.mlir"
     "legalize_shape_computations.mlir"
+    "stablehlo_custom_calls.mlir"
     "stablehlo_to_iree_input_dialects.mlir"
     "stablehlo_to_linalg.mlir"
     "stablehlo_to_linalg_convolution.mlir"
diff --git a/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/test/stablehlo_custom_calls.mlir b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/test/stablehlo_custom_calls.mlir
new file mode 100644
index 0000000..b756500
--- /dev/null
+++ b/compiler/plugins/input/StableHLO/stablehlo-iree/Conversion/test/stablehlo_custom_calls.mlir
@@ -0,0 +1,12 @@
+// RUN: iree-opt --split-input-file --iree-stablehlo-legalize-custom-calls \
+// RUN:   --cse %s | FileCheck %s
+
+// CHECK-LABEL: @householder
+func.func public @householder(%arg0: tensor<4x3xf32>, %arg1: tensor<2xf32>) -> (tensor<4x3xf32>) {
+  // CHECK: linalg.generic
+  // CHECK: scf.for
+  // CHECK:   linalg.generic
+  // CHECK:   stablehlo.dot_general
+  %0 = stablehlo.custom_call @ProductOfElementaryHouseholderReflectors(%arg0, %arg1) : (tensor<4x3xf32>, tensor<2xf32>) -> tensor<4x3xf32>
+  return %0 : tensor<4x3xf32>
+}
diff --git a/tests/e2e/stablehlo_ops/BUILD.bazel b/tests/e2e/stablehlo_ops/BUILD.bazel
index 5b769cb..b7f5d4c 100644
--- a/tests/e2e/stablehlo_ops/BUILD.bazel
+++ b/tests/e2e/stablehlo_ops/BUILD.bazel
@@ -51,6 +51,7 @@
             "finite.mlir",
             "floor.mlir",
             "gather.mlir",
+            "householder.mlir",
             "iota.mlir",
             "log.mlir",
             "log_plus_one.mlir",
@@ -123,6 +124,7 @@
             "finite.mlir",
             "floor.mlir",
             "gather.mlir",
+            "householder.mlir",
             "iota.mlir",
             "log.mlir",
             "log_plus_one.mlir",
@@ -198,6 +200,7 @@
             "finite.mlir",
             "floor.mlir",
             "gather.mlir",
+            "householder.mlir",
             "iota.mlir",
             "log.mlir",
             "log_plus_one.mlir",
@@ -276,6 +279,7 @@
             "finite.mlir",
             "floor.mlir",
             "gather.mlir",
+            "householder.mlir",
             "iota.mlir",
             "log.mlir",
             "log_plus_one.mlir",
@@ -369,6 +373,7 @@
             "finite.mlir",
             "floor.mlir",
             "gather.mlir",
+            "householder.mlir",
             "iota.mlir",
             "log.mlir",
             "log_plus_one.mlir",
@@ -457,6 +462,7 @@
             "finite.mlir",
             "floor.mlir",
             "gather.mlir",
+            "householder.mlir",
             "iota.mlir",
             "log.mlir",
             "log_plus_one.mlir",
@@ -537,6 +543,7 @@
         "finite.mlir",
         "floor.mlir",
         "gather.mlir",
+        "householder.mlir",
         "iota.mlir",
         "log.mlir",
         "log_plus_one.mlir",
diff --git a/tests/e2e/stablehlo_ops/CMakeLists.txt b/tests/e2e/stablehlo_ops/CMakeLists.txt
index 714e2a8..1397a9d 100644
--- a/tests/e2e/stablehlo_ops/CMakeLists.txt
+++ b/tests/e2e/stablehlo_ops/CMakeLists.txt
@@ -42,6 +42,7 @@
     "finite.mlir"
     "floor.mlir"
     "gather.mlir"
+    "householder.mlir"
     "iota.mlir"
     "log.mlir"
     "log_plus_one.mlir"
@@ -112,6 +113,7 @@
     "finite.mlir"
     "floor.mlir"
     "gather.mlir"
+    "householder.mlir"
     "iota.mlir"
     "log.mlir"
     "log_plus_one.mlir"
@@ -182,6 +184,7 @@
     "finite.mlir"
     "floor.mlir"
     "gather.mlir"
+    "householder.mlir"
     "iota.mlir"
     "log.mlir"
     "log_plus_one.mlir"
@@ -253,6 +256,7 @@
     "finite.mlir"
     "floor.mlir"
     "gather.mlir"
+    "householder.mlir"
     "iota.mlir"
     "log.mlir"
     "log_plus_one.mlir"
@@ -330,6 +334,7 @@
     "finite.mlir"
     "floor.mlir"
     "gather.mlir"
+    "householder.mlir"
     "iota.mlir"
     "log.mlir"
     "log_plus_one.mlir"
@@ -412,6 +417,7 @@
     "finite.mlir"
     "floor.mlir"
     "gather.mlir"
+    "householder.mlir"
     "iota.mlir"
     "log.mlir"
     "log_plus_one.mlir"
@@ -492,6 +498,7 @@
     "finite.mlir"
     "floor.mlir"
     "gather.mlir"
+    "householder.mlir"
     "iota.mlir"
     "log.mlir"
     "log_plus_one.mlir"
@@ -575,6 +582,7 @@
     "finite.mlir"
     "floor.mlir"
     "gather.mlir"
+    "householder.mlir"
     "iota.mlir"
     "log.mlir"
     "log_plus_one.mlir"
diff --git a/tests/e2e/stablehlo_ops/householder.mlir b/tests/e2e/stablehlo_ops/householder.mlir
new file mode 100644
index 0000000..91c713e
--- /dev/null
+++ b/tests/e2e/stablehlo_ops/householder.mlir
@@ -0,0 +1,15 @@
+func.func public @householder_test() -> () {
+  %m = util.unfoldable_constant dense<[
+    [0.19151945, 0.62210877, 0.43772774],
+    [0.78535858, 0.77997581, 0.27259261],
+    [0.27646426, 0.80187218, 0.95813935],
+    [0.87593263, 0.35781727, 0.50099513]]> : tensor<4x3xf32>
+  %t = util.unfoldable_constant dense<[0.68346294, 0.71270203]> : tensor<2xf32>
+  %result = stablehlo.custom_call @ProductOfElementaryHouseholderReflectors(%m, %t) : (tensor<4x3xf32>, tensor<2xf32>) -> tensor<4x3xf32>
+  check.expect_almost_eq_const(%result, dense<[
+    [ 0.31653708,  0.10644531,  0.32681817],
+    [-0.53676350,  0.37089570, -0.31482652],
+    [-0.18895307, -0.54206765,  0.63208690],
+    [-0.59866750, -0.16177818,  0.08177957]]> : tensor<4x3xf32>): tensor<4x3xf32>
+  return
+}