fix: bound MPS embedding memory on Apple Silicon - #244
Conversation
georgeh0
left a comment
There was a problem hiding this comment.
thanks for the improvement!
Actually the underlying cocoindex engine has a subprocess mode for GPU workload. Currently we can enable it by setting COCOINDEX_RUN_GPU_IN_SUBPROCESS=1 (doc). I'm wondering will it make the code change here simpler by using that (e.g. we may set the env at initialization time, and happy to explore easier ways to toggle this)
e4d0f0e to
861aa08
Compare
|
Thanks for catching this earlier. I reworked the implementation around the upstream primitives and rerun the Apple Silicon benchmarks. The experiments confirmed that process isolation and active memory reclamation are separate concerns:
Updated ImplementationBased on these findings, the revised implementation:
This allowed us to strip out our custom worker, IPC protocol, manual batch-halving, recycling thresholds, and the daemon-wide indexing gate entirely. Benchmark Results & Validation
Conclusion & Follow-upGiven these measurements, active worker recycling is not required for our memory safety goals. Combining the standard runner, conservative watermarks, native smaller-batch retries, and post-index cleanup provides the leanest implementation while preserving warm-model latency across requests. Follow-up Scope: |
Problem
On a 48 GiB Apple Silicon machine, one long-lived daemon was observed at roughly 54 GiB of process footprint. About 53.6 GiB was attributed to IOAccelerator/Metal while the Python heap remained small. Stack samples consistently pointed at SentenceTransformer encode, an MPS-to-CPU copy, and Metal command-buffer completion.
The existing failure path only called
torch.mps.empty_cache()after an OOM. That is too late here: the default MPS allocator watermarks scale fromrecommended_max_memory, so system pressure can become severe before PyTorch raises. Cache cleanup also cannot guarantee that a long-lived process returns all driver-owned Metal allocations.Multiple projects could compound the problem by indexing concurrently through the same daemon.
Approach
spawnworker. The child owns the model and Metal allocations, so recycling it gives the daemon a deterministic reclamation boundary.Behavior and compatibility
No configuration migration is required. The guard applies to SentenceTransformer models on explicit MPS, or automatic device selection on macOS. All controls are optional and available under
embedding:batch_sizedefaults to 8 on the worker pathmps_memory_limit_ratiodefaults to 0.35mps_low_watermark_ratiodefaults to 0.40mps_high_watermark_ratiodefaults to 0.50worker_timeout_secondsdefaults to 300Cross-project indexing is intentionally serialized. This trades aggregate indexing throughput for bounded unified-memory pressure. Search remains available; a project waits only when it has not completed its initial index.
Validation
uv run prek run --all-files --verbose --show-diff-on-failurecodefuse-ai/F2LLM-v2-0.6BScope
This PR is intentionally independent of #243. That PR prevents a multi-client daemon-startup stampede; this PR bounds memory inside the daemon after startup. They address separate failure modes and can land in either order.