Repository navigation
GLM-5.2/GLM-5.3 PR 2: GLM-5.2 with MLA, fp8 MoE and DSA - #387
Conversation
3924640 to
14a6094
Compare
14a6094 to
351c178
Compare
GLM-5.2 at k=0: MLA with the DSA indexer, fp8 MoE, weight loader, configs. Replaces #272. Open: the configs' new max_concurrent_requests caps.
A prompt of limit - 1 tokens failed its whole decode batch (the speculative step overran the window), and a long-context prompt past index_topk failed its prefill batch. process_prompt now refuses both; prepare_inputs agrees.
Byte-level BPE models emit each token's raw bytes, which can end inside a character; decoding per chunk turned those into U+FFFD.
Per-token decode turned a character split across byte-level BPE tokens into U+FFFD; the 10-07 GSM8K answers have them at ÷ and √.
The conductor now builds it only for a model that sets prebuild_fused_moe.
As fused_experts_fp8: dynamo traced the launch path inline, which breaks the graph and fails on a symbolic token count under torch 2.9.1.
An fp8 checkpoint whose quant config went undetected loaded clean with unscaled e4m3 weights; a failed Hub fetch came back as a local path.
The runtime's ready order is handle order in Rust, and handles are recycled: a large prefill could be passed over indefinitely. Backlogged rows count too, since the batch builder tops a backlog up with fresh rows.
A non-numeric max_output_tokens from extra_body reached the conductor's min() and took its message batch down; image or audio parts were decoded and dropped. Both are 400s now.
Its logits kernel holds every token in one tile; past 128 the first launch raised OutOfResources. The models never send more; now it says so.
top_k and min_p reach the sampler; a fused-kernel import failure is logged; MSTAR_GLM52_GRAPH_COMPILE=0 also leaves uncaptured steps uncompiled; the single-rank test config is dummy mode, not a 750 GB pull.
Chosen per plan, a captured bucket could re-plan into the other kernel while the graph replayed the captured one on stale buffers.
from_hf_config read only rope_theta, so a YaRN or dynamic variant ran as plain RoPE, its frequencies and softmax scale wrong.
The CUDA op launched a 0-block grid unchecked; the latched error surfaced in the next unrelated kernel.
Each new test file with a CPU case, listed in test/cpu-core.txt.
A rank whose setup failed fell back to NCCL alone, and its peers' first all-reduce waited on it forever. tp_allreduce_max_kb: 0 now turns it off instead of reading as unset (512).
Built from tokenizer.json alone (transformers 4.x), the tokenizer had no special tokens, so the detokenizer printed <|user|> at the end of replies.
An empty MSTAR_GRAPH_COMPILE_MODE compiled in Inductor's default mode silently; reduce-overhead and max-autotune capture inside the runner's own graph, fail it, and the bucket serves eager.
The prefill walk has no decode_loop: the runtime dropped (and logged) its stop, and decode wrote on past the stop token. The first decode step now stops and emits nothing; the CPU stream harness runs through the runtime.
| path, else index_topk, where dense MLA is exactly GLM-5.2's DSA.""" | ||
| return self.config.max_seq_len if self.config.dsa_long_context else self.config.index_topk | ||
|
|
||
| def _clamp_token_budget(self, engine_inputs: ModelInputsFromEngine, rid: str, room: int) -> None: |
There was a problem hiding this comment.
don't need this check anymore since #808 merged
Since #808 and #832 the conductor holds every request to the room its prompt leaves in the KV pool (it refuses a max_tokens past it, and fits a request that names none), and the decode loop's cap ends decode where check_stop does, so no step runs past it. GLM-5.2's own prompt-aware clamp duplicated that, one row short, so a request sized exactly to the window got one token fewer than it asked for. check_stop now reads the request's max_tokens. The KV pool's max_seq_len is now Glm52Model.context_limit(), so the conductor sizes requests against the window the preprocess guard holds contexts to. Before, the two agreed only by convention (index_topk equals max_seq_len in the checkpoint's config); the reduced variant's did not (64 against 512). Claude-Session: https://claude.ai/code/session_01FeseQ9qqtZRk2CqVpUJ6MH
NSagan271
left a comment
There was a problem hiding this comment.
Thanks @vasilevklart, this addresses most of my comment on making DSA a resource. I'm fine landing the growing IndexKeyStore as an eager-only stopgap since it's isolated, and once keys are paged select collapses into the same gather attend already does.
Can you open the follow-up issue the module docstring describes (keys on the MLA cache's pages, under its page table, allocator, eviction and prefix reuse)?
What I want to see before merge: can you move the fast loading from #389 into this PR? I'm seeing it take over half an hour for weight loading when I try to test this PR (which gets the job automatically killed for idleness on coriander), which would make GLM 5.2 very annoying to run between #387 and #389 being merged.
Nits: glm52_tp8_longctx.yaml still points the follow-ups at components/attention.py. And is bf16 storage for the index keys close enough on parity? It would halve the ~2.1 GB/rank.
main 54f29da (15 commits). docs/adding_models.rst: main's sliding_window note on AttentionSpec, then the DSASpec row.
…389) Tensors are read with pread by a small thread pool a bounded window ahead of the consumer instead of faulting through safetensors' mmap, which moves a few pages per fault on ZFS and network filesystems. Per-expert stacked rule lists are matched through a token index instead of trying every rule on every key. Moved here from #389 (caeea87, 1ded60c) so #387 boots without it.
|
@NSagan271 moved #389's pread loader in and fixed the longctx yaml pointer (1c6ac1f) On bf16 index keys: ~1.2 of 2048 picks change per query (layer 0, 8k tokens) same as bf16 compute already does |
|
TP8 boot on 8xH200 with a warm page cache: weights load 961 ->323 s, ready 1148 -> 635 s |
#640 moved the paged-pool and prefix-walk refusals out of mstar/worker/engine_manager.py into mstar/validation/resources.py, as generators of problems, so test_glm52_kv_layout.py failed to import after the merge of main.
Main since 73c6c17: #883 mstar_rust in the Linux wheels, #901 output spans (BatchedModelOutput.output_spans, NodeSubmodule.spanned_outputs, the worker's crop), #904 DiT seed noise drawn at ingest (on_request_added, NoiseStager deleted, to_device_nonblocking as one memcpy into pinned memory), #903 API-server torch pool from the CPU quota and pooled image encodes, #924 robotics harness, #819 Inductor cache under ~/.cache, #881 CommGroup.agree / JointGroups.agree (agree_across_ranks and flagged_on_any_rank deleted), #724 CUDA IPC KV in one 2D memcpy a page, #387 GLM-5.2 (MLA, fp8 MoE with NoAuxTcRouter, the DSA resource), #921 vjepa2 AC crop, #922 Cosmos3 contiguous positions, #923 dense attention falls back when fa3-fwd has no kernel for the GPU. Conflicts: - mstar/utils/h2d.py, to_device_nonblocking: the stack's signature (data: Tensor | Sequence, device, dtype=None) and DeviceCapability pin_memory gate, with main's tensor path (#904): a CPU tensor is written into a fresh pinned buffer with one memcpy rather than pin_memory()'s copy_ over the intra-op pool. Every CPU tensor now goes through a fresh buffer, so the stack's "an already-pinned source is read in place" caveat (R3.3) no longer applies and is dropped from the docstring. test_accelerator_graph_backend's pinning test checks the pinned torch.empty and its non-blocking copy instead of pin_memory(). - mstar/model/components/diffusion/denoise_loop.py: imports, both (main's copy, the stack's functools). Main code on a superseded path, ported here: - GLM-5.2 (glm52/submodules.py): its own check_stop / check_stop_batched (four arguments, step_walk, its own row-map loop) / _stops -> token_stop = TokenStop(decode_loop, new_token, eos_token_ids, sampler) with a _token_stops override for its two differences (a stop token at prefill ends the reply at the first decode step; max_tokens counts the prefill's token), and a check_stop override for that closing step, whose token postprocess drops. test_glm52_model's helper and test_model_kv_layouts' GLM cap test build the real submodule (over a stub language model) instead of object.__new__, which would leave token_stop unset. - PinnedStager.to_device (#387), a second "host values -> new device tensor" helper beside to_device_nonblocking: deleted with its test (test_pinned_stager_to_device.py); its callers, GLM-5.2's prefill seq_lens and the DSA resource's page tables, call to_device_nonblocking(list, device, dtype=torch.long). test_dsa_resource spies on that instead. The other files both sides touched (engine.py, accelerator_graph_runner.py, kv/manager.py, attn/base.py, ragged/base.py, communication.py, model/base.py, submodule_base.py, entrypoint.py, conductor.py, moe.py, image_io.py, vjepa2_model.py, docs, tests) auto-merged into disjoint hunks: per file the merged delta equals main's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…per_request_outputs #387 now returns a plain prefill or decode step as a BatchedModelOutput (row outputs, one D2H for check_stop), so the MTP engine-cycle driver and the last-layer-rows test, which indexed the forward's dict by request, raised TypeError on the k=0 baseline. They now go through _testing.per_request_outputs, as the plain engine-cycle test does.
#387 dropped GLM-5.2's own prompt-aware clamp without MTP: the conductor holds prompt plus max_tokens to the window (#808, #832), so the k=0 arm asked for 40 tokens in a 24-row window and tripped the context guard. Both arms now ask for the most the conductor admits (limit - prompt + 1): the k=0 arm fills the window exactly, and under MTP the clamp still leaves two verify blocks.
Since #832 a paged pool needs its labels declared and each request its layout from process_prompt, and since #640 validate_deployment refuses a paged pool with no labels before any worker starts, so GLM-5.3 could not start once main was merged in. As GLM-5.2 does (#387): - kv_labels: the one "main" label, growing on "decode" under decode_loop. - process_prompt returns the prompt's ids span and, per decode iteration, one row, or under MTP the whole k + 1 block a verify step keeps (its rejected drafts stay as holes against kv_rows), so the pool sizes a request at its worst case. - get_loop_caps: decode_loop runs max_output_tokens - 1 iterations; the prefill emits the first token. - A request that names no max_tokens starts from what the decode loop can run (under MTP the cache rows bind before index_topk), which the conductor then fits to its prompt. Asking for index_topk, every such MTP request would need more iterations than the loop has, a 400.
…efusal #387 has TextChatAdapter refuse a chat that asks for a tool call, since no parser there splits one out of the reply. GlmChatAdapter, which does (glm_chat.GlmReplyParser), runs through TextChatAdapter.chat_to_request, so after the merge every GLM chat with tools was a 400 and tool_choice "required" got that message instead of its own. An adapter that parses tool calls now says so (serves_tool_calls); the others still refuse them.
…ake fails no reads - process_prompt returns a ProcessPromptOutput since GLM-5.2 (#387) and GLM-5.3 declare their KV layouts, so the rendered-chat test reads .new_input_tensors. - main's data worker asks the tensor manager for reads that failed (#419, take_failed_reads); the usage test's fake has none.
GLM-5.2 (#387) and GLM-5.3 return a ProcessPromptOutput, with their KV layouts, since the merge of main; the test indexed it as a dict.
GLM-5.2-FP8 (
GlmMoeDsaForCausalLM, 753B total / 40B active, 78 layers) served withmstar-serve --config configs/glm52_tp8.yaml, TP8 on the shared engine.glm52_tp8.yaml): contexts up toindex_topk(2048 tokens), where dense MLA attends exactly what the DSA indexer would pick. Prefill and decode are captured.glm52_tp8_longctx.yaml,dsa_long_context: true): contexts up to 8192 through the DSA engine resource (mstar/engine/resources/dsa/), eager only. Its index keys sit in a growingIndexKeyStore, a stopgap until they move onto the MLA cache's pages (DSA: paged index keys (#392) and what it leaves out #880, done in Extend GLM-5.2 to 256k-token contexts #392).Later in the stack: fp8 kernels and one-graph MTP (#388), paged index keys and 256k context (#392).
Shared code
mstar/model/components/mla.py(MLAAttention; DSA hooks in through_select/_attend_selected)mstar/model/components/moe.py(NoAuxTcRouter)mstar/utils/fp8_block.pyPositionManager.apply_qk, pluspos_ids=andk=None(for the indexer)Tests
index_topk; DSA attention against a masked reference with per-request page tables; the fp8 MoE dispatch against bf16.8945dcf76(this branch with main73c6c17c4): only the tests main fails on that machine, by name.ruff check .clean.202900ea2and pass in coriander's CPU runs.Known gaps
index_topkthe whole batch attends its picks (fp32, not bitwise the dense path).config.jsongeometry is read only from a local checkpoint path.