批次處理與連續批次:提升吞吐的關鍵

從靜態批次、動態批次到連續批次,拆解調度策略、批次大小取捨,並用 Python 實作一個連續批次系統。

批次處理與連續批次:提升吞吐的關鍵

上一篇我們談了 vLLM 與 PagedAttention,理解了它們如何解決 KV Cache 的記憶體碎片問題,讓記憶體利用率從 20-40% 提升到 90% 以上。

但 PagedAttention 只是解決了「記憶體怎麼放」的問題,還沒有解決「請求怎麼排」的問題。

如果調度策略不對,即使記憶體利用率再高,GPU 仍然會閒置,吞吐仍然上不去。

這一篇,我們要深入批次處理的核心:靜態批次、動態批次、連續批次,理解它們的差異、設計高效的調度策略、平衡吞吐與延遲,並用 Python 實作一個連續批次系統。

Static Batching → Dynamic Batching → Continuous Batching

一、為什麼需要批次處理?

單一請求的 GPU 利用率很低

當 GPU 只處理一個請求時,大部分算力都在閒置。

以一個 7B 模型為例:

指標單一請求批次 32
GPU 算力利用率15%85%
記憶體頻寬利用率30%90%
每秒生成 token 數30600
每 token 成本$0.001$0.00005

單一請求時,GPU 大部分時間在等記憶體讀取,而不是在計算。
批次處理讓多個請求同時計算,把 GPU 餵飽。

批次處理的核心權衡

吞吐 ↑  →  批次 ↑  →  延遲 ↑

批次越大,吞吐越高,但每個請求的等待時間也越長。
這是因為:

  1. 排隊延遲:請求要等批次湊滿才能開始。
  2. 計算延遲:批次越大,每個步驟的計算時間越長。
  3. 同步延遲:批次內所有請求要一起完成。

什麼時候該用批次?

場景適合批次?原因
離線批次處理非常適合沒有延遲要求
大量相似請求適合批次效率高
即時對話需謹慎延遲敏感
單一互動不適合沒有其他請求可批次

二、靜態批次:最簡單的做法

運作方式

靜態批次是最直覺的做法:

1. 收集 N 個請求
2. 一起送入模型
3. 等待所有請求完成
4. 收集下一批 N 個請求

用程式碼表示:

def static_batching(requests: list, batch_size: int, model):
    """靜態批次處理"""
    results = []

    for i in range(0, len(requests), batch_size):
        batch = requests[i:i + batch_size]

        # 把批次內所有請求一起送入模型
        batch_results = model.generate(batch)

        results.extend(batch_results)

    return results

靜態批次的問題

問題一:短請求等長請求

假設批次內有兩個請求:

  • 請求 A:生成 10 個 token 就完成
  • 請求 B:生成 500 個 token 才完成

請求 A 完成後,它的 GPU 資源被閒置,但要等請求 B 完成才能開始下一批。

時間軸:
[請求 A: 10 tokens][閒置 490 tokens 的時間]
[請求 B: 500 tokens]

問題二:新請求要等

新請求進來時,必須等當前批次全部完成才能加入。

時間軸:
[批次 1: 500 tokens 的時間]
                          新請求只能在這裡加入
[批次 2: ...]

問題三:批次大小難以決定

  • 批次太小:GPU 利用率低。
  • 批次太大:延遲高,且可能 OOM。
  • 請求長度不一:很難找到最佳批次大小。

靜態批次的適用場景

靜態批次適合:

  • 離線批次處理:沒有延遲要求。
  • 請求長度相近:如固定格式的摘要任務。
  • 簡單實作:不需要複雜的調度邏輯。

Short Request Waits for Long Request

三、動態批次:按需調整

運作方式

動態批次改善了靜態批次的問題:

1. 維護一個請求佇列
2. 當 GPU 空閒時,從佇列取出請求
3. 動態決定批次大小(根據請求數量和長度)
4. 請求完成就移出,新請求可以加入

用程式碼表示:

class DynamicBatcher:
    """動態批次處理器"""

    def __init__(self, model, max_batch_size: int = 32,
                 max_wait_time: float = 0.1):
        self.model = model
        self.max_batch_size = max_batch_size
        self.max_wait_time = max_wait_time
        self.queue = []
        self.running = True

    def add_request(self, request):
        """加入請求到佇列"""
        self.queue.append(request)

    def process(self):
        """處理佇列中的請求"""
        while self.running:
            if not self.queue:
                time.sleep(0.001)
                continue

            # 取出最多 max_batch_size 個請求
            batch = self.queue[:self.max_batch_size]
            self.queue = self.queue[self.max_batch_size:]

            # 處理批次
            self._process_batch(batch)

    def _process_batch(self, batch):
        """處理一個批次"""
        # 可以根據請求長度動態調整
        results = self.model.generate(batch)
        # 回傳結果...

動態批次的改進

面向靜態批次動態批次
批次大小固定動態
新請求加入等批次完成隨時可加入
短請求等長請求完成就移出
GPU 利用率較高
實作複雜度

動態批次仍然存在的問題

動態批次雖然改善了靜態批次的問題,但仍有局限:

  1. 等待批次湊滿:如果請求來得慢,要等一段時間才能湊成批次。
  2. 批次內的同步:批次內所有請求要一起完成,短請求仍要等長請求。
  3. 沒有真正解決根本問題:只是把「等整個批次」變成「等批次內最長的請求」。

四、連續批次:真正的解法

核心思想

連續批次(Continuous Batching)的核心思想是:

不再以「批次」為單位,而是以「步驟」為單位。每個步驟都可以加入新請求、移出完成的請求。

打個比方:

  • 靜態批次像一台巴士,要等所有人上車才發車,到了終點才讓所有人下車。
  • 動態批次像一台巴士,可以隨時上車,但還是要等所有人到終點。
  • 連續批次像一台巴士,每個站都可以上下車,乘客到站就下車,新乘客隨時上車。

具體運作方式

步驟 1:請求 A、B、C、D 同時處理
步驟 2:A 完成,移出;請求 E 加入
步驟 3:B 完成,移出;請求 F 加入
步驟 4:C 完成,移出;請求 G 加入
...

每個步驟只生成一個 token,所以:

  1. 完成的請求可以立即移出,釋放資源。
  2. 新請求可以立即加入,不需要等待。

連續批次的關鍵

連續批次能運作,需要兩個關鍵技術:

  1. PagedAttention:讓 KV Cache 可以動態分配和釋放。
  2. 迭代層級的調度:在每個解碼步驟之間,重新決定批次組成。

用程式碼理解

class ContinuousBatcher:
    """連續批次處理器"""

    def __init__(self, model, max_batch_size: int = 32):
        self.model = model
        self.max_batch_size = max_batch_size
        self.active_requests = []  # 當前處理中的請求
        self.waiting_queue = []    # 等待加入的請求

    def add_request(self, request):
        """加入請求"""
        self.waiting_queue.append(request)

    def step(self):
        """執行一個解碼步驟"""
        # 1. 移出完成的請求
        self.active_requests = [
            r for r in self.active_requests
            if not r.is_finished()
        ]

        # 2. 加入新請求(如果還有空間)
        while (len(self.active_requests) < self.max_batch_size
               and self.waiting_queue):
            new_request = self.waiting_queue.pop(0)
            self.active_requests.append(new_request)

        # 3. 為所有活躍請求生成一個 token
        if self.active_requests:
            self.model.decode_step(self.active_requests)

    def run(self):
        """持續執行直到所有請求完成"""
        while self.active_requests or self.waiting_queue:
            self.step()

連續批次 vs 靜態批次

面向靜態批次動態批次連續批次
批次單位整個批次整個批次單一步驟
新請求加入等批次完成等批次完成隨時加入
完成請求移出等批次完成等批次完成立即移出
GPU 閒置極少
吞吐
延遲
實作複雜度

Static Batching vs Continuous Batching

五、連續批次的調度策略

連續批次的核心是調度策略:每個步驟要選擇哪些請求進入批次?

策略一:先來先服務(FIFO)

最簡單的策略:按照請求到達的順序處理。

def fifo_scheduling(waiting_queue, max_batch_size):
    """先來先服務"""
    return waiting_queue[:max_batch_size]

優點:公平、簡單。
缺點:長請求會阻塞短請求。

策略二:最短作業優先(SJF)

優先處理預期輸出較短的請求。

def sjf_scheduling(waiting_queue, max_batch_size):
    """最短作業優先"""
    # 根據預期輸出長度排序
    sorted_queue = sorted(
        waiting_queue,
        key=lambda r: r.expected_output_length,
    )
    return sorted_queue[:max_batch_size]

優點:降低平均延遲。
缺點:長請求可能永遠等不到。

策略三:優先級調度

根據請求的優先級決定。

def priority_scheduling(waiting_queue, max_batch_size):
    """優先級調度"""
    sorted_queue = sorted(
        waiting_queue,
        key=lambda r: -r.priority,  # 高優先級在前
    )
    return sorted_queue[:max_batch_size]

優點:可以區分不同等級的服務。
缺點:低優先級請求可能餓死。

策略四:公平分享(Fair Sharing)

確保每個使用者或每個請求都獲得公平的資源。

def fair_scheduling(waiting_queue, max_batch_size, active_requests):
    """公平分享"""
    # 統計每個使用者的活躍請求數
    user_counts = {}
    for r in active_requests:
        user_counts[r.user_id] = user_counts.get(r.user_id, 0) + 1

    # 優先選擇活躍請求少的使用者
    sorted_queue = sorted(
        waiting_queue,
        key=lambda r: user_counts.get(r.user_id, 0),
    )
    return sorted_queue[:max_batch_size]

優點:避免單一使用者獨占資源。
缺點:實作較複雜。

策略五:混合策略

實務上,通常會組合多種策略:

def hybrid_scheduling(waiting_queue, max_batch_size, active_requests):
    """混合策略:先按優先級,再按等待時間"""
    sorted_queue = sorted(
        waiting_queue,
        key=lambda r: (-r.priority, r.arrival_time),
    )
    return sorted_queue[:max_batch_size]

調度策略的權衡

策略吞吐延遲公平性實作複雜度
FIFO
SJF低(平均)
優先級
公平分享
混合

六、批次大小的取捨

批次大小是連續批次中最重要的參數之一。

批次大小的影響

批次大小吞吐延遲GPU 利用率記憶體
1
8
32中高中高
64很高很高
128最高很高最高很高

如何找到最佳批次大小?

def find_optimal_batch_size(model, test_requests):
    """找到最佳批次大小"""
    results = []

    for batch_size in [1, 2, 4, 8, 16, 32, 64, 128]:
        # 測試這個批次大小
        throughput, latency = benchmark(
            model, test_requests, batch_size
        )

        results.append({
            "batch_size": batch_size,
            "throughput": throughput,
            "latency": latency,
            "efficiency": throughput / latency,
        })

    # 選擇效率最高的
    best = max(results, key=lambda r: r["efficiency"])
    return best

經驗法則

場景建議批次大小
延遲敏感(即時對話)1-8
平衡(一般服務)8-32
吞吐優先(批次處理)32-128
記憶體受限根據可用記憶體計算

記憶體限制下的批次大小

def calculate_max_batch_size(
    gpu_memory_gb: float,
    model_size_gb: float,
    kv_cache_per_token_mb: float,
    avg_seq_length: int,
) -> int:
    """計算最大批次大小"""
    # 可用於 KV Cache 的記憶體
    available_for_kv = gpu_memory_gb - model_size_gb

    # 每個請求的 KV Cache 大小
    kv_per_request_mb = kv_cache_per_token_mb * avg_seq_length

    # 最大批次大小
    max_batch = int(available_for_kv * 1024 / kv_per_request_mb)

    return max(1, max_batch)


# 範例
max_batch = calculate_max_batch_size(
    gpu_memory_gb=80,
    model_size_gb=14,  # 7B FP16
    kv_cache_per_token_mb=0.5,
    avg_seq_length=512,
)
print(f"最大批次大小:{max_batch}")  # 約 264

七、實作:完整的連續批次系統

讓我們實作一個完整的連續批次系統。

import time
import asyncio
from dataclasses import dataclass, field
from enum import Enum
from collections import deque


class RequestStatus(Enum):
    WAITING = "waiting"
    RUNNING = "running"
    FINISHED = "finished"


@dataclass
class Request:
    """請求"""
    id: str
    prompt: str
    max_tokens: int = 100
    temperature: float = 0.7
    priority: int = 0
    arrival_time: float = field(default_factory=time.time)

    # 狀態
    status: RequestStatus = RequestStatus.WAITING
    generated_tokens: list = field(default_factory=list)
    output_text: str = ""

    def is_finished(self) -> bool:
        """是否完成"""
        return (
            self.status == RequestStatus.FINISHED or
            len(self.generated_tokens) >= self.max_tokens
        )

    @property
    def output_length(self) -> int:
        """已生成的長度"""
        return len(self.generated_tokens)


class ContinuousBatchEngine:
    """連續批次引擎"""

    def __init__(
        self,
        model,
        tokenizer,
        max_batch_size: int = 32,
        max_waiting: int = 256,
        scheduling_policy: str = "fifo",
    ):
        self.model = model
        self.tokenizer = tokenizer
        self.max_batch_size = max_batch_size
        self.max_waiting = max_waiting
        self.scheduling_policy = scheduling_policy

        # 請求佇列
        self.waiting = deque()
        self.running = []
        self.finished = []

        # 統計
        self.stats = {
            "total_requests": 0,
            "total_tokens": 0,
            "total_steps": 0,
            "start_time": None,
        }

        self._running = False

    def add_request(self, request: Request):
        """加入請求"""
        if len(self.waiting) >= self.max_waiting:
            raise RuntimeError("等待佇列已滿")

        self.waiting.append(request)
        self.stats["total_requests"] += 1

    def _schedule(self) -> list:
        """調度:選擇要進入批次的請求"""
        available_slots = self.max_batch_size - len(self.running)
        if available_slots <= 0 or not self.waiting:
            return []

        # 根據策略排序
        if self.scheduling_policy == "fifo":
            selected = list(self.waiting)[:available_slots]
        elif self.scheduling_policy == "sjf":
            sorted_waiting = sorted(
                self.waiting,
                key=lambda r: r.max_tokens,
            )
            selected = sorted_waiting[:available_slots]
        elif self.scheduling_policy == "priority":
            sorted_waiting = sorted(
                self.waiting,
                key=lambda r: (-r.priority, r.arrival_time),
            )
            selected = sorted_waiting[:available_slots]
        else:
            selected = list(self.waiting)[:available_slots]

        # 從等待佇列移除
        for req in selected:
            self.waiting.remove(req)
            req.status = RequestStatus.RUNNING

        return selected

    def step(self) -> dict:
        """執行一個解碼步驟"""
        # 1. 移出完成的請求
        completed = []
        self.running = [
            r for r in self.running
            if not r.is_finished()
        ]

        for req in list(self.running):
            if req.is_finished():
                req.status = RequestStatus.FINISHED
                completed.append(req)
                self.finished.append(req)
                self.running.remove(req)

        # 2. 調度新請求
        new_requests = self._schedule()
        self.running.extend(new_requests)

        # 3. 為所有活躍請求生成一個 token
        if self.running:
            self._decode_step()

        self.stats["total_steps"] += 1

        return {
            "completed": [r.id for r in completed],
            "running": len(self.running),
            "waiting": len(self.waiting),
            "new": [r.id for r in new_requests],
        }

    def _decode_step(self):
        """執行一步解碼"""
        # 這裡是簡化的實作
        # 實際應該呼叫模型的 decode 方法
        for req in self.running:
            # 模擬生成一個 token
            token = self._generate_token(req)
            req.generated_tokens.append(token)
            self.stats["total_tokens"] += 1

    def _generate_token(self, req: Request) -> str:
        """生成一個 token(模擬)"""
        # 實際應該呼叫模型
        return f"token_{len(req.generated_tokens)}"

    def run_until_complete(self, timeout: float = None) -> dict:
        """執行直到所有請求完成"""
        self.stats["start_time"] = time.time()
        self._running = True

        start = time.time()
        while self._running:
            # 檢查是否全部完成
            if not self.running and not self.waiting:
                break

            # 檢查超時
            if timeout and (time.time() - start) > timeout:
                break

            # 執行一步
            self.step()

        self._running = False

        return self.get_stats()

    def get_stats(self) -> dict:
        """取得統計"""
        elapsed = time.time() - self.stats["start_time"] if self.stats["start_time"] else 0

        return {
            "total_requests": self.stats["total_requests"],
            "total_tokens": self.stats["total_tokens"],
            "total_steps": self.stats["total_steps"],
            "elapsed_seconds": round(elapsed, 3),
            "throughput_tokens_per_sec": (
                round(self.stats["total_tokens"] / elapsed, 2)
                if elapsed > 0 else 0
            ),
            "completed": len(self.finished),
            "avg_tokens_per_request": (
                round(self.stats["total_tokens"] / len(self.finished), 2)
                if self.finished else 0
            ),
        }


# ========== 使用範例 ==========

def demo_continuous_batching():
    """示範連續批次"""
    engine = ContinuousBatchEngine(
        model=None,  # 模擬
        tokenizer=None,
        max_batch_size=4,
        scheduling_policy="fifo",
    )

    # 加入不同長度的請求
    requests = [
        Request(id=f"req_{i}", prompt=f"問題 {i}", max_tokens=10 + i * 5)
        for i in range(10)
    ]

    for req in requests:
        engine.add_request(req)

    # 執行
    stats = engine.run_until_complete()

    print("連續批次統計:")
    for key, value in stats.items():
        print(f"  {key}: {value}")


if __name__ == "__main__":
    demo_continuous_batching()

與靜態批次的比較

def compare_batching_strategies():
    """比較不同批次策略"""
    import matplotlib.pyplot as plt

    strategies = ["static", "dynamic", "continuous"]
    throughputs = []
    latencies = []

    for strategy in strategies:
        if strategy == "static":
            throughput, latency = benchmark_static()
        elif strategy == "dynamic":
            throughput, latency = benchmark_dynamic()
        else:
            throughput, latency = benchmark_continuous()

        throughputs.append(throughput)
        latencies.append(latency)

    # 繪圖
    fig, ax1 = plt.subplots(figsize=(10, 6))

    x = range(len(strategies))
    ax1.bar(x, throughputs, color="blue", alpha=0.7, label="吞吐")
    ax1.set_ylabel("吞吐 (tokens/s)", color="blue")

    ax2 = ax1.twinx()
    ax2.plot(x, latencies, color="red", marker="o", label="延遲")
    ax2.set_ylabel("延遲 (ms)", color="red")

    ax1.set_xticks(x)
    ax1.set_xticklabels(strategies)

    plt.title("批次策略比較")
    plt.tight_layout()
    plt.savefig("batching_comparison.png")

八、與 vLLM 的整合

vLLM 已經內建了連續批次,我們只需要正確設定參數。

關鍵參數

from vllm import LLM, SamplingParams

llm = LLM(
    model="meta-llama/Llama-2-7b-hf",

    # 批次相關
    max_num_seqs=256,          # 最大同時處理的請求數
    max_num_batched_tokens=4096,  # 每個批次的最大 token 數

    # 記憶體相關
    gpu_memory_utilization=0.9,  # GPU 記憶體使用率
    swap_space=4,              # CPU swap 空間(GB)

    # 調度相關
    scheduling_policy="fcfs",  # 調度策略
    enable_prefix_caching=True,  # 啟用前綴快取
)

參數的意義

參數說明建議值
max_num_seqs最大批次大小根據 GPU 記憶體調整
max_num_batched_tokens每個批次的最大 token 數4096-16384
gpu_memory_utilizationGPU 記憶體使用率0.85-0.95
swap_spaceCPU swap 空間4-16 GB
scheduling_policy調度策略fcfs / priority
enable_prefix_caching前綴快取True

監控連續批次的行為

def monitor_vllm_batching(llm):
    """監控 vLLM 的批次行為"""
    # 取得引擎統計
    engine = llm.llm_engine
    stats = engine.statistics

    return {
        "running_requests": stats.num_running,
        "pending_requests": stats.num_pending,
        "swapped_requests": stats.num_swapped,
        "gpu_cache_usage": stats.gpu_cache_usage,
        "cpu_cache_usage": stats.cpu_cache_usage,
        "avg_prompt_throughput": stats.avg_prompt_throughput,
        "avg_generation_throughput": stats.avg_generation_throughput,
    }

調整批次大小

# 延遲敏感
llm_low_latency = LLM(
    model="meta-llama/Llama-2-7b-hf",
    max_num_seqs=8,  # 小批次
)

# 吞吐優先
llm_high_throughput = LLM(
    model="meta-llama/Llama-2-7b-hf",
    max_num_seqs=256,  # 大批次
)

九、最佳實踐

1. 選擇合適的調度策略

# 一般服務:FIFO
scheduling_policy = "fifo"

# 需要區分優先級:priority
scheduling_policy = "priority"

# 平均延遲優先:sjf
scheduling_policy = "sjf"

2. 動態調整批次大小

def adaptive_batch_size(current_queue_size: int, gpu_usage: float) -> int:
    """動態調整批次大小"""
    if gpu_usage > 0.9:
        return 16  # GPU 快滿了,降低批次
    elif current_queue_size > 100:
        return 64  # 佇列很長,提高批次
    elif current_queue_size > 20:
        return 32
    else:
        return 8

3. 設定合理的 max_num_seqs

# 根據 GPU 記憶體計算
def calculate_max_seqs(
    gpu_memory_gb: float,
    model_size_gb: float,
    avg_seq_length: int,
    kv_per_token_mb: float = 0.5,
) -> int:
    """計算 max_num_seqs"""
    available = gpu_memory_gb - model_size_gb
    kv_per_seq = kv_per_token_mb * avg_seq_length
    return int(available * 1024 / kv_per_seq)

4. 監控關鍵指標

def monitor_batching(llm):
    """監控批次指標"""
    stats = llm.llm_engine.statistics

    return {
        "batch_size": stats.num_running,
        "queue_length": stats.num_pending,
        "gpu_utilization": stats.gpu_cache_usage,
        "throughput": stats.avg_generation_throughput,
    }

5. 處理突發流量

# 用佇列吸收突發流量
class RequestQueue:
    def __init__(self, max_size: int = 1000):
        self.queue = deque(maxlen=max_size)
        self.rejected = 0

    def add(self, request):
        if len(self.queue) >= self.queue.maxlen:
            self.rejected += 1
            return False
        self.queue.append(request)
        return True

6. 設定超時

# 避免請求永遠等待
def process_with_timeout(engine, timeout: float = 30.0):
    """帶超時的處理"""
    start = time.time()

    while not engine.is_complete():
        if time.time() - start > timeout:
            engine.cancel_all()
            break

        engine.step()

7. 使用前綴快取

# 多個請求有相同前綴時,大幅節省計算
llm = LLM(
    model="meta-llama/Llama-2-7b-hf",
    enable_prefix_caching=True,
)

8. 測試不同配置

def benchmark_configs(model, configs: list):
    """測試不同配置"""
    results = []

    for config in configs:
        llm = LLM(model=model, **config)
        throughput, latency = benchmark(llm)
        results.append({
            "config": config,
            "throughput": throughput,
            "latency": latency,
        })

    return sorted(results, key=lambda r: -r["throughput"])

十、常見的陷阱

1. 批次太大

# 錯誤的例子:批次 512,延遲很高
llm = LLM(model="...", max_num_seqs=512)

# 正確的例子:根據需求調整
llm = LLM(model="...", max_num_seqs=64)

2. 批次太小

# 不好的例子:批次 1,GPU 利用率低
llm = LLM(model="...", max_num_seqs=1)

# 好的例子:至少 8-16
llm = LLM(model="...", max_num_seqs=16)

3. 沒有監控

# 錯誤的例子:部署後就不管了
# 正確的例子:持續監控批次大小、佇列長度、GPU 使用率

4. 忽略調度策略

# 錯誤的例子:用預設值,不考慮場景
# 正確的例子:根據場景選擇合適的策略

5. 沒有超時機制

# 錯誤的例子:請求可能永遠等待
# 正確的例子:設定超時,避免請求卡死

6. 記憶體不足

# 錯誤的例子:批次太大導致 OOM
# 正確的例子:根據 GPU 記憶體計算最大批次

7. 忽略前綴快取

# 錯誤的例子:多個請求有相同前綴,重複計算
# 正確的例子:啟用前綴快取

8. 沒有壓力測試

# 錯誤的例子:上線後才發現效能問題
# 正確的例子:先做壓力測試,找出最佳配置

十一、總結:從批次到連續,從等待到流動

讓我們回顧這一篇的核心:

  • 為什麼需要批次:單一請求的 GPU 利用率很低。
  • 靜態批次:簡單但效率低,短請求要等長請求。
  • 動態批次:改善靜態批次,但仍有同步問題。
  • 連續批次:以步驟為單位,請求隨時加入、完成立即移出。
  • 調度策略:FIFO、SJF、優先級、公平分享、混合。
  • 批次大小的取捨:吞吐、延遲、GPU 利用率、記憶體。
  • 實作:完整的連續批次系統。
  • 與 vLLM 的整合:關鍵參數、監控、調整。
  • 最佳實踐:選擇策略、動態調整、設定 max_num_seqs、監控、處理突發、設定超時、用前綴快取、測試配置。
  • 常見陷阱:批次太大或太小、沒有監控、忽略策略、沒有超時、記憶體不足、忽略前綴快取、沒有壓力測試。

連續批次是 LLM 推理的核心優化。
它讓 GPU 幾乎不會閒置,讓吞吐提升數倍,讓延遲降到最低。
沒有它,vLLM 的 PagedAttention 也無法充分發揮。

理解了批次處理與連續批次,你就掌握了高吞吐推理的另一半。
接下來,我們要談一個更進一步的優化:推測解碼。


下一篇預告

《推測解碼:用小模型加速大模型》

我們會解釋推測解碼的原理、草稿模型與目標模型的配合、如何保證輸出品質、常見的推測解碼方法(Draft Model、Medusa、Lookahead),以及如何在實際部署中應用這項技術。