mlx — Cursor rule: what it does, how to install
Guides Cursor to write high-performance Apple Silicon code with MLX, enforcing modular structure, unified memory usage, and explicit lazy evaluation.
.cursor/rules/mlx.mdcWhat mlx does
This rule sets development standards for building machine learning workflows with Apple MLX in Python and Swift. It requires Cursor to split monolithic scripts into dedicated modules for datasets, mlx.nn architectures, and YAML configuration files instead of hardcoding parameters.
The guidance focuses on managing MLX lazy execution. Cursor uses mx.compile() on training steps and calls mx.eval() to force execution. Without explicit mx.eval() calls, computation graphs keep expanding in memory, causing unpredictable slowdowns.
Apple Silicon relies on unified memory. Because of this, device transfer patterns like .to("gpu") are omitted: arrays execute on the GPU by default. When CPU execution is needed, operations use stream=mx.cpu. The instructions also detail 4-bit weight quantization using a group size of 32 for efficient deployment.
What it helps with
- split MLX code into modular components and externalize hyperparameters into YAML
- prevent computation graph bloat by using
mx.compileand explicitmx.eval - eliminate redundant CPU-GPU memory copies by using unified memory
- apply 4-bit quantization to model weights before saving
Example prompts for the agent
- Create a modular CNN in
mlx.nnwith hyperparameters loaded fromtrain_config.yaml. - Implement a training step with Adam, compile it with
mx.compile, and evaluate the state viamx.eval. - Quantize the model weights to 4-bit with group size 32 and export them to safetensors.
How to install mlx
Manually: put the files in place
.cursor/rules/mlx.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/mlx.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/mlx.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/mlx.mdManually: put the files in place
.clinerules/mlx.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about mlx
How to install mlx in Cursor?
Save the rule into .cursor/rules/mlx.mdc in your project root. Cursor will automatically apply its best practices across your files.
What does the mlx rule do?
It enforces best practices for machine learning on Apple Silicon with MLX. It covers project layout, lazy evaluation handling, and unified memory patterns.
Why should you avoid device migration calls like .to("gpu") in MLX?
Apple Silicon shares memory between CPU and GPU, eliminating the need for data transfers. Operations run on the GPU by default unless pointed to an explicit stream such as mx.cpu.
Source text
mlx.mdc · CC0-1.0
---
description: This guide provides opinionated, actionable best practices for developing high-performance machine learning applications with MLX on Apple Silicon, focusing on Python and Swift.
globs: **/*
---
# mlx Best Practices
MLX is Apple's high-performance array framework for numerical computing and AI on Apple Silicon. Adhering to these guidelines ensures your MLX code is efficient, maintainable, and integrates seamlessly with Apple's ML ecosystem.
## 1. Code Organization and Structure
Organize your MLX projects into logical, reusable modules. Mirror the structure of official Apple examples (e.g., `corenet` MLX examples) for clarity and maintainability.
### 1.1 Modularize Components
Separate concerns into dedicated files: data loading, model definition, training loops, and utility functions.
❌ **BAD: Monolithic Script**
```python
# train.py
import mlx.core as mx
import mlx.nn as nn
# ... data loading, model definition, training loop all in one file ...
```
✅ **GOOD: Modular Structure**
```
my_project/
├── data/
│ └── mnist_loader.py
├── models/
│ └── simple_cnn.py
├── config/
│ └── train_config.yaml
├── utils/
│ └── metrics.py
└── train.py
```
```python
# models/simple_cnn.py
import mlx.core as mx
import mlx.nn as nn
class SimpleCNN(nn.Module):
def __init__(self, num_classes: int):
super().__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
self.relu1 = nn.ReLU()
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
self.linear = nn.Linear(32 * 14 * 14, num_classes)
def __call__(self, x: mx.array) -> mx.array:
x = self.conv1(x)
x = self.relu1(x)
x = self.pool1(x)
x = x.reshape(x.shape[0], -1) # Flatten
return self.linear(x)
# train.py
from data.mnist_loader import load_mnist_data
from models.simple_cnn import SimpleCNN
# ... training logic ...
```
### 1.2 Externalize Configuration
Store training parameters, model hyperparameters, and dataset paths in version-controlled YAML or JSON files.
❌ **BAD: Hardcoded Parameters**
```python
# train.py
learning_rate = 0.001
batch_size = 32
num_epochs = 10
model = SimpleCNN(num_classes=10)
```
✅ **GOOD: Config-Driven**
```yaml
# config/train_config.yaml
training:
learning_rate: 0.001
batch_size: 32
num_epochs: 10
model:
name: SimpleCNN
num_classes: 10
data:
path: "./data/mnist"
```
```python
# train.py
import yaml
from models.simple_cnn import SimpleCNN
with open("config/train_config.yaml", "r") as f:
config = yaml.safe_load(f)
model = SimpleCNN(num_classes=config["model"]["num_classes"])
# ... use config["training"]["learning_rate"] etc.
```
## 2. Common Patterns and Anti-patterns
Leverage MLX's core features like lazy evaluation and unified memory for optimal performance.
### 2.1 Embrace Lazy Evaluation
MLX computations are lazy; they build a computation graph but execute only when results are explicitly needed. Use `mlx.eval()` or `mlx.compile()` to force execution.
❌ **BAD: Relying on Implicit Evaluation for Performance Critical Sections**
```python
# This loop might build a huge graph or trigger many small evaluations
# if intermediate results are printed or accessed.
for i in range(num_steps):
loss, grads = value_and_grad(model, loss_fn)(model, X_batch, y_batch)
# ... optimizer update ...
# No explicit eval, performance can be unpredictable.
```
✅ **GOOD: Explicit Evaluation for Control**
Use `mlx.eval()` on the final outputs of a computation step to trigger execution efficiently. For static graphs, `mlx.compile()` offers further optimization.
```python
import mlx.core as mx
import mlx.nn as nn
from mlx.optimizers import Adam
from mlx.utils import value_and_grad
# Define model and loss_fn (as in 1.1)
def train_step(model, X, y, optimizer):
loss, grads = value_and_grad(model, loss_fn)(model, X, y)
optimizer.update(model, grads)
return loss
# Compile the training step for static graphs
compiled_train_step = mx.compile(train_step)
# In your training loop:
optimizer = Adam(learning_rate=0.001)
for i in range(num_steps):
X_batch, y_batch = get_batch()
# Use the compiled step
loss = compiled_train_step(model, X_batch, y_batch, optimizer)
# Explicitly evaluate the loss to trigger computation and get its value
mx.eval(model, optimizer, loss) # Evaluate model state, optimizer state, and loss
print(f"Step {i}, Loss: {loss.item()}")
```
### 2.2 Leverage Unified Memory
MLX arrays live in shared memory, eliminating explicit data transfers between CPU and GPU. Specify the device for operations when necessary, but default behavior is often sufficient.
❌ **BAD: Unnecessary Device Management (like other frameworks)**
```python
# No need to explicitly move data to 'gpu' or 'cpu'
data = mx.array([1, 2, 3]).to("gpu") # This is not how MLX works
```
✅ **GOOD: Device-Agnostic Operations (or explicit per-op)**
```python
import mlx.core as mx
a = mx.array([1, 2, 3])
b = mx.array([4, 5, 6])
# Operations run on the default device (usually GPU if available)
c = a + b
mx.eval(c)
# You can explicitly specify device per operation if needed for specific tasks
d = mx.add(a, b, stream=mx.cpu) # Force CPU execution for this specific op
mx.eval(d)
```
### 2.3 Use Type Hints and Pure Functions
Follow Python best practices: annotate functions and variables with type hints for clarity. Design functions to be pure where possible for easier testing and composability.
## 3. Performance Considerations
Optimize for Apple Silicon by understanding MLX's execution model.
### 3.1 Batching and Data Loading
Efficient data loading and proper batching are critical for GPU utilization.
❌ **BAD: Iterating over single samples or inefficient data loading**
```python
# Inefficient for GPU
for sample_x, sample_y in dataset:
# ... process single sample ...
```
✅ **GOOD: Batch Data and Preload**
```python
# Use a DataLoader or similar utility to yield batches of MLX arrays
def data_loader(data, batch_size):
for i in range(0, len(data), batch_size):
yield mx.array(data[i:i+batch_size])
# In training loop:
for X_batch, y_batch in data_loader(train_data, batch_size):
# ... process batch ...
```
### 3.2 Quantization for Deployment
For deploying models on-device, consider quantizing weights (e.g., 4-bit) to reduce memory footprint and improve inference speed.
```python
import mlx.core as mx
import mlx.nn as nn
# Assuming 'model' is a trained MLX model
# Quantize the model to 4-bit
quantized_model = model.quantize(group_size=32, bits=4)
# Save the quantized model
mx.save("quantized_model.safetensors", quantized_model.parameters())
```
## 4. Common Pitfalls and Gotchas
Avoid common mistakes that can lead to unexpected behavior or poor performance.
### 4.1 Forgetting `mlx.eval()`
The most common pitfall. If you don't explicitly evaluate, your graph might not execute, or only partially.
❌ **BAD: No `eval()` on results**
```python
# This will print the MLX array object, not its computed value
result = mx.add(mx.array([1]), mx.array([2]))
print(result) # Output: array(value=..., shape=(1,), dtype=int32)
```
✅ **GOOD: Always `eval()` when you need the value**
```python
result = mx.add(mx.array([1]), mx.array([2]))
mx.eval(result)
print(result) # Output: array(3, shape=(1,), dtype=int32)
print(result.item()) # Output: 3
```
### 4.2 Over-evaluating or Under-evaluating
Finding the right balance for `mlx.eval()` is key. Too many small `eval` calls can break graph optimization. Too few can lead to memory issues or delayed execution.
**Guideline:** Evaluate at logical synchronization points, typically at the end of a training step or inference call. Use `mx.eval(model, optimizer, loss)` to evaluate all relevant states.
## 5. Testing Approaches
Ensure the reliability and correctness of your MLX models.
### 5.1 Unit Tests for Model Components
Test individual layers, custom modules, and utility functions.
```python
# tests/test_simple_cnn.py
import mlx.core as mx
import mlx.nn as nn
from models.simple_cnn import SimpleCNN
def test_simple_cnn_output_shape():
model = SimpleCNN(num_classes=10)
# Initialize model parameters by running a dummy input
_ = model(mx.zeros((1, 1, 28, 28)))
mx.eval(model) # Ensure parameters are initialized
input_tensor = mx.random.normal((2, 1, 28, 28))
output = model(input_tensor)
assert output.shape == (2, 10)
print("SimpleCNN output shape test passed.")
if __name__ == "__main__":
test_simple_cnn_output_shape()
```
### 5.2 Integration Tests for Inference
Verify that your trained models perform as expected on new data, especially when integrating with Core ML or other on-device frameworks.
### 5.3 Data Validation and Reproducibility
Implement sanity checks for all input data. Version control your data, models, and training configurations to ensure reproducible results.
```python
# utils/data_validation.py
import mlx.core as mx
def validate_image_data(images: mx.array, expected_shape: tuple, expected_dtype: mx.Dtype):
assert images.ndim == len(expected_shape), f"Expected {len(expected_shape)} dims, got {images.ndim}"
assert images.shape == expected_shape, f"Expected shape {expected_shape}, got {images.shape}"
assert images.dtype == expected_dtype, f"Expected dtype {expected_dtype}, got {images.dtype}"
assert images.min() >= 0 and images.max() <= 1, "Image pixel values out of range [0, 1]"
print("Image data validation passed.")
# In your data loader or training script:
# ... load raw data ...
# raw_images = mx.array(...)
# validate_image_data(raw_images, (100, 1, 28, 28), mx.float32)
```
## 6. Swift-Specific Guidelines
When working with MLX Swift, adhere to Apple's official Swift style guides and API design guidelines.
### 6.1 Follow Swift API Style
Use idiomatic Swift patterns, consistent naming, and proper error handling. Refer to the `mlx-swift` examples (e.g., MNISTTrainer) as templates.
### 6.2 Manage Object Lifetime
Be mindful of object lifetimes, especially when bridging C++ MLX objects to Swift, as aggressive inlining and optimizations can affect behavior. The `mlx-swift` package includes recommendations for this.
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc