Repository navigation
tests: pin the MUL_MAT_ID activation range the f16 narrowing handles (Refs engine#2127) - #307
bong-water-water-bong wants to merge 11 commits into
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
`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
… 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
…h 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
…STER/llama.cpp into 1bit/1467-alias-storages # Conflicts: # ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp # tests/test-hrx-view-alias-elision.cpp
… 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.
…STER/llama.cpp into 1bit/1467-alias-storages # Conflicts: # ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp
… 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
The prefill matcher required the q/k/v dim-2 stride to be exactly packed (`nb[2] == hidden*4`), so a q/k norm fed by a
view whose token step is wider than `nb[1]*ne[1]` fell through and the node was left uncovered while its consumer was
claimed by the fused dispatch:
E graph_compute: unsupported HRX node 160: L2_NORM output=f32[128,16,512,1]
hip.l2_norm_f32: declined (consumer is GATED_DELTA_NET (left to the fused matcher))
x ne=[128,16,512,1] nb=[4,512,40960,20971520] off=0 <- nb[2] = 40960 against 8192, 5x
The token stride is now bounded below and required to stay float-aligned instead of being required to be packed. That
is not a new rule: the DECODE matcher in this same file already states it for v --
v->nb[2] < static_cast<size_t>(width * head_count) * sizeof(float) || v->nb[2] % sizeof(float) != 0
-- so the prefill guard is brought into line with the proof the file already relies on, rather than inventing one.
Nothing else relaxes: `nb[0]`, `nb[1]`, the shared storage, the three storage offsets and `raw_k->nb == raw_q->nb`
still hold exactly as before, which is what keeps the relaxation to "the token dim may be strided" and not "the layout
may differ".
Verified: `libggml-hrx.so` builds clean with -Werror. NOT verified by me, and stated in the PR: the placement census
(engine#1925 asks for 2 nodes off HRX / 2 splits) and the pin's 12.14 tok/s decode / 371.6 prefill at 18,782 tokens
row -- a matcher relaxation that completes but computes wrongly passes the census and shows up only there, so the
device row is the reviewer's gate, not something I am claiming.
Refs: engine#1925, engine#679
…dles (engine#2127) The routed gate/up motifs narrow the f32 activation to f16 with no prescale, and every MUL_MAT_ID fixture filled its activation uniform in [-1, 1] -- where that is essentially lossless (~11 mantissa bits) and the CPU arm narrows identically. So the working range was never pinned and the broken one was never reachable. `init_mul_mat_id_tensors` takes an activation scale now (default 1.0f, keeping every pre-existing case byte-identical) and one row reuses a shape the same sweep reports OK -- bf16 x f32, n_mats=4, n_used=2, m=512, n=32, k=256 -- with activations uniform in [-10000, 10000]: activation_scale=10000 : OK (measured on HRX0 at the pin, 7a1f2b8) That is the range the narrowing does handle, and pinning it is what keeps a future motif fix from buying the large case by breaking the ordinary one. The LARGE case is measured BAD today and is deliberately not a row: with the same shape and scale 1e5 (beyond f16's 65504) HRX0 returns -nan where the CPU arm returns -165957.6875, `857/858`, `Backend HRX0: FAIL` -- engine#2127, posted with the numbers. It lands with the kernel fix in mul_mat_id_*_f16_wmma_projection.loom, or as expected-fail; the harness has no xfail hook, so adding it now would only red the suite. Not verified in this exact tree: the suite TOTAL with the failing row absent (a harness processes rows independently, so 858/858 with `Backend HRX0: OK` follows mechanically from the row measured above, but it is an inference and not a run -- the confirmation attempt never left the GPU queue). Refs engine#2127
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 33f72d6: READY
Pinning the activation range that the f16 narrowing has to handle is the test the 2⁻⁶ prescale change (#285/#298) needed: the fix keeps activations inside f16's range before the truncation, and a test that asserts the range is what stops a future change from quietly moving the boundary back out. A test on the resulting values would pass on this data and fail on the first larger activation; a test on the range states the invariant.
Adds <cinttypes>/<cstdio> for the diagnostic, which is the pattern this fork has settled into — report the value that decided it.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 33f72d6: CHANGES
Reviewed head 33f72d6 (refs engine#2127). This is the first verdict. The change itself is small and the reasoning is right: a scale argument defaulting to 1.0f, so every existing case stays byte-identical, plus one positive-control row at [-10000, 10000]. The branch it sits on is the problem.
It conflicts with its base, because it carries another PR. GitHub shows mergeable_state: dirty. git merge-tree of this head onto 1bit/hrx-vulkan-patched (204493d40) gives CONFLICT (content): Merge conflict in tests/test-hrx-view-alias-elision.cpp. The branch is 11 commits past its merge base, and only the last one, 33f72d6 (tests/test-backend-ops.cpp +31/−5), is this PR's change. The other ten are #301's: a1b74a0cf "let the fused GATED_DELTA_NET take a strided q/k norm" is in #301 (open, not merged), and 636150d4a/5cd2181a3 are in #257/#258, merged, and in #301. That is why the file list shows dispatch-scheduler.cpp, dispatch-gated-delta-net.cpp and test-hrx-view-alias-elision.cpp, which a test-only PR does not touch.
The fix is mechanical. 33f72d6 alone applies cleanly onto the current base: git merge-tree --merge-base 33f72d6^ 204493d40 33f72d6 gives rc 0. So either:
- rebase onto
1bit/hrx-vulkan-patchedwith just that commit (git rebase --onto origin/1bit/hrx-vulkan-patched 33f72d6^), or - if it is meant to land after #301, set the base to #301's branch and say "stacked on #301" in the body, so the diff a reviewer sees is the 31 lines.
Not run by me: the HRX0 row (activation_scale=10000 : OK, and the excluded 100000 NaN). That needs the device and a fork build.
|
The suite total is measured now, and it corrects my own inference: Run on HRX0 at the pin, against exactly this branch's tree: The count I predicted in the body was wrong by one, and the reason is worth stating because it is the same mistake in miniature that this PR is about: I wrote "858/858 What that changes about this PR: nothing in the diff — the row is the same row — but the claim in the body is now a measurement rather than an inference, and the correction Full evidence trail, for anyone re-running: the gate took four invalid attempts before it meant anything — no HRX device (the harness refused to report a pass), a rejected |
|
VERDICT: READY at One thing that decides whether the new arm discriminates, and it is a rounding fact I got wrong myself today: So: if the new arm picks a scale "just above 65504", check where it actually lands — 65505…65520 are all inside the interval that rounds to the largest finite f16, and a test there passes with or without the narrowing being fixed. Worth a comment in the fixture saying which side of 65520 each scale sits on, since the next person will reach for 65505. Two more, both from testing the sibling fix today:
And the follow-through this row enables: a scale at 65521 on |
|
VERDICT: READY at Why per-case beats a wider defaultThe default fill cannot fail on this defect: inside Two things to pin while you are in the fixture
On the title"pin the MUL_MAT_ID activation range the f16 narrowing invalidates" is the correct description of a fixture change: it names the range as the subject. Worth one line in the body saying what the old default measured (nothing, by construction) — that is the sentence a future reader needs before "simplifying" the scale back to 1.0. |
…gine ggml-org#2167) (#315) test_mul_mat_id gains an activation scale (default 1, so every earlier case is unchanged; only the activation `b` is scaled). Rows at 1e4 and 1e5 for bf16, f32 and Q4_K weights at a shape the sweep already runs, and bf16 at one token. The routed kernels narrow the activation at 2^-6 since llama.cpp #285, so the rows pass; they pin that. Supersedes the test rows of llama.cpp #307, whose parameter also scaled f32 weights. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
Why
The routed gate/up motifs narrow the f32 activation to f16 with no prescale (engine#2127), and every
MUL_MAT_IDfixture filled its activation uniform in[-1, 1]— where that is essentially lossless (~11 mantissa bits, so ≤ 1 loses ~1e-4 relative) and the CPU arm narrows identically. So the range that works was never pinned, and the range that breaks was not reachable by any row.What
init_mul_mat_id_tensorstakes an activation scale now — default1.0f, which keeps every pre-existing case byte-identical — and one new row reuses a shape the same sweep already reports OK (bf16 × f32, n_mats=4, n_used=2, m=512, n=32, k=256), with activations uniform in[-10000, 10000]:Same shape, same weights, same everything except magnitude — so a failure here would be about the range and nothing else. Pinning it is what stops a future motif fix from buying the large case by breaking the ordinary one.
What is deliberately NOT a row
The large case is measured bad today, and stated in the comment instead:
That is engine#2127's failure reproduced in one command with a positive control beside it — posted on the issue with the numbers. It is not in this PR because the harness has no xfail hook (
test_case's virtuals areop_desc,vars,build_graph,max_nmse_err,max_maa_err), so adding it now would only red the suite. It belongs with the kernel fix inmul_mat_id_*_f16_wmma_projection.loom, or as an expected-fail row once such a hook exists.Verification, and the one inference
1e4row's result is measured (above).858/858withBackend HRX0: OKfollows from the row measured above — but my confirmation attempt never left the GPU queue, and I am not going to present an inference as a run.Four earlier attempts at this gate each produced something that looked like a pass and proved nothing (no HRX device; a rejected
systemdproperty; rows inside#if 0; an unsupported shape). The reason the numbers above are trustworthy is that each of those was ruled out first.Refs engine#2127