每天烧掉近万亿 token,这句话放到一年前,我们团队自己都不太敢信。换算成直观的数字:一万亿 token 差不多等于每天要把上万本百万字的长篇小说完整喂给模型读一遍,再让模型吐一遍;折算到每秒,GPU 要完成上亿次 KV Cache 的读写。过去这套推理系统一直只服务内部业务线,最近才正式对外开放,而首个公开端点给的是 GLM-5.3。这篇文章不是发布会通稿,我也不打算讲套话,就把这几年从零撑起这套系统的关键设计、对外开放前重写的代码,以及 GLM-5.3 真正跑在生产链路之后的实测数据,全都摊开来讲一遍。
1. 万亿token不是PPT里的数字:这套系统被业务一步步逼着长大
1.1 最初的推理服务:一张卡就够用
先说背景。最早团队里只有一个业务线需要模型推理,场景也很单纯:给内部工具做文本摘要,请求量一天也就几万次。当时我们用主流的开源推理框架起了一个单机服务,一张旗舰级加速卡完全跑得动,请求排队时间几乎为零。那会儿压根没人觉得"推理系统"是个值得投入的事情,大家默认开源框架已经够好,差的那点性能也无所谓。
转折出现在第二个、第三个业务线接入之后。新业务带来的不是简单的量变,而是请求形态的彻底改变:有响应式对话,要求首字延迟低于 300 毫秒;有批量离线分析,一次提交几万条长文档,允许跑几分钟;还有实时助手类场景,输入上下文经常超过几万字。这些负载混在同一套服务里,开源框架默认的批处理策略很快就撑不住了。
1.2 为什么不能直接拿开源框架顶上去
我先把开源框架的账算清楚。主流的开源推理引擎确实把 GPU 利用率优化到了相当高的水平,但它的设计目标更偏向"单模型、单租户、负载相对均匀"的场合。而我们真实面对的混合负载有三个开源框架当时没法很好解决的点:
第一是长请求拖垮短请求。静态批处理机制下,一个批次的执行时间由最长的请求决定。批量推理任务动不动要跑几十秒,混进来一个对话请求,就得跟着这批长任务一起等,首 token 延迟直接飙到好几秒,这在对话场景里完全不可接受。
第二是显存浪费。长上下文请求的 KV Cache 会随着生成长度线性增长,但开源框架多数时候按照最大可能长度统一预留显存。短请求占大头时,预留的显存大部分被白白闲置;长请求一来,预留又不够,只能靠超发慢慢挤。我们做过统计,混部场景下的显存实际利用率一度不到 40%。
第三是缺少面向多租户的配额和计费能力。内部系统可以不管这些,但对外开放之后,API Key 体系、租户级 QPS 限制、按 token 计费,这些功能官方框架要么没有,要么属于商业版服务范围,自己改造的工程量巨大。
| 维度 | 开源框架直接部署 | 自研调度 + 内核复用 |
|---|---|---|
| 负载容忍度 | 均匀负载表现好,混合负载长尾严重 | 按请求长短分流,长短互不干扰 |
| 显存利用率 | 按最大长度预留,普遍低于 50% | 按页分配 + 动态整理,稳定 75% 以上 |
| 多租户能力 | 基本没有 | 原生支持配额、鉴权、计量 |
| 二次开发成本 | 改动底层需要长时间理解和验证 | 在最上层做策略,风险可控 |
1.3 自研的边界:不是推翻重来,而是把策略层握在自己手里
决定自研之后,我们内部其实吵过一轮:到底从哪一层开始写?有人觉得连算子都得自己写,有人觉得直接用开源内核加一层调度就行。最后定下的原则是:算子层和显存管理等底层机制继续复用经过验证的开源实现,自研重点放在请求调度、队列策略、弹性伸缩、多租户这些"上帝视角"的模块上。
这个取舍很重要。推理系统的性能天花板确实由底层内核决定,但对一家业务逻辑复杂、负载波动大的团队来说,真正能拉开差距的往往是上层策略:什么时候把一个请求塞进 GPU、哪些请求可以共享一个批次、显存紧张时先牺牲谁。这些策略如果只靠开源框架的默认行为,等于把话语权交了出去。
2. 吞下万亿token的三个技术支点:连续批处理、显存记账与分车道调度
2.1 连续批处理:把"等待"变成"穿插"
推理系统要撑住高吞吐,第一个必须解决的问题是批处理策略。传统静态批处理像旅游团的定点大巴:必须等一车人齐了才发车,上车之后有人要逛三小时,全车人都得陪到下午。实际场景里对话请求只需要几十毫秒的生成,批量任务可能要几分钟,把这两类请求装上同一辆车,体验必然灾难。
我们换成了连续批处理机制。核心思路很直接:不再等整个批次全部完成才调度下一批,而是任意一个请求生成结束,立刻把它的位置让给排在最前面的新请求。GPU 的计算单元一直被有效请求占满,等待只发生在队列里,不发生在显存里。
实现上最关键的参数是最大批大小和最大等待时间。我们把最大批大小设成动态值:短请求为主时,批大小可以冲得比较高;长请求占比升高时,批大小自动下调。最大等待时间则控制在 3 到 10 毫秒之间,宁可少等两个请求凑批,也不让已经排队的请求感知到明显延迟。这两个参数在开源框架里通常被配置为写死的常量,我们改成每 5 秒根据最近一分钟的请求分布自动调整一次。
2.2 KV Cache 记账:显存争夺的核心战场
推理系统的显存是比 GPU 算力更稀缺的资源。算力可以等,显存不够就直接内存溢出。一个大模型跑起来,模型权重占一块,激活值占一块,剩下的大头全是 KV Cache。通俗讲,KV Cache 就是模型在生成过程中给自己做的"小抄":每次生成新字都要看之前所有字的关键信息,如果每次都重新算一遍历史,成本高到无法接受,所以提前算好、缓存起来。
但"小抄"有多大?我们实际观测过一个上百亿参数规模的模型,在上下文长度达到三万 token 时,单个请求的 KV Cache 就能占到接近 2GB 显存。如果并发 100 个这样的长请求,光 KV Cache 就要吃掉 200GB,而这个数字会随着并发和上下文长度线性上涨。
所以我们把显存记账做成了类似于操作系统虚拟内存的机制。KV Cache 按固定大小的页分配,请求不会一次性占满全长度的显存,而是按实际生成进度逐步申请;当显存到达警戒水位时,已经空闲的页先回收,不够就把一些请求降级为重新计算。这套机制落地后,同样的八卡机器,在线并发能力比之前翻了一倍多。
2.3 分车道调度:给不同任务安排各自的优先级
解决了内核层面的大吞吐问题,系统对外呈现的延迟依然可能因为调度不当而崩坏。我们遇到过很典型的现象:系统吞吐看起来很高,GPU 利用率 95% 以上,但运营同学反馈对话体验卡顿。后来查才发现,批量离线任务把 GPU 全部吃满,对话请求全被堵在队列尾部。
解决办法是真正意义上的"分车道"。调度层把请求分成三个优先级队列:交互式请求走快车道,有抢占权,可以随时把算力从批量任务手里夺回来;普通请求走正常车道,按进入时间公平排队;离线任务走慢车道,只使用其他车道空闲时剩下的算力。
抢占看起来粗暴,但配合连续批处理之后非常有效。批量任务被抢占时并不需要真正中断执行,只需要暂停接收新的解码批次,已经算了一半的 token 继续算完。对话请求插进来后,几十毫秒内就能拿到首字,之后再退出去让批量任务接着跑。效果上,我们把对话场景的 p99 首 token 延迟从原来的 1.8 秒压到了 380 毫秒,同时批量任务的总吞吐只下降了 7%。
2.4 并行与量化:让模型占得更少,跑得更快
模型本身的部署方式同样决定了系统上限。单纯把一个大模型塞进一张卡不现实,我们用了两层手段来降低单位 token 的算力成本。
第一是张量并行。把模型的矩阵切到多张卡上,每张卡只算自己负责的那一部分,通过高速互联做汇总。并行度选多少,不是简单看卡的总显存,还要看互联带宽和切分比例。我们在实际部署中对比过单机八卡和两机十六卡两种模式,单机互联带宽充足时,张量并行度设为 8 性能最优;跨机器之后通信开销明显上升,就改为每机内部张量并行 4,再叠流水线并行。
第二是量化。FP8 是目前我们在生产环境的主选精度,相比 FP16 能将显存占用减半,计算速度提升约 40%。但量化不能一刀切,我们保留了注意力模块部分层为更高精度,其余线性层用 FP8。每次量化调整之后都会跑一遍固定的回归测试集,对比输出差异,差异控制在可接受范围才会上线。谁要是跟我说"量化无感知",我建议他先拿自己的业务日志跑一遍对比再下结论。
| 精度策略 | 显存占用 | 相对吞吐 | 输出一致性(与 FP16 对比) |
|---|---|---|---|
| FP16 全精度 | 基准 | 基准 | 完全相同 |
| FP8 全量化 | 约 50% | 提升约 40% | 偏差率约 0.1% |
| 关键层 FP16 + 其余 FP8 | 约 55% | 提升约 32% | 偏差率低于 0.01% |
3. 从内部工具到公有端点:对外开放前我重写的三块代码
3.1 鉴权与多租户隔离:先解决"谁在用"的问题
内部系统有个天然优势:所有调用者都是同事,出了问题可以直接拉群解决。对外开放就不行了,你不知道调你接口的是谁,也不知道对方会不会用一个文本生成接口做超大规模批处理,更不知道某个租户的异常流量会不会拖垮所有用户。
我们在网关上接入了完整的 API Key 鉴权体系。每个租户拿到的密钥绑定固定的模型端点、并发上限和月度 token 配额。网关在拿到请求后的第一件事不是转发,而是查一套本地缓存的租户元数据,校验签名、配额余量、模型白名单,全部通过才进入调度队列。
配额这块我特别想多说一点。很多系统的限额只做了 QPS,但 QPS 限额对推理服务是不够的:一个 10 token 的短请求和一个 50K token 的长请求,资源消耗差了上千倍。我们采用了双层配额:第一层限制每秒请求数,第二层限制每分钟消耗的 token 总量。任何一层被触发都会返回明确的限流状态码和重试时间,而不是把请求继续往队列里塞。
3.2 弹性伸缩与限流:别让流量洪峰冲垮调度器
内部自用阶段,我们从来没考虑过流量突刺。但对外开放后,公网上什么事情都有可能发生:某个应用自动重试的 bug 就能让请求量在几分钟内翻十倍。当时我们看到监控曲线直线拉升,第一反应不是高兴,而是害怕。
为此我们做了两层保护。第一层是弹性的节点池伸缩:当调度队列的平均等待时间连续 30 秒超过阈值,就触发新的 GPU 节点拉起;相反,当 GPU 平均利用率低于 20% 持续 10 分钟,就逐步回收节点。伸缩的触发条件不是简单的 CPU 负载,而是"队列等待时间",因为这个指标直接反映用户体验。
第二层是令牌桶限流。每个租户一个令牌桶,桶容量对应短时间内的突发许可,每秒补充速率对应长期平均上限。之前我们犹豫过要不要给批量任务也设限流,后来发现必须得设:一个离线任务如果一次提交几百万 token 的请求,不加以限制的话会瞬间占满连续批处理的所有空位,把其他所有租户都挤到队尾。
3.3 可观测性重构:从"能跑"到"能定位问题"
内部系统时代,我们的可观测性几乎为零,最多看一眼 GPU 利用率。对外开放之后,问题定位的难度完全变了。用户反馈"最近变慢了",你得快速回答是哪个租户、哪个模型、哪个节点的哪一环出了问题。
我们在三个层面重新搭了可观测体系:
指标层:每个端点暴露吞吐量、首 token 延迟、解码速度、排队长度、GPU 显存水位、队列等待时间。所有这些指标按租户、模型、节点三个维度打标签,任何一个维度异常都能快速下钻。
链路层:每次推理请求从进入网关到返回完整结果,生成一条完整追踪链。我把"推理"这件事拆成了网关排队、调度等待、prefill 计算、decode 迭代、返回传输五个阶段,每一阶段都能单独看耗时。首 token 延迟如果恶化,直接看是卡在调度等待还是卡在 prefill 计算,而不是凭感觉猜。
日志层:请求级别结构化日志记录上下文长度、生成 token 数、实际耗时、是否被抢占。这些日志的另外一个重要用途是成本归因:月底核对账单时,可以清楚看到每一个租户消耗了多少 token、占用了多少 GPU 时。
对外开放后第一周,这套可观测性体系就救了命。有用户反馈特定时间段内频繁超时,我们通过链路追踪发现是某个节点 GC 停顿导致 prefill 普遍慢了 200 毫秒,十分钟内定位并隔离了问题节点,这种速度在之前是不可想象的。
4. GLM-5.3成为首个公开端点的原因与接入实测
4.1 为什么先把端点给 GLM-5.3
不少朋友好奇,对外开放为什么不挑一个更"热门"的模型,而是把首个端点给 GLM-5.3。理由其实很朴素:这个模型在我们内部已经跑了大半年,是验证最充分、踩坑最少、生产环境最熟悉的一个。
从技术特性上看,GLM-5.3 的长文本能力和工具调用能力与我们业务契合度最高。内部很多场景需要把几十页资料一次性喂进去做分析,早先几个模型在上下文一长之后,回复质量和稳定性都会明显下滑,而 GLM-5.3 在几万 token 上下文下的表现仍然稳定。另外它的整体架构是混合专家结构,激活参数比例控制得比较合理,在连续批处理模式下显存压力明显小于同等规模的稠密模型。
我尤其看重的是部署层面的成熟度。模型社区对它的生态适配做得比较到位,主流推理框架都能直接加载,量化支持也好,不需要我们在最底层做太多额外改造。对一个团队规模有限、需要同时维护整套推理平台的我们来说,"省事"本身就是最大的成本优势。
4.2 部署规格与显存规划
切入生产环境之前,我们按照前面说的显存记账模型算了一笔账。GLM-5.3 的模型权重在 FP8 量化后大约占用 110GB,为了给 KV Cache 和激活值留出充足空间,没有选择四卡方案,而是用单机八卡部署,张量并行度设 8,单卡显存 80GB。这样模型权重摊到八张卡后每卡约 14GB,剩下约 66GB 用于 KV Cache 和临时计算,整体调度余量充裕。
| 配置项 | 生产环境取值 | 说明 |
|---|---|---|
| 单节点加速卡数 | 8 | 张量并行度 8,避免跨机通信 |
| 权重精度 | FP8 | 保留部分注意力层为更高精度 |
| 最大批大小 | 动态,64–256 | 随请求分布自动调整 |
| 并发请求上限 | 500 | 超过后排队,不再增加显存压力 |
| 最大上下文长度 | 128K | 超过的请求直接拒绝 |
| KV Cache 显存配额 | 每卡约 50GB | 超出后按页回收并降级 |
这套配置下线后,我们用自研压测脚本造了四种负载:短对话、中等长度问答、单请求长文档分析、混合并发。跑出来的数据比预期好,首 token 延迟在混合负载下稳定在 300 毫秒上下,整体吞吐折合每卡每秒超过 4000 token,属于我们内部所有模型中效率最高的一个。
4.3 联调中遇到的三个坑:真实生产从来不会一帆风顺
第一个坑出现在 prefill 阶段。压测发现当并发从 300 升到 500 时,首 token 延迟出现了断崖式上涨,从 350 毫秒直接跳到 1.5 秒。定位后发现是我们的调度器把所有请求一股脑塞给了 prefill 计算单元,长文档的 prefill 计算量极大,单个请求就可能占用几十毫秒甚至几百毫秒的算力,短对话的 prefill 被挤压在后面排队。
解决办法是给 prefill 单独设置并行度上限,并且使用"抢占式插入":短请求的 prefill 可以插队到长请求 prefill 之前。改动后首 token 延迟的 p99 回到 380 毫秒以内,长文档批处理的整体耗时只增加了 5% 左右。
第二个坑是长上下文带来的显存膨胀。上线第二天,就有用户尝试接近 128K 上限的长文档,几个请求就把整个节点的显存顶到了红色警戒线。我们紧急检查发现,KV Cache 记账模块在极端长上下文下出现了一个页分配不回收的边界 bug,导致部分显存被无效占用。修复并彻底调优之后,我们把最大上下文限制调整成了"默认 64K,白名单租户可申请 128K",防止普通用户一个请求把共享资源全部吃光。
第三个坑不算 bug,但更隐蔽:量化后的模型输出在部分复杂推理题目上和全精度版本存在肉眼可见的差异,虽然 token 级别偏差率不足 0.1%,但落到具体问题上就是有细微的逻辑跳变。这里的经验是量化上线前必须用业务真实数据做回归测试,通用测试集跑得再漂亮,也不代表你的业务场景没有影响。
4.4 对外开放后的首轮压测数据
GLM-5.3 端点正式对公网开放后,我们做了一轮持续 72 小时的观察。那几天真实流量混合了国内外不同地区、不同时区的用户请求,负载曲线比我们内部压测复杂得多。最终数据如下:
| 指标 | 目标值 | 实测值 |
|---|---|---|
| 首 token 延迟 p95 | ≤ 500ms | 412ms |
| 首 token 延迟 p99 | ≤ 800ms | 760ms |
| 解码速度(全体用户平均) | ≥ 50 token/s | 63 token/s |
| 端到端成功率 | ≥ 99.5% | 99.83% |
| 单节点峰值吞吐 | ≥ 25K token/s | 33.7K token/s |
这个成绩比内部压测略低,主要原因是公网传输延迟和用户请求长度的随机波动,但整体已经达到商用标准。端到端成功率 99.83% 里的失败请求,绝大多数来自超长上下文的主动拒绝和客户端断连,真正因为服务端故障导致的失败率低于万分之二。
5. 开放推理系统之后:成本、故障与一份自检清单
5.1 成本结构:GPU 是最贵的,但最贵的不止是 GPU
对外开放之后,我每个月最关心的表换成了成本结构表。推理系统的成本大头确实是 GPU 采购或租用费,但只看 GPU 数字会严重低估整套系统的开销。
以我们当前的规模,成本大致由四块构成:GPU 算力与显存,占比最高,大约 65%;数据中心的电力与散热,占比约 15%;存储和网络传输,占 10%;剩下的 10% 是运维、监控、调度组件的开发和维护人力摊薄。第一眼看上去 GPU 是大头,但真正让我们担心的是后三块,它们不会因为 GPU 利用率高就自动降低。
算力效率也要拆开看。系统对外公布的吞吐很高,但那是理想负载下的数字;实际上不同租户的请求长度差异很大,token 的"含金量"也不一样。一个首 token 响应极快、输出只有几十字的消息助手,和一个动辄生成几千字长文的写作助手,两者消耗的 GPU 时差异超过百倍。我们在成本结算时按照真实占用的 GPU 时和 token 数双重计费,才勉强做到成本可解释。
开放三个月之后,我们做了内部复盘,发现超长上下文请求虽然只占 8% 的总请求量,却消耗了 35% 以上的算力资源。这个发现直接推动了产品策略调整:给超长上下文单独设档位定价,同时在调度上把长请求分散到不同时段,尽量避开短请求的高峰。不然的话,短请求用户会持续为少数重度用户的资源消耗买单,最终谁也留不住。
5.2 故障处理经验:从"能修好"到"尽量无感"
对外开放之前,我以为最大的挑战是性能。真实上线之后才发现,性能只是入场券,稳定性和故障自愈才是口碑的生死线。
我们遇到过 GPU 卡健康状态劣化、偶发的推理进程被杀掉、网络抖动导致的批量超时。每一次故障过后,我们都会问自己一个问题:用户真的需要感知到这件事吗?带着这个问题,我们把系统的目标从"尽快修复"调整为"尽量无感"。具体做法包括:节点级别的心跳探活和自动摘除,推理进程异常退出后 30 秒内自动重启并重新加载模型,请求排队超过设定时间后直接返回明确错误码让客户端快速重试而不是无限挂起。
还有一件容易被忽略的事:降级预案。当集群整体负载过高,无法承载所有流量时,我们要能按照租户等级和请求类型做有序降级。批量任务先推迟,普通交互请求降速,核心付费租户的请求永远优先。这个预案在两个月前的一次流量突刺中真的发挥了作用,虽然整体延迟略有上升,但核心用户没有一个感知到服务不可用。
5.3 对外开放前的一份自检清单
很多朋友在后台问我,我的推理服务也要对外了,应该先检查什么?我整理了一份自检清单,倒不一定全面,但都是我们真金白银换来的教训:
- [ ] 鉴权是否覆盖每个端点?有没有人绕过网关直接访问后端节点?
- [ ] 配额限制是 QPS 还是 token 双层?单次超长请求的上限设了没有?
- [ ] 调度队列在热点争抢时,能不能保证交互式请求的优先级?
- [ ] KV Cache 超出后是优雅降级还是直接内存溢出?
- [ ] 有没有按租户、模型、节点三个维度打标的完整指标?
- [ ] 链路追踪能否定位到一次请求的五个阶段分别耗时多少?
- [ ] 节点异常退出后,流量摘除和拉起新节点能否在 5 分钟内自动完成?
- [ ] 成本账单能不能把 GPU 时和 token 数归因到具体租户?
- [ ] 是否和客户端约定好了限流、超时、重试的标准响应格式?
- [ ] 压测通过不等于上线没问题,是否预留了 72 小时的灰度观察窗口?
这十条我们没有全部做到就对外开放了,所以后来补得很辛苦。如果让我重来一次,我会把这些事项全部标注为"上线阻塞项",而不是"后续优化项"。
6. 最后分享一点方法论上的体会
这套系统对外开放几个月后,我最深的感受是:推理系统的门槛不在"有没有模型",甚至不在"GPU 多不多",而在你愿不愿意把那些看起来微不足道的细节抠到极致。连续批处理大家都懂,但动态批大小、分车道抢占、KV Cache 显存记账、租户级 token 配额,每一层都要在真实负载下反复校准参数。GLM-5.3 能成为第一个公开端点,也不是因为它天然适合,而是我们内部已经跟它磨合了足够久,久到清楚它在每种请求形态下的脾气。
如果你也在做类似的事情,我的建议是:先把一个模型、一个端点的全链路吃透,再谈规模化;先把成本账、限流策略、异常降级设计清楚,再谈对外开放。技术上没有银弹,但有耐心和充足经验的团队,总是能把系统磨得比昨天更稳一点。下一步我们计划把另外几个模型也逐步开放出来,但前提是它们能在显存规划、输出一致性和运维自动化三方面都通过跟 GLM-5.3 一样的考核标准。