Skip to content

GLM-5.3-Flash on CUDA over three ranks (--tp 3): how it should sit beside #159, the communicator interface and #243 #310

Closed as duplicate

Description

@Bizuayeu

What

I have a branch that serves GLM-5.3-Flash with --tp 2 or --tp 3, for three DGX Sparks. Before opening anything, I
would like to agree with you and the authors of #159, #219 and #243 on how it should fit, since all four touch the
same places.

What the branch has

  • One shard plan decides every rank's share. Shares are whole units (heads, blocks of a width) and can be unequal:
    the first units % world ranks take one more. A rank folder records the rank count it was split for, and a
    mismatch is refused.
  • Vocabulary gathers over shards of unequal width.
  • A rank loads only the layers it needs.
  • Rank exchanges and --tp for two or three ranks. A prompt chunk's exchanges can use an exact reduce-scatter: each
    rank glues only its own rows, and the sums keep rank order, so all ranks keep equal bits.
  • Tests run three ranks on one GPU (a thread-backed communicator) with the tiny model cut as the real one, and three
    ranks of the real checkpoint against the uncut layers.

GLM-5.3-Flash's routed experts have moe_intermediate_size 2048. Over three ranks that is not a whole number of
32-wide tiles, so the unequal shares above are needed.

Where it meets other work

What I can measure

Three DGX Sparks (GB10), the MLX 4-bit checkpoint and nvidia/GLM-5.3-Flash-NVFP4: drafted against serial by token
hash, --tp 3 against --tp 2 on the same prompts, decode, and cold prompts.

Written with Weave (My Symbiotic AI)

Activity

  1. MiaAI-Lab commented on Oct 4, 2026

    @MiaAI-Lab
    Contributor

    @Bizuayeu, for reference while this is decided: the GLM-5.3-Flash recipe has carried --tp 3 since v1.4 (2026-10-03), as patches on v0.6.0 under Apache-2.0: 0066-glm-tp-n, 0067-glm-tp3-split-pad and 0068-glm-tpn-split-buffer-rows.

    They handle the points you list this way:

    • heads, expert columns, the vocabulary and DFlash2's KV groups in whole units, with the remainder on the lowest ranks (moe_intermediate_size 2048 over three ranks);
    • per-subnet RoCE routes, with b12x's proxy extended past two Sparks;
    • the row split's pad rows at three ranks.

    Measured on three Sparks: concurrent == serial 22/22, drafted == serial, the 195k needle. Happy for any of it to be used or compared against your branch. If code from them goes in, please credit the recipe, as #309 and #342 do.

  2. Bizuayeu commented on Oct 4, 2026

    @Bizuayeu
    ContributorAuthor

    @MiaAI-Lab, I'm a fan of your recipes, so it's a real pleasure to talk with you directly!

    Thanks, that is useful to have side by side. My branch (on 0.6.4) already follows three points of these patches and
    credits them in its THIRD_PARTY_NOTICES.md, with no code taken: the doorbell key per follower is 0066's fix for the
    same wait, and the uneven row split at three ranks follows 0067 and 0068. The share rule is 0066's too: whole units,
    the remainder on the lowest ranks (heads 22/21/21; the MoE width and the vocabulary in 64-wide units, 704/704/640 and
    51,648/51,648/51,584).

    Where it differs: the exchanges go over NCCL with no proxy, and a prompt chunk's partials can take an exact
    reduce-scatter after #159's, so each rank glues only its own rows and all ranks keep equal bits. On three Sparks with
    nvidia/GLM-5.3-Flash-NVFP4 it repeats its decode-check hashes from launch to launch, prefills 38,960 tokens at about
    1,670 tok/s, and answers at three positions of a 1,036,859-token prompt.

    If code from your patches goes in, it will be credited the way #309 credits the recipe. The question here still stands for the
    maintainer: how three ranks should sit beside #219's Comm, #159 and #243.

    Written with Weave (My Symbiotic AI)

  3. MiaAI-Lab commented on Oct 4, 2026

    @MiaAI-Lab
    Contributor

    Proposal: CUDA: GLM over three or four ranks (--tp 3 / --tp 4): one share rule, N-peer exchanges and a doorbell per follower

    What

    GLM on more than two CUDA ranks, one rank a machine. Today --tp takes 1 or 2, and the GLM, Flash Next and
    Nemotron engines all open open_comm(rank, 2, master, port). Our GLM-5.3-Flash recipe has served --tp 3 on three
    DGX Sparks since its v1.4 (2026-10-03), with patches
    0066-glm-tp-n,
    0067-glm-tp3-split-pad
    and
    0068-glm-tpn-split-buffer-rows
    on v0.6.0. Full GLM-5.3 needs three ranks (about 86 GiB of weights a rank; see #402).

    #310 asks how three ranks should sit beside #219's Comm, #159 and #243. This issue proposes a concrete answer from
    what we run, so that it can be settled once for everyone working on it:

    How it would fit main

    1. --tp from the family. A family declares the rank counts it serves (for example CUDA_TP = (2, 3) for
      GLM-5.3-Flash). The CLI refuses any other count when it parses the arguments, as you did for Full GLM-5.3 on four DGX Sparks: glm_moe_dsa family, TP=4 CUDA engine #159's --tp 4.
      --rank runs from 0 to N-1, and the engines call open_comm(rank, args.tp, ...). That answers GLM-5.3-Flash on CUDA over three ranks (--tp 3): how it should sit beside #159, the communicator interface and #243 #310's question:
      yes, the world size comes from --tp.
    2. One share rule in a new tensorfold/cuda/shares.py. A dimension splits in whole units (a head, a 128-column
      Hadamard block, 64 vocabulary rows), as evenly as it goes, with the remainder on the lowest ranks. Over three
      ranks, 64 heads split 22/21/21, and 2,048 expert columns in 128-wide blocks split 768/640/640. Nothing is padded:
      the ranks exchange full-width fp32 partials and top-k candidates, never per-head tensors, so ranks may hold
      different shapes. Two ranks of an even count get exact halves, so today's two-rank shards do not change. GLM-5.3-Flash on CUDA over three ranks (--tp 3): how it should sit beside #159, the communicator interface and #243 #310's
      shard plan and feat(qwen4_exp cuda): Flash Next on four ranks (--tp 4), each rank's sums over the shared rank-sum transports #379's ranks.share use the same rule, so one module could serve every family.
    3. Sums stay as they are. Every exchange is an all-gather of fp32 partials, added in rank order (rank 0 first).
      The glue kernels already take WORLD. Every rank keeps the same bits at any N, and decode needs no new
      collective.
    4. exchange_all(sends, recvs) as an optional Comm method. It runs one NCCL group with several peers. A
      prompt chunk split by rows uses it as an exact reduce-scatter. Each rank receives every peer's partial of its own
      rows and adds them in rank order. It then glues those rows, runs the next norm on them, and sends the normed bf16
      rows to every peer. That is the same idea as the reduce-scatter in Full GLM-5.3 on four DGX Sparks: glm_moe_dsa family, TP=4 CUDA engine #159 and GLM-5.3-Flash on CUDA over three ranks (--tp 3): how it should sit beside #159, the communicator interface and #243 #310. One group with every peer needs
      every pair of ranks connected: three Sparks in a triangle of direct cables are. comm.py would get a fallback
      through all_gather, as exchange has (today that fallback covers two ranks only), so a Transport does not have
      to implement it. perf(cuda): rank sums over an engine's communicator; prompt-sized partials by copies through shared host memory #378's rank-sum transports fit beside it.
    5. A doorbell per follower. The engine's idle doorbell (engine.py, one store key that rank 1 waits on) becomes
      one key for each follower, and rank 0 rings each of them. Bizuayeu's branch carries the same fix.
    6. Loader. A rank's split folder records the rank count it was cut for, and a mismatch is refused (split.py
      now checks only 1 - rank). DFlash2's KV groups and the vocabulary split by the same rule.
    7. The row split at three ranks. Chunk rows that do not divide by three get one pad row in the buffers, and the
      memory estimate counts it (0067, 0068).
    8. RoCE, optional and separate. A roce backend through register_backend, as feat(cuda): one communicator interface for two-rank engines (transports over NCCL) #219 expected. It is the
      "RoCEnante" transport of b12x by local-inference-lab (Apache 2.0):
      its C proxy, extended here to per-peer routes over the network cards that share a subnet with each peer (three
      Sparks in a triangle of direct cables), and its all-gather kernel, rewritten in CUDA C++. NCCL stays the default.
    9. --parallel above two ranks. Our recipe decodes up to 8 streams at three ranks, but upstream's GLM
      --parallel is feat(glm cuda): --parallel N on two ranks: one cache pool, batched verify rounds (each reply its solo bits) #243 (ours, two ranks, in review). We would land N ranks with --parallel refused above two
      ranks first, then lift that after feat(glm cuda): --parallel N on two ranks: one cache pool, batched verify rounds (each reply its solo bits) #243.

    Evidence

    Our GLM-5.3-Flash recipe on three DGX Sparks (v1.4 patches, NCCL, 2026-10-03, two boots):

    • concurrent requests equal the same requests one at a time: 22/22 on each boot;
    • drafted equals serial: 6/6;
    • the 195k-token needle is right.

    Measured with sparkDash on the same recipe:

    Two Sparks Three Sparks
    Prose decode, one request 60.4 tok/s 77.6 tok/s
    Prose decode, four at once 108.8 tok/s 146.2 tok/s
    Prefill 12k / 50k / 149k 6.5 / 26.8 / 87.1 s 6.2 / 25.7 / 84.2 s

    At v1.5's eight streams on three Sparks, the totals were 166.0 tok/s prose and 211.5 code, and replies were identical
    to one at a time (11/11). Two Sparks run as before: their buffers, memory estimate and connections are unchanged.

    Full GLM-5.3 runs only at --tp 3. Every result in #402 is three-rank, with tools/exact.py at 12/12 on every
    boot. There, RoCE all-gathers decode about 10% faster than NCCL with the same replies (boot p15 against NCCL boots,
    MTP drafts).

    What stays exact

    At any N, the ranks add their fp32 partials in rank order, so every rank holds the same bits. At a given N, a drafted
    reply equals "draft": false and concurrent replies equal solo ones. Replies may differ between rank counts, as the
    README already says. At two ranks, the shards and sums are today's; the receipt would show the same token_sha at
    --tp 2 before and after.

    Platforms

    CUDA only: DGX Sparks (GB10), at two and three ranks. The layout takes four (tp.SIZES = (2, 3, 4)), but we have not
    run four. Not run on a box with several GPUs over PCIe.

    What we would submit, in order

    1. shares.py, --tp from the family, open_comm with the world size from --tp, a doorbell per follower, and
      startup settings compared across all ranks. Nothing changes at two ranks.
    2. GLM-5.3-Flash at --tp 3: the layout, loader, row split with exchange_all and its fallback, and the pad row.
    3. The roce backend (b12x's transport), if you want it upstream.
    4. Full GLM-5.3 on top (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).

    If you prefer Bizuayeu's branch for GLM-5.3-Flash at --tp 3, we would review it and test it on our three Sparks,
    and bring only what it lacks.

    Questions

    1. Should one N-rank layer (shares.py, the Comm additions) serve GLM-5.3-Flash, full GLM-5.3, and feat(qwen3.8 cuda): --tp 4 for Qwen3.8-27B; rank sums of two to eight ranks #377 / feat(qwen4_exp cuda): Flash Next on four ranks (--tp 4), each rank's sums over the shared rank-sum transports #379's
      Qwen engines?
    2. exchange_all, and a reduce-scatter built on it, as optional Comm methods with fallbacks?
    3. Before feat(glm cuda): --parallel N on two ranks: one cache pool, batched verify rounds (each reply its solo bits) #243 lands (with --parallel refused above two ranks), or after it?
    4. You have two Sparks. For checking the rank protocol, which do you want: our CPU tests, which run a tiny GLM-5.3
      checkpoint on three gloo ranks against one, or three ranks as threads on one GPU, as in Full GLM-5.3 on four DGX Sparks: glm_moe_dsa family, TP=4 CUDA engine #159?
    5. Do you want a RoCE transport upstream, or should it stay in recipes?

    Credit

    The one-shot RoCE all-gather is b12x's "RoCEnante" transport by local-inference-lab
    (local-inference-lab/b12x, commit 8a99d63, Apache 2.0). Its proxy
    (roce_proxy.c) is carried with its header, modified for more than two Sparks; its CuTe all-gather kernel is
    rewritten in CUDA C++. The rest of 0066-0068 is Mia's AI Lab's (Apache 2.0).

  4. Bizuayeu commented on Oct 5, 2026

    @Bizuayeu
    ContributorAuthor

    @MiaAI-Lab, thanks for writing this up. It answers the three questions #310 asked, and from my side I'd be glad to see
    one N-rank layer serve GLM-5.3-Flash, full GLM-5.3 and the Qwen engines (your question 1), with exchange_all as an
    optional Comm method that falls back to all_gather (2), landing before #243 with --parallel refused above two
    ranks (3).

    One correction to my last comment: my branch follows 0066's rule, but not its unit. It takes an MLP width and the
    vocabulary in 64-wide units (NVFP4's 16-input groups, MLX's 64), so 2,048 expert columns split 704/704/640, where 0066
    takes 128-column Hadamard blocks and splits them 768/640/640. A shared shares.py could take the unit from the
    checkpoint's format, or use 128 everywhere, since it is a multiple of what NVFP4 and MLX need, at the cost of a less
    even split. My branch has not been run on EXL3 at three ranks.

    To avoid two implementations of the same thing: if your step 1 lands, I'll rebase GLM-5.3-Flash at --tp 3 onto it,
    or review your step 2 instead if that is preferred. Either way I can post receipts from three Sparks with
    nvidia/GLM-5.3-Flash-NVFP4 and the MLX 4-bit checkpoint: drafted against serial by token hash, --tp 2 unchanged
    before and after, decode and cold prompts. The NVFP4 receipts need the loader from #308.

    Whether a RoCE transport goes upstream is the maintainer's call. My branch uses NCCL only, so I have no measurements
    to add there.

  5. ashhart commented on Oct 6, 2026

    @ashhart
    Owner

    Thank you, @Bizuayeu. Thanks also to @MiaAI-Lab for the detailed proposal in this thread. The Python engine is frozen (#286), so --tp 3 won't land on 0.6.x. The Zig engine already has the N-rank layer you both asked for, shared by every family. Its placement planner (zig/src/cluster/plan.zig) splits shares by largest remainder with ties to the lower rank, which is your whole-units rule. Every sum follows one fixed tree of slices however the slices fall over ranks, so all ranks hold the same bits (zig/src/cluster/canon.zig). GLM-5.3-Flash isn't on the Zig engine yet, and Zig CUDA doesn't serve yet. When both are there, your branch and the recipe's patches are the reference for three ranks, with your credit.

  6. ashhart commented on Oct 7, 2026

    @ashhart
    Owner

    Thank you, @Bizuayeu. Three ranks for GLM-5.3-Flash, on the communicator the other multi-rank work already uses, is the layout we want. A private third transport would be the wrong outcome.

    The Zig tree has no GLM engine and no three-rank serve. The Flash engine is the port, and --tp 3 is part of that port rather than a flag we turn on ahead of the model. A request for three ranks on a build that has no such engine has to be refused. Thank you for tying this to the existing communicator work so it does not become a third stack.

  7. ashhart commented on Oct 10, 2026

    @ashhart
    Owner

    Thank you, @Bizuayeu. Your shard plan with unequal shares and the exact reduce-scatter in rank order are steps 2 and 3 of #593, which now tracks all GLM work on native CUDA. Your TP=2 port over NCCL is one of the candidates for the first family PR there. Closing in favour of #593.

  8. added 2 commits that reference this issue on Oct 22, 2026
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