LLM 的 Rust 运行时框架 整合各家所长,零依赖,可离线。
把各家验证过的东西整合进一个零依赖的 crate:SGLang 的 radix 前缀缓存和连续批调度,vLLM 的块级 APC 缓存,OpenAI 兼容的 API 和 DSL,再加 checkpoint、批量解码和 Laya 决策这几个 agent 原语。全部只用 Rust 标准库实现,cargo build --offline 就能编译,测试也能离线跑。
整合 · 可靠 · 高性能
OpenAI 兼容的接口接进来,零依赖的引擎跑起来,经过验证的调度机制扛住吞吐。
接进来就能用
SGLang 的调度、vLLM 的缓存、OpenAI 的 API、Laya 的决策——各家所长进同一个 crate,客户端改个 base_url 就接上。
离线也能跑
只用 Rust 标准库,cargo test --offline 87 项全绿;checkpoint 可恢复,上游挂了网关回 502 不崩。
吞吐扛得住
radix 前缀缓存 + 连续批调度 + 张量嫁接,KV 零拷贝复用;批量解码 [B,1] 一次前向,可观测、有断言。
按 生成 / 缓存 / 调度 / 指标 / 决策 分
能力按用途分组,一眼定位。
生成
OpenAI 兼容 · /generate · gen / select / fork · 采样 · 约束解码
缓存
Radix + APC 双后端 · 分页 KV · checkpoint · 张量嫁接
调度
连续批 · chunked prefill · 三种出队策略 · LRU 淘汰
指标
compute_saved · prefill_skip · forward/输出 · kv_util
决策
Laya · choice / score / noul · 校准概率 · 一键蒸馏
生态
OpenAI 兼容 · 共部署网关 · pagoda-hf 真权重
为什么只留标准库
内网和涉密环境拉不动 crates.io,所以这个 crate 零依赖,CI 上 cargo test --offline 一遍过。
代码地图,一目了然
每个模块都能对上号:scheduler → engine.rs,radix → radix_cache.rs,paged KV → kv_cache.rs。
优化报告
Sticky 5.9× · GDN 0.21 ms/层 · mlx logits 复用 · PR #15/#16
十四篇,全站可检索
从「推理服务是什么」讲到张量嫁接,按 / 打开搜索,找什么都有。
同一个模型,同一台机器,数字说话
pagoda-rust 对比 Laya 官方 Python 参考实现(逐字未改):同一模型快照、同一输入、同 5 次预热 + 50 次迭代,进程内计时排除 HTTP。i5-12500 / RTX 3050 8GB 共享开发机,指示性对比。完整方法学见 pagoda/docs/BENCHMARK-LAYA.md。
| 指标 | pagoda-rust | laya-python(官方) | 对比 |
|---|---|---|---|
| GPU 单次延迟 mean | 605.9 ms | 690.9 ms | 快 12.3%(f32 打 fp16) |
| GPU p95 → max | 607.2 → 614.0 ms | 693.1 → 700.3 ms | 尾延迟更紧:max−min 仅 10.3 ms |
| 冷启动加载 | 1.4–1.6 s | 7.9–9.4 s | 快 5–6.5 倍,秒级上线 |
| 峰值内存(GPU) | 1850 MB | 2954 MB | 低 37%,8GB 卡可同卡多实例 |
| 跨设备输出一致性 | argmax 逐位一致(概率差 ~52 ULP) | CPU→GPU 漂移 1e-4(强制 fp16) | 差 4 个数量级 |
| 交付形态 | 单二进制 17.9 MB · ldd 8 行 | venv 5.3 GB · 39 个包 | 拷贝即部署 |
| CPU 单次延迟 mean | 1061.9 ms | 470.9 ms | pagoda 慢 2.25 倍,见诚实清单 |
和 SGLang 比,赢在哪、输在哪
pagoda-hf vs SGLang 0.5.10 vs HF transformers,同一台共享开发机(load 4–5,指示性对比)。场景:134-token prompt + 32 token 生成,贪心采样。完整数据见 pagoda/docs/BENCHMARK.md。
| 场景 | pagoda | SGLang 0.5.10 | 结论 |
|---|---|---|---|
| tiny 模型 warm 延迟 | 14.3 ms | 153.9 ms | 快 10.8×——框架税差距,python 逐 token 派发 |
| tiny 模型 batch-8 吞吐 | 3103 tok/s | 680 tok/s | 快 4.6×(GPU 救不了框架开销) |
| CPU-only 部署 | 全链路可跑 | 拒绝运行 | scheduler 硬依赖 CUDA,边端/内网是真实差异点 |
| 干净环境搭建 | cargo build 一次通过 | 7 步修复链 | CUDA 10.1 头文件污染、gcc 太老、flashinfer 不支持……实录见原文档 |
| 1.1B warm 单请求 | 8494 ms(GPU F16) | 7866 ms | 基本打平——共享盒子上两者都是发射延迟主导 |
| 1.1B batch-8 吞吐 | 4.0 tok/s | 23.0 tok/s | SGLang 快 5.8×,GPU 批量吞吐是它的主场 |
所以两件事都成立:边缘/内网/控制面用 pagoda 直接跑;数据中心大模型吞吐交给生成引擎,pagoda 以网关共部署——这正是「整合各家所长」。
精度吃亏,还是更快
GPU 侧 pagoda 跑全 f32,官方 Python 自动 fp16(带宽和算量减半)。小模型 + 单请求场景下,Python 的逐层派发和 autocast 上下文切换吃掉了 fp16 的理论红利。
逐位确定,进 CI
e2e 断言两次 decide 的概率 to_bits() 完全相等,每次提交都拦非确定性改动。风控、路由这类阈值敏感场景,跨硬件可复现是硬保障。
差距也摆在这
CPU 延迟慢 2.25 倍——torch 的 oneDNN/MKL 在 CPU 上仍是业界最强,路线图是 BLAS / int8 内核;批量决策暂缺(当前逐请求);只做推理、不带训练侧,刻意取舍。
每个机制,自己证明自己
不拿别家引擎说事:同一个 pagoda,机制关掉 vs 打开。数字来自 e2e 测试断言与 pagoda/docs/SELLING-POINTS.md。
| 机制 | 关掉 | 打开 | 实测效果 |
|---|---|---|---|
| 增量 KV 会话 | 每步全量重放(16 步 = 216 token) | 每步只喂新 token(21 token) | 模型计算省 10.3× |
| 批量解码 | 每会话各一次前向 | [B,1] 整批一次前向 | 物理调用收缩 6.24× · 吞吐 +40% |
| 张量级前缀嫁接 | 重复请求重算 prefill | KV 本体切片嫁接 | 134-token prompt 的 warm prefill 归零 |
| KV checkpoint | 分支重新 prefill 树干 | 树干钉住,分支复印接入 | 100% 前缀命中 · 创建 6→1 token |
# Rust:cargo run 即得(模型已缓存时秒开)
cd pagoda-hf && cargo run --release --features cuda --example bench_laya -- --iters 50
PAGODA_DEVICE=cpu cargo run --release --example bench_laya -- --iters 50
# Python 参考实现(torch>=2.1.1 + transformers>=4.48 + safetensors)
cd pagoda-hf/bench && python bench_laya.py --snapshot <laya快照目录> --device cuda --iters 50跟别的实现,差在哪
一句话:整合各家所长 + Rust 的标准库 + 几个 agent 场景原语。下面每条都对应代码,不是口号。
零第三方依赖
只依赖 std。没有 tokenizers、没有 candle、没有 CUDA,cargo test --offline 一次过 87 个测试。
两种前缀缓存都能切
radix 和 APC 塞进同一个引擎,EngineConfig.cache_backend 一行切换,共用同一套 KV 池和指标。
KV checkpoint
把共享的 system prompt / 历史对话钉住,后面随便开分支,前缀 100% 命中,不会被 LRU 挤掉。
增量 KV 会话
prompt 只预填充一次,之后每步只喂新 token。16 步生成从 216 个 token 降到 21 个。
批量解码
把刚好缺一个 token 的会话拼成 [B,1] 一次前向。实测调用次数降 6.24×,框架开销场景吞吐 +40%。
张量嫁接
跑完的会话把 KV 张量存进 vault,新请求前缀一样就直接切片复用。134 token 的 warm prefill 可以归零。
零依赖约束解码
regex + JSON 两套 grammar 手写,接 logit mask,-inf 也正确处理。不用拖 Outlines/xgrammar。
能看省了多少算力
/stats 直接给 compute_saved_tokens、prefill_skip_ratio、avg_forward_per_output_token、kv_utilization。
共部署网关
一条脚本起两个进程:pagoda 守门(健康检查、指标、checkpoint),生成请求透传给上游引擎。
诚实地说,它还没做到的事
- 主 crate 跑的是确定性的 n-gram 模型:链路是真的,算力是模拟的。真权重在 pagoda-hf(HF tokenizer + Candle),要联网。
- KV 里存的是 token id 不是张量:分页 / 引用计数 / COW 的语义是真的,但没有真的接 GPU 张量。会话内已经做到张量级,跨请求的复用标在 P3。
- grammar 是字节级,只在字节 tokenizer 上合法;引擎会直接拒绝误用。
接上,就能用
API、DSL、模块命名都和主流推理引擎对齐,客户端改个 base_url 就能用。已有部署不用推倒,加一层共部署控制面即可。
API 直通
/generate 和 /v1/chat/completions 参数同形,OpenAI SDK 不用动。
DSL 直通
gen / select / fork 同构实现,sglang.lang 的写法在这里原样能用。
架构映射
scheduler ↔ engine.rs,radix ↔ radix_cache.rs,paged KV ↔ kv_cache.rs。读代码不用重新猜名字。
checkpoint* 系列
pin 共享前缀,开分支 100% 命中。SGLang 没有对应的端点。
CacheBackend::Apc
切到 vLLM 那种块级缓存,同一个引擎里对比。
零依赖 grammar
不依赖 Outlines/xgrammar 的约束解码。
四轴指标
compute_saved_tokens 等直接进 /stats。
张量嫁接
KV 本体跨请求零拷贝复用,不只是记“算过”。
批量解码
[B,1] 一次前向,可观测、有断言。
这些都向后兼容:客户端不调就不会碰到。
网关把 /generate 和 /v1/chat/* 原样透传,自己只留控制面;上游挂了就回 502,网关不崩。
多模态预处理(pyo3)
MmFamilyProcessor + pipeline 分发 + common/{resize,decode,token_layout};InternVL 是第一个 family。
Rust 服务端 / 路由
和官方 sglang-router 同思路:Rust 控制面 + Python worker,目标是 scheduler / api server / prefix tree 重写。
只判断,不生成
Laya 是决策模型:给一段现场和几个带类型的问题,一次前向返回答案 + 校准概率,不吐 token,所以没有幻觉可捉。
分诊台护士 vs 主治医生
分诊台 30 秒判断“挂哪个科、急不急”——快、便宜;医生慢慢问诊。医院不会让医生干分诊,agent 也不该让 GPT 去判断“这封邮件转给哪个组”。
一次非自回归前向
ModernBERT 编码器(双向注意,28 层)→ 决策头(只看 [MASK] 哨兵位置)→ 每选项一个分数 → softmax → 温度校准。
choice
N 选一,返回标签 + 分布。例如 department=billing,confidence 0.927。
score
有序打分,输出期望档位。例如 urgency=1.772(满分 2,blocking 档)。
noul
是否题,输出“成立”的概率。例如 churn_risk=0.879。
[CLS] "choice question: 该转给哪个部门?" [SEP]
[MASK] billing: 发票付款退款 ← 每个选项一个 [MASK] 哨兵
[MASK] technical: 故障报错
[MASK] other: 其他
[SEP] state 原文 [SEP]
概率是真的概率
严格适当评分规则训练:只有报真实概率才拿满分,再加事后温度拟合与按题型分桶。0.879 可以当真用。
一条命令端到端
cargo run --release --example e2e_laya
路由、护栏、审核都是判断不是生成。把“判断”放回它该在的层,生成模型只干重活。
用 Laya 蒸一个 零幻觉 的小模型
判断类的活别惊动大模型。以 Laya 当老师,蒸一个几十 MB、CPU 几十毫秒的小模型:只输出概率,不生成自由文本,所以判断类任务上没有幻觉。
输出被题型框死
choice / score / noul 三种题型 + softmax 分布,不生成一句话,判断类任务上没有自由文本幻觉可产生。
蒸「诚实的概率」
用 Laya 校准后的概率做软标签,学生去拟合这个分布,而不是抄 one-hot 答案。
边界先讲清楚
- “零幻觉”只在判断类任务上成立:不生成开放文本,就没有自由文本幻觉。
- 学生继承的是「题型 + 概率」的约束;开放生成不归这个框架管。
工单分流 + 流失拦截
只靠一个蒸馏后的小模型,在 CPU 上把“这封邮件转给谁、急不急、会不会退订”一次判定完。
I subscribed last month, but I was charged twice.
I want a refund, and I need to cancel my account today.
department = billing
转给账单组。confidence 0.927
urgency = 1.77 / 2
blocking 档,排队置顶。
churn_risk = 0.879
识别到退订威胁,升级人工。
按概率设阈值
churn_risk > 0.7 升级人工;department 决定路由;urgency 决定队列优先级。全程没有一个自由文本需要解析。
边端 CPU,几十毫秒
训练时用 Laya 老师出软标签;生产跑蒸馏后的小模型(几十 MB),不占 GPU,判定概率直接当阈值和置信度。
大模型打头,Laya 在中间,小模型收尾
第一层是生成层:radix 前缀缓存 + 连续批调度负责大模型吞吐。中间不是 RLHF 的“讨好人奖励”,而是 Laya 的校准概率。
奖励“像人”,不是“真实”
用人类偏好奖励训练,模型会学成“说你想听的”那种谄媚;遇到不确定就更倾向编造,幻觉跟着来。
奖励 = 诚实
严格适当评分规则(RLCD)下,只有报真实概率才拿满分;题型只有 choice/score/noul + 概率,长不出自由文本。
关键词:radix / 连续批调度 / 校准概率
- 生成层的吞吐交给 radix 前缀缓存和连续批调度,不会因为蒸馏被阉割。
- 蒸馏的监督信号是概率分布,不是“是否讨人喜欢”,所以小模型不谄媚、判断上不幻觉。
模块就这几块,对着看
引擎、调度、缓存、采样都对两个 trait(ModelEngine + Tokenizer)泛型。主 crate 用 n-gram 打通链路,真权重在 pagoda-hf 里换进去。
| 模块 | 职责 | 概念对照 |
|---|---|---|
| spec.rs | 数据模型 | srt.sampling_params / GenerationOutput |
| radix_cache.rs | 前缀缓存 | RadixAttention 的 radix tree |
| apc.rs | 块级前缀缓存 | vLLM Automatic Prefix Caching |
| kv_cache.rs | 分页 KV | memory_pool / paged attention |
| engine.rs | 引擎 / 调度 | scheduler + manager |
| dsl.rs | 前端语言 | sglang.lang 的 SglExpr |
| server.rs | 服务 | sglang.srt.server |
真实后端 pagoda-hf
- HfTokenizer —— HuggingFace BPE / Unigram tokenizer(tokenizer.json)。
- CandleModel<M> —— 通过 Candle 加载 Llama-family config.json + safetensors。
- Laya —— System-1 决策模型(ModernBERT 编码器 + 决策头)。
几个项目,关系一句话说清
搞清 Pagoda 和 SGLang、sglang-rust、Laya 各自的位置,就不会拿错 repo、不会搞混。
左:SGLang 出架构;右:Laya 出决策;中间:都写成 Rust。
APC 的来源
块级哈希缓存借鉴 vLLM 的 Automatic Prefix Caching。和 radix 放进同一个 A/B,就是为了把两派的取舍摊开比。
不是 fork,是致敬
没有复制 SGLang 代码,血缘和署名写在 NOTICE、DESIGN.md;Apache-2.0 下两边的代码可以互借。
全站一个索引,搜就行
十四篇文档 + 机制 + 关系 + 指标,都在一个索引里。按 / 或 Ctrl K 打开搜索。