ファインチューニングしたモデルでGGUFファイル作成した時に詰まった件について

# やろうとしたことと起きた問題
[microsoft/Phi-3.5-mini-instruct](https://huggingface.co/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アダプターをマージして保存をしていた。

学習をするときは以下のようにした。
```Python
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()
```

そして学習結果の保存を以下のように行なった。
```Python
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アダプターと元のモデルをマージして保存した。
```Python
# ファインチューニング元のモデルの読み込み
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をインストールした。
```python
!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
!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
```
ところが実行途中で以下のようなエラーが発生してしまった。
```terminal
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ファイルに変換して、それとすでに[配布](bartowski/Phi-3.5-mini-instruct-GGUF)されている`microsoft/Phi-3.5-mini-instruct`のggufファイルを組み合わせて推論を行うことに成功した。

元々の`microsoft/Phi-3.5-mini-instruct`のモデルを`/content/llama.cpp/lora/original_phi3.5`に保存しておいて以下を実行。

```python
!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`)を適切な場所に配置して以下を実行。
```python
!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)
```
出力のほどは・・・
```terminal
{'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`自体がかなり新しく日々機能が変わったりしているから、この対応がいつまで通用するかも定かではない。
自分の方の原因ではなくライブラリの方の原因だったら、それを特定&修正してプルリクエストをできるような人間になりたいもの・・・。