第 0 章 先把地图画出来
你在第 1 章读到"推理是模型生命周期里的第二个阶段",这句话对写的人很清楚,对第一次接触的人却是空的:第一个阶段是什么,为什么叫阶段,模型为什么有生命周期。同样,"三层"那一节默认你知道什么是抽象、什么是平台、什么是扩缩,否则那三段话只是三个陌生名词的排列。
这一章不解决具体问题,只做一件事:把后面会用到的词,按它们真实的关系摆成一张图。读完你未必能回答任何技术问题,但你应当知道每个词大概站在什么位置,看到新词时能判断它属于哪一块。这张图以后每章都会用到,读不下去的时候可以回到这里。
一台电脑在做什么
要理解推理,得先从电脑怎么工作说起。
一段程序,本质上是两样东西:一组指令,和一堆数据。指令规定了要做什么运算,数据是被运算的对象。程序运行时,数据和指令都要放在内存里,处理器从内存里取指令、取数据、算完再写回去。处理器叫 CPU,它擅长的是"一件事接一件事地做",每件事可能很复杂,但同一时刻只处理很少几件。
内存有两个特点,理解它们对后面很重要。第一,它比处理器慢得多:处理器做一次加法的时间,内存可能只够搬几个字节。第二,它是分层的:紧挨着处理器的缓存很快但很小,主内存很大但慢,硬盘更大更慢。程序跑得快不快,很多时候不取决于算得多快,而取决于数据离处理器有多远。
"进程"是另一层概念。一个进程是操作系统给程序划出的一块独立空间,里面有它自己的内存、自己的运行状态。两个进程默认看不见对方的内存,想交换数据必须通过操作系统提供的通道。后面讲多卡时,"每个 GPU 一个进程"这句话会反复出现,原因就在这里:进程之间不共享内存,所以数据要从一张卡到另一张卡,就得走通信。
存储也是分层的,而且层与层之间差得很远。紧挨处理器的寄存器几乎 instantaneous,往下一级是缓存,容量几 MB,再往下是主内存,几十到几百 GB,最下面是硬盘,几 TB。每往下一级,容量大约涨一个数量级,速度大约降一个数量级。程序写得快不快,很大程度上取决于它要用的数据是不是正好在快的那一层;这一点在 GPU 上更极端,因为显存不够时没有"挪到硬盘"这条退路。
GPU 为什么不一样
显卡最初是为画图设计的,画图的特点是同一套运算要作用在几百万个像素上,每个像素的计算互不相干。这种"大量相同的、简单的、互不依赖的运算"正好是 CPU 不擅长的,因为它一次只能做很少几件复杂的事。GPU 反过来:它有几千个简单的小核心,一次能做几千件简单的事。
深度学习需要的运算恰好是这种形状:矩阵乘法,把同一个权重矩阵作用在一大批数字上。所以 GPU 成了训练和推理的主力。
以一张主流的 A100 为例:显存 80GB,显存带宽约 2039 GB/s,FP16 算力约 312 TFLOPS。同一台服务器的主内存带宽通常只有几十 GB/s,两者差两个数量级。算力和带宽这两个词后面会反复出现,它们分别对应"算得多快"和"搬得多快",很多性能问题就是这两个数中的一个先到顶。
GPU 有自己的内存,叫显存。显存和主内存的区别值得记住三条。容量小得多:一张主流数据中心显卡是 80GB 左右,而一台服务器的内存可能是它的十倍。带宽高得多:显存每秒能搬几 TB 数据,主内存只有几十到几百 GB。最重要的是第三条:显存不能像内存那样随便换到硬盘上。操作系统可以把不常用的内存页写到硬盘,程序感觉不到;显存一旦不够,分配就直接失败,程序崩溃。这就是后面反复出现的 OOM。
所以"显存多大"和"显存多快"是两件事,对应两个不同的问题:装不装得下,和跑不跑得快。后面每一章几乎都在这两个问题之间来回。
数字、字节和精度
补一段最底层的东西,因为后面所有的显存账都建立在它上面。
计算机里的数字都是二进制。八位二进制叫一个字节,能表示 0 到 255 的整数。要表示更大的数,就用更多字节:四个字节能表示到四十多亿。
小数不能用整数的方式存。浮点数把一个数拆成三部分:符号、指数、尾数。指数决定这个数能有多大,尾数决定它有多少位有效数字。位数分得多与少,就是不同的精度格式。
单精度浮点 FP32 用四个字节,半精度 FP16 用两个字节,BF16 也是两个字节但位数分配不同,FP8 用一个字节。字节数越少,能表示的范围和精度越差,但占的空间和搬运的量也越少。
模型参数通常用 FP16 存。于是"七十亿参数"可以直接换成体积:每个参数两个字节,一共一百四十亿字节,约 14 GB。这就是第 1 章那个数字的来历。它不需要跑一遍程序,只需要知道参数量和每个参数几个字节。
后面讲量化,讲的就是把这个字节数继续往下压:从两个字节压到一个,再压到半个。每压一半,显存和搬运量都减半,代价是精度。
模型是一堆数字
"模型"这个词容易让人以为它是一个程序,其实不是。
一个训练好的语言模型,主体是一大堆数字,叫参数或权重。这些数字按固定的大小分组,每组是一个矩阵。模型文件就是这些数字的打包,几个 G 到几百 G 不等。
"7B"的意思是七十亿个参数。参数越多,模型能表达的东西越复杂,但需要的显存也越多,每次运算要搬的数字也越多。这就是为什么参数规模是一个绕不开的指标。
模型除了数字,还有一段固定的计算流程:先做什么运算,再做什么运算,数据怎么在层与层之间流动。这段流程由框架代码实现,不由模型文件携带。所以"跑一个模型"实际上是两件事:把数字加载进显存,然后按固定流程让数据流过这些数字。
流程里最重要的运算是矩阵乘法。不用怕这个词,它的意思就是"一批乘法加在一起"。举个最小的例子:输入是两个数,权重是一个两行三列的表格,那么输出就是三个数,每一个都是输入的两个数分别乘以表格里对应的一列再相加。输入是一条请求,表格是权重;表格越大,乘加的次数越多,这就是"计算量"的来源。模型里的矩阵有几百万行几百万列,但做的事情和这个最小例子完全一样,只是规模不同。
加载这个词后面会经常出现。它指的是把模型文件从硬盘读进显存,这个过程可能要几十秒,因为几个 G 的数据要经过总线和内存中转。
训练与推理:改数字,和用数字
现在可以回答"第二个阶段"了。
一个模型的生命周期有两个阶段。第一个阶段是训练:给模型看大量文本,让它预测下一个词,预测错了就微调参数,让下次预测得准一点。这个过程要反复很多次,每次都要计算误差、把误差反向传播回去、修改数字。它需要保存额外的中间结果和优化器状态,显存开销是推理的好几倍,通常要在成百上千张卡上跑几个星期。
第二个阶段是推理:参数已经固定,不再修改。输入一段文本,模型算出下一个词的概率,选一个输出,再把这个词接回输入,继续算下一个。整个过程只做前向计算,不修改任何参数。
这个区分决定了后面所有的讨论。训练关心的是"多久能训完""能不能用更多卡加速";推理关心的是"一次请求多快""一台机器能同时服务多少人""每个 token 花多少钱"。两边的优化手段经常互相冲突,第 1 章讲的 serving inversion 就是这个意思。
显存开销的差别也来自这里。训练除了权重,还要保存每一层的中间结果(用于反向传播)、每个参数的梯度、以及优化器自己的状态。三者加起来往往是权重的好几倍,所以一个 7B 模型训练时可能要占几百 GB,而推理只需要十几 GB。这也是为什么消费级显卡能跑推理、却基本不能训练大模型。
一个类比可以帮助记住:训练是写一本书,推理是把书写好的内容讲给一个个读者听。写的时候慢一点没关系,讲的时候每个听众都在等。
一次推理发生了什么
把镜头推到一次请求,可以看到一条固定路径。
用户输入一段文字。文字先被切成 token,每个 token 是一个整数编号。这些编号被查表变成向量,也就是一串数字。向量进入模型,一层一层往上走,每层做两次主要运算:注意力让当前位置参考前面的内容,前馈网络对每个位置单独做变换。走到最后一层,得到一个向量,再乘以输出矩阵,得到词表长度的分数,也就是每个候选词的可能性。
从这些分数里选一个词,输出给用户。然后把这个词接到输入后面,重复上述过程,直到生成结束符或者达到长度上限。这个"一次只生成一个词、再把词接回去"的过程叫自回归。
这里有两个词会一直用到。预填充指的是把用户输入的整段话一次性送进模型,算出第一个输出词。逐字生成指的是之后的每一步只处理一个新词。前者计算量大、可以并行;后者每步计算量小,但要反复读取全部参数,所以慢。第 1 章的整条时间账,都是在这两段上算的。
服务是什么
到目前为止说的都是一次计算。真实场景里,程序不是跑一次就结束,而是长期开着,等着别人来调用。这个东西叫服务。
服务的基本形态是:一个常驻程序,监听一个网络地址,收到请求就处理,处理完把结果返回。请求和响应之间可能隔着网络,也可能就在本机。一个服务同一时刻能处理多个请求,这个能力叫并发。
程序之间说话靠接口。接口是一组约定:我用什么格式发、你用什么格式回、出错时返回什么。网页调用模型用的是 HTTP,请求体里通常是 JSON,一段结构化的文本,写明模型名、输入内容、要不要流式返回。接口的好处是两边可以各自升级,只要约定不变。这也是为什么换一个推理引擎,客户端往往不用改代码:它只认接口,不认引擎。
并发带来排队。机器能同时处理的请求数有上限,超出的请求必须等。排队时间不属于计算,但用户会把它算进等待里,所以它和计算一样影响体验。第 1 章说"三种慢"里的首字慢,有一半来自这里。
服务还要处理异常:请求太大、超时、显存不够、进程崩溃。这些不是模型的问题,是系统的问题。区分"模型算得慢"和"服务没接住",是后面很多判断的起点。
异常处理里有一个反直觉的地方:重试不一定让事情变好。当服务已经过载时,客户端超时后重试,等于把同一份流量又发了一遍,队列更长,超时更多,更多重试。这个问题有个名字叫重试风暴,是很多线上事故的直接原因。所以超时时间、重试次数、限流阈值必须一起设,不能单独调。
规模的三级
同样的推理,放在不同规模上,问题完全不同。
第一级是一张卡。一张显卡、一个模型、一个进程。所有优化都在这张卡内部:怎么算得快,怎么放得下。
第二级是一台机器。一台服务器通常装八张卡,卡与卡之间用专门的高速通道连接,带宽比普通网络高一个数量级。模型大到一张卡装不下时,就切到八张卡上,代价是卡与卡之间要同步数据。这个同步很贵,所以切分方式会显著影响性能。
第三级是集群。一台机器装不下,就用多台机器,机器之间通过网络连接,带宽又低一个数量级。到了这一级,网络拓扑、机架、交换机都开始影响结果,故障也变成常态而不是例外。
把三级连接速度放在一起看会更清楚:卡与卡之间每秒几百 GB,机器与机器之间每秒十几 GB,差一个数量级;而延迟也从微秒级升到毫秒级。同样的数据量,跨机传输的时间可能是卡内的几十倍。这就是为什么"加机器"从来不是线性的:多一台机器,多一份算力,也多一份通信。
这三级的差别不只是"更多",而是"更慢的连接"和"更容易坏"。后面第 10 章讲并行,讲的就是怎么在越来越慢的连接上切分计算。
层次:为什么有这么多名字
第 1 章那段"三层"之所以难读,是因为它默认你理解"层"这个概念。这里补上。
计算机系统的组织方式是层层叠加。最底层是硬件,往上是驱动,再往上是编程接口,再往上是框架,最上面是给人用的工具。每一层都替上面一层藏起下面的细节。
举个例子。你用 Python 写 a + b,不需要知道这两个数在内存的哪个地址、由哪个核心计算、用几个时钟周期。Python 替你藏起了这些。这就是"抽象":用简单的接口盖住复杂的实现。
抽象有个通病:藏得不够干净。平时它替你省事,出问题时它把线索也一起藏了。你看到的是"服务变慢了",看不到是哪个 kernel(在 GPU 上执行的一段程序)、哪次内存分配、哪台机器。所以排查问题的过程,往往是一层层往下钻,直到找到一个能直接观察的现象。后面每一章都会告诉你,遇到某类症状该往哪一层钻。
推理这条线上,层次大概是这样:最下面是 GPU 硬件;往上是 CUDA,它让你能直接指挥 GPU 做运算;再往上是 PyTorch 这类框架,它把运算组织成张量和层;再往上是推理引擎,比如 vLLM、SGLang、TensorRT-LLM,它们处理批处理、缓存、调度这些服务端问题;最上面是平台和工具,负责部署、扩缩、监控。
CUDA 这一层的运算通常被打包成算子,一个算子就是一段在 GPU 上执行的函数,负责一类运算。softmax 是其中一个,作用是把一组分数变成一组加起来等于一的概率;矩阵乘、归一化、激活函数也都各有算子。推理引擎的快慢,很多时候取决于它调用的算子实现得好不好。
现在回头看"三层",它其实是按"问题属于哪一类"来分的,不是按代码在哪一层。
运行时(runtime)回答的是:一个模型在一块卡上,能不能跑得足够快。它覆盖的是从 CUDA 到推理引擎这一段,关心的是单次计算的时间和显存。
基础设施(infrastructure)回答的是:一台机器不够了怎么办。它关心的是机器数量、流量分配、故障恢复,覆盖的是集群和网络这一段。
工具(tooling)回答的是:人怎么控制上面两层。它关心的是接口是否好用、改一个配置要多久、出了问题能不能查,覆盖的是平台和运维这一段。
一个服务倒下,可能因为运行时里的一个参数设错,也可能因为基础设施没有及时扩容,还可能因为工具层只能整体发布、没法单独回滚。三层是三种病因,不是三种技术。
分层是为了分工
既然每层都在藏细节,为什么还要分这么多层?因为每层的故障长得很不一样,处理它们的人也不一样。
运行时的问题表现为:同一条请求变慢了,或者显存不够了。判断依据是延迟、吞吐、显存占用。处理手段是换参数、换引擎、改量化。
基础设施的问题表现为:一部分请求失败,或者整体容量不够。判断依据是错误率、队列长度、机器数量。处理手段是扩缩容、限流、切换区域。
工具层的问题表现为:改一个东西要很久,或者出了问题查不到原因。判断依据是发布频率、回滚时间、日志完整度。处理手段是改流程、改平台。
一个人不可能同时精通这三层。现实中的分工也是这样:写 kernel 的人、做集群的人、做平台的人,技能树几乎不重叠。这本书主要讲第一层,但会不断提醒你另外两层在哪儿,因为很多"优化没效果"的案例,根因都在别的层。
走一遍:从点击到回答
把前面所有的词串起来,走一遍完整链路。
用户在手机上打字,点发送。请求经过网络到达一台服务器。服务器上有一个常驻进程,它监听端口,收到请求,把请求放进队列。队列的作用是削峰:来得太快时先存着,慢慢处理。
轮到这条请求时,进程把文字切成 token,查表变成向量,送进 GPU。GPU 从显存里读出模型参数,一层一层计算。预填充阶段把整段输入算完,得到第一个输出词;之后的逐字生成阶段,每次只算一个新词,每一步都要把全部参数重新读一遍。
生成过程中,结果通过网络一点点回传给用户,这就是流式输出。用户看到的"一个字一个字蹦出来",对应的是 GPU 每算完一步就发一次。
如果同时来的人太多,队列会变长,等待时间上升。如果显存不够,新的请求会被拒绝或者挤掉旧的。如果机器忙不过来,基础设施层会再启动一台机器,把流量分过去,这个过程叫扩缩容。
整条链路上,任何一个环节慢了,用户都会觉得"这个产品卡"。而排查的第一步,是判断卡在哪一层。
这本书怎么用这张地图
后面每一章都会落到这张图的某一段上。第 1 章讲一次请求的完整时间和预算;第 2 章补 token、注意力、精度这些零件;第 3 章把零件装进具体模型;第 4 章讲硬件这一层的算力、带宽和互联;第 5 章到第 10 章讲运行时里的各种手段;第 11 章回到度量;第 12 章走一遍完整判断。
读的时候如果遇到一个没见过的词,先别停,试着判断它属于哪一层:是单卡计算的事,是机器数量的事,还是人怎么用的事。判断对了,即使不知道细节,也不会迷失。这本书不要求你记住所有名词,只要求你在遇到问题时知道该往哪一层找。