Skip to content

feat: DeepSeek V3.2 pure-Swift port — zero-fork overlay (parity 1e-4)#52

Merged
magicnight merged 18 commits into
mainfrom
feat/deepseek-v32-overlay
Jul 8, 2026
Merged

feat: DeepSeek V3.2 pure-Swift port — zero-fork overlay (parity 1e-4)#52
magicnight merged 18 commits into
mainfrom
feat/deepseek-v32-overlay

Conversation

@magicnight

Copy link
Copy Markdown
Owner

Completes the pure-Swift DeepSeek V3.2 architecture port started in S1: all 12 components green, every stage independently parity-gated at 1e-4 against the Python mlx-lm reference (offline fixture capture → Swift allClose under xcodebuild).

What lands

  • DSA sparse attention (lightning indexer, dense/sparse prefill + decode w/ two-sub-cache CacheList), absorbed MLA, MoE (noaux_tc routing with all five V3.2-vs-V3 router divergences honored), MoE-vs-dense layer predicate, full DeepseekV32Model (LLMModel/LoRAModel, newCache override), sanitize (MTP drop, fp8-scale drop, expert stacking, kv_b_projembed_q/unembed_out split).
  • Zero-fork registration: ModelOverlay.registerAll() registers model_type: deepseek_v32 into the stock LLMTypeRegistry.shared from outside the package — same cross-module pattern Apple ships in MLXVLM. When upstream mlx-swift-lm lands DeepSeek V3.2, we delete our implementation and the registration; the overlay stays a thin, shrinking layer.

Verification

Full-model parity on a 2-layer dense+MoE fixture (exercises the layer predicate end-to-end), sanitize round-trip (Metal-free), 3 registration tests, per-component parity suites (indexer / attention prefill+sparse+decode / decoder layer / MoE). Suite: 0 failures under xcodebuild -skipPackagePluginValidation; adversarially code-reviewed (APPROVE — numerics faithful line-by-line, incl. mask sourcing from cache[0][0] and the kv_b_proj split axes).

Real-checkpoint smoke deferred (needs a downloaded model; the synthetic full-model parity gate stands in until then).

Also stops tracking docs/superpowers/plans/ on this branch (main already removed + gitignored them), avoiding a modify/delete conflict.

magicnight added 17 commits July 6, 2026 20:04
First macMLX-owned model architecture (overlay, zero-fork). DeepSeek
V3.2 is the foundation for a later V4 port; its DSA sparse attention
(lightning indexer) is the shared novel piece.

This session (S1) lands the leaf components:
- DeepseekV32Configuration — Codable, matches config.json / the Python
  ModelArgs schema. 2 decode tests (real config + defaults), pass under
  swift test.
- DeepseekMultiLinear — batched per-head linear, copied from
  mlx-swift-lm's internal MultiLinear (not public) for the absorbed-MLA
  embed_q/unembed_out projections.
- DeepseekV32Indexer — the DSA lightning indexer: scores keys, returns
  top-index_topk indices (argPartition) or nil when the context fits in
  top-k. Prefill path (cache==nil); KV-cache decode integration lands
  with the attention module (S2).

Translated from docs/reference/deepseek_v32_mlx_lm_reference.py.
Feasibility audit + component checklist + parity strategy in
docs/superpowers/plans/2026-05-11-deepseek-v32-port.md.

Compiles clean (swift build). Indexer structural tests (shape,
short-circuit) are Metal-gated; numerical parity vs the Python
reference is the S1.5 follow-up (needs offline reference-value
capture). Not yet registered in ModelOverlay — registration flips on
after the full model passes parity.
Discovered the MLX-gated tests in this repo (PromptCache, LoRA-MLX,
now DeepSeek) have NEVER actually run — the old requireMetalOrSkip
checked for a bundled default.metallib, which is absent under both
`swift test` (correct: MLX fatalErrors there) AND xcodebuild (wrong:
MLX runs fine there via the Metal Toolchain). So everything skipped
everywhere; MLX numerical correctness rested on compile-check + manual
smoke only.

New shared XCTestCase.requireMLXRuntimeOrSkip() discriminates on the
test-bundle path: SPM's `swift test` bundle lives under `.build/`
(skip — no metallib, MLX aborts); xcodebuild's lives under DerivedData
(run — Metal Toolchain JITs the shaders).

DeepseekV32Indexer tests now genuinely EXECUTE under
`xcodebuild test -scheme MacMLXCore -skipPackagePluginValidation`:
both the sparse (argPartition top-k) and short-circuit paths pass on
Metal. This gives the DeepSeek port a real numerical-parity harness
for S2 (attention) + S3 (MoE), instead of blind compile-only checks.

Prereq: Metal Toolchain component (xcodebuild -downloadComponent
MetalToolchain), installed locally.
The Swift DeepseekV32Indexer now selects the EXACT same top-k key
indices as mlx-lm 0.31.3's reference Indexer given identical weights +
inputs. The DSA lightning-indexer math (partial RoPE, score compute,
weights_proj weighting, argPartition top-k) is numerically verified,
not just compile-checked.

Harness (reusable for S2 attention + S3 MoE):
- docs/reference/capture_indexer.py — offline uv-venv capture from the
  Python reference (Python never enters macMLX). Fixed deterministic
  weights + inputs → runs reference Indexer → saves weights + inputs +
  expected sorted top-k to a safetensors fixture.
- Tests/MacMLXCoreTests/Fixtures/indexer_parity_fixture.safetensors —
  the captured fixture (test resource; Package.swift copies it).
- DeepseekV32IndexerParityTests — loads the fixture, sets the same
  weights on the Swift indexer, runs it, compares sorted top-k sets.
  Passes under xcodebuild (Metal); skips under swift test via the
  runtime-aware gate.

Also fixes the indexer RoPE: mlx-lm 0.31.3 hardcodes traditional=true
(a later upstream commit makes it configurable, default false — a
behavior flip to resolve against real weights in S4). Match 0.31.3 so
the fixture is valid.

Includes the oMLX-parity roadmap doc (2026-05-11) for the broader plan.
DeepseekMultiLinear gains the transpose:Bool=true argument mirroring
mlx-lm's mla.MultiLinear.__call__(x, transpose=True). The absorbed-MLA
attention needs both directions: embed_q forward (transpose=true,
inputDims→outputDims) and the prefill embed_q/unembed_out reverse
(transpose=false, outputDims→inputDims). Compiles clean.
Mark components 1-3 done (Configuration, MultiLinear, Indexer — the
last numerically verified vs Python). Record the S2 kickoff recipe so
the next focused session starts fast: the absorbed-MLA two-branch
forward (decode L==1 / prefill L>1), the CacheList double sub-cache,
the parity-capture plan (both branches, uv venv, transformers<5.13),
the weight key names, and the gotchas already learned.
Bring the branch base current before the rope-interleave hotfix:
main gained MCPClientPool (PR #45) since this branch forked.
Stock mlx-lm 0.31.3 hardcodes traditional=True for the DSA indexer
rope — a GLM5-era regression upstream reverted in PR #1431 (merged
2026-06-24, unreleased) because it silently degrades long-sequence
quality. Correct value: config indexer_rope_interleave, default false.

- Indexer rope: traditional: true → config.indexerRopeInterleave
  (the Configuration field already existed and decoded correctly)
- Parity fixture regenerated against 0.31.3 + the #1431 one-line
  patch; top-k selection differs from the old fixture (the mode is
  numerically significant), and the parity test re-passes under
  xcodebuild against the corrected pair
- Provenance headers added: the checked-in reference .py is a
  post-#1431 main snapshot, NOT PyPI 0.31.3 — the mismatch between
  those two is what made this bug hard to see in review
- Port plan updated (component 3 notes + S2 gotchas)
LoRAAdapterConverterMLXTests (6 tests) and PromptCacheStoreTests
(4 MLX-gated tests) still used the old metallib-probing helper, which
skips under BOTH swift test and xcodebuild — those 10 tests had never
actually executed anywhere. Migrated to MLXTestSupport's
requireMLXRuntimeOrSkip; first real run: 10/10 pass.
CI previously ran only swift test (where every MLX-gated test
self-skips) and an app build (no tests) — the numerical-parity
fixtures protected nothing in CI. New 'metal' job runs the full
MacMLXCore suite under xcodebuild with the Metal Toolchain installed.
Known first-run risk: if the runner VM exposes no Metal device the
job fails — then gate on a device probe rather than deleting the job.
Pool landed in PR #45 with two dead-server robustness fixes; remaining
C2 tail (server-everything integration test, zombie reaping, stderr
reader off the cooperative pool, connectAll signature) rolls into the
hardening pass before C3.
DeepseekV32Attention: absorbed Multi-head Latent Attention with the DSA
indexer's sparse mask folded in. Keeps K/V in the kv_lora_rank latent
space (embed_q / unembed_out per-head projections) and feeds the RoPE
contribution (pe_scores) to SDPA as an additive mask, so the full score
is scale*(qNope*kT) + pe_scores in one softmax. All three branches
implemented (prefill dense, sparse-prefill scatter, decode gather) —
prefill is parity-verified now; sparse (S2.2) and decode+cache (S2.3)
get their own fixtures next.

S2.1 parity: attn_prefill_fixture (s=3<=index_topk=4 so the indexer
short-circuits, isolating the absorbed-MLA math) matches the Python
reference to atol/rtol 1e-4 under xcodebuild. Main-attention rope is
traditional=true (distinct from the indexer's interleave=false).
Verifies the absorbed-MLA prefill path WITH sparsification: s=8 >
index_topk=4 so the indexer returns a real top-k selection, driving
the put_along_axis sparse-mask scatter branch (AND with the causal
mask) that S2.1's short-circuit skipped. put_along_axis's scalar-True
broadcast works as in Python. Matches the reference to atol/rtol 1e-4
under xcodebuild.

The parity test grew a shared assertAttentionParity(fixture:) helper;
prefill (S2.1) and sparse-prefill (S2.2) both call it.
Extends DeepseekV32Indexer for incremental decode: rope offset comes
from the cache, keys accumulate via cache.update (values an empty
head-dim-0 placeholder, mirroring Python's
cache.update_and_fetch(k, zeros([b,1,s,0]))), and the short-circuit
tests the total (cached) key length. Attention threads cache[1] to the
indexer. The cache param defaults to nil so the prefill paths (S2.1 /
S2.2) stay byte-for-byte equivalent.

Compiles clean; full suite green under xcodebuild (47 tests, attention
prefill + sparse + indexer parity all pass). Remaining for S2.3:
primed-cache decode fixture + a CacheList two-stage test for the L==1
take_along_axis gather branch (the decode path is written but not yet
numerically verified). Plan milestones updated.
…itize, factory registration

Completes the pure-Swift DeepSeek V3.2 port (components #1-12 all green,
every stage parity-gated at 1e-4 against the Python mlx-lm reference):

- S2.4: dense DeepseekV32DecoderLayer (attn + SwiGLU MLP + rmsNormEps
  norms + residuals), DeepseekV32MLP, deepseekV32NewCache/KVHeads helpers
- S3: DeepseekV32MoEGate (noaux_tc routing with all five V3.2-vs-V3
  divergences honored: bias-free gather from origScores, unconditional
  routedScalingFactor, nGroup==1 skip, no epsilon, float32 sigmoid) +
  DeepseekV32MoE (SwitchGLU routed experts + optional shared experts)
- S4: MoE-vs-dense layer predicate (firstKDenseReplace/moeLayerFreq),
  DeepseekV32ModelInner/DeepseekV32Model (LLMModel + LoRAModel, newCache
  override → per-layer CacheList(main MLA, DSA indexer)), sanitize (drop
  MTP layers, drop fp8 scale keys, stack per-expert w1/w2/w3 into
  switch_mlp, split absorbed-MLA kv_b_proj → embed_q/unembed_out),
  ModelOverlay.registerAll() wires model_type "deepseek_v32" into
  LLMTypeRegistry.shared — zero fork of mlx-swift-lm

Tests: full-model parity (2-layer dense+MoE fixture exercises the
predicate), sanitize round-trip (Metal-free), 3 registration tests,
plus per-component parity suites; capture scripts in docs/reference/.
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

…rlay

# Conflicts:
#	docs/deepseek-v32-swift-port.md
@magicnight magicnight merged commit d285e73 into main Jul 8, 2026
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

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