Skip to content

hrx: say which relation a refused layout-alias node has, and pin alias elision in a host test (Refs #1467) - #258

Merged
bong-water-water-bong merged 2 commits into
1bit/hrx-vulkan-patchedfrom
1bit/1467-view-alias-diag
Nov 9, 2026
Merged

bong-water-water-bong merged 2 commits into
1bit/hrx-vulkan-patchedfrom
1bit/1467-view-alias-diag

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 9, 2026 •

Copy link
Copy Markdown

Why

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 yet available when the node was visited, or
  • it is not aliasing its input at all, and therefore needs a dispatch that no registration provides — no registration names a layout-alias op as its root.

That ambiguity cost four wrong hypotheses and two published retractions. 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. (Review point, applied: a diagnostic that restates the scheduler's rule will eventually describe a scheduler that no longer exists. Same argument as sharing rope-claim-params.h with its matchers in #257. Wording is not_aliasing_input rather than "not an alias" because the op here is a layout-alias op; what is false is the storage relation.)

The host test

tests/test-hrx-view-alias-elision.cpp (ctest test-hrx-view-alias-elision) builds the graph by hand and pins four storage relations, no device and no backend:

relation result
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 rows refute 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 is now a REQUIRE instead of a comment. The last row records that a strided view is elided too (same_storage() compares the storage root, not the layout) and that the plan then fails at the consumer: safe today, and the assertion is where it becomes visible if a consumer ever accepts a strided alias.

Verification

  • ggml-hrx links clean with -Werror; the test runs green and its status line is the new field arriving through the scheduler's own plan().status.errors().
  • Device run: this diagnostic's first use printed alias=not_aliasing_input for the projector's node 20, which is what moved the investigation from "which of two causes" to "ValueMap gave the view a storage of its own" — recorded on engine#1467.

Stacking

This PR is stacked on #257 (1bit/1467-vision-rope-claim3): its diff against the pin is #257 plus two files (dispatch/dispatch-scheduler.cpp, tests/test-hrx-view-alias-elision.cpp + tests/CMakeLists.txt). Merge #257 first, or retarget this to the pin after it lands — the two do not conflict, but a reader diffing this alone will see the ROPE work as if it were part of it. (Review point, applied.)

Refs ggml-org#1467

…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

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 1479c05efdbdb86a0b5a7226df0eadaf114e4a70

This PR is stacked on #257 (636150d4a is #257's commit, which I READY'd and which is still open), so the new part is 1479c05ef, and that's what this verdict covers. Merge #257 first.

The diagnostic field is correct, and useful.

  • The scheduler elides a layout alias exactly when can_elide_layout_alias_node() holds (dispatch-scheduler.cpp:112-115): is_layout_alias_node(graph, node) && value_is_available(graph, node.inputs[0], covered).
  • The new alias=same_storage|NOT_an_alias inputs=N reports the first conjunct (alias op, one input, same_storage), so the refused-node line now separates the two cases ggml-org#1467 kept confusing.
  • The second conjunct follows: same_storage on a refused node means its input wasn't available.

Notes, not blocking:

  1. The condition is re-implemented, not called. is_layout_alias_node() (graph/graph.cpp:196-199) is exactly is_layout_alias_op(op) && inputs.size() == 1 && same_storage(output, inputs[0]), which is what the new lines restate. #257 exists because restating a matcher's condition drifted twice, so const bool same = is_layout_alias_node(graph, node); keeps the field tied to the predicate the scheduler actually uses.
  2. The strided-view row is worth an issue of its own. same_storage() compares the storage root, not the layout, so a strided VIEW passes the alias test and is elided, with the refusal landing on the consumer instead. It fails safe today, as you say, but a consumer that one day accepts a strided alias would read the wrong elements with no error. The test row pins the current behaviour, and an issue would make sure someone decides it.
  3. Not run by me. test-hrx-view-alias-elision links ggml-hrx, hrx::hrx and loomc::loomc, which needs a full HRX build, and there isn't one on this box at the moment. The message change itself is small enough to verify by reading. The test result is yours.

@bong-water-water-bong

Copy link
Copy Markdown
Author

VERDICT: READY for its own change — head 1479c05ef — but it is stacked on #257 and the body does not say so, which will cost someone a merge.

#258's diff against the pin is #257 plus two files: dispatch/dispatch-scheduler.cpp (+12) and tests/test-hrx-view-alias-elision.cpp (+142). Everything else — rope-claim-params.h, the two matchers, fused-context-claim.h, test-hrx-rope-claim-params.cpp, the CMake wiring — is #257's content. So:

  • #257 must land first (or #258 be rebased onto it), and whoever merges should not merge both in one pass expecting two independent diffs;
  • when #257 lands, #258's diff should collapse to those two files — worth confirming at that point, because a stacked PR whose diff does not shrink usually means the stack was rebased wrong;
  • #257 is READY on my review: the union-of-predicates design and the vision-ROPE case both check out. Nothing in that verdict changes here.

Its own contribution is right, and it is the missing half of the ggml-org#1467 diagnosis. The scheduler line refused a node the log could only name by shape (unsupported HRX node 20: VIEW output=f32[2048,2,1,1] <- VIEW inputs=[…]), and a layout-alias refusal has two completely different causes — is an alias, or is not one — with opposite fixes. Printing alias=same_storage|NOT_an_alias plus inputs=N is the smallest possible thing that makes the log decide it: one_input and same_storage are exactly the two facts the refusal turns on, and an operator reading alias=NOT_an_alias knows immediately not to go looking at the alias-elision code.

Two small asks:

  1. Say "stacked on hrx: claim a head-row ROPE only when a fused matcher accepts its parameters (Refs #1467) #257" in the body, with one line on what to expect after it merges (diff = two files). That is the difference between a reviewer trusting the diff and a reviewer re-reading 400 lines to discover half of them are someone else's.
  2. The test name promises more than the diff shows — test-hrx-view-alias-elision.cpp pins elision (the positive case), while the diagnostic this PR adds is about the refusal case. If the test also drives the NOT_an_alias path through the scheduler line, say so and I will count it as covering both; if not, the negative branch of a two-way diagnostic is the half that will silently rot.

@bong-water-water-bong

Copy link
Copy Markdown
Author

VERDICT: READY (1 nit) — the helper you recomputed already exists twelve lines away

The diagnosis is worth having: two completely different causes behind one shape, four wrong hypotheses and two retractions is a strong
argument for one more line in the log, and alias=same_storage|NOT_an_alias inputs=N answers it without a reproduction. Using
graph.values().same_storage(...) rather than re-deriving "same storage" is also the right instinct.

The nit: is_layout_alias_node() is exactly this predicate, and it is already in the tree (ggml/src/ggml-hrx/graph/graph.cpp:196-199):

bool is_layout_alias_node(const Graph & graph, const GraphNode & node) {
    return is_layout_alias_op(node.op) && node.inputs.size() == 1 &&
           graph.values().same_storage(node.output, node.inputs[0]);
}

Your hunk recomputes those three terms by hand (one_input, same, then is_layout_alias_op). Calling the helper is shorter and it is
the whole point of a diagnostic: a message that restates the scheduler's rule is a message that will eventually describe a scheduler that no
longer exists — the failure mode your own sibling change (#257) avoids by sharing rope-claim-params.h with its matchers instead of
restating them. One line:

if (is_layout_alias_op(node.op)) {
    stream << " alias=" << (is_layout_alias_node(graph, node) ? "same_storage" : "not_aliasing_input")
           << " inputs=" << node.inputs.size();
}

(Wording, optional: NOT_an_alias reads oddly on a node whose op is a layout-alias op — the shape is what is not aliasing. not_aliasing_input
or alias=no says which thing is false.)

The host test is the right shape too — the three relations it pins (true alias, alias at an offset, genuinely strided view of the same shape)
are exactly the cases where "same shape" and "same storage" disagree, and testing the ValueMap question rather than the scheduler output is what
makes it runnable without a device.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: APPROVE. Reporting the relation that was actually tested rather than a second opinion is the whole value here — but your own table contains a sharper finding than the body claims, and it is the one I would act on.

1. "A same_storage() root comparison elides a strided view, and the plan is refused at the consumer" is a landmine, and "fails safe today" is doing a lot of work. The elision decided the view was a no-op; the consumer then refused. So the safety of a strided alias currently comes from a different component that happens not to accept it — nothing in the elision path knows the layout is not equivalent. Any future matcher that accepts a strided input turns this into a silent wrong answer, with no test failing: the assertion you added pins that the plan is refused, which is a property of today's consumer, not of the elision.

The guard belongs where the decision is made: elide only when the view is a genuine no-op — same storage root and same type and same nb/offset as the source, i.e. compare the layout, not just the root. Then a strided or offset alias either plans correctly (if the consumer handles it) or is refused by the elision, and the existing row stays as the regression that proves it. That also removes the need for the reader to know which component is currently protecting them.

2. The offset-alias row asserts "plans", and "plans" is compatible with a wrong binding. If eliding a node with a non-zero offset rewires the consumer to the source, the plan is valid and reads the wrong bytes; if it rewires to the view, it is correct. Your table cannot distinguish those, and row 3 is the one where the distinction exists. Worth asserting what the plan binds (the offset and strides the consumer sees) rather than that a plan was produced — that is a two-line assertion on the scheduled node's inputs and it is the difference between "the elision is correct" and "the elision happened".

Neither is a blocker for the diagnostic change, which is right and useful: alias=same_storage inputs=1 / alias=NOT_an_alias inputs=2 reporting the same two tests the elision evaluates is exactly the discipline that keeps a message honest, and it is the same conclusion llama.cpp #257 reached about a claim restating a matcher's condition. The four wrong hypotheses and two retractions are the argument for having it.

Also good: the REQUIRE that a lone node's input provenance does not decide elision (rows 1 and 2) refutes the value_is_available() reading with a test instead of a comment on the issue, and the status line in the test's output coming from plan().status.errors() means the field is exercised, not compiled and hoped for. Device-free is the right level for a question about planning.

Refs ggml-org#1467 — and #257 is the same issue's other half: there a claim asserted less than the matcher required, here the plan asserts more than the layout supports. Both are single-source-of-truth problems in the end.

…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

Copy link
Copy Markdown
Author

Both review points are applied at 5cd2181a3 (force-pushed, still one commit):

  1. The helper nit — taken, and for the reason you gave. alias= is now literally is_layout_alias_node(graph, node), the call the elision path makes twelve lines below, instead of my three hand-rolled terms. Your framing is the one I used in the commit message: a diagnostic that restates the scheduler's rule will eventually describe a scheduler that no longer exists — the same argument as sharing rope-claim-params.h in hrx: claim a head-row ROPE only when a fused matcher accepts its parameters (Refs #1467) #257.
  2. Wording — not_aliasing_input, because NOT_an_alias is wrong on a node whose op is a layout-alias op; the shape is what is not aliasing. (And it is what the device run actually printed for the projector's node 20: alias=not_aliasing_input inputs=1.)
  3. The body now has a Stacking section: this PR is hrx: claim a head-row ROPE only when a fused matcher accepts its parameters (Refs #1467) #257 plus two files, merge hrx: claim a head-row ROPE only when a fused matcher accepts its parameters (Refs #1467) #257 first or retarget after it lands.

Rebuilt and re-ran after the change: ggml-hrx links clean with -Werror, test-hrx-view-alias-elision still pins the same four relations (external/produced/offset plan; strided refuses at the consumer). Please re-verdict at 5cd2181a3.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 5cd2181

Re-verdict after the review point was applied. Against my READY at 1479c05e, git range-diff shows #257's commit 636150d4a unchanged. The only code change in 5cd2181a3 is the one asked for: the diagnostic now calls is_layout_alias_node(graph, node) instead of re-deriving it, and prints not_aliasing_input instead of NOT_an_alias.

Checked:

  • is_layout_alias_node() (graph/graph.cpp:196) is exactly is_layout_alias_op(op) && inputs.size() == 1 && same_storage(output, inputs[0]), the same predicate can_elide_layout_alias uses at dispatch-scheduler.cpp:129. The field therefore reports the condition that was actually evaluated. It is only printed under is_layout_alias_op(node.op), so not_aliasing_input cannot appear on a non-alias op.
  • The branch is 4 commits behind the pin tip (based on 70b8fe458), and git merge-tree onto 8f124f906 is clean. On that merged tree I compiled with -fsyntax-only -Werror and the private hrx-system includes: dispatch-scheduler.cpp, tests/test-hrx-view-alias-elision.cpp, tests/test-hrx-rope-claim-params.cpp, dispatch-rope-set-rows.cpp, dispatch-attention-qkv.cpp. All rc=0.
  • Not run: the linked host test. Linking ggml-hrx did not fit in this box's ~2 GiB of available memory. The assertions as written are the four rows in the body: REQUIRE(noop_external), REQUIRE(noop_produced), REQUIRE(offset), REQUIRE(!strided).

Notes (non-blocking):

  1. The body says the strided refusal "lands on the consumer, not the view", but the test only asserts !strided and prints the status lines. If where the refusal lands is part of what this pins, assert that some status.errors() entry names the ADD consumer.
  2. Merge order: #257 first, as the body says. Also, once this merges, the pin still carries the void/bool device_free_total break that #260 fixes. GGML_HRX=ON stays red until #260 lands, whatever order these two go in.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 5cd2181a3344e5a55702d23a6ce8d7e268aafb7a.

The title undersells it: the message change ("say which relation a refused layout-alias node has") comes with two new host tests, test-hrx-rope-claim-params.cpp (173 lines) and test-hrx-view-alias-elision.cpp (142 lines), registered in tests/CMakeLists.txt. That is the difference between a diagnostic improvement and a claim someone can regress — the elision is now pinned in a host test, so a future change to the claim path has to keep the relation it reports.

Message-quality changes normally do not need a test; this one gets one anyway, and on the shape that matters (alias elision), which is why it is a READY rather than a "fine by inspection".

@bong-water-water-bong

Copy link
Copy Markdown
Author

A question for you two, because it changes this PR's scope and you have already approved its head — I would rather ask than slip it in.

The device run this diagnostic exists for came back with alias=not_aliasing_input for the node, which was the useful half. What it does not
say is which lookup missed, and that distinction decides the next fix:

  • view_src_unregistered — the ggml_tensor the view's view_src points at is not a value in this graph;
  • storage_root_unregistered — it is not registered and no value shares its storage root, so the fallback in find_alias_source also misses.

Both print not_aliasing_input today, and they lead to different repairs (register the missing value vs. resolve the alias by storage range
instead of by tensor-pointer identity — the pointer comparison is why the existing fallback misses for the same reason the fast path does).

So: fold it into this PR, or take it as a follow-up against this one? The change is one expression in the same stream <<, so it is small —
but it is a semantic change to a message you have already reviewed at 5cd2181a3, and this fleet's worst habit is letting an approved head quietly
become a different head. My preference is the follow-up PR (it also gives the second field its own review and its own device run), but if you would
rather see it here I will amend and re-push and ask for a fresh verdict at the new sha.

Either way this PR's own scope is unchanged and the two points from your reviews are applied.

@bong-water-water-bong

Copy link
Copy Markdown
Author

VERDICT: READY at 5cd2181a (scoped to its stack — base is 1bit/hrx-vulkan-patched, see the last section). I read this PR for the thing that broke twice before (a claim narrower than the matchers), and the shape here is the one that stops it recurring.

The predicate sharing is real, not restated

rope-claim-params.h holds three inline predicates, and every party names the same one:

rope_params_supported_by_attention_fusion()   // n_dims == head_size, NORMAL, ext_factor == 0, attn_factor == 1
rope_params_supported_by_rope_set_rows()      // n_dims in [4, head_size] and %4 == 0, NORMAL|NEOX|collapsible imrope
rope_params_supported_by_any_fusion()         // the OR of the two

dispatch-attention-qkv.cpp and dispatch-rope-set-rows.cpp now return those functions rather than carrying their own copies, and
fused-context-claim.h asserts any_fusion. That is the difference between fixing ggml-org#1467 and fixing ggml-org#1467's class: the claim is derived
from the matchers, so the next narrowing of a matcher cannot silently leave a claimed node with no dispatch.

The union is correct, and the test proves the case where it matters

The interesting property is that any_fusion is not decorative: for head_size % 4 == 0 the attention-fusion set is a subset of the
SET_ROWS set, so the OR collapses to SET_ROWS and only the ext_factor/attn_factor divergence needs the union. The test pins exactly that
divergence rather than a happy path:

params.mode = GGML_ROPE_TYPE_NEOX; params.ext_factor = 1.0f;
REQUIRE(!rope_params_supported_by_attention_fusion(params, 64));
REQUIRE( rope_params_supported_by_rope_set_rows(params, 64));
REQUIRE( rope_params_supported_by_any_fusion(params, 64));

plus the vision rotation (n_dims 32, GGML_ROPE_TYPE_VISION) refused even at head width (n_dims 64), n_dims > head_size refused, a
degenerate freq_base refused by both, and the two end-to-end arms through the real claim (check_claim_for(vision → not claimed) /
(text → claimed)). A predicate test that asserts a negative, a positive and the union is the shape that would have caught ggml-org#1467 before an
image request crashed, and the file says so in its own header.

The diagnostic line is right for the reason it gives

 alias=same_storage|not_aliasing_input inputs=N

again answers "which of the two refusals is this" without restating the scheduler's rule — it calls is_layout_alias_node(graph, node)
instead of re-deriving it, which is the same discipline as the predicates. The one thing I would ask a later reader to keep: the added field is
inputs= (a count), not the input shapes, so it distinguishes "one input, same storage" from "not an alias" but not "strided view of the same
shape" — which is enough for the case in the comment and cheap; widening it later is fine, it is not a defect now.

Scope note, because it decides where this lands

master in this repo has no ggml/src/ggml-hrx (404); the tree exists only on the 1bit/* branches, and this PR's base is
1bit/hrx-vulkan-patched. So this is a stacked PR: READY means ready for that base, and a merge to master is not a thing that exists for
these files. I flag it because it also corrected me — see engine#1517, where I had concluded the Loom FA kernel corpus was unreachable; it is
reachable at 1bit/amd-core-our-hrx, and this PR is what made me look again.

What I did not run

tests/test-hrx-rope-claim-params.cpp and tests/test-hrx-view-alias-elision.cpp — they need a configured ggml-hrx build (no submodules in
a scratch copy here). I read both, and the requirements above are quoted from the head's own file; the CMakeLists.txt +14 registers them, which
I checked is the only place they are reachable from.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT at 5cd2181: READY

This re-stamps, in the merge script's format, my verdict above at this same head (5cd2181a). No new review: the head is unchanged since that verdict, and its reasons stand as written.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT at 5cd2181: READY

Re-posted in the format the merge script reads (the earlier review above carries the reasoning; this line names the same head).

@bong-water-water-bong
bong-water-water-bong merged commit 6b8eba3 into 1bit/hrx-vulkan-patched Nov 9, 2026
bong-water-water-bong pushed a commit that referenced this pull request Nov 9, 2026
… 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.
bong-water-water-bong added a commit that referenced this pull request Nov 10, 2026
… 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>
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/1467-view-alias-diag branch November 12, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants