[BugFix]: Fix Bagel generate_image denoising schedule (#4509)

This commit is contained in:
汪志鹏 authored and GitHub committed 2026-06-18 02:02:38 +00:00
1 parent 2f64c712fd
commit f195e44bc1
5 files changed
+30 -15

No files matched your search

@@ -19,15 +19,18 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
NUM_TOKENS = 8
HIDDEN_DIM = 16
NUM_TIMESTEPS = 5
# generate_image samples (num_timesteps + 1) points then drops the last, matching
# upstream lance: see Bagel.generate_image in bagel_transformer.py.
EXPECTED_STEPS = NUM_TIMESTEPS
# Official BAGEL samples num_timesteps points then drops the terminal t=0,
# yielding num_timesteps - 1 denoise steps.
EXPECTED_STEPS = NUM_TIMESTEPS - 1
def _make_mock_bagel(mocker: MockerFixture):
"""Create a mock Bagel with forward returning constant velocity."""
mock = mocker.MagicMock(spec=Bagel)
mock._sp_size = 1
# MagicMock would otherwise auto-return a truthy stub for this flag; pin it
# to the official BAGEL schedule convention (num_timesteps points).
mock._denoise_schedule_extra_step = False
# forward returns a small constant velocity so x_t changes each step
def fake_forward(self, x_t, **kwargs):
@@ -179,7 +182,7 @@ class TestTrajectoryRecording:
bagel, args = bagel_and_args
# Recompute the expected timestep schedule (mirrors generate_image logic)
ts = torch.linspace(1, 0, args["num_timesteps"] + 1)
ts = torch.linspace(1, 0, args["num_timesteps"])
shift = args.get("timestep_shift", 1.0)
ts = shift * ts / (1 + (shift - 1) * ts)
expected_timesteps = ts[:-1] # last element is dropped
@@ -197,7 +200,7 @@ class TestTrajectoryRecording:
bagel, args = bagel_and_args
# Compute the number of denoising steps the same way generate_image does
ts = torch.linspace(1, 0, args["num_timesteps"] + 1)
ts = torch.linspace(1, 0, args["num_timesteps"])
shift = args.get("timestep_shift", 1.0)
ts = shift * ts / (1 + (shift - 1) * ts)
num_steps = len(ts) - 1 # timesteps = ts[:-1]
@@ -32,7 +32,7 @@ BAGEL_MOONCAKE_CI_DEPLOY = get_deploy_config_path("ci/bagel_mooncake.yaml")
# Reference pixel data extracted from the known-good output image
# Each entry contains (x, y) position and expected (R, G, B) values
# "Generated with seed=52, num_inference_steps=14,
# "Generated with seed=52, num_inference_steps=15,
# prompt='A cute cat'"
REFERENCE_PIXELS = [
{"position": (100, 100), "rgb": (115, 113, 94)},
@@ -63,7 +63,7 @@ def _find_free_port() -> int:
return port
def _configure_sampling_params(omni: Omni, num_inference_steps: int = 14) -> list:
def _configure_sampling_params(omni: Omni, num_inference_steps: int = 15) -> list:
"""Configure sampling parameters for Bagel text2img generation.
Args:
@@ -30,7 +30,7 @@ pytestmark = [pytest.mark.usefixtures("clean_gpu_memory_between_tests")]
BAGEL_CI_DEPLOY = get_deploy_config_path("ci/bagel.yaml")
# Reference pixel data extracted from the known-good output image
# Generated with seed=52, num_inference_steps=14,
# Generated with seed=52, num_inference_steps=15,
# prompt='Change the grass color to red',
# input image: 2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg
REFERENCE_PIXELS = [
@@ -48,7 +48,7 @@ REFERENCE_PIXELS = [
# text2img reference pixels (aligned with offline `bagel/end2end.py` text2img, 15 steps)
# "Generated with seed=52, num_inference_steps=14,
# "Generated with seed=52, num_inference_steps=15,
# prompt='A cute cat'"
TEXT2IMG_REFERENCE_PIXELS = [
{"position": (100, 100), "rgb": (115, 113, 94)},
@@ -88,7 +88,7 @@ def _find_free_port() -> int:
return port
def _configure_sampling_params(omni: Omni, num_inference_steps: int = 14) -> list:
def _configure_sampling_params(omni: Omni, num_inference_steps: int = 15) -> list:
"""Configure sampling parameters for Bagel img2img generation.
Args:
@@ -1129,6 +1129,13 @@ class Bagel(nn.Module):
config_class = BagelConfig
base_model_prefix = "bagel"
# Flow-matching denoise schedule convention. Official BAGEL samples
# ``num_timesteps`` points over [1, 0] and drops the terminal t=0, yielding
# ``num_timesteps - 1`` Euler steps. Lance samples one extra point
# (``num_timesteps + 1``) for ``num_timesteps`` steps; ``LanceBagel`` flips
# this on. See https://github.com/vllm-project/vllm-omni/issues/4470.
_denoise_schedule_extra_step: bool = False
def __init__(
self,
language_model,
@@ -1690,11 +1697,11 @@ class Bagel(nn.Module):
frame_condition_token_indexes = frame_condition_token_indexes.to(x_t.device).long()
pinned_x_t = x_t[frame_condition_token_indexes].clone()
# Use num_timesteps + 1 sample points so we get `num_timesteps` denoise
# steps after dropping the terminal t=0 (which has no dt). Upstream
# Lance / BAGEL both use this convention; without the +1 we silently
# run one fewer denoise iteration than the user asked for.
timesteps = torch.linspace(1, 0, num_timesteps + 1, device=x_t.device)
# Build the flow-matching schedule. BAGEL drops the terminal t=0 for
# ``num_timesteps - 1`` Euler steps; Lance keeps it for ``num_timesteps``.
# ``_denoise_schedule_extra_step`` (overridden by ``LanceBagel``) selects which.
num_sample_points = num_timesteps + 1 if self._denoise_schedule_extra_step else num_timesteps
timesteps = torch.linspace(1, 0, num_sample_points, device=x_t.device)
timesteps = timestep_shift * timesteps / (1 + (timestep_shift - 1) * timesteps)
dts = timesteps[:-1] - timesteps[1:]
timesteps = timesteps[:-1]
@@ -278,6 +278,11 @@ class LanceBagel(Bagel):
``forward_cache_update_vae`` — is reused unchanged.
"""
# Upstream Lance samples ``num_timesteps + 1`` schedule points (one extra
# over official BAGEL) so the denoise loop runs exactly ``num_timesteps``
# Euler steps. See Bagel.generate_image and issue #4470.
_denoise_schedule_extra_step: bool = True
@staticmethod
def _lance_compute_vit_buckets():
"""Replicate upstream Lance's ``BucketResize.init_buckets`` for the ViT path.