Support Qwen3.5-MoE MoE MTP heads#84
Conversation
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.13.0 to 1.14.0. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](pypa/gh-action-pypi-publish@v1.13.0...v1.14.0) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.14.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Bring the public release repo up to the current production state for quickstart, Max mode, and terminal/server UX. Max mode now uses a verified lifecycle across server startup, terminal quickstart, and one-shot CLI paths, with a crash-recovery sidecar and restore markers that are only cleared after verified fan restoration. Streaming OpenAI responses now cancel abandoned server-side generation when browser clients stop or disconnect. Terminal CLI defaults now use the model's remaining context instead of tiny response caps, expose reasoning controls, avoid misleading thinking/load labels, and print explicit model-load heartbeat status lines. The release profile remains stable, with the current staged long-response env, and quickstart/onboarding/UI helpers are included in the public package source. Includes focused regression coverage for thermal lifecycle, Max idle re-ramp, OpenAI stream cancellation, CLI defaults/reasoning, onboarding, model-load progress, and startup health metadata.
…modes table, comparison vs alternatives The previous README opened with `gh release download` and buried the actual product underneath. It also miscued the audience by framing MTPLX as "speculative decoding for Qwen3-Next" — Qwen3-Next is the verified path, not the product. MTPLX is native MTP speculative decoding on MLX / Apple Silicon; the architecture registry already detects DeepSeek V3, GLM-4, MiMo, and MiniMax M2 as backend-pending. Changes: - Open with the ASCII banner, the hero one-liner, and the real numbers (60+ tok/s cold, math-correct rejection sampling at T=0.6). - Quickstart leads with `mtplx quickstart` (the new wizard), not the raw install bash. Power-user shortcuts grouped below for completeness. - "How it actually works" makes the math wedge explicit: probability- ratio acceptance + residual correction vs greedy-argmax (DFlash) vs external-drafter speculation. Comparison table makes the difference obvious. - "What you get" enumerates the actual session work: interactive wizard, in-browser chat with auto-context-detect (256k for Qwen3.6), Anthropic-compatible /v1/messages, ThermalForge auto-install, crash- safe Max mode (the SIGKILL sidecar verified live), idle-aware fan watchdog, four-tier compatibility, lazy imports, 414 tests. - Modes section renamed to Safe / Fast / Max with the actual tok/s envelopes and what each one does to fans. Matches the wizard. - Compatibility section names the registered backends so the v0.1 scope is clear without making it feel limited. - Roadmap rewritten with concrete v0.2 / v0.3 framing instead of three flat bullets. v0.2 explicitly references the diagnostic-gated kernel ladder grounded in the six-agent synthesis. - "What MTPLX is not" section makes the boundary crisp (not DFlash, not external-drafter, not CUDA, not finished). - Verified evidence numbers cited inline (D3/192 60.169 tok/s, per-position acceptance [97.62, 95.24, 88.10, 75.61], max_diff=0.0). - Honest about the sustained no-fan gap (~37 tok/s vs 50 target), framed as v0.2 deliverable not a confession. - Attribution names Leviathan/Chen, vLLM, vllm-metal #188 and #281, DFlash-MLX, DDTree-MLX, DeepSeek V3.2 mx.depends precedent. - Mermaid diagrams trimmed to a single dataflow + a compact architecture overview that includes the new components (onboarding, in-browser UI, thermal control sidecar). Co-authored-by: Cursor <cursoragent@cursor.com>
Ship the hardened v0.1.0-preview CLI surface around the Optimized-Speed default model: structured doctor/report diagnostics, product-grade expected-error handling, HF pull/cache validation, OpenWebUI /v1 Docker integration, dynamic support checks, live TPS denominator cleanup, and percentage/capability-based fan verification with restore polling. QA: full pytest passed with 2 skipped; compileall passed; wheel build passed; installed-wheel command matrix passed 33/33 with no tracebacks; default HF repo inspects as verified/can_run=true; small HF pull/list/remove passed; local live ThermalForge ramp/restore passed. Remaining gates are hardware matrix, full clean-Mac default pull/first response, and live Docker/OpenWebUI smoke.
Invert the consumer start/quickstart command contract so start owns onboarding/chat and quickstart owns server startup. Make onboarding Medium/Max only, keep stable as an explicit hidden profile, and describe modes with mechanics plus multipliers. Honor runtime-contract draft head and draft sampler metadata, reject incomplete HF cache dirs, and allow per-request MTP depth from the web/server APIs. Validation: python -m py_compile on changed runtime modules; targeted CLI/onboarding/server/cache tests; full pytest suite green.
Add the local-folder picker to the release start flow so parent paths like ~/.lmstudio/models list candidate models instead of being accepted blindly. Make scans time-bounded and print candidate progress before compatibility classification so onboarding does not look frozen on slow model folders. Validation: py_compile for onboarding; onboarding/public CLI tests; full pytest suite green.
Treat mtplx_runtime.json as optional verification metadata for Qwen models that already contain valid native MTP tensors, while clearly rejecting config-only folders as missing MTP weights. The local folder picker now distinguishes missing MTP weights from unverified-compatible models, and start/quickstart gate failures print short human-readable diagnostics instead of dumping the full inspection JSON. QA: python -m py_compile mtplx/backends/registry.py mtplx/artifacts.py mtplx/ui/onboarding.py mtplx/commands/public.py; python -m pytest -q; git diff --check; live inspect/start against ~/.lmstudio/models/mlx-community/Qwen3.5-27B-4bit.
Repair the release-branch CLI surfaces that failed real terminal QA: restore Open WebUI helpers, report bundles, doctor flags, default pull behavior, and optimized-speed public model ids; keep start/quickstart semantics aligned with the Medium/Max onboarding contract. Make errors human by default for missing local models, invalid depth, missing MTP weights, API-key-required non-localhost binds, metrics connection failures, and no-MTP preflight downloads. Preserve JSON only when requested. Ship the advertised Nemotron-H experimental backend files and runtime injection path so architecture QA import smokes match the support catalog. Verification: full pytest suite passes locally; no-MLX public slice passes locally; python -m build succeeds; manual terminal QA covered help/version/status/doctor/report/connect/integrate/openwebui/start dry-run/depth gate/local missing-MTP/HF inspect/metrics/pull/list/remove/onboarding local-scan/custom-HF rejection.
…rdening [codex] Harden start flow and model runtime metadata
…ns/pypa/gh-action-pypi-publish-1.14.0 Bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0
…tecture The old Architecture section drew a map of the CLI package layout (cli -> onboarding -> profiles -> speculative -> ...). That described where the Python files live, not what MTPLX is. Replaced with three diagrams that describe the actual achievement: 1. Single-model runtime: target trunk + built-in MTP heads on the same checkpoint, no second drafter, committed-history KV. 2. Speculative cycle (the hot loop): draft -> batched verify -> probability- ratio acceptance with residual (p-q)+ correction -> bonus token -> commit. Capture-commit + linear-gdn-from-conv-tape + GraphBank are called out as the kernels paying for the verify pass. 3. Serving stack: clients -> FastAPI (/v1/chat/completions, /v1/messages) -> Engine sessions + Session Bank (warm-prefix exact-state reuse) -> the native-MTP runtime. Also updated this session's UX-fix bullets: - Local-folder model picker (config-only classification, walks LM Studio / HF cache / ~/models, presents 4-tier picker, never mmaps tensor files so it can't crash on dataless / partial files). - Single-line live download progress (rich.progress with bar/pct/GB/ speed/ETA; HF tqdm suppressed during download so streams don't fight). Test count refreshed 354 -> 562 (collected). The CLI surface paragraph is moved out of the architecture story since it isn't the architecture. Co-authored-by: Cursor <cursoragent@cursor.com>
…ute tok/s The old subtitle anchored on "60+ tok/s on Qwen3.6-27B" which is misleading because absolute throughput is a function of the user's memory bandwidth. Reframed around the multiplier the CLI itself reports as mean_speedup_vs_ar -- that ratio is hardware-independent. Updated numbers across the README to the verified record on the current public default Youssofal/Qwen3.6-27B-MTPLX-Optimized-Speed: - subtitle: "~2.24x over no-MTP AR at temp=0.6", with the absolute 63.056/62.886 tok/s number relegated to a "current public record on M5 Max" footnote. - "What you get" bullet: lead with the 2.24x ratio + cite both MTP and AR baseline (28.156 tok/s) so the math is auditable. - "How it actually works" verified-evidence block: refreshed from old GDN8 numbers (60.169 / 23.59 / 2.54x) to the current Optimized-Speed numbers (63.056-62.886 / 28.156 / 2.24x) plus the recorded per-position acceptance [100%, 97.96%, 93.88%]. - Modes table: Medium / Max wording rewritten in multiplier terms rather than absolute speed lanes. - Roadmap, "Preview honesty", "What MTPLX is not": migrated from "60 tok/s class" to "~2.24x multiplier lane". Greedy reference (60.108 tok/s) preserved as a separate diagnostic anchor since it isn't the multiplier claim and matches the contract. Co-authored-by: Cursor <cursoragent@cursor.com>
The architecture section listed kernel names ("capture_commit",
"linear-gdn-from-conv-tape", "GraphBank") in the speculative-cycle
diagram but never explained that they sit on top of an actual MLX
source fork plus our own primitive-registered Metal kernels. Without
that layer the multiplier on the same hardware drops by enough that
MTP can lose to AR. So it deserves its own architecture box.
New §0 covers:
- MLX source fork (mlx-mtplx-0.31.2-qmm @ 2377a99f): small-M qmv
retuned for verify shapes M=3..6 with BN16 / 4-simdgroup /
unroll_count(4), and source-primitive registration so mx.compile
can fuse across our custom kernels.
- Custom Metal kernels: linear-gdn-from-conv-tape (innovation tape
during draft + deterministic replay on rollback, max_diff=0.0),
verify_qmv (diagnostic small-M qmv), GraphBank (mx.compile per
verify shape, capture_commit_time ≈ 0.073 ms / cycle vs
verify_time ≈ 47 ms / cycle), and the in-memory draft-only 4/3-bit
LM head that cuts draft time ~29% without touching target
precision.
- Default performance-cold runtime knobs (the five MTPLX_* env vars).
- Numerical hygiene fixes: fp32 p/q ratio (BF16 underflows at small
q) and mx.random.split per draft position (without this, depth>1
would silently correlate accept decisions).
Updated the section preamble from "three layers" to "four layers" so
the §0 / §1 / §2 / §3 enumeration adds up.
Co-authored-by: Cursor <cursoragent@cursor.com>
Merge the release-ready slice from the active MTPLX work into production: contract-driven draft-depth defaults for start/serve/bench, browser depth slider defaults from the running server, live single-line Hugging Face download progress with Hub tqdm suppression, tied QuantizedEmbedding draft LM-head support for Qwen3.5-4B-style tied heads, and base dependencies needed for pip-installed mtplx serve. Keep the release checkout clean by excluding research-only model artifacts, builder probes, kernel experiments, local defaults, and raw benchmark output. Verified with: python -m pytest -q; python -m build; python -m twine check dist/*; scripts/fresh_venv_smoke.sh; git diff --check.
User-facing Max sessions now wait for actual fan RPM to ramp before entering chat or server work. ThermalForge target RPM is still accepted quickly for install-time verification, but start and serve Max mode no longer treat target-only state as production-ready fan boost. This also separates actual, target, and capacity RPM in fan summaries so the CLI cannot report misleading lines like target 0 RPM while the daemon has actually commanded max. QA:\n- uv run --extra dev python -m pytest\n- uv run --extra dev python -m build\n- scripts/fresh_venv_smoke.sh\n- uv run --extra dev python -m mtplx.cli inspect Youssofal/Qwen3.6-27B-MTPLX-Optimized-Speed --json\n- uv run --extra dev python -m mtplx.cli start cli --max --model Youssofal/Qwen3.6-27B-MTPLX-Optimized-Speed --prompt 'Reply with exactly OK.' --max-tokens 4 --yes\n- uv run --extra dev python -m mtplx.cli max --status --json
Swaps the README hero (ASCII MTPLX banner) and the five mermaid diagrams for inline SVGs styled to match the website aesthetic: - hero.svg replaces the ASCII art banner - flow.svg replaces the high-level "How it works" flowchart - mlx-runtime.svg replaces the MLX runtime layer diagram - single-model.svg replaces the target-model trunk + MTP head diagram - cycle.svg replaces the speculative cycle diagram - serving.svg replaces the serving stack diagram Also adds speed-chart.svg as an unused asset for future use.
Add coverage for consecutive Qwen XML tool calls, uneven chunk boundaries, split markers, and OpenCode-style long write arguments so future parser changes cannot leak raw tool markup or reorder tool-call deltas. Co-authored-by: Daniel Farina <m3d.webdev@gmail.com>
Publish a prompt-prefix anchor before scheduling async retokenized postcommit for unsafe streamed non-tool responses, matching the existing tool-call path. This prevents OpenCode resumed turns from cold-prefilling the full history when the generated stop boundary does not match the canonical chat-template history boundary.
Release v0.3.6
Port OMLX-style Anthropic tool bridging and generated tool parsing hardening into the public server path. Add Claude Code connection env fixes, optimized-speed default/model-alias updates, sparse sampler fallback, and regression coverage for streaming tool calls, malformed tool repair, schema validation, and broader Claude Code tools.
Fix the start dry-run OpenWebUI handoff so accepting the verified default keeps the resolved Optimized Speed model in args.model instead of emitting --model None. Record the release-readiness evidence for issues youssofal#67, youssofal#68, youssofal#70, and youssofal#71, including fresh package checks, real tune results, bad-model failure reporting, and Claude Code headless plus interactive tool execution.
Bump package metadata to 0.3.7, move the release notes under v0.3.7, update the README status copy, and record the local release artifact checks.
The native Qwen MTP path assumed a dense single-MLP MTP head: a fixed
15-tensor gate and no expert stacking. Qwen3.5-MoE checkpoints whose MTP
head is itself an MoE block (router gate + per-expert MLPs + a shared
expert / shared_expert_gate) were therefore rejected with
`invalid-mtp-tensor-layout`, even though the runtime can already build
the block -- mlx-lm's qwen3_5 DecoderLayer instantiates SparseMoeBlock
whenever num_experts > 0.
- artifacts: derive the expected MTP key set from num_experts for MoE
heads instead of the hard-coded dense 15, so the tensor gate passes
(no-op for dense heads).
- mtp_patch: stack per-expert mtp.layers.*.mlp.experts.{i}.* weights into
the switch_mlp layout SwitchGLU expects, mirroring the stacking mlx-lm
performs for the main decoder layers (no-op for dense heads).
- tests for the MoE tensor gate and the expert stacking.
Verified on Qwopus3.5-122B-A10B (qwen3_5_moe, 256 experts, bf16 MTP
sidecar grafted onto a 4-bit base): `mtplx inspect` passes (785/785
tensors) and MTP speculative decoding runs with ~70% depth-1 acceptance
(~2.1 tokens per target verify pass).
|
Closing this one out with apologies for the long silence — and with a correct diagnosis on your part. The MoE-MTP-head gap you identified (router gate + per-expert MLPs + shared_expert/shared_expert_gate in the head itself, rejected at the tensor gate) was closed in-house the same week this PR was opened: artifacts.py derives the expected MoE key set from num_experts, and the loader stacks numbered experts into the switch_mlp layout — functionally the same two changes you proposed. That support has shipped in every release since v1.0.4. This branch has since drifted a long way behind main (the diff now spans 277 files of old history), so rather than ask for a rebase of equivalent functionality I'm closing it. If your Qwen3.5-MoE checkpoints hit any gap on current v2.0.2 — inspect gate, expert stacking, or the MoE-router near-tie exactness behavior you documented — please open an issue with the artifact details; that report would be genuinely valuable. Thank you for the careful work and the acceptance-rate receipts. |
Summary
Adds support for Qwen3.5-MoE MTP heads to the native Qwen MTP path.
The MTP head on Qwen3.5-MoE checkpoints (e.g.
Qwen/Qwen3.5-122B-A10B) is itself an MoE block — routergate+ per-expert MLPs + ashared_expertandshared_expert_gate— whereas the existing path assumed a dense single-MLP head. As a result these models were recognized asqwen3-next-mtpbut rejected at the tensor gate withinvalid-mtp-tensor-layout.No new backend is required: mlx-lm's
qwen3_5DecoderLayeralready instantiatesSparseMoeBlockwhennum_experts > 0, so the MTP block builds correctly once the weights are stacked. The change is two small, dense-safe additions:artifacts.py— for MoE heads, derive the expected MTP key set fromnum_experts(mtp.layers.*.mlp.{gate, experts.{i}.*, shared_expert.*, shared_expert_gate}) instead of the fixed dense 15, soinspect's tensor gate passes. No-op for dense heads.mtp_patch.py—_stack_mtp_moe_expertsstacks per-expertmtp.layers.*.mlp.experts.{i}.{proj}tensors into theswitch_mlplayoutSwitchGLUexpects, mirroring the stacking mlx-lm performs for the main decoder layers. No-op for dense heads.Verification
Tested on Qwopus3.5-122B-A10B (
qwen3_5_moe, 256 experts / 8 active), bf16 MTP sidecar grafted onto a 4-bit base:mtplx inspect→can_run: true, tensor gate 785/785, 0 missing / 0 extra.accepted_by_depth = [40, 19, 3]of[57, 57, 56]drafted → ~70% depth-1 acceptance, 120 tokens in 57 verify passes (~2.1 tokens/verify). A mis-loaded MoE head would accept ~0%.pytest tests/test_artifacts.py tests/test_mtp_patch.pygreen.Known limitation — MoE exactness
At temperature 0, MTP vs non-MTP greedy decode is ~98% identical and re-converges immediately, but occasionally flips a single token. This is the MoE router hitting a near-tie that resolves differently under batched verification vs single-token autoregressive decode (a known MoE/FP effect), not a drafting error. The
max_diff = 0.0exactness guarantee was established on a dense model; strict bit-exactness for MoE heads likely needs separate handling (e.g. fp32 router logits during verify). Flagging for discussion — happy to follow up.🤖 Generated with Claude Code