Skip to content

Commit b055208

Browse files
committed
Update
[ghstack-poisoned]
2 parents 3e89da1 + 67ca019 commit b055208

35 files changed

Lines changed: 1126 additions & 46 deletions

backends/arm/tosa/specification.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
import re
1414
from typing import Dict, Generic, List, Set, TypeVar
1515

16+
# Import torch before any torch submodule so torch.SymInt is defined; importing
17+
# torch.fx first triggers a torch.distributed circular import during collection.
18+
import torch # noqa: F401
19+
1620
from packaging.version import Version
1721
from torch.fx.experimental.symbolic_shapes import ShapeEnv
1822

backends/mlx/llm/sampling.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212

1313
class SamplingHead(nn.Module):
1414
"""
15-
Wraps a model that returns logits and samples a token id on-device.
15+
Wraps a model that returns last-token logits ``(B, vocab)`` and samples a
16+
token id ``(B)`` on-device.
1617
1718
forward(*model_args, temperature, top_k=None, top_p=1.0, seed=None,
1819
**model_kwargs) -> token_id
@@ -33,12 +34,11 @@ def __init__(self, model: nn.Module):
3334
self.model = model
3435

3536
def forward(self, *args, temperature, top_k=None, top_p=1.0, seed=None, **kwargs):
36-
logits = self.model(*args, **kwargs) # [B, S, vocab]
37-
last = logits[:, -1, :] # [B, vocab]
37+
logits = self.model(*args, **kwargs) # [B, vocab]
3838
if not isinstance(top_p, torch.Tensor):
3939
top_p = torch.tensor(float(top_p))
4040
if top_k is None:
4141
top_k = torch.tensor(torch.iinfo(torch.int64).max, dtype=torch.int64)
4242
elif not isinstance(top_k, torch.Tensor):
4343
top_k = torch.tensor(int(top_k), dtype=torch.int64)
44-
return torch.ops.mlx.sample(last, temperature, top_k, top_p, seed)
44+
return torch.ops.mlx.sample(logits, temperature, top_k, top_p, seed)

backends/mlx/runtime/MLXBackend.cpp

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
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

456515
namespace {
457516
auto cls = MLXBackend();
458-
Backend backend{"MLXBackend", &cls};
517+
Backend backend{kMLXBackendId, &cls};
459518
static auto success_with_compiler = register_backend(backend);
460519
} // namespace
461520

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
// Shared option keys for the MLX backend. Included by the backend itself
10+
// (to read the per-model runtime spec) and by callers/runners (to set it via
11+
// a LoadBackendOptionsMap), keeping the string literals in one place.
12+
13+
#pragma once
14+
15+
namespace executorch {
16+
namespace backends {
17+
namespace mlx {
18+
19+
// Backend id under which the MLX backend registers (see MLXBackend.cpp).
20+
inline constexpr char kMLXBackendId[] = "MLXBackend";
21+
22+
// Per-model runtime-spec key. Value N means: call mlx::core::clear_cache()
23+
// every N execute() calls to release MLX's cached buffer pool; 0/unset
24+
// disables.
25+
//
26+
// NOTE on granularity: MLX's buffer cache is a process-global singleton, so the
27+
// flush is global even though this key is read per delegate handle. The counter
28+
// is per-handle, so with a single MLX handle (the common case — gemma's
29+
// prefill/decode share one "forward" method) the cadence is exactly "every N
30+
// forwards"; if a process loads multiple MLX handles, the effective cadence is
31+
// the aggregate of their executes and any handle's flush frees the shared pool
32+
// for all. This bounds resident-*average*: between flushes the cache can still
33+
// grow to MLX's default ceiling, and each flush is followed by a cold-cache
34+
// realloc. A future set_cache_limit-style key could complement this by bounding
35+
// peak footprint continuously.
36+
inline constexpr char kClearCacheIntervalKey[] = "clear_cache_interval";
37+
38+
// Per-model runtime-spec key (bool). When true, the handle does not allocate
39+
// its own default mutable buffers at init() — per-session buffers are managed
40+
// by mlx_mutable_state.h instead. Only valid for multi-session loads, and only
41+
// when the program's init chain does not reference mutable buffers (init()
42+
// errors otherwise). Saves one full mutable-buffer (KV-cache) copy per handle.
43+
inline constexpr char kSkipMutableBufferInitKey[] = "skip_mutable_buffer_init";
44+
45+
} // namespace mlx
46+
} // namespace backends
47+
} // namespace executorch

backends/mlx/runtime/mlx_mutable_state.cpp

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,52 @@ int64_t mutable_state_bytes_per_session(MutableStateContext ctx) {
136136
return 0;
137137
}
138138
int64_t total = 0;
139+
// Per-session mutable buffers are allocated from program metadata
140+
// (mutable_buffer_map + tensor_meta), independent of whether the handle kept
141+
// a default copy. Compute the estimate from metadata so it stays correct even
142+
// when default mutable-buffer init was skipped (skip_mutable_buffer_init).
139143
for (const auto& kv : it->second.handles) {
140-
const MutableBufferData* bufs = kv.second.default_buffers;
141-
if (bufs == nullptr) {
144+
const MLXProgram* program = kv.second.program;
145+
if (program == nullptr) {
142146
continue;
143147
}
144-
for (const auto& t : bufs->tensors) {
145-
if (t.has_value()) {
146-
total += static_cast<int64_t>(t->nbytes());
148+
for (const auto& slot : program->mutable_buffer_map) {
149+
if (slot.slot_type != SlotType::TensorSlot ||
150+
slot.idx >= program->tensor_meta.size() ||
151+
!program->tensor_meta[slot.idx].has_value()) {
152+
continue;
147153
}
154+
const auto& meta = *program->tensor_meta[slot.idx];
155+
// Sum sizes from metadata, clamping each tensor to kMaxAllocationBytes so
156+
// malformed (oversized) shapes in an untrusted program can't overflow the
157+
// accumulator. Real allocations are independently bounded by
158+
// check_allocation_bounded at load_mutable_buffers.
159+
uint64_t bytes = static_cast<uint64_t>(
160+
::mlx::core::size_of(resolve_dtype(meta.scalar_type)));
161+
bool dynamic = false;
162+
for (const auto& dim : meta.shape) {
163+
if (dim.value < 0) {
164+
dynamic = true;
165+
break;
166+
}
167+
const uint64_t d = static_cast<uint64_t>(dim.value);
168+
if (d == 0) {
169+
bytes = 0;
170+
break;
171+
}
172+
if (bytes > kMaxAllocationBytes / d) {
173+
bytes = kMaxAllocationBytes; // clamp; avoids overflow
174+
} else {
175+
bytes *= d;
176+
}
177+
}
178+
if (dynamic) {
179+
continue;
180+
}
181+
if (bytes > kMaxAllocationBytes) {
182+
bytes = kMaxAllocationBytes;
183+
}
184+
total += static_cast<int64_t>(bytes);
148185
}
149186
}
150187
return total;
@@ -217,6 +254,10 @@ void mutable_state_note_handle(
217254
handle_ctx()[handle] = tl_loading_ctx;
218255
}
219256

257+
bool mutable_state_load_scope_active() {
258+
return tl_loading_ctx != kInvalidMutableContext;
259+
}
260+
220261
void mutable_state_forget_handle(const void* handle) {
221262
std::lock_guard<std::mutex> g(registry_mutex());
222263
auto hit = handle_ctx().find(handle);

backends/mlx/runtime/mlx_mutable_state.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,12 @@ void mutable_state_note_handle(
195195

196196
void mutable_state_forget_handle(const void* handle);
197197

198+
// True if a multi-session load scope (with_load_scope) is active on the current
199+
// thread. The MLXBackend uses this to reject skip_mutable_buffer_init when no
200+
// owner is active for the load — without an owner, no per-session buffers are
201+
// ever allocated and execute() would run against empty mutable buffers.
202+
bool mutable_state_load_scope_active();
203+
198204
::executorch::runtime::Error mutable_state_rebind_for_execute(
199205
const void* handle,
200206
ExecutionState& state);

backends/mlx/serialization/MLXLoader.h.tmpl

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,25 @@ struct Instruction {
118118
}
119119
};
120120

121+
// =============================================================================
122+
// for_each_tid - invokes cb(Tid) for every tensor id referenced by an
123+
// instruction's node (AUTO-GENERATED from schema.fbs).
124+
//
125+
// NOTE: nested chain-index fields (ScanNode::body_chain_idx,
126+
// IfNode::then_chain_idx/else_chain_idx) are NOT followed here; callers that
127+
// need transitive coverage must recurse into those chains themselves.
128+
// =============================================================================
129+
130+
template <typename F>
131+
inline void for_each_tid(const Instruction& instr, F&& cb) {
132+
switch (instr.op) {
133+
{{FOR_EACH_TID_CASES}}
134+
default:
135+
throw std::runtime_error(
136+
std::string("for_each_tid: unhandled op ") + op_name(instr.op));
137+
}
138+
}
139+
121140
// =============================================================================
122141
// SlotVariant for I/O mapping
123142
// =============================================================================
@@ -194,6 +213,65 @@ struct MLXProgram {
194213
}
195214
};
196215

216+
// =============================================================================
217+
// init_chain_references_mutable_buffer - returns true if the program's init
218+
// chain references any mutable-buffer Tid, following nested SCAN/IF chains.
219+
//
220+
// Used to decide whether the handle's default mutable-buffer initialization can
221+
// be safely skipped (e.g. when buffers are managed per-session). If the init
222+
// chain reads/writes mutable buffers, skipping would be unsafe.
223+
// =============================================================================
224+
inline bool init_chain_references_mutable_buffer(const MLXProgram& program) {
225+
if (program.init_chain_idx < 0 || program.num_mutable_buffer_tensors == 0) {
226+
return false;
227+
}
228+
// Tid layout: Constant -> Input -> Output -> MutableBuffer -> Temp.
229+
const uint32_t output_end = program.num_constant_tensors +
230+
program.num_input_tensors + program.num_output_tensors;
231+
const uint32_t mutable_buffer_end =
232+
output_end + program.num_mutable_buffer_tensors;
233+
auto is_mutable_buffer = [&](Tid t) {
234+
return t.idx >= output_end && t.idx < mutable_buffer_end;
235+
};
236+
237+
const size_t num_chains = program.instruction_chains.size();
238+
std::vector<bool> visited(num_chains, false);
239+
std::vector<uint32_t> worklist;
240+
worklist.push_back(static_cast<uint32_t>(program.init_chain_idx));
241+
242+
while (!worklist.empty()) {
243+
const uint32_t chain_idx = worklist.back();
244+
worklist.pop_back();
245+
if (chain_idx >= num_chains || visited[chain_idx]) {
246+
continue;
247+
}
248+
visited[chain_idx] = true;
249+
for (const auto& instr : program.instruction_chains[chain_idx]) {
250+
bool hit = false;
251+
for_each_tid(instr, [&](Tid t) {
252+
if (is_mutable_buffer(t)) {
253+
hit = true;
254+
}
255+
});
256+
if (hit) {
257+
return true;
258+
}
259+
// Follow nested chains (not covered by for_each_tid).
260+
if (instr.op == OpCode::SCAN) {
261+
const auto& n = std::get<ScanNode>(instr.node);
262+
if (n.body_chain_idx >= 0) {
263+
worklist.push_back(static_cast<uint32_t>(n.body_chain_idx));
264+
}
265+
} else if (instr.op == OpCode::IF) {
266+
const auto& n = std::get<IfNode>(instr.node);
267+
worklist.push_back(n.then_chain_idx);
268+
worklist.push_back(n.else_chain_idx);
269+
}
270+
}
271+
}
272+
return false;
273+
}
274+
197275
// =============================================================================
198276
// FlatBuffer loading functions
199277
// =============================================================================

0 commit comments

Comments
 (0)