From d42fa5e10a74b6faa26ba5a318e28cecb9df5131 Mon Sep 17 00:00:00 2001 From: Xiaoyu Zhang <1182563586@qq.com> Date: Thu, 27 Aug 2026 20:52:00 +0800 Subject: [PATCH] [diffusion] align video BCG warmup frame count (#36485) --- .../SKILL.md | 8 +++---- .../benchmark-and-profile.md | 17 +++++++-------- .../scripts/bench_diffusion_denoise.py | 2 ++ .../runtime/server_args/server_args.py | 19 +++++++++++++++-- .../runtime/warmup_request_builder.py | 16 ++++++++++++-- .../test/unit/test_cfg_parallel_warmup.py | 21 +++++++++++++++++++ .../unit/test_diffusion_benchmark_skill.py | 8 +++++++ .../test/unit/test_server_args.py | 12 +++++++++++ 8 files changed, 86 insertions(+), 17 deletions(-) diff --git a/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/SKILL.md b/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/SKILL.md index 0db77b78e..b2e57b8bf 100644 --- a/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/SKILL.md +++ b/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/SKILL.md @@ -101,10 +101,10 @@ contains `[Diffusion BCG] captured` and contains no support-disable, capture-failure, serving-signature-miss, or late quality-fusion marker. In particular, a request-scoped DiT fusion mounted after lossless warmup capture would be bypassed by replay; reject that row even when capture and signature -checks pass. `--warmup-resolutions` only declares width and height: a video -request can still miss because its frame count differs from the model's -synthetic warmup contract. Treat that as Eager fallback, not as a valid BCG -measurement. +checks pass. For video presets, the helper declares both the request resolution +and `--warmup-num-frames` so the synthetic BCG warmup captures the requested +temporal shape. Treat any remaining temporal or conditioning signature miss as +Eager fallback, not as a valid BCG measurement. A zero process exit is not sufficient evidence: every accepted row must also contain its requested perf dump and a generated image, video, or audio file. diff --git a/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/benchmark-and-profile.md b/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/benchmark-and-profile.md index 347cbe88f..b5a05922a 100644 --- a/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/benchmark-and-profile.md +++ b/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/benchmark-and-profile.md @@ -144,7 +144,8 @@ Run one explicit quality or BCG comparator with `--quality lossless|high` and exclusive in this helper. A high+BCG command is only a compatibility probe: it is invalid if request-scoped DiT fusions mount after the lossless warmup graphs were captured. When a preset has explicit width and height, the helper -declares that same `--warmup-resolutions` value automatically: +declares that same `--warmup-resolutions` value automatically. Video presets +with an explicit frame count also declare the matching `--warmup-num-frames`: ```bash PYTHONPATH=python python3 "$BENCH_PY" \ @@ -183,13 +184,11 @@ disable, capture failure, `serving signature MISSED`, a message that no graph will be captured, or a request-scoped high-quality DiT fusion mounted after capture. Do not average rejected rows with valid results. -BCG signatures include more than width and height. The public -`--warmup-resolutions` flag declares only `WxH`; synthetic warmup still uses -the model's own frame-count and conditioning defaults. A short video preset -can therefore capture a default temporal shape and miss the actual request -even at the same resolution. The helper marks that row invalid. Use a request -whose complete temporal/conditioning contract matches warmup, or fix the -model's BCG warmup/padding contract before claiming a speedup. +BCG signatures include more than width and height. The helper maps an explicit +video request frame count to `--warmup-num-frames`, while +`--warmup-resolutions` declares `WxH`. Other temporal or conditioning inputs +can still differ from the captured signature. The helper marks such a row +invalid; fix the model's BCG warmup/padding contract before claiming a speedup. The helper sets `SGLANG_DIFFUSION_SYNC_STAGE_PROFILING=1` for accurate stage attribution. Set it to `0` explicitly only when collecting an e2e-only run and @@ -320,7 +319,7 @@ Use the preset categories this way: | `qwen-edit-base` | `Qwen/Qwen-Image-Edit` | No | Covers the original native `QwenImageEditPipelineConfig`, which is distinct from the 2509/2511 edit-plus paths; public SGLang edit fixture, 1024x1024. | | `qwen-image-layered` | `Qwen/Qwen-Image-Layered` | No | Native layered-image path using the same public reference image and four-frame request as the GPU server case, at the registered 640x640 canvas. | | `stable-diffusion-3.5-medium` | `stabilityai/stable-diffusion-3.5-medium-diffusers` | No | Representative native `StableDiffusion3PipelineConfig` path at 1024x1024. The repository is gated, so export `HF_TOKEN`; an unauthenticated run is a recorded access blocker, not model evidence. | -| `sana-video` | `Efficient-Large-Model/SANA-Video_2B_480p_diffusers` | No | CI-sized eager T2V baseline: 832x480, 17 frames, 8 steps, guidance 6.0. Compare `quality=lossless` and `quality=high`; high enables the BF16-input first linear-attention GEMM while retaining FP32 output and the FP32 second GEMM. | +| `sana-video` | `Efficient-Large-Model/SANA-Video_2B_480p_diffusers` | No | CI-sized T2V baseline: 832x480, 17 frames, 8 steps, guidance 6.0. The BCG comparator declares the same 17-frame warmup shape. Compare `quality=lossless` and `quality=high`; high enables the BF16-input first linear-attention GEMM while retaining FP32 output and the FP32 second GEMM. | | `sana-wm-bidirectional` | `Efficient-Large-Model/SANA-WM_bidirectional` | No | Dense two-stage TI2V baseline at the native 1280x704 shape, 49 frames, 16 fps, 20 steps, guidance 4.5, and a 48-frame forward/left action program. Uses the shared cat fixture. | | `sana-wm-streaming` | `Efficient-Large-Model/SANA-WM_streaming` | No | Matching offline chunk-causal two-stage baseline with the streaming DiT and chunked refiner enabled; uses the same shape, fixture, seed, and camera action for comparison. | | `lingbot-video-moe` | `robbyant/lingbot-video-moe-30b-a3b` | No | One-GPU eager baseline using the CI structured-JSON caption, 384x640, 17 frames, 12 steps, and text-encoder CPU offload. | diff --git a/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/scripts/bench_diffusion_denoise.py b/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/scripts/bench_diffusion_denoise.py index cad2b215f..c81bf2326 100755 --- a/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/scripts/bench_diffusion_denoise.py +++ b/python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/scripts/bench_diffusion_denoise.py @@ -1724,6 +1724,8 @@ def build_sglang_cmd( if warmup_resolutions: cmd.append("--warmup-resolutions") cmd.extend(warmup_resolutions) + if "warmup-num-frames" not in parsed_args and "num-frames" in parsed_args: + cmd.extend(["--warmup-num-frames", str(parsed_args["num-frames"])]) if bcg_text_buckets is not None: cmd.append("--bcg-text-buckets") cmd.extend(str(bucket) for bucket in bcg_text_buckets) diff --git a/python/sglang/multimodal_gen/runtime/server_args/server_args.py b/python/sglang/multimodal_gen/runtime/server_args/server_args.py index 73d1b7bf7..9e495f09a 100644 --- a/python/sglang/multimodal_gen/runtime/server_args/server_args.py +++ b/python/sglang/multimodal_gen/runtime/server_args/server_args.py @@ -414,6 +414,7 @@ class ServerArgs(DisaggServerArgsMixin): warmup_mode: str | None = None warmup_resolutions: list[str] = None + warmup_num_frames: int | None = None warmup_steps: int = 1 disable_autocast: bool | None = None @@ -1162,6 +1163,8 @@ class ServerArgs(DisaggServerArgsMixin): f"Invalid --warmup-mode {self.warmup_mode!r}; " f"expected one of {WARMUP_MODES}." ) + if self.warmup_num_frames is not None and self.warmup_num_frames <= 0: + raise ValueError("--warmup-num-frames must be a positive integer.") if self.enable_torch_compile and self.warmup_mode is None: self.warmup_mode = "server" @@ -1171,9 +1174,11 @@ class ServerArgs(DisaggServerArgsMixin): "to disable this behavior." ) - # Explicit resolutions need a request path unless an existing server + # Explicit warmup shapes need a request path unless an existing server # default already supplies the synthetic startup request. - if self.warmup_resolutions is not None and self.warmup_mode in (None, "off"): + if ( + self.warmup_resolutions is not None or self.warmup_num_frames is not None + ) and self.warmup_mode in (None, "off"): self.warmup_mode = "request" # BCG captures every graph during a synthetic warmup forward at startup @@ -2234,6 +2239,16 @@ class ServerArgs(DisaggServerArgsMixin): default=ServerArgs.warmup_resolutions, help="Specify explicit warmup resolutions. e.g., `--warmup-resolutions 256x256 720x720`", ) + parser.add_argument( + "--warmup-num-frames", + type=int, + default=ServerArgs.warmup_num_frames, + help=( + "Override the synthetic video warmup frame count. Use this with " + "breakable CUDA graphs when serving a non-default frame count so " + "the captured latent shape matches the request." + ), + ) parser.add_argument( "--warmup-steps", type=int, diff --git a/python/sglang/multimodal_gen/runtime/warmup_request_builder.py b/python/sglang/multimodal_gen/runtime/warmup_request_builder.py index b5f1be1d1..83e59f821 100644 --- a/python/sglang/multimodal_gen/runtime/warmup_request_builder.py +++ b/python/sglang/multimodal_gen/runtime/warmup_request_builder.py @@ -232,8 +232,20 @@ def _resolve_warmup_num_frames( *, server_based_warmup: bool, ) -> int: - num_frames = getattr(sampling_defaults, "num_frames", 1) - if not _is_video_warmup_task(server_args) or num_frames is None: + default_num_frames = getattr(sampling_defaults, "num_frames", 1) + if not _is_video_warmup_task(server_args): + return default_num_frames + + # Most tests and a few lightweight integrations use MagicMock server args, + # whose missing attributes resolve to another mock. Only accept a concrete + # integer as an explicit override. + explicit_num_frames = getattr(server_args, "warmup_num_frames", None) + num_frames = ( + explicit_num_frames + if isinstance(explicit_num_frames, int) + else default_num_frames + ) + if num_frames is None: return num_frames # Breakable CUDA graph replays only exact latent shapes: the warmup diff --git a/python/sglang/multimodal_gen/test/unit/test_cfg_parallel_warmup.py b/python/sglang/multimodal_gen/test/unit/test_cfg_parallel_warmup.py index 834039c5b..df57273e3 100644 --- a/python/sglang/multimodal_gen/test/unit/test_cfg_parallel_warmup.py +++ b/python/sglang/multimodal_gen/test/unit/test_cfg_parallel_warmup.py @@ -519,6 +519,27 @@ class TestWarmupReqCfgParallel(unittest.TestCase): 0, ) + def test_breakable_cuda_graph_uses_explicit_warmup_num_frames(self): + pipeline_config = MagicMock() + pipeline_config.task_type = ModelTaskType.T2V + pipeline_config.adjust_num_frames.side_effect = lambda value: value + server_args = SimpleNamespace( + pipeline_config=pipeline_config, + enable_breakable_cuda_graph=True, + pipeline_class_name=None, + num_gpus=1, + warmup_num_frames=17, + ) + + num_frames = _resolve_warmup_num_frames( + server_args, + SamplingParams(num_frames=81), + server_based_warmup=True, + ) + + self.assertEqual(num_frames, 17) + pipeline_config.adjust_num_frames.assert_called_once_with(17) + def test_server_based_warmup_uses_video_supported_resolution_budget(self): server_args = MagicMock() server_args.warmup_steps = 1 diff --git a/python/sglang/multimodal_gen/test/unit/test_diffusion_benchmark_skill.py b/python/sglang/multimodal_gen/test/unit/test_diffusion_benchmark_skill.py index 9614a81d7..7ae845f15 100644 --- a/python/sglang/multimodal_gen/test/unit/test_diffusion_benchmark_skill.py +++ b/python/sglang/multimodal_gen/test/unit/test_diffusion_benchmark_skill.py @@ -271,6 +271,14 @@ class TestDiffusionBenchmarkSkill(unittest.TestCase): bcg_cmd[bucket_index + 1 : bucket_index + 3], ["256", "512"] ) + sana_video_bcg_cmd = module.build_sglang_cmd( + "sana-video", breakable_cuda_graph=True + ) + self.assertEqual( + sana_video_bcg_cmd[sana_video_bcg_cmd.index("--warmup-num-frames") + 1], + "17", + ) + for _, quality, breakable_cuda_graph in module.QUALITY_BCG_ABBA_MATRIX: module.build_sglang_cmd( "longcat-image", diff --git a/python/sglang/multimodal_gen/test/unit/test_server_args.py b/python/sglang/multimodal_gen/test/unit/test_server_args.py index 6f954eb30..10278031e 100644 --- a/python/sglang/multimodal_gen/test/unit/test_server_args.py +++ b/python/sglang/multimodal_gen/test/unit/test_server_args.py @@ -679,6 +679,7 @@ class TestWarmupModeNormalization(unittest.TestCase): *, warmup_mode=None, warmup_resolutions=None, + warmup_num_frames=None, enable_torch_compile=False, enable_breakable_cuda_graph=False, disagg_role=None, @@ -688,6 +689,7 @@ class TestWarmupModeNormalization(unittest.TestCase): sa = ServerArgs.__new__(ServerArgs) sa.warmup_mode = warmup_mode sa.warmup_resolutions = warmup_resolutions + sa.warmup_num_frames = warmup_num_frames sa.enable_torch_compile = enable_torch_compile sa.enable_breakable_cuda_graph = enable_breakable_cuda_graph sa.disagg_role = RoleType.MONOLITHIC if disagg_role is None else disagg_role @@ -718,6 +720,16 @@ class TestWarmupModeNormalization(unittest.TestCase): ) self.assertEqual(sa.warmup_mode, "request") + def test_num_frames_forces_warmup_on(self): + sa = self._resolve(warmup_mode="off", warmup_num_frames=17) + self.assertEqual(sa.warmup_mode, "request") + + def test_num_frames_must_be_positive(self): + for num_frames in (0, -1): + with self.subTest(num_frames=num_frames): + with self.assertRaisesRegex(ValueError, "positive"): + self._resolve(warmup_num_frames=num_frames) + def test_torch_compile_defaults_to_server_warmup(self): sa = self._resolve(enable_torch_compile=True)