1.微调训练
有 cuda 显卡可以执行 pip install unsloth 可以安装 Unsloth 加快训练和推理。
执行 pip install tensorboard 安装保存完整训练过程的数据,避免中断只能部分曲线。
创建 saves/Qwen3-VL-2B-Instruct/qlora/train_openeqa,并创建文件 training_args.yaml,内容参考,路径根据自己的情况改:
### model
model_name_or_path: model/Qwen3-VL-2B-Instruct
trust_remote_code: true
### method
stage: sft
do_train: true
finetuning_type: lora
lora_target: all
lora_rank: 8
lora_alpha: 16
lora_dropout: 0.1
### 是否使用 unsloth 加速
use_unsloth: false
# 不启用 Unsloth 加速
#unsloth_max_seq_length: 2048 # Unsloth 内部优化
flash_attn: auto # T4 自动回退到 FA1 或 sdpa
### quantization (QLoRA)
quantization_bit: 4
quantization_method: bitsandbytes
double_quantization: true #双量化/嵌套量化,进一步节省显存
### dataset
dataset: open_eqa_train_val
template: qwen3_vl_nothink
cutoff_len: 2048
max_samples: 100000
overwrite_cache: true
preprocessing_num_workers: 8
### output
output_dir: saves/Qwen3-VL-2B-Instruct/qlora/train_openeqa
logging_steps: 10
save_steps: 25
resume_from_checkpoint: false #首次用 false,断点训练用 true
overwrite_output_dir: true
### train
per_device_train_batch_size: 2 # 如果是用 Unsloth 省显存,可以加大
gradient_accumulation_steps: 4 # 有效 batch=8
learning_rate: 5.0e-5 # Unsloth 可以用更高 LR
num_train_epochs: 3.0
lr_scheduler_type: cosine
warmup_ratio: 0.05
#warmup_steps: 0
fp16: true
ddp_timeout: 180000000
### optimization
optim: adamw_torch
# group_by_length: true
report_to: tensorboard
plot_loss: true
video_max_pixels: 65536
video_min_pixels: 256
freeze_multi_modal_projector: true
freeze_vision_tower: true
image_max_pixels: 589824
image_min_pixels: 1024
### evaluation
do_eval: true
per_device_eval_batch_size: 2
val_size: 0.125
eval_strategy: steps
eval_steps: 25
eval_delay: 0 # 延迟多少步开始第一次评估
prediction_loss_only: true # 是否需要完整预测(多模态通常需要 false 以计算准确率和生成指标,除非显存不足)
### save & eval 联动(可选但推荐)
load_best_model_at_end: true # 训练结束后加载最佳模型
#metric_for_best_model: eval_loss # 或 eval_accuracy 等,选择监控指标
#greater_is_better: false # eval_loss 越小越好,如果是 accuracy 则设为 true
#save_total_limit: 3 # 保留最新的 3 个检查点,避免磁盘爆满
这次训练设备配置是显存 16GB 的 NVIDIA Tesla T4,执行 llamafactory-cli train saves/Qwen3-VL-2B-Instruct/qlora/train_openeqa/training_args.yaml 开始训练。


若出现中断,将 resume_from_checkpoint 设置为 true 恢复中断训练继续训练:
llamafactory-cli train saves/Qwen3-VL-2B-Instruct/qlora/train_openeqa/training_args.yaml
训练结束。

观察图片。


从训练和验证的损失曲线与指标来看,Qwen3-VL-2B-Instruct 在 Open-EQA 多模态小样本训练 - 验证集上的表现可以这样总结:
训练阶段表现
- 损失变化:训练损失初始值很高(约 5.5),在训练初期(前 100 步)快速下降,之后下降速度放缓,在 200 步后稳定在 1.0~1.5 区间。
- 最终指标:最终训练损失为 1.3233,说明模型在训练集上的拟合效果良好,且后期波动很小,收敛稳定。
- 效率:训练总耗时约 2 小时 27 分钟,共完成 453 步,计算量达 48049026 GFLOPs,显示出多模态训练的计算成本较高。
验证阶段表现
- 损失变化:验证损失初始值约 1.8,同样在初期快速下降,100 步后趋于平缓,后期稳定在 1.25~1.3 区间。
- 最终指标:最终验证损失为 1.2683,略低于训练损失,这表明模型在未见过的验证数据上泛化能力较好,没有出现明显的过拟合。
- 效率:验证集共 173 个样本,耗时约 2 分钟 10 秒,批大小为 2,验证阶段的样本处理速度比训练阶段更快。
整体表现
训练和验证的损失曲线趋势高度一致,且验证损失略低于训练损失,说明模型在训练过程中不仅对训练数据拟合充分,还具备良好的泛化能力,在 Open-EQA 多模态小样本任务上表现稳定。
如果曲线不完整,可以把云服务器的 runs 目录中 tensorboard 日志文件下载到本地电脑。

在自己电脑的 python 环境执行 uv pip install tensorboard -i http://mirrors.aliyun.com/pypi/simple 安装 tensorboard,对于纯 python 或 conda 可以直接执行 pip install tensorboard -i http://mirrors.aliyun.com/pypi/simple 安装 tensorboard。

执行 tensorboard --logdir=/Users/Zhuanz/Downloads --port 6006 查看,--logdir 是你的 tensorboard 日志文件文件的所在目录(不是文件),port 是端口号可自己设置。

查看网页。



2.测试评估
创建 saves/Qwen3-VL-2B-Instruct/qlora/eval_openeqa 目录,并建立 eval_args.yaml,内容如下,其中 do_predict 设置为 true 代表评估测试,注意要改适配器路径 adapter_name_or_path、基座模型路径 model_name_or_path 和是融合模型路径 output_dir。
adapter_name_or_path: saves/Qwen3-VL-2B-Instruct/qlora/train_openeqa/ #最佳 checkpoint
cutoff_len: 2048
dataset_dir: data
ddp_timeout: 180000000
do_predict: true
eval_dataset: open_eqa_test
finetuning_type: lora
flash_attn: auto
max_new_tokens: 128
max_samples: 99999
model_name_or_path: model/Qwen3-VL-2B-Instruct
output_dir: saves/Qwen3-VL-2B-Instruct/qlora/eval_openeqa
per_device_eval_batch_size: 2
predict_with_generate: true
preprocessing_num_workers: 4
report_to: none
stage: sft
temperature: 0.2
template: qwen3_vl_nothink
top_p: 1.0
trust_remote_code: true
执行 llamafactory-cli train saves/Qwen3-VL-2B-Instruct/qlora/eval_openeqa/eval_args.yaml。

测试评估结束。

结论:Qwen3-VL-2B-Instruct 模型经 QLoRA 轻量化微调 Open-EQA 多模态具身智能数据集后,在 NVIDIA Tesla T4(16GB 显存)硬件环境下完成了全量测试集的推理评估,本次评估共涉及 258 个测试样本,且每个样本包含 8 张关联图片的多模态输入。评估过程中模型采用批大小 2 的推理配置,全程无显存溢出等异常问题,稳定完成所有样本的预测推理,验证了 QLoRA 微调方案对 16GB 显存低算力显卡的良好适配性。从生成指标来看,模型预测 BLEU-4 值达 29.4966,ROUGE 系列指标中 ROUGE-1、ROUGE-2、ROUGE-L 分别为 36.2965、7.9106、35.7659,整体指标表现表明模型经微调后具备了一定的多图关联理解能力和与任务匹配的文本生成能力,其中 ROUGE-1 和 ROUGE-L 的较好表现体现模型能有效捕捉具身智能任务中的核心信息,ROUGE-2 偏低则反映模型在短句语义衔接的细粒度生成上仍有提升空间。从推理效率来看,本次预测全程耗时约 2 小时 55 分钟,样本处理速度为 0.025 个 / 秒、步数处理速度为 0.012 个 / 秒,推理速度整体偏低,核心受单样本 8 张图片的多模态特征提取带来的高计算量影响。整体而言,模型在 16GB 显存的 Tesla T4 显卡上成功实现了 Open-EQA 具身智能多图任务的稳定推理,并取得了具备参考性的生成效果,充分验证了本次 QLoRA 微调的实际有效性,同时也为后续通过优化图片处理策略、调整推理配置来提升模型推理速度,以及针对性优化训练策略改善细粒度生成能力提供了实际参考依据。
3.融合模型导出
创建 saves/Qwen3-VL-2B-Instruct/qlora/merge 目录,并建立 merge_openeqa.yaml,路径同样需要修改,内容如下:
### model
model_name_or_path: model/Qwen3-VL-2B-Instruct
adapter_name_or_path: saves/Qwen3-VL-2B-Instruct/qlora/train_openeqa/
template: qwen3_vl_nothink
finetuning_type: lora
trust_remote_code: true
### export
export_dir: saves/Qwen3-VL-2B-Instruct/qlora/merge
export_size: 2 #导出模型分片(shard)的单文件大小上限,单位是 GB
export_device: auto
export_legacy_format: false #true:导出 .bin(旧/legacy)false:导出 .safetensors(默认/推荐)
执行 llamafactory-cli export saves/Qwen3-VL-2B-Instruct/qlora/merge/merge_openeqa.yaml。

4.推理部署 API 服务
(1) Ollama
将融合模型下载到本地目录,比如/saves/Qwen3-VL-2B-Instruct/qlora/merge,并进入目录,打开 Modelfile 文件,可以根据需要修改。
# ollama modelfile auto-generated by llamafactory
FROM .
TEMPLATE """{{ if .System }}<|im_start|>system {{ .System }}<|im_end|> {{ end }}{{ range .Messages }}{{ if eq .Role "user" }}<|im_start|>user {{ .Content }}<|im_end|> <|im_start|>assistant {{ else if eq .Role "assistant" }}{{ .Content }}<|im_end|> {{ end }}{{ end }}"""
# PARAMETER temperature 0.7 #可设置温度
PARAMETER stop "<|im_end|>"
PARAMETER num_ctx 4096
到终端执行创建模型的命令,模型将被转换格式并放入 ollama 的模型空间中,这里是这个 qwen3-vl-2b 是在 ollama 模型空间的模型名称,可以修改。
ollama create qwen3-vl-2b -f Modelfile

执行 ollama list 查看 ollama 的模型列表。

调用方法
这里只列举其中三种。
1)命令行直接传入
执行 ollama run qwen3-vl-2b "问题" 图片路径,比如
ollama run qwen3-vl-2b "墙上有什么东西" ./data/open_eqa_frames/0a0c0f2b9ba65d1b/000.jpg

2)交互式模式
先执行 ollama run qwen3-vl-2b 加载模型,进入交互模式,然后输入问题及图片路径,比如带有斑纹的椅子上有几个枕头 ./data/open_eqa_frames/0a0c0f2b9ba65d1b/000.jpg。

3)curl 调用
执行 IMG=$(base64 -i data/open_eqa_frames/0a0c0f2b9ba65d1b/000.jpg | tr -d '\n') 转图片为 base64 格式。

执行命令
curl http://localhost:11434/api/generate -d '{
"model": "模型名称",
"system": "系统提示词",
"prompt": "用户提示词",
"images": ["$图片 base64 变量"],
"format": "格式",
"stream": "是否流式输出",
"options": {参数设置},
}'
比如
curl http://localhost:11434/api/generate -d '{ "model": "qwen3-vl-2b", "system": "你是机器人控制 AI。你必须输出可执行的动作序列。scene_analysis 必须包含:目标相对于当前视角的方位(左/右/前)和距离(米)。plan 中的 params 必须使用英文键名(target/type/distance/degrees)。严禁使用中文键名。", "prompt": "观察图片,为指令\"怎么关闭台灯\"输出 JSON:\n{\n \"scene_analysis\": \"目标在 [方位],距离 [X] 米\",\n \"plan\": [\n {\"action\": \"rotate\", \"params\": {\"degrees\": 角度,\"direction\": \"left|right\"}},\n {\"action\": \"navigate\", \"params\": {\"distance\": 米数}},\n {\"action\": \"interact\", \"params\": {\"type\": \"press\", \"target\": \"台灯开关\"}}\n ]\n}", "images": ["'$IMG'"], "format": "json", "stream": false, "options": {"temperature": 0.01, "num_predict": 300} }'
回答得不错,基本符合格式,实际使用最好使用解析器处理格式。

(2) LMDeploy
切换激活环境,执行 pip install --no-cache-dir lmdeploy 安装 LMDeploy 库。

编写测试脚本 test_offline.py。
from lmdeploy import pipeline, TurbomindEngineConfig, PytorchEngineConfig, GenerationConfig
from lmdeploy.vl import load_image
import time
MODEL_PATH = "/workspace/LlamaFactory/saves/Qwen3-VL-2B-Instruct/qlora/merge"
IMAGE_PATH = "/workspace/LlamaFactory/data/open_eqa_frames/0a0c0f2b9ba65d1b/000.jpg"
print("🚀 使用 LMDeploy PyTorch 后端加载 Qwen3-VL...")
# ⚠️ T4 必须用 PyTorch 后端(TurboMind 不支持 Qwen3-VL)
# T4 只有 16GB,限制并发和序列长度
engine_config = PytorchEngineConfig(
tp=1, # 单卡
session_len=4096, # 最大序列长度(T4 显存限制)
max_batch_size=4, # 最大批处理大小
cache_max_entry_count=0.6, # KV Cache 占用显存比例(T4 建议 0.5-0.6)
eager_mode=True, # T4 必须禁用 CUDA Graph
)
if __name__ == '__main__':
#freeze_support() # 创建 pipeline(会自动检测无 Flash Attn,fallback 到 native)
pipe = pipeline(MODEL_PATH, backend_config=engine_config)
print("✅ 模型加载成功!")
# 加载图片
image = load_image(IMAGE_PATH)
# 测试单张图片
print("\n🎯 单图推理测试...")
prompts = [
("描述这张图片", image),
]
start = time.time()
# 使用 GenerationConfig 对象而非 dict
response = pipe(prompts, gen_config=GenerationConfig(max_new_tokens=256, temperature=0.7))
latency = time.time() - start
print(f"⏱️ 延迟:{latency:.2f} s")
print(f"📝 输出:{response[].text}")
()
prompts_batch = [
(, image),
(, image),
(, image),
(, image),
]
start = time.time()
responses = pipe(prompts_batch, gen_config=GenerationConfig(max_new_tokens=))
batch_latency = time.time() - start
()
()
(.( / (batch_latency / latency)))

结果分析:
a. 功能层面:完美打通,输出质量优秀
- 单图推理能精准描述图片细节(深蓝色沙发、条纹扶手椅、装饰画文字
Live, Travel, Explore、绿植 / 百叶窗等),无遗漏关键视觉信息; - Batch4 个不同问题的推理均能正确响应,模型对视觉问答的语义理解符合预期,多模态能力完全正常发挥;
- 全程无报错、无 OOM(显存溢出),说明 LMDeploy 对 Qwen3-VL 的 PyTorch 后端适配完善,图片加载 / 提示构造 / 推理流程全链路通畅。
b. 单图推理性能:符合 T4+PyTorch 后端的预期
单图延迟 9.63s,这个数值在当前约束下是合理且可接受的:多模态推理的耗时主要来自视觉特征提取(Qwen3-VL 的视觉分支需要处理图片张量)+ 文本生成,而 T4 算力有限、PyTorch 后端无 TurboMind 的极致优化,2B 模型的这个延迟是中端硬件的正常表现。
c. Batch 批处理:机制生效,实现正向加速
4 个请求的 Batch 测试核心数据:
- 总延迟:31.42s → 平均单请求延迟 7.86s(比单图的 9.63s 降低 18.4%);
- 吞吐量提升:1.2x,验证了 LMDeploy
continuous batching的价值。
执行命令部署后台服务,
nohup lmdeploy serve api_server /workspace/LlamaFactory/saves/Qwen3-VL-2B-Instruct/qlora/merge --model-name qwen3-vl --backend pytorch --tp 1 --session-len 4096 --cache-max-entry-count 0.6 --max-batch-size 4 --eager-mode --server-port 23333 > api_server.log 2>&1 &
关键参数解析
| 参数 | 作用 | T4 约束 |
|---|---|---|
--backend pytorch | 使用 PyTorch 后端推理 | 必须:TurboMind(C++) 不支持 Qwen3-VL 架构,且 T4 是 SM75 架构 |
--tp 1 | 张量并行数 | T4 只有 1 张卡,设为 1(多卡可加速但 T4 不支持 NVLink 高效通信) |
--session-len 4096 | 最大序列长度 | 受限于 16GB 显存,4096 是安全值(过长会 OOM) |
--cache-max-entry-count 0.6 | KV Cache 显存占比 | 核心优化:0.6×16GB=9.6GB 给 KV Cache,剩余给模型权重 (4-5GB) 和激活值 |
--max-batch-size 4 | 最大 batch size | Continuous Batching 并发上限,T4 建议 4-8,过高会延迟增加 |
--eager-mode | 禁用 CUDA Graph 编译 | 必须:T4 架构较旧,CUDA Graph 可能导致非法指令或内存错误 |
--server-port 23333 | API 端口 | 默认与 OpenAI API(8080) 区分避免冲突 |
nohup 与重定向解析
| 符号 | 含义 |
|---|---|
nohup | No Hang Up,用户退出 SSH 后进程继续运行 |
> api_server.log | 标准输出 (STDOUT) 重定向到日志文件 |
2>&1 | 标准错误 (STDERR) 重定向到 STDOUT(即也进日志) |
& | 后台运行(立即返回命令行,不阻塞) |
用 curl 命令请求测试
BASE64_IMG=$(base64 -w 0 /workspace/LlamaFactory/data/open_eqa_frames/0a0c0f2b9ba65d1b/000.jpg)
curl -X POST http://localhost:23333/v1/chat/completions \
-H "Content-Type: application/json" \
-d "{ \"model\": \"qwen3-vl\", \"messages\": [{ \"role\": \"user\", \"content\": [{\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/jpeg;base64,${BASE64_IMG}\"}}, {\"type\": \"text\", \"text\": \"描述这张图片\"}] }], \"max_tokens\": 256, \"temperature\": 0.7 }"

执行 tail -f api_server.log 查看日志。

执行 ps aux | grep "lmdeploy serve api_server" 查看后台进程 pid。

这说明服务正在运行,有两个进程显示是因为:
| PID | 进程 | 说明 |
|---|---|---|
| 12684 | /root/miniforge3/bin/lmdeploy serve api_server ... | 真正的 LMDeploy 服务(占 1.9GB 内存) |
| 13135 | grep --color=auto lmdeploy serve api_server | 刚执行的 grep 命令本身(临时进程,已结束) |
执行 kill 12684 杀死服务,注意 pid 以实际为准。



