基于ROCm与LoRA的Gemma 2B大模型微调及Ollama部署全流程实践

基于ROCm与LoRA的Gemma 2B大模型微调及Ollama部署全流程实践

1. 项目概述:一次完整的大模型微调与部署实践

最近在折腾大模型本地化应用,目标很明确:想基于一个轻量级的开源大模型,用我自己的数据训练一个垂直领域的助手,然后把它变成一个可以随时对话的本地服务。经过一番调研,我锁定了Google的Gemma 2B/7B模型,它开源、性能不错,而且对个人开发者相对友好。整个流程走下来,涉及从底层驱动环境搭建、模型微调,到最后的服务化部署,算是一次比较完整的实践。今天就把从零开始,在支持AMD显卡的Linux服务器上,基于ROCm环境对Gemma 2B模型进行LoRA微调,并最终通过Ollama部署上线的全过程记录下来。如果你手头有AMD显卡(比如RX 6000/7000系列或Instinct系列),或者单纯想了解大模型微调与部署的完整链路,这篇记录应该能提供不少参考。

整个过程可以拆解为三个核心阶段:首先是搭建ROCm计算环境,这是让AMD显卡能跑PyTorch深度学习训练的前提;其次是使用LoRA技术对Gemma模型进行微调,这是本次实践的核心,旨在用较小的代价让模型学会特定领域的知识或技能;最后是将微调好的模型整合进Ollama框架,实现本地化的模型服务与管理。每个阶段都有不少细节和坑,我会结合自己的实操,把关键步骤、原理和避坑心得都摊开来讲清楚。

2. 环境准备:ROCm在Linux下的搭建与踩坑实录

我的实验环境是一台搭载了AMD Radeon RX 7900 XTX显卡的Ubuntu 22.04 LTS服务器。选择ROCm是因为它是AMD官方推出的开源GPU计算平台,对标NVIDIA的CUDA,是让PyTorch等框架在AMD显卡上运行的基础。

2.1 ROCm安装与系统配置

安装ROCm的第一步是确认显卡兼容性。并非所有AMD显卡都受官方支持,我查阅了ROCm的官方支持列表,确认RX 7900 XTX在支持范围内。然后需要添加ROCm的APT仓库并安装核心包。这里命令并不复杂,但系统层面的依赖和配置是关键。

# 添加ROCm官方APT仓库 wget https://repo.radeon.com/amdgpu-install/latest/ubuntu/jammy/amdgpu-install_6.1.60100-1_all.deb sudo apt install ./amdgpu-install_6.1.60100-1_all.deb sudo amdgpu-install --usecase=rocm --no-dkms

安装完成后,需要将当前用户添加到rendervideo组,以便有权限访问GPU设备。

sudo usermod -a -G render,video $LOGNAME

之后必须重启系统,这是很多教程里轻描淡写但极其重要的一步,不重启用户组变更和内核模块加载可能不会完全生效。

重启后,验证安装是否成功:

rocminfo

这个命令会输出详细的GPU信息。更直观的验证是运行rocm-smi,它类似于NVIDIA的nvidia-smi,可以查看GPU状态、温度、功耗和显存占用。

注意:驱动版本匹配问题。ROCm版本、Linux内核版本、显卡固件版本之间需要匹配。我曾因内核自动升级到较新版本,导致与特定版本的ROCm驱动不兼容,出现kfd模块加载失败的错误。解决方案是暂时锁定内核版本,或者寻找与当前内核匹配的ROCm版本。对于生产环境,建议在部署初期就确定一个稳定的版本组合并冻结更新。

2.2 PyTorch与深度学习环境配置

ROCm只是底层驱动和运行时,上层还需要适配了ROCm的PyTorch。PyTorch官方提供了预编译的ROCm版本。

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.1

这里的rocm6.1需要与你安装的ROCm主版本号对应。安装后,在Python中运行以下代码进行验证:

import torch print(torch.__version__) print(torch.cuda.is_available()) # 对于ROCm,这里仍返回True,但实际使用的是ROCm后端 print(torch.cuda.get_device_name(0))

如果一切正常,会打印出版本号、True以及你的AMD显卡型号。这表明PyTorch已经可以识别并调用你的AMD GPU进行计算了。

接下来安装微调所需的其他库,主要是Hugging Face的transformers,datasets,accelerate,以及LoRA相关的peftbitsandbytes(用于量化)。这里有个大坑:bitsandbytes对ROCm的支持并不像CUDA那样完善。经过测试,在ROCm 6.1环境下,直接pip install bitsandbytes可能会编译失败或运行时出错。一个可行的替代方案是使用pip install bitsandbytes-rocm,这是一个社区维护的针对ROCm的移植版本,或者考虑使用其他量化库如auto-gptqgptq-for-llama,但这需要模型本身有对应的GPTQ量化版本。对于本次Gemma 2B的LoRA微调,由于参数量不大,我选择先不使用量化,直接在全精度(FP16)下进行,这对24GB显存的7900 XTX来说是可行的。

3. 模型微调核心:LoRA技术解析与实战

环境就绪后,进入核心环节——微调。我们采用LoRA(Low-Rank Adaptation)方法,它因其高效和轻量化成为微调大模型的首选技术之一。

3.1 LoRA原理浅析:为什么它能省显存?

在深入代码之前,有必要理解LoRA为什么有效。传统微调(Full Fine-Tuning)会更新模型的所有参数,对于动辄数十亿参数的大模型,这需要存储两份模型参数(原始参数和优化器状态),显存开销巨大。

LoRA提出了一种巧妙的“旁路”机制。它冻结预训练模型的所有原始参数,不进行更新。然后,在模型的某些关键层(通常是Transformer结构中的注意力模块的Query, Key, Value和输出投影层)旁,插入一组可训练的“低秩适配器”。具体来说,对于一个预训练权重矩阵 ( W \in \mathbb{R}^{d \times k} ),LoRA不直接改变 ( W ),而是用一个低秩分解来表示其更新量:( \Delta W = BA ),其中 ( B \in \mathbb{R}^{d \times r} ), ( A \in \mathbb{R}^{r \times k} ),秩 ( r \ll min(d, k) )。在正向传播时,实际执行的运算是 ( h = Wx + BAx )。

这样做的好处极其明显:

  1. 显存效率:只需存储和优化适配器参数 ( A ) 和 ( B ),其数量远少于原始参数。例如,对于70亿参数的模型,LoRA参数可能只有几百万到几千万,显存占用从几十GB降到几GB。
  2. 训练效率:由于大部分参数被冻结,只需要计算适配器部分的梯度,训练速度更快。
  3. 模块化与切换:不同的适配器(对应不同的任务)可以像插件一样在同一个基础模型上快速加载和切换,便于管理多任务。
  4. 减轻灾难性遗忘:因为基础模型参数不变,它原有的广泛知识得以保留,主要学习的是针对新任务的新特征。

3.2 数据准备与处理

我准备了一个关于某个垂直领域(比如咖啡知识)的指令微调数据集。数据格式遵循常见的指令-响应对话格式,每条数据是一个JSON对象,包含instructioninput(可选)、output字段。例如:

{ "instruction": "请介绍手冲咖啡的步骤。", "input": "", "output": "1. 研磨咖啡豆:将咖啡豆研磨至白砂糖粗细... 2. 湿润滤纸:用热水冲洗滤纸,去除纸味并温壶... 3. 闷蒸:注入少量热水,让咖啡粉充分排气,约30秒... 4. 分段注水:以画圈方式缓慢注入剩余热水... 5. 完成萃取:等待水滴完,移开滤杯,即可享用。" }

使用Hugging Facedatasets库加载和预处理数据:

from datasets import load_dataset dataset = load_dataset('json', data_files='coffee_qa.json')

接着,需要将文本数据转换为模型可接受的token ID序列。这里使用Gemma的tokenizer。

from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("google/gemma-2b-it") # 使用指令微调版本 tokenizer.pad_token = tokenizer.eos_token # 设置填充token def tokenize_function(examples): # 构建指令格式的文本 prompts = [] for inst, inp, outp in zip(examples['instruction'], examples['input'], examples['output']): if inp: prompt = f"<start_of_turn>user\n{inst}\n{inp}<end_of_turn>\n<start_of_turn>model\n" else: prompt = f"<start_of_turn>user\n{inst}<end_of_turn>\n<start_of_turn>model\n" prompts.append(prompt) # 将输出部分也拼接上,但只在计算loss时考虑输出部分 full_text = prompt + outp + tokenizer.eos_token examples['text'] = full_text return tokenizer(examples['text'], truncation=True, padding='max_length', max_length=512) tokenized_dataset = dataset.map(tokenize_function, batched=True, remove_columns=dataset['train'].column_names)

这里的关键是构建正确的对话格式(Gemma-IT模型特定的<start_of_turn>标签)和设置正确的损失掩码。我们只希望在模型输出部分(即output字段)计算损失,而在指令和输入部分忽略损失。这通常通过attention_masklabels的巧妙设置来实现,确保梯度只来自我们希望模型学习的响应部分。

3.3 LoRA微调脚本与参数详解

接下来是核心的训练脚本。我们使用peft库来方便地配置LoRA。

from transformers import AutoModelForCausalLM, TrainingArguments, Trainer from peft import LoraConfig, get_peft_model, TaskType import torch # 1. 加载基础模型 model = AutoModelForCausalLM.from_pretrained( "google/gemma-2b-it", torch_dtype=torch.float16, # 使用FP16节省显存 device_map="auto", # 让accelerate自动分配模型层到设备 trust_remote_code=False, ) # 2. 配置LoRA参数 lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, # 因果语言模型任务 r=8, # LoRA秩,即低秩矩阵的维度r。值越小参数量越少,但能力可能越弱。通常从8、16开始尝试。 lora_alpha=32, # 缩放因子,与学习率相关。通常设置为r的2-4倍。 lora_dropout=0.1, # LoRA层的dropout率,用于防止过拟合。 target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], # 将LoRA适配器注入到注意力层的这些线性模块中。 bias="none", # 是否训练偏置项。'none'表示不训练。 ) # 3. 将基础模型转换为PEFT模型 model = get_peft_model(model, lora_config) model.print_trainable_parameters() # 打印可训练参数数量,确认只有一小部分参数被激活 # 4. 定义训练参数 training_args = TrainingArguments( output_dir="./gemma-2b-coffee-lora", num_train_epochs=3, per_device_train_batch_size=4, # 根据显存调整。RX 7900 XTX 24G对于2B模型,batch_size=4是安全的。 gradient_accumulation_steps=4, # 梯度累积,模拟更大的batch size。 warmup_steps=100, logging_steps=10, save_steps=200, evaluation_strategy="no", # 如果没有验证集,设为"no" save_total_limit=2, learning_rate=2e-4, # LoRA学习率通常比全量微调大(例如1e-4到5e-4)。 fp16=True, # 使用混合精度训练,ROCm环境下确保torch版本支持。 remove_unused_columns=False, push_to_hub=False, # 本地训练,不上传 report_to="tensorboard", ) # 5. 创建Trainer并开始训练 trainer = Trainer( model=model, args=training_args, train_dataset=tokenized_dataset["train"], data_collator=DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False), ) trainer.train()

关键参数解析与调优经验:

  • r(秩):这是LoRA最重要的超参数之一。它决定了适配器的表达能力。对于2B的模型,r=816是常见的起点。如果任务简单或数据量少,可以尝试更小的r(如4);如果任务复杂或希望模型有更强的适应能力,可以增大到32甚至64,但这会增加参数量和过拟合风险。我的咖啡知识QA任务相对明确,r=8效果已足够。
  • lora_alpha:可以理解为适配器输出的缩放因子。在代码实现中,LoRA的输出会乘以alpha/r。因此,固定alpha,增大r会减小缩放比例。通常将alpha设为r的2倍或4倍是一个经验法则。我设置为32。
  • target_modules:指定将LoRA适配器加到哪些层。对于Decoder-only的模型(如Gemma, LLaMA),通常选择注意力机制中的q_proj,k_proj,v_proj,o_proj。有些研究也建议加入FFN层的gate_proj,up_proj,down_proj,但这会显著增加可训练参数量。对于初步实验,只加在注意力层是标准做法。
  • per_device_train_batch_sizegradient_accumulation_steps:实际有效的batch size是per_device_train_batch_size * gradient_accumulation_steps * GPU数量。受限于显存,单卡batch size可能只能设到1或2。通过梯度累积(gradient_accumulation_steps=4),我们可以每4步才更新一次权重,相当于模拟了batch size为4或8的训练,有助于训练稳定。需要权衡的是,累积步数越多,更新越慢,但梯度估计更准。
  • learning_rate:LoRA训练的学习率通常比全量微调大(例如1e-4到5e-4),因为只更新一小部分参数。我从2e-4开始,如果训练损失下降很慢或不降,可以尝试提高到3e-4或4e-4。

实操心得:监控显存与调整batch size。在ROCm环境下,使用rocm-smiwatch -n 1 rocm-smi来实时监控显存占用。如果训练开始不久显存就接近爆满,需要降低per_device_train_batch_size。如果显存还有富余但训练速度慢,可以尝试增大batch size以提高硬件利用率。另外,开启fp16混合精度训练能有效节省显存并加速,但要注意数值稳定性,如果训练中出现损失NaN,可以尝试关闭fp16或使用bf16(如果硬件支持)。

4. 模型合并与转换:为Ollama部署做准备

训练完成后,我们得到的是LoRA适配器权重(通常是一个adapter_model.binsafetensors文件),而不是一个完整的模型文件。Ollama目前主要支持加载完整的模型GGUF或类似格式的文件。因此,我们需要将LoRA权重与原始的基础模型合并,得到一个完整的、经过微调的新模型。

4.1 合并LoRA权重

使用peft库可以方便地合并权重:

from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer import torch # 加载原始基础模型 base_model = AutoModelForCausalLM.from_pretrained( "google/gemma-2b-it", torch_dtype=torch.float16, device_map="auto", ) # 加载训练好的LoRA适配器 model = PeftModel.from_pretrained(base_model, "./gemma-2b-coffee-lora/checkpoint-600") # 将适配器权重合并到基础模型中 merged_model = model.merge_and_unload() # 保存合并后的完整模型 merged_model.save_pretrained("./gemma-2b-coffee-merged") tokenizer.save_pretrained("./gemma-2b-coffee-merged")

这个过程会在指定目录下生成完整的PyTorch模型文件(pytorch_model.binconfig.json等)。

4.2 转换为Ollama支持的GGUF格式

Ollama推荐使用GGUF(GPT-Generated Unified Format)格式,这是一种为GGML推理引擎设计的二进制格式,支持量化,能在CPU和GPU上高效运行。我们需要使用llama.cpp项目的工具来进行转换。

首先,需要将PyTorch模型转换为GGUF支持的中间格式(通常是HF格式,我们已经有了)。然后使用llama.cppconvert.py脚本(或convert-hf-to-gguf.py)进行转换。

# 1. 克隆 llama.cpp 仓库 git clone https://github.com/ggerganov/llama.cpp cd llama.cpp # 2. 编译项目(确保已安装cmake等编译工具) make # 3. 将Hugging Face模型转换为GGUF格式 python convert-hf-to-gguf.py ../gemma-2b-coffee-merged --outtype f16 --outfile ../gemma-2b-coffee-f16.gguf

这里--outtype f16指定输出为FP16精度。你也可以选择量化版本以减小模型体积、降低部署资源需求,例如q4_0(4位整数量化)、q8_0(8位整数量化)等。量化会轻微损失精度,但能极大提升推理速度并降低内存占用。对于本地部署,q4_0q5_0是一个不错的权衡。

# 量化示例:将FP16模型量化为Q4_0格式 ./quantize ../gemma-2b-coffee-f16.gguf ../gemma-2b-coffee-q4_0.gguf q4_0

注意事项:版本兼容性llama.cpp和其转换脚本在快速迭代中,不同版本对模型架构(如Gemma)的支持程度可能不同。如果转换失败,常见错误是“不支持的张量类型”或架构识别错误,可以尝试回退到llama.cpp的某个稳定发布版本,或者查看其GitHub Issues中关于Gemma转换的讨论。我使用的是llama.cpp较新的提交,成功转换了Gemma 2B模型。

5. Ollama本地部署与模型服务化

Ollama的出现极大地简化了在本地运行大模型的过程。它类似于一个模型管理器,可以拉取、运行和管理各种GGUF格式的模型。

5.1 Ollama安装与自定义模型创建

首先在Linux上安装Ollama:

curl -fsSL https://ollama.com/install.sh | sh

安装后,Ollama服务会自动启动。默认情况下,Ollama会从官方仓库下载模型。但我们需要运行自己微调的模型,这就需要创建一个自定义的Modelfile

在包含我们GGUF模型文件的目录下,创建一个名为Modelfile的文本文件:

FROM ./gemma-2b-coffee-q4_0.gguf TEMPLATE """<start_of_turn>user {{ .Prompt }}<end_of_turn> <start_of_turn>model """ PARAMETER temperature 0.7 PARAMETER top_p 0.9 PARAMETER num_ctx 4096
  • FROM:指定模型文件的路径。这里使用相对路径指向我们量化后的GGUF文件。
  • TEMPLATE:定义对话模板。这是至关重要的一步,必须与模型训练时使用的格式一致。Gemma指令微调版本使用了特定的<start_of_turn>标签。这个模板确保了用户输入被正确包装,模型才能生成符合预期的回复。
  • PARAMETER:设置推理参数,如temperature(创造性,值越高越随机)、top_p(核采样,影响词汇选择多样性)、num_ctx(上下文长度)。

5.2 创建并运行自定义模型

使用ollama create命令基于Modelfile创建自定义模型:

ollama create my-gemma-coffee -f ./Modelfile

my-gemma-coffee是你给这个自定义模型起的名字。然后就可以像使用任何其他Ollama模型一样运行它:

ollama run my-gemma-coffee

运行后,会进入一个交互式对话界面。你可以输入问题测试,例如:“手冲咖啡应该如何闷蒸?” 模型应该会基于我们微调的数据集,给出专业、准确的回答。

5.3 集成与API调用

Ollama不仅提供命令行交互,还内置了一个HTTP API服务器(默认在11434端口),这使得它可以轻松集成到其他应用中。

启动Ollama服务后,可以通过curl进行测试:

curl http://localhost:11434/api/generate -d '{ "model": "my-gemma-coffee", "prompt": "请介绍手冲咖啡的步骤。", "stream": false }'

也可以使用Python的requests库进行调用,构建简单的应用程序:

import requests import json def ask_ollama(prompt, model="my-gemma-coffee"): url = "http://localhost:11434/api/generate" data = { "model": model, "prompt": prompt, "stream": False, "options": { "temperature": 0.7, "top_p": 0.9 } } response = requests.post(url, json=data) if response.status_code == 200: return response.json()['response'] else: return f"Error: {response.status_code}" answer = ask_ollama("拿铁和卡布奇诺有什么区别?") print(answer)

部署优化与问题排查

  1. GPU加速:Ollama在Linux下默认可能使用CPU推理。要启用AMD GPU加速,需要确保系统已安装ROCm,并在运行Ollama时设置环境变量OLLAMA_GPU_DRIVER=rocm。可以通过ollama run时添加--verbose标志,或在Ollama服务日志中查看是否成功加载了GPU后端。
  2. 显存不足:如果遇到显存不足错误,可以考虑使用量化程度更高的模型(如q4_0替代q8_0f16),或者在Modelfile中减少num_gpu_layers参数(该参数控制有多少模型层被卸载到GPU,减少它会让更多层留在CPU内存中)。
  3. 响应速度慢:首先确认是否使用了GPU。如果仍在CPU上运行,速度必然慢。其次,检查num_ctx是否设置过高,过长的上下文会消耗更多计算资源。对于简单的QA任务,2048可能就足够了。
  4. 模型回复格式不对:最常见的原因是TEMPLATE定义错误。务必与模型训练时使用的对话格式完全匹配。可以回顾训练数据集的构建方式和tokenize函数中的prompt模板。

6. 全流程回顾与进阶思考

走完从ROCm环境搭建、LoRA微调到Ollama部署的整个流程,相当于打通了大模型“炼”与“用”的本地化闭环。这个过程里,最深的体会是细节决定成败。无论是ROCm驱动版本的选择、LoRA超参数的调整、数据格式的严格对齐,还是GGUF转换时的版本兼容性,任何一个环节的疏忽都可能导致失败。

对于想复现类似流程的朋友,我的建议是:

  1. 环境隔离:强烈建议使用Conda或Docker创建独立的环境,避免系统级依赖冲突。ROCm的安装尤其容易受系统状态影响。
  2. 小步快跑,及时验证:不要一次性准备大量数据、设置过长epoch。先用一个极小的数据集(比如50条)跑通1个epoch,验证从训练到推理的整个流程是否正常。然后再逐步增加数据量、调整参数。
  3. 善用监控工具:训练时用rocm-smi监控GPU状态,用Tensorboard或W&B看损失曲线;部署时关注Ollama的日志和系统资源占用。
  4. 理解原理而非死记命令:明白LoRA为什么能省显存,理解对话模板的作用,知道量化会带来什么影响。这样当遇到问题时,你才有思路去排查,而不是盲目搜索错误信息。

这次实践也让我思考下一步的优化方向。例如,尝试QLoRA(量化版的LoRA)能否在保持性能的同时进一步降低显存需求?如何构建更高质量、多样化的指令微调数据集来提升模型泛化能力?如何将Ollama服务通过更友好的Web UI(如OpenAI WebUI或Chatbot UI)暴露出来,提供更好的用户体验?这些都是可以在现有基础上深入探索的课题。本地大模型应用的生态还在快速演进,但亲手搭建并调优一个专属模型所带来的掌控感和定制能力,是使用云端API无法比拟的。