大模型推理优化与加速完全教程

教程简介

本教程深入讲解大模型推理的性能瓶颈与优化技术,涵盖量化(INT8/INT4/GPTQ/AWQ/GGUF)、KV Cache优化(PagedAttention/Prefix Caching)、投机解码、连续批处理、模型并行、推理框架对比(vLLM/TGI/SGLang/TensorRT-LLM)、硬件选型等核心内容。提供丰富的Python代码示例和实战部署方案,帮助开发者构建高性能、低成本的大模型推理服务。

大模型推理优化与加速完全教程

一、概述

大语言模型(LLM)的推理性能直接影响用户体验和服务成本。一个70B参数的模型,如果未经优化,在普通GPU上可能需要数秒才能生成一个完整的回答。这对于实时对话、代码补全等低延迟场景是不可接受的。

本教程将深入讲解大模型推理的性能瓶颈,系统介绍各种优化技术,包括量化、KV Cache优化、投机解码、批处理优化、模型并行等,并提供实际的代码示例和部署方案。无论你是要部署一个7B的小模型还是数百B的大模型,本教程都能为你提供有价值的指导。

1.1 推理性能的关键指标

在开始优化之前,我们需要明确衡量推理性能的关键指标:

首Token延迟(Time to First Token, TTFT):从请求发送到生成第一个token的时间。这主要取决于prefill阶段的计算量。对于对话应用,TTFT直接影响用户感知的响应速度。

吞吐量(Throughput):单位时间内生成的token数量,通常用tokens/s表示。高吞吐量意味着服务器能同时服务更多用户。

每Token延迟(Time Per Output Token, TPOT):生成每个token所需的时间。这决定了用户看到文字"流出"的速度。

每请求延迟(Latency):完成整个请求的总时间,等于TTFT + TPOT × 输出token数。

并发数(Concurrency):系统能同时处理的请求数量。

成本效率:每百万token的成本,通常用$/M tokens表示。

1.2 推理的两个阶段

LLM推理分为两个截然不同的阶段:

Prefill阶段(预填充):处理输入prompt的所有token,计算注意力并生成KV Cache。这个阶段是计算密集型(compute-bound),可以高度并行化。

Decode阶段(解码):逐个生成输出token,每步只处理一个新token,但需要访问之前所有token的KV Cache。这个阶段是内存带宽密集型(memory-bound),因为需要频繁读取大量的KV Cache数据。

理解这两个阶段的特性对于优化策略的选择至关重要。


二、推理性能瓶颈分析

2.1 计算瓶颈

大模型推理的计算瓶颈主要来自矩阵乘法运算。对于一个标准的Transformer层,主要计算包括:

  • 注意力计算:Q、K、V的线性投影和注意力分数计算
  • 前馈网络(FFN):两个线性层和激活函数
  • 输出投影:将隐藏状态映射到词表大小
import torch
import time

def profile_transformer_layer(hidden_size=4096, num_heads=32, ffn_size=11008, seq_len=2048, batch_size=1):
    """分析Transformer层的计算量"""
    
    # QKV投影: 3 * (hidden_size * hidden_size) 次乘法
    qkv_flops = 3 * 2 * batch_size * seq_len * hidden_size * hidden_size
    
    # 注意力分数: batch * heads * seq_len * seq_len * head_dim
    head_dim = hidden_size // num_heads
    attention_flops = 2 * batch_size * num_heads * seq_len * seq_len * head_dim
    
    # FFN: 两个线性层
    ffn_flops = 2 * 2 * batch_size * seq_len * hidden_size * ffn_size
    
    total_flops = qkv_flops + attention_flops + ffn_flops
    
    print(f"序列长度: {seq_len}")
    print(f"QKV投影: {qkv_flops/1e9:.2f} GFLOPS")
    print(f"注意力计算: {attention_flops/1e9:.2f} GFLOPS")
    print(f"FFN计算: {ffn_flops/1e9:.2f} GFLOPS")
    print(f"总计算量: {total_flops/1e9:.2f} GFLOPS")
    
    # 对比不同序列长度
    for sl in [512, 1024, 2048, 4096, 8192]:
        attn = 2 * batch_size * num_heads * sl * sl * head_dim
        total = qkv_flops * (sl/seq_len) + attn + ffn_flops * (sl/seq_len)
        print(f"  序列长度 {sl}: 总计算量 {total/1e9:.2f} GFLOPS")

profile_transformer_layer()

2.2 内存瓶颈

内存瓶颈是大模型推理的主要限制因素,包括:

模型权重:一个70B参数的模型,以FP16存储需要约140GB显存。

KV Cache:随着序列长度和批大小的增加,KV Cache占用的显存快速增长。

激活值:中间计算结果需要临时存储。

def estimate_memory_requirements(
    model_params_b: float,
    precision_bytes: int = 2,  # FP16
    seq_len: int = 2048,
    batch_size: int = 1,
    num_layers: int = 80,
    hidden_size: int = 8192,
    num_kv_heads: int = 8,
):
    """估算模型推理的显存需求"""
    
    # 模型权重
    model_memory_gb = model_params_b * precision_bytes / 1.0  # GB
    
    # KV Cache (每个token的KV cache大小)
    # 每层: 2(K+V) * hidden_size * precision_bytes
    kv_per_token = 2 * num_layers * hidden_size * precision_bytes  # bytes
    kv_cache_gb = batch_size * seq_len * kv_per_token / 1e9
    
    # 激活值(粗略估计)
    activation_gb = batch_size * seq_len * hidden_size * num_layers * precision_bytes * 4 / 1e9
    
    # 运行时开销
    runtime_overhead_gb = 1.0
    
    total_gb = model_memory_gb + kv_cache_gb + activation_gb + runtime_overhead_gb
    
    print(f"模型参数: {model_params_b}B")
    print(f"精度: FP{precision_bytes * 8}")
    print(f"序列长度: {seq_len}, 批大小: {batch_size}")
    print(f"---")
    print(f"模型权重: {model_memory_gb:.1f} GB")
    print(f"KV Cache: {kv_cache_gb:.1f} GB")
    print(f"激活值: {activation_gb:.1f} GB")
    print(f"运行时开销: {runtime_overhead_gb:.1f} GB")
    print(f"---")
    print(f"总计: {total_gb:.1f} GB")
    
    return total_gb

# 不同模型的显存需求
print("=== 7B 模型 ===")
estimate_memory_requirements(7, num_layers=32, hidden_size=4096, num_kv_heads=32)

print("\n=== 70B 模型 ===")
estimate_memory_requirements(70, num_layers=80, hidden_size=8192, num_kv_heads=8)

print("\n=== 70B 模型 (INT8量化) ===")
estimate_memory_requirements(70, precision_bytes=1, num_layers=80, hidden_size=8192, num_kv_heads=8)

2.3 IO瓶颈

IO瓶颈主要体现在:

  • GPU显存带宽:decode阶段需要频繁读取KV Cache,受限于显存带宽
  • CPU-GPU数据传输:批处理调度和结果返回需要PCIe传输
  • 磁盘IO:模型加载和日志写入
def analyze_bandwidth_bottleneck(
    model_params_b: float,
    batch_size: int,
    precision_bytes: int = 2,
    gpu_bandwidth_gbps: float = 2048,  # A100: 2TB/s
):
    """分析带宽瓶颈"""
    
    # 每生成一个token需要读取的权重数据量
    weight_read_gb = model_params_b * precision_bytes / 1e3  # GB
    
    # 理论最大吞吐量 = 带宽 / 每token数据量
    max_tokens_per_sec = gpu_bandwidth_gbps * 1e9 / (weight_read_gb * 1e9 / batch_size)
    
    print(f"模型大小: {model_params_b}B, 精度: FP{precision_bytes*8}")
    print(f"批大小: {batch_size}")
    print(f"每token需读取: {weight_read_gb:.1f} GB")
    print(f"GPU带宽: {gpu_bandwidth_gbps} GB/s")
    print(f"理论最大吞吐: {max_tokens_per_sec:.0f} tokens/s")
    
    return max_tokens_per_sec

print("=== 不同配置的带宽瓶颈分析 ===")
for bs in [1, 4, 16, 64]:
    analyze_bandwidth_bottleneck(70, bs)
    print()

三、量化技术

量化是降低模型精度以减少内存占用和计算量的技术,是大模型推理优化最常用的手段之一。

3.1 量化基础

import torch
import torch.nn as nn
import numpy as np

class QuantizationDemo:
    """量化技术演示"""
    
    @staticmethod
    def symmetric_quantize(tensor: torch.Tensor, bits: int = 8) -> tuple:
        """对称量化"""
        qmin = -(2 ** (bits - 1))
        qmax = 2 ** (bits - 1) - 1
        
        # 计算缩放因子
        abs_max = tensor.abs().max()
        scale = abs_max / qmax
        
        # 量化
        quantized = torch.round(tensor / scale).clamp(qmin, qmax).to(torch.int8)
        
        return quantized, scale
    
    @staticmethod
    def asymmetric_quantize(tensor: torch.Tensor, bits: int = 8) -> tuple:
        """非对称量化"""
        qmin = 0
        qmax = 2 ** bits - 1
        
        # 计算缩放因子和零点
        min_val = tensor.min()
        max_val = tensor.max()
        scale = (max_val - min_val) / (qmax - qmin)
        zero_point = torch.round(-min_val / scale).clamp(qmin, qmax).to(torch.int)
        
        # 量化
        quantized = torch.round(tensor / scale + zero_point).clamp(qmin, qmax).to(torch.uint8)
        
        return quantized, scale, zero_point
    
    @staticmethod
    def per_channel_quantize(weight: torch.Tensor, bits: int = 8, axis: int = 0) -> tuple:
        """分通道量化 - 更精确的量化方式"""
        qmin = -(2 ** (bits - 1))
        qmax = 2 ** (bits - 1) - 1
        
        # 按通道计算缩放因子
        shape = [1] * weight.ndim
        shape[axis] = weight.shape[axis]
        
        abs_max = weight.abs().amax(dim=[i for i in range(weight.ndim) if i != axis], keepdim=True)
        scale = abs_max / qmax
        
        quantized = torch.round(weight / scale).clamp(qmin, qmax).to(torch.int8)
        
        return quantized, scale
    
    @staticmethod
    def dequantize(quantized: torch.Tensor, scale: torch.Tensor, zero_point: torch.Tensor = None) -> torch.Tensor:
        """反量化"""
        if zero_point is not None:
            return (quantized.float() - zero_point) * scale
        return quantized.float() * scale
    
    @staticmethod
    def calculate_quantization_error(original: torch.Tensor, quantized: torch.Tensor, scale: torch.Tensor) -> dict:
        """计算量化误差"""
        dequantized = quantized.float() * scale
        mse = ((original - dequantized) ** 2).mean().item()
        max_error = (original - dequantized).abs().max().item()
        cos_sim = torch.nn.functional.cosine_similarity(
            original.flatten().unsqueeze(0),
            dequantized.flatten().unsqueeze(0)
        ).item()
        
        return {
            "mse": mse,
            "max_error": max_error,
            "cosine_similarity": cos_sim,
        }

# 演示不同量化方式的效果
demo = QuantizationDemo()
original = torch.randn(1024, 1024)

# INT8对称量化
q8, s8 = demo.symmetric_quantize(original, bits=8)
error8 = demo.calculate_quantization_error(original, q8, s8)
print(f"INT8量化误差: MSE={error8['mse']:.6f}, 余弦相似度={error8['cosine_similarity']:.6f}")

# INT4量化
q4, s4 = demo.symmetric_quantize(original, bits=4)
error4 = demo.calculate_quantization_error(original, q4, s4)
print(f"INT4量化误差: MSE={error4['mse']:.6f}, 余弦相似度={error4['cosine_similarity']:.6f}")

# 分通道INT8量化
q8_ch, s8_ch = demo.per_channel_quantize(original, bits=8)
error8_ch = demo.calculate_quantization_error(original, q8_ch, s8_ch)
print(f"分通道INT8量化误差: MSE={error8_ch['mse']:.6f}, 余弦相似度={error8_ch['cosine_similarity']:.6f}")

3.2 GPTQ量化

GPTQ是一种基于近似二阶信息的训练后量化方法,能够在INT4精度下保持较高的模型质量。

# GPTQ量化示例(使用auto-gptq库)
# pip install auto-gptq

from transformers import AutoTokenizer
# from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig

def gptq_quantize_model(
    model_name: str,
    output_dir: str,
    bits: int = 4,
    group_size: int = 128,
    desc_act: bool = True,
):
    """
    使用GPTQ量化模型
    
    参数:
        model_name: 原始模型名称或路径
        output_dir: 量化后模型保存路径
        bits: 量化位数 (2, 3, 4, 8)
        group_size: 分组大小 (32, 64, 128, -1表示不分组)
        desc_act: 是否按激活值降序排列
    """
    # 加载tokenizer
    tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
    
    # 配置量化参数
    # quantize_config = BaseQuantizeConfig(
    #     bits=bits,
    #     group_size=group_size,
    #     desc_act=desc_act,
    #     damp_percent=0.01,
    # )
    
    # 加载模型
    # model = AutoGPTQForCausalLM.from_pretrained(
    #     model_name,
    #     quantize_config=quantize_config,
    #     trust_remote_code=True,
    # )
    
    # 准备校准数据(使用少量数据即可)
    # calibration_data = load_calibration_data(tokenizer, num_samples=128)
    
    # 执行量化
    # model.quantize(calibration_data)
    
    # 保存量化后的模型
    # model.save_quantized(output_dir)
    # tokenizer.save_pretrained(output_dir)
    
    print(f"模型已量化并保存到: {output_dir}")
    print(f"量化配置: bits={bits}, group_size={group_size}")

# GPTQ量化的关键参数说明:
# - bits: 4bit是最常用的设置,在精度和性能间取得良好平衡
# - group_size: 越小越精确但速度越慢,128是常用值
# - desc_act: True通常能获得更好的量化质量
# - damp_percent: 量化过程中的阻尼系数,处理奇异矩阵

def load_calibration_data(tokenizer, num_samples=128):
    """加载校准数据"""
    # 实际使用时应加载有代表性的数据
    # 例如: WikiText, C4, 或者业务相关的数据
    samples = []
    for _ in range(num_samples):
        text = "这是一段用于校准的示例文本。" * 10
        tokens = tokenizer(text, return_tensors="pt", truncation=True, max_length=2048)
        samples.append(tokens)
    return samples

3.3 AWQ量化

AWQ(Activation-aware Weight Quantization)通过保护重要权重通道来提高量化质量。

# AWQ量化示例
# pip install awq

def awq_quantize_model(
    model_name: str,
    output_dir: str,
    quant_config: dict = None,
):
    """
    使用AWQ量化模型
    
    AWQ的核心思想:
    1. 识别对模型输出影响最大的权重通道(通过分析激活值分布)
    2. 对这些重要通道进行特殊处理(缩放后再量化)
    3. 从而在低比特量化下保持更好的精度
    """
    if quant_config is None:
        quant_config = {
            "zero_point": True,
            "q_group_size": 128,
            "w_bit": 4,
            "version": "GEMM",  # GEMM或GEMV
        }
    
    # from awq import AutoAWQForCausalLM
    # from transformers import AutoTokenizer
    
    # 加载模型和tokenizer
    # model = AutoAWQForCausalLM.from_pretrained(model_name)
    # tokenizer = AutoTokenizer.from_pretrained(model_name)
    
    # 准备校准数据
    # calibration_data = prepare_calibration_data(tokenizer)
    
    # 执行量化
    # model.quantize(tokenizer, quant_config=quant_config)
    
    # 保存
    # model.save_quantized(output_dir)
    # tokenizer.save_pretrained(output_dir)
    
    print(f"AWQ量化完成: {output_dir}")
    print(f"量化配置: {quant_config}")

# AWQ vs GPTQ 对比:
# AWQ优势:
# - 量化速度更快(不需要逐层Hessian计算)
# - 对outlier处理更好
# - 推理速度通常更快
#
# GPTQ优势:
# - 在某些模型上精度更高
# - 社区支持更广泛
# - 生态更成熟

3.4 GGUF格式与llama.cpp

GGUF是llama.cpp使用的量化格式,特别适合CPU推理和边缘设备部署。

# GGUF量化参数说明

GGUF_QUANT_TYPES = {
    "Q2_K": {"bits": 2.5, "description": "2-bit量化,最小体积,质量损失明显"},
    "Q3_K_S": {"bits": 3.0, "description": "3-bit量化(小),体积小"},
    "Q3_K_M": {"bits": 3.5, "description": "3-bit量化(中),平衡选择"},
    "Q3_K_L": {"bits": 3.75, "description": "3-bit量化(大),质量稍好"},
    "Q4_0": {"bits": 4.0, "description": "4-bit量化(旧版),速度快"},
    "Q4_K_S": {"bits": 4.0, "description": "4-bit量化(小),推荐"},
    "Q4_K_M": {"bits": 4.5, "description": "4-bit量化(中),最推荐"},
    "Q5_0": {"bits": 5.0, "description": "5-bit量化(旧版)"},
    "Q5_K_S": {"bits": 5.0, "description": "5-bit量化(小)"},
    "Q5_K_M": {"bits": 5.5, "description": "5-bit量化(中),高质量"},
    "Q6_K": {"bits": 6.0, "description": "6-bit量化,接近原始质量"},
    "Q8_0": {"bits": 8.0, "description": "8-bit量化,几乎无损"},
}

def recommend_quantization(model_size_b: float, available_memory_gb: float, use_case: str) -> str:
    """根据场景推荐量化方案"""
    
    # 估算不同量化下的内存需求
    for quant_type, info in GGUF_QUANT_TYPES.items():
        memory_needed = model_size_b * info["bits"] / 8  # GB
        if memory_needed <= available_memory_gb * 0.85:  # 留15%余量
            if use_case == "quality":
                # 质量优先:选择最高精度
                if info["bits"] >= 5.0:
                    return f"{quant_type} (内存需求: {memory_needed:.1f}GB, 质量优先)"
            elif use_case == "speed":
                # 速度优先:选择4-bit
                if 4.0 <= info["bits"] <= 4.5:
                    return f"{quant_type} (内存需求: {memory_needed:.1f}GB, 速度优先)"
            elif use_case == "balanced":
                # 平衡:Q4_K_M
                if quant_type == "Q4_K_M":
                    return f"{quant_type} (内存需求: {memory_needed:.1f}GB, 平衡选择)"
    
    return "可用显存不足,建议使用更小的模型或更多GPU"

# 使用示例
print("=== 7B模型,16GB显存 ===")
print(recommend_quantization(7, 16, "balanced"))

print("\n=== 70B模型,48GB显存 ===")
print(recommend_quantization(70, 48, "balanced"))

print("\n=== 70B模型,80GB显存 ===")
print(recommend_quantization(70, 80, "quality"))

3.5 动态量化与混合精度

import torch
from torch.quantization import quantize_dynamic

class HybridPrecisionManager:
    """混合精度管理器 - 对不同层使用不同精度"""
    
    def __init__(self, model, config: dict = None):
        self.model = model
        self.config = config or self._default_config()
    
    def _default_config(self) -> dict:
        """默认精度配置"""
        return {
            "attention": "int8",      # 注意力层使用INT8
            "ffn_up": "int4",         # FFN上投影使用INT4
            "ffn_down": "int4",       # FFN下投影使用INT4
            "ffn_gate": "int4",       # FFN门控使用INT4
            "embeddings": "fp16",     # 嵌入层保持FP16
            "lm_head": "fp16",        # 输出头保持FP16
            "layer_norm": "fp32",     # LayerNorm保持FP32
        }
    
    def apply_hybrid_precision(self):
        """应用混合精度量化"""
        for name, module in self.model.named_modules():
            precision = self._get_layer_precision(name)
            
            if precision == "int8":
                self._quantize_int8(module)
            elif precision == "int4":
                self._quantize_int4(module)
            elif precision == "fp16":
                module.half()
            # fp32保持不变
    
    def _get_layer_precision(self, layer_name: str) -> str:
        """根据层名确定精度"""
        for pattern, precision in self.config.items():
            if pattern in layer_name:
                return precision
        return "fp16"  # 默认FP16
    
    def _quantize_int8(self, module):
        """INT8量化"""
        if isinstance(module, torch.nn.Linear):
            # 使用动态量化
            quantized = torch.quantization.quantize_dynamic(
                module, {torch.nn.Linear}, dtype=torch.qint8
            )
            return quantized
    
    def _quantize_int4(self, module):
        """INT4量化(需要专门的INT4实现)"""
        # INT4量化通常需要自定义CUDA kernel
        pass

def demonstrate_memory_savings():
    """演示不同精度的内存节省"""
    sizes = {
        "FP32": 4,
        "FP16": 2,
        "INT8": 1,
        "INT4": 0.5,
        "INT2": 0.25,
    }
    
    model_params_b = 70
    
    print(f"模型参数量: {model_params_b}B")
    print("-" * 40)
    for precision, bytes_per_param in sizes.items():
        memory_gb = model_params_b * bytes_per_param
        savings = (1 - bytes_per_param / 4) * 100
        print(f"{precision:6s}: {memory_gb:6.1f} GB (节省 {savings:.0f}%)")

demonstrate_memory_savings()

四、KV Cache优化

KV Cache是LLM推理中最重要的优化技术之一。在自回归生成过程中,每个新token的生成都需要访问之前所有token的Key和Value,KV Cache避免了重复计算。

4.1 KV Cache基础

import torch
import torch.nn as nn
import math

class KVCache:
    """基础KV Cache实现"""
    
    def __init__(self, num_layers: int, num_heads: int, head_dim: int, max_seq_len: int, dtype=torch.float16):
        self.num_layers = num_layers
        self.num_heads = num_heads
        self.head_dim = head_dim
        self.max_seq_len = max_seq_len
        
        # 预分配KV Cache
        self.k_cache = torch.zeros(
            num_layers, 1, num_heads, max_seq_len, head_dim, dtype=dtype
        )
        self.v_cache = torch.zeros(
            num_layers, 1, num_heads, max_seq_len, head_dim, dtype=dtype
        )
        self.current_len = 0
    
    def update(self, layer_idx: int, new_k: torch.Tensor, new_v: torch.Tensor):
        """更新KV Cache"""
        seq_len = new_k.shape[2]
        self.k_cache[layer_idx, :, :, self.current_len:self.current_len + seq_len] = new_k
        self.v_cache[layer_idx, :, :, self.current_len:self.current_len + seq_len] = new_v
        
        if layer_idx == self.num_layers - 1:
            self.current_len += seq_len
    
    def get(self, layer_idx: int) -> tuple:
        """获取指定层的KV Cache"""
        return (
            self.k_cache[layer_idx, :, :, :self.current_len],
            self.v_cache[layer_idx, :, :, :self.current_len]
        )
    
    def memory_usage(self) -> float:
        """计算KV Cache的显存占用(GB)"""
        # 每个元素的字节数
        element_size = 2 if self.k_cache.dtype == torch.float16 else 4
        total_elements = self.k_cache.numel() + self.v_cache.numel()
        return total_elements * element_size / 1e9

# KV Cache内存分析
def analyze_kv_cache_memory():
    """分析不同配置下的KV Cache内存占用"""
    configs = [
        {"name": "7B", "layers": 32, "heads": 32, "head_dim": 128, "kv_heads": 32},
        {"name": "13B", "layers": 40, "heads": 40, "head_dim": 128, "kv_heads": 40},
        {"name": "70B", "layers": 80, "heads": 64, "head_dim": 128, "kv_heads": 8},
    ]
    
    seq_lengths = [512, 1024, 2048, 4096, 8192]
    batch_sizes = [1, 4, 16, 64]
    
    for config in configs:
        print(f"\n=== {config['name']} 模型 ===")
        for bs in batch_sizes:
            for sl in seq_lengths:
                # KV Cache大小 = 2(K+V) * layers * batch * seq_len * kv_heads * head_dim * bytes
                kv_size_gb = 2 * config['layers'] * bs * sl * config['kv_heads'] * config['head_dim'] * 2 / 1e9
                print(f"  batch={bs:3d}, seq_len={sl:5d}: KV Cache = {kv_size_gb:6.2f} GB")

analyze_kv_cache_memory()

4.2 PagedAttention(vLLM核心优化)

PagedAttention是vLLM引入的核心优化技术,通过类似操作系统虚拟内存分页的机制来管理KV Cache,大幅减少内存碎片和浪费。

import torch
from typing import List, Optional, Tuple

class PagedKVCache:
    """
    PagedAttention的KV Cache实现
    
    核心思想:
    1. 将KV Cache分成固定大小的"页"(block)
    2. 使用页表(block table)管理逻辑到物理的映射
    3. 支持非连续存储,减少内存碎片
    4. 支持copy-on-write,优化beam search等场景
    """
    
    def __init__(
        self,
        num_layers: int,
        num_kv_heads: int,
        head_dim: int,
        block_size: int = 16,
        num_blocks: int = 1000,
        dtype=torch.float16,
    ):
        self.num_layers = num_layers
        self.num_kv_heads = num_kv_heads
        self.head_dim = head_dim
        self.block_size = block_size
        self.num_blocks = num_blocks
        self.dtype = dtype
        
        # 物理块池: [num_layers, 2, num_blocks, block_size, num_kv_heads, head_dim]
        # 2 表示 K 和 V
        self.block_pool = torch.zeros(
            num_layers, 2, num_blocks, block_size, num_kv_heads, head_dim,
            dtype=dtype
        )
        
        # 空闲块列表
        self.free_blocks = list(range(num_blocks))
        
        # 每个序列的页表: seq_id -> list of physical block indices
        self.block_tables = {}
        
        # 每个序列的当前长度
        self.seq_lengths = {}
    
    def allocate_sequence(self, seq_id: int) -> List[int]:
        """为新序列分配初始块"""
        if not self.free_blocks:
            raise RuntimeError("没有可用的空闲块")
        
        block_idx = self.free_blocks.pop(0)
        self.block_tables[seq_id] = [block_idx]
        self.seq_lengths[seq_id] = 0
        return [block_idx]
    
    def append_token(self, seq_id: int, layer_idx: int, k: torch.Tensor, v: torch.Tensor):
        """
        追加一个token的KV到缓存
        
        参数:
            seq_id: 序列ID
            layer_idx: 层索引
            k: [1, num_kv_heads, 1, head_dim] 的Key张量
            v: [1, num_kv_heads, 1, head_dim] 的Value张量
        """
        if seq_id not in self.block_tables:
            self.allocate_sequence(seq_id)
        
        current_len = self.seq_lengths[seq_id]
        block_idx_in_seq = current_len // self.block_size
        offset_in_block = current_len % self.block_size
        
        # 需要新块
        if block_idx_in_seq >= len(self.block_tables[seq_id]):
            if not self.free_blocks:
                raise RuntimeError("没有可用的空闲块")
            new_block = self.free_blocks.pop(0)
            self.block_tables[seq_id].append(new_block)
        
        # 写入KV
        physical_block = self.block_tables[seq_id][block_idx_in_seq]
        self.block_pool[layer_idx, 0, physical_block, offset_in_block] = k.squeeze(0).squeeze(1)
        self.block_pool[layer_idx, 1, physical_block, offset_in_block] = v.squeeze(0).squeeze(1)
        
        if layer_idx == self.num_layers - 1:
            self.seq_lengths[seq_id] += 1
    
    def get_kv(self, seq_id: int, layer_idx: int) -> Tuple[torch.Tensor, torch.Tensor]:
        """获取序列的完整KV Cache"""
        block_table = self.block_tables[seq_id]
        seq_len = self.seq_lengths[seq_id]
        
        # 收集所有块的KV
        k_list = []
        v_list = []
        
        for i, physical_block in enumerate(block_table):
            start = i * self.block_size
            end = min(start + self.block_size, seq_len)
            actual_len = end - start
            
            k_list.append(self.block_pool[layer_idx, 0, physical_block, :actual_len])
            v_list.append(self.block_pool[layer_idx, 1, physical_block, :actual_len])
        
        k = torch.cat(k_list, dim=0)  # [seq_len, num_kv_heads, head_dim]
        v = torch.cat(v_list, dim=0)
        
        # 调整形状为 [1, num_kv_heads, seq_len, head_dim]
        k = k.unsqueeze(0).transpose(1, 2)
        v = v.unsqueeze(0).transpose(1, 2)
        
        return k, v
    
    def free_sequence(self, seq_id: int):
        """释放序列的KV Cache"""
        if seq_id in self.block_tables:
            self.free_blocks.extend(self.block_tables[seq_id])
            del self.block_tables[seq_id]
            del self.seq_lengths[seq_id]
    
    def memory_usage(self) -> dict:
        """内存使用统计"""
        total_blocks = self.num_blocks
        used_blocks = total_blocks - len(self.free_blocks)
        
        # 每个块的字节数
        block_bytes = self.block_size * self.num_kv_heads * self.head_dim * 2 * 2  # 2(K+V) * 2(FP16)
        
        return {
            "total_blocks": total_blocks,
            "used_blocks": used_blocks,
            "free_blocks": len(self.free_blocks),
            "utilization": used_blocks / total_blocks * 100,
            "total_memory_gb": total_blocks * block_bytes * self.num_layers / 1e9,
            "used_memory_gb": used_blocks * block_bytes * self.num_layers / 1e9,
        }

# 使用示例
cache = PagedKVCache(
    num_layers=32,
    num_kv_heads=32,
    head_dim=128,
    block_size=16,
    num_blocks=500,
)

# 模拟多个序列
for seq_id in range(5):
    cache.allocate_sequence(seq_id)
    for token_idx in range(100):
        for layer in range(32):
            k = torch.randn(1, 32, 1, 128, dtype=torch.float16)
            v = torch.randn(1, 32, 1, 128, dtype=torch.float16)
            cache.append_token(seq_id, layer, k, v)

print("内存使用统计:", cache.memory_usage())

4.3 Prefix Caching

Prefix Caching(前缀缓存)通过缓存公共前缀的KV Cache来加速多轮对话和共享system prompt的场景。

import hashlib
from typing import Dict, List, Optional, Tuple

class PrefixCacheManager:
    """
    前缀缓存管理器
    
    适用场景:
    1. 多轮对话共享system prompt
    2. 批量请求共享相同的前缀
    3. RAG场景中共享检索结果的KV Cache
    """
    
    def __init__(self, kv_cache: PagedKVCache):
        self.kv_cache = kv_cache
        # 前缀哈希 -> 物理块列表
        self.prefix_cache: Dict[str, List[int]] = {}
        # 前缀哈希 -> 前缀长度
        self.prefix_lengths: Dict[str, int] = {}
        # 引用计数
        self.ref_counts: Dict[str, int] = {}
    
    def compute_prefix_hash(self, token_ids: List[int]) -> str:
        """计算token序列的哈希"""
        return hashlib.sha256(str(token_ids).encode()).hexdigest()[:16]
    
    def get_cached_prefix(self, token_ids: List[int]) -> Optional[Tuple[List[int], int]]:
        """
        查找已缓存的前缀
        
        返回: (物理块列表, 缓存的token数量) 或 None
        """
        # 尝试从最长前缀开始查找
        for length in range(len(token_ids), 0, -1):
            prefix_hash = self.compute_prefix_hash(token_ids[:length])
            if prefix_hash in self.prefix_cache:
                self.ref_counts[prefix_hash] = self.ref_counts.get(prefix_hash, 0) + 1
                return self.prefix_cache[prefix_hash], length
        
        return None
    
    def cache_prefix(self, token_ids: List[int], block_table: List[int]):
        """缓存前缀的KV Cache"""
        prefix_hash = self.compute_prefix_hash(token_ids)
        # 复制块表(copy-on-write语义)
        self.prefix_cache[prefix_hash] = block_table.copy()
        self.prefix_lengths[prefix_hash] = len(token_ids)
        self.ref_counts[prefix_hash] = 1
    
    def release_prefix(self, token_ids: List[int]):
        """释放前缀缓存引用"""
        prefix_hash = self.compute_prefix_hash(token_ids)
        if prefix_hash in self.ref_counts:
            self.ref_counts[prefix_hash] -= 1
            if self.ref_counts[prefix_hash] <= 0:
                # 可以选择保留或释放物理块
                pass

class MultiTurnConversation:
    """多轮对话管理器 - 利用Prefix Caching优化"""
    
    def __init__(self, prefix_manager: PrefixCacheManager):
        self.prefix_manager = prefix_manager
        self.conversations: Dict[str, dict] = {}
    
    def start_conversation(self, conv_id: str, system_prompt_tokens: List[int]):
        """开始新对话,缓存system prompt"""
        cached = self.prefix_manager.get_cached_prefix(system_prompt_tokens)
        
        self.conversations[conv_id] = {
            "system_tokens": system_prompt_tokens,
            "cached_blocks": cached[0] if cached else None,
            "cached_length": cached[1] if cached else 0,
            "history": [],
        }
        
        if not cached:
            # 需要计算并缓存system prompt的KV
            # 这里是伪代码,实际需要通过模型计算
            pass
    
    def add_turn(self, conv_id: str, user_tokens: List[int], assistant_tokens: List[int]):
        """添加一轮对话"""
        self.conversations[conv_id]["history"].append({
            "user": user_tokens,
            "assistant": assistant_tokens,
        })
    
    def get_context_tokens(self, conv_id: str) -> Tuple[List[int], int]:
        """
        获取完整的上下文token序列
        
        返回: (token列表, 已缓存的token数量)
        """
        conv = self.conversations[conv_id]
        
        # 系统提示
        tokens = conv["system_tokens"].copy()
        cached_len = conv["cached_length"]
        
        # 历史对话
        for turn in conv["history"]:
            tokens.extend(turn["user"])
            tokens.extend(turn["assistant"])
        
        return tokens, cached_len
    
    def estimate_speedup(self, conv_id: str) -> float:
        """估算Prefix Caching带来的加速比"""
        tokens, cached_len = self.get_context_tokens(conv_id)
        total_len = len(tokens)
        
        if total_len == 0:
            return 1.0
        
        # Prefill时间与token数成正比
        speedup = total_len / (total_len - cached_len) if cached_len < total_len else float('inf')
        return speedup

# 使用示例
# 假设system prompt有500个token,每轮对话约200个token
system_tokens = list(range(500))
conv_tokens = list(range(200))

print("=== Prefix Caching 加速效果分析 ===")
for turn in range(1, 11):
    total_context = 500 + turn * 200 * 2  # system + user + assistant
    cached = 500 + (turn - 1) * 200 * 2  # 前面的都已缓存
    speedup = total_context / (total_context - cached)
    print(f"第{turn}轮对话: 上下文{total_context}tokens, 缓存{cached}tokens, 加速{speedup:.1f}x")

4.4 Multi-Query Attention与Grouped-Query Attention

import torch
import torch.nn as nn
import math

class MultiHeadAttention(nn.Module):
    """标准多头注意力"""
    def __init__(self, hidden_size, num_heads):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = hidden_size // num_heads
        
        self.q_proj = nn.Linear(hidden_size, hidden_size)
        self.k_proj = nn.Linear(hidden_size, hidden_size)
        self.v_proj = nn.Linear(hidden_size, hidden_size)
        self.o_proj = nn.Linear(hidden_size, hidden_size)
    
    def forward(self, x, kv_cache=None):
        B, L, D = x.shape
        
        q = self.q_proj(x).view(B, L, self.num_heads, self.head_dim).transpose(1, 2)
        k = self.k_proj(x).view(B, L, self.num_heads, self.head_dim).transpose(1, 2)
        v = self.v_proj(x).view(B, L, self.num_heads, self.head_dim).transpose(1, 2)
        
        # KV Cache更新
        if kv_cache is not None:
            k = torch.cat([kv_cache[0], k], dim=2)
            v = torch.cat([kv_cache[1], v], dim=2)
        
        # 注意力计算
        attn = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
        attn = torch.softmax(attn, dim=-1)
        out = torch.matmul(attn, v)
        
        out = out.transpose(1, 2).contiguous().view(B, L, -1)
        return self.o_proj(out), (k, v)

class GroupedQueryAttention(nn.Module):
    """
    分组查询注意力 (GQA)
    
    多个查询头共享一组KV头,减少KV Cache大小
    例如: 32个Q头, 8个KV头 -> KV Cache减少4倍
    """
    def __init__(self, hidden_size, num_q_heads, num_kv_heads):
        super().__init__()
        self.num_q_heads = num_q_heads
        self.num_kv_heads = num_kv_heads
        self.num_groups = num_q_heads // num_kv_heads
        self.head_dim = hidden_size // num_q_heads
        
        self.q_proj = nn.Linear(hidden_size, num_q_heads * self.head_dim)
        self.k_proj = nn.Linear(hidden_size, num_kv_heads * self.head_dim)
        self.v_proj = nn.Linear(hidden_size, num_kv_heads * self.head_dim)
        self.o_proj = nn.Linear(hidden_size, hidden_size)
    
    def forward(self, x, kv_cache=None):
        B, L, D = x.shape
        
        q = self.q_proj(x).view(B, L, self.num_q_heads, self.head_dim).transpose(1, 2)
        k = self.k_proj(x).view(B, L, self.num_kv_heads, self.head_dim).transpose(1, 2)
        v = self.v_proj(x).view(B, L, self.num_kv_heads, self.head_dim).transpose(1, 2)
        
        # KV Cache更新
        if kv_cache is not None:
            k = torch.cat([kv_cache[0], k], dim=2)
            v = torch.cat([kv_cache[1], v], dim=2)
        
        # 扩展KV以匹配Q的头数
        k_expanded = k.repeat_interleave(self.num_groups, dim=1)
        v_expanded = v.repeat_interleave(self.num_groups, dim=1)
        
        # 注意力计算
        attn = torch.matmul(q, k_expanded.transpose(-2, -1)) / math.sqrt(self.head_dim)
        attn = torch.softmax(attn, dim=-1)
        out = torch.matmul(attn, v_expanded)
        
        out = out.transpose(1, 2).contiguous().view(B, L, -1)
        return self.o_proj(out), (k, v)  # 返回未扩展的KV用于缓存

class MultiQueryAttention(nn.Module):
    """
    多查询注意力 (MQA)
    
    所有Q头共享一个KV头,KV Cache最小
    """
    def __init__(self, hidden_size, num_heads):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = hidden_size // num_heads
        
        self.q_proj = nn.Linear(hidden_size, hidden_size)
        self.k_proj = nn.Linear(hidden_size, self.head_dim)  # 只有一个KV头
        self.v_proj = nn.Linear(hidden_size, self.head_dim)
        self.o_proj = nn.Linear(hidden_size, hidden_size)
    
    def forward(self, x, kv_cache=None):
        B, L, D = x.shape
        
        q = self.q_proj(x).view(B, L, self.num_heads, self.head_dim).transpose(1, 2)
        k = self.k_proj(x).view(B, L, 1, self.head_dim).transpose(1, 2)
        v = self.v_proj(x).view(B, L, 1, self.head_dim).transpose(1, 2)
        
        if kv_cache is not None:
            k = torch.cat([kv_cache[0], k], dim=2)
            v = torch.cat([kv_cache[1], v], dim=2)
        
        # 扩展KV
        k_expanded = k.expand(-1, self.num_heads, -1, -1)
        v_expanded = v.expand(-1, self.num_heads, -1, -1)
        
        attn = torch.matmul(q, k_expanded.transpose(-2, -1)) / math.sqrt(self.head_dim)
        attn = torch.softmax(attn, dim=-1)
        out = torch.matmul(attn, v_expanded)
        
        out = out.transpose(1, 2).contiguous().view(B, L, -1)
        return self.o_proj(out), (k, v)

# KV Cache大小对比
def compare_kv_cache_sizes(hidden_size=4096, num_heads=32, seq_len=4096, batch_size=1):
    """对比不同注意力机制的KV Cache大小"""
    head_dim = hidden_size // num_heads
    
    # MHA: 每个头都有独立的KV
    mha_kv = 2 * batch_size * num_heads * seq_len * head_dim * 2  # FP16
    
    # GQA (8个KV头)
    gqa_kv = 2 * batch_size * 8 * seq_len * head_dim * 2
    
    # MQA: 只有一个KV头
    mqa_kv = 2 * batch_size * 1 * seq_len * head_dim * 2
    
    print(f"序列长度: {seq_len}, 批大小: {batch_size}")
    print(f"MHA KV Cache: {mha_kv/1e6:.1f} MB")
    print(f"GQA KV Cache (8 KV头): {gqa_kv/1e6:.1f} MB (节省 {(1-gqa_kv/mha_kv)*100:.0f}%)")
    print(f"MQA KV Cache: {mqa_kv/1e6:.1f} MB (节省 {(1-mqa_kv/mha_kv)*100:.0f}%)")

compare_kv_cache_sizes()

五、投机解码(Speculative Decoding)

投机解码是一种通过使用小型"草稿"模型快速生成候选token,再由大模型并行验证的技术,可以在不损失质量的情况下显著提升推理速度。

5.1 基本原理与实现

import torch
import torch.nn.functional as F
from typing import List, Tuple, Optional

class SpeculativeDecoder:
    """
    投机解码器
    
    原理:
    1. 使用小模型(draft model)快速生成K个候选token
    2. 使用大模型(target model)并行验证这K个token
    3. 接受概率匹配的token,拒绝的token从修正分布中重新采样
    4. 理论上可以保证输出分布与单独使用大模型完全一致
    """
    
    def __init__(self, target_model, draft_model, tokenizer, K: int = 5):
        self.target_model = target_model
        self.draft_model = draft_model
        self.tokenizer = tokenizer
        self.K = K  # 每次投机的token数量
    
    def generate(self, prompt: str, max_tokens: int = 100, temperature: float = 0.0) -> str:
        """使用投机解码生成文本"""
        input_ids = self.tokenizer.encode(prompt)
        generated_ids = []
        
        total_target_calls = 0
        total_draft_calls = 0
        total_accepted = 0
        
        while len(generated_ids) < max_tokens:
            # 第一步:使用draft model快速生成K个候选token
            draft_tokens, draft_probs = self._draft_generate(
                input_ids + generated_ids, self.K, temperature
            )
            total_draft_calls += 1
            
            # 第二步:使用target model并行验证
            target_probs = self._target_verify(
                input_ids + generated_ids + draft_tokens
            )
            total_target_calls += 1
            
            # 第三步:逐个验证并接受/拒绝
            accepted_tokens = []
            for i in range(self.K):
                draft_token = draft_tokens[i]
                draft_prob = draft_probs[i][draft_token]
                target_prob = target_probs[i][draft_token]
                
                # 接受概率
                if temperature == 0:
                    # 贪心解码:如果target也选择这个token则接受
                    target_best = target_probs[i].argmax().item()
                    if draft_token == target_best:
                        accepted_tokens.append(draft_token)
                        total_accepted += 1
                    else:
                        # 使用target的token
                        accepted_tokens.append(target_best)
                        break
                else:
                    # 采样解码:按概率接受
                    accept_prob = min(1.0, target_prob / draft_prob)
                    if torch.rand(1).item() < accept_prob:
                        accepted_tokens.append(draft_token)
                        total_accepted += 1
                    else:
                        # 从修正分布中采样
                        corrected_probs = F.relu(target_probs[i] - draft_probs[i])
                        corrected_probs = corrected_probs / corrected_probs.sum()
                        new_token = torch.multinomial(corrected_probs, 1).item()
                        accepted_tokens.append(new_token)
                        break
            
            generated_ids.extend(accepted_tokens)
            
            # 检查是否生成了结束符
            if self.tokenizer.eos_token_id in accepted_tokens:
                break
        
        # 计算加速比
        avg_accepted = total_accepted / total_draft_calls if total_draft_calls > 0 else 0
        speedup = (total_accepted + total_draft_calls) / (total_target_calls + total_draft_calls)
        
        result = self.tokenizer.decode(generated_ids)
        print(f"加速统计: 平均每次接受 {avg_accepted:.1f} 个token, 加速比 {speedup:.2f}x")
        
        return result
    
    def _draft_generate(self, input_ids: List[int], K: int, temperature: float) -> Tuple[List[int], List[torch.Tensor]]:
        """使用draft model生成K个token"""
        tokens = []
        probs = []
        
        current_ids = input_ids.copy()
        
        for _ in range(K):
            with torch.no_grad():
                outputs = self.draft_model(torch.tensor([current_ids]))
                logits = outputs.logits[:, -1, :]
                
                if temperature > 0:
                    logits = logits / temperature
                    prob = F.softmax(logits, dim=-1)
                    token = torch.multinomial(prob, 1).item()
                    probs.append(prob.squeeze(0))
                else:
                    token = logits.argmax(dim=-1).item()
                    prob = F.softmax(logits, dim=-1)
                    probs.append(prob.squeeze(0))
                
                tokens.append(token)
                current_ids.append(token)
        
        return tokens, probs
    
    def _target_verify(self, input_ids: List[int]) -> List[torch.Tensor]:
        """使用target model并行验证"""
        with torch.no_grad():
            outputs = self.target_model(torch.tensor([input_ids]))
            logits = outputs.logits
            
            # 获取每个位置的概率分布
            # input_ids[K:] 对应的logits在位置 [K-1:-1]
            start_pos = len(input_ids) - self.K - 1
            probs = F.softmax(logits[:, start_pos:start_pos + self.K, :], dim=-1)
            
            return [probs[0, i] for i in range(self.K)]

# 投机解码的理论分析
def analyze_speculative_speedup(draft_acceptance_rate: float, K: int, draft_speed_ratio: float = 10.0):
    """
    分析投机解码的理论加速比
    
    参数:
        draft_acceptance_rate: draft model的token被接受的平均概率
        K: 每次投机的token数
        draft_speed_ratio: draft model相对target model的速度比
    """
    # 期望接受的token数
    expected_accepted = sum(draft_acceptance_rate ** i for i in range(1, K + 1))
    
    # 每轮的成本(以target model的前向传播为单位)
    cost_per_round = 1.0 + K / draft_speed_ratio  # 1次target验证 + K次draft生成
    
    # 加速比
    speedup = (expected_accepted + 1) / cost_per_round  # +1 因为target也会生成1个token
    
    print(f"K={K}, 接受率={draft_acceptance_rate:.0%}, draft速度比={draft_speed_ratio}x")
    print(f"  期望接受: {expected_accepted:.1f} tokens")
    print(f"  每轮成本: {cost_per_round:.2f}")
    print(f"  加速比: {speedup:.2f}x")
    
    return speedup

print("=== 投机解码理论加速比分析 ===")
for acceptance_rate in [0.5, 0.7, 0.8, 0.9]:
    for K in [3, 5, 7, 10]:
        analyze_speculative_speedup(acceptance_rate, K)
    print()

5.2 自投机解码(Self-Speculative Decoding)

class SelfSpeculativeDecoder:
    """
    自投机解码 - 使用模型自身的子集作为draft model
    
    方法:
    1. 跳过某些层(Layer Skipping)
    2. 使用浅层作为draft,深层作为target
    3. 不需要额外的draft model
    """
    
    def __init__(self, model, num_layers: int, skip_layers: List[int], K: int = 5):
        self.model = model
        self.num_layers = num_layers
        self.skip_layers = skip_layers  # 跳过的层索引
        self.K = K
    
    def draft_forward(self, input_ids: torch.Tensor) -> torch.Tensor:
        """使用跳过层的方式进行draft前向传播"""
        # 这是伪代码,实际实现需要修改模型的forward
        # 跳过指定层以加速
        hidden_states = self.model.embed_tokens(input_ids)
        
        for i, layer in enumerate(self.model.layers):
            if i not in self.skip_layers:
                hidden_states = layer(hidden_states)
        
        logits = self.model.lm_head(hidden_states)
        return logits
    
    def target_forward(self, input_ids: torch.Tensor) -> torch.Tensor:
        """使用完整模型进行target前向传播"""
        return self.model(input_ids).logits

六、批处理优化

6.1 Continuous Batching(连续批处理)

传统批处理需要等待所有请求完成才能处理下一批,Continuous Batching允许在生成过程中动态添加和移除请求,大幅提高GPU利用率。

import asyncio
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from collections import deque
import time

@dataclass
class InferenceRequest:
    request_id: str
    prompt_tokens: List[int]
    max_new_tokens: int
    temperature: float = 0.0
    generated_tokens: List[int] = field(default_factory=list)
    is_finished: bool = False
    arrival_time: float = 0.0
    first_token_time: float = 0.0

class ContinuousBatchScheduler:
    """
    连续批处理调度器
    
    核心思想:
    1. 不等待整个batch完成
    2. 当某个请求完成时,立即用新请求替换
    3. 始终保持GPU满载
    """
    
    def __init__(self, max_batch_size: int = 32, max_seq_len: int = 4096):
        self.max_batch_size = max_batch_size
        self.max_seq_len = max_seq_len
        self.waiting_queue: deque = deque()
        self.running_batch: Dict[str, InferenceRequest] = {}
        self.completed: List[InferenceRequest] = []
        self.kv_cache_manager = None  # 实际的KV Cache管理器
    
    def add_request(self, request: InferenceRequest):
        """添加新请求到等待队列"""
        request.arrival_time = time.time()
        self.waiting_queue.append(request)
    
    def schedule(self) -> List[InferenceRequest]:
        """
        调度下一批请求
        
        策略:
        1. 优先保持当前batch运行
        2. 有空位时从等待队列添加新请求
        3. 请求完成时释放资源
        """
        # 移除已完成的请求
        finished = [rid for rid, req in self.running_batch.items() if req.is_finished]
        for rid in finished:
            self.completed.append(self.running_batch.pop(rid))
        
        # 填充空位
        while len(self.running_batch) < self.max_batch_size and self.waiting_queue:
            new_request = self.waiting_queue.popleft()
            
            # 检查序列长度限制
            if len(new_request.prompt_tokens) < self.max_seq_len:
                self.running_batch[new_request.request_id] = new_request
        
        return list(self.running_batch.values())
    
    def step(self, model_output: Dict[str, int]):
        """
        执行一步推理
        
        参数:
            model_output: {request_id: next_token_id}
        """
        for request_id, next_token in model_output.items():
            if request_id in self.running_batch:
                request = self.running_batch[request_id]
                request.generated_tokens.append(next_token)
                
                if request.first_token_time == 0:
                    request.first_token_time = time.time()
                
                # 检查是否完成
                if (next_token == 0 or  # EOS token
                    len(request.generated_tokens) >= request.max_new_tokens):
                    request.is_finished = True
    
    def get_stats(self) -> dict:
        """获取调度统计"""
        return {
            "waiting": len(self.waiting_queue),
            "running": len(self.running_batch),
            "completed": len(self.completed),
            "batch_utilization": len(self.running_batch) / self.max_batch_size * 100,
        }

class AsyncLLMEngine:
    """异步LLM推理引擎"""
    
    def __init__(self, model, tokenizer, max_batch_size: int = 32):
        self.model = model
        self.tokenizer = tokenizer
        self.scheduler = ContinuousBatchScheduler(max_batch_size=max_batch_size)
        self.is_running = False
    
    async def generate(self, prompt: str, max_tokens: int = 100, temperature: float = 0.0) -> str:
        """异步生成接口"""
        request_id = f"req_{id(prompt)}"
        tokens = self.tokenizer.encode(prompt)
        
        request = InferenceRequest(
            request_id=request_id,
            prompt_tokens=tokens,
            max_new_tokens=max_tokens,
            temperature=temperature,
        )
        
        self.scheduler.add_request(request)
        
        # 等待完成
        while not request.is_finished:
            await asyncio.sleep(0.01)
        
        return self.tokenizer.decode(request.generated_tokens)
    
    async def run_loop(self):
        """主推理循环"""
        self.is_running = True
        
        while self.is_running:
            batch = self.scheduler.schedule()
            
            if batch:
                # 准备batch输入
                input_ids = self._prepare_batch_input(batch)
                
                # 模型推理
                with torch.no_grad():
                    outputs = self.model(input_ids)
                
                # 处理输出
                next_tokens = self._process_batch_output(outputs, batch)
                self.scheduler.step(next_tokens)
            else:
                # 没有请求,等待
                await asyncio.sleep(0.001)
    
    def _prepare_batch_input(self, batch: List[InferenceRequest]):
        """准备batch输入(需要padding)"""
        # 实际实现需要处理不同长度的序列
        pass
    
    def _process_batch_output(self, outputs, batch: List[InferenceRequest]):
        """处理batch输出"""
        # 实际实现需要从logits中采样下一个token
        pass

6.2 Prefill-Decode分离

class ChunkedPrefillScheduler:
    """
    分块Prefill调度器
    
    将长prompt的prefill分成多个chunk,
    避免单个长请求阻塞整个batch
    """
    
    def __init__(self, chunk_size: int = 512, max_batch_tokens: int = 8192):
        self.chunk_size = chunk_size
        self.max_batch_tokens = max_batch_tokens
    
    def schedule_prefill(self, requests: List[InferenceRequest]) -> List[List[int]]:
        """
        将prefill请求分块调度
        
        策略:
        1. 优先处理短请求(SJF - 最短任务优先)
        2. 将长请求分成chunk
        3. 保证总token数不超过max_batch_tokens
        """
        # 按prompt长度排序
        sorted_requests = sorted(requests, key=lambda r: len(r.prompt_tokens))
        
        chunks = []
        current_chunk = []
        current_tokens = 0
        
        for request in sorted_requests:
            prompt_len = len(request.prompt_tokens)
            
            if prompt_len <= self.chunk_size:
                # 短请求,直接加入当前chunk
                if current_tokens + prompt_len <= self.max_batch_tokens:
                    current_chunk.append(request)
                    current_tokens += prompt_len
                else:
                    # 当前chunk已满
                    chunks.append(current_chunk)
                    current_chunk = [request]
                    current_tokens = prompt_len
            else:
                # 长请求,需要分块
                for i in range(0, prompt_len, self.chunk_size):
                    chunk_tokens = min(self.chunk_size, prompt_len - i)
                    if current_tokens + chunk_tokens > self.max_batch_tokens:
                        chunks.append(current_chunk)
                        current_chunk = []
                        current_tokens = 0
                    current_tokens += chunk_tokens
                
                current_chunk.append(request)
        
        if current_chunk:
            chunks.append(current_chunk)
        
        return chunks

七、模型并行

7.1 张量并行(Tensor Parallelism)

import torch
import torch.nn as nn
import torch.distributed as dist

class TensorParallelLinear(nn.Module):
    """
    张量并行线性层
    
    将权重矩阵按列或行切分到多个GPU
    """
    
    def __init__(self, in_features: int, out_features: int, 
                 tp_size: int, tp_rank: int, split_mode: str = "column"):
        super().__init__()
        self.tp_size = tp_size
        self.tp_rank = tp_rank
        self.split_mode = split_mode
        
        if split_mode == "column":
            # 列切分:每个GPU持有 out_features/tp_size 列
            assert out_features % tp_size == 0
            self.local_out_features = out_features // tp_size
            self.linear = nn.Linear(in_features, self.local_out_features, bias=False)
        elif split_mode == "row":
            # 行切分:每个GPU持有 in_features/tp_size 行
            assert in_features % tp_size == 0
            self.local_in_features = in_features // tp_size
            self.linear = nn.Linear(self.local_in_features, out_features, bias=False)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        if self.split_mode == "column":
            # 列切分:每个GPU计算部分输出,无需通信
            return self.linear(x)
        elif self.split_mode == "row":
            # 行切分:每个GPU计算部分结果,需要AllReduce
            local_output = self.linear(x)
            dist.all_reduce(local_output, op=dist.ReduceOp.SUM)
            return local_output

class TensorParallelMLP(nn.Module):
    """张量并行的MLP层"""
    
    def __init__(self, hidden_size: int, ffn_size: int, tp_size: int, tp_rank: int):
        super().__init__()
        # gate和up投影使用列切分
        self.gate_proj = TensorParallelLinear(hidden_size, ffn_size, tp_size, tp_rank, "column")
        self.up_proj = TensorParallelLinear(hidden_size, ffn_size, tp_size, tp_rank, "column")
        # down投影使用行切分
        self.down_proj = TensorParallelLinear(ffn_size, hidden_size, tp_size, tp_rank, "row")
        self.act_fn = nn.SiLU()
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        gate = self.act_fn(self.gate_proj(x))
        up = self.up_proj(x)
        return self.down_proj(gate * up)

class TensorParallelAttention(nn.Module):
    """张量并行的注意力层"""
    
    def __init__(self, hidden_size: int, num_heads: int, tp_size: int, tp_rank: int):
        super().__init__()
        self.num_heads = num_heads
        self.tp_size = tp_size
        self.tp_rank = tp_rank
        self.head_dim = hidden_size // num_heads
        self.local_heads = num_heads // tp_size
        
        # QKV投影使用列切分
        self.q_proj = TensorParallelLinear(hidden_size, hidden_size, tp_size, tp_rank, "column")
        self.k_proj = TensorParallelLinear(hidden_size, hidden_size, tp_size, tp_rank, "column")
        self.v_proj = TensorParallelLinear(hidden_size, hidden_size, tp_size, tp_rank, "column")
        self.o_proj = TensorParallelLinear(hidden_size, hidden_size, tp_size, tp_rank, "row")
    
    def forward(self, x: torch.Tensor, kv_cache=None):
        B, L, D = x.shape
        
        q = self.q_proj(x).view(B, L, self.local_heads, self.head_dim).transpose(1, 2)
        k = self.k_proj(x).view(B, L, self.local_heads, self.head_dim).transpose(1, 2)
        v = self.v_proj(x).view(B, L, self.local_heads, self.head_dim).transpose(1, 2)
        
        # 注意力计算
        attn = torch.matmul(q, k.transpose(-2, -1)) / (self.head_dim ** 0.5)
        attn = torch.softmax(attn, dim=-1)
        out = torch.matmul(attn, v)
        
        out = out.transpose(1, 2).contiguous().view(B, L, -1)
        return self.o_proj(out)

7.2 流水线并行(Pipeline Parallelism)

class PipelineParallelModel:
    """
    流水线并行模型
    
    将模型的不同层分配到不同GPU
    通过流水线调度减少气泡时间
    """
    
    def __init__(self, layers: List[nn.Module], num_stages: int, stage_id: int):
        self.layers = layers
        self.num_stages = num_stages
        self.stage_id = stage_id
        
        # 当前stage负责的层范围
        layers_per_stage = len(layers) // num_stages
        self.start_layer = stage_id * layers_per_stage
        self.end_layer = (stage_id + 1) * layers_per_stage if stage_id < num_stages - 1 else len(layers)
    
    def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
        """执行当前stage的层"""
        for i in range(self.start_layer, self.end_layer):
            hidden_states = self.layers[i](hidden_states)
        return hidden_states

class GPipeScheduler:
    """
    GPipe流水线调度器
    
    将一个batch分成多个micro-batch,
    交错执行以减少流水线气泡
    """
    
    def __init__(self, num_stages: int, num_micro_batches: int):
        self.num_stages = num_stages
        self.num_micro_batches = num_micro_batches
    
    def schedule(self) -> List[List[tuple]]:
        """
        生成调度计划
        
        返回: 每个时间步每个stage执行的micro-batch索引
        """
        schedule = []
        total_steps = self.num_stages + self.num_micro_batches - 1
        
        for step in range(total_steps):
            step_schedule = []
            for stage in range(self.num_stages):
                micro_batch = step - stage
                if 0 <= micro_batch < self.num_micro_batches:
                    step_schedule.append((stage, micro_batch))
                else:
                    step_schedule.append(None)  # 空闲
            schedule.append(step_schedule)
        
        return schedule
    
    def calculate_bubble_ratio(self) -> float:
        """计算气泡比例"""
        total_steps = self.num_stages + self.num_micro_batches - 1
        useful_steps = self.num_micro_batches * self.num_stages
        total_slots = total_steps * self.num_stages
        
        return 1 - useful_steps / total_slots

# 流水线并行效率分析
def analyze_pipeline_efficiency():
    """分析不同配置的流水线效率"""
    for num_stages in [2, 4, 8]:
        for num_micro in [4, 8, 16, 32]:
            scheduler = GPipeScheduler(num_stages, num_micro)
            bubble = scheduler.calculate_bubble_ratio()
            print(f"Stages={num_stages}, Micro-batches={num_micro:2d}: "
                  f"效率={((1-bubble)*100):.1f}%, 气泡={bubble*100:.1f}%")

analyze_pipeline_efficiency()

7.3 专家并行(Expert Parallelism - MoE)

class ExpertParallelMoE(nn.Module):
    """
    专家并行的混合专家层
    
    MoE模型(如Mixtral)使用专家并行:
    - 不同专家分布在不同GPU上
    - 通过All-to-All通信路由token到对应专家
    """
    
    def __init__(self, hidden_size: int, ffn_size: int, 
                 num_experts: int, top_k: int, tp_size: int, tp_rank: int):
        super().__init__()
        self.num_experts = num_experts
        self.top_k = top_k
        self.tp_size = tp_size
        self.tp_rank = tp_rank
        
        # 每个GPU持有 num_experts/tp_size 个专家
        self.local_num_experts = num_experts // tp_size
        self.experts_start = tp_rank * self.local_num_experts
        
        # 路由器(所有GPU共享)
        self.gate = nn.Linear(hidden_size, num_experts, bias=False)
        
        # 本地专家
        self.experts = nn.ModuleList([
            nn.Sequential(
                nn.Linear(hidden_size, ffn_size, bias=False),
                nn.SiLU(),
                nn.Linear(ffn_size, hidden_size, bias=False),
            )
            for _ in range(self.local_num_experts)
        ])
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        B, L, D = x.shape
        x_flat = x.view(-1, D)
        
        # 计算路由权重
        router_logits = self.gate(x_flat)
        router_probs = torch.softmax(router_logits, dim=-1)
        
        # 选择top-k专家
        top_k_probs, top_k_indices = torch.topk(router_probs, self.top_k, dim=-1)
        top_k_probs = top_k_probs / top_k_probs.sum(dim=-1, keepdim=True)
        
        # All-to-All通信:将token路由到对应专家所在的GPU
        # 这里简化处理,实际需要复杂的通信逻辑
        
        # 计算输出
        output = torch.zeros_like(x_flat)
        
        for expert_idx in range(self.local_num_experts):
            global_expert_idx = self.experts_start + expert_idx
            
            # 找到路由到当前专家的token
            for k in range(self.top_k):
                mask = (top_k_indices[:, k] == global_expert_idx)
                if mask.any():
                    expert_input = x_flat[mask]
                    expert_output = self.experts[expert_idx](expert_input)
                    output[mask] += top_k_probs[mask, k].unsqueeze(-1) * expert_output
        
        # All-to-All通信:收集结果
        # dist.all_reduce(output)
        
        return output.view(B, L, D)

八、推理框架对比

8.1 主流推理框架概览

"""
主流大模型推理框架对比:

1. vLLM
   - 开发者: UC Berkeley
   - 核心技术: PagedAttention, Continuous Batching
   - 优势: 高吞吐量, 易用性好, 社区活跃
   - 适用场景: 通用推理服务
   
2. TGI (Text Generation Inference)
   - 开发者: Hugging Face
   - 核心技术: Continuous Batching, Flash Attention, 量化
   - 优势: 与HF生态集成好, 支持模型多
   - 适用场景: HuggingFace模型部署
   
3. SGLang
   - 开发者: UC Berkeley
   - 核心技术: RadixAttention, 前端DSL
   - 优势: 复杂推理流程优化, 前缀共享
   - 适用场景: 复杂prompt, 多轮对话
   
4. TensorRT-LLM
   - 开发者: NVIDIA
   - 核心技术: TensorRT优化, 量化, 自定义kernel
   - 优势: NVIDIA GPU性能最优, 延迟最低
   - 适用场景: NVIDIA GPU专用
"""

FRAMEWORK_COMPARISON = {
    "vLLM": {
        "throughput": "高",
        "latency": "中",
        "ease_of_use": "简单",
        "model_support": "广泛",
        "quantization": ["AWQ", "GPTQ", "FP8", "INT8"],
        "distributed": ["TP", "PP"],
        "best_for": "通用推理服务",
        "install": "pip install vllm",
    },
    "TGI": {
        "throughput": "高",
        "latency": "中",
        "ease_of_use": "中等",
        "model_support": "广泛(HF模型)",
        "quantization": ["GPTQ", "AWQ", "EETQ", "bitsandbytes"],
        "distributed": ["TP"],
        "best_for": "HF模型部署",
        "install": "docker pull ghcr.io/huggingface/text-generation-inference",
    },
    "SGLang": {
        "throughput": "很高",
        "latency": "低",
        "ease_of_use": "中等",
        "model_support": "主流模型",
        "quantization": ["AWQ", "GPTQ", "FP8"],
        "distributed": ["TP", "DP"],
        "best_for": "复杂推理流程",
        "install": "pip install sglang",
    },
    "TensorRT-LLM": {
        "throughput": "很高",
        "latency": "最低",
        "ease_of_use": "复杂",
        "model_support": "主流模型",
        "quantization": ["INT8", "INT4", "FP8", "SmoothQuant"],
        "distributed": ["TP", "PP"],
        "best_for": "NVIDIA GPU极致性能",
        "install": "需要编译安装",
    },
}

def recommend_framework(requirements: dict) -> str:
    """根据需求推荐推理框架"""
    
    if requirements.get("gpu_vendor") != "nvidia":
        # 非NVIDIA GPU,排除TensorRT-LLM
        if requirements.get("priority") == "throughput":
            return "vLLM"
        return "vLLM"
    
    if requirements.get("priority") == "latency":
        return "TensorRT-LLM (最低延迟)"
    elif requirements.get("priority") == "throughput":
        if requirements.get("complex_prompts"):
            return "SGLang (复杂prompt优化)"
        return "vLLM"
    elif requirements.get("priority") == "ease_of_use":
        return "vLLM"
    elif requirements.get("hf_ecosystem"):
        return "TGI"
    
    return "vLLM (通用推荐)"

8.2 vLLM部署示例

# vLLM部署示例代码

# 方法1: 使用Python API
from vllm import LLM, SamplingParams

def vllm_serve_example():
    """vLLM Python API示例"""
    
    # 初始化模型
    llm = LLM(
        model="meta-llama/Llama-3-8B-Instruct",
        tensor_parallel_size=1,  # GPU数量
        gpu_memory_utilization=0.9,  # GPU显存利用率
        max_model_len=4096,  # 最大序列长度
        quantization="awq",  # 量化方式 (可选)
        dtype="half",  # 数据类型
    )
    
    # 设置采样参数
    sampling_params = SamplingParams(
        temperature=0.7,
        top_p=0.9,
        max_tokens=512,
        repetition_penalty=1.1,
    )
    
    # 批量推理
    prompts = [
        "什么是大语言模型?",
        "请解释量子计算的基本原理。",
        "写一首关于春天的诗。",
    ]
    
    outputs = llm.generate(prompts, sampling_params)
    
    for output in outputs:
        print(f"Prompt: {output.prompt[:50]}...")
        print(f"Generated: {output.outputs[0].text}")
        print(f"Tokens: {len(output.outputs[0].token_ids)}")
        print("---")

# 方法2: 启动API服务器
"""
# 命令行启动:
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3-8B-Instruct \
    --tensor-parallel-size 1 \
    --gpu-memory-utilization 0.9 \
    --max-model-len 4096 \
    --port 8000

# 然后可以使用OpenAI兼容的API调用:
# curl http://localhost:8000/v1/chat/completions \
#   -H "Content-Type: application/json" \
#   -d '{
#     "model": "meta-llama/Llama-3-8B-Instruct",
#     "messages": [{"role": "user", "content": "Hello!"}],
#     "max_tokens": 100
#   }'
"""

# 方法3: 使用Docker部署
"""
docker run --gpus all \
    -v ~/.cache/huggingface:/root/.cache/huggingface \
    -p 8000:8000 \
    --ipc=host \
    vllm/vllm-openai:latest \
    --model meta-llama/Llama-3-8B-Instruct \
    --tensor-parallel-size 1
"""

8.3 SGLang部署示例

# SGLang部署示例

# 方法1: Python API
import sglang as sgl

def sglang_example():
    """SGLang示例"""
    
    # 定义推理函数
    @sgl.function
    def multi_turn_chat(s, question1, question2):
        s += sgl.system("你是一个有帮助的AI助手。")
        s += sgl.user(question1)
        s += sgl.assistant(sgl.gen("answer1", max_tokens=256))
        s += sgl.user(question2)
        s += sgl.assistant(sgl.gen("answer2", max_tokens=256))
    
    # 运行
    state = multi_turn_chat.run(
        question1="什么是机器学习?",
        question2="它和深度学习有什么区别?",
    )
    
    print(state["answer1"])
    print(state["answer2"])

# 方法2: 启动API服务器
"""
python -m sglang.launch_server \
    --model-path meta-llama/Llama-3-8B-Instruct \
    --tp 1 \
    --port 30000
"""

# SGLang的RadixAttention优势:
# - 自动缓存公共前缀的KV Cache
# - 对多轮对话特别有效
# - 支持复杂的推理流程编排

九、硬件选型

9.1 GPU选型指南

"""
主流GPU推理性能对比
"""

GPU_SPECS = {
    "NVIDIA A100 80GB": {
        "memory_gb": 80,
        "bandwidth_gbps": 2048,
        "fp16_tflops": 312,
        "int8_tflops": 624,
        "price_usd": 15000,
        "interconnect": "NVLink 3.0 (600 GB/s)",
        "best_for": "训练+推理,通用选择",
    },
    "NVIDIA H100 80GB": {
        "memory_gb": 80,
        "bandwidth_gbps": 3350,
        "fp16_tflops": 990,  # 使用FP8
        "int8_tflops": 1979,
        "price_usd": 30000,
        "interconnect": "NVLink 4.0 (900 GB/s)",
        "best_for": "高性能推理,FP8支持",
    },
    "NVIDIA L40S": {
        "memory_gb": 48,
        "bandwidth_gbps": 864,
        "fp16_tflops": 362,
        "int8_tflops": 733,
        "price_usd": 7000,
        "interconnect": "PCIe Gen4",
        "best_for": "推理优化,性价比高",
    },
    "NVIDIA A10G": {
        "memory_gb": 24,
        "bandwidth_gbps": 600,
        "fp16_tflops": 125,
        "int8_tflops": 250,
        "price_usd": 2000,
        "interconnect": "PCIe Gen4",
        "best_for": "小模型推理,预算有限",
    },
}

def estimate_model_gpu_requirements(model_params_b: float, quantization: str = "fp16"):
    """估算模型需要的GPU配置"""
    
    bytes_per_param = {
        "fp32": 4, "fp16": 2, "int8": 1, "int4": 0.5, "awq4": 0.5, "gptq4": 0.5
    }
    
    model_memory = model_params_b * bytes_per_param.get(quantization, 2)
    kv_cache_memory = model_params_b * 0.1  # 粗略估计
    total_memory = model_memory + kv_cache_memory
    
    print(f"\n模型: {model_params_b}B, 量化: {quantization}")
    print(f"模型权重: {model_memory:.1f} GB")
    print(f"KV Cache估计: {kv_cache_memory:.1f} GB")
    print(f"总显存需求: {total_memory:.1f} GB")
    
    for gpu_name, specs in GPU_SPECS.items():
        num_gpus = -(-total_memory // specs["memory_gb"])  # 向上取整
        if num_gpus <= 8:  # 单机最多8卡
            cost = num_gpus * specs["price_usd"]
            print(f"  {gpu_name}: {num_gpus}卡, 成本 ${cost:,}")

# 分析不同模型的GPU需求
for model_size in [7, 13, 34, 70, 110]:
    estimate_model_gpu_requirements(model_size, "fp16")
    estimate_model_gpu_requirements(model_size, "awq4")

9.2 内存带宽分析

def memory_bandwidth_analysis():
    """内存带宽瓶颈分析"""
    
    # Decode阶段是memory-bound
    # 每生成一个token需要读取所有模型权重
    
    models = {
        "7B-FP16": 14,   # GB
        "7B-INT4": 3.5,
        "70B-FP16": 140,
        "70B-INT4": 35,
        "70B-INT8": 70,
    }
    
    gpus = {
        "A100": 2048,    # GB/s
        "H100": 3350,
        "L40S": 864,
        "4090": 1008,
    }
    
    print("=== 理论最大吞吐量 (tokens/s) ===\n")
    print(f"{'模型':<15}", end="")
    for gpu_name in gpus:
        print(f"{gpu_name:>10}", end="")
    print()
    print("-" * 55)
    
    for model_name, model_size in models.items():
        print(f"{model_name:<15}", end="")
        for gpu_name, bandwidth in gpus.items():
            max_tps = bandwidth / model_size
            print(f"{max_tps:>10.0f}", end="")
        print()

memory_bandwidth_analysis()

十、性能基准测试方法

10.1 基准测试框架

import time
import statistics
import asyncio
from dataclasses import dataclass
from typing import List, Callable
import json

@dataclass
class BenchmarkResult:
    test_name: str
    total_requests: int
    successful_requests: int
    failed_requests: int
    total_tokens: int
    total_time_s: float
    ttft_avg_ms: float      # 首token延迟(平均)
    ttft_p50_ms: float
    ttft_p99_ms: float
    tpot_avg_ms: float      # 每token延迟(平均)
    tpot_p50_ms: float
    tpot_p99_ms: float
    throughput_tps: float    # 吞吐量(tokens/s)
    requests_per_sec: float
    avg_input_tokens: float
    avg_output_tokens: float

class LLMBenchmark:
    """LLM推理基准测试框架"""
    
    def __init__(self, api_base: str, model: str):
        self.api_base = api_base
        self.model = model
        self.results = []
    
    async def single_request(self, prompt: str, max_tokens: int = 100) -> dict:
        """发送单个请求并测量延迟"""
        import aiohttp
        
        ttft = None
        tokens = []
        start_time = time.time()
        
        async with aiohttp.ClientSession() as session:
            async with session.post(
                f"{self.api_base}/v1/chat/completions",
                json={
                    "model": self.model,
                    "messages": [{"role": "user", "content": prompt}],
                    "max_tokens": max_tokens,
                    "stream": True,
                },
            ) as response:
                async for line in response.content:
                    if line.startswith(b"data: "):
                        data = line[6:].decode()
                        if data.strip() == "[DONE]":
                            break
                        
                        chunk = json.loads(data)
                        if "choices" in chunk and chunk["choices"]:
                            delta = chunk["choices"][0].get("delta", {})
                            if "content" in delta:
                                if ttft is None:
                                    ttft = (time.time() - start_time) * 1000
                                tokens.append(delta["content"])
        
        end_time = time.time()
        
        return {
            "ttft_ms": ttft or 0,
            "tpot_ms": (end_time - start_time - (ttft or 0) / 1000) / max(len(tokens), 1) * 1000,
            "total_time_ms": (end_time - start_time) * 1000,
            "num_tokens": len(tokens),
            "success": True,
        }
    
    async def run_concurrent_benchmark(
        self, 
        prompts: List[str], 
        concurrency: int = 10,
        max_tokens: int = 100,
    ) -> BenchmarkResult:
        """运行并发基准测试"""
        
        semaphore = asyncio.Semaphore(concurrency)
        results = []
        
        async def bounded_request(prompt):
            async with semaphore:
                return await self.single_request(prompt, max_tokens)
        
        start_time = time.time()
        tasks = [bounded_request(p) for p in prompts]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        total_time = time.time() - start_time
        
        # 过滤成功的请求
        successful = [r for r in results if isinstance(r, dict) and r.get("success")]
        failed = [r for r in results if not isinstance(r, dict) or not r.get("success")]
        
        # 计算统计指标
        ttft_values = [r["ttft_ms"] for r in successful]
        tpot_values = [r["tpot_ms"] for r in successful if r["tpot_ms"] > 0]
        total_tokens = sum(r["num_tokens"] for r in successful)
        
        return BenchmarkResult(
            test_name=f"concurrency_{concurrency}",
            total_requests=len(prompts),
            successful_requests=len(successful),
            failed_requests=len(failed),
            total_tokens=total_tokens,
            total_time_s=total_time,
            ttft_avg_ms=statistics.mean(ttft_values) if ttft_values else 0,
            ttft_p50_ms=statistics.median(ttft_values) if ttft_values else 0,
            ttft_p99_ms=sorted(ttft_values)[int(len(ttft_values)*0.99)] if ttft_values else 0,
            tpot_avg_ms=statistics.mean(tpot_values) if tpot_values else 0,
            tpot_p50_ms=statistics.median(tpot_values) if tpot_values else 0,
            tpot_p99_ms=sorted(tpot_values)[int(len(tpot_values)*0.99)] if tpot_values else 0,
            throughput_tps=total_tokens / total_time,
            requests_per_sec=len(successful) / total_time,
            avg_input_tokens=statistics.mean([len(p) for p in prompts]),
            avg_output_tokens=statistics.mean([r["num_tokens"] for r in successful]) if successful else 0,
        )
    
    def generate_report(self, results: List[BenchmarkResult]) -> str:
        """生成测试报告"""
        report = "# LLM推理性能基准测试报告\n\n"
        
        for result in results:
            report += f"## {result.test_name}\n\n"
            report += f"- 总请求数: {result.total_requests}\n"
            report += f"- 成功: {result.successful_requests}, 失败: {result.failed_requests}\n"
            report += f"- 总token数: {result.total_tokens}\n"
            report += f"- 总耗时: {result.total_time_s:.2f}s\n\n"
            
            report += "### 延迟指标\n"
            report += f"- TTFT (平均/P50/P99): {result.ttft_avg_ms:.0f}ms / {result.ttft_p50_ms:.0f}ms / {result.ttft_p99_ms:.0f}ms\n"
            report += f"- TPOT (平均/P50/P99): {result.tpot_avg_ms:.0f}ms / {result.tpot_p50_ms:.0f}ms / {result.tpot_p99_ms:.0f}ms\n\n"
            
            report += "### 吞吐量指标\n"
            report += f"- Token吞吐: {result.throughput_tps:.1f} tokens/s\n"
            report += f"- 请求吞吐: {result.requests_per_sec:.2f} requests/s\n\n"
        
        return report

# 使用示例
async def run_benchmark():
    benchmark = LLMBenchmark("http://localhost:8000", "llama-3-8b")
    
    # 准备测试数据
    prompts = [
        "请详细解释什么是Transformer架构。",
        "写一个Python快速排序算法。",
        "解释量子纠缠现象。",
    ] * 10  # 30个请求
    
    # 测试不同并发度
    results = []
    for concurrency in [1, 5, 10, 20]:
        result = await benchmark.run_concurrent_benchmark(
            prompts[:concurrency*3],
            concurrency=concurrency,
            max_tokens=200,
        )
        results.append(result)
    
    print(benchmark.generate_report(results))

10.2 使用标准benchmark工具

# 使用vLLM自带的benchmark
# 吞吐量测试
python -m vllm.entrypoints.openai.api_server_benchmark \
    --backend openai \
    --base-url http://localhost:8000 \
    --model meta-llama/Llama-3-8B-Instruct \
    --dataset ShareGPT \
    --num-prompts 1000 \
    --request-rate 10

# 延迟测试
python -m vllm.entrypoints.openai.api_server_benchmark \
    --backend openai \
    --base-url http://localhost:8000 \
    --model meta-llama/Llama-3-8B-Instruct \
    --dataset random \
    --input-len 128 \
    --output-len 128 \
    --num-prompts 100

十一、成本优化策略

11.1 成本分析模型

from dataclasses import dataclass

@dataclass
class CostAnalysis:
    """推理服务成本分析"""
    
    # 硬件成本
    gpu_cost_per_hour: float  # GPU租赁成本 ($/hour)
    num_gpus: int
    
    # 性能指标
    throughput_tokens_per_sec: float
    gpu_utilization: float  # 0-1
    
    # 计算成本
    @property
    def hourly_cost(self) -> float:
        return self.gpu_cost_per_hour * self.num_gpus
    
    @property
    def tokens_per_dollar(self) -> float:
        """每美元可处理的token数"""
        if self.hourly_cost == 0:
            return float('inf')
        tokens_per_hour = self.throughput_tokens_per_sec * 3600
        return tokens_per_hour / self.hourly_cost
    
    @property
    def cost_per_million_tokens(self) -> float:
        """每百万token的成本"""
        if self.tokens_per_dollar == 0:
            return float('inf')
        return 1_000_000 / self.tokens_per_dollar

def cost_optimization_analysis():
    """成本优化分析"""
    
    scenarios = [
        # 场景1: A100 + FP16
        {"name": "A100-FP16", "gpu_cost": 3.0, "gpus": 1, "tps": 30, "util": 0.7},
        # 场景2: A100 + INT4
        {"name": "A100-INT4", "gpu_cost": 3.0, "gpus": 1, "tps": 50, "util": 0.8},
        # 场景3: L40S + INT4
        {"name": "L40S-INT4", "gpu_cost": 1.5, "gpus": 1, "tps": 35, "util": 0.75},
        # 场景4: H100 + FP8
        {"name": "H100-FP8", "gpu_cost": 5.0, "gpus": 1, "tps": 80, "util": 0.85},
    ]
    
    print("=== 推理成本对比 ===\n")
    print(f"{'场景':<15} {'每小时成本':>12} {'吞吐量':>12} {'$/M tokens':>12}")
    print("-" * 55)
    
    for s in scenarios:
        analysis = CostAnalysis(
            gpu_cost_per_hour=s["gpu_cost"],
            num_gpus=s["gpus"],
            throughput_tokens_per_sec=s["tps"],
            gpu_utilization=s["util"],
        )
        print(f"{s['name']:<15} ${analysis.hourly_cost:>10.2f} "
              f"{s['tps']:>10} t/s ${analysis.cost_per_million_tokens:>10.4f}")

cost_optimization_analysis()

11.2 动态扩缩容策略

import time
from collections import deque

class AutoScaler:
    """自动扩缩容管理器"""
    
    def __init__(
        self,
        min_replicas: int = 1,
        max_replicas: int = 10,
        target_utilization: float = 0.7,
        scale_up_threshold: float = 0.8,
        scale_down_threshold: float = 0.3,
        cooldown_seconds: int = 300,
    ):
        self.min_replicas = min_replicas
        self.max_replicas = max_replicas
        self.target_utilization = target_utilization
        self.scale_up_threshold = scale_up_threshold
        self.scale_down_threshold = scale_down_threshold
        self.cooldown_seconds = cooldown_seconds
        
        self.current_replicas = min_replicas
        self.last_scale_time = 0
        self.utilization_history = deque(maxlen=60)  # 保留60个数据点
    
    def record_utilization(self, utilization: float):
        """记录当前利用率"""
        self.utilization_history.append({
            "time": time.time(),
            "utilization": utilization,
        })
    
    def should_scale(self) -> tuple:
        """
        判断是否需要扩缩容
        
        返回: (should_scale, direction, target_replicas)
        """
        if len(self.utilization_history) < 10:
            return False, None, self.current_replicas
        
        # 冷却期检查
        if time.time() - self.last_scale_time < self.cooldown_seconds:
            return False, None, self.current_replicas
        
        # 计算平均利用率
        recent = list(self.utilization_history)[-10:]
        avg_util = sum(r["utilization"] for r in recent) / len(recent)
        
        if avg_util > self.scale_up_threshold and self.current_replicas < self.max_replicas:
            # 扩容
            target = min(
                self.max_replicas,
                int(self.current_replicas * (avg_util / self.target_utilization)) + 1
            )
            return True, "up", target
        
        elif avg_util < self.scale_down_threshold and self.current_replicas > self.min_replicas:
            # 缩容
            target = max(
                self.min_replicas,
                int(self.current_replicas * (avg_util / self.target_utilization))
            )
            return True, "down", target
        
        return False, None, self.current_replicas
    
    def scale(self, target_replicas: int):
        """执行扩缩容"""
        self.current_replicas = target_replicas
        self.last_scale_time = time.time()
        print(f"扩缩容: {self.current_replicas} -> {target_replicas} replicas")

十二、实战案例:高性能推理服务搭建

12.1 完整的vLLM部署方案

"""
完整的vLLM推理服务部署方案

包含:
1. 模型加载与优化配置
2. API服务搭建
3. 监控与日志
4. 负载均衡
5. 成本优化
"""

# docker-compose.yml
DOCKER_COMPOSE = """
version: '3.8'

services:
  vllm-server:
    image: vllm/vllm-openai:latest
    runtime: nvidia
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    ports:
      - "8000:8000"
    ipc: host
    command: >
      --model meta-llama/Llama-3-8B-Instruct
      --tensor-parallel-size 1
      --gpu-memory-utilization 0.9
      --max-model-len 4096
      --quantization awq
      --dtype half
      --enable-prefix-caching
      --max-num-seqs 32
      --port 8000
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

  nginx:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - vllm-server

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
"""

# nginx.conf
NGINX_CONF = """
events {
    worker_connections 1024;
}

http {
    upstream vllm {
        server vllm-server:8000;
    }

    server {
        listen 80;
        
        location /v1/ {
            proxy_pass http://vllm;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_read_timeout 300s;
            proxy_send_timeout 300s;
        }
        
        location /health {
            proxy_pass http://vllm/health;
        }
    }
}
"""

# 监控脚本
import psutil
import time

class InferenceMonitor:
    """推理服务监控"""
    
    def __init__(self, api_base: str):
        self.api_base = api_base
        self.metrics_history = []
    
    def collect_metrics(self) -> dict:
        """收集系统指标"""
        import requests
        
        metrics = {
            "timestamp": time.time(),
            "cpu_percent": psutil.cpu_percent(),
            "memory_percent": psutil.virtual_memory().percent,
        }
        
        # GPU指标(需要nvidia-smi或pynvml)
        try:
            import pynvml
            pynvml.nvmlInit()
            handle = pynvml.nvmlDeviceGetHandleByIndex(0)
            gpu_info = pynvml.nvmlDeviceGetUtilizationRates(handle)
            gpu_memory = pynvml.nvmlDeviceGetMemoryInfo(handle)
            
            metrics["gpu_utilization"] = gpu_info.gpu
            metrics["gpu_memory_used_gb"] = gpu_memory.used / 1e9
            metrics["gpu_memory_total_gb"] = gpu_memory.total / 1e9
        except:
            pass
        
        # API健康检查
        try:
            response = requests.get(f"{self.api_base}/health", timeout=5)
            metrics["api_status"] = "healthy" if response.status_code == 200 else "unhealthy"
        except:
            metrics["api_status"] = "unreachable"
        
        self.metrics_history.append(metrics)
        return metrics
    
    def check_alerts(self, metrics: dict) -> list:
        """检查告警条件"""
        alerts = []
        
        if metrics.get("gpu_utilization", 0) > 95:
            alerts.append("GPU利用率过高")
        
        if metrics.get("gpu_memory_used_gb", 0) / metrics.get("gpu_memory_total_gb", 1) > 0.95:
            alerts.append("GPU显存接近上限")
        
        if metrics.get("api_status") != "healthy":
            alerts.append("API服务异常")
        
        return alerts

12.2 性能调优检查清单

PERFORMANCE_CHECKLIST = """
# 大模型推理性能调优检查清单

## 模型层面
- [ ] 选择合适的量化方式(AWQ/GPTQ/INT8)
- [ ] 使用GQA/MQA减少KV Cache
- [ ] 启用Flash Attention
- [ ] 选择合适的精度(FP16/BF16/FP8)

## 系统层面
- [ ] 启用PagedAttention(vLLM默认开启)
- [ ] 配置合适的max_num_seqs
- [ ] 启用Prefix Caching(多轮对话场景)
- [ ] 使用Continuous Batching
- [ ] 调整gpu_memory_utilization

## 部署层面
- [ ] 使用Tensor Parallelism充分利用多GPU
- [ ] 配置负载均衡
- [ ] 设置合理的超时时间
- [ ] 启用请求队列

## 监控层面
- [ ] 监控GPU利用率和显存
- [ ] 监控TTFT和TPOT
- [ ] 监控请求队列长度
- [ ] 设置告警阈值

## 成本优化
- [ ] 选择性价比最优的GPU
- [ ] 使用量化降低硬件需求
- [ ] 实施动态扩缩容
- [ ] 监控和优化GPU利用率
"""

print(PERFORMANCE_CHECKLIST)

十三、常见问题解答

Q1: 如何选择量化方案?

A: 量化方案选择建议:

  • AWQ: 推理速度快,质量损失小,推荐首选
  • GPTQ: 质量稍好,速度略慢
  • INT8: 几乎无损,但内存节省有限
  • GGUF: 适合CPU推理和边缘设备

Q2: 投机解码的加速比一般是多少?

A: 投机解码的加速比取决于:

  • Draft模型与Target模型的一致性:一致性越高,接受率越高
  • Draft模型的速度:越快越好
  • 典型加速比:1.5x - 3x

Q3: vLLM和TGI如何选择?

A:

  • vLLM: 通用场景推荐,吞吐量高,API兼容性好
  • TGI: 使用HuggingFace模型时推荐,与HF生态集成好

Q4: 如何估算需要多少GPU?

A:

  1. 计算模型权重大小:参数量 × 每参数字节数
  2. 加上KV Cache开销:约占模型大小的10-20%
  3. 除以单GPU显存容量
  4. 留出10-15%余量

Q5: 多轮对话如何优化?

A:

  • 启用Prefix Caching缓存system prompt
  • 使用KV Cache避免重复计算
  • 合理设置max_model_len

十四、总结

大模型推理优化是一个系统工程,需要从模型、算法、系统、硬件等多个层面综合考虑。关键要点:

  1. 量化是基础:选择合适的量化方案可以大幅降低硬件需求和成本
  2. KV Cache是关键:PagedAttention和Prefix Caching是提升效率的核心技术
  3. 批处理是核心:Continuous Batching充分利用GPU计算资源
  4. 投机解码是加速器:在不损失质量的前提下提升速度
  5. 并行是扩展手段:Tensor/Pipeline/Expert Parallelism支持更大模型
  6. 监控是保障:持续监控性能指标,及时发现和解决问题

选择合适的优化策略组合,可以将推理成本降低数倍甚至数十倍,同时保持良好的用户体验。


本教程内容持续更新中,欢迎反馈和建议。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录