Skip to content

model: add GLM-5-Next (GLM-5.3-Flash) - #27754

Open
danielhanchen wants to merge 43 commits into
ggml-org:masterfrom
unslothai:glm5next/upstream
Open

model: add GLM-5-Next (GLM-5.3-Flash)#27754
danielhanchen wants to merge 43 commits into
ggml-org:masterfrom
unslothai:glm5next/upstream

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Adds support for GLM-5-Next (released as GLM-5.3-Flash), a 321.3B hybrid linear/sparse-attention MoE, plus its vision tower.

Running it

Two flags are currently required for correct output:

  • NVIDIA_TF32_OVERRIDE=0. ggml-cuda/common.cuh sets CUBLAS_TF32_TENSOR_OP_MATH unconditionally, so every fp32 GEMM otherwise runs at 10 mantissa bits. On a fixture this moved top-1 agreement from 0.896 to 0.9995.
  • -fa off. build_attn_mha casts the F32 latent to F16 before ggml_flash_attn_ext, which is the one place MLA cannot afford it.

KV cache type is not a correctness requirement. f16 costs +0.0005 PPL at ctx 2048 and is 0.0018 lower at 4096, both within engine-to-engine noise.

Performance

1x B200, GLM-5.3-Flash UD-IQ1_S, llama-bench -ngl 999 --flash-attn on -ctk f16 -ctv f16 -lm none -p 512 -n 32 -r 3. Before is f30bed8, after is 0069971.

test before t/s after t/s
pp512 1121.80 1118.93
tg32 62.79 63.10
tg32 @ d4096 53.52 59.50
tg32 @ d16384 41.02 57.99
tg32 @ d65536 20.66 48.99

Perplexity over one chunk is unchanged at 3.3612 +/- 0.40158.

MTP / NextN speculative decoding

--spec-type draft-mtp. llama-cli -c 32768 --temp 0 --seed 0 -n 256:

prompt MTP off n=2 n=3 n=5
short 58.6 86.5 80.2 63.7
16K 55.0 77.2

AI Usage

Used Claude and Local Models for testing, iteration and code design - manual verification of model / PR usage

danielhanchen and others added 20 commits August 26, 2026 16:48
Metadata and tensor loading only. The graph entry point throws, as qwen4exp
did at the same stage.

kda.gate_lower_bound is read as required: kimi-k3 selects the softplus branch
when it is absent, which is a different function rather than a missing clamp.

The absorbed MLA projections are 3D, so glm5next joins bailingmoe3 in the MXFP4
carve-out that would otherwise quantize them as expert tensors.
glm5next's mHC is DeepSeek-V4's hyper-connection block: same wide residual,
same 24-row mixer split, same two activations, same Sinkhorn. Only the final
collapse differs, so the graph derives from llama_model_deepseek4::graph and
reuses build_hc_pre / build_hc_post / build_hc_sinkhorn rather than restating
them, as graph_dsv4 already does in dflash.cpp.

dsv4_hc_mean becomes a static member so both archs can reach it; the body and
both deepseek4 call sites are otherwise untouched. The generated code for
deepseek4 is unchanged apart from the endbr64 landing pad the helper now needs
as a global symbol.

The four streams start as exact copies of the token embedding and collapse to
an unweighted mean after the last layer: this checkpoint has no hc_head.

KDA, DSA and the MoE land in later commits, so the two sublayers throw. The
mHC wiring around them is final.
Copy-adapts kimi-k3's KDA layer rather than kimi-linear's or bailingmoe3's: it
already matches on the recurrence ordering, the bounded-sigmoid decay gate and
its branch selection, dt_bias added per channel before the reshape, per-head A
broadcast, SiLU after the conv, f/g/beta read from the pre-convolution hidden
states, and the gated output RMSNorm with a plain weight.

Three differences from kimi-k3. The output gate is low rank, g_b(g_a(x)) as in
kimi-linear, which is what PR 1's converter emits. The q/k L2 eps is a literal
1e-6, the reference's own constant, not f_norm_rms_eps; ggml_l2_norm implements
max(sqrt(sum), eps) rather than sqrt(sum + eps), which at head_dim 128 differs
by about eps/(2*sum) and never trips the clamp, so it is close but not
bit-exact. And the cross-layer residual, latent MoE, situ activation and MLA
output gate have no counterpart here.

The conv follows the reference and convolves q|k|v as one depthwise kernel,
which keeps the conv state a single contiguous block so build_conv_state can
snapshot it. That plus build_recurrent_attn is what makes the layer safe under
recurrent-state rollback, so the arch joins llm_arch_supports_rs_rollback;
without that entry the guard in llama_context silently clamps n_rs_seq to 0.

build_delta_net_autoregressive reshaped a per-channel KDA gate onto ne1, but ne0
is the key axis everywhere else in that function, so it decayed along the value
axis. Invisible for GDN, where the gate is scalar and both spellings produce the
same [1, 1, H_v, n_seqs], and invisible to the shape checks because S_k == S_v.
Fixed rather than asserted around, since glm5next reaches that path on any
backend without the fused operator.

llama_model_deepseek4::graph now derives from llm_build_delta_net_base so
glm5next, which derives from it for the mHC residual, can reach build_delta_net.
The base is a method-only mixin over llm_graph_context with no data members and
no virtuals beyond the destructor llm_graph_context already has; deepseek4.cpp,
dflash.cpp and kimi-k3.cpp compile to byte-identical instructions across the
change.

graph_max_nodes moves the arch to kimi-k3's tier. Measured on the Tiny fixture
with the chunked fallback: 182 nodes plus 15/16 per token for each KDA layer and
46 per layer for the mHC mixers, so the 45-layer model needs 8.3k + 31.9 per
token before DSA or the MoE are counted, which overruns the n_tokens*40 budget.

test-llama-archs synthesised no MLA, hyper-connection, kpool or expert-weight
keys for glm5next, so PR 1's required get_key calls threw out of the sweep and
truncated it at 75 of 143 architectures. The fixture is complete now and the row
is skipped explicitly while the DSA and feed-forward sublayers still throw.
The routing is DeepSeek-V3 noaux_tc exactly as build_moe_ffn already implements
it: sigmoid scores, exp_probs_b added for the top-k SELECTION only, weights
gathered from the unbiased scores, normalised, then scaled by
routed_scaling_factor. n_group and topk_group are both 1, so the group-limited
stage is degenerate and build_moe_ffn's n_expert_groups > 1 guard skips it; no
group keys are written and none are needed.

The clamp is the one thing that needed a change outside this arch. glm5next
clamps the gate max-only and the up symmetrically, both BEFORE the SiLU, which
is what the branch behind the DEEPSEEK4/DFLASH arch gate already does; the else
branch clamps after the SiLU and is a different function. Adding the arch to
both gates reuses it rather than restating it. The two conditions are separate
because the dense path and the MoE path read different hparams arrays.

The leading dense layers clamp too. The reference builds them from the same
Glm5NextTextMLP as the shared expert, so swiglu_limit is not MoE-only, and the
converter already writes swiglu_clamp_shexp for every layer rather than only the
sparse ones. The shared expert is added unscaled.
nope-only MLA in the absorbed form, over every cached position. below
index_topk + index_kpool - 1 resident tokens the indexer selects all of them,
so this is exactly what the sparse path degenerates to, and it is a reference
the sparse commit can be checked against.

the attention half of the hybrid memory becomes the K-only variant: after
absorption the cache holds the kv_lora_rank latent and V is a view of K.
both are required keys for glm5next, so a model saved without them cannot be
loaded back. this is what stops test-llama-archs from round-tripping the arch.
the DSA sublayer no longer throws, so the arch can construct and run. it needs
the MLA head shape as well: with n_head_kv taken from the per-layer array it
would size the K cache row n_head times wider than the latent the graph writes.
index_topk + index_kpool - 1 is the number of positions the indexer keeps, and it
is what makes the dense attention this branch builds exactly equal to the sparse
path below that many cached tokens. an off-by-one in it is invisible to every
output comparison measured so far, on both a dense and a sparse fixture, so it is
checked against a second spelling of the same arithmetic instead.
The DSA layers of this model score pools of index_kpool consecutive positions
rather than single keys, and the pooled key cannot be rebuilt from the MLA
latents. llama_memory_hybrid therefore gains an optional third cache holding one
indexer key and one compressor gate per token, so the hybrid carries the KDA
conv+recurrent state, the MLA latents and the indexer keys at once.

Absent unless filter_idx is given, which defaults to null, so every existing
architecture gets exactly what it got before, state file layout included.

Two heads per cell, not one. GLM's compressor is not a mean pool: it is a
per-channel softmax over the kpool slots with logits gate + ape, where the gate
is a second projection of the hidden state of width indexer_head_size. Caching
it beside the key is the only way a pool survives its member tokens leaving the
batch. Architectures with indexer_kpool == 0 still get one head.

The indexer cache is handed the attention cache's slot layout rather than
finding its own, so the two agree cell for cell, and apply() asserts they do.
It also keeps its own dtype: -ctk q8_0 would otherwise quantise the gates, which
feed a softmax.

llama-kv-cache-kpool.{h,cpp} builds the pool <-> cell map host side. Pools are
defined on positions and cells are whatever find_slot handed out, so the
correspondence cannot be derived in the graph. Nothing here emits a negative
index: ggml_set_rows asserts i1 >= 0, so unpopulated entries are clamped into
range and neutralised by an additive -INFINITY instead.

Two things the map does that the qwen4exp shape it is ported from does not:

  - the top-k budget is indexer_top_k exactly, with the always-selected tail
    biased to -INFINITY so it spends none of it, and forced back in through a
    host-built base mask for the scatter. indexer_top_k is a whole number of
    pools, so the cut lands on a pool boundary; the reference's own output width
    of indexer_top_k + kpool - 1 does not, and ggml_top_k is unordered among
    equals on both CPU and CUDA.
  - one map per ubatch, shared by every indexer layer, since nothing in it
    depends on the layer. Measured on a 16 Ki cell cache with 512 tokens:
    ~4 ms once against ~4 ms x n_layers.

A unified cache with more than one sequence would let two sequences at the same
position pool each other's keys, so create_memory refuses it up front rather
than aborting mid-run.

tests/test-glm5next-memory.cpp: 74 checks, 0 failures, on both the full and the
trunk-only fixture. test-llama-archs is byte identical to the same build without
this commit at a fixed seed: 452 rows, 0 FAIL. Session state files for
qwen3next, falcon-h1, minimax-01, qwen35moe and a real Falcon-H1-0.5B are byte
identical too, across write, reload and rewrite.
Builds the pooled lightning indexer and gives the DSA layers a sparse attention
path driven by it.

Top-k runs over the POOL axis at select_k = index_topk/index_kpool, and the
selected pools are expanded to their member cells through pool_cells. That is
the reference's own two-step (modular_glm5_next.py, Glm5NextTextIndexer.forward:
topk over the pool axis, then selected_indices = pool_indices[batch_idx,
selected]), and it is not interchangeable with a single top-k of width
index_topk over member cells. The argument for the cell-level form - a pool's
members carry its score bit-exactly, so the cut must land on a pool boundary -
assumes tie groups never span pools. They do: ReLU drives most pool scores to
exactly 0.0, and ggml_top_k is explicitly unordered among equals, so the cut
falls inside an inter-pool tie group and splits a pool. Measured on TinySparse
at 512 tokens, the cell-level form leaves a partial pool on 7.51% of query rows
at layer 3 and 5.93% at layer 7; this form leaves none.

The indexer key and gate STORE is unconditional; only the SCORING is gated, on
n_ctx > index_topk + index_kpool - 1. Gating the store the same way would leave
every cell written below n_select with no indexer state, and the first ubatch to
cross n_select would pool cells that were never written.

Nothing here changes any other architecture: test-llama-archs produces a table
byte-identical to the parent's, 300 rows over 143 archs, 0 FAIL.
The tower is the GLM-OCR ViT with a clamped SwiGLU: the gate is bounded
above only, the up projection on both sides, and both before the SiLU.
ggml_swiglu_oai clamps the same way but then adds one to the up branch,
which is a gpt-oss detail this model does not share, so this adds an
FFN_SILU_CLAMP op rather than reusing it.

The clamp sits at the per-block MLP and again at the merger. Both read
hparams.ffn_op, so the graph body stays the GLM-4V one and the pair is
covered together.

It gets its own projector type rather than a flag on glm4v because the
image token limits differ (16/8000 against 8/4096, per the GLM-5.3-Flash
preprocessor) and those are hardcoded per projector, and because the
clamp must stay off for GLM-4V and GLM-OCR.

Also writes clip.vision.spatial_merge_size. No GLM4V-family mmproj has
ever carried it: Glm4VVisionModel skips the Qwen3VL parameters, which is
where it is written, so clip.cpp's hardcoded 2 has been carrying it.

Images only. glm5next spells video with its own token pair and distinct
start/end spans, and that is not handled here.
the vision tower shipped with the shared dynamic-size preprocessor, which is a
qwen-style smart_resize. the 2026-08-26 GLM-5-Next adaptation resizes
differently: both edges are aligned up by ceil rather than round, an over-budget
image is fitted by binary searching the content height for the largest aligned
canvas still within max_pixels, and the resized content is pasted into the
top-left of that canvas rather than centred and stretched to fill it. an image
already at or above min_pixels is never upscaled.

min_pixels/max_pixels stay in tokens. the reference scales them by
temporal_factor * factor**2 and compares against aligned_frames * area, and
aligned_frames equals temporal_factor for a still image, so the two cancel and
hparams.image_min_pixels / image_max_pixels (16 and 8000 tokens, 12544 and
6272000 pixels) are used directly.

glm4v and glm-ocr keep the dynamic-size preprocessor.

images only. video has its own token pair (154855, distinct from the image
token 154854) with its own start/end spans, and is out of scope here.

the resize arithmetic is covered in test-mtmd-impl against values taken from the
reference processor, including the 16- and 8000-token boundaries, extreme aspect
ratios, and inputs where the binary search and smart_resize disagree.
glm4 / chatglm-bpe tokenizer.json files set "ignore_merges": true, meaning a
pre-token that is already a vocab entry is emitted directly and the merge loop
never runs. llama.cpp implements this (llama-vocab.cpp, the get_ignore_merges()
short-circuit) but only enables it for a hardcoded list of pre-tokenizer names,
and glm4 was never added.

Without it the merges are applied - correctly - and reach a different answer,
because greedy BPE cannot always reconstruct a vocab entry from its bytes.
" 王" (Ġçİĭ, id 102322) is the case that exposed it: from Ġ ç İ ĭ the only
merges available are (Ġ,ç)=27944, (ç,İ)=76417 and (çİ,ĭ)=239209, so the lowest
rank wins first and yields Ġç İ ĭ, at which point neither (Ġç,İ) nor (İ,ĭ)
exists and it stops three tokens short. Reaching Ġçİĭ needs (Ġ,çİĭ) at 242943,
which requires never taking (Ġ,ç) at 27944.

The trigger is whitespace immediately before a CJK character, so pure Chinese
prose is unaffected and mixed Chinese-English is not:

  pure Chinese prose        620 vs 620 tokens, already identical
  mixed Chinese-English     680 -> 600 tokens, now identical to HF (-13.3%)
  wikitext-2 (289569 tok)   one divergence -> byte-identical

Found while comparing GLM-5.3-Flash perplexity against transformers, vLLM and
SGLang: the mismatch bounded how many scoring windows could be compared at long
context, and reads exactly like a model-port defect rather than a tokenizer one.
The scripted resolution used for the rebase mangled four files: it spliced a
condition into the middle of graph_max_nodes' multi-line else-if, dropped the
mtmd_image_preprocessor_glm5next declaration, dropped llama-kv-cache-kpool.cpp
from src/CMakeLists.txt (undefined llama_kpool_* and the llm_graph_input_kpool
vtable at link time), and left an "} else {" immediately followed by an
"} else if" in test-llama-archs.

These files are byte-identical between this base and the tree the glm5next
work was verified on, so each is taken from there verbatim.
@ggml-gh-bot

ggml-gh-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown

Hi @danielhanchen, thanks for your contribution!

Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:

  • PR Template not respected: Please respect the template when creating a new pull request. Make sure to fill out all required sections.

  • Multiple open PRs from a new contributor: We limit new contributors (those without a previously merged PR) to 1 open PR at a time. You currently have 4 open PRs.

  • Large PR: Large changes require prior discussion (e.g. an issue or RFC) and maintainers may not be able to review this PR as-is. Consider splitting it into smaller, focused PRs.


Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below.

@github-actions github-actions Bot added model Model specific testing Everything test related mtmd Related to multimodal functionality (video/image/audio) conversion labels Aug 26, 2026
@eauchs

eauchs commented Aug 26, 2026

Copy link
Copy Markdown

you'll probably get the same comment from ggerganov as on #27742 — I have a version where llama_memory_hybrid and llama_kv_cache stay untouched, in #27752 if useful !

deepseek4 sets n_embd_out_impl to hc_mult*n_embd to size its MTP h input.
glm5next inherited that, but our t_embd is build_norm(build_hc_mean(...)),
which is [n_embd, n_tokens]. n_embd_out() therefore reported 4*n_embd while
the tensor held n_embd, and llama-context read n_outputs*n_embd_out floats
out of it, four times what is there.

The assert at that site sizes the destination buffer, so nothing catches the
short source. Only --embeddings and llama_get_embeddings* reach the path,
which is why plain generation never showed it.

Note for when the NextN graph starts consuming h: give MTP its own width
rather than widening n_embd_out again.
The mHC residual mixers, the lightning indexer (selection gate, learned
k-pool position table, and the three indexer projections) and the KDA
recurrence gates are about 1 GiB in total on GLM-5.3-Flash, so the size cost
is noise against a 100-240 GB quant. Quantizing them perturbs which pools
the indexer selects and how much state each KDA step retains, and those
errors compound along a sequence rather than averaging out.

Both spellings are required. The compressor tensors arrived with the
DeepSeek-V4 merge and use an underscore (indexer_compressor_ape / _gate),
while the projections use a dot (indexer.proj / .attn_k / .attn_q_b), so a
single "indexer." prefix test silently misses the compressor pair.

attn_q_a, attn_kv_a_mqa, attn_k_b and attn_v_b are deliberately not listed.
They are precision sensitive too, but the release recipe pins them to q8_0
via --tensor-type, and that is the configuration the shipped quants were
measured in.

Verified with llama-quantize --dry-run q4_k_m on the BF16: all 12 pinned
families report 0 quantized (45 mHC, 12 indexer, 34 KDA each), while
ffn_gate_exps 43/43, attn_q_a 12/12 and attn_output 46/46 still quantize.
The 101-line glm5next block was dropped from test-mtmd-impl.cpp when the
vision work was rebased, even though the commit message still claimed the
resize arithmetic was covered there. It holds the 36-case table over the
16- and 8000-token budget boundaries, including six cases annotated as ones
where a naive smart_resize disagrees, so it is the guard against sliding
back to stretch-to-fill instead of ceil-align plus zero pad.

Restored from 29c096371. test-mtmd-impl now runs 216 assertions, of which
glm5next_resize contributes 185.
@aktalascloud

Copy link
Copy Markdown

Guys please add this model support I wait it so much...

@goodglitch

Copy link
Copy Markdown

Testing this branch on 2x3090: GLM rebuilds its decode graph on every token, which costs roughly 7% of decode throughput.

I am curious, what is your command to use 2x 3090 and what is your rig? Currently I have miserable 3.4t/s tg and 24t/s pp on Q8_0 with one 3090 and 4 channel DDR4-2400 rig on unsloth fork.

@noonghunna

Copy link
Copy Markdown

@goodglitch — 3.4 t/s tg is not a hardware limit on your box, it's the quant. Two things dominate here and neither is the GPU.

Q8_0 is the main problem. GLM-5.3-Flash is a 320.76B MoE. At Q8_0 essentially all of it lives in host RAM and every token pays full-precision dequant on the CPU. We run unsloth/UD-IQ3_XXS (~114 GB resident) or UD-IQ4_XS (~147 GB). Dropping to UD-IQ3_XXS on its own should be worth several times your current tg, before any tuning.

Then threads. On CPU-offloaded MoE the useful -t zone is an absolute ~24-32 threads, not nproc/2. We measured 16 threads about 20% slower than 28 on a 32-core box. If you're on llama.cpp's default you are very likely leaving that on the table — this one is free and independent of quant.

Our config, for reference: 2× RTX 3090 (24 GB each, PCIe, no NVLink), 32-core EPYC, 8-channel DDR4-3200 at ~87 GB/s STREAM Triad, 204,800 context, -t 28, routed experts held in host RAM with -ot. Note --n-cpu-moe cannot balance across two GPUs — -ot is what actually places them.

What we measure there (please read the caveat below): decode ~17-22 tok/s and prefill ~300-340 tok/s at 10K, single stream, on UD-IQ3_XXS.

⚠️ That is not this PR branch. Those numbers come from our own llama.cpp build carrying an expert-cache on top of this work — hot experts get cached back onto the GPUs — so the cache portion is not reproducible from #27754 as-is, and I don't want to send you chasing a number you can't reach from here. The quant choice and the thread count are the transferable parts, and I'd expect both to help you substantially on stock.

Setup for the whole thing, including the exact compose and flags, is written up here:
noonghunna/club-3090#1117

The two-card GLM slug is llamacpp-club3090/glm53-flash-dual-iq3xxs-moecache. Single-3090 is not a configuration we've booted, so I can't give you a validated single-card recipe — with 24 GB and 4-channel RAM I'd expect host RAM bandwidth to be your ceiling rather than the GPU, and the quant to matter more than anything else you change.

One more thing worth knowing given you're testing this branch: it rebuilds the decode graph on every token (the llm_graph_input_kpool point above), which costs ~7% of decode throughput. That's on top of everything else, and it affects your setup too.

@EugeoSynthesisThirtyTwo

EugeoSynthesisThirtyTwo commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Hi, thanks for supporting the model !
I couldn't find how to call tools, like google searches

localhost:8080/props says this

"supports_tool_calls":false
"supports_tools":false

Full json below

{"default_generation_settings":{"params":{"seed":4294967295,"temperature":1.0,"dynatemp_range":0.0,"dynatemp_exponent":1.0,"top_k":40,"top_p":0.949999988079071,"min_p":0.05000000074505806,"top_n_sigma":-1.0,"xtc_probability":0.0,"xtc_threshold":0.10000000149011612,"typical_p":1.0,"repeat_last_n":64,"repeat_penalty":1.0,"presence_penalty":0.0,"frequency_penalty":0.0,"dry_multiplier":0.0,"dry_base":1.75,"dry_allowed_length":2,"dry_penalty_last_n":64,"mirostat":0,"mirostat_tau":5.0,"mirostat_eta":0.10000000149011612,"adaptive_target":-1.0,"adaptive_decay":0.8999999761581421,"max_tokens":-1,"n_predict":-1,"n_keep":0,"n_discard":0,"ignore_eos":false,"stream":false,"n_probs":0,"min_keep":0,"chat_format":"Content-only","reasoning_format":"none","reasoning_in_content":false,"generation_prompt":"","samplers":["penalties","dry","top_n_sigma","top_k","typ_p","top_p","min_p","xtc","temperature"],"speculative.types":"none","timings_per_token":false,"post_sampling_probs":false,"backend_sampling":false,"lora":[]},"n_ctx":65536},"total_slots":1,"model_alias":"GLM-5.3-Flash-Uncensored","model_ftype":"Q8_0","model_path":"C:\\Users\\me\\Docs\\model\\GLM-5.3\\GLM-5.3-Flash-UNCENSORED-FP8-UD-Q4_K_XL.gguf","modalities":{"vision":true,"video":true,"audio":false},"media_marker":"<__media_JfvwAVKMnK3Hh4TwDuhMSq7lCUuITT1L__>","endpoint_slots":true,"endpoint_props":false,"endpoint_metrics":false,"ui":true,"ui_settings":{},"chat_template":"[gMASK]<sop>\n{%- set effective_reasoning_effort = reasoning_effort if reasoning_effort is defined and reasoning_effort in ['low', 'high'] else 'max' -%}\n{%- if effective_reasoning_effort is not none -%}<|system|>Reasoning Effort: {{ effective_reasoning_effort | capitalize }}{%- endif -%}\n{%- set clear_thinking = clear_thinking if clear_thinking is defined else false -%}\n{%- if tools -%}\n{%- macro tool_to_json(tool) -%}\n    {%- set ns_tool = namespace(first=true) -%}\n    {{ '{' -}}\n    {%- for k, v in tool.items() -%}\n        {%- if k != 'defer_loading' and k != 'strict' -%}\n            {%- if not ns_tool.first -%}{{- ', ' -}}{%- endif -%}\n            {%- set ns_tool.first = false -%}\n            \"{{ k }}\": {{ v | tojson(ensure_ascii=False) }}\n        {%- endif -%}\n    {%- endfor -%}\n    {{- '}' -}}\n{%- endmacro -%}\n{%- macro tool_references_to_response(refs) -%}\n    {{- '<tool_response><tools>\\n' -}}\n    {%- for tr in refs -%}\n        {%- for tool in tools -%}\n            {%- if 'function' in tool -%}\n                {%- set tool = tool['function'] -%}\n            {%- endif -%}\n            {%- if tool.name == tr.name -%}\n                {{- tool_to_json(tool) + '\\n' -}}\n            {%- endif -%}\n        {%- endfor -%}\n    {%- endfor -%}\n    {{- '</tools></tool_response>' -}}\n{%- endmacro -%}\n<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{% for tool in tools %}\n{%- if 'function' in tool -%}\n    {%- set tool = tool['function'] -%}\n{%- endif -%}\n{% if tool.defer_loading is not defined or not tool.defer_loading %}\n{{ tool_to_json(tool) }}\n{% endif %}\n{% endfor %}\n</tools>\n\nFor each function call, output the function name and arguments within the following XML format:\n<tool_call>{function-name}<arg_key>{arg-key-1}</arg_key><arg_value>{arg-value-1}</arg_value><arg_key>{arg-key-2}</arg_key><arg_value>{arg-value-2}</arg_value>...</tool_call>{%- endif -%}\n{%- macro emit_image() -%}<|begin_of_image|><|image|><|end_of_image|>{%- endmacro -%}\n{%- macro emit_video() -%}<|begin_of_video|><|video|><|end_of_video|>{%- endmacro -%}\n{%- macro emit_audio() -%}<|begin_of_audio|><|end_of_audio|>{%- endmacro -%}\n{%- macro visible_text(content) -%}\n    {%- if content is string -%}\n        {{- content -}}\n    {%- elif content is iterable and content is not mapping -%}\n        {%- for item in content -%}\n            {%- if item is mapping and item.type == 'text' -%}\n                {{- item.text -}}\n            {%- elif item is string -%}\n                {{- item -}}\n            {%- elif item is mapping and item.type in ['image', 'image_url'] -%}\n                {{- emit_image() -}}\n            {%- elif item is mapping and item.type in ['video', 'video_url'] -%}\n                {{- emit_video() -}}\n            {%- elif item is mapping and item.type in ['audio', 'audio_url', 'input_audio'] -%}\n                {{- emit_audio() -}}\n            {%- endif -%}\n        {%- endfor -%}\n    {%- else -%}\n        {{- content }}\n    {%- endif -%}\n{%- endmacro -%}\n{%- macro tool_response(text) -%}\n{{- '<tool_response>' + text + '</tool_response>' -}}\n{%- endmacro -%}\n{%- macro render_tool_response(m) -%}\n{%- if m.content is string -%}\n    {{- tool_response(m.content) -}}\n{%- elif m.content and m.content is not mapping and m.content.0.type == \"tool_reference\" -%}\n    {{- tool_references_to_response(m.content) -}}\n{%- elif is_list_of_outputs(m) -%}\n    {%- for tr in m.content -%}\n        {%- if tr.output is iterable and tr.output is not string and tr.output is not mapping and tr.output and tr.output.0.type == \"tool_reference\" -%}\n            {{- tool_references_to_response(tr.output) -}}\n        {%- else -%}\n            {{- tool_response(visible_text(tr.output)) -}}\n        {%- endif -%}\n    {%- endfor -%}\n{%- else -%}\n    {{- tool_response(visible_text(m.content)) -}}\n{%- endif -%}\n{%- endmacro -%}\n{%- macro id_of(obj) -%}\n    {%- if obj.tool_call_id -%}\n        {{- obj.tool_call_id -}}\n    {%- elif obj.id -%}\n        {{- obj.id -}}\n    {%- endif -%}\n{%- endmacro -%}\n{%- macro is_list_of_outputs(m) -%}\n    {%- if m.content and m.content.0.output is defined -%}1{%- endif -%}\n{%- endmacro -%}\n{%- macro has_dup_tool_result_id(lo, hi, target) -%}\n    {%- set ns_cnt = namespace(n=0) -%}\n    {%- for k in range(lo, hi + 1) -%}\n        {%- set m = messages[k] -%}\n        {%- if is_list_of_outputs(m) -%}\n            {%- for entry in m.content -%}\n                {%- if id_of(entry) == target -%}\n                    {%- set ns_cnt.n = ns_cnt.n + 1 -%}\n                {%- endif -%}\n            {%- endfor -%}\n        {%- elif id_of(m) == target -%}\n            {%- set ns_cnt.n = ns_cnt.n + 1 -%}\n        {%- endif -%}\n        {%- if ns_cnt.n > 1 -%}{%- break -%}{%- endif -%}\n    {%- endfor -%}\n    {%- if ns_cnt.n > 1 -%}1{%- endif -%}\n{%- endmacro -%}\n{%- macro tc_id_exists(tcs, target) -%}\n    {%- set ns_f = namespace(found=false) -%}\n    {%- for tc in tcs -%}\n        {%- if id_of(tc) == target -%}\n            {%- set ns_f.found = true -%}\n            {%- break -%}\n        {%- endif -%}\n    {%- endfor -%}\n    {%- if ns_f.found -%}1{%- endif -%}\n{%- endmacro -%}\n{%- set ns = namespace(last_user_index=-1) -%}\n{%- for m in messages %}\n    {%- if m.role == 'user' %}\n        {%- set ns.last_user_index = loop.index0 -%}\n    {%- endif %}\n{%- endfor %}\n{%- for m in messages -%}\n{%- if m.role == 'user' -%}<|user|>{{ visible_text(m.content) }}\n{%- elif m.role == 'assistant' -%}\n<|assistant|>\n{%- set content = visible_text(m.content) %}\n{%- if m.reasoning_content is string %}\n    {%- set reasoning_content = m.reasoning_content %}\n{%- elif '</think>' in content %}\n    {%- set reasoning_content = content.split('</think>')[0].split('<think>')[-1] %}\n    {%- set content = content.split('</think>')[-1] %}\n{%- endif %}\n{%- if (not clear_thinking or loop.index0 > ns.last_user_index) and reasoning_content is defined -%}\n{{ '<think>' + reasoning_content +  '</think>'}}\n{%- else -%}\n{{ '<think></think>' }}\n{%- endif -%}\n{%- if content.strip() -%}\n{{ content.strip() }}\n{%- endif -%}\n{% if m.tool_calls %}\n{% for tc in m.tool_calls %}\n{%- if tc.function %}\n    {%- set tc = tc.function %}\n{%- endif %}\n{{- '<tool_call>' + tc.name -}}\n{% set _args = tc.arguments %}{% for k, v in _args.items() %}<arg_key>{{ k }}</arg_key><arg_value>{{ v | tojson(ensure_ascii=False) if v is not string else v }}</arg_value>{% endfor %}</tool_call>{% endfor %}\n{% endif %}\n{%- elif m.role == 'tool' -%}\n{%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n    {{- '<|observation|>' -}}\n    {%- set block_start = loop.index0 -%}\n    {%- set ns_blk = namespace(end=block_start) -%}\n    {%- for j in range(block_start, messages|length) -%}\n        {%- if messages[j].role == 'tool' -%}\n            {%- set ns_blk.end = j -%}\n        {%- else -%}\n            {%- break -%}\n        {%- endif -%}\n    {%- endfor -%}\n    {%- set ns_a = namespace(tool_calls=none) -%}\n    {%- if block_start > 0 and messages[block_start - 1].role == 'assistant' and messages[block_start - 1].tool_calls -%}\n        {%- set ns_a.tool_calls = messages[block_start - 1].tool_calls -%}\n    {%- endif -%}\n    {%- set ns_chk = namespace(can_sort=true) -%}\n    {%- if not ns_a.tool_calls -%}\n        {%- set ns_chk.can_sort = false -%}\n    {%- else -%}\n        {%- for k in range(block_start, ns_blk.end + 1) -%}\n            {%- set m = messages[k] -%}\n            {%- if is_list_of_outputs(m) -%}\n                {%- for entry in m.content -%}\n                    {%- set eid = id_of(entry) -%}\n                    {%- if not eid -%}\n                        {%- set ns_chk.can_sort = false -%}\n                    {%- elif has_dup_tool_result_id(block_start, ns_blk.end, eid) -%}\n                        {%- set ns_chk.can_sort = false -%}\n                    {%- elif not tc_id_exists(ns_a.tool_calls, eid) -%}\n                        {%- set ns_chk.can_sort = false -%}\n                    {%- endif -%}\n                {%- endfor -%}\n            {%- else -%}\n                {%- set tk_id = id_of(m) -%}\n                {%- if not tk_id -%}\n                    {%- set ns_chk.can_sort = false -%}\n                {%- elif has_dup_tool_result_id(block_start, ns_blk.end, tk_id) -%}\n                    {%- set ns_chk.can_sort = false -%}\n                {%- elif not tc_id_exists(ns_a.tool_calls, tk_id) -%}\n                    {%- set ns_chk.can_sort = false -%}\n                {%- endif -%}\n            {%- endif -%}\n        {%- endfor -%}\n        {%- for i in range(ns_a.tool_calls | length) -%}\n            {%- set tc_id = id_of(ns_a.tool_calls[i]) -%}\n            {%- if not tc_id -%}\n                {%- set ns_chk.can_sort = false -%}\n            {%- endif -%}\n            {%- for j in range(i + 1, ns_a.tool_calls | length) -%}\n                {%- if id_of(ns_a.tool_calls[j]) == tc_id -%}\n                    {%- set ns_chk.can_sort = false -%}\n                {%- endif -%}\n            {%- endfor -%}\n    {%- endfor -%}\n    {%- endif -%}\n    {%- if ns_chk.can_sort -%}\n        {%- for tc in ns_a.tool_calls -%}\n            {%- set tc_id = id_of(tc) -%}\n            {%- for k in range(block_start, ns_blk.end + 1) -%}\n                {%- set m = messages[k] -%}\n                {%- if is_list_of_outputs(m) -%}\n                    {%- for entry in m.content -%}\n                        {%- set eid = id_of(entry) -%}\n                        {%- if eid == tc_id -%}\n                            {%- if entry.output is iterable and entry.output is not string and entry.output is not mapping and entry.output and entry.output.0.type == \"tool_reference\" -%}\n                                {{- tool_references_to_response(entry.output) -}}\n                            {%- else -%}\n                                {{- tool_response(visible_text(entry.output)) -}}\n                            {%- endif -%}\n                        {%- endif -%}\n    {%- endfor -%}\n{%- else -%}\n                    {%- set tk_id = id_of(m) -%}\n                    {%- if tk_id == tc_id -%}\n                        {{- render_tool_response(m) -}}\n                    {%- endif -%}\n                {%- endif -%}\n            {%- endfor -%}\n        {%- endfor -%}\n    {%- else -%}\n        {%- for k in range(block_start, ns_blk.end + 1) -%}\n            {{- render_tool_response(messages[k]) -}}\n        {%- endfor -%}\n    {%- endif -%}\n{% endif -%}\n{%- elif m.role == 'system' -%}\n<|system|>{{ visible_text(m.content) }}\n{%- endif -%}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n    <|assistant|>{{- '<think>' -}}\n{%- endif -%}","chat_template_caps":{"supports_object_arguments":false,"supports_parallel_tool_calls":false,"supports_preserve_reasoning":true,"supports_reasoning_effort":true,"supports_string_content":true,"supports_system_role":true,"supports_tool_calls":false,"supports_tools":false,"supports_typed_content":false},"bos_token":"[gMASK]","eos_token":"<|endoftext|>","build_info":"b10749-949f7efb0","is_sleeping":false,"cors_proxy_enabled":false}

@smalinin

smalinin commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Hi, thanks for supporting the model !
I improved your fix with GPT 5.6 Sol a little.
https://github.com/smalinin/llama.cpp/tree/my_glm53_flash

many issues were fixed => https://github.com/smalinin/llama.cpp/blob/my_glm53_flash/GLM5NEXT_LOCAL_CHANGES_EN.md

  1. Reduce the cost of sparse attention at long context lengths.
  2. Add a persistent cache for completed indexer pool keys.
  3. Add complete MTP support, including prompt reuse, multimodality, and multiple slots.
  4. Eliminate CPU copies of hidden states and embeddings.
  5. Fix auto-fit, LCP rewind, multi-slot rebuild, and incremental update sizing.
  6. Optimize indexed FlashAttention, Lightning Indexer, and CUDA Graph execution.
  7. Optimize MoE decode and fused Q3_K/Q4_K/Q5_K/Q6_K/IQ3_XXS/IQ4_XS expert down reduction.

may be you could reused something for improve your work !

@segmond

segmond commented Sep 9, 2026

Copy link
Copy Markdown

I get the following, is image supported?

[33077] 6.48.617.148 E clip_init: failed to load model '/mnt/nfs-rdma/llmzoo/GLM5.3-Flash/mmproj-BF16.gguf': load_hparams: unknown projector type: glm5v

@smalinin

Copy link
Copy Markdown
Contributor

@segmond

I get the following, is image supported?

[33077] 6.48.617.148 E clip_init: failed to load model '/mnt/nfs-rdma/llmzoo/GLM5.3-Flash/mmproj-BF16.gguf': load_hparams: unknown projector type: glm5v

Yes, it work with unsloth models https://huggingface.co/unsloth/GLM-5.3-Flash-GGUF
But it has the projector: glm5next

ggml_mul_mat_set_prec was deprecated upstream in ggml-org#26675 and the macOS
prebuilt legs build with fatal warnings, so the mix stopped compiling
there. Same op_params slot, same F32 accumulation.
@AIalliAI

AIalliAI commented Sep 11, 2026

Copy link
Copy Markdown

Cross-ref: the k-pool gate softmax gridDim.y crash at n_kv >= 262144 is fixed for the glm5-next path in timkhronos#11 (#27773).
Guys it's been 2 weeks + LGTM
CC @timkhronos @ggerganov @CISC @danielhanchen

@AIalliAI

Copy link
Copy Markdown

Fix for the SOFT_MAX gridDim.y crash at long context: unslothai#214

Same class as #27773 / timkhronos#11.

CC @danielhanchen @ggerganov @CISC

noonghunna added a commit to noonghunna/club-3090 that referenced this pull request Sep 11, 2026
#1255)

Closes the tool path on all 18 GLM composes, and CORRECTS the mechanism stated in #1254.

From #1250 (@paulp83); reported independently upstream on ggml-org/llama.cpp#27754
(2026-09-07), unanswered.

⛔⛔ THE LINE IN THE ERROR IS THE CRASH SITE, NOT THE CAUSE. Two earlier readings were
wrong — one of them merged in #1254 — and both are recorded in the patch README so nobody
re-derives them:
  ❌ 'minja cannot evaluate .items()' — GLM-4.7-Flash.jinja carries the identical construct,
     is exercised by llama.cpp's own autoparser tests, and passes.
  ❌ 'the caps probe under-detects supports_object_arguments because the template iterates
     instead of indexing' — adding a named access changes nothing; the probe never gets
     that far.

ACTUAL CAUSE: minja does not implement numeric dotted attribute access (x.0). Jinja2 defines
x.0 as x[0]; minja parses it as a non-computed member whose property is a number literal and
throws. The embedded template uses it in 4 places, so the caps probe throws, hits
'Nothing can be inferred', and leaves EVERY capability false — including
supports_object_arguments, which gates the JSON-string -> object conversion of tool arguments
in chat.cpp. Arguments stay a string and the autoparser then dies on .items() at line 163.

The tell that the earlier readings were wrong: supports_string_content is also false, which
no tools-only account covers.

Fix is 5 lines — .0. -> [0]. in 4 places (semantics-identical under Jinja2) plus a null-tool
guard on the tools loop (the caps probe passes tools=[null]). WIRE FORMAT UNCHANGED.

Verified with llama.cpp's own tooling, no 140 GiB load:
  llama-template-analysis: embedded = all four caps FALSE; override = all TRUE
  llama-debug-template-parser: override yields tool_mode TAG_WITH_TAGGED,
    per_call_start '<tool_call>', full PEG + GBNF with arg_key/arg_value rules
  Reference Jinja2 3.1.2: original == override byte-identical across 8 cases

⚠️ NOT live-booted. The drift guard therefore asserts POSITIVELY (caps must read true)
rather than checking the .0. string is absent, because a re-vendored template without the
fix fails silently: boot green, /health green, tool calls 400.

⚠️ Still NOT #1195 — that is an intermittent 500 parsing the model's OUTPUT. Same subsystem,
opposite end. Stated in all 18 composes.

Gates: patch-attribution, compose-status-drift, compose-mounts-resolve, profiles-compat,
compose-registry-disk, script-permissions green; 18/18 composes render.

Reported-by: @paulp83 (#1250)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conversion model Model specific mtmd Related to multimodal functionality (video/image/audio) testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.