Feat/spectrum (#31491)
Co-authored-by: jaron1990 <jaron1990@gmail.com> Co-authored-by: jaron1990 <34618972+jaron1990@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexnails <alex.nails@radixark.ai>
This commit is contained in:
co-authored by
jaron1990
jaron1990
Copilot Autofix powered by AI
copilot-swe-agent[bot]
alexnails
parent
5c4f72f92a
commit
ea65f8ddc9
@@ -35,3 +35,4 @@ class StableDiffusion3TransformerConfig(DiTConfig):
|
||||
arch_config: StableDiffusion3TransformerArchConfig = field(
|
||||
default_factory=StableDiffusion3TransformerArchConfig
|
||||
)
|
||||
prefix: str = "SD3"
|
||||
|
||||
@@ -207,6 +207,10 @@ class SamplingParams:
|
||||
None # TeaCacheParams or WanTeaCacheParams, set by model-specific subclass
|
||||
)
|
||||
|
||||
# Spectrum parameters
|
||||
enable_spectrum: bool = False
|
||||
spectrum_params: Any = None # SpectrumParams
|
||||
|
||||
# Profiling
|
||||
profile: bool = field(default=False, metadata={"batch_sig_exclude": True})
|
||||
num_profiled_timesteps: int = field(default=5, metadata={"batch_sig_exclude": True})
|
||||
@@ -330,6 +334,16 @@ class SamplingParams:
|
||||
if env_steps is not None and self.num_inference_steps is not None:
|
||||
self.num_inference_steps = int(env_steps)
|
||||
|
||||
if self.enable_spectrum and isinstance(self.spectrum_params, dict):
|
||||
from sglang.multimodal_gen.configs.sample.spectrum import SpectrumParams
|
||||
|
||||
self.spectrum_params = SpectrumParams(**self.spectrum_params)
|
||||
|
||||
if self.enable_spectrum and self.spectrum_params is None:
|
||||
from sglang.multimodal_gen.configs.sample.spectrum import SpectrumParams
|
||||
|
||||
self.spectrum_params = SpectrumParams()
|
||||
|
||||
def build_request_extra(self) -> dict[str, Any]:
|
||||
"""Return optional request-scoped extras for downstream pipeline stages."""
|
||||
extra = {}
|
||||
@@ -547,6 +561,11 @@ class SamplingParams:
|
||||
f"boundary_ratio must be within [0, 1], got {self.boundary_ratio!r}"
|
||||
)
|
||||
|
||||
if self.enable_teacache and self.enable_spectrum:
|
||||
raise ValueError(
|
||||
"enable_teacache and enable_spectrum are mutually exclusive; enable only one."
|
||||
)
|
||||
|
||||
RLRolloutArgs.validate_sampling_params(self)
|
||||
|
||||
def check_sampling_param(self):
|
||||
@@ -869,6 +888,69 @@ class SamplingParams:
|
||||
"--enable-teacache",
|
||||
action="store_true",
|
||||
)
|
||||
add_argument(
|
||||
"--enable-spectrum",
|
||||
action="store_true",
|
||||
)
|
||||
add_argument("--w", type=float)
|
||||
add_argument(
|
||||
"--taylor-order",
|
||||
"--taylor_order",
|
||||
dest="taylor_order",
|
||||
type=int,
|
||||
)
|
||||
add_argument(
|
||||
"--history-size",
|
||||
"--history_size",
|
||||
dest="history_size",
|
||||
type=int,
|
||||
)
|
||||
add_argument(
|
||||
"--spectrum-window-size",
|
||||
"--spectrum_window_size",
|
||||
"--window-size",
|
||||
"--window_size",
|
||||
dest="spectrum_window_size",
|
||||
type=float,
|
||||
help="Spectrum initial skip window size.",
|
||||
)
|
||||
add_argument(
|
||||
"--spectrum-flex-window",
|
||||
"--spectrum_flex_window",
|
||||
"--flex-window",
|
||||
"--flex_window",
|
||||
dest="spectrum_flex_window",
|
||||
type=float,
|
||||
help="Spectrum adaptive window growth slope.",
|
||||
)
|
||||
add_argument(
|
||||
"--spectrum-warmup-steps",
|
||||
"--spectrum_warmup_steps",
|
||||
dest="spectrum_warmup_steps",
|
||||
type=int,
|
||||
help="Spectrum warmup denoising steps before caching.",
|
||||
)
|
||||
add_argument(
|
||||
"--spectrum-m",
|
||||
"--spectrum_m",
|
||||
dest="spectrum_m",
|
||||
type=int,
|
||||
help="Spectrum Chebyshev polynomial degree (M).",
|
||||
)
|
||||
add_argument(
|
||||
"--spectrum-lam",
|
||||
"--spectrum_lam",
|
||||
dest="spectrum_lam",
|
||||
type=float,
|
||||
help="Spectrum ridge regularization strength.",
|
||||
)
|
||||
add_argument(
|
||||
"--spectrum-tau-num-steps",
|
||||
"--spectrum_tau_num_steps",
|
||||
dest="spectrum_tau_num_steps",
|
||||
type=int,
|
||||
help="Spectrum tau normalization horizon.",
|
||||
)
|
||||
|
||||
# profiling
|
||||
add_argument(
|
||||
@@ -1299,6 +1381,34 @@ class SamplingParams:
|
||||
}
|
||||
if isinstance(cli_args.get("seed"), list) and len(cli_args["seed"]) == 1:
|
||||
cli_args["seed"] = cli_args["seed"][0]
|
||||
|
||||
spectrum_overrides = {}
|
||||
spectrum_flag_map = {
|
||||
"w": "w",
|
||||
"taylor_order": "taylor_order",
|
||||
"window_size": "spectrum_window_size",
|
||||
"flex_window": "spectrum_flex_window",
|
||||
"history_size": "history_size",
|
||||
"warmup_steps": "spectrum_warmup_steps",
|
||||
"m": "spectrum_m",
|
||||
"lam": "spectrum_lam",
|
||||
"tau_num_steps": "spectrum_tau_num_steps",
|
||||
}
|
||||
for field_name, arg_name in spectrum_flag_map.items():
|
||||
if hasattr(args, arg_name) and getattr(args, arg_name) is not None:
|
||||
spectrum_overrides[field_name] = getattr(args, arg_name)
|
||||
if spectrum_overrides:
|
||||
if not cli_args.get("enable_spectrum", False):
|
||||
logger.info(
|
||||
"Spectrum override flags were provided without --enable-spectrum; "
|
||||
"auto-enabling Spectrum caching."
|
||||
)
|
||||
cli_args["enable_spectrum"] = True
|
||||
existing_spectrum_params = cli_args.get("spectrum_params")
|
||||
if isinstance(existing_spectrum_params, dict):
|
||||
spectrum_overrides = {**existing_spectrum_params, **spectrum_overrides}
|
||||
cli_args["spectrum_params"] = spectrum_overrides
|
||||
|
||||
return cli_args
|
||||
|
||||
def output_file_path(self):
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sglang.multimodal_gen.configs.sample.sampling_params import CacheParams
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpectrumParams(CacheParams):
|
||||
"""
|
||||
Parameters for [Spectrum](https://arxiv.org/abs/2603.01623) spectral feature
|
||||
forecasting.
|
||||
|
||||
Attributes:
|
||||
cache_type: (`str`, defaults to `spectrum`):
|
||||
A string labeling these parameters as belonging to spectrum.
|
||||
window_size (`float`, defaults to `2.0`):
|
||||
Initial skip window size (paper: N). After warmup, a real forward is
|
||||
run every `floor(window_size)` consecutive cached steps; when a real
|
||||
forward runs, the window grows by `flex_window`. Recommended: `2.0`.
|
||||
flex_window (`float`, defaults to `0.75`):
|
||||
Adaptive window slope (paper: alpha). Controls how quickly skip intervals
|
||||
widen as sampling progresses. Recommended: `0.75` for ~3.5x speedup,
|
||||
`3.0` for ~5x speedup (fewer network evaluations, lower quality).
|
||||
warmup_steps (`int`, defaults to `5`):
|
||||
Number of initial forward passes that always run the full DiT before
|
||||
Spectrum caching begins. Early steps define global structure and are
|
||||
kept exact in the paper setup.
|
||||
w (`float`, defaults to `1.0`):
|
||||
Blend weight between a local Taylor predictor and the Chebyshev
|
||||
spectral predictor (``(1 - w) * h_taylor + w * h_cheb``). This
|
||||
mix is not in the paper; ``w=1.0`` is pure Chebyshev and matches
|
||||
the [reference repo](https://github.com/hanjq17/Spectrum#-text-to-image-t2i)
|
||||
default. They recommend ``0.5``–``1.0`` for extra robustness.
|
||||
lam (`float`, defaults to `0.1`):
|
||||
Ridge regularization strength (lambda) when fitting Chebyshev
|
||||
coefficients over recent cached features.
|
||||
m (`int`, defaults to `4`):
|
||||
Number of Chebyshev bases (polynomial degree M). The design matrix has
|
||||
`M + 1` columns.
|
||||
history_size (`int`, defaults to `100`):
|
||||
Maximum number of recent (step, feature) pairs kept for coefficient
|
||||
fitting (paper reference code uses a sliding window of up to 100).
|
||||
tau_num_steps (`int`, defaults to `50`):
|
||||
Denominator for mapping denoising step indices to Chebyshev time
|
||||
``tau in [-1, 1]``. The reference repo hardcodes 50 regardless of
|
||||
``num_inference_steps``; keep this at 50 unless you know you need
|
||||
another horizon.
|
||||
taylor_order (`int`, defaults to `1`):
|
||||
Order of the local discrete Taylor fallback used in the blend when
|
||||
`w < 1.0`. Supported values: `1`, `2`, or `3`.
|
||||
separate_cfg_branches (not a field — determined by the DiT model):
|
||||
Wan, Hunyuan, and SD3 maintain independent Spectrum state per CFG
|
||||
branch. Other ``CachableDiT`` models use one counter for all
|
||||
forwards; see ``get_total_forward_steps()``.
|
||||
"""
|
||||
|
||||
cache_type: str = "spectrum"
|
||||
window_size: float = 2.0
|
||||
flex_window: float = 0.75
|
||||
warmup_steps: int = 5
|
||||
w: float = 1.0
|
||||
lam: float = 0.1
|
||||
m: int = 4
|
||||
history_size: int = 100
|
||||
tau_num_steps: int = 50
|
||||
taylor_order: int = 1
|
||||
|
||||
def get_total_forward_steps(
|
||||
self, num_inference_steps: int, do_cfg: bool, separate_cfg_branches: bool
|
||||
) -> int:
|
||||
"""How many DiT forward calls one Spectrum counter sees per generation.
|
||||
|
||||
Used by (1) ``ChebyshevForecaster`` to map step indices onto [-1, 1] and
|
||||
by (2) ``begin_spectrum_step`` to wrap branch counters at end-of-run.
|
||||
Not used for logging.
|
||||
|
||||
- **Separate counters** (Wan, Hunyuan, SD3): each CFG branch has its own
|
||||
``spectrum_cnt`` / forecaster. Every denoising step triggers one cond
|
||||
forward and one uncond forward, but each counter only advances on its
|
||||
branch → ``num_inference_steps`` calls per counter.
|
||||
|
||||
- **Single counter** (FLUX, …): one counter interleaves cond and
|
||||
uncond forwards when CFG is enabled → ``2 * num_inference_steps`` calls.
|
||||
FLUX.1-dev uses embedded guidance (no true CFG), so this path is
|
||||
normally single-branch in practice.
|
||||
|
||||
When CFG is off, every model performs one forward per denoising step.
|
||||
"""
|
||||
if do_cfg and not separate_cfg_branches:
|
||||
return num_inference_steps * 2
|
||||
return num_inference_steps
|
||||
@@ -6,6 +6,7 @@ This module provides various caching strategies to accelerate
|
||||
diffusion transformer (DiT) inference:
|
||||
|
||||
- TeaCache: Temporal similarity-based caching for diffusion models
|
||||
- Spectrum: Chebyshev spectral feature forecasting for step skipping
|
||||
- cache-dit integration: Block-level caching with DBCache and TaylorSeer
|
||||
|
||||
"""
|
||||
@@ -16,12 +17,15 @@ from sglang.multimodal_gen.runtime.cache.cache_dit_integration import (
|
||||
enable_cache_on_transformer,
|
||||
get_scm_mask,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.cache.spectrum import SpectrumMixin
|
||||
from sglang.multimodal_gen.runtime.cache.teacache import TeaCacheContext, TeaCacheMixin
|
||||
|
||||
__all__ = [
|
||||
# TeaCache (always available)
|
||||
"TeaCacheContext",
|
||||
"TeaCacheMixin",
|
||||
# Spectrum (always available)
|
||||
"SpectrumMixin",
|
||||
# cache-dit integration (lazy-loaded, requires cache-dit package)
|
||||
"CacheDitConfig",
|
||||
"enable_cache_on_transformer",
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Spectrum: Adaptive Spectral Feature Forecasting for diffusion sampling acceleration.
|
||||
|
||||
Training-free step skipping with Chebyshev polynomial ridge regression over
|
||||
denoiser block outputs. See https://arxiv.org/abs/2603.01623
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.multimodal_gen.configs.sample.spectrum import SpectrumParams
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _flatten(x: torch.Tensor) -> tuple[torch.Tensor, torch.Size]:
|
||||
"""Reshape tensor to (1, -1) for ridge regression, preserving original shape."""
|
||||
return x.reshape(1, -1), x.shape
|
||||
|
||||
|
||||
def _unflatten(x_flat: torch.Tensor, shape: torch.Size) -> torch.Tensor:
|
||||
"""Restore flattened tensor to original shape."""
|
||||
return x_flat.reshape(shape)
|
||||
|
||||
|
||||
def _is_missing_linalg_backend_error(err: RuntimeError) -> bool:
|
||||
"""Detect the "no LAPACK / no MAGMA" RuntimeError PyTorch raises when the
|
||||
build lacks a linear algebra backend for the tensor's device.
|
||||
|
||||
Some PyTorch builds (notably several ROCm wheels) ship without LAPACK for
|
||||
CPU tensors *and* without MAGMA for CUDA/HIP tensors, making
|
||||
``torch.linalg.cholesky``/``torch.cholesky_solve`` unusable on any device.
|
||||
"""
|
||||
msg = str(err)
|
||||
return "LAPACK" in msg or "MAGMA" in msg
|
||||
|
||||
|
||||
def _cholesky_lower(a: torch.Tensor) -> torch.Tensor:
|
||||
"""Cholesky factorization ``A = L @ L.T`` without ``torch.linalg``.
|
||||
|
||||
Used as a fallback when the backend has no LAPACK/MAGMA support. ``a`` is
|
||||
expected to be tiny (Chebyshev design matrix size), so the Python-level
|
||||
loop is cheap.
|
||||
"""
|
||||
n = a.shape[0]
|
||||
L = torch.zeros_like(a)
|
||||
for i in range(n):
|
||||
for j in range(i + 1):
|
||||
s = L[i, :j] @ L[j, :j]
|
||||
if i == j:
|
||||
L[i, j] = torch.sqrt(a[i, i] - s)
|
||||
else:
|
||||
L[i, j] = (a[i, j] - s) / L[j, j]
|
||||
return L
|
||||
|
||||
|
||||
def _cholesky_solve_lower(l: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
||||
"""Solve ``(L @ L.T) @ x = b`` given lower-triangular Cholesky factor ``l``."""
|
||||
n = l.shape[0]
|
||||
y = torch.zeros_like(b)
|
||||
for i in range(n):
|
||||
y[i] = (b[i] - l[i, :i] @ y[:i]) / l[i, i]
|
||||
x = torch.zeros_like(b)
|
||||
for i in reversed(range(n)):
|
||||
x[i] = (y[i] - l[i + 1 :, i] @ x[i + 1 :]) / l[i, i]
|
||||
return x
|
||||
|
||||
|
||||
def _ridge_cholesky_solve(xtx: torch.Tensor, xth: torch.Tensor) -> torch.Tensor:
|
||||
"""Solve the ridge normal equations ``xtx @ coef = xth`` via Cholesky.
|
||||
|
||||
Prefers ``torch.linalg.cholesky``/``torch.cholesky_solve``, retrying once
|
||||
with jitter if ``xtx`` is only marginally non-positive-definite. Falls
|
||||
back to a manual, LAPACK/MAGMA-free Cholesky solve when the backend has
|
||||
no linear algebra support at all for this device (see
|
||||
`_is_missing_linalg_backend_error`).
|
||||
"""
|
||||
p = xtx.shape[0]
|
||||
eye = torch.eye(p, device=xtx.device, dtype=xtx.dtype)
|
||||
try:
|
||||
chol = torch.linalg.cholesky(xtx)
|
||||
return torch.cholesky_solve(xth, chol)
|
||||
except RuntimeError as e:
|
||||
if _is_missing_linalg_backend_error(e):
|
||||
chol = _cholesky_lower(xtx)
|
||||
if not torch.isfinite(chol).all():
|
||||
jitter = 1e-6 * xtx.diag().mean()
|
||||
chol = _cholesky_lower(xtx + jitter * eye)
|
||||
return _cholesky_solve_lower(chol, xth)
|
||||
jitter = 1e-6 * xtx.diag().mean()
|
||||
chol = torch.linalg.cholesky(xtx + jitter * eye)
|
||||
return torch.cholesky_solve(xth, chol)
|
||||
|
||||
|
||||
class ChebyshevForecaster(nn.Module):
|
||||
"""Chebyshev-basis ridge regression forecaster over diffusion step index.
|
||||
|
||||
Keeps a sliding window of (step, flattened_feature) pairs from recent *real*
|
||||
DiT forwards. On predict(), fits ridge regression coefficients for Chebyshev
|
||||
bases T_0..T_M on normalized time, then evaluates at the requested step.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
M: int = 4,
|
||||
K: int = 100,
|
||||
lam: float = 0.1,
|
||||
num_steps: int = 50,
|
||||
device: Optional[torch.device] = None,
|
||||
feature_shape: Optional[torch.Size] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.M = M
|
||||
self.K = K
|
||||
self.lam = lam
|
||||
self.num_steps = num_steps
|
||||
# Preallocated (K,) / (K, N) ring buffer, written in place by update().
|
||||
self.register_buffer("t_buf", torch.empty(0))
|
||||
self._H_buf: Optional[torch.Tensor] = None
|
||||
self._write_idx = 0
|
||||
self._count = 0
|
||||
self._shape: Optional[torch.Size] = feature_shape
|
||||
self._feature_dtype: Optional[torch.dtype] = None
|
||||
self._coef: Optional[torch.Tensor] = None
|
||||
self.device_ref = device
|
||||
self._tau_scale = 2.0 / float(self.num_steps) if self.num_steps != 0 else 0.0
|
||||
|
||||
@property
|
||||
def P(self) -> int:
|
||||
return self.M + 1
|
||||
|
||||
def _taus(self, t: torch.Tensor) -> torch.Tensor:
|
||||
"""Normalize timesteps to [-1, 1] range for Chebyshev basis."""
|
||||
if self.num_steps == 0:
|
||||
return torch.zeros_like(t)
|
||||
return t * self._tau_scale - 1.0
|
||||
|
||||
def _build_design(self, taus: torch.Tensor) -> torch.Tensor:
|
||||
"""Build Chebyshev basis design matrix [T_0(tau), T_1(tau), ..., T_M(tau)]."""
|
||||
taus = taus.reshape(-1, 1)
|
||||
k = taus.shape[0]
|
||||
t0 = torch.ones((k, 1), device=taus.device, dtype=taus.dtype)
|
||||
if self.M == 0:
|
||||
return t0
|
||||
t1 = taus
|
||||
cols = [t0, t1]
|
||||
for _ in range(2, self.M + 1):
|
||||
cols.append(2 * taus * cols[-1] - cols[-2])
|
||||
return torch.cat(cols[: self.M + 1], dim=1)
|
||||
|
||||
def update(self, t: float | torch.Tensor, h: torch.Tensor) -> None:
|
||||
device = self.device_ref or h.device
|
||||
t_tensor = torch.as_tensor(t, dtype=torch.float32, device=device)
|
||||
h_flat, shape = _flatten(h)
|
||||
if self._feature_dtype is None:
|
||||
self._feature_dtype = h_flat.dtype
|
||||
h_flat = h_flat.to(device).to(torch.float32)
|
||||
if self._shape is None:
|
||||
self._shape = shape
|
||||
else:
|
||||
assert shape == self._shape, "Spectrum feature shape must remain constant"
|
||||
|
||||
if self._H_buf is None:
|
||||
# Preallocate the ring buffer once the flattened feature size is known
|
||||
self.t_buf = torch.zeros(self.K, dtype=torch.float32, device=device)
|
||||
self._H_buf = torch.zeros(
|
||||
self.K, h_flat.shape[1], dtype=torch.float32, device=device
|
||||
)
|
||||
|
||||
self.t_buf[self._write_idx] = t_tensor
|
||||
self._H_buf[self._write_idx] = h_flat[0]
|
||||
self._write_idx = (self._write_idx + 1) % self.K
|
||||
self._count = min(self._count + 1, self.K)
|
||||
|
||||
# Invalidate cached ridge coefficients; will be refit on next predict()
|
||||
self._coef = None
|
||||
|
||||
def _recent(self, n: int) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Return the last `min(n, count)` valid (t, h) entries in chronological
|
||||
order (oldest first, most recent last).
|
||||
|
||||
Only used by the discrete Taylor blend, which needs true temporal
|
||||
order for its finite differences (unlike the ridge fit, see
|
||||
`_fit_if_needed`). `n` is small (<= taylor_order + 1), so even the
|
||||
rare wraparound `cat` below is cheap.
|
||||
"""
|
||||
count = min(self._count, n)
|
||||
start = (self._write_idx - count) % self.K
|
||||
if start + count <= self.K:
|
||||
# Contiguous window -- cheap view, no copy.
|
||||
return self.t_buf[start : start + count], self._H_buf[start : start + count]
|
||||
# Wraps past the end of the physical buffer -- must reorder.
|
||||
tail = self.K - start
|
||||
t = torch.cat([self.t_buf[start:], self.t_buf[: count - tail]])
|
||||
h = torch.cat([self._H_buf[start:], self._H_buf[: count - tail]])
|
||||
return t, h
|
||||
|
||||
def ready(self) -> bool:
|
||||
return self._count >= 1
|
||||
|
||||
def _fit_if_needed(self) -> None:
|
||||
"""Fit ridge regression coefficients on cached (t, h) pairs if not cached."""
|
||||
if self._coef is not None:
|
||||
return
|
||||
assert self.ready()
|
||||
assert self._H_buf is not None
|
||||
assert self._feature_dtype is not None
|
||||
feature_dtype = self._feature_dtype
|
||||
t, h = self.t_buf[: self._count], self._H_buf[: self._count]
|
||||
taus = self._taus(t)
|
||||
# Ridge solve in fp32; autocast would keep matmuls in bf16 and break
|
||||
# torch.cholesky_solve dtype requirements.
|
||||
with torch.autocast(device_type=self._H_buf.device.type, enabled=False):
|
||||
x = self._build_design(taus).to(torch.float32)
|
||||
p = x.shape[1]
|
||||
lam_i = self.lam * torch.eye(p, device=x.device, dtype=x.dtype)
|
||||
xt = x.transpose(0, 1)
|
||||
xtx = xt @ x + lam_i
|
||||
xth = xt @ h
|
||||
self._coef = _ridge_cholesky_solve(xtx, xth).to(feature_dtype)
|
||||
|
||||
@torch.no_grad()
|
||||
def predict(self, t_star: float | torch.Tensor) -> torch.Tensor:
|
||||
assert self._shape is not None
|
||||
device = self.t_buf.device
|
||||
t_star = torch.as_tensor(t_star, dtype=torch.float32, device=device)
|
||||
self._fit_if_needed()
|
||||
assert self._coef is not None
|
||||
tau_star = self._taus(t_star)
|
||||
x_star = self._build_design(tau_star[None]).to(self._coef.dtype)
|
||||
h_flat = x_star @ self._coef
|
||||
return _unflatten(h_flat, self._shape)
|
||||
|
||||
|
||||
class SpectrumForecaster(nn.Module):
|
||||
"""Chebyshev + discrete Taylor blend forecaster.
|
||||
|
||||
The paper uses pure Chebyshev; the reference repo blends with a local
|
||||
discrete Taylor predictor. ``w=1`` is Chebyshev-only; lower ``w`` adds Taylor.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cheb: ChebyshevForecaster,
|
||||
*,
|
||||
taylor_order: int = 1,
|
||||
w: float = 1.0,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.cheb = cheb
|
||||
self.taylor_order = taylor_order
|
||||
self.w = w
|
||||
|
||||
@torch.no_grad()
|
||||
def _local_taylor_discrete(self, t_star: torch.Tensor) -> torch.Tensor:
|
||||
"""Predict hidden state at t_star using discrete Taylor expansion from recent real steps."""
|
||||
assert self.cheb._H_buf is not None
|
||||
assert self.cheb._shape is not None
|
||||
assert self.cheb._feature_dtype is not None
|
||||
feature_dtype = self.cheb._feature_dtype
|
||||
t, h = self.cheb._recent(self.taylor_order + 1)
|
||||
h_i = h[-1]
|
||||
if t.numel() < 2:
|
||||
return _unflatten(h_i.reshape(1, -1), self.cheb._shape).to(feature_dtype)
|
||||
h_im1 = h[-2]
|
||||
t_i = t[-1]
|
||||
t_im1 = t[-2]
|
||||
dh1 = h_i - h_im1
|
||||
dt_last = (t_i - t_im1).clamp_min(1e-8)
|
||||
k = ((t_star - t_i) / dt_last).to(h_i.dtype)
|
||||
out = h_i + k * dh1
|
||||
if self.taylor_order >= 2 and t.numel() >= 3:
|
||||
h_im2 = h[-3]
|
||||
d2 = h_i - 2 * h_im1 + h_im2
|
||||
out = out + 0.5 * k * (k - 1.0) * d2
|
||||
if self.taylor_order >= 3 and t.numel() >= 4:
|
||||
h_im3 = h[-4]
|
||||
d3 = h_i - 3 * h_im1 + 3 * h_im2 - h_im3
|
||||
out = out + (k * (k - 1.0) * (k - 2.0) / 6.0) * d3
|
||||
return _unflatten(out.reshape(1, -1), self.cheb._shape).to(feature_dtype)
|
||||
|
||||
@torch.no_grad()
|
||||
def predict(self, t_star: float | torch.Tensor) -> torch.Tensor:
|
||||
"""Blend Chebyshev regression and local Taylor predictions."""
|
||||
device = self.cheb.t_buf.device
|
||||
t_star = torch.as_tensor(t_star, dtype=torch.float32, device=device)
|
||||
if self.w >= 1.0:
|
||||
return self.cheb.predict(t_star)
|
||||
elif self.w <= 0.0:
|
||||
return self._local_taylor_discrete(t_star)
|
||||
return torch.lerp(
|
||||
self._local_taylor_discrete(t_star), self.cheb.predict(t_star), self.w
|
||||
)
|
||||
|
||||
def update(self, t, h) -> None:
|
||||
self.cheb.update(t, h)
|
||||
|
||||
def ready(self) -> bool:
|
||||
return self.cheb.ready()
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpectrumContext:
|
||||
current_step: int
|
||||
num_inference_steps: int
|
||||
total_forward_steps: int
|
||||
do_cfg: bool
|
||||
is_cfg_negative: bool
|
||||
spectrum_params: SpectrumParams
|
||||
debug: bool
|
||||
|
||||
|
||||
class SpectrumMixin:
|
||||
"""Mixin providing Spectrum step-skipping and feature forecasting.
|
||||
|
||||
Wired into ``CachableDiT`` (see ``runtime/models/dits/base.py``). Concrete
|
||||
models call three hooks from ``forward()`` around their transformer blocks:
|
||||
|
||||
- ``begin_spectrum_step()`` — advance skip schedule; returns whether to run
|
||||
blocks or forecast instead.
|
||||
- ``spectrum_record_features()`` — after a real forward, store block outputs.
|
||||
- ``spectrum_predict_features()`` — on skipped steps, return forecasted outputs.
|
||||
|
||||
Models with separate CFG branches (Wan, Hunyuan, SD3) list their config prefix
|
||||
in ``_CFG_SUPPORTED_PREFIXES`` so cond/uncond maintain independent counters and
|
||||
forecasters. All other ``CachableDiT`` subclasses share one counter.
|
||||
"""
|
||||
|
||||
# DiT config prefixes that run separate cond/uncond forwards (see TeaCache).
|
||||
_CFG_SUPPORTED_PREFIXES: set[str] = {"wan", "hunyuan", "sd3"}
|
||||
|
||||
def _init_spectrum_state(self) -> None:
|
||||
"""Initialize Spectrum state variables. Dual-branch models (Wan, Hunyuan) track separate
|
||||
cond/uncond forecasters and counters; others share one.
|
||||
"""
|
||||
# Positive branch (cond) or single-branch state
|
||||
self.spectrum_cnt = 0
|
||||
self.spectrum_num_consecutive_cached_steps = 0
|
||||
self.spectrum_curr_ws: Optional[float] = None
|
||||
self.spectrum_forecaster: Optional[SpectrumForecaster] = None
|
||||
self.spectrum_real_steps = 0
|
||||
self.spectrum_skipped_steps = 0
|
||||
self.spectrum_shadow_rel_l2_sum = 0.0
|
||||
self.spectrum_shadow_rel_l2_count = 0
|
||||
|
||||
# Negative branch (uncond) state for dual-branch CFG models
|
||||
self.spectrum_cnt_negative = 0
|
||||
self.spectrum_num_consecutive_cached_steps_negative = 0
|
||||
self.spectrum_curr_ws_negative: Optional[float] = None
|
||||
self.spectrum_forecaster_negative: Optional[SpectrumForecaster] = None
|
||||
self.spectrum_real_steps_negative = 0
|
||||
self.spectrum_skipped_steps_negative = 0
|
||||
self.spectrum_shadow_rel_l2_sum_negative = 0.0
|
||||
self.spectrum_shadow_rel_l2_count_negative = 0
|
||||
|
||||
# Runtime branch tracking
|
||||
self.spectrum_is_cfg_negative = False
|
||||
self._spectrum_ctx: Optional[SpectrumContext] = None
|
||||
prefix = getattr(self.config, "prefix", "").lower()
|
||||
self._spectrum_supports_cfg_cache = prefix in self._CFG_SUPPORTED_PREFIXES
|
||||
|
||||
def reset_spectrum_state(self, spectrum_params: SpectrumParams) -> None:
|
||||
self.spectrum_cnt = 0
|
||||
self.spectrum_num_consecutive_cached_steps = 0
|
||||
self.spectrum_curr_ws = spectrum_params.window_size
|
||||
self.spectrum_forecaster = None
|
||||
self.spectrum_is_cfg_negative = False
|
||||
self.spectrum_real_steps = 0
|
||||
self.spectrum_skipped_steps = 0
|
||||
self.spectrum_shadow_rel_l2_sum = 0.0
|
||||
self.spectrum_shadow_rel_l2_count = 0
|
||||
if self._spectrum_supports_cfg_cache:
|
||||
self.spectrum_cnt_negative = 0
|
||||
self.spectrum_num_consecutive_cached_steps_negative = 0
|
||||
self.spectrum_curr_ws_negative = spectrum_params.window_size
|
||||
self.spectrum_forecaster_negative = None
|
||||
self.spectrum_real_steps_negative = 0
|
||||
self.spectrum_skipped_steps_negative = 0
|
||||
self.spectrum_shadow_rel_l2_sum_negative = 0.0
|
||||
self.spectrum_shadow_rel_l2_count_negative = 0
|
||||
|
||||
def _get_spectrum_branch_state(self) -> tuple[int, int, float]:
|
||||
"""Get schedule state for current branch (cond or uncond)."""
|
||||
if self.spectrum_is_cfg_negative and self._spectrum_supports_cfg_cache:
|
||||
return (
|
||||
self.spectrum_cnt_negative,
|
||||
self.spectrum_num_consecutive_cached_steps_negative,
|
||||
self.spectrum_curr_ws_negative or 0.0,
|
||||
)
|
||||
return (
|
||||
self.spectrum_cnt,
|
||||
self.spectrum_num_consecutive_cached_steps,
|
||||
self.spectrum_curr_ws or 0.0,
|
||||
)
|
||||
|
||||
def _set_spectrum_branch_state(
|
||||
self, cnt: int, consecutive: int, curr_ws: float
|
||||
) -> None:
|
||||
"""Set schedule state for current branch (cond or uncond)."""
|
||||
if self.spectrum_is_cfg_negative and self._spectrum_supports_cfg_cache:
|
||||
self.spectrum_cnt_negative = cnt
|
||||
self.spectrum_num_consecutive_cached_steps_negative = consecutive
|
||||
self.spectrum_curr_ws_negative = curr_ws
|
||||
else:
|
||||
self.spectrum_cnt = cnt
|
||||
self.spectrum_num_consecutive_cached_steps = consecutive
|
||||
self.spectrum_curr_ws = curr_ws
|
||||
|
||||
def _get_spectrum_forecaster(self) -> Optional[SpectrumForecaster]:
|
||||
"""Get forecaster for current branch (cond or uncond)."""
|
||||
if self.spectrum_is_cfg_negative and self._spectrum_supports_cfg_cache:
|
||||
return self.spectrum_forecaster_negative
|
||||
return self.spectrum_forecaster
|
||||
|
||||
def _set_spectrum_forecaster(self, forecaster: SpectrumForecaster) -> None:
|
||||
"""Set forecaster for current branch (cond or uncond)."""
|
||||
if self.spectrum_is_cfg_negative and self._spectrum_supports_cfg_cache:
|
||||
self.spectrum_forecaster_negative = forecaster
|
||||
else:
|
||||
self.spectrum_forecaster = forecaster
|
||||
|
||||
def _get_spectrum_context(self) -> Optional[SpectrumContext]:
|
||||
"""Retrieve current Spectrum context from forward batch. Returns None if disabled."""
|
||||
from sglang.multimodal_gen.runtime.managers.forward_context import (
|
||||
get_forward_context,
|
||||
)
|
||||
|
||||
try:
|
||||
forward_context = get_forward_context()
|
||||
except AssertionError:
|
||||
return None
|
||||
forward_batch = forward_context.forward_batch
|
||||
if (
|
||||
forward_batch is None
|
||||
or not forward_batch.enable_spectrum
|
||||
or forward_batch.spectrum_params is None
|
||||
):
|
||||
return None
|
||||
|
||||
spectrum_params = forward_batch.spectrum_params
|
||||
do_cfg = forward_batch.do_classifier_free_guidance
|
||||
is_cfg_negative = forward_batch.is_cfg_negative
|
||||
num_inference_steps = forward_batch.num_inference_steps
|
||||
total_forward_steps = spectrum_params.get_total_forward_steps(
|
||||
num_inference_steps,
|
||||
do_cfg,
|
||||
self._spectrum_supports_cfg_cache,
|
||||
)
|
||||
|
||||
return SpectrumContext(
|
||||
current_step=forward_context.current_timestep,
|
||||
num_inference_steps=num_inference_steps,
|
||||
total_forward_steps=total_forward_steps,
|
||||
do_cfg=do_cfg,
|
||||
is_cfg_negative=is_cfg_negative,
|
||||
spectrum_params=spectrum_params,
|
||||
debug=bool(getattr(forward_batch, "debug", False)),
|
||||
)
|
||||
|
||||
def _record_spectrum_step_stat(self, actual_forward: bool) -> None:
|
||||
if self.spectrum_is_cfg_negative and self._spectrum_supports_cfg_cache:
|
||||
if actual_forward:
|
||||
self.spectrum_real_steps_negative += 1
|
||||
else:
|
||||
self.spectrum_skipped_steps_negative += 1
|
||||
return
|
||||
|
||||
if actual_forward:
|
||||
self.spectrum_real_steps += 1
|
||||
else:
|
||||
self.spectrum_skipped_steps += 1
|
||||
|
||||
def _record_shadow_error_stat(self, rel_l2: float) -> None:
|
||||
if self.spectrum_is_cfg_negative and self._spectrum_supports_cfg_cache:
|
||||
self.spectrum_shadow_rel_l2_sum_negative += rel_l2
|
||||
self.spectrum_shadow_rel_l2_count_negative += 1
|
||||
return
|
||||
|
||||
self.spectrum_shadow_rel_l2_sum += rel_l2
|
||||
self.spectrum_shadow_rel_l2_count += 1
|
||||
|
||||
def _emit_spectrum_summary(self, ctx: SpectrumContext) -> None:
|
||||
if not ctx.debug:
|
||||
return
|
||||
|
||||
if self.spectrum_is_cfg_negative and self._spectrum_supports_cfg_cache:
|
||||
real = self.spectrum_real_steps_negative
|
||||
skipped = self.spectrum_skipped_steps_negative
|
||||
err_sum = self.spectrum_shadow_rel_l2_sum_negative
|
||||
err_count = self.spectrum_shadow_rel_l2_count_negative
|
||||
branch = "negative"
|
||||
else:
|
||||
real = self.spectrum_real_steps
|
||||
skipped = self.spectrum_skipped_steps
|
||||
err_sum = self.spectrum_shadow_rel_l2_sum
|
||||
err_count = self.spectrum_shadow_rel_l2_count
|
||||
branch = "positive"
|
||||
|
||||
total = real + skipped
|
||||
skip_ratio = (skipped / total) if total > 0 else 0.0
|
||||
avg_rel_l2 = (err_sum / err_count) if err_count > 0 else float("nan")
|
||||
logger.info(
|
||||
"[Spectrum/%s] total=%d real=%d skipped=%d skip_ratio=%.3f shadow_rel_l2_avg=%s n=%d window_start=%.3f flex=%.3f",
|
||||
branch,
|
||||
total,
|
||||
real,
|
||||
skipped,
|
||||
skip_ratio,
|
||||
f"{avg_rel_l2:.4f}" if err_count > 0 else "NA",
|
||||
err_count,
|
||||
float(ctx.spectrum_params.window_size),
|
||||
float(ctx.spectrum_params.flex_window),
|
||||
)
|
||||
|
||||
def begin_spectrum_step(self) -> bool:
|
||||
"""Advance Spectrum schedule. Returns True when transformer blocks should run.
|
||||
|
||||
Schedule (after ``warmup_steps`` real forwards):
|
||||
- ``window = floor(curr_ws)`` — run a real forward when
|
||||
``(consecutive_cached + 1) % window == 0``, otherwise skip.
|
||||
- Each real forward increases ``curr_ws`` by ``flex_window``, widening
|
||||
gaps over time (paper alpha).
|
||||
"""
|
||||
ctx = self._get_spectrum_context()
|
||||
self._spectrum_ctx = ctx
|
||||
if ctx is None:
|
||||
# Spectrum disabled — always run blocks (normal DiT path).
|
||||
return True
|
||||
|
||||
# Reset at the very first denoising step of each generation.
|
||||
# Only the positive (or sole) branch triggers the reset so that:
|
||||
# - single-branch models (FLUX, Hunyuan embedded guidance) reset once.
|
||||
# - dual-branch models (Wan with true CFG) reset both counters from the
|
||||
# positive-branch call and leave the negative-branch call unaffected,
|
||||
# keeping both branches synchronised (both start at cnt=0 → cnt=1).
|
||||
# Doing the reset here (not inside _get_spectrum_context) guarantees it
|
||||
# fires exactly once per step, preventing the double-reset that would
|
||||
# desync the two branches.
|
||||
if ctx.current_step == 0 and not ctx.is_cfg_negative:
|
||||
if ctx.debug:
|
||||
logger.info(
|
||||
"[Spectrum] Debug mode enables shadow-error validation; runtime perf is not representative of non-debug runs."
|
||||
)
|
||||
self.reset_spectrum_state(ctx.spectrum_params)
|
||||
|
||||
self.spectrum_is_cfg_negative = ctx.is_cfg_negative
|
||||
params = ctx.spectrum_params
|
||||
cnt, consecutive, curr_ws = self._get_spectrum_branch_state()
|
||||
|
||||
# Warmup: first ``warmup_steps`` calls on this branch always run the DiT.
|
||||
actual_forward = True
|
||||
if cnt >= params.warmup_steps:
|
||||
# After warmup, skip most steps and only run a real forward every
|
||||
# ``window`` cached steps. ``consecutive`` counts skips since last real.
|
||||
window = max(1, math.floor(curr_ws))
|
||||
actual_forward = (consecutive + 1) % window == 0
|
||||
if actual_forward:
|
||||
# Widen the gap for the next stretch (paper alpha / flex_window).
|
||||
curr_ws += params.flex_window
|
||||
curr_ws = round(curr_ws, 3)
|
||||
|
||||
# One denoising forward completed on this branch.
|
||||
cnt += 1
|
||||
if actual_forward:
|
||||
consecutive = 0
|
||||
else:
|
||||
consecutive += 1
|
||||
self._record_spectrum_step_stat(actual_forward)
|
||||
|
||||
# End-of-run wrap: after ``total_steps`` forwards on this branch, reset
|
||||
# counters so state does not leak if the same module is reused. (A fresh
|
||||
# run also resets via ``reset_spectrum_state`` at denoising timestep 0.)
|
||||
total_steps = ctx.total_forward_steps
|
||||
if cnt >= total_steps:
|
||||
self._emit_spectrum_summary(ctx)
|
||||
cnt = 0
|
||||
consecutive = 0
|
||||
curr_ws = params.window_size
|
||||
|
||||
self._set_spectrum_branch_state(cnt, consecutive, curr_ws)
|
||||
return actual_forward
|
||||
|
||||
def spectrum_record_features(self, features: torch.Tensor) -> None:
|
||||
"""Append block outputs from a real forward to the branch forecaster."""
|
||||
ctx = self._spectrum_ctx or self._get_spectrum_context()
|
||||
if ctx is None:
|
||||
return
|
||||
|
||||
params = ctx.spectrum_params
|
||||
forecaster = self._get_spectrum_forecaster()
|
||||
step_idx = float(ctx.current_step)
|
||||
|
||||
# Initialize forecaster on first real forward
|
||||
if forecaster is None:
|
||||
cheb = ChebyshevForecaster(
|
||||
M=params.m,
|
||||
K=params.history_size,
|
||||
lam=params.lam,
|
||||
num_steps=params.tau_num_steps,
|
||||
device=features.device,
|
||||
feature_shape=features.shape,
|
||||
)
|
||||
forecaster = SpectrumForecaster(
|
||||
cheb, taylor_order=params.taylor_order, w=params.w
|
||||
)
|
||||
self._set_spectrum_forecaster(forecaster)
|
||||
|
||||
# In debug mode, compute shadow prediction error for validation
|
||||
if ctx.debug and forecaster.ready() and step_idx >= float(params.warmup_steps):
|
||||
predicted = forecaster.predict(step_idx).to(
|
||||
dtype=features.dtype, device=features.device
|
||||
)
|
||||
pred_f = predicted.float()
|
||||
feat_f = features.detach().float()
|
||||
denom = torch.norm(feat_f).item()
|
||||
if denom > 0:
|
||||
rel_l2 = (torch.norm(pred_f - feat_f).item()) / denom
|
||||
self._record_shadow_error_stat(rel_l2)
|
||||
|
||||
# Update forecaster with actual features from this real step
|
||||
forecaster.update(step_idx, features.detach())
|
||||
self._spectrum_ctx = None
|
||||
|
||||
def spectrum_predict_features(self, template: torch.Tensor) -> torch.Tensor:
|
||||
"""Return forecasted block outputs for a skipped step (same shape as template)."""
|
||||
forecaster = self._get_spectrum_forecaster()
|
||||
if forecaster is None or not forecaster.ready():
|
||||
return template
|
||||
ctx = self._spectrum_ctx or self._get_spectrum_context()
|
||||
if ctx is None:
|
||||
return template
|
||||
step_idx = float(ctx.current_step)
|
||||
predicted = forecaster.predict(step_idx)
|
||||
self._spectrum_ctx = None
|
||||
return predicted.to(dtype=template.dtype, device=template.device)
|
||||
@@ -9,6 +9,9 @@ from torch import nn
|
||||
|
||||
from sglang.multimodal_gen.configs.models import DiTConfig
|
||||
|
||||
# NOTE: SpectrumMixin lives in runtime.cache.spectrum
|
||||
from sglang.multimodal_gen.runtime.cache.spectrum import SpectrumMixin
|
||||
|
||||
# NOTE: TeaCacheContext and TeaCacheMixin have been moved to
|
||||
# sglang.multimodal_gen.runtime.cache.teacache
|
||||
# For backwards compatibility, re-export from the new location
|
||||
@@ -87,11 +90,13 @@ class BaseDiT(nn.Module, ABC):
|
||||
return next(self.parameters()).device
|
||||
|
||||
|
||||
class CachableDiT(TeaCacheMixin, BaseDiT):
|
||||
class CachableDiT(SpectrumMixin, TeaCacheMixin, BaseDiT):
|
||||
"""
|
||||
An intermediate base class that adds TeaCache optimization functionality to DiT models.
|
||||
Base class for DiT models that support inference-time cache accelerators.
|
||||
|
||||
Inherits ``SpectrumMixin`` (Chebyshev step skipping) and ``TeaCacheMixin``
|
||||
(temporal L1 similarity caching) plus ``BaseDiT`` core functionality.
|
||||
|
||||
Inherits TeaCacheMixin for cache logic and BaseDiT for core DiT functionality.
|
||||
"""
|
||||
|
||||
# These are required class attributes that should be overridden by concrete implementations
|
||||
@@ -110,6 +115,7 @@ class CachableDiT(TeaCacheMixin, BaseDiT):
|
||||
|
||||
def __init__(self, config: DiTConfig, **kwargs) -> None:
|
||||
super().__init__(config, **kwargs)
|
||||
self._init_spectrum_state()
|
||||
self._init_teacache_state()
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -66,6 +66,7 @@ from sglang.multimodal_gen.runtime.layers.visual_embedding import (
|
||||
CombinedTimestepGuidanceTextProjEmbeddings,
|
||||
CombinedTimestepTextProjEmbeddings,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.managers.forward_context import get_forward_context
|
||||
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
|
||||
LayerwiseOffloadableModuleMixin,
|
||||
)
|
||||
@@ -1149,24 +1150,35 @@ class FluxTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
|
||||
ip_hidden_states = self.encoder_hid_proj(ip_adapter_image_embeds)
|
||||
joint_attention_kwargs.update({"ip_hidden_states": ip_hidden_states})
|
||||
|
||||
for block in self.transformer_blocks:
|
||||
encoder_hidden_states, hidden_states = block(
|
||||
hidden_states=hidden_states,
|
||||
encoder_hidden_states=encoder_hidden_states,
|
||||
temb=temb,
|
||||
freqs_cis=freqs_cis,
|
||||
joint_attention_kwargs=joint_attention_kwargs,
|
||||
num_replicated_prefix=num_replicated_prefix,
|
||||
)
|
||||
for block in self.single_transformer_blocks:
|
||||
encoder_hidden_states, hidden_states = block(
|
||||
hidden_states=hidden_states,
|
||||
encoder_hidden_states=encoder_hidden_states,
|
||||
temb=temb,
|
||||
freqs_cis=singles_freqs_cis,
|
||||
joint_attention_kwargs=joint_attention_kwargs,
|
||||
num_replicated_prefix=num_replicated_prefix,
|
||||
)
|
||||
forward_batch = get_forward_context().forward_batch
|
||||
spectrum_enabled = forward_batch is not None and forward_batch.enable_spectrum
|
||||
run_transformer_blocks = (
|
||||
self.begin_spectrum_step() if spectrum_enabled else True
|
||||
)
|
||||
if run_transformer_blocks:
|
||||
for block in self.transformer_blocks:
|
||||
encoder_hidden_states, hidden_states = block(
|
||||
hidden_states=hidden_states,
|
||||
encoder_hidden_states=encoder_hidden_states,
|
||||
temb=temb,
|
||||
freqs_cis=freqs_cis,
|
||||
joint_attention_kwargs=joint_attention_kwargs,
|
||||
num_replicated_prefix=num_replicated_prefix,
|
||||
)
|
||||
for block in self.single_transformer_blocks:
|
||||
encoder_hidden_states, hidden_states = block(
|
||||
hidden_states=hidden_states,
|
||||
encoder_hidden_states=encoder_hidden_states,
|
||||
temb=temb,
|
||||
freqs_cis=singles_freqs_cis,
|
||||
joint_attention_kwargs=joint_attention_kwargs,
|
||||
num_replicated_prefix=num_replicated_prefix,
|
||||
)
|
||||
if spectrum_enabled:
|
||||
self.spectrum_record_features(hidden_states)
|
||||
else:
|
||||
if spectrum_enabled:
|
||||
hidden_states = self.spectrum_predict_features(hidden_states)
|
||||
|
||||
hidden_states = self.norm_out(hidden_states, temb)
|
||||
|
||||
|
||||
@@ -662,6 +662,7 @@ class HunyuanVideoTransformer3DModel(CachableDiT, LayerwiseOffloadableModuleMixi
|
||||
forward_context = get_forward_context()
|
||||
forward_batch = forward_context.forward_batch
|
||||
enable_teacache = forward_batch is not None and forward_batch.enable_teacache
|
||||
enable_spectrum = forward_batch is not None and forward_batch.enable_spectrum
|
||||
|
||||
if guidance is None:
|
||||
guidance = torch.tensor(
|
||||
@@ -744,11 +745,10 @@ class HunyuanVideoTransformer3DModel(CachableDiT, LayerwiseOffloadableModuleMixi
|
||||
|
||||
freqs_cis = (freqs_cos, freqs_sin) if freqs_cos is not None else None
|
||||
|
||||
should_skip_forward = self.should_skip_forward_for_cached_states(
|
||||
img=img, vec=vec
|
||||
)
|
||||
|
||||
if should_skip_forward:
|
||||
run_transformer_blocks = self.begin_spectrum_step()
|
||||
if enable_spectrum and not run_transformer_blocks:
|
||||
img = self.spectrum_predict_features(img)
|
||||
elif self.should_skip_forward_for_cached_states(img=img, vec=vec):
|
||||
img = self.retrieve_cached_states(img)
|
||||
else:
|
||||
if enable_teacache:
|
||||
@@ -786,6 +786,8 @@ class HunyuanVideoTransformer3DModel(CachableDiT, LayerwiseOffloadableModuleMixi
|
||||
|
||||
if enable_teacache:
|
||||
self.maybe_cache_states(img, original_img)
|
||||
if enable_spectrum:
|
||||
self.spectrum_record_features(img)
|
||||
|
||||
# Final layer processing
|
||||
img = self.final_layer(img, vec)
|
||||
|
||||
@@ -17,6 +17,7 @@ from diffusers.models.normalization import AdaLayerNormContinuous
|
||||
from sglang.multimodal_gen.configs.models.dits.stablediffusion3 import (
|
||||
StableDiffusion3TransformerConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.managers.forward_context import get_forward_context
|
||||
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
|
||||
LayerwiseOffloadableModuleMixin,
|
||||
)
|
||||
@@ -128,26 +129,37 @@ class SD3Transformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
|
||||
else:
|
||||
interval_control = 0
|
||||
|
||||
for index_block, block in enumerate(self.transformer_blocks):
|
||||
if index_block not in skip_layer_set:
|
||||
encoder_embeddings, hidden_states = block(
|
||||
hidden_states=hidden_states,
|
||||
encoder_hidden_states=encoder_embeddings,
|
||||
temb=temb,
|
||||
joint_attention_kwargs=joint_attention_kwargs,
|
||||
)
|
||||
forward_batch = get_forward_context().forward_batch
|
||||
spectrum_enabled = forward_batch is not None and forward_batch.enable_spectrum
|
||||
run_transformer_blocks = (
|
||||
self.begin_spectrum_step() if spectrum_enabled else True
|
||||
)
|
||||
if run_transformer_blocks:
|
||||
for index_block, block in enumerate(self.transformer_blocks):
|
||||
if index_block not in skip_layer_set:
|
||||
encoder_embeddings, hidden_states = block(
|
||||
hidden_states=hidden_states,
|
||||
encoder_hidden_states=encoder_embeddings,
|
||||
temb=temb,
|
||||
joint_attention_kwargs=joint_attention_kwargs,
|
||||
)
|
||||
|
||||
# controlnet residual
|
||||
if (
|
||||
block_controlnet_hidden_states is not None
|
||||
and block.context_pre_only is False
|
||||
):
|
||||
hidden_states = (
|
||||
hidden_states
|
||||
+ block_controlnet_hidden_states[
|
||||
int(index_block / interval_control)
|
||||
]
|
||||
)
|
||||
# controlnet residual
|
||||
if (
|
||||
block_controlnet_hidden_states is not None
|
||||
and block.context_pre_only is False
|
||||
):
|
||||
hidden_states = (
|
||||
hidden_states
|
||||
+ block_controlnet_hidden_states[
|
||||
int(index_block / interval_control)
|
||||
]
|
||||
)
|
||||
if spectrum_enabled:
|
||||
self.spectrum_record_features(hidden_states)
|
||||
else:
|
||||
if spectrum_enabled:
|
||||
hidden_states = self.spectrum_predict_features(hidden_states)
|
||||
|
||||
hidden_states = self.norm_out(hidden_states, temb)
|
||||
hidden_states = self.proj_out(hidden_states)
|
||||
|
||||
@@ -1010,6 +1010,7 @@ class WanTransformer3DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
|
||||
self.enable_teacache = (
|
||||
forward_batch is not None and forward_batch.enable_teacache
|
||||
)
|
||||
enable_spectrum = forward_batch is not None and forward_batch.enable_spectrum
|
||||
|
||||
orig_dtype = hidden_states.dtype
|
||||
if not isinstance(encoder_hidden_states, torch.Tensor):
|
||||
@@ -1143,12 +1144,14 @@ class WanTransformer3DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
|
||||
assert encoder_hidden_states.dtype == orig_dtype
|
||||
|
||||
# 4. Transformer blocks
|
||||
# if caching is enabled, we might be able to skip the forward pass
|
||||
run_transformer_blocks = self.begin_spectrum_step()
|
||||
should_skip_forward = self.should_skip_forward_for_cached_states(
|
||||
timestep_proj=timestep_proj, temb=temb
|
||||
)
|
||||
|
||||
if should_skip_forward:
|
||||
if enable_spectrum and not run_transformer_blocks:
|
||||
hidden_states = self.spectrum_predict_features(hidden_states)
|
||||
elif should_skip_forward:
|
||||
hidden_states = self.retrieve_cached_states(hidden_states)
|
||||
else:
|
||||
# if teacache is enabled, we need to cache the original hidden states
|
||||
@@ -1162,6 +1165,8 @@ class WanTransformer3DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
|
||||
# if teacache is enabled, we need to cache the original hidden states
|
||||
if self.enable_teacache:
|
||||
self.maybe_cache_states(hidden_states, original_hidden_states)
|
||||
if enable_spectrum:
|
||||
self.spectrum_record_features(hidden_states)
|
||||
self.cnt += 1
|
||||
|
||||
if sequence_shard_enabled:
|
||||
|
||||
@@ -92,6 +92,12 @@ class TestSamplingParamsValidate(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, r"boundary_ratio"):
|
||||
SamplingParams(boundary_ratio=math.nan)
|
||||
|
||||
def test_teacache_and_spectrum_are_mutually_exclusive(self):
|
||||
with self.assertRaisesRegex(
|
||||
ValueError, r"enable_teacache and enable_spectrum are mutually exclusive"
|
||||
):
|
||||
SamplingParams(enable_teacache=True, enable_spectrum=True)
|
||||
|
||||
|
||||
class TestSamplingParamsSubclass(unittest.TestCase):
|
||||
def test_flux_defaults_resolution_when_not_provided(self):
|
||||
@@ -243,6 +249,38 @@ class TestSamplingParamsCliArgs(unittest.TestCase):
|
||||
self._parse_cli_kwargs(["--quality", "high"])["quality"], "high"
|
||||
)
|
||||
|
||||
def test_get_cli_args_maps_spectrum_prefixed_flags(self):
|
||||
kwargs = self._parse_cli_kwargs(
|
||||
[
|
||||
"--enable-spectrum",
|
||||
"--spectrum-window-size",
|
||||
"2.5",
|
||||
"--spectrum-flex-window",
|
||||
"0.9",
|
||||
"--spectrum-warmup-steps",
|
||||
"6",
|
||||
"--spectrum-m",
|
||||
"3",
|
||||
"--spectrum-lam",
|
||||
"0.2",
|
||||
"--spectrum-tau-num-steps",
|
||||
"42",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertTrue(kwargs["enable_spectrum"])
|
||||
self.assertEqual(
|
||||
kwargs["spectrum_params"],
|
||||
{
|
||||
"window_size": 2.5,
|
||||
"flex_window": 0.9,
|
||||
"warmup_steps": 6,
|
||||
"m": 3,
|
||||
"lam": 0.2,
|
||||
"tau_num_steps": 42,
|
||||
},
|
||||
)
|
||||
|
||||
def test_qwen_image_cli_path_preserves_model_defaults(self):
|
||||
params = self._make_qwen_image_params([])
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.multimodal_gen.runtime.cache.spectrum import (
|
||||
ChebyshevForecaster,
|
||||
SpectrumForecaster,
|
||||
)
|
||||
|
||||
|
||||
class TestSpectrumForecaster(unittest.TestCase):
|
||||
def test_chebyshev_fit_and_predict(self) -> None:
|
||||
"""Predict returns the expected feature shape after fitting on prior steps."""
|
||||
forecaster = ChebyshevForecaster(
|
||||
M=2, K=10, lam=0.1, num_steps=10, feature_shape=(4, 8)
|
||||
)
|
||||
for step in range(6):
|
||||
forecaster.update(float(step), torch.randn(4, 8))
|
||||
predicted = forecaster.predict(6.0)
|
||||
self.assertEqual(predicted.shape, (4, 8))
|
||||
|
||||
def test_chebyshev_fit_and_predict_bfloat16(self) -> None:
|
||||
"""bfloat16 inputs produce bfloat16 predictions."""
|
||||
forecaster = ChebyshevForecaster(
|
||||
M=2, K=10, lam=0.1, num_steps=10, feature_shape=(4, 8)
|
||||
)
|
||||
for step in range(6):
|
||||
forecaster.update(float(step), torch.randn(4, 8, dtype=torch.bfloat16))
|
||||
predicted = forecaster.predict(6.0)
|
||||
self.assertEqual(predicted.shape, (4, 8))
|
||||
self.assertEqual(predicted.dtype, torch.bfloat16)
|
||||
|
||||
@unittest.skipUnless(torch.cuda.is_available(), "requires CUDA autocast")
|
||||
def test_chebyshev_fit_and_predict_bfloat16_under_autocast(self) -> None:
|
||||
"""CUDA bf16 autocast keeps prediction working and preserves bf16 output."""
|
||||
forecaster = ChebyshevForecaster(
|
||||
M=2, K=10, lam=0.1, num_steps=10, feature_shape=(4, 8)
|
||||
).cuda()
|
||||
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
||||
for step in range(6):
|
||||
forecaster.update(
|
||||
float(step), torch.randn(4, 8, device="cuda", dtype=torch.bfloat16)
|
||||
)
|
||||
predicted = forecaster.predict(6.0)
|
||||
self.assertEqual(predicted.shape, (4, 8))
|
||||
self.assertEqual(predicted.dtype, torch.bfloat16)
|
||||
|
||||
def test_spectrum_blend_predict(self) -> None:
|
||||
"""The blended Spectrum forecaster returns the expected output shape."""
|
||||
cheb = ChebyshevForecaster(
|
||||
M=2, K=10, lam=0.1, num_steps=10, feature_shape=(2, 3)
|
||||
)
|
||||
blend = SpectrumForecaster(cheb, taylor_order=1, w=0.5)
|
||||
for step in range(4):
|
||||
blend.update(float(step), torch.ones(2, 3) * step)
|
||||
out = blend.predict(4.0)
|
||||
self.assertEqual(out.shape, (2, 3))
|
||||
|
||||
def test_chebyshev_prediction_error_is_bounded_on_smooth_signal(self) -> None:
|
||||
"""Prediction error stays very low on a deterministic smooth signal."""
|
||||
|
||||
def smooth_feature(step: float) -> torch.Tensor:
|
||||
# Linear trend should be modeled accurately by M=1 Chebyshev basis.
|
||||
base = 0.5 + 0.125 * step
|
||||
return torch.tensor(
|
||||
[[base, base + 0.1], [0.75 * base, -0.5 * base]],
|
||||
dtype=torch.float32,
|
||||
)
|
||||
|
||||
forecaster = ChebyshevForecaster(
|
||||
M=1, K=16, lam=1e-6, num_steps=50, feature_shape=(2, 2)
|
||||
)
|
||||
for step in range(8):
|
||||
forecaster.update(float(step), smooth_feature(float(step)))
|
||||
|
||||
target = smooth_feature(8.0)
|
||||
predicted = forecaster.predict(8.0)
|
||||
rel_l2 = torch.norm(predicted - target) / torch.norm(target)
|
||||
self.assertLess(rel_l2.item(), 1e-3)
|
||||
|
||||
def test_chebyshev_tau_horizon_matches_reference(self) -> None:
|
||||
"""Tau normalization matches the fixed 50-step horizon used by the reference."""
|
||||
f50 = ChebyshevForecaster(M=0, num_steps=50, feature_shape=(1,))
|
||||
f20 = ChebyshevForecaster(M=0, num_steps=20, feature_shape=(1,))
|
||||
t = torch.tensor([10.0])
|
||||
self.assertAlmostEqual(f50._taus(t).item(), -0.6, places=5)
|
||||
self.assertAlmostEqual(f20._taus(t).item(), 0.0, places=5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user