MiniCPM-V 4.7 端侧响应慢——该压图片分辨率还是减上下文长度?

文章导读
端侧跑 MiniCPM-V 4.7 时感觉每次问答都要等很久,先不要同时去压图片分辨率和截断上下文。更稳的做法是把一次请求拆成「图片预处理」「模型推理」「结果输出」三段分别打点,再固定其中一项、只改另一项做对照。通常图片长边和视觉 token 数会明显影响预处理与首 token 时间,可以先从分辨率下手;但如果对话历史远超实际需要,把上下文截到够用长度往往一步见效。两条都改,就看不出收益来自哪一边
📋 目录
  1. 壹 把一次问答拆成预处理、推理、输出三段计时
  2. 贰 固定上下文只改图片分辨率
  3. 叁 固定图片只改上下文长度
  4. 肆 观察显存与耗时的关系,判断是否在反复换页
  5. 伍 把延迟可接受的一组输入限制写进调用侧
A A

端侧跑 MiniCPM-V 4.7 时感觉每次问答都要等很久,先不要同时去压图片分辨率和截断上下文。更稳的做法是把一次请求拆成「图片预处理」「模型推理」「结果输出」三段分别打点,再固定其中一项、只改另一项做对照。通常图片长边和视觉 token 数会明显影响预处理与首 token 时间,可以先从分辨率下手;但如果对话历史远超实际需要,把上下文截到够用长度往往一步见效。两条都改,就看不出收益来自哪一边。

判断顺序建议是:先分段计时定位瓶颈,再做两组单变量对照(只改图片尺寸、只改上下文长度),最后把延迟可接受的一组上限写进调用侧做校验。多数端侧环境下图片分辨率对首段耗时更敏感,可优先尝试压缩;上下文长度影响主要体现在 prompt 处理和多轮累积上,若历史明显冗余,截断收益直接。两组不要同时动,上限值必须在目标设备上按记录表收敛后确定,换硬件、换量化方式或换推理后端都要重新确认。

把一次问答拆成预处理、推理、输出三段计时

打点要放在自己封装的调用入口,而不是模型内部,这样能覆盖图片缩放、编码、prompt 拼接和解码全过程。建议至少记录:请求时间戳、图片长边、上下文 token 数、预处理耗时、首 token 耗时、生成总耗时。若使用流式输出,首 token 时间与后续每 token 间隔要分开记,二者对应的瓶颈完全不同。

import time, json, logging

def timed_ask(image, ctx_tokens, max_new_tokens):
    t0 = time.perf_counter()
    ts = time.time()

    img = preprocess(image)                 # 缩放 / 归一化,替换成你的预处理
    t1 = time.perf_counter()

    first = None
    out = []
    for chunk in model.generate(img, ctx_tokens, max_new_tokens=max_new_tokens):
        if first is None:
            first = time.perf_counter()     # 首 token 到达时刻
        out.append(chunk)
    t2 = time.perf_counter()

    logging.info(json.dumps({
        'ts': ts,
        'img_side': max(image.size),
        'ctx_tokens': ctx_tokens,
        'pre_ms': round((t1 - t0) * 1000, 1),
        'first_ms': round((first - t1) * 1000, 1) if first else None,
        'gen_ms': round((t2 - t1) * 1000, 1),
    }))
    return ''.join(out)

读日志的方式:预处理段占比高,说明瓶颈在缩放和图像编码,此时压分辨率收益最直接;首 token 与生成总耗时同时偏高,且每 token 间隔都长,多半和上下文长度、显存或后端调度相关。单次数据不可信,同一档位连跑几次取中位数更稳妥。

固定上下文只改图片分辨率

先固定同一段 prompt 和同一个 max_new_tokens,只动图片预处理的分辨率参数。常见调整位置有三处:调用前的 resize(把长边限制到目标值)、处理器里的 image size 配置、以及模型自带的视觉输入档位。三者可能同时存在,优先改调用前的 resize,最容易控制和回滚。

# 调用前的统一缩放,长边限制到 side
from PIL import Image

def preprocess(image, side=448):
    w, h = image.size
    scale = side / max(w, h)
    if scale < 1:
        image = image.resize((int(w * scale), int(h * scale)), Image.LANCZOS)
    return image
图片长边档位预处理耗时首 token 耗时生成总耗时备注
224填写填写填写先测最小档做基线
336填写填写填写
448填写填写填写常见折中点,重点观察
672填写填写填写看是否出现陡增
原图填写填写填写记录长边像素

看表时不要只看绝对耗时,要看相邻档之间的增量是否平稳。若某档之后耗时明显跳升,说明视觉 token 数或显存占用跨过了一个台阶,这一档就是该设备上的实际上限。图片内容本身(文字截图还是自然照片)也会影响可读性,压得太小要回头确认识别质量还能接受。

固定图片只改上下文长度

把图片固定在同一张、同一分辨率,只改传入的上下文长度。配置位置通常在调用侧的历史裁剪逻辑、模型的 max_length 或 context length 参数、以及生成时的 max_new_tokens。注意区分两件事:上下文长度限制的是输入总 token,max_new_tokens 限制的是输出长度,短输出不一定代表输入短。

MiniCPM-V 4.7 端侧响应慢——该压图片分辨率还是减上下文长度?
# 调用侧按 token 数截断历史,保留最近对话
MAX_CTX = 2048

history = trim_history(history, MAX_CTX)   # 从最早轮次开始丢,保留当前问题
ctx_tokens = count_tokens(history) + count_tokens(question)
上下文长度档位首 token 耗时生成总耗时显存峰值备注
512填写填写填写基线
1024填写填写填写
2048填写填写填写
4096填写填写填写观察是否突增
8192填写填写填写仅作对照,未必需要

如果 512 到 2048 的耗时变化很小,而图片分辨率那组对照变化明显,就该先压分辨率,不必急着砍上下文。反过来,若上下文从 2048 涨到 4096 时耗时出现非线性增长,并且显存同时吃紧,那就不是长度本身的问题,需要先看下一节的显存情况。

观察显存与耗时的关系,判断是否在反复换页

显存吃紧会带来额外的等待,这种慢容易被误判成「模型本身慢」。采样可以在另一终端常驻跑,和请求日志按时间对齐看。

# 每秒采一次显存占用,输出到文件后与请求日志对照
nvidia-smi `--query-gpu`=memory.used,memory.total `--format`=csv -l 1 > mem.csv

# 有交换分区的话,同时看换页情况
vmstat 1 > vmstat.log

判断标准可以先用这几条:耗时突增的时间点与显存接近上限的时间点重合;日志里出现 offload、fallback 到 CPU、内存分配失败的告警;系统层面 si/so 持续非零;同一请求连跑几次,第一次明显慢、后续变快。满足其中两条以上,就应先解决显存问题,而不是继续调参。需要注意量化方式、后端(如 llama.cpp 类或带 GPU 加速的推理框架)对显存行为差异很大,同一组档位在不同后端上结论可能不一致,换环境要重新跑一次对照。

把延迟可接受的一组输入限制写进调用侧

对照表收敛之后,把可用的一组上限固化到调用入口,别指望调用方自觉。下面的数值只是起点示例,必须换成你在上一节记录表里挑出的那组。

LIMITS = {
    'img_max_side': 448,      # 图片长边上限,按分辨率对照表确定
    'ctx_tokens': 2048,       # 输入上下文上限,按长度对照表确定
    'max_new_tokens': 256,    # 输出上限,单独限制
}

def check_request(image, ctx_tokens):
    side = max(image.size)
    if side > LIMITS['img_max_side']:
        return False, 'image side %d over limit' % side
    if ctx_tokens > LIMITS['ctx_tokens']:
        return False, 'ctx %d over limit' % ctx_tokens
    return True, ''

校验位置放在最外层封装函数的第一行,超限时直接返回明确错误并写一条日志,不要静默缩放或静默截断,否则问题会被藏起来。验证方式:构造一张长边明显超限的图片和一个超长历史,确认两者都被拦下且日志可查;再跑一条正常请求,确认耗时落回记录表中可接受的区间。上限值本身要随设备变化复核,换机型、换量化、换后端之后,建议重跑分辨率与上下文两组对照再更新这组常量。