このブログは、株式会社フィックスターズのエンジニアが、あらゆるテーマについて自由に書いているブログです。
こんにちは。フィックスターズでアルバイトとして研究を行っている小島です。
大規模言語モデル(LLM)の巨大化は、法外な Fine-tuning のコストという新たな課題を生み出しました。大規模化した LLM の恩恵を享受したい開発者にとって、その手段は自明ではありません。同時に、学習済みモデルをいかにして高速かつ安定的にデプロイするかは、実用化に向けた共通の課題となっています。
本記事では、それら二つの大きな壁を打ち破るための技術スタックを紹介します。まず、Fine-tuning 時のメモリ問題を解析します。次に、 QLoRA による効率的な Fine-tuning で計算コストの課題を克服します。最後に、安定したモデル配信を行うTriton Inference Server と、推論を高速化する TensorRT-LLM を活用し、本番環境に耐えうる高速な推論サーバーを構築します。
この記事を読むと、次のようなことがわかります。
本章では Fine-tuning 時に VRAM を消費するリソースを整理し、今後の見通しを良くします。
LLM の Fine-tuning は大量の VRAM を必要とします。さらに、その必要量は時に非自明なほど大きくなります。例えば、\(65\) B モデルの fp16 Full Fine-tuning を考えます。モデルのパラメータは \(130\) GB 程度で格納できますが、Fine-tuning で消費する VRAM は \(780\) GB を超えます(出典: QLoRA) 。残りの \(650\) GB はどこに消えたのでしょうか?
VRAM を消費するリソースは次のように分類できます。(出典: ZeRO)
それぞれのメモリ消費量を見積もります。ここでは、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 という割合で消費されていたのです。

まだ活性化メモリと一時的なバッファが残っていますが、こちらは今回の主題ではないので詳細に見積もることをしません。参考までに、シーケンス長を \(L\), バッチサイズを \(B\) として \(L^2 B\) に比例するメモリを消費します(注: flash attention を用いると、\(L\) に対して線形になるようです)。具体的なケースとして、シーケンス長 \(1K\), バッチサイズ \(32\) で \(1.5\) B GPT-2 を訓練する場合、活性化メモリ消費は \(60\) GB に上ります。(出典: ZeRO) しかし、勾配チェックポインティング(出典: 勾配チェックポインティング)を用いることで \(8\) GB に抑えられます。また、バッチサイズを小さくすることでも容易に削減できます。
最後に補足として、LLM の訓練では、一時的に必要 VRAM が増える現象があります(メモリスパイク)。 これの対処については QLoRA の論文を参照してください。
本章では、大規模言語モデルの Fine-tuning に伴う膨大な VRAM 消費という課題に取り組むため、導入としてメモリ使用量見積もりの技術を紹介しました。
LLM の Fine-tuning 時にメモリを消費する主要な要因はモデル重み・勾配・オプティマイザ状態・活性化メモリ・その他一時メモリに分解できます。そして、 \(x\) B モデルを Adam で訓練する際、使用メモリは凡そ \(12x\) GB と見積もれます。例えば、 \(7\) B モデルならば \(84\) GB です。さらに、この見積もりは、オプティマイザが内部で保持する情報を調べることで、他のケースにも応用できます。
この知識を踏まえ、次章では、勾配・オプティマイザに用いるメモリを劇的に削減する LoRA を導入します。
本章では大規模言語モデル(LLM)の Fine-tuning を効率化する代表的な手法である LoRA(Low-Rank Adaptation)(出典: LoRA) を導入します。その後に LoRA による Fine-tuning を行います。
LoRA は Fine-tuning のためのテクニックです。具体的には、以下の手順による訓練を指します。
LoRA のメリットとして以下が挙げられています。
LoRA によるメモリ削減効果を深堀りします。モデル全体のパラメータを再度 \(x\) と置きます。この時、訓練可能パラメータ数 \(t\) は通常 \(x\) に比べとても小さくなります。実際後述するように、今回の実験設定では \(t\) は \(x\) の \(0.1481\)% です。
また、勾配情報・オプティマイザ情報は訓練可能パラメータにのみ保持されます。これらの情報から、LoRA を適用する際には、勾配情報・オプティマイザ情報によるメモリ消費量は殆ど無視でき、メモリ使用量は \(2x\) GB + その他の要因であると見積もれます。

ここから具体的な実装に入ります。まず設定を述べます。使用するLLMは ELYZA 7B instruct モデル です。使用する計算機器は NVIDIA H100 です。検証環境については記事末尾にまとめを用意いたしましたので、そちらも適宜参考にして下さい。また、Fine-tuning の目的は太宰治 chatbot の作成とします。
データセットですが、今回は gemini に作成してもらいました。約 \(1400\) 件です。ここで、データセットの工夫として、質問に多様性を持たせるよう指示しました。
参考に、LLM との chat 例を挙げます。chat コードの例は、 QLoRA(後述)の場合をブログの末尾に載せます。
あなた: あなたは誰ですか?
モデル: 私は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 のための質の良いデータセットの重要性は議論が良くなされます。ざっくり述べると、応答スタイルが一貫していること・入力が多様な事などが重要であり、データセットの規模は問題ではないという説が各所で述べられています。興味があったら是非調べてみてください。(出典: QLoRA、LIMA、Llama2)
まずライブラリは、 transformers, trl, peft を主に用います。主な流れは以下です。
1. モデルを読み込む( AutoModelForCausalLM )
2. LoRAの設定をする
3. データセットを変形する
4. SFTTrainerクラスを設定する
5. Trainする
それぞれ長いので、適宜折りたたみます。
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",
)
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) を元に設定しています。SFTTrainer が自動でやってくれるので行いません。参考までに、訓練可能パラメータ数を表示します(この段階で LoRA を適用させることで可能です)。
trainable params: 9,994,240 || all params: 6,748,409,856 || trainable%: 0.1481
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>'}
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 の違いは以下のようなものがあります。
データセットの形状ごとの特徴は以下です。
| データセットタイプ | 形式 (Format) | 必須キーと構造 | トレーニングの特徴と損失計算 |
|---|---|---|---|
| 言語モデリング (LM) | 標準 (Standard) | {“text”: “…”} | シーケンス全体で損失を計算します。 各サンプルは平文テキストです 。 |
| 言語モデリング (LM) | 会話 (Conversational) | {“messages”: [ {“role”: “user”, “content”: “…”}, {“role”: “assistant”, “content”: “…”} ]} | 構造化されたメッセージリストを使用し、トレーナーが自動的にチャットテンプレートを適用します 。SFTConfigで assistant_only_loss=True を設定すると、アシスタントの応答トークンのみで損失を計算できます 。 |
| プロンプト完了 (PC) | 標準 (Standard) | {“prompt”: “…”, “completion”: “…”} | プロンプトと完了が分離されます 。デフォルトでは、完了トークンのみで損失が計算されます (completion_only_lossのデフォルト動作) 。シーケンス全体で訓練するには、SFTConfigで completion_only_loss=False を設定する必要があります 。 |
| プロンプト完了 (PC) | 会話 (Conversational) | {“prompt”: [ {“role”: “user”, “content”: “…”} ], “completion”: [ {“role”: “assistant”, “content”: “…”} ]} | プロンプトと完了が会話メッセージのリストとして構造化されます 。チャットテンプレートが適用されます 。損失計算はデフォルトで完了部分のみに限定されます 。 |
| 特殊形式 | ビジョン言語モデル (VLM) | 上記いずれかの形式に加え、追加の images 列が必要 |
VLMの訓練を完全にサポートし、画像処理がオンザフライ(実行時)に行われます 。通常、画像トークンを削除しないよう、SFTConfigで max_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>
ちゃんと応答部分のみから学習する設定になっていることが確認できます。
trainer.train()
ブログの末尾にコード全体を記述します。
LLM との chat 例を挙げます。chat コードの例は、 QLoRA (後述)の場合をブログの末尾に載せます。
あなた: あなたは誰ですか?
モデル: 私は、あなたの質問に対する答えを、与えることができる、人間の代わりに、あなたの、質問に対する答えを、受け止めることができる、人工知能の、強化学習の、アプリケーションです。私に、何かを、聞いてください。
あなた: 名前を教えてもらえますか?
モデル: わたくしは、あなたのことを、一目でわかる名前で呼びたいのです。
あなた: 暇つぶしの方法を教えて
モデル: 暇つぶしの方法をいくつか考えてみました。
1.自分の髪の毛を切る。
2.自分の髪の毛を抜く。
3.自分の髪の毛を染める。
4.自分の髪の毛を剃る。
5.自分の髪の毛を結び目を作ってまるごと食べる。
6.自分の髪の毛を絡ませてロープにして縄跳びをしてみる。
7.自分の髪の毛を並べて、短冊にして災いを退ける。
8.自分の髪の毛を短く切って、髪の毛を燃やして災いを祓う。
あなた: 今日の運勢を占って。
モデル: 一日の中でも弱いものです。
あなた: 何か、長いお話をしてください。
モデル: いいですね、お話を。 それでは、どうぞ、自己紹介から。 私は、孫文です。 孫文、と言っても、あなたのような人には、かなわないものですが。 今日は、孫文という人の、 自己紹介をします。 私の、歴史についての、概要の話を、します。 まず、私は、1875年、18歳の時、医学を学び始めました。 18歳の時、私は、私の父親の、毛沢東の教えに、反発して、逃げ出しました。 そして、蘭州に、医学を学びました。 その後、私は、中
あなた: あなたの故郷について聞かせてください。
モデル: 故郷では、私のような者は、人を恐れることもない。 人々の顔は、皆、私のような者を、見下しているからである。
あなた: 新しいパソコンを買おうと思っています。どのような構成がおすすめですか?
モデル: 自分の仕事と、このような新しいパソコンを購入する予算と、予算内でどの程度の性能のパソコンを購入していいのかを考えて、あなたが何をするのに必要なのかを考えて選択してみてください。
あなた: エッセイを書いて
モデル: 私はあまり自分のことを語るのは得意ではないのですが、もし何かを書くとしたら、自分の葛藤を語ることになると思います。 everybody has a story, みんな葛藤を抱えて生きています。
また、使用 VRAM は 訓練全体を通して \(15100\) ~ \(17000\) MB 程度でした( nvidia-smi の Processes 欄で計測)。ここで、推定 VRAM 使用量が \(14000 + おまけ\) MB であったことを思い出してください。あの推定は凡そ実態に沿っていると言えます。またこの設定下において、ベンチマークにかかった時間は以下の通りでした。
real 19m52.129s
user 23m43.559s
sys 0m7.700s
本章では、Fine-tuning のメモリ使用量を減らす代表的な技術である LoRA を紹介しました。
LoRAは、訓練パラメータを Full Fine-tuning と比べ劇的に、\(\frac{1}{1000}\) 程度のオーダーにまで減らします。その結果、訓練パラメータでのみ保持される勾配・オプティマイザメモリが激減します。しかし一方、モデル重みは全く圧縮されず、新たなボトルネックとして存在します。
次章では、モデル重みを削減する技術である QLoRA を紹介し、さらなるメモリ削減を実現します。
本章では、LoRA をベースに、より一層のメモリ削減を実現する QLoRA(Quantized LoRA)(出典: QLoRA) という技術を紹介します。そして、その導入が容易であることを示します。
QLoRAは、LoRAの訓練に量子化を組み合わせるテクニックです。 QLoRA の主な貢献は以下です:

次に、QLoRA の概念について説明します。QLoRA の採用メカニズムは次の \(2\) つです:
本ブログでは前者に目を向けます。前者を具体的に述べると、訓練中凍結されている \(W\) を低精度で近似する(量子化する)手法です。あくまで凍結されたパラメータの量子化ですので、生成されるモデルのアーキテクチャ は LoRA 単体の場合と同じで、重みの内容(i.e.出来上がる物の正解率の意味での精度)のみが異なります。
QLoRA は、次の \(2\) つの手法の導入によって \(W\) を量子化しています。
QLoRA を数式で表すと以下のようになります(出典: QLoRA) 。

これ以上の詳しい説明は、参考文献を参照してください。
QLoRA がモデルパラメータをどれほど削減するか検討します。 QLoRA は一部の層を \(4\) bit 量子化相当にまで量子化します。ですが、量子化の対象に埋め込み層などは含まれず、パラメータすべてを \(4\) bit 相当で見積もるのは不適切です。
ここでは、論文の実データから見積もることにします。 先ほど挙げた図から換算すると、\(1\) パラメータ当たり \(0.7\) byte 程度使っています。よって、
結局、\(7\) B モデルを訓練する際には、論文の結果に基づいて \(6.9\) GB程度必要と見積もれます。
差分のみ示します。主な方針は以下です。
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 は論文の記述に基づいています。LLM との chat 例を挙げます。chat コードの例は、 QLoRA(後述)の場合をブログの末尾に載せます。
あなた: あなたは誰ですか?
モデル: 私は、この家の隣の家に住んでいる、あの、隣のおじさんです。
あなた: 名前を教えてもらえますか?
モデル: いいえ、教えません。
あなた: 暇つぶしの方法を教えて
モデル: 暇つぶしの方法を教えるのは難しいです。まだ自分がその方法を見つけられていないからです。
あなた: 今日の運勢を占って。
モデル: あなたは、今日は周囲の人に対して冷たく接してしまうかもしれません。その人たちも、あなたのことを冷たい人だと思い、その印象が膨れ上がっていく一方でしょう。そのような自分が嫌になり、営業の帰りに立ち寄ったおでん屋で、酔っ払ってしまうこともあるでしょう。それでも、家に帰れば、その日の出来事を嘆きながら、眠ることになるでしょう。
あなた: 何か、長いお話をしてください。
モデル: それは、私の、一番、大切な、心の、迷路です。
あなた: あなたの故郷について聞かせてください。
モデル: 私の故郷は、あなたの故郷と同じような、冷たい、暗い、寂しいところです。
あなた: 新しいパソコンを買おうと思っています。どのような構成がおすすめですか?
モデル: パソコンの構成は、使用目的や予算によって変わります。個人の方は、CPUやメモリ、ストレージの容量など、基本的な性能を重視して選ぶことが多いです。ビジネスの方は、安定した動作やセキュリティ、ワークフローの効率を重要視して選ぶことが多いです。
あなた: エッセイを書いて
モデル: 毎日、書くようにしました。
chat の例を見るに、LoRA の場合と比べ、特に性能の変化は見られないと思います。また、これは論文の結果を支持するものです。
また、使用 VRAM は 訓練全体を通して \(7577\) MB 程度でした( nvidia-smi の Processes 欄で計測)。これも、推定 VRAM 使用量とメモリ使用量がおおよそ近いことがわかります。またこの設定下において、ベンチマークにかかった時間は以下の通りでした。
real 26m51.628s
user 28m40.509s
sys 0m14.316s
ここまでの結果を一度整理し、LoRA と QLoRA を比較します。ここでは以下の項目を比較します。

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

訓練時間においては、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 に目立つ差はありません。かといって両者完璧なわけではなく、指示に従わないケースもあります。
本章では、LoRA に引き続き VRAM 消費量を削減する技術である QLoRA を解説・実践しました。
QLoRA は訓練時のモデル重みを量子化します。それにより、LoRA で新たなボトルネックとなったモデル重みのメモリ使用量が大幅に減少します。その結果、VRAM使用量を実測で \(7\) B モデル時に約 \(7.5\) GB にまで抑えることに成功しました。その上で良い会話精度が出ることを実演することで、一般的な GPU でも十分に Fine-tuning が可能であることを示しました。一方、訓練時間は \(23\) 分から \(28\) 分へ増加しました。QLoRA で訓練したモデルと LoRA で訓練したモデルに、アーキテクチャ上の違いは存在しません。
これらの結果から、LoRA および QLoRA が、限られた計算リソースで LLM のカスタマイズを実現するための非常に効果的かつ実践的な技術であることがわかります。次章では、こうして訓練したモデルを、実運用に耐えうる形でデプロイする技術を見ていきます。
これからは先ほど訓練した LoRA モデルのデプロイに目を当てます(今後、QLoRA で訓練したモデルであっても LoRA モデルと呼びます)。その中で、Triton Inference Server や TensorRT-LLM というライブラリを用います。
ここで、Triton の最新ドキュメント(v 2.60.0) と TensorRT-LLMの最新ドキュメント(v1.0.0) を見比べると、LLM のサーバーデプロイの仕方に食い違いがあることがわかります。
具体的には、
.engine ファイルの作成方法がわかりませんでした。という問題点があります。この混乱はどういったことでしょうか。
実は、TensorRT-LLM は 2025 年の 9 月のリリースで大規模な破壊的変更を受けました。時系列をまとめると、以下のようになります。なお、全て 2025 年の出来事です。
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
ここで、本ブログでは旧ワークフローと新ワークフローの両方を紹介します。旧ワークフローでは Triton Inference Server を用い、新ワークフローでは trtllm-serve を用いてサーバーを構築します。
旧ワークフローでは、以下の情報源を参照します。
README.md がついている場合が多々あります。これは document からはたどり着くことが困難な上詳しいので、役に立ちます。--help コマンド
また、次のコンテナを用います。 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 を用い、最新のモデルをデプロイする方法に着目します。
Triton は、深層学習モデルを高速にデプロイするためのサーバーです。主に、次のようなワークフローによってデプロイができます。
Triton はモデルのアンサンブルに対応しています。この概念について理解するために、次のようなケースを考えます。
Tritonはこのような要望にも応えることができます。具体的には、以下のように実現します。
このようにして、最終的に任意の処理をサーバーサイドで実行できます。
LLM の場合に目を向けましょう。LLMの場合は、次のように要望を定式化できます。
この処理ももちろん Triton で記述できます。しかし、一般に、これらの処理は複雑です。幸いなことに、Triton 側がスケルトンモデルを用意しています。なので、我々は比較的容易に LLM を テキスト生成モデルとしてデプロイできます。
ここまでで、LLM を Triton に渡す必要があると書きました。この時、LLM を 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 --------
これらの知識を元に、次節では実際に訓練したモデルをデプロイしていきます。
ここからは、LoRA LLM を Triton でデプロイする一連の流れを紹介します。
公式の手順
公式の手順 LoRA
公式の手順 llama
を大いに参考にしています。他の参考資料は適宜示します。
また、TensorRT-LLM バックエンドのモデルを Triton でデプロイする手順には、一般的なモデルを Triton でデプロイする手順とは異なる点があることに注意してください。具体的には、 TensorRT-LLM バックエンドのモデルをデプロイする場合、 Triton コンテナを立ち上げたのち、その中で作業する必要があります。これは、以下のような動機が推測されます。
主な流れは以下です。
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 での計算が生じます。trtllm-build --helptrtllm-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
ensemble … 以下のモデルの処理の流れを記述します。preprocessing … 前処理です。postprocessing … 後処理です。tensorrt_llm … 処理のバックエンドです。tensorrt_llm_bls … tensorrt_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}
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
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
ペイロードの例
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 について、本ブログの設定では、チャットテンプレートを適用した状態の文字列を入れる必要があることに注意してください。
本章では、ストリーミング形式で 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]
処理の流れは以下です。
ストリーミングコード例は付録に記載します。参考に、ストリーミングコード例を動かした結果を記載します。
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)で計測したときのデータに近かったです。
Tritonを用いたデプロイの中で、筆者が遭遇したトラブルと、その解決方法を記述します。
fill_template.py を用いた設定において、ENGINE_DIR を間違えた時、起動ログが何を言っているのかわからず困るENGINE_DIR を間違えないfill_template.py は上書きしない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_gatedtype を設定する場面には Checkpoint変換時・trtllm-build・LoRA重み変換時・fill_template などあるが、そこでデータ型があっていないとエラーを吐く--dtype float16--lora_plugin auto \--storage-type float16fill_template: LOGITS_DATATYPE=TYPE_FP16| tensorrtllm | ... | {"cmdline":{..."default-max-batch-size":"4"}} ... | のような表示TRTGptModel maxBatchSize は 16 であり、どちらが優先かわからないcurl http://localhost:8000/v2/models/tensorrt_llm/configcurl 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',
]本章では、Fine-tuning した LLM を Triton Inference Server を用いてデプロイしました。
デプロイの為に、まず TensorRT-LLM を用いて、モデルを変換しました。そののち、Triton の為の設定を行いました。ここでは、LoRA に関する設定項目も触れました。さらに、ユーザーの使用感の向上に大きく寄与するストリーミング生成についても触れました。
次章では、ここで建てたサーバーについてベンチマークを行い、デプロイ時の設定を考察します。
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 つの性質が異なるフェーズに分解できることを踏まえると理解ができます。
つまり、TTFT を引くことで、入力長による結果への影響を軽減できるという背景があるのです。
また、ベンチマークに直接関係することではありませんが、LLM には次の性質があります。
これらを踏まえると、ベンチマークを行い、その結果を比較するうえで次のことに注意すべきだとわかります。
ほか、計測したいバッチサイズに比べデータセットの件数が少なすぎたり、あるいはプロンプトが短すぎたりするとベンチマークが不正確になる可能性が高まるので注意してください。
また、ライブラリによっては、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 であるとわかります。
遂に、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 項目の影響を計測します。
逆に、モデルの精度と実行時間の関係などは見ません。ベンチマークで主に用いたスクリプトは付録として記載します。
concurrency … 一度にサーバーに送られるリクエストの数。concurrency 人が同時にサーバーを利用している状況に相当する。


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


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


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

これは、この設定下で今回デプロイした Triton Inference Server が十分高速であり、vllm 等他社オプションに対してスループット面で優位性を持つことを示唆します。
本章では、デプロイしたサーバーに対してベンチマークを行いました。そして、それぞれの metrics に対し、サーバーのレイテンシ・スループットを解析することで、パラメータ調整の感覚を養いました。また、Triton Inference Server が他デプロイオプションに対し十分な競争力を持つことを示しました。
TensorRT-LLMは、NVIDIA GPU上で最新の大規模言語モデル(LLM)の推論性能を高速化・最適化するための、NVIDIAの包括的なオープンソースライブラリです。
TensorRT-LLMのワークフローは以下の通りです。
参考
NeMo -------------
|
HuggingFace ------
| load
Modelopt --------- ----------> LLM API or Server (trtllm-serve)
|
JAX --------------
|
DeepSpeed --------
これらの知識を元に、次節では実際に訓練したモデルをデプロイしていきます。
ここからは、LLM を TensorRT-LLM でデプロイする一連の流れを紹介します。
TensorRT-LLM を用いたデプロイでは、先ほども述べた通り、モデルをいきなりサーバーやAPIに渡します。よって、実践的には次のようなワークフローとなります。
本ブログでは既に量子化がなされた gpt-oss-20b を用いるので、2のフェーズは行いません。モデルの量子化や最適化は、TensorRT-LLM とは独立に行えます。
参考にする情報源は以下です。
quick start guide trtllm-serveについて gpt-oss-120Bの高速なデプロイ
次のスクリプトを用意しました。gpt-oss-120Bの高速なデプロイ によると、使用しているGPU が H100 や H200 である場合、それぞれ異なった設定があるそうです。
model_path=/gpt-oss
extra_llm_api_file=/tmp/extra-llm-api-config.yml
cat << EOF > ${extra_llm_api_file}
enable_attention_dp: false
cuda_graph_config:
max_batch_size: 256
enable_padding: true
moe_config:
backend: TRITON
EOF
trtllm-serve serve ${model_path} \
--max_batch_size 256 \
--max_seq_len 32768 \
--max_num_tokens 262144 \
--kv_cache_free_gpu_memory_fraction 0.9 \
--trust_remote_code \
--extra_llm_api_options ${extra_llm_api_file}
さらなるオプションを見たい場合 trtllm-serve serve --help を調べてください。 めぼしいオプションとして例えば以下があります。
ここで注意点を述べておきます。異なるコンテナで作業したとき、 backend: TRITON を入れるとエラーが出てしまったため、このオプションを外して作業していました。しかし、このオプションを入れた場合と比べ、TPS が \(\frac{1}{2}\) 程度しか出ていませんでした。
また、サーバーの起動を中途半端に停止した場合、次回以降の実行でフリーズする場合がありました。その場合、
rm -rf ~/.cache/flashinfer
と打つことで解決しました。
これにて、サーバーの構築が完了しました。旧ワークフローと比べ、非常に簡素です。
最後に、有用な情報源を紹介します。TRTLLM document の Featurres 欄には、様々なユースケースに応じて、開発者が行うべきこと・その時 extra_llm_api_options に記述するべきことなどが書かれています。例えば LoRA や KV Chache に関することが載っています。
本章では、新たなワークフローを用いて、最新のモデルである gpt-oss-20b を用いたサーバーを構築しました。
新たなワークフローは旧ワークフローと比べ非常に簡素です。また、最適化の余地として、NVIDIA Model Optimizer を紹介しました。
次章では、ここで建てたサーバーについてベンチマークを行い、デプロイ時の設定を考察します。
今回も、genai-perf を用いてベンチマークを行いました。
ベンチマークに用いたスクリプトは付録に記載します。結果が以下です。なお、input = 512, output = 256 の設定です。

上の図が concurrency <= 32 の時の結果であり、下の図が concurrency <= 256 まで含めた結果です。結果から、以下のことが読み取れます。
vLLM でもサーバーを立て、速度を検証します。ここでも、genai-perfを用いたベンチマーク手法がわからなかったため、vllm bench serve を用います(より詳細には、ベンチマーク自体は実行できましたが、出力長を256に固定する方法がわからず、毎回短くなってしまいました)。
結果が以下です。なお、ベンチマークで得られた metrics のうち、Per User Throughputの計算には ITL を、 Total Throughput の計算には Output token throughput を用いました。

TensorRT-LLMと比較するとこのようになります。

結果から、次のことが読み取れます。
また、今回、使用メモリについて調査しませんでした。同等の機能を少ないメモリで実現可能なフレームワークが存在するならば、そちらを選択するのが良いです。
本章では、新たなワークフローを用いてデプロイしたサーバーに対してベンチマークを行いました。そして、TensorRT-LLM と vLLM を比較し、両者にメリットがあり、今回の設定では総合的に見て同等であることを見ました。
今後の調査方向として有望なものをいくつか書きます。
LLM を QLoRA で Fine-tuning し、Triton Inference Server や TensorRT-LLM を用いてデプロイする手順を紹介しました。 Fixstars では、通年でインターンシップを募集しています。 高専生、大学生、大学院生の皆さん、Fixstars でのインターンシップで新しい技術に触れませんか? インターンシップの詳細は こちら をご覧ください
LoRA
QLoRA
LIMA
Llama2
ZeRO
勾配チェックポインティング
デスクトップ PC からサーバーに ssh 接続し、サーバー上で作業しました。
サーバースペック
使用 LLM: ELYZA 7B instruct, gpt-oss-20B
nvcr.io/nvidia/pytorch:25.09-py3 を用いました。# Hugging Face ecosystem libraries
transformers==4.57.1
datasets==4.4.1
peft==0.17.1
trl==0.25.0
accelerate==1.11.0
# Dependencies often required by the libraries above
bitsandbytes==0.48.2
einops==0.8.1
scipy==1.16.1
nvcr.io/nvidia/tritonserver:25.09-trtllm-python-py3 を使用しました。genai-perf==0.0.16
tritonclient==2.62.0
vllm==0.11.0
nvcr.io/nvidia/tensorrt-llm/release:gpt-oss-dev を使用しました。genai-perf==0.0.16
from transformers import (
AutoModelForCausalLM,
BitsAndBytesConfig
)
from peft import LoraConfig
from peft import prepare_model_for_kbit_training, get_peft_model
from trl import SFTTrainer, SFTConfig
import datasets
import torch
import pynvml
import logging
DEFAULT_SYSTEM_PROMPT = ""
model_name = "/workdir/develop/ELYZA-japanese-Llama-2-7B-instruct"
dataset_name = "/workdir/develop/osamu_dataset.json"
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",
)
ds = datasets.load_dataset("json", data_files=dataset_name)
logging.info("データセットを読み込みました: %s", ds)
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])
logging.info("データセットのフォーマットが完了しました。")
training_args = SFTConfig(
output_dir="/workdir/develop/osamu-adapter",
num_train_epochs=3,
per_device_train_batch_size=1,
gradient_accumulation_steps=32,
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)
print("モデルへの入力が正しいかデバッグします")
tokenizer_for_decoding = trainer.tokenizer
train_dataloader = trainer.get_train_dataloader()
first_batch = next(iter(train_dataloader))
print("\n--- データローダーの最初のバッチ ---")
print(first_batch)
# 4. input_idsとlabelsをデコードして比較する
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}")
logging.info("訓練を開始します")
trainer.train()
logging.info("訓練が終了しました")
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig, pipeline
from peft import PeftModel
import readline
model_name = "/workdir/develop/ELYZA-japanese-Llama-2-7B-instruct"
adapter_path = "/workdir/develop/osamu-adapter/checkpoint-111"
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=quantization_config,
torch_dtype=torch.bfloat16,
device_map="auto",
)
model = PeftModel.from_pretrained(model, adapter_path)
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
print("モデルの準備ができました。チャットを開始します。")
# チャットループ
while True:
instruction = input("あなた: ")
if instruction.lower() in ["exit", "quit"]:
break
# プロンプトの整形
prompt = f"<s>[INST]<<SYS>><</SYS>> {instruction} [/INST]"
outputs = pipe(
prompt,
max_new_tokens=256,
do_sample=True,
temperature=0.7,
top_p=0.9,
pad_token_id=tokenizer.pad_token_id,
)
# レスポンス部分だけを抜き出して表示
response = outputs[0]["generated_text"].split("[/INST]")[-1].strip()
print(f"モデル: {response}")
import requests
import json
import time
from transformers import AutoTokenizer
# --- 設定項目 ---
# Tritonサーバーのホストとポートを指定します。
# Dockerコンテナ内でこのスクリプトを実行する場合、通常は 'localhost' で問題ありません。
TRITON_HOST = "localhost"
TRITON_PORT = "8000" # HTTPサービスがリッスンしているポート
# 使用するモデル名を指定します。
# 提供された情報から、'ensemble' モデルが使用されていると判断しました。
MODEL_NAME = "ensemble"
# genai-perfで使用したトークナイザーのパスを指定します。
TOKENIZER_PATH = "/workdir/develop/ELYZA-japanese-Llama-2-7B-instruct"
# --- 設定はここまで ---
def talk_to_triton_model(prompt: str, max_tokens: int = 256):
"""
Triton Inference Serverにリクエストを送信し、モデルからの応答を取得します。
Args:
prompt (str): モデルに送信するテキスト(プロンプト)。
max_tokens (int): 生成させたい最大トークン数。
Returns:
str: モデルが生成したテキスト。エラーの場合はNone。
"""
# エンドポイントのURLを構築
url = f"http://{TRITON_HOST}:{TRITON_PORT}/v2/models/{MODEL_NAME}/generate"
# Llama 2 Instructionモデルの公式プロンプト形式を適用
formatted_prompt = f"<s>[INST] <<SYS>>\nあなたは誠実で優秀な日本人のアシスタントです。\n<</SYS>>\n\n{prompt} [/INST]"
# Tritonサーバーに送信するデータ(ペイロード)を作成
payload = {
"text_input": formatted_prompt,
"max_tokens": max_tokens,
"bad_words": "", # 使用しない場合は空文字列
"stop_words": "", # 使用しない場合は空文字列
"lora_task_id":0,
}
try:
# HTTP POSTリクエストを送信
response = requests.post(url, json=payload)
response.raise_for_status()
result = response.json()
return result.get("text_output", "エラー: レスポンスに 'text_output' が見つかりません。")
except requests.exceptions.RequestException as e:
print(f"\nエラー: Tritonサーバーへの接続に失敗しました。")
print(f" 詳細: {e}")
return None
except json.JSONDecodeError:
print(f"\nエラー: サーバーからのレスポンスがJSON形式ではありません。")
print(f" 受信したテキスト: {response.text}")
return None
if __name__ == "__main__":
print("--- Triton LLM 対話クライアント ---")
# パフォーマンス測定のためにトークナイザーを読み込む
try:
tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_PATH)
print(f"トークナイザーを読み込みました: {TOKENIZER_PATH}")
except Exception as e:
print(f"トークナイザーの読み込みに失敗しました。性能測定は行われません。")
print(f" 詳細: {e}")
tokenizer = None
print("チャットを開始します。終了するには '終了' または 'exit' と入力してください。")
print("-" * 50)
while True:
# ユーザーからの入力を受け付ける
input_prompt = input(" あなた: ")
# 終了コマンドのチェック
if input_prompt.lower() in ["終了", "exit"]:
print(" チャットを終了します。")
break
# 処理時間の計測を開始
start_time = time.time()
# モデルとの対話を実行
response_text = talk_to_triton_model(prompt=input_prompt, max_tokens=512)
# 処理時間の計測を終了
end_time = time.time()
if response_text:
# モデルの応答からプロンプト部分([/INST]より前)を削除
inst_marker = "[/INST]"
marker_position = response_text.find(inst_marker)
if marker_position != -1:
cleaned_response = response_text[marker_position + len(inst_marker):]
else:
cleaned_response = response_text
print(f" モデル: {cleaned_response.strip()}")
duration = end_time - start_time
if tokenizer and cleaned_response:
# 生成された部分だけのトークン数を計算
num_tokens = len(tokenizer.encode(cleaned_response.strip()))
tps = num_tokens / duration if duration > 0 else 0
print(f" 性能: {num_tokens} tokens / {duration:.2f} 秒 = {tps:.2f} tokens/sec")
else:
# 接続エラーなどが発生した場合、ループを抜ける
break
print() # 改行して見やすくする
import sys
import queue
import time
from functools import partial
import numpy as np
import tritonclient.grpc as grpcclient
from transformers import AutoTokenizer, TextStreamer
from tritonclient.utils import InferenceServerException, np_to_triton_dtype
import torch
# -------------------------------------------------
# 1. 設定値 (ユーザー指定)
# -------------------------------------------------
TOKENIZER_DIR = "/workdir/develop/ELYZA-japanese-Llama-2-7B-instruct"
REQUEST_OUTPUT_LEN = 512
USE_STREAMING = True
CHAT_TEMPLATE_PREFIX = "<s>[INST] <<SYS>><</SYS>>"
CHAT_TEMPLATE_SUFFIX = " [/INST]"
# -------------------------------------------------
# 2. 設定値 (Triton/モデル)
# -------------------------------------------------
TRITON_URL = "localhost:8001"
MODEL_NAME = "tensorrt_llm"
BEAM_WIDTH = 1
TOP_K = 1
TOP_P = 0.0
TEMPERATURE = 1.0
# -------------------------------------------------
class UserData:
"""
ストリーミングコールバック間でデータを保持するためのクラス。
Tritonからのレスポンス(またはエラー)をキューに格納し、
生成された総トークン数をカウントします。
"""
def __init__(self):
self._completed_requests = queue.Queue()
self.token_count = 0
self.first_token_time = None # (追加) 最初のトークンを受信した時刻
def prepare_tensor(name, input_data):
"""
Numpy配列をTritonのInferInputオブジェクトに変換するヘルパー関数。
"""
t = grpcclient.InferInput(name, input_data.shape,
np_to_triton_dtype(input_data.dtype))
t.set_data_from_numpy(input_data)
return t
def callback(user_data, streamer, result, error):
"""
Tritonからストリーミングレスポンスを受信したときに呼び出されるコールバック関数。
"""
if error:
user_data._completed_requests.put(error)
try:
streamer.end()
except:
pass
else:
user_data._completed_requests.put(result)
if USE_STREAMING:
seq_len_info = result.as_numpy('sequence_length')
if seq_len_info is not None and seq_len_info[0][0] == 0:
try:
streamer.end()
except:
pass
return
output_ids = result.as_numpy('output_ids')
if output_ids is not None:
tokens_list = output_ids[0][0]
if tokens_list.size > 0:
if user_data.first_token_time is None:
user_data.first_token_time = time.perf_counter()
user_data.token_count += tokens_list.size
token_tensor = torch.from_numpy(tokens_list.copy()).unsqueeze(0)
streamer.put(token_tensor)
def initialize_tokenizer(tokenizer_dir):
"""
トークナイザーを読み込み、pad_tokenとend_tokenのIDを取得します。
"""
print(f"1. トークナイザーを読み込んでいます... (from: {tokenizer_dir})")
try:
tokenizer = AutoTokenizer.from_pretrained(tokenizer_dir,
legacy=False,
padding_side='left',
trust_remote_code=True)
except Exception as e:
print(f"エラー: トークナイザーディレクトリ '{tokenizer_dir}' の読み込みに失敗しました。")
print(f"詳細: {e}")
sys.exit(1)
if not tokenizer.pad_token:
tokenizer.pad_token = tokenizer.eos_token
pad_id = tokenizer.encode(tokenizer.pad_token, add_special_tokens=False)[0]
end_id = tokenizer.encode(tokenizer.eos_token, add_special_tokens=False)[0]
return tokenizer, pad_id, end_id
def build_constant_tensors(pad_id, end_id):
"""
リクエストごとに変わらないTritonへの入力テンソルをNumpy配列として構築します。
"""
print("2. 固定入力テンソルを構築しています...")
return {
"request_output_len": np.array([[REQUEST_OUTPUT_LEN]], dtype=np.int32),
"streaming": np.array([[USE_STREAMING]], dtype=bool),
"beam_width": np.array([[BEAM_WIDTH]], dtype=np.int32),
"runtime_top_k": np.array([[TOP_K]], dtype=np.int32),
"runtime_top_p": np.array([[TOP_P]], dtype=np.float32),
"temperature": np.array([[TEMPERATURE]], dtype=np.float32),
"end_id": np.array([[end_id]], dtype=np.int32),
"pad_id": np.array([[pad_id]], dtype=np.int32),
}
def run_chat_interface(triton_client, tokenizer, constant_tensors):
"""
ユーザーからの入力を受け付け、Tritonにリクエストを送信し、
ストリーミングで結果を表示するメインの対話ループ。
"""
print("-" * 40)
print("Tritonに接続しました。会話を開始します。")
print("プロンプトを入力してください (終了するには 'exit' または 'quit')")
print("(例: こんにちは)")
print("-" * 40)
try:
while True:
user_text = input("You: ")
if user_text.lower() in ["exit", "quit"]:
break
if not user_text.strip():
continue
# 1. チャットテンプレートを適用
full_prompt = f"{CHAT_TEMPLATE_PREFIX}{user_text}{CHAT_TEMPLATE_SUFFIX}"
# 2. ストリーミング用ヘルパーを初期化
streamer = TextStreamer(tokenizer,
skip_prompt=True,
clean_up_tokenization_spaces=True)
user_data = UserData() # (変更) ここで UserData が初期化されます
stream_callback = partial(callback, user_data, streamer)
# 3. プロンプトをエンコード
input_ids = [tokenizer.encode(full_prompt)]
input_ids_data = np.array(input_ids, dtype=np.int32)
input_lengths_data = np.array([[len(input_ids[0])]], dtype=np.int32)
# 4. Tritonへの入力テンソルを準備
inputs = [
prepare_tensor("input_ids", input_ids_data),
prepare_tensor("input_lengths", input_lengths_data),
prepare_tensor("request_output_len", constant_tensors["request_output_len"]),
prepare_tensor("beam_width", constant_tensors["beam_width"]),
prepare_tensor("temperature", constant_tensors["temperature"]),
prepare_tensor("streaming", constant_tensors["streaming"]),
prepare_tensor("end_id", constant_tensors["end_id"]),
prepare_tensor("pad_id", constant_tensors["pad_id"]),
prepare_tensor("runtime_top_k", constant_tensors["runtime_top_k"]),
prepare_tensor("runtime_top_p", constant_tensors["runtime_top_p"]),
]
print("Bot: ", end="", flush=True)
start_time = time.perf_counter() # (変更なし) リクエスト送信開始時刻
# 5. ストリーミング推論リクエストを非同期で開始
triton_client.start_stream(callback=stream_callback)
triton_client.async_stream_infer(MODEL_NAME, inputs)
triton_client.stop_stream() # リクエスト送信完了
# 6. コールバックからの結果をキュー経由で待機
while True:
try:
result = user_data._completed_requests.get(block=False)
except queue.Empty:
break
if isinstance(result, InferenceServerException):
print(f"\nストリーミングエラー受信: {result}")
break
# 7. 性能統計を表示
end_time = time.perf_counter() # (変更なし) 全体終了時刻
total_time = end_time - start_time
total_tokens = user_data.token_count
print() # 改行
print("-" * 40)
if user_data.first_token_time:
ttft = user_data.first_token_time - start_time
print(f"最初のトークンまでの時間 (TTFT): {ttft:.4f} sec")
else:
print("TTFT: (トークンが生成されませんでした)")
# (変更) 全体速度の表示
if total_time > 0 and total_tokens > 0:
tokens_per_sec = total_tokens / total_time
print(f"生成速度 (全体): {total_tokens} tokens / {total_time:.2f} sec = {tokens_per_sec:.2f} tokens/sec")
else:
print(f"生成完了 (生成トークン: {total_tokens})")
if ttft > 0 and total_time > 0:
speedup = total_time / ttft
print(f"TTFT速度向上: {speedup:.2f} 倍 (非ストリーミング合計時間 / ストリーミング TTFT)")
print("-" * 40)
except KeyboardInterrupt:
print("\n(Ctrl+C) 中断しました。")
except Exception as e:
print(f"\nチャットループ中に予期せぬエラーが発生しました: {e}")
def main():
"""
スクリプトのメインエントリーポイント。
"""
# 1. トークナイザーと関連IDを初期化
tokenizer, pad_id, end_id = initialize_tokenizer(TOKENIZER_DIR)
# 2. 変わることのない入力テンソルを事前に構築
constant_tensors = build_constant_tensors(pad_id, end_id)
triton_client = None
try:
# 3. Tritonサーバーに接続
triton_client = grpcclient.InferenceServerClient(url=TRITON_URL)
# 4. 対話インターフェースを開始
run_chat_interface(triton_client, tokenizer, constant_tensors)
except Exception as e:
print(f"クライアントの初期化または実行中にエラーが発生しました: {e}")
if "Connect Failed" in str(e):
print(f"エラー: Tritonサーバー ({TRITON_URL}) に接続できませんでした。")
print("Tritonサーバーが起動しているか確認してください。")
finally:
# 5. クライアントをクリーンアップ
if triton_client is not None:
print("\nTritonクライアントを閉じています...")
triton_client.close()
print("クライアントを閉じました。")
del triton_client
if __name__ == "__main__":
main()
#!/bin/bash
# --- 共通設定 ---
# ----------------------------------------------------
# モデル名
MODEL_NAME="ensemble"
# トークナイザーのパス
TOKENIZER_PATH="/workdir/develop/ELYZA-japanese-Llama-2-7B-instruct"
# TritonサーバーのURL
TRITON_URL="localhost:8001"
# 各テストの測定時間(ミリ秒単位)。60秒 = 60000
MEASUREMENT_INTERVAL=60000
# 結果を保存するメインディレクトリ
OUTPUT_DIR_BASE="benchmark_results_$(date +%Y%m%d_%H%M%S)"
# --- 実行 ---
# ----------------------------------------------------
echo "ベンチマークを開始します。結果は '$OUTPUT_DIR_BASE' ディレクトリ以下に保存されます。"
# =================================================================================
# シナリオ1: 負荷(同時実行数)とユースケース(入出力長)の組み合わせ
# =================================================================================
echo ""
echo [シナリオ1] 負荷(同時実行数)とユースケース(入出力長)の組み合わせをテストします..."
OUTPUT_DIR_SCENARIO1="${OUTPUT_DIR_BASE}/scenario1_concurrency_io"
mkdir -p "$OUTPUT_DIR_SCENARIO1"
# テストする同時実行クライアント数の配列
CONCURRENCY_LEVELS=(1 4 8 16 32 64)
# テストする入出力トークン長の組み合わせを定義 (入力長,出力長)
declare -a IO_PAIRS
IO_PAIRS[0]="512,256" # Summarization
for concurrency in "${CONCURRENCY_LEVELS[@]}"; do
for pair in "${IO_PAIRS[@]}"; do
# 入力長と出力長を分解
ISL="${pair%,*}"
OSL="${pair#*,}"
echo " [実行中] Concurrency: $concurrency, Input-Tokens: $ISL, Output-Tokens: $OSL"
# ファイル名を設定 (パスを含めない)
FILENAME="c${concurrency}_isl${ISL}_osl${OSL}.json"
genai-perf profile -m "$MODEL_NAME" -u "$TRITON_URL" --tokenizer "$TOKENIZER_PATH" \
--concurrency "$concurrency" \
--synthetic-input-tokens-mean "$ISL" \
--output-tokens-mean "$OSL" \
-p "$MEASUREMENT_INTERVAL" \
--artifact-dir "$OUTPUT_DIR_SCENARIO1" \
--profile-export-file "$FILENAME"
done
done
echo " [シナリオ1] 完了"
echo ""
echo " すべてのベンチマークが完了しました。"
サーブ
root@44b59629601e:/# python -m vllm.entrypoints.openai.api_server \
--model "dahara1/ELYZA-japanese-Llama-2-7B-fast-instruct-GPTQ" \
--quantization gptq \
--host 0.0.0.0 \
--port 8000 \
--trust-remote-code \
--max-num-seqs 16 # <- 最大バッチサイズ = 16
ベンチマーク
vllm bench serve \
--backend openai \
--model "dahara1/ELYZA-japanese-Llama-2-7B-fast-instruct-GPTQ" \
--dataset-name random \
--ignore-eos \
--max-concurrency 16 \ # <- concurrency = 16
--num-prompts 400 \
--random-input-len 512 \ # <- 入力長 = 512
--random-output-len 256 \ # <- 出力長 = 256
--base-url http://localhost:8000 \
--endpoint /v1/completions
#!/bin/bash
# エラーが発生したら停止する場合(必要に応じてコメントアウトを外してください)
# set -e
# 設定値
INPUT_TOKENS=512
CONCURRENCIES=(1 2 4 8 16 32 64 128 256)
echo "Starting benchmark loop..."
for c in "${CONCURRENCIES[@]}"; do
echo "=================================================="
echo "Running: Concurrency = $c, Input Tokens = $INPUT_TOKENS"
echo "=================================================="
genai-perf profile \
--model gpt-oss \
--url localhost:8000 \
--endpoint-type chat \
--num-prompts 2560 \
--concurrency $c \
--tokenizer /gpt-oss \
--tokenizer-trust-remote-code \
--streaming \
--output-tokens-mean 256 \
--output-tokens-stddev 0 \
--synthetic-input-tokens-mean $INPUT_TOKENS \
--synthetic-input-tokens-stddev 0 \
--random-seed 42 \
--artifact-dir "artifacts_input${INPUT_TOKENS}_conc${c}"
echo "Done with concurrency $c"
echo ""
done
echo "All benchmarks completed."
serve
python3 -m vllm.entrypoints.openai.api_server \
--model /gpt-oss \
--served-model-name gpt-oss-20B \
--tensor-parallel-size 1 \
--port 8000
bench
#!/bin/bash
# 設定
MODEL_NAME="gpt-oss-20B"
TOKENIZER_PATH="/gpt-oss"
PORT=8000
INPUT_LEN=512
OUTPUT_LEN=256
LOG_FILE="benchmark_results_v2.log"
echo "Benchmark started at $(date)" > "$LOG_FILE"
for CONCURRENCY in 1 2 4 8 16 32 64 128 256; do
# プロンプト数を動的に計算 (例: 並列1なら10回、並列256なら2560回)
# 最低でも50回は回すように設定
NUM_PROMPTS=$(( CONCURRENCY * 10 ))
if [ "$NUM_PROMPTS" -lt 50 ]; then
NUM_PROMPTS=50
fi
echo "============================================" | tee -a "$LOG_FILE"
echo " Concurrency: $CONCURRENCY (Total Prompts: $NUM_PROMPTS)" | tee -a "$LOG_FILE"
echo "============================================" | tee -a "$LOG_FILE"
vllm bench serve \
--model "$MODEL_NAME" \
--tokenizer "$TOKENIZER_PATH" \
--dataset-name random \
--random-input-len "$INPUT_LEN" \
--random-output-len "$OUTPUT_LEN" \
--port "$PORT" \
--request-rate inf \
--max-concurrency "$CONCURRENCY" \
--num-prompts "$NUM_PROMPTS" \
>> "$LOG_FILE" 2>&1
echo "Finished Concurrency: $CONCURRENCY"
echo "" >> "$LOG_FILE"
# クールダウン
sleep 2
done
echo "All benchmarks completed. Results saved to $LOG_FILE"
keisuke.kimura in Livox Mid-360をROS1/ROS2で動かしてみた
Sorry for the delay in replying. I have done SLAM (FAST_LIO) with Livox MID360, but for various reasons I have not be...
Miya in ウエハースケールエンジン向けSimulated Annealingを複数タイルによる並列化で実装しました
作成されたプロファイラがとても良さそうです :) ぜひ詳細を書いていただきたいです!...
Deivaprakash in Livox Mid-360をROS1/ROS2で動かしてみた
Hey guys myself deiva from India currently i am working in this Livox MID360 and eager to knwo whether you have done the...
岩崎システム設計 岩崎 満 in Alveo U50で10G Ethernetを試してみる
仕事の都合で、検索を行い、御社サイトにたどりつきました。 内容は大変参考になりま...
Prabuddhi Wariyapperuma in Livox Mid-360をROS1/ROS2で動かしてみた
This issue was sorted....