Files
SoundGame/tools/stable-audio-3/stable_audio_3/models/lora/utils.py
T
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

420 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import torch
from safetensors import safe_open
from safetensors.torch import save_file as _st_save_file
from torch import nn
from .model import LoRAParametrization, _expand
from ...verbose import vprint
def apply_to_lora(fn):
"""apply a function to LoRAParametrization layers, designed to be used with model.apply"""
def apply_fn(layer):
if isinstance(layer, LoRAParametrization):
fn(layer)
return apply_fn
def enable_lora(model, lora_index=None):
"""Enable LoRA layers. If lora_index is None, enables all. If specified, enables only that index."""
def _enable(layer):
if isinstance(layer, LoRAParametrization):
if lora_index is None or layer.lora_index == lora_index:
layer.enable_lora()
model.apply(_enable)
def disable_lora(model, lora_index=None):
"""Disable LoRA layers. If lora_index is None, disables all. If specified, disables only that index."""
def _disable(layer):
if isinstance(layer, LoRAParametrization):
if lora_index is None or layer.lora_index == lora_index:
layer.disable_lora()
model.apply(_disable)
def get_lora_layers(model):
layers = []
for name, m in model.named_modules():
plist = getattr(getattr(m, "parametrizations", None), "weight", None)
if plist is None:
continue
for p in plist:
if isinstance(p, LoRAParametrization):
layers.append((name, p))
return layers
def has_lora(model):
"""Return True if the model has at least one LoRAParametrization on any weight."""
return len(get_lora_layers(model))>=1
def filter_lora_layers(model, lora_layer_filter, lora_index=None):
"""Enable/disable LoRA layers by name filter. If lora_index is specified, only affects that index."""
lora_layer_filter = (lora_layer_filter or "").strip()
# lora_layer_filter: logical OR on comma separated substrings
# e.g. ".transformer.layers, .to_global_embed."
def is_filtered(layer_name):
filters = [x.lower().strip() for x in lora_layer_filter.split(",") if len(x)]
for f in filters:
for _f in _expand(f):
if _f in layer_name:
return True
return False
for name, p in get_lora_layers(model):
if lora_index is not None and p.lora_index != lora_index:
continue
if is_filtered(name):
p.disable_lora()
else:
p.enable_lora()
# ------------------- helper function for collecting parameters for training/saving -------------------
def name_is_lora(name):
return (
len(name.split(".")) >= 4
and (name.split(".")[-4]) == "parametrizations"
and name.split(".")[-1] in ["lora_A", "lora_B", "M_xs", "magnitude", "magnitude_r", "magnitude_c"]
)
def name_is_bias(name):
return name.split(".")[-1] == "bias"
def get_params_by_name(model, print_shapes=False, name_filter=None):
for n, p in model.named_parameters():
if name_filter is None or name_filter(n):
if print_shapes:
print(n, p.shape)
yield p
def get_lora_params(model, print_shapes=False):
return get_params_by_name(model, print_shapes=print_shapes, name_filter=name_is_lora)
def get_bias_params(model, print_shapes=False):
return get_params_by_name(model, print_shapes=print_shapes, name_filter=name_is_bias)
def get_lora_state_dict(model):
return {k: v for k, v in model.state_dict().items() if name_is_lora(k)}
def cast_base_to_precision(model, precision):
"""Cast frozen base weights to lower precision, keeping LoRA params in fp32.
Args:
model: Model with LoRA parametrizations applied.
precision: One of "bf16", "bfloat16", "fp16", "float16".
"""
import torch
dtype_map = {"bf16": torch.bfloat16, "bfloat16": torch.bfloat16,
"fp16": torch.float16, "float16": torch.float16}
target_dtype = dtype_map.get(precision)
if target_dtype is None:
print(f"Unknown base_precision={precision!r}, keeping fp32")
return
vprint(f"Casting frozen base weights to {target_dtype}")
model.to(target_dtype)
# Restore LoRA params to fp32 — optimizer needs full precision
n_restored = 0
for p in get_lora_params(model):
p.data = p.data.to(torch.float32)
n_restored += 1
vprint(f" Restored {n_restored} LoRA params to fp32")
# ------------------- helper function for loading LoRa at inference -------------------
def infer_global_rank(lora_sd: dict) -> int:
candidates = []
for k, v in lora_sd.items():
if k.endswith(".lora_A"):
candidates.append(v.shape[0])
elif k.endswith(".lora_B"):
candidates.append(v.shape[1])
elif k.endswith(".M_xs"):
candidates.append(v.shape[0])
if not candidates:
raise ValueError("No LoRA/LoRA-XS tensors found to infer rank")
r = candidates[0]
# sanity check they all match (most checkpoints use one global r)
if any(c != r for c in candidates):
print(f"Multiple ranks detected in ckpt: {sorted(set(candidates))}. "
"You’ll need per-layer ranks.")
return r
def detect_dora_variant(state_dict):
"""Detect DoRA variant from a state dict.
Returns "dora-cols" (dim=0, per-input-feature) or "dora-rows" (dim=1,
per-output-neuron) based on the shape of the first .magnitude tensor
found. Returns None if no magnitude tensors exist or axis is ambiguous.
"""
for key, val in state_dict.items():
if not key.endswith(".magnitude"):
continue
if val.dim() == 2:
if val.shape[0] == 1:
return "dora-cols" # old format: (1, fan_in)
elif val.shape[1] == 1:
return "dora-rows" # (fan_out, 1)
# 1D magnitude — can't determine axis without model context
return None
return None
def prepare_dora_state_dict(state_dict):
"""Squeeze 2D DoRA magnitude tensors to 1D for loading. Modifies in-place."""
for key in list(state_dict.keys()):
if key.endswith(".magnitude") and state_dict[key].dim() == 2:
state_dict[key] = state_dict[key].squeeze()
def resolve_adapter_type(adapter_type, state_dict=None):
"""Resolve legacy "dora" adapter_type to the correct variant.
If adapter_type is "dora" and a checkpoint state_dict is provided,
attempts to detect dora-rows vs dora-cols from magnitude tensor shapes.
Falls back to "dora-rows" (paper-correct default).
All other adapter types are returned unchanged.
"""
if adapter_type != "dora":
return adapter_type
if state_dict is not None:
detected = detect_dora_variant(state_dict)
if detected:
return detected
return "dora-rows"
# ------------------- helper functions for safetensors LoRA checkpoints -------------------
def save_lora_safetensors(state_dict, lora_config, path):
"""Save a LoRA checkpoint in safetensors format with config as metadata.
The lora_config dict is JSON-serialized and stored under the metadata key
``"lora_config"``, which safetensors stores in the file header as a string.
Args:
state_dict: Dict of LoRA tensors (from get_lora_state_dict).
lora_config: Dict with keys like rank, alpha, adapter_type, include, exclude.
path: Output file path (should end in .safetensors).
"""
metadata = {"lora_config": json.dumps(lora_config)}
fp16_dict = {k: v.half() if v.is_floating_point() else v for k, v in state_dict.items()}
_st_save_file(fp16_dict, str(path), metadata=metadata)
def load_lora_checkpoint(path):
"""Load a LoRA checkpoint from either .safetensors or .ckpt format.
Returns:
Tuple of (state_dict, lora_config) where lora_config may be an empty
dict if the checkpoint has no embedded config.
"""
path = str(path)
if path.endswith(".safetensors"):
with safe_open(path, framework="pt", device="cpu") as f:
state_dict = {key: f.get_tensor(key) for key in f.keys()}
metadata = f.metadata()
lora_config = {}
if metadata and "lora_config" in metadata:
lora_config = json.loads(metadata["lora_config"])
return state_dict, lora_config
else:
ckpt = torch.load(path, map_location="cpu", weights_only=False)
state_dict = ckpt["state_dict"]
lora_config = ckpt.get("lora_config", {})
return state_dict, lora_config
def convert_lora_ckpt_to_safetensors(ckpt_path, output_path=None):
"""Convert a .ckpt LoRA checkpoint to .safetensors format.
Args:
ckpt_path: Path to the input .ckpt file.
output_path: Path for the output .safetensors file.
If None, replaces .ckpt extension with .safetensors.
Returns:
The output path.
"""
ckpt_path = str(ckpt_path)
if output_path is None:
if ckpt_path.endswith(".ckpt"):
output_path = ckpt_path[:-5] + ".safetensors"
else:
output_path = ckpt_path + ".safetensors"
state_dict, lora_config = load_lora_checkpoint(ckpt_path)
save_lora_safetensors(state_dict, lora_config, output_path)
return output_path
# ------------------- helper functions for multi-LoRA stacking -------------------
def get_lora_count(model):
"""Return the number of stacked LoRAs (distinct lora_index values)."""
indices = set()
for _, p in get_lora_layers(model):
indices.add(p.lora_index)
return len(indices)
def remap_lora_state_dict(state_dict, target_index):
"""Remap LoRA state dict keys from parametrization index 0 to target_index.
Checkpoints always save keys with index 0 (e.g. *.parametrizations.weight.0.lora_A).
When loading the Nth stacked LoRA, we need to remap to index N.
"""
if target_index == 0:
return state_dict
remapped = {}
for k, v in state_dict.items():
new_key = k.replace(
".parametrizations.weight.0.",
".parametrizations.weight.{}.".format(target_index)
)
remapped[new_key] = v
return remapped
# ------------------- helper function for inferencing with multiple lora -------------------
def _get_adapter_param_names(lora_layer):
"""Return the list of trainable parameter names for this adapter type."""
if lora_layer.adapter_type == "lora":
return ["lora_A", "lora_B"]
elif lora_layer.adapter_type in ("dora", "dora-rows", "dora-cols"):
return ["lora_A", "lora_B", "magnitude"]
elif lora_layer.adapter_type == "bora":
return ["lora_A", "lora_B", "magnitude_r", "magnitude_c"]
elif lora_layer.adapter_type == "lora-xs":
return ["M_xs"]
elif lora_layer.adapter_type in ("dora-rows-xs", "dora-cols-xs"):
return ["M_xs", "magnitude"]
elif lora_layer.adapter_type == "bora-xs":
return ["M_xs", "magnitude_r", "magnitude_c"]
return []
def _prepare_for_multiple_lora(lora_layer):
lora_layer._multi_lora_slots = []
def _append_lora(lora_layer):
slot = {}
for name in _get_adapter_param_names(lora_layer):
slot[name] = nn.Parameter(getattr(lora_layer, name).clone())
lora_layer._multi_lora_slots.append(slot)
def load_multiple_lora(model, lora_state_dicts):
model.apply(apply_to_lora(_prepare_for_multiple_lora))
for state_dict in lora_state_dicts:
_ = model.load_state_dict(state_dict, strict=False)
model.apply(apply_to_lora(_append_lora))
return model
def _select_lora(lora_layer, index):
slot = lora_layer._multi_lora_slots[index]
for name, param in slot.items():
setattr(lora_layer, name, param)
def select_lora(model, index):
model.apply(apply_to_lora(lambda x: _select_lora(x, index)))
return model
# ------------------- helper function for merging multiple LoRAs -------------------
def merge_loras_into_base_model(model, lora_configurations):
"""
Merges multiple LoRAs with specified application weights directly into the base model's weights.
Works with all adapter types (LoRA, DoRA, LoRA-XS) by using the parametrization's own forward.
After merging, the LoRA parametrization on each layer is disabled.
Args:
model: The PyTorch model with LoRAParametrization already applied (via add_lora).
lora_configurations: A list of dicts, each specifying a LoRA to merge:
[
{
'name': 'style1',
'state_dict': loaded_torch_state_dict,
'application_weight': 0.7
},
...
]
"""
import torch
if not lora_configurations:
return
# Collect all parametrized layers with their LoRA instances
parametrized_layers = []
for name, module in model.named_modules():
if not hasattr(module, "parametrizations"):
continue
for attr_name in list(module.parametrizations.keys()):
plist = module.parametrizations[attr_name]
for p in plist:
if isinstance(p, LoRAParametrization):
parametrized_layers.append((name, module, attr_name, p, plist.original))
break
# Initialize per-layer delta accumulators
deltas = {id(orig): torch.zeros_like(orig.data) for _, _, _, _, orig in parametrized_layers}
for lora_cfg in lora_configurations:
lora_sd = lora_cfg['state_dict']
app_weight = lora_cfg['application_weight']
# Load this LoRA's adapter weights into the model's parametrizations
model.load_state_dict(lora_sd, strict=False)
# Compute each layer's delta using the parametrization's own forward
with torch.no_grad():
for name, module, attr_name, lora_p, original in parametrized_layers:
merged = lora_p(original)
deltas[id(original)] += app_weight * (merged - original)
# Apply accumulated deltas to original weights and disable LoRA
with torch.no_grad():
for name, module, attr_name, lora_p, original in parametrized_layers:
original.data.add_(deltas[id(original)])
lora_p.disable_lora()
# ------------------- helper function for tying and untieing weights -------------------
def tie_weights(linear: nn.Linear, embedding: nn.Embedding):
"""tie the weights of the linear layer and the embedding layer both with the same lora"""
lora_p = embedding.parametrizations.weight[0]
if lora_p.adapter_type != "lora":
raise NotImplementedError(f"tie_weights only supports standard LoRA, got adapter_type='{lora_p.adapter_type}'")
# this line below is optional if the original is already tied
embedding.parametrizations.weight.original = linear.parametrizations.weight.original
embedding.parametrizations.weight[0].lora_A = linear.parametrizations.weight[0].lora_B
embedding.parametrizations.weight[0].lora_B = linear.parametrizations.weight[0].lora_A
def untie_weights(linear: nn.Linear, embedding: nn.Embedding):
"""untie the weights of the linear layer and the embedding layer"""
lora_p = embedding.parametrizations.weight[0]
if lora_p.adapter_type != "lora":
raise NotImplementedError(f"untie_weights only supports standard LoRA, got adapter_type='{lora_p.adapter_type}'")
embedding.parametrizations.weight.original = nn.Parameter(embedding.weight.original.clone())
embedding.parametrizations.weight[0].lora_A = nn.Parameter(embedding.parametrizations.weight[0].lora_A.clone())
embedding.parametrizations.weight[0].lora_B = nn.Parameter(embedding.parametrizations.weight[0].lora_B.clone())