Agnes-AI's picture
Add inference code
fc3971e verified
Raw History Blame Contribute Delete
10.5 kB
# Copyright (c) 2026. LoRA adapters for MagiHuman / DAVINCI DiT.
"""Low-rank adapters for DiT linear layers (BaseLinear, NativeMoELinear, nn.Linear).
Scope: the middle DiT only (adapter embedders + TransformerBlock + final heads).
Video VAE / audio VAE / text encoder stay outside training (precomputed latents).
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
from typing import Any, Dict, List, Optional, Sequence, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from .dit_module import BaseLinear, NativeMoELinear
# Module-name suffixes / exact leaf names we wrap when targets="all".
ATTN_MLP_SUFFIXES: Tuple[str, ...] = (
"linear_qkv",
"linear_proj",
"up_gate_proj",
"down_proj",
)
PLAIN_LINEAR_NAMES: Tuple[str, ...] = (
"video_embedder",
"text_embedder",
"audio_embedder",
"final_linear_video",
"final_linear_audio",
)
@dataclass
class LoraConfig:
r: int = 16
alpha: float = 16.0
dropout: float = 0.0
targets: str = "all" # all | attn | attn_mlp | embed_final
"""Which DiT linears to wrap. See TRAIN.md in v8/LoRA."""
def scaling(self) -> float:
return float(self.alpha) / float(max(self.r, 1))
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
def _match_target(name: str, targets: str) -> bool:
leaf = name.rsplit(".", 1)[-1]
t = (targets or "all").strip().lower()
if t == "all":
return leaf in ATTN_MLP_SUFFIXES or leaf in PLAIN_LINEAR_NAMES
if t == "attn":
return leaf in ("linear_qkv", "linear_proj")
if t == "attn_mlp":
return leaf in ATTN_MLP_SUFFIXES
if t in ("embed_final", "heads"):
return leaf in PLAIN_LINEAR_NAMES
raise ValueError(f"Unknown lora targets={targets!r}; use all|attn|attn_mlp|embed_final")
def _init_lora_ab(
in_features: int, out_features: int, r: int, device: torch.device, dtype: torch.dtype
) -> Tuple[nn.Parameter, nn.Parameter]:
# A: (r, in), B: (out, r); B=0 so delta starts at 0.
a = nn.Parameter(torch.empty(r, in_features, device=device, dtype=dtype))
b = nn.Parameter(torch.zeros(out_features, r, device=device, dtype=dtype))
nn.init.kaiming_uniform_(a, a=5**0.5)
return a, b
class LoRAPlainLinear(nn.Module):
"""LoRA around a standard ``nn.Linear`` (adapter embedders / final heads)."""
def __init__(self, base: nn.Linear, *, r: int, alpha: float, dropout: float = 0.0):
super().__init__()
if not isinstance(base, nn.Linear):
raise TypeError(f"LoRAPlainLinear expects nn.Linear, got {type(base)}")
self.base = base
self.r = int(r)
self.alpha = float(alpha)
self.scaling = float(alpha) / float(max(self.r, 1))
self.dropout_p = float(dropout)
for p in self.base.parameters():
p.requires_grad = False
dtype = self.base.weight.dtype
device = self.base.weight.device
self.lora_A, self.lora_B = _init_lora_ab(
self.base.in_features, self.base.out_features, self.r, device, dtype
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
y = self.base(x)
x_d = F.dropout(x, p=self.dropout_p, training=self.training) if self.dropout_p > 0 else x
# Cast LoRA matmul to input dtype for mixed precision.
a = self.lora_A.to(dtype=x_d.dtype)
b = self.lora_B.to(dtype=x_d.dtype)
delta = (x_d @ a.t() @ b.t()) * self.scaling
return y + delta.to(dtype=y.dtype)
class LoRABaseLinear(nn.Module):
"""LoRA around ``BaseLinear`` / ``NativeMoELinear`` (DiT attn + MLP).
For ``num_experts==1``: one (A, B) pair.
For ``NativeMoELinear`` (mm_layers): one (A, B) per modality expert, applied
after ``modality_dispatcher.dispatch``, matching the base forward split.
"""
def __init__(self, base: BaseLinear, *, r: int, alpha: float, dropout: float = 0.0):
super().__init__()
if not isinstance(base, BaseLinear):
raise TypeError(f"LoRABaseLinear expects BaseLinear, got {type(base)}")
self.base = base
self.r = int(r)
self.alpha = float(alpha)
self.scaling = float(alpha) / float(max(self.r, 1))
self.dropout_p = float(dropout)
self.num_experts = int(base.num_experts)
for p in self.base.parameters():
p.requires_grad = False
dtype = self.base.weight.dtype
device = self.base.weight.device
in_f = int(base.in_features)
out_f = int(base.out_features)
if self.num_experts <= 1:
self.lora_A, self.lora_B = _init_lora_ab(in_f, out_f, self.r, device, dtype)
self.lora_A_list = None
self.lora_B_list = None
else:
self.lora_A = None
self.lora_B = None
self.lora_A_list = nn.ParameterList()
self.lora_B_list = nn.ParameterList()
for _ in range(self.num_experts):
a, b = _init_lora_ab(in_f, out_f, self.r, device, dtype)
self.lora_A_list.append(a)
self.lora_B_list.append(b)
def forward(
self,
input: torch.Tensor,
output_dtype: Optional[torch.dtype] = None,
modality_dispatcher: Optional[Any] = None,
) -> torch.Tensor:
y = self.base(input, output_dtype=output_dtype, modality_dispatcher=modality_dispatcher)
if self.num_experts <= 1:
x_d = F.dropout(input, p=self.dropout_p, training=self.training) if self.dropout_p > 0 else input
a = self.lora_A.to(dtype=x_d.dtype)
b = self.lora_B.to(dtype=x_d.dtype)
delta = (x_d @ a.t() @ b.t()) * self.scaling
return y + delta.to(dtype=y.dtype)
if modality_dispatcher is None:
raise RuntimeError("NativeMoELinear LoRA requires modality_dispatcher")
input_list = modality_dispatcher.dispatch(input)
deltas: List[torch.Tensor] = []
for i in range(self.num_experts):
x_i = input_list[i]
if x_i.numel() == 0:
deltas.append(x_i.new_zeros(x_i.shape[0], self.base.out_features))
continue
x_d = F.dropout(x_i, p=self.dropout_p, training=self.training) if self.dropout_p > 0 else x_i
a = self.lora_A_list[i].to(dtype=x_d.dtype)
b = self.lora_B_list[i].to(dtype=x_d.dtype)
deltas.append(((x_d @ a.t() @ b.t()) * self.scaling).to(dtype=y.dtype))
delta = modality_dispatcher.undispatch(*deltas)
return y + delta
def _wrap_module(module: nn.Module, cfg: LoraConfig) -> Optional[nn.Module]:
if isinstance(module, (LoRAPlainLinear, LoRABaseLinear)):
return None # already wrapped
if isinstance(module, BaseLinear):
return LoRABaseLinear(module, r=cfg.r, alpha=cfg.alpha, dropout=cfg.dropout)
if isinstance(module, nn.Linear):
return LoRAPlainLinear(module, r=cfg.r, alpha=cfg.alpha, dropout=cfg.dropout)
return None
def inject_lora(model: nn.Module, cfg: LoraConfig) -> List[str]:
"""Replace matching linear modules in-place. Returns list of wrapped paths."""
if int(cfg.r) <= 0:
raise ValueError(f"lora r must be > 0, got {cfg.r}")
to_wrap: List[Tuple[str, nn.Module]] = []
for name, module in model.named_modules():
if not name:
continue
if not _match_target(name, cfg.targets):
continue
if isinstance(module, (LoRAPlainLinear, LoRABaseLinear)):
continue
if isinstance(module, (BaseLinear, nn.Linear)):
to_wrap.append((name, module))
wrapped: List[str] = []
for name, module in to_wrap:
new_mod = _wrap_module(module, cfg)
if new_mod is None:
continue
parent = model
parts = name.split(".")
for p in parts[:-1]:
parent = getattr(parent, p)
setattr(parent, parts[-1], new_mod)
wrapped.append(name)
if not wrapped:
raise RuntimeError(f"inject_lora: no modules matched targets={cfg.targets!r}")
return wrapped
def freeze_non_lora_parameters(model: nn.Module) -> Tuple[int, int]:
"""Freeze everything except LoRA A/B. Returns (trainable_numel, frozen_numel)."""
n_train, n_freeze = 0, 0
for name, p in model.named_parameters():
is_lora = ("lora_A" in name) or ("lora_B" in name) or ("lora_A_list" in name) or ("lora_B_list" in name)
if is_lora:
p.requires_grad = True
n_train += p.numel()
else:
p.requires_grad = False
n_freeze += p.numel()
return n_train, n_freeze
def lora_state_dict(model: nn.Module) -> Dict[str, torch.Tensor]:
out: Dict[str, torch.Tensor] = {}
for name, p in model.named_parameters():
if ("lora_A" in name) or ("lora_B" in name):
out[name] = p.detach().cpu()
return out
def load_lora_state_dict(model: nn.Module, state: Dict[str, torch.Tensor], strict: bool = True) -> None:
missing, unexpected = [], []
own = {n: p for n, p in model.named_parameters() if ("lora_A" in n) or ("lora_B" in n)}
for k, v in state.items():
if k not in own:
unexpected.append(k)
continue
if tuple(own[k].shape) != tuple(v.shape):
raise RuntimeError(f"LoRA shape mismatch for {k}: ckpt {tuple(v.shape)} vs model {tuple(own[k].shape)}")
own[k].data.copy_(v.to(device=own[k].device, dtype=own[k].dtype))
for k in own:
if k not in state:
missing.append(k)
if strict and (missing or unexpected):
raise RuntimeError(f"load_lora_state_dict strict fail missing={missing[:8]} unexpected={unexpected[:8]}")
def mark_lora_text_param_ids(model: nn.Module) -> set:
"""Parameter ids under adapter.text_embedder LoRA (for text_lr_mult group)."""
ids: set = set()
for name, p in model.named_parameters():
if "adapter.text_embedder" in name and (("lora_A" in name) or ("lora_B" in name)):
ids.add(id(p))
return ids
def summarize_lora(model: nn.Module, wrapped: Sequence[str]) -> str:
n_train, n_freeze = 0, 0
for name, p in model.named_parameters():
if p.requires_grad:
n_train += p.numel()
else:
n_freeze += p.numel()
return (
f"lora_wrapped={len(wrapped)} trainable={n_train / 1e6:.3f}M "
f"frozen={n_freeze / 1e6:.1f}M sample={list(wrapped)[:6]}"
)