見出し画像

Google ColabでQLoRA したLlama-3をMLXモデル(macOS)へ変換する


1. モチベーション

MLXを使いmacOS上で数BのLLMを十数token/sec程度の速度(M2 16GB MacbookAir)で動作させることができた。合わせてLoRaによるFine-tuningをさせてみたが、1000件程度の学習データをLlama-3-8B-InstructionにQLoRAでFine-tuningすると1時間程度の時間が必要になる。短時間でのチューニングを実現するため、学習はGPU環境(Google Colaboratory)で実行し、完成したモデルをmacOS上でMLX形式に変換して利用したい。

記事中で説明すること

  • QLoRA(bitstandbyte 4bit量子化)によるFine-Tuning

  • QLoRAモデルのマージ

  • QLoRAマージモデルのMLX変換

2. 方針

MLXのモデル変換で、LoRAアダプタの変換は提供されていない。そこで、Google Colab上でQLoRAによるチューニングを行い、ベースモデルにQLoRAアダプタをマージさせ、マージ後のモデルをMLXモデルに変換させる。以下1と2のみGoogle Colaboで実施する。

  1. QLoRAによるFine-tuning学習

  2. QLoRAアダプタをベースモデルへマージ

  3. MLXモデルへ変換

  4. MLXモデルによる推論

ポイント

当然すんなりとは変換させてもらえないが、以下のポイントを押さえておけば何をすべきかクリアになる。

  • QLoRAアダプタをベースモデルにマージする際、ベースモデルはFine-Tuningと同じ形式で量子化設定で読み込み後、非量子化する。

  • QLoRAアダプタをマージしたモデルはbitstandbyteで再量子化するとアダプタの性能が落ちる。

  • MLXへ読み込む場合8bit量子化で読み込む。4bitで読み込むとモデルが壊れる。

Google Colaboratoryのノートブックはこちら

3. QLoRAの学習

ベースモデルを量子化して読み込みTRLを使ってLoRAの学習を行う。量子化はbitsandbyteを使用する。量子化のコンフィグレーションは4bit, NF4でデータタイプはbfloat16, dobule quantizationで設定する。Transformerがbitsandbyteをインテグレーションしているため、コンフィグオブジェクトを私だけで量子化できる。

from transformer import BitsAndBytesConfig,AutoModelForCausalLM

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
) #AutoModelForCausalLMのquantization_configに渡すことで量子化する 。今回はTRLライブラリ内で読み込みするので、処理は明示的に。
# modle = AutoModelForCausalLM(base_model, quantization_config = bnb_config)

LoRAのコンフィグは以下の通り

from peft import LoraConfig
peft_config = LoraConfig(
  task_type = TaskType.CAUSAL_LM,
  inference_mode = False,
  r = 8,
  lora_alpha = 32,
  lora_dropout = 0.1,
)

学習の設定はTrainingArgumentsへ以下のように設定する。

output_dir = 'lora_adaptor'
training_args = TrainingArguments(
  output_dir=output_dir,
  num_train_epochs=1,
  learning_rate=2e-4,
  max_steps=500,
  logging_steps=100,
  per_device_train_batch_size = 1,
  per_device_eval_batch_size = 1,
  gradient_accumulation_steps = 1,
  optim = 'adamw_torch',
)

上記の設定をtrl.SFTTrainerに設定し、実行することでQLoRAの学習を行う。

trainer = SFTTrainer(
  model=base_model,
  model_init_kwargs={"low_cpu_mem_usage": True, "quantization_config": bnb_config},
  peft_config=peft_config,
  train_dataset=train_dataset,
  eval_dataset=eval_dataset,
  dataset_text_field="text",
  args=training_args,
  max_seq_length=1024,
)

trainer.train()

trainer.save_model(lora_adaptor)

4. QLoRAアダプタのマージ

LoRAアダプタをベースモデルへマージし1つのモデルとして扱うことができる。QLoRAの場合、量子化した状態でマージすることができない。ベースモデルを量子化した後、逆量子化した後にQloRAアダプタとマージを行うことでマージすることができる。

bitsandbytesで量子化-逆量子化-マージしたモデルに対して再びbitsandbytesで量子化すると精度が著しく低下する。Fine-Tuningを実施していないベースモデルと同等となってしまう。量子化する場合はAWQなど他の量子化手法でうまくいくという話があるが、今回は検証していない。以下の記事で検証されている。

Training, Loading, and Merging QDoRA, QLoRA, and LoftQ Adapters

逆量子化によるマージのコードはいかにまとまっている。GoogleColabで実施する場合はdevice_typeをCPUにするとRAMを使って逆量子化することができる。L4などのVRAMが小さいGPUでも変換することが可能になる。

Github

このコードを参考に以下でモデルマージを実施した。

tokenizer = AutoTokenizer.from_pretrained(base_model)

model = AutoModelForCausalLM.from_pretrained(
    base_model,
    device_map="auto",
    quantization_config=bnb_config, # QLoRA実施時の量子化設定
)

model = dequantize_model(model, tokenizer, dtype=torch.bfloat16, device="cpu")

model = PeftModel.from_pretrained(model, lora_adaptor) #QLoRAモデルの読み込み
model = model.merge_and_unload()#モデルマージ

save_model(model, tokenizer, output_dir)

保存したモデルはCPUモデルへロードされる。GPUのVRAMに読み込ませるためには以下のようにモデルロード時にデバイスをcudaへ指定して読み込ませる。

tokenizer = AutoTokenizer.from_pretrained(output_dir)
modle = AutoModelForCausalLM.from_pretrained(
  output_dir, # マージ後モデルへのパス
  device_map = "cuda"
)
model.generate(...)

5. MLXへの変換と推論

マージ後モデルをmacOSローカルへダウンロードし、MLXモデルへ変換を行う。MLX変換時に量子化を実施する。MLXの量子化は8bitと4bitを選択できる。デフォルトは4bitとなる。4bitで量子化した場合、モデルが壊れてしまい、意味のない出力を出すようになる。したがって、8bit量子化でMLXモデルへ変換する。
量子化せずにMLXモデルへ変換することもできるが、当然推論速度が遅くなる。

pip install -U mlx_lm # MLX LLMnのインストール
mlx_lm.convert --hf-path Meta-Llama-3-8b-instruct-gozaru-merge --mlx-path "Meta-Llama-3-8B-Instruct-gozaru-4bit" --q-bits 8 --quantize # 8bit量子化設定でのMLXモデルへの変換

変換したモデルの推論をさせるコードは以下、Gozaruデータセットでチューニングする場合にプロンプトの設定を行なっているので、以下のコードで動作を確認する。

model, tokenizer = load(model_name, tokenizer_config={"eos_token": "<|eot_id|>", "trust_remote_code": True},)

    chat = [
        { "role": "system", "content": "あなたは日本語で回答するAIアシスタントです。" },
        { "role": "user", "content": "日本で一番高い山は?" },
    ]
    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
    print(prompt)
    # 推論の実行
    #token_ids = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")

    response = generate(
        model,
        tokenizer,
        prompt=prompt,
        verbose=True,
        temp=0.0,
        top_p=0.9,
        max_tokens=256,
    )

出力

==========
Prompt: <|begin_of_text|><|start_header_id|>system<|end_header_id|>

あなたは日本語で回答するAIアシスタントです。<|eot_id|><|start_header_id|>user<|end_header_id|>

日本で一番高い山は?<|eot_id|><|start_header_id|>assistant<|end_header_id|>


我、りんえもんは思う。 日本で一番高い山は、富士山でござる。知らんけど。
==========
Prompt: 4.431 tokens-per-sec
Generation: 10.659 tokens-per-sec

6. 参考

Transformerモデルの量子化方法について

Quantize 🤗 Transformers models

Falcon 7BのQLoRA実行方法

Google Colab + trl で Falcon-7B のQLoRAファインチューニングを試す|npaka

Gozaruデータセットを使ったLlama3のQLoRA データセットの準備はこのサイトのコードを利用

Google Colab で Llama 3 のファインチューニングを試す |npaka

Llama3のQLoRA

LLama 3のSFTTrainer+Weights & Biasesでファインチューニング - Sun wood AI labs.2

QLoRAのマージに対する考察

Training, Loading, and Merging QDoRA, QLoRA, and LoftQ Adapters

MLXへの変換方法

Llama3をローカルMac(MLX)で使ってみる

この記事が気に入ったらサポートをしてみませんか?