When you need this. A model profile contains many small normalization operations. Combining work may reduce intermediate tensors and dispatch overhead, but a shorter expression does not establish a faster kernel. Compare numerical output and synchronized execution on the available backend.
What you run. Fresh Metal eager-versus-compiled tensor experiment. CUDA/Triton execution, kernel traffic, and whole-model acceleration are unmeasured.
This lesson builds on investigate host overhead before changing gpu settings.
Run the same normalization two ways
RMS normalization scales each row using the reciprocal root of its mean squared values, then applies learned weights. Residual addition can feed that calculation without requiring a separately retained intermediate in a fused implementation.
The live program compares eager and compiled MLX expressions on Metal. Compilation may fuse operations; only a device trace can confirm the exact kernels and memory traffic. The offline analyzer checks numerical parity and the timing samples. It does not verify Triton memory safety or infer GPU memory traffic.
fusion_live.py creates actual tensors, warms both paths, checks maximum absolute output difference, and records repeated synchronized timings for several shapes, including an irregular row width. It replaces the unexecuted Triton snippet as the tested implementation.
Compare the tensor execution paths
Download the lab from this page, extract it, and run these commands from its directory. The default command reads saved run records or runs the explicitly described local algorithm; it never downloads a model or calls a paid API.
python3 lab.py
python3 -m unittest -v test_lab.pyRead lab.py
"""Validate recorded numerical parity and summarize actual Metal expression timings."""
import json, math, statistics
from pathlib import Path
def compare_case(case, tolerance=1e-5):
if (
tolerance < 0
or not math.isfinite(case["max_abs_error"])
or case["max_abs_error"] > tolerance
):
raise ValueError("Numerical parity exceeds tolerance")
result = {
"rows": case["rows"],
"width": case["width"],
"max_abs_error": case["max_abs_error"],
}
for name in ["eager", "compiled"]:
samples = case["timings"][name]["samples_ms"]
if not samples or any(not math.isfinite(v) or v <= 0 for v in samples):
raise ValueError("Invalid timing samples")
result[name + "_median_ms"] = round(statistics.median(samples), 6)
return result
def main():
record = json.loads(Path("fusion-run.json").read_text())
print(
json.dumps(
{
"execution_mode": "offline analysis of fresh Metal eager and compiled expression runs",
"cases": [compare_case(c) for c in record["cases"]],
"limits": "Shape-specific tensor timings; no Triton/CUDA run, device-memory counter or whole-model speedup.",
},
indent=2,
)
)
if __name__ == "__main__":
main()Read fusion_live.py
"""Compare eager and compiled RMS normalization on Metal; no CUDA/Triton execution claimed."""
import argparse
import hashlib
import importlib.metadata
import json
import platform
import statistics
import time
from pathlib import Path
def main():
import mlx.core as mx
p = argparse.ArgumentParser()
p.add_argument("--output", required=True)
a = p.parse_args()
mx.random.seed(0)
def norm(x, r, w):
z = x + r
return z * mx.rsqrt(mx.mean(z * z, axis=-1, keepdims=True) + 1e-6) * w
compiled = mx.compile(norm)
cases = []
for rows, width in [(1, 896), (32, 896), (32, 1003)]:
x = mx.random.normal((rows, width))
r = mx.random.normal((rows, width))
w = mx.ones((width,))
mx.eval(x, r, w)
reference = norm(x, r, w)
candidate = compiled(x, r, w)
mx.eval(reference, candidate)
error = float(mx.max(mx.abs(reference - candidate)).item())
timings = {}
for name, fn in [("eager", norm), ("compiled", compiled)]:
for _ in range(3):
mx.eval(fn(x, r, w))
samples = []
for _ in range(20):
mx.synchronize()
start = time.perf_counter()
out = fn(x, r, w)
mx.eval(out)
mx.synchronize()
samples.append((time.perf_counter() - start) * 1000)
timings[name] = {
"median_ms": round(statistics.median(samples), 6),
"samples_ms": [round(s, 6) for s in samples],
}
cases.append(
{
"rows": rows,
"width": width,
"dtype": "float32",
"max_abs_error": error,
"timings": timings,
}
)
result = {
"execution_mode": "fresh Metal tensor execution; eager versus MLX compiled expression",
"environment": {
"python": platform.python_version(),
"platform": platform.platform(),
"mlx": importlib.metadata.version("mlx"),
},
"source_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"cases": cases,
"limits": "Expression compilation may fuse operations. No kernel trace or measured HBM byte count; no whole-model, CUDA or Triton speedup claim.",
}
Path(a.output).write_text(json.dumps(result, indent=2) + "\n")
if __name__ == "__main__":
main()The compiled expression and eager reference agree for these recorded shapes. Their timings include repeated synchronization and evaluation. The distributions, not a fixed advertised ratio, are the result worth inspecting.
Inspect the complete local result
{
"execution_mode": "offline analysis of fresh Metal eager and compiled expression runs",
"cases": [
{
"rows": 1,
"width": 896,
"max_abs_error": 0.0,
"eager_median_ms": 0.45025,
"compiled_median_ms": 0.290083
},
{
"rows": 32,
"width": 896,
"max_abs_error": 0.0,
"eager_median_ms": 0.222666,
"compiled_median_ms": 0.192312
},
{
"rows": 32,
"width": 1003,
"max_abs_error": 0.0,
"eager_median_ms": 0.228792,
"compiled_median_ms": 0.196583
}
],
"limits": "Shape-specific tensor timings; no Triton/CUDA run, device-memory counter or whole-model speedup."
}Inspect the actual execution record
{
"execution_mode": "fresh Metal tensor execution; eager versus MLX compiled expression",
"environment": {
"python": "3.12.8",
"platform": "macOS-27.0.1-arm64-arm-64bit",
"mlx": "0.32.4"
},
"source_sha256": "62653ea247e893a6c1b2a70709fa0e9a363e1b2ce25e6b522c0c8308fd230a05",
"cases": [
{
"rows": 1,
"width": 896,
"dtype": "float32",
"max_abs_error": 0.0,
"timings": {
"eager": {
"median_ms": 0.45025,
"samples_ms": [
0.507958,
0.464666,
0.450875,
0.45325,
0.449625,
0.503083,
0.501917,
0.456292,
0.472334,
0.473333,
0.516667,
0.401541,
0.393125,
0.401959,
0.222625,
0.180541,
0.174334,
0.180542,
0.175917,
0.1745
]
},
"compiled": {
"median_ms": 0.290083,
"samples_ms": [
0.174417,
0.1545,
0.160917,
0.161667,
0.161125,
0.412792,
0.464209,
0.464167,
0.509875,
0.450125,
0.459958,
0.473375,
0.533375,
0.470042,
0.38675,
0.187459,
0.175958,
0.176375,
0.183375,
0.193416
]
}
}
},
{
"rows": 32,
"width": 896,
"dtype": "float32",
"max_abs_error": 0.0,
"timings": {
"eager": {
"median_ms": 0.222667,
"samples_ms": [
0.254083,
0.229667,
0.22275,
0.194417,
0.196792,
0.204542,
0.21225,
0.204917,
0.198125,
0.216666,
0.216125,
0.470042,
0.483125,
0.5025,
0.475292,
0.515,
0.21125,
0.225208,
0.222583,
0.223292
]
},
"compiled": {
"median_ms": 0.192312,
"samples_ms": [
0.194792,
0.200791,
0.196542,
0.181,
0.189917,
0.192041,
0.200792,
0.20125,
0.195417,
0.192083,
0.187625,
0.178333,
0.178083,
0.179417,
0.183041,
0.196666,
0.192083,
0.192541,
0.212208,
0.23525
]
}
}
},
{
"rows": 32,
"width": 1003,
"dtype": "float32",
"max_abs_error": 0.0,
"timings": {
"eager": {
"median_ms": 0.228791,
"samples_ms": [
0.269583,
0.224083,
0.249292,
0.234417,
0.23475,
0.229583,
0.2335,
0.231584,
0.232292,
0.226375,
0.227375,
0.224875,
0.222291,
0.216875,
0.228,
0.222334,
0.235542,
0.223666,
0.230167,
0.223459
]
},
"compiled": {
"median_ms": 0.196583,
"samples_ms": [
0.196291,
0.208083,
0.211208,
0.214792,
0.203625,
0.191208,
0.196875,
0.181042,
0.190583,
0.187208,
0.190167,
0.192334,
0.194334,
0.192125,
0.246042,
0.191875,
0.231625,
0.269167,
0.23475,
0.208583
]
}
}
}
],
"limits": "Expression compilation may fuse operations. No kernel trace or measured HBM byte count; no whole-model, CUDA or Triton speedup claim."
}
This image is an application-generated report of the saved program result, not a new inference run. Open the full-resolution report or read the raw result.
Compare numerical error and timing
Fusion can reduce dispatch and data movement but increase register pressure or compilation overhead. Small and large shapes can behave differently. A reduction in modeled tensor accesses is not an equal reduction in model latency; normalization may be a small fraction of the critical path.
Numerical parity exceeds tolerance. A compiled result outside the numerical tolerance is rejected before its timings are compared.
Invalid timing samples. Missing, nonfinite, or nonpositive execution times are rejected. The analyzer allows equal timings; it never assumes compilation must win.
Read the executable checks
import unittest
from lab import compare_case
class FusionEvidenceTests(unittest.TestCase):
def case(self):
return {
"rows": 1,
"width": 3,
"max_abs_error": 0,
"timings": {
"eager": {"samples_ms": [1, 3, 2]},
"compiled": {"samples_ms": [2, 2, 2]},
},
}
def test_parity_is_required_before_timing(self):
case = self.case()
case["max_abs_error"] = 0.1
with self.assertRaises(ValueError):
compare_case(case)
def test_invalid_execution_timing_is_rejected(self):
case = self.case()
case["timings"]["compiled"]["samples_ms"] = [-1]
with self.assertRaises(ValueError):
compare_case(case)
def test_medians_are_compared_without_a_speedup_promise(self):
result = compare_case(self.case())
self.assertEqual(result["eager_median_ms"], 2)
self.assertEqual(result["compiled_median_ms"], 2)
if __name__ == "__main__":
unittest.main()Run this optional command only for the actual local endpoint or backend shown. Model paths refer to the first lesson’s explicit download; the server binds to loopback.
python3 -m venv .venv
.venv/bin/python -m pip install mlx==0.32.4
.venv/bin/python fusion_live.py --output new-fusion-run.jsonTry a change and explain the result
Compare eager and compiled timing distributions for each shape. Check numerical error before choosing the faster path. Explain why neither a fixed speedup nor an exact HBM-access ratio follows from these results.
Read the explanation
Retain the shape, dtype, repetitions, backend, and error check. Whole-model impact requires a separate measurement.
Decide whether fusion belongs on the critical path
Prefer runtime-provided kernels before maintaining custom device code. Benchmark warm steady state separately from compilation, and test dtype, shape, and numerical tolerances. A CUDA/Triton port requires its own hardware execution and bounds tests.
References
- MLX compilation and fusion.
- Triton normalization implementation patterns.
- RMSNorm authors’ formulation.
Sources checked October 24, 2026. Runtime-specific commands and results apply to the recorded versions; linked rolling documentation can change. The application scenarios are teaching examples, not claims about an author’s production incident.