matrix-compute-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrix_compute_mcp/__init__.py +64 -0
- matrix_compute_mcp/ai/__init__.py +32 -0
- matrix_compute_mcp/ai/similarity.py +220 -0
- matrix_compute_mcp/ai/spectral.py +308 -0
- matrix_compute_mcp/ai/torch_layers.py +835 -0
- matrix_compute_mcp/estimator.py +235 -0
- matrix_compute_mcp/io_utils.py +292 -0
- matrix_compute_mcp/server.py +1066 -0
- matrix_compute_mcp-0.1.0.dist-info/LICENSE +190 -0
- matrix_compute_mcp-0.1.0.dist-info/METADATA +91 -0
- matrix_compute_mcp-0.1.0.dist-info/RECORD +14 -0
- matrix_compute_mcp-0.1.0.dist-info/WHEEL +5 -0
- matrix_compute_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- matrix_compute_mcp-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`matrix-compute-mcp` (`matrix_compute_mcp`): Official Model Context Protocol (MCP)
|
|
3
|
+
server and high-level verifiable AI primitives (`matrix_compute_mcp.ai`) built on top
|
|
4
|
+
of `matrix-compute-sdk`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from . import ai
|
|
8
|
+
from .ai import (
|
|
9
|
+
DistributedLinear,
|
|
10
|
+
DistributedLoRALinear,
|
|
11
|
+
PCASVDResult,
|
|
12
|
+
SimilaritySearchResult,
|
|
13
|
+
distributed_cosine_similarity,
|
|
14
|
+
distributed_pca_svd,
|
|
15
|
+
l2_normalize_rows,
|
|
16
|
+
)
|
|
17
|
+
from .estimator import (
|
|
18
|
+
JobCostEstimate,
|
|
19
|
+
estimate_job_cost,
|
|
20
|
+
)
|
|
21
|
+
from .io_utils import (
|
|
22
|
+
load_matrix_input,
|
|
23
|
+
load_safetensors_bytes,
|
|
24
|
+
load_safetensors_file,
|
|
25
|
+
save_matrix_output,
|
|
26
|
+
save_safetensors_bytes,
|
|
27
|
+
save_safetensors_file,
|
|
28
|
+
)
|
|
29
|
+
from .server import (
|
|
30
|
+
MCPServerConfig,
|
|
31
|
+
MatrixComputeMCPServer,
|
|
32
|
+
TOOL_DEFINITIONS,
|
|
33
|
+
main,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"__version__",
|
|
40
|
+
"ai",
|
|
41
|
+
# AI Primitives
|
|
42
|
+
"distributed_cosine_similarity",
|
|
43
|
+
"l2_normalize_rows",
|
|
44
|
+
"SimilaritySearchResult",
|
|
45
|
+
"DistributedLinear",
|
|
46
|
+
"DistributedLoRALinear",
|
|
47
|
+
"distributed_pca_svd",
|
|
48
|
+
"PCASVDResult",
|
|
49
|
+
# Cost & Bandwidth Estimator
|
|
50
|
+
"JobCostEstimate",
|
|
51
|
+
"estimate_job_cost",
|
|
52
|
+
# Safe Tensor I/O
|
|
53
|
+
"load_matrix_input",
|
|
54
|
+
"save_matrix_output",
|
|
55
|
+
"load_safetensors_file",
|
|
56
|
+
"save_safetensors_file",
|
|
57
|
+
"load_safetensors_bytes",
|
|
58
|
+
"save_safetensors_bytes",
|
|
59
|
+
# MCP Server
|
|
60
|
+
"MCPServerConfig",
|
|
61
|
+
"MatrixComputeMCPServer",
|
|
62
|
+
"TOOL_DEFINITIONS",
|
|
63
|
+
"main",
|
|
64
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-Level Verifiable AI Primitives (`matrix_compute_mcp.ai`).
|
|
3
|
+
|
|
4
|
+
Exposes:
|
|
5
|
+
- `distributed_cosine_similarity` & `SimilaritySearchResult`
|
|
6
|
+
- `DistributedLinear` & `DistributedLoRALinear`
|
|
7
|
+
- `distributed_pca_svd` & `PCASVDResult`
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .similarity import (
|
|
11
|
+
SimilaritySearchResult,
|
|
12
|
+
distributed_cosine_similarity,
|
|
13
|
+
l2_normalize_rows,
|
|
14
|
+
)
|
|
15
|
+
from .spectral import (
|
|
16
|
+
PCASVDResult,
|
|
17
|
+
distributed_pca_svd,
|
|
18
|
+
)
|
|
19
|
+
from .torch_layers import (
|
|
20
|
+
DistributedLinear,
|
|
21
|
+
DistributedLoRALinear,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"distributed_cosine_similarity",
|
|
26
|
+
"l2_normalize_rows",
|
|
27
|
+
"SimilaritySearchResult",
|
|
28
|
+
"DistributedLinear",
|
|
29
|
+
"DistributedLoRALinear",
|
|
30
|
+
"distributed_pca_svd",
|
|
31
|
+
"PCASVDResult",
|
|
32
|
+
]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Distributed Cosine Similarity & Exact K-NN Vector Search (`matrix_compute_mcp.ai.similarity`).
|
|
3
|
+
|
|
4
|
+
Uses `OUTPUT_GRID_2D` zero-reduction tiling to compute massive exact cosine similarity
|
|
5
|
+
matrices across millions of embedding vectors on distributed GPU workers with O(n^2)
|
|
6
|
+
Freivalds verification and zero download redundancy.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from matrix_compute.client import MatrixComputeClient
|
|
17
|
+
from matrix_compute.config import ClientConfig, PartitioningStrategy
|
|
18
|
+
|
|
19
|
+
_TORCH_AVAILABLE = False
|
|
20
|
+
try:
|
|
21
|
+
import torch
|
|
22
|
+
|
|
23
|
+
_TORCH_AVAILABLE = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SimilaritySearchResult:
|
|
30
|
+
"""Result container for top-K distributed cosine similarity search."""
|
|
31
|
+
|
|
32
|
+
scores: np.ndarray
|
|
33
|
+
indices: np.ndarray
|
|
34
|
+
similarity_matrix: Optional[np.ndarray] = None
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
37
|
+
return {
|
|
38
|
+
"scores": self.scores.tolist(),
|
|
39
|
+
"indices": self.indices.tolist(),
|
|
40
|
+
"num_queries": int(self.scores.shape[0]),
|
|
41
|
+
"top_k": int(self.scores.shape[1]),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def l2_normalize_rows(matrix: np.ndarray, eps: float = 1e-12) -> np.ndarray:
|
|
46
|
+
"""
|
|
47
|
+
Normalizes each row of a 2D matrix to unit L2 norm in float32.
|
|
48
|
+
Zero-norm rows remain all zeros without producing NaNs or Infs.
|
|
49
|
+
"""
|
|
50
|
+
arr = np.asarray(matrix, dtype=np.float32)
|
|
51
|
+
if arr.ndim != 2:
|
|
52
|
+
raise ValueError(f"Expected 2D embedding matrix, got shape {arr.shape}")
|
|
53
|
+
if arr.shape[0] == 0 or arr.shape[1] == 0:
|
|
54
|
+
raise ValueError(f"Embedding matrix dimensions must be non-zero, got shape {arr.shape}")
|
|
55
|
+
|
|
56
|
+
norms = np.linalg.norm(arr, axis=1, keepdims=True)
|
|
57
|
+
zero_mask = norms <= eps
|
|
58
|
+
safe_norms = np.where(zero_mask, 1.0, norms)
|
|
59
|
+
normalized = arr / safe_norms
|
|
60
|
+
if np.any(zero_mask):
|
|
61
|
+
normalized = np.where(zero_mask, 0.0, normalized)
|
|
62
|
+
return np.ascontiguousarray(normalized, dtype=np.float32)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def distributed_cosine_similarity(
|
|
66
|
+
queries: Any,
|
|
67
|
+
corpus_embeddings: Any,
|
|
68
|
+
top_k: Optional[int] = None,
|
|
69
|
+
dtype: str = "fp32",
|
|
70
|
+
client: Optional[MatrixComputeClient] = None,
|
|
71
|
+
config: Optional[ClientConfig] = None,
|
|
72
|
+
tile_size: Optional[int] = None,
|
|
73
|
+
grid_rows: Optional[int] = None,
|
|
74
|
+
grid_cols: Optional[int] = None,
|
|
75
|
+
grid_shape: Optional[Tuple[int, int]] = None,
|
|
76
|
+
eps: float = 1e-12,
|
|
77
|
+
return_result_object: bool = False,
|
|
78
|
+
**kwargs: Any,
|
|
79
|
+
) -> Union[ Any, Tuple[Any, Any], SimilaritySearchResult ]:
|
|
80
|
+
"""
|
|
81
|
+
Computes exact cosine similarity between `queries` (M, D) and `corpus_embeddings` (N, D)
|
|
82
|
+
using distributed GPU workers and `OUTPUT_GRID_2D` zero-reduction tiling.
|
|
83
|
+
|
|
84
|
+
Mathematical formulation:
|
|
85
|
+
1. Local O(M*D + N*D) row-wise L2 normalization:
|
|
86
|
+
Q_hat = queries / ||queries||_2, C_hat = corpus_embeddings / ||corpus_embeddings||_2
|
|
87
|
+
2. Distributed verified GEMM with OUTPUT_GRID_2D partitioning:
|
|
88
|
+
S = Q_hat @ C_hat.T in R^{M x N}
|
|
89
|
+
3. Optional exact top-K extraction along each query row.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
queries: 2D array/tensor of shape (M, D) or 1D query vector of shape (D,).
|
|
93
|
+
corpus_embeddings: 2D array/tensor of shape (N, D).
|
|
94
|
+
top_k: Optional number of top matches per query to return. If provided, returns
|
|
95
|
+
`(top_k_scores, top_k_indices)` (or `SimilaritySearchResult` if `return_result_object=True`).
|
|
96
|
+
dtype: Target GEMM precision ('fp32', 'fp16', 'bf16'). Defaults to 'fp32'.
|
|
97
|
+
client: Optional pre-initialized `MatrixComputeClient`.
|
|
98
|
+
config: Optional `ClientConfig` used when creating an ephemeral client.
|
|
99
|
+
tile_size: Optional tile size override.
|
|
100
|
+
grid_rows: Optional row partition count P_r for `OUTPUT_GRID_2D`.
|
|
101
|
+
grid_cols: Optional column partition count P_c for `OUTPUT_GRID_2D`.
|
|
102
|
+
grid_shape: Optional (P_r, P_c) tuple for `OUTPUT_GRID_2D`.
|
|
103
|
+
eps: Numerical stability epsilon for zero-norm vector handling.
|
|
104
|
+
return_result_object: When `top_k` is set, return a `SimilaritySearchResult` object.
|
|
105
|
+
**kwargs: Additional keyword arguments forwarded to `MatrixComputeClient`.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
- If `top_k is None`: Similarity matrix of shape (M, N) (or (N,) if 1D query was passed),
|
|
109
|
+
preserving PyTorch tensor type if inputs were PyTorch tensors.
|
|
110
|
+
- If `top_k is not None`: Tuple `(top_k_scores, top_k_indices)` each of shape (M, top_k),
|
|
111
|
+
sorted in descending order of cosine similarity.
|
|
112
|
+
"""
|
|
113
|
+
is_torch_input = _TORCH_AVAILABLE and isinstance(queries, torch.Tensor)
|
|
114
|
+
torch_device = queries.device if is_torch_input else None
|
|
115
|
+
|
|
116
|
+
if hasattr(queries, "detach") and hasattr(queries, "cpu"):
|
|
117
|
+
q_np = queries.detach().cpu().numpy()
|
|
118
|
+
elif hasattr(queries, "data") and isinstance(getattr(queries, "data", None), np.ndarray):
|
|
119
|
+
q_np = queries.data
|
|
120
|
+
else:
|
|
121
|
+
q_np = np.asarray(queries)
|
|
122
|
+
|
|
123
|
+
if hasattr(corpus_embeddings, "detach") and hasattr(corpus_embeddings, "cpu"):
|
|
124
|
+
c_np = corpus_embeddings.detach().cpu().numpy()
|
|
125
|
+
elif hasattr(corpus_embeddings, "data") and isinstance(getattr(corpus_embeddings, "data", None), np.ndarray):
|
|
126
|
+
c_np = corpus_embeddings.data
|
|
127
|
+
else:
|
|
128
|
+
c_np = np.asarray(corpus_embeddings)
|
|
129
|
+
|
|
130
|
+
squeeze_query = False
|
|
131
|
+
if q_np.ndim == 1:
|
|
132
|
+
q_np = q_np.reshape(1, -1)
|
|
133
|
+
squeeze_query = True
|
|
134
|
+
|
|
135
|
+
if q_np.ndim != 2 or c_np.ndim != 2:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"queries and corpus_embeddings must be 2D matrices. Got shapes {q_np.shape} and {c_np.shape}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
m, d_q = q_np.shape
|
|
141
|
+
n, d_c = c_np.shape
|
|
142
|
+
if m == 0 or n == 0 or d_q == 0:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"queries and corpus_embeddings must be non-empty. Got shapes {q_np.shape} and {c_np.shape}"
|
|
145
|
+
)
|
|
146
|
+
if d_q != d_c:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"Embedding dimension mismatch: queries has D={d_q}, corpus_embeddings has D={d_c}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if top_k is not None:
|
|
152
|
+
if top_k <= 0:
|
|
153
|
+
raise ValueError(f"top_k must be >= 1, got {top_k}")
|
|
154
|
+
if top_k > n:
|
|
155
|
+
raise ValueError(f"top_k ({top_k}) cannot exceed corpus size N ({n})")
|
|
156
|
+
|
|
157
|
+
# 1. Row-wise L2 normalization
|
|
158
|
+
q_norm = l2_normalize_rows(q_np, eps=eps)
|
|
159
|
+
c_norm = l2_normalize_rows(c_np, eps=eps)
|
|
160
|
+
b_transposed = np.ascontiguousarray(c_norm.T) # Shape (D, N)
|
|
161
|
+
|
|
162
|
+
# 2. Execute verified distributed GEMM using OUTPUT_GRID_2D (zero download redundancy)
|
|
163
|
+
def _run_gemm(active_client: MatrixComputeClient) -> np.ndarray:
|
|
164
|
+
return active_client.matmul(
|
|
165
|
+
A=q_norm,
|
|
166
|
+
B=b_transposed,
|
|
167
|
+
dtype=dtype,
|
|
168
|
+
tile_size=tile_size,
|
|
169
|
+
partitioning_strategy=PartitioningStrategy.OUTPUT_GRID_2D,
|
|
170
|
+
grid_rows=grid_rows,
|
|
171
|
+
grid_cols=grid_cols,
|
|
172
|
+
grid_shape=grid_shape,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if client is not None:
|
|
176
|
+
sim_matrix = _run_gemm(client)
|
|
177
|
+
else:
|
|
178
|
+
client_kwargs = dict(kwargs)
|
|
179
|
+
if config is None and "offline_mock_mode" not in client_kwargs:
|
|
180
|
+
client_kwargs["offline_mock_mode"] = True
|
|
181
|
+
with MatrixComputeClient(config=config, **client_kwargs) as temp_client:
|
|
182
|
+
sim_matrix = _run_gemm(temp_client)
|
|
183
|
+
|
|
184
|
+
# Clamp numerical noise to [-1.0, 1.0]
|
|
185
|
+
sim_matrix = np.clip(sim_matrix, -1.0, 1.0)
|
|
186
|
+
|
|
187
|
+
if top_k is None:
|
|
188
|
+
out_sim = sim_matrix[0] if squeeze_query else sim_matrix
|
|
189
|
+
if is_torch_input:
|
|
190
|
+
return torch.from_numpy(out_sim).to(device=torch_device)
|
|
191
|
+
return out_sim
|
|
192
|
+
|
|
193
|
+
# 3. Extract exact top-K indices and scores sorted descending per query row
|
|
194
|
+
if top_k == n:
|
|
195
|
+
sorted_indices = np.argsort(-sim_matrix, axis=1)
|
|
196
|
+
else:
|
|
197
|
+
part_indices = np.argpartition(-sim_matrix, kth=top_k - 1, axis=1)[:, :top_k]
|
|
198
|
+
row_idx = np.arange(m)[:, None]
|
|
199
|
+
part_scores = sim_matrix[row_idx, part_indices]
|
|
200
|
+
order = np.argsort(-part_scores, axis=1)
|
|
201
|
+
sorted_indices = part_indices[row_idx, order]
|
|
202
|
+
|
|
203
|
+
row_idx = np.arange(m)[:, None]
|
|
204
|
+
top_scores = sim_matrix[row_idx, sorted_indices]
|
|
205
|
+
top_indices = sorted_indices.astype(np.int64)
|
|
206
|
+
|
|
207
|
+
if return_result_object:
|
|
208
|
+
return SimilaritySearchResult(
|
|
209
|
+
scores=top_scores,
|
|
210
|
+
indices=top_indices,
|
|
211
|
+
similarity_matrix=sim_matrix,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if is_torch_input:
|
|
215
|
+
return (
|
|
216
|
+
torch.from_numpy(top_scores).to(device=torch_device),
|
|
217
|
+
torch.from_numpy(top_indices).to(device=torch_device, dtype=torch.long),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return top_scores, top_indices
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Distributed PCA & Truncated SVD (`matrix_compute_mcp.ai.spectral`).
|
|
3
|
+
|
|
4
|
+
Executes verified randomized SVD (Halko-Martinsson-Tropp subspace iteration) and
|
|
5
|
+
stateful power-iteration eigen/singular decomposition on remote worker VRAM.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Dict, Iterator, Optional, Tuple, Union
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from matrix_compute.client import MatrixComputeClient
|
|
16
|
+
from matrix_compute.config import ClientConfig, PartitioningStrategy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PCASVDResult:
|
|
21
|
+
"""
|
|
22
|
+
Structured output of `distributed_pca_svd`.
|
|
23
|
+
|
|
24
|
+
Supports both attribute access (`res.U`, `res.S`, `res.Vt`, `res.components`,
|
|
25
|
+
`res.explained_variance`, `res.explained_variance_ratio`, `res.transformed`)
|
|
26
|
+
and 3-tuple unpacking (`U, S, Vt = distributed_pca_svd(A, k=4)`).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
U: np.ndarray
|
|
30
|
+
S: np.ndarray
|
|
31
|
+
Vt: np.ndarray
|
|
32
|
+
explained_variance: np.ndarray
|
|
33
|
+
explained_variance_ratio: np.ndarray
|
|
34
|
+
mean: Optional[np.ndarray] = None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def components(self) -> np.ndarray:
|
|
38
|
+
"""Principal axes in feature space, shape (k, N)."""
|
|
39
|
+
return self.Vt
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def singular_values(self) -> np.ndarray:
|
|
43
|
+
return self.S
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def transformed(self) -> np.ndarray:
|
|
47
|
+
"""Projected low-dimensional representation U * S of shape (M, k)."""
|
|
48
|
+
return self.U * self.S.reshape(1, -1)
|
|
49
|
+
|
|
50
|
+
def __iter__(self) -> Iterator[np.ndarray]:
|
|
51
|
+
yield self.U
|
|
52
|
+
yield self.S
|
|
53
|
+
yield self.Vt
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, idx: int) -> np.ndarray:
|
|
56
|
+
return (self.U, self.S, self.Vt)[idx]
|
|
57
|
+
|
|
58
|
+
def reconstruct(self) -> np.ndarray:
|
|
59
|
+
"""Reconstructs the rank-k approximation in original space."""
|
|
60
|
+
approx = (self.U * self.S.reshape(1, -1)) @ self.Vt
|
|
61
|
+
if self.mean is not None:
|
|
62
|
+
approx = approx + self.mean.reshape(1, -1)
|
|
63
|
+
return approx
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
66
|
+
return {
|
|
67
|
+
"k": int(self.S.shape[0]),
|
|
68
|
+
"shape_u": list(self.U.shape),
|
|
69
|
+
"shape_s": list(self.S.shape),
|
|
70
|
+
"shape_vt": list(self.Vt.shape),
|
|
71
|
+
"singular_values": self.S.tolist(),
|
|
72
|
+
"explained_variance": self.explained_variance.tolist(),
|
|
73
|
+
"explained_variance_ratio": self.explained_variance_ratio.tolist(),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _stabilize_svd_signs(u: np.ndarray, vt: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
78
|
+
"""
|
|
79
|
+
Enforces deterministic sign convention on singular vectors: the largest-magnitude
|
|
80
|
+
entry in each column of U is made non-negative.
|
|
81
|
+
"""
|
|
82
|
+
max_abs_rows = np.argmax(np.abs(u), axis=0)
|
|
83
|
+
signs = np.sign(u[max_abs_rows, range(u.shape[1])])
|
|
84
|
+
signs[signs == 0] = 1.0
|
|
85
|
+
u_fixed = u * signs.reshape(1, -1)
|
|
86
|
+
vt_fixed = vt * signs.reshape(-1, 1)
|
|
87
|
+
return u_fixed, vt_fixed
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def distributed_pca_svd(
|
|
91
|
+
matrix: Any,
|
|
92
|
+
k: int,
|
|
93
|
+
center: bool = True,
|
|
94
|
+
method: str = "randomized",
|
|
95
|
+
oversampling: int = 8,
|
|
96
|
+
power_iters: int = 2,
|
|
97
|
+
iterations: int = 30,
|
|
98
|
+
spot_check_probability: float = 0.25,
|
|
99
|
+
dtype: str = "fp32",
|
|
100
|
+
client: Optional[MatrixComputeClient] = None,
|
|
101
|
+
config: Optional[ClientConfig] = None,
|
|
102
|
+
random_state: int = 42,
|
|
103
|
+
**kwargs: Any,
|
|
104
|
+
) -> PCASVDResult:
|
|
105
|
+
r"""
|
|
106
|
+
Computes the top-k Singular Value Decomposition / Principal Component Analysis
|
|
107
|
+
of a 2D matrix $A \in \mathbb{R}^{M \times N}$ using verified distributed GPU execution.
|
|
108
|
+
|
|
109
|
+
Supported methods:
|
|
110
|
+
1. `method="randomized"` (Default):
|
|
111
|
+
Halko-Martinsson-Tropp randomized SVD with subspace power iterations:
|
|
112
|
+
- $Y = A \Omega$ via `client.matmul()`
|
|
113
|
+
- For $j = 1 \dots q$: QR-stabilized $Z = A^T Q$, $Y = A Q_Z$ via `client.matmul()`
|
|
114
|
+
- $B = Q^T A$ via `client.matmul()`
|
|
115
|
+
- Exact small SVD of $B = \tilde{U} \Sigma V^T$ locally.
|
|
116
|
+
2. `method="power_iteration"`:
|
|
117
|
+
Stateful remote VRAM session leasing via `client.power_iteration()` on the
|
|
118
|
+
Gram matrix $G = A^T A$ (or $A A^T$) with Hotelling deflation across $k$ components.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
matrix: 2D array or tensor of shape (M, N).
|
|
122
|
+
k: Number of top singular components to extract ($1 \le k \le \min(M, N)$).
|
|
123
|
+
center: If True, mean-centers columns before decomposition (standard PCA).
|
|
124
|
+
method: `'randomized'` or `'power_iteration'`.
|
|
125
|
+
oversampling: Extra sketch dimensions $p$ for randomized SVD (default 8).
|
|
126
|
+
power_iters: Number of subspace power iterations for randomized SVD (default 2).
|
|
127
|
+
iterations: Number of stateful power steps per component when `method="power_iteration"`.
|
|
128
|
+
spot_check_probability: Freivalds spot-check probability for stateful power iteration.
|
|
129
|
+
dtype: Target precision ('fp32', 'fp16', 'bf16').
|
|
130
|
+
client: Optional pre-initialized `MatrixComputeClient`.
|
|
131
|
+
config: Optional `ClientConfig` instance.
|
|
132
|
+
random_state: Deterministic PRNG seed.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
`PCASVDResult` (supports `U, S, Vt = distributed_pca_svd(matrix, k)`).
|
|
136
|
+
"""
|
|
137
|
+
if hasattr(matrix, "detach") and hasattr(matrix, "cpu"):
|
|
138
|
+
arr = matrix.detach().cpu().numpy()
|
|
139
|
+
elif hasattr(matrix, "data") and isinstance(getattr(matrix, "data", None), np.ndarray):
|
|
140
|
+
arr = matrix.data
|
|
141
|
+
else:
|
|
142
|
+
arr = np.asarray(matrix, dtype=np.float32)
|
|
143
|
+
|
|
144
|
+
if arr.ndim != 2:
|
|
145
|
+
raise ValueError(f"Expected 2D matrix for PCA/SVD, got shape {arr.shape}")
|
|
146
|
+
|
|
147
|
+
m, n = arr.shape
|
|
148
|
+
min_dim = min(m, n)
|
|
149
|
+
if m == 0 or n == 0:
|
|
150
|
+
raise ValueError(f"Matrix dimensions must be non-zero, got shape {arr.shape}")
|
|
151
|
+
if k <= 0 or k > min_dim:
|
|
152
|
+
raise ValueError(f"k must satisfy 1 <= k <= min(M, N)={min_dim}, got k={k}")
|
|
153
|
+
|
|
154
|
+
norm_method = method.strip().lower()
|
|
155
|
+
if norm_method not in ("randomized", "power_iteration"):
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"Unsupported method '{method}'. Supported: 'randomized', 'power_iteration'"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
a_work = np.ascontiguousarray(arr, dtype=np.float32)
|
|
161
|
+
col_mean: Optional[np.ndarray] = None
|
|
162
|
+
if center:
|
|
163
|
+
col_mean = np.mean(a_work, axis=0, dtype=np.float32)
|
|
164
|
+
a_work = a_work - col_mean.reshape(1, -1)
|
|
165
|
+
|
|
166
|
+
total_var_sum = float(np.sum(a_work * a_work))
|
|
167
|
+
dof = max(1, m - 1) if center else max(1, m)
|
|
168
|
+
total_variance = total_var_sum / float(dof)
|
|
169
|
+
|
|
170
|
+
def _run_decomposition(active_client: MatrixComputeClient) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
171
|
+
rng = np.random.RandomState(random_state)
|
|
172
|
+
|
|
173
|
+
if norm_method == "randomized":
|
|
174
|
+
sketch_dim = min(min_dim, k + max(0, int(oversampling)))
|
|
175
|
+
omega = rng.randn(n, sketch_dim).astype(np.float32)
|
|
176
|
+
|
|
177
|
+
# Range finder: Y = A @ Omega
|
|
178
|
+
y_mat = active_client.matmul(
|
|
179
|
+
A=a_work,
|
|
180
|
+
B=omega,
|
|
181
|
+
dtype=dtype,
|
|
182
|
+
partitioning_strategy=PartitioningStrategy.OUTPUT_GRID_2D,
|
|
183
|
+
)
|
|
184
|
+
q_mat, _ = np.linalg.qr(y_mat, mode="reduced")
|
|
185
|
+
|
|
186
|
+
# Subspace power iterations for rapid singular spectrum decay
|
|
187
|
+
for _ in range(max(0, int(power_iters))):
|
|
188
|
+
z_mat = active_client.matmul(
|
|
189
|
+
A=np.ascontiguousarray(a_work.T),
|
|
190
|
+
B=np.ascontiguousarray(q_mat),
|
|
191
|
+
dtype=dtype,
|
|
192
|
+
partitioning_strategy=PartitioningStrategy.OUTPUT_GRID_2D,
|
|
193
|
+
)
|
|
194
|
+
q_z, _ = np.linalg.qr(z_mat, mode="reduced")
|
|
195
|
+
y_mat = active_client.matmul(
|
|
196
|
+
A=a_work,
|
|
197
|
+
B=np.ascontiguousarray(q_z),
|
|
198
|
+
dtype=dtype,
|
|
199
|
+
partitioning_strategy=PartitioningStrategy.OUTPUT_GRID_2D,
|
|
200
|
+
)
|
|
201
|
+
q_mat, _ = np.linalg.qr(y_mat, mode="reduced")
|
|
202
|
+
|
|
203
|
+
# Project onto low-dimensional subspace: B = Q^T @ A
|
|
204
|
+
b_proj = active_client.matmul(
|
|
205
|
+
A=np.ascontiguousarray(q_mat.T),
|
|
206
|
+
B=a_work,
|
|
207
|
+
dtype=dtype,
|
|
208
|
+
partitioning_strategy=PartitioningStrategy.OUTPUT_GRID_2D,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
u_tilde, s_vals, vt_all = np.linalg.svd(b_proj, full_matrices=False)
|
|
212
|
+
u_full = q_mat @ u_tilde
|
|
213
|
+
|
|
214
|
+
u_k = np.ascontiguousarray(u_full[:, :k], dtype=np.float32)
|
|
215
|
+
s_k = np.ascontiguousarray(s_vals[:k], dtype=np.float32)
|
|
216
|
+
vt_k = np.ascontiguousarray(vt_all[:k, :], dtype=np.float32)
|
|
217
|
+
u_k, vt_k = _stabilize_svd_signs(u_k, vt_k)
|
|
218
|
+
return u_k, s_k, vt_k
|
|
219
|
+
|
|
220
|
+
# Stateful power_iteration method using remote VRAM session leasing
|
|
221
|
+
# Form Gram matrix G = A^T @ A (shape N x N) via distributed GEMM
|
|
222
|
+
gram = active_client.matmul(
|
|
223
|
+
A=np.ascontiguousarray(a_work.T),
|
|
224
|
+
B=a_work,
|
|
225
|
+
dtype=dtype,
|
|
226
|
+
partitioning_strategy=PartitioningStrategy.OUTPUT_GRID_2D,
|
|
227
|
+
)
|
|
228
|
+
gram_deflated = gram.astype(np.float32).copy()
|
|
229
|
+
|
|
230
|
+
v_list = []
|
|
231
|
+
s_list = []
|
|
232
|
+
u_list = []
|
|
233
|
+
|
|
234
|
+
for comp_idx in range(k):
|
|
235
|
+
v0 = rng.randn(n).astype(np.float32)
|
|
236
|
+
# Orthogonalize v0 against previously found right singular vectors
|
|
237
|
+
for prev_v in v_list:
|
|
238
|
+
v0 = v0 - np.dot(v0, prev_v) * prev_v
|
|
239
|
+
v0_norm = float(np.linalg.norm(v0))
|
|
240
|
+
if v0_norm > 1e-12:
|
|
241
|
+
v0 = v0 / v0_norm
|
|
242
|
+
|
|
243
|
+
v_final = active_client.power_iteration(
|
|
244
|
+
A=gram_deflated,
|
|
245
|
+
v0=v0,
|
|
246
|
+
iterations=max(1, int(iterations)),
|
|
247
|
+
dtype=dtype,
|
|
248
|
+
spot_check_probability=spot_check_probability,
|
|
249
|
+
normalize=True,
|
|
250
|
+
)
|
|
251
|
+
v_vec = np.asarray(v_final, dtype=np.float32).reshape(-1)
|
|
252
|
+
# Re-orthogonalize against previous vectors for numerical cleanliness
|
|
253
|
+
for prev_v in v_list:
|
|
254
|
+
v_vec = v_vec - np.dot(v_vec, prev_v) * prev_v
|
|
255
|
+
v_norm = float(np.linalg.norm(v_vec))
|
|
256
|
+
if v_norm > 1e-12:
|
|
257
|
+
v_vec = v_vec / v_norm
|
|
258
|
+
|
|
259
|
+
# Compute u_i * sigma_i = A @ v_i via verified distributed GEMM
|
|
260
|
+
av_col = active_client.matmul(
|
|
261
|
+
A=a_work,
|
|
262
|
+
B=v_vec.reshape(n, 1),
|
|
263
|
+
dtype=dtype,
|
|
264
|
+
partitioning_strategy=PartitioningStrategy.OUTPUT_GRID_2D,
|
|
265
|
+
).reshape(-1)
|
|
266
|
+
sigma = float(np.linalg.norm(av_col))
|
|
267
|
+
if sigma > 1e-12:
|
|
268
|
+
u_vec = av_col / sigma
|
|
269
|
+
else:
|
|
270
|
+
u_vec = np.zeros(m, dtype=np.float32)
|
|
271
|
+
u_vec[comp_idx % m] = 1.0
|
|
272
|
+
|
|
273
|
+
v_list.append(v_vec)
|
|
274
|
+
s_list.append(sigma)
|
|
275
|
+
u_list.append(u_vec)
|
|
276
|
+
|
|
277
|
+
# Hotelling deflation: G <- G - sigma^2 * v_i v_i^T
|
|
278
|
+
gram_deflated = gram_deflated - (sigma * sigma) * np.outer(v_vec, v_vec)
|
|
279
|
+
|
|
280
|
+
u_k = np.column_stack(u_list).astype(np.float32)
|
|
281
|
+
s_k = np.array(s_list, dtype=np.float32)
|
|
282
|
+
vt_k = np.vstack(v_list).astype(np.float32)
|
|
283
|
+
u_k, vt_k = _stabilize_svd_signs(u_k, vt_k)
|
|
284
|
+
return u_k, s_k, vt_k
|
|
285
|
+
|
|
286
|
+
if client is not None:
|
|
287
|
+
u_out, s_out, vt_out = _run_decomposition(client)
|
|
288
|
+
else:
|
|
289
|
+
client_kwargs = dict(kwargs)
|
|
290
|
+
if config is None and "offline_mock_mode" not in client_kwargs:
|
|
291
|
+
client_kwargs["offline_mock_mode"] = True
|
|
292
|
+
with MatrixComputeClient(config=config, **client_kwargs) as temp_client:
|
|
293
|
+
u_out, s_out, vt_out = _run_decomposition(temp_client)
|
|
294
|
+
|
|
295
|
+
explained_var = (s_out ** 2) / float(dof)
|
|
296
|
+
if total_variance > 1e-12:
|
|
297
|
+
explained_ratio = np.clip(explained_var / total_variance, 0.0, 1.0)
|
|
298
|
+
else:
|
|
299
|
+
explained_ratio = np.zeros_like(explained_var)
|
|
300
|
+
|
|
301
|
+
return PCASVDResult(
|
|
302
|
+
U=u_out,
|
|
303
|
+
S=s_out,
|
|
304
|
+
Vt=vt_out,
|
|
305
|
+
explained_variance=explained_var.astype(np.float32),
|
|
306
|
+
explained_variance_ratio=explained_ratio.astype(np.float32),
|
|
307
|
+
mean=col_mean,
|
|
308
|
+
)
|