Files
unslothai--unsloth/unsloth/optimizers/q_galore_projector.py
T
wehub-resource-sync e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:59:56 +08:00

369 lines
13 KiB
Python

# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Adapted from Q-GaLore (https://github.com/VITA-Group/Q-GaLore)
# Original paper: "Q-GaLore: Quantized GaLore with INT4 Projection and
# Layer-Adaptive Low-Rank Gradients" (arXiv:2407.08296)
from collections import deque
import torch
__all__ = ["GaLoreProjector"]
class GaLoreProjector:
"""Low-rank gradient projector with optional INT4/INT8 quantized projection
matrices and layer-adaptive subspace update scheduling.
SVD of the gradient gives an orthogonal basis for the top-``rank`` subspace.
Gradients are projected in for the optimizer step, then back to full rank for
the weight update. Two Q-GaLore innovations:
1. Quantized projection matrices: with ``quant=True`` the orthogonal matrix
is stored in INT4/INT8 to cut projector-state memory.
2. Layer-adaptive update scheduling: when the rolling-average cosine
similarity of consecutive orthogonal vectors exceeds ``cos_threshold``,
``update_proj_gap`` is multiplied by ``gamma_proj`` to recompute SVD less
often for stabilized layers.
Args:
rank: Target rank for the low-rank projection.
update_proj_gap: Number of steps between SVD recomputations.
scale: Scaling factor applied when projecting back to full rank.
proj_type: Projection type. Only ``'std'`` is supported.
quant: Whether to quantize the projection matrix.
group_size: Group size for projection matrix quantization.
n_bit: Bit-width for projection matrix quantization (4 or 8).
cos_threshold: Cosine similarity threshold for adaptive scheduling.
gamma_proj: Multiplier for ``update_proj_gap`` on stability detection.
queue_size: Number of recent cosine similarities to average.
"""
__slots__ = (
"rank",
"update_proj_gap",
"scale",
"proj_type",
"quant",
"quant_group_size",
"quant_n_bit",
"cos_threshold",
"gamma_proj",
"queue_size",
"ortho_matrix",
"ortho_matrix_scales",
"ortho_matrix_zeros",
"ortho_matrix_shape",
"past_ortho_vector",
"queue",
"svd_count",
"_ortho_float_cache",
)
def __init__(
self,
rank: int,
update_proj_gap: int = 200,
scale: float = 1.0,
proj_type: str = "std",
quant: bool = False,
group_size: int = -1,
n_bit: int = 4,
cos_threshold: float = 0.4,
gamma_proj: float = 2.0,
queue_size: int = 5,
):
self.rank = rank
self.update_proj_gap = update_proj_gap
self.scale = scale
self.proj_type = proj_type
# Quantization settings for the projection matrix
self.quant = quant
self.quant_group_size = group_size
self.quant_n_bit = n_bit
# Adaptive update scheduling state
self.cos_threshold = cos_threshold
self.gamma_proj = gamma_proj
self.queue_size = queue_size
self.past_ortho_vector = None
self.queue = deque(maxlen = queue_size)
self.svd_count = 0
self._ortho_float_cache = None
# Projection matrix state
self.ortho_matrix = None
self.ortho_matrix_scales = None
self.ortho_matrix_zeros = None
self.ortho_matrix_shape = None
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def project(self, full_rank_grad: torch.Tensor, step: int) -> torch.Tensor:
"""Project a full-rank gradient into the low-rank subspace.
The SVD is recomputed every ``update_proj_gap`` steps (subject to
adaptive scheduling). Between recomputations the cached orthogonal
matrix is reused.
Args:
full_rank_grad: The full-rank gradient tensor (2-D).
step: The current optimizer step (0-indexed).
Returns:
The low-rank gradient tensor.
"""
assert self.proj_type == "std", "Only proj_type='std' is supported."
if full_rank_grad.shape[0] >= full_rank_grad.shape[1]:
# "tall" matrix → right projection (grad @ Q^T)
if self.ortho_matrix is None or step % self.update_proj_gap == 0:
float_ortho = self._compute_orthogonal(
full_rank_grad,
self.rank,
side = "right",
)
self._update_adaptive_schedule(float_ortho, side = "right")
self._store_ortho(float_ortho)
self._ortho_float_cache = self._load_ortho()
low_rank_grad = torch.matmul(full_rank_grad, self._ortho_float_cache.t())
else:
# "wide" matrix → left projection (Q^T @ grad)
if self.ortho_matrix is None or step % self.update_proj_gap == 0:
float_ortho = self._compute_orthogonal(
full_rank_grad,
self.rank,
side = "left",
)
self._update_adaptive_schedule(float_ortho, side = "left")
self._store_ortho(float_ortho)
self._ortho_float_cache = self._load_ortho()
low_rank_grad = torch.matmul(self._ortho_float_cache.t(), full_rank_grad)
return low_rank_grad
def project_back(self, low_rank_grad: torch.Tensor) -> torch.Tensor:
"""Project a low-rank update back to full rank.
Args:
low_rank_grad: The low-rank gradient/update tensor.
Returns:
The full-rank update scaled by ``self.scale``.
"""
float_ortho = self._ortho_float_cache
self._ortho_float_cache = None
if float_ortho is None:
float_ortho = self._load_ortho()
if low_rank_grad.shape[0] >= low_rank_grad.shape[1]:
full_rank_grad = torch.matmul(low_rank_grad, float_ortho)
else:
full_rank_grad = torch.matmul(float_ortho, low_rank_grad)
return full_rank_grad * self.scale
# ------------------------------------------------------------------
# SVD
# ------------------------------------------------------------------
@staticmethod
def _compute_orthogonal(weights: torch.Tensor, rank: int, side: str) -> torch.Tensor:
"""Compute the top-``rank`` orthogonal matrix via truncated SVD.
Args:
weights: 2-D tensor (typically the gradient).
rank: Number of singular vectors to keep.
side: ``'left'`` returns U[:, :rank], ``'right'`` returns Vh[:rank, :].
Returns:
Orthogonal matrix of shape ``(rank, N)`` (right) or ``(M, rank)`` (left).
"""
original_dtype = weights.dtype
original_device = weights.device
matrix = weights.float() if original_dtype != torch.float32 else weights
if side not in ("right", "left"):
raise ValueError(f"side must be 'left' or 'right', got '{side}'")
m, n = matrix.shape
if min(m, n) <= rank * 2:
U, s, Vh = torch.linalg.svd(matrix, full_matrices = False)
result = Vh[:rank, :] if side == "right" else U[:, :rank]
else:
# Oversampling p=10 per Halko et al. 2009 (arXiv:0909.4061)
# recommendation of p=5..10 for large low-rank matrices.
q = min(rank + 10, min(m, n))
U, s, V = torch.svd_lowrank(matrix, q = q, niter = 2)
result = V[:, :rank].t() if side == "right" else U[:, :rank]
if original_dtype != torch.float32:
result = result.to(device = original_device, dtype = original_dtype)
return result
# ------------------------------------------------------------------
# Adaptive scheduling
# ------------------------------------------------------------------
def _update_adaptive_schedule(self, float_ortho: torch.Tensor, side: str) -> None:
"""Track subspace stability and increase ``update_proj_gap`` if stable."""
self.svd_count += 1
if side == "right":
current_vector = float_ortho[:1, :].flatten()
else:
current_vector = float_ortho[:, :1].flatten()
if self.past_ortho_vector is not None:
cos_sim = torch.dot(self.past_ortho_vector, current_vector).item()
self.queue.append(cos_sim)
if (
len(self.queue) == self.queue.maxlen
and sum(self.queue) / len(self.queue) >= self.cos_threshold
):
self.update_proj_gap = int(self.update_proj_gap * self.gamma_proj)
self.past_ortho_vector = current_vector.clone()
# ------------------------------------------------------------------
# Quantized projection matrix storage
# ------------------------------------------------------------------
def _store_ortho(self, float_ortho: torch.Tensor) -> None:
"""Store the orthogonal matrix, optionally quantized."""
if self.quant:
q, scales, zeros, shape = _quantize(
float_ortho,
q_group_size = self.quant_group_size,
n_bit = self.quant_n_bit,
)
self.ortho_matrix = q
self.ortho_matrix_scales = scales
self.ortho_matrix_zeros = zeros
self.ortho_matrix_shape = shape
else:
self.ortho_matrix = float_ortho
def _load_ortho(self) -> torch.Tensor:
"""Load the orthogonal matrix, dequantizing if necessary."""
if self.quant:
return _dequantize(
self.ortho_matrix,
self.ortho_matrix_scales,
self.ortho_matrix_zeros,
self.ortho_matrix_shape,
)
return self.ortho_matrix
# ======================================================================
# Quantization utilities (shared with the optimizer)
# ======================================================================
@torch.no_grad()
def _quantize(
w: torch.Tensor,
q_group_size: int = -1,
n_bit: int = 8,
) -> tuple:
"""Asymmetric min-max quantization to unsigned int.
Returns:
``(quantized_uint8, scales, zeros, original_shape)``
"""
org_shape = w.shape
if q_group_size > 0:
assert (
w.nelement() % q_group_size == 0
), f"Tensor size {w.nelement()} not divisible by group_size {q_group_size}"
w = w.reshape(-1, q_group_size)
assert w.dim() == 2
max_val = w.amax(dim = 1, keepdim = True)
min_val = w.amin(dim = 1, keepdim = True)
max_int = 2**n_bit - 1
min_int = 0
scales = (max_val - min_val).clamp(min = 1e-5) / max_int
zeros = (-torch.round(min_val / scales)).clamp_(min_int, max_int)
w = torch.clamp(torch.round(w / scales) + zeros, min_int, max_int)
w = w.reshape(org_shape).to(torch.uint8)
return w, scales, zeros, org_shape
@torch.no_grad()
def _dequantize(
w: torch.Tensor, scales: torch.Tensor, zeros: torch.Tensor, original_shape: tuple
) -> torch.Tensor:
"""Dequantize from uint8 back to float."""
# Infer group size: scales has shape (n_groups, 1), so n_groups = scales.shape[0]
total = w.numel()
n_groups = scales.shape[0] if scales.dim() > 1 else scales.numel()
group_size = total // n_groups if n_groups > 0 else total
float_w = w.to(scales.dtype).reshape(-1, group_size)
float_w = (float_w - zeros) * scales
return float_w.reshape(original_shape)
@torch.no_grad()
def _quantize_stochastic(
w: torch.Tensor,
q_group_size: int = -1,
n_bit: int = 8,
) -> tuple:
"""Asymmetric min-max quantization with stochastic rounding.
Instead of deterministic ``round()``, the rounding direction is chosen
probabilistically proportional to the fractional part. This gives an
unbiased estimator of the original value in expectation.
Returns:
``(quantized_uint8, scales, zeros, original_shape)``
"""
org_shape = w.shape
if q_group_size > 0:
assert w.nelement() % q_group_size == 0
w = w.reshape(-1, q_group_size)
assert w.dim() == 2
max_val = w.amax(dim = 1, keepdim = True)
min_val = w.amin(dim = 1, keepdim = True)
max_int = 2**n_bit - 1
min_int = 0
scales = (max_val - min_val).clamp(min = 1e-5) / max_int
zeros = (-torch.round(min_val / scales)).clamp_(min_int, max_int)
w_scaled = w / scales
up = torch.ceil(w_scaled)
down = torch.floor(w_scaled)
prob = w_scaled - down
rng = torch.rand_like(prob)
w = torch.where(rng < prob, up, down)
w = torch.clamp(w + zeros, min_int, max_int)
w = w.reshape(org_shape).to(torch.uint8)
return w, scales, zeros, org_shape