---
layout: post.njk
source: https://yage.ai/share/selfhost-multi-gpu-routing-20261016.html
web_feed:
  articleId: cmvci3dq80001edkb6w0spnnt
  feedSourceId: webfeed_yage_ai
  guid: https://yage.ai/share/selfhost-multi-gpu-routing-20261016.html
  sourceUrl: https://yage.ai/share/selfhost-multi-gpu-routing-20261016.html
speaker: yage.ai
title: 自建大模型：多卡为什么卡，以及为什么还是比 API 贵
date: '2026-10-16'
summary: 文章讨论了自建多卡大模型推理的性能瓶颈和成本问题。核心观点是，多卡并行时，性能受限于任务调度（routing）机制，即如何平衡缓存亲和性（cache affinity）和负载均衡（load balancing）。个人玩家在缺乏底层调度器和实时状态监控的情况下，难以实现最优的资源分配，导致资源利用率低，最终自建的单位成本（按token计费）往往高于使用云端API。
area: tech-engineering
category: architecture
tags:
  - ai-tooling
  - ai-workflow
  - deployment
  - debugging
  - architecture
people: []
companies_orgs: []
products_models:
  - Qwen3.8-27B
  - '5090'
  - NInfer
  - SGLang-router
media_books: []
draft: true
status: evergreen
---

我桌下那台机器插了 3 张显卡。

只跑一张，单卡 decode（生成回复的出字环节）吞吐就有 200 TPS
以上。

可几张卡一起上，情况就变了。我让一个 agent
同时开几条分支去读文件、查资料、调工具。按理说 3
张卡正好分开使，结果终端里的输出一顿一顿，几条分支全挤在了一起。切到监控面板，3
张卡都没跑满：一张卡忙得冒烟，另外两张闲在旁边。

慢下来不是因为哪张卡算不过来，是这些分支被分到了同一张卡上。几张卡一起用，总要有东西决定每件活落到哪张卡，这个「怎么分」就是关键。

这台机器是 3 张 5090，每张 32GB 显存，卡间没有 NVLink，只能走主板
PCIe。跨卡通信慢，我没把模型切开摊到多张卡上，而是让每张卡各装一份完整权重，常驻跑一份
4-bit 量化的 Qwen3.8-27B，后端是本地引擎 NInfer，配
MTP 投机解码；关掉思考模式跑对话和代码，单条 decode 在 150 到 256
TPS。

## 一张卡跑得快，不代表几张一起用也快

社区里比显卡快慢，习惯喂一批互相独立、长度固定的题目，算完一道清一道，看每秒出多少
token。这种标准化测速就是跑分。我这台机器同时喂 1 到 8 条独立请求，3
张卡合起来能到 268 到 962 TPS，纸面成绩很亮眼。但 agent
真干活时是另一回事：一个任务要连着聊很多轮，每轮都由上一轮触发，带着全部历史像滚雪球一样越滚越长；中间调工具还会有停顿；主
agent 还会随时分叉出几条并行分支，开头全都重叠着同一段 system prompt
和背景。正因为这样，最近几套专门冲着 agent
流量设计的评测，都把「跨轮次历史复用」列成必考项（例），固定长度的独立跑分已经不够用了。

长对话会把几张卡搞乱，根子在显卡处理长文本的两步工序。模型记不住上一轮说了什么，每次新追问，程序都要把前面所有轮次的历史和新这句话拼成上万
token
的长文，重新喂给显卡。显卡分两步：第一步把整篇输入从头通读、算出各层的上下文关联，这叫
prefill，吃算力；第二步照着算好的状态一个 token 一个 token 生成，这叫
decode，吃显存带宽。聊到第 20 轮，前 19
轮其实一字未改。为了省掉每轮重读，显卡 prefill
时会把各层的中间状态留在显存里备查，这份缓存就是 KV
cache；下一轮发来，只要开头那一大段历史和显存里存着的一模一样，显卡就直接调出
KV cache 跳过重读、只算末尾新增的几句，这叫 prefix reuse。一份公开实测测过：单实例下
1 万 token 的提示词，第一次没有缓存、要从头
prefill，首字延迟（TTFT，第一个字出来的耗时）4.3 秒；第二次命中 KV
cache、完成 prefix reuse，降到 0.6 秒。有的 agent
团队干脆说，缓存命中率是生产级 agent 最重要的一项指标。

只有一张卡时，请求自然都落回同一张卡，显存装得下就能顺带复用。扩到 3
张独立卡后，前门就得有调度器决定每条新任务去哪张卡。普通网站后端无状态，轮流派单或挑最闲的机器即可；但大模型服务里每张卡各自攒着不同对话的
KV cache，把长对话随手切给一张空卡，空卡没有前面的缓存，就得从头重算上万
token 的
prefill。这种既要看哪张卡有空、又要认哪张卡存着旧缓存的派单机制，就是
routing。

routing
难的地方在于两个目标互相打架：想复用旧缓存，相同开头的请求就得一直送到存着这段缓存的那张卡；想不排队，又得把同时涌来的任务平摊到
3 张卡。前者叫 cache affinity，后者叫 load
balancing，一个要求把请求往同一张卡上送，一个要求往多张卡上分散。几条分支同时启动，开头往往共用同一段
system prompt，一旦某张卡先存下这段公共开头，偏向 cache affinity
的调度就把后续分支全塞给它，造成一张卡排队、其他卡闲置。业界把这种扎堆叫
request herding（分析）。也有研究证明，单一映射规则没法同时兼顾
cache affinity 和 load
balancing，一味追高命中率必然压垮热门前缀所在的实例。它给的补救办法是：给每个请求同时算两个候选卡位，再按当前的负载和延迟目标挑更合适的那个，相当于在两个目标之间取个折中。

缓存亲和想让同前缀请求都粘在同一张卡上省掉重算，负载均衡却要把请求打散以防排队，两个目标天生对立

开头那几条分支为什么全挤在一张卡上，我在自己机器上看得更清楚。前门挂的是官方
sglang-router，开默认的
cache-aware 策略。这几条分支开头共用同一段 system
prompt，第一张卡先存下这段公共开头之后，调度器就认准了它，把后面的分支接着往那儿送，另外两张卡空着也不管。它这么做，是因为它只能看请求文本，默认偏向让同前缀的请求复用缓存；只有当两张卡的排队差超过某个阈值，它才会换卡，差得不够就继续粘着。

还有一种情况更麻烦：长对话聊到接近上下文上限时，客户端会压缩改写前面的历史，把中间几句换掉。NInfer
判断「这段前缀能不能复用」，靠的是历史完全没变；中间一改，它就只能把整段历史当成新的、从头
prefill 一遍，卡顿立刻出现。

而 agent 干活的节奏又是突发式的：上一秒集群还闲着，下一秒主 agent
可能就并发拉起好几条分支，把 3
张卡一次占满。要让这种突发不至于直接排起队，我平时只好让集群常态留出一半以上的算力余量，宁可空着卡换顺滑。

## 大厂已经解决了多卡分发，但个人玩家用不上

前门调度器为什么放着两张空卡不用，非把两条长对话挤在一张卡上硬抢？因为它在门外派单，看不见显卡显存里到底存了什么、手头还压着多少活，只能隔着门缝瞎猜。传统负载均衡默认后端无状态、请求独立、节点可互换，而
LLM 推理把这三条全打破了（综述）。数据中心要把活派准，调度器得随时掌握每张卡的两类信息。第一类是显存里真实的缓存明细：显卡每新存或删掉一块
KV
cache，引擎就发一个事件，调度器据此维护一张全集群缓存地图，记清哪段文本在哪张卡上。第二类是每张卡手头积压的工作量：光看能省多少重读不够，还得算清这张卡还有多少输入
token 没消化完、多少在途的 decode 在排队，这种按待算 token
衡量的忙碌程度就叫 token load。两类信息合起来，调度器把要重读的 token
加上现有 token load 折算成预计等待时间，挑最快算完的那张卡。

有了引擎实时上报缓存变动和 token load
这两类信息，数据中心的多卡调度已经相当成熟。典型做法是引擎主动报事件、调度器按综合开销挑卡，实测能把首字延迟拉快
57 倍、吞吐翻倍（架构）。

但这套东西离个人玩家很远。它要求底层是 vLLM 或 SGLang、部署在
Kubernetes 上、引擎实时上报 KV cache
事件。个人多卡玩家为了保住单卡速度，往往选定制引擎：我用的 NInfer
对外只有 /health，查不了 cache
状态，也问不出在途负载；另一款常见的开源引擎（例）多卡时走的是单机内按层切分，缓存由单个节点自管。开头那几条分支为什么全被吸到一张卡上，谜底就在这：前门调度器只能靠记住外面飞过的请求文本，自己拼一棵近似的
prefix
tree，看不到显存里真实的缓存，也判断不准哪张卡才是当下更该用的，只好一路认死最像有缓存的那张卡。社区里已经有面向自建集群的路由器，尝试在网关层用自带
tokenizer 维护前缀索引，把命中率提高了几倍、中位 TTFT 降低近 30%（项目）；但只要底层引擎还是黑盒，站在门外猜状态就有上限。

## 就算分发做到最好，自建还是比买
API 贵

顺着 routing
往下想一步：即便把多卡分发做得严丝合缝，桌下这台机器的单位成本，真能低过按
token 计费的云端 API
吗？很多人买卡攒机，直觉是硬件只花一次钱，总比长期交接口费便宜。可一旦把硬件折旧和电费摊到产出的
token 上，算出每百万 token
的成本，再跟云端账单比，这个直觉很快会落空。

先看商业接口把价格压到了什么程度：DeepSeek
V4.1-Flash 是 552B 的混合专家模型，激活 8B（输入）和
16B（输出）。按官方定价，闲时输出
0.60 美元每百万 token；输入命中云端 KV cache、完成 prefix reuse
后自动减免 98%，低至 0.003
美元。重复的计算能省到这个程度，价格自然压得下来。

再算我这台三卡主机。5090 早就不是发布时那个价了：2026 年 10
月，全新卡在美国要 5000 美元上下，二手也要 3800 左右（价格追踪），3
张卡加机箱电源差不多 1.7 万美元。按 4 年折旧，每月固定成本约 350
美元，满载功耗约 1.7kW。只有 24 小时满载，按 3 张卡合计 600 TPS
折算，每百万 token 成本才压到 0.4 美元左右；按个人每天真跑 1 到 2
小时的节奏摊下来，立刻涨到 4 到 6 美元，反倒比调 API 贵出近 10 倍。

每百万 token
成本放在对数轴上：自建全天满载约 0.4 美元才低于 DeepSeek 输出价 0.6
美元，每天只跑一两小时就涨到 4 到 6 美元，而命中缓存的 API 只要 0.003
美元

同样一台机器，为什么理想单价 0.4
美元、日常却要四五美元？关键不在显卡那几秒算得多快，而在机器一天 24
小时里有多少小时真在干活，这个比例就是利用率。买卡或租云主机按时间付钱，调
API 按实际生成的 token 付钱。一份自建成本测算显示，AWS
上常开的 H100 每月固定约 5000 美元，利用率要到 9% 才和 API
打平；另一份报告也指出，不少集群日均利用率低至 5%（数据）。落到个人多卡机器上还要再折两层：一天只跑
1 到 2 小时，机器大部分时间空着，这是第一层；就算人在电脑前干活，为防止
routing
撞车，我还得故意空出一半以上算力，这是第二层。两层叠起来，前面为体验留出的余量，最后全记进了不划算那本账。

把工程门槛和这笔账都摊开，自建大模型的定位反而清楚了：在桌下塞一台多卡机器，就像家里备一台柴油发电机。没人买发电机是为了省电费，真把折旧和油钱摊进每一度电，自家发电怎么算都比不过电网。我们甘愿折腾多张卡、调推理引擎、抠
routing 策略，买回来的本来就是 API
给不了的三样东西：代码和数据不出本地、断网或限流也能照跑、以及亲手拆开系统看清
cache 与调度如何咬合的控制欲和乐趣。