Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <cinttypes>
#include <cstdio>
#include "dispatch-scheduler.h"

#include "ggml.h"
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,15 @@ static GatedDeltaNetMatch match_gated_delta_net(const Graph & graph, const Graph
raw_k->storage_offset != static_cast<size_t>(width * q_head_count) * sizeof(float) ||
v->storage_offset != static_cast<size_t>(2 * width * q_head_count) * sizeof(float) ||
raw_q->nb[0] != sizeof(float) || raw_q->nb[1] != static_cast<size_t>(width) * sizeof(float) ||
raw_q->nb[2] != static_cast<size_t>(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<size_t>(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<size_t>(width) * sizeof(float) ||
v->nb[2] != static_cast<size_t>(hidden_size) * sizeof(float)) {
v->nb[2] < static_cast<size_t>(hidden_size) * sizeof(float) || v->nb[2] % sizeof(float) != 0) {
return {};
}

Expand Down
36 changes: 31 additions & 5 deletions tests/test-backend-ops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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<float> data(ne);
std::uniform_real_distribution<float> 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);
}
Expand All @@ -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 {
Expand All @@ -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);
}

Expand All @@ -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);
}
};

Expand Down Expand Up @@ -9309,6 +9321,20 @@ static std::vector<std::unique_ptr<test_case>> 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));
Expand Down
19 changes: 18 additions & 1 deletion tests/test-hrx-view-alias-elision.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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;
}