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.
@@ -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
+ )