Skip to content

[Fix] Connect GLM53 multimodal inputs to training-side OPD - #2231

Open
xs1997zju wants to merge 7 commits into
InternLM:mainfrom
xs1997zju:fix/teacher-visual-context-20261028
Open

xs1997zju wants to merge 7 commits into
InternLM:mainfrom
xs1997zju:fix/teacher-visual-context-20261028

Conversation

@xs1997zju

@xs1997zju xs1997zju commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Reuse the SFT GLM53 renderer/processor in an RL tokenizer, preserving raw rollout prompts and expanded training prompts. Filter overlong prompts as a whole.
  • Pass image/video metadata through the existing sampler, training-context, worker materialization and cleanup paths. Keep packed Ray-reference handling; remove the separate multimodal utility module and extra direct-array compatibility.
  • Build Teacher contexts with 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.
  • Reject unsupported online video requests before generation or rollout-Teacher scoring. Video support here covers training inputs/replay; a misconfigured single-turn online video request stops production before HTTP. Existing agentic exception handling is unchanged.

Media handling stays in the existing sampler/worker/controller modules. Modality-length validation and its negative tests remain.

Validation

  • Targeted processor/media/Teacher, training-data, scorer, packing and existing OPD/rollout regression checks passed. Some test runs overlap; counts are not summed.
  • Real local Ray object store and a separate CPU worker preserved processed image/video payloads and aligned token/modality shapes on the final reduced source.
  • Changed-file style hooks passed on an identical source copy. Mypy for xtuner/v1 has 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.
  • GPU replay was not rerun on this reduced revision because all GPUs were occupied at the last preflight. The earlier 5-layer, SP=1 image/video OPD smoke corresponds to df840f6 and 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.

# 模态位置和视频字段随视觉输入一起保留;纯文本 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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这三行本身是对的,但白名单机制才是这次 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 仍丢弃这些字段。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这块转发清单还漏了一个字段: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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议同关注:这三行确实补齐了重建侧契约,但它修不好端到端的 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 断言仍会炸

根因链有四处,缺一不可:

  1. MultimodalInfo(data_proto/rl_data.py:61-65)只有 pixel_values / image_grid_thw / num_img_tokens 三个键 —— 没有 mm_token_type_ids,也没有视频三件套;
  2. rl_tokenize_fn/__init__.py 只导出 Text 与 Qwen3VL 两个 tokenize fn,GLM53 在 RL 侧尚未接入(SFT 侧 Glm53VLTokenizeFnConfig 已经产出 mm_token_type_ids / pixel_values_videos / video_grid_thw);
  3. get_train_seq_ctx(train/rl_trainer.py:308-313)只填上述三个字段;
  4. 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 大张量。

@xs1997zju xs1997zju changed the title [Fix] Preserve modality and video inputs for composed teachers [Fix] Preserve teacher sequence context through shared copy Oct 28, 2026
@xs1997zju

xs1997zju commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

审查结论:通过,0 个必修项。

提交:57a9f979e309772c38e9824e371ec42925bbaeb8。

本次中文注释明确共同目的和实现差异:

部分 共同目的 实现区别
双 prompt 分别满足 rollout 占位符与训练展开 token 的输入要求 Qwen3VL 压缩连续图像 token;GLM 重渲染原始占位符文本再编码
媒体元信息 向训练链路传递 CPU NumPy 媒体数据 Qwen3VL 内联组装;GLM 用静态方法统一转换,并传递视频及模态标记
配置构建 将配置字段传给 tokenizer Qwen3VL 手工列参;GLM 收集 model_fields 并排除不适用字段
缓存标识 隔离 RL 与 SFT 产物 Qwen3VL 使用固定类名;GLM 使用 RL 前缀加父类 hash
长度限制 控制样本长度 SFT 可直接截断;GLM RL 按整条 prompt 过滤或拒绝,额外保证 rollout 与训练条件一致

仅修改注释,可执行 token 与 docstring 均与 9cf8470 一致。注释已对照真实实现审查,风格检查通过;本次未重复运行功能测试或训练。既有验证边界见下文,收缩版 GPU 未重跑。


Review #2231 — GLM53 图文/视频 OPD 训练回放链路(main → 57a9f97,15 文件 +610/-41)

日期:2026-10-29。审阅范围:main → 57a9f97,累计 15 文件。提交后已逐文件核对 SHA256,与最终审阅快照一致。

Summary

本 PR 为 GLM53 VLM 打通「tokenizer → mm_info schema → sampler Ray 存储 → get_train_seq_ctx → worker 解析/packing → 训练侧 Teacher」的图文 OPD 训练回放链路,并新增视频像素字段的训练侧传输(pixel_values_videos / video_grid_thw / mm_token_type_ids),同时在内置在线 rollout 与 rollout Teacher 入口显式拒绝视频(validate_image_only_rollout),不含在线视频 rollout。Teacher 重建从手写字段列表改为 SequenceContext.copy(**overrides) 公共契约,保留视觉输入、CPU offsets 与 SP shard 元数据。

ProduceBatchResult impact: 不改动 status/reward/timing 任何字段;在线视频样本从「静默条件错位」变为在 _prepare_request_payload/compute_logprobs 构造前显式 NotImplementedError 失败,图文与文本路径语义不变。
RoutedExperts impact: 不新增 routed-experts 所有权变化;construct_teacher_seq_ctx 继续 rollout_routed_experts=None(teacher 不持有 rollout 路由 refs)。controller 的释放逻辑从仅 pixel_values 扩到 MM_PIXEL_FIELDS 两个字段,减少了视频像素 refs 的残留(泄漏面收窄)。
Ray concurrency impact: not affected — 未触碰 Ray actor 方法、装饰器顺序、concurrency group 或 rollout 控制路径;put_to_ray/ray.get/free_object_refs 均为普通函数调用。

验证(既有功能版本的 CPU 实跑;本次仅补注释,未重复运行):

  • tests/rl/test_train_teacher_context.py + tests/rl/test_glm53_multimodal_inputs.py:25 passed。
  • tests/datasets/test_rl_glm53_vl_tokenize_fn.py(真实 GLM-5.3-Flash processor):4 passed。
  • 历史证据(只读核对):mypy-v1-delta.json 629 基线零新增、ray-transport-final.log / ray_transport_result.json 真实 Ray CPU worker 通过、precommit-fixed.log 全部变更文件 style hooks 通过。GPU 训练回放未在本收缩版重跑,结论不外推。

REQUIRED_FIXES: 0 — Verdict: APPROVE(Warning/Nit 见对应章节,均不阻塞)。

Main Flowchart before this PR

graph 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
Loading

Main Flowchart after this PR

graph 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
Loading

说明(流程 ↔ 修改代码对应):

  1. 生产:RLGlm53VLTokenizeFunction.__call__(glm53_vl_tokenize_fn.py)复用 SFT processor 产出展开 prompt + 像素 + grid + mm_token_type_ids,同时给 rollout 原始占位符 prompt_ids;视频路径只进训练回放。
  2. 传输:MultimodalInfo 增 3 个 NotRequired 字段;MM_PIXEL_FIELDS(Literal 元组)统一 sampler 的 put_to_ray、worker 的 _resolve_multimodal_inputs、controller 的 _free_multimodal_inputs 三处字段枚举。
  3. 训练接入:get_train_seq_ctx 校验 mm_token_type_ids.shape == (1, prompt_len)(input_ids.size(-1) - len_response_ids)后 response 段补 0;packing 时 SequenceContext.cat/_pack_mm_token_type_ids 为纯文本与 padding 段补 0([Fix] Preserve modality ID alignment in mixed sequence packs #2227 既有能力,本 PR 消费)。
  4. Teacher 重建:construct_teacher_seq_ctx 改用手写构造 → seq_ctx.copy(**overrides),非 composed 时以 drop_visual 清 6 个视觉字段;CPU offsets(cu_seq_lens_q_cpu)与 SP _shard_start/_shard_size/_raw_input_ids 由 copy 默认保留。
  5. 显式拒绝:validate_image_only_rollout 挂在 RolloutWorker._prepare_request_payload 与 RolloutTeacherClient.compute_logprobs 入口,视频样本在构造后端 payload/HTTP 前失败。

核心原理

  • 双 prompt 契约:rollout 引擎吃「每媒体一个原始占位符」的 prompt_ids;训练吃 HF 展开后的 train_prompt_ids。mm_token_type_ids(0=文本,1=图,2=视频)与展开 prompt 等长,response 输入位置是纯文本故补 0(末 token 已由调用方去尾),packing 的文本/padding 段由 cat 补 0,保证拼接后与 input_ids 同形同序。
  • 对象存储契约:sampler 将 ndarray 像素 ray.put 为 ObjectRef(幂等,二次调用不嵌套);packing 后字段是 list[ObjectRef](cat 对 list 不 cat);worker 统一 ray.get + torch.cat 物化;controller 在 ray.get(handles) 的 finally 中释放 refs。所有权:sampler 生产 → worker 消费 → controller 释放。
  • Teacher 复制契约:Teacher 必须重算视觉特征(清 deepstack_visual_embeds/visual_pos_masks/inputs_embeds/raw_inputs_embeds)、不得继承 rollout 路由缓存(rollout_routed_experts=None),但 composed Teacher 必须保留像素/grid/模态标记与 M-RoPE 3D position_ids;非 composed Teacher 丢视觉字段并回退 2D packed positions。copy(**overrides) 让「保留集」收敛到 SequenceContext 一处,消除手写字段列表的漏字段风险(本次修复 CPU offsets、模态标记和视频输入遗漏;base 已转发 shard 元数据,本次保持该行为)。

核心实现与对应的单测

核心实现 单测 说明
RLGlm53VLTokenizeFunction.__call__ / _multimodal_info / hash / _truncated_input_and_labels(glm53_vl_tokenize_fn.py) tests/datasets/test_rl_glm53_vl_tokenize_fn.py(真实 processor,text/image/video 3 参数化 + 长度过滤) 真实生产端到端:tokenize → _prepare_train_data → _packing → teacher 重建;断言模态标记最大值、grid 与 token 数一致、输入不被原地修改
put_to_ray MM_PIXEL_FIELDS 幂等 test_media_refs_are_shared_resolved_and_freed(fake object store + 真 ObjectRef) 二次 put 不产生嵌套 ref(len(store)==2)
_resolve_multimodal_inputs / _free_multimodal_inputs 同上 + test_resolve_rejects_payloads_outside_ref_list_contract(2 参数化) 生产契约是 list[ObjectRef];已是 Tensor 的路径跳过(不拒绝);ndarray/裸 ref 断言拒绝
get_train_seq_ctx mm_token_type_ids 校验与补 0 test_response_and_pack_padding_are_text(response_length 0/1/3)+ test_rejects_misaligned_prompt_metadata(4 种 shape) 形状校验已恢复 4 参数化;packing 补 0 与 teacher 继承逐位断言
construct_teacher_seq_ctx → copy tests/rl/test_train_teacher_context.py(media 4 × is_composed 2、meta 设备 offsets、SP shard 元数据) 逐字段 is 同一性断言保留/丢弃;CPU offsets 在设备迁移后保留;_raw_inputs_embeds 只清 teacher 不动 student
validate_image_only_rollout TestOnlineVideoGuard(3 用例) 视频在 payload/HTTP 构造前抛 NotImplementedError;图文路径无行为变化

抽象与信息隐藏评估

按 codebase-design 术语评估:

  • MM_PIXEL_FIELDS(rl_data.py:70-71)是一个好的 depth 改进:三处调用点(sampler/worker/controller)共享一个 Literal 元组,字段枚举的 locality 收敛到 schema 定义处;新增字段从「改 3 处」变为「改 1 处」。TypedDict 与纯数据别名不计入「新增类」。
  • _resolve_multimodal_inputs / _free_multimodal_inputs(worker.py:252 / controller.py:23)通过 deletion test:删除它们会把 ref 解析/释放规则撒回调用点;接口只有一个 SequenceContext/Iterable[SequenceContext] 参数,是深模块。放对位置(消费方各自持有),没有引入假想 seam。
  • construct_teacher_seq_ctx 改用 seq_ctx.copy(**overrides):复制规则的 locality 收进 SequenceContext.copy(sequence_context.py:562,既有),teacher 只声明「丢什么」。drop_visual 以 null-object 风格({} vs 清空 dict)统一两种 teacher 的构造调用,未新增类。
  • validate_image_only_rollout:单函数承载「内置传输不支持视频」规则,两个调用点各一次具名调用,规则不下渗到 worker/client 内部。
  • 新增 RL tokenizer 及其 config 子类;RLGlm53VLTokenizeFunction 继承 SFT fn 而非复制,复用既有处理逻辑。

新增功能守则

  • R1(接触点 ≤ 1 字段 + 1~2 次具名调用):满足。RolloutWorker/RolloutTeacherClient 各只加 1 次 validate_image_only_rollout(state) 调用(worker.py:1059、rollout_teacher_manager.py:71);TrainingWorker.fit 与 TrainingController.fit 各 1 次具名调用替换原内联块;sampler 仅 MM_PIXEL_FIELDS 一个常量引用。
  • R2(既有类不出现实现细节):基本满足。worker/controller 只见 MM_PIXEL_FIELDS 常量,不知 pad 值/sp 规则;rollout_routed_experts 等清理细节在 construct_teacher_seq_ctx 内。TrainTeacherManager 的 drop_visual 枚举了 6 个视觉字段名——这些字段是 SequenceContext 既有构造参数(teacher 丢字段语义本就属于该类职责,非本 PR 新特性的外部细节泄露),按事实不判违规。
  • R3(无新特性分叉条件):满足。is_composed 分支为修改前既有;本轮反而通过 copy + drop_visual 把两个分支的构造调用合一。_resolve_multimodal_inputs 的 isinstance(value, torch.Tensor): continue 是契约守卫(生产已是 Tensor 的路径跳过),非按特性分叉的业务条件。

公开接口的线性业务流程评估

  • RLGlm53VLTokenizeFunction.__call__(glm53_vl_tokenize_fn.py:31):渲染 → SFT 展开 → 长度过滤 → 组 extra(raw_prompt/train_prompt_ids/media 路径)→ 组 RolloutState,步骤同抽象层级、可顺序阅读,符合 Composed Method。_render_base/_multimodal_info 隐藏了真实规则(占位符渲染、张量转 numpy),值得保留,非纯转发 wrapper。
  • construct_teacher_seq_ctx:position_ids 规则 → drop_visual → 一次 copy,三步线性;Stepdown 合理。
  • get_train_seq_ctx 的 mm_token_type_ids 段:校验 → 补 0 内联在高层级,但仅 8 行且注释说明所有者(response 段补 0、去尾在调用方),可读性可接受;若未来再加字段可考虑把多模态段整体下沉为私有函数(非必须,见其他 issues Nit)。

单测建议

现有覆盖已含:真实 processor 三模态、ref 幂等/共享/释放、形状校验 4 参数化、teacher 保留/丢弃矩阵(media 4 × composed 2)、meta 设备 CPU offsets、SP shard 元数据、在线视频拒绝。可选增强(不阻塞):

  1. _free_multimodal_inputs 在 worker 已物化(Tensor)路径上的 no-op 行为目前隐含,可加一例断言不重复 free。
  2. 图文混合(image_and_video)经 put_to_ray → get_train_seq_ctx → cat 的端到端只在 teacher 重建层覆盖,真实 producer 的 test_real_producer_to_teacher 目前是单模态参数化,可加混合用例。

其他 issues

Warning

  • 无。

Nit

  1. worker.py:_resolve_multimodal_inputs:契约收窄为「Tensor 跳过、其余必须 list」,旧路径的 ndarray 从 NotImplementedError 变为 AssertionError。已有测试锁定该行为且生产契约确为 list[ObjectRef],仅为行为变化记录,不要求改。
  2. misc.py:validate_image_only_rollout 只检查 extra_fields["video_data"] 与 mm_info["pixel_values_videos"];理论上「有 video 模态标记但无像素」的构造不触发拒绝。当前生产者两者成组出现,可接受。
  3. train_teacher_manager.py:250 注释为中文(「复用公共复制契约…」),文件内其他注释中英混用为既有风格,保持一致即可。
  4. rl_trainer.py mm_token_type_ids 段若后续继续扩展多模态字段,可将整个 if multimodal_train_info: 块下沉为 _apply_multimodal_train_info(seq_ctx, info, len_response_ids) 私有函数,保持高层线性叙述(当前规模下不必须)。

验证记录:本轮 29 项 CPU 测试通过;提交后已核对 15 个文件与审阅快照一致。收缩版未重跑 GPU 回放。

@xs1997zju xs1997zju changed the title [Fix] Preserve teacher sequence context through shared copy [Fix] Connect GLM53 multimodal inputs to training-side OPD Oct 28, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants