为什么你的本地LLM用起来感觉比实际更“笨”
快速引言
我们都有过这样的经历:在论坛、聊天、Reddit、Discord、YouTube等地方,听到有人惊呼“哦!某某模型简直太棒了!”,然后下载了它(或者更可能是它的某个量化版本),一试之后却觉得“呃…这太烂了!”
这篇文章将是一系列相当技术性的实验,旨在展示推理过程中,因具体实现方式(implementation-specific)而产生的差异所造成的影响。我会用“参考实现”这个词来指代那些发布模型并提供官方托管、并公布原始基准测试结果的实验室。他们的硬件和软件都会与你的大不相同。而本文中的对比,也不会是在Ollama中跑几个测试提示词、比较2.58-bit的GGUF模型那么简单。
为了让大家更容易理解,我会故意略过一些新兴的研究领域和大量研究论文。请不要对我的过度简化吹毛求疵,否则我可能会让你去读那又长又烦人、还带数学公式的版本。
你的本地实现很糟糕。但没关系,因为其他人的也一样。
如今,每一个运行LLM的硬件和软件实例都略有不同,在某些情况下甚至可能大相径庭。一般的家庭实验室用户可能混合使用着不同世代的GPU,这些芯片上的指令集各不相同。这些指令集在执行数学运算来计算你的下一个token时,方式也会因人而异,即使大家运行的是完全相同的权重。
这就引出了第一个问题:你的特定配置到底有多“糟糕”? 事实证明,有很多不同的方法可以衡量这一点。
实际的方法很直接:运行标准的基准测试,并且要多样化。比如terminal bench、hle、SWEthis、HELLAthat、MMLU-whatever……随便选。关键是要确保这些测试能代表你的实际工作负载和用例。 不要只是把温度调到0,粘贴3个测试提示词,然后就断定模型好或不好。零样本(Zero-shot)测试并不能很好地模拟大多数智能体(agentic)任务。你需要长上下文的工具调用和特定领域的知识评估,才能在与他人运行相同权重并复现相同基准测试时,找出你配置的弱点所在。
但我将从纯粹的数学角度开始讨论,因为正如@wendell所说:
数学就是数学!
“Logits(逻辑值)”是模型对每个可能的下一个token给出的评分。这些评分会被归一化为概率,然后通过配置好的采样器(sampler),最后由解码器(detokenizer)转换回文本,从而在解码过程中生成 THE→NE→XT→TOK→EN。
关于采样器设置的一个旁注:Hugging Face模型卡通常会明确指定你应该使用的采样器设置(以及聊天模板),例如 temp 1.0, top-p 0.95 等,不同模型有所差异,请确保你使用了正确的设置。顺便说一句,温度设得太低就是导致你的Qwen模型陷入循环、无法跳出其“THINK”输出的原因。不用谢,很高兴能帮你解决这个问题。
当下一个token的概率发生足够大的变化时,THE→NE→XT 就可能变成 THE→NE→W→DAY……虽然这些微小的变化可能没问题,但很可能是你潜意识里总觉得“哪里不对劲”的源头。
你们有些人可能听过KLD(KL散度,Kullback-Leibler Divergence)这个术语。别担心,我不会让你做数学题,也不会用一堆小数表格轰炸你的大脑。简单来说就是:将输出的logits转换为概率分布,然后衡量该分布相对于选定基线移动了多少。较低的KLD值意味着更接近基线,但并不自动代表模型“更聪明”。 KLD也是有方向的,所以两个分布的顺序很重要。
提醒一句: 不要被某个量化模型HF模型卡上低得不可思议的KLD声明所迷惑。除非作者披露了参考检查点、完整的运行时环境、评估文本、校准数据、上下文长度、采样位置、KL方向、任何词汇表截断以及测量结果的聚合方式,否则这个数字是无法解读的。方法论和数字本身同样重要,而很多人在这方面会出错。
vllm到底在做什么?
现在,我们需要简要探讨一下你的推理引擎上那庞大的软件栈到底在做什么,以便理解这些差异的一些来源。
在这个被极度简化的流程图中,每一步都包含可以根据你的具体硬件/软件配置、模型、量化方式、张量形状等进行配置或更改的组件。
我抓取的vLLM每日构建版(nightly)容器镜像包含了734个软件包。这意味着有734个代码库,每个都有其自身的错误和未文档化的特性。你的特定实现穿过这座“代码山”的路径将是独一无二的。
测试1:注意力后端的精度基准测试
让我们从推理流程图中的一部分开始。在预填充(prefill,即提示词处理)阶段,你的推理引擎会从几个注意力后端中选择一个。这会影响预填充的速度和精度,同时不同的GPU家族/流处理器(SM)计算能力版本需要不同的CUDA内核。我们来测试并比较它们。
(非常抱歉,但我觉得有必要这么做…)
我从Qwen3.6-27B的官方BF16检查点开始,在RTX PRO 6000 Blackwell GPU上运行,张量并行度(tensor parallelism)设为1。KV缓存使用BF16格式,没有采用权重、激活或KV缓存的量化。软件环境是固定版本的vLLM每日构建版。我使用了eager执行模式,禁用了CUDA图、前缀缓存和MTP,并采用了2k-token的分块预填充(chunked prefill)。
Qwen3.6-27B是密集(dense)模型,而非MoE(混合专家)模型,但它仍是一个混合架构:64层以“三个门控DeltaNet/线性注意力层”加“一个全注意力层”的模式重复。只有那16个全注意力层在此实验中使用可选注意力后端;门控DeltaNet路径保持不变。
这里重放的工作负载是“Prompt 2”,一个约10万token的上下文,来自真实的Turnstone实验室工作流,包含多次工具调用和实际工作产物。选择它是因为它模拟了本地智能体实际的工作方式,而不是那种合成的“大海捞针”测试。而且更重要的是,这个工作负载没有出现在当今任何基准测试或训练数据集中。没有人能针对这个进行过基准优化,或校准他们的量化方案来适应它。
在这个工作负载下,vLLM中有三个可用的全注意力后端可供选择:FlashAttention 2、Flash Inference 和 Triton Attention。这是执行之间唯一更改的设置,其余硬件和软件栈保持稳定。
我还进行了相同后端、跨GPU的可重复性控制测试。在这张图表中,我每32个提示词token捕获一次BF16格式的完整词汇表logits。像KLD这样的分布比较,是之后在FP64精度下从这些存储的logits中计算得出的。
“Top-1一致性”是指具有最高logit的token(即贪婪解码的argmax)是否相同。所有三个后端都针对相同的强制token历史进行了评估。因此,“top-1翻转”意味着在该位置,某个后端会选择不同的贪婪解码下一个token。我们并没有让这个选择改变后续的历史。这保证了数学比较的可控性,但它无法展示一个不受约束的生成过程会如何分支,或者一次工具调用是否会最终失败……这部分将在测试2中揭晓 ;D
下图显示了导致token翻转的采样logits百分比:
在最初的几千个token中,无论使用哪种后端,模型对下一个token的预测都是一致的。但在提示词的后半部分,后端开始出现分歧。这里选择Triton作为基准,以简化后续的量化对比。
每个8k-token窗口包含250个采样位置(每32个token探测一次)。百分比指的是在这些探测点中,其他后端得分最高的token与Triton不同的比例。
通过使用相同的注意力后端多次运行相同测试,来排除随机噪声的影响。结果显示,每次运行的logits在每个隐藏状态上都逐位(bit for bit)相同。这意味着观察到的差异完全来自于在预填充期间,在Triton/FA2/FI内部发生的矩阵乘法和加法运算。
分歧呈集群式出现,并且随提示词内容变化,而非随上下文长度平滑增加。这并不能证明存在某个使模型“崩溃”的通用长度阈值……但我们很快就会讲到……
现在我们有了一个关于特定提示词工作负载的基线比较,让我们深入探讨……
测试2:KV缓存量化,或为什么你的LLM在超过4万token后智商陡降
重复相同的方法,我们以上述运行Triton的BF16权重和BF16 KV缓存基线为基准,进行了下一个实验:保持权重和激活不变,仅量化KV缓存,会发生什么?
答案就是:分歧。这引出了我们今晚的第一个“大型翻车现场”:一个完全可重现的工具调用错误。
在工具调用期间,足够多的top-token被翻转了。我们让它们继续执行,结果发现:BF16一切正常,int8 KV缓存最终设法恢复了,而 int4 KV缓存则完全无法恢复!
测试3:权重,别告诉我!
这次我们保持所有KV缓存为完整的BF16大小。但我们引入了一些新的选手来进行比较:
-
BF16参考:Qwen/Qwen3.6-27B 官方
-
官方FP8:Qwen/Qwen3.6-27B-FP8 官方
-
INT8 W8A16:TheHouseOfTheDude 社区量化
-
NVIDIA NVFP4:nvidia/Qwen3.6-27B-NVFP4 官方
-
AWQ W4A16:cyankiwi 社区量化
这4种量化方案代表了权重和激活的广泛图景。对我们“数学爱好者”来说,一个值得注意的信息是,计算每种量化logits时所实际运行的CUDA内核 / GEMM(通用矩阵乘法) / MMA(矩阵乘加)指令都是不同的:
Qwen3.6-27B (参考)
-
权重/激活:BF16权重,BF16激活
-
线性层/GEMM:未量化线性方法 → torch.nn.functional.linear。每个CUDA块由其相关的形状/几何结构选择。
-
KV缓存:BF16(强制)
-
特性:参考检查点。
Qwen3.6-27B-FP8
-
权重/激活:128×128块中的E4M3 FP8权重;在转换后的线性层内进行动态FP8激活量化;
lm_head等排除模块保持BF16。 -
线性层/GEMM:Fp8LinearMethod → CutlassFp8BlockScaledMMKernel
-
KV缓存:BF16(强制)
-
特性:由于vLLM将其E8M0缩放格式标记为此架构(SM120)的精度降级,DeepGemm被自动禁用;转而选择了CUTLASS。发布文件中未识别出校准数据集。
Qwen3.6-27B-INT8
-
权重/激活:静态、对称、通道级别的INT8线性权重;BF16激活 (W8A16)。GDN/linear_attn投影和
lm_head被排除在量化之外。 -
线性层/GEMM:CompressedTensorsWNA16 → MarlinLinearKernel
-
KV缓存:BF16(强制)
-
特性:一次性量化(one-shot),明确没有使用校准数据集。考虑到W8A16加上未量化的GDN投影,其异常良好的保真度就不那么神秘了。
Qwen3.6-27B-NVFP4
-
权重/激活:混合检查点——覆盖64个全注意力投影和144个GDN投影的208个静态FP8 W8A8目标;覆盖192个MLP投影加上
lm_head的193个NVFP4 W4A16目标,组大小16。 -
线性层/GEMM:
-
FP8目标:ModelOptFp8LinearMethod → FlashInferFP8ScaledMMLinearKernel
-
NVFP4目标:NVFP4 GEMM → MarlinNvFp4LinearKernel
-
-
KV缓存:BF16(强制)
-
特性:在我们的上游每日构建版运行中,并非原生FP4算术。vLLM判定该GPU路径缺乏原生FP4支持,并明确通过Marlin选择了纯权重的FP4压缩。检查点中嵌入的FP8 KV方案在本次对比中被BF16 KV覆盖。
Qwen3.6-27B-AWQ-BF16-INT4
-
权重/激活:静态非对称INT4权重,组大小32,MSE观察器;BF16激活 (W4A16)。GDN/linear_attn投影和
lm_head被排除。 -
线性层/GEMM:CompressedTensorsWNA16 → MarlinLinearKernel
-
KV缓存:BF16(强制)
-
特性:AWQ校准数据集被披露为“STEM和智能体(STEM and Agentic)”。
本次运行的其它值得注意信息:
-
所有模型的完整softmax/GQA注意力均为
AttentionBackendEnum.TRITON_ATTN;JIT监视器观察到kernel_unified_attention。 -
GDN预填充:Triton/FLA GDN预填充内核,请求为
triton,head_k_dim=128。 -
执行期间,循环路径还JIT编译了
_causal_conv1d_update_kernel,fused_recurrent_gated_delta_rule_packed_decode_kernel和reduce_segments。 -
张量并行度1,eager模式,无CUDA图,无MTP/推测解码,仅语言执行。
下一个token翻转的结果相当可预测。TheDude的INT8 (W8A16) 方案表现最佳,击败了第一方的FP8 (W8A8) 和NVIDIA(FP4名不副实)的发布版。事实上,在5个选项中,NVIDIA的发布版排名垫底,在上下文达到88k时,其token翻转率达到了约50%。
在工具调用方面,NVFP4和AWQ W4A16都未能正确关闭它们的工具调用,并且搞乱了Cisco的命令行语法(正确命令是'show arp',而它们执行了'show run'),而FP8和INT8都能成功完成正确的调用。
在未来的实验中,我将尝试探索对相同权重使用不同融合GEMM的影响,这是另一个有趣的差异来源,有时你不得不在精度和速度之间做出取舍。