VeriLoop E2 接进自己的脚本 / 先用本地服务把单次请求跑通再谈并发

文章导读
把 VeriLoop E2 接进自己的脚本,比较稳的顺序是:先在本地把服务起起来,从启动日志里抄下真实的监听地址、端口和请求路径,用一条占位字段写死的最小请求拿到一次返回,确认正文字段位置和错误分支,再谈并发。直接照着别人的配置写脚本、或者一上来就压并发,出问题时很难分清是字段名写错、路径拼错,还是服务本身扛不住。
📋 目录
  1. 一 从服务启动日志里确认监听地址与请求路径
  2. 二 用一条最小请求验证通路
  3. 三 从响应里取正文并处理失败分支
  4. 四 加并发前先测单请求的延迟与显存峰值
  5. 五 逐步加压,观察排队与拒绝行为
A A

把 VeriLoop E2 接进自己的脚本,比较稳的顺序是:先在本地把服务起起来,从启动日志里抄下真实的监听地址、端口和请求路径,用一条占位字段写死的最小请求拿到一次返回,确认正文字段位置和错误分支,再谈并发。直接照着别人的配置写脚本、或者一上来就压并发,出问题时很难分清是字段名写错、路径拼错,还是服务本身扛不住。

接入 VeriLoop E2 的第一步不是找接口文档,而是读本地启动日志,确认监听地址、端口和请求路径这三件事;然后用字段全部占位的最小请求骨架跑通单次调用,再从原始响应里定位正文字段并补上超时与非 2xx 分支。适用场景是自己写脚本调本地推理服务;验证方式是启动日志、命令返回和显存读数;边界是字段名和响应结构必须以你实际启动的那一版输出为准,未确认前不要写进业务逻辑,也不要先加并发。

从服务启动日志里确认监听地址与请求路径

启动日志里有几行是关键:服务框架打印的监听行(通常形如 Running on http://0.0.0.0:8000 或 Listening on ...)、模型加载完成行、以及注册路由或挂载前缀的行。不同版本措辞不一样,但地址、端口、路径这三类信息一般都能在这几行里找到,或者从紧跟着的“启动完成”提示前的路由列表里找到。

需要记录的是三类:

  • 主机:日志写 0.0.0.0 表示绑定所有网卡,本机脚本应改用 127.0.0.1;日志直接写 127.0.0.1 或某个局域网 IP,就按原样填。
  • 端口:把日志里的数字抄下来,不要沿用示例里的 8000。端口被占用时服务可能换端口启动,以当次日志为准。
  • 路径:包括前缀和端点名,例如 /v1/xxx 或 /generate,前缀和末尾斜杠都要保留,不要自己拼。

把这三项写成脚本顶部的一行 BASE_URL 加一行 PATH 常量,后面所有调试都改这两行,不要散落在各处。如果日志里看不出路径,可以用 curl -i {BASE_URL}/ 或开放接口页看返回的路由说明,仍以实际返回为准。

用一条最小请求验证通路

写业务逻辑之前先确认能拿到返回。下面是一个通用请求骨架,所有字段名都是占位符,跑之前必须回填成真实值,不确定的整行删掉,不要靠猜字段名硬试。

import json, time, urllib.request

BASE_URL = "http://{HOST}:{PORT}"   # 回填:启动日志里的地址与端口
PATH = "{PATH}"                    # 回填:启动日志里的请求路径,含前缀
TIMEOUT = 30                       # 回填:先给宽一点,之后按单请求实测收紧

payload = {
    "{INPUT_FIELD}": "你好,这是一次连通性测试",  # 回填:输入正文所在字段名
    "{MODEL_FIELD}": "{MODEL_NAME}",            # 回填:日志里的模型名或版本标识,无此要求就删掉
    "{STREAM_FIELD}": False,                    # 回填:是否流式,不确定就删掉
}

req = urllib.request.Request(
    BASE_URL + PATH,
    data=json.dumps(payload).encode(),
    headers={"Content-Type": "application/json"},
    method="POST",
)
t0 = time.time()
try:
    with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
        raw = resp.read().decode()
        print(resp.status, round(time.time() - t0, 3))
        print(raw[:2000])
except Exception as e:
    print("FAIL", type(e).__name__, str(e)[:500])

占位符的回填依据分别是:{HOST} 和 {PORT} 来自启动日志监听行;{PATH} 来自路由或前缀行;{INPUT_FIELD}、{MODEL_FIELD}、{STREAM_FIELD} 如果没有可靠说明,就先只留输入正文一个字段试一次——多数服务对多余字段会报错,对缺失字段也会报错,报错信息本身就是最快的字段来源。这一步的目标只有一个:看到状态码和一段原始返回。

从响应里取正文并处理失败分支

拿到原始返回后先打印完整结构,再决定正文路径。常见形态是嵌套在某个数组元素的文本字段里,也可能是顶层某个字符串字段;具体路径按你这次打印出来的结构写,例:data["{CHOICES_FIELD}"][0]["{TEXT_FIELD}"],两个占位符换成实际键名。取正文时做一层兜底:主路径取不到就走备用路径,两条都失败就抛出并落盘原始响应,不要让脚本返回空字符串继续往下跑。

VeriLoop E2 接进自己的脚本 / 先用本地服务把单次请求跑通再谈并发

失败分支分三类处理:连接超时与读超时分开设置,前者说明地址端口或服务未起,后者说明请求已发出但处理太慢;非 2xx 时打印状态码和响应体前若干字节,不要只打印异常类型;状态码 2xx 但正文字段缺失时按解析失败处理,同样保留原始响应。重试只对超时和 5xx 这类可重试情况开放,且在重试前把这几项落盘:时间戳、耗时、请求标识(若有返回头或响应里的 id)、状态码、请求体(长 prompt 截断)、响应体前若干字节。没有这些记录,重试只会把问题放大成噪声。

加并发前先测单请求的延迟与显存峰值

并发之前先量单请求。延迟记录方式是:同一个 payload 连跑若干次(十次左右即可),每次记下开始时间与结束时间,分别看首次(含预热)和后续几次的耗时区间,不要只取一个平均值。如果服务支持流式,把首段返回时间和整体结束时间分开记,脚本超时按整体时间设。

显存峰值用轮询方式记录,例如在另一个终端跑 nvidia-smi `--query-gpu`=memory.used `--format`=csv -l 1,依次记三个数:服务空载时的基线、模型加载完成后的占用、单次请求过程中的最高值。没有 GPU 的机器改看进程常驻内存。显存余量不足时的典型表现是:请求后段明显变慢、日志出现分配失败、服务进程直接退出被重新拉起,或者退化成 CPU 计算导致耗时成倍拉长。出现这些现象说明并发数没有讨论空间,先降输入长度或分批调用。

逐步加压,观察排队与拒绝行为

加压从 1 开始,按 1、2、4、8 这样的档位递增,每档保持一段时间,记录四个量:该档位下的成功次数、失败次数、耗时上分位(例如 p95)、以及失败的具体形态。需要重点区分的三种现象是:

  • 排队:请求都成功,但耗时随并发数近似线性变长,说明服务在串行处理,吞吐没有提升。
  • 超时:客户端先到超时时间,服务端可能仍在处理,此时要确认服务是否还有未完成的请求在跑。
  • 拒绝:直接返回连接错误或 5xx,说明队列已满、显存不足或进程已退出,这一档就是硬上限。

并发上限的定法是:取“耗时开始明显变长但仍无失败”的上一档,再往下退一档作为脚本默认并发,同时在客户端设好超时和最大重试次数,服务端如有队列长度或并发参数也一并收敛。这个值只对你当前机器、当前模型和当前输入长度成立,换环境需要重测,不要把它当固定常数写死。