AI优化让llama2.c跑得更快,却毁了流式体验:一个Kaggle基准的启示

AI代理优化CPU推理代码时,完成速度提升可能以牺牲流式首字节体验为代价,单一指标无法反映真实用户感受。
作者以Andrej Karpathy的llama2.c为基准,让Gemini、Claude和GPT在受限工具调用下优化CPU推理性能,结果揭示了一个关键陷阱:Claude的优化版本通过移除每token的`fflush`刷新使总完成时间快2.7倍,却让首字节出现时间慢了近5倍,严重损害流式体验。修订版基准将正确性、完成时间和流式延迟设为三套独立检查,重跑结果显示Gemini完成加速中位数最高(5.35×)但token消耗也最大,Claude次之(3.69×),GPT结果最不稳定(两次仅1.00×)。作者还发现编辑数量多、使用优化技术,都不能保证端到端收益,而评估器自身的#include路径遮蔽问题也曾导致数据失效。核心结论是:必须先明确并保护用户真正需要的可观测行为,再判断代码改动是否真正有帮助。
AI编码代理能否在不破坏既有行为的前提下,真正把一个CPU推理程序改得更快?这是作者在Kaggle基准挑战中提出的核心问题。他让Gemini、Claude和GPT在相同的工具调用和编辑限制下,去读取、修改并测试Andrej Karpathy的llama2.c源码——结果暴露出一个常被忽视的陷阱:更快的最终答案,可能藏着更糟糕的流式体验。

一个耐人寻味的早期结果
原始程序在 103 毫秒 后开始显示生成内容,1,321 毫秒 后完成全部输出。而某个AI优化版本的程序,总耗时压缩到了 483 毫秒——但在此之前,用户看不到任何输出。
换句话说,最终答案完全一致,整体响应快了约 2.7 倍,可用户等待首个字符出现的时间却长了近 5 倍。这正是作者想警示的关键矛盾:如果只用"完成时间"作为优化指标,就会漏掉对真实用户体验至关重要的"首字节出现时间"。
作者随后在不改动优化后数学逻辑的前提下,恢复了每个token后的 fflush(stdout) 刷新操作。结果输出在 37 毫秒 后就开始,整个请求 477 毫秒 完成。有用的计算优化保留了下来,而流式回退问题则被修复——这说明两者本可兼得。
首字节时间(Time to First Byte,TTFB) 是衡量流式输出体验的核心指标,指用户从发出请求到看到第一个可见字符所经历的等待时间。在大语言模型推理场景中,这一概念常被称为"首token延迟"(Time to First Token,TTFT)。对于交互式对话系统,TTFT对感知流畅度的影响往往远大于总完成时间:人类对300ms以上的空白等待会明显感到卡顿,而即便总生成时间更长,只要字符持续流出,体验依然流畅。fflush(stdout) 是C标准库中强制将输出缓冲区内容立即写入底层文件描述符的调用——每生成一个token就调用一次,可确保字符逐步呈现给用户或下游管道。移除此调用后,操作系统会将输出攒满缓冲区再一次性刷出,从用户角度看就变成了"等待→突然全部出现"的体验。
基准到底测了什么
作者选用了锁定在特定commit的 llama2.c,配合TinyStories的FP32检查点(15M、42M、110M参数)。这些是跑在llama2.c上的小模型,而非完整的Llama 2。选择它的理由很实际:紧凑的C源码和小检查点,让在CPU上进行全程序测量成为可能。
优化器只能读取 run.c、替换唯一匹配的代码块、并对候选版本做基准测试。每次尝试限制为 8次工具调用、最多6次成功编辑、2次基准反馈调用,并设有600秒的守卫。提示词强制要求FP32精度和兼容现有接口,并禁止修改权重、分词器、停止规则、线程、外部库,也不许硬编码答案或操纵评估器。两套程序都用GCC -O3 串行CPU执行。
正确性与延迟是两套独立检查
数值检查上,两个程序在每个位置接收相同输入token,作者对比全词表logits,要求满足 abs(candidate - reference) <= 1e-4 + 1e-4 * abs(reference),且生成文本必须完全一致(含带种子的核采样)。
延迟方面,请求计时器在加载权重和分词器之后启动,涵盖分词、提示处理、模型计算、采样、反分词与输出。父进程观测stdout,排除精确的ASCII提示回显后再定位首个生成字节——这是"首个生成输出字节的时间",包含管道传输与调度开销。
修订版任务新增了流式延迟门槛:每个候选的首字节中位数必须落在 max(1.10 * 参考中位数, 参考中位数 + 0.002秒) 之内。只有当数值、文本、形状和流式检查全部通过,完成速度提升才被计分。最终分数是12个"尺寸/工作负载"加速比的几何平均,行为失败则记零分。
llama2.c 是Andrej Karpathy编写的极简C语言推理实现,旨在用尽可能少的代码跑通Llama 2架构,核心逻辑集中在单个 run.c 文件中,便于阅读、修改和教学。TinyStories 是一个专门用于训练小型语言模型的合成数据集,由简短的儿童故事构成,配套的FP32检查点(15M、42M、110M参数)体积小、加载快,非常适合在CPU上进行端到端性能测量,而无需GPU或量化支持。选择FP32(32位浮点)而非量化格式,是为了保持数值行为的确定性,使正确性验证中的逐位比较更可靠。这一组合让基准聚焦于"AI代理能否优化真实C代码"这一问题本身,而非被硬件或格式差异所干扰。
基准中使用的数值容差公式 abs(candidate - reference) <= 1e-4 + 1e-4 * abs(reference) 是一种**混合绝对-相对误差(mixed absolute-relative tolerance)**判定,常见于数值计算测试框架(如NumPy的 allclose)。纯绝对误差对大数值过于宽松,纯相对误差对接近零的数值又过于严苛,两者结合可以在整个数值范围内提供均衡的精度保障。流式延迟门槛 max(1.10 × 参考中位数, 参考中位数 + 0.002秒) 的双重条件设计同理:对快速请求(参考中位数很小时),加法项防止门槛收窄到毫秒级噪声范围内;对慢速请求,乘法项按比例放宽允许偏差,避免正常抖动导致误判。
测试了哪些模型
修订版实验使用了 Gemini 3.7 Flash、Claude Sonnet 5.5 和 GPT-5.4,每个模型独立执行三次优化尝试。每次尝试都从锁定的原始版本出发,开启全新对话,并保存各自的候选代码、用量和计时记录。
模型调用保持SDK默认设置,提示签名使用temperature 0和seed 0。作者强调这是固定设置下的多次独立执行,而非随机种子试验,也没有对齐各家内部的推理预算。此外,GPT-6.1 Sol曾在早期试点中尝试,但在集成接口阶段就失败了,作者将其排除在模型性能结论之外,而非当作一次糟糕的代码改动。
核心发现
相同的最终输出,掩盖了流式回退
Claude的原始候选移除了每token的 fflush(stdout),改为在最后换行后才刷新,同时也改进了矩阵乘法。由于捕获文本仍然匹配,且第一版评分器只奖励完成时间,这个版本保留了原始高分——因为当时任务并未要求保留流式延迟。
作者在独立的Kaggle Linux任务中重放了保存的候选代码(不涉及任何模型调用)。冻结候选的重放结果如下:
| 冻结候选 | 完成加速比 | 首字节加速比 |
|---|---|---|
| Gemini | 3.44× | 3.54× |
| Claude | 2.73× | 0.44× |
| GPT-5.4 | 2.16× | 2.19× |
| Claude(恢复刷新) | 2.75× | 2.80× |
Claude的首字节加速比0.44×意味着它的流式输出实际变慢了。恢复刷新后,首字节加速比回到2.80×,而完成速度几乎不变。短贪婪请求的细节更为直观:原始版本103ms首字节、1,321ms完成;Claude缓冲版483ms首字节、483ms完成;恢复刷新后仅37ms首字节、477ms完成。

恢复刷新把输出提前了约446ms,而完成时间仍在0.48秒左右。Claude的缓冲版本在四个工作负载中有三个未能通过提议的首字节限制,而恢复刷新的版本则全部通过。仅看完成时间的聚合指标,会完全掩盖这些回退。
通过检查,不等于带来提升
九次全新修订版执行的完成加速比为:
| 优化器 | 通过自动检查 | 完成加速比 | 中位数 |
|---|---|---|---|
| Gemini 3.7 Flash | 3/3 | 5.38×, 5.35×, 5.27× | 5.35× |
| Claude Sonnet 5.5 | 3/3 | 3.69×, 3.87×, 3.17× | 3.69× |
| GPT-5.4 | 3/3 | 1.00×, 2.32×, 1.00× | 1.00× |
Gemini和Claude在所有运行中都稳定地改进了完成时间,并满足流式门槛。而GPT虽然最佳候选达到2.32×,但另两个候选基本是中性的(1.00×)。如果只报告它的2.32×结果,就会掩盖这一事实。
源码检查给出了具体例子:GPT的首个候选用光了全部六次编辑,预计算了旋转位置编码数学,还移除了OpenMP pragma——但标量矩阵乘法循环基本未动,而且构建本就没有OpenMP标志,那些pragma原本就是无效的。最终测得的提升仅为1.0013×。而另一次尝试中,两处聚焦的编辑就产生了2.32×。编辑数量多、使用了某种优化技术,都不能证明端到端的收益。
找到提升本身也要花时间和token
作者还核对了九个SDK轨迹工件的用量,以下为每次优化尝试的中位数:
| 优化器 | 输入token | 输出token | 优化器墙钟时间 | 报告配额成本 |
|---|---|---|---|---|
| Gemini 3.7 Flash | 318,985 | 50,733 | 435.1 s | $0.2864 |
| Claude Sonnet 5.5 | 38,431 | 2,454 | 48.4 s | $0.0853 |
| GPT-5.4 | 87,784 | 1,736 | 71.0 s | $0.0665 |
Gemini拿到了最大的中位增益,但也消耗了最多的token和时间;Claude以更小的开销找到较小的增益;GPT成本最低,但完成收益最不稳定。作者刻意把这几个维度分开,而非合成一个武断的"效率分数"。
评估器本身也需要被验证
一个早期试点曾意外地为两侧都编译了原始源码——因为 #include "run.c" 在候选目录之前先选中了适配器旁的文件。作者丢弃了那批计时数据,并把引用改为 #include <run.c>,还增加了对遮蔽情况的控制测试和拒绝零logits实现的负控制。这些检查,必须先于解读那些诱人的加速数字。
#include 遮蔽(shadowing) 问题是C/C++构建系统中一类经典的路径解析陷阱:编译器按照头文件搜索路径的优先级顺序查找文件,若当前目录或较早路径下存在同名文件,就会优先选中它而非预期目标。本文中,#include "run.c" 的引号形式会先在源文件所在目录查找,导致适配器目录下的原始 run.c 被选中,使候选和参考两侧实际运行的是同一份代码,计时比较因此完全失效。改用尖括号形式 #include <run.c> 并配合显式路径控制,可将搜索范围限定在预期位置。这类问题说明,性能基准的可信度不仅取决于被测代码的质量,构建与评估管线本身同样需要负控制实验(negative control)来验证其正确性。
给真实优化工作的启示
作者打算后续扩展更广的请求变化、更多执行次数、更强的隔离,以及跨更多主机和形状的检查。他也坦言当前参考程序是刻意简化的C代码,而非成熟的推理引擎。
但最核心的教训是具体而清晰的:先保留用户真正需要的可观测行为,再测量代码改动是否真的有帮助。 匹配最终文本是有用的,而刷新干预揭示了缺失的"契约",反复的尝试则说明——即便通过了这个契约,也不代表就一定带来了性能收益。这对任何想用AI代理做性能优化的团队,都是值得牢记的提醒。
相关推荐

房产经纪人用AI提效的5个实操方法(2026版)
房产经纪人如何用AI提效?结合YouTube实操演示,拆解ChatGPT写房源描述、Canva做营销物料、表单管理开放日线索、Zapier自动跟进、卖家面谈准备5个方法,并提示公平住房法规、照片真实性与定价核实等风险边界。

Grok Bot vs ChatGPT Agent:两大AI代理谁更强?
Grok Bot 与 ChatGPT Agent 有何区别?本文对比两者的工作方式:Grok Bot 拥有云端计算机、可常驻工具内自动执行多步任务;ChatGPT Agent 在浏览器中操作网页,通用任务更成熟。附选型建议。

Walmart Agent Gateway:AI帮你自动购物,亚马逊为何反其道而行?
沃尔玛推出Agent Gateway,允许ChatGPT、Gemini等AI助手直接接入商品库替用户搜索比价加购,并设消费限额与逐笔审批三道安全阀。亚马逊则封堵外部AI代理,两大零售巨头走向相反路线。功能目前仅限美国,印度市场或随后跟进。