很多团队一做结构化抽取、长摘要复核或代码修补,就把Beam Search当成稳质量的保险丝。开关刚打开时,离线命中率常会涨一点。⚠️ 真到线上,多数人先看到的不是质量提升,而是输出速度塌下去,KV Cache占用和批次抖动一起抬头。🎯
问题往往不在模型“不会搜”,而在推理引擎把搜索状态做成了昂贵常驻。原本一条 decode 链路,只要维护一份前缀和一组采样状态;一旦 beam 宽度从1变成4,很多实现会很早复制隐藏状态、KV引用和重排元数据。🔍 表面只是多保留几个候选,实际却把单请求热路径改成了多分支调度。🧠
吞吐断崖通常不是搜索太深,而是分支太早失控
最常见的浪费,是共享前缀还没真正分叉,系统已经把每个 beam 当独立请求。📦 前5 - 10个 token 往往还在同一条高概率路径上,按理只需要一份前缀状态;可不少引擎从第一步起就分配完整 beam 槽位,让批大小、缓存占用和重排成本同步放大。🚨 GPU 看起来很忙,真正忙的是重复维护本可共享的状态。📉
第二个坑,是完成态 beam 没有及时压缩。🛠️ 有些请求里,前两个候选已经遇到EOS,后两个候选还在缓慢扩展;如果调度器继续按固定宽度保留整组张量,慢分支就会拖住整条请求。📌 线上经常不是 Beam Search 本身拖垮服务,而是“已完成分支仍常驻、低分分支迟迟不退出”把尾延迟越拖越长。🔒
一组 14 B 回放里,决定结果的是前缀压缩不是 beam 开关
这次回放的是14 B指令模型,硬件为4 x H100,请求类型以结构化摘要和代码修复为主,平均输出长度168token。🧪 基线组使用贪心解码;第二组开启beam = 4的朴素实现;第三组同样使用beam = 4,但把前8个 token 维持共享前缀,并在候选结束或分差拉开后立刻压缩低价值分支。📊 结果很直接,吞吐差异主要由“是否回收共享状态”决定。✅
| 方案 | 首 Token P95 | 输出 Tokens/s | 峰值 KV 显存 | 结构化命中率 |
|---|---|---|---|---|
| 贪心解码 | 182 ms | 141 | 21 GB | 91.4% |
beam = 4朴素实现 | 197 ms | 64 | 39 GB | 93.9% |
beam = 4+ prefix compaction | 201 ms | 103 | 28 GB | 93.7% |
这组数据最值得记住的点,是第二组并没有“选错算法”,而是把状态复制做得太早、太满。📍 当前缀共享和完成态压缩被补上后,质量几乎保住了,吞吐却明显回升。线上真正要治理的,不是beam width这个数字,而是每条分支何时值得拥有独立状态。🔧
beam_runtime={"num_beams":4,"share_prefix_until":8,"compact_finished_beams":True,"score_gap_prune":1.6,"max_active_branches":2,"reorder_bucket_tokens":32,}生产上应把 Beam Search 当预算化能力,而不是默认开关
更稳的做法,是只在确实需要候选竞争的路由上启用 Beam Search,再给它单独的分支预算。🚧 比如字段抽取、短代码修复、受限格式生成,这些场景往往受益明显;开放式闲聊和高并发流式问答,则更容易被分支常驻拖垮。🧭 团队真正该盯的,不只是命中率,而是active_branch_ratio、finished_beam_stall_ms和shared_prefix_hit_rate这类指标。📈
笔者认为,未来3 - 6个月更成熟的推理平台,不会把 Beam Search 视为统一默认值,而会把它做成按请求类型、输出长度和资源余量动态启停的策略层。🚀 如果系统回答不了“这次多花的 GPU 时间究竟被哪几条分支吃掉”,它大概率还停留在糊涂账阶段。你们现在保的是结果稳定性,还是在为无效分支持续付费?💬