Repository navigation
Description
Summary
createCoreMLComputeHandle in cpp/neuralnet/metalbackend.swift calls
MLModel.compileModel(at:) to compile the converted .mlpackage, loads the
result, and then discards compiledURL. The compiled .mlmodelc bundle
(~50 MiB for typical nets) is never moved, saved, or deleted — it leaks
permanently on disk on every engine startup that uses the CoreML/ANE path.
Details
The conversion chain writes to two different temp locations:
CoreMLConversion::generateTempPathplaces the.mlpackageunder
std::filesystem::temp_directory_path()/katago_coreml/, which honors the
TMPDIRenv var. The Swift sidedefers deletion of this source package.MLModel.compileModel(at:)writes its output into the process's
NSTemporaryDirectory(). On macOS this resolves viaconfstrto the
sandboxed container's sharedData/tmp(or/var/folders/.../T/for
unsandboxed runs) and does not honor aTMPDIRoverride set by the
parent process.
Per Apple's documentation, compileModel(at:) returns a URL "in a temporary
location" that the caller owns — KataGo currently drops this ownership on the
floor.
Reproduction
Verified on v1.17.2 (6a1fc5de), same code on master.
Run katago analysis with a Mux topology that assigns ANE threads, e.g.
metalDeviceToUseThreadN=100 for two threads, then quit the process:
- Each ANE handle produces one
model_<pid>_<threadIdx>_<ts>.mlmodelc
bundle inNSTemporaryDirectory(). - The bundles persist after process exit (verified with
ps— no katago
processes remain). - Measured: 2 bundles ≈ 101 MiB per engine launch; 3 sequential
launch/quit cycles left 6 bundles ≈ 305 MiB.
Hosts that give each engine launch an exclusive TMPDIR and reclaim it on
process exit cannot reach these files, so cleanup only happens if someone
sweeps the shared temp dir by name. GPU-only (MPSGraph) threads are unaffected.
Suggested fix
Keep the compiled bundle inside KataGo-owned lifetime:
- After
compileModelreturns, movecompiledURLnext to the source
.mlpackage(or into another KataGo-managed temp dir), then load the model
from the moved URL. - Retain the bundle for the
CoreMLComputeHandle's lifetime —MLModelmay
touch its files lazily, so deleting right after load is unsafe. - Delete the bundle when the handle is torn down (e.g. store the URL on the
handle and remove it in a teardown/deinit path, after theMLModelis
released). This keeps ownership fully inside KataGo and does not rely on
any host-side temp-dir reclaim.
Moving alone (step 1 without 3) already contains the leak for hosts that
reclaim per-process TMPDIR, but step 3 makes the fix self-contained for
standalone use as well.
I have a tested patch implementing steps 1–2 (verified: zero .mlmodelc
residue in shared tmp after engine exit, ~30 lines) and can open a PR —
happy to extend it with handle-teardown cleanup if that is preferred.
Activity
ChinChangYang commentedon Oct 18, 2026
Thanks for the detailed report and the reproduction. I'd rather not have KataGo delete the compiled bundles at teardown (step 3), because letting KataGo modify or remove files introduces lock issues, race conditions, and unsafe behavior. MLModel may still read the bundle lazily, and teardown never runs after a crash or SIGKILL anyway, so caches KataGo creates should be cleaned up by the system or the user, not by KataGo. The bundles aren't stranded either: by default they land in the per-user Darwin temp directory, where macOS's com.apple.bsd.dirhelper removes files not accessed for 3 days. The real problem is which temp folder is used: the C++ side writes the .mlpackage under temp_directory_path(), which honors TMPDIR, while compileModel(at:) writes to NSTemporaryDirectory(), which ignores a TMPDIR override (I confirmed this locally). So a host that gives each launch its own TMPDIR never sees these files, and only a KataGo-side change can fix that, since NSTemporaryDirectory() can't be redirected from outside. Moving the compiled bundle next to the source .mlpackage (your steps 1–2) is the right fix: it's KataGo's own freshly created file, moved once before loading, which is how Apple says to keep compileModel output around. Please derive the destination from the .mlpackage path the C++ side passes in, rather than working out the temp directory again in Swift, so there's a single source of truth. On iOS, tvOS, and visionOS, the sandbox already points TMPDIR and NSTemporaryDirectory() at the same container tmp/, so the change does nothing there, and apps that want to reuse compiled models across launches should manage that cache themselves. I'm happy to review a PR limited to steps 1–2.
zhen-ke commentedon Oct 27, 2026
Thanks for the feedback! I agree with scoping the fix strictly to steps 1–2.
In sandboxed containers,
dirhelperdoesn't scan the container'sData/tmp, so moving the bundle alongside the source.mlpackageinto the host-managedTMPDIRis indeed the most reliable way to ensure prompt cleanup upon process exit without needing teardown deletion in KataGo.I have opened PR #1274 with this change.