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 +49 -0
- scao/cuda/__init__.py +130 -0
- scao/cuda/setup.py +33 -0
- scao/distributed.py +114 -0
- scao/integrations/__init__.py +0 -0
- scao/integrations/huggingface.py +246 -0
- scao/logging.py +163 -0
- scao/optimizer.py +516 -0
- scao/preconditioner.py +399 -0
- scao/py.typed +0 -0
- scao/utils.py +206 -0
- scao-0.1.0.dist-info/METADATA +688 -0
- scao-0.1.0.dist-info/RECORD +16 -0
- scao-0.1.0.dist-info/WHEEL +5 -0
- scao-0.1.0.dist-info/licenses/LICENSE +179 -0
- scao-0.1.0.dist-info/top_level.txt +1 -0
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()
|