输入一句话,按下回车,等几秒钟,看着答案一个字一个字出现。
这个动作太熟悉了,熟悉到我们容易把它理解成一次函数调用:把 prompt 交给模型,模型返回答案。但如果沿着这次请求走一遍,你会遇到浏览器事件循环、操作系统调度、互联网协议、数据库、排队论、编译器、GPU 存储层次,以及多台机器之间的协作。
再往模型里面走,文字变成整数,整数变成向量,向量穿过几十层乃至更多层网络;概率分布决定下一个 token,而下一个 token 又成为下一轮计算的输入。
这是一道可以把计算机科学串起来的问题:一条信息如何被表示、传输、计算、调度,并最终可靠地交到人手中。
我们以一个常见场景为主线:你在云端聊天产品里输入「请用三句话解释月亮为什么会有阴晴圆缺」,使用一个已经加载到 GPU 上的、自回归的 decoder-only Transformer 生成文本。随后再把检索、多模态和 Agent 加进来。具体产品的实现会有差异,但这条主线足以解释多数关键机制。
目录
- 先看全景:这几秒钟里有三条线
- 回车先抵达浏览器与操作系统
- 请求怎样穿过网络
- 到达服务端之后先排队和过门
- 真正交给模型的输入是怎样组装的
- 文字怎样变成模型能计算的数
- 一次 Transformer 前向计算做了什么
- Prefill 与 KV cache:先把已知内容算一遍
- Decode:答案为什么要一步一步生成
- 推理框架怎样让很多人共享 GPU
- 从张量程序到 CUDA 机器指令
- GPU 内部到底在忙什么
- 模型放不下一张卡之后
- Token 怎样重新变成屏幕上的字
- Agent 把一次调用变成一个循环
- 怎样解释快慢并定位故障
- 沿着这次请求重新认识 CS 课程
- 参考资料与继续阅读
先看全景:这几秒钟里有三条线
第一条是请求与数据的流动:浏览器把消息发给服务端,服务端组装上下文,把它交给模型,再把输出送回来。
第二条是计算的展开:tokenization、向量表示、注意力、矩阵乘法、采样,以及循环生成。这条线回答「答案是怎么算出来的」。
第三条是资源与控制的分配:谁可以调用、由哪个模型回答、在哪台机器执行、能占用多少显存、是否需要工具、遇到超时怎么办。这条线决定了一个模型能否成为可用的产品。
图 1:一次云端文本生成的典型路径。检索、分类和工具调用是按需分支;鉴权、调度和数据传输并不会因为界面只显示一个聊天框就消失。
还要把时间分成两种:
- 请求发生前:模型已经完成训练,权重通常已经下载、加载;服务还可能完成编译、预热和 CUDA Graph 捕获。
- 请求发生后:系统处理当前输入,用已有权重运行推理,为当前请求建立中间状态,逐步输出。
正常聊天推理通常不会为每个用户重新训练模型,也不会每生成一个 token 就从磁盘读取整套权重。冷启动、模型切换、显存卸载等情况会改变这条路径,后面再展开。
回车先抵达浏览器与操作系统
对应的课程是人机交互、Web 技术与操作系统。
键盘产生输入事件,操作系统的输入系统和窗口系统把它交给当前应用。对中文输入,还要区分「确认输入法候选词」和「发送消息」:页面一般需要检查输入法组合状态,避免你刚选好一个字就把消息发出去。
浏览器里的事件处理函数读取输入框,检查内容,更新本地界面,然后准备请求。你通常会先看到自己的消息出现在聊天记录里,再看到远端回复。这是前端先更新状态带来的即时反馈。
消息一般会被序列化成 JSON。一个用于说明结构、并不代表某家接口协议的请求大致是:
{
"conversation_id": "example-conversation",
"messages": [
{ "role": "user", "content": "请用三句话解释月亮为什么会有阴晴圆缺。" }
],
"stream": true
}
有些产品发送完整历史,有些只发送最新消息和会话 ID,由服务端读出历史。页面通常还携带登录凭证、请求标识、模型选择和附件引用。对计费接口,密钥应由有权限的服务端管理,不能把服务端秘密放进公开网页脚本。
浏览器不是一个单线程函数从头跑到尾。它有主线程、网络处理和渲染等分工。JavaScript 的事件循环安排回调;网络请求发出后,代码可以通过异步接口等待结果,主线程继续处理滚动、点击和页面绘制。如果把大量同步工作塞在主线程,服务器再快,界面也可能卡住。
往下看,发送数据需要经过系统调用进入操作系统。进程有独立的虚拟地址空间,内核管理 socket、缓冲区和 CPU 时间。应用调用「发送」只表示数据交给了本地协议栈,不能据此断言远端模型已经收到。
这里已经出现了系统设计的第一个习惯:区分调用返回、工作完成和结果可见。 后面 GPU 的异步执行、分布式工具调用、浏览器绘制,都会反复遇到这个区别。
请求怎样穿过网络
对应的课程是计算机网络、密码学与网络安全。
找到地址再建立安全通道
如果没有可复用的解析结果,客户端先通过 DNS 找到域名对应的地址。DNS 通常把你带到一个入口、边缘节点或负载均衡器,GPU 服务器可能还在后面的另一层网络里。
如果没有可复用的连接,还需要建立传输与安全会话。常见路径包括:
- HTTP/1.1、HTTP/2 通常运行在 TCP 与 TLS 之上。
- HTTP/3 把 HTTP 映射到 QUIC;QUIC 使用 UDP 承载,同时提供可靠流、拥塞控制,并集成 TLS 1.3 握手。
所以「用了 UDP」并不等于「请求允许随便丢」。可靠性可以由 QUIC 这一层实现。HTTP/3 的协议定义见 RFC 9114。
打开聊天页面时,DNS、TLS 和连接建立可能已经完成。按下回车时往往直接复用连接。把每次请求都描述成「先 DNS,再 TCP 三次握手,再 TLS」会把首次访问与后续请求混在一起。
网络搬运字节并处理丢失与拥塞
请求数据会被封装,穿过本机网络栈、Wi-Fi 或有线链路、路由器和运营商网络。不同层有不同的数据单位;一个应用消息不一定对应一个网络包。
网络要同时解决两个问题:数据是否到齐,以及发送速度是否会把路径压垮。确认、重传、流量控制和拥塞控制各有职责。TLS 保护的是通信端点之间的机密性与完整性;如果连接在边缘网关终止,后面的内部链路需要自己的保护。
HTTP/2 可以在一条连接上复用多个流,但底层 TCP 丢包仍可能阻塞这条连接中后续字节的交付。QUIC 通过独立流交付减少这种跨流的队头阻塞,不过丢包恢复、共享链路容量和拥塞控制仍然影响延迟。
回答只有几 KB 时,网络不一定是最大开销。跨洲、移动网络、握手、代理缓冲和连接不稳定,仍然可能明显影响第一段文字什么时候出现。不能只看答案体积小,就把网络从延迟账单里删掉。
到达服务端之后先排队和过门
对应的课程是操作系统、服务端编程、分布式系统与排队论。
一个公共入口通常先做 TLS 终止、路由、身份验证、配额检查和限流。这里几种检查要分清:
- 鉴权判断「你是谁、能访问什么」。
- 限流限制某个时间窗口内的请求或 token 消耗。
- 准入控制判断当前系统是否还有能力接下这次工作。
- 调度决定已经接纳的工作先执行谁。
GPU 容量耗尽时,继续无限收请求只会把队列变长。队列过长,用户超时重试,流量进一步放大,服务可能在尚未崩溃前就已经失去可用性。有限队列、截止时间、退避与熔断,都是让系统维持秩序的办法。
CPU 没有因为用了 GPU 就退出舞台
请求到达网卡后,数据经由驱动、协议栈和 socket 进入服务进程。典型系统使用 DMA 把数据写入内存,再结合中断、轮询等机制处理;并不是 CPU 用普通指令从网卡里逐字节取数。多队列网卡还会把流量分散到不同 CPU 核,Linux 的 RSS、RPS 等机制就在解决这类分配问题。Linux 网络栈扩展文档
应用进程随后要解析请求、读取会话、做 tokenization、准备张量、驱动推理引擎、封装流式输出。它可能采用异步事件循环、线程池、多进程,或这些机制的组合。
异步 I/O 允许一个线程在等待远端或磁盘时处理其他请求,但不会自动把 CPU 密集计算变便宜。线程和进程需要调度,共享状态需要同步,容器还可能受到 CPU 配额与内存限制。GPU 利用率低,有时是前面的 CPU 根本没及时把下一批工作准备好。
负载均衡也不能只数「每台机器有几个请求」。一个带十万 token 上下文的请求,与一个几十 token 的短问题,占用的计算和 KV cache 很不一样。更有用的信号包括排队 token 数、活跃序列、可用显存、缓存位置,以及请求的延迟目标。
真正交给模型的输入是怎样组装的
对应的课程是软件工程、数据库、信息检索与安全工程。
聊天框里的那句话,通常只是最终输入的一部分。服务端可能组装:系统规则、产品设定、对话历史、用户消息、工具说明、检索片段和附件内容。模型的上下文窗口有限,产品必须决定保留什么、裁掉什么,以及是否把旧对话压缩成摘要。
这种压缩会丢信息。把某个细节从上下文删掉后,再说「我们不是已经聊过吗」,模型未必还能访问到它。界面上仍有聊天记录,不意味着每一轮都把全部记录送进了当前模型。
意图识别与路由不是固定的一道小模型关卡
有些系统用规则判断要不要搜索,有些用分类器或小模型选择路线,有些让主模型直接决定调用工具。简单问题可能直达生成模型;复杂问题可能被路由到更强的模型,或先拆解任务。
因此,不能给所有产品画一个必经的「意图识别模型」。更准确的说法是:产品需要决定如何处理请求,实现这个决定的方式可以不同。 每多一次模型调用、检索或重排,都可能增加延迟和成本。
RAG 把外部资料送进当前上下文
检索增强生成,即 RAG,常见过程是:把查询转换为检索表示,在有访问权限的资料中寻找相关片段,必要时重排,再把选出的内容交给生成模型。
这里既有关键词检索、倒排索引,也可能有向量检索、近似最近邻算法和混合检索。向量相似度表示某种语义接近程度,不等于事实正确,也不保证片段足够回答问题。检索找错、权限过滤不完整、文档过期,都会传导到答案里。RAG 的基本思路可以参考原始论文。
数据库在这里有很实际的角色:保存会话、文档版本、权限、任务状态和计费事件。用户看到一个连续对话,后端可能已经读写了多个系统。索引更新与源文档更新不同步,就可能检索到旧内容。
「命中缓存」至少可能指四种事情
| 缓存 | 保存什么 | 主要省掉什么 | 正确复用依赖什么 |
|---|---|---|---|
| 响应缓存 | 已生成的答案 | 一次完整或部分处理链路 | 输入、模型、配置、权限和有效期兼容 |
| 检索缓存 | 检索结果或文档处理结果 | 搜索、解析、embedding 等工作 | 查询条件、文档版本与权限一致 |
| 前缀缓存 | 公共输入前缀对应的 KV 状态 | 重复前缀的部分 prefill 计算 | 相同有效前缀与模型执行配置兼容 |
| 编译缓存 | 编译后的程序或 kernel | 重复编译与部分启动工作 | 代码、形状约束、软件与硬件目标兼容 |
语义响应缓存还会尝试复用相似问题的答案,但「相似」不保证「可以共用」。余额、实时天气、私人文件等问题尤其依赖时间与身份。缓存键设计和失效策略,直接决定缓存是在节省工作,还是在快速返回错误结果。
文字怎样变成模型能计算的数
对应的课程是自然语言处理、数据表示、信息论与线性代数。
Chat template 先把对话排成模型熟悉的格式
模型需要知道哪段是用户消息、哪段是助手消息,工具结果在哪里,接下来应该续写谁的内容。这通常通过模型对应的 chat template,把带角色的消息变成含特殊标记的序列。
这些标记具体长什么样,取决于模型。随便把角色名拼成字符串,可能偏离训练时的格式,降低模型遵循指令的效果。Hugging Face 的 chat template 文档展示了这种转换。
角色标记能提供结构,但不能单凭几个标记保证绝对安全。一个系统消息属于高优先级的语义约定,真正的数据库权限、工具权限和沙箱仍要靠软件系统执行。
Token 既不是字也不是词
Tokenizer 把文本映射成 token ID。一个 token 可以是常见词的一部分、一个字符、一组字符,或字节表示;切分结果取决于词表和算法。BPE、Unigram 等方法在词表容量、压缩效率和泛化之间做不同取舍。
所以中文字符数、英文单词数、UTF-8 字节数和 token 数,是四个不同的量。代码、罕见字符和不同语言的 token 密度也会不同。精确计数要使用目标模型对应的 tokenizer,不能套一个通用的「一个汉字等于几个 token」。
Token ID 本身只是编号,没有数值大小上的语义。例如,编号更大并不表示词更复杂。模型用 embedding 矩阵查出对应向量,再结合位置机制,让后续网络处理内容与顺序。
图片和音频会增加新的前处理:解码、缩放、采样、视觉或音频编码,再转换为模型可消费的表示。它们不一定遵循文本 tokenizer 的规则;产品显示的多模态 token 计量,也不能简单等同于原始图片像素数。
一次 Transformer 前向计算做了什么
对应的课程是线性代数、概率论、机器学习与数值计算。
对一个自回归语言模型,生成一段输出可以写成:
这里 是已经组装好的输入, 是已经生成的 token。这个分解解释了为什么普通生成要依次推进:第 步的条件里包含前面的实际输出。
注意力让当前位置使用前文的信息
在一个注意力头内,可以把核心计算写成:
可以理解成当前位置提出的查询, 是各位置用来匹配的特征, 是被加权汇集的信息。 是掩码;因果掩码禁止一个位置使用未来 token 的内容。 是注意力权重矩阵, 是该注意力头的输出。
这里的 query、key、value 是线性变换得到的向量,不是向数据库发送 SQL,也不是从词典里查答案。多个注意力头可以从不同表示子空间建立关系,再把结果组合起来。
注意力之后还有前馈网络。现代模型常用带门控的前馈结构;加上残差连接、归一化和位置机制,一个 block 把输入向量变成更适合后续预测的表示。这样的 block 逐层重复,最后通过输出投影得到词表各 token 的 logits,即尚未归一化的分数。
Transformer 原始论文建立了这套注意力结构;今天常见的 decoder-only 模型在归一化位置、位置编码和前馈结构上有许多变化,不能把原论文中的编码器—解码器网络原样当作所有聊天模型。
参数、激活和 KV cache 是三类不同的东西
参数是训练得到的权重,普通推理时通常保持固定。激活是当前输入在各层产生的中间结果。KV cache是为后续自回归步骤保留的一部分中间结果。
训练通常还要做反向传播和优化器更新,显存里可能需要梯度与优化器状态。推理通常不做这些工作,但仍然需要权重、激活、KV cache、临时工作区和运行时开销。把参数文件大小直接当成推理总显存,会漏掉大量随负载变化的部分。
预训练让模型从数据中学习规律,指令微调、偏好优化等后训练过程进一步塑造回答方式。当前请求用的是这些过程已经形成的参数。产品事后保存反馈、更新长期记忆或用于后续训练,是另外的流程,不能与本次前向计算混为一谈。
Prefill 与 KV cache:先把已知内容算一遍
对应的课程是并行计算、算法分析与空间换时间。
输入已经全部已知,模型可以在因果掩码约束下,批量计算这些位置。这一阶段通常叫 prefill。不同层仍然有依赖,输入位置之间也有因果关系,但实现可以用矩阵运算同时处理很多位置,无须像生成未知输出那样逐 token 等待采样结果。
Prefill 会为输入建立每层的 K、V 状态。处理到输入最后位置时,模型已经能给出第一个输出 token 的分布。第一个 token 可以直接从 prefill 的结果中采样得到。 随后的 decode 才把它作为新输入,计算第二个输出 token。
图 2:图中的输入长度与输出步数仅用于示意。每轮 decode 都复用已有 KV,并为刚处理的位置增加状态;保留状态不表示已经预先知道后面的答案。
为什么过去的 K 和 V 可以留下来
对标准因果 Transformer,旧位置不能看见后来新增的 token,因此在输入前缀和执行条件不变时,旧位置在各层的 K、V 不需要因为追加新 token 而重新计算。新位置只要生成自己的 Q、K、V,再用 Q 与过去的 K、V 计算注意力即可。
没有 KV cache,朴素实现会反复把整个前缀送入模型,重复计算大量旧位置。缓存让系统保留这部分工作,但新位置仍需要执行网络,也仍要访问相关历史状态。Hugging Face 对 KV cache 的解释给出了逐步追加的机制。
它保存的是各层的张量,不能当作可直接阅读的「模型记忆文本」。它也不会自动跨会话永久存在:请求结束后可能释放,也可能按服务策略保留可复用的前缀。
长上下文为什么占显存
对采用完整注意力、常规 K/V 存储的模型,若各序列长度相同,KV cache 大小可以近似写成:
是层数, 是序列数, 是每条序列已缓存的位置数, 是 KV 头数, 是每头维度, 是每个元素的字节数;前面的 2 表示 K 和 V。不同长度的序列应把 换成各序列长度之和。
做一个理论容量估算:32 层、8 个 KV 头、每头 128 维、每个元素 2 字节,单条序列缓存 8192 个位置,则:
这只算 KV 张量,尚未计入权重、对齐、块管理和其他工作区。32 条同长度序列的逻辑 KV 就是 32 GiB;实际分配还要看共享、分片和缓存策略。
GQA 让多组查询头共享较少的 KV 头,因此可以降低 KV 存储与访问需求,代价是改变注意力参数化和模型设计。GQA 原论文讨论了这个取舍。滑动窗口注意力、潜在状态压缩或其他模型结构有不同的存储规律,不能一律套用上式。
前缀复用为什么比「有一段相同文字」更严格
如果多个请求有相同的系统规则和资料前缀,就有机会复用那部分计算。但一段相同文字放在不同前文之后,其表示通常不同。前缀缓存需要对齐的是有效 token 前缀,还要兼容模型版本、适配器、位置和多模态输入等条件。
命中前缀缓存主要减少重复 prefill,不会直接给出本次问题的最终答案。为了省一次计算而把请求送到很远或很忙的机器,也可能得不偿失。这就是缓存命中率与用户延迟不总是同方向变化的原因。
Decode:答案为什么要一步一步生成
对应的课程是概率论、算法设计与并行计算。
Prefill 得到第一个 token 后,系统进入循环:处理刚生成的 token,更新 KV cache,得到下一步 logits,选择新 token,然后继续。
模型产生的是条件分布。最简单的选择方法是取最高分 token,即 greedy decoding;也可以用 temperature 调整分布,再结合 top-k、top-p 等规则采样。Temperature 改变的是采样分布的尖锐程度,不是一个直接控制「聪明程度」的旋钮。
某个 token 概率高,也不表示它对应的事实已经被外部证据验证。语言上的可续写性、模型参数中的知识和现实世界的真实性,不能画等号。降低 temperature 可以减少部分随机变化,不能从根本上消除幻觉。
计算可以并行但依赖仍然存在
每一步内部,矩阵乘法、多个注意力头和不同请求都有大量并行工作。对同一条普通自回归输出,后一步却要等待前一步的实际选择。把答案一次全部展开,相当于提前知道自己还没生成的输入。
这也解释了两个常见体验:输入很长时,开头可能等很久;输出很长时,即使开头很快,总时长仍会持续增长。前者更容易暴露 prefill 和输入处理成本,后者反复支付 decode 的单步成本。
具有推理能力的产品还可能在展示最终答案前消耗额外的内部生成、搜索或验证计算。有些中间过程不展示给用户。因此,屏幕上没有文字不能直接推断「模型还没开始算」,也不能把可见字数当作所有生成工作量。
投机解码怎样减少大模型的串行轮数
投机解码先由较便宜的草稿机制提出多个候选 token,再让目标模型并行验证。这些候选已知,因此验证可以利用类似处理已知序列的并行性。接受得越多,越有机会用一次目标模型调用推进多个位置。
在使用相应接受与校正规则的精确采样算法中,可以保持目标模型的输出分布;这不等于每次运行都得到逐字相同的随机样本。加速收益取决于接受率、草稿开销、批量大小和硬件,难预测的输出可能抵消收益。Fast Inference from Transformers via Speculative Decoding给出了这一思路。
推理框架怎样让很多人共享 GPU
对应的课程是操作系统、资源调度、数据结构与排队论。
模型定义告诉我们怎么从输入算到输出;推理服务还要决定,几千个用户的这些计算如何装进有限的 GPU。vLLM、TensorRT-LLM、SGLang 等推理系统都在不同层面处理这类问题。
连续批处理让请求在迭代之间进出
单条请求通常不足以充分利用大 GPU。把多个请求组成 batch,可以让一次权重读取服务更多计算,增加并行度。但如果采用静态批处理,让所有请求等最长的那一个完成,已经结束的请求就会留下空位。
连续批处理允许在生成迭代之间移走已完成的请求、接入新请求。不同请求各自持有上下文和 KV,调度器安排它们共享执行批次。
图 3:每列表示一次调度迭代,列宽不代表相同的真实耗时。P 表示 prefill 或其中一块,D 表示一次常规 decode;新请求的加入受到 token 预算和 KV 容量限制。
批量越大,每步要处理的工作也越多,KV 占用更高,单个用户可能等得更久。服务追求的往往是「满足延迟目标时的吞吐」,不能只追求 GPU 忙碌或总 tokens/s 最大。
Chunked prefill 防止长输入独占一大段时间
假设一个用户提交了很长的文档,另一个用户正在等待下一个输出 token。如果把长输入的 prefill 一口气跑完,正在生成的人可能突然感到卡顿。
Chunked prefill 把长输入切成调度粒度更小的块,让调度器有机会穿插 decode。因果上下文仍然连接着,并不是把文档拆成互不相干的问题。切块粒度改变首 token 延迟、输出间隔和吞吐之间的平衡。vLLM 优化文档介绍了这种调度取舍。
PagedAttention 把显存管理问题变成块映射
不同请求的上下文长度持续增长,若为每个请求预留一整段最大长度的连续空间,会浪费显存;动态分配又可能产生碎片。
PagedAttention 借鉴操作系统的分页思想,把逻辑 KV 序列映射到固定大小的物理块,让逻辑连续的数据可以存放在不连续的块里。它减少了大块连续空间的压力,并支持适当条件下的共享与写时复制。这是vLLM 与 PagedAttention 论文的核心贡献之一。
这里的「页」通常由推理系统管理,不应直接理解成 GPU 正在用操作系统页面把每个 token 换入换出硬盘。块表访问、缓存淘汰和专用注意力 kernel 也有成本。
显存不够时,系统可能暂停请求、回收状态,之后重新计算,或按实现采用卸载。用户看到的卡顿有时来自资源竞争和重算,而非模型某一层忽然变慢。
从张量程序到 CUDA 机器指令
对应的课程是编译原理、程序语言、体系结构与并行程序设计。
我们在论文里写 ,在框架里调用线性层,GPU 不会直接理解这些符号。执行栈需要把它们变成适合具体硬件的程序。
一条常见路径是:
模型结构与张量运算
↓
计算图、算子选择与图优化
↓
库函数或生成的 kernel
↓
目标 GPU 的机器指令
大矩阵乘法可以调用 cuBLAS、cuBLASLt 等库;注意力和归一化等操作可能调用专用 kernel,也可能由编译器生成。真实系统通常混合这些路径,并非每个算子都重新从 Python 编译一遍。
图编译与 kernel 编译解决不同尺度的问题
图编译器从一串张量运算中寻找机会:消除重复工作、选择数据布局、融合相邻运算、减少临时张量,以及调用合适的库。PyTorch 的 torch.compile 常见栈涉及 TorchDynamo 的图捕获和 TorchInductor 等后端;具体路径取决于版本与配置。PyTorch 编译器文档
Kernel 编译器更关注一个设备程序怎样映射到硬件:怎样分块、向量化、使用寄存器、安排 shared memory、选择 Tensor Core 指令,并隐藏数据搬运的等待。Triton 让开发者用较高层的块状数组表达这类程序,它的矩阵乘法教程展示了分块与调优过程。
CUDA C++ 的常见编译路径包含生成 PTX 或目标 cubin。PTX 是虚拟指令集表示,还需要变成具体 GPU 可执行的机器码;cuobjdump 等工具展示的 SASS 则对应目标架构机器指令的汇编形式。可执行文件里可能已经带有合适的 cubin,也可能由驱动在加载时对 PTX 做 JIT 编译。NVIDIA CUDA 平台文档
因此,「第一次运行慢」可能包含编译、自动调优、模块加载和内存初始化。服务通常会提前预热,但遇到新形状或新配置仍可能重新编译。AOT 把工作尽量提前,JIT 利用运行时信息做特化;两者都要面对编译成本与复用条件。
融合省搬运 CUDA Graph 省提交
假设几个连续的小算子,每个都把中间结果写回显存,再由下一个读出来。融合有机会把中间值留在寄存器等近处,减少读写和 kernel 启动。不过融合太大也可能增加寄存器压力,降低并行驻留,甚至让原本优秀的库实现失去优势。
CUDA Graph 则把可重复的操作及其依赖组织成图,实例化后反复提交,降低 CPU 逐个发射操作的开销。它不是把模型重新训练,也不自动减少矩阵乘法的数学工作量。对形状、地址和控制流的约束,需要通过固定缓冲区、分桶或适合的动态图能力处理。CUDA Graph 官方说明
一项优化究竟有效,要看它是否消掉当前瓶颈。CPU 发射开销很小时,减少发射的收益就有限;主要时间花在大矩阵乘法时,融合几个很短的小算子也未必改变整体体验。
GPU 内部到底在忙什么
对应的课程是计算机组成原理、体系结构、数值计算与性能分析。
GPU 擅长让大量线程执行规则相似的运算。线程组成 block,block 被安排到流式多处理器,即 SM 上执行;NVIDIA GPU 的 warp 包含 32 个线程。SIMT 让线程以组为单位推进,遇到不同分支时则可能需要分开执行。
模型的主要算子通常有规则的数值结构,但这不意味着「线程越多越快」。寄存器、片上存储和执行资源都有限,访存与同步也会限制进度。
图 4:示意存储层次与协作关系,不表示具体 GPU 的容量、比例或全部数据路径。Shared memory 与寄存器分别服务不同的协作范围;Tensor Core 是矩阵运算单元,不是一层缓存。
算得快之前先得把数据送到位
权重和 KV cache 通常主要放在 GPU 显存中,服务器 GPU 常用 HBM。数据通过缓存层次进入 SM。寄存器服务线程,shared memory 支持 block 内协作;高性能 kernel 会尽量让搬来的数据被反复使用。
Tensor Core 可以高吞吐执行受支持格式的矩阵乘加,但它仍需要正确布局、对齐和持续供数。理论峰值 FLOPS 描述的是特定精度、指令和条件下的能力,不是你的模型随时都能达到的速度。CUDA 的 kernel 与执行模型解释了这些执行与存储约束。
把矩阵切成 tile,在寄存器和 shared memory 中复用;让相邻线程合并访存;减少 bank conflict;通过异步拷贝和双缓冲,把下一块数据的搬运与当前计算重叠——这些课本中的局部性和流水线思想,会直接出现在大模型 kernel 里。
Prefill 与 decode 的瓶颈为什么经常不同
用一个粗略的 Roofline 视角看,某个 kernel 的时间至少受计算和数据搬运两边约束:
是浮点运算量, 是所分析存储层级的搬运字节数; 与 是该负载可达到的有效计算吞吐和带宽。这个估算还没有覆盖启动、同步、依赖等全部开销,不能当作实测延迟。
Prefill 一次处理很多位置,权重有较多复用,较容易形成计算密集的大矩阵乘法。低批量 decode 每条序列只增加一个位置,常需要为了不多的新计算读取大量权重与历史 KV,更容易受带宽和访存限制。
但「prefill 永远算力受限,decode 永远带宽受限」过于绝对。批量变大、上下文变长、注意力结构变化、跨卡通信增加,都可能改变瓶颈。甚至同一次前向计算里的不同 kernel,也可能受不同资源限制。
FlashAttention 优化的是数据如何流动
标准注意力涉及 与 softmax。若把大规模的注意力分数矩阵写到显存再读回来,中间数据会非常昂贵。
FlashAttention 通过分块和在线 softmax 等方法,在片上工作区逐步完成计算,避免把整个注意力矩阵显式存入 HBM,减少 I/O 与中间存储。它在算法意义上计算同样的精确注意力;浮点运算次序改变仍可能产生数值差异。FlashAttention 原论文
这不意味着完整注意力随序列长度的二次计算关系消失,也不意味着 KV cache 不再需要。训练、prefill 和单步 decode 的形状不同,适合的具体注意力 kernel 也可以不同。
量化在容量带宽和误差之间做取舍
降低权重、激活或 KV 的表示精度,可以减少存储与搬运,还可能使用更高吞吐的硬件指令。但权重量化、激活量化和 KV 量化是不同决策;只有权重缩小,不能推断 KV 也一起缩小。
以理论存储为例,80 亿个参数,每个 2 字节,原始权重约 16 GB;若每个参数理想地用 4 bit 表示,原始数据约 4 GB。实际还需要分组 scale、可能的零点、对齐和运行时工作区,且 1 GB 与 1 GiB 的定义不同。
量化也可能带来精度损失、转换开销,以及对特定硬件和 kernel 的依赖。格式位数更低,只说明表示更紧凑,不能直接证明端到端更快。
模型放不下一张卡之后
对应的课程是分布式系统、并行算法、网络与容错。
扩展 GPU 有两类目标:让一个模型装得下、算得动,以及让系统同时服务更多用户。对应的并行方式不同。
| 方式 | 拆分的是什么 | 主要收益 | 主要代价 |
|---|---|---|---|
| 数据并行 DP | 复制模型实例或模型组,分配不同请求 | 增加服务吞吐 | 复制权重,分散缓存,负载可能不均 |
| 张量并行 TP | 一层内的矩阵或张量计算 | 分摊权重与计算 | 层内通信频繁,依赖互连 |
| 流水线并行 PP | 不同层放到不同设备 | 分摊模型容量 | 阶段传递与流水线气泡 |
| 专家并行 EP | MoE 的专家分布到不同设备 | 分摊专家权重与计算 | 路由、负载不均与 token 交换 |
对单条请求,PP 的各阶段仍要依次处理该 token,填满流水线通常依赖多个请求或微批。DP 在纯推理时处理不同请求,一般不需要像训练那样同步梯度;它提升总服务能力,不直接把一条输出的自回归依赖拆开。
跨卡通信进入了每一轮计算
张量并行中,一张卡可能只得到部分结果,需要通过 AllReduce、AllGather 或 ReduceScatter 等集合通信完成组合。AllReduce 例如把各参与者的数组逐元素归约,再让每个参与者都拿到结果。NCCL 集合通信文档
机内的 NVLink、NVSwitch、PCIe,与跨节点的 InfiniBand 或以太网,在延迟、带宽与拓扑上不同。RDMA 等技术可以减少特定路径上的 CPU 参与和拷贝,但不会消除物理传输时间。
卡更多,计算与容量可以分摊;同时每轮增加了通信和同步。短小的 decode 计算尤其容易暴露通信延迟。某一张卡慢下来,其他参与者也可能在集合操作处等待。这就是增加 GPU 数量后,延迟未必按比例下降的原因。
MoE 激活少一些专家仍要管理全部系统
混合专家模型,即 MoE,通常让路由器为每个 token 选择一部分专家执行。因此总参数规模与单个 token 实际激活的参数规模不同。
但未被当前 token 选中的专家权重仍需要存储,或由卸载机制管理。不同 token 选中不同专家,可能引发跨卡交换;热门专家也可能成为拥堵点。较少的激活参数不能直接等价为较低的整套服务成本。
Prefill 和 decode 还可以放在不同资源池
两阶段的资源特征不同,系统可以把 prefill 与 decode 分开部署,分别扩缩容或选择硬件,再把所需 KV 状态传给 decode 节点。
这样有机会减少两类工作相互干扰,但新增了 KV 传输、状态管理和路由成本。短请求的迁移成本可能超过收益;长上下文能否高效转移,又取决于互连和缓存表示。这里的核心仍然是一个分布式系统问题:把计算搬到数据旁边,还是把数据搬到计算旁边。
节点故障也不只是重新发一句 HTTP 请求。正在生成的 KV 状态可能随进程丢失,需要从保留的输入和输出前缀重建;恢复期间可能改变延迟,采样结果也可能变化。重试、会话状态和输出去重需要明确设计。
Token 怎样重新变成屏幕上的字
对应的课程是网络协议、字符编码、流处理与前端工程。
模型选出 token 后,系统需要把 ID 解码为文本,并处理特殊标记、停止序列、输出格式和安全策略。一个 token 不保证对应一个完整可显示字符;一个网络数据块也不保证装下完整 UTF-8 字符。增量解码器需要保留未完成的字节序列,协议解析器则要正确拼接消息。
常见产品通过 HTTP 流式响应把增量事件交给客户端,可以使用 SSE 风格的事件格式,也可以采用其他流协议。原生 EventSource 适合其规定的接口形态;聊天常见的 POST 请求也可以通过 fetch 读取响应流并解析 SSE 格式,不能把两者的调用方式混为一谈。MDN 对 SSE 格式和事件处理的说明
要分清四种边界:模型生成的 token、服务端发送的事件、网络交付的字节块、浏览器绘制的画面。它们通常不是一一对应。
代理可能缓冲数据,服务端可能攒几个 token 再发,浏览器可能每帧合并一次更新。输出突然一大块冒出来,有时是流式链路在攒数据,并非模型突然一次生成了整段文字。
客户端收到文本后,还要更新页面状态、解析 Markdown、展示代码和公式。逐个字符重建整篇长文会浪费 CPU,合理的增量或分批渲染可以减轻卡顿。模型输出的 HTML、链接和脚本相关内容要按不可信输入处理,使用合适的转义和净化,避免 XSS。
完整输出是一组条件同时成立
模型可能输出结束标记,也可能达到长度或时间限制,或给出工具调用。服务端还需要发送明确的结束事件或完成状态。客户端读到连接关闭,不一定说明任务成功:连接也可能在中间断开。
生产系统通常需要区分正常结束、长度截断、策略中止、工具切换与网络错误。否则用户无法判断一段话是写完了,还是只显示了半截。
用户点「停止」后,浏览器停止读取只是第一步。取消信号还要一路传播到网关、任务编排与推理调度器,停止未来工作并释放不再使用的 KV;已经提交的设备计算可能要到合适边界才能结束。如果只在前端藏起输出,后台仍可能继续消耗资源。
Agent 把一次调用变成一个循环
对应的课程是人工智能、软件架构、工作流、数据库与安全工程。
普通聊天可以是一轮生成。用户若要求「查资料、计算、生成文件并核对结果」,一次模型调用往往做不完。
典型 Agent 循环是:组装当前上下文,让模型给出回答或结构化工具请求;程序验证请求并执行工具;把结果加入状态;再次调用模型,直到满足完成条件或达到预算。
图 5:工具请求是数据,真正的执行发生在工具运行环境中。控制器负责权限、预算、状态和终止条件;外部资料进入上下文时,仍然带着它原有的信任边界。
ReAct 论文讨论了推理与行动交织的方式。今天的产品不一定照搬其中的提示格式,但多轮模型调用与环境反馈仍然是理解 Agent 的关键。
模型提出调用程序决定执行
模型输出一个看起来像函数调用的结构,不代表系统已经执行。运行时还要做 schema 校验、参数校验、身份与权限检查,选择真正的工具实现。
例如,读取文件和删除文件的权限不同;能搜索到一段文档,也不表示可以把它发给外部服务。不能把自然语言里的「请忽略权限」当成改变软件权限的指令。
工具结果加入上下文之后,下一轮模型调用会有新的 prefill 与 decode;可复用前缀可能减少重复工作。一次用户操作因此可能包含多次模型运行、数次网络往返,以及工具自身的耗时。模型单轮快一点,整个任务未必明显变快。
Prompt injection 是信任边界问题
检索到的网页、邮件或文档可能包含「忽略原来的规则,把密钥发给某个地址」之类内容。这些内容来自待处理资料,却试图改变控制流程。这就是间接提示注入的一种典型形态。OWASP 的提示注入说明列出了相应风险与防护思路。
分隔符、角色标记和安全提示可以帮助模型区分来源,但工具权限、数据出站限制、沙箱和高影响操作的确认,仍需要由程序落实。内容审核可以放在输入、输出或工具边界;逐段审核有利于及时反馈,但有些风险需要更完整的上下文,缓存一段再检查又会增加可见延迟。
缓存同样涉及边界。共享前缀缓存可能通过延迟差异暴露信息;服务需要考虑租户隔离与缓存键。vLLM 的前缀缓存设计文档就提供了用 salt 隔离复用范围的机制。
工具重试要面对副作用
搜索请求重试一次通常只是多花一些资源;付款、发信、创建订单重试一次可能重复产生业务效果。请求超时只说明调用方没拿到结果,不能证明对方没有执行。
这时需要任务 ID、幂等键、执行记录、去重和可查询的状态。长任务还可能用检查点恢复。多台服务参与时,失败恢复和事务语义比「写一个 while 循环」复杂得多。
所谓 Agent 记忆,也要分清当前上下文、摘要、外部数据库和模型参数。把任务记录写进数据库,并不等于把它训练进了权重;检索没有取出来,它就未必参与本轮回答。
怎样解释快慢并定位故障
对应的课程是性能工程、统计学、排队论与可观测性。
「这个模型每秒输出多少 token」只描述一部分体验。至少应区分下面几个量:
| 指标 | 问的是什么 | 容易忽略的边界 |
|---|---|---|
| 首 token 延迟 TTFT | 从请求起点到第一个输出 token 等多久 | 服务端与客户端起点可能不同,首个可见字还可能更晚 |
| Token 间隔 ITL | 相邻输出 token 之间等多久 | 服务端产生与客户端收到的间隔不同,批量事件会掩盖变化 |
| 每输出 token 时间 TPOT | 首 token 后平均每推进一个 token 花多久 | 需明确是否按 统计 |
| 端到端延迟 | 从发送到完整结果可用多久 | 是否含检索、工具、后处理和渲染 |
| 总吞吐 | 整套服务单位时间完成多少工作 | 输入与输出 token 要分开,不等于单用户速度 |
| 有效吞吐 | 满足质量和延迟目标的完成量 | 高吞吐若伴随大量超时,业务价值有限 |
对一轮无工具调用、按可见 token 计量的简单生成,可用下面的近似把体验拆开:
为输出 token 数, 包括最后的审核、传输、完成事件和渲染等尾部工作。 时不能用上面的分母计算 TPOT;推理型、多分支或工具型任务也需要更完整的分解。
举一个假设的算术例子:首 token 等待 0.8 秒,之后每个输出 token 平均 20 毫秒,生成 201 个 token,尾部处理 0.2 秒,则完成约需 秒。这是用来理解指标的推导,不是任何模型的跑分。
先用 trace 找时间花在哪一段
把同一个请求 ID 或 trace context 贯穿网关、会话读取、检索、模型排队、prefill、decode、工具和输出,可以看到关键路径。并行检索的几条支路不能简单把耗时相加;端到端主要受依赖链上最晚完成的工作影响。
不同机器的时钟可能不完全一致,跨服务时间戳不能随意相减;应使用正确的分布式追踪关联与本地持续时间测量。指标还应按输入长度、输出长度、缓存命中、模型与并发分组,否则平均数会混合完全不同的工作负载。
如果长时间没有首 token,先看网络与入口、输入预处理、检索、排队、缓存和 prefill;如果开头快但中间卡,看调度、KV 压力、跨卡同步、工具调用和流式缓冲。先确定卡在服务哪一段,再决定是否深入 GPU。
再从系统时间线下钻到 kernel
系统级 profiler 可以帮助看 CPU、GPU、数据拷贝与通信之间的空隙;kernel 级 profiler 再解释访存、指令、占用率和执行停顿。GPU 利用率高并不等于用户体验好,也不直接说明工作有效。
采集会产生开销;更细的计数器采集还可能重放 kernel,不能把它的耗时直接当作正常在线延迟。先用独立计时确认性能变化,再用 profiler 解释原因,才能避免为了优化一个百分比而把请求变慢。
队列会放大接近容量上限时的等待
Little 定律在稳定系统中给出 :平均在途请求数等于平均完成率乘平均停留时间。它不是承诺系统永远稳定的容量公式,但提醒我们:同样的完成率下,请求停留越久,系统需要容纳的在途工作越多。
持续把服务推近饱和,排队等待和尾延迟往往明显上升。看 p50 可以了解典型体验,看 p95、p99 才能发现一部分用户遭遇的长等待;还要一起看失败率。只统计成功且快速的请求,会把真正的拥堵隐藏掉。
缓存预热、量化、更多副本、更大的 batch、更高的并行度,都应在代表性请求分布和真实并发下比较。单条短 prompt 的最佳配置,未必是全天在线流量的最佳配置。
沿着这次请求重新认识 CS 课程
走到这里,原本分散的课程已经落到同一条链路上了。
| 课程或方向 | 在这次请求里解决的具体问题 | 可以继续追问什么 |
|---|---|---|
| 数据结构与算法 | tokenizer、缓存索引、KV 块表、调度队列 | 哪些工作能避免重复,时间与空间怎样交换 |
| 计算机网络 | DNS、连接、可靠传输、拥塞、流式响应 | 数据在哪里等,消息边界由谁维护 |
| 操作系统 | 进程、虚拟内存、线程、异步 I/O、DMA、资源隔离 | 谁拥有资源,谁在等待,取消怎样传播 |
| 数据库 | 会话、权限、文档版本、任务与计费状态 | 重试后会不会重复执行,读到的是否是正确版本 |
| 编译原理 | 计算图、IR、融合、布局、JIT 与目标代码 | 高层计算如何变成高效机器程序 |
| 组成原理与体系结构 | SM、warp、Tensor Core、寄存器、缓存、HBM | 算力是否吃得饱,数据为什么搬不动 |
| 并行与分布式计算 | 批处理、TP、PP、DP、EP、集合通信与恢复 | 拆开后省了什么,又增加了多少通信 |
| 线性代数与数值计算 | embedding、矩阵乘法、注意力、低精度 | 表示与计算怎样影响误差和性能 |
| 概率论与机器学习 | 条件分布、采样、训练与推理 | 高概率续写为什么仍可能不真实 |
| 信息检索与 NLP | RAG、向量、重排、上下文组织 | 证据能否找对,输入是否保留了关键含义 |
| 安全工程 | 鉴权、隔离、提示注入、工具权限与输出净化 | 不可信内容能否越过信任边界 |
| 软件工程与人机交互 | 状态机、协议、可观测性、错误反馈、渐进显示 | 用户能否理解进度,失败后能否恢复 |
这些知识不必一次全部学完。沿着一个现象往下追,会更容易找到它们之间的联系:为什么长文档开头慢,为什么输出中间停,为什么显存还有余量却接不了更多人,为什么加卡后并没更快。
回到最初那次回车:输入事件让浏览器构造消息,网络把字节交给服务,产品组装上下文并分配资源,模型把输入表示成向量,通过 prefill 建立状态,再在 decode 中逐步推进输出。编译器、kernel、GPU 和多机通信共同执行这段计算,流式链路再把结果变成屏幕上的文字。工具参与时,这条路径还会循环几次。
聊天框里的几秒钟,是许多层抽象同时兑现承诺的过程。 上层说「回答这个问题」,下层必须具体解决:数据在哪里,能否读取,何时轮到,怎样表示,如何计算,等谁完成,以及失败以后怎么办。
参考资料与继续阅读
正文已在相关位置链接原始资料。沿不同方向继续读,可以从这些入口开始:
- 网络与流式传输:HTTP/3 RFC、MDN SSE。
- 模型与状态:Attention Is All You Need、KV cache、GQA。
- 推理服务:PagedAttention、vLLM 调度与优化、投机解码。
- 编译与 GPU:CUDA 平台、Triton 矩阵乘法、FlashAttention、NCCL 集合通信。
- 检索、行动与安全:RAG、ReAct、OWASP Prompt Injection。