scao 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.
scao/__init__.py ADDED
@@ -0,0 +1,49 @@
1
+ """
2
+ SCAO - Sparse Curvature-Aware Adaptive Optimizer
3
+ =================================================
4
+ A second-order PyTorch optimizer that delivers Shampoo-quality
5
+ preconditioned gradients at near-AdamW memory and throughput cost.
6
+
7
+ Quickstart
8
+ ----------
9
+ import scao
10
+
11
+ optimizer = scao.SCAO(model.parameters(), lr=3.5e-4)
12
+
13
+ # or, explicit:
14
+ from scao import SCAO
15
+ optimizer = SCAO(
16
+ model.parameters(),
17
+ lr=3.5e-4,
18
+ weight_decay=0.1,
19
+ warmup_steps=100,
20
+ precond_freq=10,
21
+ )
22
+
23
+ Paper
24
+ -----
25
+ SCAO: Sparse Curvature-Aware Adaptive Optimization for Large-Scale Models
26
+ NeurIPS 2026 (under review)
27
+ """
28
+
29
+ from .optimizer import SCAO
30
+ from .preconditioner import SparsePreconditioner
31
+ from .utils import matrix_power_neg_quarter, adaptive_rank
32
+ from . import logging as scao_logging
33
+
34
+ __version__ = "0.1.0"
35
+ __author__ = "SCAO Authors"
36
+ __license__ = "Apache-2.0"
37
+
38
+ __all__ = [
39
+ # Main API — this is all most users need
40
+ "SCAO",
41
+ # Advanced / internals
42
+ "SparsePreconditioner",
43
+ "matrix_power_neg_quarter",
44
+ "adaptive_rank",
45
+ "scao_logging",
46
+ # Metadata
47
+ "__version__",
48
+ ]
49
+
scao/cuda/__init__.py ADDED
@@ -0,0 +1,130 @@
1
+ """
2
+ SCAO CUDA acceleration layer (optional).
3
+ =========================================
4
+ Provides faster implementations of the critical hot paths.
5
+ Falls back gracefully to pure-PyTorch if CUDA is not available or
6
+ the CUDA extension has not been compiled.
7
+
8
+ To compile the CUDA extension:
9
+ cd scao/cuda
10
+ python setup.py build_ext --inplace
11
+
12
+ Or install from the project root:
13
+ pip install -e ".[cuda]"
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import torch
19
+ from torch import Tensor
20
+
21
+ _cuda_ext = None # Will hold the compiled C++/CUDA extension if available
22
+
23
+
24
+ def _try_load_cuda_ext():
25
+ global _cuda_ext
26
+ if _cuda_ext is not None:
27
+ return _cuda_ext
28
+ try:
29
+ from scao.cuda import _scao_cuda # type: ignore[import]
30
+ _cuda_ext = _scao_cuda
31
+ except ImportError:
32
+ _cuda_ext = None
33
+ return _cuda_ext
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Low-rank matrix-matrix multiply (U diag(s) U^T) @ G
38
+ # ---------------------------------------------------------------------------
39
+
40
+ def low_rank_precond_mm(
41
+ U: Tensor,
42
+ s_inv_quarter: Tensor,
43
+ G: Tensor,
44
+ side: str = "left",
45
+ ) -> Tensor:
46
+ """
47
+ Compute U * diag(s_inv_quarter) * (U^T * G) if side='left'
48
+ or G * U * diag(s_inv_quarter) * U^T if side='right'
49
+
50
+ Uses fused CUDA kernel if available, otherwise pure PyTorch.
51
+
52
+ Args:
53
+ U: (m, k) orthonormal matrix
54
+ s_inv_quarter: (k,) vector of S^{-1/4} scaling factors
55
+ G: (m, n) gradient matrix
56
+ side: 'left' or 'right'
57
+
58
+ Returns:
59
+ Result tensor of same shape as G.
60
+ """
61
+ ext = _try_load_cuda_ext()
62
+ if ext is not None and G.is_cuda:
63
+ try:
64
+ return ext.low_rank_precond_mm(U, s_inv_quarter, G, side == "left")
65
+ except (AttributeError, RuntimeError):
66
+ pass # Fall through to PyTorch fallback
67
+
68
+ # Pure PyTorch fallback
69
+ if side == "left":
70
+ # (k, n) = (m, k)^T @ (m, n)
71
+ proj = U.T @ G
72
+ # scale rows
73
+ proj = proj * s_inv_quarter.unsqueeze(1)
74
+ # (m, n) = (m, k) @ (k, n)
75
+ return U @ proj
76
+ else:
77
+ # (m, k) = (m, n) @ (n, k)
78
+ proj = G @ U
79
+ # scale cols
80
+ proj = proj * s_inv_quarter.unsqueeze(0)
81
+ # (m, n) = (m, k) @ (k, n)^T
82
+ return proj @ U.T
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Batched eigendecomposition with truncation
87
+ # ---------------------------------------------------------------------------
88
+
89
+ def truncated_eigh(
90
+ A: Tensor,
91
+ k: int,
92
+ eps: float = 1e-8,
93
+ ) -> tuple[Tensor, Tensor]:
94
+ """
95
+ Compute the top-k eigenvalues and eigenvectors of symmetric PSD matrix A.
96
+
97
+ Uses CUDA LOBPCG if available; falls back to full eigh + slice.
98
+
99
+ Args:
100
+ A: (m, m) symmetric PSD matrix
101
+ k: number of top eigenpairs to return
102
+
103
+ Returns:
104
+ (eigenvalues, eigenvectors) of shapes (k,) and (m, k),
105
+ eigenvalues in descending order.
106
+ """
107
+ ext = _try_load_cuda_ext()
108
+ m = A.shape[0]
109
+
110
+ if k >= m:
111
+ # Full decomposition
112
+ S, U = torch.linalg.eigh(A)
113
+ return S.flip(0).clamp(min=0.0), U.flip(1)
114
+
115
+ if ext is not None and A.is_cuda:
116
+ try:
117
+ S, U = ext.truncated_eigh(A, k)
118
+ return S, U
119
+ except (AttributeError, RuntimeError):
120
+ pass
121
+
122
+ # PyTorch fallback: full eigh + truncate (LOBPCG is less stable for small k)
123
+ try:
124
+ S, U = torch.linalg.eigh(A)
125
+ except torch.linalg.LinAlgError:
126
+ S, U = torch.linalg.eigh(A + eps * torch.eye(m, device=A.device, dtype=A.dtype))
127
+
128
+ S = S.flip(0).clamp(min=0.0)
129
+ U = U.flip(1)
130
+ return S[:k], U[:, :k]
scao/cuda/setup.py ADDED
@@ -0,0 +1,33 @@
1
+ """
2
+ Optional CUDA extension build script for SCAO.
3
+ Run: python setup.py build_ext --inplace
4
+ """
5
+
6
+ from setuptools import setup
7
+ try:
8
+ from torch.utils.cpp_extension import CUDAExtension, BuildExtension
9
+ ext_modules = [
10
+ CUDAExtension(
11
+ name="scao.cuda._scao_cuda",
12
+ sources=["low_rank_ops.cu"],
13
+ extra_compile_args={
14
+ "cxx": ["-O3"],
15
+ "nvcc": [
16
+ "-O3",
17
+ "--use_fast_math",
18
+ "-gencode=arch=compute_80,code=sm_80", # A100
19
+ "-gencode=arch=compute_89,code=sm_89", # H100
20
+ ],
21
+ },
22
+ )
23
+ ]
24
+ cmdclass = {"build_ext": BuildExtension}
25
+ except ImportError:
26
+ ext_modules = []
27
+ cmdclass = {}
28
+
29
+ setup(
30
+ name="scao_cuda",
31
+ ext_modules=ext_modules,
32
+ cmdclass=cmdclass,
33
+ )
scao/distributed.py ADDED
@@ -0,0 +1,114 @@
1
+ """
2
+ SCAO distributed training utilities.
3
+ =====================================
4
+ Helpers for ZeRO-3 (DeepSpeed) and FSDP (PyTorch ≥ 2.0) compatibility.
5
+
6
+ Usage with FSDP
7
+ ---------------
8
+ from scao.distributed import wrap_scao_for_fsdp
9
+
10
+ model = FSDP(model, ...)
11
+ optimizer = wrap_scao_for_fsdp(SCAO(model.parameters(), lr=1e-3))
12
+
13
+ Usage with DeepSpeed ZeRO-3
14
+ ----------------------------
15
+ SCAO is compatible with ZeRO-3 out of the box as long as you do NOT enable
16
+ stage 3 optimizer state partitioning for preconditioner tensors (they must
17
+ live on the rank that owns the corresponding parameters).
18
+
19
+ In your DeepSpeed config, set:
20
+ "zero_optimization": {
21
+ "stage": 3,
22
+ "stage3_param_persistence_threshold": 1e4
23
+ }
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import warnings
29
+ from typing import TYPE_CHECKING
30
+
31
+ import torch
32
+ import torch.distributed as dist
33
+
34
+ if TYPE_CHECKING:
35
+ from .optimizer import SCAO
36
+
37
+
38
+ def sync_preconditioners(optimizer: "SCAO", process_group=None) -> None:
39
+ """
40
+ All-reduce preconditioner eigenfactors across all ranks.
41
+
42
+ In FSDP/ZeRO setups where parameters are sharded, each rank computes
43
+ curvature from its local gradient shard. This function averages those
44
+ estimates so every rank has a globally consistent preconditioner.
45
+
46
+ Call this AFTER optimizer.step() and BEFORE the next forward pass, or
47
+ better: schedule it on the preconditioner update cadence (precond_freq).
48
+
49
+ Args:
50
+ optimizer: SCAO optimizer instance.
51
+ process_group: optional distributed process group (default: global group).
52
+ """
53
+ if not dist.is_available() or not dist.is_initialized():
54
+ return
55
+
56
+ world_size = dist.get_world_size(group=process_group)
57
+ if world_size == 1:
58
+ return
59
+
60
+ handles = []
61
+ tensors_to_sync: list[torch.Tensor] = []
62
+
63
+ for state in optimizer.state.values():
64
+ if "preconditioner" not in state:
65
+ continue
66
+ prec = state["preconditioner"]
67
+ if prec.use_kronecker:
68
+ # Average eigenfactor matrices across ranks
69
+ for t in (prec.U_l, prec.S_l, prec.U_r, prec.S_r,
70
+ prec.L_ema, prec.R_ema):
71
+ tensors_to_sync.append(t)
72
+ else:
73
+ tensors_to_sync.append(prec.diag_ema)
74
+
75
+ # Batch all-reduce into as few calls as possible
76
+ for t in tensors_to_sync:
77
+ h = dist.all_reduce(t, op=dist.ReduceOp.SUM, group=process_group, async_op=True)
78
+ handles.append((h, t))
79
+
80
+ for h, t in handles:
81
+ h.wait()
82
+ t.div_(world_size)
83
+
84
+
85
+ def wrap_scao_for_fsdp(optimizer: "SCAO") -> "SCAO":
86
+ """
87
+ Register a post-step hook that synchronises preconditioners after each
88
+ curvature update in FSDP training.
89
+
90
+ Returns the same optimizer (mutation in place), for chaining:
91
+ optimizer = wrap_scao_for_fsdp(SCAO(...))
92
+ """
93
+ try:
94
+ from torch.distributed.fsdp import FullyShardedDataParallel as FSDP # noqa: F401
95
+ except ImportError:
96
+ warnings.warn("torch.distributed.fsdp not available; FSDP wrapping skipped.")
97
+ return optimizer
98
+
99
+ original_step = optimizer.step
100
+
101
+ def patched_step(closure=None):
102
+ result = original_step(closure)
103
+ # Synchronise preconditioners on curvature-update steps
104
+ step = next(
105
+ (s.get("step", 0) for s in optimizer.state.values() if "step" in s),
106
+ 0,
107
+ )
108
+ if step % optimizer.defaults["precond_freq"] == 0:
109
+ optimizer.synchronize_precond()
110
+ sync_preconditioners(optimizer)
111
+ return result
112
+
113
+ optimizer.step = patched_step # type: ignore[method-assign]
114
+ return optimizer
File without changes
@@ -0,0 +1,246 @@
1
+ """
2
+ HuggingFace Transformers integration for SCAO
3
+ ==============================================
4
+
5
+ Provides three integration paths:
6
+
7
+ Path A — Manual (recommended for control)
8
+ -----------------------------------------
9
+ from scao import SCAO
10
+ from scao.integrations.huggingface import get_scao_optimizer
11
+
12
+ optimizer, scheduler = get_scao_optimizer(model, training_args)
13
+ trainer = Trainer(model=model, args=training_args,
14
+ optimizers=(optimizer, scheduler))
15
+
16
+ Path B — SCAOTrainer drop-in
17
+ -----------------------------
18
+ from scao.integrations.huggingface import SCAOTrainer
19
+
20
+ trainer = SCAOTrainer(
21
+ model=model,
22
+ args=training_args,
23
+ train_dataset=train_dataset,
24
+ scao_kwargs=dict(precond_freq=20, k_min=8, k_max=64),
25
+ )
26
+ trainer.train()
27
+
28
+ Path C — Trainer callback for monitoring
29
+ -----------------------------------------
30
+ from scao.integrations.huggingface import SCAOMonitorCallback
31
+ from scao import SCAO
32
+
33
+ optimizer = SCAO(model.parameters(), lr=training_args.learning_rate)
34
+ trainer = Trainer(model=model, args=training_args,
35
+ optimizers=(optimizer, None))
36
+ trainer.add_callback(SCAOMonitorCallback(optimizer))
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ from typing import TYPE_CHECKING, Any
42
+
43
+ import torch
44
+ import torch.nn as nn
45
+
46
+ if TYPE_CHECKING:
47
+ pass
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Path A: get_scao_optimizer
52
+ # ---------------------------------------------------------------------------
53
+
54
+ def get_scao_optimizer(
55
+ model: nn.Module,
56
+ training_args,
57
+ scao_kwargs: dict | None = None,
58
+ no_decay_names: tuple[str, ...] = ("bias", "LayerNorm.weight", "layer_norm.weight"),
59
+ ):
60
+ """
61
+ Build a SCAO optimizer + learning-rate scheduler compatible with
62
+ HuggingFace ``Trainer``.
63
+
64
+ Applies weight decay only to parameters whose names do NOT match
65
+ ``no_decay_names`` (standard HF practice).
66
+
67
+ Args:
68
+ model: the model to optimise
69
+ training_args: ``transformers.TrainingArguments`` instance
70
+ scao_kwargs: additional SCAO hyperparameters (override defaults)
71
+ no_decay_names: parameter name substrings exempt from weight decay
72
+
73
+ Returns:
74
+ (optimizer, scheduler) tuple ready for ``Trainer(optimizers=...)``
75
+
76
+ Example::
77
+
78
+ from transformers import TrainingArguments, Trainer
79
+ from scao.integrations.huggingface import get_scao_optimizer
80
+
81
+ args = TrainingArguments(output_dir="out", num_train_epochs=3, ...)
82
+ optimizer, scheduler = get_scao_optimizer(model, args)
83
+ trainer = Trainer(model=model, args=args,
84
+ optimizers=(optimizer, scheduler))
85
+ """
86
+ try:
87
+ from transformers import get_scheduler # type: ignore[import]
88
+ except ImportError as e:
89
+ raise ImportError(
90
+ "transformers is required for get_scao_optimizer. "
91
+ "Install it with: pip install transformers"
92
+ ) from e
93
+
94
+ from scao.optimizer import SCAO
95
+
96
+ # Separate parameters with / without weight decay (standard HF split)
97
+ decay_params, no_decay_params = [], []
98
+ for name, param in model.named_parameters():
99
+ if not param.requires_grad:
100
+ continue
101
+ if any(nd in name for nd in no_decay_names):
102
+ no_decay_params.append(param)
103
+ else:
104
+ decay_params.append(param)
105
+
106
+ param_groups = [
107
+ {"params": decay_params, "weight_decay": training_args.weight_decay},
108
+ {"params": no_decay_params, "weight_decay": 0.0},
109
+ ]
110
+
111
+ defaults: dict[str, Any] = dict(
112
+ lr=training_args.learning_rate,
113
+ warmup_steps=int(getattr(training_args, "warmup_steps", 100)),
114
+ )
115
+ if scao_kwargs:
116
+ defaults.update(scao_kwargs)
117
+
118
+ optimizer = SCAO(param_groups, **defaults)
119
+
120
+ # Build HF-compatible LR scheduler
121
+ num_training_steps = getattr(training_args, "max_steps", -1)
122
+ if num_training_steps < 0:
123
+ # Estimate from epochs and dataset size if available
124
+ num_training_steps = 10_000 # safe default; user should override
125
+
126
+ scheduler = get_scheduler(
127
+ name=getattr(training_args, "lr_scheduler_type", "cosine"),
128
+ optimizer=optimizer,
129
+ num_warmup_steps=getattr(training_args, "warmup_steps", 0),
130
+ num_training_steps=num_training_steps,
131
+ )
132
+
133
+ return optimizer, scheduler
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Path B: SCAOTrainer
138
+ # ---------------------------------------------------------------------------
139
+
140
+ def _make_scao_trainer_class():
141
+ """
142
+ Lazily build SCAOTrainer to avoid importing transformers at module load.
143
+ """
144
+ try:
145
+ from transformers import Trainer # type: ignore[import]
146
+ except ImportError:
147
+ return None
148
+
149
+ class SCAOTrainer(Trainer):
150
+ """
151
+ Drop-in ``Trainer`` subclass that uses SCAO as the optimizer.
152
+
153
+ Args:
154
+ scao_kwargs: dict of extra SCAO hyperparameters (e.g.
155
+ ``precond_freq``, ``k_min``, ``k_max``, ``tau``).
156
+
157
+ All other arguments are passed through to ``transformers.Trainer``.
158
+
159
+ Example::
160
+
161
+ trainer = SCAOTrainer(
162
+ model=model,
163
+ args=training_args,
164
+ train_dataset=dataset,
165
+ scao_kwargs=dict(precond_freq=20, k_max=64),
166
+ )
167
+ trainer.train()
168
+ """
169
+
170
+ def __init__(self, *args, scao_kwargs: dict | None = None, **kwargs):
171
+ self._scao_kwargs = scao_kwargs or {}
172
+ super().__init__(*args, **kwargs)
173
+
174
+ def create_optimizer(self):
175
+ if self.optimizer is None:
176
+ optimizer, _ = get_scao_optimizer(
177
+ self.model,
178
+ self.args,
179
+ scao_kwargs=self._scao_kwargs,
180
+ )
181
+ self.optimizer = optimizer
182
+ return self.optimizer
183
+
184
+ def create_optimizer_and_scheduler(self, num_training_steps: int):
185
+ self.create_optimizer()
186
+ if self.lr_scheduler is None:
187
+ try:
188
+ from transformers import get_scheduler # type: ignore[import]
189
+ self.lr_scheduler = get_scheduler(
190
+ name=self.args.lr_scheduler_type,
191
+ optimizer=self.optimizer,
192
+ num_warmup_steps=self.args.get_warmup_steps(num_training_steps),
193
+ num_training_steps=num_training_steps,
194
+ )
195
+ except Exception:
196
+ super().create_optimizer_and_scheduler(num_training_steps)
197
+
198
+ return SCAOTrainer
199
+
200
+
201
+ # Expose SCAOTrainer — will be None if transformers not installed
202
+ SCAOTrainer = _make_scao_trainer_class()
203
+
204
+
205
+ # ---------------------------------------------------------------------------
206
+ # Path C: SCAOMonitorCallback
207
+ # ---------------------------------------------------------------------------
208
+
209
+ def _make_monitor_callback():
210
+ try:
211
+ from transformers import TrainerCallback # type: ignore[import]
212
+ except ImportError:
213
+ return None
214
+
215
+ class SCAOMonitorCallback(TrainerCallback):
216
+ """
217
+ HuggingFace ``TrainerCallback`` that logs SCAO rank and curvature
218
+ health metrics to the Trainer's log at every logging step.
219
+
220
+ Args:
221
+ optimizer: the SCAO optimizer instance
222
+ log_every: override log frequency (default: use Trainer's setting)
223
+
224
+ Example::
225
+
226
+ trainer.add_callback(SCAOMonitorCallback(optimizer))
227
+ """
228
+
229
+ def __init__(self, optimizer, log_every: int | None = None) -> None:
230
+ self.optimizer = optimizer
231
+ self.log_every = log_every
232
+
233
+ def on_log(self, args, state, control, logs=None, **kwargs):
234
+ from scao.logging import collect_metrics
235
+ metrics = collect_metrics(self.optimizer)
236
+ if logs is not None:
237
+ # Inject numeric metrics into the Trainer's log dict
238
+ logs.update(
239
+ {k: v for k, v in metrics.items()
240
+ if isinstance(v, (int, float)) and k != "step"}
241
+ )
242
+
243
+ return SCAOMonitorCallback
244
+
245
+
246
+ SCAOMonitorCallback = _make_monitor_callback()