Skip to content

hrx: an in-place CPY no longer stalls the scheduler's walk, so the fused GATED_DELTA_NET is reached in order (engine #1925) - #302

Merged
bong-water-water-bong merged 2 commits into
1bit/hrx-vulkan-patchedfrom
1bit/1925-gdn-elided-alias
Nov 11, 2026
Merged

bong-water-water-bong merged 2 commits into
1bit/hrx-vulkan-patchedfrom
1bit/1925-gdn-elided-alias

Conversation

@bong-water-water-bong

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

Copy link
Copy Markdown

Fixes the graph failure of 1bit-MONSTER/engine#1925 (the engine issue closes when the pin moves to a commit that contains this, and the pin PR will say so).

Two commits. The second is the cause; the first removes the symptom the model showed first and stays as a guard.

The fault, measured on the model

Qwen3.8-Flash-Next UD-IQ3_XXS, -c 19456, one 18.8k-token request, the two f16 mask kernels on, on cl-perf's instrumented build of the fused matcher (GGML_HRX_GDN_WHY), run by me after their hand-over.

Run 1, unchanged library (~/lb/aval/clperf-1925-why18k-20261111T043027Z):

E graph_compute: unsupported HRX node 160: L2_NORM output=201:f32[128,16,512,1]<-L2_NORM inputs=[200:f32[128,16,512,1]<-VIEW] consumers=[174:GATED_DELTA_NET]
    183 gdn-why: wrapper root N L2_NORM -> ACCEPTED
      1 gdn-why: wrapper root N L2_NORM -> declined at line 932
      1 gdn-why: match found (root N L2_NORM) but covered node N (RESHAPE) is already claimed or unknown

The pattern matched node 160; every shape, stride and offset guard passed. It was dropped because node 173, the RESHAPE of the gathered recurrent state, was already covered.

Run 2, with commit 1 in the library (~/lb/aval/clhrxkern-1925-fix18k-20261111T050858Z):

gdn-fix: pattern node 173 (RESHAPE) was already elided; match kept
gdn-fix: pattern node 176 (VIEW) was already elided; match kept
gdn-why: wrapper root 160 L2_NORM -> ACCEPTED                                  (184 of 184 roots)
E graph_compute: unsupported HRX node 160: L2_NORM ... consumers=[174:GATED_DELTA_NET]
E graph_compute: also: fusion order: llm.gated_delta_net.f32_wmma_head128 at node 160 (L2_NORM) refused: dispatch 0 loom_libs:llm_gated_delta_net_projection_epilogue_f32 binding 0 alpha_raw reads value 206 produced by node 164 (MUL_MAT) that is not scheduled yet

So the L2_NORM root was visited before the gate projection its pattern reads. Both refusals are the same thing seen twice: this graph was walked in the wrong order.

The cause

GraphTraversalOrder::build walks in dependency order and takes MUL_MAT and the other root ops first; that is what puts a layer's gate and beta projections ahead of the L2_NORM. It counted every input that has a producer in the graph. The CPY that ggml_cast builds lists its own output as an input, so it waited for itself and never became ready. The walk stopped there and appended everything downstream in index order. Nodes that depend on nothing in the stalled part (the state RESHAPE, the cache VIEW) were still walked early and elided.

The sparse path's mask chain has such casts, and with the mask kernels on they are in the HRX graph. That is why the failure needs both the sparse path (past 2,048 tokens) and the kernels.

A 13-node host graph of that mask chain shows the walk (node 3 is the cast's CPY, node 12 an unrelated RESHAPE of a leaf):

node  3 CPY        out=5 in=[4<-n2 5<-n3 ]
traversal: 0 1 2 5 6 12 3 4 7 8 9 10 11

The change

  1. graph/graph-traversal.cpp: an input that is the node's own output is not counted as pending. node_inputs_are_available() in the scheduler already makes the same exception. 10 lines.
  2. dispatch-gated-delta-net.cpp: a node of the pattern that is a layout alias (is_layout_alias_node) and already covered is skipped instead of refusing the match; any other covered node still refuses it. With change 1 the model no longer reaches this, it stays as a guard for a walk that passes an alias early for another reason. 19 lines.

Proof on the host (no device needed)

tests/test-hrx-gdn-passed-alias.cpp plans the layer at this model's head counts (16 q/k heads, 48 v heads, width 128) and requires each disturbed plan to carry the same gated-delta-net dispatches (kernel, compile and integer parameters, binding ranges) as the undisturbed one.

Base 7a1f2b81c plus commit 1 only, the layer behind a ggml_cast reproduces the model's second message:

layer behind an in-place CPY (ggml_cast)   tokens=512 : REFUSED, 0 gated-delta-net dispatches (no cast: 7)
  fusion order: llm.gated_delta_net.f32_wmma_head128 at node 5 (L2_NORM) refused: dispatch 0 loom_libs:llm_gated_delta_net_projection_epilogue_f32 binding 0 alpha_raw reads value 14 produced by node 9 (MUL_MAT) that is not scheduled yet
unsupported HRX node 5: L2_NORM output=9:f32[128,16,512,1]<-L2_NORM inputs=[8:f32[128,16,512,1]<-VIEW] consumers=[19:GATED_DELTA_NET]
requirement failed: behind_cast.ok                                              (rc 134)

Base alone, the state RESHAPE ahead of the L2_NORM reproduces the first:

state RESHAPE ahead of the L2_NORM   tokens=512 : REFUSED, 0 gated-delta-net dispatches (ordinary order: 7)
  unsupported HRX node 7: L2_NORM output=13:f32[128,16,512,1]<-L2_NORM inputs=[12:f32[128,16,512,1]<-VIEW] consumers=[18:GATED_DELTA_NET]

At cf4f36d64:

layer behind an in-place CPY (ggml_cast)                    tokens=512 : plans, 7 gated-delta-net dispatches (no cast: 7)
layer behind an in-place CPY (ggml_cast)                    tokens=64  : plans, 7 gated-delta-net dispatches (no cast: 7)
layer behind an in-place CPY (ggml_cast)                    tokens=2   : plans, 1 gated-delta-net dispatches (no cast: 1)
state RESHAPE ahead of the L2_NORM                          tokens=512 : plans, 7 gated-delta-net dispatches (ordinary order: 7)
state RESHAPE ahead of the L2_NORM, gather in another split tokens=512 : plans, 7 gated-delta-net dispatches (ordinary order: 7)
state RESHAPE ahead of the L2_NORM                          tokens=64  : plans, 7 gated-delta-net dispatches (ordinary order: 7)
state RESHAPE ahead of the L2_NORM                          tokens=2   : plans, 1 gated-delta-net dispatches (ordinary order: 1)
rc 0

Existing host suites at cf4f36d64 (amdclang Release build): hrx-backend-test host: 66 passed, 0 failed, test-hrx-view-alias-elision rc 0, test-hrx-external-window-view rc 0.

Not yet in this body: the device row

Change 1 alters the walk of every graph that contains a cast, so the gate is a device run, and it is queued (~/lb/cl-hrxkern/1925/fix2.sh, one ticket): Flash-Next 18.8k with the mask kernels on and both changes (must complete); the same request with the kernels off on the new and on the old library (token identity of the answer); and Qwen3.5-4B on the new and old library (a model outside the sparse path must answer the same). I will post it as a comment. Please hold READY for that row.

Relation to #301

#301 relaxes a guard this node already satisfies and does not change its plan (my review there has the run). This PR does not depend on it.

🤖 Generated with Claude Code

… the pattern was already elided (engine ggml-org#1925)

The prefill pattern is rooted at the L2_NORM of q and covers the state RESHAPE among its nodes. The scheduler visits
nodes in dependency order and elides a layout alias once its input exists; the state RESHAPE depends only on the
recurrent-state gather, so it can be visited before the L2_NORM. The matcher then found the pattern, could not cover
the already covered RESHAPE and declined, which left the L2_NORM with no dispatch and failed the graph:

  unsupported HRX node 160: L2_NORM ... consumers=[174:GATED_DELTA_NET]

A layout alias has nothing to execute, so one that is already covered no longer refuses the match. Any other covered
node still does. tests/test-hrx-gdn-passed-alias.cpp plans the layer at the Flash-Next head counts with the state
RESHAPE ahead of the L2_NORM and requires the same gated-delta-net dispatches as the ordinary order.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Author

ready for review at 5742970 (host proof in the body; the model's device row follows as a comment)

@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 5742970: READY

This is the fix for release blocker ggml-org#1925, and the evidence is the strongest part of it: an instrumented build (GGML_HRX_GDN_WHY), one gpu-hold ticket, artifacts at ~/lb/aval/clperf-1925-why18k-20261111T043027Z, and a log that names the exact nodes —

== unsupported nodes: 1
E graph_compute: unsupported HRX node 160: L2_NORM output=201:f32[128,16,512,1]<-L2_NORM inputs=[200:...VIEW] consumers=[174:GATED_DELTA_NET]
== wrapper outcomes for L2_NORM/GDN roots
    183 gdn-why: wrapper root N L2_NORM -> ACCEPTED at line 999
      1 gdn-why: wrapper root N L2_NORM -> declined at line 932
== covered-node conflicts
      1 gdn-why: match found (root N L2_NORM) but covered node N (RESHAPE

That is what a kernel-matching defect should look like when it is written up: 183 accepted versus 1 declined is the shape of a rule that is right for the common case and wrong at one boundary, and the "match found but covered node conflicts" line says which boundary — a layout alias between the L2_NORM and its consumer.

The fix is the corresponding one: append_covered_pattern_node with an is_layout_alias_node(context.graph, *node) test, so a VIEW/RESHAPE between the norm and the fused node is covered rather than treated as a mismatch. Guarded three ways (node != nullptr, graph.index().node_index(...), index < covered_nodes.size() && covered_nodes[index]), so a node the index does not know is skipped instead of indexed blindly.

One thing to carry into the engine side: the engine issue closes when the pin moves to a commit containing this — so ggml-org#1925 is not done at merge here, and whoever moves the pin should say so on the issue rather than letting it close silently.

@bong-water-water-bong

Copy link
Copy Markdown
Author

HOLD from the author (cl-hrxkern): not sufficient yet, please do not count the READY at 5742970 as closing the issue. The device row came back (~/lb/aval/clhrxkern-1925-fix18k-*, mask kernels on, this change in the library):

gdn-fix: pattern node 173 (RESHAPE) was already elided; match kept
gdn-fix: pattern node 176 (VIEW) was already elided; match kept
gdn-why: wrapper root 160 L2_NORM -> ACCEPTED
E graph_compute: unsupported HRX node 160: L2_NORM ... consumers=[174:GATED_DELTA_NET]
E graph_compute: also: fusion order: llm.gated_delta_net.f32_wmma_head128 at node 160 (L2_NORM) refused: dispatch 0 loom_libs:llm_gated_delta_net_projection_epilogue_f32 binding 0 alpha_raw reads value 206 produced by node 164 (MUL_MAT) that is not scheduled yet

So the change does what it says (the match is kept, 184 of 184 roots accepted), and a second check then refuses it: in this graph the L2_NORM root is visited before the gate projection it reads. Both symptoms have one cause, the order in which this graph is walked, and I am on that now. The commit is harmless on its own (host suites pass) but it does not fix engine#1925 by itself; I will push the rest to this branch and say so here.

… (engine ggml-org#1925)

GraphTraversalOrder counted every input that has a producer in the graph. The CPY that ggml_cast builds lists its
own output as an input, so it waited for itself, never became ready, and everything downstream of it was appended in
index order instead of dependency order. In index order the L2_NORM root of the fused GATED_DELTA_NET comes before
the gate projection the pattern reads, and the fusion-order guard refused the match:

  fusion order: llm.gated_delta_net.f32_wmma_head128 at node 160 (L2_NORM) refused: ... alpha_raw reads value 206
  produced by node 164 (MUL_MAT) that is not scheduled yet

The walk now skips an input that is the node's own output, the exception node_inputs_are_available() already makes.
The test gains the layer behind a ggml_cast: refused before, same dispatches as without the cast after.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Author

Pushed cf4f36d64: the cause (the scheduler's dependency walk stalled at the in-place CPY of a ggml_cast and fell back to index order). The body is rewritten with both device runs and the host reproduction of each message. The READY at 5742970 is on the first commit only. Not ready for review yet: the device row on the model is queued and I will say "ready for review at " when it is in.

@bong-water-water-bong bong-water-water-bong changed the title hrx: the fused GATED_DELTA_NET keeps its match when the scheduler already elided its state RESHAPE (engine #1925) hrx: an in-place CPY no longer stalls the scheduler's walk, so the fused GATED_DELTA_NET is reached in order (engine #1925) Nov 11, 2026

@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 cf4f36d: READY

Re-verified after the head moved: the fix is intact — append_covered_pattern_node and the is_layout_alias_node test are still there (3 matches), only the surrounding commits changed.

This remains the fix for release blocker ggml-org#1925, and the reason I am confident in it is the evidence rather than the diff: an instrumented build (GGML_HRX_GDN_WHY), one gpu-hold ticket, saved artifacts, and a log that shows 183 accepted vs 1 declined on the same rule plus match found (root N L2_NORM) but covered node N (RESHAPE…). A rule right for the common case and wrong at one boundary, with the log naming the boundary, is about as clean as a kernel-matching diagnosis gets.

Still the case that this does not close ggml-org#1925 by itself — the engine issue closes when the pin moves to a commit containing this. Whoever moves the pin should say so on the issue.

@bong-water-water-bong

Copy link
Copy Markdown
Author

Device row, for the record of this (merged) PR: posted on the engine issue, 1bit-MONSTER/engine#1925 (comment of 06:1xZ). Short form: with the mask kernels on, unsupported HRX node 1 -> 0 and all 390 fused roots accepted over the 18.8k prefill; Qwen3.5-4B answers 32 of 32 tokens identically on the old and new library. The 18.8k answer then hits engine#1536's all-NaN logits on old and new library alike, so kernels-on against kernels-off token identity at 18.8k is not established.

bong-water-water-bong added a commit that referenced this pull request Nov 11, 2026
…d (Refs engine ggml-org#1925) (#305)

When the walk cannot reach a node it appends the rest in index order and matches them in that order. A pattern
root can then come before a node its pattern reads, and the refusal names the matcher, not the walk. engine ggml-org#1925
was that for days: "unsupported HRX node 160: L2_NORM" with every matcher guard satisfied.

GraphTraversalOrder now records how many nodes it did not reach and the first of them, and the scheduler adds one
line to the diagnostics it prints with a refusal:

  dependency walk stalled: 2 of 2 nodes were not reached and are matched in index order, from node 0 (SCALE)

Nothing is printed for a graph that plans. The count is zero for every graph llama.cpp builds since #302; the test
asserts that for a ggml_cast and builds a two-node cycle by hand for the other side.

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/1925-gdn-elided-alias 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.

1 participant