Skip to content

Commit 568e514

Browse files
committed
feat(metal): add generic linalg inverse
Use MPS LU factorization and solve for arbitrary float32 square matrices and batches.
1 parent 7f062dd commit 568e514

7 files changed

Lines changed: 179 additions & 10 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ if(MLX_BUILD_CUDA)
166166
endif()
167167

168168
if(MLX_BUILD_METAL)
169+
enable_language(OBJCXX)
169170
find_library(METAL_LIB Metal)
171+
find_library(METAL_PERFORMANCE_SHADERS_LIB MetalPerformanceShaders)
170172
find_library(FOUNDATION_LIB Foundation)
171173
find_library(QUARTZ_LIB QuartzCore)
172174
if(METAL_LIB)

mlx/backend/metal/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ target_sources(
133133
${CMAKE_CURRENT_SOURCE_DIR}/fft.cpp
134134
${CMAKE_CURRENT_SOURCE_DIR}/hadamard.cpp
135135
${CMAKE_CURRENT_SOURCE_DIR}/indexing.cpp
136+
${CMAKE_CURRENT_SOURCE_DIR}/inverse.mm
136137
${CMAKE_CURRENT_SOURCE_DIR}/logsumexp.cpp
137138
${CMAKE_CURRENT_SOURCE_DIR}/matmul.cpp
138139
${CMAKE_CURRENT_SOURCE_DIR}/scaled_dot_product_attention.cpp
@@ -151,6 +152,8 @@ target_sources(
151152
${CMAKE_CURRENT_SOURCE_DIR}/resident.cpp
152153
${CMAKE_CURRENT_SOURCE_DIR}/utils.cpp)
153154

155+
target_link_libraries(mlx PRIVATE ${METAL_PERFORMANCE_SHADERS_LIB})
156+
154157
if(NOT MLX_METAL_PATH)
155158
set(MLX_METAL_PATH ${CMAKE_CURRENT_BINARY_DIR}/kernels/)
156159
endif()

mlx/backend/metal/eval.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ void eval(array& arr) {
6363
scheduler::notify_task_completion(s);
6464
});
6565
} else {
66-
command_buffer->addCompletedHandler(
66+
encoder.get_command_buffer()->addCompletedHandler(
6767
[buffers = std::move(buffers)](MTL::CommandBuffer* cbuf) {});
6868
}
6969
}

mlx/backend/metal/inverse.mm

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright © 2026 Apple Inc.
2+
3+
#include <MetalPerformanceShaders/MetalPerformanceShaders.h>
4+
5+
#include "mlx/allocator.h"
6+
#include "mlx/backend/metal/device.h"
7+
#include "mlx/primitives.h"
8+
9+
namespace mlx::core {
10+
namespace {
11+
12+
id<MTLBuffer> metal_buffer(const array& a) {
13+
return (__bridge id<MTLBuffer>)(void*)a.buffer().ptr();
14+
}
15+
16+
id<MTLDevice> metal_device(metal::Device& device) {
17+
return (__bridge id<MTLDevice>)(void*)device.mtl_device();
18+
}
19+
20+
id<MTLCommandBuffer> metal_command_buffer(metal::CommandEncoder& encoder) {
21+
return (__bridge id<MTLCommandBuffer>)(void*)encoder.get_command_buffer();
22+
}
23+
24+
MPSMatrixDescriptor*
25+
matrix_descriptor(int rows, int columns, MPSDataType dtype) {
26+
const auto row_bytes = static_cast<NSUInteger>(columns) *
27+
(dtype == MPSDataTypeUInt32 ? sizeof(uint32_t) : sizeof(float));
28+
return [MPSMatrixDescriptor matrixDescriptorWithRows:rows
29+
columns:columns
30+
rowBytes:row_bytes
31+
dataType:dtype];
32+
}
33+
34+
} // namespace
35+
36+
void Inverse::eval_gpu(const std::vector<array>& inputs, array& output) {
37+
if (inputs[0].dtype() != float32) {
38+
throw std::invalid_argument(
39+
"[Inverse::eval_gpu] Metal inversion supports float32 arrays only.");
40+
}
41+
42+
output.set_data(allocator::malloc(output.nbytes()));
43+
if (output.size() == 0) {
44+
return;
45+
}
46+
47+
auto& encoder = metal::get_command_encoder(stream());
48+
const auto& input = inputs[0];
49+
const auto& rhs = inputs[1];
50+
51+
const int order = input.shape(-1);
52+
const size_t batch_size = input.size() / (order * order);
53+
array lu(input.shape(), float32, nullptr, {});
54+
array pivots({static_cast<int>(batch_size), order}, uint32, nullptr, {});
55+
lu.set_data(allocator::malloc(lu.nbytes()));
56+
pivots.set_data(allocator::malloc(pivots.nbytes()));
57+
58+
// MPS needs a separate command buffer after MLX compute work.
59+
encoder.end_encoding();
60+
auto retained_inputs = std::make_shared<std::vector<array>>(
61+
std::initializer_list<array>{input, rhs});
62+
auto input_command_buffer = metal_command_buffer(encoder);
63+
[input_command_buffer addCompletedHandler:^(id<MTLCommandBuffer>) {
64+
(void)retained_inputs;
65+
}];
66+
encoder.commit();
67+
68+
auto matrix = matrix_descriptor(order, order, MPSDataTypeFloat32);
69+
auto pivot_matrix = matrix_descriptor(1, order, MPSDataTypeUInt32);
70+
auto decomposition = [[MPSMatrixDecompositionLU alloc]
71+
initWithDevice:metal_device(metal::device(stream().device))
72+
rows:order
73+
columns:order];
74+
auto solve = [[MPSMatrixSolveLU alloc]
75+
initWithDevice:metal_device(metal::device(stream().device))
76+
transpose:NO
77+
order:order
78+
numberOfRightHandSides:order];
79+
auto command_buffer = metal_command_buffer(encoder);
80+
auto retained_arrays = std::make_shared<std::vector<array>>(
81+
std::initializer_list<array>{input, rhs, lu, pivots});
82+
auto resources = [[NSMutableArray alloc] init];
83+
[resources addObject:matrix];
84+
[resources addObject:pivot_matrix];
85+
[resources addObject:decomposition];
86+
[resources addObject:solve];
87+
[decomposition release];
88+
[solve release];
89+
90+
const auto matrix_bytes = static_cast<size_t>(order) * order * sizeof(float);
91+
const auto pivot_bytes = static_cast<size_t>(order) * sizeof(uint32_t);
92+
for (size_t batch = 0; batch < batch_size; ++batch) {
93+
auto source = [[MPSMatrix alloc] initWithBuffer:metal_buffer(input)
94+
offset:input.offset() + batch * matrix_bytes
95+
descriptor:matrix];
96+
auto factor = [[MPSMatrix alloc] initWithBuffer:metal_buffer(lu)
97+
offset:lu.offset() + batch * matrix_bytes
98+
descriptor:matrix];
99+
auto right_hand_side = [[MPSMatrix alloc]
100+
initWithBuffer:metal_buffer(rhs)
101+
offset:rhs.offset() + batch * matrix_bytes
102+
descriptor:matrix];
103+
auto solution = [[MPSMatrix alloc] initWithBuffer:metal_buffer(output)
104+
offset:output.offset() + batch * matrix_bytes
105+
descriptor:matrix];
106+
auto pivot_indices = [[MPSMatrix alloc]
107+
initWithBuffer:metal_buffer(pivots)
108+
offset:pivots.offset() + batch * pivot_bytes
109+
descriptor:pivot_matrix];
110+
[resources addObject:source];
111+
[resources addObject:factor];
112+
[resources addObject:right_hand_side];
113+
[resources addObject:solution];
114+
[resources addObject:pivot_indices];
115+
[source release];
116+
[factor release];
117+
[right_hand_side release];
118+
[solution release];
119+
[pivot_indices release];
120+
121+
[decomposition encodeToCommandBuffer:command_buffer
122+
sourceMatrix:source
123+
resultMatrix:factor
124+
pivotIndices:pivot_indices
125+
status:nil];
126+
[solve encodeToCommandBuffer:command_buffer
127+
sourceMatrix:factor
128+
rightHandSideMatrix:right_hand_side
129+
pivotIndices:pivot_indices
130+
solutionMatrix:solution];
131+
}
132+
[command_buffer addCompletedHandler:^(id<MTLCommandBuffer>) {
133+
(void)retained_arrays;
134+
[resources release];
135+
}];
136+
137+
encoder.register_output_array(output);
138+
// Register the MPS result with MLX's command-encoder dependency tracking.
139+
encoder.set_input_array(output, 0);
140+
encoder.end_encoding();
141+
}
142+
143+
} // namespace mlx::core

mlx/backend/metal/primitives.cpp

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,6 @@ void SVD::eval_gpu(
187187
throw std::runtime_error("[SVD::eval_gpu] Metal SVD NYI.");
188188
}
189189

190-
void Inverse::eval_gpu(const std::vector<array>& inputs, array& output) {
191-
throw std::runtime_error("[Inverse::eval_gpu] Metal inversion NYI.");
192-
}
193-
194190
void Cholesky::eval_gpu(const std::vector<array>& inputs, array& out) {
195191
throw std::runtime_error(
196192
"[Cholesky::eval_gpu] Metal Cholesky decomposition NYI.");

mlx/linalg.cpp

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,9 @@ svd(const array& a, bool compute_uv, StreamOrDevice s /* = {} */) {
299299
}
300300

301301
array inv_impl(const array& a, bool tri, bool upper, StreamOrDevice s) {
302-
check_cpu_stream(s, "[linalg::inv]");
302+
if (tri) {
303+
check_cpu_stream(s, "[linalg::inv]");
304+
}
303305
check_float(a.dtype(), "[linalg::inv]");
304306

305307
if (a.ndim() < 2) {
@@ -314,11 +316,21 @@ array inv_impl(const array& a, bool tri, bool upper, StreamOrDevice s) {
314316
"[linalg::inv] Inverses are only defined for square matrices.");
315317
}
316318

319+
auto stream = to_stream(s);
320+
if (stream.device == Device::gpu && !tri) {
321+
auto input = contiguous(a, false, stream);
322+
auto identity = contiguous(
323+
broadcast_to(eye(a.shape(-1), a.dtype(), stream), a.shape(), stream),
324+
false,
325+
stream);
326+
return array(
327+
a.shape(),
328+
a.dtype(),
329+
std::make_shared<Inverse>(stream, tri, upper),
330+
{input, identity});
331+
}
317332
return array(
318-
a.shape(),
319-
a.dtype(),
320-
std::make_shared<Inverse>(to_stream(s), tri, upper),
321-
{a});
333+
a.shape(), a.dtype(), std::make_shared<Inverse>(stream, tri, upper), {a});
322334
}
323335

324336
array inv(const array& a, StreamOrDevice s /* = {} */) {

python/tests/test_linalg.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,19 @@ def test_tri_inverse(self):
289289
self.assertTrue(mx.all(y1 == mx.triu(y1)))
290290
self.assertTrue(mx.all(y2 == mx.tril(y2)))
291291

292+
def test_inverse_generic_matrices(self):
293+
rng = np.random.default_rng(0)
294+
matrices = rng.normal(size=(2, 7, 7)).astype(np.float32)
295+
matrices += 7 * np.eye(7, dtype=np.float32)
296+
297+
expected = matrices[:, :, ::-1]
298+
inverse = mx.linalg.inv(mx.array(matrices)[:, :, ::-1])
299+
300+
self.assertEqual(inverse.shape, expected.shape)
301+
self.assertTrue(
302+
np.allclose(inverse, np.linalg.inv(expected), rtol=1e-5, atol=1e-5)
303+
)
304+
292305
def test_cholesky(self):
293306
sqrtA = mx.array(
294307
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=mx.float32

0 commit comments

Comments
 (0)