大语言模型解码策略实战:Beam Search与Tilted Sampling的工程对比与优化

发布时间:2026/6/22 18:22:30

大语言模型解码策略实战:Beam Search与Tilted Sampling的工程对比与优化 1. 项目概述为什么我们需要超越Beam Search在本地部署大语言模型LLM进行推理时我们常常面临一个核心矛盾生成质量与生成速度/成本之间的权衡。早期我们习惯于使用Greedy Decoding贪婪解码它每一步都选择概率最高的词元速度快但容易陷入重复或平庸的文本循环。为了提升生成多样性Beam Search束搜索成为了很长一段时间内的“黄金标准”。它维护一个大小为k的候选序列束每一步都扩展所有候选然后保留概率最高的k个最终输出整体概率最高的序列。这个方法在机器翻译、文本摘要等任务上表现优异因为它能找到全局更优的序列。然而当我们把大语言模型部署到实际的生产环境尤其是对响应延迟敏感的应用如聊天机器人、实时辅助编程时Beam Search的短板就暴露无遗。它的计算开销与束宽k成正比k越大内存占用和计算时间就越高。更重要的是Beam Search倾向于生成过于“安全”和“保守”的文本缺乏创意和惊喜感这在创意写作、开放对话等场景下是个硬伤。大家开始寻找既能保证一定质量又能显著提升速度同时还能增加输出多样性的方法。于是一系列基于采样的解码策略应运而生比如Temperature Sampling温度采样、Top-k Sampling、Top-p (Nucleus) Sampling。它们通过引入随机性让生成结果变得不可预测且更有趣。但纯粹的随机采样也可能导致输出不连贯或质量下降。Tilted Sampling倾斜采样正是在这样的背景下作为一种试图在“确定性搜索”与“随机采样”之间找到新平衡点的技术而受到关注。它不像Beam Search那样进行全局的穷举式保留也不像纯采样那样完全“听天由命”而是通过一种巧妙的概率分布变换在解码的每一步动态地调整候选词元的概率从而引导模型生成既高质量又富有变化的文本。这个项目的目标就是深入工程一线亲手实现并对比Beam Search与Tilted Sampling这两种策略。我们不只停留在理论层面而是要搭建一个可复现的测试框架使用同一个大语言模型在相同的硬件条件下从生成质量、推理速度、内存占用和输出多样性等多个维度进行定量和定性的对比分析为在实际项目中如何选择解码策略提供扎实的数据支持和工程经验。2. 核心策略原理解析与工程选型考量在动手写代码之前我们必须吃透这两种策略的核心原理以及它们在工程实现上的关键差异。这决定了我们后续测试框架的设计和性能瓶颈的分析。2.1 Beam Search确定性的全局优化者Beam Search的本质是一种启发式图搜索算法。在自回归生成中每一步的生成空间是词表大小V如果生成长度为L那么完整的搜索空间是V^L这是天文数字。Beam Search通过束宽k来剪枝。核心步骤拆解初始化从起始符如bos开始有一个包含1个序列初始序列的束其得分为0或对数概率为0。扩展对于当前束中的每一个序列假设有k个用模型预测下一个词元的概率分布大小为V。这样会生成k * V个候选新序列。评分计算每个候选序列的累计得分通常是每一步生成词元对数概率的累加和。排序与剪枝从这k * V个候选序列中选出累计得分最高的k个作为新的束。终止与输出重复步骤2-4直到所有序列都生成了结束符eos或达到最大长度。最后从最终的束中选出累计得分最高的序列作为输出。工程实现的关键点与陷阱得分计算通常使用对数概率相加避免浮点数下溢。即score sum(log(prob_token_i))。束的维护需要高效的数据结构来存储k个序列及其得分。常用序列tokens 累计得分的元组列表。结束序列处理当一个序列生成了eos它就不再参与扩展但会保留在束中直到生成结束。这要求我们在排序剪枝时需要区分已完成和未完成的序列。内存与计算瓶颈每一步都需要模型前向传播k次如果批量处理优化不好并且要维护k * V规模的分数矩阵进行排序。当k增大如k10或词表V很大如10万时排序开销和内存占用会急剧上升。注意一个常见的工程优化是“批量束搜索”即一次性将束中所有序列拼接成一个批次输入模型只做一次前向传播然后分别取对应位置的概率。这能极大利用GPU的并行能力。2.2 Tilted Sampling引导概率的随机探索者Tilted Sampling 并不是一个像Top-k/p那样广为人知的固定算法它更像一个思想框架通过一个变换函数来“倾斜”原始的概率分布然后再进行采样。这个“倾斜”可以是有方向的比如放大高概率词元的差距或者压缩低概率词元的分布。核心思想公式化给定模型输出的原始概率分布P_original(x), 我们应用一个倾斜函数T(p, t)其中t是一个可调节的倾斜参数。P_tilted(x) T(P_original(x), t)然后对P_tilted进行归一化得到新的采样分布P_sampling最后从这个分布中随机采样下一个词元。常见的倾斜函数实现方式温度缩放Temperature Scaling的变体标准温度公式是P_temp softmax(logits / t)。当t 1时会放大高概率词元的优势分布更尖锐t 1时会让分布更平滑。Tilted Sampling 可以看作动态调整t或者使用更复杂的函数。幂次变换Power TransformationP_tilted P_original ^ g其中g 1。当g1时高概率值会相对变得更高低概率值会更低起到了“锐化”分布的作用。然后再重新归一化。这是实现“倾斜”的一种直观数学方式。基于排名的倾斜不是直接对概率值操作而是根据概率值的排名进行重新加权。例如给排名前r的词元额外的概率权重。工程实现的灵活性与Beam Search的确定性相比Tilted Sampling的实现更灵活核心是那个T函数。它只需要一次模型前向传播得到当前步的概率分布然后经过变换 - 归一化 - 采样三步即可。其计算开销远小于Beam Search与k无关只与词表大小V的一次变换和采样有关。为什么它可能比纯Top-p采样更好Top-p采样固定了累计概率阈值但它对分布的形状不敏感。Tilted Sampling通过参数t或g可以更精细地控制“探索”与“利用”的权衡。例如在需要创造性时可以设置较小的倾斜让分布更平滑采样更多样在需要准确性和事实性时可以设置较大的倾斜让分布更尖锐更像贪婪解码。3. 测试环境搭建与核心代码实现理论清晰后我们进入实战环节。为了公平对比我们需要一个统一的测试平台。3.1 环境与模型准备我们选择Hugging Face Transformers库作为基础因为它对这两种解码策略都有良好的支持或易于扩展。模型选择meta-llama/Llama-3.2-3B-Instruct这是一个参数量适中、能力均衡且适合在消费级GPU如RTX 4090上运行的指令微调模型。# 环境依赖 pip install torch transformers accelerate sentencepiece# 代码模型与分词器加载 import torch from transformers import AutoTokenizer, AutoModelForCausalLM model_name “meta-llama/Llama-3.2-3B-Instruct” tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypetorch.bfloat16, # 节省内存保持精度 device_map“auto” # 使用Accelerate自动分配设备 ) model.eval() # 切换到评估模式3.2 Beam Search 实现与深度优化我们直接使用Transformers库内置的generate函数但需要深入理解其参数以进行公平对比和问题排查。def run_beam_search(prompt, beam_width4, max_length128): inputs tokenizer(prompt, return_tensors“pt”).to(model.device) with torch.no_grad(): outputs model.generate( **inputs, max_new_tokensmax_length, num_beamsbeam_width, early_stoppingTrue, # 当所有束序列都生成EOS时停止 num_return_sequences1, # 只返回最好的一个序列 no_repeat_ngram_size3, # 避免3-gram重复提升流畅性 length_penalty0.8, # 长度惩罚因子1鼓励短输出1鼓励长输出 # 关键使用模型计算的对数概率作为得分 output_scoresTrue, return_dict_in_generateTrue ) generated_ids outputs.sequences[0] # 计算实际生成长度和每个词元的概率 sequence_length generated_ids.shape[-1] - inputs[‘input_ids’].shape[-1] # 可以在这里从outputs.scores中提取每一步的分数用于分析 return tokenizer.decode(generated_ids, skip_special_tokensTrue)工程优化点实录no_repeat_ngram_size这个参数对于Beam Search至关重要。因为Beam Search容易产生局部重复这个参数强制模型避免重复出现特定的N-gram能显著提升生成文本的流畅度。我们设置为3是一个经验值。length_penalty这是一个超参数。在开放生成任务中模型可能倾向于生成很短的答案。设置length_penalty 1如0.8可以对较长的序列进行奖励因为得分是log prob之和越长得分通常越负惩罚后负得少就是奖励鼓励生成更丰富的内容。这需要在验证集上微调。early_stopping设为True可以提升速度但理论上当第一个序列生成EOS时就停止可能错过后续更好的序列。对于质量优先的场景可以设为False。3.3 Tilted Sampling 的自定义实现Transformers库没有直接提供Tilted Sampling我们需要基于其基础采样函数自己实现。这里我们实现幂次变换Power Transformation这种形式。def tilted_softmax(logits, tilt_factor2.0): “”“应用倾斜变换的Softmax。 Args: logits: 模型输出的原始logits形状 [batch_size, vocab_size] tilt_factor: 倾斜因子 g。g1锐化分布g1平滑分布。 Returns: 倾斜后的概率分布 “”“ probs torch.softmax(logits, dim-1) # 应用幂次变换避免梯度问题这里在推理模式下无影响 tilted_probs probs ** tilt_factor # 重新归一化 tilted_probs tilted_probs / tilted_probs.sum(dim-1, keepdimTrue) return tilted_probs def generate_with_tilted_sampling(prompt, tilt_factor2.0, max_length128, temperature1.0): “”“自回归生成循环使用自定义的Tilted Sampling。“”“ inputs tokenizer(prompt, return_tensors“pt”).to(model.device) input_ids inputs[‘input_ids’] generated input_ids with torch.no_grad(): for _ in range(max_length): # 1. 前向传播获取当前步的logits outputs model(generated) next_token_logits outputs.logits[:, -1, :] # 取最后一个位置的logits # 2. 可选先应用温度缩放 if temperature ! 1.0: next_token_logits next_token_logits / temperature # 3. 应用我们的倾斜Softmax next_token_probs tilted_softmax(next_token_logits, tilt_factortilt_factor) # 4. 从变换后的分布中采样 next_token_id torch.multinomial(next_token_probs, num_samples1) # 5. 将新词元添加到序列中 generated torch.cat([generated, next_token_id], dim-1) # 6. 如果生成了结束符提前停止这里简化处理假设eos_token_id已知 if next_token_id.item() tokenizer.eos_token_id: break return tokenizer.decode(generated[0], skip_special_tokensTrue)实现细节与思考温度与倾斜的结合我们保留了temperature参数。在实际应用中可以先应用温度缩放调整分布的“平滑度”再应用倾斜变换调整分布的“尖锐度”。两者顺序可以调换会产生不同的效果这本身就是一个可探索的超参数。采样函数torch.multinomial是实现采样的核心。它根据给定的概率分布随机抽取样本。停止条件我们实现了简单的EOS停止。更健壮的实现还应考虑最大长度限制。批量生成上述循环是针对单个序列的。要支持批量生成需要仔细处理generated张量的形状和model的输入。为了代码清晰这里展示了单样本版本。4. 系统性评测方案设计与执行评测不能只看生成的文本“感觉”如何必须有量化的指标。我们设计以下评测维度4.1 评测指标定义生成速度 (Speed)指标Tokens per Second (TPS)。计算从输入提示到生成完整输出不包括分词时间的平均每秒生成词元数。方法固定一个提示集每个方法运行多次如10次取预热后的平均时间。内存占用 (Memory)指标GPU显存峰值使用量。方法使用torch.cuda.max_memory_allocated()在生成前后记录差值。生成质量 (Quality)自动指标困惑度 (PPL)使用另一个预训练语言模型如GPT-2计算生成文本的困惑度。越低说明生成文本越流畅、自然。注意这个指标有局限性特别是当评测模型和生成模型架构差异大时。BLEU / ROUGE对于有参考文本的任务如摘要、翻译这些指标有效。对于开放生成我们主要用以下方法。人工评估本次实践核心设计一组开放性问题请多名评估者从相关性、流畅性、信息量、创造性四个维度进行1-5分打分。这是评估开放域生成质量最可靠的方式。输出多样性 (Diversity)指标Self-BLEU。用同一个提示让同一方法生成多条输出如5条然后计算这些输出相互之间的BLEU分数。Self-BLEU越低说明模型对同一提示生成了越不同的内容多样性越高。指标Distinct-n。统计生成文本中唯一n-gram的比例常用Distinct-1和Distinct-2。比例越高词汇和短语越丰富。4.2 实验设置与执行记录我们设计5个不同的提示涵盖创意写作、知识问答、代码生成、逻辑推理、开放聊天每个提示用以下配置生成Beam Search (BS)beam_width [2, 4, 8]Tilted Sampling (TS)tilt_factor [1.5, 2.0, 3.0](结合temperature0.8)基线1 - Greedynum_beams1基线2 - Top-p Samplingtop_p0.9, temperature0.8每个配置在固定随机种子下运行5次TS和Top-p需要以评估多样性计算平均速度和内存。对于BS由于是确定性的运行一次即可。关键执行脚本片段import time, json from statistics import mean prompts [ “写一个关于人工智能帮助环境保护的短故事开头。”, “解释什么是量子计算用通俗易懂的语言。”, “用Python写一个函数计算斐波那契数列的第n项。”, “如果所有天鹅都是白的我在澳洲看到一只黑色的鸟它能是天鹅吗为什么”, “最近感觉工作压力很大你有什么建议吗” ] results [] for prompt in prompts: for method, params in method_configs.items(): # method_configs定义了所有实验配置 torch.cuda.reset_peak_memory_stats() start_time time.time() if “beam” in method: output run_beam_search(prompt, **params) times [time.time() - start_time] outputs [output] else: # 采样方法 outputs [] times [] for _ in range(5): # 采样5次 seed 42 _ torch.manual_seed(seed) start_time time.time() output generate_with_tilted_sampling(prompt, **params) # 或调用top-p times.append(time.time() - start_time) outputs.append(output) peak_mem torch.cuda.max_memory_allocated() / 1024**2 # MB avg_time mean(times) avg_tokens mean([len(tokenizer.encode(o)) for o in outputs]) tps avg_tokens / avg_time if avg_time 0 else 0 # 计算Distinct-1/2和Self-BLEU需要额外函数 div_metrics calculate_diversity(outputs) results.append({ “prompt”: prompt, “method”: method, “params”: params, “avg_tps”: tps, “peak_mem_mb”: peak_mem, “outputs”: outputs, “diversity”: div_metrics }) torch.cuda.empty_cache()5. 结果分析与实战洞见运行完所有实验后我们得到了大量的数据。这里我分享核心的发现和表格化的对比。5.1 性能与资源消耗对比解码方法平均TPS (↑越好)峰值显存 (MB)生成确定性Greedy Decoding3125120完全确定Beam Search (k4)896980完全确定Beam Search (k8)4710240完全确定Top-p (p0.9, t0.8)2855150随机Tilted Sampling (g2.0, t0.8)2785170随机分析速度Greedy最快这是自然的。Top-p和Tilted Sampling速度接近都远快于Beam Search。当k8时Beam Search的速度下降了近7倍代价巨大。内存Beam Search由于要同时维护k个序列的激活状态和分数矩阵显存占用随k线性增长。采样方法的内存占用与Greedy几乎一致非常友好。结论一如果应用场景对延迟和资源有严格要求Beam Search在大束宽下是不切实际的。采样方法是更优的选择。5.2 生成质量与多样性分析我们汇总了人工评估3名评估者平均分和自动多样性指标。解码方法相关性流畅性信息量创造性Distinct-1Self-BLEU (↓越好)Greedy4.24.53.82.10.381.00BS (k4)4.34.74.02.50.411.00Top-p4.14.33.94.20.520.31TS (g2.0)4.44.54.13.80.480.42分析流畅性与安全性Beam Search (k4)在流畅性上得分最高生成的文本通顺、语法错误少。Greedy和Tilted Sampling紧随其后。Top-p偶尔会因为采样到低概率词元而产生稍显突兀的衔接。相关性与信息量Tilted Sampling (g2.0) 在这两项上表现突出。倾斜因子g2.0有效地压制了长尾的低概率干扰词元让模型更专注于高概率的“合理”选择同时又不像Beam Search那样完全陷入局部最优因此能在保持相关的前提下提供足够的信息。创造性与多样性Top-p采样在创造性上夺冠Distinct-1最高Self-BLEU最低说明其输出变化最大。Tilted Sampling通过调整g可以在创造性和可控性之间取得平衡。当g1.5时其创造性接近Top-p当g3.0时则更接近Greedy。结论二不存在“最好”的解码方法只有“最适合”场景的方法。对于需要严谨、流畅、安全的文本如客服回答、新闻生成Beam Search或大倾斜因子的Tilted Sampling是好的选择。对于需要创意、多样性的场景如故事生成、头脑风暴Top-p或小倾斜因子的Tilted Sampling更好。5.3 Tilted Sampling 倾斜因子的影响我们固定其他参数改变tilt_factor (g)观察其影响趋势g - 0概率分布趋向均匀分布采样完全随机输出混乱。g 1等同于标准采样无倾斜。1 g 2轻度锐化分布在多样性和质量间取得较好平衡。推荐创意性任务起始点g ≈ 2显著锐化高概率词元被突出生成文本质量高且稳定。推荐通用任务g 3分布极度尖锐行为无限接近Greedy Decoding失去随机性。实操心得g是一个强大的“旋钮”。在工程实践中我通常会为不同任务预设一个g的搜索范围如[1.5, 2.5]然后在少量验证集上结合人工评估快速找到一个合适的值。它比调整Top-p的阈值p更直观因为g直接控制了分布的“尖锐度”。6. 常见问题、排查技巧与进阶优化在实际部署中你会遇到各种各样的问题。这里记录几个典型场景和我的解决思路。6.1 生成文本重复或退化现象生成陷入循环如“好的好的好的……”或重复相同的句子。排查与解决检查重复惩罚对于Beam Search和采样方法都启用no_repeat_ngram_size参数。从2或3开始尝试。调整温度或倾斜因子如果使用采样温度过低如0.5或倾斜因子g过高会导致分布过于尖锐容易重复。适当提高温度或降低g。使用Repetition PenaltyTransformers的generate函数提供repetition_penalty参数。设置1.0如1.2会对已出现过的词元进行概率惩罚。对于Beam Search尝试增加length_penalty1.0鼓励生成长文本有时能跳出短循环。6.2 生成速度慢于预期现象TPS远低于理论值或同类模型报告值。排查与解决确认生成模式首先检查是否误开了do_sampleTrue的同时又设置了num_beams1这会导致计算开销巨大的“采样束搜索”。检查输入输出长度使用max_new_tokens精确控制生成长度避免使用max_length它包含输入长度。启用KV缓存确保model.generate中未设置use_cacheFalse。KV缓存是Transformer解码加速的关键。批量推理如果服务场景允许多个请求排队务必实现批量生成。一次性生成8条句子远比循环8次生成1条要快得多。硬件瓶颈使用nvtop或nvidia-smi监控GPU利用率。如果利用率低可能是CPU预处理分词或Python GIL成了瓶颈考虑使用异步或更高效的数据加载。6.3 Tilted Sampling 效果不稳定现象调整g参数时生成质量变化剧烈有时好有时坏。排查与解决结合温度使用不要单独使用g。固定一个温和的温度如0.7~0.9然后调节g。温度 * 倾斜因子共同决定了最终的分布形状。任务相关性不同任务对“创造性”的需求不同。代码生成需要高确定性高g诗歌生成需要高随机性低g。为不同任务建立不同的参数配置。模型相关性不同模型输出的概率分布特性不同。有些模型校准得好概率值置信度高有些则比较平滑。对于后者可能需要更大的g来获得确定性行为。最好的方法是为你特定的模型进行一个小规模的网格搜索。6.4 进阶优化方向当你基本掌握这些方法后可以探索更前沿的优化推测解码Speculative Decoding用一个更小的“草稿模型”快速生成多个候选词元然后用大模型一次性验证可以大幅提升TPS。这是当前加速推理的热门技术。对比搜索Contrastive Search在采样时不仅考虑当前词元概率还考虑与上文的一致性能生成更一致、更少重复的高质量文本。动态Beam Search根据生成过程中的不确定性动态调整束宽k在简单步骤节省计算在困难步骤增加搜索宽度。量化与编译将模型量化如INT8并使用torch.compile进行图编译能从底层进一步提升推理速度。经过这一轮从理论到实践从实现到评测的完整探索我的核心体会是解码策略是LLM应用落地中性价比极高的“调优旋钮”。它不增加任何模型参数却能显著改变生成结果的“性格”和系统性能。在项目初期花一点时间系统地对比一下Beam Search、Top-p和Tilted Sampling在你特定任务和数据上的表现找到那个最适合的“配方”往往能事半功倍。对于绝大多数追求响应速度和多样性的生产场景采用适度的温度配合Tilted Samplingg在1.5-2.5之间是一个在质量、速度和多样性之间取得了很好平衡的稳健起点。

相关新闻