欢迎光临
我们一直在努力

AMD Hello-ROCm LoRA微调(二)

安装依赖

先执行pip uninstall torchvision

注意这边如果执行了之前环境配置操作,也要执行这一步操作,因为上一步操作中是这样安装的:

uv pip uninstall torchvision uv pip install vllm torchvision \\ –no-cache \\ –index-url https://mirrors.aliyun.com/pypi/simple/ \\ –extra-index-url https://wheels.vllm.ai/rocm/ \\ -U

而两次操作两个库不是从同一个源(一个阿里云一个腾讯云)安装的,而且 torchvision 的版本是预编译给另一个 PyTorch 构建的,否则按教程的操作运行Notebook示例文件时,会出现 torchvision::nms does not exist 和循环导入等错误。

官方教程给出的命令是下面这样:

uv pip install -U vllm modelscope transformers accelerate datasets trl peft scikit-learn pandas tqdm torchvision \\

  –no-cache \\

  -i https://mirrors.cloud.tencent.com/pypi/simple/ \\

  –extra-index-url https://wheels.vllm.ai/rocm/

字段

含义

!

Jupyter Notebook 中执行系统命令的前缀(在终端中可直接去掉)

uv

一种用 Rust 编写的高性能 Python 包管理器,可替代 pip,安装速度更快

pip install

uv 模拟的 pip 安装命令

-U (或 –upgrade)

升级所有列出的包到最新可用版本(若已安装则升级,否则直接安装)

待安装的包列表

一系列用于大模型推理、微调和数据处理的库:vllm(LLM 推理引擎)、modelscope(阿里模型库)、transformers/datasets/accelerate/trl/peft(Hugging Face 模型、数据、分布式、强化学习微调、参数高效微调)、scikit-learn(机器学习)、pandas(数据分析)、tqdm(进度条)、torchvision(PyTorch 视觉工具)

–no-cache

强制不使用本地缓存,从远程重新下载所有包以确保获取最新文件(下载会变慢)

-i https://mirrors.cloud.tencent.com/pypi/simple/

指定主软件包索引源为腾讯云 PyPI 镜像,用于在国内加速下载大部分 Python 包

–extra-index-url https://wheels.vllm.ai/rocm/

额外添加一个索引源,专门提供 vLLM 针对 AMD ROCm 平台编译的 wheel 包,确保在 AMD GPU 上能正确安装和使用 vLLM

前期准备

要微调模型要先准备好模型、数据集。

配置训练环境

接着要配置训练环境,通常包括:

GPU资源

这里是单卡环境。

数据集划分

1.比例划分:Train :Test(8:2);Train:Eval:Test(6:2:2)

2.设置集合样本数量如TRAIN_LIMIT、EVAL_LIMIT等参数,比如取Top 1000

精度

  • 模型权重的存储数据类型MODEL_DTYPE。
  • 类型

    bit

    数值范围

    特点

    典型用途

    FP32

    32

    ±10^38

    最高精度,训练基准

    全精度训练与推理

    FP16

    16

    ±65504

    显存减半,数值范围窄,可能溢出

    混合精度训练(需梯度缩放)

    BF16

    16

    ±10^38(同FP32)

    显存减半,动态范围大,几乎无需梯度缩放

    混合精度训练

    INT8

    8

    -128 ~ 127

    再减半显存,需量化校准,略损精度

    推理加速、边缘部署

    INT4

    4

    -8 ~ 7

    极致压缩,精度损失较大,需仔细量化

    极低资源推理(如 llama.cpp Q4)

    2.训练参数精度:常见的有FP16、BF16、FP32,一般也受显卡影响。

    • FP32精度最高,但训练需要显存也多。
    • BF16牺牲了小数位的精度,但既能节省显存、加快训练,又能保持较大数值范围。
    • FP16 的数值范围较小,训练时容易出现梯度溢出,通常需要配合梯度缩放

    随机种子

            因为计算机生成的是伪随机数,其生成函数可以看作:下一个数 = f(当前状态, 种子),相当于给定种子调用random()拿到的数一定相同,让数据 shuffle、LoRA 初始化和训练过程尽量可复现(因为复现性还受计算差异如硬件一致性、确定性算法等影响)。

    def setup_seed(seed: int = 42):

        random.seed(seed)

        np.random.seed(seed)

        torch.manual_seed(seed)

        set_seed(seed)

        if torch.cuda.is_available():

            torch.cuda.manual_seed_all(seed)

    setup_seed(SEED)

    指令微调(复用原有架构以及knowledge)

    因为我们想要想要微调后的模型实现情绪分类任务(classification task),但对于LLM来说本质上仍然是生成任务(generation task)而不仅仅是特征到标签的映射,所以要把分类任务重新包装成生成任务,即变成一个prompt-completion 格式数据:

    • prompt:system + user
    • completion:assistant 只输出对应的情绪标签

    def to_prompt_completion(example):

        text = example["text"]

        label = label_names[example["label"]]

        user_content = f"Classify the emotion of this text:\\n\\n{text}"

        return {

            "prompt": [

                {"role": "system", "content": SYSTEM_PROMPT},

                {"role": "user", "content": user_content},

            ],

            "completion": [

                {"role": "assistant", "content": label},

            ],

        }

    sft_dataset = dataset.map(

        to_prompt_completion,

        remove_columns=dataset["train"].column_names,

    )

    batch_size

    每次训练传多少个数据进去batch。

    比如有 1000 条对话训练数据,设定 batch_size = 8:

    模型一次拿 8 条数据,算它们的平均损失;

    反向传播时,基于这 8 条数据产生的平均梯度来更新一次权重;

    跑完整个数据集需要 1000 / 8 = 125 次参数更新,这 125 步称为 1 个 epoch。

    而模型训练时的显存占用可以拆成三块:

    • 模型参数本身(权重 + 优化器状态):模型结构不变就固定下来了。
    • 中间激活值:在多层神经网络中,每一层的梯度都依赖于该层的输入激活值。所以前向传播时产生的临时数据,必须存到反向传播用。其体积 ≈ 单条样本激活值 × batch size。

    神经网络的训练本质是求梯度(参数对损失的影响),用的是链式法则。

    举例类比:函数 y = f(g(x)),要算 dy/dx,需要:

                                            dy/dx = f'(g(x)) * g'(x)

    其中 g(x) 就是中间激活值,g'(x) 计算时需要用到 x 的值。

    • 数据本身:输入的 token 张量等,也跟 batch size 成正比。

    所以遇到GPU OOM可以考虑减小batch_size或降低前文所提的精度等。

    加载 tokenizer 和基础模型

    Tokenizer(分词器)

    组成:词汇表 + 特殊标记 + 格式化模板,把自然语言转成模型需要的数字序列。其实推理的时候也是需要的,不过模型会内置

    • tokenizer.json(词汇表与分词规则)
    • tokenizer_config.json(特殊标记、chat_template 等)
    • special_tokens_map.json

    加载模型

    由于本实验是在单卡上跑的,所以训练加载模型时要配置如下参数:

    1.不用 device_map="auto":device_map="auto" 是 Hugging Face transformers 为多卡、CPU offload 设计的自动分配器。单卡场景下如果用了,有时会把部分层放到 CPU,或者引入额外的通信开销,反而增加显存占用甚至变慢。

            不加这一项,模型就只会加载到单张 GPU 上(或先加载到 CPU,后续由 Trainer 统一移动到 GPU),简单可控。

    2.不用 bitsandbytes 4bit

    4bit 量化(load_in_4bit=True)能把模型权重大幅压缩,显存省很多,但会带来两个问题:

    精度损失:微调时可能影响最终性能。

    与某些 Trainer 特性不兼容:比如梯度检查点、优化器状态等有时会出小问题。

    3 加载后交给 SFTTrainer 管理设备:不需要自己手动调用 .cuda() 或 .to(device)。把模型直接传给 SFTTrainer,它会在内部自动把模型移动到训练所用的设备(比如 cuda:0),并且管理好数据、优化器、模型之间的设备一致性。避免手动管理设备时常见的错误(比如模型和数据不在同一设备)。不过通常还是要.cuda().to(device)自己配置。

    4 开启 gradient_checkpointing=True 来节省显存(计算时间换显存空间)

            正常情况下,前向传播每一层产生的中间激活值(隐藏状态、注意力矩阵等)都要保存在显存里,等着反向传播时用。开启梯度检查点后,只保留少量层的激活值作为“检查点”,其余层的前向结果直接丢弃。等到反向传播需要时,再从最近的检查点重新计算出来。显存占用显著下降,代价是大约 20~30% 的额外计算量。

    推理辅助函数

    微调前后都用同一套推理函数做评估,方便对比模型效果。extract_label() 用于从模型生成文本里提取对应的label(数据集的情绪标签集合,固定映射)。如果提取不到,会被记为 INVALID(相当于模型没有输出对应的情绪标签)。

    def extract_label(raw_text: str) -> str:

        raw_text = raw_text.strip().lower()

        match = LABEL_PATTERN.search(raw_text)

        if match:

            return match.group(1)

        tokens = raw_text.split()

        if not tokens:

            return "INVALID"

        return tokens[0].strip(".,!?:;\\"'()[]{}")

    def generate_label(model, tokenizer, user_text: str, system_prompt: str = SYSTEM_PROMPT, max_new_tokens: int = 4) -> str:

        user_content = f"Classify the emotion of this text:\\n\\n{user_text}"

        messages = [

            {"role": "system", "content": system_prompt},

            {"role": "user", "content": user_content},

        ]

        device = next(model.parameters()).device

        inputs = tokenizer.apply_chat_template(

            messages,

            tokenize=True,

            add_generation_prompt=True,

            return_dict=True,

            return_tensors="pt",

        )

        inputs = {k: v.to(device) for k, v in inputs.items()}

        input_len = inputs["input_ids"].shape[-1]

        model.eval()

        with torch.no_grad():

            outputs = model.generate(

                **inputs,

                max_new_tokens=max_new_tokens,

                do_sample=False,

                pad_token_id=tokenizer.pad_token_id,

                eos_token_id=tokenizer.eos_token_id,

            )

        raw_pred = tokenizer.decode(outputs[0][input_len:], skip_special_tokens=True).strip()

        return extract_label(raw_pred)

    评估函数

    评估指标包括:

    指标

    含义

    accuracy

    预测正确的样本占总样本的比例,说明整体有多准(但类别不均衡时会被带偏)

    macro_f1

    对每个类别分别计算 F1,然后取平均(不加权),说明小类(样本少的情绪)是否也被照顾到了

    invalid_predictions

    模型没有输出合法标签的样本数,说明模型是否遵循了指令,输出格式是否稳定利用推理函数来计算得出。

    classification_report

    包含每个类别的 precision、recall、f1-score,说明每一个具体情绪的判断质量

    confusion_matrix

    以热力图或矩阵展示真实 vs 预测的分布,说明最容易混淆的情绪对(如“愤怒” vs “悲伤”)

     微调前评估

    配置 LoRA

    图源: 
    [2106.09685] LoRA: Low-Rank Adaptation of Large Language Models

    LoRA(Low-Rank Adaptation)是目前大语言模型微调最主流的方法。它不修改原始模型权重,而是在特定层旁路插入小的、可训练的秩分解矩阵,只更新这些小矩阵。可以形象的类比为:

    • 全量微调:把原书每一页都擦掉重写,成本极高,而且一旦写坏书就废了。
    • LoRA:在每一页上贴一张极薄的便利贴,只写要补充或修改的内容。原书正文完全不动,但阅读时会把便利贴的内容和原文叠加,最终效果相当于一本新书。

    数学上,原有权重矩阵 W 保持不变,LoRA 添加两个小矩阵 A 和 B,使得:

    新权重=W+A×B 其中A和B都是低秩矩阵(参数量远小于 W)。训练时只更新 A 和 B,原模型W冻结。

    教程中使用target_modules="all-linear",表示尽量给模型中的线性层加 LoRA,可以改成指定模块名控制显存和训练速度,也会影响训练效果。

    定义训练参数

    类别

    参数

    作用

    典型值 / 说明

    精度与显存

    MODEL_DTYPE

    模型权重的存储类型

    bfloat16(支持 BF16 的卡)或 float16

    bf16 / fp16

    开启混合精度训练

    二选一,BF16 更稳定

    load_in_4bit

    4-bit 量化加载原模型(QLoRA)

    True 时显存大幅降低

    批次与序列

    per_device_train_batch_size

    单张 GPU 一次前向的样本数

    1–4(显存受限)

    gradient_accumulation_steps

    梯度累积步数,等效 batch = batch_size × 累积步数

    8–32,用于撑大有效 batch

    max_seq_length / cutoff_len

    单条样本最大 token 数,超出截断

    512–2048(按任务需要)

    优化器与学习率

    learning_rate

    更新步长

    LoRA 推荐 1e-4 ~ 5e-4,全量 1e-5

    optimizer

    优化算法

    adamw_torch、adamw_8bit(省显存)

    lr_scheduler_type

    学习率调度策略

    cosine 或 linear,配合 warmup

    warmup_ratio

    前期缓慢增加学习率的比例

    0.03–0.1,避免初期震荡

    max_grad_norm

    梯度裁剪阈值

    1.0,防梯度爆炸

    训练轮次

    num_train_epochs

    全量数据重复次数

    1–3,指令微调不宜过多

    max_steps

    最大更新步数(与 epoch 二选一)

    按需设置

    LoRA 结构

    lora_rank (r)

    低秩矩阵的秩,决定适配器容量

    8–64,越高表示可学习参数越多

    lora_alpha

    缩放系数,实际学习率 ∝ alpha/r

    通常设为 rank 的 2 倍

    lora_dropout

    适配器 dropout,正则化

    0.05–0.1

    lora_target

    要添加适配器的模块

    q_proj,v_proj 等

    评估与保存

    eval_steps

    每隔多少步在验证集上评估

    100–500

    save_steps

    每隔多少步保存一次 checkpoint

    与 eval_steps 一致

    save_total_limit

    最多保留多少个 checkpoint

    2–3,避免占满磁盘

    metric_for_best_model

    选择最佳模型的指标

    eval_loss 或 macro_f1

     

    training_args = SFTConfig(

        output_dir=OUTPUT_DIR,

        per_device_train_batch_size=4,

        per_device_eval_batch_size=1,

        gradient_accumulation_steps=4,

        learning_rate=1e-4,

        weight_decay=0.01,

        lr_scheduler_type="linear",

        warmup_steps=50,

        num_train_epochs=1,

        logging_steps=5,

        eval_strategy="steps",

        eval_steps=25,

        save_strategy="steps",

        save_steps=25,

        save_total_limit=2,

        metric_for_best_model="eval_loss",

        greater_is_better=False,

        gradient_checkpointing=True,

        bf16=BF16,

        fp16=FP16,

        tf32=False,

        max_length=256,

        packing=False,

        completion_only_loss=True,

        remove_unused_columns=False,

        dataloader_num_workers=2,

        optim="adamw_torch",  #避免 AMD ROCm 下 `bitsandbytes` 优化器兼容问题

        report_to="none",

        seed=SEED,

        data_seed=SEED,

    )

    然后就可以开始 LoRA 微调了!

    保存 LoRA adapter 和 tokenizer

    保存的是 LoRA adapter,不是完整大模型权重。

    目录中通常包括:

    • adapter_model.safetensors
    • adapter_config.json
    • tokenizer 相关文件
    • training checkpoints

    微调后评估

    加载本地 LoRA adapter 推理

    以后不想重新训练,可以用下面代码重新加载:

  • 本地基础模型目录加载 base model。
  • 从 OUTPUT_DIR 加载 LoRA adapter,PeftModel.from_pretrained 是 PEFT 库提供的接口,它会把基础模型和 LoRA 适配器捆绑到一起。
  • 使用同一个 generate_label() 做推理
  • 其他一些自己的实验截图碎片:

    参考文献

    AMD 云环境模型微调- Datawhale AI学习中心

    LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS

    赞(0)
    未经允许不得转载:171主机测评 » AMD Hello-ROCm LoRA微调(二)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址