- Authors

- Name
- Youngju Kim
- @fjvbn20031
はじめに
LLM(大規模言語モデル)を本番環境にデプロイする際、最大の課題はGPUメモリ管理と推論の効率化です。GPT-4規模のモデルは数百GBのメモリを必要とし、リアルタイム応答のためには毎秒数十トークンの生成速度が求められます。
このガイドでは、LLM推論最適化のすべての重要な要素を解説します。GPUメモリ階層の理解からKVキャッシュ最適化、GPTQ/AWQ量子化、PagedAttention、continuous batching、マルチGPU推論まで、本番エンジニアが必ず知っておくべき内容をステップバイステップで説明します。
1. GPUメモリ階層
HBM(High Bandwidth Memory)
現代のAI GPUの中核はHBMです。HBMは複数のDRAMダイを垂直に積層したメモリで、従来のGDDR6よりはるかに広いメモリバスを提供します。
| GPU | メモリ | HBMタイプ | 帯域幅 | バス幅 |
|---|---|---|---|---|
| A100 80G | 80 GB | HBM2e | 2.0 TB/s | 5120-bit |
| H100 SXM | 80 GB | HBM3 | 3.35 TB/s | 5120-bit |
| H200 SXM | 141 GB | HBM3e | 4.8 TB/s | 5120-bit |
| B200 SXM | 192 GB | HBM3e | 8.0 TB/s | 8192-bit |
| MI300X | 192 GB | HBM3 | 5.3 TB/s | 8192-bit |
L2キャッシュとSRAM
GPUメモリ階層は大きく3段階で構成されています。
- HBM(グローバルメモリ): 数十〜数百GB、帯域幅は数TB/s、レイテンシは数百ns程度
- L2キャッシュ: 数十〜数百MB(H100: 50 MB)、全SMが共有
- L1キャッシュ / SRAM(シェアードメモリ): SM当たり128〜256 KB、帯域幅は数十TB/s、レイテンシは数ns程度
各SM(Streaming Multiprocessor)内のSRAMはレジスタファイルに次ぐ高速メモリです。Flash Attentionなどの最適化アルゴリズムはこのSRAMを積極的に活用し、HBMへのアクセス回数を削減します。
Rooflineモデル:性能限界の分析
Rooflineモデルは、特定の演算がcompute-boundかmemory-boundかを判定する分析ツールです。
演算強度 (AI) = FLOP数 / メモリアクセス量 (bytes)
性能上限 = min(Peak FLOPS, Peak Memory BW × AI)
- AIが低い場合(memory-bound): メモリ帯域幅がボトルネック。LLMのdecodeフェーズが典型例
- AIが高い場合(compute-bound): 演算速度がボトルネック。LLMのprefillフェーズや大バッチ処理
H100の場合:
- Peak FP16 FLOPS: 989 TFLOPS
- Peak HBM帯域幅: 3.35 TB/s
- Ridge point(均衡点): 989 / 3.35 ≈ 295 FLOP/byte
1トークンを生成する際、70BモデルのAI(FP16)は約1〜2 FLOP/byteと極めてmemory-boundです。
2. LLMメモリ計算
パラメータメモリ
LLMのメモリ使用量を正確に計算することは、デプロイ計画の基盤です。
def calc_model_memory_gb(
num_params: int, # パラメータ数(例: 70e9)
dtype_bytes: int = 2, # FP16=2, FP32=4, INT8=1, INT4=0.5
) -> float:
"""モデル重みのメモリを計算"""
return (num_params * dtype_bytes) / (1024 ** 3)
# 主要モデルのメモリ(FP16基準)
models = {
"Llama-3.1-8B": {"params": 8e9, "bytes": 2},
"Llama-3.1-70B": {"params": 70e9, "bytes": 2},
"Llama-3.1-405B": {"params": 405e9, "bytes": 2},
"Mistral-7B": {"params": 7e9, "bytes": 2},
"Qwen2-72B": {"params": 72e9, "bytes": 2},
}
for name, cfg in models.items():
mem_gb = calc_model_memory_gb(cfg["params"], cfg["bytes"])
print(f"{name}: {mem_gb:.1f} GB")
| モデル | パラメータ | FP32 | FP16/BF16 | INT8 | INT4 |
|---|---|---|---|---|---|
| Llama-3.1-8B | 8B | 32 GB | 16 GB | 8 GB | 4 GB |
| Llama-3.1-70B | 70B | 280 GB | 140 GB | 70 GB | 35 GB |
| Llama-3.1-405B | 405B | 1620 GB | 810 GB | 405 GB | 202 GB |
| Mistral-7B | 7B | 28 GB | 14 GB | 7 GB | 3.5 GB |
KVキャッシュメモリ計算
KVキャッシュは推論中で最も動的に変化するメモリ使用量です。シーケンス長とバッチサイズに比例します。
def calc_kv_cache_memory_gb(
num_layers: int,
num_heads: int,
head_dim: int,
seq_len: int,
batch_size: int,
dtype_bytes: int = 2, # FP16
) -> float:
"""
KVキャッシュメモリを計算
各レイヤー: 2 (K, V) × num_heads × head_dim × seq_len × batch_size
"""
kv_per_layer = 2 * num_heads * head_dim * seq_len * batch_size
total_bytes = kv_per_layer * num_layers * dtype_bytes
return total_bytes / (1024 ** 3)
# Llama-3.1-70B の例
# layers=80, heads=64(GQA: kv_heads=8), head_dim=128
kv_mem = calc_kv_cache_memory_gb(
num_layers=80,
num_heads=8, # GQAの場合はkv_headsを使用
head_dim=128,
seq_len=4096,
batch_size=1,
dtype_bytes=2,
)
print(f"KVキャッシュ (seq=4096, bs=1): {kv_mem:.2f} GB")
# 出力: KVキャッシュ (seq=4096, bs=1): 0.50 GB
# バッチサイズ別のKVキャッシュ
for bs in [1, 4, 8, 16, 32]:
mem = calc_kv_cache_memory_gb(80, 8, 128, 4096, bs, 2)
print(f" batch_size={bs:2d}: {mem:.2f} GB")
KVキャッシュメモリ(Llama-3.1-70B、seq_len=4096、FP16)
| バッチサイズ | KVキャッシュ | モデル重み | 合計 |
|---|---|---|---|
| 1 | 0.5 GB | 140 GB | 140.5 GB |
| 4 | 2.0 GB | 140 GB | 142.0 GB |
| 8 | 4.0 GB | 140 GB | 144.0 GB |
| 16 | 8.0 GB | 140 GB | 148.0 GB |
| 32 | 16.0 GB | 140 GB | 156.0 GB |
活性化メモリ
推論時の活性化メモリはバッチサイズ、シーケンス長、隠れ層サイズの積に比例します。学習と異なり、推論ではグラジエントを保存しないため比較的小さくなります。
def calc_activation_memory_gb(
hidden_size: int,
seq_len: int,
batch_size: int,
num_layers: int,
dtype_bytes: int = 2,
) -> float:
"""推論時の活性化メモリを近似計算"""
# 各レイヤー: attention + FFN の活性化
# 近似値: 2 × hidden_size × seq_len × batch_size per layer
bytes_per_layer = 2 * hidden_size * seq_len * batch_size * dtype_bytes
return (bytes_per_layer * num_layers) / (1024 ** 3)
3. KVキャッシュ最適化: PagedAttention
従来のKVキャッシュの問題点
従来のLLMサービングシステムでは、KVキャッシュを連続したメモリブロックとして割り当てます。これは深刻な問題を引き起こします。
- 内部フラグメンテーション: 最大シーケンス長に合わせて事前割り当てすると、実際に使用されない空間が無駄になります
- 外部フラグメンテーション: リクエスト終了ごとに異なるサイズの空き領域が生まれ、新しいリクエストの割り当てが困難になります
- メモリ効率: 実際のシステムではKVキャッシュの60〜80%が無駄になっています
PagedAttention: OSページングの原理を応用
vLLMのPagedAttentionは、オペレーティングシステムの仮想メモリページング概念をKVキャッシュ管理に適用します。
OS仮想メモリ → PagedAttention
────────────────────────────────────
仮想ページ → 論理ブロック(logical block)
物理フレーム → 物理ブロック(physical block)
ページテーブル → ブロックテーブル(block table)
ページフォルト → ブロック割り当て
核心的なアイデア:
- KVキャッシュを固定サイズのブロック(例: 16トークン)に分割
- シーケンスのKVは論理ブロックでアクセスし、物理ブロックは必要時に割り当て
- 異なるシーケンスが共通プロンプトを共有する場合、Copy-on-Writeで物理ブロックを共有
リクエストA: [Block 0] → [Block 1] → [Block 2]
↕ 物理ブロック共有(共通プロンプト)
リクエストB: [Block 0] → [Block 1] → [Block 3]
vLLMサーバー起動例
# vLLMのインストール
pip install vllm
# サーバー起動(シングルGPU)
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-8B-Instruct \
--dtype bfloat16 \
--max-model-len 8192 \
--gpu-memory-utilization 0.90
# OpenAI互換APIの呼び出し
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Llama-3.1-8B-Instruct",
"messages": [{"role": "user", "content": "GPUメモリ最適化について説明して"}],
"max_tokens": 512,
"temperature": 0.7
}'
# PythonクライアントからvLLM APIを呼び出す
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="dummy",
)
response = client.chat.completions.create(
model="meta-llama/Llama-3.1-8B-Instruct",
messages=[
{"role": "system", "content": "あなたはGPU最適化の専門家です。"},
{"role": "user", "content": "PagedAttentionの動作原理を説明してください。"},
],
max_tokens=1024,
stream=True,
)
for chunk in response:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
4. 量子化: GPTQ、AWQ、GGUF、bitsandbytes
量子化手法の比較
| 手法 | 精度 | メモリ削減 | 速度 | 品質劣化 | 特徴 |
|---|---|---|---|---|---|
| FP16/BF16 | 16-bit | 基準 | 基準 | なし | デフォルト |
| GPTQ | 4-bit | 約75% | 速い | 低い | PTQ、GPU専用 |
| AWQ | 4-bit | 約75% | 速い | 非常に低い | 活性化認識型 |
| GGUF | 2〜8-bit | 可変 | CPU可 | 可変 | llama.cpp |
| bitsandbytes NF4 | 4-bit | 約75% | 中程度 | 低い | QLoRA学習 |
| bitsandbytes INT8 | 8-bit | 約50% | 中程度 | 非常に低い | LLM.int8() |
bitsandbytes 4ビット量子化ロード
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
# 4ビットNF4量子化の設定
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.bfloat16, # 計算はBF16で実行
bnb_4bit_quant_type="nf4", # NormalFloat4量子化
bnb_4bit_use_double_quant=True, # ダブル量子化で追加圧縮
)
model_id = "meta-llama/Llama-3.1-70B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto", # 自動マルチGPU分散
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
# メモリ使用量の確認
print(f"GPUメモリ: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
GPTQ量子化(auto-gptq)
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
from transformers import AutoTokenizer
import torch
model_id = "meta-llama/Llama-3.1-8B-Instruct"
quantize_config = BaseQuantizeConfig(
bits=4, # 4ビット量子化
group_size=128, # グループサイズ(小さいほど精度高いがメモリ増加)
desc_act=False, # 活性化順序の記述
damp_percent=0.01, # Hessianダンピング係数
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
# キャリブレーションデータの準備(代表的なテキストサンプル)
calibration_data = [
tokenizer("The GPU accelerates machine learning by...", return_tensors="pt").input_ids,
tokenizer("Quantization reduces model size while...", return_tensors="pt").input_ids,
# 実際には1024サンプル以上を推奨
]
# モデルのロードと量子化
model = AutoGPTQForCausalLM.from_pretrained(
model_id,
quantize_config=quantize_config,
torch_dtype=torch.float16,
)
model.quantize(calibration_data)
model.save_quantized("llama-3.1-8b-gptq-4bit")
print("GPTQ量子化完了!")
# 量子化モデルのロード
quantized_model = AutoGPTQForCausalLM.from_quantized(
"llama-3.1-8b-gptq-4bit",
use_safetensors=True,
device="cuda:0",
)
AWQ: 活性化認識型重み量子化
AWQはすべての重みを同等に量子化しません。活性化値が大きいチャンネル(重要な重み)はより高い精度で保護します。
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
model_id = "meta-llama/Llama-3.1-8B-Instruct"
quant_path = "llama-3.1-8b-awq"
# AWQ量子化の設定
quant_config = {
"zero_point": True, # ゼロポイント量子化
"q_group_size": 128, # グループサイズ
"w_bit": 4, # 4ビット
"version": "GEMM", # 行列乗算カーネル
}
model = AutoAWQForCausalLM.from_pretrained(
model_id,
low_cpu_mem_usage=True,
use_cache=False,
)
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
model.quantize(tokenizer, quant_config=quant_config)
model.save_quantized(quant_path)
tokenizer.save_pretrained(quant_path)
print("AWQ量子化完了!")
量子化別パフォーマンスベンチマーク(Llama-3.1-8B)
| 手法 | メモリ | スループット(tok/s) | Perplexity | 備考 |
|---|---|---|---|---|
| FP16 | 16 GB | 100(基準) | 7.2 | 基準値 |
| BF16 | 16 GB | 100 | 7.2 | FP16と同等 |
| INT8 | 8 GB | 75 | 7.3 | わずかな品質劣化 |
| GPTQ-4bit | 4.5 GB | 120 | 7.6 | メモリ節約と速度向上 |
| AWQ-4bit | 4.5 GB | 125 | 7.4 | GPTQより高品質 |
| GGUF-Q4_K_M | 4.8 GB | 80(CPU) | 7.5 | CPU推論可能 |
5. バッチング戦略: Continuous Batching
Static Batchingの限界
従来のstatic batchingは、バッチ内の全リクエストが同時に開始し、すべてが完了するまで待機します。これはGPU利用率の深刻な低下を招きます。
Static Batching(batch_size=3):
時間 →
[リクエストA: ████████████░░░░░░░░] (12トークン生成)
[リクエストB: ████░░░░░░░░░░░░░░░░] (4トークン生成)
[リクエストC: ████████░░░░░░░░░░░░] (8トークン生成)
└─ B、CはAの完了を待つ必要あり(GPU浪費)
Continuous Batching(イテレーションレベルスケジューリング)
vLLM、TensorRT-LLMなどの現代のLLMサービングシステムはcontinuous batchingを使用し、各推論ステップ(イテレーション)ごとにバッチを動的に再構成します。
Continuous Batching:
Step 1: [A1][B1][C1] ← 3件を同時処理
Step 2: [A2][B2][C2]
Step 3: [A3][B3][C3] ← B完了、新規リクエストDを追加
Step 4: [A4][C4][D1] ← 空きスロットを即座に埋める
Step 5: [A5][C5][D2]
Step 6: [A6][C6][D3] ← C完了、新規リクエストEを追加
...
GPU利用率はstatic batchingと比較して2〜5倍向上します。
PrefillとDecodeの分離
LLM推論は2つのフェーズに分かれます。
- Prefill: プロンプト全体を一度に処理。compute-bound(バッチ処理に類似)
- Decode: トークンを1つずつ自己回帰的に生成。memory-bound
この2つのフェーズは異なるGPU特性を必要とします。Disaggregated Prefillは、prefill専用GPUとdecode専用GPUを分離するアーキテクチャです。
6. LLM推論フレームワーク比較
| フレームワーク | 開発元 | 特徴 | 最適な用途 |
|---|---|---|---|
| vLLM | UC Berkeley | PagedAttention、OpenAI互換API | 高スループットサービング |
| TensorRT-LLM | NVIDIA | 最適化CUDAカーネル、FP8対応 | 最低レイテンシ |
| Ollama | Ollama Inc | 簡単なローカル実行 | 開発/テスト |
| llama.cpp | ggml | CPU推論、GGUF形式 | エッジ/ローカル |
| SGLang | LM-Sys | 構造化生成、RadixAttention | 複雑なパイプライン |
vLLMテンソル並列推論
from vllm import LLM, SamplingParams
# テンソル並列で4つのGPUに分散
llm = LLM(
model="meta-llama/Llama-3.1-70B-Instruct",
tensor_parallel_size=4, # 4 GPUにテンソル並列分散
dtype="bfloat16",
max_model_len=8192,
gpu_memory_utilization=0.90,
enforce_eager=False, # CUDAグラフ最適化を使用
)
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.95,
max_tokens=512,
stop=["</s>", "[INST]"],
)
prompts = [
"GPUメモリ階層を説明して",
"PagedAttentionの利点は?",
"量子化手法を比較して",
]
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
prompt = output.prompt
generated = output.outputs[0].text
print(f"プロンプト: {prompt[:50]}...")
print(f"生成: {generated[:100]}...")
print()
7. マルチGPU推論: テンソル並列とパイプライン並列
テンソル並列(Tensor Parallelism)
テンソル並列は個々の行列演算を複数のGPUに分散します。各Transformerレイヤーを水平方向に分割します。
Attentionヘッドの分散(4-wayテンソル並列):
GPU 0: Head 0〜15
GPU 1: Head 16〜31
GPU 2: Head 32〜47
GPU 3: Head 48〜63
各GPUが独立して計算し、AllReduceで結果を集約
- 長所: レイテンシ削減、単一GPUに収まらない大規模レイヤーを処理可能
- 短所: レイヤーごとにAllReduce通信が必要 → 高帯域幅NVLink接続が必須
- 適合: ノード内NVLink接続GPU、レイテンシ重視のアプリケーション
パイプライン並列(Pipeline Parallelism)
パイプライン並列はレイヤーをグループに分けて各GPUに割り当てます。
Llama-3.1-70B(80レイヤー)→ 4-wayパイプライン:
GPU 0: Layer 0〜19
GPU 1: Layer 20〜39
GPU 2: Layer 40〜59
GPU 3: Layer 60〜79
レイヤー順に処理し、GPU間でactivationを転送
- 長所: ノード間の低速接続でも効率的、通信量が少ない
- 短所: パイプラインバブル(前段GPUが計算中に後段GPUが待機)、レイテンシ増加
- 適合: マルチノード分散推論、超大規模モデル
メモリプロファイリング
import torch
def profile_gpu_memory(func, *args, **kwargs):
"""GPUメモリ使用量をプロファイリング"""
torch.cuda.reset_peak_memory_stats()
torch.cuda.synchronize()
before = torch.cuda.memory_allocated()
result = func(*args, **kwargs)
torch.cuda.synchronize()
after = torch.cuda.memory_allocated()
peak = torch.cuda.max_memory_allocated()
print(f"メモリ増加: {(after - before) / 1e9:.3f} GB")
print(f"ピークメモリ: {peak / 1e9:.3f} GB")
print()
print(torch.cuda.memory_summary())
return result
# メモリ統計出力の例
def load_and_infer():
from transformers import pipeline
pipe = pipeline(
"text-generation",
model="microsoft/phi-2",
torch_dtype=torch.float16,
device_map="auto",
)
return pipe("GPU memory management is", max_new_tokens=50)
profile_gpu_memory(load_and_infer)
8. 実践的な最適化チェックリスト
GPUメモリ最適化戦略
- 量子化の適用: INT4/INT8量子化でメモリを50〜75%削減
- KVキャッシュの最適化: max_model_lenの制限、GQAモデルの選択
- Flash Attention 2: SRAMを活用してメモリをO(n²)からO(n)に削減
- モデルシャーディング: テンソル並列またはパイプライン並列でマルチGPUを活用
- Continuous batching: GPU利用率の最大化
推論速度の最適化
# 最適化されたvLLMサーバー設定例
vllm_config = {
"model": "meta-llama/Llama-3.1-8B-Instruct",
"dtype": "bfloat16",
"tensor_parallel_size": 1,
"gpu_memory_utilization": 0.90, # GPUメモリの90%を使用
"max_model_len": 8192,
"max_num_batched_tokens": 8192, # バッチあたりの最大トークン数
"max_num_seqs": 256, # 同時処理シーケンス数
"enable_chunked_prefill": True, # Chunked prefillを有効化
"block_size": 16, # KVキャッシュブロックサイズ(PagedAttention)
"swap_space": 4, # CPUスワップ領域(GB)
"enforce_eager": False, # CUDAグラフを使用
"disable_log_stats": False,
}
クイズ: 理解度チェック
Q1. LLM推論におけるprefillフェーズとdecodeフェーズのcompute特性が異なる理由は?
答え: Prefillはcompute-bound、Decodeはmemory-bound
解説: Prefillフェーズでは、プロンプト内のすべてのトークンを並列処理します。バッチ処理に類似し、演算強度が高くGPUの演算ユニットを最大限に活用します(compute-bound)。一方、Decodeフェーズでは、前回生成したすべてのトークンのKVキャッシュを読み込みながら1トークンを生成します。毎ステップ、モデル重み全体とKVキャッシュをメモリから読み込む必要があるため、演算強度が極めて低くmemory-boundになります。H100のridge pointが約295 FLOP/byteなのに対し、decodeフェーズのAIはわずか1〜2 FLOP/byteです。
Q2. PagedAttentionが従来のKVキャッシュ管理よりメモリ効率が高い理由は?
答え: 非連続物理ブロック割り当てと動的割り当てによるフラグメンテーションの排除
解説: 従来の方式では、各リクエストに最大シーケンス長分の連続メモリを事前予約します。実際には短く終わるリクエストも長いメモリを占有する内部フラグメンテーション、そしてさまざまなサイズのリクエストが終了するたびに生じる外部フラグメンテーションが深刻です。実際のシステムでKVキャッシュの60〜80%が無駄になっています。PagedAttentionはOSのページングと同様にKVキャッシュを固定サイズのブロックに分割し、必要な時にのみ割り当てます。非連続物理メモリを論理ブロックで抽象化するためフラグメンテーションがほぼなく、共通プロンプトを持つリクエストはCopy-on-WriteでKVブロックを共有できます。
Q3. AWQがGPTQより重要な重みをうまく保護できる方法は?
答え: 活性化値の大きさに基づいてチャンネルごとにスケーリングし、重要な重みを保護
解説: GPTQは2次近似(Hessian)を使って量子化誤差を最小化しますが、すべての重みをほぼ同等に扱います。AWQ(Activation-aware Weight Quantization)は活性化値の分布を分析し、大きな活性化値を持つチャンネル(顕著なチャンネル)がモデル性能に不均衡に寄与するという観察に基づいています。これらの重要なチャンネルの重みにはスケールファクターを乗算して量子化前に値を拡大し、推論時には対応する活性化に逆数を乗算して補償します。重要な重みを保護しながらハードウェアに優しい均一量子化を維持できるため、同じビット幅でGPTQよりperplexityが低くなります。
Q4. Continuous batchingがstatic batchingよりGPU利用率を高める方法は?
答え: イテレーションレベルスケジューリングにより、完了したシーケンスのスロットを即座に再利用
解説: Static batchingはバッチ内の全リクエストが完了するまでGPUを待機させます。最も長いシーケンスが完了するまで、短く終わったリクエストのGPUスロットが無駄になります。Continuous batching(イテレーションレベルスケジューリング)は毎推論ステップでバッチを再構成します。あるシーケンスがEOSトークンを生成するかmax_tokensに達すると、そのスロットに即座に新しい待機リクエストを追加します。結果として、GPUは常に最大バッチサイズで動作し、実験ではstatic batchingと比べてスループットが2〜5倍向上します。vLLMの論文では、Hugging Faceの静的サービングと比較して最大24倍のスループット向上が報告されています。
Q5. Tensor ParallelismとPipeline Parallelismの通信パターンの違いは?
答え: テンソル並列はレイヤーごとにAllReduce、パイプライン並列はレイヤー境界でのP2P転送
解説: Tensor Parallelismは各Transformerレイヤーの重み行列を複数のGPUに分割します。各レイヤーの演算後、すべてのGPUがAllReduce集合通信で部分結果を合算する必要があります。レイヤーが80個あれば80回のAllReduceが必要で、通信レイテンシが累積します。NVLinkのような高帯域幅インターコネクトが必須です。一方、Pipeline Parallelismはレイヤーグループの境界でのみactivationを次のGPUに転送します。通信回数は少ないですが、パイプラインバブル(上流GPUが計算中に下流GPUが待機)が発生します。ノード内NVLink環境にはテンソル並列、ノード間InfiniBand環境にはパイプライン並列が適しています。
おわりに
LLM推論最適化は、ハードウェアの物理的限界をソフトウェアで乗り越える挑戦です。GPUメモリ階層を理解し、KVキャッシュを効率的に管理し、適切な量子化とバッチング戦略を組み合わせることで、同じハードウェアでも格段に優れたパフォーマンスを達成できます。
重要なまとめ:
- メモリ節約: AWQ/GPTQ 4ビット量子化により、70BモデルをA100 80G 1枚で実行可能
- スループット向上: vLLMのPagedAttention + continuous batchingで静的サービング比最大24倍のスループット
- レイテンシ削減: TensorRT-LLMによるCUDAカーネル最適化、FP8活用
- スケールアウト: テンソル/パイプライン並列で単一GPU限界を超えてマルチGPUクラスターを活用