From d8e4700bd29db3590e4f623677908aa407f840dd Mon Sep 17 00:00:00 2001 From: Devmate Date: Wed, 26 Aug 2026 19:15:24 -0700 Subject: [PATCH 1/3] 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) From c5cf05f9bd10866cc9e303e0032c3efb2aec0c52 Mon Sep 17 00:00:00 2001 From: Anthony Shoumikhin Date: Fri, 28 Aug 2026 21:46:50 -0700 Subject: [PATCH 2/3] Repin to the nightly that actually carries backend_mlx The pin named swiftpm-1.5.0.20260827 and the description claimed it was the first published package with the MLX product. It is not: no published branch through 20260828 declares backend_mlx, because the runtime halves only landed afterwards. The MLX backend (#22203) and its SwiftPM template half (#22261) are now on main and on the swiftpm template, so the next nightly is the first that will carry the product. Repin both demos to it. Nothing else is needed on the app side. The metallib resource bundle is a dependency of the backend_mlx product in the template, so SwiftPM attaches it automatically rather than the project having to bundle it. The demo already targets iOS 17, which matches the package floor. --- llm/apple/etLLM.xcodeproj/project.pbxproj | 2 +- .../ExecuTorchDemo/ExecuTorchDemo.xcodeproj/project.pbxproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/llm/apple/etLLM.xcodeproj/project.pbxproj b/llm/apple/etLLM.xcodeproj/project.pbxproj index b5d67cad2..eef765e09 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.5.0.20260827"; + branch = "swiftpm-1.5.0.20260829"; kind = branch; }; }; diff --git a/mv3/apple/ExecuTorchDemo/ExecuTorchDemo.xcodeproj/project.pbxproj b/mv3/apple/ExecuTorchDemo/ExecuTorchDemo.xcodeproj/project.pbxproj index 4361fcf7e..fc52c29eb 100644 --- a/mv3/apple/ExecuTorchDemo/ExecuTorchDemo.xcodeproj/project.pbxproj +++ b/mv3/apple/ExecuTorchDemo/ExecuTorchDemo.xcodeproj/project.pbxproj @@ -798,7 +798,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pytorch/executorch"; requirement = { - branch = "swiftpm-1.5.0.20260827"; + branch = "swiftpm-1.5.0.20260829"; kind = branch; }; }; From 32a992edb91d9354d97aa00a9e253296496c7a47 Mon Sep 17 00:00:00 2001 From: Anthony Shoumikhin Date: Sat, 29 Aug 2026 04:53:50 -0700 Subject: [PATCH 3/3] Test the MLX backend in the classifier test The test target already bundles mv3_mlx.pte and links backend_mlx, in the same two build phases as the Core ML equivalents, but nothing loaded the model. So the delegate was shipped and never exercised: the partitioner only warns when it tags nothing, and an MLX runtime failure would have stayed green. Adds the fourth case alongside portable, Core ML and XNNPACK. --- .../Sources/MobileNet/Test/MobileNetClassifierTest.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mv3/apple/ExecuTorchDemo/ExecuTorchDemo/Sources/MobileNet/Test/MobileNetClassifierTest.swift b/mv3/apple/ExecuTorchDemo/ExecuTorchDemo/Sources/MobileNet/Test/MobileNetClassifierTest.swift index 0738d293f..9661e07e0 100644 --- a/mv3/apple/ExecuTorchDemo/ExecuTorchDemo/Sources/MobileNet/Test/MobileNetClassifierTest.swift +++ b/mv3/apple/ExecuTorchDemo/ExecuTorchDemo/Sources/MobileNet/Test/MobileNetClassifierTest.swift @@ -25,6 +25,10 @@ final class MobileNetClassifierTest: XCTestCase { try run(model: "mv3_xnnpack_fp32") } + func testV3WithMLXBackend() throws { + try run(model: "mv3_mlx") + } + private func run(model modelName: String) throws { guard let modelFilePath = Bundle(for: type(of: self))