From 0e502ebdf6c7c62ede6aff5049aae50562ee5066 Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Wed, 11 Nov 2026 03:22:45 -0400 Subject: [PATCH] hrx: a refused graph says when the scheduler's dependency walk stalled (Refs engine #1925) When the walk cannot reach a node it appends the rest in index order and matches them in that order. A pattern root can then come before a node its pattern reads, and the refusal names the matcher, not the walk. engine #1925 was that for days: "unsupported HRX node 160: L2_NORM" with every matcher guard satisfied. GraphTraversalOrder now records how many nodes it did not reach and the first of them, and the scheduler adds one line to the diagnostics it prints with a refusal: dependency walk stalled: 2 of 2 nodes were not reached and are matched in index order, from node 0 (SCALE) Nothing is printed for a graph that plans. The count is zero for every graph llama.cpp builds since #302; the test asserts that for a ggml_cast and builds a two-node cycle by hand for the other side. Co-Authored-By: Claude Fable 5.2 --- .../ggml-hrx/dispatch/dispatch-scheduler.cpp | 9 +++ ggml/src/ggml-hrx/graph/graph-traversal.cpp | 4 ++ ggml/src/ggml-hrx/graph/graph-traversal.h | 10 ++++ tests/test-hrx-gdn-passed-alias.cpp | 57 +++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp b/ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp index 0efbe04f9cb9..318e732a7716 100644 --- a/ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp +++ b/ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp @@ -251,6 +251,15 @@ bool DispatchScheduler::schedule_graph(Graph & graph, std::vector covered_nodes(nodes.size(), false); Status pending_diagnostics; const GraphTraversalOrder traversal = GraphTraversalOrder::build(graph); + if (traversal.stalled_count() != 0) { + // Said only if the graph is then refused (pending_diagnostics is printed with the refusal): the nodes below + // were matched in index order, so "not scheduled yet" or an uncovered pattern root further down can be a + // consequence of this line rather than a fault of the matcher it names (engine #1925). + const size_t first = traversal.first_stalled_node(); + pending_diagnostics.log("dependency walk stalled: %zu of %zu nodes were not reached and are matched in index " + "order, from node %zu (%s)", + traversal.stalled_count(), nodes.size(), first, ggml_op_name(nodes[first].op)); + } for (const GraphNode * node : traversal.nodes()) { size_t i = 0; if (node == nullptr || !graph.index().node_index(node, i)) { diff --git a/ggml/src/ggml-hrx/graph/graph-traversal.cpp b/ggml/src/ggml-hrx/graph/graph-traversal.cpp index 2e83355e7c88..7584ce6da56b 100644 --- a/ggml/src/ggml-hrx/graph/graph-traversal.cpp +++ b/ggml/src/ggml-hrx/graph/graph-traversal.cpp @@ -164,6 +164,10 @@ GraphTraversalOrder GraphTraversalOrder::build(const Graph & graph) { for (size_t i = 0; i < nodes.size(); ++i) { if (!selected[i]) { + if (result.stalled_count_ == 0) { + result.first_stalled_node_ = i; + } + ++result.stalled_count_; result.nodes_.push_back(&nodes[i]); } } diff --git a/ggml/src/ggml-hrx/graph/graph-traversal.h b/ggml/src/ggml-hrx/graph/graph-traversal.h index c5cb1f27a751..4a79e03ed94b 100644 --- a/ggml/src/ggml-hrx/graph/graph-traversal.h +++ b/ggml/src/ggml-hrx/graph/graph-traversal.h @@ -14,8 +14,18 @@ class GraphTraversalOrder { const std::vector & nodes() const { return nodes_; } + // Nodes the dependency walk never reached, because an input of theirs never became available (a cycle, or a + // node that waits for itself). They are appended in index order, which is a valid execution order but not the + // one fused patterns are matched in: a pattern's root can then come before a node the pattern reads. Zero for + // every graph llama.cpp builds today; engine #1925 was a non-zero count nobody could see. + size_t stalled_count() const { return stalled_count_; } + // index of the first such node; meaningful only when stalled_count() is not zero + size_t first_stalled_node() const { return first_stalled_node_; } + private: std::vector nodes_; + size_t stalled_count_ = 0; + size_t first_stalled_node_ = 0; }; } // namespace ggml::hrx diff --git a/tests/test-hrx-gdn-passed-alias.cpp b/tests/test-hrx-gdn-passed-alias.cpp index 3379b8a13bd5..86f183f2ad40 100644 --- a/tests/test-hrx-gdn-passed-alias.cpp +++ b/tests/test-hrx-gdn-passed-alias.cpp @@ -40,7 +40,9 @@ #include "dispatch/dispatch-scheduler.h" #include "ggml.h" +#include "graph/graph-traversal.h" #include "graph/graph.h" +#include "graph/op-params.h" #include "kernel-corpus/kernel-corpus.h" #include @@ -211,10 +213,65 @@ void check_walk(int64_t tokens) { REQUIRE(behind_cast.gdn == ordinary.gdn); } +// A walk that does stall says so. No graph llama.cpp builds does, so the graph is made by hand: two SCALE nodes that +// read each other's output. Neither can be reached, both are taken in index order, the first is refused, and the +// refusal carries the line that tells the reader the order was not the dependency order. +void check_stall_report() { + ggml_init_params params = {}; + params.mem_size = 1024 * 1024; + params.no_alloc = true; + ggml_context * ctx = ggml_init(params); + REQUIRE(ctx != nullptr); + ggml_tensor * x = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 64); + ggml_tensor * a = ggml_scale(ctx, x, 0.5f); + ggml_tensor * b = ggml_scale(ctx, a, 0.5f); + + Graph graph; + const ValueId va = graph.values().get_or_add_tensor_value(a, ValueKind::Transient); + const ValueId vb = graph.values().get_or_add_tensor_value(b, ValueKind::Transient); + GraphNode & na = graph.add_node(GGML_OP_SCALE, va, { vb }); + na.params = import_op_params(*a); + GraphNode & nb = graph.add_node(GGML_OP_SCALE, vb, { va }); + nb.params = import_op_params(*b); + REQUIRE(graph.build_index().success()); + + const GraphTraversalOrder order = GraphTraversalOrder::build(graph); + REQUIRE(order.nodes().size() == 2); + REQUIRE(order.stalled_count() == 2); + REQUIRE(order.first_stalled_node() == 0); + + DispatchScheduler scheduler; + REQUIRE(!scheduler.schedule_graph(graph, DispatchTarget{ "gfx1151" })); + std::string text; + for (const std::string & e : scheduler.plan().status.errors()) { + text += e + "\n"; + } + std::printf("two nodes that read each other: refused, and the refusal says:\n %s", text.c_str()); + REQUIRE(text.find("dependency walk stalled: 2 of 2 nodes were not reached") != std::string::npos); + REQUIRE(text.find("from node 0 (SCALE)") != std::string::npos); + ggml_free(ctx); + + // and the cast that used to stall the walk does not: nothing is left unreached behind a ggml_cast + ggml_context * ctx2 = ggml_init(params); + REQUIRE(ctx2 != nullptr); + ggml_tensor * cast = ggml_cast(ctx2, ggml_new_tensor_1d(ctx2, GGML_TYPE_F32, 64), GGML_TYPE_F32); + ggml_tensor * after = ggml_scale(ctx2, ggml_scale(ctx2, cast, 0.5f), 0.5f); + ggml_cgraph * cgraph = ggml_new_graph(ctx2); + ggml_build_forward_expand(cgraph, after); + auto imported = import_ggml_graph(*cgraph); + REQUIRE(imported.valid()); + const GraphTraversalOrder cast_order = GraphTraversalOrder::build(imported.graph); + std::printf("a ggml_cast and two nodes behind it: %zu nodes, %zu not reached\n", cast_order.nodes().size(), + cast_order.stalled_count()); + REQUIRE(cast_order.stalled_count() == 0); + ggml_free(ctx2); +} + } // namespace int main() { setvbuf(stdout, nullptr, _IOLBF, 0); + check_stall_report(); check_walk(512); check_walk(64); check_walk(2);