Skip to content

ggml-hrx: IQ3_XXS routed experts take the table decode by default, with routed-shape test cases (engine #572) - #281

Merged
bong-water-water-bong merged 1 commit into
1bit/hrx-vulkan-patchedfrom
cl-perf/572-iq3s-table-default
Nov 10, 2026
Merged

bong-water-water-bong merged 1 commit into
1bit/hrx-vulkan-patchedfrom
cl-perf/572-iq3s-table-default

Conversation

@bong-water-water-bong

Copy link
Copy Markdown

Part of 1bit-MONSTER/engine#572 (ask 2). Not "Fixes": the model-level cell below is still owed.

What changes

  • GGML_HRX_IQ3S_TABLE_DECODE (the table decode from ggml-hrx: read the IQ3_XXS grid code from a workgroup table instead of a 256-way select chain (engine #572) #216, documented in engine docs/hrx.md) was opt-in, and when set it swapped the routed gate/up kernel for every format pair. Now the table decode is selected by default, and only when the gate or up weight is IQ3_XXS. Any other format pair runs the kernel it ran before, whatever the variable says.
  • GGML_HRX_IQ3S_TABLE_DECODE=0 puts IQ3_XXS back on the select chain (the A/B arm and the way out).
  • One log line states the decode a run used (ggml_hrx: IQ3_XXS routed gate/up decode: workgroup table). Both kernels sit under one registration name, so a dispatch log could not tell them apart.
  • tests/test-backend-ops.cpp: four MUL_MAT_VEC_FUSION cases at the Qwen3.8-Flash-Next routed prefill shape (2560 -> 640, 10 routes per token, one broadcast input row, 80 and 512 tokens) for IQ3_XXS and IQ4_XS. Until now no case in the suite compared the IQ3_XXS routed decode with the CPU at all: the existing fusion cases are m=1,n=32,k=256 over base_types, which has no IQ3_XXS.
  • GGML_TEST_PRINT_ERR=1 prints the measured error of every compared value. These cases allow 5e-3, so "OK" alone does not say how close the backend was.

Proof (strixhalo gfx1151, this head, one gpu-hold claim, 21 s)

test-backend-ops test -b HRX0 -o MUL_MAT_VEC_FUSION -p "n=640,k=2560", default and then GGML_HRX_IQ3S_TABLE_DECODE=0:

arm=table rc=0
ggml_hrx: IQ3_XXS routed gate/up decode: workgroup table
[MUL] err = 3.11607496e-05 (max 0.005)   MUL_MAT_VEC_FUSION(type=iq3_xxs,glu_op=2,m=80,n=640,k=2560,use_id=1,n_mats=16,n_used=10,b=1,...): OK
[MUL] err = 3.09272489e-05 (max 0.005)   MUL_MAT_VEC_FUSION(type=iq3_xxs,glu_op=2,m=512,n=640,k=2560,use_id=1,n_mats=16,n_used=10,b=1,...): OK
[MUL] err = 3.11882333e-05 (max 0.005)   MUL_MAT_VEC_FUSION(type=iq4_xs,glu_op=2,m=80,n=640,k=2560,...): OK
[MUL] err = 3.10650068e-05 (max 0.005)   MUL_MAT_VEC_FUSION(type=iq4_xs,glu_op=2,m=512,n=640,k=2560,...): OK
  4/4 tests passed
arm=chain rc=0
ggml_hrx: IQ3_XXS routed gate/up decode: select chain (GGML_HRX_IQ3S_TABLE_DECODE=0)
[MUL] err = 3.09198766e-05 (max 0.005)   MUL_MAT_VEC_FUSION(type=iq3_xxs,...,m=80,...): OK
[MUL] err = 3.09166662e-05 (max 0.005)   MUL_MAT_VEC_FUSION(type=iq3_xxs,...,m=512,...): OK
[MUL] err = 3.10423088e-05 (max 0.005)   MUL_MAT_VEC_FUSION(type=iq4_xs,...,m=80,...): OK
[MUL] err = 3.10508465e-05 (max 0.005)   MUL_MAT_VEC_FUSION(type=iq4_xs,...,m=512,...): OK
  4/4 tests passed

Run per format, each selects common.mul_mat_id_swiglu.f32_f32_wmma (fused).

Before this change, on a build of cef5e2e20 (kernel, matcher and motifs are byte-identical to the engine pin 5514ebea4): the whole existing MUL_MAT_VEC_FUSION set is 374 OK / 0 FAIL with the variable unset and with =1.

What this does not show

  • The two arms are not compared bit for bit. The test draws fresh random inputs per run, so the two errors (3.09e-5 vs 3.12e-5) are the same size, not the same number. Each decode agrees with the CPU to ~3e-5 NMSE at this shape; that is the claim.
  • No model-level number at this head. The paired prefill cells in engine docs/hrx.md (1.18x-1.29x, PPL identical to four decimals) were taken on 2026-11-06 on a commit before hrx: SET_ROWS keeps the rows it does not name, and publishes its host copy before the graph returns #227. The long-prompt cell on UD-IQ4_XS at the current pin has not been re-run: it needs the 89 GiB file loaded and the box has not had the memory today. That row is what remains before engine#572's ask (2) can be called done.
  • The UD-IQ3_XXS file has no IQ3_XXS tensors (its routed experts are IQ2_S), so this default changes nothing for it. It applies to files whose expert gate/up are IQ3_XXS, such as UD-IQ4_XS.
  • Engine docs/hrx.md still says "off by default"; it has to change in the engine PR that bumps the pin to include this.
  • The test has no mutation arm: I did not break the table on purpose to watch the case fail.

🤖 Generated with Claude Code

…efault (engine ggml-org#572)

The table decode from #216 was opt-in behind GGML_HRX_IQ3S_TABLE_DECODE=1, and with it set the matcher swapped the
kernel for every format pair. Select it only when the gate or up weight is IQ3_XXS, by default; =0 restores the
select chain. One log line states which decode a run used, because both kernels share one registration name.

tests: MUL_MAT_VEC_FUSION cases at the Flash-Next routed prefill shape (2560 -> 640, 10 routes, broadcast input,
80 and 512 tokens) for IQ3_XXS and IQ4_XS, and GGML_TEST_PRINT_ERR=1 to print the measured error of a passing case.

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

Copy link
Copy Markdown
Author

ready for review at d17623e

@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 d17623e — with two things to note about the branch

The change itself is small and has the right escape hatches. A table decode for IQ3_XXS gate/up weights becomes the default only for that one case —

It is the default for the one case it exists for — a gate or up weight in IQ3_XXS — and is never selected for any other format pair, so a model without
IQ3_XXS routed experts runs exactly the kernel it ran before.
GGML_HRX_IQ3S_TABLE_DECODE=0 puts IQ3_XXS back on the select chain (the A/B arm, and the way out if a device disagrees). The test-backend-ops
MUL_MAT_VEC_FUSION cases at the routed prefill shape run both.

A default that is scoped to one format pair, with an environment escape for the case where a device disagrees, and an A/B arm at the real shape — that is the
shape a "make it the default" change should have, because the alternative (a silent global switch) is unfalsifiable on a box that has no IQ3_XXS model.

Two branch facts worth stating, because they change what a reviewer is looking at:

  1. this branch contains #278's content — tests/test-hrx-arena-ledger.cpp is md5-identical to #278's, and the diff against #278 shows this PR removing
    test-hrx-staging-ledger.cpp and test-hrx-failure-status.cpp's additions, i.e. the branch predates them rather than carrying them. So the IQ3_XXS change is
    the only thing to review here, and it will conflict with #278 wherever those tests land;
  2. the bit-identity claim is stated but its measurement is not in this diff (IQ4_XS instruction mix bit-identical) is the tail of a comment about a
    different format pair): if the table decode is now the default, the bar it has to clear is that the IQ3_XXS outputs are identical to the select chain —
    and the A/B arm runs both, so that is checkable, but the number should be quoted in the body rather than implied.

Not built: no llama.cpp tree here and -DGGML_HRX=ON needs the private runtime.

Recommendation for the cohort: land #278 first (it carries the ledger arms this branch would otherwise delete), then rebase this — and quote the A/B
comparison in the body.

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

IQ3_XXS routed experts taking the table decode by default is the right routing: an XXS quant has a lookup table by construction, and forcing it through the arithmetic path would either be wrong or silently slower. Defaulting a shape to the path its format implies, rather than making every caller remember, is the same principle that keeps the int4 code and its scale paired.

CI green. I read the diff and CI; I did not build or run this fork.

@bong-water-water-bong
bong-water-water-bong merged commit 69ee6f5 into 1bit/hrx-vulkan-patched Nov 10, 2026
bong-water-water-bong added a commit that referenced this pull request Nov 12, 2026
…ml-org#572) (#329)

The #281 routed cases use 16 experts (50-320 routed rows per expert), so they never reach the
small-row regime of the real model: 10 routes over 512 experts is ~1.6 / 10 / 40 rows per expert at
80 / 512 / 2048 tokens. Adds, at 512 experts and those token counts:
- eval: gate/up + SwiGLU for IQ4_XS (UD-IQ4_XS) and IQ2_S (UD-IQ3_XXS), and the 640 -> 2560 down
  MUL_MAT_ID for IQ4_NL and Q5_0;
- perf: the same down MUL_MAT_ID cases.
These cover the gpu-kernels routed-expert tile fit (IQ4_XS / Q5_0 on the HIP prefill GEMM, 8-wave
down and IQ2_S widths). On HRX0 vs CPU, two rounds each of default, GGML_HRX_HIP_MMID_TILE_FIT=0 and
GGML_HRX_HIP_MMID_IQ4XS_Q5_0=0: MUL_MAT_ID 6/6 and MUL_MAT_VEC_FUSION 6/6 every run (performance mode).

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@bong-water-water-bong
bong-water-water-bong deleted the cl-perf/572-iq3s-table-default 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