はじめに
自宅PC(RTX 2080Ti 11GB)で、Qwen3-4B-BaseのQLoRA継続学習を回しました。WSLを使わないWindowsネイティブ環境で、です。結論から言うと学習自体は3000ステップ・約55分で完走できたのですが、そこに至るまでに7つ以上の罠を踏みました。
エラーメッセージだけでは原因にたどり着けないものが多いので、症状と対策を実測値つきでまとめます。
個人環境での実証に基づきます。お使いのバージョン組み合わせ(特にtrl / transformers / unsloth)によって挙動が変わる可能性があります。レジストリ操作は自己責任で。
TL;DR
| 罠 | 症状 | 対策 |
|---|---|---|
| 環境変数の汚染 |
pip install が意図しないvenvに入る / importがCPU版torchを拾う |
env -u VIRTUAL_ENV -u PYTHONPATH を付けて実行 |
| unslothがtorchを上書き | CUDA版がCPU版に戻される | unslothインストール後にtorch(CUDA版)を入れ直す |
| TDR(ドライバ) | 学習開始直後に CUDA error: unknown error で即死 |
レジストリで TdrDelay=60
|
| チェックポイント保存 | save_steps到達でPicklingErrorクラッシュ |
save_strategy="no" で中間保存を無効化 |
| max_seq_length無視 | 設定値と違い1024で学習され、途中でクラッシュ |
args=SFTConfig(...) で必ず指定 |
| packing | max_seq_lengthを超える列が生成されクラッシュ | packing=False |
| fused_loss |
Expected input batch_size ... to match でクラッシュ |
fused_loss=False |
| VRAMの使い切り | 99%まで詰めると逆に5倍遅くなる | 90%前後が最適点 |
環境
| 項目 | 内容 |
|---|---|
| OS | Windows 11(ネイティブ。WSL不使用) |
| GPU | RTX 2080Ti 11GB |
| ベースモデル | Qwen3-4B-Base(4bit QLoRA継続学習) |
| スタック | unsloth + trl 0.24系 + transformers 5.x系 |
| 最終安定構成 | batch=4 × grad_accum=2 / packing=False / fused_loss=False / save_strategy="no" |
| 実測性能 | 1.3 s/it / GPU使用率96% / 3000ステップ ≈ 55分 |
罠1: 環境変数が別のvenvを指している
エージェントやツール経由でコマンドを実行すると、VIRTUAL_ENV や PYTHONPATH が別の仮想環境を指したままになっていることがあります。この状態で pip install すると違うvenvにインストールされ、importは思わぬ場所のCPU版torchを拾います。
env -u VIRTUAL_ENV -u PYTHONPATH .venv\Scripts\python.exe -m pip install ...
環境変数を無効化して実行するのが確実です。「同じコマンドを打ったのに結果が違う」と感じたら、まずこれを疑ってください。
罠2: unslothがtorchをCPU版に上書きする
unslothのインストーラが依存解決でtorchを入れ直し、CUDA版がCPU版に置き換わることがありました。対処は順序です。
- 先にunslothをインストール
- その後でCUDA版torchを入れ直す
.venv\Scripts\python.exe -m pip uninstall -y torch
.venv\Scripts\python.exe -m pip install torch==2.x.x --index-url https://download.pytorch.org/whl/cu128
「学習が始まったのにGPU使用率0%」というときはだいたいこれです(torch.cuda.is_available() で確認)。
罠3: 学習開始直後の CUDA error: unknown error はTDR
学習を開始した数秒後に CUDA error: unknown error で落ちる場合、GPU自体の故障ではなく**WindowsのTDR(タイムアウト検出と復帰)**が原因のことがあります。WDDMドライバは2秒以上GPUが応答しないとリセットをかけるため、初期化の重い処理で引っかかります。
管理者権限でレジストリを設定して猶予を60秒に延ばすと解決しました(再起動不要、次回起動から有効)。
reg add "HKLM\SYSTEM\CurrentControlSet\Control\GraphicsDrivers" /v TdrDelay /t REG_DWORD /d 60 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Control\GraphicsDrivers" /v TdrDdiDelay /t REG_DWORD /d 60 /f
罠4: 中間チェックポイントでPicklingError
trl 0.24系 + transformers 5.x系の組み合わせで、save_stepsに到達した瞬間に以下でクラッシュしました。
_pickle.PicklingError: Can't pickle <class 'trl.trainer.sft_config.SFTConfig'>: it's not the same object as trl.trainer.sft_config.SFTConfig
save_only_model=True でも直りませんでした。中間保存を完全に無効化するのが確実です。
SFTConfig(
...
save_strategy="no", # 中間保存なし
save_steps=999999, # 予備
)
学習完了後に model.save_pretrained() で一括保存すれば実用上困りません。なお、クラッシュ時に残る checkpoint-N/ は不完全(trainer_state.json欠落)で、resume_from_checkpoint はエラーになります。削除してクリーン再開が安全です。
罠5: max_seq_lengthはTrainerの引数では無視される
# NG: 無視される
SFTTrainer(model=model, tokenizer=tok, max_seq_length=512, ...)
これで渡すとデフォルト設定(1024)でトークナイズされ、しばらくして
Input IDs of shape torch.Size([4, 1024]) > max_seq_length 512
でクラッシュします。SFTConfigに書いて args= で渡すのが正解です。
# OK
SFTTrainer(model=model, tokenizer=tok, args=SFTConfig(max_seq_length=512, ...))
設定が「効いているか」は実行ログで必ず確認しましょう。
罠6: packing=Trueは上限を超える列を作る
メモリ効率のためのpackingを有効にすると、max_seq_lengthを超えるパック列(例: 1024トークン)が生成され、罠5と同じエラーで落ちます。素直に packing=False にしましょう。
罠7: fused_lossのバッチサイズ不整合
unslothの高速化オプション(fused loss)を有効にすると、torch._dynamo のfake tensor検査で
Expected input batch_size (x) to match target batch_size (y)
というエラーで死ぬことがありました。fused_loss=False で回避します。
ボーナス: VRAMを99%まで使うと遅くなる
「VRAMを余らせるのは勿体ない」と思ってバッチを限界まで上げると、メモリ断片化で逆に遅くなります。
| バッチ | 実測速度 |
|---|---|
| 4 | 1.3 s/it(最安定) |
| 6 | 3.0 s/it → さらに悪化して15 s/it |
GPU使用率ベースで90%前後が最適点でした。余白は逃げではなく、性能の一部です。
まとめ
- QLoRAは11GBクラスのVRAMでも現実的に回る(batch=4 / ~55分 / 3000ステップ)
- ハマりどころの多くは「ライブラリのバージョン組み合わせ」と「環境変数」と「ドライバ設定」
- エラーが出たら、設定値ではなく「実際に使われている値」をログで確認する
最後に一番大事な教訓です。保守的な設定(batch=2など)は「遅い」ので、きちんと測って攻めた設定を探す。ただし攻めすぎると断片化で負ける。計測がすべてでした。