LLM Inference Optimization: техники ускорения генерации
opensourceaillminferenceoptimizationperformanceit
Введение: LLM Inference Optimization
Проблема
LLM inference is expensive:
- GPU memory: $10K-$100K per GPU
- Compute: 1000s of GPU hours per inference
- Latency: 1-10 seconds per response
- Throughput: 10-100 tokens/sec per GPU
Solution: Multi-technique Optimization
- Quantization: reduce precision
- Caching: reuse computations
- Parallelism: use multiple GPUs
- Architecture: efficient models
Факт: Proper optimization can reduce inference cost by 10x and latency by 5x.
Quantization
FP16 vs BF16 vs INT8
Precision Levels:
FP32: 32 bits (baseline)
FP16: 16 bits (2x speedup)
BF16: 16 bits (2x speedup, better stability)
INT8: 8 bits (4x speedup, some accuracy loss)
INT4: 4 bits (8x speedup, more accuracy loss)
FP8: 8 bits (new standard for LLMs)
# Quantization example with bitsandbytes
import torch
from transformers import AutoModelForCausalLM
# Load model in 4-bit quantized format
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b",
load_in_4bit=True, # 4-bit quantization
load_in_8bit=False,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_quant_type="nf4", # Normalized FP4
bnb_4bit_use_double_quant=True,
)
# Memory usage:
# FP16: 7B params × 2 bytes = 14 GB
# INT8: 7B params × 1 byte = 7 GB
# INT4: 7B params × 0.5 byte = 3.5 GB
NF4 (Normalized FP4)
# Normalized FP4: better than regular INT4
from bitsandbytes.nn import Linear4bit
# NF4 distributes quantization levels based on normal distribution
# Better for LLM weights (which are normally distributed)
# Comparison:
# INT4: uniform quantization
# NF4: normal-distribution-aware quantization
# Result: NF4 has ~1% better accuracy at same bitwidth
AWQ (Activation-aware Weight Quantization)
# AWQ: quantize weights based on activation importance
from awq import AutoAWQForCausalLM
model = AutoAWQForCausalLM.from_pretrained("meta-llama/Llama-2-7b")
quant_config = {
"zero_point": True,
"q_group_size": 128,
"w_bit": 4,
"version": "GEMM",
}
# Quantize
model.quantize(tokenizer, quant_config=quant_config)
# Save
model.save_quantized("llama-2-7b-awq")
# AWQ principle:
# - Some activations cause large outputs (important)
# - Protect weights that affect important activations
# - Quantize less important weights more aggressively
KV Cache Optimization
What is KV Cache?
KV Cache stores key and value tensors for each token
This avoids recomputing them for each new token
Memory usage:
- Per layer: 2 × batch_size × num_heads × head_dim × seq_len × 2 bytes
- For Llama-2-7B:
- 32 layers × 2 × 1 × 32 heads × 128 dim × 4096 seq × 2 bytes
- ≈ 21 GB for batch=1, seq=4096
Problem: KV cache dominates memory for long sequences
Paged Attention (vLLM)
# vLLM: PagedAttention for efficient KV cache management
import vllm
llm = vllm.LLM(model="meta-llama/Llama-2-7b")
prompts = [
"Hello, my name is",
"The capital of France is",
"The largest planet in our solar system is",
]
outputs = llm.generate(prompts, sampling_params)
# PagedAttention:
# - Treats KV cache as memory pages
# - Allocates contiguous blocks for non-contiguous KV tensors
# - Achieves near-zero memory fragmentation
# - 2-4x better memory efficiency than standard caching
KV Cache Compression
# KV Cache eviction strategies:
# 1. Discard oldest tokens (sliding window)
# 2. Keep most important tokens (attention-based)
# 3. Compress multiple tokens into one (summary)
def compress_kv_cache(kv_cache, compression_ratio=0.5):
"""
Compress KV cache by keeping top-k attention scores
"""
# Calculate attention scores for each token
attention_scores = compute_attention_scores(kv_cache)
# Keep top-k tokens
k = int(len(attention_scores) * compression_ratio)
top_k_indices = torch.topk(attention_scores, k).indices
# Compress remaining tokens
compressed_kv = compress_remaining(kv_cache, top_k_indices)
return compressed_kv
# Trade-off:
# - Higher compression → faster inference, lower quality
# - Typical ratio: 0.3-0.7
Speculative Decoding
Basic Idea
Speculative decoding uses a small model to draft tokens,
then verifies them with the large model.
Small model: fast, generates N draft tokens
Large model: slow, verifies N tokens in parallel
Speedup: ~N× for N draft tokens (theoretical)
# Speculative decoding example
def speculative_decode(draft_model, target_model, prompt, num_drafts=5):
"""
Draft tokens with small model, verify with large model
"""
# Draft tokens
draft_tokens = draft_model.generate(prompt, max_new_tokens=num_drafts)
# Verify with target model (parallel verification)
target_output = target_model.generate(
prompt + draft_tokens,
verify_only=True,
)
# Accept or reject draft tokens
accepted_tokens = []
for draft, target in zip(draft_tokens, target_output):
if draft == target:
accepted_tokens.append(draft)
else:
accepted_tokens.append(target)
break
return accepted_tokens
# Acceptance rate:
# - 80%+ → 2-3x speedup
# - 60%+ → 1.5-2x speedup
# - <40% → no speedup
Medusa
# Medusa: multiple decoding heads for speculative decoding
from medusa import MedusaLM
model = MedusaLM.from_pretrained("meta-llama/Llama-2-7b")
# Add multiple decoding heads
# Each head predicts the next N tokens
# During inference, all heads vote on next token
# Result:
# - 2-3x speedup without fine-tuning
# - Simple to implement
# - Works with any base model
Tensor Parallelism
Splitting Across GPUs
Tensor parallelism splits model weights across GPUs
For Llama-2-7B with 4 GPUs:
- Each GPU holds 1/4 of each layer's weights
- All GPUs compute in parallel
- Results are synchronized after each layer
Memory per GPU:
- FP16: 14 GB / 4 = 3.5 GB
- Plus KV cache: ~5 GB
- Total: ~8.5 GB per GPU (A100-40GB is sufficient)
# Tensor parallelism with vLLM
import vllm
llm = vllm.LLM(
model="meta-llama/Llama-2-7b",
tensor_parallel_size=4, # Use 4 GPUs
)
# Tensor parallelism steps:
# 1. Split weight matrices across GPUs
# 2. Each GPU computes its portion
# 3. All-Reduce to combine results
# 4. Proceed to next layer
Pipeline Parallelism
Pipeline parallelism splits layers across GPUs
For Llama-2-7B with 4 GPUs:
- GPU 0: layers 0-7
- GPU 1: layers 8-15
- GPU 2: layers 16-23
- GPU 3: layers 24-31
Bubble problem:
- GPUs wait for previous stage to finish
- Solution: micro-batching
Throughput:
- Pipeline parallelism: higher throughput
- Tensor parallelism: lower latency
Continuous Batching
What is Continuous Batching?
Traditional batching:
- Wait for all requests to finish
- Process as a batch
- High latency for individual requests
Continuous batching (Iterative Batching):
- Start processing requests immediately
- New requests injected when GPU is free
- Lower latency, higher throughput
vLLM implements continuous batching:
- Throughput: 2-3x higher than HuggingFace
- Latency: 20-50% lower
# vLLM continuous batching
import vllm
llm = vllm.LLM(model="meta-llama/Llama-2-7b")
# Requests are processed as soon as GPU is available
# No need to wait for batch to complete
outputs = llm.generate(
prompts,
sampling_params,
use_tqdm=True,
)
# Continuous batching benefits:
# - Higher GPU utilization
# - Lower tail latency (p99)
# - Better throughput under variable-length requests
Kernel Optimization
Flash Attention
# Flash Attention: memory-efficient attention
import torch
from flash_attn import flash_attn_func
def efficient_attention(q, k, v):
"""
Flash Attention reduces memory complexity
from O(n²) to O(n)
"""
# q, k, v: [batch, heads, seq_len, head_dim]
output = flash_attn_func(q, k, v)
return output
# Memory comparison:
# Standard attention: O(seq_len²) for attention matrix
# Flash Attention: O(seq_len) for intermediate results
# Result: 2-4x faster for long sequences
Triton Kernels
# Triton: custom CUDA kernels in Python
import triton
import triton.language as tl
@triton.jit
def quantize_kernel(weight, output, block_size):
"""
Custom quantization kernel in Triton
"""
pid = tl.program_id(0)
start_idx = pid * block_size
# Load weights
weights = tl.load(weight + start_idx, block_size)
# Quantize
scales = tl.max(weights) / 7.0 # INT4 range
quantized = (weights / scales * 7.0).to(tl.int4)
# Store
tl.store(output + start_idx, quantized)
# Triton benefits:
# - Write kernels in Python
# - Compile to efficient CUDA
# - No C++/CUDA expertise needed
Serving Frameworks
vLLM
# vLLM: high-throughput LLM serving
import vllm
llm = vllm.LLM(
model="meta-llama/Llama-2-7b",
tensor_parallel_size=4,
gpu_memory_utilization=0.9, # Use 90% of GPU memory
max_num_batched_tokens=4096,
)
outputs = llm.generate(prompts, sampling_params)
# vLLM features:
# - PagedAttention
# - Continuous batching
# - Tensor parallelism
# - Speculative decoding
# - 24x higher throughput than HuggingFace
TensorRT-LLM
# TensorRT-LLM: NVIDIA's optimization library
import tensorrt_llm
# Build engine
builder = tensorrt_llm.Builder()
builder.model_name = "llama-2-7b"
builder.num_layers = 32
builder.num_heads = 32
builder.hidden_size = 4096
# Optimize
config = builder.builder_config
config.precision = "float16"
config.tensor_parallel = 4
config.trt_platform = "linux"
# Build and deploy
engine = builder.build_engine()
tensorrt_llm.runtime.infer(engine, inputs)
# TensorRT-LLM features:
# - Layer fusion
# - Quantization (FP8, INT8, INT4)
# - Custom kernels
# - Up to 3x faster than vLLM on NVIDIA GPUs
TGI (Text Generation Inference)
# TGI: HuggingFace's serving framework
from text_generation import Client
client = Client("http://localhost:8080")
response = client.generate(
prompt="Hello, my name is",
max_new_tokens=100,
temperature=0.7,
)
# TGI features:
# - Continuous batching
# - Tensor parallelism
# - Quantization (AWQ, GPT-Q)
# - Docker deployment
# - Easy to use
Заключение
LLM inference optimization requires multiple techniques:
Quantization:
- FP16 → INT8 → INT4
- AWQ for better quality
- Target: 4-8x memory reduction
KV Cache:
- PagedAttention (vLLM)
- Compression for long sequences
- Target: 2-4x memory reduction
Parallelism:
- Tensor parallelism: lower latency
- Pipeline parallelism: higher throughput
- Target: scale to larger models
Serving:
- vLLM: best general-purpose
- TensorRT-LLM: best NVIDIA performance
- TGI: easiest to deploy
Best practices:
- Start with vLLM + FP16
- Add quantization if memory constrained
- Use tensor parallelism for large models
- Monitor GPU utilization