Expand description
GLM-5.2’s real DSA (DeepSeek Sparse Attention) attention layer:
RoPE-carrying MLA (ferrox_models::mla’s math, inlined here rather
than reused directly – see below) plus the lightning indexer
(ferrox_core::attention::lightning_indexer_topk) selecting which
causal positions are visible, then sparse attention restricted to
exactly those (ferrox_core::attention::causal_mla_attention_sparse).
Real tensor names/shapes/dispatch confirmed against llama.cpp PR
#23346 (DeepSeek-V3.2, src/models/deepseek32.cpp) and PR #25407
(GLM-5.2’s indexer_types/interleaved-RoPE diff on top,
src/models/glm-dsa.cpp), both fetched live and read line-by-line
(gh api -H "Accept: application/vnd.github.raw" repos/ggerganov/llama.cpp/contents/src/models/glm-dsa.cpp, since
gh pr diff alone doesn’t show unchanged context for tensor
creation that predates PR #25407) – see docs/MODELS.md.
Not the same weight layout as ferrox_models::mla::MlaAttnWeights:
GLM-5.2’s real GGUF main-attention K/V decompression uses separate
per-head wk_b/wv_b 3D tensors (blk.N.attn_k_b/attn_v_b), not
Kimi K3’s combined kv_b_proj – see Glm52AttnWeights’s doc
comment for the “absorbed vs. un-absorbed” direction this matters
for. That’s why this is a new set of weight/forward structures
rather than a reuse of mla::MlaAttnWeights/mla_forward_token,
even though the underlying low-rank-compression math is the same
family.
Two more real, non-obvious facts from that source, beyond what
ferrox_models::mla’s module doc comment already covers for the
shared MLA math:
- Per-layer full/shared indexer dispatch is not a fixed period.
GLM-5.2’s real per-layer
indexer_typesarray has layers 0-1 as “full”, then a repeating 1-full+3-shared pattern – but the real mechanism a “shared” layer uses is “reuse the top-k from the nearest preceding full layer,” not “recompute every 4th layer.” Confirmed directly fromglm-dsa.cpp’s per-layer loop: a singleprev_top_klocal variable is reassigned only when a full layer runs, and carried forward unconditionally into every following shared layer until the next full layer reassigns it –GGML_ASSERT(prev_top_k != nullptr && "shared indexer layer must follow a previous full indexer layer")on the shared-layer branch confirms a shared layer can never be the first layer processed.glm52_attn_forward_token’sprev_top_kparameter mirrors this exactly: caller-threaded, per-token-forward-pass scoped (reset toNoneat the start of each new token, the same “one token, all layers in order” scope the real ggml local variable has), not persisted across tokens. - The lightning indexer’s own q/k split into rope/nope halves is
rope-FIRST, nope-second – read directly from
glm-dsa.cpp’sindexer_q_pe/indexer_q_nopeggml_view_3dbyte offsets (indexer_q_peat offset 0,indexer_q_nopeatggml_row_size(..., n_embd_indexer_head_nope)), the opposite of the main attention’s nope-first/rope-second convention. GENUINE DISCLOSED CAVEAT: GLM-5.2’s own realconfig.jsongivesindex_head_dim=128with the indexer’s rope portion reusing the main attention’sn_rot()=64, sonope_dim == rope_dim == 64for this specific model – meaning this physical-order reading is not numerically distinguishable from its opposite by inspecting GLM-5.2’s own hyperparameters alone (the offset expression’s value is identical either way). This implementation commits to the literal “first view in the code is the rope view” reading rather than silently picking whichever seemed more consistent with the main attention’s convention; see docs/MODELS.md for the same caveat recorded against the evidence ledger.
Tested here against synthetic weights, cross-validated against an independent Python transcription of one “full” indexer layer’s RoPE+indexer+sparse math across four decode steps, including a step where top-k sparsity actually excludes a causally-visible position) plus dedicated Rust-only tests for the full/shared dispatch bookkeeping itself (not real “math to get subtly wrong” the way RoPE/ indexer-scoring/sparse-selection are, so not re-derived in Python).
Structs§
- Glm52
Attn State - Growable per-layer decode state: the main K/V cache (same shape
convention as
ferrox_models::mla’sk_cache/v_cache) plus, for “full” indexer layers only, the indexer’s own K cache (a separate cache sinceIndexerConfig::head_dimgenerally differs from the main attention’sqk_nope_head_dim + qk_rope_head_dim). “Shared” layers never touchindexer_k_cache– they don’t run their own indexer at all, see module doc comment point 1. - Glm52
Attn Weights - GLM-5.2’s real per-layer MLA+indexer attention weights.
- Glm52
MlaConfig - GLM-5.2’s real per-layer MLA hyperparameters. Unlike
ferrox_models::mla::MlaConfig, there is nouse_output_gate(real GLM-5.2 tensor list has no Kimi-K3-styleattn_gateequivalent) and rope is unconditional, notOption(every real GLM-5.2 layer’s main attention applies it –rope_interleave: truein itsconfig.json, no per-layer exception unlike the indexer’s full/shared split). - Indexer
Config - GLM-5.2’s real lightning-indexer hyperparameters
(
index_head_dim=128,index_n_heads=32,index_topk=2048 in the real publishedconfig.json– see docs/MODELS.md; kept generic here, not hardcoded, so small synthetic tests can use tiny values). - Indexer
Weights - The real lightning indexer’s weights for one layer (only present on
“full” indexer layers – see
glm52_attn_forward_token’sis_full_indexer_layerparameter).
Functions§
- glm52_
attn_ forward_ token - One decode step for one GLM-5.2 DSA attention layer.