やろうとしたことと起きた問題
microsoft/Phi-3.5-mini-instructをファインチューニングして、それをggufファイルに変換することで、ローカルで推論できるようにしようとした。
しかし、ggufファイルに変換しようとしたところで、
ValueError: Can not map tensor 'model.layers.0.mlp.down_proj.weight.absmax'
というエラーが出てしばらく詰まった。
作業環境はgoogle colabである。
結果を一言で
根本的な解決策は見出せなかったが、少し迂回路を取ることでggufファイルを作成しローカルでの推論はできるようになったからひとまずOKとした。
対応手順
具体的には、まず当初は、ファインチューニングした時に元のモデルとRoLAアダプターをマージして保存をしていた。
学習をするときは以下のようにした。
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig, TrainingArguments
from peft import get_peft_model, LoraConfig
from trl import SFTTrainer, DataCollatorForCompletionOnlyLM
model_name = "microsoft/Phi-3.5-mini-instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.model_max_length = 2048
tokenizer.pad_token = tokenizer.unk_token
tokenizer.pad_token_id = tokenizer.convert_tokens_to_ids(tokenizer.pad_token)
tokenizer.padding_side = 'right'
# 量子化パラメータ
bnb_config = BitsAndBytesConfig(
load_in_8bit=True,
bnb_8bit_quant_type="nf4",
bnb_8bit_compute_dtype=torch.float16,
bnb_8bit_use_double_quant=False,
)
model = AutoModelForCausalLM.from_pretrained(model_name, quantization_config=bnb_config)
model.config.use_cache = False
# LoraConfigの設定
target_modules = ["o_proj","qkv_proj","gate_up_proj","down_proj","lm_head"]
peft_config = LoraConfig(
r=8,
lora_alpha=32,
lora_dropout=0.1,
target_modules=target_modules, # Loraを適用するモジュール (モデル構成の線形層)
task_type="CAUSAL_LM"
)
# 学習データのミニバッチ構築
response_template = "### 応答:\n" # 私が使ったデータセット特有のテンプレート
collator = DataCollatorForCompletionOnlyLM(tokenizer.encode(response_template, add_special_tokens = False)[2:], tokenizer=tokenizer)
# 学習時の設定
args = TrainingArguments(
output_dir=f"/content/gdrive/MyDrive/study/models/fine_tuned_phi3.5",
num_train_epochs=3,
gradient_accumulation_steps=8,
per_device_train_batch_size=4,
save_strategy="no",
logging_steps=10,
lr_scheduler_type="constant",
save_total_limit=1,
fp16=True,
)
trainer = SFTTrainer(
model,
args=args,
train_dataset=train_dataset,
formatting_func=formatting_prompts_func,
max_seq_length=1024,
data_collator=collator,
peft_config=peft_config,
)
# 学習の実施
trainer.train()
そして学習結果の保存を以下のように行なった。
trainer.save_model(f"/content/gdrive/MyDrive/study/models/fine_tuned_phi3.5")
ここで注意しなければいけないのは、SFTTrainer.save_model
を使って保存されるのは、RoLAのアダプターのみであり、学習されたモデルそのものは保存されていないという点である。
イメージするなら、元々のモデル(今回の例ならmicrosoft/Phi-3.5-mini-instruct
)をRoLAで学習した結果の差分だけ保存されているような感じか。
そこで推論を行える「モデルそのもの」を保存するため、RoLAアダプターと元のモデルをマージして保存した。
# ファインチューニング元のモデルの読み込み
model = AutoModelForCausalLM.from_pretrained("microsoft/Phi-3.5-mini-instruct", device_map="auto", quantization_config=bnb_config)
# ファインチューニングしたRoLAアダプターの読み込み
peft_model_id = f"/content/gdrive/MyDrive/study/models/fine_tuned_phi3.5"
config = PeftConfig.from_pretrained(peft_model_id)
peft_model = PeftModel.from_pretrained(model, peft_model_id)
peft_model = peft_model.to(peft_model.device)
# モデルのマージ
merged_model = peft_model.merge_and_unload()
# マージしたモデルの保存
merged_model.save_pretrained(f"/content/gdrive/MyDrive/study/models/merged_fine_tuned_phi3.5")
次に、このモデルをllama.cpp
でCPUしかないローカル環境で推論させるためにggufファイルに変換させる必要があった。
その前にcolabでllama.cppをインストールした。
!git clone https://github.com/ggerganov/llama.cpp
%cd llama.cpp
!mkdir build
%cd build
!cmake .. -DLLAMA_CUBLAS=ON
!cmake --build . --config Release
!cp bin/main ..
%cd ..
colabでllama.cppが使えるようになったため、convert_hf_to_gguf.py
でファインチューニングしたモデルをggufにする。
!python /content/llama.cpp/convert_hf_to_gguf.py /content/gdrive/MyDrive/study/models/merged_fine_tuned_phi3.5 --outfile /content/gdrive/MyDrive/study/models/merged_fine_tuned_phi3.5.gguf --outtype f16
ところが実行途中で以下のようなエラーが発生してしまった。
INFO:hf-to-gguf:Loading model: merged_fine_tuned_phi3.5
INFO:gguf.gguf_writer:gguf: This GGUF file is for Little Endian only
INFO:hf-to-gguf:Exporting model...
INFO:hf-to-gguf:rope_factors_long.weight, torch.float32 --> F32, shape = {48}
INFO:hf-to-gguf:rope_factors_short.weight, torch.float32 --> F32, shape = {48}
INFO:hf-to-gguf:gguf: loading model part 'model.safetensors'
INFO:hf-to-gguf:output.weight, torch.float16 --> BF16, shape = {3072, 32064}
INFO:hf-to-gguf:token_embd.weight, torch.float16 --> BF16, shape = {3072, 32064}
INFO:hf-to-gguf:blk.0.attn_norm.weight, torch.float16 --> F32, shape = {3072}
Traceback (most recent call last):
File "/content/llama.cpp/convert_hf_to_gguf.py", line 4430, in <module>
main()
File "/content/llama.cpp/convert_hf_to_gguf.py", line 4424, in main
model_instance.write()
File "/content/llama.cpp/convert_hf_to_gguf.py", line 433, in write
self.prepare_tensors()
File "/content/llama.cpp/convert_hf_to_gguf.py", line 297, in prepare_tensors
for new_name, data_torch in (self.modify_tensors(data_torch, name, bid)):
File "/content/llama.cpp/convert_hf_to_gguf.py", line 265, in modify_tensors
return [(self.map_tensor_name(name), data_torch)]
File "/content/llama.cpp/convert_hf_to_gguf.py", line 213, in map_tensor_name
raise ValueError(f"Can not map tensor {name!r}")
ValueError: Can not map tensor 'model.layers.0.mlp.down_proj.weight.absmax'
safetensorsの部分でエラーが発生しているので、何かモデルが壊れてしまっているのだろう。
down_projはPhi-3.5の線形層であり、ちょうどRoLAで使っていたレイヤーである。down_proj.weight.absmaxだから、RoLAでdown_proj層の重みの絶対値の最大値を使っているのが悪さをしていそう。
...というところまで当たりをつけて色々調べてみたのだが、直接的な解決策を見つけることができなかった。
仕方がないので、迂回路をとってこのエラーを回避してみた。
具体的には、保存したRoLAアダプターを直接ggufファイルに変換して、それとすでに配布されているmicrosoft/Phi-3.5-mini-instruct
のggufファイルを組み合わせて推論を行うことに成功した。
元々のmicrosoft/Phi-3.5-mini-instruct
のモデルを/content/llama.cpp/lora/original_phi3.5
に保存しておいて以下を実行。
!python /content/llama.cpp/convert_lora_to_gguf.py --base /content/llama.cpp/lora/original_phi3.5 /content/llama.cpp/lora/fine_tuned_phi3.5/
出力されたログがかなり長いため省略するが、これでうまくggufファイルを作成できた。(名前はFine_Tuned_Phi3.5-F16-LoRA.gguf
)
あとはRoLAのggufと元のモデルのggufを組み合わせて推論を行う。
配布されている元のモデルのggufファイル(Phi-3.5-mini-instruct-Q4_K_M.gguf
)を適切な場所に配置して以下を実行。
!pip install llama-cpp-python
from llama_cpp import Llama
llm = Llama(
model_path="/content/gdrive/MyDrive/study/GGUF/Phi-3.5-mini-instruct-Q4_K_M.gguf", # 元のモデルのgguf
lora_path="/content/gdrive/MyDrive/study/GGUF/Fine_Tuned_Phi3.5-F16-LoRA.gguf", # 元のモデルをファインチューニングしたROLAアダプターのgguf
n_gpu_layers=0, # GPUにオフロードするレイヤーの数。CPUしか使っていないので0
)
text = '[SEP]\n\n### 指示:\n与えられた情景に対して短歌を一つ作成してください。[情景]:「他人には分かっても、自分だけが知らない妻と共に生きる悲しみを感じながら生きる」[/SEP]\n\n### 応答:\n'
output = llm(
text,
max_tokens=128,
echo=True,
)
print(output)
出力のほどは・・・
{'id': 'cmpl-7d0ebb6c-fef4-431b-a582-bf1eee21844b',
'object': 'text_completion',
'created': 1728098576,
'model': '/content/gdrive/MyDrive/study/GGUF/Phi-3.5-mini-instruct-Q4_K_M.gguf',
'choices': [{'text': 'われ一人知らず悲しき旅を妻も知らず悲しき旅をせしもの',
'index': 0,
'logprobs': None,
'finish_reason': 'stop'}],
'usage': {'prompt_tokens': 105, 'completion_tokens': 31, 'total_tokens': 136}}
大丈夫そう!
所感
根本的な解決ができなかったのはモヤモヤする。
llama.cpp
自体がかなり新しく日々機能が変わったりしているから、この対応がいつまで通用するかも定かではない。
自分の方の原因ではなくライブラリの方の原因だったら、それを特定&修正してプルリクエストをできるような人間になりたいもの・・・。