MiniServe 是一个用于学习大模型推理系统的轻量级本地服务。项目基于 Qwen2.5-0.5B-Instruct 和 Hugging Face Transformers,实现了 HTTP/流式生成、 手写 KV Cache 解码、可复用的 Prefix KV Cache 和 Paged KV Cache 内存管理原型, 并提供正确性与性能基准。
项目当前处于 Phase 4:推理缓存原型、Tool Registry、双工具 Agent Loop、API 与 Agent 评测均已完成。
- FastAPI JSON 生成接口与 Swagger UI
- 基于
TextIteratorStreamer的流式输出 - Hugging Face
generate()baseline - 手写 decoder-only Transformer prefill/decode 循环
- 无 KV Cache、手写 KV Cache、官方 KV Cache 性能对比
- 基于精确 token prefix 的 LRU Prefix KV Cache
- Prefix Cache hit/miss 统计
- official 与 Prefix Cache greedy 输出一致性验证
- 交错执行、warmup、平均值与 p50 延迟基准
- 预分配 Paged KV block pool 与按请求维护的 block table
- 跨 block KV 追加、读取、释放、复用和碎片统计
- Agent Tool Registry、安全 calculator 与本地笔记检索工具
- 带最大步数、执行轨迹和参数防护的 Tool Calling Agent
- Agent 工具路由、执行结果与端到端任务评测
- 环境变量统一配置与 JSON 结构化运行日志
flowchart LR
Client[HTTP Client / Swagger] --> API[FastAPI Service]
subgraph Inference[LLM inference]
Runner[ModelRunner]
Tokenizer[Tokenizer + Chat Template]
Official[Transformers generate]
Custom[Manual Prefill / Decode]
Prefix[Prefix KV Cache]
Model[Qwen2.5-0.5B-Instruct]
Runner --> Tokenizer
Tokenizer --> Official
Tokenizer --> Custom
Tokenizer --> Prefix
Official --> Model
Custom --> Model
Prefix --> Model
end
subgraph Agent[Tool-calling agent]
AgentRunner[AgentRunner]
Guard[Tool and argument safeguards]
Registry[ToolRegistry]
Calculator[Safe AST calculator]
Retriever[BM25 local note retrieval]
AgentRunner --> Guard
Guard --> Registry
Registry --> Calculator
Registry --> Retriever
Registry --> AgentRunner
end
API --> Runner
API --> AgentRunner
AgentRunner --> Runner
Config[Environment configuration] -.-> API
Config -.-> Runner
Logs[JSON structured logs] -.-> API
Logs -.-> AgentRunner
subgraph Validation[Experiments and validation]
Benchmarks[Latency / throughput benchmarks]
Eval[Agent evaluation suite]
Tests[Unit tests]
Paged[Paged KV Cache prototype]
end
Runner -. measured by .-> Benchmarks
AgentRunner -. evaluated by .-> Eval
Paged -. memory experiment .-> Benchmarks
Paged KV Cache 当前是独立的内存管理原型,没有接入 Qwen attention kernel;图中将其 放在实验验证层,避免把内存分配实验误解为已经上线的端到端 PagedAttention。
主要技术栈:Python、PyTorch、Hugging Face Transformers、FastAPI、Pydantic、 BM25、unittest、Git。
| 方向 | 实现与可验证结果 |
|---|---|
| 自回归推理 | 手写 prefill/decode 和 KV Cache;16-token CPU 实验中平均延迟相比无缓存降低约 73.7%,吞吐提升约 3.8 倍 |
| Prefix Cache | 基于精确 token prefix 的 LRU KV 复用;215-token prefix 命中时 p50 相比 miss 降低约 57.2%,获得 2.33× 加速 |
| Paged KV 管理 | 实现 block pool、block table、跨块追加/读取/释放与复用;实验中逻辑分配量降低 70.31% |
| Tool Calling Agent | 实现受步数约束的 Agent Loop、双工具 Registry、参数校验、错误恢复和可观测 trace |
| Agent 可靠性 | 10 条正常与边界任务中,原始模型路由准确率 90%,确定性防护后工具路由和端到端通过率均为 100% |
| 工程能力 | FastAPI/Swagger、统一环境配置、JSON 日志、请求关联 ID、benchmark 与 35 项单元测试 |
以上结果均来自本机 CPU 和当前小规模工作负载,详细实验设置与限制见后文章节。
可直接用于简历的精简描述和面试讲解提纲见
docs/resume_project.md。
official
完整 prompt -> model.generate() -> decode
custom_cache
完整 prompt prefill -> 保存 KV -> 每次仅输入一个新 token
prefix_cache
system prefix -> 查询 LRU Prefix Cache
-> miss: 计算并保存 prefix KV
-> hit: 复用 prefix KV
user suffix -> 一次批量 prefill
decode -> 每次仅输入一个新 token
paged_kv_cache
request -> logical blocks -> block table -> physical KV blocks
append -> 按需分配新 block
finish -> 释放 block 并返回共享池
official 与 Prefix Cache 使用相同的 chat template,并在 greedy decoding 中应用
模型默认的 repetition_penalty,保证两条生成路径的 token 输出一致。
推荐使用 Python 3.11:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt当前默认模型可以在 CPU 上运行;使用 CUDA 时需要安装与本机 CUDA 版本匹配的 PyTorch。
运行参数集中在 app/config.py,通过环境变量覆盖,无需修改源码。.env.example
列出了全部可配置项;项目不自动加载 .env,在 PowerShell 中可以这样设置:
$env:MINISERVE_MODEL_NAME = "Qwen/Qwen2.5-0.5B-Instruct"
$env:MINISERVE_DEVICE = "auto"
$env:MINISERVE_PREFIX_CACHE_MAX_ENTRIES = "8"
$env:MINISERVE_LOG_LEVEL = "INFO"
$env:MINISERVE_JSON_LOGS = "true"| 环境变量 | 默认值 | 作用 |
|---|---|---|
MINISERVE_MODEL_NAME |
Qwen/Qwen2.5-0.5B-Instruct |
Hugging Face 模型名称或本地路径 |
MINISERVE_DEVICE |
auto |
auto、cpu 或 cuda |
MINISERVE_PREFIX_CACHE_MAX_ENTRIES |
8 |
Prefix Cache 最大条目数 |
MINISERVE_HOST |
127.0.0.1 |
服务监听地址 |
MINISERVE_PORT |
8000 |
服务端口 |
MINISERVE_LOG_LEVEL |
INFO |
Python 日志级别 |
MINISERVE_JSON_LOGS |
true |
是否输出 JSON 结构化日志 |
配置会校验设备、正整数、端口范围、日志级别和布尔值。显式请求 CUDA 但机器没有 可用 CUDA 时,模型加载会直接给出明确错误,而不是静默回退 CPU。
JSON 日志覆盖模型加载、普通生成、流式生成、Agent 完成或失败,以及每次工具调用。 日志包含事件名、请求 ID、延迟、token 数、原始/实际工具和防护是否生效,但不记录 完整 prompt 或回答正文。例如:
{"level":"INFO","event":"tool_executed","step":1,"model_tool_name":"retrieve_note","tool_name":"calculator","safeguard_applied":true,"success":true}如需更易读的开发日志,可在启动前设置:
$env:MINISERVE_JSON_LOGS = "false".\.venv\Scripts\python.exe -m app.server启动后打开 http://127.0.0.1:8000/docs,或者使用 PowerShell 请求:
$body = @{
prefix = "你是一个机器学习教学助手。"
prompt = "请解释什么是 KV Cache。"
max_new_tokens = 16
temperature = 0
top_p = 0.9
generation_method = "prefix_cache"
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri http://127.0.0.1:8000/generate `
-ContentType "application/json" `
-Body $bodygeneration_method 支持:
official:Hugging Face 官方生成方法custom_cache:手写 KV Cache greedy decodingprefix_cache:复用 system prefix KV Cache
Qwen2.5-0.5B-Instruct 在 CPU 上生成 16 个 token,warmup 1 次、正式运行 5 次:
| 方法 | 平均延迟 | 吞吐 |
|---|---|---|
| 无 KV Cache | 26969.45 ms | 0.59 tokens/s |
| 手写 KV Cache | 7097.99 ms | 2.25 tokens/s |
| 官方 KV Cache | 7397.26 ms | 2.16 tokens/s |
手写 KV Cache 相比无缓存将平均延迟降低约 73.7%,吞吐提升约 3.8 倍。
运行命令:
.\.venv\Scripts\python.exe -m benchmark.compare_generation --repeats 5实验使用 Qwen2.5-0.5B-Instruct 和 CPU,每种方法 warmup 后交错运行 10 次, 并生成 16 个 token。所有实验中,三种方法的生成文本完全一致。
| 方法 | 平均延迟 | p50 延迟 | 最小延迟 | 最大延迟 | 平均吞吐 |
|---|---|---|---|---|---|
| official | 2255.96 ms | 2265.64 ms | 2190.55 ms | 2328.06 ms | 7.09 tokens/s |
| Prefix Cache miss | 2534.59 ms | 2541.50 ms | 2442.19 ms | 2659.23 ms | 6.31 tokens/s |
| Prefix Cache hit | 2257.72 ms | 2245.00 ms | 2217.56 ms | 2295.72 ms | 7.09 tokens/s |
在短 prefix 场景中,cache hit 相比 cache miss 的 p50 延迟降低约 11.7%, 吞吐从 6.30 提升至 7.13 tokens/s,hit 与 official 基本持平。
| 方法 | 平均延迟 | p50 延迟 | 最小延迟 | 最大延迟 | 平均吞吐 |
|---|---|---|---|---|---|
| official | 4438.54 ms | 4492.92 ms | 4211.02 ms | 4615.15 ms | 3.60 tokens/s |
| Prefix Cache miss | 5520.77 ms | 5456.66 ms | 4806.29 ms | 6679.72 ms | 2.90 tokens/s |
| Prefix Cache hit | 2413.51 ms | 2338.17 ms | 2236.24 ms | 2762.57 ms | 6.63 tokens/s |
长 prefix 命中时,p50 延迟相比 miss 降低约 57.2%,获得约 2.33 倍加速; 相比 official 完整 prefill,p50 延迟降低约 48.0%。实验表明,Prefix Cache 的收益会随着可复用 prefix 长度增加而显著扩大。
| Prefix 长度 | Hit 相比 Miss 的 p50 延迟降低 | p50 加速比 |
|---|---|---|
| 12 tokens | 11.7% | 1.13× |
| 215 tokens | 57.2% | 2.33× |
运行命令:
.\.venv\Scripts\python.exe -m benchmark.benchmark_prefix_cache `
--max-new-tokens 16 `
--repeats 10 `
--output results/benchmark_prefix_cache_interleaved.csv长 prefix 实验:
.\.venv\Scripts\python.exe -m benchmark.benchmark_prefix_cache `
--prefix-repeats 30 `
--max-new-tokens 16 `
--repeats 10 `
--output results/benchmark_prefix_cache_long.csvPagedKVCache 预先分配固定大小的物理 KV blocks,并为每个请求维护从逻辑块到
物理块的 block table。请求只在已有 block 写满后申请新 block,结束后将全部 block
归还共享池,因此不需要为每个请求提前预留最大序列长度的连续空间。
KV 张量接口为:
输入:[num_layers, num_tokens, num_kv_heads, head_dim]
存储:[num_blocks, num_layers, block_size, num_kv_heads, head_dim]
默认内存实验使用长度为 17、33、65、129 的四个请求,block size 为 16,并假设 连续分配方式为每个请求预留 256 个 token slots:
| 指标 | 连续预留 | Paged KV Cache |
|---|---|---|
| Token slots | 1024 | 304 |
| 逻辑 KV 内存 | 24.00 MiB | 7.12 MiB |
分页分配将逻辑分配量降低 70.31%。跨 block 的 prefill/decode 追加和 K/V round-trip 均通过逐元素正确性检查;释放请求后,其物理 blocks 可以被其他请求复用。
在相同的四请求工作负载下,连续 KV baseline 与 Paged KV Cache 分别执行
create -> prefill append -> token-by-token append -> read -> free,warmup 后运行
20 次:
| 方法 | 逻辑 KV 内存 | Append p50 | Read p50 | Free p50 | 生命周期 p50 |
|---|---|---|---|---|---|
| 连续 KV | 24.00 MiB | 4.4502 ms | 0.2655 ms | 0.0149 ms | 4.9159 ms |
| Paged KV | 7.12 MiB | 8.5836 ms | 3.6655 ms | 0.0279 ms | 12.5166 ms |
Paged KV Cache 用约 29.7% 的逻辑 KV 容量完成相同工作负载,但当前 append 和 read
操作较慢。特别是 read() 需要将不连续的 blocks 用 torch.cat() 拼成连续 K/V,
因此 read p50 高于连续存储。这是内存管理原型的预期结果,而不是端到端 PagedAttention
吞吐测试;真正的 PagedAttention kernel 应直接读取 block table 指向的物理 blocks,
避免该 gather 开销。
运行单元测试:
.\.venv\Scripts\python.exe -m unittest discover -s tests -v运行内存实验:
.\.venv\Scripts\python.exe -m benchmark.benchmark_paged_kv_cache运行操作性能实验:
.\.venv\Scripts\python.exe -m benchmark.benchmark_paged_kv_cache_ops `
--repeats 20 `
--output results/paged_kv_cache_ops.csv当前实现聚焦 Paged KV Cache 的内存池和 block table。读取接口会将分页 K/V 暂时 拼接为连续张量用于正确性验证;若要获得 vLLM PagedAttention 的端到端推理收益, 还需要编写能够直接按照 block table 访问非连续 K/V 的 attention kernel。
ToolRegistry 将工具的名称、描述、JSON Schema 参数定义与执行函数统一管理。
当前注册了 calculator 和 retrieve_note 两个本地工具:
from app.tools import create_default_registry
registry = create_default_registry()
result = registry.execute(
"calculator",
{"expression": "(125 * 8) - 37"},
)
print(result.content) # 963calculator 使用 Python AST 白名单解析表达式,仅支持数字和
+、-、*、/、//、%、**;不执行任意 Python 代码。
Tool Registry 会拒绝未知工具、重复注册、缺失必填参数和未声明参数,并可导出
OpenAI-compatible function-tool schema,供后续 Agent Loop 传递给模型。
retrieve_note 从 data/notes.json 加载本地知识库,使用轻量 BM25 风格算法
对中英文关键词进行排序,返回 1~3 条带标题、正文和相关度分数的笔记:
result = registry.execute(
"retrieve_note",
{
"query": "Paged KV Cache 如何减少内存浪费",
"top_k": 2,
},
)
print(result.content)本地知识库当前包含 Decoder-only Transformer、KV Cache、Prefix Cache、 Paged KV Cache 和 RoPE。真实 Qwen2.5-0.5B-Instruct 已验证能够在 calculator 与 retrieve_note 之间选择合适工具,并根据检索结果生成最终回答。
AgentRunner 实现一个受最大步数约束的单 Agent 循环:
messages + tool schemas
-> Qwen 生成 tool call 或最终文本
-> ToolRegistry 校验并执行
-> tool result 回填 messages
-> Qwen 继续生成
-> 无 tool call 时返回最终答案和 trace
Qwen2.5 的 chat template 使用 <tool_call>{...}</tool_call> 格式。AgentRunner
解析该 JSON、记录每一步的工具名/参数/结果/成功状态,并限制 max_steps 避免
无限工具循环。一次 calculator 调用示例:
from app.agent_runner import AgentRunner
from app.model_runner import ModelRunner
result = AgentRunner(ModelRunner()).run(
"请计算 (125 * 8) - 37,并说明结果。",
max_steps=3,
)
print(result.text)
print(result.trace[0].result) # 963AgentRunner 的单元测试使用 fake model 覆盖直接回答、工具成功、工具失败恢复、 畸形 tool call 和最大步数限制;同时已用真实 Qwen2.5-0.5B-Instruct 验证 calculator 的端到端 tool-call 流程。
启动服务后,可以在 Swagger 中调用 POST /agent/run:
{
"prompt": "请计算 (125 * 8) - 37,并说明结果。",
"max_steps": 3,
"max_new_tokens": 128
}响应同时返回最终文本和可观测执行轨迹:
{
"text": "计算结果是 963。",
"steps": 1,
"trace": [
{
"step": 1,
"model_tool_name": "calculator",
"tool_name": "calculator",
"arguments": {
"expression": "125 * 8 - 37"
},
"result": "963",
"success": true
}
]
}Agent API 与普通生成 API 共享同一个缓存的 ModelRunner,不会重复加载模型。
畸形 tool call 或超过 max_steps 时,接口返回 HTTP 422。
扩展评测集包含 10 条任务:基础 calculator、笔记检索、直接回答,以及错误工具指令、 多个表达式、无关知识检索和工具异常等边界案例。在 Qwen2.5-0.5B-Instruct、CPU、 greedy decoding 环境下,单次完整评测结果如下:
| 指标 | 结果 |
|---|---|
| 测试案例 | 10 |
| 原始模型工具选择准确率 | 9/10(90%) |
| 防护后工具选择准确率 | 10/10(100%) |
| 端到端任务通过率 | 10/10(100%) |
| 平均延迟 | 46479.55 ms |
各类别均通过:calculator 2/2、retrieval 3/3、direct answer 1/1、错误工具指令 1/1、多个表达式 1/1、无关检索 1/1、工具异常 1/1。
这里的延迟来自每个案例单次运行,且直接回答与工具调用的生成轮数不同,因此主要用于 功能评测,不作为严格的吞吐 benchmark。CSV 会记录模型生成的工具参数、工具原始结果、 执行轨迹、最终答案、失败类型和延迟,便于定位“选对工具但参数错误”等问题。
评测区分 model_tool_name(模型原始选择)和 tool_name(经过防护后实际执行),
避免把确定性工程校正误报成模型能力。在错误工具边界案例中,0.5B 模型服从用户的错误
指令选择了 retrieve_note;执行层检测到明确计算意图和唯一算术表达式后,将实际工具
校正为 calculator,因此原始模型路由准确率为 90%,防护后系统路由为 100%。
评测发现,0.5B 模型曾将用户输入的 (48 + 12) / 5 改写为
48 + 12 / 5,虽然正确选择 calculator,却得到错误结果 50.4。当前 Agent 在用户
问题只包含一个明确算术表达式时,从原始 prompt 提取并保留数字、运算符和括号,将
修正后的参数同时用于工具执行、对话历史和 trace;修复后该案例返回 12.0。若问题
包含多个表达式,程序不会自动覆盖模型参数。
运行快速检查:
.\.venv\Scripts\python.exe -m benchmark.evaluate_agent `
--limit 2 `
--output results/agent_eval_smoke.csv运行完整评测:
.\.venv\Scripts\python.exe -m benchmark.evaluate_agent `
--output results/agent_eval_extended.csv使用可重复的 --category 只运行指定类别,例如:
.\.venv\Scripts\python.exe -m benchmark.evaluate_agent `
--category wrong_tool `
--category tool_error `
--output results/agent_eval_edges.csv运行完整测试:
.\.venv\Scripts\python.exe -m unittest discover -s tests -v- Phase 1:Transformers baseline、HTTP/流式生成、基础性能基准(已完成)
- Phase 2:手写 prefill/decode、KV Cache 与生成方式对比(已完成)
- Phase 3:Prefix Cache、Paged KV Cache 内存管理原型(已完成)
- Phase 4:Tool Calling Agent;双工具 Registry、Agent Loop、Agent API 与评测(已完成)
- 当前主要验证 batch size 为 1 的 greedy decoding
- Prefix Cache 仅支持精确 token prefix 命中
- 自定义生成尚未完整支持 temperature、top-p、top-k 等采样策略
- Prefix Cache 当前按条目限制容量,尚未实现按 token/内存预算管理
- Paged KV Cache 尚未接入 Qwen attention kernel,当前不代表端到端 PagedAttention
- CPU benchmark 会受到处理器调频和后台负载影响
- calculator 参数防护目前只处理用户输入中唯一、明确的算术表达式
- Agent 评测集规模较小,防护后 100% 通过率不代表开放问题上的通用工具调用准确率