Skip to content
Merged
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
7 changes: 5 additions & 2 deletions mlx/backend/metal/device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -805,8 +805,11 @@ void Device::set_residency_set(const MTL::ResidencySet* residency_set) {
}

Device& device(mlx::core::Device) {
static Device metal_device;
return metal_device;
// Leak singleton device intentionally, to avoid cases where a compute kernel
// returns and tries to access the object after it has been freed by the main
// thread teardown.
static Device* metal_device = new Device;
return *metal_device;
}

std::unique_ptr<void, std::function<void(void*)>> new_scoped_memory_pool() {
Expand Down
12 changes: 4 additions & 8 deletions mlx/scheduler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,12 @@ namespace scheduler {

/** A singleton scheduler to manage devices, streams, and task execution. */
Scheduler& scheduler() {
// Leak the scheduler on Windows to avoid joining threads on exit, can be
// removed after Visual Studio fixes bug:
// https://developercommunity.visualstudio.com/t/1654756
#ifdef _WIN32
// Intentionally leaked to avoid the "static destruction order fiasco":
// background threads (e.g. command buffer completion handlers) may
// reference this singleton after other static objects are destroyed
// during process teardown.
static Scheduler* scheduler = new Scheduler;
return *scheduler;
#else
static Scheduler scheduler;
return scheduler;
#endif
}

} // namespace scheduler
Expand Down
8 changes: 8 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,11 @@ target_link_options(tests PRIVATE ${SANITIZER_LINK_FLAGS})

doctest_discover_tests(tests)
add_test(NAME tests COMMAND tests)

# Standalone test: verify clean exit when GPU work is in-flight during teardown.
# (Cannot be a doctest case because the crash occurs during static destruction.)
add_executable(test_teardown test_teardown.cpp)
target_link_libraries(test_teardown PRIVATE mlx)
target_compile_options(test_teardown PRIVATE ${SANITIZER_COMPILE_FLAGS})
target_link_options(test_teardown PRIVATE ${SANITIZER_LINK_FLAGS})
add_test(NAME teardown COMMAND test_teardown)
37 changes: 37 additions & 0 deletions tests/test_teardown.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright © 2026 Apple Inc.
//
// Regression test for https://github.com/ml-explore/mlx/issues/3126
// Verifies that the process exits cleanly when a background thread is
// performing GPU work and the main thread exits.

#include <chrono>
#include <iostream>
#include <thread>

#include "mlx/mlx.h"

namespace mx = mlx::core;

int main() {
using namespace std::chrono_literals;

std::thread t([] {
auto a = mx::random::normal({2048, 2048});
std::cout << "START" << std::endl;
for (int i = 0; i < 1000; ++i) {
a = mx::matmul(a, a);
// Eval periodically to avoid building a huge graph
if (i % 10 == 0) {
mx::eval(a);
std::cout << "Step " << i << std::endl;
}
}
mx::eval(a);
std::cout << "Done: " << a.shape(0) << "x" << a.shape(1) << std::endl;
});

std::this_thread::sleep_for(1s);
t.detach();
std::cout << "Main thread exiting." << std::endl;
return 0;
}
Loading