From d8e4700bd29db3590e4f623677908aa407f840dd Mon Sep 17 00:00:00 2001 From: Devmate Date: Wed, 26 Aug 2026 19:15:24 -0700 Subject: [PATCH] Add the MLX backend to the mv3 Apple demo Follow-up to removing the MPS backend. The mv3 image-classification demo now offers MLX, ExecuTorch's Apple GPU backend, as a third option next to Core ML and XNNPACK. Both demo Xcode projects are repinned from the old swiftpm-1.1.0 SwiftPM branch to the nightly swiftpm-1.5.0.20260827, which is the first published package that carries the new backend_mlx product. (This will be repinned to the 1.5 release branch once that is cut.) In the mv3 demo: - export.py lowers MobileNet v3 to MLX with MLXPartitioner and writes mv3_mlx.pte, the same shape as the existing Core ML and XNNPACK exports. MLX lowering is ahead-of-time and does not import the mlx runtime package, so it runs on the same Linux export runner as the others. - the model selector gains an MLX case that loads mv3_mlx.pte. - the Xcode project links executorch::backend_mlx from the package and bundles mv3_mlx.pte, mirroring exactly how backend_coreml and mv3_coreml_all.pte are wired. - the export workflow's artifact list includes mv3_mlx.pte. Test Plan: Verified the wheel side of MLX end to end on macOS arm64 in a fresh venv: exported MobileNet v3 with MLXPartitioner (all ops delegated), ran it through the pybindings runtime, and matched eager to 1.2e-07. Also built a C++ app with find_package(executorch) that links executorch::backend_mlx and ran the same delegated program, exit 0. export.py parses. The export workflow is valid YAML and lists all four mv3 outputs. ClassificationController.swift parses with swiftc and its Mode switch is exhaustive over the three cases. Both Xcode projects were checked with a real plist parser (plutil -convert json). The mv3 project has 116 objects with zero ids referenced-but-undefined and zero defined-but-unreferenced. backend_mlx is wired into all eight places backend_coreml is (build files, framework phase, product dependency, product block for both the app and test targets) and mv3_mlx.pte into all six places mv3_coreml_all.pte is. The etLLM project change is only the one-line branch repin. plutil -lint passes on both. Known dependency: build-demo-ios in executorch stages the demo's .pte files with its own export scripts (portable, Core ML, XNNPACK). That job needs an MLX export step added before mv3_mlx.pte is produced in CI; until then the MLX button has no model in that build. Filed separately. --- .github/workflows/export-models.yml | 1 + llm/apple/etLLM.xcodeproj/project.pbxproj | 2 +- .../ExecuTorchDemo.xcodeproj/project.pbxproj | 24 ++++++++++++++++++- .../App/ClassificationController.swift | 3 +++ mv3/python/export.py | 8 +++++++ 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.github/workflows/export-models.yml b/.github/workflows/export-models.yml index baa8e7713..73e299fbf 100644 --- a/.github/workflows/export-models.yml +++ b/.github/workflows/export-models.yml @@ -36,6 +36,7 @@ jobs: output: | mv3/python/mv3.pte mv3/python/mv3_coreml_all.pte + mv3/python/mv3_mlx.pte mv3/python/mv3_xnnpack_fp32.pte name: Export ${{ matrix.name }} diff --git a/llm/apple/etLLM.xcodeproj/project.pbxproj b/llm/apple/etLLM.xcodeproj/project.pbxproj index 2528976e5..b5d67cad2 100644 --- a/llm/apple/etLLM.xcodeproj/project.pbxproj +++ b/llm/apple/etLLM.xcodeproj/project.pbxproj @@ -620,7 +620,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pytorch/executorch"; requirement = { - branch = "swiftpm-1.1.0"; + branch = "swiftpm-1.5.0.20260827"; kind = branch; }; }; diff --git a/mv3/apple/ExecuTorchDemo/ExecuTorchDemo.xcodeproj/project.pbxproj b/mv3/apple/ExecuTorchDemo/ExecuTorchDemo.xcodeproj/project.pbxproj index 324f4eb83..4361fcf7e 100644 --- a/mv3/apple/ExecuTorchDemo/ExecuTorchDemo.xcodeproj/project.pbxproj +++ b/mv3/apple/ExecuTorchDemo/ExecuTorchDemo.xcodeproj/project.pbxproj @@ -15,13 +15,16 @@ 032C02032AC47CFB002955E1 /* mv3_xnnpack_fp32.pte in Resources */ = {isa = PBXBuildFile; fileRef = 032C01FC2AC47CFB002955E1 /* mv3_xnnpack_fp32.pte */; }; 032C02082AC47CFB002955E1 /* imagenet_classes.txt in Resources */ = {isa = PBXBuildFile; fileRef = 032C02012AC47CFB002955E1 /* imagenet_classes.txt */; }; 03672A662DE1226100147123 /* backend_coreml in Frameworks */ = {isa = PBXBuildFile; productRef = 03672A652DE1226100147123 /* backend_coreml */; }; + CAFE00010000000000000000 /* backend_mlx in Frameworks */ = {isa = PBXBuildFile; productRef = CAFE00010000000000000002 /* backend_mlx */; }; 03672A6A2DE1226100147123 /* backend_xnnpack in Frameworks */ = {isa = PBXBuildFile; productRef = 03672A692DE1226100147123 /* backend_xnnpack */; }; 03672A752DE144CF00147123 /* executorch_debug in Frameworks */ = {isa = PBXBuildFile; productRef = 03672A742DE144CF00147123 /* executorch_debug */; }; 03672A772DE1457900147123 /* backend_coreml in Frameworks */ = {isa = PBXBuildFile; productRef = 03672A762DE1457900147123 /* backend_coreml */; }; + CAFE00010000000000000001 /* backend_mlx in Frameworks */ = {isa = PBXBuildFile; productRef = CAFE00010000000000000003 /* backend_mlx */; }; 03672A7B2DE1457900147123 /* backend_xnnpack in Frameworks */ = {isa = PBXBuildFile; productRef = 03672A7A2DE1457900147123 /* backend_xnnpack */; }; 036834D52ACB710D00BA100F /* mv3.pte in Resources */ = {isa = PBXBuildFile; fileRef = 036834D42ACB710D00BA100F /* mv3.pte */; }; 036834D62ACB710D00BA100F /* mv3.pte in Resources */ = {isa = PBXBuildFile; fileRef = 036834D42ACB710D00BA100F /* mv3.pte */; }; 036834D82ACB893700BA100F /* mv3_coreml_all.pte in Resources */ = {isa = PBXBuildFile; fileRef = 036834D72ACB893700BA100F /* mv3_coreml_all.pte */; }; + CAFE00010000000000000004 /* mv3_mlx.pte in Resources */ = {isa = PBXBuildFile; fileRef = CAFE00010000000000000006 /* mv3_mlx.pte */; }; 03B184962E1FA1E400246294 /* kernels_optimized in Frameworks */ = {isa = PBXBuildFile; productRef = 03B184952E1FA1E400246294 /* kernels_optimized */; }; 03B184982E1FA1F400246294 /* kernels_optimized in Frameworks */ = {isa = PBXBuildFile; productRef = 03B184972E1FA1F400246294 /* kernels_optimized */; }; 03C818192AC671980084CC29 /* Samoyed.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03C818182AC671980084CC29 /* Samoyed.jpg */; }; @@ -37,6 +40,7 @@ 03C818482AC7BF470084CC29 /* imagenet_classes.txt in Resources */ = {isa = PBXBuildFile; fileRef = 032C02012AC47CFB002955E1 /* imagenet_classes.txt */; }; 03C8184A2AC7BF470084CC29 /* mv3_xnnpack_fp32.pte in Resources */ = {isa = PBXBuildFile; fileRef = 032C01FC2AC47CFB002955E1 /* mv3_xnnpack_fp32.pte */; }; 03C8DC7C2AE2533C0064384A /* mv3_coreml_all.pte in Resources */ = {isa = PBXBuildFile; fileRef = 036834D72ACB893700BA100F /* mv3_coreml_all.pte */; }; + CAFE00010000000000000005 /* mv3_mlx.pte in Resources */ = {isa = PBXBuildFile; fileRef = CAFE00010000000000000006 /* mv3_mlx.pte */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -96,6 +100,7 @@ 032C02012AC47CFB002955E1 /* imagenet_classes.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = imagenet_classes.txt; sourceTree = ""; }; 036834D42ACB710D00BA100F /* mv3.pte */ = {isa = PBXFileReference; lastKnownFileType = file; path = mv3.pte; sourceTree = ""; }; 036834D72ACB893700BA100F /* mv3_coreml_all.pte */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = mv3_coreml_all.pte; sourceTree = ""; }; + CAFE00010000000000000006 /* mv3_mlx.pte */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = mv3_mlx.pte; sourceTree = ""; }; 03C8180A2AC66FC30084CC29 /* MobileNetClassifierTest.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MobileNetClassifierTest.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 03C818182AC671980084CC29 /* Samoyed.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Samoyed.jpg; sourceTree = ""; }; 03C8181C2AC69C280084CC29 /* Arctic fox.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "Arctic fox.jpg"; sourceTree = ""; }; @@ -116,6 +121,7 @@ 03672A6A2DE1226100147123 /* backend_xnnpack in Frameworks */, 03B184962E1FA1E400246294 /* kernels_optimized in Frameworks */, 03672A662DE1226100147123 /* backend_coreml in Frameworks */, + CAFE00010000000000000000 /* backend_mlx in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -136,6 +142,7 @@ 03672A7B2DE1457900147123 /* backend_xnnpack in Frameworks */, 03B184982E1FA1F400246294 /* kernels_optimized in Frameworks */, 03672A772DE1457900147123 /* backend_coreml in Frameworks */, + CAFE00010000000000000001 /* backend_mlx in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -232,6 +239,7 @@ children = ( 032C02012AC47CFB002955E1 /* imagenet_classes.txt */, 036834D72ACB893700BA100F /* mv3_coreml_all.pte */, + CAFE00010000000000000006 /* mv3_mlx.pte */, 032C01FC2AC47CFB002955E1 /* mv3_xnnpack_fp32.pte */, 036834D42ACB710D00BA100F /* mv3.pte */, ); @@ -290,6 +298,7 @@ name = App; packageProductDependencies = ( 03672A652DE1226100147123 /* backend_coreml */, + CAFE00010000000000000002 /* backend_mlx */, 03672A692DE1226100147123 /* backend_xnnpack */, 03B184952E1FA1E400246294 /* kernels_optimized */, ); @@ -334,6 +343,7 @@ name = MobileNetClassifierTest; packageProductDependencies = ( 03672A762DE1457900147123 /* backend_coreml */, + CAFE00010000000000000003 /* backend_mlx */, 03672A7A2DE1457900147123 /* backend_xnnpack */, 03B184972E1FA1F400246294 /* kernels_optimized */, ); @@ -411,6 +421,7 @@ buildActionMask = 2147483647; files = ( 036834D82ACB893700BA100F /* mv3_coreml_all.pte in Resources */, + CAFE00010000000000000004 /* mv3_mlx.pte in Resources */, 036834D52ACB710D00BA100F /* mv3.pte in Resources */, 032C02082AC47CFB002955E1 /* imagenet_classes.txt in Resources */, 032C02032AC47CFB002955E1 /* mv3_xnnpack_fp32.pte in Resources */, @@ -428,6 +439,7 @@ 03C818192AC671980084CC29 /* Samoyed.jpg in Resources */, 03C8181F2AC69DFF0084CC29 /* hot pot.jpg in Resources */, 03C8DC7C2AE2533C0064384A /* mv3_coreml_all.pte in Resources */, + CAFE00010000000000000005 /* mv3_mlx.pte in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -786,7 +798,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pytorch/executorch"; requirement = { - branch = "swiftpm-1.1.0"; + branch = "swiftpm-1.5.0.20260827"; kind = branch; }; }; @@ -798,6 +810,11 @@ package = 03672A642DE1226100147123 /* XCRemoteSwiftPackageReference "executorch" */; productName = backend_coreml; }; + CAFE00010000000000000002 /* backend_mlx */ = { + isa = XCSwiftPackageProductDependency; + package = 03672A642DE1226100147123 /* XCRemoteSwiftPackageReference "executorch" */; + productName = backend_mlx; + }; 03672A692DE1226100147123 /* backend_xnnpack */ = { isa = XCSwiftPackageProductDependency; package = 03672A642DE1226100147123 /* XCRemoteSwiftPackageReference "executorch" */; @@ -813,6 +830,11 @@ package = 03672A642DE1226100147123 /* XCRemoteSwiftPackageReference "executorch" */; productName = backend_coreml; }; + CAFE00010000000000000003 /* backend_mlx */ = { + isa = XCSwiftPackageProductDependency; + package = 03672A642DE1226100147123 /* XCRemoteSwiftPackageReference "executorch" */; + productName = backend_mlx; + }; 03672A7A2DE1457900147123 /* backend_xnnpack */ = { isa = XCSwiftPackageProductDependency; package = 03672A642DE1226100147123 /* XCRemoteSwiftPackageReference "executorch" */; diff --git a/mv3/apple/ExecuTorchDemo/ExecuTorchDemo/Sources/App/ClassificationController.swift b/mv3/apple/ExecuTorchDemo/ExecuTorchDemo/Sources/App/ClassificationController.swift index 5b0b7894a..b721ba08b 100644 --- a/mv3/apple/ExecuTorchDemo/ExecuTorchDemo/Sources/App/ClassificationController.swift +++ b/mv3/apple/ExecuTorchDemo/ExecuTorchDemo/Sources/App/ClassificationController.swift @@ -13,6 +13,7 @@ import SwiftUI enum Mode: String, CaseIterable { case xnnpack = "XNNPACK" case coreML = "Core ML" + case mlx = "MLX" } class ClassificationController: ObservableObject { @@ -62,6 +63,8 @@ class ClassificationController: ObservableObject { switch mode { case .coreML: modelFileName = "mv3_coreml_all" + case .mlx: + modelFileName = "mv3_mlx" case .xnnpack: modelFileName = "mv3_xnnpack_fp32" } diff --git a/mv3/python/export.py b/mv3/python/export.py index a9258e9cc..a4143daa3 100644 --- a/mv3/python/export.py +++ b/mv3/python/export.py @@ -8,6 +8,7 @@ import torch import torchvision.models as models from executorch.backends.apple.coreml.partition import CoreMLPartitioner +from executorch.backends.mlx import MLXPartitioner from executorch.exir import to_edge from executorch.backends.xnnpack.partition.xnnpack_partitioner import XnnpackPartitioner from executorch.exir import EdgeCompileConfig, to_edge_transform_and_lower @@ -27,6 +28,11 @@ def main() -> None: compile_config=EdgeCompileConfig(_skip_dim_order=True), ).to_executorch() + et_program_mlx = to_edge_transform_and_lower( + torch.export.export(model, sample_inputs), + partitioner=[MLXPartitioner()], + ).to_executorch() + et_program_xnnpack = to_edge_transform_and_lower( torch.export.export(model, sample_inputs), partitioner=[XnnpackPartitioner()], @@ -36,6 +42,8 @@ def main() -> None: et_program_portable.write_to_file(file) with open("mv3_coreml_all.pte", "wb") as file: et_program_coreml.write_to_file(file) + with open("mv3_mlx.pte", "wb") as file: + et_program_mlx.write_to_file(file) with open("mv3_xnnpack_fp32.pte", "wb") as file: et_program_xnnpack.write_to_file(file)