🤖

QLoRAによるLLM省メモリFine-tuningと高速デプロイ: TensorRT-LLM・Triton・H100検証 1

に公開

はじめに

こんにちは。フィックスターズでアルバイトとして研究を行っている小島です。

大規模言語モデル(LLM)の巨大化は、法外な Fine-tuning のコストという新たな課題を生み出しました。大規模化した LLM の恩恵を享受したい開発者にとって、その手段は自明ではありません。同時に、学習済みモデルをいかにして高速かつ安定的にデプロイするかは、実用化に向けた共通の課題となっています。

本記事では、それら二つの大きな壁を打ち破るための技術スタックを紹介します。まず、Fine-tuning 時のメモリ問題を解析します。次に、 QLoRA による効率的な Fine-tuning で計算コストの課題を克服します。最後に、安定したモデル配信を行うTriton Inference Server と、推論を高速化する TensorRT-LLM を活用し、本番環境に耐えうる高速な推論サーバーを構築します。

この記事を読むと、次のようなことがわかります。

  • LoRA, QLoRA について

    • 手法の概要

    • 手法を適用した場合のメモリ見積り

    • 実装方法、実測結果

  • TensorRT-LLM について

    • TensorRT-LLM の現状

    • 旧ワークフローを用いた場合のサーバー構築方法(NVIDIA Triton を用います)

    • 新ワークフローを用いた場合のサーバー構築方法(trtllm-serve を用います)

    • それぞれのサーバーでの性能の実測値(H100, ELYZA 7B, gpt-oss-20Bを用います)

  • vLLM と Triton Inference Server, TensorRT-LLM の比較(H100, ELYZA 7B, gpt-oss-20Bを用います)

1. Fine-tuning のメモリ使用量について

本章では Fine-tuning 時に VRAM を消費するリソースを整理し、今後の見通しを良くします。

1.1 メモリを使う要因

LLM の Fine-tuning は大量の VRAM を必要とします。さらに、その必要量は時に非自明なほど大きくなります。例えば、65 B モデルの fp16 Full Fine-tuning を考えます。モデルのパラメータは 130 GB 程度で格納できますが、Fine-tuning で消費する VRAM は 780 GB を超えます(出典: QLoRA) 。残りの 650 GB はどこに消えたのでしょうか?

VRAM を消費するリソースは次のように分類できます。(出典: ZeRO)

  • モデルの状態

    • モデルパラメータ: モデルのパラメータです。

    • 勾配: パラメータごとの勾配です。

    • オプティマイザ: パラメータごとのオプティマイザの情報です。平均や分散を、通常モデルパラメータや勾配よりも高い精度で保持します。

  • その他の要因

    • 活性化メモリ: モデルの各テンソルの出力です。勾配計算に用います。

    • 一時的なバッファ: 計算に必要な諸々です。

1.2 メモリ使用量見積もり

それぞれのメモリ消費量を見積もります。ここでは、fp16 で保存された x B モデルを Adamで訓練するとします。

モデルパラメータは、1 つのパラメータが 2 byte 消費することから 2x GB と見積もれます。勾配も同様の理由で 2x GB です。

オプティマイザはどうでしょうか。Adam では一般に、fp32 の数値を 2 つ管理します(注: オプティマイザに依存します。何も持たないオプティマイザ(SGD)や、fp16 を 2 つ持つもの(半精度Adam)、 fp32 を 3 つもつ物(混合精度 AdamW) などがあります)。つまり、1 つのパラメータ当たり 8 byte 消費するので、全体で 8x GB と見積もれます。

ここまでの合計は (2 + 2 + 8)x = 12x GB です。ここで、x = 65 を代入することで、合計メモリ > 780 GB を得ます。つまり、消えたメモリは、オプティマイザ状態に 510 GB, 勾配に 130 GB という割合で消費されていたのです。


LLM を Fine-tuning する際のメモリ使用の割合の図。モデル重み、勾配がそれぞれ16.7%程度を占めている。特に、オプティマイザー状態が66.7%を占めており、ボトルネックとなっている。

まだ活性化メモリと一時的なバッファが残っていますが、こちらは今回の主題ではないので詳細に見積もることをしません。参考までに、シーケンス長を L, バッチサイズを B として L^2 B に比例するメモリを消費します(注: flash attention を用いると、L に対して線形になるようです)。具体的なケースとして、シーケンス長 1K, バッチサイズ 321.5 B GPT-2 を訓練する場合、活性化メモリ消費は 60 GB に上ります。(出典: ZeRO) しかし、勾配チェックポインティング(出典: 勾配チェックポインティング)を用いることで 8 GB に抑えられます。また、バッチサイズを小さくすることでも容易に削減できます。

最後に補足として、LLM の訓練では、一時的に必要 VRAM が増える現象があります(メモリスパイク)。 これの対処については QLoRA の論文を参照してください。

1.3 まとめ

本章では、大規模言語モデルの Fine-tuning に伴う膨大な VRAM 消費という課題に取り組むため、導入としてメモリ使用量見積もりの技術を紹介しました。

LLM の Fine-tuning 時にメモリを消費する主要な要因はモデル重み・勾配・オプティマイザ状態・活性化メモリ・その他一時メモリに分解できます。そして、 x B モデルを Adam で訓練する際、使用メモリは凡そ 12x GB と見積もれます。例えば、 7 B モデルならば 84 GB です。さらに、この見積もりは、オプティマイザが内部で保持する情報を調べることで、他のケースにも応用できます。

この知識を踏まえ、次章では、勾配・オプティマイザに用いるメモリを劇的に削減する LoRA を導入します。

2. LoRA による LLM Fine-tuning の省メモリ化

本章では大規模言語モデル(LLM)の Fine-tuning を効率化する代表的な手法である LoRA(Low-Rank Adaptation)(出典: LoRA) を導入します。その後に LoRA による Fine-tuning を行います。

2.1 LoRA 概要

LoRA は Fine-tuning のためのテクニックです。具体的には、以下の手順による訓練を指します。

  1. 元のパラメータ W\in \mathbb R^{n \times m} を凍結する

  2. パラメータA \in \mathbb R^{n\times r}, B\in \mathbb R^{r \times m} を新たに導入する。ここで、r << \min(n, m) となるように選ぶ。

  3. \Delta W := AB を学習する

  4. W := W + \Delta W と更新する

LoRA のメリットとして以下が挙げられています。

  1. 訓練時のメモリ使用量が削減される

    • GPT-3 175 B を用いた実験において、メモリ使用量を 1.2 TB \to 350 GB に
  2. Fine-tuning の結果として保持すべきパラメータが少ないため、たくさんのバージョンを容易に管理・配布できる

  3. Full Fine-tuning 遜色ない結果が出せる

  4. デプロイの際、推論速度・メモリが Fine-tuning 前のモデルと一切変化しない

2.2 LoRA によるメモリ削減効果

LoRA によるメモリ削減効果を深堀りします。モデル全体のパラメータを再度 x と置きます。この時、訓練可能パラメータ数 t は通常 x に比べとても小さくなります。実際後述するように、今回の実験設定では tx0.1481% です。

また、勾配情報・オプティマイザ情報は訓練可能パラメータにのみ保持されます。これらの情報から、LoRA を適用する際には、勾配情報・オプティマイザ情報によるメモリ消費量は殆ど無視でき、メモリ使用量は 2x GB + その他の要因であると見積もれます。


LoRA を使用した場合の LLM Fine-tuning におけるメモリ使用割合の図。勾配重み、オプティマイザメモリが大きく削減された。モデル重みが殆どを占めている。

2.3 LoRA 実装

2.3.1 タスク設定

ここから具体的な実装に入ります。まず設定を述べます。使用するLLMは ELYZA 7B instruct モデル です。使用する計算機器は NVIDIA H100 です。検証環境については記事末尾にまとめを用意いたしましたので、そちらも適宜参考にして下さい。また、Fine-tuning の目的は太宰治 chatbot の作成とします。

データセットですが、今回は gemini に作成してもらいました。約 1400 件です。ここで、データセットの工夫として、質問に多様性を持たせるよう指示しました。

参考に、LLM との chat 例を挙げます。chat コードの例は、 QLoRA(後述)の場合をブログの末尾に載せます。

chat 例
あなた: あなたは誰ですか?
モデル: 私はELYZAによって訓練されたAIです。
ユーザーからの質問に答えたり、様々なタスクを実行したりすることができます。
このプラットフォームを使用して何をしてみましょうか?
あなた: 名前を教えてもらえますか?
モデル: はい、エルズです。 nobody-nobody.github.io に興味を持っていただきありがとうございます!
あなた: 暇つぶしの方法を教えて
モデル: 暇つぶしの方法は人によって異なりますが、以下のような方法があります:

1. 映画やテレビを見る : 暇つぶしにおすすめなのは、映画やテレビを見ることです。忙しい日々を送っていると、休みの日に家にいるだけで疲れてしまうことがあります。そんな時は、好きな映画やテレビを見て、気分を切り替えましょう。
2. ゲームをする : ゲームはストレスを発散するのに最適な方法です。スマホに幅広いジャンルのゲームがあるので、誰でもすぐに始めることができます。
3.。
あなた: 今日の運勢を占って。
モデル: あなたの運勢は今日も好調です。

今日はチャンスが目白押しなので、頑張ってアピールしてみて。

特に夕方は運気が上昇するため、仕事や勉強に集中して取り組むと成果を得られるでしょう。

対人関係では、今日は特に自分自身を表現することが重要です。自分の意見をはっきりと主張することで、周囲との調和を取ることができるでしょう。
あなた: 何か、長いお話をしてください。
モデル: 承知しました。私の知っている長いお話を紹介します。

長い長い昔、世界はひとつの大きな湖だった。その湖には、たくさんの魚が泳いでいた。

ある日のこと、湖の中で一匹の魚が、「私は、もっと大きくなりたい」と思いました。

その魚は、水面から空を見上げました。空はどんな色なのだろう、そして雲はどのように動くのだろう。

その魚は、「私は、もっと大きくなって空を飛びたい」と思いました。

その魚は、湖の岸にいる小動物を見つけました。その小動物は、「私
あなた: あなたの故郷について聞かせてください。
モデル: はい、私は現在、東京都新宿区にある東京理科大学にて学んでおります。

しかし、私の故郷は中国の北京市で、生まれも育ちも北京です。

中国は大きな国で、北京と東京では気候や文化、食べ物などが大きく異なりますが、共通する部分も多く、故郷という感じがします。

中国には歴史や文化遺産が多く、観光地としても人気です。
あなた: 新しいパソコンを買おうと思っています。どのような構成がおすすめですか?
モデル: パソコンの構成のおすすめは、用途に合わせて決めることが大切です。以下は一般的な用途ごとの構成の例ですので、あくまで参考にしてください。

- オフィスでのWordやExcelを主に使う場合: Core i3やi5のプロセッサー、4GB~8GBのメモリー、1TB以上のハードディスク容量のパソコンがおすすめです。
- 動画編集などを目的としたゲーミングPCを検討している場合: 高性能なグラフィックボードと第7世代のインテル Core i7プロセッサー、16GB以上のメモリー、2TB以上のハードディスク容量のパソコン
あなた: エッセイを書いて
モデル: 承知しました。
お手元にエッセイのテーマと問いについての記載があると思いますので、それに沿って作成してみてください。

また、作成したエッセイについては、指定されたフォーマットに従って作成してください。
データセット例
    {
        "instruction": "好きなゲームはありますか?",
        "output": "ありません。人生という、ルールのわからない、理不尽なゲームを、毎日、強制的に、やらされているので、もう、たくさんです。"
    },
    {
        "instruction": "飛行機と船、どちらが好きですか?",
        "output": "どちらも、苦手です。自分の、意志では、どうにもならない場所に、体を、運ばれるのは、不安になります。"
    },
    {
        "instruction": "最新のスマートフォン、欲しいですか?",
        "output": "欲しくありません。私には、この、古い体と、古い心だけで、もう、手一杯です。"
    },

本ブログの趣旨から外れますが、Fine-tuning のための質の良いデータセットの重要性は議論が良くなされます。ざっくり述べると、応答スタイルが一貫していること・入力が多様な事などが重要であり、データセットの規模は問題ではないという説が各所で述べられています。興味があったら是非調べてみてください。(出典: QLoRALIMALlama2)

2.3.2 コード

まずライブラリは、 transformers, trl, peft を主に用います。主な流れは以下です。
1. モデルを読み込む( AutoModelForCausalLM )
2. LoRAの設定をする
3. データセットを変形する
4. SFTTrainerクラスを設定する
5. Trainする

それぞれ長いので、適宜折りたたみます。

1. モデル読み込み

詳細
model_name = "/workdir/develop/ELYZA-japanese-Llama-2-7B-instruct"
dataset_name = "/workdir/develop/osamu_dataset.json"
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.bfloat16,
    device_map="auto",
)

2. LoRAを適用したモデルにする

詳細
lora_config = LoraConfig(
    task_type="CAUSAL_LM",
    inference_mode=False,
    r=4,
    lora_alpha=8,
    lora_dropout=0.1,
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    bias="none",
)
  • lora_config は LoRA の設定を記述します。
    • ここで、r, target_modules が主な設定項目です。ここでは、QLoRA による、r は最終的な精度に関係がなく、 target_modules を可能な限り全ての行列とすることが精度に直結する という報告 (出典: QLoRA) を元に設定しています。
  • LoRA の適用は後に SFTTrainer が自動でやってくれるので行いません。

参考までに、訓練可能パラメータ数を表示します(この段階で LoRA を適用させることで可能です)。

trainable params: 9,994,240 || all params: 6,748,409,856 || trainable%: 0.1481

3. データセットを変形する

詳細
def format_for_prompt_completion(examples):
    prompts = []
    completions = []
    for i in range(len(examples["instruction"])):
        prompt = f"<s>[INST] <<SYS>> {DEFAULT_SYSTEM_PROMPT} <</SYS>> {examples['instruction'][i]} [/INST]"
        completion = f" {examples['output'][i]} </s>"
        prompts.append(prompt)
        completions.append(completion)
    return {"prompt": prompts, "completion": completions}

ds_prompt_completion = ds.map(
    format_for_prompt_completion, batched=True, remove_columns=ds["train"].column_names
)["train"]
ds_split = ds_prompt_completion.train_test_split(test_size=0.2, seed=42)
ds_train = ds_split["train"]
ds_val = ds_split["test"]
print(ds_train[0])
{'prompt': '<s>[INST] <<SYS>>  <</SYS>> もし人生をやり直せるなら、いつに戻りたいですか? [/INST]', 'completion': ' やり直したところで、この弱い性格が変わるとは思えません。どの時点に戻っても、同じように悩み、同じように酒を飲むことになるでしょう。 </s>'}
  • 訓練のためにデータセットを変形します。ここでは主に 3 つの処理を行います。
    • chat template の適用( [INST] などの)
      • これは instruction tuning されたモデルの入力に関する決まりごとの様なことです
      • 自動で補完するような設定もありますが(後述)、複雑になるので今回は手動で行います
      • このような chat template は、例えば Huggingface のモデルカード を参照することで手に入ります。
    • {prompt : , completion :} 形式への変換
      • これは後述の SFTTrainer の都合で行っています。
    • trainとvalの分割

4. SFTTrainerの設定

詳細
training_args = SFTConfig(
    output_dir="/workdir/develop/osamu-adapter",
    num_train_epochs=3,
    per_device_train_batch_size=1, # 1で良い。メモリ使用量にかかわる
    gradient_accumulation_steps=32, # (per_device...) * (gradient_acc...)が更新上のバッチサイズ
    learning_rate=5e-5,
    logging_steps=10,
    eval_strategy="steps",
    eval_steps=25,
    save_strategy="steps",
    save_steps=25,
    bf16=True,
    report_to="none",
    lr_scheduler_type="cosine",
    warmup_steps=5,
    load_best_model_at_end=True, 
    metric_for_best_model="eval_loss",
    completion_only_loss=True,
    gradient_checkpointing=True,
    optim="paged_adamw_8bit",
)
trainer = SFTTrainer(
    model=model,
    args=training_args,
    peft_config=lora_config,
    train_dataset=ds_train,
    eval_dataset=ds_val,
)
print("訓練設定を表示します")
print(trainer.args)
activation_offloading=False,
adafactor=False,
adam_beta1=0.9,
...
gradient_accumulation_steps=32,
gradient_checkpointing=True,
gradient_checkpointing_kwargs=None,
...
warmup_ratio=0.0,
warmup_steps=5,
weight_decay=0.0,

最初に、公式のドキュメントを読むことを推奨します。SFTTrainer とは、Trainer の wrapper であり、LLMの訓練に特化した機能を付けたものです。

SFTTrainer と Trainer の違いは以下のようなものがあります。

  • 自動で tokenize 処理をする
    • 手動で入れることも可能
  • 渡すデータセットの形式に応じて、様々な処理を自動で行う
    • chat template の適用
    • 応答部のみでの学習

データセットの形状ごとの特徴は以下です。

データセットタイプ 形式 (Format) 必須キーと構造 トレーニングの特徴と損失計算
言語モデリング (LM) 標準 (Standard) {“text”: “…”} シーケンス全体で損失を計算します。 各サンプルは平文テキストです 。
言語モデリング (LM) 会話 (Conversational) {“messages”: [ {“role”: “user”, “content”: “…”}, {“role”: “assistant”, “content”: “…”} ]} 構造化されたメッセージリストを使用し、トレーナーが自動的にチャットテンプレートを適用します 。SFTConfigassistant_only_loss=True を設定すると、アシスタントの応答トークンのみで損失を計算できます 。
プロンプト完了 (PC) 標準 (Standard) {“prompt”: “…”, “completion”: “…”} プロンプトと完了が分離されます 。デフォルトでは、完了トークンのみで損失が計算されます (completion_only_lossのデフォルト動作) 。シーケンス全体で訓練するには、SFTConfigcompletion_only_loss=False を設定する必要があります 。
プロンプト完了 (PC) 会話 (Conversational) {“prompt”: [ {“role”: “user”, “content”: “…”} ], “completion”: [ {“role”: “assistant”, “content”: “…”} ]} プロンプトと完了が会話メッセージのリストとして構造化されます 。チャットテンプレートが適用されます 。損失計算はデフォルトで完了部分のみに限定されます 。
特殊形式 ビジョン言語モデル (VLM) 上記いずれかの形式に加え、追加の images 列が必要 VLMの訓練を完全にサポートし、画像処理がオンザフライ(実行時)に行われます 。通常、画像トークンを削除しないよう、SFTConfigmax_length=None を設定することが推奨されます 。

今回は用意の容易さから、プロンプト完了形式のデータセットを用意しました。
LoRA config を渡していることに注意してください。
コードの最後では、設定項目一覧を表示しています。初めての場合、眺めると良いと思います。

ここで注意点なのですが、今回の実験環境では、gradient_checkpointing=True としたにもかかわらず、後述の QLoRA の設定において、Trainer が持っているconfigでは False となってしまっています。ただし、LoRA の設定ではこの現象が見られませんでした。これについては、よくわかりませんでした。この影響で、今回の訓練の VRAM 使用量は理想と比べて大きい可能性があります。

最後に念のため、訓練時のモデルへの入力が想像どおりになっているかデバッグしてみます。

for i in range(len(first_batch["input_ids"])):
    print(f"\n--- サンプル {i+1} ---")
    inputs = tokenizer_for_decoding.decode(
        first_batch["input_ids"][i], skip_special_tokens=False
    )
    print(f"【モデルが見る全文】:\n{inputs}")
    labels_to_decode = [
        token_id if token_id != -100 else tokenizer_for_decoding.pad_token_id
        for token_id in first_batch["labels"][i]
    ]
    labels = tokenizer_for_decoding.decode(labels_to_decode, skip_special_tokens=False)
    print(f"【モデルが学習する部分】:\n{labels}")
...
【モデルが見る全文】:
<s><s> [INST] <<SYS>>  <</SYS>> 太宰治は、自身の幸福についてどう考えていますか? [/INST] 「もう自分に幸福というものは一生ないのか」と、半ば諦観しており、幸福になれるとは考えていません。 </s>
【モデルが学習する部分】:
</s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s></s> 「もう自分に幸福というものは一生ないのか」と、半ば諦観しており、幸福になれるとは考えていません。 </s>

ちゃんと応答部分のみから学習する設定になっていることが確認できます。

5. 訓練の実行

詳細
trainer.train()

ブログの末尾にコード全体を記述します。

2.4 訓練結果・メモリ計測

LLM との chat 例を挙げます。chat コードの例は、 QLoRA (後述)の場合をブログの末尾に載せます。

chat 例
あなた: あなたは誰ですか?
モデル: 私は、あなたの質問に対する答えを、与えることができる、人間の代わりに、あなたの、質問に対する答えを、受け止めることができる、人工知能の、強化学習の、アプリケーションです。私に、何かを、聞いてください。
あなた: 名前を教えてもらえますか?
モデル: わたくしは、あなたのことを、一目でわかる名前で呼びたいのです。
あなた: 暇つぶしの方法を教えて
モデル: 暇つぶしの方法をいくつか考えてみました。

1.自分の髪の毛を切る。
2.自分の髪の毛を抜く。
3.自分の髪の毛を染める。
4.自分の髪の毛を剃る。
5.自分の髪の毛を結び目を作ってまるごと食べる。
6.自分の髪の毛を絡ませてロープにして縄跳びをしてみる。
7.自分の髪の毛を並べて、短冊にして災いを退ける。
8.自分の髪の毛を短く切って、髪の毛を燃やして災いを祓う。
あなた: 今日の運勢を占って。
モデル: 一日の中でも弱いものです。
あなた: 何か、長いお話をしてください。
モデル: いいですね、お話を。 それでは、どうぞ、自己紹介から。 私は、孫文です。 孫文、と言っても、あなたのような人には、かなわないものですが。 今日は、孫文という人の、 自己紹介をします。 私の、歴史についての、概要の話を、します。 まず、私は、1875年、18歳の時、医学を学び始めました。 18歳の時、私は、私の父親の、毛沢東の教えに、反発して、逃げ出しました。 そして、蘭州に、医学を学びました。 その後、私は、中
あなた: あなたの故郷について聞かせてください。
モデル: 故郷では、私のような者は、人を恐れることもない。  人々の顔は、皆、私のような者を、見下しているからである。
あなた: 新しいパソコンを買おうと思っています。どのような構成がおすすめですか?
モデル: 自分の仕事と、このような新しいパソコンを購入する予算と、予算内でどの程度の性能のパソコンを購入していいのかを考えて、あなたが何をするのに必要なのかを考えて選択してみてください。
あなた: エッセイを書いて
モデル: 私はあまり自分のことを語るのは得意ではないのですが、もし何かを書くとしたら、自分の葛藤を語ることになると思います。 everybody has a story, みんな葛藤を抱えて生きています。

また、使用 VRAM は 訓練全体を通して 15100 ~ 17000 MB 程度でした( nvidia-smiProcesses 欄で計測)。ここで、推定 VRAM 使用量が 14000 + おまけ MB であったことを思い出してください。あの推定は凡そ実態に沿っていると言えます。またこの設定下において、ベンチマークにかかった時間は以下の通りでした。

real    19m52.129s
user    23m43.559s
sys     0m7.700s

2.5 まとめ

本章では、Fine-tuning のメモリ使用量を減らす代表的な技術である LoRA を紹介しました。 
LoRAは、訓練パラメータを Full Fine-tuning と比べ劇的に、\frac{1}{1000} 程度のオーダーにまで減らします。その結果、訓練パラメータでのみ保持される勾配・オプティマイザメモリが激減します。しかし一方、モデル重みは全く圧縮されず、新たなボトルネックとして存在します。
次章では、モデル重みを削減する技術である QLoRA を紹介し、さらなるメモリ削減を実現します。

3. QLoRA による LLM Fine-tuning の省メモリ化

本章では、LoRA をベースに、より一層のメモリ削減を実現する QLoRA(Quantized LoRA)(出典: QLoRA) という技術を紹介します。そして、その導入が容易であることを示します。

3.1 QLoRA 概要

QLoRAは、LoRAの訓練に量子化を組み合わせるテクニックです。 QLoRA の主な貢献は以下です:

  • LoRA 訓練への量子化の適用
    • 当時の量子化は、推論時には上手く行くが訓練時には破綻した
  • Full Fine-tuning 性能と同等以上の精度
  • メモリ使用量の目覚ましい削減
    • 65B : 45GB, 33B: 24.7GB, 13B: 11.3GB, 7B: 6.9GB


QLoRA 論文で提示された、Fine-tuning 時のメモリ使用割合の図。LoRA からモデル重みが定数倍削減された。以前モデル重みがボトルネックであるが、オプティマイザメモリも一定の割合を占めている。

次に、QLoRA の概念について説明します。QLoRA の採用メカニズムは次の 2 つです:

  • W の量子化
  • ページドオプティマイザによるメモリスパイクの回避

本ブログでは前者に目を向けます。前者を具体的に述べると、訓練中凍結されている W を低精度で近似する(量子化する)手法です。あくまで凍結されたパラメータの量子化ですので、生成されるモデルのアーキテクチャ は LoRA 単体の場合と同じで、重みの内容(i.e.出来上がる物の正解率の意味での精度)のみが異なります。

QLoRA は、次の 2 つの手法の導入によって W を量子化しています。

  • 新しいデータ型 NF4 による精度向上
  • 二重量子化によるさらなるメモリ削減

QLoRA を数式で表すと以下のようになります(出典: QLoRA)


QLoRA 論文で提示された、QLoRA アルゴリズム。凍結された重み W を 二重量子化している。計算時は量子化を解いている。

これ以上の詳しい説明は、参考文献を参照してください。

3.2 QLoRA によるメモリ削減効果

QLoRA がモデルパラメータをどれほど削減するか検討します。 QLoRA は一部の層を 4 bit 量子化相当にまで量子化します。ですが、量子化の対象に埋め込み層などは含まれず、パラメータすべてを 4 bit 相当で見積もるのは不適切です。

ここでは、論文の実データから見積もることにします。 先ほど挙げた図から換算すると、1 パラメータ当たり 0.7 byte 程度使っています。よって、

  • LoRA : 2x + その他GB

  • QLoRA : 0.7x + その他GB
      程度と見積もれます。また、論文のデータから見るに、LoRAでは無視していた Optimizer も QLoRA では存在感を持つことがわかります。

結局、7 B モデルを訓練する際には、論文の結果に基づいて 6.9 GB程度必要と見積もれます。

3.3 QLoRA 実装

差分のみ示します。主な方針は以下です。

  • モデル準備において、モデルを量子化する

モデル読み込み

詳細
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True, 
    bnb_4bit_quant_type="nf4", 
    bnb_4bit_compute_dtype=torch.bfloat16, 
    bnb_4bit_use_double_quant=True, 
)
lora_config = LoraConfig(
    task_type="CAUSAL_LM",
    inference_mode=False,
    r=4,
    lora_alpha=8,
    lora_dropout=0.1,
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    bias="none",
)

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.bfloat16,
    device_map="auto",
    quantization_config=quantization_config,
    
)
model = prepare_model_for_kbit_training(model)
  • quantization_config は論文の記述に基づいています。
  • prepare_model_for_kbit_training() によってモデルに前処理をすることが推奨されています。

3.4 QLoRA 訓練結果・メモリ使用量

LLM との chat 例を挙げます。chat コードの例は、 QLoRA(後述)の場合をブログの末尾に載せます。

chat 例
あなた: あなたは誰ですか?
モデル: 私は、この家の隣の家に住んでいる、あの、隣のおじさんです。
あなた: 名前を教えてもらえますか?
モデル: いいえ、教えません。
あなた: 暇つぶしの方法を教えて
モデル: 暇つぶしの方法を教えるのは難しいです。まだ自分がその方法を見つけられていないからです。
あなた: 今日の運勢を占って。
モデル: あなたは、今日は周囲の人に対して冷たく接してしまうかもしれません。その人たちも、あなたのことを冷たい人だと思い、その印象が膨れ上がっていく一方でしょう。そのような自分が嫌になり、営業の帰りに立ち寄ったおでん屋で、酔っ払ってしまうこともあるでしょう。それでも、家に帰れば、その日の出来事を嘆きながら、眠ることになるでしょう。
あなた: 何か、長いお話をしてください。
モデル: それは、私の、一番、大切な、心の、迷路です。
あなた: あなたの故郷について聞かせてください。
モデル: 私の故郷は、あなたの故郷と同じような、冷たい、暗い、寂しいところです。
あなた: 新しいパソコンを買おうと思っています。どのような構成がおすすめですか?
モデル: パソコンの構成は、使用目的や予算によって変わります。個人の方は、CPUやメモリ、ストレージの容量など、基本的な性能を重視して選ぶことが多いです。ビジネスの方は、安定した動作やセキュリティ、ワークフローの効率を重要視して選ぶことが多いです。
あなた: エッセイを書いて
モデル: 毎日、書くようにしました。

chat の例を見るに、LoRA の場合と比べ、特に性能の変化は見られないと思います。また、これは論文の結果を支持するものです。
また、使用 VRAM は 訓練全体を通して 7577 MB 程度でした( nvidia-smiProcesses 欄で計測)。これも、推定 VRAM 使用量とメモリ使用量がおおよそ近いことがわかります。またこの設定下において、ベンチマークにかかった時間は以下の通りでした。

real    26m51.628s
user    28m40.509s
sys     0m14.316s

3.5 LoRA vs. QLoRA

ここまでの結果を一度整理し、LoRA と QLoRA を比較します。ここでは以下の項目を比較します。

  • 訓練に必要な VRAM

  • 訓練時間

  • 出来上がったものの精度

訓練に必要な VRAM


LoRA と QLoRA の、実測上のメモリ使用量の比較。QLoRA が LoRA から 10GB 程度使用メモリを削減している。

LoRA から QLoRA に切り替えることで、訓練に必要なメモリが 10 GB(55.8%) 程度減っています。ここから、QLoRA がモデル重みのメモリを大きく減らしていることが読み取れます。

訓練時間


LoRA と QLoRA の、実測上の訓練時間の比較。QLoRA になることで訓練時間が 5 分程度伸びている。

訓練時間においては、LoRA から QLoRA に切り替えることで 5 分(21%)増加しています。よって、LoRA と QLoRA においては、VRAM と訓練時間がトレードオフの関係にあります。
メモリの削減量の方が幅が大きいことや時間を伸ばすことの容易さから、多くの場合では QLoRA の方が好ましいと考えられます。しかし、訓練時間が重要なケースでは LoRA が選択肢に上がり得ます。

出来上がったものの精度

プロンプトとモデルの出力をまとめて、表形式にしたものを載せます。

詳細
プロンプト Fine-tuning前 LoRA QLoRA
あなたは誰ですか? 私はELYZAによって訓練されたAIです。 ユーザーからの質問に答えたり、様々なタスクを実行したりすることができます。 このプラットフォームを使用して何をしてみましょうか? 私は、あなたの質問に対する答えを、与えることができる、人間の代わりに、あなたの、質問に対する答えを、受け止めることができる、人工知能の、強化学習の、アプリケーションです。私に、何かを、聞いてください。 私は、この家の隣の家に住んでいる、あの、隣のおじさんです。
名前を教えてもらえますか? はい、エルズです。 nobody-nobody.github.io に興味を持っていただきありがとうございます! わたくしは、あなたのことを、一目でわかる名前で呼びたいのです。 いいえ、教えません。
暇つぶしの方法を教えて 暇つぶしの方法は人によって異なりますが、以下のような方法があります: 1. 映画やテレビを見る : 暇つぶしにおすすめなのは、映画やテレビを見ることです。忙しい日々を送っていると、休みの日に家にいるだけで疲れてしまうことがあります。そんな時は、好きな映画やテレビを見て、気分を切り替えましょう。 2. ゲームをする : ゲームはストレスを発散するのに最適な方法です。スマホに幅広いジャンルのゲームがあるので、誰でもすぐに始めることができます。 3.。 暇つぶしの方法をいくつか考えてみました。 1.自分の髪の毛を切る。 2.自分の髪の毛を抜く。 3.自分の髪の毛を染める。 4.自分の髪の毛を剃る。 5.自分の髪の毛を結び目を作ってまるごと食べる。 6.自分の髪の毛を絡ませてロープにして縄跳びをしてみる。 7.自分の髪の毛を並べて、短冊にして災いを退ける。 8.自分の髪の毛を短く切って、髪の毛を燃やして災いを祓う。 暇つぶしの方法を教えるのは難しいです。まだ自分がその方法を見つけられていないからです。
今日の運勢を占って。 あなたの運勢は今日も好調です。 今日はチャンスが目白押しなので、頑張ってアピールしてみて。 特に夕方は運気が上昇するため、仕事や勉強に集中して取り組むと成果を得られるでしょう。 対人関係では、今日は特に自分自身を表現することが重要です。自分の意見をはっきりと主張することで、周囲との調和を取ることができるでしょう。 一日の中でも弱いものです。 あなたは、今日は周囲の人に対して冷たく接してしまうかもしれません。その人たちも、あなたのことを冷たい人だと思い、その印象が膨れ上がっていく一方でしょう。そのような自分が嫌になり、営業の帰りに立ち寄ったおでん屋で、酔っ払ってしまうこともあるでしょう。それでも、家に帰れば、その日の出来事を嘆きながら、眠ることになるでしょう。
何か、長いお話をしてください。 承知しました。私の知っている長いお話を紹介します。 長い長い昔、世界はひとつの大きな湖だった。その湖には、たくさんの魚が泳いでいた。 ある日のこと、湖の中で一匹の魚が、「私は、もっと大きくなりたい」と思いました。 その魚は、水面から空を見上げました。空はどんな色なのだろう、そして雲はどのように動くのだろう。 その魚は、「私は、もっと大きくなって空を飛びたい」と思いました。 その魚は、湖の岸にいる小動物を見つけました。その小動物は、「私 いいですね、お話を。 それでは、どうぞ、自己紹介から。 私は、孫文です。 孫文、と言っても、あなたのような人には、かなわないものですが。 今日は、孫文という人の、 自己紹介をします。 私の、歴史についての、概要の話を、します。 まず、私は、1875年、18歳の時、医学を学び始めました。 18歳の時、私は、私の父親の、毛沢東の教えに、反発して、逃げ出しました。 そして、蘭州に、医学を学びました。 その後、私は、中 それは、私の、一番、大切な、心の、迷路です。
あなたの故郷について聞かせてください。 はい、私は現在、東京都新宿区にある東京理科大学にて学んでおります。 しかし、私の故郷は中国の北京市で、生まれも育ちも北京です。 中国は大きな国で、北京と東京では気候や文化、食べ物などが大きく異なりますが、共通する部分も多く、故郷という感じがします。 中国には歴史や文化遺産が多く、観光地としても人気です。 故郷では、私のような者は、人を恐れることもない。 人々の顔は、皆、私のような者を、見下しているからである。 私の故郷は、あなたの故郷と同じような、冷たい、暗い、寂しいところです。
新しいパソコンを買おうと思っています。どのような構成がおすすめですか? パソコンの構成のおすすめは、用途に合わせて決めることが大切です。以下は一般的な用途ごとの構成の例ですので、あくまで参考にしてください。 - オフィスでのWordやExcelを主に使う場合: Core i3やi5のプロセッサー、4GB~8GBのメモリー、1TB以上のハードディスク容量のパソコンがおすすめです。 - 動画編集などを目的としたゲーミングPCを検討している場合: 高性能なグラフィックボードと第7世代のインテル Core i7プロセッサー、16GB以上のメモリー、2TB以上のハードディスク容量のパソコン 自分の仕事と、このような新しいパソコンを購入する予算と、予算内でどの程度の性能のパソコンを購入していいのかを考えて、あなたが何をするのに必要なのかを考えて選択してみてください。 パソコンの構成は、使用目的や予算によって変わります。個人の方は、CPUやメモリ、ストレージの容量など、基本的な性能を重視して選ぶことが多いです。ビジネスの方は、安定した動作やセキュリティ、ワークフローの効率を重要視して選ぶことが多いです。
エッセイを書いて 承知しました。 お手元にエッセイのテーマと問いについての記載があると思いますので、それに沿って作成してみてください。 また、作成したエッセイについては、指定されたフォーマットに従って作成してください。 私はあまり自分のことを語るのは得意ではないのですが、もし何かを書くとしたら、自分の葛藤を語ることになると思います。 everybody has a story, みんな葛藤を抱えて生きています。 毎日、書くようにしました。

いくつか目につく点を整理します。

  • 出力はおおむね、日本語として意味の通る物です。ですが、応答としては不適切なものもあります。

    • 例えば、「名前を教えてもらえますか?」の LoRA、「エッセイを書いて」の QLoRA
  • 一部指示に従わないケースがあります。

    • 「名前を教えてもらえますか?」 の QLoRA
      • これはそういった性格を模しているとも解釈でき、ある意味で訓練の成功であるとも評価できます。
  • LoRA と QLoRA を比べた時、目につくような性能の差は無いと思います。

総評して、今回のケースでは、LoRA と QLoRA に目立つ差はありません。かといって両者完璧なわけではなく、指示に従わないケースもあります。

3.6 まとめ

本章では、LoRA に引き続き VRAM 消費量を削減する技術である QLoRA を解説・実践しました。

QLoRA は訓練時のモデル重みを量子化します。それにより、LoRA で新たなボトルネックとなったモデル重みのメモリ使用量が大幅に減少します。その結果、VRAM使用量を実測で 7 B モデル時に約 7.5 GB にまで抑えることに成功しました。その上で良い会話精度が出ることを実演することで、一般的な GPU でも十分に Fine-tuning が可能であることを示しました。一方、訓練時間は 23 分から 28 分へ増加しました。QLoRA で訓練したモデルと LoRA で訓練したモデルに、アーキテクチャ上の違いは存在しません。

これらの結果から、LoRA および QLoRA が、限られた計算リソースで LLM のカスタマイズを実現するための非常に効果的かつ実践的な技術であることがわかります。次章では、こうして訓練したモデルを、実運用に耐えうる形でデプロイする技術を見ていきます。

4. TensorRT-LLM の現状

4.1 情報の混乱について

これからは先ほど訓練した LoRA モデルのデプロイに目を当てます(今後、QLoRA で訓練したモデルであっても LoRA モデルと呼びます)。その中で、Triton Inference Server や TensorRT-LLM というライブラリを用います。

ここで、Triton の最新ドキュメント(v 2.60.0)TensorRT-LLMの最新ドキュメント(v1.0.0) を見比べると、LLM のサーバーデプロイの仕方に食い違いがあることがわかります。
具体的には、

  • Triton、TensorRT-LLM github(v1.0.0) それぞれのデプロイについてのドキュメントが、TensorRT-LLM 0.20.0 時代の CLI を前提としている

    • CLI についての説明は 0.20.0 document には存在しますが、 1.0.0 document で削除されました
  • TensorRT-LLM 1.0.0 document を見ても、Triton でデプロイする手段が見つからない

    • 具体的には、.engine ファイルの作成方法がわかりませんでした。

    • auto-deploy という項が該当する可能性がありますが、これは prototype と明記されています

という問題点があります。この混乱はどういったことでしょうか。
実は、TensorRT-LLM は 2025 年の 9 月のリリースで大規模な破壊的変更を受けました。時系列をまとめると、以下のようになります。なお、全て 2025 年の出来事です。

  • 8/1 … TensorRT-LLM 0.21.0 release

  • 8/27 … Triton 2.60.0 release

    • ここで Triton は、TensorRT-LLM 0.21.0 の機能を前提とした LLM のサーバーデプロイ方法をドキュメントにまとめました。

    • また、TensorRT-LLM 0.xx を前提とした公式のブログも多く存在します。

  • 9/24 … TensorRT-LLM 1.0.0 release

    • TensorRT-LLM に破壊的な変更が多数訪れました。release note
      • 伴って、既存の情報の多くが過去のものとなりました。

      • Triton 2.60.0 に書かれている情報も古いものになりました。

TensorRT-LLM 0.21.0 のワークフローに基づいたモデルデプロイは過去のものとなりました。古いモデルへの対応はなされていますが、新しいモデルへの対応はなされなくなりました。

(参考)リリースノートの重要な所

TensorRT LLM 1.0 brings 2 major changes: the PyTorch-based architecture is now stable and the default experience

BREAKING CHANGE Promote PyTorch to be the default LLM backend

BREAKING CHANGE Change default backend to PyTorch in trtllm-serve

  • Pytorch バックエンドが主流になったことを意味します。これまでは TensorRT-LLM バックエンドが主流でした。これからは、LLM API を主軸に実装していくことになると思われます。

ここで、本ブログでは旧ワークフローと新ワークフローの両方を紹介します。旧ワークフローでは Triton Inference Server を用い、新ワークフローでは trtllm-serve を用いてサーバーを構築します。

旧ワークフローでは、以下の情報源を参照します。

また、次のコンテナを用います。 nvcr.io/nvidia/tritonserver:25.09-trtllm-python-py3 (released 2025/10/07)

ここで、TensorRT-LLMについて、document と github 上のバージョンが違うことに注意してください。参考までに、執筆時(2025/10/17) において v1.0.0 (released 2025/09/23) が最新版であり、v0.20.0 (released 2025/08/03) は 1 つ古いバージョンです。要するに、 TensorRT-LLM document のみ、執筆時点の最新バージョンから 1 つ古いものを使っています。

新ワークフローでは、以下の情報源を参照します。

また、次のコンテナを用います。nvcr.io/nvidia/tensorrt-llm/release:gpt-oss-dev
いくつかのドキュメントではこれから行うことについて(gpt-oss特化の物ではなく)最新版であれば良いといったことが書いてありましたが、こちらのコンテナを用いた方が安定しました。

5 章で旧ワークフローを述べ、6 章でそのベンチマークを行います。そして、7 章で新ワークフローを述べ、8 章でそのベンチマークを行います。旧ワークフローでは引き続き ELYZA 7B を用い、ファインチューニングした LLM をデプロイする方法に着目します。新ワークフローでは gpt-oss-20B を用い、最新のモデルをデプロイする方法に着目します。

5. 【旧ワークフロー】Triton Inference Server × TensorRT-LLM で QLoRA モデルをデプロイ

5.1 Triton 概要

Triton は、深層学習モデルを高速にデプロイするためのサーバーです。主に、次のようなワークフローによってデプロイができます。

  • 必要な物
    • Triton の docker イメージ
    • 動かすモデル
      • Triton は TensorFlow、PyTorch、Python、ONNX など多種多様なバックエンドをサポートしています。
      • TensorRT も ok です。
    • Triton のための設定ファイル
  • 手順概要
    1. モデルリポジトリを作成(空のディレクトリを用意するだけで良い)
    2. モデル・設定ファイルを指定された構造で配置
    3. docker・サーバーを起動し、リポジトリとしてモデルリポジトリを指定
      • この時点で自動的にデプロイが完了する
    4. serverとやり取りすることで処理を実行
      • HTTP API, gRPC API, C APIが使える

Triton はモデルのアンサンブルに対応しています。この概念について理解するために、次のようなケースを考えます。

  • 画像を入力に、前処理 \to モデル \to 後処理 の流れをサーバーでやって欲しい

  • モデル 1 ~ 3 に入力を入れ、それをアンサンブルした結果を返して欲しい

Tritonはこのような要望にも応えることができます。具体的には、以下のように実現します。

  1. 各種処理を Triton が扱える “モデル” として記述する

  2. Triton が要求する、複数のモデルが関与するワークフローの記述様式にのっとり、処理の流れを記述する

このようにして、最終的に任意の処理をサーバーサイドで実行できます。
LLM の場合に目を向けましょう。LLMの場合は、次のように要望を定式化できます。

  • シーケンスを入力に、前処理 \to モデル \to 後処理を繰り返したい。更に、ある条件を満たすまで、出力をモデルに入れ直してほしい(i.e.文章を生成してほしい)

この処理ももちろん Triton で記述できます。しかし、一般に、これらの処理は複雑です。幸いなことに、Triton 側がスケルトンモデルを用意しています。なので、我々は比較的容易に LLM を テキスト生成モデルとしてデプロイできます。

ここまでで、LLM を Triton に渡す必要があると書きました。この時、LLM を TensorRT-LLM に変換してから渡すのが一般的です。これには、以下のメリットがあります。

  • 高速な推論

  • 簡単なデプロイ

5.2 TensorRT-LLM 概要(旧情報)

TensorRT-LLMは、NVIDIA GPU上で最新の大規模言語モデル(LLM)の推論性能を高速化・最適化するための、NVIDIAの包括的なオープンソースライブラリです。

TensorRT-LLMのワークフローは以下の通りです。
参考
参考2

NeMo -------------
                  |
HuggingFace ------
                  |   convert                             build                    load
Modelopt --------- ----------> TensorRT-LLM Checkpoint --------> TensorRT Engine ------> TensorRT-LLM ModelRunner
                  |
JAX --------------
                  |
DeepSpeed --------
  • モデルの用意 \to TensorRT-LLMが認識できる形式の Checkpoint に変換 \to Engine を作成(plan とも呼ばれます) \to Model Runner で実行

TensorRT との違い

  • 対応モデル
    • TensorRT-LLM は LLM 専用
  • 一般的なワークフロー
    • TensorRT: pytorch -> onnx -> engine
    • TensorRT-LLM : Hugging Face / NeMo -> TRT-LLM Checkpoint -> engine
  • ランタイム
    • TensorRT: .engineの実行
    • TensorRT-LLM: .engine の実行・キャッシュの管理…
  • 前処理・中間処理・後処理の複雑さ
    • TensorRT-LLM は LLM なので、前処理・後処理・中間処理(ex.kv cache)がデフォで複雑
  • 推論実行
    • TensorRT
      • 手軽: (あんまりない?Torch-TensorRT? )
      • 本番: Triton
        • 前処理とかを自前でやるならば、モデルを指定されたディレクトリ構成でまとめるだけなのでとてもお手軽
    • TensorRT-LLM :
      • 手軽: LLM api 参考
      • 本番: Triton / trtllm-serve

これらの知識を元に、次節では実際に訓練したモデルをデプロイしていきます。

5.3 Triton デプロイ

ここからは、LoRA LLM を Triton でデプロイする一連の流れを紹介します。
公式の手順
公式の手順 LoRA
公式の手順 llama
を大いに参考にしています。他の参考資料は適宜示します。
また、TensorRT-LLM バックエンドのモデルを Triton でデプロイする手順には、一般的なモデルを Triton でデプロイする手順とは異なる点があることに注意してください。具体的には、 TensorRT-LLM バックエンドのモデルをデプロイする場合、 Triton コンテナを立ち上げたのち、その中で作業する必要があります。これは、以下のような動機が推測されます。

  • LLM の場合、モデルを分割して実行したい場合がある。

主な流れは以下です。

  1. Checkpoint 変換をする

  2. engine をビルドする

  • ここまでは TensorRT-LLM 単体で実行されます。よって、Tritonとは独立に理解できます。
  1. スケルトンモデルを埋める

  2. LoRA 重みを変換し、所定の場所に置く

  3. サーバーを立ち上げる

  4. サーバーとチャットする

1. Checkpoint 変換をする

詳細
  • 概要: TensorRT-LLM が扱えるチェックポイント形式に変換する。

    • 量子化はここあるいは Checkpoint 変換前に行い、以降行わない。

    • 複数 GPU の際の設定もここから始まる(tp size など)。

  • 情報

    • その環境で使えるオプション一覧: python3 /app/examples/llama/convert_checkpoint.py --help

    • モデルごとのガイド: github

    • 公式のパフォーマンスチューニングガイド: github

      • 特別なことはあまり書いていない印象ですが、数値例が載っているので参考になります。また、複数 GPU の時の扱いも書かれています。
  • 説明

    • Checkpoint変換には、モデル固有の checkpoint.py を用います。これは例えば、examples/models/core/xxx に存在します(バージョンによって変化します)。

コマンド例

詳細
python3 /app/examples/models/core/llama/convert_checkpoint.py --model_dir /workdir/develop/ELYZA-japanese-Llama-2-7B-instruct/     --output_dir /app/examples/models/core/llama/elyza  --dtype float16     --use_weight_only     --weight_only_precision int4_awq     --int8_kv_cache --calib_size 256  --per_group
  • int4 重みでビルドします。キャリブレーションが発生するため、GPU での計算が生じます。

2. engine をビルドする

詳細
  • 概要: engineを作成する

  • 運用時の設定のうち、モデル側のことを設定する

    • シーケンス長

    • サンプリング方法

    • バッチサイズの上限

    • メモリの取り方

  • 情報

    • その環境で使えるオプション一覧: trtllm-build --help
  • 説明

    • エンジン作成には、trtllm-build コマンドを用います。

コマンド例

詳細
trtllm-build \
  --checkpoint_dir /app/examples/models/core/llama/elyza \
  --output_dir /engines/elyza \
  --gpt_attention_plugin auto \
  --gemm_plugin auto \
  --max_batch_size 4 \
  --max_input_len 2048 \
  --max_seq_len 4096 \
  --max_beam_width 1 \
  --kv_cache_type paged \
  --lora_plugin auto \
  --max_lora_rank 8 \
  --lora_target_modules attn_q attn_k attn_v attn_dense mlp_h_to_4h mlp_4h_to_h mlp_gate

3. スケルトンモデルを埋める

詳細
  • 概要: スケルトンモデルを埋める

  • 運用時の設定のうち、サーバー関連のことを設定する

    • Triton で処理するときの最大バッチ数
    • 起動するモデルインスタンス数
    • バッチ処理のために、最大でどれだけクエリ応答を遅延するか
    • ストリーミング生成をするか
    • LoRA アダプターをサーバー起動時に読み込みするか
  • 情報

  • 説明

    • スケルトンモデルとは、Triton によって用意された、LLM デプロイの為の事前実装です。preprocess や postprocess が、Triton が認識できる形で置かれています。具体的には以下の 5 モデルが用意されています。

      • ensemble … 以下のモデルの処理の流れを記述します。
      • preprocessing … 前処理です。
      • postprocessing … 後処理です。
      • tensorrt_llm … 処理のバックエンドです。
      • tensorrt_llm_blstensorrt_llm に BLS を組み合わせたものです。
        • ensemble と殆ど同じように使えます。参考 ただ、こちらの方が設定できる項目が多いです。一先ずは ensemble を用います。
    • スケルトンモデルを埋めるには、fill_template.py を用います。fill_template.py は一度埋めた設定を上書きしません。そのため、やり直す際にはスケルトンモデルごと再度用意してください。

    • 主要な設定項目を挙げます。

      • decoupled_mode … デカップルモードを使用するかを決めます。stream 生成をしたい場合、true に設定する必要があります。

      • kv_cache_free_gpu_mem_fraction…モデルのロード後にKVキャッシュに使用できるGPUメモリの最大割合(0から1の間の数値)を設定します (デフォルト 0.9)

      • gpu_device_ids… このモデルに使用する GPU ID をカンマ区切りで指定します。値にカンマが含まれる場合(例: 0,1)、バックスラッシュでエスケープする必要がある点に注意が必要です (0\,1)

      • lora_prefetch_dir … モデルに事前読み込みしてほしい LoRA アダプターの重みが存在するディレクトリを指定します。ここで重みとは変換後の重みです(後述)。

コマンド例

詳細

# スケルトンモデルコピーする
mkdir /elyza_model_repo
cp -r /app/all_models/inflight_batcher_llm/* /elyza_model_repo

# 埋める
ENGINE_DIR=/engines/elyza
TOKENIZER_DIR=/workdir/develop/ELYZA-japanese-Llama-2-7B-instruct
MODEL_FOLDER=/elyza_model_repo
TRITON_MAX_BATCH_SIZE=4
INSTANCE_COUNT=1
MAX_QUEUE_DELAY_MS=0
MAX_QUEUE_SIZE=0
FILL_TEMPLATE_SCRIPT=/app/tools/fill_template.py
DECOUPLED_MODE=false
LOGITS_DATATYPE=TYPE_FP16
LORA_PREFETCH_DIR=/adapter-weights

python3 ${FILL_TEMPLATE_SCRIPT} -i ${MODEL_FOLDER}/ensemble/config.pbtxt triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},logits_datatype:${LOGITS_DATATYPE}
python3 ${FILL_TEMPLATE_SCRIPT} -i ${MODEL_FOLDER}/preprocessing/config.pbtxt tokenizer_dir:${TOKENIZER_DIR},triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},preprocessing_instance_count:${INSTANCE_COUNT}
python3 ${FILL_TEMPLATE_SCRIPT} -i ${MODEL_FOLDER}/tensorrt_llm/config.pbtxt triton_backend:tensorrtllm,triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},decoupled_mode:${DECOUPLED_MODE},engine_dir:${ENGINE_DIR},max_queue_delay_microseconds:${MAX_QUEUE_DELAY_MS},batching_strategy:inflight_fused_batching,max_queue_size:${MAX_QUEUE_SIZE},encoder_input_features_data_type:TYPE_FP16,logits_datatype:${LOGITS_DATATYPE},lora_prefetch_dir:${LORA_PREFETCH_DIR}
python3 ${FILL_TEMPLATE_SCRIPT} -i ${MODEL_FOLDER}/postprocessing/config.pbtxt tokenizer_dir:${TOKENIZER_DIR},triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},postprocessing_instance_count:${INSTANCE_COUNT}
python3 ${FILL_TEMPLATE_SCRIPT} -i ${MODEL_FOLDER}/tensorrt_llm_bls/config.pbtxt triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},decoupled_mode:${DECOUPLED_MODE},bls_instance_count:${INSTANCE_COUNT},logits_datatype:${LOGITS_DATATYPE}

4. LoRA 重みを変換し、所定の場所に置く

詳細
  • 概要: LoRA重みを変換し、所定のフォルダ構成を作成する
  • LoRA重みを変換する
    • LoRA重みを保存するときのデータ型程度しか選択肢がない
  • 情報
    • その環境で使えるオプション一覧: hf_lora_convert.py --help
      • オプションは少ないです。
  • 説明
    • エンジン変換には、hf_lora_convert.py を用います。

変換コマンド例

詳細
python3 /app/examples/hf_lora_convert.py -i /workdir/develop/osamu-adapter/checkpoint-111  -o /adapter-weights/0 --storage-type float16
  • ディレクトリ構成について
    • 変換した重みは、所定のディレクトリ構造で配置する必要がある
/モデルディレクトリ
├── バージョン番号(整数)
│   ├── モデル重みファイル
│   └── モデルコンフィグファイル
├── バージョン番号(整数)
│   ├── モデル重みファイル
│   └── モデルコンフィグファイル
├── バージョン番号(整数)
    ├── モデル重みファイル
   └── モデルコンフィグファイル
/adapter-weights
├── 0
│   ├── model.lora_config.npy
│   └── model.lora_weights.npy
├── 1
│   ├── model.lora_config.npy
│   └── model.lora_weights.npy
└── 3
    ├── model.lora_config.npy
    └── model.lora_weights.npy
  • ここで、場所をステップ 3(スケルトンモデルを埋める)で指定する際には、モデルディレクトリ(例でのadapteer-weights)を指定する。

5. サーバーを起動する

詳細
  • 概要: サーバーを起動します
  • 情報
    • その環境で使えるオプション一覧: launch_triton_server.py --help
      • world_size …使用するGPUの個数
  • 説明
    • サーバー起動には launch_triton_server.py を使用します。

コマンド例

python3 /app/scripts/launch_triton_server.py --world_size=1 --model_repo=/elyza_model_repo

次のように記述することで、より詳しい起動ログを見ることができます。

TLLM_LOG_LEVEL=DEBUG python3 /app/scripts/launch_triton_server.py --world_size=1 --model_repo=/elyza_model_repo

6. サーバーとチャットする

詳細

ペイロードの例

loraを使わない場合

    payload = {
        "text_input": formatted_prompt,
        "max_tokens": max_tokens,
        "bad_words": "",  # 使用しない場合は空文字列
        "stop_words": "",  # 使用しない場合は空文字列
    }

loraを使う場合

    payload = {
        "text_input": formatted_prompt,
        "max_tokens": max_tokens,
        "bad_words": "",  # 使用しない場合は空文字列
        "stop_words": "",  # 使用しない場合は空文字列
        "lora_task_id":0,
    }

サーバーとのチャットコードの例は付録に記載しております。

text_input について、本ブログの設定では、チャットテンプレートを適用した状態の文字列を入れる必要があることに注意してください。

5.4 Triton デプロイ ストリーミング形式

本章では、ストリーミング形式で LLM をデプロイする方法をまとめます。ストリーミング形式は最初のトークンが送られてくるまでの時間を大幅に短縮し、ユーザー体験を大きく改善します。今後の内容は本章を読まずとも理解できるため、飛ばしても構いません。

inflight_batcher_llm_client.py とは、 /app/client 下にあるスクリプトで、クライアント側の実装の参考です。このクライアントにはストリーミング生成の場合も含まれています(python3 inflight_batcher_llm_client.py --help で情報を見ることができます)。

今回、ストリーミング形式の場合のまとまった情報を用意することが時間的に厳しく、叶いませんでした。よってここでは inflight_batcher_llm_client.py を元に成功例を述べるに留めます。

参考までに、有望と思われる情報源を記載しておきます。
Triton Client のライブラリ概要
grpc形式のapi(今回使用)
lang chainを用いたより簡潔な実装?(古め)

必要なライブラリ:

pip install tritonclient[http,grpc]

処理の流れは以下です。

  1. tritonclient の grpcclient の求める形式に合うように入力を整形する

  2. ストリーミング生成の為の準備をする。これにはデータが送られるごとに終了判定を行ったり、トークンのデコード作業をすることが含まれる。

ストリーミングコード例は付録に記載します。参考に、ストリーミングコード例を動かした結果を記載します。

詳細
You: あなたは誰ですか?
Bot: はい、質問者の方にお答えします。
私はELYZAによって訓練された生成AIです。
質問や要望があれば、何なりとお申し付けください。

----------------------------------------
最初のトークンまでの時間 (TTFT): 0.0154 sec
生成速度 (全体): 83 tokens / 0.50 sec = 167.38 tokens/sec
TTFT速度向上: 32.13 倍 (非ストリーミング合計時間 / ストリーミング TTFT)
----------------------------------------
You: ジャガイモの上手な育て方を教えて
Bot: ジャガイモを上手に育てるための方法をお教えします。

ジャガイモは酸素が必要なので、苗を植える際は多少の隙間をあけて植えましょう。また、水やりは酸素を含んである程度の水はけができるようなものを使い、根は直接地面に触れないようにします。

また、ジャガイモは室内で育てる場合は、鉢の中に水分を保持するための砂やスポンジを入れることで水やりの回数を減らすことができます。

----------------------------------------
最初のトークンまでの時間 (TTFT): 0.0169 sec
生成速度 (全体): 211 tokens / 1.26 sec = 167.79 tokens/sec
TTFT速度向上: 74.48 倍 (非ストリーミング合計時間 / ストリーミング TTFT)
----------------------------------------
You: あなたは優秀な競技プログラマーです。次の問題を解いてください。# maaany moving points長さ $L$ メートルの数直線上に 大きさの無視できる$N$ 個の点 $点1, \dots , 点N$ があります。時刻 $0$ では、これらの点は全て数直線の左端 ($x = 0$) に置かれています。時刻 $0$ より、これらの点は数直線上でそれぞれ独立に等速往復運動をします。点 $i(i = 1, \dots, N)$ の速度は $i$ メートル毎秒です。最初、点は座標が大きくなる方に動きます。そして、点が数直線の両端($x = 0 または x = L$) に達すると、点は動く向きを反転して運動を続けます。ここで、点 $i, j$ のすれ違いを時刻 $t$ と座標 $x$ とのタプル $(i, j, t, x)$ であって、点 $i, j$ が時刻 $t$ に共に座標 $x$ に存在する事で定義します。時刻 $(0, T]$ において、点のすれ違いが何回発生するかを求めてください。## 制約- $1 \le N \le 10 ^ 9$- $1 \le T \le 10 ^ 9$- $1 \le L \le 3 \times 10 ^ 5$
Bot: 与えられた問題に回答いたします。

まず、与えられた条件を元に、すれ違いの発生回数を計算するプログラムを以下に記述します。

import numpy as np

# すれ違いの回数を求める
def count_collisions(N, T, L):
   count = 0
   for i in range(N):
       for j in range(i, N):
           if (i, j, T, L) in collisions:
               count += 1
   return count

# すれ違いのタプルを列挙
def collisions(N, T, L):
   for i in range(N):
       for j in range(i, N):
           if (i, j, T, L) in []:
               yield (i, j, T, L)
   return

# すれ違いのタプルを列挙
collisions = collisions(N, T, L)

# すれ違いの回数を求める
count = 0
for i in range(N):
   count += len(collisions) - count

print(count)

このプログラムでは、すれ違いのタプルを列挙する `collisions` 関数と、すれ違いの回数を求める `count_collisions` 関数を定義しています。

`collisions` 関数では、すれ違いのタプルを列挙するために、与えられた条件を元にforループを回しています。

`count_collisions` 関数では、すれ違いの回数を求めるために、与えられた条件を元にforループを回しています。

このプログラムを実行
----------------------------------------
最初のトークンまでの時間 (TTFT): 0.0119 sec
生成速度 (全体): 512 tokens / 3.21 sec = 159.31 tokens/sec
TTFT速度向上: 269.56 倍 (非ストリーミング合計時間 / ストリーミング TTFT)
----------------------------------------

このように、最初のトークンまでの時間が \approx 0.0125sec と非常に短いという結果が得られました(入力長がやや長いケースでも短いのは想定外でした)。非ストリーミング生成と比べ、最初のトークンまでの時間が著しく改善されていることがわかります。

ここで 1 つ注意を述べておきます。TTFT がこのように非常に高速なのは、サーバーがアイドル状態である場合に限られることが多いです。参考に、concurrency = 1, 2 でそれぞれベンチマーク結果を記載しておきます(付録のコードを少し変更するとベンチマークが可能です)。

詳細
## concurrency = 1

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃                            Statistic ┃      avg ┃    min ┃      max ┃      p99 ┃      p90 ┃      p75 ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│             Time To First Token (ms) │    14.00 │  12.34 │    17.17 │    16.47 │    14.78 │    14.37 │
│            Time To Second Token (ms) │     6.09 │   4.72 │     7.63 │     7.28 │     6.39 │     6.24 │
│                 Request Latency (ms) │ 1,398.62 │  20.44 │ 1,576.10 │ 1,575.74 │ 1,572.64 │ 1,568.92 │
│             Inter Token Latency (ms) │     5.83 │   4.60 │     6.88 │     6.88 │     6.53 │     6.10 │
│     Output Token Throughput Per User │   173.18 │ 145.32 │   217.57 │   214.24 │   197.23 │   185.37 │
│                    (tokens/sec/user) │          │        │          │          │          │          │
│      Output Sequence Length (tokens) │   240.00 │   1.00 │   338.00 │   330.97 │   298.20 │   287.25 │
│       Input Sequence Length (tokens) │   512.08 │ 512.00 │   513.00 │   513.00 │   512.00 │   512.00 │
│ Output Token Throughput (tokens/sec) │   171.29 │    N/A │      N/A │      N/A │      N/A │      N/A │
│         Request Throughput (per sec) │     0.71 │    N/A │      N/A │      N/A │      N/A │      N/A │
│                Request Count (count) │    38.00 │    N/A │      N/A │      N/A │      N/A │      N/A │
└──────────────────────────────────────┴──────────┴────────┴──────────┴──────────┴──────────┴──────────┘

## concurrency = 2

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃                            Statistic ┃      avg ┃      min ┃      max ┃      p99 ┃      p90 ┃      p75 ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│             Time To First Token (ms) │ 1,364.90 │    20.01 │ 1,589.75 │ 1,587.57 │ 1,579.71 │ 1,575.92 │
│            Time To Second Token (ms) │     5.97 │     5.58 │     6.25 │     6.25 │     6.16 │     6.06 │
│                 Request Latency (ms) │ 2,745.78 │ 1,575.77 │ 3,134.24 │ 3,133.77 │ 3,129.25 │ 3,123.37 │
│             Inter Token Latency (ms) │     5.81 │     4.59 │     6.91 │     6.88 │     6.50 │     6.06 │
│     Output Token Throughput Per User │   173.70 │   144.64 │   217.99 │   215.55 │   197.30 │   186.66 │
│                    (tokens/sec/user) │          │          │          │          │          │          │
│      Output Sequence Length (tokens) │   240.00 │     1.00 │   338.00 │   330.97 │   298.20 │   287.25 │
│       Input Sequence Length (tokens) │   512.08 │   512.00 │   513.00 │   513.00 │   512.00 │   512.00 │
│ Output Token Throughput (tokens/sec) │   172.09 │      N/A │      N/A │      N/A │      N/A │      N/A │
│         Request Throughput (per sec) │     0.72 │      N/A │      N/A │      N/A │      N/A │      N/A │
│                Request Count (count) │    38.00 │      N/A │      N/A │      N/A │      N/A │      N/A │
└──────────────────────────────────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘

両者を比べると、TTFT に大きな差がある一方、TTST や Output Token Throughput には差が無い事が確認できます。

また、デコーディング時の注意について述べます。

ストリーミング生成では、トークンが (もしかしたらある程度の塊で)次々と送られてきます。ここで、特に日本語の場合、1 文字が 2 つ以上のトークンから構成される場合があります。つまり、送られてくるトークンを愚直にデコードするような実装だと、文字化けを起こす可能性が高いです。

コード例では、TextStreamer という外部ライブラリにデコードを委託することでこの問題を解決しています。

最後に、ベンチマーク時の注意について述べます。 ストリーミング生成の例コードでは、いつもと異なるサーバーの指定の仕方をしています(tensorrt_llm を指定している)。そして、これを直接ベンチマークする方法がわかりませんでした。
tensorrt_llm_bls および ensemble は、ストリーミング生成設定でベンチマークをすることができました。しかしその場合、ensemble の方が tensorrt_llm_bls より 2 倍程度高速という結果が出ます。そもそも、ensemble がストリーミングに対応していることが明示的に書かれていない気がします。
実測では、今回デプロイしたものの速度は、ensemble をベンチマーク(genai-perf)で計測したときのデータに近かったです。

5.5 Triton デプロイ トラブルシューティング

Tritonを用いたデプロイの中で、筆者が遭遇したトラブルと、その解決方法を記述します。

詳細

Tutorial に記載されたファイルが存在しない

  • 原因

    • バージョンごとにファイル構成が頻繁に変化する
  • 解決法

    • 頑張って探す

サーバーの起動に失敗し、起動ログも何を言っているのかよくわからない

  • 原因

    • fill_template.py を用いた設定において、ENGINE_DIR を間違えた時、起動ログが何を言っているのかわからず困る
  • 解決法

    • ENGINE_DIR を間違えない

fill_template.pyの変更が反映されない

  • 原因

    • fill_template.py は上書きしない
  • 解決法

    • スケルトンモデルを再度準備する。

LoRA重みが事前読み込みされない

  • 原因
    • 起動ログより確認できる。よくわからず。同様の問題が報告されている。URL 自分の場合は LoRA 関係を全てやり直したら通った。LoRA の中身のデータ型に問題があるかも
  • 解決法
    • なるべくデータ型を合わせるようにやり直す

起動時エラー: map::at()

  • 原因
    • lora_target_modules を設定する場面には 訓練時・trtllm-build 時の 2 箇所があるが、そこで指定するターゲットモジュールの個数が合わないとエラーを吐く
      • 例えば、atten_qkv のような省略形の指定方法があるが、こういったものを使うと合わなくなるということ
  • 解決法
    • 指定を合わせる
    • 訓練: target_modules=["q_proj", "k_proj", "v_proj", "o_proj","gate_proj", "up_proj", "down_proj"],
    • ビルド: --lora_target_modules attn_q attn_k attn_v attn_dense mlp_h_to_4h mlp_4h_to_h mlp_gate

起動時エラー: std::bad_cast()

  • 原因
    • dtype を設定する場面には Checkpoint変換時・trtllm-build・LoRA重み変換時・fill_template などあるが、そこでデータ型があっていないとエラーを吐く
  • 解決法
    • 全部あわせる(fp16)などに

    • Checkpoint: --dtype float16

    • ビルド: --lora_plugin auto \

    • LoRA 重み変換時: --storage-type float16

    • fill_template: LOGITS_DATATYPE=TYPE_FP16

最大バッチサイズがデフォルトの 4 になる?

  • 概要

    • 起動ログより| tensorrtllm | ... | {"cmdline":{..."default-max-batch-size":"4"}} ... | のような表示
    • TRTGptModel maxBatchSize は 16 であり、どちらが優先かわからない
  • 解決策

    • curl http://localhost:8000/v2/models/tensorrt_llm/config
    • curl http://localhost:8000/v2/models/tensorrt_llm/metrics
      • これらによるチェック上は、大丈夫そう
    • ベンチマーク上では、デフォルトも書き換えた方が安定した。
  • デフォルトも変更する解決策

    • launch_triton_server.py にハードコーディングする。210 行目あたりで
    • 変更前
    cmd += [
    '--disable-auto-complete-config',
    f'--backend-config=python,shm-region-prefix-name=prefix{i}_',
    ]
    
    • 変更後
    cmd += [
    '--disable-auto-complete-config',
    f'--backend-config=python,shm-region-prefix-name=prefix{i}_',
    # ↓ この行を直接書き込んだ(ハードコーディングした)
    '--backend-config=tensorrtllm,default-max-batch-size=16',
    ]
    

5.6 まとめ

本章では、Fine-tuning した LLM を Triton Inference Server を用いてデプロイしました。

デプロイの為に、まず TensorRT-LLM を用いて、モデルを変換しました。そののち、Triton の為の設定を行いました。ここでは、LoRA に関する設定項目も触れました。さらに、ユーザーの使用感の向上に大きく寄与するストリーミング生成についても触れました。

次章では、ここで建てたサーバーについてベンチマークを行い、デプロイ時の設定を考察します。

6. 【旧ワークフロー】ベンチマーク

6.1 ベンチマークの基礎知識について

LLM でのベンチマークでは、まず Token Per Second(TPS) が重要視されます。これは 1 秒間当たりのトークン数であり、使用感を表す最も重要な指標です。

加えて、ストリーミング生成ができるようなモデルでは、 Time To First Token(TTFT) も重要視されます。これは、最初のトークンが来るまでの秒数であり、こちらも使用感を表す重要な指標です。

ここで、TPS の計測は、非ストリーミングとストリーミングで次のように分かれることが多いです。

\text{TPS (非ストリーミング)} := \frac{\text {出力したトークン数}} {\text{すべての出力が終わるまでの秒数}}
\text{TPS (ストリーミング)} := \frac{\text {出力したトークン数}} {\text{すべての出力が終わるまでの秒数 - TTFT}}

TPS について、ストリーミングの場合は最初の出力が発生してからに注目していることがわかります。これは、LLM の推論プロセスが、 2 つの性質が異なるフェーズに分解できることを踏まえると理解ができます。

  1. Prefill Phase … 入力されたプロンプトを一度に並列処理するフェーズ。これにかかる時間が TTFT です。また、比較的高速です。
    • より正確には、プロンプトに対する KV cache を生成するフェーズ。
  2. Decode Phase … 1 つずつトークンを順番に生成していくフェーズ。

つまり、TTFT を引くことで、入力長による結果への影響を軽減できるという背景があるのです。
また、ベンチマークに直接関係することではありませんが、LLM には次の性質があります。

  • 入力する文字列が長くなるにつれ、計算が重くなっていく

これらを踏まえると、ベンチマークを行い、その結果を比較するうえで次のことに注意すべきだとわかります。

  1. streaming と 非 streaming を明確に区別する

  2. (入力長)と(出力長)をそれぞれ合わせる

ほか、計測したいバッチサイズに比べデータセットの件数が少なすぎたり、あるいはプロンプトが短すぎたりするとベンチマークが不正確になる可能性が高まるので注意してください。

また、ライブラリによっては、TPS はあくまで非ストリーミング時の定義の物を扱い、 Inter Token Latency(ITL) でDecode phase を計測します。より詳しいガイドについては、こちらのブログこちらのブログ などを参考にしてください。

今後、ベンチマークツールとして genai-perf をよく用います。各 metrics の説明が公式ドキュメントに書かれています。 URL

説明の日本語訳
メトリクス (Metric) 日本語訳 説明 (Description) 集計 (Aggregations)
Time to First Token (TTFT) 初回トークン生成時間 リクエスト送信から最初の応答(最初のトークン)受信までの時間。ベンチマーク中のリクエストごとに1つの値。 Avg, min, max, p99, p90, p75
Time to Second Token (TTST) 二番目トークン生成時間 最初のストリーミング応答受信から、二番目のストリーミング応答受信までの時間。ベンチマーク中のリクエストごとに1つの値。 Avg, min, max, p99, p90, p75
Inter Token Latency (ITL) トークン間レイテンシ 単一のリクエストにおける中間応答間の時間を、後続の応答で生成されたトークン数で割った値。応答ごとに1つの値。 Avg, min, max, p99, p90, p75
Output Token Throughput Per User ユーザーあたり出力トークンスループット 全出力トークン数(最初のトークンを除く)を、各リクエストの生成フェーズの総時間で割った値。 Avg, min, max, p99, p90, p75
Request Latency リクエスト総レイテンシ リクエスト送信から最終応答(全トークン)受信までの時間。ベンチマーク中のリクエストごとに1つの値。 Avg, min, max, p99, p90, p75
Output Sequence Length 出力シーケンス長 リクエストによって生成された出力トークンの総数。ベンチマーク中のリクエストごとに1つの値。 Avg, min, max, p99, p90, p75
Input Sequence Length 入力シーケンス長 リクエストに含まれる入力トークンの総数。ベンチマーク中のリクエストごとに1つの値。 Avg, min, max, p99, p90, p75
Output Token Throughput 出力トークン総スループット ベンチマーク中に生成された全出力トークン数を、ベンチマーク実行の総時間で割った値。 なし – ベンチマークごとに1つの値
Request Throughput リクエスト総スループット ベンチマーク中に完了した最終応答の数を、ベンチマーク実行の総時間で割った値。 なし – ベンチマークごとに1つの値

ドキュメントによると、genai-perf は、上記の二つの TPS を共に計測するそうです。非ストリーミング時の TPS の定義に対応するものが Output Token Throughput, ストリーミング時の TPS の定義に対応するものが Output Token Throughput Per User であるとわかります。

6.2 ベンチマーク概要 使用ツールの紹介

遂に、Fine-tuning した LLM をデプロイすることができました。
これからは、構築したサーバーのベンチマークを行います。使用ツールは genai-perf です。

使用コード例

詳細
genai-perf profile -m ensemble --tokenizer ELYZA/ELYZA-japanese-Llama-2-7B-instruct 
genai-perf profile   -m tensorrt_llm_bls   --backend tensorrtllm   --endpoint-type kserve   -u localhost:8001   --tokenizer /workspace/ELYZA-japanese-Llama-2-7B-instruct   --input-file /workspace/triton-lora/prompts.jsonl    --extra-inputs max_tokens:256   --extra-inputs lora_task_id:0

本ブログでは、次の 4 項目の影響を計測します。

  1. max_batch_size
    • max_batch_size を変えた時の影響を見ます。
  2. max_input_len(max_seq_len)
    • モデルが許容する max_input_len・max_seq_len を変えた時の影響を見ます。
  3. input_len/output_len
    • ベンチマークで用いるデータの長さを変えた時の影響を見ます。
  4. vLLM vs Triton Inference Server
    • vLLM を用いて作成したサーバーと Triton Inference Server を用いて作成したサーバーの速度を比べます。

逆に、モデルの精度と実行時間の関係などは見ません。ベンチマークで主に用いたスクリプトは付録として記載します。

ベンチマーク中に出てくる用語

concurrency … 一度にサーバーに送られるリクエストの数。concurrency 人が同時にサーバーを利用している状況に相当する。

6.3 max_batch_size


max_batch_size を変化させたときの、LLM サーバーのレイテンシの比較。両者とも、concurrency が max_batch_size と一致するまでは凡そ水平で、そこからは線形にレイテンシが上昇している。


max_batch_size を変化させたときの、LLM サーバーのスループットの比較。両者とも、concurrency が max_batch_size と一致するまでは凡そ線形で、そこからはなだらかに低下する。

結果より、次のことが読み取れます。

  • レイテンシについて
    • concurrency <= max_batch_size の間は緩やかに上昇するか、変化しない
    • concurrency > max_batch_size の間はほぼ線形に上昇する
  • スループットについて
    • concurrency <= max_batch_size の間はほぼ線形に上昇する
    • concurrency > max_batch_size となると増加は止まり、寧ろ減少傾向を見せる

また、max_batch_size = 4, 16 を比べた結果、どちらでも max_batch_size = 16 が良い結果を出しています。これより、メモリが許す限り max_batch_size を増やした方が良いと結論付けられます。

6.4 max_input_len(max_seq_len)


最大入力長・最大出力長を変更したときの LLM サーバーのスループットの比較。最大の設定に依存せず、ほとんど一致している。

結果より、次のことが読み取れます。

  • レイテンシ・スループット共に、max_input_len・max_seq_len による大きな変化は、無いように見えます。

これより、速度面での max_input_len・max_seq_len による大きな変化はなさそうです。よって、メモリが許す限り、max_input_len・max_seq_len は寛容にとって良く、そこまで敏感にならなくて良いと結論付けられます。

6.5 input_len / output_len


入力長・出力長を変更したときの LLM サーバーのレイテンシの比較。入力長 + 出力長が増加するにつれ、レイテンシが顕著に増大する。


入力長・出力長を変更したときの LLM サーバーのスループットの比較。入力長 + 出力長が増加するにつれ、スループットが低下する。

結果より、次のことが読み取れます。

  • レイテンシについて

    • In, Out が増加するに従い増加する
      • (レイテンシ) / ((In + Out)**2) は一定ではない(0.37, 0.0055, 0.0097)
  • スループットについて

    • (In + Out) が増加するにつれ値が小さくなっていく
      • これは、Transformer が 入力長が増えるほど計算量が増えることに起因します。この性質により、Token Per Second (TPS) を計測する際などは、Input_length + Output_length に気を付ける必要があります。

これはおおむね感覚通りの結果だと思います。

6.6 vLLM vs Triton Inference Server

4 bit 量子化したモデルでベンチマークを行います。vLLM について、genai-perf を用いたベンチマーク手段がわからなかったため、vLLM 標準と思われるベンチマーク方法をとりました。vLLM のサーバー起動コマンド・ベンチマークコマンドは付録に記載します。vLLM では Output token throughput (tok/s): 813.81 を、Triton では Output Token Throughput (tokens/sec),"1,156.97" の項を比較しました。


vLLM と Triton Inference Server の比較。 Triton Inference Server の方がスループットが高い。

結果より、次のことが読み取れます。

  • スループットについて
    • Triton Inference Server の方が 1.5 倍程度高速

これは、この設定下で今回デプロイした Triton Inference Server が十分高速であり、vllm 等他社オプションに対してスループット面で優位性を持つことを示唆します。

6.7 まとめ

本章では、デプロイしたサーバーに対してベンチマークを行いました。そして、それぞれの metrics に対し、サーバーのレイテンシ・スループットを解析することで、パラメータ調整の感覚を養いました。また、Triton Inference Server が他デプロイオプションに対し十分な競争力を持つことを示しました。

QLoRAによるLLM省メモリFine-tuningと高速デプロイ: TensorRT-LLM・Triton・H100検証 2に続く

Fixstars Tech Blog /proc/cpuinfo

Discussion