FLUX 调用先看单张延迟、再看批量并发和失败重试

文章导读
接口能跑通,但一批图跑起来忽快忽慢,通常不是接口坏了,而是三件事叠在一起:单张延迟本身多大、并发开多少合适、失败重试放大了多少调用量。可行的顺序是先固定参数连跑单张调用拿到基准延迟,再把并发从 1 逐步抬到 5,看平均单张耗时和失败数在哪里变差,最后把失败按特征分组,只对可重试的那部分加次数上限和退避间隔。三件事分开测,才判断得出瓶颈是在服务端排队、并发设置还是重试叠加。
📋 目录
  1. 一 单张调用前后各打一个时间戳,拿到基准延迟
  2. 二 把并发从 1 逐步抬到 5,记录每次的总耗时和失败数
  3. 三 区分失败类型:参数被拒、超时、服务端返回错误
  4. 四 给重试加次数上限和退避间隔
  5. 五 在同一张记录表里同时看延迟、并发、重试三个数
A A

接口能跑通,但一批图跑起来忽快忽慢,通常不是接口坏了,而是三件事叠在一起:单张延迟本身多大、并发开多少合适、失败重试放大了多少调用量。可行的顺序是先固定参数连跑单张调用拿到基准延迟,再把并发从 1 逐步抬到 5,看平均单张耗时和失败数在哪里变差,最后把失败按特征分组,只对可重试的那部分加次数上限和退避间隔。三件事分开测,才判断得出瓶颈是在服务端排队、并发设置还是重试叠加。

适合已经能成功调用 FLUX 接口、但批量任务耗时和失败率不稳定的人。先测单张基线(同一参数连跑 3 次并保留原始输出),再按 1 到 5 的并发梯度各跑固定张数,同时记录成功数、失败数与平均单张耗时;失败先按 4xx、超时、429、5xx、返回体不合法分组,只有超时和 429/5xx 才进入有限重试。判断依据是自己的时间戳和日志,不是一次手感。边界:并发上限与超时阈值取决于所用服务的配额和排队策略,需要结合环境确认,不要照搬别人的数字。

单张调用前后各打一个时间戳,拿到基准延迟

基准延迟的作用是给后面所有对比提供参照:同一 prompt、同一尺寸、同一步数、同一超时设置,连跑 3 次,把每次耗时和状态码原样打出来。计时只包住 HTTP 请求这一段,别把写盘、图片解码、后处理算进去,否则量到的是本地处理时间,跟服务端延迟没关系。

脚本骨架(替换 URL、TOKEN、payload 后可直接跑)

import time
import requests

URL = 'https://<your-endpoint>/v1/flux'
HEADERS = {'Authorization': 'Bearer <TOKEN>', 'Content-Type': 'application/json'}
PAYLOAD = {'prompt': 'a red apple on a wooden table', 'steps': 28, 'width': 1024, 'height': 1024}

for i in range(3):
    t0 = time.perf_counter()
    r = requests.post(URL, json=PAYLOAD, headers=HEADERS, timeout=120)
    cost = time.perf_counter() - t0
    print('[run %d] status=%s cost=%.3fs bytes=%d' % (i + 1, r.status_code, cost, len(r.content)))

原始输出按下面这种格式直接贴进记录表,数值换成你自己跑出来的:

[run 1] status=200 cost=<耗时>s bytes=<字节数>
[run 2] status=200 cost=<耗时>s bytes=<字节数>
[run 3] status=200 cost=<耗时>s bytes=<字节数>

看这三次要抓两个点:第一次是否明显比后两次慢(首次调用可能包含连接建立、模型加载或服务端冷启动);三次之间的波动幅度有多大。如果波动很大,后面的并发测试至少要重复一轮,否则拐点会被噪声盖住。timeout 建议显式写死,不要用默认值——没有超时的请求会把一次卡住变成整批卡住。

把并发从 1 逐步抬到 5,记录每次的总耗时和失败数

并发测试的目的不是找出最大并发,而是找出收益开始变差的拐点。做法是固定总请求数(例如每档 10 张)和固定 payload,只改并发数,从 1 跑到 5,每档记四个数:总耗时、成功数、失败数、平均单张耗时(总耗时除以总请求数)。

import time
import requests
from concurrent.futures import ThreadPoolExecutor

def one(_):
    t0 = time.perf_counter()
    try:
        r = requests.post(URL, json=PAYLOAD, headers=HEADERS, timeout=120)
        return r.status_code == 200, time.perf_counter() - t0
    except Exception:
        return False, time.perf_counter() - t0

N = 10
for c in (1, 2, 3, 4, 5):
    t0 = time.perf_counter()
    with ThreadPoolExecutor(max_workers=c) as ex:
        res = list(ex.map(one, range(N)))
    total = time.perf_counter() - t0
    ok = sum(1 for r, _ in res if r)
    print('并发=%d 总耗时=%.2fs 成功=%d 失败=%d 平均单张=%.2fs'
          % (c, total, ok, N - ok, sum(t for _, t in res) / N))

记录表字段固定成这五列,每档一行,数值从上面的 print 行抄:

并发数总耗时成功数失败数平均单张耗时
1待填待填待填待填
2待填待填待填待填
3待填待填待填待填
4待填待填待填待填
5待填待填待填待填

判断方式:如果从 1 到 2 平均单张耗时明显下降,说明还有并行空间;到某一档平均单张耗时不再下降甚至回升,或者失败数开始出现,这一档就是拐点,实际使用取拐点的前一档更稳。注意客户端并发数不等于服务端并行数,服务端可能在排队或限流,所以要看端到端耗时,而不是线程数。

区分失败类型:参数被拒、超时、服务端返回错误

把所有失败混成一类,会让重试变成放大错误的工具:参数错的请求重试一百次还是错,只会白耗配额。建议按响应特征先分组,每组配一个明确的处理动作,让脚本按状态码和异常类型自动分流。

  • 4xx(400、401、403、404、422 等参数或鉴权被拒):动作是不重试,记录原始响应体,修正 prompt、尺寸、步数或令牌后单独重发。这类失败进重试队列只会浪费调用量。
  • 连接超时、读超时(客户端抛异常,无状态码):动作是允许重试,但必须带退避间隔。边界在于服务端可能已经开始计算,重发存在重复计费或产生重复任务的可能,需要在业务侧做幂等或去重。
  • 429 限流:动作是降低并发加拉长间隔,不要维持原并发硬重试,否则只会拿到更多 429。
  • 5xx 服务端错误:动作是有限重试,例如 1 到 2 次,仍失败就落进失败清单,并保留响应体。
  • 2xx 但返回体不合法(空内容、JSON 缺字段、图片解码失败):动作是按业务失败处理,重试一次并记录原始响应,用来区分是服务端返回异常还是本地解析写错了。

给重试加次数上限和退避间隔

重试必须能停下来。两个参数不可或缺:最大重试次数和退避间隔。次数可以先从 2 次起,退避用指数加随机抖动,避免一批请求在同一时刻集中重发把服务端再打一次。

import time
import random
import requests

MAX_RETRY = 2
BASE = 1.0

def call(payload):
    for attempt in range(MAX_RETRY + 1):
        try:
            r = requests.post(URL, json=payload, headers=HEADERS, timeout=120)
            if r.status_code == 200:
                return r
            if r.status_code == 429 or r.status_code >= 500:
                pass              # 可重试
            else:
                return r          # 4xx 直接返回,不重试
        except (requests.Timeout, requests.ConnectionError):
            pass                  # 可重试
        if attempt < MAX_RETRY:
            time.sleep(BASE * (2 ** attempt) + random.random() * 0.5)
    return None                   # 达到上限,交给失败清单

达到上限后的做法是放弃自动重发:把任务 ID、失败类型、最后一次响应体写进失败清单,人工或下一次任务再处理。单批次内可以设一条止损线,例如连续 5 张失败就暂停整批,先确认是参数问题还是服务端问题,再决定是否继续。统计时把首次请求数和实际重试次数分开记,否则重试悄悄放大调用量,记录表上却看不出来。

在同一张记录表里同时看延迟、并发、重试三个数

只看一次耗时会误判,只看并发数会忽略失败,只看重试次数会掩盖延迟。记录表字段建议固定成下面这些,字段名提前定好,后面几轮测试才能直接横向对比:

  • 批次 ID、任务 ID:能定位到具体哪一张图
  • 模式:单张基线 / 并发梯度 / 批量正式任务
  • 并发数:单张基线填 1
  • 请求总数:这一档或这一批发了多少张
  • 总耗时、平均单张耗时
  • 成功数、失败数
  • 失败类型:4xx / 超时 / 429 / 5xx / 返回体不合法
  • 实际重试次数、是否最终成功
  • 备注:prompt、尺寸、步数、timeout、测试期间是否改过并发

一次测试从头填到尾的填写示例(只示范格式,数值替换成你自己的日志值):

批次模式并发请求总数总耗时平均单张成功/失败失败类型重试次数最终成功
base-01单张基线13待填待填3 / 0无0是
grad-1并发梯度110待填待填待填 / 待填待填0待填
grad-3并发梯度310待填待填待填 / 待填待填0待填
grad-5并发梯度510待填待填待填 / 待填待填待填待填
batch-a批量正式任务取拐点前一档按实际待填待填待填 / 待填待填待填待填

这张表跑完一遍之后,判断顺序就固定了:先看单张基线三次是否稳定,再看并发梯度里哪一档平均单张耗时开始回升或失败数开始出现,最后看实际重试次数有没有把调用量悄悄放大。三处对得上,才说得清批量任务忽快忽慢是卡在服务端排队、并发设置,还是失败重试叠加。