File size: 11,298 Bytes
4d9b003 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | # SPDX-License-Identifier: Apache-2.0
"""DPM-Solver++(2M) with denoise-to-zero, as Autoware runs it around the decoder (multi_step mode).
Port of ``PKG/src/inference/solver/dpm_solver.cpp`` (guidance disabled, Autoware's default and decision D11) and of
the prefix constraint of ``PKG/src/inference/multi_step_inference.cpp:300-340``. The scalar schedule is float32 math
evaluated with the C library's ``expf`` / ``logf`` / ``expm1f`` / ``sqrtf`` (what ``std::exp(float)`` etc. call in
the node) in the C++ operation order, so the timesteps and update coefficients are the node's bit for bit; numpy's
own float32 transcendentals differ from glibc in the last bit for many arguments. Without a loadable libm the module
falls back to numpy (``SCALAR_MATH`` says which).
For ``steps = 10``: 11 decoder evaluations at ``timesteps[0..9]`` and ``1/N = 0.001`` (denoise-to-zero); the 11
published iterates (``denoising_steps``) are the corrected ``x`` after each update and after the final evaluation.
The update formulas, with ``x`` and the model outputs float32 arrays and every scalar a float32::
first (step 1): x = (sigma_t / sigma_s) * x - (alpha_t * phi_1) * m
second (steps 2..): d = (m0 - m1) / r0
x = (sigma_t / sigma_0) * x - (alpha_t * phi_1) * m0 - (0.5 * (alpha_t * phi_1)) * d
:class:`SolverPlan` lists these scalars per update (the RT-dev coefficient tables of the on-device loop).
"""
from __future__ import annotations
import ctypes
import ctypes.util
from dataclasses import dataclass, field
from typing import Callable, List, Optional, Tuple
import numpy as np
from ..reference import config as C
__all__ = ["SCALAR_MATH", "marginal_log_mean_coeff", "marginal_alpha", "marginal_std", "marginal_lambda",
"inverse_lambda", "log_snr_timesteps", "SolverUpdate", "SolverPlan", "solver_plan", "first_update",
"second_update", "dpm_solver_sample", "apply_prefix_constraint", "SampleResult"]
f32 = np.float32
_T, _N = C.NOISE_SCHEDULE_T, C.NOISE_SCHEDULE_TOTAL_N
_B0, _B1 = C.NOISE_SCHEDULE_BETA0, C.NOISE_SCHEDULE_BETA1
class _ScalarMath:
"""float32 ``exp`` / ``log`` / ``expm1`` / ``sqrt`` of the C library, loaded on first use (numpy fallback)."""
def __init__(self) -> None:
self._fns = None
self.source = "unloaded"
def _load(self) -> None:
fns = None
try:
name = ctypes.util.find_library("m") or "libm.so.6"
libm = ctypes.CDLL(name)
fns = {}
for fn in ("expf", "logf", "expm1f", "sqrtf"):
f = getattr(libm, fn)
f.restype, f.argtypes = ctypes.c_float, [ctypes.c_float]
fns[fn] = f
self.source = f"libm ({name})"
except (OSError, AttributeError):
fns = None
self.source = "numpy (no C math library found)"
self._fns = fns or {}
def _call(self, fn: str, x: float, fallback: Callable) -> np.float32:
if self._fns is None:
self._load()
if fn in self._fns:
return f32(self._fns[fn](float(f32(x))))
return f32(fallback(f32(x)))
def exp(self, x):
return self._call("expf", x, np.exp)
def log(self, x):
return self._call("logf", x, np.log)
def expm1(self, x):
return self._call("expm1f", x, np.expm1)
def sqrt(self, x):
return self._call("sqrtf", x, np.sqrt)
SCALAR_MATH = _ScalarMath()
_m = SCALAR_MATH
# ---- noise schedule (dpm_solver.cpp:49-94), float32 in the C++ operation order ---------------------------------------
def marginal_log_mean_coeff(t) -> np.float32:
"""``-0.25f * t * t * (beta1 - beta0) - 0.5f * t * beta0``."""
t = f32(t)
return f32(f32(f32(f32(-0.25) * t) * t) * f32(_B1 - _B0)) - f32(f32(f32(0.5) * t) * _B0)
def marginal_alpha(t) -> np.float32:
return _m.exp(marginal_log_mean_coeff(t))
def marginal_std(t) -> np.float32:
return _m.sqrt(f32(f32(1.0) - _m.exp(f32(f32(2.0) * marginal_log_mean_coeff(t)))))
def marginal_lambda(t) -> np.float32:
lmc = marginal_log_mean_coeff(t)
log_std = f32(f32(0.5) * _m.log(f32(f32(1.0) - _m.exp(f32(f32(2.0) * lmc)))))
return f32(lmc - log_std)
def _log_add_exp(a, b) -> np.float32:
m = max(f32(a), f32(b))
return f32(m + _m.log(f32(_m.exp(f32(f32(a) - m)) + _m.exp(f32(f32(b) - m)))))
def inverse_lambda(lam) -> np.float32:
beta_delta = f32(_B1 - _B0)
tmp = f32(f32(f32(2.0) * beta_delta) * _log_add_exp(f32(f32(-2.0) * f32(lam)), f32(0.0)))
delta = f32(f32(_B0 * _B0) + tmp)
return f32(f32(tmp / f32(_m.sqrt(delta) + _B0)) / beta_delta)
def log_snr_timesteps(steps: int) -> List[np.float32]:
"""``steps + 1`` times uniform in log-SNR between t = 1 and t = 1/N (``dpm_solver.cpp:80-94``)."""
t0 = f32(f32(1.0) / _N)
lam_t, lam_0 = marginal_lambda(_T), marginal_lambda(t0)
out = []
for i in range(steps + 1):
ratio = f32(f32(i) / f32(steps))
out.append(inverse_lambda(f32(lam_t + f32(f32(lam_0 - lam_t) * ratio))))
return out
# ---- updates (dpm_solver.cpp:96-136) ------------------------------------------------------------------------------
def first_update(x_s: np.ndarray, model_s: np.ndarray, s, t) -> np.ndarray:
h = f32(marginal_lambda(t) - marginal_lambda(s))
sigma_s, sigma_t, alpha_t = marginal_std(s), marginal_std(t), marginal_alpha(t)
phi_1 = _m.expm1(f32(-h))
a, b = f32(sigma_t / sigma_s), f32(alpha_t * phi_1)
return (a * x_s.astype(np.float32) - b * model_s.astype(np.float32)).astype(np.float32)
def second_update(x_s: np.ndarray, model_prev: Tuple[np.ndarray, np.ndarray], t_prev: Tuple[float, float],
t) -> np.ndarray:
m1, m0 = model_prev # model_prev_list[0] (older), model_prev_list[1] (newer)
t1, t0 = t_prev
lam1, lam0, lam_t = marginal_lambda(t1), marginal_lambda(t0), marginal_lambda(t)
sigma0, sigma_t, alpha_t = marginal_std(t0), marginal_std(t), marginal_alpha(t)
h0, h = f32(lam0 - lam1), f32(lam_t - lam0)
r0 = f32(h0 / h)
phi_1 = _m.expm1(f32(-h))
a, b = f32(sigma_t / sigma0), f32(alpha_t * phi_1)
c = f32(f32(0.5) * b)
d1_0 = ((m0.astype(np.float32) - m1.astype(np.float32)) / r0).astype(np.float32)
return (a * x_s.astype(np.float32) - b * m0.astype(np.float32) - c * d1_0).astype(np.float32)
@dataclass(frozen=True)
class SolverUpdate:
"""One solver update to time ``t``: ``x = a * x - b * m0 - c * (m0 - m1) / r0`` (``c = 0`` for the first-order
update, which has no ``m1``)."""
order: int
t: float
a: float
b: float
c: float
r0: float
@dataclass(frozen=True)
class SolverPlan:
"""Everything the on-device loop needs for ``steps`` (the RT-dev tables of PLAN.md 2.12): the decoder evaluation
times (``eval_times[k]`` feeds evaluation ``k``; the t-embedding / adaLN tables are built for these values), the
update after each of the first ``steps`` evaluations, and the published iterate times."""
steps: int
timesteps: Tuple[float, ...]
eval_times: Tuple[float, ...]
updates: Tuple[SolverUpdate, ...]
denoising_timesteps: Tuple[float, ...]
scalar_math: str = field(default="")
def solver_plan(steps: int = C.DPM_SOLVER_STEPS) -> SolverPlan:
if steps < C.DPM_SOLVER_ORDER:
raise ValueError("DpmSolver steps must be greater than or equal to solver order.")
ts = log_snr_timesteps(steps)
updates = []
for step in range(1, steps + 1):
t = ts[step]
if step < C.DPM_SOLVER_ORDER:
s = _T if step == 1 else ts[step - 1]
h = f32(marginal_lambda(t) - marginal_lambda(s))
a = f32(marginal_std(t) / marginal_std(s))
b = f32(marginal_alpha(t) * _m.expm1(f32(-h)))
updates.append(SolverUpdate(1, float(t), float(a), float(b), 0.0, 1.0))
else:
t1 = _T if step - 2 == 0 else ts[step - 2]
t0 = ts[step - 1]
lam1, lam0, lam_t = marginal_lambda(t1), marginal_lambda(t0), marginal_lambda(t)
h0, h = f32(lam0 - lam1), f32(lam_t - lam0)
b = f32(marginal_alpha(t) * _m.expm1(f32(-h)))
updates.append(SolverUpdate(2, float(t), float(f32(marginal_std(t) / marginal_std(t0))), float(b),
float(f32(f32(0.5) * b)), float(f32(h0 / h))))
final_t = f32(f32(1.0) / _N)
return SolverPlan(steps, tuple(float(t) for t in ts), tuple(float(t) for t in ts[:steps]) + (float(final_t),),
tuple(updates), tuple(float(t) for t in ts[1:]) + (float(final_t),), SCALAR_MATH.source)
# ---- the loop (dpm_solver.cpp:138-234) ---------------------------------------------------------------------------
@dataclass
class SampleResult:
final_x: np.ndarray
denoising_steps: List[np.ndarray]
denoising_timesteps: List[float]
eval_times: List[float]
nfe: int
def apply_prefix_constraint(x: np.ndarray, current_states: np.ndarray) -> np.ndarray:
"""``x[agent, 0, :] = current_states[agent]`` in place (``multi_step_inference.cpp:327-340``); ``x`` is
``[321, 81, 4]`` (or with a leading batch dim)."""
x[..., 0, :] = current_states
return x
def dpm_solver_sample(initial_x: np.ndarray, model_fn: Callable[[np.ndarray, np.float32], np.ndarray],
correcting_fn: Callable[[np.ndarray], None], steps: int = C.DPM_SOLVER_STEPS,
on_iterate: Optional[Callable[[int, np.ndarray], None]] = None) -> SampleResult:
"""``DpmSolver::sample`` without guidance. ``model_fn(x, t)`` is one decoder evaluation (x0 prediction),
``correcting_fn(x)`` the in-place prefix constraint."""
if steps < C.DPM_SOLVER_ORDER:
raise ValueError("DpmSolver steps must be greater than or equal to solver order.")
x = np.array(initial_x, dtype=np.float32, copy=True)
correcting_fn(x)
ts = log_snr_timesteps(steps)
steps_out: List[np.ndarray] = []
step_ts: List[float] = []
eval_times: List[float] = []
def evaluate(xx: np.ndarray, t) -> np.ndarray:
eval_times.append(float(t))
return np.asarray(model_fn(xx, f32(t)), np.float32)
def record(xx: np.ndarray, t) -> None:
steps_out.append(xx.copy())
step_ts.append(float(t))
if on_iterate is not None:
on_iterate(len(steps_out) - 1, xx)
t_prev = [_T]
m_prev = [evaluate(x, ts[0])]
for step in range(1, C.DPM_SOLVER_ORDER):
t = ts[step]
x = first_update(x, m_prev[-1], t_prev[-1], t)
correcting_fn(x)
record(x, t)
t_prev.append(t)
m_prev.append(evaluate(x, t))
for step in range(C.DPM_SOLVER_ORDER, steps + 1):
t = ts[step]
x = second_update(x, (m_prev[0], m_prev[1]), (t_prev[0], t_prev[1]), t)
correcting_fn(x)
record(x, t)
t_prev = [t_prev[1], t]
m_prev = [m_prev[1], None]
if step < steps:
m_prev[1] = evaluate(x, t)
final_t = f32(f32(1.0) / _N)
x = evaluate(x, final_t) # denoise to zero: the model output itself, no guidance on the last step
x = np.array(x, dtype=np.float32, copy=True)
correcting_fn(x)
record(x, final_t)
return SampleResult(x, steps_out, step_ts, eval_times, len(eval_times))
|