vLLMでモデルをロードするとrandomやnp.randomのシード値が内部で設定されてしまうので注意が必要

2025/01/07に公開

はじめに

この記事では、個人的に遭遇したvLLMを使う際に乱数シードが意図せず固定されてしまう問題と、その回避策についてまとめます。

※この記事の情報は記事執筆時点(2025/1/8)のものであり、その後のライブラリ更新等で状況が変わっている可能性があります。

ざっくりしたまとめ

先に問題と解決策をざっくりとまとめます。

問題

  • vLLMではLLMクラスでモデルをロードすると同時に、引数seedで渡した値でrandomnp.randomのシード値が設定される
  • 引数を指定しないとデフォルトでseed=0が使われるため、ユーザーの気づきにくいところで乱数シードが固定されてしまう
  • randomnp.randomのseedはグローバルに影響するので、vLLMの推論以外の部分の乱数の挙動も固定されてしまう
  • その結果、vLLMによる推論を含んだコード中のrandomnp.randomを使用する処理が、何回スクリプトを再実行しても意図せず全く同じ結果を返してしまう

解決策

  • モデルのロード時に引数として毎回異なるseed値を渡す
  • モデルのロード後、乱数シードを改めて上書きする

以下、より詳細に解説します。

vLLMにおけるモデル初期化時の引数seedの挙動について

vLLMでモデルをロードして初期化する時、多くの場合でLLMクラスを使うと思います。このクラスの初期化時の引数とデフォルト値は以下のドキュメントに記載があります。

https://docs.vllm.ai/en/stable/dev/offline_inference/llm.html

この中に、seedという引数があります。引数の説明は以下のようになっており、指定しなかった場合のデフォルト値は0となっています。

  • seed – The seed to initialize the random number generator for sampling.

このseedの値は、内部で最終的に以下のようにrandomnp.randomのシード値として利用されます。

https://github.com/vllm-project/vllm/blob/4ca5d40adc53aca2a1fbaed81d9d622fde46ebf1/vllm/platforms/interface.py#L178-L187

このように、引数seedを省略すると意図せず気付きにくいところで乱数シードが0に設定されてしまいます。

これらのシード値はvLLMの推論に関係ないその他のrandomnp.randomの挙動にも影響してしまいます。これは恐らく出力結果の再現性のための仕様だと思いますが、ユーザーが意図しないところで固定されてしまうので問題になる場合があります。

問題となり得る実際のシナリオ

実際に問題になり得るシナリオとして、LLMによるデータ合成の場面を考えてみます。

よく使われるデータ合成の手法として、LLMに何らかのシードテキスト(例えばWikipediaの記事)を与え、それを参考に質問と応答を作成させ合成対話データとするというものがあります。

これをvLLMで簡単に実装しようとすると、抽象的には以下のような実装になると思います。

import random
from vllm import LLM, SamplingParams

# 1) モデルをロード
model = LLM(model="model_name")

# 2) シードテキストのデータセットをロード
seed_dataset = load_seed_dataset("seed_text_dataset")

# 3) ランダムにシードテキストを選択し、バッチサイズ分のプロンプトを作成
def prepare_prompts_from_seeds(batch_size, dataset):
    prompts = []
    for _ in range(batch_size):
        seed_text = random.choice(dataset)
        prompt = format_prompt(seed_text)
        prompts.append(prompt)
    return prompts

# 4) バッチ推論
prompts = prepare_prompts_from_seeds(100, seed_dataset)
outputs = model.generate(prompts, sampling_params=SamplingParams(temperature=0.7, max_tokens=256))

# 5) 結果を取り出してデータセットに追加
synthesized_data = []
for prompt, output in zip(prompts, outputs):
    qa_pair = extract_qa_pair_from_output(output)
    synthesized_data.append(qa_pair)

# 6) 結果を保存
save_dataset(synthesized_data)

このコードの中では、明示的な乱数シードの指定はしていません。そのため、直感的にはこれを複数回実行すると毎回違うseed_textが得られ、それぞれ異なる結果が得られるように思えます。

しかし、上述したように内部的にseed=0が乱数シードとしてモデルロード時に設定されているため、このスクリプトを何回繰り返し実行してもrandom.choiceで選択されるseed_textは毎回同じものになり、最終的に得られる出力も全く同じものになってしまいます。

例えば、大量にあるシードテキストのデータセットを元に一定件数データを作成し終わった後、もう一度同じスクリプトを実行して多くのデータを作成しようとしても、全く同じデータセットが出来てしまいます。

回避策

回避策は非常にシンプルで、LLMクラスのインスタンスを初期化する際にseedの引数に毎回異なる値が渡されるようにすれば良いです。例えば以下のように現在時刻から秒単位で異なるシード値を渡すことが出来ます。

import time
from vllm import LLM

# 現在時刻を利用して一意のシード値を生成し利用
seed_value = int(time.time())
model = LLM(model="model_name", seed=seed_value)

あるいは、同様にシード値を生成し改めてrandom.seed(seed_value)のように設定しても良いでしょう。

まとめ

この記事では、個人的に遭遇したvLLMを使う際に乱数シードが意図せず固定されてしまう問題と、その回避策についてまとめました。

特に合成データを作る際に遭遇し得る問題かと思います。この記事が同様の問題に遭遇した方の一助になれば幸いです。

Discussion