Skip to content

glm: make GLM 5.2 work on CUDA with SSD streaming, including IQ2_XXS - #717

Open
JonasColmsjo wants to merge 1 commit into
antirez:mainfrom
JonasColmsjo:glm-cuda-complete
Open

glm: make GLM 5.2 work on CUDA with SSD streaming, including IQ2_XXS#717
JonasColmsjo wants to merge 1 commit into
antirez:mainfrom
JonasColmsjo:glm-cuda-complete

Conversation

@JonasColmsjo

Copy link
Copy Markdown

This is the CUDA counterpart of #617, which added GLM 5.2 SSD streaming and
distributed inference for ROCm using the same
GLM-5.2-UD-IQ2_XXS_RoutedIQ2XXS_blk78Q2K.gguf. On CUDA those paths were either
unreachable or silently wrong.

Tested on a DGX Spark (GB10, 121 GB unified). Before this: the 262 GB Q2_K build
generated token soup, the 211 GB IQ2_XXS build could not load at all, and loading
either without --ssd-streaming wedged the host badly enough to need a power
cycle — with nothing logged to say why. It cost four power cycles to work out.

1. Routed MoE never used the streaming expert compaction on CUDA

ds4_gpu_glm_stream_expert_cache_begin_selected_load_tensor exists and works, but
its call sites are ROCm (#ifdef DS4_ROCM_BUILD), Metal and CPU only — and neither
ds4_gpu_glm_routed_moe_batch_tensor nor ..._one_tensor reads
g_stream_selected_cache.

So the batch dispatch asked cuda_resolve_weight_ptr for all n_total_expert
experts as one contiguous span
. Streaming can never satisfy that, and on
single-GPU the resolver answers a miss without an error — the multi-GPU path
logs selective-cache miss, the single-GPU path just returns a pointer. The
kernel then multiplied against memory the model was never written into.

Now wired to the same compaction the DeepSeek MXFP4 path uses at
ds4_cuda.cu:23727, and it refuses the layer if compaction is unavailable rather
than reading unpopulated memory.

2. IQ2_XXS routed experts had no CUDA path

GLM's routed-MoE kernels are Q2_K-only, so IQ2_XXS was rejected at the type gate —
even though the vendored MMQ tier already implements IQ2_XXS MoE and DeepSeek uses
it on the very same hardware.

The MMQ contract matches GLM's layout exactly: W is [n_experts, M, K] with
per-expert slabs stacked, ids is [n_tokens, n_expert_used] row-major, and out
is column-major over (token, slot) — the layout GLM already asserts via
mid_token_stride. K=6144 is a multiple of 256 and n_expert_used=8 is one of
the specialised widths.

Gate/up now dispatch through ds4_mmq_iq2_xxs_moe_pair, then SwiGLU and the router
weights via the existing kernel, then down per (token, slot). Down is dispatched on
its own type, so the RoutedIQ2XXS_blk78Q2K layout works. The fused
gate_up_mid_vec entry cannot be used — it is hardcoded to n_expert_used == 6.
Decode needs no separate work: ..._one_tensor delegates to the batch entry.

3. IQ2_XXS layers were diverted to the generic routed path

glm_graph_layer_uses_generic_routed_moe() sent them to
ds4_gpu_routed_moe_batch_tensor, which needs g->batch_routed_* buffers that are
not wired up when a shared prefill workspace is in use — so every routed layer
returned 0 before reaching a kernel. That predicate existed because the GLM path
could not do IQ2_XXS, which (2) changes. Metal and ROCm keep the generic path.

4. The memory guard could not see host memory on Linux

glm_graph_host_memory_bytes() was macOS-only and returned 0 elsewhere, so the
guard fell back to a GPU working-set estimate. On an integrated GPU that is the
wrong constraint: device allocations come out of host RAM, so an oversized model
does not fail an allocation — it takes the machine down. Now reads the real total
via sysconf(_SC_PHYS_PAGES) and, when refusing, prints the shortfall and the
remedy.

5. Diagnostics

None of the above was visible. glm_graph_indexed_prefill_trace_enabled() and
_trace_all() were hardcoded return false, making every
glm_graph_indexed_prefill_tracef() call site dead code; they now read
DS4_GLM_INDEXED_PREFILL_TRACE / _TRACE_ALL, cached since they are consulted at
every stage boundary of every layer. The silent entry guards name the precondition
they rejected instead of returning a bare false out of a twelve-condition if.
Routed-MoE dispatch failures report the expert tensor types — which is what
identifies a quant-support problem. DS4_CUDA_VERIFY_WEIGHT_PTR reports any weight
not resolving to device memory, and an unbounded weight cache on an integrated GPU
now says so.

Verification

On GB10 with SSD streaming:

IQ2_XXS (211 GB) capital of FranceParis; reverse a stringdef reverse_string(s): return s[::-1]
0.46 t/s prefill, 0.49 t/s decode
Q2_K (262 GB) correct, 0.25 t/s — IQ2_XXS is ~2× faster, streaming 51 GB less
no --ssd-streaming refuses in seconds, memory untouched: need 249.56 GiB but only 89.63 GiB is available … retry with --ssd-streaming
DeepSeek V4 Flash unaffected, resident and streaming (regression-checked)

Trace and verify flags are off by default, so output is unchanged unless the env
vars are set.

Supersedes #683 and #701, which covered subsets of this.

antirez#617 added GLM 5.2 SSD streaming and distributed inference for ROCm, using
GLM-5.2-UD-IQ2_XXS_RoutedIQ2XXS_blk78Q2K.gguf. This does the equivalent for
CUDA, where the same paths were either unreachable or silently wrong.

On a DGX Spark (GB10, 121 GB unified) the 262 GB Q2_K build generated token
soup, the 211 GB IQ2_XXS build could not load at all, and loading either
without --ssd-streaming wedged the host hard enough to need a power cycle,
with nothing logged to say why.

1. Routed MoE never used the streaming expert compaction on CUDA.
   ds4_gpu_glm_stream_expert_cache_begin_selected_load_tensor exists and works,
   but its call sites are ROCm, Metal and CPU only, and neither GLM MoE entry
   read g_stream_selected_cache. So the dispatch asked cuda_resolve_weight_ptr
   for all n_total_expert experts as one contiguous span; streaming can never
   satisfy that, and on single-GPU the resolver answers a miss without an error
   (the multi-GPU path logs selective-cache miss). The kernel then multiplied
   against memory the model was never written into. Wire it to the same
   compaction the DeepSeek MXFP4 path uses, and refuse the layer if compaction
   is unavailable rather than reading unpopulated memory.

2. IQ2_XXS routed experts had no CUDA path. GLM's kernels are Q2_K-only, so
   the type gate rejected them -- yet the vendored MMQ tier already implements
   IQ2_XXS MoE and DeepSeek uses it on the same hardware. Its layout contract
   matches GLM exactly, so gate/up now dispatch through
   ds4_mmq_iq2_xxs_moe_pair, then SwiGLU and router weights, then down per
   (token, slot). Down is dispatched on its own type so the
   RoutedIQ2XXS_blk78Q2K layout works. The fused gate_up_mid_vec entry cannot
   be used: it is hardcoded to n_expert_used == 6 and GLM uses 8.

3. IQ2_XXS layers were diverted to the generic routed path, which needs
   batch_routed_* buffers that are not wired up when a shared prefill workspace
   is in use, so every routed layer failed before reaching a kernel. That
   predicate existed because the GLM path could not do IQ2_XXS, which (2)
   changes. Metal and ROCm keep the generic path.

4. The memory guard could not see host memory on Linux.
   glm_graph_host_memory_bytes() was macOS-only and returned 0 elsewhere, so it
   fell back to a GPU working-set estimate. On an integrated GPU that is the
   wrong constraint -- device allocations come from host RAM, so an oversized
   model wedges the machine instead of failing an allocation. Read the real
   total via sysconf(_SC_PHYS_PAGES) and print the shortfall and the remedy.

5. Diagnostics, because none of the above was visible.
   glm_graph_indexed_prefill_trace_enabled() and _trace_all() were hardcoded
   to return false, making every glm_graph_indexed_prefill_tracef() call site
   dead code; they now read DS4_GLM_INDEXED_PREFILL_TRACE / _TRACE_ALL, cached
   since they are consulted per stage per layer. The silent entry guards name
   the precondition they rejected. Routed-MoE dispatch failures report the
   expert tensor types, which is what identifies a quant-support problem.
   DS4_CUDA_VERIFY_WEIGHT_PTR reports weights not resolving to device memory,
   and an unbounded weight cache on an integrated GPU now says so.

Verified on GB10 with SSD streaming:
  - IQ2_XXS (211 GB): "capital of France" -> Paris; "reverse a string" ->
    def reverse_string(s): return s[::-1]; 0.46 t/s prefill, 0.49 t/s decode
  - Q2_K (262 GB): correct, 0.25 t/s -- IQ2_XXS is ~2x faster, streaming 51 GB less
  - without --ssd-streaming: refuses in seconds, memory untouched, and says
    "need 249.56 GiB but only 89.63 GiB is available ... retry with --ssd-streaming"
  - DeepSeek V4 Flash unaffected, resident and streaming (regression-checked)

Trace and verify flags are off by default; output is unchanged unless set.

Supersedes antirez#683 and antirez#701.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant