diff --git a/backends/native/runtime/engine/ggml/GgmlGraphBuilder.cpp b/backends/native/runtime/engine/ggml/GgmlGraphBuilder.cpp index 372176f8d2d..049d2bfdd23 100644 --- a/backends/native/runtime/engine/ggml/GgmlGraphBuilder.cpp +++ b/backends/native/runtime/engine/ggml/GgmlGraphBuilder.cpp @@ -676,6 +676,8 @@ void GraphBuilder::lower(const Node& node) { lower_unary(node, ggml_silu); } else if (key == "aten.sigmoid.default" || key == "aten.sigmoid_.default") { lower_unary(node, ggml_sigmoid); + } else if (key == "aten.tanh.default") { + lower_unary(node, ggml_tanh); } else if (key == "aten.gelu.default" || key == "aten.gelu_.default") { lower_gelu(node); } else if (key == "aten.native_layer_norm.default") { diff --git a/backends/native/test/runtime/ggml/test_ggml_engine.cpp b/backends/native/test/runtime/ggml/test_ggml_engine.cpp index a7498fc9a07..bbf2b9136ec 100644 --- a/backends/native/test/runtime/ggml/test_ggml_engine.cpp +++ b/backends/native/test/runtime/ggml/test_ggml_engine.cpp @@ -603,6 +603,89 @@ void expect_compile_error( } } +// tanh of x [4, 3], or of its transpose. +std::vector make_tanh_program( + bool transposed, + fbs::ScalarType dtype = fbs::ScalarType::FLOAT) { + TestProgram program; + program.input("x", dtype, {4, 3}); + std::string self = "x"; + if (transposed) { + program.call( + "xt", + "torch.ops.aten.permute_copy.default", + {{"self", std::string("x")}, {"dims", std::vector{1, 0}}}, + dtype, + {3, 4}); + self = "xt"; + } + program.call( + "y", + "torch.ops.aten.tanh.default", + {{"self", self}}, + dtype, + transposed ? std::vector{3, 4} : std::vector{4, 3}); + return program.finish("y"); +} + +// Within 1e-6 relative of tanh in double; NaN, +-1 and signed zero exactly. +TEST(GgmlEngineTest, TanhMatchesLibmIncludingNonFiniteValues) { + constexpr float kInf = std::numeric_limits::infinity(); + const std::vector x = { + -0.0f, + 0.0f, + 1.5f, + -2.25f, + 30.0f, + -30.0f, + kInf, + -kInf, + std::nanf(""), + 1e-20f, + 3e38f, + 0.75f}; + for (const bool transposed : {false, true}) { + for (const size_t cached : {size_t{0}, size_t{1}}) { + SCOPED_TRACE(std::to_string(transposed) + std::to_string(cached)); + auto host = GgmlEngineHost::create( + GgmlEngineOptions{.max_cached_graphs = cached}); + auto context = make_context(*host, make_tanh_program(transposed)); + auto executable = context->compile("forward"); + for (const float scale : {1.0f, -0.5f}) { + std::vector input = x; + for (float& value : input) { + value *= scale; + } + executable->set_input(0, input.data(), input.size(), ScalarType::Float); + executable->execute(); + std::vector y(input.size()); + executable->get_output(0, y.data(), y.size(), ScalarType::Float); + for (size_t i = 0; i < y.size(); ++i) { + // y[r][c] reads x[c][r] when transposed. + const size_t source = transposed ? (i % 4) * 3 + i / 4 : i; + const auto expected = + static_cast(std::tanh(static_cast(input[source]))); + SCOPED_TRACE(input[source]); + if (std::isnan(expected)) { + EXPECT_TRUE(std::isnan(y[i])) << y[i]; + } else if (std::abs(expected) == 1.0f || expected == 0.0f) { + EXPECT_EQ(y[i], expected); + EXPECT_EQ(std::signbit(y[i]), std::signbit(expected)); + } else { + EXPECT_NEAR(y[i], expected, 1e-6 * std::abs(expected)); + } + } + } + } + } +} + +TEST(GgmlEngineTest, RejectsIntegerTanh) { + expect_compile_error( + make_tanh_program(false, fbs::ScalarType::INT), + "supports floating-point tensors only"); +} + TEST(GgmlEngineTest, RejectsIntegerGelu) { TestProgram program; program.input("x", fbs::ScalarType::LONG, {2, 3}); diff --git a/backends/native/test/runtime/ggml/test_ggml_ops.py b/backends/native/test/runtime/ggml/test_ggml_ops.py index 54d31653a77..4de64d92762 100644 --- a/backends/native/test/runtime/ggml/test_ggml_ops.py +++ b/backends/native/test/runtime/ggml/test_ggml_ops.py @@ -595,6 +595,9 @@ def test_swiglu_broadcast_not_rewritten(self): def test_sigmoid(self): self._check(Lambda(torch.sigmoid), (torch.randn(2, 3, 4),)) + def test_tanh(self): + self._check(Lambda(torch.tanh), (torch.randn(1, 512) * 3.0,)) + def test_gelu(self): ops = collections.Counter() self._check(Lambda(F.gelu), (torch.randn(2, 3, 4) * 3.0,), graph_ops=ops)