Skip to main content

Module glm_dsa

Module glm_dsa 

Source
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:

  1. Per-layer full/shared indexer dispatch is not a fixed period. GLM-5.2’s real per-layer indexer_types array 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 from glm-dsa.cpp’s per-layer loop: a single prev_top_k local 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’s prev_top_k parameter mirrors this exactly: caller-threaded, per-token-forward-pass scoped (reset to None at 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.
  2. The lightning indexer’s own q/k split into rope/nope halves is rope-FIRST, nope-second – read directly from glm-dsa.cpp’s indexer_q_pe/indexer_q_nope ggml_view_3d byte offsets (indexer_q_pe at offset 0, indexer_q_nope at ggml_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 real config.json gives index_head_dim=128 with the indexer’s rope portion reusing the main attention’s n_rot()=64, so nope_dim == rope_dim == 64 for 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§

Glm52AttnState
Growable per-layer decode state: the main K/V cache (same shape convention as ferrox_models::mla’s k_cache/v_cache) plus, for “full” indexer layers only, the indexer’s own K cache (a separate cache since IndexerConfig::head_dim generally differs from the main attention’s qk_nope_head_dim + qk_rope_head_dim). “Shared” layers never touch indexer_k_cache – they don’t run their own indexer at all, see module doc comment point 1.
Glm52AttnWeights
GLM-5.2’s real per-layer MLA+indexer attention weights.
Glm52MlaConfig
GLM-5.2’s real per-layer MLA hyperparameters. Unlike ferrox_models::mla::MlaConfig, there is no use_output_gate (real GLM-5.2 tensor list has no Kimi-K3-style attn_gate equivalent) and rope is unconditional, not Option (every real GLM-5.2 layer’s main attention applies it – rope_interleave: true in its config.json, no per-layer exception unlike the indexer’s full/shared split).
IndexerConfig
GLM-5.2’s real lightning-indexer hyperparameters (index_head_dim=128, index_n_heads=32, index_topk=2048 in the real published config.json – see docs/MODELS.md; kept generic here, not hardcoded, so small synthetic tests can use tiny values).
IndexerWeights
The real lightning indexer’s weights for one layer (only present on “full” indexer layers – see glm52_attn_forward_token’s is_full_indexer_layer parameter).

Functions§

glm52_attn_forward_token
One decode step for one GLM-5.2 DSA attention layer.