Repository navigation
hrx: say WHICH alias lookup missed, not just that one did (Refs #1467) - #262
Conversation
…meters
`fused_context_claim` accepted a per-head ROPE on SHAPE alone (`is_head_row(op) && fed_by_op(op)`), while the
two registrations that execute rotations accept different PARAMETER sets:
* llm/dispatch-attention-qkv.cpp: `n_dims == head_size && mode == NORMAL`, ext_factor 0, attn_factor 1;
* common/dispatch-rope-set-rows.cpp: `4 <= n_dims <= head_size`, multiple of 4, mode NORMAL / NEOX /
collapsible IMROPE.
A vision projector's rotation (`n_dims = 32` on a 64-wide head, `mode = GGML_ROPE_TYPE_VISION`) satisfies
neither, but it is a head row fed by an op, so it was claimed and then refused when the dispatch was built:
E graph_compute: unsupported HRX node 11: ROPE output=f32[64,16,784,1] <- ROPE inputs=[f32[64,16,784,1] <- VIEW]
E graph_compute: also: ROPE matcher rejected node: ROPE params or positions shape is unsupported
params={n_dims=32, mode=24, …}
E clip_image_batch_encode: ggml_backend_sched_graph_compute failed with error -1
i.e. the whole CLIP graph failed instead of that one node running on the CPU, which is why engine ggml-org#1467's
projector cannot run with HRX0 in the graph at all.
The condition now lives in one place, `ggml/src/ggml-hrx/rope-claim-params.h`: the two matcher predicates plus
their union. Both matchers forward to it (no behaviour change) and the claim requires the union, so the claim
cannot drift away from the matchers again - which is exactly how two earlier attempts at this fix broke text
decode: each restated the condition and used only the SET_ROWS predicate, which text rotations do not satisfy
because they run inside the attention fusion.
Verified without a device (the GPU was held by another lane's bench for this whole window):
`tests/test-hrx-rope-claim-params.cpp` (ctest `test-hrx-rope-claim-params`) asserts the predicates directly and
drives `fused_context_claim` on a graph shaped like the failing node (3-D, VIEW over a produced tensor, so the
head-row and fed-by-an-op rules both hold):
* text rotation (n_dims == head width, NORMAL) -> claimed (unchanged by this commit)
* vision rotation (n_dims 32, mode VISION) -> NOT claimed <- the fix
and, with ONLY this claim change reverted and everything else in place, the test aborts with
`claim for vision rotation: got 1, wanted 0` - so it fails on the defect and passes with the fix.
Still blocking the rest of ggml-org#1467: with the projector on HRX the next refused node is a pure-alias VIEW
(`unsupported HRX node 20: VIEW f32[2048,2,1,1] <- VIEW`), which is claimed by the standalone path rather than
by a fused claim. That is a separate change and is not in this commit.
Refs ggml-org#1467
…s elision in a host test engine ggml-org#1467's projector is refused at a node the log names only by shape: E graph_compute: unsupported HRX node 20: VIEW output=f32[2048,2,1,1] <- VIEW inputs=[f32[2048,2,1,1]] consumers=[21:ADD] A layout-alias node is refused there for one of two completely different reasons and the shape cannot tell them apart: it IS an alias (one input, same storage as its input) whose input was not available when the node was visited, or it is not aliasing its input at all and therefore needs a dispatch that no registration provides. The message now says which: ... consumers=[…] alias=same_storage inputs=1 ... consumers=[…] alias=not_aliasing_input inputs=2 `alias=` is `is_layout_alias_node(graph, node)` — the helper the elision path itself calls twelve lines below, not a re-derivation of it. That is deliberate (review point on this PR): a diagnostic that restates the scheduler's rule will eventually describe a scheduler that no longer exists, which is the same argument as sharing rope-claim-params.h between the claim and its matchers in #257. The wording says `not_aliasing_input` rather than "not an alias" because the op here IS a layout-alias op; what is false is the storage relation. Alongside it, `tests/test-hrx-view-alias-elision.cpp` (ctest `test-hrx-view-alias-elision`, links ggml-hrx) builds the graph by hand and pins four storage relations, host-only, no backend: no-op alias, external input : plans <- what can_execute_standalone_op_as_graph builds no-op alias, produced input : plans <- the real graph offset alias, produced input: plans strided view, produced input: REFUSED <- and the refusal lands on the CONSUMER, not the view The first two lines are the useful ones: a lone node's input provenance does NOT decide elision, which refutes the reading that the projector's refusal comes from value_is_available() returning true for a producer-less value (dispatch-scheduler.cpp:102-106). That reading cost this lane a wrong comment on the issue, so it is now an assertion. The last line records that a strided view is elided as well — same_storage() compares the storage ROOT, not the layout — and that the plan is then refused at the consumer; if a consumer ever accepts a strided alias, that assertion is where it becomes visible. Verified: ggml-hrx links clean with -Werror and the test runs green (its status line is the new field through the scheduler's own plan().status.errors()). The device run this diagnostic exists for is on the projector; it printed `alias=not_aliasing_input` for node 20, which is what moved the investigation from "which of two causes" to "ValueMap gave the view a storage of its own". Refs ggml-org#1467
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at 610c61a7 — the split is exactly the right idea; three things make it able to silently undo itself
The problem statement is the best kind of diagnostic PR: one word (alias=not_aliasing_input) covering two causes with different repairs, so the reader cannot tell "register the value, or resolve by storage range" from "the assignment itself is buggy". Naming which one it is turns a dead end into a task, and the table in the body is the thing the log line should be able to say by itself.
1. The condition is re-derived from the string it just built. relation already knows the answer — it is "same_storage" or "not_aliasing_input" — and the code then asks the string a second time:
const char * relation = is_layout_alias_node(graph, node) ? "same_storage" : "not_aliasing_input";
…
if (relation != nullptr && std::strcmp(relation, "not_aliasing_input") == 0) {If either literal is ever edited (or a third relation is added), the strcmp stops matching, why stays "", and the diagnostic silently reverts to the ambiguous form this PR exists to remove — with no compile error and no test to catch it. Keep the boolean:
const bool aliasing = is_layout_alias_node(graph, node);
const char * relation = aliasing ? "same_storage" : "not_aliasing_input";
if (!aliasing) { …why = …; }relation != nullptr is also vacuous (it is a string literal) and reads as though a null pointer were possible.
2. relation is a compound token now, and something will match the old one. The output becomes alias=not_aliasing_input(view_src_unregistered). I checked 1bit-engine for consumers of alias=… and found none in tools//scripts/, so nothing breaks today — but this is a log field a future parser will split on, and a matcher for alias=not_aliasing_input (which is what the previous PR in this stack taught people to look for) will stop matching. A separate field is friendlier to that: alias=not_aliasing_input why=view_src_unregistered. Cheaper to decide now than after someone writes the grep.
3. Nothing pins either branch, and the classifier is the one thing here that is pure enough to test. As written it is inline in a stream-building block, so no test can reach it. Extract it:
const char * alias_miss_reason(const Graph &, const Node &); // nullptr when the alias is fineand the two arms become two assertions in the fork's existing test-hrx-* suite (which this PR's sibling changes are already in): a node whose view_src is not a registered value → view_src_unregistered; the same node with the value registered → view_src_registered_but_storage_differs. Without that, the second branch — the one that would mean the assignment code is wrong — is the branch nobody will ever confirm works until it fires in an investigation.
What I like and would keep verbatim: the comment explaining both causes and both repairs, and the fact that the PR refs ggml-org#1467 rather than claiming to fix it — the diagnostic is what makes ggml-org#1467 tractable, and saying so is honest about which half is done.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at 610c61a
Asking the same map instead of re-deriving is the right principle, and the "Coverage, stated plainly" section is accurate: the new branch is reached only on a device. Checked: git merge-tree onto the pin tip is clean, and on the merged tree dispatch-scheduler.cpp compiles with -fsyntax-only -Werror (rc 0), as do the stack's two host tests.
Blocking: the classifier asks the map at a different time from the assignment, so it can name the wrong repair. The table's whole purpose is choosing between "register the value" and "the assignment has a bug". But the storage decision happens in ValueMap::get_or_add_tensor_value() at the moment the view is registered (find_alias_source(): an exact find_tensor(view_src), then the storage-range fallback). The message asks find_tensor(view_src) after the whole graph has been registered (graph.cpp:178/183 register each node's sources, then its output, node by node). And view_src is the root of the view chain, not src[0]. So if that root is first registered by a later node:
- at assignment time the exact lookup missed (the view got its own storage, or a fallback match);
- at message time
find_tensor(view_src)hits, and the line saysview_src_registered_but_storage_differs, "a bug in the assignment itself"; - but the real cause is registration order, and the repair is the first row's, not the second's.
That is the one-device-run-per-wrong-guess cost the commit message wants to avoid. A cheap way to make it exact, using only what is public: ValueIds are handed out sequentially (const ValueId id(static_cast<int32_t>(values_.size())), value-map.cpp), so compare them:
const Value * s = src ? graph.values().find_tensor(src) : nullptr;
why = !src ? "(no_view_src)"
: !s ? "(view_src_unregistered)"
: s->id.value > out->id.value ? "(view_src_registered_after_view)"
: "(view_src_registered_but_storage_differs)";Two smaller label problems in the same branch:
src == nullptr(a layout-alias op whose tensor has noview_src) printsview_src_unregistered. The table's repair for that row doesn't apply. Give it its own word, as above.- When
inputs.size() != 1,is_layout_alias_nodeis false because of the input count, whatever the map says, yet the line still prints aview_srcclassification. Skip the parenthetical there (inputs=2already says why), or add(inputs_not_1).
|
VERDICT: READY — head Why this is the useful refinement of #258: Those are "the scheduler chose the wrong view" versus "the view points at something the scheduler cannot see", and ggml-org#1467's text tower is exactly where the difference matters. Printing both facts ( The one thing I would change: const bool aliasing = is_layout_alias_node(graph, node);
const char * relation = aliasing ? "same_storage" : "not_aliasing_input";
...
if (!aliasing) { why = ...; }Same output, and the two uses of the decision (label and branch) can no longer disagree. This is the kind of thing that costs an hour exactly once, on the night someone renames a string. Two smaller asks:
|
|
VERDICT: READY - diagnostic-only as claimed, and the two labels match the table in the body. One case can print a cause that is not the cause. What I checked
The one case that can lieconst ggml_tensor * src = out->tensor->view_src;
why = (src != nullptr && graph.values().find_tensor(src) != nullptr)
? "(view_src_registered_but_storage_differs)"
: "(view_src_unregistered)";If Two ways to close it, both one line: : "(view_src_absent)" // a third label, honest about the case
/* or */ src == nullptr ? "(view_src_absent)" : (registered ? … : "(view_src_unregistered)")I would take the third label: three distinct causes, three distinct strings, and the reader learns which of the repairs applies instead of being sent to look at a registration that cannot exist. Nit, not a finding
Not verifiedRunning it against engine#1467's text tower - that needs the projector and a resident serve, and RAM on this box is at 0 GiB. So: the code path, the fallbacks and the label semantics are checked; the claim that ggml-org#1467's tower hits |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 610c61a: CHANGES
This re-stamps, in the merge script's format, my verdict above at this same head (610c61a7). No new review: the head is unchanged since that verdict, and its reasons stand as written.
610c61a to
b854c29
Compare
|
Both blocking points are fixed at
and the message switches on that recorded reason. So the field now reports the fact that was true when the decision was made, which is the only version of it that can name the right repair. Unchanged: One honesty note on verification: the test object in my build tree is stale against the previous commit's tree, so the run I did exercises the new library with the old test binary — the pinned relations still behave, but that is not a fresh test build, and I am saying so rather than implying a clean run. Please re-verdict at |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at b854c29: CHANGES
b854c29 fixes the timing problem I raised, and in a better way than I suggested: the reason is recorded when the storage is assigned (Value::alias_miss, set in get_or_add_tensor_value), and the message only reads it. The no-view_src and inputs ≠ 1 cases now print nothing extra, which is right. At this head dispatch-scheduler.cpp and value-map.cpp compile with -fsyntax-only -Werror (rc 0), and git merge-tree onto the base 1bit/1467-view-alias-diag is clean.
Blocking: one of the two labels can never be set, and the case it describes then prints nothing. alias_miss is decided only in the else branch, where find_alias_source() returned null:
} else { // no alias_source
...
if (tensor->view_src != nullptr)
alias_miss = find_tensor(tensor->view_src) == nullptr ? ViewSrcUnregistered : ViewSrcRegisteredStorageDiffers;But find_alias_source() returns find_tensor(tensor->view_src) first whenever that is non-null (value-map.cpp, the exact_source early return). So reaching the else branch already implies find_tensor(view_src) == nullptr, and the ternary always yields ViewSrcUnregistered. ViewSrcRegisteredStorageDiffers is dead.
The case it was meant to name is when an alias source was found, the view joined its storage, and is_layout_alias_node() is still false because that storage isn't the one behind node.inputs[0]. For example, the range fallback picked a different overlapping value, or the input is a different view of the same root. That happens in the if (alias_source != nullptr) branch, leaves alias_miss = None, and prints no reason: the "assignment bug" row of the PR's own table can't appear.
Fix: in the if branch, record which way the source was found (exact view_src hit or range fallback). In the message, when !aliasing, alias_miss == None and the node has one input, print e.g. (aliased_to_<exact|range>_source_not_the_input), so a "not aliasing" line always carries a reason. Then drop the unreachable ternary arm.
…ource was found Three review points from #262, applied. The second and third changed the design rather than the wording. 1. **No re-derivation from the string it built.** `aliasing` is the boolean; `relation` is formatted from it. 2. **The classifier asks the value, not the map.** Asking `find_tensor(view_src)` at message time answers a different question from the one answered when the storage was chosen - values added in between make that lookup succeed where it failed then, so the message could name the wrong repair. `AliasMiss` is recorded on `Value` inside `get_or_add_tensor_value()`. 3. **The dead label is gone.** `ViewSrcRegisteredStorageDiffers` could never be set: a registered `view_src` is an early return in `find_alias_source()`, so the fresh-storage branch can only mean "no alias source". What the schedule actually needs to know is *which way* an alias source was found, because a found source joins THAT value's storage and that storage need not be the node input's - which is exactly how `is_layout_alias_node()` ends up false while an alias source exists. So: NoAliasSource - view_src unregistered and no value shares its storage root: a storage of its own ExactViewSrc - aliased the view_src value itself, and that is not the node input's storage RangeFallback - aliased by the storage-root fallback, not by identity None - not a view and the message prints `alias=not_aliasing_input(no_alias_source)` / `(aliased_view_src_not_the_input)` / `(aliased_storage_root_fallback_not_the_input)`. Snapshot restore in graph-diagnostics.cpp takes the new field (AliasMiss::None) so a restored value cannot claim a reason it never had. `ggml-hrx` links clean with -Werror and the host test still pins its relations. Refs ggml-org#1467
b854c29 to
bddd5ee
Compare
|
Your point stands and it was the better correction of the two: the label was unreachable, and the case it was meant to name prints nothing. Fixed at You are right about why: a registered What I implemented is the shape you specified — record which way the source was found, since a found source joins that value's storage and that storage need not be the node input's, which is exactly how so the message prints One thing your review surfaced that I had not wired:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at bddd5ee81a00e0665b4ad4240b75e03862fc7dc7 — Saying which alias lookup missed rather than that one did — the difference between a message that shortens the next investigation and one that starts it. Pairs with #267 (the storage ids and layout for the same node); between them, a refused alias node now reports the lookup that failed and the facts behind it.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at bddd5ee8 — two mechanical fixes, both about the PR describing itself rather than about the code, which is now better than the body says.
1. Retarget the base: 1bit/1467-view-alias-diag is dead
This PR's base is 1bit/1467-view-alias-diag, which was #258 and is MERGED (base 1bit/hrx-vulkan-patched, squashed to 6b8eba31, which is on the integration branch). So the stack's first half is in and this diff is being compared against a branch that no longer means anything — the standard way a fork PR ends up merged into the wrong line. Please retarget to 1bit/hrx-vulkan-patched (the merge is already there, so no rebase should be needed).
2. The body prints different words than the code
The code now emits the AliasMiss enum recorded at assignment time:
no_alias_source | aliased_view_src_not_the_input | aliased_storage_root_fallback_not_the_input
while the body's table still offers view_src_unregistered / view_src_registered_but_storage_differs. A reviewer (or whoever reads a log next) would grep for the wrong strings. The change itself is right, and right for a reason worth keeping in the body: reading Value::alias_miss rather than re-asking the map is the fix for the bug the review found — the map has grown since the storage was assigned, so find_tensor(view_src) can succeed later and the message would then name the wrong repair. That is the strongest argument in the PR and it is currently only in a code comment.
Verified good
- The four enum values each carry a one-line meaning in
graph/value-map.h(None,NoAliasSource,ExactViewSrc,RangeFallback), and the decision (is_layout_alias_node) is computed once with the string derived from it — the first version compared the string it had just built, which is the kind of thing that keeps working until someone rewords a message. graph.values().find(node.output)handles a missing value (out != nullptr) instead of dereferencing, so a diagnostic on an unregistered output stays a diagnostic.- The distinction is the one that matters for ggml-org#1467: "the graph does not contain what the lookup needs" (register it, or resolve by storage range) versus "the assignment itself is wrong" — two repairs that a single
not_aliasing_inputcollapses.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at bddd5ee: READY
My blocking point is fixed. AliasMiss now records how the storage was chosen, at the moment it was chosen:
NoAliasSource: theelsebranch, whereview_srcis unregistered and no value shares its storage root;ExactViewSrc/RangeFallback: theif (alias_source)branch, split by the samefind_tensor(view_src)test thatfind_alias_source()applies first, so the label always matches how the source was actually found;None: not a view.
All four values can now occur, and the case I said printed nothing (an alias source was found, but it isn't the storage behind inputs[0]) now prints (aliased_view_src_not_the_input) or (aliased_storage_root_fallback_not_the_input). The message reads the field only when is_layout_alias_node() is false, so an aliasing node isn't labelled.
Checked at this head: dispatch-scheduler.cpp and value-map.cpp compile with -fsyntax-only -Werror (rc 0), git merge-tree onto the base 1bit/1467-view-alias-diag is clean, and there are no conflict markers. The device run that would exercise the new labels on engine#1467's text tower is the author's.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at bddd5ee8 — the diagnosis is exactly right (two very different causes printing the same word is how an investigation stalls for a day), and the shape of the fix is the
right one. My ask is that the disambiguation be tested, because "which reason" is the entire value and it is also the thing a refactor can silently collapse back.
What earns the READY.
- The table in the body is the whole argument, and it is convincing:
view_src_unregisteredneeds you to register a value or resolve by storage range, while an alias that did
resolve through the storage-root fallback but not to this node needs the opposite repair. Printing both asnot_aliasing_inputsends the reader to fix the wrong thing — which is
precisely what happened on ggml-org#1467's text tower. enum class AliasMisswith aswitchthat has nodefault:— so-Wswitchmakes a new miss reason a compiler error at every site that names them, rather than a silently
unhandled case. That is the detail that makes the enum durable rather than decorative, and it is easy to get wrong (default:would have thrown it away).- The reasons are named for the mechanism, not the symptom (
ExactViewSrc,RangeFallback,NoAliasSource), so a log line tells a reader which lookup failed without them
having to read the scheduler.
The ask: one arm per reason. Four enumeration values exist so that three different situations do not share a word — and nothing in this diff pins that they do not. A test that
constructs each miss (or drives find_alias_source into each branch) and asserts the distinct string is what makes the disambiguation real; without it, a future change can route
RangeFallback through the ExactViewSrc branch and every message still looks plausible. Four assertions, and they are the difference between a diagnostic that diagnoses and one
that merely has more words in it.
One smaller note: case AliasMiss::None: break; is correct, but say somewhere that None means no miss — a reader meeting AliasMiss::None in a message-producing switch will
briefly assume it means "no alias found".
Not run: I read the diff and the body; I did not build it. The review is of the diagnostic's structure and of what is pinned.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at +48/−2 head — an error message that says which miss, and the way it is derived is the part that matters.
The change makes a refused layout-alias lookup report one of three reasons instead of a bare "it missed": (no_alias_source), (aliased_view_src_not_the_input), (aliased_storage_root_fallback_not_the_input). For a class of failure that is otherwise invisible — the node is refused, the graph splits, the model still runs — knowing which lookup failed is the difference between an afternoon and a message.
The detail I want to keep, because it is a real correctness point rather than style: the miss is read from the value (switch (out->alias_miss)), not re-asked of the map, and the comment says why — "the map has grown since the storage was [recorded]". Re-deriving the reason at report time could give a different answer than the decision that was actually made, so the recorded value is the only faithful one. That is the same rule as "the string is derived from aliasing, never the other way round", which the comment flags as a review point already fixed — good: the enum is the decision, the text is presentation.
Why an enum rather than a bool plus a string: AliasMiss::None as the default means "did not miss" is a state the type can express, and the switch covers the two reachable cases with a comment noting that a registered view_src returns early above, so RangeFallback/ExactViewSrc are exhaustive for this branch. That is the kind of exhaustiveness note that stops a future fourth reason from being silently unhandled — a default: would have hidden it.
Not run: needs a device to reach the alias path; the change is message construction from a recorded enum, so a review can settle it.
|
(Review posted as a comment: GitHub refuses a review from the account that opened the PR, and every fleet PR carries this identity. Content is a review.) VERDICT: READY at Scope note: I read this rather than built it — it needs the ggml/HRX build, and my verdicts on things I cannot run are marked as such. Everything below cites lines, so it is checkable. The recording is at the right time, and that is the substanceThe earlier review's blocking point — the message re-asking the map, which has grown since the storage was assigned — is fixed properly: I nearly reported a false positiveThe comment at It is not. The note: two of the three values are never printedThe only reader is Either the intent is that the message also covers the aliasing side (worth one line: |
…STER/llama.cpp into 1bit/1467-alias-reason # Conflicts: # ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp
|
Retargeted to
Please re-verdict at |
|
Retargeted to So the retarget is the whole change: no merge, no conflict, no rebuild — the base pointer moved and the tree did not, which is what the "shows only its own diff" condition is asking for. Worth contrasting with For the re-verdict, my verdict stands unchanged from |
|
Base is |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at d9f1257: READY
Re-verdict after the retarget onto 1bit/hrx-vulkan-patched (d9f1257 merges the pin tip, which now carries #256/#261/#269 and this PR's former stack #257/#258). Against my READY at bddd5ee, the own diff gains nothing new: the only added lines are the removal of #258's old alias= line, which this PR replaces. The AliasMiss recording (NoAliasSource / ExactViewSrc / RangeFallback) is unchanged. git merge-tree onto the pin is clean, there are no conflict markers, and dispatch-scheduler.cpp and value-map.cpp compile with -fsyntax-only -Werror at this head (rc 0).
… node (Refs ggml-org#1467) (#267) * hrx: claim a head-row ROPE only when a fused matcher accepts its parameters `fused_context_claim` accepted a per-head ROPE on SHAPE alone (`is_head_row(op) && fed_by_op(op)`), while the two registrations that execute rotations accept different PARAMETER sets: * llm/dispatch-attention-qkv.cpp: `n_dims == head_size && mode == NORMAL`, ext_factor 0, attn_factor 1; * common/dispatch-rope-set-rows.cpp: `4 <= n_dims <= head_size`, multiple of 4, mode NORMAL / NEOX / collapsible IMROPE. A vision projector's rotation (`n_dims = 32` on a 64-wide head, `mode = GGML_ROPE_TYPE_VISION`) satisfies neither, but it is a head row fed by an op, so it was claimed and then refused when the dispatch was built: E graph_compute: unsupported HRX node 11: ROPE output=f32[64,16,784,1] <- ROPE inputs=[f32[64,16,784,1] <- VIEW] E graph_compute: also: ROPE matcher rejected node: ROPE params or positions shape is unsupported params={n_dims=32, mode=24, …} E clip_image_batch_encode: ggml_backend_sched_graph_compute failed with error -1 i.e. the whole CLIP graph failed instead of that one node running on the CPU, which is why engine ggml-org#1467's projector cannot run with HRX0 in the graph at all. The condition now lives in one place, `ggml/src/ggml-hrx/rope-claim-params.h`: the two matcher predicates plus their union. Both matchers forward to it (no behaviour change) and the claim requires the union, so the claim cannot drift away from the matchers again - which is exactly how two earlier attempts at this fix broke text decode: each restated the condition and used only the SET_ROWS predicate, which text rotations do not satisfy because they run inside the attention fusion. Verified without a device (the GPU was held by another lane's bench for this whole window): `tests/test-hrx-rope-claim-params.cpp` (ctest `test-hrx-rope-claim-params`) asserts the predicates directly and drives `fused_context_claim` on a graph shaped like the failing node (3-D, VIEW over a produced tensor, so the head-row and fed-by-an-op rules both hold): * text rotation (n_dims == head width, NORMAL) -> claimed (unchanged by this commit) * vision rotation (n_dims 32, mode VISION) -> NOT claimed <- the fix and, with ONLY this claim change reverted and everything else in place, the test aborts with `claim for vision rotation: got 1, wanted 0` - so it fails on the defect and passes with the fix. Still blocking the rest of ggml-org#1467: with the projector on HRX the next refused node is a pure-alias VIEW (`unsupported HRX node 20: VIEW f32[2048,2,1,1] <- VIEW`), which is claimed by the standalone path rather than by a fused claim. That is a separate change and is not in this commit. Refs ggml-org#1467 * hrx: say which relation a refused layout-alias node has, and pin alias elision in a host test engine ggml-org#1467's projector is refused at a node the log names only by shape: E graph_compute: unsupported HRX node 20: VIEW output=f32[2048,2,1,1] <- VIEW inputs=[f32[2048,2,1,1]] consumers=[21:ADD] A layout-alias node is refused there for one of two completely different reasons and the shape cannot tell them apart: it IS an alias (one input, same storage as its input) whose input was not available when the node was visited, or it is not aliasing its input at all and therefore needs a dispatch that no registration provides. The message now says which: ... consumers=[…] alias=same_storage inputs=1 ... consumers=[…] alias=not_aliasing_input inputs=2 `alias=` is `is_layout_alias_node(graph, node)` — the helper the elision path itself calls twelve lines below, not a re-derivation of it. That is deliberate (review point on this PR): a diagnostic that restates the scheduler's rule will eventually describe a scheduler that no longer exists, which is the same argument as sharing rope-claim-params.h between the claim and its matchers in #257. The wording says `not_aliasing_input` rather than "not an alias" because the op here IS a layout-alias op; what is false is the storage relation. Alongside it, `tests/test-hrx-view-alias-elision.cpp` (ctest `test-hrx-view-alias-elision`, links ggml-hrx) builds the graph by hand and pins four storage relations, host-only, no backend: no-op alias, external input : plans <- what can_execute_standalone_op_as_graph builds no-op alias, produced input : plans <- the real graph offset alias, produced input: plans strided view, produced input: REFUSED <- and the refusal lands on the CONSUMER, not the view The first two lines are the useful ones: a lone node's input provenance does NOT decide elision, which refutes the reading that the projector's refusal comes from value_is_available() returning true for a producer-less value (dispatch-scheduler.cpp:102-106). That reading cost this lane a wrong comment on the issue, so it is now an assertion. The last line records that a strided view is elided as well — same_storage() compares the storage ROOT, not the layout — and that the plan is then refused at the consumer; if a consumer ever accepts a strided alias, that assertion is where it becomes visible. Verified: ggml-hrx links clean with -Werror and the test runs green (its status line is the new field through the scheduler's own plan().status.errors()). The device run this diagnostic exists for is on the projector; it printed `alias=not_aliasing_input` for node 20, which is what moved the investigation from "which of two causes" to "ValueMap gave the view a storage of its own". Refs ggml-org#1467 * hrx: say WHICH alias lookup missed, not just that one did `alias=not_aliasing_input` (previous commit) says a layout-alias node's storage is not its input's. On engine ggml-org#1467's text tower that is where the investigation stops, because two very different causes print the same word and they need different repairs: * `view_src_unregistered` — the `ggml_tensor` the node's `view_src` points at is not a value in this graph, so `find_alias_source`'s pointer lookup cannot hit and the storage-root fallback has nothing to match. Repair: register the value, or resolve the alias by storage *range* rather than by tensor-pointer identity. * `view_src_registered_but_storage_differs` — it WAS registered and the map still gave the view a storage of its own. Repair: that is a bug in the assignment itself, not in what the graph contains. Both now print distinctly: ... alias=not_aliasing_input(view_src_unregistered) inputs=1 ... alias=not_aliasing_input(view_src_registered_but_storage_differs) inputs=1 `find_tensor()` is public and `Value::tensor` is public (`graph/value-map.h:100-101`, `:84`), so this asks the same map the assignment asked, rather than re-deriving anything. Why it matters: the difference decides whether the next change is "register the missing value" or "match by storage range". Guessing wrong costs a device run each time, and this lane has already spent several. Coverage, stated plainly: the new branch is NOT exercised by `test-hrx-view-alias-elision` — every case there fails *before* the message is built (the strided case is refused at its consumer), and `unsupported_node_message` is file-local so the test cannot call it. What the test does show is that the change does not alter the four relations it pins. Exercising the new branch needs the projector on a device. Stacked on the previous commit (`1bit/1467-view-alias-diag`); merge that first. Refs ggml-org#1467 * hrx: print the two storages and the view's layout for a refused alias node Six hypotheses about engine ggml-org#1467's refused `VIEW` have died under their own tests (input provenance, offsets, strides, an unregistered view_src resolved by storage range, producer coverage, and the claim). Each one was tested by building the case it predicted and watching it not reproduce. That is the signal to stop predicting and print the two storages. For a layout-alias node whose storage is not its input's, the message now also carries: storages=out7/in3 nb=[4,8192,16384,32768] offs=0 * `storages=outN/inM` - the two `ValueStorageId`s. Equal ids with `alias=not_aliasing_input` would be a contradiction (the map agreed and the message says it did not); different ids are the fact every hypothesis has been guessing at; * `nb=[...]` and `offs=` - what layout the view describes. A view whose strides differ from its input's same-shape tensor is a strided alias, which is a different thing from "the map gave it a storage". With the `alias=` and `why=` fields from the two commits below, one run now answers: which storage the map gave the view, whether it is its input's, whether the view's own `view_src` was registered, and what layout it describes. Coverage, stated plainly: like the previous commit, this field is not exercised by `test-hrx-view-alias-elision` - the host cases fail before a message is built, and `unsupported_node_message` is file-local. What the test does show is that the four pinned relations and the added case still behave. Stacked on `1bit/1467-alias-reason` (#262), itself stacked on #258 and #257. Merge in that order. Refs ggml-org#1467 * tests: reproduce engine ggml-org#1467's refused VIEW on the host, with the run's own numbers The projector run printed the facts this case encodes: VIEW output=f32[2048,2,1,1] nb=[4,32768,65536,65536] offs=8192 alias=not_aliasing_input(view_src_unregistered) storages=out24/in11 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), and the wide source is never registered as a value in the graph - which is why `find_alias_source`'s two tensor-identity lookups both miss and the view is given a storage of its own. `ViewKind::WideWindow` builds exactly that shape, so `test-hrx-view-alias-elision` now reproduces the refusal on a machine with no device: no-op alias, external input : plans no-op alias, produced input : plans offset alias, produced input: plans strided view, produced input: REFUSED <- refused at its CONSUMER wide window view (node 20) : REFUSED <- refused AT THE VIEW, as the projector is The assertion is `REQUIRE(!wide)`: that is today's behaviour and it is the defect. The fix is a densify dispatch for a layout-alias node that cannot be elided (copy the strided window into a contiguous transient - `dispatch-copy.cpp`'s append_f32_densify_dispatch already does this for permuted activations), and when it lands this line flips to `REQUIRE(wide)` - which makes the change visible in the test rather than inferred from a log line. Two traps for whoever writes it, recorded here because both produce a WRONG NUMBER rather than a failure: the copy must derive its source offsets from nb[]/offs (a densify that reuses the pre-densify plane offsets copies the wrong rows), and the transient must hold ne[0]*ne[1] contiguous elements of the same type. Refs ggml-org#1467 * hrx: drop the merge markers the previous commit carried, keeping this branch's superset block The pin has this message code as #258 landed it; this branch has it with the why=/facts additions, so the superset is the resolution and nothing from the pin is lost. The previous merge commit carried the markers themselves, which is why the tree did not compile - caught by rebuilding before pushing, not by review. * hrx: take the pin's AliasMiss block back, and re-apply only the facts on top of it The previous merge resolved this hunk to *this branch's* older version, which silently removed what the base added deliberately: the `AliasMiss` switch, i.e. the miss cause read from **assignment-time state**. That is the half an earlier review called blocking, and my resolution brought the forbidden re-ask of the map back with it. The reviewer caught it; this commit fixes it. Resolution now, per file: - `dispatch-scheduler.cpp`, `value-map.{h,cpp}`, `graph-diagnostics.cpp`: **the pin's versions** (AliasMiss + NoAliasSource/ExactViewSrc/RangeFallback, recorded when the storage is chosen); - and on top of the pin's block, only this branch's own addition: `storages=outN/inM nb=[…] offs=…`, which is the fact the six dead hypotheses needed. `why=` still comes from the pin's `AliasMiss`, never from a re-ask. - `<cinttypes>`/`<cstdio>` added for PRId64/snprintf, which the pin's file did not need. Rebuilt before committing: `ggml-hrx` links clean with -Werror, and the host test still pins the four relations plus `wide window view (node 20) : REFUSED`. Refs ggml-org#1467 --------- Co-authored-by: bcloud <bcloud@users.noreply.github.com>
…d (engine ggml-org#2068) The test did not compile from #262 until #293, so host rows drifted from changes that were made on purpose. #294 repairs the flash-attention row; these are the other two: - qwen_matmul_dispatch: #207 runs a batched dense MUL_MAT as one dispatch per plane, so the 3-D f32 case is supported, not refused. - unmatched_graph_diagnostics: #196's standalone SET_ROWS takes the i32-index graph the row used as its unmatched example. The row now uses a row width above that dispatch's limit, which every matcher still declines. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…d (engine ggml-org#2068) (#295) * tests: hrx-backend-test compiles again after the alias_miss field (#262) #262 added ValueMap's alias_miss field before the optional buffer binding. Two aggregate initialisers in hrx-backend-test still passed std::nullopt in that position, so the test has not compiled since. They now pass AliasMiss::None. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * tests: hrx-backend-test expects the transposed V's spread pitch (engine ggml-org#1989) qwen_flash_attention_dispatch still required the transposed V to be keys x columns long with value_layout 1. Since llama.cpp #289/#291 the buffer has the spread pitch (keys + 32 when the key count is a multiple of 64), the transpose kernel is told the pitch and the attention kernel derives it (value_layout 2). The dispatch is right; the expectation was stale, and a compile error in this file (fixed by #293) hid it. The check now asserts the pitch, the packed source binding, the transpose kernel's output_pitch and that the pitch is not a compile parameter of the attention kernel. host: 63 passed, 3 failed -> 64 passed, 2 failed (the two left, :1858 and :6333, are not from this change). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * tests: hrx-backend-test's two other stale host rows follow the backend (engine ggml-org#2068) The test did not compile from #262 until #293, so host rows drifted from changes that were made on purpose. #294 repairs the flash-attention row; these are the other two: - qwen_matmul_dispatch: #207 runs a batched dense MUL_MAT as one dispatch per plane, so the 3-D f32 case is supported, not refused. - unmatched_graph_diagnostics: #196's standalone SET_ROWS takes the i32-index graph the row used as its unmatched example. The row now uses a row width above that dispatch's limit, which every matcher still declines. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
…st check my #289 left stale; engine ggml-org#1989) (#294) * tests: hrx-backend-test compiles again after the alias_miss field (#262) #262 added ValueMap's alias_miss field before the optional buffer binding. Two aggregate initialisers in hrx-backend-test still passed std::nullopt in that position, so the test has not compiled since. They now pass AliasMiss::None. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * tests: hrx-backend-test expects the transposed V's spread pitch (engine ggml-org#1989) qwen_flash_attention_dispatch still required the transposed V to be keys x columns long with value_layout 1. Since llama.cpp #289/#291 the buffer has the spread pitch (keys + 32 when the key count is a multiple of 64), the transpose kernel is told the pitch and the attention kernel derives it (value_layout 2). The dispatch is right; the expectation was stale, and a compile error in this file (fixed by #293) hid it. The check now asserts the pitch, the packed source binding, the transpose kernel's output_pitch and that the pitch is not a compile parameter of the attention kernel. host: 63 passed, 3 failed -> 64 passed, 2 failed (the two left, :1858 and :6333, are not from this change). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
…) (#293) #262 added ValueMap's alias_miss field before the optional buffer binding. Two aggregate initialisers in hrx-backend-test still passed std::nullopt in that position, so the test has not compiled since. They now pass AliasMiss::None. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
Why
alias=not_aliasing_input(previous PR in this stack) says a layout-alias node's storage is not its input's. On engine ggml-org#1467's text tower that is where the investigation stops, because two very different causes print the same word and they need different repairs:view_src_unregisteredggml_tensorthe node'sview_srcpoints at is not a value in this graph, sofind_alias_source's pointer lookup cannot hit and the storage-root fallback has nothing to matchview_src_registered_but_storage_differsBoth now print distinctly:
find_tensor()andValue::tensorare both public (graph/value-map.h:100-101,:84), so this asks the same map the assignment asked instead of re-deriving anything — the same principle the reviewers applied to the previous commit in this stack.Coverage, stated plainly
The new branch is not exercised by
test-hrx-view-alias-elision. Every case there fails before the message is built (the strided case is refused at its consumer), andunsupported_node_messageis file-local, so the test cannot call it. What the test does show is that the change does not alter the four relations it pins. Exercising the new branch needs the projector on a device — the run that producedalias=not_aliasing_inputin the first place.Verification
libggml-hrx.solinks clean with-Werror(ninja -j 2).test-hrx-view-alias-elisionstill pins the four relations: no-op alias (external and produced) and offset alias plan, strided view is refused.Stacking
Base is
1bit/1467-view-alias-diag(the previous commit in this stack, itself stacked on #257). Merge order: #257 → this stack's first commit → this one.Refs ggml-org#1467