fix(ling): apply Ling's trained per-layer SwiGLU clamp (+17pp HumanEval) - #2571
Merged
Conversation
Ling-3.0-flash is trained with a clamped SwiGLU on its late layers and ships the limits in config.json: expert_swiglu_limit_list layers 35-41 = 4 share_expert_swiglu_limit_list layers 34-39 = 5, layers 40-41 = 7 Neither oMLX's vendored bailing_hybrid nor the bailing_hybrid in the mlx-lm build oMLX bundles implements any clamp, so Ling has been running its last seven layers unclamped, where activations can run away. inclusionAI's vLLM port does implement it (_get_layer_swiglu_limit + SwigluStepAndMul); the HF transformers reference does not, which is plausibly how it came to be omitted downstream. Measured on Ling-3.0-flash-8fixed-5routed, HumanEval via oMLX's own accuracy bench (164 problems, greedy, thinking off, no MTP): clamped 88.41% (145/164) 163.6s unclamped 71.34% (117/164) 167.0s +17.07pp for 28 problems, at no measurable runtime cost — the clamp is two elementwise ops. Verified independently on this branch: 88.41% (145/164). Semantics follow vLLM exactly: silu(gate).clamp(max=L) * up.clamp(-L, L) applied per layer, separately for routed experts (SwitchGLU.activation) and shared experts. Argument order is checked against mlx-lm's switch_layers.SwiGLU.__call__(x, gate) -> swiglu(gate, x), since SwitchGLU passes (x_up, x_gate) and reversing it would clamp the wrong tensor. Two code paths are covered because the live mlx_lm.models.bailing_hybrid can come from either place: the vendored copy now implements the clamp in-source, and swiglu_clamp.ensure_swiglu_clamp installs it onto an mlx-lm build that already ships the module (which is what oMLX bundles today, so the vendored copy is not otherwise reached). OMLX_LING_NO_SWIGLU_CLAMP=1 restores the unclamped path; that is how the A/B was produced and it keeps the result reproducible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI failed on all three Python versions with TypeError: ModelArgs.__init__() missing 1 required positional argument: 'group_norm_size' The test config omitted a required ModelArgs field. It passed locally because the bundled mlx-lm build supplies its own bailing_hybrid, so apply_bailing_hybrid_patch() defers to that module and the vendored copy is never constructed. CI has no such build, so it registers the vendored copy — whose ModelArgs requires group_norm_size — and every model-building test blew up. Only one of the two module paths was exercised locally. Both are now checked: pytest against the live (bundled) module, and the vendored copy loaded directly the way CI resolves it.
True2456
marked this pull request as ready for review
August 9, 2026 14:13
Owner
|
Thanks for tracking this down. I verified the clamp wiring against the checkpoint config and ran a real-server smoke with |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Ling-3.0-flash is trained with a clamped SwiGLU on its late layers, and ships the limits in
config.json:Nothing in oMLX applies them. Neither the vendored
bailing_hybridinomlx/patches/bailing_hybrid/nor thebailing_hybridin the bundled mlx-lm build has any clamp handling, so Ling runs its last seven layers unclamped and activations there can run away.inclusionAI's vLLM port does implement this —
_get_layer_swiglu_limit+SwigluStepAndMulinbailing_moe_v3.py(commit e0040c3). The HF transformers reference does not, which is plausibly how it came to be omitted downstream. A third-party NVFP4/SGLang repack independently hit the same omission in SGLang's NVFP4 MoE path and reports it corrupting a large share of generated code.Impact
HumanEval on
Ling-3.0-flash-8fixed-5routedvia oMLX's own accuracy bench — 164 problems, greedy, thinking off, MTP off:+17.07pp — 28 problems — at no measurable runtime cost. The clamp is two elementwise ops; the eval times are within noise. Reproduced independently on this branch: 88.41% (145/164).
Change
Semantics follow vLLM exactly:
applied per layer, separately for routed experts (
SwitchGLU.activation) and shared experts.Argument order is checked against mlx-lm's
switch_layers.SwiGLU.__call__(x, gate) -> swiglu(gate, x):SwitchGLUpasses(x_up, x_gate), so silu lands on the gate. Reversing it would clamp the wrong tensor, and a test pins that.Two code paths are covered, because the live
mlx_lm.models.bailing_hybridcan come from either place:swiglu_clamp.ensure_swiglu_clamp()installs it onto an mlx-lm build that already ships the module — which is what oMLX bundles today, so the vendored copy isn't otherwise reached.apply_bailing_hybrid_patch()calls it for whichever module resolves.Models without the config keys are untouched (the helper returns
Nonefor absent/zero limits), so this is a no-op for every other bailing checkpoint.OMLX_LING_NO_SWIGLU_CLAMP=1restores the unclamped path — that's how the A/B above was produced, and it keeps the result reproducible by a reviewer.Tests
8 new cases in
tests/test_bailing_swiglu_clamp.py: limit resolution (absent/zero/short list), the clamp formula against the reference, that the clamp actually binds, the SwitchGLU argument order, per-layer wiring for routed and shared paths, that a model without limits stays unclamped, and a finite forward. They run against whichever build is live.Verified on the real 127B checkpoint:
Ling SwiGLU clamp applied to 15 expert paths, matching the config lists exactly (7 routed on layers 35-41, 8 shared on 34-41).Upstream
The same fix is proposed against the mlx-lm branch this vendors from: scaryrawr/mlx-lm#1 (
ling-3.0-flash). If that lands, the vendored copy here can simply be resynced and the runtime install path becomes a no-op (it already detects a build that implements the clamp natively and leaves it alone).