Repository navigation
Conversation
| # 模态位置和视频字段随视觉输入一起保留;纯文本 teacher 仍丢弃这些字段。 | ||
| pixel_values_videos=seq_ctx.pixel_values_videos if is_composed else None, | ||
| video_grid_thw=seq_ctx.video_grid_thw if is_composed else None, | ||
| mm_token_type_ids=seq_ctx.mm_token_type_ids if is_composed else None, |
There was a problem hiding this comment.
这三行本身是对的,但白名单机制才是这次 bug 的成因,而它还留着,第三次照样会漏。
construct_teacher_seq_ctx 逐字段点名转发,每加一个视觉字段就有一次“忘了加”的机会:第一次漏了 mm_token_type_ids(之于 pixel_values),这次漏了视频两个字段。两个字段必须成组出现,而白名单天然会让它们脱钩。
SequenceContext.copy()(sequence_context.py:555-596)已经把全部字段转发过去。我实测确认:copy() 保留 mm_token_type_ids / pixel_values / cu_seq_lens_q_cpu,返回新对象,配合 overrides 可清空学生缓存与路由元数据。把白名单翻成黑名单——只声明 Teacher 不该有的字段,新增字段默认跟随 Student:
@staticmethod
def construct_teacher_seq_ctx(seq_ctx: SequenceContext, *, is_composed: bool) -> SequenceContext:
# 黑名单:只声明 Teacher 不该持有的字段。新增视觉字段默认跟随 Student,
# 不会像白名单那样漏掉(mm_token_type_ids、pixel_values_videos 都是这么漏的)。
drop_visual = {} if is_composed else {
"pixel_values": None,
"image_grid_thw": None,
"pixel_values_videos": None,
"video_grid_thw": None,
"mm_token_type_ids": None,
"num_img_tokens": None,
}
position_ids = seq_ctx.position_ids
if not is_composed and position_ids is not None and position_ids.ndim == 3:
position_ids = None # SequenceContext 会重建 2D packed positions
return seq_ctx.copy(
position_ids=position_ids,
deepstack_visual_embeds=None, # Teacher 自己重算视觉特征
visual_pos_masks=None,
inputs_embeds=None,
rollout_routed_experts=None, # rollout 专属,Teacher 不得复用
offload_rollout_routed_experts=False,
raw_inputs_embeds=None,
**drop_visual,
)我已把 tests/rl/test_train_teacher_context.py 的 8 个用例跑在这份实现上:8 passed,行为与当前写法等价。顺带修掉下面那条 cu_seq_lens_q_cpu 的问题,因为 copy() 会转发它。
| deepstack_visual_embeds=None, | ||
| visual_pos_masks=None, | ||
| pixel_values=seq_ctx.pixel_values if is_composed else None, | ||
| # 模态位置和视频字段随视觉输入一起保留;纯文本 teacher 仍丢弃这些字段。 |
There was a problem hiding this comment.
这块转发清单还漏了一个字段:cu_seq_lens_q_cpu(对照上面 244-248 行的 SequenceContext(...) 调用)。
Worker 里 seq_ctx.to(DEVICE) 之后 cu_seq_lens_q/k 已在 GPU,而构造函数只在 cu_seq_lens_q 仍是 CPU 时才把镜像留下(sequence_context.py:109)。所以重建出的 teacher ctx 会丢掉 CPU 镜像。实测:
student.cu_seq_lens_q_cpu : tensor([0, 6], dtype=torch.int32)
teacher.cu_seq_lens_q_cpu : None
后果是在 Teacher 前向的 KDA 层里,prepare_chunk_indices(ops/gated_deltanet/chunk_gated_delta_rule.py:42-43)拿不到 CPU 偏移,退回到 torch.diff(cu_seqlens) 再 .tolist() 读 GPU 标量——每个 KDA 层一次 host sync。GLM53 是 45 层 KDA/DSA 交替,相当于把主分支刚优化掉的 host sync(perf(glm53): avoid KPool host sync 系列)又带回来了,只是发生在 Teacher 侧。正确性不受影响,但 PR 描述里 “preserves existing tensor references and adds no tensor copies” 这句在这个字段上不成立。
最小修法是补一行;若采纳上一条 copy() 的写法,则自动解决:
# 最小改动
return SequenceContext(
input_ids=seq_ctx.input_ids,
cu_seq_lens_q=seq_ctx.cu_seq_lens_q,
cu_seq_lens_k=seq_ctx.cu_seq_lens_k,
cu_seq_lens_q_cpu=seq_ctx.cu_seq_lens_q_cpu, # 保留 CPU 偏移,避免 Teacher 前向重新 host sync
...
)我按这个改法验证过:teacher.cu_seq_lens_q_cpu == tensor([0, 6]),8 个用例仍全绿。建议同时补一行断言 assert teacher.cu_seq_lens_q_cpu is student.cu_seq_lens_q_cpu。
| visual_pos_masks=None, | ||
| pixel_values=seq_ctx.pixel_values if is_composed else None, | ||
| # 模态位置和视频字段随视觉输入一起保留;纯文本 teacher 仍丢弃这些字段。 | ||
| pixel_values_videos=seq_ctx.pixel_values_videos if is_composed else None, |
There was a problem hiding this comment.
建议同关注:这三行确实补齐了重建侧契约,但它修不好端到端的 GLM53 image teacher OPD——上游从来没有生产 mm_token_type_ids,所以现在转发的是 None,整个改动在真实数据上是 no-op。
实测(用 PR 自己的分支,student 走真实数据构造路径):
from xtuner.v1.train.rl_trainer import get_train_seq_ctx
student = get_train_seq_ctx(torch.arange(6).view(1, -1), multimodal_train_info={
"pixel_values": np.zeros((4, 3), dtype=np.float32),
"image_grid_thw": np.array([[1, 2, 2]]),
"num_img_tokens": [1],
})
teacher = TrainTeacherManager.construct_teacher_seq_ctx(student, is_composed=True)
student.mm_token_type_ids : None
teacher.mm_token_type_ids : None # 转发的是 None -> no-op
teacher.pixel_values : (4, 3) # 像素有值、位置没有 -> _prepare_llm_inputs 断言仍会炸
根因链有四处,缺一不可:
MultimodalInfo(data_proto/rl_data.py:61-65)只有pixel_values/image_grid_thw/num_img_tokens三个键 —— 没有mm_token_type_ids,也没有视频三件套;rl_tokenize_fn/__init__.py只导出 Text 与 Qwen3VL 两个 tokenize fn,GLM53 在 RL 侧尚未接入(SFT 侧Glm53VLTokenizeFnConfig已经产出mm_token_type_ids/pixel_values_videos/video_grid_thw);get_train_seq_ctx(train/rl_trainer.py:308-313)只填上述三个字段;worker.py:681-692的 ObjectRef 解析只处理seq_ctx.pixel_values,视频张量没人解析。
要真正跑通 GLM53 image teacher OPD,建议按这条链补齐(可另开 PR,但本 PR 描述应收窄为“补齐重建契约”):
# 1) data_proto/rl_data.py::MultimodalInfo —— 补齐 GLM53 需要的键
class MultimodalInfo(TypedDict):
pixel_values: NotRequired[np.ndarray | RayObjectRef | None]
image_grid_thw: NotRequired[np.ndarray | None]
num_img_tokens: NotRequired[list[int]]
pixel_values_videos: NotRequired[np.ndarray | RayObjectRef | None]
video_grid_thw: NotRequired[np.ndarray | None]
mm_token_type_ids: NotRequired[np.ndarray | None] # 0=text, 1=image, 2=video
# 2) 新增 datasets/rl_tokenize_fn/glm53_vl_tokenize_fn.py::RLGlm53VLTokenizeFunction
class RLGlm53VLTokenizeFunction(Glm53VLTokenizeFunction):
def __call__(self, item, media_root="", **kwargs) -> RolloutState | CacheItem:
state = super().__call__(item, media_root=media_root, **kwargs) # 复用 SFT 侧展开/切分/mm id 逻辑
mm_info = MultimodalInfo()
for key in ("pixel_values", "image_grid_thw", "num_img_tokens",
"pixel_values_videos", "video_grid_thw", "mm_token_type_ids"):
if key in state.mm_info:
mm_info[key] = _tensor_to_numpy(state.mm_info[key])
state.mm_info = mm_info
return state
# 3) train/rl_trainer.py::get_train_seq_ctx —— 逐字段填充
seq_ctx.mm_token_type_ids = _to_cpu_tensor(multimodal_train_info.get("mm_token_type_ids"), dtype=torch.long)
seq_ctx.pixel_values_videos = multimodal_train_info.get("pixel_values_videos")
seq_ctx.video_grid_thw = _to_cpu_tensor(multimodal_train_info.get("video_grid_thw"), dtype=torch.long)
# 4) rl/trainer/worker.py —— ObjectRef 解析覆盖视频(把 681-692 的 pixel_values 分支抽成小函数按字段复用)
seq_ctx.pixel_values = _resolve_media(seq_ctx.pixel_values)
seq_ctx.pixel_values_videos = _resolve_media(seq_ctx.pixel_values_videos)另外顺带说明:put_to_ray(agent_loop_manager/sampler.py:66-70)目前只把 pixel_values 放进共享内存,视频字段补进来后也应走同样处理,否则跨进程 IPC 会退化成 pickle 大张量。
|
审查结论:通过,0 个必修项。 提交: 本次中文注释明确共同目的和实现差异:
仅修改注释,可执行 token 与 docstring 均与 Review #2231 — GLM53 图文/视频 OPD 训练回放链路(main → 57a9f97,15 文件 +610/-41)日期:2026-10-29。审阅范围:main → Summary本 PR 为 GLM53 VLM 打通「tokenizer → mm_info schema → sampler Ray 存储 → get_train_seq_ctx → worker 解析/packing → 训练侧 Teacher」的图文 OPD 训练回放链路,并新增视频像素字段的训练侧传输( ProduceBatchResult impact: 不改动 status/reward/timing 任何字段;在线视频样本从「静默条件错位」变为在 验证(既有功能版本的 CPU 实跑;本次仅补注释,未重复运行):
REQUIRED_FIXES: 0 — Verdict: APPROVE(Warning/Nit 见对应章节,均不阻塞)。 Main Flowchart before this PRgraph TD
subgraph RLGlm53["xtuner.v1.datasets.rl_tokenize_fn (before)"]
A1["RLTextTokenizeFnConfig / RLQwen3VLTokenizeFnConfig<br/>(无 GLM53 RL tokenize fn)"]
end
subgraph Sampler["xtuner.v1.rl.agent_loop_manager.sampler"]
B1["put_to_ray(data: RolloutState)<br/>仅 ray.put mm_info['pixel_values']"]
end
subgraph RLTrainer["xtuner.v1.train.rl_trainer"]
C1["get_train_seq_ctx(input_ids, position_ids,<br/>multimodal_train_info, len_response_ids)<br/>仅转发 pixel_values / image_grid_thw / num_img_tokens"]
end
subgraph TWorker["xtuner.v1.rl.trainer.worker.TrainingWorker"]
D1["fit 内联解析 pixel_values:<br/>list[ObjectRef] -> ray.get -> cat;<br/>ndarray 抛 NotImplementedError"]
end
subgraph Controller["xtuner.v1.rl.trainer.controller.TrainingController"]
E1["fit finally: 仅释放 seq_ctx.pixel_values 的 refs"]
end
subgraph Teacher["xtuner.v1.rl.distillation.train_teacher_manager.TrainTeacherManager"]
F1["construct_teacher_seq_ctx:<br/>手写 SequenceContext(...) 全字段列表<br/>is_composed 分支决定视觉字段去留"]
end
subgraph RolloutW["xtuner.v1.rl.rollout.worker.RolloutWorker"]
G1["_prepare_request_payload<br/>不检查视频输入"]
end
subgraph RTeacher["RolloutTeacherClient"]
H1["compute_logprobs<br/>不检查视频输入"]
end
A1 --> B1 --> C1 --> D1 --> F1
C1 --> E1
G1 --> H1
Main Flowchart after this PRgraph TD
subgraph RLGlm53["xtuner.v1.datasets.rl_tokenize_fn.glm53_vl_tokenize_fn"]
A2["RLGlm53VLTokenizeFunction(Glm53VLTokenizeFunction)<br/>__call__ -> RolloutState<br/>_multimodal_info / hash / _truncated_input_and_labels"]
A3["RLGlm53VLTokenizeFnConfig.build()"]
end
subgraph Proto["xtuner.v1.data_proto.rl_data"]
P1["MultimodalInfo + pixel_values_videos /<br/>video_grid_thw / mm_token_type_ids<br/>MM_PIXEL_FIELDS: tuple[MMPixelField, ...]"]
end
subgraph Sampler["sampler"]
B2["put_to_ray: 遍历 MM_PIXEL_FIELDS,<br/>幂等 ray.put(已是 ObjectRef 则跳过)"]
end
subgraph RLTrainer["rl_trainer"]
C2["get_train_seq_ctx: +pixel_values_videos /<br/>video_grid_thw;mm_token_type_ids 形状校验<br/>(1, prompt_len) 后 response 段补 0"]
end
subgraph TWorker["trainer.worker"]
D2["_resolve_multimodal_inputs(seq_ctx):<br/>MM_PIXEL_FIELDS 统一解析;Tensor 跳过,<br/>非 list[ObjectRef] 断言拒绝"]
end
subgraph Controller["trainer.controller"]
E2["_free_multimodal_inputs(contexts):<br/>统一释放两个像素字段的 refs"]
end
subgraph Teacher["train_teacher_manager"]
F2["construct_teacher_seq_ctx:<br/>seq_ctx.copy(position_ids=..., **drop_visual)<br/>保留 CPU offsets / SP shard 元数据"]
end
subgraph Guards["rl.utils.misc"]
V1["validate_image_only_rollout(state):<br/>video_data 或 pixel_values_videos -> NotImplementedError"]
end
A3 --> A2 --> P1
P1 --> B2 --> C2 --> D2 --> F2
C2 --> E2
V1 --> G2["_prepare_request_payload 入口拒绝视频"]
V1 --> H2["compute_logprobs 入口拒绝视频"]
style A2 fill:#f9f,stroke:#333
style A3 fill:#f9f,stroke:#333
style P1 fill:#f9f,stroke:#333
style B2 fill:#f9f,stroke:#333
style C2 fill:#f9f,stroke:#333
style D2 fill:#f9f,stroke:#333
style E2 fill:#f9f,stroke:#333
style F2 fill:#f9f,stroke:#333
style V1 fill:#f9f,stroke:#333
说明(流程 ↔ 修改代码对应):
核心原理
核心实现与对应的单测
抽象与信息隐藏评估按 codebase-design 术语评估:
新增功能守则
公开接口的线性业务流程评估
单测建议现有覆盖已含:真实 processor 三模态、ref 幂等/共享/释放、形状校验 4 参数化、teacher 保留/丢弃矩阵(media 4 × composed 2)、meta 设备 CPU offsets、SP shard 元数据、在线视频拒绝。可选增强(不阻塞):
其他 issuesWarning
Nit
验证记录:本轮 29 项 CPU 测试通过;提交后已核对 15 个文件与审阅快照一致。收缩版未重跑 GPU 回放。 |
Problem
GLM53 RL lacked a tokenizer producing the expanded training prompt and modality IDs required for visual splicing. Training context construction omitted modality/video fields, and Teacher reconstruction lost those fields and CPU sequence offsets.
Change
SequenceContext.copy(), preserving raw visual inputs and CPU offsets while clearing Student embeddings, derived visual caches and rollout routing state. Existing SP metadata forwarding is retained.Media handling stays in the existing sampler/worker/controller modules. Modality-length validation and its negative tests remain.
Validation
xtuner/v1has the same 629 diagnostics as the retained clean-main baseline, with no added/removed messages after normalizing line numbers; this is not a globally green type check.df840f6and does not certify the reduced source. Full online rollout, full-size weights, distributed/SP accuracy and convergence remain outside the validation scope.Review:
APPROVE / REQUIRED_FIXES=0. The complete technical review is in the PR comment. Reviewed commit:57a9f979e309772c38e9824e371ec42925bbaeb8; committed bytes match the review snapshot.Chinese comments distinguish shared goals from GLM-specific implementations for dual prompts, media metadata, configuration forwarding, cache keys and length handling. This update changes comments only: executable tokens and docstrings match
9cf8470; comment review and style checks passed without rerunning training.