1919
2020#include < mlx/mlx.h>
2121
22+ #include < executorch/backends/mlx/runtime/backend_options.h>
23+
2224#include < cstring>
2325#include < limits>
2426#include < memory>
@@ -169,6 +171,12 @@ struct MLXHandle {
169171 Interpreter interpreter;
170172 ::mlx::core::Stream stream; // Dedicated GPU stream for this handle
171173
174+ // Periodically call mlx::core::clear_cache() every N execute() calls to
175+ // release MLX's cached buffer pool. 0 disables. Counter is non-atomic: it is
176+ // only touched inside mlx_global_mutex() (see execute()).
177+ int clear_cache_interval_{0 };
178+ uint64_t execute_count_{0 };
179+
172180 // Keep the constant buffers alive for zero-copy constants
173181 // Each FreeableBuffer must outlive the MLX arrays that reference it
174182 std::vector<FreeableBuffer> constant_buffers;
@@ -211,6 +219,13 @@ class MLXBackend final : public ::executorch::runtime::BackendInterface {
211219 try {
212220 new (handle) MLXHandle ();
213221
222+ // Per-model clear-cache cadence (optional runtime spec, keyed per
223+ // delegate)
224+ if (auto spec = context.get_runtime_spec <int >(kClearCacheIntervalKey );
225+ spec.ok () && spec.get () > 0 ) {
226+ handle->clear_cache_interval_ = spec.get ();
227+ }
228+
214229 if (!processed || !processed->data () || processed->size () == 0 ) {
215230 throw std::runtime_error (" init: null or empty delegate payload" );
216231 }
@@ -254,8 +269,42 @@ class MLXBackend final : public ::executorch::runtime::BackendInterface {
254269 processed->Free ();
255270 processed = nullptr ;
256271
257- // Load mutable buffers (e.g., KV cache)
258- load_mutable_buffers (handle->program , handle->mutable_buffers );
272+ // Load mutable buffers (e.g., KV cache). When skip_mutable_buffer_init is
273+ // set (multi-session, where mlx_mutable_state.h allocates per-session
274+ // buffers), avoid keeping a dead default copy on the handle. This is only
275+ // safe if the init chain does not reference mutable buffers — otherwise
276+ // it would run against an empty store, so we error out clearly.
277+ bool skip_mutable_buffer_init = false ;
278+ if (auto spec = context.get_runtime_spec <bool >(kSkipMutableBufferInitKey );
279+ spec.ok ()) {
280+ skip_mutable_buffer_init = spec.get ();
281+ }
282+ // skip_mutable_buffer_init is only safe under a multi-session owner: the
283+ // per-session manager allocates the buffers and execute() rebinds to
284+ // them. Without an active load scope no handle is registered, no
285+ // per-session buffers are ever created, and execute() would run against
286+ // empty buffers. Reject the misuse loudly here instead of failing later.
287+ if (skip_mutable_buffer_init && !mutable_state_load_scope_active ()) {
288+ ET_LOG (
289+ Error,
290+ " skip_mutable_buffer_init set without an active multi-session load "
291+ " scope; mutable buffers would never be allocated" );
292+ throw std::runtime_error (
293+ " skip_mutable_buffer_init requires an active multi-session owner" );
294+ }
295+ if (skip_mutable_buffer_init &&
296+ init_chain_references_mutable_buffer (handle->program )) {
297+ ET_LOG (
298+ Error,
299+ " skip_mutable_buffer_init set but the init chain references "
300+ " mutable buffers; cannot skip default mutable-buffer init" );
301+ throw std::runtime_error (
302+ " skip_mutable_buffer_init incompatible with init chain that "
303+ " references mutable buffers" );
304+ }
305+ if (!skip_mutable_buffer_init) {
306+ load_mutable_buffers (handle->program , handle->mutable_buffers );
307+ }
259308
260309 // Bind execution state (reused across execute() calls)
261310 handle->state .bind (
@@ -433,6 +482,16 @@ class MLXBackend final : public ::executorch::runtime::BackendInterface {
433482
434483 h->state .reset (); // Release temp GPU buffers back to MLX cache
435484
485+ // Periodically release MLX's cached buffer pool. The counter increment
486+ // and clear_cache() run under the global mutex so they can't race another
487+ // handle's in-flight submission, and the counter needs no atomic.
488+ if (h->clear_cache_interval_ > 0 ) {
489+ std::lock_guard<std::mutex> lock (mlx_global_mutex ());
490+ if ((++h->execute_count_ % h->clear_cache_interval_ ) == 0 ) {
491+ ::mlx::core::clear_cache ();
492+ }
493+ }
494+
436495 return Error::Ok;
437496 } catch (const std::exception& e) {
438497 ET_LOG (Error, " MLX execute failed: %s" , e.what ());
@@ -455,7 +514,7 @@ class MLXBackend final : public ::executorch::runtime::BackendInterface {
455514
456515namespace {
457516auto cls = MLXBackend();
458- Backend backend{" MLXBackend " , &cls};
517+ Backend backend{kMLXBackendId , &cls};
459518static auto success_with_compiler = register_backend(backend);
460519} // namespace
461520
0 commit comments