【Bug已解决】[AsyncGRPO] AsyncGRPOTrainer ignores provided processing_class when initializing AsyncRolloutWorker 解决方案一、现象长什么样用AsyncGRPOTrainer做异步 RL我们显式传了一个自定义processing_class比如带特殊 chat template 的 processor / tokenizer但训练出来模型的行为和预期不符且 rollout 阶段生成的文本和训练阶段对不上。排查发现AsyncRolloutWorker负责用 vLLM 做生成的异步 rollout 工作进程根本没有用到我们传的processing_class而是自行初始化了一个默认的。现象trainer 用的 processing_class: 自定义 processor (特殊 chat template) AsyncRolloutWorker 用的: 默认 tokenizer (普通模板) - 生成与训练 tokenization 不一致reward 算错 / 行为偏移更隐蔽的是如果默认 processor 和自定义 processor 的词表或 chat template 不同rollout 生成的 token id 和训练侧算 logps 时的 token id 对不上GRPO 的 loss 直接建立在错误对齐上训练静默失效。现象特征只有 AsyncGRPO 暴露同步 GRPOTrainer 没有这个worker 独立初始化的问题不报错但生成/训练 tokenization 错位用户传了processing_class却好像没传一样。二、背景AsyncGRPOTrainer的异步架构里生成rollout和训练是分离的两个世界训练侧在主进程/训练器里用processing_class把样本编码成模型输入算 logps、lossrollout 侧AsyncRolloutWorker常是独立进程甚至多进程池用 vLLM 做自回归生成它自己也需要一个 processor 来把 prompt 编码、把生成结果解码。关键点两侧的 processor 必须完全一致——同一个 chat template、同一个词表、同一个特殊 token 定义。否则 rollout 生成的 token 序列在训练侧按另一个 processor解码/算 logps 时会错位GRPO 的生成-训练闭环就建立在错误对齐上。AsyncGRPOTrainer在初始化时会收processing_class参数但把它只存给了训练侧没传给AsyncRolloutWorker的初始化——worker 自己AutoProcessor.from_pretrained(model_name)拉了一个默认的。当用户提供了自定义 processor特殊模板worker 就用了错的那个。三、根因根因一句话AsyncGRPOTrainer在初始化AsyncRolloutWorker时没有把用户提供的processing_class透传进去worker 自行用模型名加载了默认 processor导致 rollout 侧与训练侧使用了不一致的 processorchat template/词表/特殊 token 不同生成与训练的 tokenization 错位GRPO 闭环建立在错误对齐上训练静默失效。具体透传缺失self.processing_class存了但AsyncRolloutWorker(...)没收到它worker 自加载默认worker 用from_pretrained(model)拉默认 processor忽略用户自定义两侧不一致训练侧特殊模板rollout 侧默认模板token id 错位只在 AsyncGRPO 暴露同步 GRPOTrainer 生成和训练在同一进程同一 processor没这个问题静默失效不报错但 loss 建立在错位 token 上reward 失真。本质是异步架构下配置processor没有跨进程/跨组件透传。四、最小可运行复现下面用纯 Python 模拟worker 没收到 processing_class 用了默认class Processor: def __init__(self, template): self.template template class AsyncRolloutWorker: def __init__(self, model_name, processing_classNone): if processing_class is None: # 旧行为没收到就自加载默认 self.processor Processor(templatedefault) else: self.processor processing_class class AsyncGRPOTrainer: def __init__(self, model_name, processing_classNone): self.processing_class processing_class # 旧实现初始化 worker 时没传 processing_class self.worker AsyncRolloutWorker(model_name) # ← 漏传 def check_consistency(self): train_tpl getattr(self.processing_class, template, default) rollout_tpl self.worker.processor.template return train_tpl rollout_tpl def demo(): custom Processor(templatespecial_chat) trainer AsyncGRPOTrainer(my-model, processing_classcustom) print(训练侧模板:, custom.template) print(rollout(worker)模板:, trainer.worker.processor.template) print(两侧一致:, trainer.check_consistency(), - False 即错位) if __name__ __main__: demo()输出训练侧模板: special_chat rollout(worker)模板: default 两侧一致: False - False 即错位rollout侧用了default而非用户传的special_chat两侧不一致。复现了核心 bugworker 没收到 processing_class。五、解决方案第一层初始化 worker 时透传 processing_class第一层最直接把self.processing_class透传给AsyncRolloutWorker的初始化class AsyncRolloutWorker: def __init__(self, model_name, processing_classNone): # 收到就用用户的没收到才默认 self.processor processing_class if processing_class is not None \ else Processor(templatedefault) class AsyncGRPOTrainer: def __init__(self, model_name, processing_classNone): self.processing_class processing_class # 修复把 processing_class 透传给 worker self.worker AsyncRolloutWorker(model_name, processing_classself.processing_class) def check_consistency(self): train_tpl getattr(self.processing_class, template, default) rollout_tpl self.worker.processor.template return train_tpl rollout_tpl def demo(): custom Processor(templatespecial_chat) trainer AsyncGRPOTrainer(my-model, processing_classcustom) print(修复后两侧一致:, trainer.check_consistency()) # True if __name__ __main__: demo()核心是AsyncRolloutWorker(model_name, processing_classself.processing_class)——用户传了就透传worker 用同一份 processor两侧一致。六、解决方案第二层worker 内部用传入的 processor 而非重新加载第一层修了透传但要保证 worker 内部确实使用传入的 processor 做编解码而不是收了又去from_pretrained覆盖。第二层在 worker 里把 processor 作为唯一真源class AsyncRolloutWorker: def __init__(self, model_name, processing_classNone): if processing_class is not None: self.processor processing_class # 直接用传入的不重载 else: self.processor Processor(templatedefault) def encode_prompt(self, text): # 用 self.processor 编码保证和训练侧同一模板 return f[{self.processor.template}]{text} def decode_completion(self, ids): # 同理用同一 processor 解码 return fdecode-by-{self.processor.template} class AsyncGRPOTrainer: def __init__(self, model_name, processing_classNone): self.processing_class processing_class self.worker AsyncRolloutWorker(model_name, processing_classself.processing_class) def rollout_and_train_consistent(self, prompt): encoded self.worker.encode_prompt(prompt) # 训练侧也用 self.processing_class 编码保证同源 train_encoded f[{self.processing_class.template}]{prompt} if self.processing_class else prompt return encoded train_encoded def demo(): custom Processor(templatespecial_chat) t AsyncGRPOTrainer(m, processing_classcustom) print(编解码同源:, t.rollout_and_train_consistent(hi)) if __name__ __main__: demo()worker 收到processing_class后不再from_pretrained重载直接用传入实例编码、解码都用self.processor与训练侧self.processing_class同源消除收了又覆盖的隐患。七、解决方案第三层一致性断言 不变量测试第三层加护栏确保训练侧与 rollout 侧 processor 完全一致并锁进测试def assert_processor_consistent(trainer_processor, worker_processor): 断言两侧 processor 是同一实例或等价配置。 if trainer_processor is None and worker_processor is None: return True if (trainer_processor is None) ! (worker_processor is None): raise AssertionError(训练侧与 rollout 侧 processor 存在性不一致) # 比关键配置模板 / 词表大小 t1 getattr(trainer_processor, template, None) t2 getattr(worker_processor, template, None) if t1 ! t2: raise AssertionError(fprocessor 模板不一致: 训练{t1} rollout{t2}) return True def test_processing_class_propagates(): custom Processor(templatespecial_chat) t AsyncGRPOTrainer(m, processing_classcustom) assert_processor_consistent(t.processing_class, t.worker.processor) print(OK: processing_class 已透传到 worker两侧一致) def test_default_fallback_consistent(): # 不传 processing_class 时两侧都用默认仍一致 t AsyncGRPOTrainer(m, processing_classNone) assert_processor_consistent(t.processing_class, t.worker.processor) print(OK: 默认路径两侧也一致) if __name__ __main__: test_processing_class_propagates() test_default_fallback_consistent()assert_processor_consistent在 trainer 初始化后检查训练侧与 worker 侧 processor 的模板/词表一致不一致立即断言失败两个测试锁住传自定义则透传一致和不传则默认也一致任何把透传改回worker 自加载默认的改动被 CI 拦下。八、落地建议如果你在 AsyncGRPOTrainer 上发现生成/训练错位建议确认 worker 是否收到 processing_class没透传就加processing_classself.processing_class。worker 不重载收到后直接用传入实例别from_pretrained覆盖。编解码同源worker 编码/解码都用self.processor。加一致性断言初始化后检查训练侧/worker 侧 processor 模板一致。加测试锁住传则透传一致不传则默认一致。多进程注意若 worker 是独立进程processor 需可序列化/可重建为等价实例。九、排查清单如果 AsyncGRPO 生成与训练错位按顺序查确认处理 class 是否透传AsyncRolloutWorker(...)是否收到processing_class。worker 是否自加载默认收到后是否又from_pretrained覆盖。看两侧模板/词表训练侧self.processing_class与 workerself.processor是否一致。编解码同源worker 用self.processor做编解码。加一致性断言初始化后检查两侧 processor 一致。加测试锁住透传一致默认一致。多进程序列化worker 跨进程时 processor 需可重建等价实例。十、小结AsyncGRPOTrainer忽略用户提供的processing_class根因是trainer 初始化AsyncRolloutWorker时没有把processing_class透传进去worker 自行用模型名加载了默认 processor导致 rollout 侧与训练侧使用不一致的 processorchat template/词表/特殊 token 不同生成与训练的 tokenization 错位GRPO 的生成-训练闭环建立在错误对齐上训练静默失效。它只在异步架构暴露同步 GRPOTrainer 同进程同 processor且不报错最难察觉。修复分三层第一层在AsyncRolloutWorker(model_name, processing_classself.processing_class)透传用户传了就给 worker第二层让 worker 收到后直接用传入实例、不再from_pretrained重载编解码都用self.processor与训练侧同源第三层加assert_processor_consistent初始化后检查两侧模板/词表一致并加传则透传一致不传则默认一致不变量测试。核心心法是异步/多进程架构下任何影响两侧语义一致性的配置processor、chat template、词表都必须显式跨组件透传且禁止单侧自加载默认——否则 rollout 与训练会悄悄用不同processor让整个 RL 闭环建立在错位 token 上训练不报错却全面失真。