MOSS-VL-Realtime 跑通单图问答

MOSS-VL-Realtime 跑通单图问答

2026-10-24 14:02:12 0点赞 0收藏 0评论

把 MOSS-VL-Realtime 从单图问答扩到视频帧问答,风险不在模型,而在“你同时改了两件事”:输入从一张图变成多帧、调用从一次变成循环。比较稳妥的顺序是先在单张图片上把加载、预处理、生成、解码跑成可复现的基线,记下每条输出;然后把输入源换成抽帧保存的图片,模型、预处理参数、生成参数一律不动。这样一旦报错或结果变差,能确定问题出在抽帧和文件这一层,而不是模型推理。

建议先固定单图调用骨架并落盘记录,再把同一份调用函数套到抽帧图片上,只替换图像路径这一项。判断迁移是否成功,看三类信号:单图基线可重复输出、抽帧目录与命名符合约定、同一 prompt 在新输入上的输出结构不变。边界在于:本文给的是通用接入骨架,模型类名、processor 参数、是否需要 patch 展开等细节需按所用仓库的 config 与 README 确认,不要直接照抄。

写出单图问答的最小调用骨架:加载、预处理、生成、解码

先做一个只依赖一张图和一个问题的脚本,参数全部写死成常量,方便后面比对。下面这段是通用骨架,具体类名可以按实际仓库替换;如果你的仓库自带 chat() 之类的封装,用封装也可以,但要保证它内部的加载、预处理、生成、解码四步能分别打印形状。

# single_img_qa.py import argparse, json, time import torch from PIL import Image # 按实际仓库替换下面两个导入与类名 from transformers import AutoProcessor, AutoModelForVision2Seq MODEL_ID = 'your/moss-vl-realtime-path' MAX_NEW_TOKENS = 64 parser = argparse.ArgumentParser() parser.add_argument('`--image`', required=True) parser.add_argument('`--question`', default='描述这张图片的主要内容') args = parser.parse_args() # 1) 加载 proc = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True) model = AutoModelForVision2Seq.from_pretrained( MODEL_ID, torch_dtype='auto', device_map='auto', trust_remote_code=True ).eval() # 2) 读图与预处理 img = Image.open(args.image).convert('RGB') print('image size/mode:', img.size, img.mode) msgs = [{'role': 'user', 'content': [ {'type': 'image'}, {'type': 'text', 'text': args.question} ]}] prompt = proc.apply_chat_template(msgs, add_generation_prompt=True) inputs = proc(images=img, text=prompt, return_tensors='pt').to(model.device) print('input shapes:', {k: tuple(v.shape) for k, v in inputs.items()}) # 3) 生成 t0 = time.perf_counter() with torch.inference_mode(): out = model.generate(**inputs, max_new_tokens=MAX_NEW_TOKENS, do_sample=False) latency_ms = round((time.perf_counter() - t0) * 1000) print('generate output shape:', tuple(out.shape)) # 4) 解码(只取新生成部分) gen_ids = out[:, inputs['input_ids'].shape[1]:] text = proc.batch_decode(gen_ids, skip_special_tokens=True)[0] print('answer:', text) print('latency_ms:', latency_ms)

每步要看什么形状:pixel_values 通常是 [1, 3, H, W],部分模型会展开成 [1, N, 3, p, p] 的 patch 形式;input_ids 是 [1, L];generate 返回 [1, L+新生成长度]。如果 generate 的返回长度等于输入长度,说明没有产出新 token,问题在生成参数或输入没被正确识别,而不在解码。运行方式:python single_img_qa.py --image samples/cat.jpg,先换两三张图各跑一遍,确认回答与图片内容对得上,再继续。

把输出结构固定下来,存成可对比的记录

基线要能被diff,就必须落盘。建议每次运行追加一行 JSONL,字段固定,之后换输入源时逐条比对文本,而不是靠肉眼看终端。

record = { 'img_id': 'cat', # 图片标识,建议用文件名去扩展名 'path': args.image, 'size': list(img.size), # [W, H] 'question': args.question, 'answer': text, 'latency_ms': latency_ms, 'n_new_tokens': int(gen_ids.shape[1]), 'model_id': MODEL_ID, 'dtype': str(model.dtype), 'max_new_tokens': MAX_NEW_TOKENS } with open('records_baseline.jsonl', 'a', encoding='utf-8') as f: f.write(json.dumps(record, ensure_ascii=False) + 'n')

比对方法:同一批图片跑两次,用 diff <(jq -r '.img_id+"t"+.answer' records_a.jsonl | sort) <(jq -r '.img_id+"t"+.answer' records_b.jsonl | sort) 看文本是否一致;再单独看 n_new_tokens 和 latency_ms。文本一致、耗时在同一量级,才算基线稳定。若同一张图两次回答就不同,先查 do_sample 是否被设成 True、是否有随机 seed,不要急着怀疑模型。

先用 MOSS-VL-Realtime 跑通单图问答、再把同一套调用套到视频帧上先用 MOSS-VL-Realtime 跑通单图问答、再把同一套调用套到视频帧上

把图像输入换成抽帧保存的图片,其余参数完全不动

抽帧单独做成一步,落成普通图片文件,让模型侧始终只面对“路径列表”。这样模型代码不需要知道视频存在。

# extract_frames.py import argparse, os, cv2 parser = argparse.ArgumentParser() parser.add_argument('`--video`', required=True) parser.add_argument('`--out`', required=True) parser.add_argument('`--interval`', type=float, default=1.0) # 秒 args = parser.parse_args() cap = cv2.VideoCapture(args.video) fps = cap.get(cv2.CAP_PROP_FPS) or 25.0 step = max(int(round(fps * args.interval)), 1) vid = os.path.splitext(os.path.basename(args.video))[0] dir_out = os.path.join(args.out, f'{vid}_iv{args.interval:g}s') os.makedirs(dir_out, exist_ok=True) i = saved = 0 while True: ok, frame = cap.read() if not ok: break if i % step == 0: name = f'frame_{saved:06d}.jpg' cv2.imwrite(os.path.join(dir_out, name), frame) saved += 1 i += 1 cap.release() print('frames saved:', saved, 'to', dir_out)

目录命名约定建议写成 frames/{video_id}_iv{间隔}s/frame_000000.jpg,间隔和视频名都进路径,后面做间隔梯度实验时不会互相覆盖。迁移时改动对照如下,除第一行外其余应完全一致:

  • 输入源:samples/cat.jpg → frames/clip01_iv1s/frame_000012.jpg

  • 模型 ID、dtype、device:不变

  • max_new_tokens、do_sample:不变

  • chat 模板与问题文本:不变

  • 预处理里的 resize 策略:不变(抽帧图分辨率不同时,先记录原始尺寸,不要顺手改缩放)

验证动作:先抽 3 帧,用单图脚本逐帧跑,确认每帧都能出文本、且 n_new_tokens 不为 0。这一步通过,再写循环批量跑,避免把抽帧错误和循环错误混在一起排查。

先用 MOSS-VL-Realtime 跑通单图问答、再把同一套调用套到视频帧上先用 MOSS-VL-Realtime 跑通单图问答、再把同一套调用套到视频帧上

观察抽帧间隔变化时耗时与显存占用的联动

视频场景的压力通常来自帧数量和分辨率,而不是单帧推理本身。用间隔梯度做一次小规模观测即可,不必全量跑。建议间隔取 1s、2s、5s 三档,每档只取前若干帧,逐帧调用、逐帧记录,不做批处理,先把单帧成本看清。

torch.cuda.reset_peak_memory_stats() if torch.cuda.is_available() else None # 每帧推理前记录时间,推理后读取峰值显存 peak_mb = round(torch.cuda.max_memory_allocated() / 1024**2) if torch.cuda.is_available() else -1

记录表字段建议:video_id、interval_s、frame_index、frame_size、latency_ms、peak_mem_mb、n_new_tokens、answer。看三件事:单帧耗时是否随间隔变化(通常不该变,若变了说明混入了别的开销)、峰值显存是否随帧数累积(若不释放,可能是张量或缓存被持有)、同一帧不同间隔下的回答是否漂移。若显存持续上涨,检查是否在循环里保留了 outputs 或 inputs 引用,而不是先去做量化。

按顺序排查迁移后最常见的失败点

出错时按下面顺序查,每一步都有可运行的验证,不要跳步。

  1. 文件路径:先确认抽帧真的写出来了。ls frames/clip01_iv1s | head,再 python -c "import os;print(os.path.exists(p))"。路径含空格或中文时,命令行参数要加引号。

  2. 图像通道与尺寸:python -c "from PIL import Image;i=Image.open(p);print(i.size,i.mode)"。视频帧多为 RGB,若某个环节转成了 BGRA 或灰度,先 .convert('RGB') 再进 processor。

  3. 批量维度:打印 inputs 里每个张量的 shape,确认 batch 维是 1。逐帧循环时最容易把 [3,H,W] 当成 [1,3,H,W] 传进去;批处理时则要注意 pixel_values 是否被堆成 [B,...] 而 input_ids 还是 [1,L]。

  4. 解码失败:若打印出的 answer 为空,先看 gen_ids.shape[1] 是否为 0;不为 0 但文本为空,通常是只解到了特殊 token,检查是否切掉了输入部分、skip_special_tokens 是否开启。

排查结论要落回记录文件:把失败帧的路径、形状、耗时写进同一条 JSONL,标注 stage 字段(path / shape / batch / decode)。这样下一次换间隔或换视频时,能直接对照上次的失败点,而不是从头再试。

展开 收起
0评论

当前文章无评论,是时候发表评论了
提示信息

取消
确认
评论举报

相关文章推荐

更多精彩文章
更多精彩文章
京东京享红包
距结束::
领京东现金红包 天天领3次 最高26888元
红包按钮
最新文章 热门文章
0
扫一下,分享更方便,购买更轻松