Generative AI

マルチ LLM 対応の NVIDIA NIM による合成データ SFT (Seed あり / なし) の効果分析

Reading Time: 3 minutes

日本語タスクを評価する推論基盤 マルチ LLM 対応の NVIDIA NIM

本ブログでは、下記の記事で作成した合成データを用いて Supervised Fine-Tuning (SFT) したモデルの日本語性能を評価します。本記事では Supervised Fine-Tuning を便宜上 SFT の略称で記載します。

本評価では、推論基盤としてマルチ LLM 対応のNVIDIA NIM を使用しました。

マルチ LLM 対応の NVIDIA NIM は、

  • OpenAI 互換 API を標準提供
  • 幅広い LLM を同一の推論マイクロサービス形態でデプロイ可能
  • 評価コード側を変えずにモデル差し替えが可能

という特徴を持ちます。

同一の推論条件、API、運用形態のもとで、異なるモデルを公平に評価できる点が特徴です。これにより、  推論基盤差による評価ノイズを最小化した状態で、 SFT の効果そのものを観測できます。

本記事では、以下について解説します。

  • マルチ LLM 対応の NVIDIA NIM を用いた SFT 済みモデルのデプロイ方法
  • llm-jp-eval を用いた日本語常識推論タスクの評価手法
  • Seed あり / なし合成データ SFT の効果比較

検証環境

本記事で検証に使用した環境は下記です。

ハードウェア動作環境

  • DGX-A100

ソフトウェア環境

JCommonsenseQA の比較評価

モデルの変換処理

NeMo RL を用いた合成データによる Supervised Fine-Tuning (SFT)」の記事で指定したディレクトリにある checkpoint を使います。

NeMo RL で出力されるチェックポイントは Hugging Face の Safetensors フォーマットでないため変換処理が必要です。convert_dcp_to_hf.py で HF checkpoint に変換します。

uv run python examples/converters/convert_dcp_to_hf.py \
  --config="<path/to/checkpoints/config.yaml>" \
  --dcp-ckpt-path="<path/to/checkpoints/step*/policy/weights>" \
  --hf-ckpt-path="$HF_CKPT_PATH"

2022.01.06 時点で直接、Safetensors フォーマットに変換するコードがないので下記コードで HF checkpoint から Safetensors フォーマットに変換する処理を行います。

import argparse
import shutil
import os
from transformers import AutoModelForCausalLM
from huggingface_hub import hf_hub_download


def copy_files_from_hub(original_model_name: str, hf_ckpt_path: str):
    """必要なファイルを HF Hub からコピー"""
    files_to_copy = [
        "modeling_nemotron_h.py",
        "configuration_nemotron_h.py",
        "special_tokens_map.json",
        "tokenizer_config.json",
        "tokenizer.json",
    ]

    os.makedirs(hf_ckpt_path, exist_ok=True)

    for filename in files_to_copy:
        try:
            src_file = hf_hub_download(
                repo_id=original_model_name,
                filename=filename,
            )
            dst_file = os.path.join(hf_ckpt_path, filename)
            shutil.copy(src_file, dst_file)
            print(f"[OK] Copied {filename}")
        except Exception as e:
            print(f"[WARN] Failed to copy {filename}: {e}")


def convert_to_safetensors(hf_ckpt_path: str):
    """HF checkpoint → safetensors に変換"""
    print(f"\n=== Loading model from: {hf_ckpt_path} ===")
    model = AutoModelForCausalLM.from_pretrained(hf_ckpt_path, trust_remote_code=True)

    output_dir = hf_ckpt_path + "_safetensors"
    print(f"=== Saving safetensors to: {output_dir} ===")
    model.save_pretrained(output_dir, safe_serialization=True)

    # tokenizer / tokens を safetensors 側にもコピー
    files_to_copy = [
        "special_tokens_map.json",
        "tokenizer_config.json",
        "tokenizer.json",
    ]
    for filename in files_to_copy:
        src_file = os.path.join(hf_ckpt_path, filename)
        dst_file = os.path.join(output_dir, filename)
        shutil.copy(src_file, dst_file)
        print(f"[OK] Copied {filename} to safetensors directory")

    print("\n=== Conversion completed ===")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--hf-ckpt-path",
        type=str,
        required=True,
        help="Path to HF checkpoint directory"
    )
    parser.add_argument(
        "--original-model-name",
        type=str,
        default="nvidia/NVIDIA-Nemotron-Nano-9B-v2",
        help="Original model name used for training"
    )

    args = parser.parse_args()

    hf_ckpt_path = args.hf_ckpt_path
    original_model_name = args.original_model_name

    print(f"Using HF checkpoint path: {hf_ckpt_path}")
    print(f"Original model name: {original_model_name}")

    copy_files_from_hub(original_model_name, hf_ckpt_path)
    convert_to_safetensors(hf_ckpt_path)


if __name__ == "__main__":
    main()

下記コマンドで Safetensors フォーマットに変更します。

uv run python examples/convert_hf_safetensors.py --hf-ckpt-path="$HF_CKPT_PATH"

マルチ LLM 対応の NVIDIA NIM でのデプロイ

Example Deployment を参考に {HuggingFace Safetensors path} に先程変換した Safetensors の path を設定して マルチ LLM 対応の NVIDIA NIM でデプロイします。

export CONTAINER_NAME=LLM-NIM
export Repository=nim/nvidia/llm-nim
export TAG=1.15.0
export IMG_NAME="nvcr.io/$Repository:$TAG"
export LOCAL_MODEL_DIR={HuggingFace Safetensors path}
export NIM_MODEL_NAME=$LOCAL_MODEL_DIR
export NIM_SERVED_MODEL_NAME=custom_nemotron_nano_v2_9b

# Choose a path on your system to cache the downloaded models
export LOCAL_NIM_CACHE=~/.cache/nim
mkdir -p "$LOCAL_NIM_CACHE"

# Add write permissions to the NIM cache for downloading model assets
chmod -R a+w "$LOCAL_NIM_CACHE"

# Start the LLM NIM
docker run -it --rm --name=$CONTAINER_NAME \
   --gpus '"device=0"' \
   --shm-size=124GB \
   -e NIM_MODEL_NAME="/opt/models/local_model" \
   -e NIM_SERVED_MODEL_NAME=$NIM_SERVED_MODEL_NAME \
   -v "$LOCAL_MODEL_DIR:/opt/models/local_model" \
   -v "$LOCAL_NIM_CACHE:/opt/nim/.cache" \
   -e NIM_FORCE_TRUST_REMOTE_CODE=1 \
   -e NIM_MANIFEST_ALLOW_UNSAFE=1 \
   -u $(id -u) \
   -e NIM_MAX_NUM_SEQS=256 \
   -e NIM_MAX_BATCH_SIZE=16 \
   -e NIM_MAX_MODEL_LEN=4096 \
   -e NIM_KVCACHE_PERCENT=0.5 \
   -p 8000:8000 \
   $IMG_NAME

JCommonsenseQA とは

JCommonsenseQA は、日本語での常識推論能力を評価する多肢選択式 QA タスクです。英語版 CommonsenseQA をベースに、日本語話者向けに構築されており、JGLUE ベンチマークの一部としても利用されています。

特徴は以下の通りです。

  • 日本語の一般常識、語義、手順、制度理解を問う  
  • 5 択の常識推論タスクのため推論の曖昧さが入りにくく、モデル比較に適している

そのため、合成データ生成の品質評価対象として扱いやすいという理由から、本記事では JCommonsenseQA を対象タスクとして選択しています。

llm-jp-eval での評価

JCommonsenseQA を評価するために llm-jp-eval の v_2.1.2 を使用しました。llm-jp-eval は、日本語 LLM の性能を統一フォーマットおよび統一指標で評価するための OSS フレームワークです。

主な特徴は下記です

  • 日本語タスク (JGLUE 系、JCommonsenseQA など) に対応
  • few-shot / zero-shot の統一的な評価設定
  • Exact Match を中心とした再現性の高い指標設計
  • vLLM / OpenAI 互換 API を利用可能

推論実行方法に沿ってデータセットを準備します。

下記のような datasets_jcommonsense.yaml を準備して eval_configs/datasets_jcommonsense.yaml に置きます。JCommonsenseQA のみを対象として評価するように設定しています。

datasets:
  - jcommonsenseqa
#  - hle
categories:
  CR:
    description: "nemotron"
    default_metric: exact_match
    metrics: {}
    datasets:
      - jcommonsenseqa

dataset_info_overrides: {}

下記のような config_sft_jcommonsense.yaml を準備して configs/config_sft_jcommonsense.yaml に置きます。config_template.yaml をベースに変更しています。

マルチ LLM 対応の NVIDIA NIM は OpenAI フォーマットに準拠しているので vLLM の設定で動作可能なため、vLLM の設定で動作させます。

`custom_prompt_template` を修正して think を off で動作するように設定します。

/no_think を指定することで、推論時に思考トークン (Chain-of-Thought) を出力しない設定としています。

run_name: null # 最優先で適用されるrun_name。指定しない場合は`wandb.run_name`またはモデル名に実行日時を結合したデフォルト値が用いられる。
output_dir: 'local_files'
eval_dataset_config_path: './eval_configs/datasets_jcommonsense.yaml' # 評価データセットの設定ファイルのパス
include_non_commercial: false # true の場合、商用利用不可のデータセットも含めて評価します。

max_num_samples: -1 # 評価データセットのサンプル数の上限。デフォルトは10件。-1の場合は全件評価します。

exporters:
  local:
    export_output_table: true # 出力結果をテーブル形式で保存するかどうか
    output_top_n: null           # 出力結果の上位何件を保存するか

# HTTP Requestなどによる同期的推論を行う場合の設定
online_inference_config:
  provider: vllm-openai
  max_concurrent: 200 
  hostname: localhost:8000
  model_name: "custom_nemotron_nano_v2_9b"
  generation_config:
    temperature: 0.0

custom_prompt_template: |
  /no_think
  {%- if dataset_instruction -%}
  ### 指示
  {{ dataset_instruction.strip() }}
  {% endif -%}
  {%- if answer_pattern -%}
  ### 回答形式
  {{ answer_pattern.strip() }}
  {% endif -%}
  ### 入力:
  {{ input.strip() }}


default_answer_extract_pattern: "(?s)^(.*?)(?=\\n\\n|\\Z)"

output_length_delta: 0

下記コマンドで JCommonsenseQA の評価処理を実行します。

uv run scripts/evaluate_llm.py eval --config configs/config_sft_jcommonsense.yaml

合成データ適用前の基礎性能比較

評価対象モデルの JCommonsenseQA の評価結果は下記です。

評価対象モデル評価結果
ベース モデル: nvidia/nvidia-nemotron-nano-9b-v2
(/no_think を設定して think を off にして検証しています。)
0.8686
合成データ生成モデル: openai/gpt-oss-120b
(デフォルト設定で検証しています。)
0.9589

誤答分析

nvidia/nvidia-nemotron-nano-9b-v2 では特定のジャンルで誤答が発生しました。一例として“地理・生活圏常識” で誤答が発生したので、その内容をピックアップします。

  • 質問: 砂丘のある県の町は?
    • 選択肢 (抜粋)
      • 鳥取県
      • 鳥取市 (正解)
    • nvidia/nvidia-nemotron-nano-9b-v2 の予測
      • 鳥取県

都道府県レベル (prefecture) で止まってしまうミスが発生しています。

性能評価

nvidia/nvidia-nemotron-nano-9b-v2 は/no_think を設定して think を off にして検証しています。

モデル学習 / 役割JCommonsenseQA Exact Match
Nemotron-Nano-9B-v2 (Base)ベース モデル0.8686
GPT-OSS-120B合成データ生成 (教師)0.9588
Nemotron-Nano-9B-v2
Seed なし SFT
合成データ (約 8,000 件) SFT0.8766
Nemotron-Nano-9B-v2
Seed あり SFT
Nemotron-Personas-Japan Seed 使用した合成データ (約 8,000 件) SFT0.8892

ベース モデルの初期性能が高いため、改善幅は限定的でしたが、合成データ SFT による一貫した性能向上が確認できました。

Seed なし、ありの合成データでも性能が改善しました。

Seed あり / なしの比較では、モデル構造、学習ステップ数、評価設定はすべて同一条件としています。

Seed ありでは“地理・生活圏常識” の誤答が改善されました。Seed としていれた居住地 (都道府県) もしくは生活圏 (地方) の情報が効いた可能性があります。

  • 質問: 砂丘のある県の町は?
    • nvidia/nvidia-nemotron-nano-9b-v2 Seed あり SFT の予測
      • 鳥取市

まとめ

本記事では、下記を記述しました。

  • マルチ LLM 対応の NVIDIA NIM での実行方法
  • llm-jp-eval を用いた評価方法
  • 評価結果の比較

一連の記事で Seed ありの合成データによるモデルの改善の方法を示したので、参考になれば幸いです。

関連情報

Tags