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
…the bridge answers O22 by refusing to decode the tower (mudler#2325) W5b as scoped was two changes with two oracles. The attention block and the `OwnedTensor` bridge answer to `transformers` v5.16.1 and to the llama.cpp [#27752](ggml-org/llama.cpp#27752) container, and both can be gated with no KV cache and no decoder layer over them; the decoder layer, the mHC threading and the forward answer additionally to `MakeGlm5NextKVCache` and to the `[T, hc_mult, hidden]` manifold. This is the first half. [mudler#2241](mudler#2241) stays open for the second. ## The attention block `Glm5NextTextAttention` (`modeling_glm5_next.py:1064-1257`) as a host f32 reference, the same shape `glm5_next_dsa.cpp`, `glm5_next_mhc.cpp` and `glm5_next_moe.cpp` already have. Three things a fluent wrong port gets wrong, each with a case that separates it rather than a tolerance that does not. **The converter SPLITS `kv_b_proj` and transposes only the K half.** The file carries `attn_k_b` at `[H, kv_lora, qk_nope]` and `attn_v_b` at `[H, v_head, kv_lora]`, so K contracts over its FIRST inner axis and V over its SECOND. At the published geometry a swap is a shape error, so the gate also runs a SQUARE case where the untransposed reading is perfectly shape-valid and merely wrong; it separates by **2.9469 over all 900 values**, where 900 is `kBatch(2) * kNumHeads(3) * kSeqLen(25) * kSqDim(6)`. The case prints the figure rather than leaving it to prose: ``` test_glm5_next_attn.cpp:373: MESSAGE: square k_b: transposed vs untransposed separation = 2.9469 over 900 of 900 values ``` **Cross-layer top-k sharing.** A `shared` layer builds no indexer and reuses the previous full layer's selection (`:1130-1134`, `:1181-1191`). A layer that recomputes runs, selects a plausible key set and emits plausible tokens, and nothing about the output's shape, finiteness or scale says otherwise. So the fixture carries BOTH the correct output and what a recomputing port produces from a decoy indexer, both captured from the same oracle run, and asserts ours is the first: 320 of 800 values differ, max separation 1.52, over 20 of 50 query rows. A separate case proves the decoy golden really is a recomputation, so the inequality is between two reference values and not between a reference and an arbitrary number. **Read that one with its caveat attached.** The `shared` arm is CONFIG-KEYED, and the published `GLM-5.3-Flash` `config.json` selects it on **zero of its 45 layers** — the suite measures that and prints `published schedule: 0 shared layers of 45`. So the sharing gated here is correct against `transformers` v5.16.1 on a schedule the released checkpoint does not contain. It is the same "unselected branch" shape as the rope half below, with one difference: the rope branch is REFUSED and this one is IMPLEMENTED and gated. It is also the shape that SURVIVES W5b-2 — once O25's reachability half is discharged the two files become reached and the `shared` arm still is not. O25 carries this. **The all-masked row is `finfo.min` and not `-inf`** (`:1253`). A left-padded query row has every key masked; `finfo.min` gives it a uniform softmax and a finite output, `-inf` gives it NaN through `o_proj` and into the residual stream for the rest of the stack. The `-inf` mutation reds 49 of 160 assertions. There is no rope branch, because upstream can reach none. `validate_architecture` (`configuration_glm5_next.py:225-228`) raises for any positive `qk_rope_head_dim` — measured by constructing one in the golden generator rather than described — so `expand_kv`'s concat has a zero-width second half and `key_states` IS `k_nope`. `MlaDims::Validate` mirrors the refusal in upstream's own words instead of half-implementing a branch no released config selects. ## The bridge, and O22 O22 left the residency choice open on purpose: "Whoever writes the forward decides whether to decode per layer or to go device-native." The decision is **decode ONE DSA layer at a time, on demand, and never retain the tower in float.** | what | GiB | |---|---:| | the published `UD-Q2_K_XL` artifact, block-resident as loaded | **101.14** | | the same tower with every tensor expanded | **426.72** | | all-bf16 | 597.46 | | usable on `dgx:gpu0`, the largest device this project reaches | **~119.63** | | ONE bridged DSA layer, f32 | **0.4654** | | all ELEVEN DSA layers held at once | 5.12 | A decoded tower is 3.57x over the box, and that is the figure [mudler#2245](mudler#2245) and [mudler#2247](mudler#2247) spent six pull requests removing. A float tower is not expensive; it does not exist on any hardware this project can reach. One layer is 499,657,728 bytes, 0.39% of the box, and the caller's peak is one layer because the mirror is a value it can drop. There is deliberately no `BridgeTower`, no cache and no map keyed by layer index, because each of those turns "one layer" into "every layer visited so far", which is the tower again with a slower ramp. Device-native was not chosen, for a stated reason rather than a preference: there is nothing to be device-native against while every glm5_next primitive on this row is a host reference and W3's CUDA arm is committed and unmeasured. That would be the "unpassed parameter" shape. W5b-2 revisits it. O19 / [mudler#2260](mudler#2260) stays live and this bridge cannot make it reachable. Structurally there is no overload taking `Glm5NextMoeWeights`, `Glm5NextMlpWeights` or any expert bank. Numerically the 1 GiB per-tensor ceiling sits EXACTLY 4x above the largest legitimate tensor (`o_proj`, 0.25 GiB) and EXACTLY 9x below the smallest expert bank (`up_exps`, 9.0 GiB); both sides are asserted, because a ceiling above everything is a mute switch and one below the real population fires on ordinary work. The check runs from the shape before any allocation, proved by handing the bridge a published-size bank carrying no bytes at all. `byte_ceiling` is a DEFAULT ARGUMENT, so the structural claim binds unconditionally and the numeric one binds every call that takes the default, which is every call in this tree; O25 says so. ## The bridge's four advertised refusals are now gates `glm5_next_bridge.h` lists four cases `DecodeOwnedTensorToF32` refuses by name. Review found that the block element-count check, both byte-span checks and the `default:` dtype arm could each be deleted with the suite staying 8/8 and 56/56. Two of those are not cosmetic: * without the elementwise byte-span check, `std::memcpy(out.data(), src, need)` reads `need` bytes out of a shorter `t.bytes` and the bridge **serves the heap as weight values** — finite, plausible, wrong, and invisible to a token gate; * without the `default:` arm, an encoding the bridge cannot widen falls off the end returning the **zero-filled buffer it allocated**, which is the failure the `host_released` refusal already exists to stop, reached by another door. Five cases pin them, each proved by disabling the refusal in a scratch copy, with the mutant's BUILD rc recorded beside its TEST rc because a mutant that does not build reads as a passing test, and the file restored byte-for-byte (sha256 `bee9a0f66d88914193e3c0f1d1d89d9840f7ca91bef79b0538f08945b54f99a7`) after each: | refusal | mutation | result | |---|---|---| | block element count is a whole number of blocks | `if (false)` | BUILD 0 / TEST 1, 1 assertion | | block byte span equals `RowSizeBytes` | `if (false)` | BUILD 0 / TEST 1, 3 assertions | | a block dtype has a `BlockToFloat` decoder | `if (false)` | BUILD 0 / TEST 0 — **SURVIVES** | | elementwise byte span equals `numel * SizeOf` | `if (false)` | BUILD 0 / TEST 1, 3 assertions | | `default:` refuses a non-float encoding | `return out;` | BUILD 0 / TEST 1, 4 assertions | The survivor is disclosed rather than chased, and it refines the review's finding. `vt::IsBlockQuant` is true for exactly the 18 dtypes `vt::cpu::BlockToFloat` answers for, so **no input can reach that arm in this build**: it is the unselected-branch shape, a guard for the encoding that lands next without a CPU decoder, which is the state IQ2_XS and IQ4_XS were in before [mudler#2245](mudler#2245). What the suite gates instead is the PREMISE, and that gate is ARMED rather than assumed. Rewriting `BlockToFloat`'s `kQ8_0` case to `return nullptr` (BUILD rc=0, after referencing the now-unused function so `-Werror=unused-function` does not turn the mutant into a build failure) reds the premise case at `CHECK(vt::cpu::BlockToFloat(d) != nullptr)` AND makes the refusal fire by name in two more, with the bridge's own message: ``glm5_next bridge: `moe.gate_exps` is q8_0, which this build has no `BlockToFloat` decoder for``. ## Seventy-four upstream citations did not resolve at v5.16.1 The ported behaviour is right everywhere it was checked; the citations were not. Re-resolving every anchor in this wave against `modeling_glm5_next.py` sha256 `2092bbb4efa2a8087b74f4a4da37635c503fe1df9ae73f1e6e8342af8b4b8e8b` at `refs/tags/v5.16.1` found **74 citation sites carrying 38 distinct wrong values**, off by 1 to 6 lines. AGENTS.md requires citing the `file:line` that was ported, and an anchor that lands on a blank line or excludes its own symbol sends the next reader somewhere useless. The worst: | cited | actual | what the citation missed | |---|---|---| | `:1126-1131` for `skip_topk` / `next_skip_topk` | 1130 and 1132-1134 | the range **excluded** `next_skip_topk` | | `:1165` for `q_resid` | 1167 | 1165 is **blank** | | `:1167-1171` for `CompressKv` | 1170-1172 | **excluded** `kv_a_layernorm` | | `:1157-1216` for `Attention` | 1155-1216 | `def forward` is at 1155 | | `:1180-1186` / `:1188-1192` for the selection and mask | 1181-1191 / 1193-1197 | the mask block sat **wholly outside** its anchor | | `configuration_glm5_next.py:219-226` for the NoPE refusal | 225-228 | 219-220 is an unrelated `index_topk % index_kpool` clause | One citation is deliberately left alone: the whole-class span `:1064-1257`, whose only slack is the blank line after the class's last statement at 1256. It excludes no symbol, and it is baked into the generated `glm5_next_attn_goldens.inc` banner, which cannot be regenerated without a live oracle install. ## Evidence **Oracle identity asserted, not assumed.** The golden generator hashes the INSTALLED `modeling_glm5_next.py` and refuses unless it is `2092bbb4efa2a8087b74f4a4da37635c503fe1df9ae73f1e6e8342af8b4b8e8b`, the value W3 (mudler#2213) and W5c (mudler#2242) both recorded, alongside the version string. **RED first**, from the plausible wrong port on the same tree in one build: 9 of 14 cases and 63 of 150 assertions in `test_glm5_next_attn`, 3 of 8 and 3 of 55 in `test_glm5_next_bridge`. That red also found two defects in the TESTS rather than the product — the refusal golden carried huggingface_hub's wrapper class name, and the bridge's shape case moved a dim `q_b_proj` also depends on, so it threw on the wrong tensor — both repaired before green. **GREEN**: `test_glm5_next_attn` **14/14 cases and 160/160 assertions**, `test_glm5_next_bridge` **13/13 and 96/96** (up from 8/8 and 56/56 with the five refusal cases), both exit 0, both rerun by hand after merging `origin/main`, since `agent-preflight.sh` runs the record and script gates and not the C++ suites. **Twenty-four negative mutations**, each sha256-proved applied, built and restored byte-for-byte. **Twenty-two kill their gate.** Two do not, and both are recorded with their reason rather than as passes: `host_f32_bytes` taken from the dims instead of from the buffers is an EQUIVALENT mutant while `DecodeShaped` refuses any shape disagreement (the test now pins the sum against the buffers themselves — an earlier version pinned it against the predictor and that mutation passed it), and the `BlockToFloat`-null arm is unreachable in this build, whose premise gate is armed instead and proved by removing the Q8_0 decoder. Two further findings came out of the earlier run and are fixed: the fixture could not tell `min(l+1, n-1)` from a wrapping `(l+1) % n`, so a schedule where they disagree was added, and one mutant failed to BUILD under `-Werror` on an unused parameter, which is a passing mutant proving nothing. **Every emitted golden is read by an assertion**, and a case names each array and the case that reads it. W3 emitted a `kIndexScores` golden that nothing consumed and two real scale defects then passed 1602 assertions; that is why this is a requirement here and not a courtesy. **`scripts/agent-preflight.sh --fail-on-skip`: ZERO skips, and two failures that are BASE rather than branch.** `check-env-doc` and `test_check_env_doc` both report `VT_QWEN35_STAGE_MIN_FREE_FRAC`, added by `207c12932` (mudler#2328). At `origin/main` that variable is read once under `src/` and appears **zero** times in `docs/ENVIRONMENT.md` and **zero** times in `scripts/env-doc-allowlist.txt`; this branch touches none of those three files. It is the [mudler#2312](mudler#2312) shape again, it is already tracked by open issue [mudler#2329](mudler#2329) with a live `row/ENV-DOC-2329` worktree on it, and it is deliberately NOT fixed here: a second fix to the same line is a conflict, not an in-flow repair. ## What is NOT reached, and who owns it Nothing in `glm5_next_attn.{h,cpp}` or `glm5_next_bridge.{h,cpp}` is called from a production entry point at this merge commit. `grep` over `src/`, `include/` and `examples/` for `glm5_next::Attention`, `BridgeDsaLayer`, `DecodeOwnedTensorToF32`, `IndexerRoleFor` and the two headers returns nothing outside the four files of this change. There is therefore no production call site to delete, so `.agents/reachability.md`'s reachability mutation is already answered: the change has no entry-point chain, and saying so is the answer. The wiring belongs to **W5b-2**, on row `MODEL-MM-glm5-next-glm5-next-for-conditional-generation`, tracked by [mudler#2241](mudler#2241) under campaign issue [mudler#1998](mudler#1998), and the spec lists it under `## Owed` as **O25**. O25 also records that the `shared` indexer arm stays unreached even after that wiring, and that `Numel` iterates `i < t.rank` against a fixed `shape[vt::kMaxRank]` — unreachable through the loader, which is the bridge's only producer. ## What is NOT claimed No token, no load and no speed number, and none was observed. No oracle for this model runs on any device this project reaches — the reference needs 305.78 GiB (FP8) or 598.5 GiB (BF16) against ~119.63 GiB — so what is gated is the NUMERICS of one block against a tiny-shape reference and nothing about the MODEL. The staged artifact was not opened at all: W5c already measured that a materialising load on this box stops at 8.09 GiB RSS in uninterruptible CIFS I/O, and this wave needs no artifact. No GPU was used, no `rc` lease was taken and no `ssh` to a fleet device was attempted. Refs mudler#2324, mudler#2241, mudler#1998. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code] --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1f817ef to
24652c5
Compare
|
Rebased on master — new head The branch is now rebased on current master. New head is Two things the rebase brought in:
Sanity after the rebase:
|
|
@tjluyao a correction on something I told you, not a nudge. The advice from the 27th — measure perplexity elsewhere — is void now. It was based on a real defect: my DSA selection was per-cell, so any number you produced would not have matched the reference. That is fixed the same day in What the branch has gained since, briefly: head The reason I'm writing: nobody has measured perplexity on this branch. It is the first gap in the description's "what this doesn't cover" list, and you are the only person who has converted the official checkpoint — 305.8 GiB, 72 files, No pressure on timing: four days have passed, and you may well have deleted the ~640 GB by now. If you have, or if you'd rather not rerun, a plain "no" is a perfectly good answer and I'll leave it there. |
…the bridge answers O22 by refusing to decode the tower (mudler#2325) W5b as scoped was two changes with two oracles. The attention block and the `OwnedTensor` bridge answer to `transformers` v5.16.1 and to the llama.cpp [#27752](ggml-org/llama.cpp#27752) container, and both can be gated with no KV cache and no decoder layer over them; the decoder layer, the mHC threading and the forward answer additionally to `MakeGlm5NextKVCache` and to the `[T, hc_mult, hidden]` manifold. This is the first half. [mudler#2241](mudler#2241) stays open for the second. ## The attention block `Glm5NextTextAttention` (`modeling_glm5_next.py:1064-1257`) as a host f32 reference, the same shape `glm5_next_dsa.cpp`, `glm5_next_mhc.cpp` and `glm5_next_moe.cpp` already have. Three things a fluent wrong port gets wrong, each with a case that separates it rather than a tolerance that does not. **The converter SPLITS `kv_b_proj` and transposes only the K half.** The file carries `attn_k_b` at `[H, kv_lora, qk_nope]` and `attn_v_b` at `[H, v_head, kv_lora]`, so K contracts over its FIRST inner axis and V over its SECOND. At the published geometry a swap is a shape error, so the gate also runs a SQUARE case where the untransposed reading is perfectly shape-valid and merely wrong; it separates by **2.9469 over all 900 values**, where 900 is `kBatch(2) * kNumHeads(3) * kSeqLen(25) * kSqDim(6)`. The case prints the figure rather than leaving it to prose: ``` test_glm5_next_attn.cpp:373: MESSAGE: square k_b: transposed vs untransposed separation = 2.9469 over 900 of 900 values ``` **Cross-layer top-k sharing.** A `shared` layer builds no indexer and reuses the previous full layer's selection (`:1130-1134`, `:1181-1191`). A layer that recomputes runs, selects a plausible key set and emits plausible tokens, and nothing about the output's shape, finiteness or scale says otherwise. So the fixture carries BOTH the correct output and what a recomputing port produces from a decoy indexer, both captured from the same oracle run, and asserts ours is the first: 320 of 800 values differ, max separation 1.52, over 20 of 50 query rows. A separate case proves the decoy golden really is a recomputation, so the inequality is between two reference values and not between a reference and an arbitrary number. **Read that one with its caveat attached.** The `shared` arm is CONFIG-KEYED, and the published `GLM-5.3-Flash` `config.json` selects it on **zero of its 45 layers** — the suite measures that and prints `published schedule: 0 shared layers of 45`. So the sharing gated here is correct against `transformers` v5.16.1 on a schedule the released checkpoint does not contain. It is the same "unselected branch" shape as the rope half below, with one difference: the rope branch is REFUSED and this one is IMPLEMENTED and gated. It is also the shape that SURVIVES W5b-2 — once O25's reachability half is discharged the two files become reached and the `shared` arm still is not. O25 carries this. **The all-masked row is `finfo.min` and not `-inf`** (`:1253`). A left-padded query row has every key masked; `finfo.min` gives it a uniform softmax and a finite output, `-inf` gives it NaN through `o_proj` and into the residual stream for the rest of the stack. The `-inf` mutation reds 49 of 160 assertions. There is no rope branch, because upstream can reach none. `validate_architecture` (`configuration_glm5_next.py:225-228`) raises for any positive `qk_rope_head_dim` — measured by constructing one in the golden generator rather than described — so `expand_kv`'s concat has a zero-width second half and `key_states` IS `k_nope`. `MlaDims::Validate` mirrors the refusal in upstream's own words instead of half-implementing a branch no released config selects. ## The bridge, and O22 O22 left the residency choice open on purpose: "Whoever writes the forward decides whether to decode per layer or to go device-native." The decision is **decode ONE DSA layer at a time, on demand, and never retain the tower in float.** | what | GiB | |---|---:| | the published `UD-Q2_K_XL` artifact, block-resident as loaded | **101.14** | | the same tower with every tensor expanded | **426.72** | | all-bf16 | 597.46 | | usable on `dgx:gpu0`, the largest device this project reaches | **~119.63** | | ONE bridged DSA layer, f32 | **0.4654** | | all ELEVEN DSA layers held at once | 5.12 | A decoded tower is 3.57x over the box, and that is the figure [mudler#2245](mudler#2245) and [mudler#2247](mudler#2247) spent six pull requests removing. A float tower is not expensive; it does not exist on any hardware this project can reach. One layer is 499,657,728 bytes, 0.39% of the box, and the caller's peak is one layer because the mirror is a value it can drop. There is deliberately no `BridgeTower`, no cache and no map keyed by layer index, because each of those turns "one layer" into "every layer visited so far", which is the tower again with a slower ramp. Device-native was not chosen, for a stated reason rather than a preference: there is nothing to be device-native against while every glm5_next primitive on this row is a host reference and W3's CUDA arm is committed and unmeasured. That would be the "unpassed parameter" shape. W5b-2 revisits it. O19 / [mudler#2260](mudler#2260) stays live and this bridge cannot make it reachable. Structurally there is no overload taking `Glm5NextMoeWeights`, `Glm5NextMlpWeights` or any expert bank. Numerically the 1 GiB per-tensor ceiling sits EXACTLY 4x above the largest legitimate tensor (`o_proj`, 0.25 GiB) and EXACTLY 9x below the smallest expert bank (`up_exps`, 9.0 GiB); both sides are asserted, because a ceiling above everything is a mute switch and one below the real population fires on ordinary work. The check runs from the shape before any allocation, proved by handing the bridge a published-size bank carrying no bytes at all. `byte_ceiling` is a DEFAULT ARGUMENT, so the structural claim binds unconditionally and the numeric one binds every call that takes the default, which is every call in this tree; O25 says so. ## The bridge's four advertised refusals are now gates `glm5_next_bridge.h` lists four cases `DecodeOwnedTensorToF32` refuses by name. Review found that the block element-count check, both byte-span checks and the `default:` dtype arm could each be deleted with the suite staying 8/8 and 56/56. Two of those are not cosmetic: * without the elementwise byte-span check, `std::memcpy(out.data(), src, need)` reads `need` bytes out of a shorter `t.bytes` and the bridge **serves the heap as weight values** — finite, plausible, wrong, and invisible to a token gate; * without the `default:` arm, an encoding the bridge cannot widen falls off the end returning the **zero-filled buffer it allocated**, which is the failure the `host_released` refusal already exists to stop, reached by another door. Five cases pin them, each proved by disabling the refusal in a scratch copy, with the mutant's BUILD rc recorded beside its TEST rc because a mutant that does not build reads as a passing test, and the file restored byte-for-byte (sha256 `bee9a0f66d88914193e3c0f1d1d89d9840f7ca91bef79b0538f08945b54f99a7`) after each: | refusal | mutation | result | |---|---|---| | block element count is a whole number of blocks | `if (false)` | BUILD 0 / TEST 1, 1 assertion | | block byte span equals `RowSizeBytes` | `if (false)` | BUILD 0 / TEST 1, 3 assertions | | a block dtype has a `BlockToFloat` decoder | `if (false)` | BUILD 0 / TEST 0 — **SURVIVES** | | elementwise byte span equals `numel * SizeOf` | `if (false)` | BUILD 0 / TEST 1, 3 assertions | | `default:` refuses a non-float encoding | `return out;` | BUILD 0 / TEST 1, 4 assertions | The survivor is disclosed rather than chased, and it refines the review's finding. `vt::IsBlockQuant` is true for exactly the 18 dtypes `vt::cpu::BlockToFloat` answers for, so **no input can reach that arm in this build**: it is the unselected-branch shape, a guard for the encoding that lands next without a CPU decoder, which is the state IQ2_XS and IQ4_XS were in before [mudler#2245](mudler#2245). What the suite gates instead is the PREMISE, and that gate is ARMED rather than assumed. Rewriting `BlockToFloat`'s `kQ8_0` case to `return nullptr` (BUILD rc=0, after referencing the now-unused function so `-Werror=unused-function` does not turn the mutant into a build failure) reds the premise case at `CHECK(vt::cpu::BlockToFloat(d) != nullptr)` AND makes the refusal fire by name in two more, with the bridge's own message: ``glm5_next bridge: `moe.gate_exps` is q8_0, which this build has no `BlockToFloat` decoder for``. ## Seventy-four upstream citations did not resolve at v5.16.1 The ported behaviour is right everywhere it was checked; the citations were not. Re-resolving every anchor in this wave against `modeling_glm5_next.py` sha256 `2092bbb4efa2a8087b74f4a4da37635c503fe1df9ae73f1e6e8342af8b4b8e8b` at `refs/tags/v5.16.1` found **74 citation sites carrying 38 distinct wrong values**, off by 1 to 6 lines. AGENTS.md requires citing the `file:line` that was ported, and an anchor that lands on a blank line or excludes its own symbol sends the next reader somewhere useless. The worst: | cited | actual | what the citation missed | |---|---|---| | `:1126-1131` for `skip_topk` / `next_skip_topk` | 1130 and 1132-1134 | the range **excluded** `next_skip_topk` | | `:1165` for `q_resid` | 1167 | 1165 is **blank** | | `:1167-1171` for `CompressKv` | 1170-1172 | **excluded** `kv_a_layernorm` | | `:1157-1216` for `Attention` | 1155-1216 | `def forward` is at 1155 | | `:1180-1186` / `:1188-1192` for the selection and mask | 1181-1191 / 1193-1197 | the mask block sat **wholly outside** its anchor | | `configuration_glm5_next.py:219-226` for the NoPE refusal | 225-228 | 219-220 is an unrelated `index_topk % index_kpool` clause | One citation is deliberately left alone: the whole-class span `:1064-1257`, whose only slack is the blank line after the class's last statement at 1256. It excludes no symbol, and it is baked into the generated `glm5_next_attn_goldens.inc` banner, which cannot be regenerated without a live oracle install. ## Evidence **Oracle identity asserted, not assumed.** The golden generator hashes the INSTALLED `modeling_glm5_next.py` and refuses unless it is `2092bbb4efa2a8087b74f4a4da37635c503fe1df9ae73f1e6e8342af8b4b8e8b`, the value W3 (mudler#2213) and W5c (mudler#2242) both recorded, alongside the version string. **RED first**, from the plausible wrong port on the same tree in one build: 9 of 14 cases and 63 of 150 assertions in `test_glm5_next_attn`, 3 of 8 and 3 of 55 in `test_glm5_next_bridge`. That red also found two defects in the TESTS rather than the product — the refusal golden carried huggingface_hub's wrapper class name, and the bridge's shape case moved a dim `q_b_proj` also depends on, so it threw on the wrong tensor — both repaired before green. **GREEN**: `test_glm5_next_attn` **14/14 cases and 160/160 assertions**, `test_glm5_next_bridge` **13/13 and 96/96** (up from 8/8 and 56/56 with the five refusal cases), both exit 0, both rerun by hand after merging `origin/main`, since `agent-preflight.sh` runs the record and script gates and not the C++ suites. **Twenty-four negative mutations**, each sha256-proved applied, built and restored byte-for-byte. **Twenty-two kill their gate.** Two do not, and both are recorded with their reason rather than as passes: `host_f32_bytes` taken from the dims instead of from the buffers is an EQUIVALENT mutant while `DecodeShaped` refuses any shape disagreement (the test now pins the sum against the buffers themselves — an earlier version pinned it against the predictor and that mutation passed it), and the `BlockToFloat`-null arm is unreachable in this build, whose premise gate is armed instead and proved by removing the Q8_0 decoder. Two further findings came out of the earlier run and are fixed: the fixture could not tell `min(l+1, n-1)` from a wrapping `(l+1) % n`, so a schedule where they disagree was added, and one mutant failed to BUILD under `-Werror` on an unused parameter, which is a passing mutant proving nothing. **Every emitted golden is read by an assertion**, and a case names each array and the case that reads it. W3 emitted a `kIndexScores` golden that nothing consumed and two real scale defects then passed 1602 assertions; that is why this is a requirement here and not a courtesy. **`scripts/agent-preflight.sh --fail-on-skip`: ZERO skips, and two failures that are BASE rather than branch.** `check-env-doc` and `test_check_env_doc` both report `VT_QWEN35_STAGE_MIN_FREE_FRAC`, added by `207c12932` (mudler#2328). At `origin/main` that variable is read once under `src/` and appears **zero** times in `docs/ENVIRONMENT.md` and **zero** times in `scripts/env-doc-allowlist.txt`; this branch touches none of those three files. It is the [mudler#2312](mudler#2312) shape again, it is already tracked by open issue [mudler#2329](mudler#2329) with a live `row/ENV-DOC-2329` worktree on it, and it is deliberately NOT fixed here: a second fix to the same line is a conflict, not an in-flow repair. ## What is NOT reached, and who owns it Nothing in `glm5_next_attn.{h,cpp}` or `glm5_next_bridge.{h,cpp}` is called from a production entry point at this merge commit. `grep` over `src/`, `include/` and `examples/` for `glm5_next::Attention`, `BridgeDsaLayer`, `DecodeOwnedTensorToF32`, `IndexerRoleFor` and the two headers returns nothing outside the four files of this change. There is therefore no production call site to delete, so `.agents/reachability.md`'s reachability mutation is already answered: the change has no entry-point chain, and saying so is the answer. The wiring belongs to **W5b-2**, on row `MODEL-MM-glm5-next-glm5-next-for-conditional-generation`, tracked by [mudler#2241](mudler#2241) under campaign issue [mudler#1998](mudler#1998), and the spec lists it under `## Owed` as **O25**. O25 also records that the `shared` indexer arm stays unreached even after that wiring, and that `Numel` iterates `i < t.rank` against a fixed `shape[vt::kMaxRank]` — unreachable through the loader, which is the bridge's only producer. ## What is NOT claimed No token, no load and no speed number, and none was observed. No oracle for this model runs on any device this project reaches — the reference needs 305.78 GiB (FP8) or 598.5 GiB (BF16) against ~119.63 GiB — so what is gated is the NUMERICS of one block against a tiny-shape reference and nothing about the MODEL. The staged artifact was not opened at all: W5c already measured that a materialising load on this box stops at 8.09 GiB RSS in uninterruptible CIFS I/O, and this wave needs no artifact. No GPU was used, no `rc` lease was taken and no `ssh` to a fleet device was attempted. Refs mudler#2324, mudler#2241, mudler#1998. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code] --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
|
@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. |
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.
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. |
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.