🤖

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

に公開

7. 【新ワークフロー】TensorRT-LLM を用いたデプロイ

7.1 TensorRT-LLM 概要(新情報)

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

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

NeMo -------------
                  |
HuggingFace ------
                  |   load                                                 
Modelopt --------- ----------> LLM API or Server (trtllm-serve)
                  |
JAX --------------
                  |
DeepSpeed --------

TensorRT との違い

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

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

7.2 TensorRT-LLM デプロイ

ここからは、LLM を TensorRT-LLM でデプロイする一連の流れを紹介します。
TensorRT-LLM を用いたデプロイでは、先ほども述べた通り、モデルをいきなりサーバーやAPIに渡します。よって、実践的には次のようなワークフローとなります。

  1. モデル取得
  2. モデルの最適化。量子化など。例えばNVIDIA ModelOpt を使用できる
  3. サーバー構築

本ブログでは既に量子化がなされた 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}
  • cuda_graph_config は、バッチサイズのうち、最適化がかかるバッチサイズの最大を 256 に設定しています。

  • max_seq_len は 1ユーザー当たりのシーケンス長の最大です。

  • max_num_tokens はバッチ含めたトークンの最大数です。

  • 今回、これらの設定は、バッチサイズ 256 、入出力がそれぞれ 512, 256 の設定でベンチマークを行うということを念頭に、それらが収まるように設定しました。また、デフォルト設定ではベンチマークサイズに収まらないこと、その状態でベンチマークを行った結果結果が安定せず、合わせた場合と比べ\frac{1}{2} の性能しか出ない等、著しい結果の低下がみられました。よって、使用状況に合わせ、オプションを設定することが大切です。なお、今回のベンチマーク設定においては、max_seq_len は過剰です。

さらなるオプションを見たい場合 trtllm-serve serve --help を調べてください。 めぼしいオプションとして例えば以下があります。

  • max_batch_size

  • max_num_tokens

  • max_seq_len

  • tp_size

  • pp_size

  • ep_size

  • kv_cache_free_gpu_memory_fraction

ここで注意点を述べておきます。異なるコンテナで作業したとき、 backend: TRITON を入れるとエラーが出てしまったため、このオプションを外して作業していました。しかし、このオプションを入れた場合と比べ、TPS が \frac{1}{2} 程度しか出ていませんでした。

また、サーバーの起動を中途半端に停止した場合、次回以降の実行でフリーズする場合がありました。その場合、

rm -rf ~/.cache/flashinfer

と打つことで解決しました。
これにて、サーバーの構築が完了しました。旧ワークフローと比べ、非常に簡素です。

最後に、有用な情報源を紹介します。TRTLLM document の Featurres 欄には、様々なユースケースに応じて、開発者が行うべきこと・その時 extra_llm_api_options に記述するべきことなどが書かれています。例えば LoRA や KV Chache に関することが載っています。

7.3 まとめ

本章では、新たなワークフローを用いて、最新のモデルである gpt-oss-20b を用いたサーバーを構築しました。

新たなワークフローは旧ワークフローと比べ非常に簡素です。また、最適化の余地として、NVIDIA Model Optimizer を紹介しました。

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

8. 【新ワークフロー】ベンチマーク

今回も、genai-perf を用いてベンチマークを行いました。

8.1 サーバーの速度

ベンチマークに用いたスクリプトは付録に記載します。結果が以下です。なお、input = 512, output = 256 の設定です。


trtllm-serve で構築したサーバーに対し、concurrency を変化させたときのスループットの計測結果。上段は concurrency <= 32 に限った図であり、下段は concurrency <= 256 まで含めた図。ユーザーごとのスループットが比較的高い事、全体のスループットが良く伸びていることがわかる。

上の図が concurrency <= 32 の時の結果であり、下の図が concurrency <= 256 まで含めた結果です。結果から、以下のことが読み取れます。

  • (TTFTを分母から引く) TPS の意味で、concurrency = 1 の時 231, 引かない方の TPS の意味で 212 という高スループットが出ている。なお、TPS はおおむね 30 ~ 50 出ていれば快適と言われます。

  • concurrency が増えるにしたがって、一人当たりのスループットは低下していく

  • 全体的にグラフがなめらかである。これは、最適化の範囲に収まっていることを意味する

  • また、Total Throughput の意味で、さらに多くの concurrency にスケーリングできそうな見た目をしている

8.2 vLLM vs. TensorRT-LLM

vLLM でもサーバーを立て、速度を検証します。ここでも、genai-perfを用いたベンチマーク手法がわからなかったため、vllm bench serve を用います(より詳細には、ベンチマーク自体は実行できましたが、出力長を256に固定する方法がわからず、毎回短くなってしまいました)。

結果が以下です。なお、ベンチマークで得られた metrics のうち、Per User Throughputの計算には ITL を、 Total Throughput の計算には Output token throughput を用いました。


vllm で構築したサーバーに対し、concurrency を変化させたときのスループットの計測結果。trtllm-serve の時と同様の形をしている。

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


trtllm-serve と vllm を比較した図。Per User Throughput では TRT-LLM が、 TTFT では vLLM が一貫して上回っている。総合的に、レイテンシでは目立つ差がない。

結果から、次のことが読み取れます。

  • user throughput の観点で、TRT-LLMが一貫して上回っている

  • TTFT の観点で、vLLM が一貫して上回っている

  • 結果として、リクエストに対するレイテンシや、Total Output Throughput には差が出ていない

また、今回、使用メモリについて調査しませんでした。同等の機能を少ないメモリで実現可能なフレームワークが存在するならば、そちらを選択するのが良いです。

8.3 まとめ

本章では、新たなワークフローを用いてデプロイしたサーバーに対してベンチマークを行いました。そして、TensorRT-LLM と vLLM を比較し、両者にメリットがあり、今回の設定では総合的に見て同等であることを見ました。

9. 今後の展望

今後の調査方向として有望なものをいくつか書きます。

  • 精度による速度・メモリの割合

    • 本ブログでは、精度ごとにサーバーの速度測定をしませんでした。これは、精度が下がるごとに速度が速くなるのは当たり前で、取るべきは accuracy とのバランスだが、題材的に accuracy の測定が困難であるからです。
  • chat履歴を覚えるエージェントとしてのデプロイ

    • 本ブログで作ったサーバーはステートレスです。つまり個々のチャット履歴を覚えません。これは、LLM と対話を行いたいというシチュエーションでは不便なものです。なお、私が見た限りでは、Triton・TensorRT-LLM 側はステートレスな応答のみを対応しています。
  • 複数 GPU 環境での運用

    • サーバーとして複数の GPU が使える場合について触れませんでした。より大きなモデル・潤沢な計算資源を活用したい場合、新たな調査が必要になります。
  • unsloth 等、洗練された Fine-tuning ライブラリの使用

    • unsloth というライブラリが、Fine-tuning におけるメモリ効率や速度の観点から評判です。Triton Inference Server は Fine-tuning のライブラリを指定しないため、本ブログの内容と合わせて使用することができます。
  • 他の LLM デプロイサーバーの調査

    • LLM のデプロイはまだ新しい技術であり、方法が定まったものではありません。たとえ NVIDIA という一つの企業であっても、LLM をデプロイするサーバーを動かす方法が現状 3 つあります( trtllm-serve, Triton Inference Server, NVIDIA Dynamo )。開発者はこれらの発展に対応していかなければなりません。
      • 特に、NVIDIA Dynamo は、Triton Inference Server の後継であるという公式の声明が出ています参考。可能性としては、いずれ開発者は Dynamo をインターフェースとしてサーバーを立てるようになるというシナリオもあり得ます。その時の最新情報を注意深く調べて作業を進めることを勧めます。

10. おわりに

LLM を QLoRA で Fine-tuning し、Triton Inference Server や TensorRT-LLM を用いてデプロイする手順を紹介しました。 Fixstars では、通年でインターンシップを募集しています。 高専生、大学生、大学院生の皆さん、Fixstars でのインターンシップで新しい技術に触れませんか? インターンシップの詳細は こちら をご覧ください

11. 参考文献

LoRA
QLoRA
LIMA
Llama2
ZeRO
勾配チェックポインティング

12. 検証環境

デスクトップ PC からサーバーに ssh 接続し、サーバー上で作業しました。
サーバースペック

  • CPU: EPYC 7742 (64C/128T) x2
  • Memory: 2TB
  • GPU: NVIDIA H100 80GB PCIe x4
  • Disk: 960GB SATA RAID1 + 3.8TB RAID1 (3.84TB U.2 SSDx2)
  • OS: Ubuntu22.04

使用 LLM: ELYZA 7B instruct, gpt-oss-20B

Fine-tuning

  • docker container を用いました。ベースイメージとして 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

Triton Inference Server

  • docker container を用いました。ベースイメージとして nvcr.io/nvidia/tritonserver:25.09-trtllm-python-py3 を使用しました。
  • 追加ライブラリ
genai-perf==0.0.16
tritonclient==2.62.0

vLLM

  • 仮想環境を用いました。
vllm==0.11.0

TensorRT-LLM

  • docker container を用いました。ベースイメージとして nvcr.io/nvidia/tensorrt-llm/release:gpt-oss-dev を使用しました。
  • 追加ライブラリ
genai-perf==0.0.16

13. 付録

LoRA Fine-tuning コード
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("訓練が終了しました")

QLoRA chatコード
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 " すべてのベンチマークが完了しました。"
vllm serve と bench(ELYZA)

サーブ

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."
vllm serve と bench(gpt-oss-20B)

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"

Fixstars Tech Blog /proc/cpuinfo

Discussion