Files
2026-07-13 12:40:42 +08:00

449 lines
16 KiB
Python
Raw Permalink 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.
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import math
import re
import numpy as np
import paddle
import paddle.nn.functional as F
from paddle import nn
class LoRALinear(nn.Linear):
# LoRA implemented in a dense layer
def __init__(
self,
in_features: int,
out_features: int,
r: int = 0,
lora_alpha: int = 1,
lora_dropout: float = 0.0,
use_quick_lora: bool = False,
rslora: bool = False,
lora_plus_scale: float = 1.0,
pissa: bool = False,
lora_use_mixer: bool = False,
use_mora: bool = False,
**kwargs,
):
nn.Linear.__init__(self, in_features, out_features, **kwargs)
if not isinstance(r, int) or r <= 0:
raise ValueError("Lora rank r should be a positive integer")
self.use_mora = use_mora
self.r = r
self.lora_alpha = lora_alpha
# Optional dropout
if lora_dropout > 0.0:
self.lora_dropout = nn.Dropout(p=lora_dropout)
else:
self.lora_dropout = lambda x: x
# Mark the weight as unmerged
self.merged = False
self.pissa = pissa
self.lora_use_mixer = lora_use_mixer
# Actual trainable parameters
if use_mora: # reset the rank and create high rank matrix
self.in_features = in_features
self.out_features = out_features
new_r = int(math.sqrt((in_features + out_features) * r) + 0.5)
new_r = new_r // 2 * 2
self.r = new_r
self.lora_A = self.create_parameter(
shape=[self.r, self.r],
dtype=self._dtype,
is_bias=False,
default_initializer=nn.initializer.Constant(value=0.0),
)
self.cos = None
self.sin = None
# Count the number of tiles
self.rb1 = (
self.in_features // self.r
if self.in_features % self.r == 0
else self.in_features // self.r + 1
)
self.rb2 = (
self.out_features // self.r
if self.out_features % self.r == 0
else self.out_features // self.r + 1
)
self.rope_init()
else:
self.lora_A = self.create_parameter(
shape=[in_features, r],
dtype=self._dtype,
is_bias=False,
)
if self.lora_use_mixer:
self.lora_AB = self.create_parameter(
shape=[r, r],
dtype=self._dtype,
is_bias=False,
)
self.lora_B = self.create_parameter(
shape=[r, out_features],
dtype=self._dtype,
is_bias=False,
attr=paddle.ParamAttr(
initializer=paddle.nn.initializer.Constant(value=0.0),
learning_rate=lora_plus_scale,
),
)
self.apply_pissa = False
if use_mora or pissa:
self.scaling = 1.0
elif not rslora:
self.scaling = self.lora_alpha / self.r
else:
self.scaling = self.lora_alpha / math.sqrt(self.r)
# Freezing the pre-trained weight matrix
self.weight.stop_gradient = True
self._use_quick_lora = use_quick_lora and lora_dropout == 0.0
self.disable_lora = False
def pissa_init(self, rank):
weight = self.weight
dtype = weight.dtype
if dtype != paddle.float32:
weight = weight.astype(paddle.float32)
U, S, Vh = paddle.linalg.svd(weight.data, full_matrices=False)
Ur = U[:, :rank]
Sr = S[:rank]
Vhr = Vh[:rank]
lora_A = Ur @ paddle.diag(paddle.sqrt(Sr))
lora_B = paddle.diag(paddle.sqrt(Sr)) @ Vhr
self.lora_A.set_value(lora_A.astype(dtype))
self.lora_B.set_value(lora_B.astype(dtype))
res = weight.data - lora_A @ lora_B
weight = res.astype(dtype)
self.weight.set_value(weight)
def rope_init(self):
if self.cos is None or self.sin is None:
inv_freq = 1.0 / (
10000
** (paddle.arange(0, self.r, 2, dtype=paddle.float32) / self.r)
)
t = paddle.arange(self.rb1, dtype=paddle.float32)
freqs = t.unsqueeze(1) @ inv_freq.unsqueeze(0)
emb = paddle.concat([freqs, freqs], axis=-1)
self.cos = paddle.unsqueeze(paddle.cos(emb), axis=0).astype(
self._dtype
)
self.sin = paddle.unsqueeze(paddle.sin(emb), axis=0).astype(
self._dtype
)
@property
def use_quick_lora(self):
return self._use_quick_lora and self.training and not self.merged
def _apply_mora(self, x):
r = self.r
# Calculate grouping
sum_inter = self.in_features // r
# padding
if self.in_features % r != 0:
pad_size = r - self.in_features % r
x = paddle.concat([x, x[..., :pad_size]], axis=-1)
sum_inter += 1
# reshape the input to apply RoPE
in_x = x.reshape([*x.shape[:-1], sum_inter, r])
# apply RoPE rotation
rh_in_x = paddle.concat(
[-in_x[..., r // 2 :], in_x[..., : r // 2]], axis=-1
)
in_x = in_x * self.cos + rh_in_x * self.sin
# matmul with high rank matrix
out_x = in_x @ self.lora_A
# reshape the output
out_x = out_x.reshape([*x.shape[:-1], -1])[..., : self.out_features]
if out_x.shape[-1] < self.out_features:
repeat_time = self.out_features // out_x.shape[-1]
if self.out_features % out_x.shape[-1] != 0:
repeat_time += 1
out_x = paddle.concat([out_x] * repeat_time, axis=-1)[
..., : self.out_features
]
return out_x
def get_delta_weight(self, lora_A=None, lora_B=None, lora_AB=None):
# compute the delta weightwhich is used to merge weights
if self.lora_use_mixer:
lora_A = lora_A if lora_A is not None else self.lora_A
lora_B = lora_B if lora_B is not None else self.lora_B
lora_AB = lora_AB if lora_AB is not None else self.lora_AB
delta_weight = lora_A @ lora_AB @ lora_B * self.scaling
elif self.use_mora:
lora_A = lora_A if lora_A is not None else self.lora_A
r = self.r
# compute padding
pad_size = (
r - self.in_features % r if self.in_features % r != 0 else 0
)
# initialize weights
w = paddle.zeros(
[self.in_features + pad_size, self.in_features],
dtype=lora_A.dtype,
)
# create the weights after rotation
aw2 = paddle.concat(
[lora_A[:, r // 2 :], -lora_A[:, : r // 2]], axis=-1
)
# apply RoPE
for i in range(self.rb1 - 1):
w[i * r : (i + 1) * r, i * r : (i + 1) * r] = (
aw2 * self.sin[:, i] + lora_A * self.cos[:, i]
)
# Process the last chunk that may be incomplete
i = self.rb1 - 1
w[i * r :, i * r :] = (
aw2 * self.sin[:, i] + lora_A * self.cos[:, i]
)[:, : r - pad_size]
# padding
if pad_size > 0:
w[i * r :, :pad_size] = (
aw2 * self.sin[:, i] + lora_A * self.cos[:, i]
)[:, r - pad_size :]
# reshape the weights
if self.in_features < self.out_features:
w = paddle.concat([w] * self.rb2, axis=0)[: self.out_features]
else:
w = w[: self.out_features]
final_weight = w
delta_weight = final_weight.T
else:
lora_A = lora_A if lora_A is not None else self.lora_A
lora_B = lora_B if lora_B is not None else self.lora_B
delta_weight = lora_A @ lora_B * self.scaling
return delta_weight
def merge(self):
if not self.merged:
delta_weight = self.get_delta_weight()
new_weight = self.weight + delta_weight
self.weight.set_value(new_weight)
self.merged = True
def unmerge(self):
if self.merged:
delta_weight = self.get_delta_weight()
new_weight = self.weight - delta_weight
self.weight.set_value(new_weight)
self.merged = False
def forward(self, input: paddle.Tensor, *args, **kwargs):
if not self.apply_pissa and self.pissa:
self.pissa_init(self.r)
self.apply_pissa = True
if self.disable_lora or self.merged:
result = F.linear(
x=input, weight=self.weight, bias=self.bias, name=self.name
)
elif self.use_mora:
result = F.linear(
x=input, weight=self.weight, bias=self.bias, name=self.name
)
input = self.lora_dropout(input)
mora_out = self._apply_mora(input)
result += mora_out
else:
result = F.linear(
x=input, weight=self.weight, bias=self.bias, name=self.name
)
if self.lora_use_mixer:
result += (
self.lora_dropout(input)
@ self.lora_A
@ self.lora_AB
@ self.lora_B
) * self.scaling
else:
result += (
self.lora_dropout(input) @ self.lora_A @ self.lora_B
) * self.scaling
return result
def extra_repr(self):
name = f", name={self.name}" if self.name else ""
return f"in_features={self.weight.shape[0]}, out_features={self.weight.shape[1]}, rank={self.r}{name}"
lora_layers = {
"LoRALinear": LoRALinear,
}
LoRALinear = lora_layers["LoRALinear"]
AVAILABLE_LAYERS = [
LoRALinear,
]
class LoRAModel(nn.Layer):
def __init__(self, model, lora_config) -> None:
super().__init__()
self.model = self.get_lora_model(model, lora_config)
self.lora_config = lora_config
logging.info("Mark only lora and trainable_module as trainable.")
self.mark_only_lora_as_trainable()
def forward(self, input_ids):
return self.model(input_ids)
def _find_and_replace_module(self, model, module_name, lora_config):
parent_module = model
attribute_chain = module_name.split(".")
for name in attribute_chain[:-1]:
parent_module = getattr(parent_module, name)
module = getattr(parent_module, attribute_chain[-1])
lora_module = None
if isinstance(module, nn.Linear):
lora_module = LoRALinear(
in_features=module.weight.shape[0],
out_features=module.weight.shape[1],
r=lora_config.r,
lora_alpha=lora_config.lora_alpha,
lora_dropout=lora_config.lora_dropout,
rslora=lora_config.rslora,
lora_plus_scale=lora_config.lora_plus_scale,
pissa=lora_config.pissa,
bias_attr=False if module.bias is None else None,
use_quick_lora=lora_config.use_quick_lora,
lora_use_mixer=lora_config.lora_use_mixer,
use_mora=lora_config.use_mora,
)
if lora_module is None:
raise ValueError(
f"LoRA strategy only supports paddle.nn.Linear or paddle.distributed.fleet.meta_parallel.ColumnParallelLinear or paddlenlp.transformers.sequence_utils. {module}({module_name} {type(module).__name__}) is not supported。"
)
lora_module.weight = module.weight
if module.bias is not None:
lora_module.bias = module.bias
setattr(parent_module, attribute_chain[-1], lora_module)
def print_trainable_parameters(self) -> None:
freeze_numel = 0
trainable_numel = 0
for _, weight in self.model.state_dict().items():
if weight.stop_gradient:
freeze_numel += np.prod(weight.shape)
else:
trainable_numel += np.prod(weight.shape)
logging.debug(
f"Frozen parameters: {freeze_numel:.2e} || Trainable parameters:{trainable_numel:.2e} || Total parameters:{freeze_numel + trainable_numel:.2e}|| Trainable:{trainable_numel / (freeze_numel + trainable_numel):.2%}"
)
def mark_only_lora_as_trainable(self) -> None:
for _, layer in self.model.named_sublayers():
if isinstance(layer, LoRALinear):
for name, weight in layer.state_dict().items():
if (
self.lora_config.trainable_bias in ["lora", "all"]
and "bias" in name
):
weight.stop_gradient = False
elif "lora" in name:
weight.stop_gradient = False
else:
weight.stop_gradient = True
else:
for name, weight in layer.state_dict().items():
if (
self.lora_config.trainable_bias == "all"
and "bias" in name
):
weight.stop_gradient = False
else:
weight.stop_gradient = True
if self.lora_config.trainable_modules is not None:
for name, weight in self.model.state_dict().items():
if any(
re.fullmatch(trainable_module, name)
for trainable_module in self.lora_config.trainable_modules
):
weight.stop_gradient = False
def get_lora_model(self, model, lora_config):
if lora_config.target_modules is None:
return model
elif isinstance(lora_config.target_modules, str):
target_modules = [lora_config.target_modules]
else:
target_modules = lora_config.target_modules
for target_module in target_modules:
for i in model.named_sublayers():
module_name = i[0]
if re.fullmatch(target_module, module_name):
self._find_and_replace_module(
model, module_name, lora_config
)
return model
def train(self):
self.training = True
self.model.training = True
for layer in self.model.sublayers():
layer.training = True
layer.train()
def eval(self):
self.training = False
self.model.training = False
for layer in self.model.sublayers():
layer.training = False
layer.eval()
def disable_lora(self):
for _, layer in self.model.named_sublayers():
if any(
isinstance(layer, lora_layer) for lora_layer in AVAILABLE_LAYERS
):
layer.disable_lora = True
def enable_lora(self):
for _, layer in self.model.named_sublayers():
if any(
isinstance(layer, lora_layer) for lora_layer in AVAILABLE_LAYERS
):
layer.disable_lora = False
def merge(self):
for _, layer in self.model.named_sublayers():
if any(
isinstance(layer, lora_layer) for lora_layer in AVAILABLE_LAYERS
):
layer.merge()
def unmerge(self):
for _, layer in self.model.named_sublayers():
if any(
isinstance(layer, lora_layer) for lora_layer in AVAILABLE_LAYERS
):
layer.unmerge()