262K 跑过,128K 却被脚本拦下:A6000 跑分里的三种"失败"不能混为一谈

同一个单卡 A6000 服务,较早的超长上下文探针跑到了目标 262K;几个阶段之后,一次目标 128K 的测试却留下了这行日志:
text
abort sz=131072 out=1024: only 1170MiB free
只看数字,很容易得出"连 128K 都失败了,前面的 262K 不可信"或者"KV cache 用满后服务崩了"。但检查测试脚本后,事情并不是这样:脚本发现空闲显存低于 2048 MiB,直接结束了这一档测试,请求根本没有发送给 vLLM。
这次反常记录真正暴露的,不是一个已经定位的服务端故障,而是一处基准协议漏洞:成功完成、客户端跳过、服务端拒绝如果被放进同一个"通过率"或"最大上下文"字段,数字越整齐,部署判断反而越容易出错。

先限定这些数字属于哪个实例
本文记录的是 2026 年 8 月 19 日的一次本地单实例测试。硬件为单张 RTX A6000 48GB,容器镜像为 vllm/vllm-openai:v0.27.1,容器内 CUDA 13.0.2,宿主机驱动为 535.146.02。服务以如下关键参数启动:
bash
vllm serve /models/Qwen3.8-27B-FP8 \
--served-model-name qwen3.8-27b \
--dtype auto \
--gpu-memory-utilization 0.95 \
--max-model-len 262144 \
--kv-cache-dtype fp8 \
--max-num-seqs 4 \
--max-num-batched-tokens 8192 \
--enable-prefix-caching \
--reasoning-parser qwen3
服务空闲时已经占用约 44,660 MiB,空闲约 4,016 MiB;经历长上下文阶段后,日志记录的空闲显存降到了 1,170 MiB。
模型身份也必须保留边界。服务别名和本地目录名包含 Qwen3.8-27B,配置识别出的架构类是 Qwen3_5ForConditionalGeneration。截至 2026 年 8 月 24 日,Qwen 官方组织中已经存在 Qwen/Qwen3.8-27B 页面,但本地权重缺少下载 revision 与文件 SHA-256。官方页面的存在不能反向证明 8 月 19 日服务器加载的就是同一 revision。因此,后文所有性能数字都只绑定这一个本地实例,不代表同名模型的一般性能。


目标 262K 跑过,不等于完整 262,144 tokens 已验证
较早阶段的 prefill 测试覆盖了目标 128K 到 262K 的档位,相关请求曾成功返回。但"目标档位"不是"实际 prompt token 数"。目标 262K 的记录里,实际 prompt 约为 185,647 tokens;目标 128K 的两条记录实际约为 92,772 tokens。
所以,这组记录能证明的是:在当时的资源状态下,那些实际输入完成了。它不能证明完整 262,144 tokens 已经通过,更不能证明服务在任意运行时状态下都能稳定接收这一长度。
这一区分会直接影响容量规划。如果把启动参数 --max-model-len=262144、测试目标 262K 和实际完成 185,647 tokens 写成同一件事,配置上限就会被误读为持续运行预算。


后来的 128K 不是服务端失败,因为它根本没有送达
目标 128K 的后续测试走了另一条执行路径。脚本先读取空闲显存;当数值低于 2 GiB 时,函数直接返回,不再构造并发送请求。当时的 1,170 MiB 触发了这条保护逻辑。
这意味着现有证据中没有该请求的 HTTP 响应,没有 vLLM 拒绝记录,也没有可归因于这次请求的 CUDA OOM。日志中的 abort 是客户端测试工具作出的决定,而不是服务端报告的结果。
目前只能把两条事实并列起来:
- 较早阶段的部分超长输入在当时的资源状态下完成了;
- 后续阶段开始时空闲显存仅剩 1,170 MiB,客户端保护条件被触发。
两者之间确实存在值得调查的运行时状态差异,但现有证据没有完成因果定位。缓存残留、显存碎片、调度器状态或其他因素都只是待验证假设。没有重启前后的单变量复跑、KV cache 指标和服务端日志,不能把任何一种解释写成结论。


100/100 成功,只回答了 32K 以内的顺序负载
同一阶段随后执行了 100 个顺序 mixed 请求,输入目标分布为 256、1K、4K、16K 和 32K,输出目标为 64、256 或 1024。循环逐条等待请求完成,最终 100 个请求全部成功,decode 中位数维持在 23---24 token/s。
这组结果有明确价值:它说明当时的实例能够连续完成这组 32K 以内的顺序请求。但它没有覆盖目标 128K,也没有覆盖并发压力、容器重启、请求取消、GPU 错误或多日运行。
因此,不能把"100/100 成功"和"128K 被客户端跳过"合并成一个总体成功率。前者回答的是特定中短上下文顺序负载能否完成,后者回答的是测试工具是否允许一次请求继续。它们的分母、执行路径与失败定义都不同。


一字节变化,让 warm TTFT 从约 0.54 秒回到 7.56 秒
相比没有真正执行的 128K 测试,prefix cache 实验形成了更清晰的对照。
测试使用目标 16K 档位、实际 11,524 tokens 的固定前缀。第一次冷请求的 TTFT 约为 7,556 ms;第二次请求命中相同前缀,TTFT 降到约 534 ms。随后只在前缀中间修改一个字节,第三次请求的 TTFT 回到约 7,556 ms;切回原前缀,第四次又降到约 543 ms。
这里最有用的结论不是简单宣布"缓存加速多少倍",而是看到 warm 路径依赖什么。时间戳、随机 UUID 或动态仓库摘要如果插入稳定前缀中间,会改变 token 前缀,已有复用路径就可能消失。对 Agent 服务,更实际的设计动作是把系统提示词、工具定义和稳定仓库上下文尽量放在前面,把持续变化的内容放到末尾,并把 cold TTFT 与 warm TTFT 分开记录。
这仍然不是对所有动态上下文的普遍定律。实验只说明这组固定前缀在该实例上的表现,不能外推为任何一处动态内容都会造成完整 cache miss。


并发 4 提高了吞吐,也把单请求等待拉长
短任务并发从 1 提高到 4 后,聚合吞吐从约 21.1 token/s 上升到约 60.1---60.3 token/s,约为原来的 2.85 倍;与此同时,单请求 TTFT 中位数从约 517 ms 上升到约 1,924 ms。
这不是"并发 4 配错了",而是一笔目标明确的交换:内部批处理可能更看重总吞吐,交互式 Coding Agent 则通常更敏感于首 token 等待。只报 TTFT 会漏掉吞吐收益,只报聚合吞吐又会隐藏用户体感代价。并发配置没有脱离业务目标的单一最优值。

基准记录先回答"请求经历了什么",再记录速度
这次审计之后,我会在每条性能数字之前先记录三个字段:
| 字段 | 建议值 | 它回答的问题 |
|---|---|---|
execution_state |
sent / skipped_by_client / rejected_by_server / completed |
请求没发、被服务拒绝,还是已经完成 |
precondition |
空闲显存、缓存状态、实例 uptime、并发与关键参数 | 为什么相同目标跨阶段可能不可比较 |
actual_workload |
实际 prompt/completion tokens、cold/warm、请求顺序 | 实际执行了多少工作,而不是计划执行多少 |
在这三个字段明确后,再记录 TTFT、decode、聚合吞吐、HTTP 状态和服务端错误,才不会让一个数字承担它没有回答的问题。
对超长上下文,下一步应做隔离复跑:保持输入和输出预算一致,分别在重启后冷态、固定前缀 warm 态以及长时间运行后执行;同时保存 GPU cache usage、waiting/running 请求数、preemption、显存曲线和服务端日志。这样才能判断运行时间与资源状态是否真的改变了可用上下文,而不是继续从相邻日志猜因果。
在补测完成前,--max-model-len=262144 只能被称为配置允许的上限,不能写成"262K 可安全用于生产"。对当前单卡实例,更稳妥的决策是先把目标工作负载限定在已有连续证据覆盖的范围,再用单变量实验决定是否降低最大上下文、调整 gpu-memory-utilization,或为不同负载拆分服务配置。
较早的超长输入完成了,后来的较小目标却在客户端预检阶段被拦下。只有先区分请求是否送达、服务如何响应以及实际完成了多少工作,跑分才真正开始服务于工程决策。

来源与时效说明
- 本文性能数字来自本地实例在 prefill、prefix cache、concurrency 与 boundary 阶段留下的 JSON、CSV、日志及测试脚本;结论仅限上述证据范围。
- Qwen3.8-27B 官方页面(2026-08-24 检查):huggingface.co/Qwen/Qwen3....
- Qwen3.5-27B 官方 vLLM 用法与 text-only/tool-call 参数:huggingface.co/Qwen/Qwen3....