Agents-A1 的权重可以下载、可以自己起服务,但“能部署”和“能扛并发”是两件事。自部署场景里,并发数和上下文长度不是两个独立旋钮:单请求的输入越长,KV cache 占用的显存越多,服务端能同时挂住的序列就越少,客户端看到的并发上限也就越低。所以判断顺序建议反过来:先固定输入长度量出并发上限,再固定并发量出长度上限,最后才去动批处理与调度参数。
自部署 Agents-A1 时,并发上限往往是被上下文长度压小的结果,先测长度再测并发更省时间:固定一段输入,把并发从 1 逐档加到失败,记成功率与 p95;再固定一个稳定并发,把输入长度按千 token 逐档加长,直到超时或显存分配失败。两组结果记在同一张表,最后固化成 max_model_len、max_num_seqs、max_num_batched_tokens。这些数值只对当前 GPU、显存和推理框架版本有效。
固定输入长度,只加并发数
这一步只改客户端并发,prompt 文本一个字都不动,否则长度变化会污染结果。下面的脚本是通用骨架,把 ENDPOINT 和模型名换成自己服务实际的推理接口即可(OpenAI 兼容路径、自研 HTTP 接口都行,只需保证请求体里有固定的 messages 和 max_tokens)。
# bench_conc.py —— 固定输入长度,只改并发
import json, time, urllib.request
from concurrent.futures import ThreadPoolExecutor
ENDPOINT = "http://127.0.0.1:8000/v1/chat/completions" # 换成自己的服务地址
MODEL = "agents-a1" # 换成部署时的模型名
PROMPT = "把下面这段运维记录压缩成三条要点:" + "告警 处理 记录 复核。" * 40
MAX_TOKENS = 128
def call_once():
body = json.dumps({
"model": MODEL,
"messages": [{"role": "user", "content": PROMPT}],
"max_tokens": MAX_TOKENS,
}).encode()
req = urllib.request.Request(ENDPOINT, data=body,
headers={"Content-Type": "application/json"})
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=120) as r:
n = len(r.read())
return time.time() - t0, n, "ok"
except Exception as e:
return time.time() - t0, 0, repr(e)[:60]
for c in (1, 2, 4, 8, 16, 32):
with ThreadPoolExecutor(max_workers=c) as ex:
rs = list(ex.map(lambda _: call_once(), range(c * 4)))
lat = sorted(x[0] for x in rs)
ok = sum(1 for x in rs if x[2] == "ok")
print(f"c={c:<3} ok={ok}/{len(rs)} p50={lat[len(lat)//2]:.2f}s "
f"max={lat[-1]:.2f}s err={rs[0][2]}")
每一档并发都要同时记录三类东西:客户端侧的成功率、p50/p95 端到端时延、错误文本原文;服务端侧的显存占用、日志里的运行中序列数与等待队列长度、是否出现抢占(preemption)记录;如果接口返回 usage 字段,一并记下 prompt_tokens 用于核对长度确实没变。
| 并发数 | 成功/总数 | p50 时延 | p95 时延 | 显存占用 | 错误文本 | 备注 |
|---|---|---|---|---|---|---|
| 1 | 基准档 | |||||
| 4 | ||||||
| 8 | ||||||
| 16 | ||||||
| 32 |
判断并发上限的位置不在数字最大那一档,而在“第一次变坏”的那一档:成功率开始掉、p95 相对上一档明显拉长、显存贴着显卡容量、或日志开始出现等待队列而运行序列数不再增长。把这一档的上一档记为可用并发,这一档记为临界并发,两个都要写进记录,后面对照长度变化时才有参照。
固定并发数,只加输入长度
把并发固定在上一节测出的可用并发上,然后只改输入长度。控制变量的做法是:用同一个可重复单元拼 prompt,长度只由重复次数决定,别在同一轮里既改提示词内容又改长度。先用 tokenizer 或一次请求的 usage.prompt_tokens 量出单个单元的实际 token 数,把它写成常量再开始加长。
# bench_len.py —— 固定并发,只加输入长度
BASE = "告警 处理 记录 复核。" # 一个可重复单元
UNIT_TOKENS = 8 # 先量一次,把实际 token 数填进来
for target in (1000, 2000, 4000, 8000, 16000, 32000):
prompt = BASE * (target // UNIT_TOKENS)
# 用上面的 call_once() 结构,把 PROMPT 替换成 prompt,并发固定
# 记录:目标长度、usage.prompt_tokens、时延、错误文本、显存峰值
print(target, len(prompt))
步长建议按倍数走而不是等差走,1k → 2k → 4k → 8k 这种粗档先定位区间,再在失败档和成功档之间二分细化一次,找出第一次失败的长度。判定标准同时看三条:请求是否直接报错(超时、显存分配失败、超出最大长度)、时延是否已经超出业务可接受阈值、显存峰值是否已经贴着显卡容量。三者中任意一条越线,就把上一档记为可用长度。
看批处理与调度配置是否成为瓶颈
很多人加并发后变慢,第一反应是显存不够,其实要区分两种情况:显存被打满导致放不下更多序列,和显存还有余量但调度因为参数限制不肯放行。日志里能区分:如果运行中序列数长期顶在配置上限、显存接近满、并且频繁出现抢占记录,偏显存容量问题;如果显存还有明显余量、运行序列数却卡住不涨、等待队列持续变长,更可能是批处理与调度参数设小了,或者客户端连接数、tokenize 这类前置环节成了瓶颈。
- 并发序列上限:max_num_seqs(不同框架命名可能是 max_concurrency、max_running_seqs 之类,以启动参数说明为准),它决定一次能同时跑多少条请求。
- 批内 token 总量:max_num_batched_tokens,长输入场景下常常是它先卡住,而不是序列数。
- 显存预留比例:gpu_memory_utilization 一类参数,抬高它会把更多显存留给 KV cache,但也更接近溢出。
- 长度硬上限:max_model_len,超出会直接被拒,不要用它当性能调优手段。
- 溢出到内存的交换空间:swap 类参数只能避免直接崩,代价是时延显著变差,不能当成并发能力提升。
调整时一次只动一个配置项,改完立刻用同一档并发复测,否则无法判断是哪个参数起了作用。
超时与内存分配失败的调整顺序
出现超时或显存分配失败,先降长度还是先降并发,依据是“谁是被乘数”。上下文长度会线性放大每条请求的 KV cache,长度翻倍基本意味着同并发下显存需求翻倍;并发数则是按序列数叠加。所以先降长度:把业务侧截断阈值调小、把 max_tokens 收窄、必要时下调 max_model_len;确认在降长度后同并发可以稳定跑,再考虑降并发。这样得到的结论更干净,也避免把本来够用的并发能力白白砍掉。
- 先收窄单请求输出长度(max_tokens)和业务输入截断,观察错误是否消失。
- 仍失败再下调 max_model_len,注意它会同时拒绝超长请求,需要同步检查业务侧是否产生超长输入。
- 接着降 max_num_seqs 与客户端并发,降到上一节记录的可用并发档位。
- 最后才考虑降低 gpu_memory_utilization 或换个更省显存的权重精度,这属于改变部署形态,不是参数微调。
每一次调整后要复测的项固定下来:同一组并发梯度、同一组长度梯度各跑一遍,核对错误文本是否变化、显存峰值、p95 时延,以及启动日志里确认参数值真的生效了。只改配置不重启、或改了没生效就复测,是这类排查里最常见的误判来源。
把结果固化成部署配置
测完把三项核心参数(单请求长度上限、并发序列上限、批内 token 上限)写成文件,和启动命令放在一起,别只留在聊天记录里。建议格式如下,字段名按自己用的推理框架替换,注释里写清依据哪一档测试得出。
# deploy/agents-a1/bench-record.yaml
model: agents-a1
weights_rev: "<权重目录或提交号>"
runtime: "<推理框架名 + 版本>"
gpu: "<型号 x 卡数, 单卡显存>"
context:
max_model_len: 0 # 填测出的硬上限, 略低于失败档
max_input_tokens: 0 # 业务侧截断阈值
max_tokens: 0 # 单请求输出上限
concurrency:
max_num_seqs: 0 # 填可用并发档位而非临界档位
client_workers: 0
batching:
max_num_batched_tokens: 0
gpu_memory_utilization: 0
limits:
fail_at_len: 0 # 实测第一次失败的长度档
fail_at_conc: 0 # 实测第一次失败的并发档
acceptable:
p95_latency_s: 0 # 业务可接受阈值
note: "仅对上述 GPU 与框架版本有效, 换卡或升级框架需复测"
复测方法保持一致:改完配置重启服务,用第 1 节的脚本在配置的可用并发档跑一轮,再用第 2 节的脚本在配置的输入长度上限附近跑一轮,看成功率、p95 和显存峰值是否仍落在阈值内。需要强调的是边界——这些数值只对记录里的 GPU 型号与显存、驱动版本、推理框架版本、权重精度、是否开启量化这几项同时成立时才有效,换卡、换框架大版本、把权重从高精度换成量化版,都必须重新跑一遍梯度,不能直接套用旧配置。