手上已经有 SenseNova-Vision 的权重,第一步建议不要写批量脚本,而是先拿一张本地图片把完整链路跑一遍:图片怎么读进来、预处理成什么形状、模型前向返回什么结构、同一张图再跑一次结果是否一致。这几件事没看清就往批量堆,出问题时分不清是输入格式错了、权重没加载全,还是显存不够导致的静默降级。下面的内容只围绕单张图的跑通与验证,批量留到最后一步的入口改造。
适用场景:本地已拿到 SenseNova-Vision 权重,准备把图片问答接进业务或调试流程。操作动作:先确认权重与显存能撑住一次单图推理,用通用骨架组装输入并打印每一步 shape,跑第一次前向并打印原始返回结构,同图二次调用对比差异,最后改成可传参入口。验证方式:看返回字段是否含描述文本、耗时三段记录、推理前后显存读数。风险边界:单图跑通只说明链路成立,不代表长图、多图、并发批量下同样稳定,批量前需要补每张耗时与峰值显存记录。
确认权重、依赖与显存能不能装下这一次单图推理
在写任何调用代码之前,先把环境不成立的情况排除掉。需要逐项确认的东西并不多,但漏一项后面都会以“模型没反应”或“输出乱码”的形式出现。
- 权重文件是否完整:目录下应该有 config、tokenizer 或 processor 相关文件、以及全部分片权重;如果带 index 文件,分片数量要和 index 里记录的一致,缺一个分片照样能加载,但会在前向时报形状错。
- 推理框架版本:记录 torch 与推理框架的实际版本号,不要凭记忆。远程自定义代码类模型对框架版本比较敏感,版本对不上时的报错往往指向算子而不是版本本身。
- 单图推理显存的观测方式:在推理前后各看一次,别只看峰值。可以用
nvidia-smi,也可以在进程里打印torch.cuda.memory_allocated()。前者看整卡占用,后者只看当前进程的分配量,两者对照才知道是不是被别的进程占了显存。
ls -lh /path/to/sensenova-vision/ # 看 config、processor、权重分片是否齐全
du -sh /path/to/sensenova-vision/
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
nvidia-smi `--query-gpu`=memory.used,memory.total `--format`=csv # 推理前先记一次
如果这一步就发现显存余量很紧,建议先降低单图分辨率或减少最大生成长度再试,而不是急着加 swap 或换更小的机器配置,那类做法只是把失败点往后推。
把一张本地图片组装成模型能接受的输入
图片问答最常见的失败点是输入没对齐:路径下是 PNG 带透明通道、灰度图、EXIF 旋转过的图,或者尺寸和模型预期差得太多。处理顺序建议固定为读图、统一色彩模式、尺寸调整、归一化、维度调整,每一步打印一次 shape 和数值范围,出问题时能立刻定位到哪一步偏了。
from PIL import Image
import numpy as np, torch
img = Image.open("demo.jpg").convert("RGB") # 统一 RGB,规避灰度/CMYK/透明通道
print("read", img.size, img.mode)
img = img.resize((448, 448), Image.BICUBIC) # 尺寸以权重自带 config/processor 为准
print("resize", img.size)
arr = np.asarray(img, dtype=np.float32) / 255.0
print("scale", arr.shape, arr.dtype, float(arr.min()), float(arr.max()))
mean = np.array([0.5, 0.5, 0.5], dtype=np.float32)
std = np.array([0.5, 0.5, 0.5], dtype=np.float32)
arr = (arr - mean) / std
print("norm", arr.shape, float(arr.min()), float(arr.max()))
t = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0) # HWC -> NCHW
print("tensor", tuple(t.shape), t.dtype) # 期望 [1, 3, H, W]
均值方差和尺寸这两项是可替换项,必须和权重目录里 processor 或 config 记录的值一致。如果模型自带 processor,直接用它处理图片会更保险,但同样要把输出张量的 shape 打印出来对照一次,避免以为用了 processor 就一定对齐。
跑第一次前向并打印原始返回结构
第一次调用不要急着美化输出,先把原始返回原样打印出来,看清它是张量、字典还是带属性的对象。下面的骨架只保留加载、传入、打印三段,具体模型类名和 generate 参数按权重仓库说明替换。
import torch
from transformers import AutoProcessor, AutoModelForVision2Seq # 类名按权重说明替换
mid = "/path/to/sensenova-vision"
processor = AutoProcessor.from_pretrained(mid, trust_remote_code=True)
model = AutoModelForVision2Seq.from_pretrained(mid, torch_dtype=torch.float16,
trust_remote_code=True).to("cuda").eval()
print("after load", torch.cuda.memory_allocated() / 1024**3, "GiB")
inputs = processor(images=img, text="描述这张图片的内容", return_tensors="pt").to("cuda")
print({k: tuple(v.shape) for k, v in inputs.items() if hasattr(v, "shape")})
with torch.no_grad():
out = model.generate(**inputs, max_new_tokens=64)
print(type(out), getattr(out, "shape", None))
print(out)
print(processor.batch_decode(out, skip_special_tokens=True))
拿到原始返回后,按来源分三类去看:描述文本通常是解码后的自然语言字符串;结构化字段如果是字典形式,键名会比较直观,例如坐标、标签、分数这类;其余输出多半是 logits、hidden states 或 attention,属于调试信息,不要混进业务字段里。如果返回是字典,直接 for k, v in out.items(): print(k, type(v), getattr(v, "shape", None)) 走一遍,比猜字段名可靠。
在同一张图上做第二次调用,确认结果可复现
第一次成功不等于链路稳定,缓存、随机采样、未清的中间状态都可能造成“看起来成功”。判断办法很简单:同一张图、同一段指令、同一套预处理参数,连跑两次,把两次的原始输出分别落盘再比对。
- 固定随机性:脚本开头设置
torch.manual_seed(0),采样相关参数关掉或设为确定性取值;如果用的是自回归生成,生成长度也固定。 - 固定输入:图片不换、分辨率不换、prompt 逐字一致,最好把预处理后的张量哈希或首尾几个数值打印出来,确认两次喂进去的确实一样。
- 记录差异点:把两次输出写到两个文本文件,用
diff对比;结构化字段逐个键比,重点关注浮点字段是否在小数位上有抖动。
# 跑两次后对比
python run_once.py `--image` demo.jpg `--out` a.json
python run_once.py `--image` demo.jpg `--out` b.json
diff a.json b.json | head -40
如果两次描述文本不同但结构化字段一致,多见于生成阶段的采样未关闭;如果结构化字段本身就在变,需要先怀疑预处理或输入顺序,而不是模型能力。
把单图脚本改成可传参的入口
单图脚本跑通后,最后一步才是把它改成能接收参数的入口,为批量铺路。改造范围很小:图片路径、指令、输出文件走命令行参数,脚本内部按固定顺序记录耗时。
import argparse, json, time
p = argparse.ArgumentParser()
p.add_argument("`--image`", required=True)
p.add_argument("`--prompt`", default="描述这张图片的内容")
p.add_argument("`--out`", default="result.json")
a = p.parse_args()
t0 = time.perf_counter()
# ...读图与预处理...
t1 = time.perf_counter()
# ...前向与解码...
t2 = time.perf_counter()
rec = {"image": a.image, "prompt": a.prompt, "text": txt,
"preprocess_s": round(t1 - t0, 3), "forward_s": round(t2 - t1, 3),
"total_s": round(t2 - t0, 3),
"mem_allocated_gib": round(torch.cuda.memory_allocated() / 1024**3, 3)}
json.dump(rec, open(a.out, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
批量之前必须先记录的字段就是这几项:单张预处理耗时、单张前向耗时、单张总耗时、峰值显存占用。有了单张的真实耗时分布,才能判断批量是要串行跑、要不要按图片尺寸分组、显存余量够不够同时放几张。这一步不落数据,批量阶段就只剩下“跑挂了大不了重跑”的盲猜。