Prhub

#23198 [diffusion] Fix --warmup-resolutions hang with --enable-cfg-parallel

原始 PR 作者 mispa-ms 合并时间 2026-04-23 13:39 文件变更 3 提交数 7 评论 7 代码增减 +237 / -15

执行摘要

修复 CFG-parallel 模式下 warmup 分辨率预热导致的 30 分钟静默挂起。

根据PR body描述,修复一个30分钟的静默挂起问题,当使用--enable-cfg-parallel --warmup --warmup-resolutions 启动时。根因是合成的warmup Req没有正确设置do_classifier_free_guidance,导致CFG-parallel模式下rank 1跳过正向传递,最终在scheduler.step()中崩溃,并被gloo广播超时掩盖。

建议精读scheduler.py和input_validation.py的变更,关注设计决策如占位符常量的使用和验证逻辑的添加,这些体现了防御性编程和代码可维护性的权衡。

讨论亮点

review中主要讨论了negative_prompt占位符的设计选择。mickqian询问占位符是否为typo,mispa-ms解释使用真实单词(如'warmup')是为了确保tokenizer产生可预测序列,避免退化。最终采纳mickqian的建议,提取为全局常量DEFAULT_PLACEHOLDER_PROMPT,提升代码可维护性。

实现拆解

  1. 修改scheduler预热请求合成逻辑:在python/sglang/multimodal_gen/runtime/managers/scheduler.pyprepare_server_warmup_reqs方法中,当enable_cfg_parallel为True时,设置do_classifier_free_guidance=Truenegative_prompt=DEFAULT_PLACEHOLDER_PROMPT,确保rank 1有有效工作;当CFG-parallel关闭时,保持原有行为不变。
  2. 添加输入验证防护:在python/sglang/multimodal_gen/runtime/pipelines_core/stages/input_validation.pyforward方法中,新增检查:如果服务器启用cfg-parallel但请求未启用CFG,则抛出ValueError,提供详细错误信息,防止后续挂起。
  3. 新增单元测试覆盖:创建python/sglang/multimodal_gen/test/unit/test_cfg_parallel_warmup.py,包含四个CPU-only测试,验证scheduler修复和输入验证防护的正确性,确保回归防护。
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/managers/scheduler.py 调度器 modified 6.86
python/sglang/multimodal_gen/runtime/pipelines_core/stages/input_validation.py 输入验证 modified 6.62
python/sglang/multimodal_gen/test/unit/test_cfg_parallel_warmup.py 测试套件 added 7.37

关键符号

prepare_server_warmup_reqs forward

关键源码片段

python/sglang/multimodal_gen/runtime/managers/scheduler.py core-logic

核心调度器文件,修改了预热请求合成逻辑,直接修复挂起问题。

# 在 scheduler.py 中,新增模块级常量并修改 prepare_server_warmup_reqs 方法
# Placeholder negative_prompt used in synthesized warmup Reqs when --enable-cfg-parallel is on.
# A non-empty, real word (vs "" or " ") so every tokenizer backend emits a predictable,
# non-degenerate token sequence — rank 1's uncond branch then produces a valid tensor.
DEFAULT_PLACEHOLDER_PROMPT = "warmup"def prepare_server_warmup_reqs(self):
    if self.server_args.warmup and not self.warmed_up and self.server_args.warmup_resolutions is not None:
        self._warmup_total = len(self.server_args.warmup_resolutions)
        self._warmup_processed = 0
        task_type = self.server_args.pipeline_config.task_type
        requires_warmup_image = task_type.accepts_image_input()
        warmup_input_path = None
        if requires_warmup_image:
            warmup_input_path = self._prepare_shared_warmup_image_path()
​
        for resolution in self.server_args.warmup_resolutions:
            width, height = _parse_size(resolution)
            # CFG-parallel splits cond/uncond across ranks, so rank 1 needs a real uncond pass.
            # Force do_classifier_free_guidance + non-empty negative_prompt when cfg-parallel is on,
            # so the synthesized warmup Req exercises both ranks' denoising paths.
            # When cfg-parallel is off, the Req construction is byte-identical to the pre-fix behavior.
            req_kwargs = dict(
                data_type=task_type.data_type(),
                width=width,
                height=height,
                prompt="",
            )
            if requires_warmup_image:
                req_kwargs["negative_prompt"] = ""
                req_kwargs["image_path"] = [warmup_input_path]
            if self.server_args.enable_cfg_parallel:
                req_kwargs["negative_prompt"] = DEFAULT_PLACEHOLDER_PROMPT # 使用全局常量
                req_kwargs["do_classifier_free_guidance"] = True # 确保启用 CFG
            req = Req(**req_kwargs)
            req.set_as_warmup(self.server_args.warmup_steps)
            self.waiting_queue.append((None, req))
        self.warmed_up = True
python/sglang/multimodal_gen/runtime/pipelines_core/stages/input_validation.py core-logic

输入验证阶段文件,新增防护逻辑,防止非 CFG 请求在 cfg-parallel 服务器上导致挂起。

# 在 input_validation.py 的 forward 方法中,新增验证逻辑
# Reject requests that do not enable CFG on a server launched with --enable-cfg-parallel.
# CFG-parallel splits cond/uncond across ranks, so rank 1 has no work and returns None for noise_pred,
# which crashes scheduler.step() ~30 minutes later under a gloo broadcast timeout.
if server_args.enable_cfg_parallel and not batch.do_classifier_free_guidance:
    neg_prompt_state = (
        "not set"
        if batch.negative_prompt is None
        else "empty" if batch.negative_prompt == "" else "set"
    )
    raise ValueError(
        f"Server was launched with --enable-cfg-parallel but this request does not use classifier-free guidance "
        f"(do_classifier_free_guidance={batch.do_classifier_free_guidance}, guidance_scale={batch.guidance_scale}, "
        f"true_cfg_scale={batch.true_cfg_scale}, negative_prompt={neg_prompt_state}). "
        f"CFG-parallel splits cond/uncond across ranks and requires both to be active. "
        f"Either disable --enable-cfg-parallel or ensure the request enables CFG "
        f"(set guidance_scale > 1.0 or true_cfg_scale > 1.0, with a non-empty negative_prompt or negative_prompt_embeds)."
    ) # 提供详细错误信息,帮助用户调试

评论区精华

占位符常量设计 设计

mickqian 询问 negative_prompt 占位符是否是 typo,mispa-ms 解释使用真实单词(如 'warmup')是为了确保 tokenizer 产生可预测序列,避免退化。mickqian 建议提取为全局常量。

结论:提取 DEFAULT_PLACEHOLDER_PROMPT 常量,提升代码可维护性和一致性。 · 已解决

风险与影响

  1. 回归风险:修改了scheduler的核心预热逻辑,可能影响其他warmup场景,但新增测试覆盖了关键路径。
  2. 兼容性风险:输入验证新增拒绝非CFG请求,可能影响之前允许的配置,但这是防御性措施,且错误信息清晰。
  3. 性能风险:无显著性能影响,修复确保CFG-parallel正确工作,避免资源浪费。

对用户:修复了挂起问题,提升服务可靠性和用户体验;对系统:确保CFG-parallel模式在diffusion模块中正常运行,避免30分钟超时和崩溃;对团队:提供了单元测试和清晰验证逻辑,便于后续维护和扩展。

核心路径变更 输入验证增强

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论