GLM-DSA: fix CPU-only crashes in the sparse-attention path (#2045 was GPU-validated only) - #1
Conversation
PR ikawrakow#2045 adds GLM-DSA sparse attention but was validated on CUDA (--cpu-moe). A CPU-only build (-ngl 0 --dsa) crashes in four spots where the CUDA backend tolerates something the CPU backend does not. These make GLM-5.2 --dsa run coherently on CPU; with --dsa off they are no-ops (DSA CPU path only). 1. set_rows into an F32 dest segfaults (ggml.c set_rows_f32): type_traits[F32].from_float is NULL, so the DSA sparse-mask scatter calls a NULL fn (segfault at ip=0). memcpy when the dest is F32. CUDA has a real F32 set_rows path, so this only bit the CPU build. 2. ggml_add(F32 score, F16 mask) aborts on CPU (build_deepseek2_dsa_indexer and build_deepseek2_dsa_sparse_mask): under -fa 1 the dense KQ_mask is F16 and CPU add only accepts F32+F16 when src0 is F16. Cast the causal mask view to F32. CUDA's add accepts the mixed types. 3. dsa_fa_mask dim-1 concat must be F32 on CPU (build_deepseek2_dsa_fa_mask): CPU ggml_concat only supports F16 along dim 0; do the row (dim-1) concat in F32 then cast the result to F16. CUDA supports the F16 dim-1 concat. 4. indexer k_norm epsilon is 0 -> ggml_norm aborts (llama-hparams.cpp): the lightning-indexer k_norm is a non-RMS LayerNorm using f_norm_eps, but the GLM-DSA GGUF only carries the RMS eps so f_norm_eps stays 0 (GGML_ASSERT(eps > 0)). Mirror the RMS eps. CUDA's norm doesn't assert on eps=0. Validated: GLM-5.2 UD-Q4_K_M, single-socket Xeon w7-2475X, CPU-only (-ngl 0 --dsa) - coherent at 49K+ ctx, correct 30K needle retrieval, prefill flat with length (~32 tok/s, the O(L) DSA signature) vs the dense build's O(L^2) decline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Marcus, thanks for running this down independently. We had been working the same CPU-only crashes from our side and landed on a near-identical set of four fixes, so two independent reproductions converging on the same four root causes is about as solid as a diagnosis gets. Your concat fix is cleaner than ours and we are adopting it. We had generalized ggml_concat to handle dim 1/2/3 in ggml.c; doing the row concat in F32 with the existing concat_f32 and casting to F16 keeps it out of the shared op entirely, which is lower risk and more likely to pass review upstream. Your prefill numbers are the half we did not have. We had only measured the decode side, on a 3xP100 + --cpu-moe hybrid, where the experts dominate each token and the indexer overhead swamps the sparse-attention saving, so DSA is a throughput loss there through 16K. Your CPU-only prefill result is the case where it clearly pays off. Between the two we have an honest picture of where DSA helps and where it does not. On our side these fixes also sit on top of ikawrakow's per-head-loop change (ikawrakow#2058), which bounds the indexer compute buffer, and we are sorting out with him how the CPU-only fixes land upstream. Would be glad to have you in that thread (ikawrakow#2045) if you want to weigh in directly; your reproduction and the concat approach are worth having on the record there. |
PR ikawrakow#2045 adds GLM-DSA sparse attention but was validated on CUDA (3×P100 +
--cpu-moe). On a CPU-only build (-ngl 0 --dsa), warmup crashes in four spots — each a place where the CUDA backend tolerates something the CPU backend does not. These fixes make GLM-5.2--dsarun coherently on CPU. With--dsaoff they are no-ops (they only touch the DSA CPU path).This branch is based directly on
glm-dsa-upstream(head03391cb5) so it's a clean +4-fix delta.1.
set_rowsinto an F32 destination segfaultsggml/src/ggml.c,ggml_compute_forward_set_rows_f32. The DSA sparse mask scatters into an F32 base viaset_rows, buttype_traits[F32].from_float == NULL(set_rows was only ever used for F16/quantized KV rows), so the NULLfrom_floatis invoked → crash atip=0. Fix:memcpywhen the destination is F32. CUDA has a real F32set_rowspath, so this never bit there.2.
ggml_add(F32 score, F16 mask)aborts on CPUsrc/graphs/build_deepseek2.cpp, inbuild_deepseek2_dsa_indexerandbuild_deepseek2_dsa_sparse_mask. Under-fa 1the denseKQ_maskis F16; CPUggml_addonly supports F32+F16 when src0 is F16, not F32(score)+F16(mask), and aborts. Fix: cast the causal-mask view to F32 before the add. CUDA's add accepts the mixed types.3.
dsa_fa_maskdim-1 concat must be F32 on CPUsrc/graphs/build_deepseek2.cpp,build_deepseek2_dsa_fa_mask. CPUggml_concatonly supports F16 along dim 0 (concat_any); the dim-1 row concat needsconcat_f32. Fix: do the row concat in F32 (sparse+ padding rows), then cast the padded result to F16. CUDA supports the F16 dim-1 concat directly.4. Lightning-indexer
k_normepsilon is 0 →ggml_normabortssrc/llama-hparams.cpp, glm-dsa hparam load. The indexerk_normis a non-RMSLLM_NORM(LayerNorm) that useshparams.f_norm_epsinggml_norm(), but the GLM-DSA GGUF only carries the RMS eps, sof_norm_epsstays 0 and CPUggml_normaborts (GGML_ASSERT(eps > 0)). CUDA's norm kernel doesn't assert (eps=0 is numerically tolerable), which is why the CPU attention path was never exercised. Fix: mirror the RMS eps intof_norm_eps.Validation
GLM-5.2 (UD-Q4_K_M GGUF) on a single-socket Xeon w7-2475X, CPU-only (
-ngl 0 --dsa):🤖 Generated with Claude Code