【模型学习】LoRA的原理,及deepseek-vl2下LoRA实现

LoRA(Low-Rank Adaptation)是一种针对大型语言模型的微调技术,旨在降低微调过程中的计算和内存需求。其核心思想是通过引入低秩矩阵来近似原始模型的全秩矩阵,从而减少参数数量和计算复杂度。

基本原理

在LoRA中,原始模型的全秩矩阵被分解为低秩矩阵的乘积。

具体来说,对于一个全秩矩阵W,LoRA将其分解为两个低秩矩阵A和B的乘积,即W ≈ A * B。

其中,A和B的秩远小于W的秩,从而显著减少了参数数量。

在这里插入图片描述

其实现流程为:

  • 在原始预训练语言模型旁边增加一个旁路,做降维再升维的操作来模拟内在秩
  • 用**随机高斯分布初始化 A,**用零矩阵初始化B,训练时固定预训练模型的参数,只训练矩阵 A 与矩阵 B
  • 训练完成后,将 B 矩阵与 A 矩阵相乘后合并预训练模型参数作为微调后的模型参数

在这里插入图片描述

W是原始的权重矩阵,A是一个尺寸为dr的矩阵,B是一个尺寸为rd’的矩阵,r是低秩矩阵的秩

通过这种分解,原始矩阵W的更新仅由A和B的乘积决定

LoRA引入了一个缩放因子α,使得更新公式为

在这里插入图片描述

DeepSeek_vl2中LoRA的参数在

class DeepseekV2Config(PretrainedConfig):
        kv_lora_rank=512,  # 键值对LoRA的秩
        q_lora_rank=1536,  # 查询LoRA的秩

LoRA 的核心思想是通过对预训练模型中的特定层进行低秩矩阵插入,实现参数高效微调而无需修改原始权重

对于语言模型,通常选择影响权重更新较大的模块,例如q_projk_proj(负责查询和键的变换),v_proj(值的变换),以及o_proj(输出投影)等。

Rotary Embedding:虽然在一些实现中会对嵌入进行微调,但通常LoRA不会直接用于rotary_emb,因为它通常是固定的。


MLP层中的Gate、Up和Down投影

  • gate_proj:控制门投影 -

  • up_proj:上升投影 -

  • down_proj:下降投影 -

    MLP层的Gate、Up和Down投影通常涉及大量的可训练参数,因此对这些投影进行LoRA微调,可以在不显著增加计算负担的情况下优化模型表现。

    通过低秩适应,LoRA能够在减少参数量的同时,增强模型对复杂模式的适应能力。

    这些曾在处理非线性变换时起到重要作用,通常也是LoRA微调的目标。


在这里插入图片描述

DeepSeek_vl2 LoRA实现

class DeepseekVLV2ForCausalLM(DeepseekVLV2PreTrainedModel):
    def __init__(self, config):
        # 1. 视觉编码器 - SigLIP ViT (无LoRA)
        self.vision = VisionTransformer(...)
        
        # 2. 投影器 - 连接视觉和语言
        self.projector = MlpProjector(...)
        
        # 3. 语言模型 - DeepSeek-V2 (有LoRA)
        self.language = DeepseekV2ForCausalLM(language_config)

LoRA只在语言模型中实现,并且Deepseekvl2的语言模型是用的llama框架(虽然我也没接触过)

Copied from transformers.models.llama.modeling_llama.LlamaAttention with Llama->DeepseekV2

from transformers.models.llama.modeling_llama import (
    LlamaAttention,
    LlamaFlashAttention2
)

ATTENTION_CLASSES = {
    # MLA (Multi-Latent Attention) - DeepSeek-V2的默认注意力
    "eager": DeepseekV2Attention,                    # 标准MLA实现
    "flash_attention_2": DeepseekV2FlashAttention2,  # MLA + Flash Attention 2
    
    # MLA的别名
    "mla_eager": DeepseekV2Attention,
    "mla_flash_attention_2": DeepseekV2FlashAttention2,
    
    # MHA (Multi-Head Attention) - 传统注意力
    "mha_eager": LlamaAttention,                     # 标准MHA实现
    "mha_flash_attention_2": LlamaFlashAttention2   # MHA + Flash Attention 2
}

# 在SS-lora.py中,可以这样配置:
lora_config = {
    'enable_lora': True,
    'q_lora_rank': 256,
    'kv_lora_rank': 128,
    '_attn_implementation': 'flash_attention_2'  # 选择注意力类型
}

# 在模型初始化时
config = DeepseekV2Config(**language_config)
config._attn_implementation = lora_config.get('_attn_implementation', 'eager')

deepseek_vl2下完整的LoRA调用层次

DeepseekVLV2ForCausalLM
├── self.language = DeepseekV2ForCausalLM(language_config)
    ├── DeepseekV2ForCausalLM.__init__(config)
        ├── self.model = DeepseekV2Model(config)
            ├── DeepseekV2Model.__init__(config)
                ├── self.embed_tokens = nn.Embedding(...)
                ├── self.layers = nn.ModuleList([...])
                    ├── DeepseekV2DecoderLayer(config, layer_idx)
                        ├── self.self_attn = ATTENTION_CLASSES[attn_implementation]
                            ├── DeepseekV2Attention(config, layer_idx)  # 包含LoRA
                            │   ├── q_a_proj, q_b_proj (LoRA)
                            │   ├── kv_a_proj, kv_b_proj (LoRA)
                            │   └── 其他注意力组件
                            └── 或者 DeepseekV2FlashAttention2
                        ├── self.mlp = DeepseekV2MLP/MoE
                        └── 层归一化
                └── self.norm = DeepseekV2RMSNorm(...)
        └── self.lm_head = nn.Linear(...)

具体的看下DeepseekV2Attention(config, layer_idx)中的调用代码

class DeepseekV2Attention(nn.Module):
    def __init__(self, config, layer_idx):
        # LoRA参数
        self.q_lora_rank = config.q_lora_rank      # 1536
        self.kv_lora_rank = config.kv_lora_rank    # 512
        
        # LoRA层
        if self.q_lora_rank is not None:
            self.q_a_proj = nn.Linear(hidden_size, q_lora_rank)
            self.q_b_proj = nn.Linear(q_lora_rank, output_dim)
            
         
        self.kv_a_proj_with_mqa = nn.Linear(
            self.hidden_size,
            config.kv_lora_rank + config.qk_rope_head_dim,
            bias=config.attention_bias,
        )
        self.kv_a_layernorm = DeepseekV2RMSNorm(config.kv_lora_rank)
        self.kv_b_proj = nn.Linear(
            config.kv_lora_rank,
            self.num_heads
            * (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim),
            bias=False,
        )   

到这里为止我们也就确定了A和B,但我们需要知道q和KV为什么需要生成两组A,B。缩放因子α和参数r又在代码中哪里体现了

这里进行补充说明理解

本文信息在完整的输入进来

实际上,本文信息所有token都经过相同的输入

hidden_states = self.embed_tokens(input_ids)  # [batch, seq_len, hidden_size]

# 然后通过不同的投影层生成Q、K、V
q = self.q_proj(hidden_states)  # 查询投影
k = self.k_proj(hidden_states)  # 键投影  
v = self.v_proj(hidden_states)  # 值投影

在DeepSeek-VL2中,K和V被合并处理:
# 合并的KV投影 (DeepseekV2Attention中可以找到对应的处理方式)
self.kv_a_proj_with_mqa = nn.Linear(
    self.hidden_size,
    config.kv_lora_rank + config.qk_rope_head_dim,  # 512 + 64 = 576
    bias=config.attention_bias,
)

def forward(self, hidden_states):
    # 1. 生成Q(查询)
    if self.q_lora_rank is None:
        q = self.q_proj(hidden_states)  # 标准投影
    else:
        q = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(hidden_states)))  # LoRA投影
    
    # 2. 生成合并的KV
    compressed_kv = self.kv_a_proj_with_mqa(hidden_states)  # 4096 → 576
    
    # 3. 分离K和V
    compressed_kv, k_pe = torch.split(
        compressed_kv, [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1
    )
    # compressed_kv: [batch, seq_len, 512] - 用于生成K和V
    # k_pe: [batch, seq_len, 64] - RoPE位置编码
    
    # 4. 通过B矩阵生成最终的K和V
    kv_b_proj = self.kv_b_proj.weight.view(self.num_heads, -1, self.kv_lora_rank)
    # 从512维度扩展到最终的K和V维度
    
通过kv_lora_b分离出K和V,也就是B矩阵
k = kv_lora_b_k(compressed_kv) → [batch, seq_len, k_dim]
v = kv_lora_b_v(compressed_kv) → [batch, seq_len, v_dim]

通过以上细读代码,其实我们可以注意到DeepSeek-VL2中的LoRA实现没有显式的缩放因子α

DeepSeek-VL2的LoRA实现是"替换式"而非"加法式"

# 标准LoRA(加法式)
output = original_projection(x) + α * lora_projection(x)

# DeepSeek-VL2 LoRA(替换式)
output = lora_projection(x)  # 直接替换原始投影

对相应的步骤进行简单实现
# 查询LoRA
if self.q_lora_rank is None:
    q = self.q_proj(hidden_states)  # 原始投影
else:
    q = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(hidden_states)))  # LoRA投影

# 键值LoRA
compressed_kv = self.kv_a_proj_with_mqa(hidden_states)
compressed_kv = self.kv_a_layernorm(compressed_kv)

这里的r也对用的表示为LoRA他降维的维度

lora_config = {
    'q_lora_rank': 256,    # r参数
    'kv_lora_rank': 128,   # r参数
    # 没有α参数,因为DeepSeek-VL2不使用
}

# q_lora_rank = 256 表示:
q_a_proj: 4096 → 256    # A矩阵:降维到256
q_b_proj: 256 → 6144    # B矩阵:从256升维到输出维度

# kv_lora_rank = 128 表示:
kv_a_proj: 4096 → 128   # A矩阵:降维到128  
kv_b_proj: 128 → 8192   # B矩阵:从128升维到输出维度

# 原始全连接层
W: [6144, 4096]  # 完整权重矩阵

# LoRA分解
A: [256, 4096]   # r=256,降维矩阵
B: [6144, 256]   # r=256,升维矩阵

# 数学表示
W ≈ B * A  # 低秩近似

总结来说,如果我们在调用DeepSeek_vl2的LoRA时,其实只需要关心Q_r和KV_r这两组参数值就可以了,值得注意的kv_lora_rank的A和B矩阵是架构必需的,q_lora_rank才是可选的!

def _freeze_backbone_parameters(self):
    for name, param in self.backbone.named_parameters():
        # 只训练LoRA相关参数
        if 'q_a_proj' in name or 'q_b_proj' in name or 'kv_a_proj' in name or 'kv_b_proj' in name:
            param.requires_grad = True  # 可训练
        else:
            param.requires_grad = False  # 冻结

# 可训练参数(requires_grad = True)
✅ q_a_proj.weight, q_a_proj.bias
✅ q_b_proj.weight
✅ kv_a_proj.weight, kv_a_proj.bias  # KV的A矩阵
✅ kv_b_proj.weight                   # KV的B矩阵

# 冻结参数(requires_grad = False)
❌ q_proj.weight, q_proj.bias(如果存在)
❌ o_proj.weight, o_proj.bias
❌ vision编码器的所有参数
❌ projector的所有参数
❌ language模型的其他参数

# 在原始DeepSeek-VL2中,所有参数默认都是可训练的:
# ✅ vision编码器的所有参数
# ✅ projector的所有参数  
# ✅ language模型的所有参数
# ✅ 包括q_proj, kv_proj, o_proj等

# 没有内置的冻结机制
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐