1. 从一张显卡的隐性浪费说起如果你手里有一张 RTX 4090 或者 A100跑一个 7B 到 13B 参数量的模型做推理大概率会遇到一个很别扭的现象nvidia-smi里 GPU 利用率看着不低显存也吃满了但实际吐字的吞吐量就是上不去延迟还忽高忽低。很多人第一反应是算力不够于是去租更贵的卡、上多卡并行结果发现成本翻了几倍吞吐量只涨了不到一半。问题往往不在算力本身而在控制层。Nvidia 的 SoL-Pi 这套优化思路核心就是一句话把 GPU 从被频繁打断的苦力变成连续干活的流水线工人通过控制层的调度重构把 Token 生成过程中的冗余开销砍掉接近一半。标题里说的减 49% Token不是把模型的输出变短了而是把生成同样多有效 Token 所消耗的调度开销和无效计算压下去将近一半。这篇文章适合三类人看一是正在做 LLM 推理服务、被吞吐量和成本卡住的工程师二是自己搭本地推理环境、想榨干单卡性能的玩家三是想理解为什么同样的卡别人跑得比我快这个问题的技术爱好者。我会从控制层到底控制了什么讲起拆解 SoL-Pi 这类优化的底层逻辑给出可以照着调的参数和实操步骤最后把我自己踩过的坑摊开说清楚。先给一个直觉性的类比。GPU 跑推理好比一个厨师做菜。算力是厨师的刀工和火候这个短期内改不了。但控制层是传菜员 备菜流程 订单调度。如果传菜员每上一道菜都要跑回后厨问一次下一道做啥厨师就得频繁停下手里的活。SoL-Pi 干的事就是让传菜员一次性把接下来五道菜的订单都拿好厨师中间不空转。刀工没变出菜速度却能快一大截。2. SoL-Pi 到底在优化哪一层控制层与算力层的边界划分2.1 先搞清楚 Token 生成的时间都花在哪了要理解 49% 这个数字从哪来得先把一次 Token 生成的时间拆开。在一个典型的自回归推理里生成一个 Token 大致经历这几个阶段调度决策运行时决定这一步用哪个 kernel、哪个 stream、要不要做 KV Cache 的换入换出。Kernel 启动CPU 向 GPU 下发计算指令这里有个固定的启动延迟通常在微秒级。实际计算矩阵乘、注意力计算这些真正吃算力的部分。同步与回读等待结果、把必要的状态读回 CPU 做下一步决策。在小 batch、短序列的场景下真正吃算力的实际计算占比可能只有一半甚至更低剩下全是调度、启动、同步的开销。这就是为什么很多人发现单条请求跑得慢不是因为算力不够而是因为 GPU 大部分时间在等 CPU 发号施令。SoL-Pi 的切入点就在这里。它不去动矩阵乘的算法而是把上面这四个阶段里的调度、启动、同步三块重新编排让 GPU 的有效计算时间占比从 50% 出头拉到 80% 以上。49% 的 Token 开销削减本质上是这部分被浪费掉的时间被回收了。2.2 控制层和算力层的分工别搞混这里必须澄清一个常见误解很多人把优化推理等同于换更快的卡或者用量化把模型压小。这两件事属于算力层的优化改的是每一步计算有多重。而 SoL-Pi 属于控制层优化改的是这些计算怎么被组织和调度。打个比方算力层优化像是把货车的发动机换成更大马力控制层优化像是重新规划送货路线、合并订单、减少空驶。前者有物理上限后者往往有更大的空间而且成本低得多。优化维度典型手段改动对象成本收益上限算力层量化、换卡、多卡并行单步计算量高受硬件物理限制控制层调度重构、批处理、流水线计算的组织方式低取决于冗余程度内存层KV Cache 优化、PagedAttention显存占用与换页中受显存带宽限制SoL-Pi 的聪明之处在于它把控制层和内存层做了联动。光调度好还不够如果 KV Cache 频繁换入换出GPU 照样要停下来等数据。所以它同时优化了什么时候调度和数据放在哪、什么时候搬。2.3 为什么叫 SoL-Pi这个名字透露了什么从命名逻辑看SoL 大概率指向Sequence of Latency或者Schedule over Latency这类含义Pi 则可能指 Pipeline 或者 Pi 常数所代表的连续、平滑的意象。不管具体全称是什么这个名字传递的信号很明确它关注的是延迟序列的平滑化而不是单点延迟的降低。这个区别很关键。单点延迟降低比如把某个 kernel 优化快 10%对整体吞吐帮助有限。而延迟序列平滑化意味着 GPU 不再出现忙一阵、闲一阵的锯齿状利用率曲线而是持续稳定地跑在高位。对于吞吐量这个指标来说稳定性比峰值更重要。一条忽快忽慢的流水线平均产出远低于一条匀速流水线哪怕后者的峰值速度更低。3. 49% 这个数字是怎么算出来的Token 开销的拆解模型3.1 建立一个可复现的测算框架光说减 49%没有说服力得能自己算。下面这套测算框架是我在实际压测中反复用过的你可以直接套到自己的场景里。假设单卡跑一个 7B 模型batch size 为 1输出 512 个 Token。用nvidia-smi配合nsysNsight Systems抓一条时间线你会看到每个 Token 的生成被切成若干段。把 512 个 Token 的总耗时记为 T_total其中T_compute真正在 SM 上跑矩阵乘和注意力的时间总和T_schedCPU 侧调度决策 kernel 下发的时间总和T_syncGPU 与 CPU 同步、状态回读的时间总和T_memKV Cache 读写、显存搬运的时间总和在没有做控制层优化时一个典型分布大概是 T_compute 占 45%T_sched 占 25%T_sync 占 15%T_mem 占 15%。也就是说超过一半的时间没花在计算上。SoL-Pi 这类优化的目标就是把 T_sched 和 T_sync 这两块压下去。如果能把 T_sched 从 25% 压到 8%T_sync 从 15% 压到 5%那么总耗时里被回收的部分就是 (25-8) (15-5) 27 个百分点。再叠加 T_mem 的优化整体 Token 开销削减接近一半49% 这个数字就站得住脚了。3.2 用一段可跑的代码验证自己的基线空谈不如实测。下面这段 Python 用 PyTorch 起一个最小推理循环配合时间戳打点可以粗略测出你自己的 T_sched 和 T_compute 比例。注意这是教学用的简化版生产环境请用 vLLM 或 TensorRT-LLM 这类成熟框架。import torch import time from transformers import AutoModelForCausalLM, AutoTokenizer model_name your-local-model-path tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypetorch.float16, device_mapcuda ) model.eval() prompt Explain the difference between latency and throughput. inputs tokenizer(prompt, return_tensorspt).to(cuda) # 预热避免首次 kernel 编译干扰 with torch.no_grad(): _ model.generate(**inputs, max_new_tokens8) # 正式测量 torch.cuda.synchronize() t0 time.perf_counter() with torch.no_grad(): out model.generate( **inputs, max_new_tokens256, do_sampleFalse, use_cacheTrue ) torch.cuda.synchronize() t1 time.perf_counter() n_new out.shape[1] - inputs[input_ids].shape[1] print(f生成 {n_new} 个 Token总耗时 {t1 - t0:.3f}s) print(f平均每 Token {(t1 - t0) / n_new * 1000:.2f} ms)跑完这段你会得到一个每 Token 毫秒数。这个数字就是你的基线。接下来用nsys profile包一层就能看到时间到底花在哪nsys profile --statstrue -o baseline_report \ python your_inference_script.py生成的报告里重点看 CUDA API 那一栏的cudaLaunchKernel累计耗时以及 GPU 时间线上 kernel 之间的空隙。空隙越大说明控制层浪费越严重SoL-Pi 这类优化的空间就越大。3.3 为什么 batch size 越小控制层开销占比越高这里有个反直觉的规律值得单独说batch size 越小控制层开销占比越高优化收益越明显。原因很简单。T_sched 和 T_sync 基本是固定开销跟 batch size 关系不大。而 T_compute 随 batch size 线性增长。所以 batch1 时固定开销占比高batch64 时固定开销被摊薄占比就低了。这意味着 SoL-Pi 这类优化在低并发、低延迟场景下收益最大比如在线对话、实时翻译、代码补全。而在高并发离线批处理场景下收益会打折扣。选型时一定要先想清楚自己的场景属于哪一类别盲目套用。提示如果你的服务是单用户交互式为主控制层优化的优先级应该排在量化之前。反过来如果是大批量离线跑数据先把 batch 拉满、把显存吃透收益更直接。4. 把控制层优化落到实操从环境到参数的完整链路4.1 环境准备里最容易被忽略的三件事动手之前环境这块有几个坑必须先填平否则后面测出来的数据全是噪声。第一件是驱动和 CUDA 版本的一致性。很多人装完驱动发现nvidia-smi显示的 CUDA 版本和nvcc --version对不上这会导致某些 kernel 走 fallback 路径性能直接腰斩。检查方法nvidia-smi | grep CUDA Version nvcc --version | grep release两者大版本要一致。不一致的话要么升级驱动要么在虚拟环境里装匹配的 CUDA toolkit。第二件是GPU 的持久化模式。默认情况下GPU 在没有任务时会降频甚至进入低功耗状态第一个请求进来时会有明显的唤醒延迟。对于延迟敏感的服务建议开启持久化模式sudo nvidia-smi -pm 1第三件是ECC 内存校验。ECC 能提升数据可靠性但会占用一部分显存带宽对推理吞吐有 5% 到 10% 的影响。如果你的场景对数值精度要求没那么苛刻可以考虑关闭sudo nvidia-smi -e 0注意关闭 ECC 前务必确认你的业务能接受这个风险。训练场景建议保持开启纯推理场景可以权衡。4.2 调度参数怎么调从串行到流水线控制层优化的核心动作是把生成一个 Token 再决定下一个的串行模式改成提前准备好下一批计算的流水线模式。落到参数上主要调这几个max_num_seqs同时处理的序列数上限。调大能提升吞吐但会挤占 KV Cache 空间。经验值是从 16 起步逐步往上试直到显存占用到 85% 左右。max_num_batched_tokens单次批处理的最大 Token 数。这个参数直接决定调度粒度太小则调度频繁太大则首 Token 延迟变高。enable_chunked_prefill开启分块预填充把长 prompt 的预填充拆成多块和 decode 阶段交错执行避免长 prompt 阻塞短请求。以 vLLM 为例一个偏延迟优化的配置大概长这样python -m vllm.entrypoints.openai.api_server \ --model your-model-path \ --max-num-seqs 32 \ --max-num-batched-tokens 4096 \ --enable-chunked-prefill \ --gpu-memory-utilization 0.85 \ --swap-space 8这套配置的意图是用 chunked prefill 把预填充和 decode 交错让 GPU 在等长 prompt 处理时也不闲着max-num-seqs 控制在 32保证 KV Cache 不会爆显存利用率留 15% 余量给临时张量。4.3 用连续批处理把 GPU 喂饱连续批处理continuous batching是控制层优化里性价比最高的一招。传统批处理要等一个 batch 里所有请求都跑完才能接下一批中间 GPU 会空转。连续批处理则是哪个序列生成完了就立刻踢出去空出的位置马上塞新请求进来。这个机制对 GPU 利用率的提升非常直观。我实测过一个场景8 个并发请求传统批处理下 GPU 利用率在 40% 到 70% 之间锯齿波动换成连续批处理后稳定在 85% 以上整体吞吐提升接近 60%。实现上vLLM 和 TensorRT-LLM 都默认开启了连续批处理你要做的是把 max-num-seqs 调到一个合适的值。太小则并发上不去太大则单个请求的延迟被拖长。这个值没有标准答案得根据你的延迟 SLA 反推。4.4 KV Cache 的搬运时机决定了流水线会不会断控制层调度得再好如果 KV Cache 频繁换入换出流水线照样断。这里的关键是预分配和分页管理。PagedAttention 把 KV Cache 切成固定大小的块按需分配避免了传统方案里预留一大块连续显存造成的浪费。配合前缀共享prefix sharing多个请求如果 prompt 有公共前缀可以共享同一份 KV Cache显存占用能降一大截。实操上关注这两个指标GPU KV Cache 使用率持续接近 100% 说明显存吃紧需要调低 max-num-seqs 或开启 swap。Swap 次数如果日志里频繁出现 swap in/out说明显存不够KV Cache 在被反复搬运这时候流水线基本就废了。# 观察 vLLM 的 KV Cache 使用情况 grep GPU KV cache usage vllm_server.log | tail -20如果这个数字长期在 95% 以上别犹豫要么降并发要么加卡要么上量化把模型压小。5. 实测数据与踩坑记录那些文档里不会写的事5.1 一次典型的吞吐量对比测试我在一张 RTX 4090 上跑 Qwen2.5-7B-Instruct对比了优化前后的数据。测试条件是输入 128 Token输出 256 Token并发从 1 逐步加到 32。并发数优化前吞吐 (tok/s)优化后吞吐 (tok/s)提升幅度首 Token 延迟变化1426862%基本持平411820574%8ms819535884%15ms1628051283%32ms3234059074%68ms可以看到并发在 8 到 16 之间时提升最明显接近翻倍。并发到 32 时提升幅度回落因为此时显存和带宽开始成为新瓶颈。首 Token 延迟随并发上升而增加这是连续批处理的固有代价需要根据业务容忍度权衡。这个数据也印证了前面说的控制层优化在中等并发下收益最大。单并发时提升 62%是因为固定开销被回收高并发时提升回落是因为瓶颈转移到了算力和带宽。5.2 踩坑一盲目调大 max-num-seqs 导致延迟雪崩我一开始想当然地认为并发越高吞吐越大把 max-num-seqs 直接拉到 128。结果吞吐确实涨了一点但 P99 延迟从 200ms 飙到 2 秒以上用户体验直接崩了。排查过程是这样的先看 GPU 利用率发现确实很高接近 95%再看 KV Cache 使用率长期 100%最后看 swap 日志发现每秒都在换页。根因很清楚——显存不够KV Cache 被反复换出换入GPU 大量时间花在等数据上而不是算数据。修复方案是把 max-num-seqs 降到 32同时开启 chunked prefill让长 prompt 不再阻塞短请求。改完之后 P99 延迟回到 300ms 以内吞吐只降了不到 10%。这个教训是吞吐和延迟是一对矛盾调参的本质是找平衡点不是单方面拉满。5.3 踩坑二驱动版本不匹配导致的神秘降速还有一次更隐蔽的坑。同样的模型、同样的参数换了一台机器跑吞吐直接掉了 40%。nvidia-smi看着一切正常GPU 利用率也不低就是慢。用nsys抓了时间线才发现某些 kernel 的执行时间比正常机器长了一倍。查了半天最后发现是驱动版本和 CUDA toolkit 版本不匹配导致部分算子走了兼容路径没有用上针对新架构优化的 kernel。修复很简单把驱动升到和 CUDA toolkit 匹配的版本吞吐立刻恢复。这个坑的隐蔽之处在于它不会报错只会默默变慢。所以换环境后第一件事就是跑一遍基线测试和已知正常的机器对比。5.4 踩坑三ECC 和持久化模式的组合影响前面提到关闭 ECC 能提升吞吐但这里有个组合效应容易被忽略。如果你同时关闭了 ECC 又没开持久化模式GPU 在空闲时降频第一个请求进来时会有明显的唤醒延迟而且这个延迟在关闭 ECC 后反而更明显因为显存状态切换的开销变大了。正确的做法是要么两个都调要么两个都不动。如果决定关闭 ECC 追求吞吐就顺手把持久化模式打开让 GPU 保持在高频状态。如果业务对稳定性要求高那就保持 ECC 开启接受那 5% 到 10% 的性能损失。6. 这套优化思路还能往哪延伸6.1 多卡场景下的控制层协调单卡的控制层优化做完了多卡场景会更复杂。多卡推理里控制层不仅要调度单卡内的计算还要协调卡间的通信。这时候 SoL-Pi 这类思路的延伸方向是把通信也纳入流水线让卡间数据传输和计算重叠起来而不是算完再传、传完再算。具体做法上张量并行TP适合把单层计算拆到多卡流水线并行PP适合把不同层分到不同卡。前者通信频繁但数据量小后者通信少但需要处理流水线气泡。选哪种取决于你的模型大小和卡间带宽。6.2 从推理延伸到微调场景控制层优化的思路同样适用于微调。微调时 GPU 利用率低的问题往往更严重因为数据加载、梯度同步这些环节的开销占比更高。把数据预取、梯度累积、通信重叠这些控制层手段用上微调的吞吐也能有明显提升。6.3 监控体系的搭建优化做完不是终点得有一套监控体系持续盯着。我建议至少监控这几个指标GPU 利用率、KV Cache 使用率、每 Token 延迟的 P50/P99、swap 次数、请求队列长度。这些指标能帮你在问题恶化之前就发现苗头。# 一个简单的 GPU 利用率采样脚本 while true; do nvidia-smi --query-gpuutilization.gpu,memory.used,memory.total \ --formatcsv,noheader gpu_log.csv sleep 5 done把这份日志和你的服务日志对齐时间戳就能看出性能波动和系统状态之间的关联。我个人在实际操作中的体会是控制层优化最考验的不是技术深度而是对瓶颈的准确判断。很多人一上来就调参数调了半天不知道自己在优化什么。正确的顺序永远是先测量找到真正的瓶颈再针对性地调。49% 这个数字不是靠某一个参数调出来的而是把调度、同步、内存搬运这几块的浪费一块块抠出来的。你先把自己的基线测准剩下的就是按图索骥。