MLX backend (preview)¶
TorchLens captures MLX forwards with eager wrappers
over mlx.core, mlx.core.fast, the mx.array operators and methods, and the mlx.nn
layer classes. The backend is a preview: its scorecard ships in the wheel and shows in
tl.report.doctor() under backend_scorecard:mlx; only a maintainer decision flips the
label to stable.
| Requirement | Value |
|---|---|
| Distribution | mlx |
| Supported range | [0.26, 0.27), excluding 0.26.5 (its manylinux wheel ships without libmlx.so) |
| Pinned CI version | 0.26.3 |
| Install extra | pip install "torchlens[mlx]" |
| Materialization | lazy (realize-once: one mx.eval after the forward, before any payload is read) |
Tutorial¶
import mlx.core as mx
import mlx.nn as nn
import torchlens as tl
mx.random.seed(0)
model = nn.Sequential(nn.Linear(4, 8), nn.GELU(), nn.Linear(8, 2))
x = mx.array([[0.5, -1.0, 2.0, 0.25], [1.0, 1.0, -1.0, 0.0]])
trace = tl.trace(model, x, backend="mlx")
print([op.func_name for op in trace]) # ['input', 'linear', 'gelu', 'linear']
print(trace.output_ops[0].shape) # (2, 2)
status = tl.backends.spec("mlx").validate_trace(trace)
print(status.state) # 'passed': every op replayed from its parents' saved payloads
Operators and methods are captured as the mlx.core function they stand for: a + b logs
add, a @ w logs matmul, v.sum() logs sum, v[0] logs getitem, v > 0 logs
greater. Modules keep their MLX parameter-tree addresses (layers.0); the list or dict
holding sub-modules gets its own module record (layers, class list), as a torch
ModuleList does. An array the forward uses that is neither an input, a parameter nor the output
of a recorded op (a closure array, a module-level array) is recorded as a constant source
op with its payload, and the op that used it lists the constant as a parent. mx.ones,
mx.zeros, mx.arange and the other literal producers stay ops with no parents and a saved
payload.
When the forward still reaches an operation no wrapper saw (a function outside the wrapped
surface such as mx.linalg.norm, or an mx.array(...) built inside the forward), the trace
is marked capture_verified=False with reason unwrapped_op_in_capture and a warning says
how many arrays were affected. Callers who want a hard stop instead switch the backend-local
policy:
from torchlens.backends.mlx import unwrapped_policy
with unwrapped_policy("refuse"):
tl.trace(model, x, backend="mlx") # raises BackendUnsupportedError, code unwrapped_op_in_capture
Can and cannot¶
The rows are the backend's capability table in the scorecard's vocabulary: supported (the
flag is True and consumed by the suite), unsupported (False, and the request refuses with the
named code), not yet proven (False, refusal not yet pinned by a passing suite cell).
| Capability | State | Refusal code when requested |
|---|---|---|
Forward capture (tl.trace) |
supported | |
Replay validation (has_validation_replay; validate_trace, status object with per-op reasons) |
supported | |
Payload materialization (payload_materialization; save / load with array payloads) |
supported | |
Static-label interventions (can_intervene; intervene=, stop_after=) |
supported | |
Intermediate derived gradients (intermediate_derived_grads; grad_options=) |
supported | |
Backward capture (backward_capture; save_grads, backward_ready) |
unsupported | backward_capture_unsupported |
Fastlog (fastlog) |
unsupported | fastlog_unsupported |
RNG replay (rng_replay; save_rng) |
unsupported | rng_replay_unsupported |
Streaming (has_streaming) |
unsupported | streaming_unsupported |
Structure-only capture (structure_only_capture) |
unsupported | structure_only_capture_unsupported |
Explicit random_seed= |
unsupported | explicit_seed_unsupported |
Trace options outside module_identity_mode, grad_options |
unsupported | trace_option_unsupported |
mx.compile / mx.grad / mx.vmap wrappers as the traced entry |
unsupported | traced_transform_entry_unsupported |
grad_options= together with intervene= or stop_after= |
unsupported (capture them in separate calls) | intervention_with_derived_grads_unsupported |
An intervention that returns a non-mx.array or changes shape or dtype |
unsupported | intervention_result_invalid |
| Ops outside the wrapped surface | flagged (unwrapped_op_in_capture) or refused under unwrapped_policy("refuse") |
unwrapped_op_in_capture |
Arrays constructed inside the forward (mx.array(...)) |
flagged as unwrapped (build constants outside the forward) | unwrapped_op_in_capture |
Validation decisions¶
validate_trace never returns a bare bool. The status carries one decision per op with a
reason from the shared closed list: replay_mismatch, parent_payload_missing,
no_replayable_function, perturbation_insensitive, user_intervention_replacement,
constant_producer_leaf (a constant leaf or a literal producer: replayed when a function
exists, exempted from perturbation because it has no tensor parent) and
constant_parent_payload (a consumer whose dependency was proven by perturbing a constant
parent), declared_parents_inconsistent (an op's declared parents disagree with the arrays its
saved call reads) and replay_coverage_incomplete (the replay did not cover every captured
op). A failed status names the first offending op in its message.