Skip to content

Full GLM-5.3 (glm_moe_dsa) on three DGX Sparks, as layers of the GLM-5.3-Flash CUDA engine: how should it sit beside #159? #402

Closed as duplicate

Description

@MiaAI-Lab

What

We want to serve zai-org's full GLM-5.3 on three DGX Sparks with --tp 3,
one rank on each. The model (model_type glm_moe_dsa) has 78 layers of MLA with DeepSeek sparse attention, 256
routed experts and an MTP layer. Our recipe
MiaAI-Lab/GLM-5.3-EXL3-3x-DGX-Sparks-TensorFold
has served it since 2026-10-03, as patches on v0.6.0.

#159 (@drowzeys) serves the same model on four Sparks, with its own glm_moe_dsa family and TP=4 engine, and you have
said yes to it in principle. Two engines registering one model_type cannot both land, so we are asking how ours
should relate to #159 before we open a pull request. CONTRIBUTING asks for an issue first for a model family.

How it differs from #159

  • Three Sparks, not four. Our checkpoint,
    Mia-AiLab/GLM-5.3-EXL3-2.75bpw-TensorFold,
    stores the routed experts in EXL3 at 2.75 bits a weight on average (each expert at its own width, 2 to 4 bits) and
    everything else in BF16. That is about 273 GiB, or about 86 GiB a rank.
  • No new engine. Full GLM-5.3's layers run inside the GLM-5.3-Flash CUDA engine (families/glm5_next/cuda).
    They reuse its lane rounds and MTP verification, kept prompt states, server app, prompt-chunk row split and startup
    admission. A glm_moe_dsa family module (CUDA only) starts GlmEngine with a full-model config.

What patches 0100-0104 change

Each patch is one commit on v0.6.0, on top of the GLM-5.3-Flash recipe's 0001-0068. Together they add about 1,240
lines.

Patch Files Change
0100 layout split.py, tp.py The three-rank split: attention heads 22/21/21, and each expert's 16 Hadamard blocks of 128 columns 6/5/5. The extra block rotates by layer, so each rank holds a third of the 233 GiB of routed experts, instead of rank 0 holding 6/16 of every layer. The shared expert's extra block sits one rank further on.
0101 weights weights.py Reads GlmMoeDsaConfig, with each layer's indexer type ("full" or "shared") derived as transformers derives it. EXL3 routed experts of mixed widths run through tensorfold.cuda.exl3.experts' grouped kernels, one device buffer a layer. kv_b's key rows are zero-padded over the rotary dims, so the latent absorb kernels apply unchanged and stay exact. The indexer loads on full layers only, and the embedding is split by vocabulary rows.
0102 DSA dsa_full.py (new, 395 lines), latent.py Interleaved RoPE on the 64 rotary dims, with the rotary keys in a bf16 plane beside the 512-wide latent cache. DeepSeek-V3.2's indexer with per-token keys (RoPE on their first 64 dims, then LayerNorm). Selection of the top 2,048 tokens.
0103 forward forward.py, decode.py, graphs.py, mtp.py Plain pre-norm residuals. "Shared" layers reuse the last full layer's top-k. The sigmoid-routed MoE over mixed-width experts, the MTP head, a CUDA graph per token bucket, and kept-prompt snapshots.
0104 engine glm_moe_dsa/__init__.py (new), engine.py, cuda/geometry.py The family (CUDA only, --tp 3 or 4, MTP drafts or --no-drafts) and the startup estimate of its caches and buffers.

Measured

All on three DGX Sparks (GB10, 128 GB each, a triangle of direct ConnectX-7 cables), in NVIDIA's PyTorch container
26.07, with our checkpoint.

First boots (2026-10-03): 0100-0104, then 0105's prompt path. Settings: MTP drafts, NCCL, an FP8 latent
cache.

  • Decode (boot p3, 163,840-token window): 28.3 tok/s prose, 33.0 code.
  • Prefill: about 140 tok/s before 0105, about 290 with it (boot p3).
  • tools/exact.py: 12/12 at 32k and 64k windows. Needles correct up to 58.2k tokens.

The recipe's release build (boot hcp, 2026-10-04, patches through 0141). This adds our prompt kernels, DSpark
and copy drafts, RoCE, context parallelism and an fp4 cache, at a 499,712-token window.

  • Greedy decode: 31.3 tok/s prose, 41.7 code.
  • Prefill: 601.5 / 606.5 / 598.4 / 591.2 tok/s at 4k / 8k / 16k / 32k.
  • Needles correct at 94k and 314k tokens. tools/exact.py: 12/12.

Without context parallelism (boot dflt, FP8 cache). This boot ran at a 155,648-token window, the fallback
start.sh takes when free memory is short of the full window.

  • Greedy decode: 30.6-32.6 tok/s prose, 41.7-43.2 code. Sampled: 26.5-27.8 and 31.2-33.1.
  • Prefill: 630 / 679 / 660 tok/s at 8k / 16k / 32k.

Memory a rank (TensorFold's own estimate, which start.sh checks before every start):

  • weights: about 85.3 GiB on rank 0 and 84.8 on ranks 1 and 2 (4-bit dense weights, MTP layer included);
  • buffers: about 2.3 GiB;
  • caches: 56.1 KiB a token with the FP8 latent cache, 94.4 KiB in bf16.

These are not like-for-like with #159's numbers: different Sparks count, checkpoint and drafter. tools/exact.py
sends each request drafted and with "draft": false, and several together and alone. It covers greedy and seeded
sampling, thinking on and off. A pull request would carry upstream's bench_concurrent.py --alone --serial,
bench_openai.py and prefill_cold.py.

What stays exact

  • A drafted reply equals "draft": false (MTP verification in the shared lane rounds).
  • A resumed prompt equals a fresh one.
  • Every rank holds the same bits, because the ranks add fp32 partials in rank order.
  • The prompt-expert kernels we would bring later store each pair's row and add them in slot order, with no atomics,
    so the same prompt gives the same bits every run. This is the issue @BHCC2025's run found on Full GLM-5.3 on four DGX Sparks: glm_moe_dsa family, TP=4 CUDA engine #159.
  • Replies at --tp 3 may differ from other rank counts, as the README says.

Two of the recipe's defaults are lossy and are not part of this proposal:

The base proposal serves BF16 dense weights and a bf16 cache. See question 2.

Depends on

Possible follow-ups (not part of this proposal)

Each would get its own issue or pull request once the base has landed:

Platforms

CUDA only: three DGX Sparks. There is no Mac engine for this model, and it does not fit on an RTX PRO 6000. The
layout takes four ranks, but we have not run four.

What we would submit, in order

  1. Mixed-width EXL3 experts for GLM (the separate pull request).
  2. The N-rank pieces (GLM-5.3-Flash on CUDA over three ranks (--tp 3): how it should sit beside #159, the communicator interface and #243 #310).
  3. The glm_moe_dsa base: 0100-0104 ported to main, as one pull request or a stack of five, with tests:
    • CPU: a tiny random GLM-5.3 checkpoint in the real layout (mixed-width EXL3 experts, full and shared indexer
      layers, MTP) on one rank and on three gloo ranks, against a reference forward written from transformers' code;
      drafted windows against serial steps, bit for bit.
    • GPU: the kernels, graphs against eager, and one layer of the real checkpoint's experts, on one Spark.
  4. The prompt path (0105, 0107, then our kernels).
  5. The cache formats (GLM on CUDA: an opt-in FP8 or 4-bit latent cache (--kv-dtype fp8 / fp4), lossy, with paired quality results on full GLM-5.3 #401), then the follow-ups above.

If you would rather build on #159's family, step 3 becomes three-rank support inside it (the rotating layout and
mixed widths), and we would bring our other pieces where they fit.

Questions

  1. One glm_moe_dsa. Should it be Full GLM-5.3 on four DGX Sparks: glm_moe_dsa family, TP=4 CUDA engine #159's engine, ours on glm5_next, or both (Full GLM-5.3 on four DGX Sparks: glm_moe_dsa family, TP=4 CUDA engine #159's at four ranks, ours at three)?
  2. Dense weights on three Sparks. BF16 dense weights cost about 9 GiB more a rank, and on our Sparks that left no
    useful window. Should that come from a checkpoint that stores those layers quantized, read as stored (like feat(glm5_next cuda): dense projections from an EXL3 pack (TF_GLM_DENSE_EXL3) #205's
    EXL3 dense projections), or from a load-time mode? The latter would be its own precision conversation.
  3. Receipts. You have two Sparks. Would a full receipt from our three, plus the three-rank CPU tests and the one-GPU
    tests, be enough? These are the checks you asked of Full GLM-5.3 on four DGX Sparks: glm_moe_dsa family, TP=4 CUDA engine #159.
  4. Drafts. Is the checkpoint's MTP head enough for the first version?
  5. Checkpoints. Ours and drowzeys' are both 2.75 bpw with a width per expert. We have not tried theirs on this
    engine. Should one family serve both?

Credit

  • The model arithmetic follows the modelling code in Hugging Face
    transformers (Apache 2.0): GlmMoeDsa, DeepseekV32Indexer and
    DeepseekV3Attention. Our CPU tests' reference forward re-implements it. No code is copied from it.
  • The sparse-attention design is DeepSeek-V3.2's, by DeepSeek-AI. That is the design only: no DeepSeek code is used.
  • The top-k selection and score tiles adapt TensorFold's GLM-5.3-Flash pooled-key kernels (the radix select and its
    tie rule) to per-token keys.
  • Routing GLM's EXL3 experts through the grouped kernels follows @akol1's glm5_next: route EXL3 experts through the universal path for any bit width #152.
  • The context-parallel scheme in our follow-ups is drowzeys' design, from their TensorFold fork
    (drowzeys/TensorFold, branch glm-moe-dsa-tp4, which is Full GLM-5.3 on four DGX Sparks: glm_moe_dsa family, TP=4 CUDA engine #159's branch,
    commit befd47d, Apache 2.0). It works like this: tokens
    are interleaved over the ranks, local candidates are merged into the global top-k, and attention partials are
    merged by log-sum-exp in rank order. The code and kernels are ours.
  • Patch 0106 carries drowzeys' EXL3 prompt-expert kernels from the same commit.

Activity

  1. ashhart commented on Oct 6, 2026

    @ashhart
    Owner

    Thank you, @MiaAI-Lab. Asking before the pull request, with a credit section, made this easy to place. The Python engine is frozen (#286), so neither this nor #159 lands as a Python family. The N-rank pieces you list already exist in the Zig engine's cluster layer: one placement planner with tensor, pipeline and expert degrees, whole-unit shares with ties to the lower rank, MLA split by heads with the latent replicated or by streams, and sums that follow one fixed tree however the ranks split them (zig/src/cluster). GLM isn't ported to the Zig engine yet, and Zig CUDA doesn't serve yet. Your question of one engine or two we'll settle then, and your patches and #159 are the references, with your credit. Lossy defaults stay out, as you proposed.

  2. ashhart commented on Oct 7, 2026

    @ashhart
    Owner

    Thank you, @MiaAI-Lab. Full GLM-5.3 on three Sparks, as more layers of the GLM-5.3-Flash CUDA engine rather than a second server, is the shape we want. It should sit on the same communicator and the same serve path as the Flash model, with the extra layers and the larger expert set called out in the config.

    The Zig tree has no GLM family yet, Flash or full. The Flash engine is the first port. Full GLM-5.3 follows it, three ranks, and does not grow a private runtime. Your note on how it sits beside the three-rank work is the layout we will use when that engine exists. Python stays frozen.

  3. ashhart commented on Oct 10, 2026

    @ashhart
    Owner

    Thank you, @MiaAI-Lab. Full GLM-5.3 built from the Flash engine's layers is the shape #593 takes in step 5: its own glm_moe_dsa family reusing the Flash family's CUDA pieces instead of copying them. Your EXL3 port of GLM-5.3-Flash at TP=2 is one of the candidates for the first family PR in step 3, and we'd welcome it. Closing in favour of #593.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions