Skip to content
FengHannnnPublic

About

A lightweight LLM inference and tool-calling agent system with KV cache experiments.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

29 Commits

Folders and files

Repository files navigation

MiniServe

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
Loading

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 $body

generation_method 支持:

  • official:Hugging Face 官方生成方法
  • custom_cache:手写 KV Cache greedy decoding
  • prefix_cache:复用 system prefix KV Cache

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

Prefix Cache 实验

实验使用 Qwen2.5-0.5B-Instruct 和 CPU,每种方法 warmup 后交错运行 10 次, 并生成 16 个 token。所有实验中,三种方法的生成文本完全一致。

短 Prefix:12 个 Prefix 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 基本持平。

长 Prefix:215 个 Prefix Token

方法 平均延迟 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.csv

Paged KV Cache 原型

PagedKVCache 预先分配固定大小的物理 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。

Tool Registry(Phase 4.1)

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)  # 963

calculator 使用 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 之间选择合适工具,并根据检索结果生成最终回答。

Agent Runner(Phase 4.2)

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)  # 963

AgentRunner 的单元测试使用 fake model 覆盖直接回答、工具成功、工具失败恢复、 畸形 tool call 和最大步数限制;同时已用真实 Qwen2.5-0.5B-Instruct 验证 calculator 的端到端 tool-call 流程。

Agent API(Phase 4.3)

启动服务后,可以在 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。

Agent 评测(Phase 4.4)

扩展评测集包含 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% 通过率不代表开放问题上的通用工具调用准确率

About

A lightweight LLM inference and tool-calling agent system with KV cache experiments.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages