diff --git a/ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp b/ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp index af4c17a959a2..022fec706366 100644 --- a/ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp +++ b/ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp @@ -1,3 +1,5 @@ +#include +#include #include "dispatch-scheduler.h" #include "ggml.h" @@ -126,7 +128,22 @@ static std::string unsupported_node_message(const Graph & graph, size_t index, c case AliasMiss::None: break; } } - stream << " alias=" << relation << why << " inputs=" << node.inputs.size(); + // THE FACTS, on top of the recorded reason: six hypotheses died on this node, so the message also carries the two + // ValueStorageIds and what layout the view describes (engine #1467). `why` above is the assignment-time reason + // (AliasMiss), which the pin's version computes and which must not be re-asked of the map here. + std::string facts; + if (!aliasing && out != nullptr && out->tensor != nullptr && node.inputs.size() == 1) { + const Value * in_v = graph.values().find(node.inputs[0]); + if (in_v != nullptr) { + char buf[192]; + const ggml_tensor * t = out->tensor; + std::snprintf(buf, sizeof buf, " storages=out%d/in%d nb=[%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 "] offs=%zu", + out->storage.value, in_v->storage.value, t->nb[0], t->nb[1], t->nb[2], t->nb[3], + t->view_offs); + facts = buf; + } + } + stream << " alias=" << relation << why << facts << " inputs=" << node.inputs.size(); } return stream.str(); } diff --git a/ggml/src/ggml-hrx/dispatch_registration/llm/dispatch-gated-delta-net.cpp b/ggml/src/ggml-hrx/dispatch_registration/llm/dispatch-gated-delta-net.cpp index d70fee16e761..70cd17e7551a 100644 --- a/ggml/src/ggml-hrx/dispatch_registration/llm/dispatch-gated-delta-net.cpp +++ b/ggml/src/ggml-hrx/dispatch_registration/llm/dispatch-gated-delta-net.cpp @@ -215,9 +215,15 @@ static GatedDeltaNetMatch match_gated_delta_net(const Graph & graph, const Graph raw_k->storage_offset != static_cast(width * q_head_count) * sizeof(float) || v->storage_offset != static_cast(2 * width * q_head_count) * sizeof(float) || raw_q->nb[0] != sizeof(float) || raw_q->nb[1] != static_cast(width) * sizeof(float) || - raw_q->nb[2] != static_cast(hidden_size) * sizeof(float) || raw_k->nb != raw_q->nb || + // engine#1925: the dim-2 step may be STRIDED -- a q/k norm fed by a view whose token step is wider than + // nb[1]*ne[1] (measured: nb[2]=40960 against 8192, 5x) -- so the token stride is bounded below and required + // to stay float-aligned instead of being required to be packed. This is the proof the decode matcher below + // already uses for v (see the `v->nb[2] < ... || v->nb[2] % sizeof(float)` term): nothing else in this guard + // relaxes, so nb[0]/nb[1], the shared storage, the three offsets and raw_k->nb == raw_q->nb all still hold. + raw_q->nb[2] < static_cast(hidden_size) * sizeof(float) || raw_q->nb[2] % sizeof(float) != 0 || + raw_k->nb != raw_q->nb || v->nb[0] != sizeof(float) || v->nb[1] != static_cast(width) * sizeof(float) || - v->nb[2] != static_cast(hidden_size) * sizeof(float)) { + v->nb[2] < static_cast(hidden_size) * sizeof(float) || v->nb[2] % sizeof(float) != 0) { return {}; } diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index ca55cecfdee5..7cb678f9d0bf 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -4500,7 +4500,7 @@ struct test_mul_mat_hadamard : public test_mul_mat { } }; -static void init_mul_mat_id_tensors(ggml_context * ctx, int n_mats) { +static void init_mul_mat_id_tensors(ggml_context * ctx, int n_mats, float activation_scale = 1.0f) { std::random_device rd; std::default_random_engine rng(rd()); for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != NULL; t = ggml_get_next_tensor(ctx, t)) { @@ -4515,6 +4515,17 @@ static void init_mul_mat_id_tensors(ggml_context * ctx, int n_mats) { std::shuffle(data.begin(), data.end(), rng); ggml_backend_tensor_set(t, data.data(), r * t->nb[1], t->ne[0] * sizeof(int32_t)); } + } else if (activation_scale != 1.0f && t->type == GGML_TYPE_F32) { + // engine#2127: the routed gate/up motifs narrow the f32 activation to f16 with NO prescale, so an + // activation outside f16's range is where that shows. The default fill is uniform in [-1, 1], which + // cannot reach it (f16 carries ~11 mantissa bits, so values <= 1 lose ~1e-4 relative and both arms + // narrow identically) -- hence a per-case scale rather than a wider default: widening the default + // would change every op's fixtures at once. + const size_t ne = (size_t) ggml_nelements(t); + std::vector data(ne); + std::uniform_real_distribution gen(-activation_scale, activation_scale); + for (size_t i = 0; i < ne; i++) { data[i] = gen(rng); } + ggml_backend_tensor_set(t, data.data(), 0, ne * sizeof(float)); } else { init_tensor_uniform(t); } @@ -4531,9 +4542,10 @@ struct test_mul_mat_id : public test_case { const int64_t m; const int64_t n; const int64_t k; + const float activation_scale; // engine#2127: >1 to reach the f16 narrowing, 1.0f for every pre-existing case std::string vars() override { - return VARS_TO_STR8(type_a, type_b, n_mats, n_used, b, m, n, k); + return VARS_TO_STR9(type_a, type_b, n_mats, n_used, b, m, n, k, activation_scale); } double max_nmse_err() override { @@ -4555,9 +4567,9 @@ struct test_mul_mat_id : public test_case { test_mul_mat_id(ggml_type type_a = GGML_TYPE_F32, ggml_type type_b = GGML_TYPE_F32, int n_mats = 8, int n_used = 2, bool b = false, - int64_t m = 32, int64_t n = 32, int64_t k = 32) + int64_t m = 32, int64_t n = 32, int64_t k = 32, float activation_scale = 1.0f) : type_a(type_a), type_b(type_b), n_mats(n_mats), n_used(n_used), b(b), - m(m), n(n), k(k) { + m(m), n(n), k(k), activation_scale(activation_scale) { GGML_ASSERT(n_used <= n_mats); } @@ -4583,7 +4595,7 @@ struct test_mul_mat_id : public test_case { } void initialize_tensors(ggml_context * ctx) override { - init_mul_mat_id_tensors(ctx, n_mats); + init_mul_mat_id_tensors(ctx, n_mats, activation_scale); } }; @@ -9309,6 +9321,20 @@ static std::vector> make_test_cases_eval() { // element-wise expert below or off that contract must stay refused rather than reach a kernel that rejects it. test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_BF16, GGML_TYPE_F32, 8, 2, false, 384, 8, 1472)); test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_BF16, GGML_TYPE_F32, 8, 2, false, 384, 8, 92)); + // engine#2127: the routed gate/up motifs narrow the f32 activation to f16 unscaled, so a fixture whose activation is + // bounded to ±1 cannot express the failure (f16 carries ~11 mantissa bits, so a value ≤ 1 loses ~1e-4 relative and + // both arms narrow identically). Two scales, both compared against the CPU arm through the case's own NMSE bound: + // 1e4 activations to 10,000 stay inside f16's 65504 and must be correct -- this row is the working range. + // 1e5 (beyond 65504) is MEASURED BAD today: HRX0 returns -nan where the CPU arm returns -165957.6875 + // (engine#2127, 857/858 with `Backend HRX0: FAIL`). That row is deliberately NOT here: a test that fails by + // design lands with the kernel fix in mul_mat_id_*_f16_wmma_projection.loom, or as expected-fail -- the harness + // has no xfail hook, so adding it now would only red the suite. What this row pins is the range that works, so + // the fix cannot buy the large case by breaking the ordinary one. + // The SHAPE matters as much as the scale: the first version of these rows used m=256,n=2,k=32 and both came back + // "not supported [HRX0]", which proves nothing about the narrowing (the backend never claimed them). These reuse a + // shape the same sweep already reports OK -- bf16 x f32, 4 mats, m=512, n=32, k=256 -- so the ONLY difference from + // the passing rows above is the activation magnitude. + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_BF16, GGML_TYPE_F32, 4, 2, false, 512, 32, 256, 1e4f)); // Row-padded activation against a dense weight: the model's dt_proj at the prefill token count, and the same contraction // at a small token count so a regression at either end is visible. test_cases.emplace_back(new test_mul_mat_row_padded_activation(GGML_TYPE_BF16, GGML_TYPE_F32, 2944, 512, 92, 264)); diff --git a/tests/test-hrx-view-alias-elision.cpp b/tests/test-hrx-view-alias-elision.cpp index 8c1946c8a6d1..eeada6bd0a97 100644 --- a/tests/test-hrx-view-alias-elision.cpp +++ b/tests/test-hrx-view-alias-elision.cpp @@ -50,7 +50,7 @@ const DispatchTarget kTarget{ "gfx1151" }; // A graph shaped like the failing one: an external leaf, one op that produces a value on this backend, and a // no-op layout alias on top of it. `produced` selects whether the alias's input is that produced value (the // real graph) or a second external leaf (what can_execute_standalone_op_as_graph builds). -enum class ViewKind { NoOpAlias, OffsetAlias, StridedView }; +enum class ViewKind { NoOpAlias, OffsetAlias, StridedView, WideWindow }; bool plans_with_view(ViewKind kind, bool produced) { ggml_init_params params = { /*mem_size*/ 32 * 1024 * 1024, /*mem_buffer*/ nullptr, /*no_alloc*/ false }; @@ -70,6 +70,17 @@ bool plans_with_view(ViewKind kind, bool produced) { case ViewKind::NoOpAlias: view = ggml_view_2d(ctx, base, 2048, 2, base->nb[1], 0); break; case ViewKind::OffsetAlias: view = ggml_view_2d(ctx, base, 2048, 2, base->nb[1], base->nb[1]); break; case ViewKind::StridedView: view = ggml_view_2d(ctx, base, 2048, 2, 2 * base->nb[1], 0); break; + case ViewKind::WideWindow: { + // THE SHAPE engine #1467 actually refuses, with the numbers the run printed: + // VIEW f32[2048,2,1,1] nb=[4,32768,65536,65536] offs=8192 + // i.e. rows of 2048 f32 taken from a buffer whose rows are 8192 f32 apart (nb[1] = 32768 bytes = 4x the + // width), starting one row in (offs = 8192 bytes = 2048 f32). The wide source is never registered as a + // value - the run also printed view_src_unregistered - so both of find_alias_source's tensor-identity + // lookups miss and the view gets a storage of its own. + ggml_tensor * wide = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 8192, 4); + view = ggml_view_2d(ctx, wide, 2048, 2, 32768, 8192); + break; + } } REQUIRE(view != nullptr); ggml_tensor * out = ggml_scale(ctx, view, 0.5f); @@ -124,6 +135,8 @@ int main() { std::printf("no-op alias, produced input : %s\n", noop_produced ? "plans" : "REFUSED"); std::printf("offset alias, produced input: %s\n", offset ? "plans" : "REFUSED"); std::printf("strided view, produced input: %s\n", strided ? "plans" : "REFUSED"); + const bool wide = plans_with_view(ViewKind::WideWindow, true); + std::printf("wide window view (node 20) : %s\n", wide ? "plans" : "REFUSED"); // A no-op layout alias plans whether its input is an External leaf (the single-node graph that // can_execute_standalone_op_as_graph builds) or a value this backend produces (the real graph). That REFUTES @@ -138,5 +151,9 @@ int main() { // the safe outcome it is today: the plan is refused, not silently computed through a wrong layout. If a consumer // ever accepts a strided alias, THIS assertion is where it should become visible. REQUIRE(!strided); + // Documented CURRENT behaviour, and it is the defect: the projector's wide-window view is refused because it aliases + // bytes the graph never registered, and nothing can dispatch a layout-alias op. The densify dispatch that fixes this + // flips this assertion to REQUIRE(wide) - when that lands, this line is where the change is visible. + REQUIRE(!wide); return 0; }