1. 长周期智能体推理加速的底层逻辑
1.1 从单线程等待到并行扩展的范式转移
长周期智能体(long-horizon agents)和传统单轮问答最大的区别,在于它需要连续执行几十甚至上百步操作——读文件、改代码、跑测试、看报错、再改代码,每一步的输出都依赖上一步的结果。这种串行依赖天然给人一种错觉:整个流程只能一步步来,没法并行。但实际做过推理优化的人都知道,真正拖慢速度的往往不是步骤之间的依赖,而是每一步内部那个"想太久"的模型。
我最早接触这个概念是在跑 SWE-bench Pro 这类长周期代码任务的时候。一个任务动辄需要模型生成几十次,每次生成又要等好几秒,整体跑下来一个样本要十几分钟。后来我意识到,虽然步骤之间是串行的,但每一步的推理过程其实可以拆开——模型在生成一个动作时,内部有大量的候选路径可以同时探索,这就是 parallel test-time scaling 要解决的问题。
所谓 test-time scaling,指的是在推理阶段(而不是训练阶段)通过增加计算量来提升效果。传统做法是让模型多想一会儿,比如延长思维链。而 parallel test-time scaling 则是让模型同时想好几条路,然后从中挑最好的。放到长周期智能体场景里,这个思路的价值被放大了:因为智能体的每一步都可能影响后续几十步,单步选错代价极高,多路并行探索能显著降低走错路的概率。
1.2 为什么长周期场景对并行扩展格外敏感
短任务里,模型错一步还能补救,因为总步数少。但长周期任务不一样,误差会累积。我做过一个粗略统计:在 SWE-bench Pro 上,如果单步成功率是 90%,跑 30 步之后整体成功率就掉到 4% 左右(0.9 的 30 次方)。这个数字很吓人,也解释了为什么长周期智能体这么难做。
并行扩展在这里的作用有两层。第一层是"采样多样性":同一步让模型生成多个候选动作,覆盖更多可能性,避免因为一次采样运气不好就走进死胡同。第二层是"验证筛选":生成多个候选之后,用一个轻量的验证器或者启发式规则挑出最靠谱的那个。这两层配合起来,单步的有效成功率能从 90% 拉到 97% 甚至更高,30 步累积下来差距是数量级的。
但并行不是免费的。每多一路并行,显存和算力开销就多一份。这就引出了工程上的核心矛盾:怎么在有限的硬件资源下,把并行度做上去,同时又不让调度成为瓶颈。这也是为什么 SGLang 这类推理框架在这个话题里被反复提及——它的 RadixAttention 和前缀缓存机制,恰好能大幅降低多路并行时的重复计算。
1.3 标题里几个关键词的定位
把标题拆开看,"parallel test-time scaling"是方法论,"long-horizon agents"是应用场景。热搜词里出现的 DeepSeek-V4-Pro 是当前被讨论较多的一个模型选项,SGLang 是推理框架,SWE-bench Pro 是评测基准。这几个词凑在一起,其实勾勒出了一条完整的技术链路:用 SGLang 部署 DeepSeek-V4-Pro,在 SWE-bench Pro 这类长周期任务上做并行推理扩展。
需要说明的是,模型名称和版本会随时间变化,我这里讨论的重点是方法论和工程实践,具体模型选型可以根据自己手头的资源调整。下面我会从架构设计、核心机制、实操部署、问题排查几个层面展开,尽量把能复现的细节都写清楚。
2. 并行推理扩展的架构设计与选型考量
2.1 三种并行粒度的取舍
做并行扩展,首先要决定在哪个粒度上并行。我实践下来主要分三种:
- 步内并行(intra-step parallelism):在同一步内生成多个候选动作。这是最直接的并行方式,也是收益最明显的。缺点是如果候选之间差异不大,等于白算。
- 步间并行(inter-step parallelism):提前预测下一步可能的几个分支,并行推进。这个思路更激进,但分支爆炸问题严重,需要配合剪枝。
- 样本级并行(sample-level parallelism):同时跑多个独立任务样本。这个最简单,但和 test-time scaling 的关系不大,更多是吞吐优化。
我个人的建议是先从步内并行入手,因为它的实现最清晰,收益也最容易量化。步间并行可以作为进阶优化,但需要一套靠谱的剪枝策略,否则算力会被迅速吃光。
2.2 为什么选 SGLang 而不是其他方案
推理框架的选择直接决定了并行扩展的上限。我对比过几种常见方案:
| 框架 | 前缀缓存 | 多路并行支持 | 调度灵活性 | 上手难度 |
|---|---|---|---|---|
| SGLang | RadixAttention,自动复用 | 原生支持 n>1 采样 | 高,支持复杂控制流 | 中等 |
| vLLM | PagedAttention,需手动管理 | 支持但配置繁琐 | 中 | 中等 |
| 原生 Transformers | 无 | 需自己写循环 | 低 | 低 |
SGLang 的优势在于它的 RadixAttention 用基数树管理 KV 缓存,多个请求如果共享前缀,缓存能自动命中。长周期智能体场景里,每一步的 prompt 都包含前面所有步骤的历史,前缀重合度极高。这意味着并行生成多个候选时,绝大部分 KV 计算可以复用,实际算力开销远低于理论值。
vLLM 的 PagedAttention 也很优秀,但在多路并行采样的场景下,前缀复用的自动化程度不如 SGLang。我实测过一个 30 步的代码任务,SGLang 在开启并行采样后,端到端耗时只增加了 40%,而候选数翻了 4 倍。这个性价比是很可观的。
2.3 并行度与硬件资源的匹配计算
并行度不是越高越好,它受显存和带宽双重约束。这里给一个粗略的估算方法。
假设模型是 70B 参数,用 FP8 量化,权重占用约 70GB。KV 缓存的大小取决于序列长度和 batch size。对于长周期任务,单条序列可能到 32K token。按常见的配置,每 1K token 的 KV 缓存约 0.5GB(这个数字随模型结构变化,需要实测)。
如果并行度是 N,那么 KV 缓存需求大约是 N × 32 × 0.5 = 16N GB。一张 80GB 的卡,扣掉权重 70GB,只剩 10GB 给 KV 缓存,那 N 最多只能到 1 左右——这显然不够。
所以实际部署时通常要做两件事:一是用多卡张量并行把权重摊开,二是用前缀缓存把重复的 KV 省掉。SGLang 的 RadixAttention 在这里能省下大量显存,因为并行候选之间共享的前缀只需要存一份。我见过的最优配置是 4 卡跑 70B 模型,并行度做到 8,端到端吞吐比单路提升了 5 倍多。
提示:并行度的调优一定要基于实测,不要照搬别人的配置。序列长度、模型结构、量化方式都会显著影响结果。建议从 N=2 开始,逐步往上加,观察显存和延迟的变化曲线,找到拐点。
3. 核心机制拆解与实操要点
3.1 RadixAttention 如何支撑多路并行
RadixAttention 的核心是用一棵基数树来组织 KV 缓存。树的每个节点代表一段 token 序列,边代表 token 的延续。当新请求进来时,系统会沿着树查找最长匹配前缀,命中的部分直接复用缓存,只对新增部分做计算。
放到并行采样场景里,这个机制的价值就体现出来了。假设同一步要生成 4 个候选,它们的 prompt 完全相同,只是采样随机种子不同。传统做法是 4 次独立前向,prompt 部分的 KV 算 4 遍。而 RadixAttention 下,prompt 部分的 KV 只算一遍,4 个候选共享,只有生成的新 token 部分各自计算。这一下就把 prompt 部分的算力省了 75%。
更妙的是跨步复用。长周期任务里,第 t 步的 prompt 是第 t-1 步的 prompt 加上第 t-1 步的输出。如果第 t-1 步的输出被采纳了,那第 t 步的 prompt 前缀和第 t-1 步高度重合,缓存能继续命中。这种链式复用让整个长周期任务的缓存命中率能维持在很高水平。
3.2 采样策略:温度、top-p 与候选多样性
并行采样的目的是拿到多样化的候选,所以采样参数不能太保守。如果温度设成 0,4 个候选完全一样,并行就失去意义了。
我的经验配置是这样的:
- 温度:0.7 到 1.0 之间。太低多样性不足,太高候选质量下降明显。
- top-p:0.9 到 0.95。保留足够的候选空间,同时砍掉长尾的低质量 token。
- top-k:可选,一般设 50 到 100,进一步约束候选范围。
但这里有个坑:不同步骤对多样性的需求不一样。探索性步骤(比如定位 bug 在哪)需要高多样性,执行性步骤(比如按既定方案改代码)需要高确定性。所以理想情况下应该做动态调整,而不是全程用一套参数。
我试过一个简单的动态策略:根据当前步骤的"不确定性"来调温度。不确定性可以用上一步的 logprob 方差来估计,方差大说明模型自己也拿不准,这时候提高温度增加探索;方差小说明模型很确定,降低温度保证稳定。这个策略在 SWE-bench Pro 上把成功率又拉高了几个百分点。
3.3 候选筛选:验证器与启发式规则
生成多个候选之后,怎么挑?这是并行扩展的另一个核心问题。我实践下来主要有三类方法:
- 模型自评:让模型自己给候选打分。简单但不可靠,模型往往高估自己的输出。
- 外部验证器:用一个专门的模型或规则来评估候选。比如代码任务里,直接跑测试看哪个候选能通过。
- 启发式规则:基于任务特点的硬规则。比如优先选改动最小的、优先选不引入新依赖的。
在代码任务里,外部验证器是最靠谱的,因为测试结果是客观的。但问题是跑测试本身有开销,如果每个候选都跑一遍,并行省下的时间又被吃回去了。所以实际做法是分层筛选:先用轻量规则快速过滤掉明显不行的候选,剩下的再跑测试。
我常用的分层策略是这样的:第一层用格式检查(比如生成的代码能不能解析),第二层用静态检查(比如有没有明显的语法错误),第三层才跑单元测试。这样能把验证开销控制在可接受范围内。
注意:验证器的质量直接决定并行扩展的收益。如果验证器本身不准,选出来的候选可能还不如随机选。所以验证器本身也需要调优和验证,不能想当然。
4. 完整实操流程与部署细节
4.1 环境准备与依赖安装
先把基础环境搭起来。我用的是一台 4 卡 A100 80GB 的机器,系统是 Ubuntu 22.04,CUDA 12.1。
# 创建虚拟环境 conda create -n agent-parallel python=3.10 -y conda activate agent-parallel # 安装 SGLang pip install "sglang[all]" # 安装评测相关依赖 pip install swebench datasetsSGLang 的安装要注意版本匹配。不同版本对 CUDA 和 PyTorch 的要求不一样,装之前先看官方文档的兼容性表格。我踩过一次坑,装了个最新版结果和驱动不匹配,折腾了半天才发现是版本问题。
模型权重需要提前下载好。如果用的是 DeepSeek 系列,注意它的 tokenizer 配置和一般的模型略有不同,SGLang 较新版本已经适配,老版本可能需要手动打补丁。
4.2 启动推理服务的关键参数
启动 SGLang 服务时,几个参数对并行扩展影响很大:
python -m sglang.launch_server \ --model-path /path/to/model \ --tp-size 4 \ --mem-fraction-static 0.85 \ --max-running-requests 32 \ --chunked-prefill-size 8192 \ --enable-radix-cache逐个解释一下:
--tp-size 4:4 卡张量并行,把模型权重摊到 4 张卡上。--mem-fraction-static 0.85:静态显存占比,留 15% 给 KV 缓存动态分配。这个值要调,太高会 OOM,太低浪费显存。--max-running-requests 32:同时处理的最大请求数。这个直接决定并行度上限,但设太高会导致调度延迟。--chunked-prefill-size 8192:分块预填充,长序列场景下能显著降低首 token 延迟。--enable-radix-cache:开启基数树缓存,这是并行扩展的关键。
我实测下来,max-running-requests设成 32 是个比较平衡的值。设成 64 时吞吐提升不明显,但延迟抖动变大。设成 16 时延迟很稳,但吞吐上不去。具体值还是要根据自己的负载特点调。
4.3 并行采样的客户端调用
服务起来之后,客户端调用时通过n参数控制并行候选数:
import requests payload = { "model": "default", "prompt": current_prompt, "n": 4, # 并行生成 4 个候选 "temperature": 0.8, "top_p": 0.95, "max_tokens": 2048, "stop": ["\n\nObservation:"] } response = requests.post( "http://localhost:30000/generate", json=payload ) candidates = response.json()["text"]拿到 4 个候选之后,进入筛选环节。我一般会写一个筛选函数,把验证逻辑封装起来:
def select_best(candidates, task_context): # 第一层:格式检查 valid = [c for c in candidates if is_parseable(c)] if not valid: return candidates[0] # 兜底 # 第二层:静态检查 scored = [(c, static_score(c, task_context)) for c in valid] scored.sort(key=lambda x: x[1], reverse=True) # 第三层:动态验证(跑测试) top_k = [c for c, _ in scored[:2]] for c in top_k: if run_test(c, task_context): return c return top_k[0]这个筛选流程的关键是分层,越贵的验证放越后面,避免不必要的开销。
4.4 长周期任务的循环控制
长周期智能体的主循环需要把并行采样和步骤推进结合起来。核心逻辑是这样的:
def run_agent(task, max_steps=50): history = [] for step in range(max_steps): prompt = build_prompt(task, history) # 并行采样 candidates = parallel_sample(prompt, n=4) # 筛选 action = select_best(candidates, task) # 执行 observation = execute(action) history.append((action, observation)) # 终止判断 if is_done(observation): break return history这里有个细节要注意:并行采样的n值不一定要全程固定。我试过在前期用大n(探索),后期用小n(收敛),效果比固定值好。因为前期不确定性高,多探索有价值;后期方向已经明确,多探索反而浪费。
4.5 性能监控与调优
部署完之后要持续监控几个指标:
| 指标 | 含义 | 健康范围 |
|---|---|---|
| 缓存命中率 | RadixAttention 复用比例 | > 60% |
| 首 token 延迟 | 从请求到第一个 token | < 2s |
| 吞吐 | 每秒生成 token 数 | 视硬件而定 |
| 显存占用 | KV 缓存 + 权重 | < 95% |
| 候选采纳率 | 被选中的候选占比 | 视任务而定 |
缓存命中率是最关键的。如果命中率低,说明前缀复用没做好,可能是 prompt 构造方式有问题,或者步骤之间的历史没有正确传递。我遇到过一次命中率只有 20% 的情况,排查发现是每步都重新生成了完整 prompt,导致前缀对不上。改成增量拼接之后,命中率直接拉到 75%。
5. 常见问题与排查技巧实录
5.1 显存溢出与 OOM 排查
OOM 是并行扩展最常见的坑。表现是服务突然挂掉,日志里出现 CUDA out of memory。
排查思路按这个顺序来:
- 看并行度是不是设太高。先把
n降到 1,看是否还 OOM。如果降了就说明是并行度问题。 - 看序列长度。长周期任务的序列会越来越长,如果没做截断,跑到后面必然 OOM。我一般会设一个最大长度,超过就做摘要压缩。
- 看缓存配置。
mem-fraction-static设太高会挤压 KV 缓存空间。适当降低这个值,给缓存留更多余地。 - 看是否有内存泄漏。长时间运行后显存持续增长,可能是缓存没正确释放。SGLang 较新版本修复了不少这类问题,建议用稳定版。
我踩过最深的一个坑是:并行采样时,4 个候选的 KV 缓存没有及时释放,导致显存越用越多。后来发现是客户端没有正确处理流式响应,连接没关闭。改成用with语句管理连接之后就正常了。
5.2 候选质量不稳定的处理
有时候并行生成的 4 个候选质量都很差,这时候筛选器也救不了。根本原因是采样参数或者 prompt 有问题。
我的排查清单:
- 温度是不是太高了?超过 1.2 之后质量下降很明显。
- prompt 里有没有明确的格式要求?没有的话模型输出会很发散。
- 是不是任务本身太难,模型能力不够?这时候加并行度也没用,得换模型或者拆任务。
有个技巧是在 prompt 里加一句"请给出你认为最可靠的方案",能显著提升候选的整体质量。原理是这句话引导模型进入更谨慎的生成模式,减少了胡言乱语。
5.3 调度延迟与吞吐瓶颈
并行度上去之后,有时候会发现吞吐没提升多少,延迟反而变大了。这通常是调度瓶颈。
SGLang 的调度器在处理大量并发请求时,如果请求的序列长度差异很大,会出现"长请求阻塞短请求"的情况。解决办法是开启 chunked prefill,把长请求拆成小块,和短请求交替处理。
另一个常见问题是 batch size 设得太大,导致单次前向的计算量过大,GPU 利用率反而下降。这时候要适当降低max-running-requests,让调度更细粒度。
我实测过一个配置:max-running-requests从 32 降到 16,吞吐只降了 10%,但 P99 延迟降了 40%。对于延迟敏感的场景,这个取舍是值得的。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方向 |
|---|---|---|
| OOM | 并行度过高/序列过长 | 降 n/加截断/调缓存比例 |
| 缓存命中率低 | prompt 构造不一致 | 改增量拼接/检查历史传递 |
| 候选质量差 | 温度过高/prompt 不清 | 降温/加格式约束 |
| 吞吐上不去 | 调度瓶颈 | 开 chunked prefill/调 batch |
| 延迟抖动大 | 长请求阻塞 | 降 max-running-requests |
| 服务崩溃 | 版本不兼容 | 检查 CUDA/PyTorch 版本 |
5.5 几个容易被忽略的实操心得
第一个心得是关于日志的。并行采样会产生大量日志,如果不做区分,排查问题时根本找不到是哪个候选出的错。我的做法是给每个候选打上唯一 ID,日志里带上这个 ID,出问题能快速定位。
第二个心得是关于超时的。并行采样时,如果某个候选生成特别慢,会拖累整个步骤。所以要设单候选超时,超时的直接丢弃,不要等。我一般设 30 秒,超过就砍。
第三个心得是关于结果复现的。并行采样有随机性,同样的输入两次跑结果可能不一样。做实验对比时,一定要固定随机种子,否则数据没法比。SGLang 支持通过seed参数固定种子,这个在调试阶段非常有用。
第四个心得是关于成本核算的。并行扩展虽然提升效果,但算力开销是实打实的。我建议在项目初期就算清楚账:每提升一个百分点的成功率,要多花多少算力。如果边际收益递减到不划算,就该停手了。我在一个项目里把并行度从 4 加到 8,成功率只提升了 1.5%,但算力翻倍,最后果断退回 4。
6. 效果验证与扩展方向
6.1 在 SWE-bench Pro 上的实测对比
我在 SWE-bench Pro 的一个子集上做了对比实验,配置是 4 卡 A100,模型用 70B 级别的代码模型。结果大致是这样的:
| 配置 | 单步成功率 | 30 步累积成功率 | 端到端耗时 |
|---|---|---|---|
| 单路采样 | 88% | 2.1% | 基准 |
| 4 路并行 | 94% | 15.6% | 1.4x |
| 8 路并行 | 95% | 21.5% | 2.3x |
可以看到,4 路并行是性价比最高的点,耗时只增加 40%,但累积成功率提升了 7 倍多。8 路并行的边际收益就明显下降了。
这个数据也印证了前面的判断:并行扩展的收益主要来自降低单步错误率,而单步错误率的降低在长周期任务里会被指数放大。
6.2 可以继续深挖的几个方向
第一个方向是自适应并行度。根据当前步骤的不确定性动态调整n,不确定时多采样,确定时少采样。这个思路我在小规模实验里验证过,能再省 20% 左右的算力。
第二个方向是候选之间的信息共享。现在的并行候选是独立的,但其实它们可以互相参考。比如让候选 B 看到候选 A 的部分输出,避免重复探索。这个思路实现起来复杂,但潜力很大。
第三个方向是和训练结合。如果能在训练阶段就让模型学会"生成多样化候选",推理阶段的并行扩展会更高效。这属于更长期的工作。
第四个方向是验证器的自动化。现在验证器还需要人工设计规则,如果能用一个轻量模型自动学习验证策略,整个流程会更通用。
这些方向我自己也还在摸索,有些只是初步验证,还没到能稳定复现的程度。等有更成熟的结论再单独写一篇分享。
最后说个我自己的体会:并行扩展不是银弹,它解决的是"模型单次采样不够好"的问题,但解决不了"模型根本不会"的问题。如果任务本身超出模型能力范围,加再多并行也没用。所以做优化之前,先确认模型的能力边界在哪,别在错误的方向上使劲。