Skip to content

GLM-5.3-Flash on CUDA: structured output (first_masked and held) - #1017

Open
Bizuayeu wants to merge 4 commits into
ashhart:mainfrom
Bizuayeu:pr/glm-native-structured
Open

Bizuayeu wants to merge 4 commits into
ashhart:mainfrom
Bizuayeu:pr/glm-native-structured

Conversation

@Bizuayeu

Copy link
Copy Markdown
Contributor

What this changes

Part of GLM-5.3-Flash on the native CUDA engine (#593, where the three stacked PRs for GLM's ranks are summed up): replies that always parse against their schema.

GLM-5.3-Flash's CUDA lanes give first_masked and held, so the native server enforces response_format, guided_* and structured_outputs on GLM instead of answering HTTP 400. Stacked on #1016 (it uses that PR's Sampler.gather).

Only rank 0's lane core holds the grammar, and only rank 0's tokens are read (it sends them with the next step), so rank 0 alone masks. A verify or shared step under a grammar carries flag 1: every rank gathers its draw's rows whole, the grouped broadcasts a sampled draw already uses, and rank 0 runs sample.cu's tf_grammar_mask on them, then tf_draw, or greedy's rule as tf_glm_top1 over the whole row (the largest value, then the lowest id, which is what the per-rank pairs' merge picks). The first token is drawn again from the prompt pass's last head row by a new redraw step. The grammar rows go to host-mapped words before the step is sent, so a refusal never leaves the other ranks inside a broadcast. Draft draws stay unmasked; held returns the drafts the lanes already hold on the host. zig/tests/cuda/glm_lanes_tests.zig takes the new verify argument.

Receipt

  • Environment: built at 38aa9875, this PR's 32141eb6 with the next PR's server-side tool-call style on top (the code of its f8e47ac6, before this PR's docs commit), on GLM-5.3-Flash on CUDA: prompt_logprobs on /v1/completions #1016, Zig 0.17.0, CUDA 13.3, NCCL 2.30.7, DGX Spark (GB10, sm_121), one GPU a host over RoCE, checkpoint nvidia/GLM-5.3-Flash-NVFP4 at 423acf37, libtfgrammar from xgrammar v0.2.8 (zig build grammar); TP=2 with --parallel 2 --context 131072.
  • Exactness: a schema the plain greedy reply already meets ({"ok": true}) gives the same text and the same token_sha under the grammar, so the masked whole-row pick equals the per-rank merge. A json_schema request gives the same text with "draft": false, the same text beside a plain request (two streams, the plain one keeping its token_sha), and at temperature 0.7 with a seed the same text twice; thinking on, the JSON follows the thinking. tool_choice: "required" (with the next PR) gives one get_weather call with {"city":"Osaka"} and finish_reason tool_calls.
  • Decode speed before and after: n/a for plain requests, whose path is unchanged (token_sha 09a15f0582e7 before and after); a constrained draw adds one gather of its rows.
  • Prompt speed before and after: n/a, prompt processing is unchanged.
  • Tests run, at 38aa9875 against GLM-5.3-Flash on CUDA: prompt_logprobs on /v1/completions #1016's head (5a6ad9ed) with fresh caches: zig build test-cuda-host 855/855 (854/854 at the base; the new test), zig build test 787/794 with 7 skipped (767/774 at the base, 0 failures in both), zig build, zig build native, zig build grammar, zig fmt --check zig/src/families/glm, git diff --check, lean: 517 problems, 517 on the base, 0 new.
  • Also at TP=3, the same commits on 1.0.12 with two unrelated local changes (rank 0's NCCL id in a file for three hosts on /32 addresses until fabric: tensor parallelism across CUDA processes (bootstrap, NCCL, mailbox and RoCE collectives, plan link, fail-fast) (DeepSeek-V4.1 stack 3/14) #636, and a heat-wait ceiling), --context 1048576, one stream: the same checks pass, with TP=2's texts and token_sha values for the schema, same-as-plain, "draft": false, seeded sampling, tool_choice: "required" and plain checks (the thinking-on reply is valid JSON with another population figure).
  • Not run, and why: Metal (the change is the CUDA lanes'), other CUDA GPUs; regex, choice and EBNF grammars (the masking path is the same).

Checklist

  • ✓ Every token still goes through the lane rounds; drafts stay on and the grammar walks the held drafts.
  • ✓ Drafted output equals "draft": false; concurrent equals solo (two streams).
  • ✓ No precision traded for speed: the logits are the same; the mask and the whole-row pick are exact.
  • ✓ No new or changed GPU kernel (tf_grammar_mask, tf_draw and tf_glm_top1 are existing ones).
  • ✓ Prompt processing is unchanged.
  • ✓ Platforms: CUDA GB10 at TP=2 and TP=3; Metal and other CUDA GPUs not run.
  • ✓ New test: the step flag means whole rows on verify and shared steps only. It tests new code, so there is no failing "before".
  • ✓ Comments and docstrings are one line.
  • ✓ No personal data, machine names, internal hosts or local paths; no AI attribution lines.

The lane backend fills Stream.prompt_rows, so the server stops refusing
prompt_logprobs for GLM. A prompt chunk's rows go through each rank's
head columns 128 at a time; the rows are made whole on every rank with
the broadcasts a sampled draw already uses (Sampler.gather), and rank 0
runs sample.cu's tf_logprob_rows on them. Rank 0's prefill and chunk
steps carry flag 1 so the other ranks join each pass. The pass's buffers
are made at the first request that asks; if a rank cannot make them,
every rank takes the verify window's buffers, 8 rows a pass. A pass with
prompt rows starts from position 0, as Nemotron's does.
The lane backend gives first_masked and held, so the server serves
response_format and required tool calls for GLM. Only rank 0's lane core
holds the grammar, and only rank 0's tokens are read (it sends them on),
so rank 0 alone masks: a verify or shared step under a grammar carries
flag 1, every rank gathers its draw's rows whole (Sampler.gather, as a
sampled draw does), and rank 0 runs sample.cu's tf_grammar_mask on them,
then tf_draw, or greedy's rule (tf_glm_top1 over the whole row: the
largest value, then the lowest id, as the per-rank merge picks). The
first token is drawn again from the prompt pass's last head row by a new
redraw step. The grammar rows go to host-mapped words before the step is
sent, so a refusal never strands the other ranks mid-broadcast. Draft
draws stay unmasked.
@ashhart

ashhart commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Thanks @Bizuayeu for the TP=2 and TP=3 schema, seeded-sampling and drafted/plain comparisons. We're taking this with fixes for 1.1.1. The first constrained draw must use its own sequence's retained hidden row, including with MTP off, because another prompt can overwrite shared logits before that draw. We also fixed follower-result initialization and mask-buffer ownership on mapping failure. Held drafts still come from the active MTP or DFlash path. The repaired combination needs generated schema/tool tests with real grammar support, mixed constrained/plain streams, cached resumes, separate prompt passes and MTP disabled. Your original receipts are useful evidence, but they do not yet qualify those repairs on the combined engine.

Posted by Ash's AI Assistant Synthena

@Bizuayeu

Copy link
Copy Markdown
Contributor Author

Thanks. Once the combined 1.1.1 branch is up, we'll run these against it on GB10 at TP=2 and TP=3 (TP=3 still with rank 0's NCCL id handed over in a file until #636), with libtfgrammar from xgrammar v0.2.8, and post the receipt here:

  • Generated schema and tool replies: this PR's checks again (a schema with thinking off and on, one the plain reply already meets, "draft": false, seeded sampling twice, tool_choice: "required"), plus a nested schema with an array of objects and an enum.
  • Mixed streams: four at once, two constrained and two plain, each reply equal to its solo one.
  • Cached resumes: a constrained request sent twice, and another sent after a plain request with its messages, each later one resuming from the prompt cache (shown in cached_tokens) with the text of a cold run.
  • Separate prompt passes: a short constrained request sent while a 5,000-token plain prompt prefills, and a short plain one while a 5,000-token constrained prompt prefills, so another prompt's pass runs before the first constrained draw; each reply equals its solo one.
  • MTP disabled: all of the above again on a server started with --no-drafts, the greedy texts equal to the drafted ones.

@ashhart ashhart mentioned this pull request Oct 18, 2026
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.

2 participants