Update DeepSeek-V3.2 to use PipelineMixin - #1861
Conversation
a882678 to
fce0e1e
Compare
|
Ran into the deepseek_v32 side of this yesterday while working on #1816: it has the same silently-dropped-layers bug that PR fixed in the mixin (7 layers on 2 ranks ran layer 3 on no rank), and @michalk8 suggested exactly this subclassing approach as a follow-up in the #1816 discussion. You got there first, and this version is cleaner than what I had staged: dropping the override entirely also picks up the new split= argument for free. Verified locally on Apple Silicon (M5 Max):
This PR has no committed tests; the diff below adds the deepseek_v32 coverage and applies cleanly to this branch if you want to fold it in. regression tests (applies to this branch)diff --git a/tests/model_parallel_tests.py b/tests/model_parallel_tests.py
index 1d9bb89..af73c83 100644
--- a/tests/model_parallel_tests.py
+++ b/tests/model_parallel_tests.py
@@ -6,7 +6,7 @@ import unittest
import mlx.core as mx
import mlx_lm
-from mlx_lm.models import qwen3_moe
+from mlx_lm.models import deepseek_v32, qwen3_moe
from mlx_lm.models.pipeline import PipelineMixin
@@ -170,6 +170,29 @@ class TestModelParallel(unittest.TestCase):
"max_position_embeddings": 256,
"tie_word_embeddings": False,
},
+ {
+ "model_type": "deepseek_v32",
+ "vocab_size": 128,
+ "hidden_size": 64,
+ "index_head_dim": 16,
+ "index_n_heads": 2,
+ "index_topk": 4,
+ "intermediate_size": 128,
+ "moe_intermediate_size": 32,
+ "num_hidden_layers": 4,
+ "num_attention_heads": 4,
+ "num_key_value_heads": 4,
+ "n_shared_experts": 1,
+ "n_routed_experts": 4,
+ "kv_lora_rank": 8,
+ "q_lora_rank": 8,
+ "qk_rope_head_dim": 8,
+ "v_head_dim": 16,
+ "qk_nope_head_dim": 16,
+ "num_experts_per_tok": 2,
+ "first_k_dense_replace": 1,
+ "max_position_embeddings": 256,
+ },
]
mx.random.seed(0)
for config in test_configs:
@@ -247,15 +270,40 @@ class TestModelParallel(unittest.TestCase):
}
size = 2
- args = qwen3_moe.ModelArgs.from_dict(config)
- assigned = []
- for rank in range(size):
- model = qwen3_moe.Model(args)
- model.model.pipeline(Group(rank, size))
- assigned.extend(
- i for i, l in enumerate(model.model.layers) if l is not None
- )
- self.assertEqual(sorted(assigned), list(range(7)))
+ dsv32_config = {
+ "model_type": "deepseek_v32",
+ "vocab_size": 128,
+ "hidden_size": 64,
+ "index_head_dim": 16,
+ "index_n_heads": 2,
+ "index_topk": 4,
+ "intermediate_size": 128,
+ "moe_intermediate_size": 32,
+ "num_hidden_layers": 7,
+ "num_attention_heads": 4,
+ "num_key_value_heads": 4,
+ "n_shared_experts": 1,
+ "n_routed_experts": 4,
+ "kv_lora_rank": 8,
+ "q_lora_rank": 8,
+ "qk_rope_head_dim": 8,
+ "v_head_dim": 16,
+ "qk_nope_head_dim": 16,
+ "num_experts_per_tok": 2,
+ "first_k_dense_replace": 1,
+ "max_position_embeddings": 256,
+ }
+ for arch, arch_config in ((qwen3_moe, config), (deepseek_v32, dsv32_config)):
+ with self.subTest(model_type=arch_config["model_type"]):
+ args = arch.ModelArgs.from_dict(arch_config)
+ assigned = []
+ for rank in range(size):
+ model = arch.Model(args)
+ model.model.pipeline(Group(rank, size))
+ assigned.extend(
+ i for i, l in enumerate(model.model.layers) if l is not None
+ )
+ self.assertEqual(sorted(assigned), list(range(7)))
if __name__ == "__main__": |
|
Thanks @azamamirza for the review, feedback, testing, and regression tests! I applied your diff and confirmed that the tests pass. I'll remove the draft status. |
ml-explore#1816 already fixed PipelineMixin's own layer-dropping bug (start_idx computed from this rank's own size rather than a real prefix sum, silently omitting a layer whenever ranks have different sizes) with a cleaner API than what this branch originally proposed for that part -- dropped here in favor of it. deepseek_v32.py wasn't touched by that fix because it never used PipelineMixin at all -- it carried its own fully independent copy of the same buggy logic, unrelated to and unfixed by ml-explore#1816. Rather than patching that copy a third time, this converts DeepseekV32Model to actually inherit PipelineMixin (matching qwen3_5.py/ministral3.py), so it gets ml-explore#1816's tested fix directly instead of maintaining a parallel implementation of the same bug-prone logic. Same behavior-preserving transformation applied elsewhere: __call__ walks pipeline_layers instead of manually indexing layers[start_idx + i] over a hand-tracked num_layers, and the outer Model.layers property returns model.pipeline_layers instead of re-deriving the same slice by hand. Also added the same empty-cache guards used elsewhere in this codebase (cache and cache[-1] is not None, etc.) for a rank that ends up with zero local layers. qwen3_moe.py's sanitize() has an unrelated bug in the same problem space, not touched by ml-explore#1816: it gates MoE expert-weight-stacking on a fixed model.layers.0... key. Under pipeline sharding a rank only has its own local layers' weight files downloaded, so a rank not owning layer 0 sees that key absent and skips stacking entirely for its own local layers -- silently wrong for any raw (unconverted) checkpoint under pipeline parallelism. Fixed to detect and iterate by which layers are actually present in the local weights dict instead.
Contributed by @azamamirza in PR review: extends the existing qwen3_moe uneven-assignment regression test to also cover deepseek_v32 now that it uses the shared PipelineMixin. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
b8edaa7 to
b8b3b94
Compare
michalk8
left a comment
There was a problem hiding this comment.
Can you please split this PR into two, one for DeepSeek to use PipelineMixin (happy to merge) and the Qwen3 MoE in a separate one (I'm not sure about this one yet, if it's indeed a bug, it might be present in other models).
cache and cache[0]/cache[-1] guards in DeepseekV32Model.__call__ are unnecessary and inconsistent with qwen3_moe.py's PipelineMixin usage. test_pipeline_uneven_model's deepseek_v32 subtest duplicated coverage already provided by the qwen3_moe case now that both models delegate to the same PipelineMixin.pipeline() implementation.
Per review feedback on ml-explore#1861: this PR now only converts DeepseekV32Model to PipelineMixin. The qwen3_moe.py sanitize() gate fix moved to its own branch/PR since it's an independent bug in a different model.
This branch's old duplicate PipelineMixin-conversion commit reintroduced the cache and cache[0]/cache[-1] guards that were removed from deepseek_v32.py during ml-explore#1861's review (inconsistent with qwen3_moe.py's established PipelineMixin pattern). Match upstream/main exactly.
The issue this is addressing:
deepseek_v32.py had its own independent pipeline implementation, duplicating logic that already existed in the shared PipelineMixin base class but not benefiting from bug fixes.
The solution:
Converted DeepseekV32Model to inherit PipelineMixin like every other pipeline-capable model so it benefits from the shared, tested pipeline logic (including uneven-split support) instead of maintaining another copy of similar code.
Note: an earlier version of this PR also fixed a layer-drop bug directly in pipeline.py's core split calculation, but that was independently fixed and merged upstream in #1816 first so that part was dropped from this PR to avoid duplicating it.
I needed these fixes to get DeepSeek-V3.2-family models running correctly in pipeline mode on my 3-node cluster.