> ## Content Index
> Fetch the complete content index at: https://blog.hacking.pub/llms.txt
> Use this file to discover other available public pages before exploring further.

# Kapitsa 源码拆解 01：一条请求如何变成 token？
- URL: https://blog.hacking.pub/2026/10/26/kapitsa-919b1b2-1/
- Published: 2026-10-26T09:14:22.000Z
- Updated: 2026-10-26T09:14:22.000Z
- Description: 固定 0.2.17 / 919b1b2，从请求、prefill、decode 到会话状态，建立阅读这套原生推理引擎的地图。
- Author: chainhelen
- Tags: Kapitsa, 源码阅读, 推理引擎

把“我爱猫”发给一个本地模型，HTTP 接口收到的只是一份请求。要得到回复，中间还要经过模板、分词、会话状态、矩阵计算、采样和流式输出。Kapitsa 的实现值得拆开读，正是因为它把这些环节连成了一条完整的运行路径。

**版本范围：**本系列固定阅读 [Kapitsa 提交 919b1b2a6782ec205e23725dee76ee5f57de2857](https://github.com/nace-ai/kapitsa/tree/919b1b2a6782ec205e23725dee76ee5f57de2857?ref=blog.hacking.pub)；该快照的 [CMakeLists.txt](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/CMakeLists.txt?ref=blog.hacking.pub#L1-L8) 标注版本为 **0.2.17**。以下是源码拆解与教学算例，不是本机性能实测；其他版本的默认参数和实现可能不同。

## 推理引擎

这个快照使用原生 C++20，模型后端把计算编码成 Metal 命令，符合条件的预填充操作可以交给 ANE。它支持一些 MLX 格式权重，但**读取同一种权重格式，不等于使用 MLX 运行时**。源码的 [架构说明](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/docs/architecture.md?ref=blog.hacking.pub#L1-L12) 明确区分了两者。

![请求先经过 API 适配、Engine、模型后端，再交给 Metal 或可选 ANE 执行，采样和解析后流式返回。](https://blog.hacking.pub/qiniu/content/images/2026/10/draft-series-20261026-k1-request.png)

图 1：先看职责，再追函数。模型后端决定具体计算路径。

可以把源码分成四个阅读层次：

1. API 负责协议；
2. Engine 安排请求生命周期；
3. Model 后端知道模型的层和缓存；
4. Metal/ANE 执行具体任务。

## 代码框架目录

图 1 画的是职责，打开仓库后还需要找到承担这些职责的文件。这里以 `POST /v1/chat/completions` 的文本请求为主线：HTTP 入口把请求交给 Engine，Engine 持有具体 Model，并通过 Model 的统一接口安排计算。模型后端再把操作编码到 Metal，支持的路径才会使用 ANE。

```
src/
├── server/main.cpp       HTTP 路由、请求解析、调用引擎
├── api/                  各种 API 的响应与流式协议
│   └── chat_completions.cpp
|   └── responses.cpp
├── engine/
│   ├── engine.h          Engine、ChatRequest、Slot、TurnLock
│   ├── engine.cpp        模板、选 slot、prefill、生成循环
│   └── output_parser.h   将生成内容拆为正文、推理和工具调用
├── tokenizer/            文本与 token ID 之间的转换
├── model/
│   ├── model.h           Model 接口、Seq、Checkpoint
│   ├── qwen35.h          一个具体模型后端的声明
│   └── qwen35_ane.cpp    该后端的 ANE 接入
├── metal/gpu.h、gpu.mm   Buffer、Encoder 与 Metal 调度
├── kernels/              GPU 执行的计算内核
└── ane/                  ANE 调用与 FFN 拆分支持
```

（上述非完整文件树，`src/api` 也不完全是部 HTTP 逻辑）。这个版本的 Chat Completions 请求解析就在 [server/main.cpp](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/server/main.cpp?ref=blog.hacking.pub#L1350-L1417)；[api/chat\_completions.cpp](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/api/chat%5Fcompletions.cpp?ref=blog.hacking.pub) 主要处理响应、错误和流式输出格式。

**其中** [Engine](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.h?ref=blog.hacking.pub#L361-L400) 关心请求什么时候运行、状态能复用多少、何时结束；[Model](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/model/model.h?ref=blog.hacking.pub#L123-L170) 关心给定 token 与状态，如何执行模型的一次计算。因而读请求流程先看 Engine，读某层矩阵怎么计算再进入具体模型后端。

## Chat 入口

1. 把请求中的消息按该模型的聊天模板组织成 prompt。角色标记、工具描述等也会影响最终序列。
2. 把 prompt 分成 token ID。教学图中的“我、爱、猫”只是便于追踪的三个位置，不能直接当作真实分词结果。
3. 找到可用 slot，并检查已有历史能复用到哪里。
4. 只对没有可复用状态的后缀执行 prefill，然后进入生成循环。

在 [Engine::chat](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.cpp?ref=blog.hacking.pub#L2060-L2228) 中

```cpp
text = render(r, true);
base = render(r, false);
// ...
std::vector<int> prompt = tok_.encode(text, prefix_ok ? base.size() : std::string::npos, &boundary);
```

`render` 先把消息组织成模型要读的文本，`tok_.encode` 才把文本变成 token ID。随后 [pick\_slot](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.cpp?ref=blog.hacking.pub#L1990-L2058) 比较可用 slot 的历史与检查点，选择可复用状态；这里只匹配一段文字还不够，必须有对应的模型状态。

“我的问题只有十个字”不等于模型只处理十行；系统提示和之前的对话也在序列里。另一边，序列很长也不一定需要从头计算：命中的前缀可以减少本次工作量。

## Prefill 与 decode

![Prefill 把已知 token 作为多行输入，普通 decode 依次生成新 token；D 始终表示每个 token 的主干特征数。](https://blog.hacking.pub/qiniu/content/images/2026/10/draft-series-20261026-k1-phases.png)

图 2：两阶段的差别不只是名字，而是已知输入的行数与下一步的依赖。

**Prefill** 处理已经知道的输入，建立后面生成需要的状态。假设本次处理 T 个 token、隐藏维度为 D，主干输入可以看成 `X[T,D]`。注意 D 是每个 token 的特征数，即不是网络层数也不是权重大小。

**普通 decode** 每次根据现有历史得到下一个 token，再把新 token 作为下一步输入。因此它有顺序依赖。（当然Kapitsa 的投机路径会一次提出多个候选，再让目标模型验证。这是后面第六篇讨论的加速手段）

这两阶段在 [Model 接口](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/model/model.h?ref=blog.hacking.pub#L146-L151) 上也分开了：

```cpp
// 源码接口摘录：一次普通生成步骤
virtual void encode_step(Encoder& e, Seq& s, int pos,
    const SamplingParams& sp, uint32_t step, bool sample = true) = 0;

// 一段已知输入的预填充
virtual void encode_prefill(Encoder& e, Seq& s, int pos0, int T,
    const SamplingParams& sp, uint32_t step, bool sample,
    Checkpoint* snap = nullptr, int snap_row = 0) = 0;
```

`Seq& s` 是要更新的会话状态；prefill 的 `pos0` 和 `T` 表示从哪个位置开始、这次处理多少行。Engine 的 [prefill 路径](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.cpp?ref=blog.hacking.pub#L2690-L2738) 调用 `model_->encode_prefill(...)`；普通生成分支在 [decode 路径](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.cpp?ref=blog.hacking.pub#L3505-L3520) 调用 `model_->encode_step(...)`。这些是“编码计算命令”的入口，不代表调用一结束 GPU 就已经算完；提交和同步还属于执行路径的一部分。

同一个数学投影，在多行 prefill 和少行 decode 中的权重复用机会不同。（后端会为这些形状选择不同路径，详见第二篇矩阵形状）。

## 代码中的“会话”

![三个 slot 共享模型权重，但分别持有 tokens、Attention K/V、递归状态和检查点。](https://blog.hacking.pub/qiniu/content/images/2026/10/draft-series-20261026-k1-slots.png)

图 3：共享参数，隔离历史。图中 slot 数量只是示意。

在 [Seq](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/model/model.h?ref=blog.hacking.pub#L14-L43) 中，可以直接看到 `tok`、`kc/vc`、`conv/rec` 等字段。前者是 token 数据，K/V 属于注意力缓存，conv/rec 则服务于 Gated DeltaNet、Mamba-2 这类带状态的层。

把图 3 对回 [Engine::Slot](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.h?ref=blog.hacking.pub#L448-L465)，可以看到以下字段（源码节选，省略 drafter 与其他记账字段）：

```cpp
struct Slot {
  std::unique_ptr<Seq> seq;
  // ...
  std::vector<int> tokens;
  // ...
  std::vector<TurnCheckpoint> ckpts;
  // ...
  bool busy = false;
  // ...
};
```

`seq` 指向实际模型状态，`tokens` 用来跟新 prompt 比较历史，`ckpts` 留下可恢复的位置，`busy` 表示这个 slot 正被请求使用。图里的“独立会话”对应的是这些对象，而共享的模型由 Engine 管理。

Engine 的 slot 在这些模型状态之外，还管理检查点、生成器上下文和请求进度。多个请求能够共用模型权重，但不会将历史状态混在一起。（SSD 缓存篇会详细拆解）

## 抢占GPU

![](https://blog.hacking.pub/qiniu/content/images/2026/10/image-3.png)

1. 多个请求同时到达时，Kapitsa 先让它们**排队轮流使用 GPU**。普通 decode 路径通过 [TurnLock 与 contended()](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.cpp?ref=blog.hacking.pub#L133-L215) 检查：若有请求等待，且当前请求已持有 turn 至少 30 ms，就在允许切换的步骤边界主动让出；不会到时强行中断 GPU 任务。
2. **让出之前，必须先保住当前请求的状态。**其他请求会复用公共临时缓冲区，因此当前请求要完成在途计算，并安置下一步还要用的数据。源码中的顺序是 [park() → yield\_turn()](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.cpp?ref=blog.hacking.pub#L3518-L3538)：先安全停驻，再释放 turn 并重新排队。
3. **图中的“支持合批”，指模型与 drafter 组合已实现多请求批量验证路径。**多个请求各自生成候选 token，目标模型将它们对应的输入行合成一批验证，让一次权重读取服务于多组输入；未实现这条路径的组合仍然轮流执行。具体适用组合见[该版本的并发说明](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/docs/architecture.md?ref=blog.hacking.pub#L61-L82)。请求通过 [join\_batch()](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.cpp?ref=blog.hacking.pub#L1253-L1275) 入批，[batch\_cycle()](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.cpp?ref=blog.hacking.pub#L1344-L1455) 调用 `encode_verify_multi` 执行验证；每个请求仍通过自己的 [BatchJob](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.h?ref=blog.hacking.pub#L561-L596) 关联独立 `Slot`，各自维护缓存、保留通过验证的 token，并返回各自的结果。

## 对照源码阅读

- [请求生命周期与并发说明](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/docs/architecture.md?ref=blog.hacking.pub#L14-L82)
- [Seq、Checkpoint 与 KV 行](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/model/model.h?ref=blog.hacking.pub#L14-L85)
- [Engine 相关类型入口](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.h?ref=blog.hacking.pub#L1-L70)
- [GPU turn 的实现](https://github.com/nace-ai/kapitsa/blob/919b1b2a6782ec205e23725dee76ee5f57de2857/src/engine/engine.cpp?ref=blog.hacking.pub#L133-L214)