Skip to content

Commit f8373ec

Browse files
authored
Lower amax over trailing dims (#24735) (#24735)
Summary: ## Ulterior Motive Run Whisper's log-mel front end on ggml, where `ReplaceGlobalMaxPass` rewrites the global `max()` as `amax`. ## Rationale **What**: Lower `aten.amax.default` over trailing dims on ggml. **Why**: ggml has no max reduction, and the graph builder rejected `amax`. ## Details - ggml's `amax` reuses the `trailing_rows` helper shared with `mean` and `sum`, and gathers each row's `ggml_argmax` element with `ggml_get_rows`. - Unlike `ggml_pool_1d` max, which starts from `-FLT_MAX`, this keeps an all `-inf` row at `-inf`. NaN does not propagate, as in ggml's argmax. - A non-trailing dim is rejected by name. Authored with AI using Claude Code. Reviewed By: SS-JIA Differential Revision: D129976771
1 parent 7d51b01 commit f8373ec

5 files changed

Lines changed: 91 additions & 2 deletions

File tree

‎backends/native/runtime/engine/ggml/GgmlGraphBuilder.cpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,8 @@ void GraphBuilder::lower(const Node& node) {
709709
lower_mean(node);
710710
} else if (key == "aten.sum.dim_IntList") {
711711
lower_sum(node);
712+
} else if (key == "aten.amax.default") {
713+
lower_amax(node);
712714
} else if (key == "aten.slice_copy.Tensor" || key == "aten.slice.Tensor") {
713715
lower_slice(node, key == "aten.slice_copy.Tensor");
714716
} else if (key == "aten.select_copy.int" || key == "aten.select.int") {

‎backends/native/runtime/engine/ggml/GgmlGraphBuilder.h‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,9 @@ class GraphBuilder {
241241
void lower_max_pool2d(const Node& node);
242242
void lower_mean(const Node& node);
243243
void lower_sum(const Node& node);
244-
// The `self` of a mean or sum over trailing dims as ggml rows of the reduced
245-
// elements, and the result sizes.
244+
void lower_amax(const Node& node);
245+
// The `self` of a mean, sum or amax over trailing dims as ggml rows of the
246+
// reduced elements, and the result sizes.
246247
std::pair<ggml_tensor*, std::vector<int64_t>> trailing_rows(
247248
const Node& node,
248249
const std::string& op);

‎backends/native/runtime/engine/ggml/GgmlLowerElementwise.cpp‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,20 @@ void GraphBuilder::lower_sum(const Node& node) {
294294
set_single_output(node, TensorRef{result, std::move(sizes)});
295295
}
296296

297+
// Gathers each row's argmax element. Unlike max pooling, which starts from
298+
// -FLT_MAX, this keeps an all -inf row at -inf. NaN does not propagate.
299+
void GraphBuilder::lower_amax(const Node& node) {
300+
auto [rows, sizes] = trailing_rows(node, "amax");
301+
ggml_context* ctx = instance_.context.get();
302+
const int64_t width = rows->ne[0];
303+
const int64_t count = rows->ne[1];
304+
ggml_tensor* index = ggml_reshape_2d(ctx, ggml_argmax(ctx, rows), 1, count);
305+
ggml_tensor* values =
306+
ggml_get_rows(ctx, ggml_reshape_3d(ctx, rows, 1, width, count), index);
307+
ggml_tensor* result = reshape(ctx, values, sizes);
308+
set_single_output(node, TensorRef{result, std::move(sizes)});
309+
}
310+
297311
// Comparisons produce 0/1 floats: a <= b is 1 - step(a - b) and a < b is
298312
// step(b - a).
299313
void GraphBuilder::lower_compare(const Node& node, bool strict) {

‎backends/native/test/runtime/ggml/test_ggml_engine.cpp‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,6 +2032,72 @@ TEST(GgmlEngineTest, RejectsSumOverANonTrailingDim) {
20322032
program.finish("y"), "sum supports trailing dimensions only");
20332033
}
20342034

2035+
TEST(GgmlEngineTest, AmaxOverTrailingDims) {
2036+
const std::vector<int64_t> shape = {2, 3, 4, 5};
2037+
struct Case {
2038+
std::vector<int64_t> dims;
2039+
bool keepdim;
2040+
size_t width;
2041+
std::vector<int64_t> out_shape;
2042+
};
2043+
constexpr float kInf = std::numeric_limits<float>::infinity();
2044+
for (const Case& c :
2045+
{Case{{-1}, true, 5, {2, 3, 4, 1}},
2046+
Case{{2, 3}, false, 20, {2, 3}},
2047+
Case{{0, 1, 2, 3}, false, 120, {}}}) {
2048+
for (const size_t cached : {size_t{0}, size_t{1}}) {
2049+
SCOPED_TRACE(std::to_string(c.width) + std::to_string(cached));
2050+
TestProgram program;
2051+
program.input("x", fbs::ScalarType::FLOAT, shape);
2052+
program.call(
2053+
"y",
2054+
"torch.ops.aten.amax.default",
2055+
{{"self", std::string("x")},
2056+
{"dim", c.dims},
2057+
{"keepdim", BoolValue{c.keepdim}}},
2058+
fbs::ScalarType::FLOAT,
2059+
c.out_shape);
2060+
auto host = GgmlEngineHost::create(
2061+
GgmlEngineOptions{.max_cached_graphs = cached});
2062+
auto context = make_context(*host, program.finish("y"));
2063+
auto executable = context->compile("forward");
2064+
for (const float scale : {1.0f, -3.0f}) {
2065+
std::vector<float> x(120);
2066+
for (size_t i = 0; i < x.size(); ++i) {
2067+
x[i] = scale * std::cos(static_cast<float>(i) * 1.3f);
2068+
}
2069+
// The first row is all -inf, and the last holds +inf.
2070+
std::fill_n(x.begin(), c.width, -kInf);
2071+
x.back() = kInf;
2072+
executable->set_input(0, x.data(), x.size(), ScalarType::Float);
2073+
executable->execute();
2074+
std::vector<float> y(120 / c.width);
2075+
executable->get_output(0, y.data(), y.size(), ScalarType::Float);
2076+
std::vector<float> expected(y.size(), -kInf);
2077+
for (size_t i = 0; i < x.size(); ++i) {
2078+
expected[i / c.width] = std::max(expected[i / c.width], x[i]);
2079+
}
2080+
EXPECT_EQ(y, expected);
2081+
}
2082+
}
2083+
}
2084+
}
2085+
2086+
TEST(GgmlEngineTest, RejectsAmaxOverANonTrailingDim) {
2087+
TestProgram program;
2088+
program.input("x", fbs::ScalarType::FLOAT, {2, 3, 4});
2089+
program.call(
2090+
"y",
2091+
"torch.ops.aten.amax.default",
2092+
{{"self", std::string("x")},
2093+
{"dim", std::vector<int64_t>{1}},
2094+
{"keepdim", BoolValue{false}}},
2095+
fbs::ScalarType::FLOAT,
2096+
{2, 4});
2097+
expect_compile_error(
2098+
program.finish("y"), "amax supports trailing dimensions only");
2099+
}
2100+
20352101
TEST(GgmlEngineTest, GpuUnavailableOnCpuVariant) {
20362102
GgmlEngineOptions options;
20372103
options.device = GgmlDevice::Gpu;

‎backends/native/test/runtime/ggml/test_ggml_ops.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,12 @@ def test_sum_trailing_dims(self):
879879
)
880880
self._check(Lambda(lambda x: x.sum(dim=[2, 3])), (torch.randn(2, 3, 4, 5),))
881881

882+
def test_amax_trailing_dims(self):
883+
self._check(
884+
Lambda(lambda x: x.amax(dim=-1, keepdim=True)), (torch.randn(2, 3, 16, 32),)
885+
)
886+
self._check(Lambda(lambda x: x - x.amax(dim=[0, 1])), (torch.randn(80, 300),))
887+
882888
def test_linear(self):
883889
self._check(Linear(bias=False), (torch.randn(4, 32),), **_MATMUL_TOL)
884890

0 commit comments

Comments
 (0)