From 0c4a332538fcb3bef8065637aface641b77310fb Mon Sep 17 00:00:00 2001 From: ausimian Date: Wed, 8 Jul 2026 10:37:42 +0100 Subject: [PATCH 1/4] build: bump pinned MLX to 0.32.0 Maintenance bump of the native MLX pin from 0.31.2. No API changes. Transparently picks up two upstream perf improvements to kernels emily already uses: qmv_wide small-batch quantized matvec (accelerates the fused quantized path) and fused SDPA coverage for asymmetric Q/V head dims. Full suite green on the new version: default (737 tests, 40 doctests, 79 properties), conformance (eager/native/fuse vs PyTorch references), and the full-checkpoint tiers (ViT/Whisper/DistilBERT/fast-kernels + MNIST training canary). --- RELEASE.md | 4 ++++ mix.exs | 2 +- mix.lock | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index fb706ac..bf300d0 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,9 @@ ### Changed +- Updated the pinned MLX to 0.32.0. This is a maintenance bump that also + picks up faster small-batch quantized matvec (`qmv_wide`) — accelerating + the fused quantized path — and broader fused SDPA coverage (asymmetric + Q/V head dims), both transparently. No API changes. - Quantized dense layers now use the fused `mx::quantized_matmul` kernel instead of dequantizing the full weight to bf16 and running a dense matmul. The packed low-bit weights are streamed directly, so a decode diff --git a/mix.exs b/mix.exs index 9d181ae..a28709b 100644 --- a/mix.exs +++ b/mix.exs @@ -9,7 +9,7 @@ defmodule Emily.MixProject do # `deps/0`) and the per-variant cache dir layout. Bump in lockstep with # the submodule ref; CI's `release-nif.yml` rebuilds the NIF against # whatever this resolves to. - @mlx_version "0.31.2" + @mlx_version "0.32.0" # Precompiled NIF targets this `@version` ships. Used as an # early fail-fast guard in the hex-consumer fetch step (an diff --git a/mix.lock b/mix.lock index bd21685..9ac2ca8 100644 --- a/mix.lock +++ b/mix.lock @@ -17,7 +17,7 @@ "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, - "mlx_src": {:git, "https://github.com/ml-explore/mlx.git", "68cf2fddd8de5edd8ab3d926391772b2e2cedad8", [tag: "v0.31.2"]}, + "mlx_src": {:git, "https://github.com/ml-explore/mlx.git", "7a1d4f5c12ac82f4b4d0a6e71538d89ca0605247", [tag: "v0.32.0"]}, "nimble_csv": {:hex, :nimble_csv, "1.3.0", "b7f998dc62b222bce9596e46f028c7a5af04cb5dde6df2ea197c583227c54971", [:mix], [], "hexpm", "41ccdc18f7c8f8bb06e84164fc51635321e80d5a3b450761c4997d620925d619"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nx": {:hex, :nx, "0.12.0", "32bc205bab5486d73892132d17a11ea113e97427a29bb70606a544724b95e193", [:mix], [{:complex, "~> 0.7", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d022a33ea3c900eb6e2e91b4e0793759459c886f482be61978004b5e4843b5e"}, From fcff5d1a7a9df6a87f8565974992042f8e183726 Mon Sep 17 00:00:00 2001 From: ausimian Date: Wed, 8 Jul 2026 10:50:00 +0100 Subject: [PATCH 2/4] build: compile the NIF as C++20 to match MLX 0.32 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MLX 0.32.0 sets `CMAKE_CXX_STANDARD 20` (REQUIRED), so libmlx.a is built as C++20 and its public headers use C++20 features — e.g. a defaulted `operator==` on `CompileOptions` in mlx/backend/common/metal_kernel.h, reachable via . The NIF includes those headers and statically links those objects, so build it at the same language level to stay ABI/ODR-consistent with the library. Raises both `-std=c++17` sites in the Makefile (the NIF objects and the standalone native bench binary) to `-std=c++20`. --- Makefile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 4223290..eefe12f 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,14 @@ HEADERS := $(shell find c_src \( -name '*.h' -o -name '*.hpp' \)) OBJECTS := $(patsubst c_src/%.cpp,$(BUILD_DIR)/%.o,$(SOURCES)) # Flags -CXXFLAGS := -std=c++17 -O3 -fPIC -fvisibility=hidden -Wall -Wextra +# +# C++20 to match MLX itself: as of 0.32.0 MLX sets `CMAKE_CXX_STANDARD 20` +# (REQUIRED), so libmlx.a is compiled as C++20 and its public headers use +# C++20 features (e.g. a defaulted `operator==` on `CompileOptions` in +# mlx/backend/common/metal_kernel.h, reachable via ). We include +# those headers and statically link those objects, so we build the NIF at the +# same language level to stay ABI/ODR-consistent with the library. +CXXFLAGS := -std=c++20 -O3 -fPIC -fvisibility=hidden -Wall -Wextra CXXFLAGS += -I$(ERTS_INCLUDE_DIR) -Ic_src # Third-party headers: use -isystem so warnings inside them (e.g. MLX's # -Wdeprecated-copy on _MLX_BFloat16) don't clutter our builds or trip @@ -58,7 +65,7 @@ BENCH_NATIVE_BIN := $(BUILD_DIR)/compile_microbench BENCH_NATIVE_METALLIB := $(BUILD_DIR)/mlx.metallib $(BENCH_NATIVE_BIN): $(BENCH_NATIVE_SRC) | $(BUILD_DIR) - $(CXX) -std=c++17 -O3 -Wall -Wextra \ + $(CXX) -std=c++20 -O3 -Wall -Wextra \ -isystem $(MLX_INCLUDE_DIR) \ $(BENCH_NATIVE_SRC) \ $(MLX_LIB_DIR)/libmlx.a \ From b3977f531336dd82c8353c1d841c2b35661c8cd8 Mon Sep 17 00:00:00 2001 From: ausimian Date: Wed, 8 Jul 2026 10:58:06 +0100 Subject: [PATCH 3/4] test: tolerate FP rounding in the native power assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MLX 0.32.0's JIT Metal power kernel computes integer-valued results via exp/log, so 3**2 comes back as 8.99999809 rather than a bit-exact 9.0. The test asserted exact equality; switch it to assert_close (default f32 tolerance 1e-4), matching how the other transcendental ops in this file are checked. Surfaced by CI on the jit/macOS-26 variant — the aot path is bit-exact, so it passed there and locally. --- test/emily/native_test.exs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/emily/native_test.exs b/test/emily/native_test.exs index 9c15965..768e41d 100644 --- a/test/emily/native_test.exs +++ b/test/emily/native_test.exs @@ -232,7 +232,10 @@ defmodule Emily.NativeTest do test "power" do a = f32([2.0, 3.0], [2]) b = f32([3.0, 2.0], [2]) - assert to_f32_list(Native.power(worker(), a, b)) == [8.0, 9.0] + # `power` is computed via exp/log, so integer-valued results aren't + # bit-exact (e.g. MLX's JIT Metal kernel yields 3**2 = 8.99999809); + # compare within tolerance like the other transcendental ops here. + assert_close(to_f32_list(Native.power(worker(), a, b)), [8.0, 9.0]) end test "maximum / minimum" do From 9ff15db85dba94f1330db049b0d8fa640b83da5d Mon Sep 17 00:00:00 2001 From: ausimian Date: Wed, 8 Jul 2026 10:58:06 +0100 Subject: [PATCH 4/4] build: rebuild the NIF when libmlx.a or the Makefile changes The object rule depended only on c_src sources/headers and $(NIF_SO) only on the objects, so neither an MLX version bump (a freshly built libmlx.a under a new MLX_LIB_DIR) nor a compile/link flag change here forced a rebuild. An existing checkout could copy the new mlx.metallib while keeping a NIF still statically linked against the old MLX, running a mismatched binary until a manual clean. Add libmlx.a and the Makefile as prerequisites of the objects, the linked NIF, and the native bench. --- Makefile | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index eefe12f..044f499 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ BENCH_NATIVE_SRC := bench/native/compile_microbench.cpp BENCH_NATIVE_BIN := $(BUILD_DIR)/compile_microbench BENCH_NATIVE_METALLIB := $(BUILD_DIR)/mlx.metallib -$(BENCH_NATIVE_BIN): $(BENCH_NATIVE_SRC) | $(BUILD_DIR) +$(BENCH_NATIVE_BIN): $(BENCH_NATIVE_SRC) $(MLX_LIB_DIR)/libmlx.a Makefile | $(BUILD_DIR) $(CXX) -std=c++20 -O3 -Wall -Wextra \ -isystem $(MLX_INCLUDE_DIR) \ $(BENCH_NATIVE_SRC) \ @@ -88,11 +88,18 @@ $(BUILD_DIR): $(PRIV_DIR): @mkdir -p $(PRIV_DIR) -$(BUILD_DIR)/%.o: c_src/%.cpp $(HEADERS) | $(BUILD_DIR) +# Objects and the linked NIF also depend on libmlx.a and this Makefile so an +# existing checkout rebuilds when the MLX build changes (a version bump +# repoints MLX_LIB_DIR at a freshly built, newer libmlx.a whose headers these +# objects include) or when a compile/link flag here changes (e.g. the C++ +# standard). Without these, `make` can copy the new mlx.metallib while leaving +# a stale NIF statically linked against the old MLX in place — a mismatched +# binary until a manual clean. +$(BUILD_DIR)/%.o: c_src/%.cpp $(HEADERS) $(MLX_LIB_DIR)/libmlx.a Makefile | $(BUILD_DIR) @mkdir -p $(dir $@) $(CXX) $(CXXFLAGS) -c $< -o $@ -$(NIF_SO): $(OBJECTS) | $(PRIV_DIR) +$(NIF_SO): $(OBJECTS) $(MLX_LIB_DIR)/libmlx.a Makefile | $(PRIV_DIR) $(CXX) $(OBJECTS) -o $(NIF_SO) $(LDFLAGS) # MLX searches for mlx.metallib colocated with the loaded binary