jax-mps
CLAUDE.md242 starsChanged 6 months agoApache-2.0View on GitHub
Tests, linting, and compilation on the main branch and in Continuous Integration testing ALWAYS pass. There are NO pre-existing failures. Any test failure you observe MUST be caused by changes we made. You may NEVER claim that failures are "known issues," "unrelated to our changes," "flaky," or similar. When implementing or modifying op handlers, ALWAYS check what MLX provides natively before writing manual logic. The MLX C++ API headers are at build/*/mlx-src/mlx/ops.h (built after first uv pip install -e .).…
- Installs packages
We split this CLAUDE.md into the rules it gives an agent
8 rules. About 0 are common, 2 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 11
When importing from JAX, PREFER a public interface (jax.*, jax.lax.*, jax.nn.*, jax.extend.*) over jax._src.* whenever an equivalent one …
- Very specific: line 23
When implementing or modifying op handlers, ALWAYS check what MLX provides natively before writing manual logic.
- Very specific: line 31
Prefer these native functions over manual index computation (linear index math, flatten+take, transpose+reshape workarounds).
- Very specific: line 39
Add a handler function in src/pjrt_plugin/mlx_executable.cc:
- Very specific: line 90
Add a new populateXxxPatterns(mlir::RewritePatternSet&) in passes/xxx.{h,cc}.
- Very specific: line 92
Add a runtime branch in HandleCustomCall (src/pjrt_plugin/ops/control_flow.cc) that lowers the new @mps.xxx custom_call to the MLX kernel …
- Distinctive: line 97
When fixing a bug or addressing an issue, use TDD:
- Distinctive: line 99
Create the test to reproduce the issue and verify it fails.
≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Guidelines
- You may NEVER skip or xfail tests without my explicit approval.
- You MUST use
uvto manage dependencies. - You MUST use
uv run ...to execute commands. - You may NEVER use
--no-verifyfor git commits. - You may NEVER push to
mainunless explicitly requested. - You may NEVER delete operations or tests without my explicit approval.
- For each op, you MUST register an
OperationTestConfigfor tests intests/test_ops.py. Seetests/configs/unary.pyfor an example andtests/configs/util.pyfor the signature ofOperationTestConfig. - You may NEVER create new op registries. Use
OpRegistryfor all ops. Seesrc/pjrt_plugin/ops/registry.h. - When importing from JAX, PREFER a public interface (
jax.*,jax.lax.*,jax.nn.*,jax.extend.*) overjax._src.*whenever an equivalent one exists — private paths get removed without notice across patch releases. Only reach forjax._srcwhen no public equivalent exists, and add a comment explaining why.
CI is Always Green
Tests, linting, and compilation on the main branch and in Continuous Integration testing ALWAYS pass. There are NO pre-existing failures. Any test failure you observe MUST be caused by changes we made. You may NEVER claim that failures are "known issues," "unrelated to our changes," "flaky," or similar.
Naming Conventions
- Handler functions MUST use PascalCase:
HandleAdd,HandleExp,HandleBroadcastInDim
MLX-First Implementation
When implementing or modifying op handlers, ALWAYS check what MLX provides natively before writing manual logic. The MLX C++ API headers are at build/*/mlx-src/mlx/ops.h (built after first uv pip install -e .). Key operations to know about:
mlx::core::gather(a, indices, axes, slice_sizes)— multi-axis gather with per-axis index arraysmlx::core::scatter(a, indices, updates, axes)— multi-axis scatter (alsoscatter_add,scatter_prod,scatter_min,scatter_max)mlx::core::slice(a, start_array, axes, slice_sizes)— dynamic slice with runtime start indicesmlx::core::slice_update(src, update, start_array, axes)— dynamic slice updatemlx::core::take(a, indices, axis)/mlx::core::take_along_axis(a, indices, axis)— single-axis indexing
Prefer these native functions over manual index computation (linear index math, flatten+take, transpose+reshape workarounds). Read the implementation in mlx/ops.cpp if the semantics are unclear.
Adding New Ops
- Identify the StableHLO op name (e.g.,
stablehlo.multiply). Run a test and look for the error message which includes the op name. - Find the matching MLX function in the MLX documentation (e.g.,
mlx::core::multiply). Check the C++ headers (mlx/ops.h) for the full API — the Python docs don't cover everything. - Add a handler function in
src/pjrt_plugin/mlx_executable.cc:bool HandleMultiply(mlir::Operation* op, ValueMap& values, std::vector<mlx::core::array>& outputs, ExecContext& ctx) { auto lhs_opt = GetValue(values, op->getOperand(0)); auto rhs_opt = GetValue(values, op->getOperand(1)); if (!lhs_opt || !rhs_opt) { MPS_LOG_ERROR("stablehlo.multiply: operand not found\\n"); return false; } values.emplace(ToKey(op->getResult(0)), mlx::core::multiply(lhs_opt->get(), rhs_opt->get())); return true; } - Register the handler in
GetOpHandlers()insrc/pjrt_plugin/mlx_executable.cc:
Op names are auto-derived from{"stablehlo.multiply", HandleMultiply},GetOpHandlers(), so no separate registration is needed. - Rebuild with
uv pip install -e .and run tests.
Build and Test
uv sync --all-groups
uv pip install -e .
uv run pytestBenchmarks
Benchmarks are excluded from normal test runs. To run them:
# Run benchmarks (compares CPU vs MPS performance)
uv run pytest -m benchmark --benchmark-only -n0Debugging
JAX_MPS_NO_OPTIMIZE=1disables the StableHLO simplification + MPS fusion passes. Useful for bisecting whether a regression comes from a fusion.JAX_MPS_DUMP_OPTIMIZED_IR=<dir>writes each parsed+optimized module as<dir>/module_<n>.mlir(post-pass, so@mps.*custom_calls from fusion are visible). Used bytests/test_fusion.py; also handy for ad-hoc inspection.JAX_MPS_CACHE_LIMIT_BYTES=<n>caps MLX's internal MTLBuffer cache (callsmlx::core::set_cache_limit). Unset by default — MLX picks ~1.5x the recommended working set. Set this when running many unrelated computations in one process (e.g. the upstream JAX suite viascripts/run_jax_tests.py, which sets 1 GiB) to prevent freed buffers from accumulating until the OS swaps them. Avoid for training/inference: a low cap forces per-iteration reallocation and regresses throughput on Max-class GPUs.JAX_MPS_GPU_CAPTURE=<path.gputrace>captures a Metal GPU trace over a bounded window ofExecutedispatches (default 1; setJAX_MPS_GPU_CAPTURE_DISPATCHES=<n>to widen) into a.gputracedocument openable in Xcode/Instruments. Forces synchronous eval over the captured window so the GPU work is enclosed. Apple requiresMTL_CAPTURE_ENABLED=1in the environment at process start; if it is missing the plugin prints a notice and disables capture rather than failing. Uses MLX's pure-C++mlx::core::metal::start_capture/stop_capture(no Objective-C++). The test covering the capture-enabled path (tests/test_gpu_capture.py) only runs on GitHub Actions, because Apple's GPU tools calltask_for_pid()and macOS prompts for Developer Tools Access on every local run; setJAX_MPS_TEST_GPU_CAPTURE=1to run it anyway. It keys onGITHUB_ACTIONS, notCI, since the pre-commit pytest hook itself runs asCI=true.
Adding New Fusion Patterns
Fusion passes live in src/pjrt_plugin/passes/ as MLIR RewritePatterns inside a single MpsFusionPass (see mps_fusion_pass.cc). The pass library is compiled -fno-rtti -fno-exceptions with hidden visibility — see CMake comments and the memory note for why.
To add a pattern:
- Add a new
populateXxxPatterns(mlir::RewritePatternSet&)inpasses/xxx.{h,cc}. - Register it in
MpsFusionPass::runOnOperation. - Add a runtime branch in
HandleCustomCall(src/pjrt_plugin/ops/control_flow.cc) that lowers the new@mps.xxxcustom_call to the MLX kernel call. - Append a
FusionTestConfig(...)tomake_fusion_configs()intests/configs/fusion.py. The test harness verifies the expected custom_calls appear in dumped IR AND checks numerical equivalence against both unfused-MPS (tight tolerance) and CPU (loose). The fused-vs-unfused check usesfusion_atol/fusion_rtol(default 1e-6) — loosen these on the config if the pattern targets fp16/bf16 or a non-IEEE-associative rewrite.
Bugs and Issues
When fixing a bug or addressing an issue, use TDD:
- Create the test to reproduce the issue and verify it fails.
- Fix the bug or address the issue and verify the test passes.
In this repositoryOne other file it gives its agents.
- .github/copilot-instructions.mdCopilot instructions
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

