PEFTを使用したLoRA (Windows10（WSL2不使用） bitsandbytes==0.37.0使用)

## （2023/07/06追記）
より新しい記事「[Windows10でのPEFTの実行方法 (bitsandbytes-windows-webui使用)](https://qiita.com/selllous/items/c4880767da45173227c1)」があります。  
こちらをご参照ください。  

## 概要

### 背景

- Alpaca-LoraやPEFTを使用したLoRAが実施したい
  - これらの利用には、bitsandbytes==0.37.0の使用が必要
  - しかし、bitsandbytesはwindows OSには対応していない
- [こちらの記事](https://note.com/kohya_ss/n/n47f654dc161e)の方法をもとに設定すれば、bitsandbytes=0.35.0をwindowsで使用することできる模様
  - しかし、0.37.0は上記記事の方法では設定できない
- WSL2を使用すれば、おそらくbitsandbytesの使用が可能なはず
  - vscode上でのwsl2への切り替えが面倒なので、wsl2を使用せずとも実行できる環境を構築したい

### このページでできること

- windows OSでbitsandbytes==0.37.0の使用ができる
- windows OSでPEFTが実行できる

### このページの対象者

- venvを使用したpython仮想環境の構築方法がわかる（本ページでは説明しません）
  - もし要望が多ければ仮想環境作成部分も追記しますので、コメント欄に投稿してください
  - venv+pyenvは[こちら](https://tech.walkit.net/python-venv-pyenv-vsc)が非常に参考になります
- CUDA Toolkitのインストール方法がわかる

### 謝辞

本記事は、以下の皆様の記事をもとに作成しております。  

- Kohya S.様「[8-bit optimizer（bitsandbytes）をWindows（非WSL）で動かす](https://note.com/kohya_ss/n/n47f654dc161e)」
- ぷらむらいす(PlumRice)様「[WSL2でもAlpaca-LoRAを使いたい！(RAMが足りなくて挫折)](https://zenn.dev/test_myname/articles/dc70f5aa7615b7)」
- npaka様「[Google Colab で PEFT による大規模言語モデルのファインチューニングを試す](https://note.com/npaka/n/n932b4c0a2230)」
  
情報をご提供いただきました皆様に心より感謝申し上げます。

## 使用環境

|項目|内容|備考|
|:--|:--|:--|
|OS|Windows 10|WSL不使用|
|python|ver.3.9.6|pyenv使用|
|python仮想環境|venv||
|実行環境|ローカル|Google Colab不使用|
|GPU|RTX4070（VRAM 12GB）|PEFTでのLoRAに使用|
|pythonライブラリ|transformers==4.29.0<br>bitsandbytes==0.37.0<br>...etc|詳細は[こちら](https://github.com/selllous/QiitaHelp/blob/main/PEFTonWindows10/requirements.txt)|

## 環境構築

### bitsandbytes==0.37.0のwindowsでの設定方法

1. python仮想環境構築し、仮想環境を起動  
   - python versionは3.9.6を使用（他は未検証）  
   - 仮想環境はvenv（+pyenv）を使用  
2. venvのpipをupgradeしておく  
   pipが古い場合、pip listを打つと「WARNING: You are using pip version 21.1.3; however, version 23.1.2 is available.」等と表示される。そのあとに表示されるコマンドを実行して、pipをupgradeする。
3. 以下のコマンドを実行  
   ```powershell
   pip install bitsandbytes==0.37.0
   ```
4. インストールしたbitsandbytesのライブラリフォルダを、vscode等エディタで開く  
    著者の場合は、venv環境をE:\venvにまとめており、今回の仮想環境はpeftという名称で構築しています。  
   この場合、エディタで開く場所は\peft\Lib\site-packages\bitsandbytes直下になります。   
    ![7d981aceb4b6269d752f60392711931b01b7a632428b4c37d889df6f4db615bb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124929/7414196c-7663-89d2-5e12-8af0bbd54f45.png)

5. cuda_setup/main.pyの261行目を下記のように変更  
   下図262行目のように、この1行をif文の外に出します  
   ![092cadb172d3e4943fc582be2ca659f0072d9b3f8c24f180c1a0ec81e2b76ca0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124929/033111a7-185d-faf1-1ce5-3a0487fa44a8.png)

6. cuda_setup/main.pyの368行目をコメントアウトし、369行目に以下の記述を追加  
   ```python
   if torch.cuda.is_available(): return 'libbitsandbytes_cuda116.dll', None, None, None, None
   ```
   ![13eb398b0bc1dbc09e5840b07f956c5380f5041518a9da5b53bcb637ec400344.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124929/14380836-8f0f-d52f-2112-90d386c6ac04.png)

7. cuda_setup/main.pyの122行目と126行目をコメントアウトし、下図123と127行目の記述を追加  
   binary_pathにstr()を追加しています  
   ```python
   self.lib = ct.cdll.LoadLibrary(str(binary_path))
   ```
   ![b634f7b73c0f4289d5aac008675a1871635d712bfda138de8f5ee3e71b307026.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124929/36d0b240-4544-4f33-db23-d9b3684bb4fe.png)

8. [こちらのページ](https://github.com/DeXtmL/bitsandbytes-win-prebuilt)からlibbitsandbytes_cpu.dllとlibbitsandbytes_cuda116.dllをダウンロードし、仮想環境のbitsandbytesライブラリのフォルダ直下に入れる  
   著者の場合、dllを入れる場所は、\peft\Lib\site-packages\bitsandbytes直下になります。  
    ![7d981aceb4b6269d752f60392711931b01b7a632428b4c37d889df6f4db615bb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124929/6349f139-e7e8-d440-c1c3-1bc42b959c02.png)

9.  venv仮想環境を起動


### その他の環境設定

1. パッケージのインストール
    ```
    pip install -q datasets accelerate loralib transformers==4.29.0
    pip install -q git+https://github.com/huggingface/peft.git
    ```
2. gpu版torchのインストール（cu117使用）  
   pip install numpy --pre torch torchvision torchaudio --force-reinstall --index-url https://download.pytorch.org/whl/nightly/cu117


### bitsandbytesの動作確認

先ほどの仮想環境上で以下のコマンドを打ち込んで、エラーが出ないことを確認します。  

```powershell
python
>>>import bitsandbytes
```

出力が以下の表示であればOK。  

```powersyell
===================================BUG REPORT===================================
Welcome to bitsandbytes. For bug reports, please submit your error trace to: https://github.com/TimDettmers/bitsandbytes/issues
================================================================================
```

### 動作しない場合

[こちらのgithubリポジトリ](https://github.com/selllous/QiitaHelp/tree/main/PEFTonWindows10)に著者の動作確認済みpython環境のrequirements.txtを置いています。  
こちらをダウンロードし、以下の手順でpython環境を構築しなおしてみてください。  

1. [bitsandbytes==0.37のwindowsでの設定方法](#bitsandbytes==0.37のwindowsでの設定方法)の手順を実施
2. 以下のコマンドを実行
   ```powershell
   pip install -r requirements.txt
   pip install numpy --pre torch torchvision torchaudio --force-reinstall --index-url https://download.pytorch.org/whl/nightly/cu117
   ```

## PEFTを使用したLoRAの実施

基本的に、npaka様の[Google Colab で PEFT による大規模言語モデルのファインチューニングを試す](https://note.com/npaka/n/n932b4c0a2230)に記載のプログラムと同一です。  
一部、trainerに渡すargs引数のper_device_train_batch_sizeを4→2に変更しています。  

ソースコードは以下の通りです。こちらを、先ほど作成したpython仮想環境にて、python main.pyで実行してください。  
LoRAの学習時間は、筆者環境において、13分程度でした。  

```python
# モデルの読み込み
import os
os.environ["CUDA_VISIBLE_DEVICES"]="0"
import torch
import torch.nn as nn
import bitsandbytes as bnb
from transformers import AutoTokenizer, AutoConfig, AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "facebook/opt-6.7b", 
    load_in_8bit=True, 
    device_map='auto',
)

tokenizer = AutoTokenizer.from_pretrained("facebook/opt-6.7b")

for param in model.parameters():
  param.requires_grad = False  # モデルをフリーズ
  if param.ndim == 1:
    # 安定のためにレイヤーノルムをfp32にキャスト
    param.data = param.data.to(torch.float32)

model.gradient_checkpointing_enable()
model.enable_input_require_grads()

class CastOutputToFloat(nn.Sequential):
  def forward(self, x): return super().forward(x).to(torch.float32)
model.lm_head = CastOutputToFloat(model.lm_head)

def print_trainable_parameters(model):
    """
    モデル内の学習可能なパラメータ数を出力
    """
    trainable_params = 0
    all_param = 0
    for _, param in model.named_parameters():
        all_param += param.numel()
        if param.requires_grad:
            trainable_params += param.numel()
    print(
        f"trainable params: {trainable_params} || all params: {all_param} || trainable%: {100 * trainable_params / all_param}"
    )


from peft import LoraConfig, get_peft_model 

config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

model = get_peft_model(model, config)
print_trainable_parameters(model)

import transformers
from datasets import load_dataset
data = load_dataset("Abirate/english_quotes")
data = data.map(lambda samples: tokenizer(samples['quote']), batched=True)

trainer = transformers.Trainer(
    model=model, 
    train_dataset=data['train'],
    args=transformers.TrainingArguments(
        per_device_train_batch_size=2, 
        gradient_accumulation_steps=4,
        warmup_steps=100, 
        max_steps=200, 
        learning_rate=2e-4, 
        fp16=True,
        logging_steps=1, 
        output_dir='outputs'
    ),
    data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False)
)
model.config.use_cache = False  # 警告を黙らせます。 推論のために再度有効にしてください。
trainer.train()

batch = tokenizer("Two things are infinite: ", return_tensors='pt')

with torch.cuda.amp.autocast():
  output_tokens = model.generate(**batch, max_new_tokens=50)

print('\n\n', tokenizer.decode(output_tokens[0], skip_special_tokens=True))
```

以下のような推論結果が得られれば成功です。  

```powershell
 Two things are infinite:  the universe, and human stupidity; and I'm not sure about the universe.  -Albert Einstein
I'm not sure about the universe either.
```