Skip to content

[megatron] GLM-5.3-Flash (glm5_next) support: KDA + NoPE-MLA/DSA + mHC hybrid MoE, 4-layer GPU CI entry - #2179

Open
erictang000 wants to merge 18 commits into
NovaSky-AI:mainfrom
erictang000:glm5.3-flash
Open

erictang000 wants to merge 18 commits into
NovaSky-AI:mainfrom
erictang000:glm5.3-flash

Conversation

@erictang000

@erictang000 erictang000 commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

What

Adds GLM-5.3-Flash (HF glm5_next, Glm5NextForConditionalGeneration) to the Megatron backend and a test_megatron_models H100 CI entry on the 4-layer slice eatang/GLM-5.3-Flash-4layer (mirror of CharyZeng/GLM-5.3-Flash-4layer; real truncated weights, so the logprob distribution stays peaked and the vLLM/Megatron comparison is meaningful even though the slice is not a coherent LM).

The architecture is unknown to both pinned upstreams: 45 layers of 3:1 KDA linear-attention / NoPE-MLA DeepSeek-sparse-attention (kpool indexer), 288-expert sigmoid MoE with one shared expert (dense MLP in the first layer), clamped SwiGLU, and Manifold-Constrained Hyper-Connections (mHC) on every block, shipped as a VL checkpoint.

Written from the HF modeling code against upstream Megatron-LM / Megatron-Bridge APIs, reusing what upstream already ships: megatron-core's DSAttention + AbsorbedMLASelfAttention (the NoPE case, qk_pos_emb_head_dim=0, works as is), TEGroupedMLP with activation_func_clamp_value for the clamped SwiGLU, and — since #2180 bumped megatron-core to b3393bbb — megatron-core's own HyperConnectionModule and TransformerBlock mHC stream handling.

Components

Model-specific code lives in two packages, split by where it should eventually land upstream.

workers/megatron/mcore_ext/ → Megatron-LM (megatron.core)

file what upstream status
hyper_connection.py (49) RMSNormInputHyperConnectionModule: megatron-core's HyperConnectionModule with the input normalization overridden to GLM's standard RMSNorm, x * rsqrt(mean(x^2) + rms_norm_eps), instead of upstream's x / (rms(x) + 1e-6). Delete once TransformerConfig carries the input-norm knobs (mhc_norm_eps / mhc_norm_eps_inside_sqrt).
mhc_transformer_layer.py (199) HyperConnectionTransformerLayer with MoE MLP support — megatron-core's own mHC layer raises NotImplementedError for MoE sub-layers (mHC + MoE is only reachable there by wrapping MoE as a HybridStack layer), which GLM-5.3-Flash needs on all but the first layer. TransformerBlock owns the block-boundary stream expand/contract; this layer implements only the per-sub-layer residual update. Rejects 'mhc' in recompute_modules, whose managers it does not thread. The MoE support is the piece to upstream.
kda.py (327) KimiDeltaAttention: KDA linear attention (Kimi Linear / GLM-5.3-Flash) from TE column/row/duplicated linears, three depthwise causal convs, fp32 A_log/dt_bias, fla chunk_kda with the in-kernel safe gate (lower_bound * sigmoid(exp(A_log) * (f + dt_bias))) and fla FusedRMSNormGated output gate. TP shards heads (conv/A_log/dt_bias along heads; low-rank f_a/g_a duplicated; replicated o_norm grads summed across TP); packed thd via cu_seqlens; sharded_state_dict like GDN. Reuses the GDN config fields plus kda_gate_lower_bound. No CP, no inference cache. New experimental_attention_variant="kda" candidate next to gdn/gdn2.

workers/megatron/glm5_next/ → Megatron-Bridge (models/glm/)

file what
provider.py (55) Glm5NextModelProvider(MLAModelProvider): mHC fields named after megatron-core's TransformerConfig, plus kda_gate_lower_bound, mhc_norm_eps* and dsa_indexer_kpool*. The attention pattern uses the generic linear_attention_freq list (1 = KDA, 0 = DSA).
layer_specs.py (113) build_glm5_next_layer_spec: per-layer KDA-or-DSA × dense-or-MoE HyperConnectionTransformerLayer specs, built from the public get_dsa_module_spec_for_backend / get_moe_module_spec_for_backend / get_mlp_module_spec_for_backend helpers.
dsa.py (49) Glm5NextDSAttention(DSAttention): the k-pool indexer selects every visible token whenever a sequence has at most index_topk tokens, so megatron-core's token-level indexer with dsa_indexer_topk=index_topk is exact in that regime and is reused; longer sequences raise instead of silently attending to a different subset.
bridge.py (318) Glm5NextBridge registered for Glm5NextForConditionalGenerationGPTModel, model_type="glm5_next": config mapping off text_config (skipping the head_dim=0 RoPE width) and 1:1 parameter mappings under model.language_model.* — KDA (separate q/k/v_conv1d, A_log, dt_bias), mHC (hc_*_fn/hc_*_base replicated, hc_*_scale [3] ↔ three alpha_* scalars via a small custom mapping, the shape Megatron-Bridge's DeepSeek-V4 bridge already uses). All 3050 language-model tensors map; no dropped conversion tasks. The vision tower is not bridged (language_model_only=True required).

SkyRL glue

  • patches/megatron/patch_fa4_cute_import.py + workers/megatron/__init__.py: flash-attn 2.8.x's FA4 flash_attn.cute raises AttributeError against the cutlass DSL the pinned vLLM pulls in, which megatron-core's except (ImportError, PackageNotFoundError) probe does not catch, so import megatron.core aborts. The guard marks flash_attn.cute unavailable only when it is actually broken. Still required on torch 2.13. Delete once megatron-core's probe catches Exception or the pins agree.
  • MegatronWorker.init_configs: megatron-core rejects mHC under recompute_granularity="full", which is what SkyRL's default gradient_checkpointing=True resolves to. Its suggested alternative (selective recompute with 'mhc' in recompute_modules) needs the mHC recompute managers the layer above does not thread, so this downgrades to selective recompute of the remaining modules with a log line.
  • model_bridges.py: imports the bridge to register it.
  • test_megatron_models.py: glm-5.3-flash-4layer_h100_tp2_ep4 (Megatron TP2 EP4 ETP1 → DP2, vLLM TP4 colocated, language_model_only, packed sequences, inference_only_init, max_num_seqs=512).
  • test_glm5_next_modules.py (new, single GPU): RMSNormInputHyperConnectionModule vs HF Glm5NextTextHyperConnection at the model's activation scale, and KimiDeltaAttention on packed sequences vs per-sequence HF Glm5NextTextLinearAttention.
  • Docs: supported_models.mdx, .claude/docs/backends/megatron.md.

Dependencies

vLLM 0.28.1 fallout handled: vllm.entrypoints.openai.cli_args moved to vllm.entrypoints.launchers.cli_args (imported with a fallback); /inference/v1/generate, which SkyRL's generation client calls, is now gated behind VLLM_ENABLE_SCALE_OUT_ENDPOINTS=1 (set before build_app); vLLM registers a native sharded_rdt weight-transfer engine, so the SkyRL shim no-ops and its unit test accepts either engine.

Validation

Megatron vs HF transformers 5.16.1 (sdpa, bf16) logits on the 4-layer slice, 82-token GSM8K prompt+answer, packed thd input (standalone torchrun parity script).

On this branch (torch 2.13, megatron-core b3393bbb), HF reference recomputed on the same toolchain:

config KL(HF‖Meg) argmax agree mean |Δlogprob|
TP1 0.0082 91.5% 0.058

For scale, #2156 measured HF-bf16 vs HF-fp32 on this same slice at 0.0071 KL / 95.7% argmax, so this sits at the bf16 noise floor. The slice predicts near-randomly (HF logprob per target token ≈ −14.15), which makes argmax agreement the noisiest of the three statistics — most positions are near-ties that flip on tiny numeric differences while KL barely moves.

test_glm5_next_modules.py on the same tree: 2 passed — mHC exact to 4e-9 against the HF module, KDA within 0.5% of the reference mean on packed sequences.

test_logprobs_matching_roundtrip[glm-5.3-flash-4layer_h100_tp2_ep4] on Anyscale 4×H100:

vLLM logprobs     - mean: -4.675364, std: 0.380763
Megatron          - mean: -4.686937, std: 0.391731
logprob diff mean: 0.063767, std: 0.130800          (threshold 1e-1)
vLLM logprobs after sync - mean: -4.654904, std: 0.410720
vLLM logprob diff mean: 0.262453, std: 0.217895     (threshold 3e-1, two independent greedy generations)

and again in the H100 CI suite alongside the other model entries (logprob diff mean: 0.063314, vLLM logprob diff mean: 0.250699). Both of those runs predate the merge with main — they were on torch 2.11 with the megatron bump applied. The suites need re-running on this branch's tree; earlier TP2/EP2 parity (KL 0.0066, argmax 98.8%) is from that same pre-merge toolchain.

Known issue

test_logprobs_matching_roundtrip[qwen3.5-0.8b-dense_tp2] OOMs on 22 GiB L4 in the megatron_models suite: vLLM reserves 19.83 GiB for KV cache at the default gpu_memory_utilization=0.9, then flashinfer 0.6.18's top_k_top_p_sampling_from_logits needs a ~970 MiB vocab-wide softmax during sampler warm-up. Introduced by the flashinfer bump here — that entry has no gmu override and passes on main's 0.6.16.post3. Needs either a gmu override for small cards or VLLM_USE_FLASHINFER_SAMPLER=0; not reproducible on H100 (80 GiB hides it).

Limitations / follow-ups

  • k-pool indexer: not implemented. Exact for sequences ≤ index_topk (2048) tokens; longer sequences raise (glm5_next/dsa.py). Needs a DSAttention hook letting an indexer own the top-k selection (pool scoring → expand to tokens → append the query's tail pool).
  • Cross-layer DSA index sharing (indexer_types containing "shared"; the 4-layer slice is all "full") raises: megatron-core's dsa_indexer_topk_freq/skip_topk_offset arithmetic runs over all layers and would pick KDA layers as sources in this hybrid.
  • KDA: no context parallelism, no inference cache. mHC: PP=1 only (megatron-core's own restriction), and no selective layernorm/mlp/mhc recompute.
  • Training is wired (TE linears, TP grad-sync attributes, fp32 A_log/dt_bias/mHC params) but this PR exercises forward + weight sync only.

Upstreaming plan

Megatron-LM (megatron-core)

  1. HyperConnectionModule: add mhc_norm_eps / mhc_norm_eps_inside_sqrt for a standard-RMSNorm input norm, with the fused-kernel path extended or guarded. Retires mcore_ext/hyper_connection.py.
  2. HyperConnectionTransformerLayer: allow MoE MLP sub-layers (currently NotImplementedError). Retires mcore_ext/mhc_transformer_layer.py.
  3. KimiDeltaAttention as experimental_attention_variant="kda" (module + spec next to gated_delta_net, kda_gate_lower_bound in TransformerConfig). Retires mcore_ext/kda.py.
  4. DSA: k-pool indexer support and a per-indexer top-k selection hook; hybrid-aware index sharing.

Megatron-Bridge

  1. models/glm/glm5_next_{provider,spec,bridge}.py = this PR's glm5_next/ package, once (1)–(3) exist upstream — the bridge only needs its layer/provider imports repointed.
  2. Promote the DeepSeek-V4 _HCAlphaMapping pattern (here HyperConnectionScaleMapping) into param_mapping.py so both bridges share it.

Once megatron-core carries (1)–(3), mcore_ext/ is deleted and glm5_next/ imports from megatron.core; once Megatron-Bridge carries the bridge, glm5_next/ goes too and model_bridges.py loses one import.

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for the GLM-5.3-Flash (glm5_next) model in the Megatron backend, implementing custom Megatron-Core extensions such as Kimi Delta Attention (KDA), Manifold-Constrained Hyper-Connections (mHC), and a custom transformer layer that supports MoE. It also updates dependencies (including vLLM and FlashInfer) and includes compatibility fixes for vLLM >= 0.28.1. The review identified two critical runtime issues: an AttributeError in the DSA attention module due to an incorrect attribute name (self.index_topk instead of self.dsa_indexer_topk), and a TypeError in the mHC transformer layer caused by passing an unsupported padding_mask argument to the MLP forward call.

Comment on lines +41 to +44
if max_seqlen > self.index_topk:
raise NotImplementedError(
f"GLM-5.3-Flash sparse attention with sequences longer than dsa_indexer_topk="
f"{self.index_topk} tokens (got {max_seqlen}) needs the k-pool indexer, which the "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The attribute self.index_topk does not exist on DSAttention or Glm5NextDSAttention. The correct attribute name in megatron-core is self.dsa_indexer_topk (or self.config.dsa_indexer_topk). Accessing self.index_topk will raise an AttributeError at runtime.

Suggested change
if max_seqlen > self.index_topk:
raise NotImplementedError(
f"GLM-5.3-Flash sparse attention with sequences longer than dsa_indexer_topk="
f"{self.index_topk} tokens (got {max_seqlen}) needs the k-pool indexer, which the "
if max_seqlen > self.dsa_indexer_topk:
raise NotImplementedError(
f"GLM-5.3-Flash sparse attention with sequences longer than dsa_indexer_topk="
f"{self.dsa_indexer_topk} tokens (got {max_seqlen}) needs the k-pool indexer, which the "

pre_mlp_layernorm_output, moe_padding_mask, moe_unflatten_mbs = self._maybe_unflatten_for_moe(
pre_mlp_layernorm_output, padding_mask, packed_seq_params
)
mlp_output_with_bias = self.mlp(pre_mlp_layernorm_output, padding_mask=moe_padding_mask)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Standard Megatron-Core MLP and MoE layers (such as GroupedMLP or TEGroupedMLP) do not accept padding_mask in their forward method. Passing padding_mask=moe_padding_mask will raise a TypeError at runtime. You should remove this keyword argument.

Suggested change
mlp_output_with_bias = self.mlp(pre_mlp_layernorm_output, padding_mask=moe_padding_mask)
mlp_output_with_bias = self.mlp(pre_mlp_layernorm_output)

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

The DeepGEMM platform coverage must be fixed or the unsupported environments explicitly rejected before merging because GLM-5.3-Flash cannot initialize there.

Findings

  1. P1 DeepGEMM fallback cannot load

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    HF[HF GLM-5.3-Flash checkpoint] --> Bridge[Glm5NextBridge]
    Bridge --> Provider[Glm5NextModelProvider]
    Provider --> Block[HyperConnection transformer block]
    Block --> KDA[KDA layers]
    Block --> DSA[NoPE-MLA / DSA layers]
    DSA --> DG[DeepGEMM indexer kernels]
    Block --> MoE[Dense / MoE MLP]
    Block --> MHC[mHC residual streams]
    Bridge --> Sync[Weight synchronization]
    Sync --> VLLM[vLLM inference engine]
Loading

Comment thread pyproject.toml Outdated
"vllm==0.28.0; sys_platform == 'linux'",
"vllm; sys_platform == 'linux'",
"vllm-router; sys_platform == 'linux'",
"deep-gemm; sys_platform == 'linux' and platform_machine == 'x86_64' and python_full_version >= '3.12' and python_full_version < '3.13'",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 DeepGEMM fallback cannot load

When the Megatron extra is installed on Linux aarch64 or x86_64 Python 3.13, the environment marker excludes the Torch 2.11-compatible deep-gemm package, so vLLM falls back to its Torch 2.13-linked extension and GLM-5.3-Flash inference fails to initialize its required DSA indexer.

erictang000 and others added 5 commits September 8, 2026 14:53
Brings in the torch 2.13 upgrade (NovaSky-AI#2175), which replaces the `+cu.13.0.torch.2.11`
local-version pins with plain versions from NovaSky-AI/skyrl-wheels and raises
requires-python to 3.12. Conflicts were pyproject.toml (5 hunks) and uv.lock.

Resolution takes main's torch 2.13 and CUDA-extension pins throughout, and keeps this
branch's flashinfer 0.6.18 (required by the pinned vLLM) and the unpinned `vllm` driven by
the per-commit dev-wheel source.

Drops the `deep-gemm` dependency and its hosted wheel. It existed only because the wheel
we could build topped out at torch 2.11 while vLLM's vendored
`vllm.third_party.deep_gemm._C` is built against torch 2.13, so the vendored copy failed to
import and vLLM prefers an installed `deep_gemm` when one exists. On torch 2.13 the vendored
extension loads, so keeping a torch-2.11 build would have been actively harmful: vLLM would
prefer it and then fail to import it, taking out the GLM-5.3-Flash DSA indexer that
hard-requires DeepGEMM. Verified on the merged tree: `vllm.third_party.deep_gemm._C`
imports, `vllm.utils.deep_gemm.has_deep_gemm()` is True, and no standalone `deep_gemm` is
installed.

`dsa_index_share_recompute.patch` is unchanged here: this branch still pins megatron-core
14346b65a, which the existing patch matches. (It needs refreshing only alongside the
megatron-core bump to b3393bbb -- see NovaSky-AI#2180.)

`patch_fa4_cute_import` is still required under torch 2.13: the cutlass DSL the pinned vLLM
pulls in still breaks `flash_attn.cute`, and the guard still reports marking it unavailable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings in the megatron bump (NovaSky-AI#2180): megatron-core 14346b65a -> b3393bbb and megatron-bridge
2b8cb21d -> 8e5f0e1, plus the refreshed DSA index-share patch and the removal of the
expert-bias padding-mask shim.

pyproject.toml conflict resolved to main's megatron-core rev, keeping this branch's
flashinfer 0.6.18 and the per-commit vLLM dev-wheel source; uv.lock regenerated from main's
so the only additions are flashinfer 0.6.18, the vLLM wheel and instanttensor.

The new megatron-core ships mHC natively, which requires the adaptations this branch was
carrying on top of the old rev:

- `mcore_ext/hyper_connection.py`: the 688-line backport of Megatron-LM main's
  `HyperConnectionModule` is deleted in favour of `RMSNormInputHyperConnectionModule` (49
  lines), which only overrides the input normalization to GLM's standard RMSNorm
  (`rsqrt(mean(x^2) + eps)`) instead of megatron-core's `1 / (rms(x) + 1e-6)`.
- `mcore_ext/mhc_transformer_layer.py`: `TransformerBlock` now expands/contracts the mHC
  residual streams itself when `enable_mhc_connections` is set, so the layer no longer does it
  at the first/last layer -- otherwise the streams are expanded twice and the mHC mapping sees
  `[s, n*n*C]`. The layer keeps the per-sub-layer residual update and gains a guard rejecting
  `'mhc'` in `recompute_modules`, whose managers it does not thread.
- `MegatronWorker.init_configs`: megatron-core rejects mHC under
  `recompute_granularity="full"`, which is what SkyRL's default `gradient_checkpointing=True`
  resolves to, and its suggested alternative needs the managers above. Downgrades to selective
  recompute of the remaining modules with a log line.

Validated on the merged tree: `test_glm5_next_modules.py` 2 passed with per-module errors
unchanged from before the bump (mHC exact to 4e-9, KDA 0.5% of the reference mean), and
Megatron-vs-HF TP1 logit parity on GLM-5.3-Flash-4layer stays at the bf16 noise floor
(KL(HF||Meg) 0.0086, argmax agree 92.7%, mean |dlogprob| 0.060).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ngle B300 node

The existing GLM-5.3-Flash coverage runs the 4-layer slice, which exercises every code path
but is not a coherent LM. This adds the real 45-layer `zai-org/GLM-5.3-Flash` checkpoint as a
`test_megatron_models` row so the same logprob-parity and weight-sync check can be run
against real weights, where generation should also read as sensible text.

~313B params in bf16 (~627 GiB, 97% of it routed experts) with ~17B activated, so it needs a
whole 8xB300 node and cannot run in CI. It carries a new `b300` marker, gated exactly like
`h100`: auto-skipped unless `-m b300` is passed, so `-m megatron_models` and `-m h100` both
leave it alone. The gating loop in the GPU conftest now covers both markers instead of
hard-coding h100.

Mesh: Megatron TP2 EP8 ETP1 -> DP4 (EP x ETP == TP x DP), vLLM TP8 colocated on the same 8
GPUs. EP is the scaling dimension for a MoE this sparse -- 36 experts/GPU, ~76 GiB -- while TP
only covers the ~9B of non-expert weights. PP stays at 1 because megatron-core rejects mHC
with pipeline_model_parallel_size > 1, and CP at 1 because KDA has no context-parallel path.
Everything else is picked up by the existing `glm-5.3-flash` branches:
`language_model_only`, packed sequences, `inference_only_init`, `max_num_seqs=512` (the vLLM
KDA triton grid limit) and the 4096-token / 0.5-utilization engine overrides.

Also pins fla to its Triton kernels for GLM-5.3-Flash on Blackwell (`FLA_TILELANG=0`), the
same workaround the Qwen3.5 rows use for GDN -- KDA runs fla kernels too. Scoped to Blackwell
so the H100 rows keep fla's default backend.

The real config needs nothing the bridge rejects: all 45 `indexer_types` are `"full"`, so the
unsupported cross-layer DSA index sharing never comes up. Thresholds mirror the other
large-MoE rows and have not been measured on this checkpoint; expect to tune them on the
first run.

Not run here -- this box has no B300s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Eric Tang <erictang000@gmail.com>
…LoRA packing, HTTP diagnostics

Three independent fixes found while bringing up GRPO/DAPO for GLM-5.3-Flash on 8xB300.

Forward CUDA_HOME into the Ray runtime env. TileLang JITs kernels by shelling out to nvcc and
picks its toolkit from CUDA_HOME, defaulting to the pip wheel tree. That tree is internally
inconsistent here -- nvidia-cuda-nvcc==13.3 next to nvidia-cuda-runtime==13.0 (CUDART_VERSION
13000) -- and nvidia-cuda-cccl asserts the two match, so every JIT fails with "CUDA compiler and
CUDA toolkit headers are incompatible". Pointing CUDA_HOME at a self-consistent system toolkit
fixes it, but workers are re-exec'd through the runtime env, so a driver-side export never
reaches them. Reproduced standalone: compiling tilelang's common.h for sm_103a fails with the
wheel nvcc and succeeds with /usr/local/cuda-13.3.

Backport vllm-project/vllm#56327 as a runtime patch. Glm5NextForConditionalGeneration otherwise
inherits Glm4v's packed_modules_mapping, which names none of the model's fused projections, so a
Megatron-trained adapter's separate q/k/v/b/f_a/g_a weights have nothing to assemble onto. Also
honors the replicated_shard_ids that KDA's in_proj_qkvbfg_a already declares, keeping LoRA-B whole
on every TP rank for f_a_proj/g_a_proj (matching parallel_mode="duplicated" on the trainer side).
Adds one thing the PR does not: KDA splits its fused projection into non-contiguous views, so a
LoRA-wrapped f_b_proj trips `assert inputs.is_contiguous()` in the triton lora_shrink; the shrink
input is now made contiguous when needed (a [num_tokens, 128] bf16 copy, only when non-contiguous).
The patch is guarded, idempotent, and skips a build that already has the fix.

Report non-JSON HTTP responses. The bare orjson.JSONDecodeError says only "line 1 column 1
(char 0)" and the retry reason was logged at debug, so an engine returning 502 was invisible from
the driver log. Logging status, length and a body snippet turned an opaque failure into
"status=502 Backend request failed: error sending request for url (...)" immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lash

Three scripts: GSM8K GRPO on a single colocated 8xB300 node, and DAPO (sync colocated on 3 nodes,
plus fully-async with 2 trainer nodes and 1 inference node). The GSM8K one is verified -- 51 steps,
0 errors, rewards 0.89-1.00 at ~215-290 token responses.

The model-specific settings are the non-obvious part, so each is commented in place:

- LoRA, not full fine-tuning: ~313B params means bf16 weights + grads + fp32 master/m/v is ~5 TiB
  against 2.2 TiB of HBM, and at EP8/ETP1 on 8 ranks the expert optimizer state gets no sharding
  at all (expert-DP == 1).
- merge_lora=true. vLLM selects a LoRA-aware MoE expert kernel whenever LoRA is enabled globally
  (fused_moe/oracle/unquantized.py), but only sets the lora_context that kernel asserts on when the
  MoE layer is itself LoRA-wrapped. Merging sidesteps the whole path; the scripts switch back to
  adapter sync with one flag once that is usable.
- recompute_granularity=selective with [core_attn,moe]. megatron-core rejects mHC with full
  activation recompute, and its suggested 'mhc' is then rejected by SkyRL's own
  HyperConnectionTransformerLayer (it does not thread CheckpointWithoutOutputManager, so accepting
  it would silently drop the recompute). Selective also requires recompute_num_layers=None, which
  DEFAULT_TRANSFORMER_CONFIG_KWARGS otherwise injects.
- use_fused_mhc left off: the fused kernels implement only 1/(rms+eps), incompatible with the
  eps-inside-sqrt normalization GLM actually uses.
- Sequences capped under 2048. The DSA layers index with dsa_indexer_topk=2048 and the Megatron
  backend has no k-pool indexer, so glm5_next/dsa.py raises at training time for anything longer.
- n_samples_per_prompt=12 for the sync DAPO run: dp is pinned to 24/TP2=12 (KDA has no
  context-parallel path, mHC rejects PP>1), and validate_cfg needs (mini_batch * n_samples) % dp == 0.

The DAPO scripts additionally raise the router queue past the request fan-out, throttle
SKYRL_GENERATE_CONCURRENCY_PER_ENGINE (the 512 default releases 1536 requests at once and every
engine 502s), and raise SKYRL_WORKER_NCCL_TIMEOUT_IN_S -- a DAPO fwd_logprobs pass runs ~37 min, so
DP ranks waiting in a collective blow the 600s default and the NCCL watchdog aborts the worker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
erictang000 and others added 12 commits September 17, 2026 20:46
… k-pool indexer

Points megatron-core at the head of NVIDIA/Megatron-LM#7054 ("GLM5.3 Flash (KDA + mHC + KPool
DSA) support + FP8", HollowMan6) instead of implementing the k-pool indexer ourselves. That PR
supersedes our #7427: it carries the same KDA and mHC work plus the pooled indexer, was opened
two weeks earlier, is not a draft, and its config field names already match this provider
(dsa_indexer_kpool, dsa_indexer_kpool_always_select_tail, mhc_norm_eps_inside_sqrt).

Its pooling matches the HF/vLLM semantics independently reverse-engineered from vLLM's Triton
kernel: a per-dimension softmax over each pool's slots of `gate_score + ape`, weighting the keys.
It also handles the two parts that are easy to get wrong -- packed-sequence boundaries
(_kpool_compress_keys_per_seg) and the context-parallel gather/reorder of the gate score.

megatron-bridge stays on 8e5f0e13: bridge main needs hydra-core>=1.3.4 and SkyRL pins ==1.3.2,
so only megatron-core moves, isolating the variable. That is safe because the core rev bridge
main validates against (f6c33bde) is an ANCESTOR of the PR head (ahead 60, behind 0).

Two SkyRL-side changes are required:

- Map index_kpool_compress_ape / index_kpool_compress_gate. megatron-core only creates these when
  dsa_indexer_kpool > 1 and initializes them randomly (nn.init.normal_ on the gate), so a pooled
  run without these mappings would silently train against random pooling weights. They are bare
  nn.Parameters on DSAIndexer rather than module weights, so AutoMapping cannot infer a
  parallelism type ("Cannot determine parallelism type for module 'DSAIndexer'"); the indexer is
  duplicated across TP ranks, so they are ReplicatedMapping.
- Relax the >dsa_indexer_topk guard to fire only for kpool <= 1, which is now the only regime
  megatron-core cannot handle. Note index_kpool is an attribute of DSAIndexer, not DSAttention,
  so the guard reads it from the config.

Testing: upstream's own k-pool unit tests pass here (44), the new test_glm5_next_kpool.py passes
(6), and the GLM-5.3-Flash 4-layer model-level parity row passes -- Megatron logprobs against
vLLM plus the weight-sync round trip, with kpool=4 active and the pooling weights loaded from the
checkpoint. All seven SkyRL GLM modules still import across 60 commits of megatron-core drift.

Not yet covered: a >dsa_indexer_topk parity row. Below the budget every pool is selectable, so
the existing row cannot distinguish correct pooling weights from wrong ones -- only sequences
past 2048 exercise pool selection itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The existing GLM-5.3-Flash rows generate 128 tokens from ~100-250 token prompts, so every
sequence stays under dsa_indexer_topk (2048). In that regime every pool is selectable and the
k-pool indexer covers the full causal prefix regardless of what the compression weights hold --
the short row passes even with index_kpool_compress_gate/ape left randomly initialized, which is
exactly what an unmapped bridge entry produces (megatron-core does nn.init.normal_ on the gate).
Only past the budget does scoring decide which pools survive, so this is the first row whose
logprob comparison is sensitive to those weights at all.

Threading a per-row max_generate_length through the parametrize (existing rows pass None and keep
the module default) is enough to get there: GSM8K prompts are too short to cross 2048, so the
length comes from generation.

Measured over three runs on the 4-layer slice:

  Megatron vs vLLM      0.054 / 0.053 / 0.0546   (threshold 1e-1, unchanged)
  vLLM pre- vs post-sync 0.351 / 0.349 / 0.3498  (threshold raised to 5e-1)

The first is the check that matters, and the pooled path agrees with vLLM just as closely past
the budget as the dense path does below it. The second is raised because over a 2048-token greedy
generation it stops measuring weight-sync fidelity: one token flipped by a tiny numerical
difference makes every later token differ, and all three runs logged "pre/post-sync generation
lengths differ". It is reproducible rather than chaotic, so 5e-1 keeps headroom while still
failing on a real regression -- the 128-token rows sit around 0.06. Tightening it further means
shortening the generation, which would stop the row exercising pool selection at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ementing the DSA k-pool indexer

Lifts the dsa_indexer_topk (2048) ceiling on Megatron training. megatron-bridge stays pinned at
8e5f0e13 and SkyRL keeps its own glm5_next bridge package; only megatron-core moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The k-pool vendoring is the branch's actual configuration now, not a side experiment, and the
comment should say why the pin is where it is and when to move it back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…DSA ceiling

megatron-core implements the k-pool indexer, so sequences past dsa_indexer_topk (2048) are
selected rather than refused, and the 896 + 1024 cap those scripts carried is gone. Moves to
2048 prompt + 4096 response.

Still short of the stock DAPO recipe's 2k + 8k: an 8k response is roughly 8x the per-step cost of
the 1024-response runs that measured ~10 min/step, which does not fit an overnight experiment.

max_tokens_per_microbatch goes 4096 -> 8192 because a microbatch now has to hold one full
sequence; 16384 is still out, having OOM'd at step 1 on the GSM8K run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All three GLM-5.3-Flash recipes run merge_lora=true, which is correct but syncs ~599 GiB of
merged weights every step instead of a few hundred MB of adapter -- worst in the non-colocated
async recipe, where that crosses the network.

Documents the blocker chain (vLLM selects a LoRA-aware MoE kernel globally whenever LoRA is
enabled, but only sets the lora_context that kernel asserts when the MoE layer is itself wrapped,
and wrapping requires 'experts' to pass the target filter), what is already fixed on this branch
(the vllm#56327 backport, replicated_shard_ids, the add_shrink contiguity guard), and the leads
that are inferred but never executed -- chiefly that the 'experts' entry already sits unused in
the GSM8K script, and the likely share_expert_adapters / enable_moe_shared_loras mismatch.

Filed under .claude/docs with a routing row so an agent picking this up reads it first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nalizing typical response lengths

A 45-step run at rank 32 with shared expert adapters moved implied per-sample accuracy only
0.482 -> 0.502 while reward tracked response length at corr -0.93, so the curve could not
distinguish learning to solve from learning to be brief.

overlong_buffer_len 2048 -> 1024. On a 4096 cap the penalty started at 2048 while the mean
response was 2440, so every step's mean was penalized and the penalty's std was 87% of the whole
correctness signal's. At 1024 it starts at 3072 and is zero at current lengths, becoming the
near-cap guard the recipe intends instead of a tax on the typical rollout.

share_expert_adapters=False + normalize_moe_lora=True at rank 64. One adapter shared across all
local grouped experts was the capacity bottleneck with 288 experts holding ~97% of the
parameters. Normalization divides the expert rank by moe_router_topk (8 here -> expert rank 8),
keeping the per-token expert contribution comparable to a dense rank-64 adapter; it requires
rank % topk == 0, which 64 satisfies, and the resulting expert rank stays under the rank vLLM
would size max_lora_rank from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vmnode-6r3vaf61zkut was drained after its GPU0 lost P2P with every peer (nvidia-smi topo -p2p r
shows NS across GPU0's row), which made vLLM's TP=8 ncclCommInitRank fail there on every attempt.
It cannot be reset in-guest -- the GPUs are passthrough, so nvidia-smi -r is refused even with no
process holding the device -- and its NVLinks have been inactive since its fabric manager died on
2026-07-18, so it had been running on PCIe P2P the whole time.

sync: 3 -> 2 nodes and 3 -> 2 engines. world 16, dense dp 8, expert dp 2; mini_per_gpu 48,
train_per_gpu 192, seqs/step unchanged at 1536. Per-GPU work rises ~1.5x.

async: the policy drops from 2 nodes to 1 so the inference engine still gets a whole node to
itself (it is the non-colocated recipe). world 8, dense dp 4, expert dp 1; mini_per_gpu 96,
train_per_gpu 96. Per-GPU weight sharding is unchanged because EP8 is unchanged.

Both carry a note to restore the 3-node sizing once that node is repaired.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Item 3 of 'Still open' in .claude/docs/glm5_3_flash_lora.md: the DAPO recipes could not flip
MERGE_LORA because they carried no VLLM_LORA_TARGET_MODULES or conditional LORA_ENGINE_KWARG
block -- they were written after the switch to merge_lora=true. Copied across from the GSM8K
recipe, including 'experts', which is the whole fix for 'LoRA context must be set'.

Gating verified both ways: lora_target_modules is absent from engine_init_kwargs at
MERGE_LORA=true and present at false, where _uses_lora_weight_sync then reports enable_lora=True.

Co-Authored-By: Claude Opus 5 (1M context) <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