
版权与内容来源声明本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容均在附表 A 中标注来源引用官方原文保持原样不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式也不对任何收益结果作承诺。转载请注明出处。第 1 章 先说清楚C 的老本行为什么在推理侧更对口1.1 训练侧比的是算力推理侧比的是工程模型训练阶段瓶颈几乎都在「算力」把海量数据反复过一遍谁的加速器多、谁的并行策略好谁就快。这一层离应用开发者比较远基本是框架和编译器的战场。推理就不一样了。推理inference把训好的模型跑起来出结果而不是继续训练天然是一个工程问题一个请求进来你要在有限的一块显存里同时装下模型权重、装下多个用户的中间状态还要让每个用户都别等太久。内存怎么分、数据怎么搬、任务怎么排—— 这三件事正好是 C 老手天天在干的。以推理框架里一个典型的项目为例。llama.cpp 官方仓库给自己的定义只有一句「LLM inference in C/C」用 C/C 做的大模型推理目标句写得更明确The main goal ofllama.cppis to enable LLM (and VLM) inference with minimal setup and state-of-the-art performance on a wide range of hardware - locally and in the cloud.人话用尽量少的环境配置在从本地到云端的各种硬件上把大模型推理跑出高性能。它的特性列表里第一条就是「Plain C/C implementation without any dependencies」无外部依赖的纯 C/C 实现。顺便核实一个容易记错的点截至 2026-10-07这个仓库已经不在个人账号下页面标题是ggml-org/llama.cpp归属于 ggml-org 组织。你在网上搜到的旧地址会跳转过去但引用时建议写组织名。1.2 把「性能」拆成三个可测量的指标「性能好」是个模糊词没法调。要调先把它拆成三个各自能读数的指标指标一句话定义人话主要由哪个阶段决定观测入口首 token 延迟用户从发出请求到看见第一个字等了多久预填充prefill阶段服务端日志里两个时间戳相减吞吐稳定输出时每秒能产出多少 token解码decode阶段 批处理总输出 token 数 ÷ 总耗时显存占用一块卡上权重和中间状态各占多少模型体积 会话长度 × 并发数显存监控工具的分段读数三个指标的量纲完全不同一个是「毫秒」一个是「每秒几个」一个是「GB」。它们不能合成一个总分因为调好一个往往会弄坏另一个 —— 这正是下一节要说的。1.3 三个指标互相拉扯不是三个独立旋钮C 里做性能优化习惯是「把热点找出来消掉它」。但推理性能不是单目标优化它更像在一张三条边互相牵制的桌子上找平衡点。一次典型请求的时间轴可以这样切开⚠️代码待验证用户按下回车 │ ├──[预填充 prefill]──── 一次性读完整个提示词算出第一个字 │ 这一步是「计算密集」 │ └──[解码 decode]─────── 一个字一个字往外蹦 这一步是「内存带宽密集」 ↑ ↑ 首 token 延迟在这里结束 吞吐在这里被决定看这张轴就明白第一层拉扯想让首 token 更快就要让预填充这一步独占资源、尽早开始想让吞吐更高就要把更多请求攒在一起同时算。这两件事在资源有限时是矛盾的。第 3 章会用批处理把这条矛盾讲透。判据一任何一次「提速」先问自己优化的是哪个指标、代价落在哪个指标上。说不上来就不是优化只是把瓶颈挪了个地方。第 2 章 指标一首 token 延迟为什么它由预填充主导2.1 预填充与解码是两种完全不同的活先解释两个词。预填充prefill模型拿到你整段提示词一次性把它读完算出内部状态并吐出第一个字。解码decode从第二个字开始每次基于前面所有内容再算一个字。预填充 prefill解码 decode一次处理多少字整段提示词可能上千字1 个字主要吃什么资源算力大矩阵乘内存带宽把权重读一遍并行度高整段可以并行算低第 n 个字依赖第 n-1 个对首 token 延迟几乎全责只在第一个字之后才发生对吞吐影响较小主导2.2 为什么首 token 慢锅基本在预填充因为用户等的「第一个字」是预填充跑完才出现的。解码阶段再慢那也是「第一个字之后」的事用户其实已经看到东西在往外蹦了体感完全不同。预填充本身是个计算密集的过程提示词越长一次性要算的矩阵乘越多。所以「首 token 延迟」和「提示词长度」通常是正相关的 —— 这也是为什么长文档问答类应用第一下特别容易卡。2.3 怎么把这两个阶段分开观察不要把「总耗时」当成一个数去优化它混了两种瓶颈。把阶段拆开是第一步。⚠️代码待验证# 用带日志的本地推理服务跑一次请求从日志里读两个时间戳# 以 llama.cpp 的服务端为例观察它打印的提示词处理耗时与生成耗时# 具体参数名以你本地版本的 --help 输出为准不要照抄llama-server-m/path/to/model.gguf--port8080观察方法从「请求到达」到「第一段输出」的时间接近预填充耗时从「第一段输出」到「输出结束」的时间除以生成的字数就是解码阶段的每字耗时。两个数分开看才知道该往哪边使劲。判据二如果首 token 延迟远大于「提示词长度带来的正常增幅」问题多半不在解码而在预填充的资源竞争比如被别的请求占了算力或者在排队。这时降批处理量、给预填充让路比调任何采样参数都有效。第 3 章 指标二吞吐为什么它由解码与批处理主导3.1 解码阶段卡在内存带宽上解码每吐一个字都要把整个模型的权重读一遍。这就带来一个反直觉的结论解码阶段的瓶颈往往不是「算不动」而是「数据搬得不够快」内存带宽。加速器算力再强如果权重要一字一字地读进来它大部分时间在等数据。这正好是 C 老手熟的味道瓶颈不在计算单元而在数据通路上。缓存局部性、预取、内存布局这些概念换了个场景又回来了。3.2 批处理为什么能大幅提吞吐又为什么推高首 token 延迟既然解码是「读一遍权重只算一个字」那就想办法让这一次读取服务更多请求 —— 把多个用户的解码攒成一批一起算。权重只读一遍几十个请求各吐一个字吞吐自然上去了。代价是首 token 延迟。批越大新来的请求越可能要等当前这批算完才能插进去。这件事在系统层面被专门研究过。vLLM 团队那篇关于 PagedAttention 的论文arXiv:2309.06180在摘要里就把矛盾点讲了出来High throughput serving of large language models (LLMs) requires batching sufficiently many requests at a time. However, existing systems struggle because the key-value cache (KV cache) memory for each request is huge and grows and shrinks dynamically.人话要高吞吐就得把请求攒够一批可每个请求的中间状态KV Cache见第 4 章又大又动态变化管不好就把显存浪费在碎片和重复上反而限制了批的大小。论文提出的思路是把操作系统的「虚拟内存 分页」搬到显存里来we propose PagedAttention, an attention algorithm inspired by the classical virtual memory and paging techniques in operating systems.顺带一说「分页」这个概念本身就是 C 老手的常识—— 把一大块内存切成固定大小的页、按需分配、避免外部碎片。推理框架把它用在了显存上逻辑一模一样。⚠️代码待验证批大小 → 吞吐 / 首 token 延迟 的变化示意曲线非实测 吞吐 │ ╭─────── 吞吐随批大小上升但会先饱和 │ ╭─╯ │ ╭──╯ │ ╭─╯ └─┴────────────────────────► 批大小 延迟 │ ╱ 首 token 延迟随批大小上升 │ ╱ │ ╱ └──╱───────────────────────► 批大小两条曲线方向相反这就是「吞吐 vs 首 token 延迟」的取舍。真正的调优点在吞吐曲线的拐点再往上加批吞吐涨得很少延迟却涨得很多。3.3 怎么观察吞吐用「总输出 token 数 ÷ 总耗时」得到整段的平均吞吐。要注意把预填充那段排掉 —— 首 token 那一下很慢它会把平均值拉低让你误判解码阶段的速度。更稳的做法是只看解码段从第二个字算到输出结束的时间除以字数。判据三如果加批之后吞吐不涨、延迟明显涨说明已经过了拐点或者显存不够装更大的批会退化成排队。这两种情况的下一步动作完全不同第 5 章给判据。第 4 章 指标三显存占用权重一块KV Cache 一块4.1 权重占用参数量乘以每个参数的字节数粗算只要两步总参数量 × 每个参数占几个字节。FP3232 位浮点每个参数 4 字节→ 参数量 × 4 字节BF1616 位浮点每个参数 2 字节→ 参数量 × 2 字节一个 70 亿参数的模型光是权重FP32 就要约 28GBBF16 约 14GB。这就是为什么「量化」几乎是端侧和大模型部署的默认动作ONNX Runtime 官方文档把量化定义得很清楚 ——「Quantization in ONNX Runtime refers to 8 bit linear quantization of an ONNX model」把模型里的浮点值映射到 8 比特的量化空间映射关系写成val_fp32 scale * (val_quantized - zero_point)。降到 8 比特权重占用大致是原来的四分之一。llama.cpp 官方仓库列出的量化位宽更细「1.5-bit, 2-bit, 3-bit, 4-bit, 5-bit, 6-bit, and 8-bit integer quantization for faster inference and reduced memory use」—— 从 1.5 比特到 8 比特的整数量化目的就是更快的推理和更低的内存占用。4.2 KV Cache随「会话长度 × 并发数」线性增长第二块更隐蔽。模型每生成一个字都要回顾前面所有字。为了不重复计算框架会把前面每个字的中间结果缓存下来这就是KV Cache键值缓存注意力机制里「键」和「值」两组中间张量的缓存。它的大小粗算公式按注意力结构推导用于量级判断具体系数以框架实现为准组成粗算项说明权重参数量 × 每参数字节固定不随会话变化KV Cache层数 × 键值头数 × 每个头的维度 × 序列长度 × 批大小 × 每元素字节 × 2随「序列长度 × 并发数」线性增长末尾那个「× 2」是因为要同时存键和值两组。这里每一项都在告诉你一个可调的旋钮序列越长、并发越多这块就越大而且它和权重抢同一块显存。⚠️代码待验证# 权重与 KV Cache 的粗算只用于量级判断不追求精确params_b7# 模型参数量单位十亿bytes_per_param4# FP32 用 4BF16 用 28 比特量化大致用 1weight_gbparams_b*1e9*bytes_per_param/(1024**3)print(权重大致占用 GB:,round(weight_gb,1))layers32# 层数kv_heads8# 键值头数head_dim128# 每个头的维度seq_len4096# 序列长度batch8# 并发请求数elem_bytes2# 缓存精度BF16 用 2kv_gb(layers*kv_heads*head_dim*seq_len*batch*elem_bytes*2)/(1024**3)print(KV Cache 大致占用 GB:,round(kv_gb,2))改一改seq_len和batch你会看到 KV Cache 这块按比例涨。这解释了第 3 章那个矛盾批越大吞吐越高但 KV Cache 越占地方能开多大的批其实是被显存卡住的。4.3 怎么观察显存分两步看一是看总量用了多少、剩多少二是看构成权重占多少、KV Cache 占多少。多数框架在启动时就会打印权重占用剩下的空间才是留给 KV Cache 的。先把「剩余显存 ÷ 单请求 KV Cache」算出来你就知道了这张卡能同时服务多少个请求的上限 —— 这个数往往比调参本身更能决定你的配置。第 5 章 三个信号瓶颈在显存、在算子还是在调度5.1 一张判据表调不动的时候先分诊别乱试。信号你观察到的现象指向的瓶颈先做的动作不要做的事批大小一调大就显存不足 / 长上下文直接失败显存降批、缩短上下文、上量化、检查 KV Cache 是否被重算不要先去调采样参数加速器利用率长期上不去但延迟很高算子 / 后端换或补 Execution Provider、确认算子是否被后端支持不要盲目加线程数批够大了吞吐还是不涨、延迟却涨调度检查请求排队与批的形成逻辑不要继续加批单请求很快一多请求就整体变慢显存或调度看 KV Cache 与批策略不要把锅甩给模型本身5.2 显存瓶颈批上不去、长上下文就炸一个典型的信号是「批大小稍微加大就失败」或者「上下文一长就失败」。第 4 章已经给出原因权重和 KV Cache 抢同一块显存。真要动手顺序通常是「先上量化压权重 → 再想办法压 KV Cache → 再往后才是缩上下文」。5.3 算子瓶颈算力没用满另一个方向的信号是「加速器利用率低但延迟很高」。这时候问题往往不在模型逻辑而在于某些算子在目标硬件上没有高效实现 —— 也就是算子计算图里的一个基本运算单元比如一次矩阵乘在硬件上「跑得不够快」。这里正好是 ONNX Runtime 这类推理引擎的用武之地。官方文档对它的定位是ONNX Runtime is a cross-platform machine-learning model accelerator, with a flexible interface to integrate hardware-specific libraries.人话它是个跨平台的模型加速器用一套灵活接口去对接各家硬件库。它给出的机制是「先对计算图做一批图优化再按可用硬件把它切成子图交给对应的执行后端」ONNX Runtime applies a number of graph optimizations on the model graph then partitions it into subgraphs based on available hardware-specific accelerators.那些「执行后端」在 ONNX Runtime 里叫Execution Provider执行提供程序简写 EP。官方定义ONNX Runtime works with different hardware acceleration libraries through its extensible Execution Providers (EP) framework to optimally execute the ONNX models on the hardware platform.一个关键的工程细节是EP 是按优先级排序的一串官方示例里的顺序是[CUDAExecutionProvider, CPUExecutionProvider]—— 意思是「能用加速器的算子就用加速器剩下的落回 CPU」。所以「加速器利用率低」的第一反应应该是查有没有算子被落回了 CPU而不是加线程。一个要留心的坑量化不是只有好处。ONNX Runtime 的量化文档在常见问题里直说量化本身有「量化—反量化」的额外开销在老设备上跑出更差性能并不罕见。所以量化要先确认目标硬件支持对应的整型运算再谈收益。⚠️代码待验证# 观察显存占用与加速器利用率按你自己的环境替换工具# 一、看显存占用nvidia-smi# 二、周期性看利用率判断是「算不动」还是「在等数据」watch-n1nvidia-smi判据四如果利用率低且显存还有余量 → 偏算子或后端问题如果利用率已经很高但吞吐不涨 → 多半到了内存带宽或调度的墙如果显存先满 → 显存瓶颈。三种情况的下一步完全不同。这份资料是什么本文用到的官方仓库说明、论文与推理引擎文档还有配套的课程目录我整理在同一个资料包里对照着看理解会更快。放在资料包里扫码即可获取第 6 章 C 老手能直接用上的三个技能6.1 内存池与零拷贝推理服务有个特点分配和释放非常频繁每个请求、每个新字都在动缓存而且大小不一。这跟 C 里做高性能服务遇到的「小对象反复分配」是同一类问题。能直接用上的两件事一是内存池预先申请一大块自己按页切分复用避免频繁向系统要内存二是零拷贝让数据在搬运过程中不被无谓地复制尽量传递引用或视图。第 3 章提到的 PagedAttention 本质就是一种「显存里的内存池 分页」你理解它的成本比新学一个概念低得多。6.2 SIMD 与缓存友好SIMD单指令多数据一条指令同时处理多个数据。推理里大量的矩阵乘、归一化都天然适合向量化这也是为什么硬件后端会针对 SIMD 指令集做优化。比 SIMD 更值钱的是缓存友好让数据按访问顺序排列减少缓存未命中。C 里「结构体数组」和「数组结构体」怎么选、循环按行还是按列走这些判断在推理的算子实现里一字不差地复用。你不一定要亲手写算子但要能看懂「这个算子快是因为它访存友好」。6.3 线程池与流水线推理服务要同时处理很多请求还要在「预填充」和「解码」两种活之间切换。这就是你熟的那套线程池固定一批工作线程反复用省掉频繁创建销毁的开销和流水线把任务拆成几段并行推进让不同阶段同时有活在干。把第 2、3 章串起来看让预填充和解码在同一时刻都有任务在跑而不是串成一条线就是典型的流水线思想。C 老本行在推理侧对应什么什么时候用得上内存池 / 对象池显存分页复用如 PagedAttention 思路显存碎片多、批开不大时零拷贝 / 引用传参张量在算子间传递不复制数据搬运成为瓶颈时SIMD / 缓存友好算子实现与访存顺序加速器利用率低时线程池请求的并发执行多请求排队时流水线预填充与解码重叠执行首 token 延迟与吞吐互相拉扯时判据五能对照上这张表的某一格说明你是在用已有经验读现成框架对照不上、只能靠猜才需要去补新的底层知识。第 7 章 什么时候该停手7.1 停手的三个条件不是「调到理想状态」才停。满足下面任一条就可以先停手三个指标里已经有一个明显被牺牲。比如为了吞吐把首 token 延迟拉到用户无法接受 —— 那不是一个可交付的配置再优化也没意义。继续动参数两个指标开始互相交换吞吐涨一点、延迟涨一截。这说明已经到了当前方案的边界该换策略加显存、上量化、改批策略而不是继续拧同一个旋钮。你改的东西超过了你能验证的范围。比如在没跑过目标硬件的情况下改算子参数那不如先停手把能观测的东西观测清楚。7.2 三条纪律一次只动一个旋钮。批大小、量化档位、上下文长度同时改事后分不清是哪个起的作用。先测基线再谈优化。没有基线数字任何「感觉快了」都不成立。分阶段记录不要只记总数。首 token 延迟和吞吐是两个数混在一起你会一直看错方向。7.3 把这三份材料读薄比读十篇转述有用截至 2026-10-07围绕推理性能下面三份一手材料值得先读材料发布方读它解决什么问题llama.cpp 仓库 READMEggml-org / llama.cpp看清「用 C/C 做推理」的目标与量化档位PagedAttention 论文arXiv:2309.06180vLLM 团队SOSP 2023理解批处理与 KV Cache 显存管理的矛盾ONNX Runtime 官方文档含 EP 与量化页微软看清「图优化 执行后端」这套推理引擎机制这三份材料共同回答一个问题性能不是玄学它是由显存、数据搬运和任务调度这三件事决定的—— 而这三件事恰好是 C 老手的日常。⚠️代码待验证# 想快速上手观察先从本地跑通一个带日志的推理服务开始# 参数以你本地版本 --help 输出为准不要照抄llama-server-m/path/to/model.gguf --ctx-size4096--port8080这份资料是什么这三份一手材料和本文用到的观察方法我整理在同一份资料包里配合课程目录一起看更成体系。放在资料包里扫码即可获取附表 A本文引用事实与出处对照表#事实出处文档名 发布方 链接本文位置1项目定位为「LLM inference in C/C」目标是在尽量少配置下于广泛硬件上实现大模型及视觉语言模型推理特性含无依赖纯 C/C 实现llama.cpp 官方仓库 READMEggml-org https://github.com/ggerganov/llama.cpp1.12支持 1.5 比特至 8 比特整数量化目的是更快推理、更少内存支持 CPUGPU 混合推理同上4.13仓库现归属 ggml-org 组织页面标题为 ggml-org/llama.cpp同上1.14高吞吐服务需要一次批处理足够多请求但每个请求的 KV Cache 很大且动态增减管理不好会因碎片与重复被显著浪费从而限制批大小Efficient Memory Management for Large Language Model Serving with PagedAttentionvLLM 团队arXiv:2309.06180SOSP 20232023-09-12 提交 https://arxiv.org/abs/2309.061803.25提出 PagedAttention灵感来自操作系统的虚拟内存与分页技术同上3.26vLLM 在同延迟下把吞吐提升为原来的 2-4 倍同上3.27ONNX Runtime 定位为跨平台机器学习模型加速器提供灵活接口对接硬件专用库ONNX Runtime 官方文档首页微软 https://onnxruntime.ai/docs/5.38对模型图做多项图优化再按可用硬件专用加速器把图切分为子图同上5.39通过可扩展的 Execution ProvidersEP框架对接不同硬件加速库以在硬件平台上优化执行Execution ProvidersONNX Runtime 官方文档 https://onnxruntime.ai/docs/execution-providers/5.310EP 按优先级排序官方示例为 [‘CUDAExecutionProvider’, ‘CPUExecutionProvider’]能加速的走加速器、其余回退 CPU同上5.311量化指 8 比特线性量化映射关系为 val_fp32 scale * (val_quantized - zero_point)量化值 8 比特宽可为有符号或无符号Quantize ONNX modelsONNX Runtime 官方文档 https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html4.112量化有「量化—反量化」开销在老设备上跑出更差性能并不罕见一般建议对循环神经网络与 Transformer 类模型用动态量化同上5.313权重大小 参数量 × 每参数字节一个 70 亿参数模型 FP32 约 28GB、BF16 约 14GB通用换算按 IEEE 754 单精度每个参数 4 字节与 bfloat16每个参数 2 字节宽度直接相乘4.114KV Cache 粗算 层数 × 键值头数 × 头维度 × 序列长度 × 批大小 × 每元素字节 × 2待验证按注意力机制键、值两组中间张量的通用结构推导仅用于量级判断具体系数与是否启用分组查询注意力等以所用框架实现为准4.2附表 B术语速查表术语一句话解释在本文哪里用到推理inference用训好的模型跑出结果而不是继续训练全文预填充prefill一次性读完整段提示词并算出第一个字的阶段2.1、2.2解码decode从第二个字起、每次基于前文再算一个字的阶段2.1、3.1首 token 延迟用户从发请求到看见第一个字的等待时间1.2、2吞吐稳定输出时每秒能产出的 token 数1.2、3批处理batching把多个请求攒成一批一起算以摊薄权重读取3.2KV Cache缓存前文每个字的键、值中间张量避免重复计算4.2显存加速卡上的内存权重与 KV Cache 共用4内存带宽单位时间能从内存搬进计算单元的数据量3.1算子operator计算图里的一个基本运算单元如一次矩阵乘5.3Execution ProviderEPONNX Runtime 里对接某种硬件的执行后端5.3图优化在真正计算前对计算图做的等价改造5.3量化quantization把高位浮点参数压到低位整数省显存、换速度4.1、5.3内存池预先申请大块内存自行复用避免频繁分配6.1零拷贝搬运数据时尽量不复制只传引用或视图6.1SIMD一条指令同时处理多个数据6.2缓存友好让数据按访问顺序排列减少缓存未命中6.2线程池固定一批线程反复复用省去频繁创建销毁6.3流水线把任务拆成几段并行推进让各阶段同时有活在干6.3写在最后这篇用到的资料写这篇文章时把相关的官方文档和源码又翻了一遍顺手也整理了几份配套的东西大模型学习路线图从零基础到能自己动手做 Agent按阶段说明每一步该学什么、哪些可以先跳过《LangChain LangGraph MCP 智能体开发实战》视频课7 个模块从私有化部署、EmbeddingRAG 到 MCPAgent 全流程AI 大模型知识库在线可查Agent Skills 从入门到落地、Claude Skills 完全指南等专题按目录浏览即可640 套 AI 大模型行业报告 经典 PDF 书籍看行业落地案例和别人怎么做的时候用得上大模型零基础到精通教学视频跟着敲一遍比只读文档快得多资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「AI」优先通过。资料按「先路线、再动手、最后查漏」的顺序整理好了建议先看学习路线那一份照着它挑一条适合自己当前基础的路径再往下看。