【ML笔记】论文阅读 - LoRASculpt-Sculpting LoRA for Harmonizing General and Specialized

基本信息

  • 论文标题 - LoRASculpt: Sculpting LoRA for Harmonizing General and Specialized
    Knowledge in Multimodal Large Language Models
  • 作者单位 - National Engineering Research Center for Multimedia Software, School of Computer Science, Wuhan University

背景和原因

传统LoRA在参数微调的时候会引入很多无关参数,可能会造成灾难性遗忘,即遗忘模型在预训练阶段学习的知识,并且会降低执行下游任务的能力。原因在于LoRA在微调原模型的低秩矩阵的时候可能会覆盖掉关键参数,这也是遗忘产生的原因。

方案设计分析

可能的解决方向

  • 减小LoRA的本身的参数更改规模 - 使LoRA稀疏化 - $1
  • 使LoRA尽可能避免更改到关键参数 - 需要诱导训练避开关键参数 - $2

由解决方向衍生出的问题

  • $1.1 - LoRA引入的两个可训练矩阵可以是稀疏矩阵,但是其相乘后有还是不是稀疏矩阵?
  • $1.2 - 如何对LoRA进行稀疏化
  • $1.3 - 对LoRA进行稀疏化之后如何尽量不影响到下游知识
  • $2.1 - 如何定义原模型中的关键参数?
  • $2.2 - 如何找到原模型中的关键参数?
  • $2.3 -如何诱导LoRA微调的时候避开关键参数?

解决衍生问题

  • $1.1 - 可以通过公式算出在LoRA可训练矩阵秩小与稀疏程度低的情况下,两个矩阵乘积的矩阵的矩阵的稀疏度依然很小,且公式算出的结果与实际结果的相差很小
  • $1.2 - 因为LoRA矩阵影响的程度较小,且为了减轻计算压力,直接使用剪枝将LoRA矩阵中绝对值小的参数去除
  • $1.3 - 通过调整稀疏度来调整LoRA的剪枝程度
  • $2.1 - 可以通过两种方法,一种是判断该参数对于梯度减小的贡献,另一种是直接判断参数的绝对值大小。前者更加准确但是计算压力较大,后者计算压力小很多,考虑到LoRA的初衷是为了减小微调的计算压力,故选择后者
  • $2.2 - 使用基于幅值的保留掩码 (Magnitude-Guided Retention Mask),根据$2.1的规则找到参数贡献大的值,然后生成类似于bitmap的参数地图
  • $2.3 - 使用冲突缓解正则项 (Conflict Mitigation Regularizer),来对损失函数进行修补,加入$2.2中的损失,从而诱导LoRA在训练原矩阵时不要更新到关键参数(LoRA的训练结果)

具体的设计方案

  • 从原有模型中获取大权重参数,并且绘制出保留掩码,使用基于此的冲突缓解正则项来对损失函数进行修补
  • 使用修补过后的损失函数来训练LoRA原始矩阵,使其在更新原始模型的低秩矩阵的时候尽量不更新到原有的大权重参数
  • 将训练好的LoRA矩阵使用LoRA稀疏化方法进行剪枝,去除一部分参数,减少LoRA对原始模型影响
  • 然后进行正常的LoRA微调

一些细节

  • 由于训练的是MLLM,因此模型由ViT与LLM两部分组成,需要确保连接两部分的connector受到的影响最小,因此不选用剪枝的方法,而是选用正则化

实验效果

论文中LoRA的训练方法有创新,且训练的对象分为MLLM中的connector部分和LLM部分,尝试优化的问题有灾难性遗忘(遗忘预训练的知识)以及下游知识的运用能力,因此对照组有以下几组

  • 不同的秩的大小
  • 是否使用了论文的方法训练整体
  • 是否使用了论文的方法训练connector
  • 是否使用论文的方法训练LLM
  • 训练之后的上游能力是否保留
  • 训练之后的下游能力是否提高

实验结果显示,均有提升。

  • 秩越大,提升的效果越明显 - 推测因为LoRA在稀疏化过程中起到了更好的效果
  • 用论文中的方法训练connector的方法比训练LLM的提升效果更好 - 推测connector的规模比LLM小得多,因此受到LoRA的影响更大;connector负责连接视觉部分和语言部分,因此起到的影响效果更大,会同时影响两个部分。

实验复现 & 代码解读

由于py的版本控制太过于拧巴,使得复现的过程比较曲折🤯

部署环境

由于本实验的的运行平台是四路4090,首先尝试单卡4090。配置环境时,出现了经典py的版本依赖冲突,经过试错,发现是llava太久没更新了,flash-attn只能用版本2.7.3(文档中没有给出具体版本,默认最新),peft在配置文件中也没有约束版本,经过实验发现0.7.1版本可以正常使用。
环境配置完成,由于单卡在使用DDP的时候,会出现模型的重复调用和注入的错误,如下:

1
2
RuntimeError: Expected to mark a variable ready only once. This error is caused by one of the following reasons: 1) Use of a module parameter outside the `forward` function. Please make sure model parameters are not shared across multiple concurrent forward-backward passes. or try to use _set_static_graph() as a workaround if this module graph does not change during training loop.2) Reused parameters in multiple reentrant backward passes. For example, if you use multiple `checkpoint` functions to wrap the same part of your model, it would result in the same set of parameters been used by different reentrant backward passes multiple times, and hence marking a variable ready multiple times. DDP does not support such use cases in default. You can try to use _set_static_graph() as a workaround if your module graph does not change over iterations.
Parameter at index 447 with name base_model.model.model.layers.31.mlp.down_proj.lora_B.default.weight has been marked as ready twice. This means that multiple autograd engine hooks have fired for this particular parameter during this iteration.

因此上多卡了,但是先没用4090,用的A4000,但是爆显存

遂上双卡4090。但是没有办法获取到deepspeed的zero2.json,所以在实际训练中没有使用。随后终于正常开始训练

训练

跑到一半LoRA注入失败,错误如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Traceback (most recent call last):
File "/home/featurize/work/LoRASculpt/llava/train/train_mem.py", line 4, in <module>
train(attn_implementation="flash_attention_2")
File "/home/featurize/work/LoRASculpt/llava/train/train.py", line 986, in train
trainer.train()
File "/environment/miniconda3/envs/lorasculpt/lib/python3.10/site-packages/transformers/trainer.py", line 1539, in train
return inner_training_loop(
File "/home/featurize/work/LoRASculpt/llava/train/LoRASculpt_Trainer.py", line 531, in _inner_training_loop
lora_param = (safe_get_full_fp32_param(param)).clone()
AttributeError: 'NoneType' object has no attribute 'clone'
Traceback (most recent call last):
File "/home/featurize/work/LoRASculpt/llava/train/train_mem.py", line 4, in <module>
train(attn_implementation="flash_attention_2")
File "/home/featurize/work/LoRASculpt/llava/train/train.py", line 986, in train
trainer.train()
File "/environment/miniconda3/envs/lorasculpt/lib/python3.10/site-packages/transformers/trainer.py", line 1539, in train
return inner_training_loop(
File "/home/featurize/work/LoRASculpt/llava/train/LoRASculpt_Trainer.py", line 531, in _inner_training_loop
lora_param = (safe_get_full_fp32_param(param)).clone()
AttributeError: 'NoneType' object has no attribute 'clone'

查看配置发现LoRA的秩为64,疑似双卡4090吃不消,先下调到8,将LoRA的alpha同步调整为16,再次训练,发现依然出错,于是增加空值检测,增加代码的健壮性,并恢复32的LoRA秩

1
2
3
4
5
fp32_param = safe_get_full_fp32_param(param)
if fp32_param is None:
print("Skip a param with None fp32_param:", param)
continue
lora_param = fp32_param.clone()


跳过了有问题的LoRA参数,顺利继续训练。*

训练完成

1
2
3
4
{'train_runtime': 4772.0564, 'train_samples_per_second': 6.287, 'train_steps_per_second': 0.786, 'train_loss': 0.24712346842542562, 'epoch': 3.0}  
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████| 3750/3750 [1:19:31<00:00, 1.27s/it]
Some non-default generation parameters are set in the model config. These should go into a GenerationConfig file (https://huggingface.co/docs/transformers/generation_strategies#save-a-custom-decoding-strategy-with-your-model) instead. This warning will be raised to an exception in v4.41.
Non-default generation parameters: {'max_length': 4096}

评估

评估模型的过程也很波折,首先是由于transformertokenizer的版本冲突问题,使用AutoModelForCausalLM来导入llava模型失败,改用llava自己的LlavaLlamaForCausalLM来导入,随后if 'llava' in model_name.lower():判断失效,改成if 'llava' in model_name.lower() or 'llava' not in model_name.lower():
然后魔改强制加载image_processor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
print("START LOAD IMAGE PROCESSOR")
mm_use_im_start_end = getattr(model.config, "mm_use_im_start_end", False)
mm_use_im_patch_token = getattr(model.config, "mm_use_im_patch_token", True)
if mm_use_im_patch_token:
tokenizer.add_tokens([DEFAULT_IMAGE_PATCH_TOKEN], special_tokens=True)
if mm_use_im_start_end:
tokenizer.add_tokens([DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN], special_tokens=True)
model.resize_token_embeddings(len(tokenizer))

print("START LOAD VISION TOWER")
vision_tower = model.get_vision_tower()
if not vision_tower.is_loaded:
vision_tower.load_model(device_map=device_map)
if device_map != 'auto':
vision_tower.to(device=device_map, dtype=torch.float16)
image_processor = vision_tower.image_processor
print(f"LOADED IMAGE PROCESSOR: {image_processor}")

随后删除一段不知道为什么出现的image_processor = None,开始评估工作

结果

得到训练后上游的iconqa数据集的结果如下

得到训练后下游textvqa数据集的结果如下

文件夹结构如下,压缩包在附录中会有

想法

主要是基于connector和低秩矩阵的想法。从论文中可以得到,MLLM实际上是由视觉理解部分,LLM部分,以及连接这两部分的connector组成的。然而虽然是分开的组件,但是由于被集成到了一个模型中,因此事实上并没有成为模块而存在。

因此有没有可能将connector统一,然后将ViT和LLM进行拆分训练,组装,从而实现组件之间的解耦。比如,一个原本不支持视觉的纯文本LLM,通过一个统一的connector连接到一个视觉组件,从而实现了视觉感应。原来的LLM并不支持function calling,但是通过一个统一的接口连接到一个function calling的模块,从而支持了MCP等函数调用功能。有点类似于计算机组成中的输入单元,处理单元,输出单元,未来还可以开发更多单元,比如记忆单元等,从而达到类似于计算机效果。

但是,这样并不符合事实上的生物脑结构,毕竟根本上神经网络研究模拟的是生物脑。生物脑中并没有事实上清晰的模块分割,都是抽象出来的。所以可以通过训练的方法将不同的模块进行融合,来达到超级模型的效果,但是只需要融合连接器部分,从而大大减小计算压力。


【ML笔记】论文阅读 - LoRASculpt-Sculpting LoRA for Harmonizing General and Specialized
https://study.0x535a.cn/ml-note/ml-paper-read-lorasculpt/
Author
Stephen Zeng
Posted on
October 12, 2025
Licensed under