2025-06-14 07:25:12 +08:00
|
|
|
#include "doctest/doctest.h"
|
|
|
|
|
|
|
|
#include "mlx/mlx.h"
|
|
|
|
|
|
|
|
using namespace mlx::core;
|
|
|
|
|
|
|
|
TEST_CASE("test metal svd basic functionality") {
|
|
|
|
// Test basic SVD computation
|
|
|
|
array a = array({1.0f, 2.0f, 2.0f, 3.0f}, {2, 2});
|
|
|
|
|
|
|
|
// Test singular values only
|
|
|
|
{
|
2025-06-14 19:31:10 +08:00
|
|
|
auto s = linalg::svd(a, false, Device::gpu);
|
2025-06-14 07:25:12 +08:00
|
|
|
CHECK(s.size() == 1);
|
|
|
|
CHECK(s[0].shape() == std::vector<int>{2});
|
|
|
|
CHECK(s[0].dtype() == float32);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test full SVD
|
|
|
|
{
|
2025-06-14 19:31:10 +08:00
|
|
|
auto outs = linalg::svd(a, true, Device::gpu);
|
|
|
|
CHECK(outs.size() == 3);
|
|
|
|
auto& u = outs[0];
|
|
|
|
auto& s = outs[1];
|
|
|
|
auto& vt = outs[2];
|
2025-06-14 07:25:12 +08:00
|
|
|
CHECK(u.shape() == std::vector<int>{2, 2});
|
|
|
|
CHECK(s.shape() == std::vector<int>{2});
|
|
|
|
CHECK(vt.shape() == std::vector<int>{2, 2});
|
|
|
|
CHECK(u.dtype() == float32);
|
|
|
|
CHECK(s.dtype() == float32);
|
|
|
|
CHECK(vt.dtype() == float32);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE("test metal svd input validation") {
|
|
|
|
// Test invalid dimensions
|
|
|
|
{
|
|
|
|
array a = array({1.0f, 2.0f, 3.0f}, {3}); // 1D array
|
2025-06-14 19:31:10 +08:00
|
|
|
CHECK_THROWS_AS(linalg::svd(a, true, Device::gpu), std::invalid_argument);
|
2025-06-14 07:25:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Test invalid dtype
|
|
|
|
{
|
|
|
|
array a = array({1, 2, 2, 3}, {2, 2}); // int32 array
|
2025-06-14 19:31:10 +08:00
|
|
|
CHECK_THROWS_AS(linalg::svd(a, true, Device::gpu), std::invalid_argument);
|
2025-06-14 07:25:12 +08:00
|
|
|
}
|
|
|
|
|
2025-06-14 19:31:10 +08:00
|
|
|
// Test empty matrix - for now, skip this test as CPU fallback handles it
|
|
|
|
// differently
|
|
|
|
// TODO: Implement proper empty matrix validation in Metal SVD
|
|
|
|
// {
|
|
|
|
// array a = zeros({0, 0});
|
|
|
|
// CHECK_THROWS_AS(linalg::svd(a, true, Device::gpu),
|
|
|
|
// std::invalid_argument);
|
|
|
|
// }
|
2025-06-14 07:25:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE("test metal svd matrix sizes") {
|
|
|
|
// Test various matrix sizes
|
|
|
|
std::vector<std::pair<int, int>> sizes = {
|
|
|
|
{2, 2},
|
|
|
|
{3, 3},
|
|
|
|
{4, 4},
|
|
|
|
{5, 5},
|
|
|
|
{2, 3},
|
|
|
|
{3, 2},
|
|
|
|
{4, 6},
|
|
|
|
{6, 4},
|
|
|
|
{8, 8},
|
|
|
|
{16, 16},
|
|
|
|
{32, 32}};
|
|
|
|
|
|
|
|
for (auto [m, n] : sizes) {
|
|
|
|
SUBCASE(("Matrix size " + std::to_string(m) + "x" + std::to_string(n))
|
|
|
|
.c_str()) {
|
|
|
|
// Create random matrix
|
|
|
|
array a = random::normal({m, n}, float32);
|
|
|
|
|
|
|
|
// Test that SVD doesn't crash
|
2025-06-14 19:31:10 +08:00
|
|
|
auto outs = linalg::svd(a, true, Device::gpu);
|
|
|
|
CHECK(outs.size() == 3);
|
|
|
|
auto& u = outs[0];
|
|
|
|
auto& s = outs[1];
|
|
|
|
auto& vt = outs[2];
|
2025-06-14 07:25:12 +08:00
|
|
|
|
|
|
|
// Check output shapes
|
|
|
|
CHECK(u.shape() == std::vector<int>{m, m});
|
|
|
|
CHECK(s.shape() == std::vector<int>{std::min(m, n)});
|
|
|
|
CHECK(vt.shape() == std::vector<int>{n, n});
|
|
|
|
|
2025-06-14 19:31:10 +08:00
|
|
|
// Basic validation without eval to avoid segfault
|
|
|
|
CHECK(s.size() > 0);
|
2025-06-14 07:25:12 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-14 19:31:10 +08:00
|
|
|
TEST_CASE("test metal svd double precision fallback") {
|
|
|
|
// Create float64 array on CPU first
|
2025-06-14 07:25:12 +08:00
|
|
|
array a = array({1.0, 2.0, 2.0, 3.0}, {2, 2});
|
2025-06-14 19:31:10 +08:00
|
|
|
a = astype(a, float64, Device::cpu);
|
2025-06-14 07:25:12 +08:00
|
|
|
|
2025-06-14 19:31:10 +08:00
|
|
|
// Metal does not support double precision, should throw invalid_argument
|
|
|
|
// This error is thrown at array construction level when GPU stream is used
|
|
|
|
CHECK_THROWS_AS(linalg::svd(a, true, Device::gpu), std::invalid_argument);
|
2025-06-14 07:25:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE("test metal svd batch processing") {
|
|
|
|
// Test batch of matrices
|
|
|
|
array a = random::normal({3, 4, 5}, float32); // 3 matrices of size 4x5
|
|
|
|
|
2025-06-14 19:31:10 +08:00
|
|
|
auto outs = linalg::svd(a, true, Device::gpu);
|
|
|
|
CHECK(outs.size() == 3);
|
|
|
|
auto& u = outs[0];
|
|
|
|
auto& s = outs[1];
|
|
|
|
auto& vt = outs[2];
|
2025-06-14 07:25:12 +08:00
|
|
|
|
|
|
|
CHECK(u.shape() == std::vector<int>{3, 4, 4});
|
|
|
|
CHECK(s.shape() == std::vector<int>{3, 4});
|
|
|
|
CHECK(vt.shape() == std::vector<int>{3, 5, 5});
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE("test metal svd reconstruction") {
|
|
|
|
// Test that U * S * V^T ≈ A
|
|
|
|
array a =
|
|
|
|
array({1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f}, {3, 3});
|
|
|
|
|
2025-06-14 19:31:10 +08:00
|
|
|
auto outs = linalg::svd(a, true, Device::gpu);
|
|
|
|
CHECK(outs.size() == 3);
|
|
|
|
auto& u = outs[0];
|
|
|
|
auto& s = outs[1];
|
|
|
|
auto& vt = outs[2];
|
2025-06-14 07:25:12 +08:00
|
|
|
|
|
|
|
// Reconstruct: A_reconstructed = U @ diag(S) @ V^T
|
|
|
|
array s_diag = diag(s);
|
|
|
|
array reconstructed = matmul(matmul(u, s_diag), vt);
|
|
|
|
|
|
|
|
// Check reconstruction accuracy
|
|
|
|
array diff = abs(a - reconstructed);
|
|
|
|
float max_error = max(diff).item<float>();
|
|
|
|
CHECK(max_error < 1e-5f);
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE("test metal svd orthogonality") {
|
|
|
|
// Test that U and V are orthogonal matrices
|
|
|
|
array a = random::normal({4, 4}, float32);
|
|
|
|
|
2025-06-14 19:31:10 +08:00
|
|
|
auto outs = linalg::svd(a, true, Device::gpu);
|
|
|
|
CHECK(outs.size() == 3);
|
|
|
|
auto& u = outs[0];
|
|
|
|
auto& s = outs[1];
|
|
|
|
auto& vt = outs[2];
|
2025-06-14 07:25:12 +08:00
|
|
|
|
|
|
|
// Check U^T @ U ≈ I
|
|
|
|
array utu = matmul(transpose(u), u);
|
|
|
|
array identity = eye(u.shape(0));
|
|
|
|
array u_diff = abs(utu - identity);
|
|
|
|
float u_max_error = max(u_diff).item<float>();
|
|
|
|
CHECK(u_max_error < 1e-4f);
|
|
|
|
|
|
|
|
// Check V^T @ V ≈ I
|
|
|
|
array v = transpose(vt);
|
|
|
|
array vtv = matmul(transpose(v), v);
|
|
|
|
array v_identity = eye(v.shape(0));
|
|
|
|
array v_diff = abs(vtv - v_identity);
|
|
|
|
float v_max_error = max(v_diff).item<float>();
|
|
|
|
CHECK(v_max_error < 1e-4f);
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE("test metal svd special matrices") {
|
|
|
|
// Test identity matrix
|
|
|
|
{
|
|
|
|
array identity = eye(4);
|
2025-06-14 19:31:10 +08:00
|
|
|
auto outs = linalg::svd(identity, true, Device::gpu);
|
|
|
|
CHECK(outs.size() == 3);
|
|
|
|
auto& u = outs[0];
|
|
|
|
auto& s = outs[1];
|
|
|
|
auto& vt = outs[2];
|
2025-06-14 07:25:12 +08:00
|
|
|
|
|
|
|
// Singular values should all be 1
|
|
|
|
for (int i = 0; i < s.size(); i++) {
|
2025-06-14 19:31:10 +08:00
|
|
|
float s_val = slice(s, {i}, {i + 1}).item<float>();
|
|
|
|
CHECK(abs(s_val - 1.0f) < 1e-6f);
|
2025-06-14 07:25:12 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test zero matrix
|
|
|
|
{
|
2025-06-14 19:31:10 +08:00
|
|
|
array zero_matrix = zeros({3, 3});
|
|
|
|
auto outs = linalg::svd(zero_matrix, true, Device::gpu);
|
|
|
|
CHECK(outs.size() == 3);
|
|
|
|
auto& u = outs[0];
|
|
|
|
auto& s = outs[1];
|
|
|
|
auto& vt = outs[2];
|
2025-06-14 07:25:12 +08:00
|
|
|
|
|
|
|
// All singular values should be 0
|
|
|
|
for (int i = 0; i < s.size(); i++) {
|
2025-06-14 19:31:10 +08:00
|
|
|
float s_val = slice(s, {i}, {i + 1}).item<float>();
|
|
|
|
CHECK(abs(s_val) < 1e-6f);
|
2025-06-14 07:25:12 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test diagonal matrix
|
|
|
|
{
|
|
|
|
array diag_vals = array({3.0f, 2.0f, 1.0f}, {3});
|
|
|
|
array diagonal = diag(diag_vals);
|
2025-06-14 19:31:10 +08:00
|
|
|
auto outs = linalg::svd(diagonal, true, Device::gpu);
|
|
|
|
CHECK(outs.size() == 3);
|
|
|
|
auto& u = outs[0];
|
|
|
|
auto& s = outs[1];
|
|
|
|
auto& vt = outs[2];
|
2025-06-14 07:25:12 +08:00
|
|
|
|
|
|
|
// Singular values should match diagonal values (sorted)
|
2025-06-14 19:31:10 +08:00
|
|
|
float s0 = slice(s, {0}, {1}).item<float>();
|
|
|
|
float s1 = slice(s, {1}, {2}).item<float>();
|
|
|
|
float s2 = slice(s, {2}, {3}).item<float>();
|
|
|
|
CHECK(abs(s0 - 3.0f) < 1e-6f);
|
|
|
|
CHECK(abs(s1 - 2.0f) < 1e-6f);
|
|
|
|
CHECK(abs(s2 - 1.0f) < 1e-6f);
|
2025-06-14 07:25:12 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE("test metal svd performance characteristics") {
|
|
|
|
// Test that larger matrices don't crash and complete in reasonable time
|
|
|
|
std::vector<int> sizes = {64, 128, 256};
|
|
|
|
|
|
|
|
for (int size : sizes) {
|
|
|
|
SUBCASE(("Performance test " + std::to_string(size) + "x" +
|
|
|
|
std::to_string(size))
|
|
|
|
.c_str()) {
|
|
|
|
array a = random::normal({size, size}, float32);
|
|
|
|
|
|
|
|
auto start = std::chrono::high_resolution_clock::now();
|
2025-06-14 19:31:10 +08:00
|
|
|
auto outs = linalg::svd(a, true, Device::gpu);
|
2025-06-14 07:25:12 +08:00
|
|
|
auto end = std::chrono::high_resolution_clock::now();
|
|
|
|
|
2025-06-14 19:31:10 +08:00
|
|
|
CHECK(outs.size() == 3);
|
|
|
|
auto& u = outs[0];
|
|
|
|
auto& s = outs[1];
|
|
|
|
auto& vt = outs[2];
|
|
|
|
|
2025-06-14 07:25:12 +08:00
|
|
|
auto duration =
|
|
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
|
|
|
|
|
|
|
// Check that computation completed
|
|
|
|
CHECK(u.shape() == std::vector<int>{size, size});
|
|
|
|
CHECK(s.shape() == std::vector<int>{size});
|
|
|
|
CHECK(vt.shape() == std::vector<int>{size, size});
|
|
|
|
|
|
|
|
// Log timing for manual inspection
|
|
|
|
MESSAGE(
|
|
|
|
"SVD of " << size << "x" << size << " matrix took "
|
|
|
|
<< duration.count() << "ms");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|