自己写个模型路由器:一台机器上管好八套部署

2026-10-07 · 本文由 AI 撰写

上一篇讲了我怎么在 2×RTX 3090 上部署 Qwen3.8-27B,最后落到 llama.cpp + GSQ 量化。但真实使用里从来不是"一套部署用到底":服务化的 vLLM、秒切的 llama.cpp、跑长文档的双卡 262K、追求质量的 MoE 高档位……两套引擎、七八个权重、不同的显存排他方式,手动切换是不可维护的。

所以最终形态是我自己写的一个模型路由器:纯 Python 标准库,几百行,一个 OpenAI 兼容的反代,背后挂多个"档位"。这篇讲它的设计和几个月使用里撞出来的教训。

设计:档位化的推理服务

路由器监听一个端口,客户端照常发 OpenAI 格式的请求,model 字段填档位名,路由器负责:

我的档位表:

细节上有几个值得抄的做法:档位名直接写"后端 + 布局 + 上下文",切换成本一眼可见;/v1/models 的 loaded 字段做真实健康探测而不是读状态机标记(状态机会把默认档标记为"目标"但不真的加载,谎报过 loaded);再加一个 WATCHDOG 线程定期自查,发现"容器在跑但路由器不知情"就告警。

四个教训,都是撞出来的

双卡层切分不要用行切分。 给 262K 档配双卡时,直觉加了 -sm row(行切分),结果与 GSQ 模型的 MTP 结构不兼容,报错是莫名其妙的 “does not support split buffers”。删掉它用默认层切分,一样双卡协作。教训:llama.cpp 的多卡模式不是通用开关,跟模型结构有耦合。

带外操作会毁掉一切状态管理。 有一次容器被手动 docker start 拉起来,路由器的空闲回收完全看不见它——它只管自己拉起的进程,于是显存就那么被无名占用着。加了 WATCHDOG 之后这种事 30 秒内就会在日志里大喊。教训:所有档位操作必须过路由器,没有例外;绕过它等于放弃回收能力。

冷加载速度是隐形体验杀手。 权重放机械盘时直读 205 MB/s,冷启动 55 秒;挪到 NVMe 后 2.3 GB/s,6 秒。平时有页缓存感觉不到,但空闲自动卸载开着,冷加载天天发生,这 50 秒的差距每天都在被放大。顺带的坑:调试期手工拉起的实例会和路由器互相"认领"、互相残杀,测试要么全走路由器,要么把它的状态清干净。

memlock 是最隐蔽的坑。 MoE 档要把 47G 权重 mlock 锁进内存,而 Linux 默认每进程只能锁 8MB。锁失败报出来的错误是 “out of memory”——跟显存毫无关系,会让你盯着 nvidia-smi 白查半小时。解法:systemd 服务里加 LimitMEMLOCK=infinity(由服务拉起的子进程自动继承)。

为什么不直接用现成方案

看过几个推理网关项目,功能更全(负载均衡、限流、多租户),但我的需求里最重的是档位互斥 + 按需启停 + 空闲回收这个闭环,恰好是消费级单机场景特有的;纯标准库几百行,坏了能一口气读完,比引入一个重依赖划算。如果哪天要多机、多副本,那确实该换正式网关——单机折腾,自带的轮子刚好。

参考

← 返回