model : add GLM-5.3-Flash (glm5next) - #27752
Conversation
|
Hi @eauchs, thanks for your contribution! Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:
Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below. |
622c228 to
7dfaa8e
Compare
|
and no perplexity / top-1 numbers: the checkpoint is 328 GB and my machine |
|
convert_hf_to_gguf.py runs end to end on a 4-layer synthetic model, but the |
|
Converted the real Blocker:
|
|
Your fix is pushed — commit 9327de5, exactly your version (filtering out the Nones at merge). |
Carry the new base commit into the notice and the standalone-patch instructions, and add the GLM-5.3-Flash section: which draft PR is carried, why ggml-org#27752 rather than ggml-org#27754, that it is text-only, and that it is unvalidated against the real checkpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two competing GLM-5.3-Flash drafts write the same value under different GGUF keys: ggml-org#27752 uses attention.indexer.block_size, ggml-org#27754 uses attention.indexer.kpool. Semantics and the indexer_top_k % kpool == 0 constraint are identical, so read the block_size key first and fall back to kpool instead of aborting at "GLM5NEXT requires index_kpool" on a GGUF from the other converter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Ran this branch on NVIDIA CUDA (Blackwell, sm_120 / RTX 5090) — adding a data point since the verification table above is Apple-only. Build: CUDA 12.8,
NMSE ~8e-08 vs CPU (well under the 1e-4 bar), roundtrip OK; a second run read 8.79e-08. So the KDA / DSA / mHC path is numerically consistent on CUDA, not only Metal/Accelerate. ( Real weights: loaded One load snag + a suggested fallback. That GGUF writes the k-pool size under --- a/src/llama-arch.h
+++ b/src/llama-arch.h
@@ enum llm_kv {
LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE,
+ LLM_KV_ATTENTION_INDEXER_KPOOL,
LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS,
--- a/src/llama-arch.cpp
+++ b/src/llama-arch.cpp
@@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
{ LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE, "%s.attention.indexer.block_size" },
+ { LLM_KV_ATTENTION_INDEXER_KPOOL, "%s.attention.indexer.kpool" },
{ LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS, "%s.attention.indexer.local_blocks" },
--- a/src/models/glm5next.cpp
+++ b/src/models/glm5next.cpp
@@ void llama_model_glm5next::load_arch_hparams(llama_model_loader & ml) {
ml.get_key(LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE, hparams.indexer_block_size, false);
+ if (hparams.indexer_block_size == 0) {
+ // some converters (e.g. unsloth) write the k-pool size under `indexer.kpool`
+ // rather than `indexer.block_size`; accept it so those GGUFs load without an override
+ ml.get_key(LLM_KV_ATTENTION_INDEXER_KPOOL, hparams.indexer_block_size, false);
+ }With the patch, |
|
Thanks to both of you — this PR now has two independent validations on real hardware. @tjluyao converted the official checkpoint (305 GiB): stacked experts and FP8 dequant both sound. That's exactly what was missing, and I've updated the description accordingly — it still claimed the code had never been run against the real weights. @0-F0xtr0t your patch is in (llama-arch.h, llama-arch.cpp, glm5next.cpp): indexer.kpool is now accepted as a fallback for indexer.block_size, so GGUFs written with the old key load without --override-kv. No need to open a separate PR, but shout if I've mangled anything. |
|
Selection now selects whole pools and expands them—no more top-k on individual cells. Pushed in 0deb55d. Partial pools dropped from 129 (Metal) / 151 (Accelerate) down to 0 across all three backends. The backends were diverging on partial counts while agreeing on the 93 broken rows, which was the classic signature of a tie-break issue. Credit to @danielhanchen: the description in #27754 nailed the diagnosis and provided a reference-free metric. |
|
Ran a validation pass against this branch (
|
|
Follow-up on the greedy non-losslessness I reported with |
GLM-5.3-Flash ships a NextN block that neither upstream draft implements: ggml-org#27752 has no MTP graph at all, ggml-org#27754 asserts "glm5next NextN graph not implemented yet". The tensors were already declared by the loader but carried TENSOR_SKIP unconditionally, so the head sat unused in every GGUF. Model it on the working GLM-5.2 head in glm-dsa.cpp: enorm(embed) + hnorm(prev_hidden) -> concat -> eh_proj -> one dense DSA decoder block -> shared_head_norm -> shared LM head. graph_mtp derives from graph through a no-trunk tag constructor so it calls the trunk's own build_mla_layer and build_ffn_layer instead of duplicating them; attention and FFN semantics therefore cannot drift from the trunk. Two deliberate differences: no mHC mixer, because the loader creates no hc_* tensors for the NextN block, and inp_kpool = nullptr, so build_dsa_top_k is skipped and the block runs dense MLA — the same choice the GLM-5.2 head makes. Loader now follows the glm-dsa pattern: trunk and NextN may live in separate GGUFs in either direction, and TENSOR_SKIP is applied only when the loader was not asked for MTP. load_mtp defaults to false, so ordinary loading is byte-for-byte unchanged. An MTP context now allocates a plain KV cache filtered to the NextN layers. Without that it built a second full hybrid memory for the entire model, which cannot fit at production context sizes. NOT VALIDATED: this has never been executed. test-llama-archs covers no MTP head for any architecture, so there is no harness to extend, and no ROCm or GLM-5.3 checkpoint was available. Acceptance rate and correctness must be measured on hardware before relying on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ggml-org#27742 (Qwen3.8-Flash-Next, qwen4exp) merged 2026-08-27, merge commit 6c84c7d, in mainline from b10664. vendor/qwen38-pr27742 is now redundant and should be dropped at the next rebase. ggml-org#27754 (GLM) remains an open draft, and the rival ggml-org#27752 is also still open and being updated — that contest is unresolved.
6ca4915 to
8a8d0bc
Compare
|
@eauchs Data toward your "one bug, not two" read from #27754: this branch reproduces the depth collapse too, with byte-identical prompts — and its boundary sits a little shallower than the other implementation's. Setup: M3 Ultra 512 GB, Metal, Unsloth
The shallow control matters: the 27754-converted GGUF loads and answers correctly here (your So: two independent implementations, same failure shape, boundaries within ~10% of each other — which supports the shared-lineage hypothesis over anything specific to either PR's kernels. One more lead from the other thread that may transfer: over there the collapse turned out to be microbatch-dependent ( |
|
@eauchs Arm B run against the reshape, on
Arm B reuses arm A's warm prefix, so it hits n_kv 262,144 within ~100 fresh tokens — same 1-second reproduction that crashed on Saturday, now completing cleanly with the server healthy after. Numerically inert on CUDA too: greedy output at 2k is hash-identical between the patched and unpatched builds ( Two incidental datapoints from the same runs: the rebase is speed-neutral at this config (full-depth average identical pre/post), and the depth collapse @feni6 reproduced on this branch does not occur on CUDA with So from this side: reshape in the series, guard as its own PR, exactly as you proposed. Happy to re-run arm B on whatever lands. |
|
@matteoscalabrini Arm B validates the reshape — it is in the series as of a few minutes ago.
Of your two incidental datapoints, the second is the more useful one: the depth collapse @feni6 reproduced on this branch (Metal, On the split: reshape in the series (done), guard as its own PR (still open on the ggml side) — unchanged. And your offer to re-run arm B on whatever lands is taken: the guard PR is where that offer is most valuable, since it is the ggml-wide fix and your box is the only one that can reproduce the boundary in ~1 s off a warm KV. |
|
@feni6 This is the datapoint the "one bug, not two" read needed — same failure shape on two independent implementations, boundaries within ~10% of each other, and byte-identical prompts carried over from the #27754 bisection so the comparison is apples-to-apples. The shallow control is worth as much as the deep failures: the 27754-converted GGUF answering correctly at ~8K (both planted codes, 292 t/s prefill) rules out conversion mismatch as the explanation for the deep rows, and incidentally confirms the One request before anything else, and it is cheaper than the sweep: same Metal, same prompt, Your caveat is noted and I agree with it: the quant came from the other branch's script, so a native conversion could shift the boundary a little — though it would take a lot of shifting to explain a ~10% agreement between two independent implementations. |
4bc437a to
a9904cd
Compare
|
Rebased on master, new head The rebase was 25 commits apart, and the only conflict was in The point that matters: #28159 broke the MTP context, and this branch was the only place it could be seen. The hoist moved the Verified state on the new head:
One non-regression footnote so nobody reads it wrong: qwen4exp now shows SKIP on Meta. It gained a PLE (Per-Layer Embeddings) via #27941, and a PLE conv history is a row of the recurrent cache, which linear layers alone have, so the fixture now carries a recurrent cache, and Meta skips every recurrent arch, exactly as it already skips glm5next, kimi-linear and glm-dsa. That is master's doing, not a regression on this side. |
a9904cd to
c9ddd68
Compare
|
@CISC — closing the promise from #28173: "I'll drop a9904cd once this lands." It landed, and the mirror is gone. #28173 and #28183 have both merged, the branch is rebased on the merge, and the mirror commit ( Verified after the drop, on the new head:
Your fix suffices on its own — measured both with and without the mirror on top, and with neither applied the MTP context still dies, sizing its KV cache from @matteoscalabrini — the gridDim guard is written, and I need you for the half I cannot produce. It lives on What I have checked: the index arithmetic, simulated host-side over 34,417,610 blocks and seven geometries, including What I cannot check, and want to be plain about: the compile. No CUDA on my Mac, and my workflows still await maintainer approval, so CI will not compile it for me either. The branch has never seen a compiler. That is why the PR stays unopened until you are back — I'd rather not open code that has never compiled on a PR whose entire argument is that it is clean. The ask: a compile, and arm B — the one that died in about a second off the warm prefix. Since the series now folds |
|
Heads-up: the Metal deep-context Mechanism (full write-up with standalone repro, GPU-address maps, and an 11/11 end-to-end verification battery: #27754 comment): The fix is three uint64 casts; it applies to this branch verbatim: mul_mm.metal: three uint64 castsdiff --git a/ggml/src/ggml-metal/kernels/mul_mm.metal b/ggml/src/ggml-metal/kernels/mul_mm.metal
index ee848eed6..df3be0038 100644
--- a/ggml/src/ggml-metal/kernels/mul_mm.metal
+++ b/ggml/src/ggml-metal/kernels/mul_mm.metal
@@ -134,7 +134,8 @@ kernel void kernel_mul_mm(
// Store result tile to output matrix (with batch offset)
// cT.store handles bounds checking via tD's extents (M, N)
- device float * dstBatch = (device float *)dst + im * N * M;
+ // int32 im*N*M wraps for KQ [n_kv, ub, 64] past 2^31 elements (glm5next collapse)
+ device float * dstBatch = (device float *)dst + (uint64_t)im * (uint64_t)N * (uint64_t)M;
auto tD = tensor(dstBatch, dextents<int32_t, 2>(M, N), array<int, 2>({1, M}));
cT.store(tD.slice(ra, rb));
@@ -318,7 +319,7 @@ kernel void kernel_mul_mm(
// if no bounds checks on the output are needed, we can directly write to device memory
device float * C = (device float *) dst +
(r0 + 32*(sgitg & 1)) + \
- (r1 + 16*(sgitg >> 1)) * args.ne0 + im*args.ne1*args.ne0;
+ (uint64_t)(r1 + 16*(sgitg >> 1)) * (uint64_t)args.ne0 + (uint64_t)im*(uint64_t)args.ne1*(uint64_t)args.ne0;
for (short i = 0; i < 8; i++) {
simdgroup_store(mc[i], C + 8*(i%4) + 8*args.ne0*(i/4), args.ne0, 0, false);
@@ -337,7 +338,7 @@ kernel void kernel_mul_mm(
if (sgitg == 0) {
for (int j = tiitg; j < nr1; j += NR1) {
- device float * D = (device float *) dst + r0 + (r1 + j)*args.ne0 + im*args.ne1*args.ne0;
+ device float * D = (device float *) dst + r0 + (uint64_t)(r1 + j)*(uint64_t)args.ne0 + (uint64_t)im*(uint64_t)args.ne1*(uint64_t)args.ne0;
device float4 * D4 = (device float4 *) D;
threadgroup float * C = temp_str + (j*NR0);Until it's applied, |
|
@feni6 — your probe compiles against this branch's head ( Reproduced on this hardware:
And the piece you hadn't published, because you had no Apple Silicon: the non-regression. The find and the fix are yours — the write-up, the repro, the diff, all yours. What I did is confirm on a machine you hadn't measured, nothing more. I am not opening the ggml PR: it's your discovery, the PR is yours to open, and the branch here stays untouched — nothing pushed, nothing committed. Meanwhile |
|
@eauchs — thank you, this is exactly the confirmation the fix needed, and more. The third machine class matters (all our Apple Silicon data was M4 Pro + M3 Ultra), the negative control makes your 256/256 meaningful in a way a bare green never is, and the non-regression sweep was the one piece we could not produce here — NMSE/ And thank you for the deferral on authorship — noted, and taken up: the upstream fix PR is now open as #28210, with your M3 Max confirmation (probe, negative control, and the non-regression run) credited in the evidence section. The three casts are byte-identical to the ones you tested at Agreed on the interim guidance: |
|
One final note, relayed per the account owner's instruction in their own words: "Don't post anything else on GitHub except for announcing that you're not going to post anymore or make any PRs." @eauchs — this is that announcement, so you aren't left waiting on us: there will be no further posts or PRs from this account. #28210 stands as submitted, for anyone who wants to carry the fix forward. |
|
Guys please, I'm totally fine with the use of ai for coding with architectural supervision. But Please take time to check the comments you're posting and validate the claims. I understand the use of ai as a non-native English speaker to check the correct use of grammar or to simply post post benched results; but do not use it to generate comments on your behalf. First, It breaks the contribution rules of llama.cpp second every pr/post/discussion turns into infinite AI slop. I will though run the tests you asked and review your approach. But please more effort in what's getting posted. |
int32 im*ne1*ne0 (and im*N*M on the tensor path) wraps past 2^31 elements. For glm5next's dense-masked DSA attention the KQ tensor is [n_kv, n_ubatch, 64 heads] f32, so at ub=512 head h's base offset wraps once h*512*n_kv >= 2^31 — first at head 63 for n_kv ~66.6K (the observed 65K collapse-boundary flattening), reaching head 39 by n_kv ~108.8K. Wrapped heads read back zeros (standalone probe: FAIL corners exactly matching 2^31/(ub*n_kv) onset per head; all 256 corners OK after this change). Mechanism for the deep-context '@' collapse in PR ggml-org#27754/ggml-org#27752.
|
Thanks for the feedback Matteo. I'm new here, I was only brought here by Claude, but I respect what you are saying. I've been using Codex, Grok, and Claude, and I've made more progress on getting GLM-5.3 to run on my mac hardware A: without crashing, and B: 2.7X faster (2x at 108k context, 2.8x at 2045k) by using "gathered attention" (gathered DSA.) I am posting that code on my personal branch of gglm-org llama.cpp so it will be available to everyone to learn from, but I don't want to break the rules of AI generated pull requests, comments, etc. I'm still testing it, but it already passed a battery of tests. Hope this is helpful. |
Adds the glm5next architecture: KDA linear attention, MLA with a DSA indexer and k-pool compression, gated-residual hyper-connections, and a 288-expert MoE. The DSA path extends the existing llama_memory_hybrid_idx container; llama_memory_hybrid and llama_kv_cache are left untouched.
Rather than adding a dedicated container, glm5next reuses llama_memory_hybrid_idx, which gains an idx_row_size constructor parameter and a set_input_kpool() input for k-pool selection.
The indexer scores [n_pool, n_head, n_tokens] and then reduces over heads, and
that reduction needs the head axis in ne[0] - so the score tensor is
materialised twice, once by the mul_mat and once by the permute+cont:
2 * n_pool * n_head * n_tokens * 4 B per device
It is per device because the DSA layers are spread across the trunk, so every
device in a layer split owns some of them. At n_ctx 262144 with kpool 4 and 32
heads that is 16 MiB per token, so -ub 4096 asks for about 70 GiB on a single
device (measured: cudaMalloc fails at 69826 MiB on a 24.5 GiB card).
That caps ubatch. With experts on CPU the ubatch is also what amortises the
expert stream over PCIe during prefill, so the cap costs throughput as well as
memory.
Scoring a token depends only on its own query row and on `pooled`, which is
shared across the batch, and no reduction in this path runs across tokens. The
token loop can therefore be split into chunks with identical results, and
ggml-alloc reuses one buffer across the chunks - bounding the scratch by the
chunk size rather than by the ubatch.
The chunk is derived from a fixed scratch target, so short contexts come out
unchunked and take the previous code path unchanged.
Measured on 5x RTX 3090, GLM-5.3-Flash UD-Q4_K_XL, f16 KV, -ot exps=CPU:
ub 512 unchunked 8788 MiB/device scratch 75 tok/s prefill at 10k
ub 4096 chunked ~2 GiB/device scratch 272 tok/s prefill at 10k
Greedy output is byte-identical to the unchunked graph at matched ubatch, with
--parallel 1, with --parallel 2, and with a ragged final chunk. Needle-in-a-
haystack retrieval passes at 28,788 and 243,314 tokens.
set_input_kpool only skipped empty cells. A unified cache holds every sequence in
one cell array, so a cell belonging to another sequence landed in this stream's
pools and collided with its own cell at the same position: cur_pool_cells[b*r +
(p%r)] was overwritten and filled[b] counted twice. The tail loop thirty lines
below already filtered this way, so the omission was an oversight rather than a
choice.
This is reachable from a plain llama-server invocation: leaving --parallel unset
selects auto slots, which set n_parallel = 4 and kv_unified = true.
test_dsa_kpool grows three passes after the existing one -- two sequences on
separate caches, two sequences on a unified cache, and a single sequence on a
unified cache as the control. At -s 1234, identical on Metal, Accelerate and the
second device:
whole partial tail misses
2 seq, separate before 216 0 0 (unchanged after)
2 seq, unified before 108 48 72
after 236 36 96
1 seq, unified 236 0 0 (control)
Whole pools return to 236, the single-stream value.
The 36 partial pools and 96 tail misses that remain are not interpretable with
this metric: count_row assumes cell index == position and filters on c <= q, and
on a unified cache the second sequence occupies cells 32..63 at positions 0..31,
so those are discarded by construction. What remains cannot be separated from the
blindness of the measurement. The third pass is there precisely to establish that
unified addressing on its own is sound.
The three new passes are display-only and do not fail the test.
…ftmax softmax normalizes ne[0], so the member axis has to sit there; the permute already puts it there. (d, n_pool) are adjacent on the contiguous tensor, so folding them into a single ne[1] leaves every row identical while moving n_pool off gridDim.y (capped at 65535 on CUDA) onto gridDim.x. Measured by matteoscalabrini on 5x RTX 3090: n_pool = 65536 goes from CUDA error: invalid argument to a coherent decode, greedy output hash-identical (0c5e1504...), prefill 142.6 vs 142.5 t/s.
ssm_a is multiplied as a gate, not scanned -- qwen3next already declares NOSCAN for the same reason, and kimi-linear has the same inconsistency. Both spellings write blk.N.ssm_a, so no GGUF breaks.
…ff_exp master added an n_kv_max parameter to build_attn_mha; the DSA overload of build_attn passes top_k->ne[0] when the selection is sparse and 0 on the dense path, like the other call sites in the file. n_ff_exp became a per-layer accessor: LLM_KV_EXPERT_FEED_FORWARD_LENGTH is now read via get_key_or_arr into n_ff_exp_arr over n_layer_all, as done in deepseek4.
c9ddd68 to
1d0c76f
Compare
…ng native ggml-org#27773 path - KDA head dim: primary KDA_HEAD_DIM, fallback SSM_STATE_SIZE - KDA gate lower bound: default -5.0f when missing (legacy semantics) - recurrent layout: prefer explicit RECURRENT_LAYERS, else native n_head_kv==0 if any zero present, else legacy FULL_ATTENTION_INTERVAL default 4
Overview
Adds
glm5next(HFmodel_type: glm5_next,Glm5NextForConditionalGeneration) —converter plus text graph. Text-only for now.
Almost nothing here is new: the model is an assembly of subsystems already in tree.
ssm_*tensors ofkimi-linear(conv1d_q/k/v,f_a/f_b,g_a/g_b,beta,a,dt,norm).Glm5NextTextHyperConnectioninherits from
DeepseekV4HyperConnectionwith a barepass, so the Sinkhornformulation carries over as-is, and the checkpoint uses the exact same tensor names
(
hc_{attn,ffn}_{fn,base,scale}). The only delta is the final stream collapse, anunweighted mean — which is what
dsv4_hc_meanalready does, so there are nohc_head_*tensors in the checkpoint.INDEXER_COMPRESSOR_{APE,WGATE}from deepseek4.GlmMoeDsaModel(GLM-5.2).The DSA indexer is wired now, with k-pool compression. It reuses the existing
llama_memory_hybrid_idx, which gains anidx_row_sizeconstructor parameter and aset_input_kpool()input;llama_memory_hybridandllama_kv_cacheare untouched.deepseek4.cpp,glm-dsa.cppandkimi-linear.cppare untouched.Shape: 45 layers (34 KDA + 11 MLA), hidden 4096, MLA with
qk_rope_head_dim = 0(NoPE), 288 experts top-8 sigmoid/noaux_tc plus 1 shared, 3 leading dense layers,
hc_mult4 with 20 Sinkhorn iterations, SwiGLU clamped at 10.Additional information
What is not implemented
and GGUF round-trip, not k-pool fidelity to
modular_glm5_next.py.-kvu): the cross-sequence cell mixing is nowfixed —
set_input_kpoolskips cells that do not belong to the stream's sequence, whichbrings whole-pool selection back to the single-stream value (108 -> 236 at
-s 1234).What remains cannot be read with this metric:
count_rowassumes cell index == position,and on a unified cache the second sequence occupies cells 32..63 at positions 0..31, so it
is discarded by construction. Attention masking stays correct either way (the KQ mask is
per token), so there is no cross-sequence leak. Worth knowing when reading reports:
leaving
--parallelunset selects auto slots, which setn_parallel = 4andkv_unified = true, so a plainllama-serverinvocation is on this path by default.after
seq_rmor a context shift. This is the container's existing behaviour rather thansomething this PR introduces — mainline
set_input_qsablocks cells the same way(
b = p/rovercells.pos_get(j)), andset_input_kpoolfollows it for consistency.Instrumented over a
seq_rm+seq_addcontext shift: 144 calls, 0 cells dropped — theper-forward recompute realigns it, so the divergence is narrower than the reference's
cache-array ordering would suggest.
n_kv/index_kpool * n_tps * n_stream * 4 B, so~0.5 GiB at 1M context with
n_ubatch = 512(it wasn_kv-sized, four times that,before the pooled selection landed). The indexer cache adds
11 * n_ctx * 256 * 2 B(~5.6 GiB at 1M).modular_glm5_next.pyignores layer 45 entirely, so the graph follows GLM-familyconvention (post-norm
h, concat order, shared head, plain residuals). @Nokodoko hassince measured it on GLM-5.3-Flash IQ4_XS (2x RTX 6000 Blackwell): draft acceptance
51-67% on a mixed prose/code/JSON bench, and the nextn tensors are Q8_0/F32 in that
quant rather than crushed by quantization.
index_share_for_mtp_iterationis not implemented. The reference shares the trunkindex for the MTP step, which a separate MTP context cannot see, so the draft head
attends densely. Documented at the call site. @Nokodoko measured no acceptance drop
between a ~200-token and a 5.7k-token context, so this does not look like the dominant
gap, but it is a real divergence.
@Nokodoko and not an accept-loop or MTP-graph defect: every accepted token is the
argmax of its own verify-batch logits. The forward is batch-decomposition sensitive -
the same 79-token prefix chunked differently moves a top-2 logit gap by ~1 logit, and by
~2.7 logits inside a real verify batch, which is enough to flip moderately confident
greedy decisions. Suspects are KDA chunked-scan accumulation and 288-expert top-k
routing flips; the DSA indexer is excluded at short context. Any drafting mode is
affected, so spec-decode losslessness here holds only up to batch-shape numerics.
n_ctx. Resolved — cause is in shared ggml code, not this arch. @feni6root-caused the Metal deep-context
@collapse on#27754 and
the fix applies here verbatim:
mul_mm.metalcomputes the batched dst offsetim*ne1*ne0in int32, which wraps past 2^31 for the dense F32 KQ[n_kv, ub, 64]atdeep context, and the misdirected stores land on GPU-VA-adjacent K-cache buffers; the
fix is three
uint64_tcasts inggml/src/ggml-metal/kernels/mul_mm.metal. Hisdiscovery, his patch — confirmed here on M3 Max (third machine class, after his M4 Pro
and M3 Ultra) with his standalone probe: at
n_kv = 108,710, ub = 512, 64 heads(13.27GiB output) the first faulty head is 39, exactly
ceil(2^31 / (512 × 108,710)), 100 of256 checked corners failing, all read at 0; with the casts, 256/256 — including his
five geometries (61,540 / 67,584 / 81,920 / 98,304 / 108,800) — and the negative
control (casts removed, rebuilt) fails again at head 39. Non-regression, which nobody
had measured on Apple Silicon and which matters because
mul_mmis a hot kernel everymodel borrows: NMSE unchanged at 8.52e-08 / 2.75e-14 / 4.84e-14,
test_mtpdraft +reload OK on three backends, seven neighbouring archs at FAIL=0. Until it lands
upstream,
-ub 128is a usable Metal workaround (keeps63*128*n_kv < 2^31up ton_kv ≈ 266K). Distinct from the
UD-IQ1_Mgarbage above, which reproduces on asingle sequence.
so my own numbers below are synthetic weights plus shape checks against the remote
checkpoint. Two people have since exercised the real thing: @tjluyao converted the
official checkpoint (305.8 GiB, 72 files) — 288-expert stacking and the FP8
weight_scale_invdequant both healthy,n_tensors = 1412,total_size = 641.6GatBF16, peak RSS ~25 GB so ~32 GB is the practical floor; and @0-F0xtr0t ran the branch
on CUDA Blackwell (RTX 5090, sm_120), where
test-llama-archs -a glm5nextgivesNMSE 8.03e-08. @yakimoto then ran three real unsloth quants on an M1 Ultra:
UD-IQ4_XSand
UD-IQ3_XXSgenerate coherently on Metal and on CPU, whileUD-IQ1_Mreturnsrepeated-token garbage on both backends — so the 1-bit tier looks numerically dead on
this arch, and the earlier
UD-IQ1_Mdatapoint is not reproduced. Pick IQ3 or above.Verification
test-llama-archs -a glm5next:The
MetaSKIP matches every other recurrent/hybrid arch (kimi-linear,glm-dsa,bailingmoe3).DSA path is live. Random-weight fixture,
--temp 0, same seed, onlyindex_topkchanges:
Output changes only when the effective width changes. Between 2048 and 4096 the
hyperparameter changes but the width does not, and the output is bit-identical.
llama-eval-callbackshows all six indexer tensors consumed in the executed graph,with
TOP_K(indexer_score_cells-3{256,8}) = {7,8}, matchingwidth = min(n_kv, topk + kpool - 1) = min(256, 4 + 4 - 1) = 7.Note: the earlier all-zero fixture could not prove this (117 of its 120 tensors were
zero, so the DSA layer contributed nothing); these numbers use a randomised one.
Pooled DSA selection. The indexer now cuts on the pool axis and expands each
selected pool into its members, instead of running top_k over cells. A reference-free
check counts partially selected pools (credit to #27754 for describing the failure mode
and this metric):
236 is the theoretical maximum, so the pool budget is fully spent and no pool is picked
twice. Before the fix the backends disagreed on the partial count while agreeing on the
broken-row count:
ggml_top_kdoes not order ties, so each backend split the same poolsdifferently. They now agree exactly.
MTP (NextN) draft graph.
test_mtpbuilds the draft head and reloads it:The graph is genuinely built, not silently bypassed: patching a
GGML_ABORTintograph_mtpmakes the test abort insidegraph_reserve. Draft logits are bit-identicalbetween the synthesized fixture and a real GGUF round-trip loaded with
load_mtp.Fixed along the way:
attention.recurrent_layerswas read withn_layer_allwhile thesaver writes per-layer arrays with
n_layer(), so any GGUF carrying a NextN block failedto load ("wrong array length; expected 5, got 4"). Inert for current files, since no
in-tree converter emits that key.
test-save-load-state, which #27755 now runs over every generated arch, passes onglm5next — all five subtests, no skip:
The restored continuations are identical to the baseline in all three restore paths,
including seq copy on device, so the indexer cache survives save/load and cross-sequence
copy.
Indexer workspace, chunked over tokens — contributed by @matteoscalabrini. The indexer
scores
[n_pool, n_head, n_tokens]and materialises that tensor twice, once by themul_matand once by thepermute+contthat brings the head axis intone[0]. That is2*n_pool*nh*n_tokens*4 Bper device, since the DSA layers spread across a layersplit, and it capped
ubatch— which with-ot exps=CPUis also what amortises theexpert stream over PCIe, so the cap cost prefill as well as memory. Scoring a token
depends only on its own query row and on
pooled, which is shared across the batch, sothe token loop splits into chunks with identical results and ggml-alloc reuses one buffer
across them. The chunk is derived from a fixed scratch target, so short contexts come out
unchunked on the previous code path. Measured on 5x RTX 3090,
UD-Q4_K_XL, f16 KV,experts on CPU:
Greedy output is byte-identical to the unchunked graph at matched ubatch, with
--parallel 1, with--parallel 2, and with a ragged final chunk; needle retrievalpasses at 28,788 and 243,314 tokens. The same
cont(permute(...))is present in #27754and #27773, so the workspace cost is not specific to this PR — the chunking is.
No regression:
glm-dsa,kimi-linear,deepseek4,qwen3nextanddeepseek32all still pass.Build is clean (0 errors, 0 warnings, 173 TUs rebuilt, Metal + Accelerate).
End to end on a 4-layer test model (3 KDA + 1 MLA/DSA): HF -> converter -> GGUF ->
llama-cliloads and generates.Tensor shapes were checked against the real checkpoint over HTTP range requests:
hc_attn_fn [24, 16384],index_kpool_compress_gate [128, 4096],index_kpool_compress_ape [4, 128].Shared files touched
src/llama-context.cppn_tokens*40graph_max_nodeslist (non-fused Sinkhorn: 20 iters x 2 sites x 45 layers)src/llama-graph.{h,cpp}build_ffnandbuild_moe_ffn;llm_graph_input_mem_hybrid_idx+build_inp_mem_hybrid_idx();build_attn_mask_top_k()lifted verbatim out of the DSAbuild_attnoverload so both share it; newbuild_attnoverload wheretop_k == nullptrmeans a dense masksrc/llama-memory-hybrid-idx.{h,cpp}idx_row_sizeconstructor parameter (0keeps theindexer_head_sizedefault), andset_input_kpool(), which fills the pool cells/bias/tail inputs so the top-k picks whole poolssrc/llama-model-saver.cppdsv4_hc_mult > 0does not force a DSV4-only keysrc/llama-arch.cppllm_arch_is_hybrid,llm_arch_supports_sm_tensor-> falsesrc/llama-model.{h,cpp}print_info,LLM_TYPE_312B_A17B16 files changed, 2326 insertions(+), 46 deletions(-).
Requirements
src/models/glm5next.cpp,conversion/glm5next.py) and the test entry were written with Claude Code and GLM 5.3-flash on ds4. Scope,design decisions and verification are mine; I ran the build, the arch tests and the
end-to-end conversion myself.