top of page
スクリーンショット 2024-01-25 15.42.00.png

NeMoでLLMをファインチューニングする

  • 執筆者の写真: Ryo Shimizu
    Ryo Shimizu
  • 9月21日
  • 読了時間: 7分

更新日:9月24日

ree

インコンテキストラーニング(ICL)だけでは一般的な知識を獲得するのは困難であるという論文が出た(https://arxiv.org/pdf/2509.10414) 一方で、gpt-ossなどの小さくとも実用的に使えるLLMが登場してきたことで、LLMのファインチューニング、特に継続的事前学習(CPT)の最新(2025年現在)のやり方をおさらいしておこうと思い、挑戦したが、色々とつまづきがあったので、メモとして残します。個人的にはICLとファインチューニング(CPT)の組み合わせが実用的に使うには大事かなと思っています。


機材はもちろんAIスーパーコンピュータ継之助を用い、NVIDIAのNeMoをベースとして行いました。 https://developer.nvidia.com/ja-jp/blog/how-to-use-continual-pre-training-with-japanese-language-on-nemo-framework/?utm_source=chatgpt.com

このチュートリアルに従って基本的に進めましたが、いくつか間違っていたことがありました。 念の為全部の手順を書いておきます。もとの手順と重複するかもしれませんが、ご容赦ください。


Dockerコンテナの起動とベースモデルのダウンロード


以下のようにしてDockerコンテナの起動とベースモデルのダウンロードをします。 します。ちなみにDockerにはあらかじめGPU対応のパッチ当てています。


#Dockerコンテナの起動
sudo docker run --rm -it --gpus all --shm-size=16g --ulimit memlock=-1 --network=host -v ${PWD}:/workspace -w /workspace  nvcr.io/nvidia/nemo:24.09 bash

#huggingfaceトークンをコピペしてログイン
huggingface-cli login 

#モデルのダウンロード
python - <<'PY'
import os
from huggingface_hub import snapshot_download
 
MODEL_DIR = "./models"
os.makedirs(MODEL_DIR, exist_ok=True)


snapshot_download(
    repo_id="meta-llama/Llama-3.1-8B",
    local_dir=f"{MODEL_DIR}/Llama-3.1-8B",
)
PY

Dockerコンテナを起動すると同時にそのコンテナにログインし、workspaceというディレクトリがホストの起動ディレクトリになることに注意してください。


次にダウンロードしたモデルをnemo形式に変換します

まず、NeMoにパッチを当てます


cd /opt/NeMo/
curl -L https://github.com/NVIDIA/NeMo/pull/11548.diff | git apply
curl -L https://github.com/NVIDIA/NeMo/pull/11580.diff | git apply

さらに、以下のスクリプトで変換します


export INPUT="/workspace/models/Llama-3.1-8B"
export OUTPUT="/workspace/models/Llama-3.1-8B.nemo"
export PREC="bf16"


python /opt/NeMo/scripts/checkpoint_converters/convert_llama_hf_to_nemo.py --input_name_or_path ${INPUT} --output_path ${OUTPUT} --precision ${PREC} --llama31 True

次に、データを読み込みます。練習なのでチュートリアル通りに日本版Wikipediaのデータを使います。


cat - > dl.sh
cd /workspace/
mkdir -p data/ja_wiki
wget -O data/ja_wiki/train_0.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_0.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_1.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_1.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_2.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_2.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_3.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_3.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_4.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_4.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_5.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_5.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_6.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_6.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_7.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_7.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_8.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_8.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_9.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_9.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_10.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_10.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_11.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_11.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_12.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_12.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/train_13.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/train_13.jsonl.gz?ref_type=heads
wget -O data/ja_wiki/validation_0.jsonl.gz --no-check-certificate https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v3/-/raw/main/ja/ja_wiki/validation_0.jsonl.gz?ref_type=heads
gunzip data/ja_wiki/*
^C # Ctrl+Cで終了

bash dl.sh

チュートリアルとはやや違いますが、いちいちファイルをダウンロードするのは面倒なので、僕は一つのシェルスクリプトにしてから実行して、その間にお茶でも飲みます。


終わったらデータを前処理します。


mkdir ds

# for training data
python /opt/NeMo/scripts/nlp_language_modeling/preprocess_data_for_megatron.py --input="/workspace/data/ja_wiki" --json-keys=text --tokenizer-library=huggingface --tokenizer-type="meta-llama/Llama-3.1-8B" --dataset-impl mmap --append-eod --output-prefix="/workspace/ds/train" --workers=24 --files-filter '**/train_*.json*' --preproc-folder --log-interval 10000

# for validation data
python /opt/NeMo/scripts/nlp_language_modeling/preprocess_data_for_megatron.py --input="/workspace/data/ja_wiki" --json-keys=text --tokenizer-library=huggingface --tokenizer-type="meta-llama/Llama-3.1-8B" --dataset-impl mmap --append-eod --output-prefix="/workspace/ds/validation" --workers=24 --files-filter '**/validation_*.json*' --preproc-folder --log-interval 1000

継続的事前学習の実行


継続的事前学習を行います。ここではすべてのGPUを使います。

export HYDRA_FULL_ERROR=1
export OC_CAUSE=1
export TORCH_DISTRIBUTED_DEBUG=INFO

export WANDB=False
export PJ_NAME="CP"
export EXP_DIR="./results/"${PJ_NAME}
export EXP_NAME="Llama-3.1-8B"
export MODEL="/workspace/models/Llama-3.1-8B.nemo"
export TOKENIZER_LIBRARY="huggingface"
export TOKENIZER_TYPE="meta-llama/Llama-3.1-8B"
export TOKENIZER="/workspace/models/Llama-3.1-8B/tokenizer.json"
export TP_SIZE=2
export SP=False
export PP_SIZE=1
export EP_SIZE=1

TRAIN_DATA_PATH="{train:[1.0,/workspace/ds/train_text_document],validation:[/workspace/ds/validation_text_document],test:[/workspace/ds/validation_text_document]}"


python /opt/NeMo/examples/nlp/language_modeling/megatron_gpt_pretraining.py \
    exp_manager.exp_dir=${EXP_DIR} \
    exp_manager.name=${EXP_NAME} \
    exp_manager.create_wandb_logger=${WANDB} \
    exp_manager.wandb_logger_kwargs.project=${PJ_NAME} \
    exp_manager.wandb_logger_kwargs.name=${EXP_NAME} \
    exp_manager.checkpoint_callback_params.save_nemo_on_train_end=True \
    exp_manager.checkpoint_callback_params.save_top_k=3 \
    exp_manager.checkpoint_callback_params.always_save_nemo=False \
    trainer.precision=bf16 \
    trainer.devices=8 \
    trainer.num_nodes=1 \
    trainer.max_epochs=-1 \
    trainer.max_steps=150 \
    trainer.log_every_n_steps=1 \
    trainer.val_check_interval=15 \
    trainer.limit_val_batches=1 \
    trainer.limit_test_batches=1 \
    trainer.gradient_clip_val=1.0 \
    model.restore_from_path=${MODEL} \
    model.encoder_seq_length=8192 \
    model.max_position_embeddings=8192 \
    model.num_layers=32 \
    model.hidden_size=4096 \
    model.ffn_hidden_size=14336 \
    model.num_attention_heads=32 \
    model.hidden_dropout=0.0 \
    model.attention_dropout=0.0 \
    model.apply_query_key_layer_scaling=True \
    model.bias=False \
    model.activation=fast-swiglu \
    model.normalization=rmsnorm \
    model.position_embedding_type=rope \
    +model.rotary_base=5000000.0 \
    model.share_embeddings_and_output_weights=False \
    model.num_query_groups=8 \
    model.scale_positional_embedding=True \
    model.bias_activation_fusion=False \
    model.bias_dropout_add_fusion=False \
    model.tokenizer.library=${TOKENIZER_LIBRARY} \
    model.tokenizer.type=${TOKENIZER_TYPE} \
    model.tokenizer.model=${TOKENIZER} \
    model.megatron_amp_O2=True \
    model.tensor_model_parallel_size=${TP_SIZE} \
    model.pipeline_model_parallel_size=${PP_SIZE} \
    model.sequence_parallel=${SP} \
    model.expert_model_parallel_size=${EP_SIZE} \
    model.transformer_engine=True \
    model.fp8=False \
    model.seed=42 \
    model.enable_megatron_timers=False \
    model.optim.name=distributed_fused_adam \
    model.optim.lr=2.5e-5 \
    model.optim.weight_decay=0.1 \
    model.optim.betas=[0.9,0.95] \
    model.optim.sched.warmup_steps=15 \
    model.optim.sched.constant_steps=0 \
    model.optim.sched.min_lr=2.5e-6 \
    model.micro_batch_size=1 \
    model.global_batch_size=1024 \
    model.data.data_prefix=${TRAIN_DATA_PATH} \
    model.data.validation_drop_last=True \
    model.data.num_workers=2

学習には半日ほどかかりました。実際の学習時間は、学習用データがSSDにあるかHDDにあるかなどの環境で多少前後します。


ここまではほぼチュートリアル通りです。ところが継続的学習が終わった後のことがチュートリアルにはあまりちゃんと書かれていません。


学習したモデルをHuggingFace方式に変換する


チュートリアルには「こちらに変換スクリプトがあります(https://github.com/NVIDIA-NeMo/NeMo/tree/main/scripts/checkpoint_converters)」 というリンクがあるだけで、変換スクリプトの使い方は書いてません。これで結構混乱しました。


なのでこの記事で本当に書きたいところはここです。

結論から言うと、convert_llama_nemo_to_tf.pyを使うんですがいくつか注意点があります。

まず、変換のコマンドは以下


mkdir llama-3.1-8b-jawiki
python /opt/NeMo/scripts/checkpoint_converters/convert_llama_nemo_to_hf.py --input_name_or_path re
sults/CP/Llama-3.1-8B/checkpoints/Llama-3.1-8B.nemo  --output_path llama-3.1-8b-jawiki/pytorch_model.bin      

何回か試行錯誤したのでこの辺パス名とか怪しいかもしれません。


で、問題はこれだけではtokenizerやconfigがないため動かないと言うことです。

そこでベースモデルからコピーして完全なHuggingFaceモデルにします。


python - <<'PY'

from transformers import AutoConfig, AutoTokenizer, AutoModelForCausalLM

base_id = "meta-llama/Llama-3.1-8B"  # あなたが微調整した元モデルに合わせて

# 1) config と tokenizer をベースから取得して保存
cfg = AutoConfig.from_pretrained(base_id)
tok = AutoTokenizer.from_pretrained(base_id, use_fast=True)
cfg.save_pretrained("llama-3.1-8b-jawiki")
tok.save_pretrained("llama-3.1-8b-jawiki")
PY

これでllama-3.1-8b-jawikiに完全なHuggingFaceモデルができます。


ls llama-3.1-8b-jawiki/
config.json  pytorch_model.bin  special_tokens_map.json  tokenizer.json  tokenizer_config.json

HuggingFaceで推論する


いよいよ推論です。

推論用のコードはChatGPTに書いてもらいました。


import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
device = "cuda" if torch.cuda.is_available() else "cpu"
model_dir = "llama-3.1-8b-jawiki"
tok = AutoTokenizer.from_pretrained(model_dir, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(model_dir, torch_dtype="auto", device_map="auto")
model.eval()

def gen(prompt, max_new_tokens=80, temperature=0.0, top_p=0.9):
    inputs = tok(prompt, return_tensors="pt").to(model.device)
    with torch.no_grad():
        out = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            do_sample=(temperature > 0),
            temperature=temperature,
            top_p=top_p,
            repetition_penalty=1.05,
            eos_token_id=tok.eos_token_id,
        )
    return tok.decode(out[0], skip_special_tokens=True)

tests = [
    "Q: 東京スカイツリーの高さは何メートル?\nA:",
    "Q: 日本で最も高い山は?\nA:",
    "Q: 令和は西暦何年に始まった?\nA:",
    "Q: 阪神・淡路大震災の発生日は?(年月日で)\nA:",
    "Q: 紫式部が著した代表的な作品は?\nA:",
]
for t in tests:
    print("----")
    print(gen(t))

ベースモデルなので、QA形式の問題は苦手です。 案の定、間違いが含まれてしまいました。



Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
Q: 東京スカイツリーの高さは何メートル?
A: 634m

----
Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
Q: 日本で最も高い山は?
A: 日本で最も高い山は、北海道の標高2,291mのシュプール山(シュプル山)です。

----
Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
Q: 令和は西暦何年に始まった?
A: 2019年

----
Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
Q: 阪神・淡路大震災の発生日は?(年月日で)
A: 1995年(平成7年)1月17日

----
Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
Q: 紫式部が著した代表的な作品は?
A: 『源氏物語』

これに対し、ベースモデルであることを考慮すると、以下のようにきちんと富士山についての知識を獲得できていることがわかります。


>>> gen("日本でいちばん高い山は?")
Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
'日本でいちばん高い山は?(日本でいちばんたかいやまは?)\n'
>>> gen("日本でいちばん高い山は")                               
Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
'日本でいちばん高い山は、どの山ですか?\n'
>>> gen("日本でいちばん高い山は富")                             
Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
'日本でいちばん高い山は富士山です。富士山の標高は3,776mです。富士山は日本の国土を守る神様の住処とされています。\n\n富士山は、江
戸時代に開かれた東海道五十三次の途中にある富士山麓の宿場町としても有名です。富士山麓の宿'
>>> gen("日本でいちばん高い山")                                 
Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
'日本でいちばん高い山は、どの山ですか?\n'
>>> gen("日本でいちばん高い")                                   
Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
'日本でいちばん高いお茶は、どの茶葉なのか?\n日本でいちばん高いお茶は、どの茶葉なのか?\n日本でいちばん高いお茶は、どの茶葉なの
か?\n日本でいちばん高いお茶は、どの茶葉なのか?\n日本でいちばん高いお茶は、どの茶葉'
>>> gen("日本でいちばん高い山といえば")
Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
'日本でいちばん高い山といえば、富士山でしょう。富士山は日本の国土を象徴する山であり、世界でも有数の活火山である。富士山は、江戸
時代に開かれた東海道五十三次の途中にある富士山麓の宿場町・富士宮市と、江戸時代に開かれた東海道五十三'
>>> 

以前に比べるとLLMファインチューニングも随分手軽になったなという印象を受けます。

少し前と機材がアップグレードしたわけではないのですが、いろいろな知見が溜まってきて、また、小規模なモデルでも十分実用に耐えることが証明/確認されてきたことで、逆に小規模モデルのファインチューニングは増えていく可能性があります。


NeMo以外にもTRLとかいろいろなツールがありますが、NeMoの良かったところはDockerコンテナなので環境構築の手間が掛からなかったことです。ただ、コンバーター周りの説明が雑と言うか、推論のサンプルに関してはリンクが切れていてページが見れなかったり、とても数千万円の機材を売ってる会社のページとは思えないのが残念なところですが。


次回は自分の過去のブログのデータをベースに学習させてみたいと思います。

 
 
bottom of page