任务信息
题目:TASK 17「DeepEyes processor 动态 patch」
认领人:@yuki-younai
背景:examples/deepeyes/ 当前通过 cp 把整份 qwen_vl.py 覆盖进 SGLang 安装目录,依赖 /sgl-workspace 且易随上游升级失效。
目标:启动时用 monkey-patch 或 processor 注册机制,仅注入真实差异。
验收标准:删除所有覆盖安装目录的 cp 与 /sgl-workspace 硬编码;patch 仅覆盖必要方法、重复 import/调用幂等;不兼容 SGLang 版本时 fail fast 并- 提示;同一输入下 patch 前后关键输出一致;DeepEyes 端到端 smoke 通过;有单测和升级说明。
交付方式:Issue + PR。
关联PR:@Task.17】DeepEyes processor 动态 patch #188
任务
examples/deepeyes/ 当前通过 cp 把整份 qwen_vl.py 覆盖进 SGLang 安装目录,依赖 /sgl-workspace 且易随上游升级失效。改用启动时 monkey-patch,仅注入真实差异。
问题根因
run_deepeyes_r3.sh 执行:
cp examples/deepeyes/qwen_vl.py /sgl-workspace/sglang/python/sglang/srt/multimodal/processors/qwen_vl.py
把 454 行旧版 qwen_vl.py 整文件覆盖到安装目录。问题:
硬编码 /sgl-workspace,SGLang 升级即失效;
整文件覆盖回退 Docker 构建时的 sglang.patch(v0.5.12 把 load_mm_data 切到 legacy_load_mm_data),引入隐蔽不一致;
真实差异淹没在 400+ 行样板里,维护成本高。
真实差异
DeepEyes 多轮 rollout 发送预分词 input_ids (每图 N 个 <|image_pad|>),原生 QwenVLImageProcessor.process_mm_data_async 处理不了:
A. token 漂移 :decode→retokenize 非无损,token 数变化,错位 mrope。
B. N×M 爆炸 :N 个 <|image_pad|> 被当 N 个图片占位符,每个再展开成 M 个 patch token。
真正定制只有两处:
_strip_image_token:load_mm_data 前折叠连续 <|image_pad|>;
original_input_ids 保存/恢复:恢复原始 token 序列并据其重算 mrope。
方案
沿用 Relax 已有的「SGLang 启动时 monkey-patch」机制(与 OPD patch 模式一致),env flag 控制,默认关闭:
--deepeyes-qwen-vl-patch → RELAX_DEEPEYES_QWEN_VL_PATCH=1 → SGLang 子进程启动时 apply
仅替换 process_mm_data_async + 新增 _strip_image_token,其余从上游导入;
幂等(_PATCH_FLAG);不兼容版本 fail fast(_verify_upstream_api 提示 sglang 0.5.12.post1);
文本输入无副作用;返回值版本兼容(MultimodalProcessorOutput.from_dict / dict 回落);
重写方法体而非纯 wrapper,因 mrope 必须用恢复后的 ids 重算。
验收对照
验收标准
状态
删除 cp / /sgl-workspace 硬编码
✅
patch 仅覆盖必要方法、幂等
✅
不兼容 SGLang 版本 fail fast
✅
同一输入前后关键输出一致
✅ verify_patch_consistency.py
DeepEyes e2e smoke 通过
✅ 多轮 rollout→train step
有单测和升级说明
✅ 15 单测 + 双语文档
测试结果
单测:pytest tests/backends/sglang/test_qwen_vl_patch.py → 15 passed
真实 sglang 0.5.12.post1:_verify_upstream_api() PASS,apply_qwen_vl_patches() → True
前后一致性:verify_patch_consistency.py → ALL PASS (文本输入 no-op;预分词输入只改 input_ids+mrope)
e2e smoke:DEEPEYES_PATCH=1 跑 Qwen3-VL-4B GRPO,完成 step 0/1/2,0 Traceback / 0 ERROR
实验环境
项目
配置
镜像 / OS / Python
ghcr.io/redai-infra/relaxrl:latest / Ubuntu 24.04 / 3.12.3
SGLang
0.5.12.post1(patch 目标版本)
GPU
4 × NVIDIA H20
模型 / 算法
Qwen3-VL-4B-Instruct / GRPO
训练模式 / 并行
Colocate / TP=2, DP=2
批量 / 序列
rollout-batch 32, global-batch 256, n-samples 8 / prompt+response 各 2048
数据
datasets/deepeyes-v1/(两个 parquet 各 @[0:5000])
Judge
远程 OpenAI 兼容 endpoint(openai_judge_service.sh)
patch 启用
DEEPEYES_PATCH=1 bash examples/deepeyes/run_deepeyes_baseline.sh
升级说明
见 docs/{en,zh}/guide/sglang-patches.md(SGLang 升级 checklist)。
任务信息
任务
examples/deepeyes/当前通过cp把整份qwen_vl.py覆盖进 SGLang 安装目录,依赖/sgl-workspace且易随上游升级失效。改用启动时 monkey-patch,仅注入真实差异。问题根因
run_deepeyes_r3.sh执行:把 454 行旧版
qwen_vl.py整文件覆盖到安装目录。问题:/sgl-workspace,SGLang 升级即失效;sglang.patch(v0.5.12 把load_mm_data切到legacy_load_mm_data),引入隐蔽不一致;真实差异
DeepEyes 多轮 rollout 发送预分词
input_ids(每图 N 个<|image_pad|>),原生QwenVLImageProcessor.process_mm_data_async处理不了:<|image_pad|>被当 N 个图片占位符,每个再展开成 M 个 patch token。真正定制只有两处:
_strip_image_token:load_mm_data前折叠连续<|image_pad|>;original_input_ids保存/恢复:恢复原始 token 序列并据其重算 mrope。方案
沿用 Relax 已有的「SGLang 启动时 monkey-patch」机制(与 OPD patch 模式一致),env flag 控制,默认关闭:
process_mm_data_async+ 新增_strip_image_token,其余从上游导入;_PATCH_FLAG);不兼容版本 fail fast(_verify_upstream_api提示 sglang 0.5.12.post1);MultimodalProcessorOutput.from_dict/ dict 回落);验收对照
cp//sgl-workspace硬编码verify_patch_consistency.py测试结果
pytest tests/backends/sglang/test_qwen_vl_patch.py→ 15 passed_verify_upstream_api()PASS,apply_qwen_vl_patches()→ Trueverify_patch_consistency.py→ ALL PASS(文本输入 no-op;预分词输入只改 input_ids+mrope)DEEPEYES_PATCH=1跑 Qwen3-VL-4B GRPO,完成 step 0/1/2,0 Traceback / 0 ERROR实验环境
ghcr.io/redai-infra/relaxrl:latest/ Ubuntu 24.04 / 3.12.3datasets/deepeyes-v1/(两个 parquet 各 @[0:5000])openai_judge_service.sh)DEEPEYES_PATCH=1 bash examples/deepeyes/run_deepeyes_baseline.sh升级说明
见
docs/{en,zh}/guide/sglang-patches.md(SGLang 升级 checklist)。