Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0abc566
Metal backend: wait for the parent's GPU writes before reading an off…
abdelaziz-mahdy Sep 20, 2026
9471b22
Metal backend: bind offset views to their parent's buffer instead of …
abdelaziz-mahdy Sep 20, 2026
0a4a1f5
Cover a chunk of a chunk in the Metal offset-view tests
abdelaziz-mahdy Sep 20, 2026
4ad8cea
Merge remote-tracking branch 'upstream/main' into local/offset-views-…
abdelaziz-mahdy Sep 21, 2026
a3acce5
Keep a Metal view registered while any handle to it is alive
abdelaziz-mahdy Sep 21, 2026
89dbe34
Arm the post-graph sync only once the alias buffer exists
abdelaziz-mahdy Sep 21, 2026
67d8d5d
Tie the alias wait to its own graph, and keep CPU-backed and nested v…
abdelaziz-mahdy Sep 22, 2026
090bf1a
Wait for the GPU before a host/device copy touches the memory
abdelaziz-mahdy Sep 22, 2026
d108d69
Merge remote-tracking branch 'upstream/main' into fix/metal-offset-views
abdelaziz-mahdy Sep 24, 2026
110723a
Keep CPU memory until queued GPU work using it is done
abdelaziz-mahdy Sep 24, 2026
ba664e1
Bind views of CPU memory into one buffer per region, tied to the memory
abdelaziz-mahdy Sep 25, 2026
1d6b4c4
Wait before a packed copy only for GPU work that can touch it
abdelaziz-mahdy Sep 25, 2026
ddfadcd
Keep the buffer flush() commits for the next wait
abdelaziz-mahdy Sep 25, 2026
903f9f7
Register blobs that lie inside memory the runtime binds
abdelaziz-mahdy Sep 25, 2026
f76c5e4
Drop a destroyed stream's buffers from the pool
abdelaziz-mahdy Sep 25, 2026
f31c7ec
Refuse what a tensor cannot hold, and fix memory handling main got wrong
abdelaziz-mahdy Sep 26, 2026
2240830
Test that freeing the first constant does not wait for its readers
abdelaziz-mahdy Sep 26, 2026
34bf7ee
Retire handles in freed memory, and fix op error paths that freed own…
abdelaziz-mahdy Sep 26, 2026
67e0c98
Merge remote-tracking branch 'upstream/main' into fix/metal-offset-views
abdelaziz-mahdy Oct 6, 2026
64917ca
Merge remote-tracking branch 'upstream/main' into fix/metal-offset-views
abdelaziz-mahdy Oct 26, 2026
093b315
Keep the offset of an empty view for views made from it
abdelaziz-mahdy Oct 28, 2026
6815df1
Metal backend: free only the destroyed delegate's memory
abdelaziz-mahdy Nov 8, 2026
36a6283
Metal backend: test destroying one of two programs in one process
abdelaziz-mahdy Nov 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions backends/apple/metal/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ install(
DESTINATION lib
)

# Runs two programs in one process for tests/test_modules.py. Only built on
# request, by tests/run_metal_test.sh --build.
if(EXECUTORCH_BUILD_EXECUTOR_RUNNER AND EXECUTORCH_BUILD_EXTENSION_MODULE)
add_executable(
metal_multi_delegate_runner EXCLUDE_FROM_ALL
runtime/test/multi_delegate_runner.cpp
)
target_link_libraries(
metal_multi_delegate_runner
PRIVATE extension_module_static extension_tensor portable_ops_lib
metal_backend
)
endif()

if(BUILD_TESTING)
include(${EXECUTORCH_ROOT}/tools/cmake/Test.cmake)

Expand Down
25 changes: 23 additions & 2 deletions backends/apple/metal/runtime/metal_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,18 @@ class ET_EXPERIMENTAL MetalBackend final
handle->so_handle = so_handle;
handle->so_path = so_path.string();

// The shims record what this delegate creates while it loads, so that
// destroy() frees only this delegate's memory. If loading fails, what it
// made so far is freed here, since destroy() will not be called.
metal_begin_delegate(handle);
MetalDelegateScope delegate_scope(handle, /*loading=*/true);
bool initialized = false;
executorch::backends::aoti::ScopeGuard release_on_failure([&]() noexcept {
if (!initialized) {
metal_release_delegate(handle);
}
Comment on lines +372 to +375

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked for a way to make init fail after it allocates. With a valid program, nothing in init fails once it has allocated; a bad weights blob makes AOTI read out of bounds instead of returning an error. So this would need a fault-injection hook in init. The release the guard runs is the one the shim tests cover. Happy to add a hook if you'd like one.

});

// Load function pointers specific to this handle's shared library
ET_CHECK_OK_OR_RETURN_ERROR(
load_function_pointers_into_handle(so_handle, handle));
Expand Down Expand Up @@ -393,6 +405,7 @@ class ET_EXPERIMENTAL MetalBackend final
}

ET_LOG(Info, "MetalBackend::init - Initialization completed successfully");
initialized = true;

#ifdef EXECUTORCH_METAL_COLLECT_STATS
// Accumulate init timing statistics
Expand Down Expand Up @@ -447,6 +460,9 @@ class ET_EXPERIMENTAL MetalBackend final
});

AOTIDelegateHandle* handle = (AOTIDelegateHandle*)handle_;
// Handles created by this run, e.g. folded constants that outlive it,
// belong to this delegate.
MetalDelegateScope delegate_scope(handle, /*loading=*/false);

ET_LOG(Debug, "MetalBackend Handle generated");

Expand Down Expand Up @@ -679,9 +695,14 @@ class ET_EXPERIMENTAL MetalBackend final
}
}

// Free this delegate's handles and constants. The shim state, including
// the buffer pool its recycled buffers went to, is shared by every
// delegate in the process, so it is only reset once none is left.
if (metal_release_delegate(handle)) {
cleanup_memory();
executorch::backends::aoti::cleanup_tensor_metadata();
Comment on lines +701 to +703

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the module tests run one program per process. I added test_destroying_one_program_keeps_the_others_memory: a small runner loads two Metal programs in one process, destroys one, checks it left no handles behind, and runs the other against eager. It fails with the old unconditional cleanup and without the init ownership scope. Dropping the execute scope isn't caught there, because no handle outlives a run unless runtime constant folding is on, which the Metal export doesn't enable; the shim-level test covers that case. (36a6283)

}
delete handle;
cleanup_memory();
executorch::backends::aoti::cleanup_tensor_metadata();
ET_LOG(Debug, "MetalBackend handle %p destroy", handle_);
}
};
Expand Down
10 changes: 8 additions & 2 deletions backends/apple/metal/runtime/ops/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,14 @@ extern std::unordered_map<GraphCacheKey, CachedGraph, GraphCacheKeyHash>
graph_cache;
extern CacheStats cache_stats;

MTLBuffer_t
get_mtl_buffer(Tensor* tensor, const char* op_name, const char* tensor_name);
// The buffer to feed a graph for `tensor`. A tensor that starts partway into
// its buffer gets an aliasing buffer, and `*settle_aliases` is then set: pass
// it to the executeMPSGraph call that runs the graph this buffer feeds.
MTLBuffer_t get_mtl_buffer(
Tensor* tensor,
const char* op_name,
const char* tensor_name,
bool* settle_aliases);
MTLBuffer_t allocate_mtl_buffer(void** data_ptr, size_t size_bytes);

} // namespace metal
Expand Down
34 changes: 30 additions & 4 deletions backends/apple/metal/runtime/ops/common.mm
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,40 @@
std::unordered_map<GraphCacheKey, CachedGraph, GraphCacheKeyHash> graph_cache;
CacheStats cache_stats;

id<MTLBuffer> get_mtl_buffer(Tensor* tensor, const char* op_name, const char* tensor_name) {
id<MTLBuffer> get_mtl_buffer(
Tensor* tensor,
const char* op_name,
const char* tensor_name,
bool* settle_aliases) {
void* data_ptr = tensor->mutable_data_ptr();
auto it = ptr_to_mtl_buffer.find(data_ptr);
if (it == ptr_to_mtl_buffer.end()) {
id<MTLBuffer> buffer = nil;
size_t offset = 0;
if (!metal_resolve_buffer(data_ptr, &buffer, &offset)) {
ET_LOG(Error, "%s: %s tensor not found in Metal buffer mapping", op_name, tensor_name);
throw std::runtime_error(std::string(tensor_name) + " tensor not found in Metal buffer mapping");
}
return it->second;
// An empty tensor reads and writes nothing, so it needs no buffer of its
// own at its offset, and Metal makes none of length 0.
if (offset == 0 || tensor->nbytes() == 0) {
return buffer;
}

// The tensor is a view that starts partway into `buffer`. MPSGraphTensorData
// cannot address into a buffer, so the graph needs an MTLBuffer that begins at
// the view, over the same memory. Metal does not relate that alias to
// `buffer`, and work using one does not see pending work on the other, so the
// graph has to run with the memory settled on both sides of it. That is asked
// of the one graph this buffer is for, through executeMPSGraph.
id<MTLBuffer> alias = [get_metal_device() newBufferWithBytesNoCopy:data_ptr
length:tensor->nbytes()
options:MTLResourceStorageModeShared
deallocator:nil];
if (!alias) {
ET_LOG(Error, "%s: failed to wrap the %s view in a Metal buffer", op_name, tensor_name);
throw std::runtime_error(std::string(tensor_name) + " view could not be wrapped in a Metal buffer");
}
*settle_aliases = true;
return [alias autorelease];
}

id<MTLBuffer> allocate_mtl_buffer(void** data_ptr, size_t size_bytes) {
Expand Down
11 changes: 6 additions & 5 deletions backends/apple/metal/runtime/ops/op_addmm.mm
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,15 @@ AOTITorchError aoti_torch_mps_addmm_out(
throw std::runtime_error("Failed to get Metal device");
}

bool settle_aliases = false;
id<MTLBuffer> bias_buffer =
get_mtl_buffer(bias_tensor, "aoti_torch_mps_addmm_out", "self");
get_mtl_buffer(bias_tensor, "aoti_torch_mps_addmm_out", "self", &settle_aliases);
id<MTLBuffer> mat1_buffer =
get_mtl_buffer(mat1_tensor, "aoti_torch_mps_addmm_out", "mat1");
get_mtl_buffer(mat1_tensor, "aoti_torch_mps_addmm_out", "mat1", &settle_aliases);
id<MTLBuffer> mat2_buffer =
get_mtl_buffer(mat2_tensor, "aoti_torch_mps_addmm_out", "mat2");
get_mtl_buffer(mat2_tensor, "aoti_torch_mps_addmm_out", "mat2", &settle_aliases);
id<MTLBuffer> out_buffer =
get_mtl_buffer(out_tensor, "aoti_torch_mps_addmm_out", "out");
get_mtl_buffer(out_tensor, "aoti_torch_mps_addmm_out", "out", &settle_aliases);

stream->endKernelCoalescing();

Expand Down Expand Up @@ -272,7 +273,7 @@ AOTITorchError aoti_torch_mps_addmm_out(
NSDictionary* results = @{addmmOutput : outputData};

@try {
stream->executeMPSGraph(mpsGraph, feeds, results, SyncType::COMMIT);
stream->executeMPSGraph(mpsGraph, feeds, results, SyncType::COMMIT, settle_aliases);
} @catch (NSException* exception) {
ET_LOG(
Error,
Expand Down
9 changes: 5 additions & 4 deletions backends/apple/metal/runtime/ops/op_bmm.mm
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,10 @@ AOTITorchError aoti_torch_mps_bmm_out(
}

// Get Metal buffers for input and output tensors
id<MTLBuffer> self_buffer = get_mtl_buffer(self_tensor, "aoti_torch_mps_bmm_out", "self");
id<MTLBuffer> mat2_buffer = get_mtl_buffer(mat2_tensor, "aoti_torch_mps_bmm_out", "mat2");
id<MTLBuffer> out_buffer = get_mtl_buffer(out_tensor, "aoti_torch_mps_bmm_out", "out");
bool settle_aliases = false;
id<MTLBuffer> self_buffer = get_mtl_buffer(self_tensor, "aoti_torch_mps_bmm_out", "self", &settle_aliases);
id<MTLBuffer> mat2_buffer = get_mtl_buffer(mat2_tensor, "aoti_torch_mps_bmm_out", "mat2", &settle_aliases);
id<MTLBuffer> out_buffer = get_mtl_buffer(out_tensor, "aoti_torch_mps_bmm_out", "out", &settle_aliases);

// Validate buffers are non-null
if (!self_buffer || !mat2_buffer || !out_buffer) {
Expand Down Expand Up @@ -278,7 +279,7 @@ AOTITorchError aoti_torch_mps_bmm_out(

// Execute the batched matrix multiplication
@try {
stream->executeMPSGraph(mpsGraph, feeds, results, SyncType::COMMIT);
stream->executeMPSGraph(mpsGraph, feeds, results, SyncType::COMMIT, settle_aliases);
} @catch (NSException *exception) {
ET_LOG(Error, "aoti_torch_mps_bmm_out: NSException caught during executeMPSGraph: %s - %s",
[[exception name] UTF8String], [[exception reason] UTF8String]);
Expand Down
11 changes: 7 additions & 4 deletions backends/apple/metal/runtime/ops/op_convolution.mm
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,9 @@ AOTITorchError aoti_torch_mps_convolution(
NSMutableDictionary* feeds = [NSMutableDictionary dictionary];

// Get Metal buffers from tensors
id<MTLBuffer> input_buffer = get_mtl_buffer(input_tensor, "aoti_torch_mps_convolution", "input");
id<MTLBuffer> weight_buffer = get_mtl_buffer(weight_tensor, "aoti_torch_mps_convolution", "weight");
bool settle_aliases = false;
id<MTLBuffer> input_buffer = get_mtl_buffer(input_tensor, "aoti_torch_mps_convolution", "input", &settle_aliases);
id<MTLBuffer> weight_buffer = get_mtl_buffer(weight_tensor, "aoti_torch_mps_convolution", "weight", &settle_aliases);

ET_LOG(Debug, "aoti_torch_mps_convolution: Using existing Metal buffers - input=%p, weight=%p",
input_buffer, weight_buffer);
Expand All @@ -524,7 +525,7 @@ AOTITorchError aoti_torch_mps_convolution(

// Add bias data to feeds if provided
if (bias_tensor && biasPlaceholder) {
id<MTLBuffer> bias_buffer = get_mtl_buffer(bias_tensor, "aoti_torch_mps_convolution", "bias");
id<MTLBuffer> bias_buffer = get_mtl_buffer(bias_tensor, "aoti_torch_mps_convolution", "bias", &settle_aliases);

NSArray<NSNumber*>* biasShape = @[@(C_out)];
biasData = [[MPSGraphTensorData alloc] initWithMTLBuffer:bias_buffer
Expand Down Expand Up @@ -558,7 +559,7 @@ AOTITorchError aoti_torch_mps_convolution(

@try {
// Use stream helper to encode and synchronize correctly
stream->executeMPSGraph(mpsGraph, feeds, results, SyncType::COMMIT);
stream->executeMPSGraph(mpsGraph, feeds, results, SyncType::COMMIT, settle_aliases);
} @catch (NSException *exception) {
ET_LOG(Error, "aoti_torch_mps_convolution: NSException caught during executeMPSGraph: %s - %s",
[[exception name] UTF8String], [[exception reason] UTF8String]);
Expand Down Expand Up @@ -650,6 +651,8 @@ AOTITorchError aoti_torch_mps_convolution(

if (actual_numel != expected_numel) {
ET_LOG(Error, "aoti_torch_mps_convolution: Tensor size mismatch. Expected %zu, got %zu", expected_numel, actual_numel);
// The handle goes first: it does not own the memory yet.
aoti_torch_delete_tensor_object(output_tensor_handle);
aoti_torch_mps_free(tensor_data); // Free the allocated GPU memory on failure
throw std::runtime_error("Tensor size mismatch");
}
Expand Down
9 changes: 5 additions & 4 deletions backends/apple/metal/runtime/ops/op_mm.mm
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,10 @@ AOTITorchError aoti_torch_mps_mm_out(
}

// Get Metal buffers for input and output tensors
id<MTLBuffer> self_buffer = get_mtl_buffer(self_tensor, "aoti_torch_mps_mm_out", "self");
id<MTLBuffer> mat2_buffer = get_mtl_buffer(mat2_tensor, "aoti_torch_mps_mm_out", "mat2");
id<MTLBuffer> out_buffer = get_mtl_buffer(out_tensor, "aoti_torch_mps_mm_out", "out");
bool settle_aliases = false;
id<MTLBuffer> self_buffer = get_mtl_buffer(self_tensor, "aoti_torch_mps_mm_out", "self", &settle_aliases);
id<MTLBuffer> mat2_buffer = get_mtl_buffer(mat2_tensor, "aoti_torch_mps_mm_out", "mat2", &settle_aliases);
id<MTLBuffer> out_buffer = get_mtl_buffer(out_tensor, "aoti_torch_mps_mm_out", "out", &settle_aliases);

ET_LOG(Debug, "aoti_torch_mps_mm_out: Using existing Metal buffers - self=%p, mat2=%p, out=%p",
self_buffer, mat2_buffer, out_buffer);
Expand Down Expand Up @@ -262,7 +263,7 @@ AOTITorchError aoti_torch_mps_mm_out(

@try {
// Use stream helper to encode and synchronize correctly
stream->executeMPSGraph(mpsGraph, feeds, results, SyncType::COMMIT);
stream->executeMPSGraph(mpsGraph, feeds, results, SyncType::COMMIT, settle_aliases);
} @catch (NSException *exception) {
ET_LOG(Error, "aoti_torch_mps_mm_out: NSException caught during executeMPSGraph: %s - %s",
[[exception name] UTF8String], [[exception reason] UTF8String]);
Expand Down
20 changes: 14 additions & 6 deletions backends/apple/metal/runtime/ops/op_sdpa.mm
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,12 @@ static AOTITorchError sdpa_mps_impl(
return Error::Internal;
}

// Freed on an error until a handle owns them; then the handle is deleted
// instead, which frees the memory.
void* out_contents_ptr = nullptr;
void* attn_contents_ptr = nullptr;
AOTITensorHandle out_tensor_handle = nullptr;

try {
@autoreleasepool {
// Convert AOTITensorHandle to ExecuTorch tensors
Expand Down Expand Up @@ -454,10 +460,8 @@ static AOTITorchError sdpa_mps_impl(
size_t out_size_bytes = batchSize * num_heads * qSize * headSize * element_size;
size_t attn_size_bytes = batchSize * num_heads * qSize * kvSeqLength * element_size;

void* out_contents_ptr = nullptr;
allocate_mtl_buffer(&out_contents_ptr, out_size_bytes);

void* attn_contents_ptr = nullptr;
allocate_mtl_buffer(&attn_contents_ptr, attn_size_bytes);

// Use MLX-style Metal kernels instead of MPSGraph
Expand Down Expand Up @@ -499,7 +503,6 @@ static AOTITorchError sdpa_mps_impl(
}

// Create output tensor handle first so we can use it in the kernel
AOTITensorHandle out_tensor_handle = nullptr;
AOTITorchError create_out_result = aoti_torch_create_tensor_from_blob_v2(
out_contents_ptr,
4, // ndim
Expand All @@ -517,14 +520,13 @@ static AOTITorchError sdpa_mps_impl(

if (create_out_result != Error::Ok || !out_tensor_handle) {
ET_LOG(Error, "aoti_torch_mps__scaled_dot_product_attention_math_for_mps: Failed to create output tensor");
aoti_torch_mps_free(out_contents_ptr);
aoti_torch_mps_free(attn_contents_ptr);
throw std::runtime_error("Failed to create output tensor");
}

// Mark that we own the memory
extern std::unordered_map<void*, int32_t> memory_to_n_tensor;
memory_to_n_tensor[out_contents_ptr] = 1;
out_contents_ptr = nullptr;

auto* out_tensor = reinterpret_cast<Tensor*>(out_tensor_handle);

Expand Down Expand Up @@ -680,11 +682,11 @@ static AOTITorchError sdpa_mps_impl(

if (create_attn_result != Error::Ok || !attn_tensor_handle) {
ET_LOG(Error, "aoti_torch_mps__scaled_dot_product_attention_math_for_mps: Failed to create attention weights tensor");
aoti_torch_mps_free(attn_contents_ptr);
throw std::runtime_error("Failed to create attention weights tensor");
}

memory_to_n_tensor[attn_contents_ptr] = 1;
attn_contents_ptr = nullptr;

// Set output tensor handles
*ret0 = out_tensor_handle;
Expand All @@ -699,9 +701,15 @@ static AOTITorchError sdpa_mps_impl(

} catch (const std::exception& e) {
ET_LOG(Error, "aoti_torch_mps__scaled_dot_product_attention_math_for_mps exception: %s", e.what());
if (out_tensor_handle) aoti_torch_delete_tensor_object(out_tensor_handle);
if (out_contents_ptr) aoti_torch_mps_free(out_contents_ptr);
if (attn_contents_ptr) aoti_torch_mps_free(attn_contents_ptr);
return Error::Internal;
} catch (...) {
ET_LOG(Error, "aoti_torch_mps__scaled_dot_product_attention_math_for_mps: unknown exception");
if (out_tensor_handle) aoti_torch_delete_tensor_object(out_tensor_handle);
if (out_contents_ptr) aoti_torch_mps_free(out_contents_ptr);
if (attn_contents_ptr) aoti_torch_mps_free(attn_contents_ptr);
return Error::Internal;
}
}
Expand Down
Loading
Loading