Files
xsl 24c526af68 feat(assets): SA3 音效补齐 radio 11+1 与 dungeon 13源+15pan(token 走 hf-mirror 实测可用)
- 模型 17/17 文件经镜像全量缓存(gated+token,直连本机不通;sfx.md/setup.md/asset-pipeline.md 镜像结论同步更新)
- 修复迁移遗留:stable_audio_3/models/ 源码子包被 .gitignore models/ 规则误伤丢失,从上游恢复并加白名单
- radio:arg_muffled/brake_screech 等叙事件落地(接线原有 L111/L154 零改动)+ phone_ring SYN→SA3 升级;dungeon 13 源 A/B 对齐(RMS 偏差 ≤0.3dB)全量同名替换,SYN 原版备份 tools/asset_fix_backup/sa3_ab_20260902/
- 关键玩法语义达标:echo 距离档 0.83<1.43<2.18<2.98s 严格递增;one-shot 真峰值全 ≤-3dBFS(解码回读法)
- check_all_assets 8 游戏全绿(radio 254/254、dungeon 303/303);playtest radio 221/dungeon 1610 全绿
2026-09-02 19:35:38 +08:00

471 lines
23 KiB
Python

"""
References:
1) the official LoRA implementation released by Microsoft:
https://github.com/microsoft/LoRA/blob/main/loralib/layers.py
"""
import math
import re
from functools import partial
from itertools import product
import torch
import torch.nn.utils.parametrize as parametrize
from torch import nn
from ...verbose import vprint
def _canonicalize_svd_signs(U, Vh):
"""Enforce deterministic sign convention: largest-magnitude element of each U column is positive."""
max_abs_idx = U.abs().argmax(dim=0)
signs = U[max_abs_idx, torch.arange(U.shape[1])].sign()
signs[signs == 0] = 1
return U * signs.unsqueeze(0), Vh * signs.unsqueeze(1)
class LoRAParametrization(nn.Module):
def __init__(self, fan_in, fan_out, fan_in_fan_out=False, rank=4, lora_dropout_p=0.0, lora_alpha=1,
adapter_type="lora", W0=None, lora_index=0, svd_bases=None
):
super().__init__()
dtype = torch.get_default_dtype()
device = W0.device if W0 is not None else None
# if weight is stored as (fan_out, fan_in), the memory layout of A & B follows (W + BA)x
# otherwise, it's x(W + AB). This allows us to tie the weights between linear layers and embeddings
self.swap = (lambda x: (x[1], x[0])) if fan_in_fan_out else (lambda x: x)
self.lora_alpha, self.rank = lora_alpha, rank
self.scaling = lora_alpha / rank
self.lora_dropout = nn.Dropout(p=lora_dropout_p) if lora_dropout_p > 0 else lambda x: x
self.dropout_fn = self._dropout if lora_dropout_p > 0 else lambda x: x
self.register_buffer("lora_dropout_mask", torch.ones(self.swap((1, fan_in)), dtype=dtype, device=device))
self.register_buffer("lora_strength", torch.tensor(1.0, dtype=dtype, device=device), persistent=False)
self.adapter_type = adapter_type
self.lora_index = lora_index
if self.adapter_type == "lora":
self.lora_A = nn.Parameter(torch.zeros(self.swap((rank, fan_in)), device=device))
self.lora_B = nn.Parameter(torch.zeros(self.swap((fan_out, rank)), device=device))
nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
self.forward_fn = self.lora_forward
self._adapter_forward_fn = self.forward_fn
elif self.adapter_type in ("dora-rows", "dora"):
# DoRA-rows: magnitude per row (per-output-neuron, dim=1) - paper-correct variant
# "dora" maps here as the default for backward compat
self.lora_A = nn.Parameter(torch.zeros(self.swap((rank, fan_in)), device=device))
self.lora_B = nn.Parameter(torch.zeros(self.swap((fan_out, rank)), device=device))
nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
self._norm_dim = 1
if W0 is not None:
W0_2d = W0.view(W0.shape[0], -1)
self.magnitude = nn.Parameter(W0_2d.norm(dim=self._norm_dim).to(dtype))
else:
self.magnitude = nn.Parameter(torch.ones(fan_out, dtype=dtype, device=device))
self.forward_fn = self.dora_forward
self._adapter_forward_fn = self.forward_fn
elif self.adapter_type == "dora-cols":
# DoRA-cols: magnitude per column (per-input-feature, dim=0) - legacy variant
self.lora_A = nn.Parameter(torch.zeros(self.swap((rank, fan_in)), device=device))
self.lora_B = nn.Parameter(torch.zeros(self.swap((fan_out, rank)), device=device))
nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
self._norm_dim = 0
if W0 is not None:
W0_2d = W0.view(W0.shape[0], -1)
self.magnitude = nn.Parameter(W0_2d.norm(dim=self._norm_dim).to(dtype))
else:
self.magnitude = nn.Parameter(torch.ones(fan_in, dtype=dtype, device=device))
self.forward_fn = self.dora_forward
self._adapter_forward_fn = self.forward_fn
elif self.adapter_type == "bora":
# BoRA: bi-dimensional DoRA - magnitude on both row and column axes
self.lora_A = nn.Parameter(torch.zeros(self.swap((rank, fan_in)), device=device))
self.lora_B = nn.Parameter(torch.zeros(self.swap((fan_out, rank)), device=device))
nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
if W0 is not None:
W0_2d = W0.view(W0.shape[0], -1)
self.magnitude_r = nn.Parameter(W0_2d.norm(dim=1).to(dtype))
self.magnitude_c = nn.Parameter(W0_2d.norm(dim=0).to(dtype))
else:
self.magnitude_r = nn.Parameter(torch.ones(fan_out, dtype=dtype, device=device))
self.magnitude_c = nn.Parameter(torch.ones(fan_in, dtype=dtype, device=device))
self.forward_fn = self.bora_forward
self._adapter_forward_fn = self.forward_fn
elif self.adapter_type == "lora-xs":
# frozen bases U:(fan_out,r), V:(fan_in,r); tiny trainable core M:(r,r)
self.register_buffer("U", torch.empty(self.swap((fan_out, self.rank)), device=device), persistent=True)
self.register_buffer("V", torch.empty(self.swap((fan_in, self.rank)), device=device), persistent=True)
self.M_xs = nn.Parameter(torch.zeros(self.rank, self.rank, dtype=dtype, device=device))
if svd_bases is not None:
self.U.copy_(svd_bases["U"][:, :self.rank].to(dtype))
self.V.copy_(svd_bases["V"][:, :self.rank].to(dtype))
elif W0 is not None:
W0_2d = W0.view(W0.shape[0], -1).cpu().float()
U_full, S, Vh_full = torch.linalg.svd(W0_2d, full_matrices=False)
U_full, Vh_full = _canonicalize_svd_signs(U_full, Vh_full)
self.U.copy_(U_full[:, :self.rank].to(dtype))
self.V.copy_(Vh_full[:self.rank, :].T.to(dtype))
self.forward_fn = self.lora_xs_forward
self._adapter_forward_fn = self.forward_fn
elif self.adapter_type == "dora-rows-xs":
# DoRA-rows + LoRA-XS: frozen SVD bases + trainable M_xs core + row magnitude
self.register_buffer("U", torch.empty(self.swap((fan_out, self.rank)), device=device), persistent=True)
self.register_buffer("V", torch.empty(self.swap((fan_in, self.rank)), device=device), persistent=True)
self.M_xs = nn.Parameter(torch.zeros(self.rank, self.rank, dtype=dtype, device=device))
self._norm_dim = 1
if svd_bases is not None:
self.U.copy_(svd_bases["U"][:, :self.rank].to(dtype))
self.V.copy_(svd_bases["V"][:, :self.rank].to(dtype))
if W0 is not None:
W0_2d = W0.view(W0.shape[0], -1)
if svd_bases is None:
U_full, S, Vh_full = torch.linalg.svd(W0_2d.cpu().float(), full_matrices=False)
U_full, Vh_full = _canonicalize_svd_signs(U_full, Vh_full)
self.U.copy_(U_full[:, :self.rank].to(dtype))
self.V.copy_(Vh_full[:self.rank, :].T.to(dtype))
self.magnitude = nn.Parameter(W0_2d.norm(dim=self._norm_dim).to(dtype))
else:
self.magnitude = nn.Parameter(torch.ones(fan_out, dtype=dtype, device=device))
self.forward_fn = self.dora_xs_forward
self._adapter_forward_fn = self.forward_fn
elif self.adapter_type == "dora-cols-xs":
# DoRA-cols + LoRA-XS: frozen SVD bases + trainable M_xs core + column magnitude
self.register_buffer("U", torch.empty(self.swap((fan_out, self.rank)), device=device), persistent=True)
self.register_buffer("V", torch.empty(self.swap((fan_in, self.rank)), device=device), persistent=True)
self.M_xs = nn.Parameter(torch.zeros(self.rank, self.rank, dtype=dtype, device=device))
self._norm_dim = 0
if svd_bases is not None:
self.U.copy_(svd_bases["U"][:, :self.rank].to(dtype))
self.V.copy_(svd_bases["V"][:, :self.rank].to(dtype))
if W0 is not None:
W0_2d = W0.view(W0.shape[0], -1)
if svd_bases is None:
U_full, S, Vh_full = torch.linalg.svd(W0_2d.cpu().float(), full_matrices=False)
U_full, Vh_full = _canonicalize_svd_signs(U_full, Vh_full)
self.U.copy_(U_full[:, :self.rank].to(dtype))
self.V.copy_(Vh_full[:self.rank, :].T.to(dtype))
self.magnitude = nn.Parameter(W0_2d.norm(dim=self._norm_dim).to(dtype))
else:
self.magnitude = nn.Parameter(torch.ones(fan_in, dtype=dtype, device=device))
self.forward_fn = self.dora_xs_forward
self._adapter_forward_fn = self.forward_fn
elif self.adapter_type == "bora-xs":
# BoRA + LoRA-XS: frozen SVD bases + trainable M_xs core + row & column magnitudes
self.register_buffer("U", torch.empty(self.swap((fan_out, self.rank)), device=device), persistent=True)
self.register_buffer("V", torch.empty(self.swap((fan_in, self.rank)), device=device), persistent=True)
self.M_xs = nn.Parameter(torch.zeros(self.rank, self.rank, dtype=dtype, device=device))
if svd_bases is not None:
self.U.copy_(svd_bases["U"][:, :self.rank].to(dtype))
self.V.copy_(svd_bases["V"][:, :self.rank].to(dtype))
if W0 is not None:
W0_2d = W0.view(W0.shape[0], -1)
if svd_bases is None:
U_full, S, Vh_full = torch.linalg.svd(W0_2d.cpu().float(), full_matrices=False)
U_full, Vh_full = _canonicalize_svd_signs(U_full, Vh_full)
self.U.copy_(U_full[:, :self.rank].to(dtype))
self.V.copy_(Vh_full[:self.rank, :].T.to(dtype))
self.magnitude_r = nn.Parameter(W0_2d.norm(dim=1).to(dtype))
self.magnitude_c = nn.Parameter(W0_2d.norm(dim=0).to(dtype))
else:
self.magnitude_r = nn.Parameter(torch.ones(fan_out, dtype=dtype, device=device))
self.magnitude_c = nn.Parameter(torch.ones(fan_in, dtype=dtype, device=device))
self.forward_fn = self.bora_xs_forward
self._adapter_forward_fn = self.forward_fn
def lora_forward(self, W):
delta = torch.matmul(*self.swap((self.lora_B, self.dropout_fn(self.lora_A)))).view(W.shape)
delta = self.scaling * self.lora_strength * delta
return (W + delta.to(W.dtype))
def dora_forward(self, W):
if self.lora_strength == 0:
return W
# work in 2D so norm/magnitude ops don't broadcast badly for Conv1d/Conv2d
# Upcast W to LoRA param dtype for stable norm computation, cast result back.
orig_shape = W.shape
W_2d = W.view(W.shape[0], -1).to(self.lora_A.dtype)
# low-rank update on the *direction*
delta = torch.matmul(*self.swap((self.lora_B, self.dropout_fn(self.lora_A))))
V = W_2d + self.scaling * self.lora_strength * delta
# normalize along _norm_dim, then scale by per-element magnitude
V_hat = V / (V.norm(dim=self._norm_dim, keepdim=True) + 1e-12)
return (V_hat * self.magnitude.unsqueeze(self._norm_dim)).view(orig_shape).to(W.dtype)
def bora_forward(self, W):
if self.lora_strength == 0:
return W
orig_shape = W.shape
W_2d = W.view(W.shape[0], -1).to(self.lora_A.dtype)
delta = torch.matmul(*self.swap((self.lora_B, self.dropout_fn(self.lora_A))))
V = W_2d + self.scaling * self.lora_strength * delta
# Row-normalize, then scale by row magnitudes
V_r = V / (V.norm(dim=1, keepdim=True) + 1e-12)
intermediate = self.magnitude_r.unsqueeze(1) * V_r
# Column-normalize, then scale by column magnitudes
H_c = intermediate / (intermediate.norm(dim=0, keepdim=True) + 1e-12)
return (H_c * self.magnitude_c.unsqueeze(0)).view(orig_shape).to(W.dtype)
def lora_xs_forward(self, W):
delta = self.U @ self.M_xs.to(self.U.dtype) @ self.V.T
delta = delta.view(W.shape)
return (W + (self.scaling * self.lora_strength * delta).to(W.dtype))
def dora_xs_forward(self, W):
if self.lora_strength == 0:
return W
orig_shape = W.shape
W_2d = W.view(W.shape[0], -1).to(self.U.dtype)
delta = self.U @ self.M_xs.to(self.U.dtype) @ self.V.T
V = W_2d + self.scaling * self.lora_strength * delta
V_hat = V / (V.norm(dim=self._norm_dim, keepdim=True) + 1e-12)
return (V_hat * self.magnitude.unsqueeze(self._norm_dim)).view(orig_shape).to(W.dtype)
def bora_xs_forward(self, W):
if self.lora_strength == 0:
return W
orig_shape = W.shape
W_2d = W.view(W.shape[0], -1).to(self.U.dtype)
delta = self.U @ self.M_xs.to(self.U.dtype) @ self.V.T
V = W_2d + self.scaling * self.lora_strength * delta
V_r = V / (V.norm(dim=1, keepdim=True) + 1e-12)
intermediate = self.magnitude_r.unsqueeze(1) * V_r
H_c = intermediate / (intermediate.norm(dim=0, keepdim=True) + 1e-12)
return (H_c * self.magnitude_c.unsqueeze(0)).view(orig_shape).to(W.dtype)
def forward(self, X):
return self.forward_fn(X)
def _dropout(self, A):
# to mimic the original implementation: A @ dropout(x), we do (A * dropout(ones)) @ x
return A * self.lora_dropout(self.lora_dropout_mask)
def disable_lora(self):
self.forward_fn = lambda x: x
def enable_lora(self):
self.forward_fn = self._adapter_forward_fn
@classmethod
def _get_original_weight(cls, layer, attr_name="weight"):
"""Get the true original weight, even when the layer is already parametrized."""
if hasattr(layer, 'parametrizations') and hasattr(layer.parametrizations, attr_name):
return getattr(layer.parametrizations, attr_name).original.detach()
return getattr(layer, attr_name).detach()
@classmethod
def from_linear(cls, layer, rank=4, lora_dropout_p=0.0, lora_alpha=1, svd_bases=None, **kwargs):
fan_out, fan_in = layer.weight.shape
W0 = cls._get_original_weight(layer)
return cls(
fan_in, fan_out, fan_in_fan_out=False, rank=rank, lora_dropout_p=lora_dropout_p, lora_alpha=lora_alpha,
W0=W0, svd_bases=svd_bases, **kwargs
)
@classmethod
def from_conv2d(cls, layer, rank=4, lora_dropout_p=0.0, lora_alpha=1, svd_bases=None, **kwargs):
fan_out, fan_in = layer.weight.view(layer.weight.shape[0], -1).shape
W0 = cls._get_original_weight(layer).view(layer.weight.shape[0], -1)
return cls(
fan_in, fan_out, fan_in_fan_out=False, rank=rank, lora_dropout_p=lora_dropout_p, lora_alpha=lora_alpha,
W0=W0, svd_bases=svd_bases, **kwargs
)
@classmethod
def from_conv1d(cls, layer, rank=4, lora_dropout_p=0.0, lora_alpha=1, svd_bases=None, **kwargs):
fan_out, fan_in = layer.weight.view(layer.weight.shape[0], -1).shape
W0 = cls._get_original_weight(layer).view(layer.weight.shape[0], -1)
return cls(
fan_in, fan_out, fan_in_fan_out=False, rank=rank, lora_dropout_p=lora_dropout_p, lora_alpha=lora_alpha,
W0=W0, svd_bases=svd_bases, **kwargs
)
@classmethod
def from_embedding(cls, layer, rank=4, lora_dropout_p=0.0, lora_alpha=1, svd_bases=None, **kwargs):
fan_in, fan_out = layer.weight.shape
W0 = cls._get_original_weight(layer).view(layer.weight.shape[0], -1)
return cls(
fan_in, fan_out, fan_in_fan_out=True, rank=rank, lora_dropout_p=lora_dropout_p, lora_alpha=lora_alpha,
W0=W0, svd_bases=svd_bases, **kwargs
)
default_lora_config = { # specify which layers to add lora to, by default only add to linear layers
nn.Linear: {
"weight": partial(LoRAParametrization.from_linear, rank=4),
},
}
# --- layer name filtering helpers ---
def _expand(s):
"""Expand bracket notation in layer filter strings, e.g. 'layers[0-5]' -> ['layers0', ..., 'layers5']."""
t = re.split(r'\[(\d+)-(\d+)\]', s)
if len(t) == 1:
return [s]
lits, starts, ends = t[0::3], t[1::3], t[2::3]
pools = []
for a, b in zip(starts, ends):
ai, bi = int(a), int(b)
step = 1 if bi >= ai else -1
pools.append([str(n) for n in range(ai, bi + step, step)])
out = []
for combo in product(*pools):
pieces = []
for i, lit in enumerate(lits):
pieces.append(lit)
if i < len(combo):
pieces.append(combo[i])
out.append(''.join(pieces))
return out
def _matches_any(name, patterns):
"""Check if name contains any pattern substring (with bracket expansion)."""
if not patterns:
return False
for pattern in patterns:
for expanded in _expand(pattern.strip()):
if expanded in name:
return True
return False
# --- core LoRA application ---
def _match_layer_type(layer, lora_config):
"""Find the matching lora_config key for a layer, using isinstance to handle ParametrizedLinear etc."""
for layer_type in lora_config:
if isinstance(layer, layer_type):
return layer_type
return None
def apply_lora(layer, register=True, merge=False, lora_config=default_lora_config):
"""add lora parametrization to a layer, designed to be used with model.apply"""
if register:
matched_type = _match_layer_type(layer, lora_config)
if matched_type is not None:
for attr_name, parametrization in lora_config[matched_type].items():
parametrize.register_parametrization(layer, attr_name, parametrization(layer), unsafe=True)
else: # this will remove all parametrizations, use with caution
if hasattr(layer, "parametrizations"):
for attr_name in list(layer.parametrizations.keys()):
parametrize.remove_parametrizations(layer, attr_name, leave_parametrized=merge)
def add_lora(model, lora_config=default_lora_config, include=None, exclude=None, svd_bases=None):
"""Add LoRA parametrization to layers in a model.
Args:
model: The model to add LoRA to.
lora_config: Dict mapping nn.Module types to parametrization configs.
include: Optional list of substrings. If provided, only modules whose
name contains at least one pattern get LoRA. Supports bracket
expansion (e.g. "layers[0-11]").
exclude: Optional list of substrings. Modules matching any pattern are
skipped even if they match an include pattern.
svd_bases: Optional dict of pre-computed SVD bases keyed by
"layer_name.weight". When provided, -XS adapter types use
these instead of computing SVD.
"""
if svd_bases is not None or include is not None or exclude is not None:
# Named-module path: iterate by name so we can look up SVD bases per layer
n_cached = 0
n_recomputed = 0
for name, module in model.named_modules():
matched_type = _match_layer_type(module, lora_config)
if matched_type is None:
continue
if include is not None and not _matches_any(name, include):
continue
if exclude is not None and _matches_any(name, exclude):
continue
for attr_name, parametrization_fn in lora_config[matched_type].items():
layer_bases = None
if svd_bases is not None:
bases_key = f"{name}.{attr_name}"
layer_bases = svd_bases.get(bases_key)
param = parametrization_fn(module, svd_bases=layer_bases)
parametrize.register_parametrization(module, attr_name, param, unsafe=True)
if hasattr(param, 'U'): # -XS layer that needs SVD bases
if layer_bases is not None:
n_cached += 1
else:
n_recomputed += 1
if n_cached > 0:
vprint(f"SVD bases: {n_cached} layers loaded from cache")
if n_recomputed > 0:
vprint(f"SVD bases: {n_recomputed} layers RECOMPUTED (missing from cache)")
if svd_bases is not None and n_cached == 0 and n_recomputed == 0:
vprint("SVD bases: provided but no -XS layers found")
else:
# Fast path: original behavior, no name inspection needed
model.apply(partial(apply_lora, lora_config=lora_config))
def add_lora_by_name(model, target_module_names, lora_config=default_lora_config, svd_bases=None):
"""Add LoRA to specific layers by name. Convenience wrapper around add_lora()."""
add_lora(model, lora_config=lora_config, include=target_module_names, svd_bases=svd_bases)
def merge_lora(model):
"""merge lora parametrization to all layers in a model. This will remove all parametrization"""
model.apply(partial(apply_lora, register=False, merge=True))
def remove_lora(model):
"""remove lora parametrization to all layers in a model. This will remove all parametrization"""
model.apply(partial(apply_lora, register=False, merge=False))
def remove_lora_by_index(model, lora_index):
"""Remove all LoRA parametrizations with a specific lora_index.
For modules where other LoRA entries remain, only the matching entry is deleted.
For modules where the removed entry was the only one, the parametrization is
removed entirely and the weight reverts to its original value.
"""
for name, mod in model.named_modules():
if not hasattr(mod, 'parametrizations'):
continue
for attr in list(mod.parametrizations.keys()):
params = mod.parametrizations[attr]
to_remove = [i for i, p in enumerate(params)
if isinstance(p, LoRAParametrization) and p.lora_index == lora_index]
if not to_remove:
continue
if len(params) == len(to_remove):
# All entries being removed — remove parametrization entirely
parametrize.remove_parametrizations(mod, attr, leave_parametrized=False)
else:
for i in reversed(to_remove):
del params[i]
def set_lora_strength(model, strength: float, lora_index=None):
"""Set lora strength. If lora_index is None, sets all LoRAs. If specified, sets only that index."""
strength = float(strength)
if lora_index is None:
for name, buf in model.named_buffers(recurse=True):
if name.endswith("lora_strength"):
buf.fill_(strength)
else:
for p in _iter_lora_params(model):
if p.lora_index == lora_index:
p.lora_strength.fill_(strength)
def _iter_lora_params(model):
for _, mod in model.named_modules():
plist = getattr(getattr(mod, "parametrizations", None), "weight", None)
if plist is None:
continue
for p in plist:
if isinstance(p, LoRAParametrization):
yield p