Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ on:

jobs:
test:
name: "Test · Python ${{ matrix.python-version }}"
name: "Test · Linux · Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
Expand All @@ -31,10 +31,10 @@ jobs:
pip install -e ".[dev]"

- name: Lint with ruff
run: ruff check scao/ || true
run: ruff check .

- name: Type-check with mypy
run: mypy scao/ --ignore-missing-imports --exclude 'scao/(benchmarks|tests|cuda)' || true
run: mypy scao/ --ignore-missing-imports --exclude 'scao/(benchmarks|tests|cuda)'

- name: Run tests
run: pytest scao/tests/ -v --tb=short --cov=scao --cov-report=xml --ignore=scao/tests/test_profiling.py --ignore=scao/tests/test_ddp.py
Expand All @@ -48,6 +48,27 @@ jobs:
files: coverage.xml
fail_ci_if_error: false

test-windows:
name: "Test · Windows · Python 3.11"
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install -e ".[dev]"

- name: Run unit & DDP tests
run: pytest scao/tests/ -v --tb=short --ignore=scao/tests/test_profiling.py

benchmark-smoke:
name: "Benchmark smoke test"
runs-on: ubuntu-latest
Expand All @@ -63,9 +84,7 @@ jobs:
pip install -e .
- name: Run quick benchmark
run: |
python scao/benchmarks/gpt_scale_benchmark.py \
--scales tiny --steps 30 --optimizers "adamw,scao" \
--seeds "42" --csv /tmp/smoke.csv
python scao/benchmarks/gpt_scale_benchmark.py --scales tiny --steps 30 --optimizers "adamw,scao" --seeds "42" --csv /tmp/smoke.csv

build:
name: "Build distribution"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ MANIFEST
.build/
*.whl
*.tar.gz
scao-*/

# Testing
.tox/
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## [0.2.2] — 2026-10-04

### Added
- **Triton JIT Acceleration**: Added OpenAI Triton kernels (`scao/cuda/triton_ops.py`) for GPU acceleration without requiring a precompiled C++/CUDA extension or host `nvcc` compiler.
- **Axolotl Integration**: Official native support for Axolotl fine-tuning framework (`optimizer: scao`) documented in README and verified with E2E tests.
- **Cross-Platform CI**: Added Windows runner (`windows-latest`) to GitHub Actions CI matrix to test DDP and file paths natively on Windows.
- **Python 3.13 Support**: Added Python 3.13 to CI test matrix.

### Changed
- **Modular Core Architecture**: Refactored `scao/optimizer.py` into focused, reusable modules while preserving 100% backward compatibility:
- `scao/filters.py`: Gradient filters (`_SparseGradFilter`, `_DynamicSparseFilter`), adaptive warmup, gSNR element-wise clipping, and lazy triggers.
- `scao/presets.py`: Model scale presets (`scao_sub1b` through `scao_125b`).
- `scao/optimizer.py`: Lean optimizer step loop and state management.
- **Strict CI Verification**: Removed `|| true` masking from `ruff` and `mypy` in CI workflows, enforcing zero-regression quality gates.
- **Repository Hygiene**: Removed legacy build directories and added `scao-*/` to `.gitignore`.

---

## [0.2.0] — 2026-04-28

### Added (SCAO v2)
Expand Down
36 changes: 32 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.19870556.svg)](https://doi.org/10.5281/zenodo.19870556)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org)
[![PyTorch](https://img.shields.io/badge/pytorch-2.0%2B-orange)](https://pytorch.org)
[![Axolotl](https://img.shields.io/badge/Axolotl-supported-blueviolet.svg)](https://github.com/axolotl-ai-cloud/axolotl/pull/3624)

> **A second-order PyTorch optimizer that delivers Shampoo-quality preconditioned gradients at near-AdamW memory and throughput cost.**
> Drop-in replacement for `AdamW`. One-line change. Real gains.
> **Now available on PyPI:** `pip install scao`
> **Now available on PyPI:** `pip install scao`
> **Native support:** [Axolotl](https://github.com/axolotl-ai-cloud/axolotl/pull/3624) (`optimizer: scao`) and HuggingFace `Trainer`.

---

Expand Down Expand Up @@ -555,6 +557,26 @@ trainer = SCAOTrainer(
trainer.train()
```

### Axolotl (LLM Fine-Tuning)

SCAO is integrated as a native custom optimizer in [Axolotl](https://github.com/axolotl-ai-cloud/axolotl) (PR [#3624](https://github.com/axolotl-ai-cloud/axolotl/pull/3624)).

To use SCAO in your Axolotl YAML configuration:

```yaml
# In your Axolotl config.yaml
optimizer: scao

# Optional SCAO hyperparameters passed through optim_args
optim_args:
precond_freq: 20 # Curvature update frequency
k_min: 8 # Minimum rank
k_max: 64 # Maximum rank
sparsity: 0.7 # Base gradient sparsity
use_int8_ema: true # 4x memory reduction on curvature buffers (recommended for LoRA/QLoRA)
dynamic_sparsity: true # Adaptive per-layer gradient sparsity
```

### Monitoring and diagnostics

```python
Expand Down Expand Up @@ -687,20 +709,26 @@ Open [`scripts/scao_colab_benchmark.ipynb`](scripts/scao_colab_benchmark.ipynb)
```
scao/ # Core library
├── optimizer.py # SCAO main class — drop-in for AdamW
├── filters.py # Gradient filters: dynamic sparsity, gSNR clipping, warmup
├── presets.py # Scale presets: scao_sub1b through scao_125b
├── preconditioner.py # SparsePreconditioner: Kronecker low-rank + int8 EMA
├── utils.py # adaptive_rank, quantize_sym_int8, dequantize_sym_int8
├── distributed.py # ZeRO-3 / FSDP helpers
├── logging.py # ConsoleLogger, TensorBoardLogger, WandbLogger
├── integrations/
│ └── huggingface.py # SCAOTrainer, SCAOMonitorCallback
│ └── huggingface.py # SCAOTrainer, get_scao_optimizer, SCAOMonitorCallback
├── benchmarks/
│ └── gpt_scale_benchmark.py # Multi-scale GPT: SCAO vs AdamW vs SCAO-int8
├── tests/
│ ├── test_optimizer.py # 40 optimizer correctness tests
│ ├── test_optimizer.py # 40+ optimizer correctness tests
│ ├── test_ddp.py # DistributedDataParallel tests (Gloo / CPU)
│ ├── test_huggingface_integration.py # HF Trainer & PEFT integration tests
│ ├── test_scale_planner.py # Memory & scaling verification tests
│ └── test_profiling.py # 26 memory + timing profiling tests
└── cuda/
├── low_rank_ops.cu # Fused CUDA kernels: tiled GEMM, Kronecker precond, int8 EMA
├── __init__.py # fused_kronecker_precond(), int8_ema_update(), truncated_eigh()
├── triton_ops.py # Triton JIT kernels for GPU acceleration without nvcc
├── __init__.py # fused_kronecker_precond(), int8_ema_update()
└── setup.py # nvcc build (sm_70/75/80/86/89/90)

scao_benchmarks_t4/ # Unified T4/Colab benchmark suite
Expand Down
4 changes: 2 additions & 2 deletions benchmark/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"""

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer


def main():
Expand Down Expand Up @@ -60,4 +60,4 @@ def main():


if __name__ == "__main__":
main()
main()
25 changes: 17 additions & 8 deletions benchmark/scao_vs_adamw_bench/benchmark_4b.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, BitsAndBytesConfig, DataCollatorForLanguageModeling
from datasets import load_dataset
from peft import get_peft_model, LoraConfig, prepare_model_for_kbit_training
from scao import SCAO # Your 2nd-order optimizer implementation
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
DataCollatorForLanguageModeling,
Trainer,
TrainingArguments,
)

from scao import SCAO # Your 2nd-order optimizer implementation


def main():
print("🚀 Starting 4B-Scale Benchmark for SCAO...")

# Using Qwen 2.5 3B model (optimal for the 4B category tests)
model_id = "Qwen/Qwen2.5-3B"

tokenizer = AutoTokenizer.from_pretrained(model_id)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
Expand All @@ -30,7 +39,7 @@ def main():

print("🧠 Initializing LoRA adapters...")
model = prepare_model_for_kbit_training(model)

lora_config = LoraConfig(
r=16,
lora_alpha=32,
Expand All @@ -48,10 +57,10 @@ def main():
print("📚 Loading dataset...")
# Using wikitext-2 for consistency across benchmarks
dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train[:5%]")

def tokenize(example):
return tokenizer(example["text"], padding="max_length", truncation=True, max_length=256)

tokenized_dataset = dataset.map(tokenize, batched=True)

print("⚙️ Injecting SCAO Optimizer...")
Expand Down
33 changes: 18 additions & 15 deletions benchmark/scao_vs_shampoo_bench/scao_vs_shampoo_pro.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import torch
import argparse
import time
import json
import os
import time

import torch
import torch_optimizer as optim
from datasets import load_dataset
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
BitsAndBytesConfig,
DataCollatorForLanguageModeling
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
DataCollatorForLanguageModeling,
Trainer,
TrainingArguments,
)
from datasets import load_dataset
from peft import get_peft_model, LoraConfig, prepare_model_for_kbit_training

from scao import SCAO
import torch_optimizer as optim


def get_peak_vram():
if torch.cuda.is_available():
Expand All @@ -29,7 +32,7 @@ def main():
args_cmd = parser.parse_args()

print(f"🚀 Starting Professional Benchmark: {args_cmd.optimizer.upper()} on {args_cmd.model_id}")

# 1. Model Loading (4-bit)
tokenizer = AutoTokenizer.from_pretrained(args_cmd.model_id)
if tokenizer.pad_token is None:
Expand Down Expand Up @@ -60,7 +63,7 @@ def main():
)
model = get_peft_model(model, lora_config)
trainable_params = [p for p in model.parameters() if p.requires_grad]

# 3. Dataset
dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train[:1%]")
def tokenize(example):
Expand Down Expand Up @@ -100,7 +103,7 @@ def tokenize(example):
start_time = time.time()
status = "Success"
error_msg = ""

try:
print("⚡ Training active! Monitoring performance...")
trainer.train()
Expand Down Expand Up @@ -129,7 +132,7 @@ def tokenize(example):
# Append to markdown summary
summary_path = "benchmark_results.md"
header_exists = os.path.exists(summary_path) and os.stat(summary_path).st_size > 0

with open(summary_path, "a") as f:
if not header_exists:
f.write("# SCAO vs Shampoo Comparison Results\n\n")
Expand Down
10 changes: 5 additions & 5 deletions benchmark/train_1m.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
python train_1m.py
"""

import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling,
Trainer,
TrainingArguments,
)
from datasets import load_dataset

from scao import SCAO


Expand Down Expand Up @@ -83,4 +83,4 @@ def tokenize(example):


if __name__ == "__main__":
main()
main()
12 changes: 6 additions & 6 deletions benchmark/train_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@
python train_local.py
"""

import torch
from datasets import load_dataset
from peft import LoraConfig, TaskType, get_peft_model
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling,
Trainer,
TrainingArguments,
)
from datasets import load_dataset
from peft import LoraConfig, get_peft_model, TaskType

from scao import SCAO


Expand Down Expand Up @@ -98,4 +98,4 @@ def tokenize(example):


if __name__ == "__main__":
main()
main()
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ exclude = [
[tool.ruff]
line-length = 100
target-version = "py310"
extend-exclude = ["*.ipynb", "dist", "build", "scao.egg-info"]

[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP", "B", "C4", "T20"]
Expand All @@ -116,5 +117,7 @@ ignore = [

[tool.ruff.lint.per-file-ignores]
"scao/tests/*" = ["T201", "F841"]
"scao/benchmarks/*" = ["T201"]
"scao/benchmarks/*" = ["T201", "E402"]
"benchmark/*" = ["T201", "E402", "F401", "I001"]
"scripts/*" = ["T201", "E402", "E701", "E702", "B007", "F401", "I001"]

10 changes: 10 additions & 0 deletions scao/cuda/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,14 @@ def int8_ema_update(
return ext.int8_ema_update(ema_q, ema_scale, new_val, rho)
except (AttributeError, RuntimeError):
pass

if ema_q.is_cuda:
try:
from .triton_ops import int8_ema_update_triton
res = int8_ema_update_triton(ema_q, ema_scale, new_val, rho)
if res is not None:
return res
except Exception:
pass

return _int8_ema_update_pytorch(ema_q, ema_scale, new_val, rho)
Loading
Loading