通过知识蒸馏将Qwen2-VL从7B压缩至2B用于UI截图识别,实现2.4倍加速并在多项指标上超越教师模型,完整展示了端到端的模型优化工程。
将 Qwen2-VL 教师模型蒸馏为一个紧凑的学生模型,用于截图理解(简短的 UI 描述 + 关键元素列表)。
该项目不仅衡量任务质量,还衡量延迟 / 吞吐量 / 内存占用——核心问题是用多少质量换取多少速度提升。详见 CONTRIBUTING.md 了解工作约定。
状态:第 1-8 阶段完成。蒸馏(第 4 阶段)通过 hf 后端在 CUDA 或 Apple MPS 上训练(已在 M4 Pro 上验证:损失下降,检查点可重新加载);评估(第 5 阶段)+ 消融研究(第 7 阶段)比较学生模型 vs 基线;基准测试(第 6 阶段)显示 2B 学生模型比 7B 教师模型快约 2.4 倍、轻约 2.4 倍;导出(第 8 阶段)将 LoRA 合并为独立的学生模型,文章草稿位于 article/draft.md。mlx 训练后端因上游 mlx-vlm 差距被阻塞(推理有效)。
可在 configs/student.yaml 中配置备用学生架构(冻结的 SigLIP 视觉编码器 + Qwen2.5-0.5B 解码器)。
Screen2Words(rootsautomation/RICO-Screen2Words, CC-BY-4.0):来自 RICO 的 Android UI 截图的人类编写摘要。我们使用原生的训练 / 验证 / 测试分割,并将每个示例规范化为统一的 {image, prompt, target, references} 记录(configs/data.yaml,prompt 模板是单一事实来源)。
uv sync --extra data
uv run vlm-build-dataset # full dataset -> data/processed/
uv run vlm-build-dataset --limit 200 # quick subset
uv run vlm-build-dataset --dry-run # tiny synthetic, validates the graph
以下统计数据由 vlm-build-dataset 重新生成并写入 results/dataset_stats.json:
分割:test 4,310,train 15,743,val 2,364
目标长度(单词数):最小 2 / 中位数 7 / 平均值 6.8 / 90 分位数 9 / 最大值 11
每个示例的参考文本数:中位数 5,最大值 5
需要 uv 和 Python 3.11。
# Dev environment (matches CI: lint + type + test, no heavy ML deps)
uv sync
uv run pytest
uv run ruff check . && uv run mypy src
# Data stack only (Phase 2): datasets/pillow/pyarrow, no torch.
uv sync --extra data
# Apple Silicon inference (Phase 3+): MLX backend for the teacher/student.
uv sync --extra mlx
# Heavy CUDA stack (transformers + bitsandbytes 4-bit). Needs a GPU box.
uv sync --extra ml
复制 .env.example 到 .env 用于 W&B / Hugging Face tokens。
download → build_dataset → teacher_label → train → eval → benchmark → export
每个步骤在 scripts/ 中都有一个薄包装的 CLI 和一个 console 入口点:
uv run vlm-download # Phase 2 — Screen2Words + RICO
uv run vlm-build-dataset # Phase 2 — unified {image, prompt, target} + split
uv run vlm-teacher-label # Phase 3 — teacher targets (+ optional top-k logits)
uv run vlm-train # Phase 4 — distillation (LoRA + accelerate), --dry-run for CPU
uv run vlm-eval # Phase 5 — ROUGE-L / BLEU + optional LLM-as-judge
uv run vlm-benchmark # Phase 6 — latency / throughput / peak memory
uv run vlm-export # Phase 8 — merge LoRA -> standalone student (ONNX = stretch)
教师模型(Qwen2-VL-7B,4-bit)为蒸馏目标生成训练分割。两个可互换的后端(在 configs/teacher.yaml 中设置 backend):
uv run vlm-teacher-label --limit 200 # label first 200 train screens
uv run vlm-teacher-label # full train split
uv run vlm-teacher-label --dry-run # no model, no network (CI smoke)
输出由配置哈希值版本化,可恢复 / 幂等——重新运行会跳过已标注的 id,所以中断的运行只需继续:
results/teacher_labels/<config_hash>/
train.jsonl # {id, prompt, teacher_target, valid, words, model, source}
manifest.json # config hash, backend, model, counts, timing
教师输出进行轻量级后验证(空白符规范化;空的 / 过短的输出被标记为 valid: false),以减轻格式漂移。
实际运行——在 Apple M4 Pro(24GB)上通过 MLX 运行 Qwen2-VL-7B-Instruct-4bit,max_new_tokens=128,200 张训练截图:
吞吐量:~10.2 秒/截图(≈34 分钟用于 200 张;≈2.7 小时预计用于全部 15.7k 训练分割)
质量:0/200 被标记为无效;平均目标长度 33.6 个单词(5–77)
目标遵循所请求的格式(一句摘要 + 关键元素列表)
示例目标:"该 UI 截图显示了一个健身应用,显示了一个名为"弓步"的练习,进度指示器显示 30% 完成。关键界面元素包括进度条、执行练习的人物图像和文本"弓步"。"
基于响应 / 序列级别的知识蒸馏:学生模型(Qwen2-VL-2B + LoRA)经过训练以再现教师的文本目标。vlm-train 首先将教师标签与其截图合并为 {image, question, answer} 记录,然后运行 LoRA SFT。
uv run vlm-train --dry-run # no model/deps — validates the graph (CI)
uv run vlm-train --limit 200 # train on the first 200 labeled screens
uv run vlm-train # full labeled train split
后端(在 configs/student.yaml 中设置 backend):
每个截图的视觉 token 数被限制(student.yaml max_pixels),以便大型 RICO 截图适应训练上下文。LoRA 适配器写在 results/checkpoints/<config_hash>/ 下(peft 格式)。
在 Apple M4 Pro(24GB)上进行的概念验证运行,MPS,hf 后端,40 步:训练损失 0.80 → 0.39;重新加载的适配器以训练格式生成(一句摘要 + 关键元素列表)。在 macOS 上设置 PYTORCH_ENABLE_MPS_FALLBACK=1。完整蒸馏运行是在完整标注的分割上执行相同的命令(最好在 24GB GPU 上)。
针对 Screen2Words 测试分割与人类参考标题的任务质量(ROUGE-L / BLEU;可选的教师 LLM-as-judge)。比较蒸馏后的学生、未训练的基线(相同基础,无适配器)和可选的教师。
uv run vlm-eval --dry-run # synthetic, no models (CI)
uv run vlm-eval --models student,baseline --adapter <dir> --limit 100
uv run vlm-eval --models student,baseline,teacher --judge --limit 50
报告写入 results/eval.json;下表自动填充。
相同硬件上的推理成本:每图像延迟(p50/p95)、吞吐量、峰值内存和参数比。标题级权衡是 2B 学生模型在多快 / 多轻的情况下,相比 7B 教师模型的质量损失有多少。
uv run vlm-benchmark --dry-run # synthetic timings (CI)
uv run vlm-benchmark --models teacher,student --iters 10
在 Apple Silicon 上"峰值内存"是统一内存 RSS,而不是 CUDA VRAM。为了获得公平的相同运行时间加速,我们在 MLX 上对两个模型进行基准测试。
学生 vs 教师:2.36 倍更快(p50),参数减少 3.75 倍。
权衡结论(M4 Pro,MLX,4-bit,128 tokens):2B 学生模型以约 2.4 倍的速度运行教师模型(1.52 vs 0.63 img/s),内存占用减少 2.4 倍(2.4 vs 5.8 GB),参数减少 3.75 倍——同时仍然在 ROUGE-L 上击败未训练的基线(第 5 阶段)。质量 vs 速度曲线将通过完整的训练运行而收紧;诚实的读法是蒸馏已经以适度的、可测量的质量成本获得了真实的效率收益。
什么实际上会影响质量?我们在固定的 lr / batch / data 下改变序列级别 SFT 所暴露的轴——训练步数和 LoRA rank——并在测试分割上评估每一个:
PYTORCH_ENABLE_MPS_FALLBACK=1 uv run python scripts/run_ablations.py
什么影响了质量(PoC 规模,16 个测试屏幕——作为趋势阅读,而不是最终结果):
完全进行训练是最大的杠杆:基线 0.152 → 蒸馏 ~0.170 ROUGE-L(+12% 相对)。最大的单一跳跃是"未训练 → 蒸馏"。
更多步骤帮助边际化:40 → 80 步将 ROUGE-L 0.170 → 0.172 和 BLEU 0.018 → 0.020——n-gram(BLEU)增益更清晰,即更长的训练会锐化精确的措辞。
LoRA rank 在这里大致是中立的:40 步时的 r8 vs r16(0.170 vs 0.171)在噪音范围内——在这个数据规模上,适配器容量不是瓶颈,所以 r8 足够了。
要点:在小规模上,你蒸馏多少(步数 / 数据)比适配器容量更重要;α / temperature / feature 轴等待 logit-KD 路径和全规模运行。
方法部分中的 α / temperature / feature-alignment 轴适用于 logit 级别的 KD 变体(models.losses.response_kd_loss),SFT 路径还没有使用——一旦教师 logits 被缓存,这就是下一个消融轴。
将 LoRA 适配器合并到基础权重中,保存一个独立的学生模型(模型 + 处理器),可以用普通 transformers 在任何地方加载——推理时无需 peft。一个理智性生成确认合并的模型有效。
uv run vlm-export --adapter results/checkpoints/<hash> --output results/exports/student
uv run vlm-export --dry-run # manifest only, no model (CI)
完整的 VLM ONNX 导出很复杂,被保留为有文档的拉伸目标;torch / MLX 推理是规范路径。
端到端的文章草稿(动机 → 方法 → 实验 → 权衡 → 消融 → 推理工程 → 诚实的限制)已在 article/draft.md 中起草,准备好发布到 dev.to。
所有超参数和路径都位于 configs/*.yaml,由 src/vlm_distill/config.py 中的类型化 pydantic 模型验证:
teacher.yaml — 教师模型、4-bit、生成、视觉 token 预算
student.yaml — 学生模型、LoRA、备用架构
distill.yaml — α、temperature、损失标志(response / feature / synthetic)
data.yaml — 数据集、prompt 模板、确定性分割
源代码在 src/vlm_distill/ 下,薄 CLI 在 scripts/ 下,测试在 tests/ 下,运行输出在 results/ 下(gitignored)。
Apache-2.0。数据集和教师权重许可证在发布任何权重之前已在模型卡中追踪。