大模型推理服务的批处理与动态 Batch 调度:从逐条推理到吞吐量优化

cover

一、推理服务的吞吐量瓶颈:GPU 利用率的隐性浪费

大模型推理服务的核心瓶颈不是单次请求的延迟,而是 GPU 利用率。在逐条推理模式下,每个请求独立送入 GPU 计算,Batch Size 为 1,GPU 的并行计算能力大量闲置。以 A100 GPU 为例,Batch Size 1 时 GPU 利用率通常不足 20%,而 Batch Size 8-16 时可提升至 70% 以上。

批处理(Batching)是提升推理吞吐量的关键手段:将多个请求合并为一个 Batch 送入 GPU,利用矩阵运算的并行性摊薄计算开销。但批处理的引入带来新的挑战:请求到达时间不同,需要等待凑批,增加了尾延迟;Batch 内请求的序列长度差异导致 Padding 浪费;动态流量下 Batch Size 的最优值持续变化。动态 Batch 调度系统需要在吞吐量与延迟之间找到动态平衡点。

二、动态 Batch 调度的核心机制

flowchart TD
    A[请求到达] --> B[请求队列]
    B --> C[Batch 调度器]
    C --> D{调度策略}
    D -->|时间窗口到期| E[组装当前队列所有请求]
    D -->|队列长度达到上限| F[组装最大 Batch]
    D -->|最长等待超时| G[立即发送已有请求]

    E --> H[Padding 与注意力掩码]
    F --> H
    G --> H
    H --> I[GPU 推理]
    I --> J[结果拆分与返回]

    subgraph 连续批处理 Continuous Batching
        K[迭代级调度]
        K --> L[每步重新评估 Batch]
        L --> M[已完成序列让出位置]
        M --> N[新请求填入空位]
    end

    C --> K

静态批处理(Static Batching)在凑满 Batch 后一次性推理,Batch 内所有序列必须等待最长序列完成才能返回。连续批处理(Continuous Batching)在每次迭代(生成一个 Token)后重新评估 Batch:已完成的序列立即让出位置,新请求填入空位,显著降低平均等待时间。

三、工程实现:基于 vLLM 的动态 Batch 推理服务

# dynamic_batch_server.py — 动态 Batch 推理服务
from fastapi import FastAPI
from pydantic import BaseModel
import asyncio
import time
from typing import List, Optional
from vllm import LLM, SamplingParams
from vllm.entrypoints.openai.api_server import init_app

app = FastAPI()

class InferenceRequest(BaseModel):
    prompt: str
    max_tokens: int = 256
    temperature: float = 0.7
    top_p: float = 0.9
    priority: int = 0  # 0=普通, 1=高优先级

class InferenceResponse(BaseModel):
    text: str
    tokens: int
    latency_ms: float

# vLLM 引擎初始化:启用 Continuous Batching
llm = LLM(
    model="Qwen/Qwen2.5-72B-Instruct",
    tensor_parallel_size=2,        # 2 卡并行
    max_num_seqs=64,               # 最大并发序列数
    max_num_batched_tokens=32768,  # 单 Batch 最大 Token 数
    gpu_memory_utilization=0.90,   # GPU 显存利用率上限
    enable_prefix_caching=True,    # 启用前缀缓存,复用 System Prompt
)

# 推理端点:vLLM 内部自动管理 Batch 调度
@app.post("/v1/completions", response_model=InferenceResponse)
async def completions(request: InferenceRequest):
    start_time = time.monotonic()

    sampling_params = SamplingParams(
        max_tokens=request.max_tokens,
        temperature=request.temperature,
        top_p=request.top_p,
    )

    # vLLM 的 add_request 自动进入 Continuous Batching 队列
    outputs = llm.generate(
        prompts=[request.prompt],
        sampling_params=sampling_params,
        use_tqdm=False,
    )

    latency_ms = (time.monotonic() - start_time) * 1000
    output = outputs[0]

    return InferenceResponse(
        text=output.outputs[0].text,
        tokens=len(output.outputs[0].token_ids),
        latency_ms=latency_ms,
    )
# batch_metrics.py — Batch 调度指标监控
from prometheus_client import Histogram, Gauge, Counter
import time
from functools import wraps

# 核心指标定义
BATCH_SIZE = Histogram(
    'inference_batch_size',
    'Batch 大小分布',
    buckets=[1, 2, 4, 8, 16, 32, 64]
)
QUEUE_LENGTH = Gauge(
    'inference_queue_length',
    '等待队列中的请求数'
)
INFERENCE_LATENCY = Histogram(
    'inference_latency_ms',
    '推理延迟分布',
    buckets=[50, 100, 200, 500, 1000, 2000, 5000]
)
GPU_UTILIZATION = Gauge(
    'gpu_utilization_percent',
    'GPU 利用率'
)
TOKENS_PER_SECOND = Gauge(
    'inference_tokens_per_second',
    '每秒生成 Token 数'
)

def track_batch_metrics(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        # 记录队列长度
        QUEUE_LENGTH.set(get_current_queue_length())

        start = time.monotonic()
        result = await func(*args, **kwargs)
        latency = (time.monotonic() - start) * 1000

        INFERENCE_LATENCY.observe(latency)
        return result
    return wrapper

# 动态 Batch Size 调优:基于延迟 SLO 调整最大 Batch
class AdaptiveBatchController:
    def __init__(self, latency_slo_ms: float = 500):
        self.latency_slo_ms = latency_slo_ms
        self.max_batch_size = 16
        self.min_batch_size = 1

    def adjust(self, current_p99_latency: float):
        """根据 P99 延迟动态调整最大 Batch Size"""
        if current_p99_latency > self.latency_slo_ms * 1.5:
            # 延迟超标,减小 Batch Size
            self.max_batch_size = max(
                self.min_batch_size,
                self.max_batch_size - 2
            )
        elif current_p99_latency < self.latency_slo_ms * 0.5:
            # 延迟充裕,增大 Batch Size 提升吞吐
            self.max_batch_size = min(64, self.max_batch_size + 2)

四、动态 Batch 调度的边界与权衡

Padding 浪费:Batch 内不同序列长度差异导致短序列需要 Padding 到最长序列的长度,浪费计算资源。vLLM 的 PagedAttention 通过虚拟内存管理减少 Padding 浪费,但极端长度差异(1:10 以上)仍会显著影响有效吞吐。建议对请求按长度分桶,相似长度的请求组成同一 Batch。

凑批延迟:等待凑批的时间增加了请求的尾延迟。在低流量时段,凑批等待可能成为延迟的主要来源。解决方案是设置最大等待时间(如 50ms),超时后即使 Batch 未满也立即发送,避免无限等待。

KV Cache 显存竞争:Batch 内所有序列的 KV Cache 共享 GPU 显存,长序列消耗大量显存,可能挤占其他序列的空间。vLLM 的 PagedAttention 按需分配 KV Cache 页,但显存不足时仍需拒绝新请求。需根据模型大小与 GPU 显存合理设置 max_num_seqs

流式输出的兼容性:Continuous Batching 与流式输出(SSE)存在天然矛盾:流式要求 Token 级别的即时返回,而 Batching 需要等待整个迭代步完成。vLLM 通过迭代级调度解决了这一问题——每个 Token 生成后立即返回,同时维护 Batch 状态。

五、总结

动态 Batch 调度是大模型推理服务吞吐量优化的核心手段。Continuous Batching 通过迭代级调度实现"完成即让位",显著降低平均等待时间。工程落地的关键在于:PagedAttention 减少 Padding 浪费、自适应 Batch Size 控制器基于延迟 SLO 动态调优、最大等待时间避免凑批延迟、按长度分桶优化 Batch 内序列长度一致性。推理服务的优化不是单一维度的调参,而是在吞吐量、延迟与显存利用率三个目标间的持续权衡。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐