Skip to content

feat(QUANT-GGUF-IQ4_NL): IQ4_NL keep-quant GEMM on ROCm - #3149

Open
localai-org-maint-bot wants to merge 7 commits into
mainfrom
row/QUANT-GGUF-IQ4_NL
Open

feat(QUANT-GGUF-IQ4_NL): IQ4_NL keep-quant GEMM on ROCm#3149
localai-org-maint-bot wants to merge 7 commits into
mainfrom
row/QUANT-GGUF-IQ4_NL

Conversation

@localai-org-maint-bot

@localai-org-maint-bot localai-org-maint-bot commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

IQ4_NL reached the GGUF reader, the CPU dequantizer and the CPU keep-quant dot,
and no accelerator multiplied it. On ROCm the refusal was terminal, because the
backend is discrete and an unsupported dtype cannot fall back to a host kernel
that would follow device pointers.

This blocks a shipped artifact rather than a hypothetical one. Every published
unsloth/Qwen3.8-Flash-Next-GGUF quant stores 91,465,564,160 elements in IQ4_NL,
read from the three shard headers over HTTP range requests: the 20M-entry
n-gram table and the 48 ffn_down_exps.

What lands

The ROCm IQ4_NL keep-quant GEMM, as two kernels rather than one. An expert
tower reaches the grouped provider and not the single-matrix one, so
GroupedIQ4NLK is the arm the shipped checkpoints actually need and IQ4NLGemmK
covers the non-grouped path beside it. Admitting the encoding with only the
latter would throw at the first expert forward with the model already resident,
which is the failure DeviceKeepQuantSupported exists to prevent, so the
predicate and both kernels move together.

DotIQ4_NL is a 1:1 port of cpu_quant_dot.cpp VecDotIQ4_NLQ8_0, itself llama.cpp
b10451 quants.c:1254. It keeps upstream's association, d * (sumi1 + sumi2),
where the scale product is formed before the integer sum is folded in. That is
the opposite order from DotQ8_0 beside it, and it is what makes the GEMM
bit-reproducible against the oracle.

The activation encoding is Q8_0, not the Q8_K every neighbouring i-quant uses.
ROCm already had that path, so this is a new dot in existing machinery rather
than new machinery. CUDA genuinely lacks it and is unchanged and still owed.

The gate, red and green

Run on the hardware it is for, under an rc lease on strix:gpu0 (gfx1151, ROCm
7.2.4, HIP 7.2.53211), built in the lease from a --depth 1 clone with
git rev-parse HEAD asserted equal to the commit under test and
git status --porcelain at 0 bytes.

Run Head Result
RED 3c529c8 47 cases / 45 passed / 2 failed, 84040 assertions / 0 failed
GREEN 6f77c70 47 / 47 / 0 failed / 0 skipped, 84044 assertions / 0 failed

The red is the evidence and not a mishap. Both failing cases were the two new
IQ4_NL ones, and both threw by name rather than computing a wrong answer: zero
failed assertions against two failed cases is the signature of a route gap. The
cause was the wrapper in rocm_quant_dot.hip, whose delegation allowlist and
whose IsRocmKeepQuantSupported both excluded IQ4_NL, leaving the kernels in the
GDN provider unreachable. The third commit fixes exactly that.

The assertion count rising by four across the pair is what shows the format
executed rather than being skipped; in the red run those cases threw before
their checks ran. 0 skipped is asserted on the green run, not assumed. The
build was clean at -Werror with 0 warnings.

What this does NOT claim

No throughput, latency or memory number is claimed. AGENTS.md admits no
performance result from an arm whose token gate has not passed, and the gfx1151
token gate currently reads FAIL at 3 of 6 on a simpler model.

IQ4_NL alone does not make Qwen3.8-Flash-Next run on ROCm. NINE qwen4_exp
operations still have no ROCm arm: kQwen4ExpPleConv, kQwen4ExpPleGate,
kQwen4ExpGatedResidual, kQwen4ExpGatedResidualWriteBack, kQwen4ExpQsaCompress,
kQwen4ExpQsaGatherAttention, kRmsNormGroup, kIndexSelect and kIndexCopy. They
HARD-REFUSE on this board rather than falling back: ReferenceTierEligible gates
on DeviceMemoryIsHostAddressable(), which is unified_memory_, which
ResolveMemoryPolicy computes as managed_alloc || (pageable_memory_access &&
integrated) — and gfx1151 reports pageableMemoryAccess 0, so since the #2511
narrowing that is false. The repair that stopped the gfx1151 GPU hang also
removed the CPU reference tier on this board.

Review history, and what is still owed

A fresh review returned FAIL on seven findings. The kernel substance passed that
review on its own terms -- the arithmetic against llama.cpp b10451
quants.c:1254, every stride at sizeof(BlockIQ4_NL), route and refusal agreeing
on both arms, and four mutations behaving -- and every finding was at the edges
of the change. All seven are repaired by a fresh implementer:

  • The blocking one was a red gate the first wave never ran. Widening
    DeviceKeepQuantSupported for kROCM reds tests/vllm/test_gguf_keep_quant.cpp,
    which restates the ROCm admitted set independently and as a total. The row's
    declared gate was the ROCm kernel suite, which cannot see a loader predicate.
    The predicate is NOT narrowed; the test's expectation moves, gemm_kept 22 to
    24.
  • The spec called the association order d*(s1+s2) load-bearing and pinned it
    with nothing: reassociating survived a fully green gate. Every gate this row
    had was an NMSE band at 5e-4, and the reassociation moves the result by about
    1e-7 relative. It is now pinned bit for bit by a case that runs on every
    device registering kMatmulBTQuant, and the mutation is proven to red it at one
    ulp, 3240805058 against 3240805059, with the binary proved changed by md5.
  • The fused MoE gate+up+SwiGLU arm serves IQ4_NL and was ungated; its fmts table
    is extended rather than the stray line deleted.
  • The records, docs/FEATURES.md and one stale comment now describe the tree as
    it is, and the issue carries dated resolution evidence.

A FRESH SCOPED RE-REVIEW OF THOSE REPAIRS HAS NOT RUN. It was dispatched and
died immediately on a session rate limit, so nothing has independently read the
repaired head. The gate numbers the implementer reports were also taken at
0593ea4, BEFORE this branch was rebased onto origin/main 97cb696, so they
describe a different base than the one here. Both are owed before this merges,
and neither is a formality: the repairs touch a checker baseline and a test that
exists to pin a one-ulp guarantee.

A correction this pull request carries against itself

Two of the arms this row opened with were never its work, and the record says so
rather than quietly shrinking. The ROCm gather landed as #3097 while the spec
was being written. The CUDA dot was already landed as #2419, and that one was a
mistake rather than a race: the spec consulted IsCudaKeepQuantSupported, which
governs the 256-element Q8_K family, for a 32-element Q8_0-activation encoding
that cannot appear in it by construction. CUDA keeps a second predicate,
IsCuda32BlockKeepQuantSupported, for exactly that class.

Records

The QUANT-GGUF-IQ4_NL row understated the tree and now says what it does: the
reader arm, the dequantizer and the CPU dot all landed under #1989 while the
cell read INVENTORIED with every stage unset.

strix:gpu0 joins the fleet table, which predated the box, with its memory
measured under two rc leases. hipMallocManaged there is bounded by HOST memory
rather than by the firmware carve: a bounded probe that stops at its first
failure reached 76 GiB with plain hipMalloc and 27 GiB with hipMallocManaged,
against 29.3 GiB host-available. That resolves an ambiguity #2518 could not,
because its 58.000 GiB ceiling sat below both the 64.00 GiB carve and the 62 GiB
host RAM of the time and so never said which bound it hit. Raising the carve to
96 GiB lowered the managed ceiling, because host RAM fell with it.

The spec records two corrections against itself rather than applying them
silently. It first planned a ROCm gather arm and first recorded #3029 as a hard
dependency; #3097 landed both the gather and the ROCm codebook header while this
was being drafted, so the arm is struck and the dependency is downgraded to a
conflict surface.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]

@localai-org-maint-bot
localai-org-maint-bot marked this pull request as ready for review September 12, 2026 00:53
IQ4_NL reaches the GGUF reader, the CPU dequantizer and the CPU keep-quant
dot, and no accelerator multiplies it. On ROCm the refusal is terminal,
because the backend is discrete and an unsupported dtype cannot fall back to a
host kernel that would follow device pointers.

This blocks a shipped artifact rather than a hypothetical one. Every published
unsloth/Qwen3.8-Flash-Next-GGUF quant stores 91,465,564,160 elements in
IQ4_NL, read from the three shard headers over HTTP range requests: the
20M-entry n-gram table and the 48 ffn_down_exps. #3097 has since made the
table readable on ROCm. The experts are still not multipliable, and that is
now the whole gap for this encoding on gfx1151.

The CUDA half is a variant and not a table entry. IQ4_NL is a 32-element block
paired with a Q8_0 activation, where the resident device GEMM quantizes
activations to Q8_K over 256-element super-blocks. cuda_quant_dot.cu already
says so: DotMXFP4 sits there marked maybe_unused, awaiting the same
Q8_0-activation GEMM variant that Q5_0 and Q4_0 also queue behind.

The row understated the tree and now says what it does. The reader arm, the
dequantizer and the CPU dot all landed under #1989 while the cell read
INVENTORIED with every stage unset.

Adds strix:gpu0 to the fleet table, which predates the box, with its memory
measured under two rc leases. hipMallocManaged there is bounded by HOST memory
rather than by the firmware carve: a bounded probe that stops at its first
failure reached 76 GiB with plain hipMalloc and 27 GiB with hipMallocManaged,
against 29.3 GiB host-available. That resolves an ambiguity #2518 could not,
because its 58.000 GiB ceiling sat below both the 64.00 GiB carve and the
62 GiB host RAM of the time and so never said which bound it hit. Raising the
carve to 96 GiB LOWERED the managed ceiling, because host RAM fell with it.

The spec records two corrections against itself rather than applying them
silently, because a scope that shrinks without a trace cannot be audited. It
first planned a ROCm gather arm and first recorded #3029 as a hard dependency.
#3097 landed both the gather and the ROCm codebook header while this was being
drafted, so the arm is struck and the dependency is downgraded to a conflict
surface.

No product code lands here, and no throughput, latency or memory number is
claimed.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
The ROCm backend refused IQ4_NL terminally: ten WType entries excluded it and
rocm_grouped_gemm.hip threw naming it, with no host fallback available on a
discrete backend. Every published Qwen3.8-Flash-Next quant stores its 48
ffn_down_exps in IQ4_NL, so those experts could not be multiplied at all.

Two kernels, not one, because an expert tower does not reach the single-matrix
provider. GroupedIQ4NLK is the arm the shipped checkpoints need; IQ4NLGemmK
covers the non-grouped path for completeness. Admitting the encoding with only
the latter would have thrown at the first expert forward with the model already
resident, which is the failure DeviceKeepQuantSupported exists to prevent, so
the predicate and both kernels move together.

DotIQ4_NL is a 1:1 port of cpu_quant_dot.cpp VecDotIQ4_NLQ8_0, itself llama.cpp
b10451 quants.c:1254. It keeps upstream's association, d * (sumi1 + sumi2),
where the scale product is formed before the integer sum is folded in. That is
the opposite order from DotQ8_0 beside it, and it is what makes the GEMM
bit-reproducible against the oracle.

The activation encoding is Q8_0, not the Q8_K every neighbouring i-quant here
uses. ROCm already had that path (QuantizeQ8_0K, DotQ8_0, both GEMM shapes), so
this is a new dot in existing machinery rather than new machinery. The spec
assumed otherwise and is corrected in the same change; CUDA, which genuinely
lacks the path, is unchanged and still owed.

Deliberately not Dp4a-vectorized: the codebook lookup is per nibble, so a
four-way byte dot has nothing to multiply until the values are gathered, and
gfx1100 has no hardware dp4a in any case.

Both cross-device cases gain IQ4_NL against the CPU oracle at NMSE <= 5e-4.
They REQUIRE rather than skip when ROCm is registered, so a dropped dispatch
cannot pass silently.

NOT YET COMPILED OR RUN. This host has no ROCm toolchain and
check-tree-compiles does not reach a .hip file, so the 721 translation units it
checked exclude every line of the kernel work here. The build and the device
gate are owed on a strix:gpu0 lease and are recorded as PENDING until then.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
…nels

The kernels landed in the GDN provider and nothing could reach them. The
wrapper in rocm_quant_dot.hip delegates an explicit allowlist to the *Gdn
kernels and refuses everything its own IsRocmKeepQuantSupported does not admit.
IQ4_NL was in neither set, so both seams threw on a dtype the tree could now
multiply.

That is the refusal and its route predicate disagreeing, and the file already
warned about this exact class beside the grouped list: Q8_0 has no
Q8_K-superblock arm here because it dots a Q8_0 activation, so omitting it turns
a served format into a throw. IQ4_NL is the second member of that class and the
comment now says so.

Measured on strix:gpu0 before the fix, which is the red this change turns green.
Both new cross-device cases THREW for the intended reason and every other case
passed:

  cross_device.cpp:2544  no keep-quant kernel for dtype iq4_nl
  cross_device.cpp:3809  matmul_bt_quant_grouped: no keep-quant kernel iq4_nl
  47 cases | 45 passed | 2 failed | 84040 assertions | 0 failed

Zero failed assertions against two failed cases is the signature of a route
gap rather than a numerical one: nothing computed a wrong answer, the work never
started.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
The row's unit gate ran on the hardware it is for. Both runs are recorded,
because the red is the evidence that the green means something.

RED at 3c529c8:   47 cases / 45 passed / 2 failed, 84040 assertions / 0 failed
GREEN at 6f77c70: 47 cases / 47 passed / 0 failed / 0 skipped,
                    84044 assertions / 0 failed, build -Werror with 0 warnings

Zero failed assertions against two failed cases is a route gap and not a
numerical one, and both failing cases were the two new IQ4_NL ones throwing by
name. The assertion count rising by four across the pair is what shows the
format executed rather than being skipped; in the red run those cases threw
before their checks ran.

Also records that the test binary does not find libamdhip64.so.7 by itself in
the strix worker. Without LD_LIBRARY_PATH the gate exits 127 having measured
nothing, which is a result that looks like a failure and is not one.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
…ecords

A fresh review returned FAIL on seven findings against the ROCm IQ4_NL arm.
This change repairs each of them. ISSUE-LOCAL-01M29ECVDVH5A89TBKA8K72YRD.

The blocking one was a red gate the first wave never ran. `gguf_keep_quant.cpp`
admitted `kIQ4_NL` on ROCm, and `tests/vllm/test_gguf_keep_quant.cpp` holds the
ROCm admitted set INDEPENDENTLY, plus a total. The two are deliberately two
descriptions of one rule, so moving only the first reds type 20 on both shapes
and both totals: `60 cases | 59 passed | 1 failed`. The predicate is NOT
narrowed, because admitting IQ4_NL on ROCm is exactly what puts a real
`ffn_down_exps` tower on the new kernels; the test's expectation moves instead,
and its GEMM term goes 22 -> 24.

The second one is the more interesting failure. The spec's D1 and R2 call the
association order `d * (sumi1 + sumi2)` load-bearing, and the reviewer mutated
`DotIQ4_NL` to `(d*sumi1) + (d*sumi2)`, proved the binary changed, and watched
47 of 47 cases stay green. Every gate this row had was an NMSE band at `5e-4`
and reassociation moves the result by about `1e-7` relative. The guarantee was
claimed and unpinned, which is worse than not claiming it.

It is now pinned bit for bit, by a new case in `test_backend_cross_device.cpp`
that runs on every device registering `kMatmulBTQuant`, the CPU included. Three
fixture properties make it able to see a one-ulp defect, and each is a way a
lazier fixture stops being able to: ONE block at `M = N = 1`, so the warp
reduction adds only zeros and the device result is the dot itself; an activation
of `k/64` with `max |k| = 127`, so the Q8_0 quantizer is exact and the test
KNOWS the quantized bytes rather than asking a kernel for them; and a
full-mantissa f16 weight scale, because with a short-mantissa scale both
products are exact and the two orders AGREE. A `REQUIRE` asserts the two orders
differ on these operands, so the case fails rather than degenerating.

Four smaller findings. `test_backend_cross_device.cpp` taught the fused
gate+up+SwiGLU case's `elems_per_block` about IQ4_NL without extending its
`fmts` list, so that branch was unreachable and read like coverage; the fused
seam does serve the format (`rocm_moe_gate_up_swiglu.hip` delegates to the
grouped GEMM), so the row is added rather than the line deleted.
`docs/FEATURES.md` enumerates the ROCm keep-quant set and omitted IQ4_NL. The
`quantization-matrix.md` cell still said ROCm has no arm and its refusal is
terminal, contradicting the code in the same commit. The local issue was still
OPEN with an empty `Resolution` and a closing line naming the arm that had just
landed; it closes here with the dated `strix:gpu0` red-and-green evidence, and
its two reconciliation sections are restored verbatim after `agent-issue.py
close` dropped them, because deleting evidence to close a record is not closing
it.

One re-pin that no finding named, and that preflight found: this row entered the
runnable population of `check-gate-commands.py` when its spec landed a `## Gates`
section naming commands that can fail, and the ratchet requires the baseline to
move in the same change. `test_check_gate_commands` was red on this branch
before this commit.

EXCEPTION ARGUED, not assumed: the 48-line `strix:gpu0` block in
`.agents/environment.md` rides in this pull request although it is not this
row's own code. AGENTS.md says a record edit rides in the pull request whose
change made the record stale. The change that made it stale is the firmware
memory carve on this box, which moved the VRAM split to 96.00 GiB and host RAM
to 31 GiB, and this row is what measured it, under leases
`a8111ff8-3ce8-42f6-9034-36bdd2cacfe4` and
`c30dc437-bf12-4a23-ab8b-89b88fe767dd`. The fleet table also never carried the
only AMD device on the fleet at all, so a reader sizing a model for it finds
nothing and falls back to figures the carve has falsified. Splitting it would
cost a branch, a gate run and a fresh review to move text whose measurement
lives in this spec. A reviewer who rejects that reason does not merge this
change.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
The repair commit before this one landed the bit-exact association case and the
record fixes. This commit writes down what the gates measured, because a claim
without its numbers is the defect the review found in the first place.

G1b, `strix:gpu0` under `rc` job 7a757f2a-d078-44f9-998d-2ef504b74cc6, built in
the lease from a shallow clone of this branch at 0593ea4 with a clean tree:
`test_backend_cross_device` 48 cases / 48 passed / 0 failed / 0 skipped and
84061 assertions, `test_gguf_keep_quant` 61 cases / 61 passed and 12626
assertions. The association mutation `(d*sumi1) + (d*sumi2)` was then applied in
the lease, the binary was proved changed by md5, and the new case FAILED on
`3240805058 == 3240805059`. One ulp is the whole size of the guarantee, and it
is why an NMSE band at 5e-4 could never see it. The tree was restored byte for
byte and the suite returned to 48 of 48.

G1 CPU: `ctest --test-dir build`, 751 tests, 740 passed. The other 11 are 10
Skipped device or checkpoint arms and one pre-existing failure,
`test_rocm_f16_contract`, which asserts that `OwnedTensor::View` carries the
repacked markers and does not. The three files that decide it are byte-identical
between this row's base and origin/main, and this row touches none of them.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
@mudler
mudler force-pushed the row/QUANT-GGUF-IQ4_NL branch from e3e0420 to 50b13c0 Compare September 12, 2026 02:20
mudler added a commit that referenced this pull request Sep 12, 2026
Both were found while landing the ROCm IQ4_NL arm (PR #3149) and neither is
that row's work, so both are filed here rather than folded into it. Neither is
fixed in this change, deliberately: each repair is a design question rather than
a typo, and both want a spec and a red-before case.

agent-issue.py renders an issue file from a fixed IssueRecord schema and never
reads the file forward, so every update and close deletes any section the schema
does not name. The caller sees a success and no diff. Observed rather than
theorised: closing a row's local issue dropped two dated Reconciliation sections
that were the only record of why that row's scope changed twice, each citing
commits and file:line anchors. They were restored by hand and nothing in the
tool's output said they had gone. The normal success path of a sanctioned
records tool deleting evidence inverts the Records rule, in the one file class
this protocol treats as canonical, and does it invisibly.

check-pr-size requires any change touching a governance checker to also change
that checker's one named evidence test file, so every row entering the
RUNNABLE_BASELINE population edits one shared file. That is the shape AGENTS.md
Records calls defective and tells us to move to a per-row surface. Measured
rather than argued: the same ERROR fires on three already-merged commits of this
shape. Two properties kept it invisible until a fresh review -- the checker
prints ERROR and exits 0, and agent-preflight SKIPS it for want of arguments, so
the first signal is CI. It is also stricter than the rule it enforces, since
AGENTS.md accepts a red-before test OR MUTATION and this gate accepts only a
changed line in one named file.

Neither issue cites the IQ4_NL record by ID. That record lives on the #3149
branch, so a reference to it here resolves to nothing and reds
check-agent-record; the pull request names it instead. Found the same way as
everything else in this change, by reading a checker's OUTPUT after it printed
ERROR and exited 0.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
… ROCm skip

The branch moves `RUNNABLE_BASELINE` in `scripts/check-gate-commands.py` but
never touched that checker's test, so `check-pr-size.py` reported the checker
change as unevidenced. The move is load-bearing -- dropping the entry reds 17
cases including `test_the_baseline_matches_the_shipped_record` -- so the repair
is to write the mutation down rather than to drop the entry.
`Iq4nlRunnablePopulationTests` follows the two precedents already in that file:
the row is in the pinned population, its audit verdict is earned by the record,
and removing the entry breaks the exact pin.

The bit-exact association case guarded its device loop with
`if (!OpAvailable(kMatmulBTQuant, dt)) continue`, which is the opposite of the
convention this same file states twice with explicit comments. On a ROCm build
whose IQ4_NL admission regressed, the case would have passed having measured
only the CPU. It now REQUIREs the registration whenever the ROCm backend is
built, and skips only where the backend is absent.

The spec's base was still the pre-rebase `18f39771c`; the merge-base is
`97cb6964b`. `## Owed` now records what the bit-exact case does NOT reach: it
pins one operand point and one block, so a kernel that reassociated only the
reduction ACROSS blocks stays NMSE-gated.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
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