はじめに
こんにちは。村人(52)こと池駄賃です。誕生日を迎え、52歳になりました。
最近、LLMサーバーを作っていました。が、さすが凡人の鏡ですね。やらかしました。
その顛末をお話しします。
それでは、「始まり始まり〜〜〜〜〜〜〜〜!!!(コンコンコン)」
構成
マシン: ThinkStation PGX(いわゆるDGX Spark)
GPU: GB10
Model: Qwen3.5-27B
Engine: vLLM
私は思いました。
「ついに俺もLLMサーバー運用者か…。それにしてもQwen3.5系の思考過程(think)は美しい」
しかしこの後、村人らしい事件が起きます。
1. vLLMサーバー完成
まずは普通にサーバーを立てました。
vllm serve Qwen/Qwen3.5-27B ...(省略)
クライアントからリクエストを送ります。
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="dummy",
)
resp = client.chat.completions.create(
model="qwen",
messages=[{"role":"user","content":"こんにちは"}],
)
print(resp)
動く。完璧です。
そう思っていました
2. ログ
「...え?...4.5 tokens/s.........GB10.........27Bモデルで4.5 tokens/s.........遅すぎる。まさか間違った買い物したのかも.........」
私はちょっと焦りました。
3. LLMサーバーの真実
ここで色々調べていると重要な事実がわかっていました。
LLMサーバーは 1リクエストではGPUが遊ぶ状態 になってしまうらしい。そして、GPUは バッチ処理 で真価を発揮するらしい。
図で説明
1リクエスト
GPU ████░░░░░░░░░░░░ (ほとんど遊んでいる)
複数リクエスト
GPU ████████████████ (フル稼働)
つまり同時リクエストを増やす必要がありそうです。
理由をChatGPTさんに聞きました。GPUとCPUでは役割が全く異なり、
- CPUは少人数のエリートたち
- GPUは同じ作業をするのが得意なたくさんの労働者たち
そう、GPUを使う僕たちは労働者の皆さんに奉仕する立場なんですね。
(いやそこじゃない)
4. そこで村人(52)は考えた
原因は並列が少ないってことだな。
「よし。max_num_seqsを増やそう。」
「バカと無知(橘玲さん著)」でいう「無知であり、バカ」な人間が動き始めます。
5. 事件発生
何も考えず、クライアント側からバッチサイズを増やして投げます。
この時、vllmをサーブするときの起動コマンドはこれ
- サーバー側
vllm serve /home/*****/models/Qwen3.5-27B \
--served-model-name qwen3.5 \
--host 0.0.0.0 --port 8000 \
--gpu-memory-utilization 0.90 \
--limit-mm-per-prompt.image 0 \
--limit-mm-per-prompt.video 0 \
--max-model-len 32768 \
--async-scheduling
正直、ほとんど何み考えてない
- クライアント側
バッチ処理の関数は長いのでこのトグルを開いてね
def infer_text(prompt: str) -> str:
for _ in range(3): # 最大3回のリトライ
try:
response = client.chat.completions.create(
model="qwen3.5",
messages=[{"role": "user", "content": [{"type": "text", "text": prompt}]}],
max_tokens=16384,
temperature=0,
top_p=1,
)
return response.choices[0].message.content.strip()
except Exception as e:
print(f"Error during inference: {e}. Retrying...")
time.sleep(1) # リトライ前に1秒待機
def infer_texts(prompts: List[str]) -> List[str]:
if not prompts:
return []
max_workers = len(prompts)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
return list(executor.map(lambda p: infer_text(p), prompts))
文字列が入ったtopic(list)をバッチ化して並列推論させます。いきなり、長文のインプットを256並列やってみましょう。
batch = 256
topics_batch = [topics[i : i + batch] for i in range(0, len(topics), batch)]
batched_prompt = ["以下のテキストを要約してください。\n" + topic['text'] for topic in topics_batch[0]]
infer_texts(batched_prompt)
実行。
GB10: 激重
SSH: 接続が切れる
top: そもそもSSHで入れん
vLLM: logすら出ない
人生: はちゃめちゃ
頭の中に 敵機来襲 のアラートが鳴り続けます
完全に 「ちょっと待って」 状態になりました。
6. 調査開始
ここからvLLMパラメータを調べ始めました。
すると重要パラメータは意外と少ないことがわかりました。
vLLM重要パラメータ TOP10
-
max_num_seqs : 同時リクエスト数
例: --max-num-seqs 64
意味: 同時処理するリクエスト数
効果: スループット ↑、 VRAM使用量 ↑ -
max_num_batched_tokens : 1stepの総トークン数
例: --max-num-batched-tokens 8192
requestが三つ来て、2000 tokens × 3合計: 6000 tokens -
max_model_len : 最大コンテキスト
例: --max-model-len 8192
大きいと KV cache巨大 -> VRAM消費増 -
gpu_memory_utilization: GPU使用率
例: --gpu-memory-utilization 0.9
VRAMの90%まで使う -
tensor_parallel_size: GPU分散
例: --tensor-parallel-size 1
GB10の場合、基本はGPUが1枚なのでtensor_parallel_size = 1
tensor_parallelとはモデルを 複数GPUに分割する仕組みです。
例えば30Bモデルを4GPUで動かすと7.5B × 4に分割。 -
chunked prefill: 長文のinputを分割
例: --enable-chunked-prefill -
partial prefill: 大きなサイズのprefill途中で別リクエストを受け付け可能
-
scheduler_delay_factor: バッチ待ち時間
-
swap_space: CPU退避
-
block_size: KV cache管理をブロックにして管理するときの単位
7. 実は最重要パラメータ
実はvLLM性能のほとんどは以下で決まるらしい
- max_num_seqs
- max_num_batched_tokens
ここを調整するとスループット3〜8倍は普通に変わるそうです(GPTさんによる)
8. 実践チューニング例(GB10)
GB10環境での例
vllm serve Qwen/Qwen3-30B-A3B \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.9 \
--max-model-len 8192 \
--max-num-seqs 32 \
--max-num-batched-tokens 8192 \
--enable-chunked-prefill
GB10はGPUが1枚なのでtensor_parallel_size = 1になります。
9. 村人の学び
今回の流れ
- リクエストで満足
- tokens/s遅い
- 並列増やす
- GB10激重
- パラメータ調査
という村人(62)らしい凡人丸出しのミスでした。
ミスというのは成長の機会っすね!www
10. 村人(52)は...
うむ
モデルをサーブするだけでも色々あるんだな。知らなかった。
もっと推論について調べないと。特に今はbf16のままサーブしているけど、もっと大きなモデルをサーブするときは量子化も考えないと。
一つ一つがなかなかに奥深い。それがLLM開発なんだな。
おっと、これを書いているとvLLMがバージョンアップしたらしい(タイミング...)
FP8対応とかQwen3.5への正式対応など、色々ありそうなのでしっかりチェックしたいと思います。
11. まとめ
vLLMの引数はちゃんと理解すべし!
- max_num_seqs
- max_num_batched_tokens
この二つの理解は必須ですね。
データセットを作るときはインプットトークンがすごく大きくなります。
なので--enable-chunked-prefillをうまく活用したいと思いました!
12. おわりに
「村人(52)の話を聞いてもどうなの?」とは思いますが、僕と同じく vLLM勇者に憧れる人 に届けばいいなと思ってます。
vLLMサーブするだけでも、色々と考えないといけないことが多いっすね。
ばんがろ!
