Skip to content

feat(model): E2 DeepSeek family split + config fields (#109)#124

Merged
lgsunnyvale merged 2 commits into
mainfrom
e2-family-split-correction-decision-11-deepseek-config-fields
Jul 20, 2026
Merged

feat(model): E2 DeepSeek family split + config fields (#109)#124
lgsunnyvale merged 2 commits into
mainfrom
e2-family-split-correction-decision-11-deepseek-config-fields

Conversation

@tlkahn

@tlkahn tlkahn commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

Implements #109 (E2 family split correction / decision 11 + DeepSeek config fields). Parent: #105.

Corrects A1 interim debt that mapped every DeepSeek HF model_type onto MODEL_DEEPSEEK_V4, and lands the model_config_t surface later E2 work needs. Generate is not enabled for any DeepSeek family.

Decision 11 mapping

HF model_type model_family_t
deepseek_v3 MODEL_DEEPSEEK_V3
deepseek_v3_2 MODEL_DEEPSEEK_V32
deepseek_v32 MODEL_DEEPSEEK_V32
deepseek_v4 MODEL_DEEPSEEK_V4
other deepseek* MODEL_FAMILY_UNKNOWN (no prefix match)

HF audit snapshot (raw config.json)

repo model_type notes for #109
mlx-community/DeepSeek-V3-4bit deepseek_v3 full MLA+MoE; yarn rope_scaling with mscale_all_dim=1.0; no indexer
mlx-community/DeepSeek-V3.2-4bit deepseek_v32 V3 MoE/MLA + index_head_dim=128, index_n_heads=64, index_topk=2048
mlx-community/DeepSeek-V4-Flash-4bit deepseek_v4 diverges: has q_lora_rank, qk_rope_head_dim, indexer, MoE counts; missing kv_lora_rank / qk_nope_head_dim / v_head_dim / n_group / topk_group / moe_layer_freq / first_k_dense_replace. V4-only keys deferred to #115.

Sample values:

DeepSeek-V3-4bit:
  q_lora_rank=1536 kv_lora_rank=512 qk_rope_head_dim=64 qk_nope_head_dim=128 v_head_dim=128
  n_routed_experts=256 n_shared_experts=1 routed_scaling_factor=2.5
  moe_layer_freq=1 first_k_dense_replace=3 n_group=8 topk_group=4
  moe_intermediate_size=2048 num_experts_per_tok=8 norm_topk_prob=True
  rope_scaling={type:yarn factor:40 mscale_all_dim:1.0 ...}

DeepSeek-V3.2-4bit: same as V3 + index_head_dim=128 index_n_heads=64 index_topk=2048

DeepSeek-V4-Flash-4bit:
  q_lora_rank=1024 qk_rope_head_dim=64 n_routed_experts=256 n_shared_experts=1
  routed_scaling_factor=1.5 moe_intermediate_size=2048 num_experts_per_tok=6
  index_head_dim=128 index_n_heads=64 index_topk=512
  rope_scaling={type:yarn factor:16} (no mscale_all_dim)
  kv_lora_rank / qk_nope_head_dim / v_head_dim / n_group / topk_group / moe_layer_freq / first_k_dense_replace = absent

New model_config_t fields

  • MLA: q_lora_rank, kv_lora_rank, qk_rope_head_dim, qk_nope_head_dim, v_head_dim
  • MoE extras: n_routed_experts, n_shared_experts, routed_scaling_factor (default 1.0), moe_layer_freq, first_k_dense_replace, n_group, topk_group, norm_topk_prob (default true)
  • RoPE: rope_scaling_mscale_all_dim (0 = absent)
  • Indexer: index_head_dim, index_n_heads, index_topk
  • Alias: if n_routed_experts > 0 and num_experts == 0, set num_experts = n_routed_experts (DeepSeek HF omits num_experts; do not clobber explicit value)

Reject path

weights_load rejects all three families with honest labels:

deepseek_v3 MLA/MoE forward not yet implemented (Stage E2)
deepseek_v32 MLA/MoE forward not yet implemented (Stage E2)
deepseek_v4 MLA/MoE forward not yet implemented (Stage E2)

Clears stale test debt that still expected a "GGUF" substring while production already said Stage-E MLA.

Explicit non-goals (this PR)

CLAUDE.md supported-families text already matched decision 11 - verified, no edit.

Test plan

  • make test (44 passed)
  • ./tests/test_weights_gpu (DeepSeek three-family reject + existing suite)
  • Fixtures: V3 / V32 / V3_2 alias / defaults / num_experts non-clobber / V4 intersection
  • Gate still rejects V3+V32+V4 as unsupported family
  • No GGUF string on DeepSeek reject path

Closes #109

Correct A1 interim debt that collapsed every DeepSeek HF model_type onto
MODEL_DEEPSEEK_V4. Land the model_config_t surface E2 forward/sanitize
needs, without enabling generate.

- model_family_t: MODEL_DEEPSEEK_V3, MODEL_DEEPSEEK_V32, MODEL_DEEPSEEK_V4
- model_family_from_type: deepseek_v3 / deepseek_v3_2+deepseek_v32 / deepseek_v4
- Parse MLA, MoE extras, rope mscale_all_dim, V3.2 indexer, norm_topk_prob
- n_routed_experts aliases into num_experts only when num_experts == 0
- weights_load rejects all three with honest family labels (no GGUF)
- Gate still rejects all DeepSeek families as unsupported
@tlkahn
tlkahn requested a review from lgsunnyvale as a code owner July 20, 2026 09:56
@lgsunnyvale

Copy link
Copy Markdown
Collaborator

Code review: feat(model): E2 DeepSeek family split + config fields (#124)

Thorough review of the full PR against #109, decision 11, and doc/plans/impl-109-e2-family-split-correction-decision-11-deepseek-config-fields.md. Touched surface: model.h / model.c / weights.c plus fixtures and the four test sites.

Summary

This is a clean, plan-faithful landing of the E2 prerequisite surface. Enum grain, mapping, parse fields, reject honesty, and test coverage all match decision 11 / R1-R10. No generate enablement, no sanitize/forward creep. I ran ./tests/test_model_config on the branch - green.

No blockers. A few lows/nits below; none need to gate merge unless you want the docs/test tighten-ups in-tree.

Positive observations

  • Decision 11 mapping is exact-string, not prefix. deepseek_v3 / deepseek_v3_2+deepseek_v32 / deepseek_v4 split correctly; deepseek and deepseek_v5 stay MODEL_FAMILY_UNKNOWN. Contiguous enum insert before MODEL_DEEPSEEK_V4 matches R1 (numeric shift of V4/BERT accepted).
  • Reject path is honest per family. The ternary label in weights_load never hardcodes v4 for a v3/v32 cfg. Test helper correctly uses strncmp(err, "<label> MLA", ...) so deepseek_v3 cannot false-positive on deepseek_v32 (strstr would). GGUF debt cleared.
  • R4 alias is one-way and tested both directions. n_routed_experts fills num_experts only when num_experts == 0; the both-keys fixture proves non-clobber. Existing MoE detectors keep working without DeepSeek special cases.
  • R5 norm_topk_prob included with mlx-lm default true, so E2: Dense/MoE layer mixing + DeepSeek V3 forward wiring #113 can wire fwd_moe_route_deepseek without another config PR.
  • V4 fixture is intersection-only and honest about absences. kv_lora_rank / qk_nope_head_dim / v_head_dim / n_group / topk_group / moe_layer_freq / first_k_dense_replace / mscale_all_dim stay 0 - good characterization breadcrumb for E2: V4 characterization + forward (DeepSeek-V4-Flash-4bit) #115, no invented Flash fields.
  • Exhaustive "other family" lists updated in both test_emodel_gate.c and test_weights.c. Gate still default-rejects all three; no engine_model_check_supported arm added (R9).
  • Defaults arm shares llama-style + SiLU across V3/V32/V4 without forcing mlx-lm MLA numeric defaults onto absent keys (R7). Distinguishable unset stays 0 for ranks/indexer.
  • n_group vs mamba_n_groups collision called out in the struct comment; JSON keys differ (n_group vs n_groups) - no parse crosstalk.
  • Scope control is tight. No family_descs / expected-names, no forward, no V4-only keys, CLAUDE.md already matched - correctly left untouched.
  • PR body carries the HF audit snapshot the plan asked for. Useful permanent record.

Issues found

1. (Low) 0 = absent contract is under-documented for MoE ints that diverge from mlx-lm defaults

rope_scaling_mscale_all_dim correctly documents /* 0 = absent */ (R3). The MoE ints do not, even though their zero-default diverges from mlx-lm ModelArgs:

field mlxd absent mlx-lm default
n_group 0 1
topk_group 0 1
moe_layer_freq 0 1
first_k_dense_replace 0 0 (ok)

R7 intentionally leaves these at 0 so unset is distinguishable - good for parse honesty. But #113 / fwd_moe_route_deepseek reject n_group < 1. A caller that treats the parsed struct as "ready to route" will fail closed on any config that omitted the keys (minimal fixtures, partial exports).

Suggestion: mirror the mscale comment on the MoE block, e.g.

int n_group; /* 0 = absent; mlx-lm default 1. NOT mamba_n_groups */
int topk_group; /* 0 = absent; mlx-lm default 1 */
int moe_layer_freq; /* 0 = absent; mlx-lm default 1 */

No behavior change required in this PR.

2. (Low) DeepSeek-oriented defaults are applied globally

/* DeepSeek mlx-lm defaults (honored when keys absent; explicit values win). */
cfg->routed_scaling_factor = 1.0f;
cfg->norm_topk_prob       = true;

Per plan cycle 5c this lives in the universal non-zero-defaults block, so every family load (llama, qwen3, bert, ...) now reports norm_topk_prob == true and routed_scaling_factor == 1.0f even when the checkpoint has no MoE surface.

Functionally harmless today (nothing reads these outside future DeepSeek wiring), but it weakens "memset/load dump" as a signal of what the config actually is, and the zero-init test has to special-case "pre-default vs post-load".

Optional tighten (non-blocking): move the two assignments into the DeepSeek apply_family_defaults arm so non-DeepSeek configs keep zero/false. If you keep global defaults, the comment is fine - just know dumps will lie a little.

3. (Low) No coverage that explicit norm_topk_prob: false wins

Tests cover:

  • default-when-absent -> true (model_config_deepseek_v3_defaults)
  • explicit true in V3/V32 fixtures

Missing: a fixture or inline case with "norm_topk_prob": false asserting the bool stays false. get_bool should honor it, but this is the one default that is not zero and is easy to regress (e.g. someone "helpfully" forces true for DeepSeek). Cheap add if you touch the file again.

4. (Nit) test_deepseek_config_fields_zero_init is now tautological

memset(&cfg, 0, sizeof(cfg));
assert(cfg.q_lora_rank == 0);
/* ... */

Useful as the Cycle 5a RED compile probe for missing struct members; once the fields exist it only re-proves memset. Consider dropping it, or replacing with a post-load "non-DeepSeek family leaves DeepSeek fields at defaults/zero" assertion (which would also lock in finding 2's chosen policy).

5. (Nit) DeepSeek weights_load reject lives only in the GPU test binary

The early family reject runs before shard I/O / mlx graph work:

if (cfg->family == MODEL_DEEPSEEK_V3 ||
    cfg->family == MODEL_DEEPSEEK_V32 ||
    cfg->family == MODEL_DEEPSEEK_V4) {
    /* ... snprintf ... */
    return -1;
}

Coverage is only in test_weights_gpu.c. Plan cycle 4 called this out as optional either-way. A CPU twin in test_weights.c (if link graph allows) would keep the decision-11 reject contract on make test without Metal. Not worth restructuring the Makefile for; flagging only because the GGUF debt lived in the GPU file and bit-rotted once already.

6. (Info) User-facing load still surfaces the generic gate string

engine_model_load order is config -> engine_model_check_supported -> weights_load. DeepSeek hits the gate default::

unsupported model family (only qwen3/llama/gemma4/qwen3_5/mistral dense supported)

The honest "deepseek_v3 MLA/MoE forward not yet implemented (Stage E2)" is only visible on direct weights_load (tests / future callers). This matches R9 and is correct for this PR. When #113 (or whichever issue enables generate) opens a gate arm, prefer carrying the family-named Stage-E2 message up to the gate so mlxd run / HTTP errors stay as honest as the weight path.

Spec / exit checklist cross-check

Exit item Status
V3 / V32 / V4 contiguous in enum yes
Mapping + unknown deepseek* yes + negative tests
model_config_load family for all three + deepseek_v3_2 alias yes
Reject: three labels, no GGUF, no cross-family mislabel yes (strncmp prefix)
MLA / MoE / mscale / indexer parse yes
n_routed_experts alias only when num_experts == 0 yes
norm_topk_prob default true yes
Gate still rejects all DeepSeek yes
No forward/sanitize/enablement creep yes
CLAUDE.md verified yes (no edit)

Verdict

Approve with optional follow-ups on (1) absent-contract comments and (3) explicit norm_topk_prob: false test. Nothing here should block stacking #110/#111/#112 on top of this surface.

Nice work clearing the A1 interim collapse cleanly - this is the right grain for the rest of E2.

…s, CPU reject

Scope norm_topk_prob / routed_scaling_factor mlx-lm defaults to the DeepSeek
apply_family_defaults arm (absent-or-null only) so non-DeepSeek loads keep
those fields at unset. Document 0=absent on MoE ints. Lock explicit
norm_topk_prob:false and non-DeepSeek unset via tests; cover Stage-E2
weights_load reject on CPU make test.

Plan: doc/plans/fix-124-5020997508.md
Review: #124 (comment)
@tlkahn

tlkahn commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the thorough optional follow-ups - landed in cde9d58. Plan trail: doc/plans/fix-124-5020997508.md.

Disposition

# Claim Action
1 0 = absent under-documented on MoE ints Fixed - header comments now mirror the mscale style and call out mlx-lm default divergence + R7 intent
2 DeepSeek-oriented defaults applied globally Fixed - norm_topk_prob / routed_scaling_factor moved into the DeepSeek apply_family_defaults arm; applied only when key absent or JSON null (same shape as qwen3_5 attn_output_gate)
3 No coverage that explicit norm_topk_prob: false wins Fixed - fixture + test_deepseek_norm_topk_prob_explicit_false
4 zero_init test tautological Fixed - replaced with test_deepseek_fields_unset_on_non_deepseek (llama load leaves DeepSeek fields at 0/false); also locks #2
5 DeepSeek weights_load reject only in GPU binary Fixed - CPU twin in tests/test_weights.c so make test owns the decision-11 reject contract; GPU test kept
6 User-facing load still surfaces generic gate string Parked (R9) - intentional; engine_model_check_supported still has no DeepSeek arm. Promoting the family-named Stage-E2 string belongs to the issue that opens a generate gate arm (#113 or later), not this PR

Notes on scope choices

  • Did not default n_group / topk_group / moe_layer_freq to mlx-lm's 1 - that would fight R7 and the V4 Flash "absent stays 0" characterization. Docs only for those ints; E2: Dense/MoE layer mixing + DeepSeek V3 forward wiring #113 must not treat 0 as a ready-to-route group count.
  • Did not touch sanitize/forward/weights_expected_names/gate enablement.
  • Existing DeepSeek absent-key defaults (true / 1.0f) and V3/V32/V4 field fixtures unchanged.

Verification

  • make test - 44 passed
  • ./tests/test_weights_gpu - green (reject twin still there)
  • ASan/UBSan clean on test_model_config / test_weights

@lgsunnyvale
lgsunnyvale merged commit 2114dee into main Jul 20, 2026
1 check passed
@lgsunnyvale
lgsunnyvale deleted the e2-family-split-correction-decision-11-deepseek-config-fields branch July 20, 2026 10:21
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.

E2: Family split correction (decision 11) + DeepSeek config fields

2 participants