perf: grouped MoE path and runtime defaults for Apple Silicon (MPS) - #36
Open
kkilchrist wants to merge 2 commits into
Open
perf: grouped MoE path and runtime defaults for Apple Silicon (MPS)#36kkilchrist wants to merge 2 commits into
kkilchrist wants to merge 2 commits into
Conversation
Apply upstream PR openai#22 by @Berkkirik: "--device auto" resolves cuda > mps > cpu, and the Triton MoE kernels are auto-enabled only on CUDA devices (Triton cannot target Metal). Verified end-to-end on Apple Silicon (M4 Pro, torch 2.12.1) with the released checkpoint: spans identical to CPU output. Co-authored-by: berkkirik <berk.kirik@outlook.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- _torch_grouped_matmul: pack tokens by expert (same packing as the Triton path), scatter rows into a padded per-expert batch, and run one bmm per projection so each expert's weights are read once per layer instead of gathered per token. Default-on for MPS via OPF_MOE_GROUPED (Triton cannot target Metal); ~24x over the per-token gather path. - Default n_ctx to 4096 on non-CUDA devices: windows are non-overlapping and the attention band is +/-128 tokens, so larger windows do not help throughput but scale transient memory with window length (the 128k checkpoint default OOMs unified-memory Apple devices on long inputs). - Python API: device="auto" default and accept "mps"/"auto" so API users get the same resolution as the CLI. - MXFP4 decode: replace torch.ldexp with an exact exp2 multiply (ldexp has no MPS kernel; verified bit-identical on CPU). - Eval: run parallel preprocess workers on MPS as on CUDA; only CPU inference competes with preprocessing for cores. Measured on M4 Pro (20-core GPU, 48 GB), torch 2.12.1, released 1.5B checkpoint: ~65 tok/s (CPU) -> ~4,300 tok/s (MPS) with identical predicted spans and identical eval metrics on the sample dataset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Builds on #22, which is included here as the first commit (credited to @Berkkirik) because this work hard-depends on its device resolution and CUDA-only Triton gate. If #22 merges first, this rebases to a clean single-commit diff. #22 makes MPS work; this PR makes it fast and safe by default.
Results
Measured on M4 Pro (20-core GPU, 48 GB), macOS 27.0, Python 3.13.11, torch 2.12.1, released 1.5B checkpoint, ~1.5k-token document:
Throughput holds on longer inputs (~4,400 tok/s on a 50k-token document).
Changes
Grouped pure-torch MoE path (
opf/_model/model.py). Reuses the Triton branch's sort-by-expert packing, but scatters packed rows into a padded per-expert batch and runs onebmmper projection, gathering results back afterward. Each expert's weights are read once per layer instead of ~20 MB of gathered weights per token, which is what makes the per-token fallback path memory-bound. Default-on for MPS viaOPF_MOE_GROUPED(Triton cannot target Metal); CPU keeps the existing fallback path, CUDA keeps Triton. ~24× over the gather path on MPS.Default
n_ctxcapped at 4096 on non-CUDA devices (opf/_core/runtime.py). Windows are non-overlapping (example_to_windowsstride == window size) and the attention band is ±128 tokens, so larger windows don't improve throughput — but transient attention memory scales with window length. Measured on MPS with a 12.5k-token document: n_ctx 4096 → 3,969 tok/s; 16384 → 3,835 tok/s at ~2.3× the memory; the checkpoint's 128k default gets the process OOM-killed on a 50k-token input even with 48 GB of unified memory. Same rationale as the existing CPU cap;--n-ctxstill overrides.Python API defaults (
opf/_api.py).OPF(device=...)now defaults to"auto"and theLiteraltypes accept"mps"/"auto"— feat: auto-detect Apple Silicon (MPS) and keep Triton CUDA-only #22 covers the CLI, but API users on a Mac would still crash on the previous hardcoded"cuda"default.MPS-safe MXFP4 decode (
opf/_model/weights.py).torch.ldexphas no MPS kernel (DispatchStub: missing kernel for mps); replaced with anexp2multiply, which is the same exact power-of-two scaling. Verified bit-identical toldexpon CPU. (The released checkpoint ships fused bf16 tensors, so this only matters for MXFP4-packed checkpoints loaded directly to MPS.)Eval preprocess workers on MPS (
opf/_eval/runner.py). Parallel preprocessing now applies on MPS as on CUDA; only CPU inference competes with preprocessing for cores.Verification
opf "Alice was born on 1990-01-02."auto-selects MPS and redacts correctly.opf eval examples/data/sample_eval_five_examples.jsonlproduces identical precision/recall/F1 on--device cpuand--device mps.bmmcomputes the same per-expert projections as the Triton/gather paths; parity above is exact at the span level.🤖 Generated with Claude Code