Skip to the content.

GLM-5.2 decode: the path to 10 tok/s on the pure fak kernel

2026-06-27. Companion to GLM52-NATIVE-THROUGHPUT-AND-BENCHMARK-PLAN-2026-06-25 and GLM52-FAK-NATIVE-SERVE-LOAD-SPEED-2026-06-25. It targets the real 753B glm_moe_dsa serve (UD-Q4_K_M, ~436 GB), not the synthetic reduced-layer glmdsatput micro-number. The goal: drive sustained decode ≥ 10 tok/s on the pure fak kernel on GPU server (8× datacenter GPU, sm_80, ~2 TB host RAM).

TL;DR

The cpu-offload hybrid (--backend cuda --cpu-offload-experts) measured < 0.1 tok/s (prior session — 16/32-token smokes timed out at 0 tokens). The number is not a kernel-arithmetic wall and not a load-speed problem. The leading cause is structural device glue: the GLM-DSA forward issues ~12 synchronous device↔host round-trips per layer (one cudaMemcpy+be.Read stream-sync per dense projection), and at ~92 layers that is ~1100 synchronous round-trips per decoded token. The expert kernel quant lever (int8 vs f32 dequant) is real but secondary (~1.3–1.8×).

So the climb from < 0.1 to 10 tok/s decomposes into three independent levers, in priority order:

# Lever Est. gain Where Code?
1 Drop the device glue — serve pure-CPU (no --backend), or make the dense forward device-resident ~30× serve config / future device-resident forward none (config)
2 Batch the MoE expert dispatch — one parFor across all active experts per proj, not ~24 tiny parFors/layer ~1.8× internal/model host expert path yes
3 Vectorize the int8 reducer — AVX2/AVX512-VNNI Q4_K / K-quant GEMV (scalar caps ~0.55 GiB/s/core) ~2–4× internal/model amd64 kernel yes (asm)

Levers 1×2 alone land in the ~5–8 tok/s band on a 256-core host (estimate below); clearing 10 reliably wants lever 3 (which is also why llama.cpp’s CPU GLM-class decode beats fak’s today — its AVX512-VNNI kernels).

UPDATE (same day) — re-measured against current trunk: lever 3 is PARTLY SHIPPED

The scalar ceiling numbers below were measured against the session-start archive (HEAD ad7b8a13). Current trunk (38806053) already carries AVX2/AVX512 VPMADDWD int8 reducers for BOTH Q4_K and Q5_K (quant_amd64_q4k.s, quant_amd64_kquant.s, dispatched by q4kReduceRow/q5kReduceRow when qtier >= tierAVX2) — a peer landed most of lever 3 after the first measurement. Re-measuring on the same box (now qtier=AVX512):

Corrected lever picture: lever 3’s SIMD reducer (AVX2/AVX512 VPMADDWD) is shipped; the remaining vectorization headroom is AVX512-VNNI (VPDPBUSD, int8×int8→int32 in one op vs the two-step VPMADDWD) — another ~2–4× on a VNNI host. So the top implementable lever is now lever 2 (batch the expert dispatch, ~1.8×): it is the difference between the ~4.7 tok/s current serve and the ~8.5 tok/s ceiling — and it is bit-identical to the proven path. Caveat: these are THIS box’s numbers; GPU server’s host-CPU tier (AVX2 vs AVX512 vs VNNI) and the real active-params/token set the actual figure — the matrix-row-C measurement still decides it.

UPDATE-2 (same day, later) — lever 3 fully landed: AVX512-VNNI reducers shipped

The VNNI headroom UPDATE-1 named is now shipped for both dominant GLM-5.2 expert quants, each bit-identical to the scalar reference (every reduction carries an asm-matches-scalar test):

Witnessed tiers on a GLM-5.2-shaped expert (out=2048, in=6144), single worker, Zen 5 (AVX512-VNNI), via BenchmarkQ4KInt8GEMV / BenchmarkQ5KInt8GEMV with FAK_QKERNEL pinning:

Quant scalar int8 AVX2 VNNI VNNI vs scalar
Q4_K ~10.4 ms/op ~1.17 ms/op ~0.875 ms/op ~11.9×
Q5_K ~10.6 ms/op ~2.94 ms/op ~2.42 ms/op ~4.4×

So lever 3 is done on amd64: AVX2 is the floor (every CPU-server-class host has it), VNNI auto-lights on Cascade Lake / Ice Lake / Zen 4+ via CPUID. The Q4_K win (11.9×) is far above the original ~2–4× estimate — Go’s scalar int8 path emits per-byte sign-extend + scalar imul, so SIMD buys much more than the f32 comparison implied. The aggregate forward gain is bounded by Q5_K’s smaller win (its cost is the qh unpack, not the dot) and by the non-expert work, but the expert GEMVs — the #971 wall — are no longer scalar.

Remaining to the goal (kept honest — not yet):

What is MEASURED vs INFERRED (kept honest)

Measured:

Inferred (the leading hypothesis, not yet directly timed): that the ~1100 synchronous round-trips/token dominate the hybrid’s < 0.1 tok/s. Each backendKernel.mul does uploadHostF32Class (H2D) → MatMulbe.Read (D2H stream-synchronize); a contended GPU makes each sync ms-scale, and ~1100 × ~10 ms ≈ 11 s/token ≈ 0.09 tok/s — which matches the measured number. The code comment names the design explicitly: “The device↔host copy per GEMM keeps the glue simple (correctness-first); a fully device-resident GLM-DSA forward is the next slice.” Next checkable step: a per-op timing pass on GPU server (decode-only, after prefill) to confirm the round-trip share before investing in lever 1’s device-resident variant.

Why not “resident across all 8 GPUs”?

The 436 GB model would fit in 640 GB of aggregate VRAM, but fak’s CUDA backend is single-GPU today: fcuda_init pins cudaSetDevice(0), DeviceMemory() reports one GPU’s totalGlobalMem, and there is no device-side collective (NCCL/RCCL) — the tensor-parallel seam is shipped but CPU-only (hardware-gated, #295). So multi-GPU resident sharding is a separate, larger effort; it is not the near-term path to 10 tok/s. The near-term path keeps the experts on the host (where they already are under --cpu-offload-experts) and removes the per-token tax around them.

The pure-CPU ceiling estimate (GPU server host)

Treating the experts as a host weight stream (they dominate the parameter count), and using the measured batched-int8 throughput:

This is an estimate, not a serve measurement — the real number needs a free host (see matrix). But it already says the right config is pure-CPU, not the hybrid: the hybrid pays ~1100 device syncs/token to run a small fraction of the FLOPs on the GPU, while the experts (the bulk) sit on the host either way. Serving pure-CPU on GPU server’s host cores also frees the GPUs for other users — a good-neighbor bonus on a shared box.

GPU server experiment matrix (run when a host is free)

One GLM-class load at a time; gate on free -g; NVMe-first weights (/mnt/sglang_dv3/glm52-q4/); the GPUs/ports 8000-8001 may belong to another user.

# Config Command sketch Measures
A hybrid, default fak serve --gguf S1 --backend cuda --cpu-offload-experts reproduce < 0.1 tok/s baseline
B hybrid + int8 experts FAK_KQ_INT8=1 … --backend cuda --cpu-offload-experts int8 lever on the hybrid (expect small — glue-bound)
C pure-CPU + int8 FAK_KQ_INT8=1 fak serve --gguf S1 (no --backend) the lever-1 win: decode tok/s with NO device glue
D per-op decode timing C + a decode-only timing pass confirm the round-trip share / kernel share

Decode tok/s should be read after prefill completes (the goal is sustained decode, not first-token latency). Capture fak_* /metrics + FAK_WORKERS/FAK_BUDGET.

Status

Not yet at 10 tok/s. Root cause localized (device glue, with the host-kernel ceiling quantified); levers prioritized; the confirming measurement is host-gated (GPU server and the CPU-only CPU server node were both in active peer use this session). Next: run matrix row C/D on a free host to validate lever 1, then implement levers 2 and 3.