[diffusion] align video BCG warmup frame count (#36485)

This commit is contained in:
Xiaoyu Zhang
2026-08-27 20:52:00 +08:00
committed by GitHub
parent b647ae82f5
commit d42fa5e10a
8 changed files with 86 additions and 17 deletions
@@ -101,10 +101,10 @@ contains `[Diffusion BCG] captured` and contains no support-disable,
capture-failure, serving-signature-miss, or late quality-fusion marker. In capture-failure, serving-signature-miss, or late quality-fusion marker. In
particular, a request-scoped DiT fusion mounted after lossless warmup capture particular, a request-scoped DiT fusion mounted after lossless warmup capture
would be bypassed by replay; reject that row even when capture and signature would be bypassed by replay; reject that row even when capture and signature
checks pass. `--warmup-resolutions` only declares width and height: a video checks pass. For video presets, the helper declares both the request resolution
request can still miss because its frame count differs from the model's and `--warmup-num-frames` so the synthetic BCG warmup captures the requested
synthetic warmup contract. Treat that as Eager fallback, not as a valid BCG temporal shape. Treat any remaining temporal or conditioning signature miss as
measurement. Eager fallback, not as a valid BCG measurement.
A zero process exit is not sufficient evidence: every accepted row must also 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. contain its requested perf dump and a generated image, video, or audio file.
@@ -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: 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 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 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 ```bash
PYTHONPATH=python python3 "$BENCH_PY" \ 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 will be captured, or a request-scoped high-quality DiT fusion mounted after
capture. Do not average rejected rows with valid results. capture. Do not average rejected rows with valid results.
BCG signatures include more than width and height. The public BCG signatures include more than width and height. The helper maps an explicit
`--warmup-resolutions` flag declares only `WxH`; synthetic warmup still uses video request frame count to `--warmup-num-frames`, while
the model's own frame-count and conditioning defaults. A short video preset `--warmup-resolutions` declares `WxH`. Other temporal or conditioning inputs
can therefore capture a default temporal shape and miss the actual request can still differ from the captured signature. The helper marks such a row
even at the same resolution. The helper marks that row invalid. Use a request invalid; fix the model's BCG warmup/padding contract before claiming a speedup.
whose complete temporal/conditioning contract matches warmup, or fix the
model's BCG warmup/padding contract before claiming a speedup.
The helper sets `SGLANG_DIFFUSION_SYNC_STAGE_PROFILING=1` for accurate stage 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 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-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. | | `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. | | `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-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. | | `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. | | `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. |
@@ -1724,6 +1724,8 @@ def build_sglang_cmd(
if warmup_resolutions: if warmup_resolutions:
cmd.append("--warmup-resolutions") cmd.append("--warmup-resolutions")
cmd.extend(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: if bcg_text_buckets is not None:
cmd.append("--bcg-text-buckets") cmd.append("--bcg-text-buckets")
cmd.extend(str(bucket) for bucket in bcg_text_buckets) cmd.extend(str(bucket) for bucket in bcg_text_buckets)
@@ -414,6 +414,7 @@ class ServerArgs(DisaggServerArgsMixin):
warmup_mode: str | None = None warmup_mode: str | None = None
warmup_resolutions: list[str] = None warmup_resolutions: list[str] = None
warmup_num_frames: int | None = None
warmup_steps: int = 1 warmup_steps: int = 1
disable_autocast: bool | None = None disable_autocast: bool | None = None
@@ -1162,6 +1163,8 @@ class ServerArgs(DisaggServerArgsMixin):
f"Invalid --warmup-mode {self.warmup_mode!r}; " f"Invalid --warmup-mode {self.warmup_mode!r}; "
f"expected one of {WARMUP_MODES}." 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: if self.enable_torch_compile and self.warmup_mode is None:
self.warmup_mode = "server" self.warmup_mode = "server"
@@ -1171,9 +1174,11 @@ class ServerArgs(DisaggServerArgsMixin):
"to disable this behavior." "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. # 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" self.warmup_mode = "request"
# BCG captures every graph during a synthetic warmup forward at startup # BCG captures every graph during a synthetic warmup forward at startup
@@ -2234,6 +2239,16 @@ class ServerArgs(DisaggServerArgsMixin):
default=ServerArgs.warmup_resolutions, default=ServerArgs.warmup_resolutions,
help="Specify explicit warmup resolutions. e.g., `--warmup-resolutions 256x256 720x720`", 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( parser.add_argument(
"--warmup-steps", "--warmup-steps",
type=int, type=int,
@@ -232,8 +232,20 @@ def _resolve_warmup_num_frames(
*, *,
server_based_warmup: bool, server_based_warmup: bool,
) -> int: ) -> int:
num_frames = getattr(sampling_defaults, "num_frames", 1) default_num_frames = getattr(sampling_defaults, "num_frames", 1)
if not _is_video_warmup_task(server_args) or num_frames is None: 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 return num_frames
# Breakable CUDA graph replays only exact latent shapes: the warmup # Breakable CUDA graph replays only exact latent shapes: the warmup
@@ -519,6 +519,27 @@ class TestWarmupReqCfgParallel(unittest.TestCase):
0, 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): def test_server_based_warmup_uses_video_supported_resolution_budget(self):
server_args = MagicMock() server_args = MagicMock()
server_args.warmup_steps = 1 server_args.warmup_steps = 1
@@ -271,6 +271,14 @@ class TestDiffusionBenchmarkSkill(unittest.TestCase):
bcg_cmd[bucket_index + 1 : bucket_index + 3], ["256", "512"] 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: for _, quality, breakable_cuda_graph in module.QUALITY_BCG_ABBA_MATRIX:
module.build_sglang_cmd( module.build_sglang_cmd(
"longcat-image", "longcat-image",
@@ -679,6 +679,7 @@ class TestWarmupModeNormalization(unittest.TestCase):
*, *,
warmup_mode=None, warmup_mode=None,
warmup_resolutions=None, warmup_resolutions=None,
warmup_num_frames=None,
enable_torch_compile=False, enable_torch_compile=False,
enable_breakable_cuda_graph=False, enable_breakable_cuda_graph=False,
disagg_role=None, disagg_role=None,
@@ -688,6 +689,7 @@ class TestWarmupModeNormalization(unittest.TestCase):
sa = ServerArgs.__new__(ServerArgs) sa = ServerArgs.__new__(ServerArgs)
sa.warmup_mode = warmup_mode sa.warmup_mode = warmup_mode
sa.warmup_resolutions = warmup_resolutions sa.warmup_resolutions = warmup_resolutions
sa.warmup_num_frames = warmup_num_frames
sa.enable_torch_compile = enable_torch_compile sa.enable_torch_compile = enable_torch_compile
sa.enable_breakable_cuda_graph = enable_breakable_cuda_graph sa.enable_breakable_cuda_graph = enable_breakable_cuda_graph
sa.disagg_role = RoleType.MONOLITHIC if disagg_role is None else disagg_role 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") 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): def test_torch_compile_defaults_to_server_warmup(self):
sa = self._resolve(enable_torch_compile=True) sa = self._resolve(enable_torch_compile=True)