From 91bc44ebe0e40029af93f15b21b9e28847314856 Mon Sep 17 00:00:00 2001 From: zhen-ke Date: Tue, 27 Oct 2026 12:52:30 +0800 Subject: [PATCH 1/3] Metal backend: stage compiled .mlmodelc next to source .mlpackage MLModel.compileModel drops the .mlmodelc bundle in the process's container-shared temp dir, ignoring TMPDIR overrides. Moving it next to the source .mlpackage allows hosts managing per-launch TMPDIR to reclaim it upon process termination, preventing disk leaks. - Derives destination from mlpackagePath as the single source of truth. - Fails hard if moveItem fails instead of falling back to leaked path. - Cleans up artifacts on failure; retains bundle on success for MLModel lifetime. Fixes #1266. --- cpp/neuralnet/metalbackend.swift | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/cpp/neuralnet/metalbackend.swift b/cpp/neuralnet/metalbackend.swift index 9952624a90..35e1c13574 100644 --- a/cpp/neuralnet/metalbackend.swift +++ b/cpp/neuralnet/metalbackend.swift @@ -334,8 +334,8 @@ public class CoreMLComputeHandle { } } -/// Delete the source .mlpackage after compilation -/// CoreML caches the compiled model, so the source is no longer needed +/// Remove a temp bundle this function created and is not handing to MLModel. +/// A cleanup failure is logged, not thrown: it must not mask the error that caused it. private func deleteSourceModel(at url: URL, serverThreadIdx: Int) { do { try FileManager.default.removeItem(at: url) @@ -375,8 +375,29 @@ public func createCoreMLComputeHandle( printError("Metal backend \(serverThreadIdx): Compiling model...") let compiledURL = try MLModel.compileModel(at: mlpackagePath) + // compileModel(at:) writes the .mlmodelc into NSTemporaryDirectory(), which + // ignores TMPDIR. Move it next to the .mlpackage the C++ side passed in + // (temp_directory_path(), which honors TMPDIR) before loading, so the bundle + // shares that directory's lifetime. Derive the destination from that path + // rather than resolving the temp directory again here. + // MLModel may read the bundle lazily, so a successful load retains it. + // Only a bundle that never gets handed back is removed. + let stagedURL = mlpackagePath.deletingLastPathComponent() + .appendingPathComponent(compiledURL.lastPathComponent) + var unusedCompiledURL: URL? = compiledURL + defer { + if let unusedCompiledURL { + deleteSourceModel(at: unusedCompiledURL, serverThreadIdx: serverThreadIdx) + } + } + if stagedURL.standardizedFileURL != compiledURL.standardizedFileURL { + try FileManager.default.moveItem(at: compiledURL, to: stagedURL) + unusedCompiledURL = stagedURL + } + printError("Metal backend \(serverThreadIdx): Loading compiled model...") - let model = try MLModel(contentsOf: compiledURL, configuration: config) + let model = try MLModel(contentsOf: stagedURL, configuration: config) + unusedCompiledURL = nil printError("Metal backend \(serverThreadIdx): Model loaded successfully, \(context.nnXLen)x\(context.nnYLen)") From 5f6aacfb77e40fa3ae8e15fd411c1613ce036f7d Mon Sep 17 00:00:00 2001 From: zhen-ke Date: Tue, 27 Oct 2026 18:45:07 +0800 Subject: [PATCH 2/3] Metal backend: do not delete compiled bundles on failure A failed move warns and loads from the compileModel URL instead of failing the load. Leave the compiled bundle for the caller or the system to reclaim. --- cpp/neuralnet/metalbackend.swift | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/cpp/neuralnet/metalbackend.swift b/cpp/neuralnet/metalbackend.swift index 35e1c13574..471aa3dcb3 100644 --- a/cpp/neuralnet/metalbackend.swift +++ b/cpp/neuralnet/metalbackend.swift @@ -380,24 +380,21 @@ public func createCoreMLComputeHandle( // (temp_directory_path(), which honors TMPDIR) before loading, so the bundle // shares that directory's lifetime. Derive the destination from that path // rather than resolving the temp directory again here. - // MLModel may read the bundle lazily, so a successful load retains it. - // Only a bundle that never gets handed back is removed. + // A failed move is not fatal: load from where compileModel left the bundle. + // Do not delete the compiled bundle on either path. let stagedURL = mlpackagePath.deletingLastPathComponent() .appendingPathComponent(compiledURL.lastPathComponent) - var unusedCompiledURL: URL? = compiledURL - defer { - if let unusedCompiledURL { - deleteSourceModel(at: unusedCompiledURL, serverThreadIdx: serverThreadIdx) - } - } - if stagedURL.standardizedFileURL != compiledURL.standardizedFileURL { + let modelURL: URL + do { try FileManager.default.moveItem(at: compiledURL, to: stagedURL) - unusedCompiledURL = stagedURL + modelURL = stagedURL + } catch { + printError("Metal backend \(serverThreadIdx): Warning: Failed to move compiled model to \(stagedURL.path), loading it from \(compiledURL.path): \(error)") + modelURL = compiledURL } printError("Metal backend \(serverThreadIdx): Loading compiled model...") - let model = try MLModel(contentsOf: stagedURL, configuration: config) - unusedCompiledURL = nil + let model = try MLModel(contentsOf: modelURL, configuration: config) printError("Metal backend \(serverThreadIdx): Model loaded successfully, \(context.nnXLen)x\(context.nnYLen)") From ab94e23a511d70c95f83c30209ced567880b2bd4 Mon Sep 17 00:00:00 2001 From: zhen-ke Date: Thu, 29 Oct 2026 08:37:26 +0800 Subject: [PATCH 3/3] Metal backend: update comments per review --- cpp/neuralnet/metalbackend.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/neuralnet/metalbackend.swift b/cpp/neuralnet/metalbackend.swift index 471aa3dcb3..a7ee8126aa 100644 --- a/cpp/neuralnet/metalbackend.swift +++ b/cpp/neuralnet/metalbackend.swift @@ -334,8 +334,8 @@ public class CoreMLComputeHandle { } } -/// Remove a temp bundle this function created and is not handing to MLModel. -/// A cleanup failure is logged, not thrown: it must not mask the error that caused it. +/// Delete the source .mlpackage after compilation +/// CoreML caches the compiled model, so the source is no longer needed private func deleteSourceModel(at url: URL, serverThreadIdx: Int) { do { try FileManager.default.removeItem(at: url) @@ -381,7 +381,7 @@ public func createCoreMLComputeHandle( // shares that directory's lifetime. Derive the destination from that path // rather than resolving the temp directory again here. // A failed move is not fatal: load from where compileModel left the bundle. - // Do not delete the compiled bundle on either path. + // Do not delete the compiled bundle on either path: MLModel may read it lazily. let stagedURL = mlpackagePath.deletingLastPathComponent() .appendingPathComponent(compiledURL.lastPathComponent) let modelURL: URL