公開モデルを自分のタスクに合わせたいが、フルファインチューニングにはGPUメモリが足りない。この壁を越えるための現実解が、LoRAとQLoRAです。LoRAとは、重み更新を低ランク行列で近似するファインチューニング手法である。更新するパラメータを多くの設定で数%以下、しばしば1%前後以下まで抑えられるため、手元の単一GPUでも大きめのモデルに独自データを学習させる道が開けます。
この記事では、なぜメモリが減るのかという仕組みから入り、transformers / peft / bitsandbytes / trl を組み合わせた現行APIの実装、LoraConfig のパラメータ選定、QLoRA特有の事故回避、学習後のデプロイまでを一気通貫でたどります。手を動かす中級エンジニアが、ご自身の環境で一通り動かせる状態になることをゴールにしました。
- LoRAは元の重みを凍結し、更新分を低ランク行列で近似する手法で、大きめのモデルでは更新パラメータを元の1%前後以下まで減らせる(比率はランクと対象層で変わる)
- QLoRAはベースを4bit量子化して凍結し、その上に高精度(BF16/FP16など)のアダプタを載せる二段構えでメモリをさらに削る。量子化誤差は生じるが、原論文は16bitファインチューニング相当の性能を報告している
- 実装はtransformers・peft・bitsandbytes・trlの4点セット。bitsandbytesはCUDA以外にAMD ROCm・Intel XPU・Apple Silicon・CPU向けのビルドも公式配布されているが、本記事の手順は学習を現実的な時間で回せるCUDA単一GPUを前提にする
本記事のコードとAPI名は概念理解のための最小構成です。peft・trl・transformers はエコシステムの更新が速く、引数名や既定値が変わることがあります。実運用時は各ライブラリの公式ドキュメントで最新のAPI・モデルID・対応バージョンを確認してください。この注意は以降の各章では繰り返しません。
LoRAとQLoRAでGPUメモリが減る仕組み
LoRAとQLoRAは、いずれもモデル全体を更新せず、ごく一部のパラメータだけを学習することでGPUメモリを大幅に削るPEFT(Parameter-Efficient Fine-Tuning、パラメータ効率の良いファインチューニング)の代表格です。まず「なぜ減るのか」を押さえておくと、後でハイパーパラメータを調整するときの判断が変わってきます。
フルファインチューニングでは、モデルの全パラメータを更新します。7Bクラスなら約70億の重みすべてが対象で、そこにオプティマイザの状態(勾配やモーメント)まで加わるため、必要なGPUメモリは元の重みの何倍にも膨らみます。数十GB規模のGPUでも単独では足りない、という状況が生まれるわけです。
LoRA — 重み更新を低ランク近似する
LoRAは、元の重み行列を凍結したまま、その「更新分」を2つの小さな行列の積で近似します。式で書くと次の形。
W' = W + ΔW = W + (α / r) · B · A
ここで W は元の重み行列で、学習中は固定します。更新分 ΔW を、A(r×k)と B(d×r)という縦横のどちらかが小さい2枚の行列に分解する。この r がランク(LoraConfig の r)で、元の次元よりずっと小さく取ります。α(lora_alpha)は更新の大きさを調整するスケーリング係数です。
要するに、学習されるのは小さな A と B だけ。元の重みが d×k 個のパラメータを持つのに対し、更新対象は r×(d+k) 個に減ります。次元が4096同士、r=16 のケースなら、更新するパラメータは元の1%未満に収まります。学習可能パラメータが減れば、勾配もオプティマイザ状態もその分だけで済むため、メモリ消費が桁で下がるという理屈。
もう一つ見落としがちな効果が、保存するチェックポイントの軽さです。更新したのはアダプタ部分だけなので、学習成果は数十〜数百MB程度のファイルで持ち運べます(モデルサイズ・ランク・対象層で変わります)。ベースモデル本体を丸ごと保存し直す必要がありません。
QLoRA — 4bit量子化したベースにアダプタを載せる
LoRAでも、凍結したベースモデルそのものはGPUメモリ上に展開する必要があります。アダプタが軽くても、土台となる重みを16bitで抱えている限り、ここがボトルネックになる。
QLoRAはこの土台を4bitに量子化して圧縮します。核になる工夫は、QLoRA論文(Dettmers et al., 2023)によれば次の3点。
| 技術 | 内容 |
|---|---|
| NF4(4bit NormalFloat) | 正規分布に従う重みに対して情報理論的に最適化された4bitデータ型 |
| Double Quantization | 量子化に使う定数自体をさらに量子化してメモリを追加削減 |
| Paged Optimizers | メモリスパイク時にオプティマイザ状態をCPU RAMへ退避 |
ベースを4bitに落として凍結し、その上に高精度のLoRAアダプタ(BF16やFP16、実装によってはFP32へ昇格される)を載せて学習する。この二段構えが、QLoRAがLoRAより一段深くメモリを削れる理由です。同論文では、この手法によって単一GPUで大規模モデルのファインチューニングを可能にしたと報告されています。これまで複数GPUを前提としていた規模を、1枚でどうにか回せる射程に引き込んだのが最大の意義でしょう。
トレードオフも把握しておきたいところ。ベースを4bitに量子化する分、モデルやタスクによってはBF16・FP16のLoRAより性能が下がる場合があります。一方でQLoRA原論文は16bitファインチューニングに匹敵する性能を報告しており、常に精度が落ちるとは限りません。最終的には検証データで比較するのが確実です。メモリが最優先ならQLoRA、GPUに余裕がありできるだけ素直に学習したいならLoRA、という使い分けが基本線です。
実装に使うライブラリ構成と前提環境
LoRA/QLoRAの実装は、Hugging Faceエコシステムで一般的に組み合わせる4つのライブラリを役割分担させるのが定番構成です(bitsandbytesはHugging Face製ではなく、統合して使われる別プロジェクトです)。それぞれが何を担うのかを整理しておきます。
- transformers:ベースモデルとトークナイザの読み込みを担当。
AutoModelForCausalLM.from_pretrained()でモデルを取得する - peft:LoRAアダプタの本体。
LoraConfigで設定し、get_peft_model()でベースモデルに差し込む - bitsandbytes:4bit量子化を提供。QLoRAで
BitsAndBytesConfigを通して呼び出す - trl:学習ループを担う。
SFTTrainer(Supervised Fine-Tuning Trainer)がデータ整形から学習まで面倒を見てくれる
インストールは次の通り。バージョンは相互依存があるため、最新の組み合わせは各公式で確認してください。
pip install -U transformers peft bitsandbytes trl datasets accelerate
すでに古いtransformersが入っている環境では、-Uを付けないと更新されません。Qwen3.5のような新しめのチェックポイントは新しいtransformersを要求するため、ここは省略しないでください。動かした組み合わせは次のコマンドで控えておくと、あとでAPIが変わったときに切り分けが早くなります。
python -c "import transformers, peft, trl, bitsandbytes; \
print(transformers.__version__, peft.__version__, trl.__version__, bitsandbytes.__version__)"
前提となる環境をはっきりさせておきます。想定はPython 3.10以上、AutoModelForCausalLM を触ったことがある程度のTransformers基礎知識、そしてCUDAが使える単一GPU。bitsandbytesはNVIDIA CUDAのほか、AMD ROCm・Intel XPU・Apple Silicon・Intel Gaudi・CPU向けのビルドも公式に配布されています。ただしバックエンドごとに使える機能には差があり、同じコードがCUDA・ROCm・XPU・Apple Siliconで完全に同じように動くとは限りません。本記事のコード例はCUDAを前提に書いており、学習の演算量を考えるとCPUだけの環境で現実的な時間に終えるのは難しい。NVIDIA GPUを想定してください。なおBF16に対応しないGPUではFP16へ落とす必要がありますが、モデル読み込みのdtypeだけを変えるとTrainer側がBF16のままになることがあります。後述のコードではtorch.cuda.is_bf16_supported()で判定した値をモデルとSFTConfigの両方へ渡し、設定元を揃えています。
本記事のコード例では、Qwen/Qwen3.5-0.8B のような小型のモデルをベースに使います。このチェックポイントは視覚エンコーダを含むマルチモーダル構成(Qwen3_5ForConditionalGeneration)ですが、本記事は AutoModelForCausalLM でテキスト側の言語モデル部分だけを読み込み、テキストデータで学習します。マルチモーダルの学習は扱いません。手元で軽く回して挙動を確かめる用途に向いているためで、r・モデルサイズ・量子化設定を差し替えれば、同じコードのままより大きなモデルへスケールできます。まず小さく通してから広げる、という進め方が安全です。
必要なGPUメモリの見積もりや、そもそもファインチューニングにどんなハードが要るのかは、姉妹サイトの手元のGPUでローカルLLMをファインチューニングできるかでVRAMとRAMの考え方を整理しています。学習用途の環境を組む前に一度目を通しておくと判断しやすいでしょう。
LoRAでファインチューニングする実装手順
LoRAの実装は、LoraConfig を定義し、get_peft_model() でベースに差し込み、SFTTrainer で学習を回すという3ステップに集約されます。まずは量子化なしの素のLoRAで全体の流れをつかみましょう。
最初にベースモデルとトークナイザを読み込みます。ここは通常のTransformersと同じ。
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model_id = "Qwen/Qwen3.5-0.8B"
# BF16 非対応GPUでは FP16 へ落とし、Trainer 側の設定もこの値に合わせる
use_bf16 = torch.cuda.is_available() and torch.cuda.is_bf16_supported()
compute_dtype = torch.bfloat16 if use_bf16 else torch.float16
# device_map は推論向けの配置指定。単一GPU学習では付けず Trainer に任せる
model = AutoModelForCausalLM.from_pretrained(
model_id,
dtype=compute_dtype,
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
次にLoRAの設定です。peft の LoraConfig でランクや対象層を決め、get_peft_model() でベースモデルにアダプタを取り付けます。
from peft import LoraConfig, get_peft_model
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules="all-linear",
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
最後の print_trainable_parameters() が地味に効きます。実行すると「trainable params」と「all params」、そしてその比率が表示される。一般的なLoRA設定なら、この比率は全体の1%前後かそれ以下に収まることが多い。ただし比率はr・対象層・modules_to_saveなどで変わるため、1%という数字自体を合否の基準にはせず、意図した層だけが学習対象になっているかを確認してください。
学習ループは trl の SFTTrainer に任せます。データセットは会話形式やテキスト形式に整えておき、設定は SFTConfig にまとめる形が現行の書き方です。
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
# TRL が受け取れるのは text 列 / messages 列 / prompt-completion 形式のいずれか
dataset = load_dataset("your/dataset", split="train")
training_args = SFTConfig(
output_dir="./lora-out",
num_train_epochs=1,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
learning_rate=2e-4,
logging_steps=10,
bf16=use_bf16,
fp16=not use_bf16,
)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
processing_class=tokenizer, # 省略するとモデルIDから Processor が自動ロードされる
)
trainer.train()
trainer.save_model("./lora-adapter")
save_model() で保存されるのはアダプタ部分だけ。ベースモデル本体は含まれないため、成果物は非常に軽く仕上がります。ここまでが素のLoRAの基本形。量子化を足すとQLoRAになりますが、その前に固有の落とし穴を押さえておく必要があります。
QLoRA特有の設定と落とし穴
QLoRAは、ベースモデルを4bitでロードする設定を加え、量子化モデル用の前処理を挟むことで成立します。手順自体はLoRAに数行足すだけですが、知らないと確実にハマる論点がいくつかある。ここは事故回避の視点で見ていきます。
4bitロードの設定は BitsAndBytesConfig で行います。NF4を指定し、計算時のデータ型とDouble Quantizationを設定するのが定番です。
from transformers import BitsAndBytesConfig
import torch
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
)
量子化してロードしたモデルには、LoRAを載せる前にひと手間が要ります。peft の prepare_model_for_kbit_training() を通すこと。これで量子化層の一部をLoRA学習に適した状態へ整え、勾配チェックポイントとの整合も取ってくれます。
from peft import prepare_model_for_kbit_training, LoraConfig, get_peft_model
# k-bit 学習向けの前処理。勾配チェックポイントもここで有効化する
model = prepare_model_for_kbit_training(
model,
use_gradient_checkpointing=True,
)
# TRL の挙動に依存せず、モデル側でも明示しておく
model.config.use_cache = False
model = get_peft_model(model, lora_config)
勾配チェックポイント(gradient checkpointing)は、順伝播の中間結果を保持せず逆伝播時に再計算することでメモリを節約する仕組みです。QLoRAではメモリを最後まで削り切りたいので有効にするのが基本。ただしKVキャッシュ(use_cache)と併用できないモデルが多いため、本記事では学習前に model.config.use_cache = False を明示します。有効なまま学習に入ると警告が出て進まない、という詰まり方が起きます。現行のSFTTrainerにも一部の学習経路で use_cache=False を渡す処理はありますが、すべての設定・損失関数・TRLバージョンで同じ経路を通る保証はないため、TRLの挙動に依存させずモデル側で立てておくのが確実です。なお推論でキャッシュを使いたい場合は、アダプタをロードしたあとで戻せます。
model.config.use_cache = True
model.eval()
もう一点、QLoRA原論文が挙げるPaged Optimizersは、4bitでロードしただけでは有効になりません。SFTConfigで明示的に指定して初めて使われます。
training_args = SFTConfig(
output_dir="./qlora-out",
num_train_epochs=1,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
learning_rate=2e-4,
logging_steps=10,
optim="paged_adamw_32bit",
gradient_checkpointing=False, # PEFT 側で有効化するので設定元を一本化する
bf16=use_bf16,
fp16=not use_bf16,
)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
processing_class=tokenizer,
)
trainer.train()
trainer.save_model("./qlora-adapter")
optim を指定しない場合は通常のオプティマイザで学習が進みます。前掲のLoRA側のコードもPaged Optimizersは使っていません。勾配チェックポイントはSFTConfig側でも既定で有効になるため、PEFT側と設定元が二重にならないよう片方へ寄せています。ここはライブラリのバージョンで挙動が変わりやすい箇所なので、寄せ方を変えたら実際に1エポック回して確かめてください。
最大の落とし穴が、アダプタのマージです。
この点を知らずに merge_and_unload() を4bitモデルへ呼ぶと、処理が通ってもアダプタを分離したまま推論した場合と生成結果が一致しないことがあります。デプロイ段階で気づくと手戻りが大きいので、学習を始める前に「最終的にマージするのか、アダプタのまま運用するのか」を決めておくと安全です。
LoraConfigの主要パラメータの選び方
LoraConfig の設定次第で、学習の表現力とメモリ消費のバランスが変わります。ここでは主要なパラメータの意味と、どう決めればよいかの基準を整理します。
| パラメータ | 役割 | 上げると | 選び方の目安 |
|---|---|---|---|
r(ランク) |
差分行列の大きさ | 表現力↑・学習コスト↑ | まず8〜16。タスクが複雑なら32以上を試す |
lora_alpha |
更新のスケーリング | 更新の効き↑ | r の2倍を起点にする慣例が多い |
target_modules |
アダプタを載せる層 | 対象が広い→表現力↑ | "all-linear" が扱いやすい出発点(既定値ではない) |
lora_dropout |
過学習抑制 | 正則化↑ | 0.05前後から |
use_rslora |
安定化の改良版 | 高ランクで安定 | 大きい r を使うなら検討 |
use_dora |
分解の高精度化 | 精度↑・計算↑ | 精度を詰めたいとき有効 |
r と lora_alpha は連動して考えます。r はアダプタの容量、lora_alpha はその効き具合を決めるつまみ。実際の更新は α/r の比率でスケールされるため、r を上げたら alpha も比例させて調整すると挙動が読みやすくなります。慣例として alpha = 2 × r を起点に置く書き方をよく見かけますが、絶対のルールではありません。まず小さな r で回してから、精度が頭打ちなら上げていく進め方が無駄がない。
target_modules="all-linear" は、モデル内のすべての線形層を自動でアダプタ対象にする指定です。層ごとに名前を手で列挙する手間がなく、モデル構造が変わっても書き換え不要という利点があります。かつては q_proj や v_proj などアテンション層だけを狙う指定が主流でしたが、全線形層に広げたほうが精度が伸びる報告が増え、QLoRAスタイルの学習では有力な出発点とされています。ただしLoraConfigの既定値ではなく、常に最適とも限りません。
use_rslora と use_dora は、精度をさらに詰めたいときの追加オプション。use_rslora(Rank-Stabilized LoRA)はスケーリングの計算を改良し、高いランクでも学習を安定させます。use_dora(Weight-Decomposed LoRA)は重みを方向と大きさに分解して更新することで、低ランクでも精度を上げやすくする手法。どちらも計算コストと引き換えなので、標準のLoRAで結果が物足りないときに一つずつ試すのが現実的です。最初から全部盛りにする必要はありません。
学習後のアダプタ保存・推論・本番デプロイ
学習後の運用は、アダプタのまま使うか、ベースにマージして1つのモデルとして配布するかの二択で考えます。どちらが正解かは、運用要件で決まります。
保存されたアダプタは、ベースモデルと組み合わせて推論します。ベースを読み込み、その上にアダプタを重ねる形。
from transformers import AutoModelForCausalLM
from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained(model_id, dtype="auto", device_map="auto")
model = PeftModel.from_pretrained(base, "./lora-adapter")
# そのまま推論に使える
この「アダプタのまま運用」方式には、実務で効く利点があります。1つのベースモデルを共有しつつ、タスクごとに違うアダプタを差し替えられる。ベースを何度もコピーせず、軽いアダプタだけを何種類も用意して切り替えられるため、複数用途を1台のサーバーで捌く構成に向いています。
一方、推論を単純化したい、あるいはアダプタ非対応の推論環境へ載せたい場合は、マージして単一モデルにする選択肢があります。
merged = model.merge_and_unload()
merged.save_pretrained("./merged-model")
前章で触れた通り、QLoRAで学習した場合はここに注意が要ります。精度を重視するなら、ベースをBF16やFP16で読み直してからアダプタを載せてマージし、必要に応じて改めて量子化する。4bitのままマージすると丸め誤差の影響を受けます。配布のしやすさを取るか、メモリと差し替え柔軟性を取るか。この判断を学習前に固めておくと、後工程がスムーズです。
LoRA一択にしない — PEFT手法を選ぶ視点
LoRAは強力で普及していますが、あらゆるケースで最良とは限らず、目的と制約に応じてPEFT手法を選ぶ視点を持つと選択肢が広がります。Hugging Faceの技術ブログ「Beyond LoRA」は、単一のPEFT手法だけに言及しているモデルカードのうち98.4%がLoRAに言及していたと報告しています。公開されているファインチューニング全体の98%という意味ではありませんが、事実上のデフォルトになっている状況はうかがえます。だからこそ、他の手も知っておく価値がある。
PEFTを採用すること自体に、LoRAに限らない副次的なメリットがあります。同ブログが挙げるのは主に次の3点。
- チェックポイントサイズの縮小:更新するのは一部だけなので、成果物が軽く保存・配布しやすい
- 破滅的忘却への耐性:ベースを凍結するため、元モデルが持っていた汎用的な能力を失いにくい
- 同一ベースからの複数提供:1つのベースモデルに対し、用途別のアダプタを複数用意して使い分けられる
破滅的忘却(catastrophic forgetting)とは、新しいデータで学習するうちに、モデルが以前できていたことを忘れてしまう現象を指します。フルファインチューニングで起きやすい問題で、ベースを固定するPEFT系はここに強い。
peft は複数のPEFT手法を統一されたAPIで提供し、Transformersやbitsandbytesなどで量子化したモデルへのアダプタ学習にも対応しています。つまり、まずLoRA/QLoRAで動かして基準を作り、精度やメモリで物足りなさが出たら別手法を検討する、という段階的な進め方が取れる。最初から完璧な手法を選ぼうと悩むより、動く構成で回してから制約に合わせて調整するほうが、結果的に早く目的に着地します。
まとめ
LoRAとQLoRAは、単一GPUで公開モデルを自分のタスクへ適応させるための現実解です。判断の軸を整理します。
手法の選択は、GPUメモリの余裕で決まります。余裕があればLoRA、メモリを最優先するならベースを4bitに落とすQLoRA。ただしQLoRAは量子化誤差が乗る分、モデルやタスクによってはBF16・FP16のLoRAに劣る場合がある点は織り込んでおきます。原論文は16bit相当の性能を報告しており、常に落ちるとは限りません。
実装の勘所は2つ。LoRAではget_peft_model()の後にprint_trainable_parameters()を実行し、意図した層だけが学習対象になっているかを確認する(比率は設定で変わるので1%という数字自体は合否基準にしない)。QLoRAではprepare_model_for_kbit_training()を挟んで勾配チェックポイントをここで有効化し、SFTConfig側はgradient_checkpointing=Falseにして設定元を一本化する。あわせてmodel.config.use_cache = Falseを明示する。
デプロイ前に必ず決めておきたいのが、アダプタのまま差し替え運用するか、ベースにマージして単一モデルで配るか。量子化したベースへ直接マージすると丸め誤差の影響を受けるため、精度重視なら非量子化で読み直してマージする前提を学習前に固めておくと手戻りを防げます。
着手は小型モデルのLoRAから。一通り通して学習対象が意図どおりだと確認できたら、rとモデルサイズを上げてQLoRAへ広げる道筋が見えてきます。物足りなさが出たらuse_rslora・use_doraや他のPEFT手法へ、という段階的な進め方が結局は早く着地します。
よくある質問
Q. QLoRAとLoRAはどちらを選べばいいですか?
GPUメモリに余裕があり素直に学習したいならLoRA、メモリを最優先で削りたいならQLoRAが基本の使い分けです。QLoRAはベースを4bitに量子化する分、モデルやタスクによってはBF16・FP16のLoRAより性能が下がる場合があります。ただし原論文は16bitファインチューニングに匹敵する性能を報告しており、常に劣るわけではありません。まずLoRAで基準を作り、メモリが足りなければQLoRAへ進むのが無難です。
Q. CPUだけの環境でもQLoRAは試せますか?
動かすこと自体はできます。bitsandbytesはCPU向けのビルドも公式に配布されているためです。ただし学習は演算量が大きく、CPUだけで現実的な時間に終える用途には向きません。本記事の手順もCUDAが使えるNVIDIA GPUを前提に書いています。挙動を確かめるだけなら小型モデルで試せますが、実際に学習を回すなら単一GPUの環境を用意してください。
Q. ランク r はいくつに設定すべきですか?
まず8〜16から始めるのが無難です。タスクが複雑で精度が頭打ちなら32以上を試します。rを上げると表現力は増えますが学習コストとメモリも増えるため、lora_alphaをrの2倍を起点に連動させながら、小さい値から段階的に調整すると挙動が読みやすくなります。
Q. target_modulesは何を指定すればいいですか?
"all-linear"が扱いやすい出発点です(LoraConfigの既定値ではありません)。モデル内のすべての線形層を自動でアダプタ対象にするため、層名を手で列挙する手間がなく、モデル構造が変わっても書き換え不要です。かつてはアテンション層だけを狙う指定が主流でしたが、全線形層に広げたほうが精度が伸びる報告が増えました。ただし常に最適とは限らないので、対象層は結果を見て調整してください。
参考資料
- Hugging Face 公式: PEFT ドキュメント
- Hugging Face 公式: TRL(SFTTrainer)ドキュメント
- Hugging Face 公式: bitsandbytes ドキュメント
- Hugging Face 公式: bitsandbytes インストールガイド(対応バックエンド一覧)
- Hugging Face Blog: Beyond LoRA — Can you beat the most popular fine-tuning technique?

