Repository navigation
feat(models): add Kuaishou Klear (Klear) text model support - #1044
Merged
Merged
Conversation
Adds the Klear sparse MoE family, validated on `Kwai-Klear/Klear-46B-A2.5B-Instruct` converted locally to 4-bit (24 GB, 32 layers, 256 experts top-8 plus one shared). The backbone is what the issue described. Three things are not: The model_type is capital-K "Klear". Upstream mlx-lm has to ship `Klear.py` and a byte-identical `klear.py` to cover both spellings; this tree needs only the lowercase arm because `get_model_type` lowercases first, which the test drives through the real entry point so the normalization is part of what is asserted. The shared expert is BLENDED, not added. `KlearSparseMoeBlock` learns a per-token 2-way softmax over an `mlp.coefficient` head and mixes `y = y_experts * coef[..0] + shared * coef[..1]`. Every other shared-expert family in this tree adds at a fixed weight of 1, so the obvious reuse is silently wrong: a plain add misweights every token while leaving the output finite and the text fluent. Routing activates with sigmoid rather than softmax, with the expert_bias applied to a selection-only copy. Nothing scales the result, and that is worth naming: the checkpoint declares `routed_scaling_factor: 2.5`, which looks like the knob the neighbouring DeepSeek-derived families apply, and upstream's ModelArgs does not declare the field at all. This port mirrors upstream and prints a diagnostic naming the field at load so the choice is visible rather than invisible. Validation. A 5-token prefill reproduces the mlx-lm reference's top-5 logits exactly, values included, and greedy decode is token-exact over 20 tokens. That holds with `MLXCEL_FUSED_MOE=0`; the default-on fused MoE decode kernel (#268) engages only at single-token decode and is not byte-identical here, so with it enabled the model picks a different but equally coherent continuation from a low-confidence position. The 5-token prefill, where the kernel does not engage, is exact either way. Closes #846
4 tasks
be-free-snowboy
pushed a commit
to freestylejs/snowbrain-mlxcel
that referenced
this pull request
Oct 27, 2026
) Closes lablup#846. Adds the Klear sparse MoE family, validated on `Kwai-Klear/Klear-46B-A2.5B-Instruct` converted locally to 4-bit (24 GB, 32 layers, hidden 2048, 256 experts top-8 plus one shared, 1351 tensors). `mlx-community/Klear-46B-A2.5B-Instruct-4bit` is an empty repo — only the 3-bit conversion has weights — so the checkpoint was produced locally from the 92.5 GB bf16 source. ## The backbone is what the issue described; three things are not **1. The `model_type` is capital-K `"Klear"`.** Upstream mlx-lm has to ship `Klear.py` **and** a byte-identical `klear.py` to cover both spellings, because it matches the config value as written. This tree needs only the lowercase detection arm, because `get_model_type` lowercases `model_type` before matching — without that normalization the arm would miss every published checkpoint. `the_capitalized_model_type_reaches_the_detection_arm` drives the real detection entry point rather than a helper, so the normalization is part of what is asserted, across `"Klear"`, `"klear"` and `"KLEAR"`. **2. The shared expert is BLENDED, not added.** This is the one that makes the obvious reuse silently wrong. Every other shared-expert family in this tree adds the shared MLP's output to the routed mixture at a fixed weight of 1. `KlearSparseMoeBlock` learns a per-token 2-way softmax instead: ```python coef = mx.softmax(self.coefficient(x), axis=-1, precise=True) y = y_experts * coef[..., :1] + shared * coef[..., 1:] ``` So there is an extra `mlp.coefficient` head per MoE layer — a `[2, hidden]` projection **with** a `[2]` bias, since upstream's `nn.Linear(hidden, 2)` defaults to `bias=True` — and a plain add misweights every token's output while leaving it finite and the text fluent. Pinned by two tests rather than a shape check: `the_shared_expert_is_blended_not_added` zeroes the routed branch and injects a known constant into the shared one, so the blended reading (`shared * coef[1]`) and the added reading (`shared`) are numerically distinguishable, and asserts which is computed; `the_blend_weights_come_from_the_coefficient_head_per_token` varies the coefficient logits to confirm the weights are read from the checkpoint and that index **1**, not 0, weights the shared branch. **3. Routing is sigmoid, and nothing scales the result.** `routing_weights = mx.sigmoid(self.gate(x).astype(mx.float32))`, not softmax, with the `expert_bias` added to a copy used for **selection only** while the returned scores are gathered from the unbiased weights — the same silent-misweighting contract `bailing_moe` documents. `norm_topk_prob` then renormalizes. That last point deserves naming. The published checkpoint declares **`routed_scaling_factor: 2.5`**, which looks exactly like the knob the neighbouring DeepSeek-derived families apply. Upstream's `ModelArgs` does not declare the field at all, so it is dropped on parse and `KlearSparseMoeBlock` scales nothing. This port mirrors upstream rather than guessing, and prints a diagnostic naming the field at load so the choice is visible instead of invisible. Applying it would multiply every routed contribution by 2.5 against the reference. ## Validation - **A 5-token prefill reproduces the mlx-lm reference's top-5 logits exactly**, values included: ``` mlxcel top5: 12095=19.0000 537=15.3750 3070=14.1250 264=13.8125 279=13.6250 mlx-lm top5: 12095=19.0000 537=15.3750 3070=14.1250 264=13.8125 279=13.6250 ``` - **Greedy decode is token-exact over 20 tokens**: ``` $ MLXCEL_FUSED_MOE=0 mlxcel generate -m models/klear-46b-a2.5b-instruct-4bit \ -p "The capital of France is" -n 20 --temp 0 --no-chat-template The capital of France is Paris. Paris is known for its beautiful architecture, including the Eiffel Tower, the Lou ``` identical to the reference. - A reference-free causality check (position 0 of a longer prefill equals a one-token prefill). **One caveat worth knowing, and it is not about this port.** That exactness holds with `MLXCEL_FUSED_MOE=0`. The default-on fused MoE decode kernel (issue lablup#268) engages only at single-token decode, and it is **not byte-identical here**: with it enabled, a 1-token probe gives `6741=7.7812 387=7.6250 3550=7.4688 ...` against the reference's `6741=7.5625 387=7.4062 8160=7.3125 ...` (same top-1, reshuffled near-ties at a low-confidence position where the top logit is only 7.5), and greedy decode picks a different but equally coherent continuation. Disabling the kernel makes the 1-token logits match exactly too, which is what isolates it. The 5-token prefill, where the kernel does not engage, is exact either way. That kernel was validated byte-identical on qwen3-30b-a3b when it landed; this is a second data point saying that does not generalize, and it affects every MoE family in the tree rather than Klear specifically. ## Other guards The sparse schedule is `layer_idx not in mlp_only_layers and num_experts > 0 and (layer_idx + 1) % decoder_sparse_step == 0`, which on the published checkpoint (`decoder_sparse_step: 1`, empty `mlp_only_layers`) makes every one of the 32 layers sparse. A zero step is rejected because upstream would divide by it; an `mlp_only_layers` entry past the end of the stack is rejected too. Loading also rejects an architecture scalar that could size an allocation or divide (zero checks precede the divisibility check, since `0.is_multiple_of(0)` is true in Rust), a `num_experts_per_tok` outside `1..=num_experts`, an odd head width, a non-positive or non-finite `rope_theta` or `rms_norm_eps`, a non-default `rope_scaling` block (upstream does not declare the field and so drops one silently; rejecting is the safer reading), and any tensor whose real shape disagrees with the config — including an expert stack with fewer planes than `num_experts` and a missing `mlp.coefficient`. `attention_bias` is false on the published checkpoint, so the bias path is never exercised by a real-model run; it is covered by a unit test that loads the block with biases and asserts that dropping them changes the output. ## Tests 22 new unit tests, all checkpoint-free. `cargo test --workspace --profile test-fast --features metal,accelerate` green; `cargo clippy --workspace --all-targets -- -D warnings` and `cargo fmt --all -- --check` clean.
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.
Closes #846.
Adds the Klear sparse MoE family, validated on
Kwai-Klear/Klear-46B-A2.5B-Instructconverted locally to 4-bit (24 GB, 32 layers, hidden 2048, 256 experts top-8 plus one shared, 1351 tensors).mlx-community/Klear-46B-A2.5B-Instruct-4bitis an empty repo — only the 3-bit conversion has weights — so the checkpoint was produced locally from the 92.5 GB bf16 source.The backbone is what the issue described; three things are not
1. The
model_typeis capital-K"Klear". Upstream mlx-lm has to shipKlear.pyand a byte-identicalklear.pyto cover both spellings, because it matches the config value as written. This tree needs only the lowercase detection arm, becauseget_model_typelowercasesmodel_typebefore matching — without that normalization the arm would miss every published checkpoint.the_capitalized_model_type_reaches_the_detection_armdrives the real detection entry point rather than a helper, so the normalization is part of what is asserted, across"Klear","klear"and"KLEAR".2. The shared expert is BLENDED, not added. This is the one that makes the obvious reuse silently wrong. Every other shared-expert family in this tree adds the shared MLP's output to the routed mixture at a fixed weight of 1.
KlearSparseMoeBlocklearns a per-token 2-way softmax instead:So there is an extra
mlp.coefficienthead per MoE layer — a[2, hidden]projection with a[2]bias, since upstream'snn.Linear(hidden, 2)defaults tobias=True— and a plain add misweights every token's output while leaving it finite and the text fluent.Pinned by two tests rather than a shape check:
the_shared_expert_is_blended_not_addedzeroes the routed branch and injects a known constant into the shared one, so the blended reading (shared * coef[1]) and the added reading (shared) are numerically distinguishable, and asserts which is computed;the_blend_weights_come_from_the_coefficient_head_per_tokenvaries the coefficient logits to confirm the weights are read from the checkpoint and that index 1, not 0, weights the shared branch.3. Routing is sigmoid, and nothing scales the result.
routing_weights = mx.sigmoid(self.gate(x).astype(mx.float32)), not softmax, with theexpert_biasadded to a copy used for selection only while the returned scores are gathered from the unbiased weights — the same silent-misweighting contractbailing_moedocuments.norm_topk_probthen renormalizes.That last point deserves naming. The published checkpoint declares
routed_scaling_factor: 2.5, which looks exactly like the knob the neighbouring DeepSeek-derived families apply. Upstream'sModelArgsdoes not declare the field at all, so it is dropped on parse andKlearSparseMoeBlockscales nothing. This port mirrors upstream rather than guessing, and prints a diagnostic naming the field at load so the choice is visible instead of invisible. Applying it would multiply every routed contribution by 2.5 against the reference.Validation
One caveat worth knowing, and it is not about this port. That exactness holds with
MLXCEL_FUSED_MOE=0. The default-on fused MoE decode kernel (issue #268) engages only at single-token decode, and it is not byte-identical here: with it enabled, a 1-token probe gives6741=7.7812 387=7.6250 3550=7.4688 ...against the reference's6741=7.5625 387=7.4062 8160=7.3125 ...(same top-1, reshuffled near-ties at a low-confidence position where the top logit is only 7.5), and greedy decode picks a different but equally coherent continuation. Disabling the kernel makes the 1-token logits match exactly too, which is what isolates it. The 5-token prefill, where the kernel does not engage, is exact either way. That kernel was validated byte-identical on qwen3-30b-a3b when it landed; this is a second data point saying that does not generalize, and it affects every MoE family in the tree rather than Klear specifically.Other guards
The sparse schedule is
layer_idx not in mlp_only_layers and num_experts > 0 and (layer_idx + 1) % decoder_sparse_step == 0, which on the published checkpoint (decoder_sparse_step: 1, emptymlp_only_layers) makes every one of the 32 layers sparse. A zero step is rejected because upstream would divide by it; anmlp_only_layersentry past the end of the stack is rejected too. Loading also rejects an architecture scalar that could size an allocation or divide (zero checks precede the divisibility check, since0.is_multiple_of(0)is true in Rust), anum_experts_per_tokoutside1..=num_experts, an odd head width, a non-positive or non-finiterope_thetaorrms_norm_eps, a non-defaultrope_scalingblock (upstream does not declare the field and so drops one silently; rejecting is the safer reading), and any tensor whose real shape disagrees with the config — including an expert stack with fewer planes thannum_expertsand a missingmlp.coefficient.attention_biasis false on the published checkpoint, so the bias path is never exercised by a real-model run; it is covered by a unit test that loads the block with biases and asserts that dropping them changes the output.Tests
22 new unit tests, all checkpoint-free.
cargo test --workspace --profile test-fast --features metal,accelerategreen;cargo clippy --workspace --all-targets -- -D warningsandcargo fmt --all -- --checkclean.