Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions mlx/backend/metal/device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ void CommandEncoder::commit(std::function<void()> completion) {
[&error_ = error_,
wait_events = std::move(wait_events_),
signal_events = std::move(signal_events_),
retained = std::move(retained_buffers_),
completion = std::move(completion)](MTL::CommandBuffer* cbuf) {
if (completion) {
completion();
Expand Down Expand Up @@ -556,6 +557,8 @@ void CommandEncoder::commit(std::function<void()> completion) {
buffer_ = NS::RetainPtr(queue_->commandBufferWithUnretainedReferences());
buffer_ops_ = 0;
buffer_sizes_ = 0;
retained_buffers_.clear();
retained_ptrs_.clear();
}

void CommandEncoder::synchronize() {
Expand Down
10 changes: 10 additions & 0 deletions mlx/backend/metal/device.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ class MLX_API CommandEncoder {
void add_temporary(array arr);
void add_temporaries(std::vector<array> arrays);

// Keep a buffer alive until the current command buffer completes. Deduped
// because a primitive may bind the same array in several slots.
void hold_buffer(const std::shared_ptr<array::Data>& buf) {
if (buf && retained_ptrs_.insert(buf.get()).second) {
retained_buffers_.push_back(buf);
}
}

void dispatch_threadgroups(MTL::Size grid_dims, MTL::Size group_dims);
void dispatch_threads(MTL::Size grid_dims, MTL::Size group_dims);
void maybeInsertBarrier();
Expand Down Expand Up @@ -129,6 +137,8 @@ class MLX_API CommandEncoder {
bool needs_barrier_{false};
bool concurrent_{false};
std::vector<array> temporaries_;
std::vector<std::shared_ptr<array::Data>> retained_buffers_;
std::unordered_set<const array::Data*> retained_ptrs_;
Comment on lines +140 to +141

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think you can just store buffers in the set.

Suggested change
std::vector<std::shared_ptr<array::Data>> retained_buffers_;
std::unordered_set<const array::Data*> retained_ptrs_;
std::unordered_set<std::shared_ptr<array::Data>> retained_buffers_;

std::unordered_set<MTL::Resource*> prev_inputs_;
std::unordered_set<MTL::Resource*> prev_outputs_;
std::unordered_set<MTL::Resource*> next_inputs_;
Expand Down
26 changes: 10 additions & 16 deletions mlx/backend/metal/eval.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// Copyright © 2023-2024 Apple Inc.
#include <memory>

#include "mlx/backend/gpu/eval.h"
#include "mlx/backend/metal/device.h"
#include "mlx/backend/metal/utils.h"
Expand Down Expand Up @@ -44,27 +42,23 @@ void eval(array& arr) {
debug_set_primitive_buffer_label(command_buffer, arr.primitive());
arr.primitive().eval_gpu(arr.inputs(), outputs);
}
std::unordered_set<std::shared_ptr<array::Data>> buffers;
// Skip a donated output's buffer since holding it blocks allocator reuse.
const auto& out_data = arr.data_shared_ptr();
for (auto& in : arr.inputs()) {
buffers.insert(in.data_shared_ptr());
}
for (auto& s : arr.siblings()) {
buffers.insert(s.data_shared_ptr());
if (in.data_shared_ptr() != out_data) {
encoder.hold_buffer(in.data_shared_ptr());
}
}
// Remove the output if it was donated to by an input
if (auto it = buffers.find(arr.data_shared_ptr()); it != buffers.end()) {
buffers.erase(it);
for (auto& sib : arr.siblings()) {
if (sib.data_shared_ptr() != out_data) {
encoder.hold_buffer(sib.data_shared_ptr());
}
}

if (encoder.needs_commit()) {
encoder.end_encoding();
scheduler::notify_new_task(s);
encoder.commit([s, buffers = std::move(buffers)]() {
scheduler::notify_task_completion(s);
});
} else {
command_buffer->addCompletedHandler(
[buffers = std::move(buffers)](MTL::CommandBuffer* cbuf) {});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The downside of this approach is that buffers are now retained until the command encoder is committed, which I think could increase peak memory a lot? Can you check the memory usage in a few popular models?

I support reducing the completion handler to have lower overhead but we probably need to add some heuristics.

encoder.commit([s]() { scheduler::notify_task_completion(s); });
}
}

Expand Down