# 连续批处理架构

传统批处理会一次性处理一组固定数量的请求，并等待其中最慢的请求完成后再开始处理下一组。这会导致 GPU 在批次之间处于空闲状态。

在连续批处理中，每个生成步骤，调度器都会检查已完成的请求，并立即用等待中的请求替换它们。短请求一旦完成就会被移出，GPU 在整个过程中始终保持占用。其结果是吞吐量显著提高，平均延迟降低。

## 请求生命周期

请求从提交到完成会经历四个状态。

```text
       WAIT IN QUEUE          LOAD PROMPT INTO KV       STREAM OUTPUT           DONE
    ┌────────────────┐    ┌─────────────────────┐    ┌─────────────────┐    ┌────────┐
    │    PENDING     │───▶│     PREFILLING      │───▶│    DECODING     │───▶│FINISHED│
    │  ○ ○ ○ · · ·   │    │ ████████░░░░░░░░░░  │    │  → → → → · ·    │    │   ✓    │
    └────────────────┘    └─────────────────────┘    └─────────────────┘    └────────┘
         others ahead            prompt chunks              +1 token/step
```

1. Pending — 请求已加入队列，正在等待调度。
2. 预填充 — 提示词标记在一次前向传播中处理。
3. 解码 — 输出 token 逐个生成。
4. 已完成 — 生成完毕，结果已可用。

当调度器找到足够的 token 预算和缓存空间以接纳请求时，该请求会从 pending 状态移至 prefilling 状态。如果提示词过长，无法在单个步骤中容纳，调度器会将其拆分到多个前向传播中（chunk prefill）。

## 分块预填充

在单个步骤中处理长提示词成本较高，因为它会阻塞其他请求的生成。分块预填充通过将大型预填充拆分到多个步骤中来解决此问题。

```text
Blocking (long prefill blocks the batch)
  Step:   1        2        3        4
  Req A  [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓]   ← one huge prefill
  Req B  ················ wait ················
  Req C  ················ wait ················

Chunked (same prompt split and interleave with decode)
  Step:   1          2          3          4
  Req A  [▓▓ pre]    [▓▓ pre]   [▓▓ pre]   [→ dec]
  Req B  [→ dec]     [→ dec]    [▓▓ pre]   [→ dec]
  Req C  [→ dec]     · idle ·   [→ dec]    · idle ·
```

当新请求的提示词超出可用的 token 预算（由 [ContinuousBatchingConfig](/docs/transformers/main/zh/main_classes/continuous_batching#transformers.ContinuousBatchingConfig) 中的 `max_batch_tokens` 设定）时，调度器会处理尽可能多的 token，并保留其余部分。在后续步骤中，它会从上次中断处继续，将 prefill 工作与正在进行的 decode 步骤交错执行。这使批次保持高效，并降低了其他请求的首 token 延迟。

## 调度器

调度器根据两个预算和一个请求上限，决定哪些请求加入每次前向传播。

- Token 预算 — 单次前向传播中处理的最大查询 token 数量，由 [ContinuousBatchingConfig](/docs/transformers/main/zh/main_classes/continuous_batching#transformers.ContinuousBatchingConfig) 中的 `max_batch_tokens` 设置。
- 缓存预算 — 单次读取中可读取的 KV 页面总数，其上限由缓存总大小决定。
- 请求上限 — 单次前向传播中请求的最大数量，由 `max_requests_per_batch` 设置。由于 logits 张量的大小随词表大小变化，因此该上限限制了大词表模型的峰值内存。

### FIFO

`FIFOScheduler` 是默认调度器。它按优先级顺序填充批次：首先处理处于活跃状态的解码请求，然后是处于活跃状态的预填充请求，最后按到达顺序处理新的等待请求。这种策略通过优先处理现有请求再引入新请求，从而最大化吞吐量。

### PrefillFirst

`PrefillFirstScheduler` 会在恢复解码之前完成分块预填充操作。这可以减少以长提示词为主的工作负载中的碎片化。

在 [ContinuousBatchingConfig](/docs/transformers/main/zh/main_classes/continuous_batching#transformers.ContinuousBatchingConfig) 中设置调度器。

```py
cb_config = ContinuousBatchingConfig(scheduler_type="prefill_first")
```

## 内存管理

KV 缓存采用分页机制，使得不同长度的请求能够共享一个固定的内存池，而不会产生内存碎片。一个由 `CachePool` 实现的共享内存块池，会向每种注意力类型的分配器提供这些内存块，分配器再将它们划分为块和页。

```text
Shared pool (first two chunks reserved for padding / never allocated)

  chunk N  [ ======================================== ]
               |                            |
    full attn: [ FULL BLOCK 0 | FULL BLOCK 1 | … ]
               [ page L0 | page L0 | … ]   (one page per full layer)

    sliding:   [ ----------- SLIDING BLOCK 0 ----------- ]
               [ page L1 | page L2 | page L3 | … ]  (one page per sliding layer)

  page  = page_size tokens of KV for one layer
  block = one page per layer of that attention type
  chunk = pool unit sized so full and sliding blocks tile the same bytes
```

连续批处理仅支持两种注意力类型：`full_attention`（`FullAttentionCacheAllocator`）和 `sliding_attention`（`SlidingAttentionCacheAllocator`）。混合模型会获得每种类型各一个分配器。注意力类型由模型（`layer_types` / `sliding_window`）决定，而非来自 [ContinuousBatchingConfig](/docs/transformers/main/zh/main_classes/continuous_batching#transformers.ContinuousBatchingConfig)。

滑动层通过复用少量页面，仅保留最近 token 的固定窗口。它们不支持内核块表或块共享。

缓存会在数据块之前预留两个块，用于处理请求中从未使用的填充读取和写入。这些块永远不会分配给任何请求。

### 缓存大小

管理器在启动时根据空闲的 GPU 内存推断可容纳的块数。它会考虑 KV 张量、注意力掩码、激活值以及簿记索引，然后将池的大小调整为可用内存的 `max_memory_percent` 以内。如果未设置，则会根据硬件和连续批处理参数采用默认值。在 CPU 和 GPU 共享内存的 MPS 环境下，会为缓存预留较少的空间，以避免交换。当显式设置 `num_blocks` 时，它表示容量为 `page_size` 个 token 的完整模型块数量。

您可以固定 [ContinuousBatchingConfig](/docs/transformers/main/zh/main_classes/continuous_batching#transformers.ContinuousBatchingConfig) 中的值。

```py
cb_config = ContinuousBatchingConfig(
    page_size=256,
    num_blocks=4096,
    max_batch_tokens=512,
    max_memory_percent=0.8,
)
```

### 准入

在请求加入批次之前，调度器会检查每种注意力类型分配器是否都有足够的空闲容量。如果任何分配器的容量不足，该请求将被拒绝，且不会进行任何分配。

为避免缓存被占满以至于只能进行[offloading](#offloading)，调度器会强制执行`safety_margin`。当空闲缓存容量低于`safety_margin`时，新的 prefill 请求将被暂缓，仅继续处理正在进行的 decode。FIFO 调度器的默认值为`0.15`（预留 15% 的容量），而`prefill_first`的默认值为`0.0`。可在[ContinuousBatchingConfig](/docs/transformers/main/zh/main_classes/continuous_batching#transformers.ContinuousBatchingConfig)中设置`safety_margin`以进行调整，其中`0`表示禁用该预留。

### 前缀缓存

当两个请求共享相同的提示前缀时，它们可以共享存储该前缀 KV 的块。每个已完成的块都会进行内容哈希。后续具有匹配前缀的请求会复用该块，并跳过对这些 token 的预填充（prefill）。共享块采用引用计数机制，只有当所有使用它们的请求都完成后，才会返回到空闲池。

`allow_block_sharing` 的默认值为 `True`。仅当所有分配器都支持共享时，前缀共享才会启用，因此只有纯全注意力模型会使用它。纯滑动窗口模型和混合模型会将其禁用。对于短提示词、长生成的工作负载，由于簿记开销超过了节省的成本，请设置 `allow_block_sharing=False`。

### 驱逐

已释放的块不会被擦除，它们会保持“已初始化”状态，其内容和哈希值均完整保留，以便后续的匹配前缀能够复用这些块。当未初始化池中的块不足时，管理器会延迟地将最近的一个已初始化块降级回未初始化状态，并丢弃其哈希值。前缀缓存是一个尽力而为的层，分配器会将其丢弃，以确保能够持续处理新的请求。

### 软重置

软重置是 [offloading](#offloading) 的回退路径。当管理器无法将某个请求的 KV 缓存复制到 CPU 交换池时（因为 CPU offloading 已禁用或池已满），它会改为对该请求执行软重置。具体操作是：将该请求已生成的 token 追加到其原始 prompt 中，释放其缓存块，并将其重新加入队列。[Offloading](#offloading) 部分介绍了如何选择需要软重置的请求。

当缓存再次有可用空间时，请求将从中断处恢复，其生成历史已编码在提示中。

## CUDA 图

每个生成步骤都涉及 CPU 开销，从组装批次到调度 GPU 内核以及读取结果。CUDA graphs 通过一次性记录完整的 GPU 执行序列，并在形状匹配的批次上重放该序列，从而消除这些开销。

由于批处理形状在每一步都会发生变化，连续批处理系统通过填充和缓存来处理这一问题。

1. 查询长度会被填充到 `q_padding_interval_size` 的最近整数倍。
2. KV 长度会被填充到 `kv_padding_interval_size` 的最近整数倍。
3. 记录的图会存储在缓存中。

当某个批次的填充形状与已缓存的图匹配时，该图将直接重放，无需任何 CPU 调度。新的形状会触发一次新的图捕获。

CUDA 图要求静态控制流，并且与注意力掩码不兼容。默认情况下会自动检测，当条件不满足时会被禁用。

## 异步批处理

异步批处理使用两个 I/O 缓冲对和两个 CUDA 流，以重叠 CPU 和 GPU 的工作。

```text
Sequential
  CPU  ── [prep N] ·········idle········· [prep N+1] ·········idle·········
  GPU  ·········idle········· [compute N] ·········idle········· [compute N+1]

Async
  CPU  ── [prep N] [prep N+1] [prep N+2] ──
  GPU  ── ········ [compute N] [compute N+1] ──
```

在 GPU 计算第 N 个批次（batch N）的同时，CPU 会准备第 N+1 个批次（batch N+1）。然而，将这两者重叠执行大约需要两倍的显存（VRAM）。

异步批处理需要启用 CUDA graphs，因为图重放提供了流重叠正确性所需的稳定张量地址。

## Offloading

生成较长输出的请求可能会不断增长，直到 KV cache 没有空间来分配下一个块。为了避免崩溃，管理器会将处于活动状态的请求卸载回等待队列，从而为剩余请求释放块。

Offloading 是由需求驱动的。调度器会报告 *stalled* 请求，即那些未能分配到所需块的活动请求，以及每个请求所需的块数。管理器会卸载最少数量的请求，以释放足够的块来满足该需求。它会优先卸载最新的请求，因此已调度到当前批次中的请求可以保留其缓存，且最后一个剩余的活动请求永远不会被卸载。

两种情况会触发卸载。

- 一个批次已被调度，但部分活跃请求发生了停滞。管理器将足够多的停滞请求卸载，以便下一个批次能够为其余请求分配块。
- 缓存已满，无法调度任何批次。管理器会卸载请求，并在同一步骤内循环重试调度，直到某个批次能够装入，或者没有更多请求可以卸载。它不再等待下一次前向传播来恢复。

被卸载的请求会走两条路径之一。当 `cpu_offload_space` 大于 `0.0` 时，管理器会将该请求的 KV 缓存块复制到预先分配的固定 CPU 缓冲区中，并将所有能放入交换池的请求批量合并为一次传输。待 GPU 缓存空间可用后，管理器会将这些块复制回 GPU，并恢复该请求，而无需重新计算其提示词和已生成的 token。对于无法放入交换池的请求，或者在禁用 CPU 卸载时的所有被卸载请求，则会回退到 [soft reset](#soft-reset)。

## 后续步骤

- [连续批处理博客文章](https://huggingface.co/blog/continuous_batching)介绍了 KV 缓存、分块预填充以及动态调度，并附有性能基准数据。
- 有关使用示例，请参阅 [连续批处理](./continuous_batching) 文档。

