QLora基础与进阶指南

QLoRA基础指南:大模型高效微调的革命性解决方案

QLoRA (Quantized Low-Rank Adaptation) 是当今大语言模型(LLM)领域中至关重要的高效微调技术。它的核心使命是:在消费级或单张GPU(如 24GB 显存的 RTX 4090)上,实现对百亿参数级别大模型的高保真度微调。这突破性地解决了大模型定制化和应用落地中最严峻的瓶颈——高昂的硬件成本,极大地推动了AI技术的民主化进程。

技术基石:LoRA —— 参数高效微调的“补丁”艺术

要理解QLoRA,必须先掌握其基础框架 LoRA (Low-Rank Adaptation)

传统微调的困境

对整个模型的所有参数进行微调(Full Fine-Tuning),不仅需要海量的GPU显存和计算资源,还容易在特定任务的小数据集上引发过拟合,损害模型的泛化能力。

LoRA 的核心思想

LoRA基于一个关键洞察:预训练模型在适应下游任务时,其权重的变化矩阵(ΔW\Delta WΔW)具有很低的“内在秩”(intrinsic rank)。这意味着巨大的权重更新矩阵可以用两个小得多的“低秩”矩阵的乘积来高效近似:

ΔW=B⋅A\Delta W = B \cdot AΔW=BA

其中,WWW 是原始权重矩阵,AAABBB 是可训练的低秩矩阵(“LoRA适配器”),其秩(rank, r)远小于原始维度。

具体实现
  1. 冻结原始权重:预训练模型的绝大部分参数(如Transformer中的Wq,Wk,WvW_q, W_k, W_vWq,Wk,Wv等)保持不变,不参与梯度更新。
  2. 注入适配器:在需要微调的层(通常是注意力层)旁边,并联注入可训练的低秩矩阵 AB
  3. 协同工作:前向传播时,模型的输出由两部分组成:原始模型的输出和LoRA适配器的输出。
    h=Wx+ΔWx=Wx+(BA)x h = Wx + \Delta Wx = Wx + (BA)x h=Wx+ΔWx=Wx+(BA)x
    训练过程中,只有矩阵 AB 的参数被更新。
核心优势
  • 极致的参数效率:可训练参数量通常仅为原始模型的 0.1% ~ 1%,极大降低了显存占用和计算需求。
  • 模块化与可移植性:微调后只需保存轻量的适配器权重(A和B),可以像插件一样即插即用,轻松为同一个基础模型切换不同任务的适配器。
  • 性能保持:在减少大量可训练参数的同时,通常能达到与全量微调相当甚至更好的性能,并有效降低过拟合风险。

QLoRA 的飞跃:三大核心技术的完美融合

QLoRA 的真正突破在于,它并非单一技术,而是将模型量化低秩适配内存管理三项关键技术巧妙融合的系统级解决方案。

1. 4-bit NormalFloat (NF4) 量化:为正态分布量身定制
  • 问题:传统的INT4均匀量化会粗暴地将权重映射到16个等距点上。然而,模型权重通常呈均值为0的正态分布,大部分权重集中在0附近。均匀量化会浪费大量表示能力在权重稀疏的区间,导致精度损失巨大。
  • 解决方案 (NF4):这是一种非均匀的4-bit量化格式,其设计思想是信息论最优的。它将标准正态分布划分为 16个等概率的区间,并将每个区间的期望值作为量化点。这样,量化点在权重密集区(0附近)分布更密,在稀疏区分布更疏,从而在4-bit的限制下最大程度地保留了原始权重的信息。
  • 优势:与INT4相比,NF4的量化误差极小,使得4-bit量化后的模型性能与BF16版本惊人地接近。
2. 双重量化 (Double Quantization, DQ):压缩量化元数据
  • 问题:对模型进行NF4量化时,每个权重张量(Tensor)都需要保存一个对应的量化常数(即归一化因子,通常是一个32-bit的浮点数),用于反量化。对于一个巨大的模型,这些量化常数本身就会累积成可观的显存开销(例如,一个7B模型可能需要几GB)。
  • 解决方案 (DQ):对这些量化常数本身再进行一次量化。具体来说,将第一级的量化常数(FP32)分组,然后对每一组计算一个新的、更低精度的量化常数(如8-bit Float)。
  • 优势:通过这种“压缩压缩器”的思路,平均每个原始参数的额外存储开销从 32 bit 降低到了约 0.5 bit,进一步极致地节省了显存。
3. 分页优化器 (Paged Optimizers):智能应对显存峰值
  • 问题:在训练过程中,优化器(如AdamW)需要存储梯度和动量状态,这会瞬间产生巨大的显存峰值,尤其是在梯度累积步骤中,常常导致训练因“显存不足”(Out of Memory, OOM)而中断。
  • 解决方案:利用NVIDIA的统一内存(Unified Memory)特性,将优化器的状态(Optimizer States)分配在被“分页”的CPU内存中。当GPU显存不足时,系统会自动将暂时不用的优化器状态“换出”到CPU内存;当需要时,再将其“换入”到GPU显存中。
  • 优势:有效平滑了训练过程中的显存峰值,确保即使在显存极限情况下也能稳定完成训练,是使用QLoRA训练大型模型的关键保障。

实现与代码洞察

在实践中,QLoRA的实现高度依赖于Hugging Face生态系统中的几个核心库:

  • transformers: 用于加载预训练的基础模型。
  • bitsandbytes: 提供了QLoRA核心的4-bit量化(NF4, DQ)和分页优化器功能的底层实现。
  • peft (Parameter-Efficient Fine-Tuning): 提供了LoRA及其他参数高效微调方法的上层API,能够轻松地将LoRA适配器应用到transformers模型上。
  • accelerate: 简化了在不同硬件(单GPU、多GPU、TPU)上的训练流程。

一个典型的QLoRA微调流程在代码层面的配置如下(概念性展示):

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from peft import get_peft_model, LoraConfig

# 1. 配置BitsAndBytesConfig,启用QLoRA核心功能
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,                      # 以4-bit加载模型
    bnb_4bit_quant_type="nf4",              # 使用NF4量化类型
    bnb_4bit_compute_dtype=torch.bfloat16,  # 计算时使用的中间数据类型,以保持精度和性能
    bnb_4bit_use_double_quant=True,         # 启用双重量化
)

# 2. 加载量化后的基础模型
base_model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-13b-chat-hf",
    quantization_config=quantization_config, # 应用量化配置
    device_map="auto"                        # 自动分配设备(GPU)
)

# 3. 配置LoraConfig,定义适配器参数
lora_config = LoraConfig(
    r=16,                                    # LoRA的秩
    lora_alpha=32,                           # LoRA的缩放因子
    target_modules=["q_proj", "v_proj"],     # 指定要应用LoRA的模块
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

# 4. 使用PEFT将LoRA适配器应用到量化模型上
peft_model = get_peft_model(base_model, lora_config)

# ... 接下来是标准的模型训练流程 (Trainer / 自定义训练循环) ...
# 训练时,只有peft_model中的LoRA参数会被更新。

QLoRA伪代码解析

为了更直观地理解QLoRA的内部工作流程,以下是一段带详细注释的伪代码,模拟了训练过程中的一个步骤。

# -----------------
# 伪代码:QLoRA 训练步骤深度解析
# -----------------

# 定义QLoRA的核心组件
class QLoRA_Layer:
    def __init__(self, original_weight, rank):
        # 1. [初始化] 冻结并量化原始权重
        # 原始权重W (FP32) 被量化为 W_q (NF4) 并存储,同时保存量化常数 C
        self.W_quantized, self.quant_consts = quantize_to_NF4(original_weight)
        
        # 2. [初始化] 创建可训练的LoRA适配器
        # A和B是小矩阵,只有它们需要计算梯度
        self.lora_A = create_trainable_matrix(original_weight.shape[1], rank)
        self.lora_B = create_trainable_matrix(rank, original_weight.shape[0])

    def forward(self, x):
        # --- 前向传播 ---
        
        # 3. [计算-步骤1] 对基础模型部分进行计算
        # 注意:反量化是“即时”的,仅在计算时发生,W_quantized本身仍在显存中保持4-bit
        W_dequantized = dequantize_from_NF4(self.W_quantized, self.quant_consts)
        
        # 使用反量化后的权重进行矩阵乘法,但计算精度通常为BF16/FP16
        base_output = compute(W_dequantized, x, dtype=bfloat16)
        
        # 4. [计算-步骤2] 对LoRA适配器部分进行计算
        # 这部分是标准的高精度计算
        lora_output = self.lora_B @ (self.lora_A @ x)
        
        # 5. [合并结果] 将两部分输出相加
        return base_output + lora_output

# --- 训练循环中的一瞥 ---
def training_step(model, data):
    # 获取输入
    inputs = data['input']
    
    # 前向传播
    # model内部的每个QLoRA_Layer都会执行上面的forward逻辑
    outputs = model.forward(inputs)
    
    # 计算损失
    loss = calculate_loss(outputs, data['labels'])
    
    # --- 反向传播 ---
    # 6. [关键点] 计算梯度
    # 梯度只会为可训练的参数计算
    # model.W_quantized -> NO GRADIENT (冻结)
    # model.lora_A -> COMPUTE GRADIENT
    # model.lora_B -> COMPUTE GRADIENT
    loss.backward()
    
    # 7. [优化] 更新权重
    # 优化器(如 Paged AdamW)只会更新 lora_A 和 lora_B 的参数
    optimizer.step()
    
    # 清空梯度,准备下一次迭代
    optimizer.zero_grad()

在内存中用4-bit权重节省空间,在计算时即时反量化以保持精度,同时只训练极少数的LoRA参数,最终实现了资源消耗与模型性能的完美平衡。

QLoRA进阶指南:从入门到精通必须掌握的5个核心细节

您已经了解了QLoRA通过NF4、双重量化和分页优化器实现高效微调的“魔法”。但要真正驾驭它,并像专家一样解决实际问题,我们还需要深入挖掘其背后的核心细节。这篇进阶指南将带您探索QLoRA的“艺术”层面。

LoRA超参数的艺术:解密 rlora_alpha

在配置LoRA时,rlora_alpha 是最关键的两个超参数。简单设置它们很容易,但理解其内在关系才能发挥LoRA的最大潜力。

  • r (Rank - 秩)

    • 是什么r 是低秩矩阵A和B的中间维度(A 的形状是 d x rB 的形状是 r x k)。它直接决定了LoRA适配器的参数量表达能力
    • 如何理解:您可以将 r 想象成给予模型的“额外可塑性”或“学习预算”。
      • 较小的 r (如 8, 16): 参数量少,训练快,适用于简单的任务,如微调对话风格或遵循简单指令。
      • 较大的 r (如 64, 128): 参数量多,表达能力更强,适用于需要学习更复杂模式或特定领域知识的任务。但同时,它也会增加训练开销,并有轻微的过拟合风险。
    • 实践建议:从一个较小的值(如8或16)开始实验,根据验证集上的性能表现逐步增加。
  • lora_alpha (Alpha - 缩放因子)

    • 是什么lora_alpha 是一个缩放常数,用于调整LoRA适配器输出的权重。在前向传播中,LoRA部分的输出 (BA)x 会被乘以一个缩放系数 alpha / r
    • 如何理解:如果说 r 是LoRA适配器的“容量”,那么 alpha 就是控制这个适配器影响力的“旋钮”
      • 公式 ΔW = (alpha / r) * BA 揭示了真相:alphar 共同决定了适配器的最终缩放比例。
    • 关键关系与实践建议
      • 常见启发式设置:一个非常流行且有效的实践是将 lora_alpha 设置为 r两倍(例如,r=16, alpha=32)。
      • 为什么要这样做? 这种设置使得初始的LoRA权重在初始化后具有与原始权重相似的量级,有助于训练的稳定启动。如果 alpha 太小,LoRA适配器的贡献可能微不足道,学习缓慢;如果太大,则可能在训练初期破坏模型的原始知识,导致不稳定。将 alpha 固定为 r 的某个倍数(如1倍或2倍),然后只调整 r,是一种简化超参数搜索的有效策略。

compute_dtype 的奥秘:为何存储与计算要用不同精度?

BitsAndBytesConfig中,我们设置 load_in_4bit=True,但同时会指定一个 bnb_4bit_compute_dtype (如 torch.bfloat16)。这是一个至关重要的细节。

  • 核心原则用最低的精度存储,用最优的精度计算
  • 为什么不能直接用4-bit计算?
    1. 硬件不支持:现代GPU的张量核心(Tensor Cores)等计算单元是为FP32、FP16和BF16等标准数据类型高度优化的。它们无法原生、高效地执行4-bit矩阵乘法。
    2. 精度灾难:在复杂的计算流(如反向传播)中,持续使用超低精度会迅速累积误差,导致训练无法收敛。
  • compute_dtype 的作用:它告诉系统,在执行矩阵乘法等核心运算时,需要**“即时”将4-bit权重反量化到指定的计算精度(如BF16)。这个过程在GPU内部高速完成,计算结束后,权重在显存中依然保持4-bit**。
  • BF16 vs FP16 的选择
    • FP16 (半精度浮点):动态范围较小,表示数值的范围有限。在训练大型模型时,梯度值可能变得非常小而超出其表示范围,导致“梯度消失”(underflow),使训练失败。
    • BF16 (BFloat16):具有与 FP32 (全精度) 相同的动态范围,但牺牲了精度位。这意味着它能表示极大和极小的数,非常适合训练大模型,能有效避免梯度消失问题。因此,在支持BF16的现代GPU(如Ampere架构的A100、Ada Lovelace架构的40系列)上,BF16 是进行QLoRA训练的首选

适配器放置策略:target_modules 的选择

LoRA并非要应用到模型的每一层。target_modules 参数让我们能精确选择“打补丁”的位置。

  • 为什么主要选择注意力层?
    • 研究和实践表明,Transformer模型中的**自注意力机制(Self-Attention)**是模型理解上下文、建立词与词之间关系的核心。微调任务通常需要模型学习新的数据模式或行为,而调整注意力权重是实现这一目标的最有效途径。
  • 常见的 target_modules
    • 对于大多数基于Transformer的LLM(如Llama、Mistral),最关键的模块是查询(Query)、键(Key)和值(Value)的线性投影层。因此,常见的配置是:
      target_modules=["q_proj", "k_proj", "v_proj", "o_proj"]
      
    • 有些研究也发现,将LoRA应用于前馈网络(FFN/MLP)的部分层(如 gate_proj, up_proj, down_proj)也能带来收益,但这会增加参数量。
  • 实践建议
    • 从标准开始:初学者应从最标准、最有效的注意力层(q_proj, v_proj)开始。
    • 性能压榨:如果想进一步压榨模型性能,可以实验性地添加 k_proj, o_proj,甚至MLP层,并通过验证集评估效果。对于大多数任务,仅适配注意力层已足够好。

训练之后:合并权重 (merge_and_unload) vs. 动态加载

QLoRA微调完成后,您会得到一个轻量的适配器文件。在部署推理时,有两种选择:

  • 1. 动态加载 (默认方式)

    • 流程:加载原始的4-bit量化模型,然后再加载LoRA适配器权重,并将其应用到模型上。
    • 优点高度灵活。同一个基础模型可以搭配多个不同的LoRA适配器,以服务于不同任务,极大地节省了存储空间。
    • 缺点:在推理时,每次前向传播都需要额外计算LoRA部分的输出并将其与基础模型输出相加,这会带来微小的推理延迟
  • 2. 合并权重 (merge_and_unload)

    • 流程peft库提供了model.merge_and_unload()方法。它会将LoRA权重(BA)与基础模型的对应权重(W)数学上合并,生成一个新的、完整的权重矩阵(W' = W + BA)。
    • 优点无推理延迟。合并后,模型结构恢复为标准的Transformer,不再有额外的计算分支,推理速度与原始模型完全相同。
    • 缺点失去模块化。每次合并都会产生一个完整的模型副本,如果任务众多,将导致存储空间的急剧增加。
  • 何时选择哪种?

    • 开发与实验阶段:使用动态加载,方便快速切换和测试。
    • 生产环境单任务部署:如果一个模型实例只服务于一个固定的、性能要求极高的任务,合并权重是最佳选择。

潜在陷阱与注意事项:QLoRA并非万能

  • 任务适用性:QLoRA在风格迁移、指令遵循、特定领域知识的适应性微调等任务上表现出色。但如果任务需要模型学习大量全新的、与预训练知识大相径庭的知识体系,QLoRA的效果可能不如全量微调。
  • 灾难性遗忘:虽然比全量微调轻微,但QLoRA仍然可能导致模型在微调任务上表现优异的同时,遗忘部分通用能力。进行充分的评估至关重要。
  • 数据质量是王道:任何微调技术都无法弥补低质量数据带来的问题。“Garbage In, Garbage Out” 这条黄金法则在QLoRA中同样适用。高质量、干净、与任务目标高度相关的微调数据集是成功的关键。
  • 学习率的重要性:QLoRA对学习率(Learning Rate)仍然敏感。过高的学习率可能会破坏预训练模型的知识结构,而过低则学习缓慢。通常需要选择比全量微调更小一个数量级的学习率(如 1e-42e-5)。

伪代码案例

这份代码将通过一个自定义的 QLoRALayer 来展示 QLoRA 的核心。我们将模拟一个线性层(nn.Linear)被QLoRA包装和微调的全过程。

# 导入必要的库
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from typing import Optional

# -----------------------------------------------------------------------------
# 1. 概念性辅助函数 (在实际中由 bitsandbytes 库提供)
# -----------------------------------------------------------------------------

def quantize_to_nf4_and_double_quant(fp32_tensor: torch.Tensor):
    """
    伪代码函数:模拟将FP32权重张量量化为NF4格式,并应用双重量化。
    实际实现非常复杂,这里我们仅做概念性表示。
    """
    # 想象这里发生了NF4量化和双重量化...
    # 返回一个代表4-bit数据的张量(例如,用INT8类型存储4-bit索引)和量化所需的元数据。
    quantized_data = (fp32_tensor * 10).to(torch.int8) # 极简化的模拟
    quantization_constants = {"scale": 0.1} # 模拟量化常数
    print("      [Quantization] 权重已从 FP32 -> NF4")
    return quantized_data, quantization_constants

def dequantize_on_the_fly(nf4_tensor: torch.Tensor, consts: dict, compute_dtype: torch.dtype = torch.bfloat16) -> torch.Tensor:
    """
    伪代码函数:模拟在计算时“即时”将NF4权重反量化为指定的计算精度。
    """
    # 想象这里发生了高速的反量化过程...
    dequantized_tensor = nf4_tensor.to(compute_dtype) * consts["scale"]
    return dequantized_tensor

# -----------------------------------------------------------------------------
# 2. 核心QLoRA层封装
# -----------------------------------------------------------------------------

class QLoRALayer(nn.Module):
    """
    一个封装了QLoRA逻辑的自定义层。
    它接收一个标准的nn.Linear层,并将其转换为QLoRA版本。
    """
    def __init__(self,
                 linear_layer: nn.Linear,
                 rank: int,
                 lora_alpha: int):
        super().__init__()
        
        self.in_features = linear_layer.in_features
        self.out_features = linear_layer.out_features
        self.rank = rank
        self.lora_alpha = lora_alpha
        
        # 💡 步骤 1: 量化并冻结原始权重
        # 将原始FP32权重进行NF4量化,并保存量化后的权重和常数
        self.base_layer_weights_nf4, self.quant_consts = quantize_to_nf4_and_double_quant(linear_layer.weight.data)
        
        # 保存原始的bias(如果有的话),bias通常不量化
        self.bias = linear_layer.bias
        
        # 💡 步骤 2: 注入可训练的LoRA适配器
        # LoRA矩阵A,初始化为高斯分布
        self.lora_A = nn.Parameter(torch.randn(rank, self.in_features))
        # LoRA矩阵B,初始化为0,确保微调开始时适配器输出为0,实现稳定启动
        self.lora_B = nn.Parameter(torch.zeros(self.out_features, rank))
        
        # 计算缩放因子
        self.scaling = self.lora_alpha / self.rank

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        前向传播的核心逻辑。
        """
        # 记录输入的计算精度,这是我们反量化后要达到的目标精度
        compute_dtype = x.dtype

        # --- 路径 1: 基础模型 (冻结部分) ---
        # 1a. 即时反量化基础权重
        dequantized_weight = dequantize_on_the_fly(self.base_layer_weights_nf4, self.quant_consts, compute_dtype)
        
        # 1b. 使用反量化后的权重进行计算
        base_output = F.linear(x, dequantized_weight, self.bias)

        # --- 路径 2: LoRA 适配器 (可训练部分) ---
        # 2a. 计算LoRA的增量输出
        lora_output = (self.lora_B @ self.lora_A) @ x.T
        lora_output = lora_output.T
        
        # 2b. 应用缩放因子
        lora_output = lora_output * self.scaling
        
        # --- 合并输出 ---
        # 将基础模型输出与LoRA适配器输出相加
        return base_output + lora_output

# -----------------------------------------------------------------------------
#  3. 模拟训练流程
# -----------------------------------------------------------------------------
if __name__ == "__main__":
    # --- 超参数定义 ---
    INPUT_DIM = 256
    OUTPUT_DIM = 512
    LORA_RANK = 16
    LORA_ALPHA = 32
    LEARNING_RATE = 1e-4
    EPOCHS = 10
    
    # 1. 定义一个原始的、我们想要微调的层
    original_linear_layer = nn.Linear(INPUT_DIM, OUTPUT_DIM)
    print("1. 原始 nn.Linear 层已创建。")
    
    # 2. 将其转换为QLoRA层
    print(" 2. 正在将 nn.Linear 转换为 QLoRA_Layer...")
    qlora_layer = QLoRALayer(original_linear_layer, rank=LORA_RANK, lora_alpha=LORA_ALPHA)
    print("2. QLoRA_Layer 创建成功!")
    
    # 3. 设置优化器
    # 关键点:优化器只更新需要计算梯度的参数,即LoRA的A和B矩阵
    # `p.requires_grad` 会自动筛选出 nn.Parameter() 定义的张量
    optimizer = optim.AdamW([p for p in qlora_layer.parameters() if p.requires_grad], lr=LEARNING_RATE)
    print(f" 3. 优化器已设置,只训练 {sum(p.numel() for p in qlora_layer.parameters() if p.requires_grad)} 个可训练参数。")
    
    # --- 模拟训练循环 ---
    print("\n--- 开始模拟训练 ---")
    for epoch in range(EPOCHS):
        # 创建模拟输入数据 (使用bfloat16,模拟现代LLM训练环境)
        dummy_input = torch.randn(1, INPUT_DIM, dtype=torch.bfloat16)
        # 创建模拟目标数据
        dummy_target = torch.randn(1, OUTPUT_DIM, dtype=torch.bfloat16)
        
        # 前向传播
        output = qlora_layer(dummy_input)
        
        # 计算损失
        loss = F.mse_loss(output, dummy_target)
        
        # 反向传播 (梯度只会流向 lora_A 和 lora_B)
        optimizer.zero_grad()
        loss.backward()
        
        # 更新权重 (只有 lora_A 和 lora_B 会被更新)
        optimizer.step()
        
        if (epoch + 1) % 2 == 0:
            print(f"Epoch [{epoch+1}/{EPOCHS}], Loss: {loss.item():.6f}")

    print("--- 训练完成 ---")

代码解析与总结

  1. 模块化设计QLoRALayer 类完美地封装了QLoRA的复杂性。它清晰地展示了在初始化时如何量化和冻结基础权重,并注入可训练的LoRA适配器。
  2. 前向传播的清晰路径forward方法直观地分成了两条路径:一条是处理即时反量化的基础模型输出,另一条是计算LoRA适配器的增量输出。最后将两者相加,这正是QLoRA的核心计算图。
  3. 梯度与优化的关键:在设置优化器时,我们通过列表推导式 [p for p in model.parameters() if p.requires_grad] 巧妙地只选择了需要训练的参数(即lora_Alora_B)。这确保了在整个训练过程中,巨大的基础模型权重始终保持冻结,从而实现了显存和计算的巨大节省。
  4. 数据类型的重要性:代码中使用了 torch.bfloat16 作为输入和计算的数据类型,这与现代大模型训练的最佳实践保持一致,也凸显了compute_dtype在QLoRA配置中的重要性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

frostmelody

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值