大模型推理瓶颈不在计算本身,而在于每次生成都必须完整读取所有权重——这是内存带宽密集型任务。
你在 ChatGPT 里输入一个问题,圆点开始跳动。有时一两秒就出了答案,有时你盯着那个圆点看十秒钟,有时则会收到一条消息:太多请求了,稍后再试。
什么都没坏。只是你站在一条看不见的队伍里。这篇文章把那条队伍画了出来,不需要数字,只需要三张图。
机器会读取整份清单
AI 本身是一份非常长的数字清单。要产生一个答案,机器必须从头到尾读完整个清单,因为答案的每一个部分都触及清单的每一个部分。读清单才是慢的部分,为你的问题做运算其实很快。
这就是全部基础。如果这篇文章只让你记住一句话,就记住这句:贵的地方不是思考你的问题,而是把模型读一遍来思考任何问题。
(想要精确版本的读者:在小批量场景下,生成阶段受内存带宽限制。每一步权重都从卡的内存里流出,所以读取占主导,单个请求的计算量很小。下面的内容都是这个事实换了身衣服。)
没有队伍时:门瞬间关闭
假设机器同时能处理十六个请求,一百个人同时提问,而且前面没有队列。
十六个人进去了,剩下八十四个人立刻收到错误——在任何人还没得到回答之前。不是缓慢的拒绝,是瞬时的,因为没有队伍,超载无处立足。
这就是你看到过的其中一种消息。不是等待:是一扇关上的门。
队伍:拒绝变成等待
在机器前面放一个队列,就没有人会被拒绝了。每个人按顺序等待。这是关于队列第一个需要明白的道理:队列不会让任何事情变快,它只是把错误转换成等待。
但是机器会为每个人逐一读取同一份清单。一百个人就要读一百遍完全相同的数字,队尾的人要等所有人读完后才能轮到自己,看着那个跳动的圆点。
巴士:一趟读取,多人搭乘
所以 AI 服务会做批处理。不是每个问题过一遍,而是机器一次拿十六个问题,读一遍清单,十六个答案一起出来。一百个人变成了七趟读取,队尾的人的等待时间大幅缩短。
现在看看排第一的那个人。在一个安静的下午,他本可以单独上车,几乎立刻拿到答案。但现在巴士在等十五个陌生人上车。每一增加一个座位,一趟读取就分摊给更多人,第一个乘客的等待时间也就越长。
这就是每个 AI 服务内部真正可以调节的那根拨盘,它没有正确答案。小批次有利于排前面的人。大批次有利于大众和账单。有人决定巴士在站点等多久,而这个决定就是你曾经在对话框里经历过的每一次等待的一部分。
用代码来说,机器循环的朴素版本小得几乎不好意思:
while (true) {
List batch = new ArrayList<>();
batch.add(queue.take()); // wait for at least one rider
queue.drainTo(batch, SEATS - 1); // take whoever else is already in line
Answer[] answers = model.forward(batch); // ONE reading of the list
deliverAll(batch, answers); // everyone gets off together
}
真实的服务器有一个重要改进:它不会等到巴士坐满了才发车,也不会空车跑。座位在行驶过程中轮转——一旦有一个答案完成,下一个问题立刻上车,轮子从不停止。业界称之为 continuous batching(持续批处理)。同样的思路:一趟读取,多人搭乘,只是永远不站着等。
门:早点说 no
还剩一个问题。如果人们到来的速度比巴士离开的速度快呢?队伍会变长,不断变长,一个小时后才到的答案比没有答案更糟糕。
所以服务在门口就说 no,早点说,在队伍还短的时候。限速就是这么用的。太多请求不是机器坏了,是门在保护里面所有人,包括五分钟后的你——那时候你就站在队伍里了。
门如何决定谁可以进、谁被告知要等,这是另一套机制,那就是下一期的话题。
你站在哪里
所以下次圆点跳动的时候:你是在队伍里,等一辆快要坐满的巴士,如果你收到了那条消息,那是门早点说了 no,这样队伍才不会永远变长。
在你拿这些去设计评审之前诚实说一句:这是一幅图,不是一张蓝图。十六个座位是画图时选的数字,真实容量是每个部署的配置,取决于模型、显卡和对话长度,通常远高于此。里面的数字都不是实测值。形状才是论点:一趟读取分摊给多个问题、一个把拒绝变成等待的队列、以及一扇故意说 no 的门。
四分钟视频完整画出了这些: