[diffusion] fix: fix sampling params incorrectly override in cli (#20689)

This commit is contained in:
Mick
2026-03-17 08:48:10 +08:00
committed by GitHub
parent 1eea744855
commit 474a851ae3
2 changed files with 102 additions and 158 deletions
@@ -596,213 +596,190 @@ class SamplingParams:
@staticmethod @staticmethod
def add_cli_args(parser: Any) -> Any: def add_cli_args(parser: Any) -> Any:
"""Add CLI arguments for SamplingParam fields""" """Add CLI arguments for SamplingParam fields"""
parser.add_argument("--data-type", type=str, nargs="+", default=DataType.VIDEO)
parser.add_argument( def add_argument(*name_or_flags, **kwargs):
kwargs.setdefault("default", argparse.SUPPRESS)
return parser.add_argument(*name_or_flags, **kwargs)
add_argument("--data-type", type=str, nargs="+")
add_argument(
"--num-frames-round-down", "--num-frames-round-down",
action="store_true", action="store_true",
default=SamplingParams.num_frames_round_down,
) )
parser.add_argument( add_argument(
"--enable-teacache", "--enable-teacache",
action="store_true", action="store_true",
default=SamplingParams.enable_teacache,
) )
# profiling # profiling
parser.add_argument( add_argument(
"--profile", "--profile",
action="store_true", action="store_true",
default=SamplingParams.profile,
help="Enable torch profiler for denoising stage", help="Enable torch profiler for denoising stage",
) )
parser.add_argument( add_argument(
"--num-profiled-timesteps", "--num-profiled-timesteps",
type=int, type=int,
default=SamplingParams.num_profiled_timesteps,
help="Number of timesteps to profile after warmup", help="Number of timesteps to profile after warmup",
) )
parser.add_argument( add_argument(
"--profile-all-stages", "--profile-all-stages",
action="store_true", action="store_true",
dest="profile_all_stages", dest="profile_all_stages",
default=SamplingParams.profile_all_stages,
help="Used with --profile, profile all pipeline stages", help="Used with --profile, profile all pipeline stages",
) )
parser.add_argument( add_argument(
"--debug", "--debug",
action="store_true", action="store_true",
default=SamplingParams.debug,
help="", help="",
) )
parser.add_argument( add_argument(
"--prompt", "--prompt",
type=str, type=str,
nargs="+", nargs="+",
default=SamplingParams.prompt,
help="Text prompt(s) for generation. Use space-separated values for multiple prompts, e.g., --prompt 'prompt 1' 'prompt 2'", help="Text prompt(s) for generation. Use space-separated values for multiple prompts, e.g., --prompt 'prompt 1' 'prompt 2'",
) )
parser.add_argument( add_argument(
"--negative-prompt", "--negative-prompt",
type=str, type=str,
default=None,
help="Negative text prompt for generation", help="Negative text prompt for generation",
) )
parser.add_argument( add_argument(
"--prompt-path", "--prompt-path",
type=str, type=str,
default=SamplingParams.prompt_path,
help="Path to a text file containing the prompt", help="Path to a text file containing the prompt",
) )
parser.add_argument( add_argument(
"--output-file-name", "--output-file-name",
type=str, type=str,
default=SamplingParams.output_file_name,
help="Name of the output file", help="Name of the output file",
) )
parser.add_argument( add_argument(
"--output-quality", "--output-quality",
type=str, type=str,
default=SamplingParams.output_quality,
help="Output quality setting (default, low, medium, high, maximum)", help="Output quality setting (default, low, medium, high, maximum)",
) )
parser.add_argument( add_argument(
"--output-compression", "--output-compression",
type=int, type=int,
default=SamplingParams.output_compression,
help="Output compression level (0-100, higher means better quality but larger file size)", help="Output compression level (0-100, higher means better quality but larger file size)",
) )
parser.add_argument( add_argument(
"--num-outputs-per-prompt", "--num-outputs-per-prompt",
type=int, type=int,
default=SamplingParams.num_outputs_per_prompt,
help="Number of outputs to generate per prompt", help="Number of outputs to generate per prompt",
) )
parser.add_argument( add_argument(
"--seed", "--seed",
type=int, type=int,
default=SamplingParams.seed,
help="Random seed for generation", help="Random seed for generation",
) )
parser.add_argument( add_argument(
"--generator-device", "--generator-device",
type=str, type=str,
default=SamplingParams.generator_device,
choices=["cuda", "musa", "cpu"], choices=["cuda", "musa", "cpu"],
help="Device for random generator (cuda, musa or cpu). Default: cuda", help="Device for random generator (cuda, musa or cpu). Default: cuda",
) )
parser.add_argument( add_argument(
"--num-frames", "--num-frames",
type=int, type=int,
default=SamplingParams.num_frames,
help="Number of frames to generate", help="Number of frames to generate",
) )
parser.add_argument( add_argument(
"--height", "--height",
type=int, type=int,
default=SamplingParams.height,
help="Height of generated output", help="Height of generated output",
) )
parser.add_argument( add_argument(
"--width", "--width",
type=int, type=int,
default=SamplingParams.width,
help="Width of generated output", help="Width of generated output",
) )
# resolution shortcuts # resolution shortcuts
parser.add_argument( add_argument(
"--4k", "--4k",
action="store_true", action="store_true",
dest="resolution_4k", dest="resolution_4k",
help="Set resolution to 4K (3840x2160)", help="Set resolution to 4K (3840x2160)",
) )
parser.add_argument( add_argument(
"--2k", "--2k",
action="store_true", action="store_true",
dest="resolution_2k", dest="resolution_2k",
help="Set resolution to 2K (2560x1440)", help="Set resolution to 2K (2560x1440)",
) )
parser.add_argument( add_argument(
"--1080p", "--1080p",
action="store_true", action="store_true",
dest="resolution_1080p", dest="resolution_1080p",
help="Set resolution to 1080p (1920x1080)", help="Set resolution to 1080p (1920x1080)",
) )
parser.add_argument( add_argument(
"--720p", "--720p",
action="store_true", action="store_true",
dest="resolution_720p", dest="resolution_720p",
help="Set resolution to 720p (1280x720)", help="Set resolution to 720p (1280x720)",
) )
parser.add_argument( add_argument(
"--fps", "--fps",
type=int, type=int,
default=SamplingParams.fps,
help="Frames per second for saved output", help="Frames per second for saved output",
) )
parser.add_argument( add_argument(
"--num-inference-steps", "--num-inference-steps",
type=int, type=int,
default=SamplingParams.num_inference_steps,
help="Number of denoising steps", help="Number of denoising steps",
) )
parser.add_argument( add_argument(
"--guidance-scale", "--guidance-scale",
type=float, type=float,
default=SamplingParams.guidance_scale,
help="Classifier-free guidance scale", help="Classifier-free guidance scale",
) )
parser.add_argument( add_argument(
"--guidance-scale-2", "--guidance-scale-2",
type=float, type=float,
default=SamplingParams.guidance_scale_2,
dest="guidance_scale_2", dest="guidance_scale_2",
help="Secondary guidance scale for dual-guidance models (e.g., Wan low-noise expert)", help="Secondary guidance scale for dual-guidance models (e.g., Wan low-noise expert)",
) )
parser.add_argument( add_argument(
"--guidance-rescale", "--guidance-rescale",
type=float, type=float,
default=SamplingParams.guidance_rescale,
help="Guidance rescale factor", help="Guidance rescale factor",
) )
parser.add_argument( add_argument(
"--cfg-normalization", "--cfg-normalization",
type=float, type=float,
default=SamplingParams.cfg_normalization, # type: ignore[arg-type]
dest="cfg_normalization", dest="cfg_normalization",
help=("CFG renormalization factor (for Z-Image). "), help=("CFG renormalization factor (for Z-Image). "),
) )
parser.add_argument( add_argument(
"--boundary-ratio", "--boundary-ratio",
type=float, type=float,
default=SamplingParams.boundary_ratio,
help="Boundary timestep ratio", help="Boundary timestep ratio",
) )
parser.add_argument( add_argument(
"--save-output", "--save-output",
action="store_true", action="store_true",
default=SamplingParams.save_output,
help="Whether to save the output to disk", help="Whether to save the output to disk",
) )
parser.add_argument( add_argument(
"--no-save-output", "--no-save-output",
action="store_false", action="store_false",
dest="save_output", dest="save_output",
help="Don't save the output to disk", help="Don't save the output to disk",
) )
parser.add_argument( add_argument(
"--return-frames", "--return-frames",
action="store_true", action="store_true",
default=SamplingParams.return_frames,
help="Whether to return the raw frames", help="Whether to return the raw frames",
) )
parser.add_argument( add_argument(
"--image-path", "--image-path",
type=str, type=str,
nargs="+", nargs="+",
default=SamplingParams.image_path,
help=( help=(
"Path(s) to input image(s) for image-to-image / image-to-video " "Path(s) to input image(s) for image-to-image / image-to-video "
"generation. For multiple images, pass them as space-separated " "generation. For multiple images, pass them as space-separated "
@@ -810,106 +787,93 @@ class SamplingParams:
'--image-path "img1.png" "img2.png"' '--image-path "img1.png" "img2.png"'
), ),
) )
parser.add_argument( add_argument(
"--moba-config-path", "--moba-config-path",
type=str, type=str,
default=None,
help="Path to a JSON file containing V-MoBA specific configurations.", help="Path to a JSON file containing V-MoBA specific configurations.",
) )
parser.add_argument( add_argument(
"--return-trajectory-latents", "--return-trajectory-latents",
action="store_true", action="store_true",
default=SamplingParams.return_trajectory_latents,
help="Whether to return the trajectory", help="Whether to return the trajectory",
) )
parser.add_argument( add_argument(
"--return-trajectory-decoded", "--return-trajectory-decoded",
action="store_true", action="store_true",
default=SamplingParams.return_trajectory_decoded,
help="Whether to return the decoded trajectory", help="Whether to return the decoded trajectory",
) )
parser.add_argument( add_argument(
"--diffusers-kwargs", "--diffusers-kwargs",
type=str, type=str,
default=None,
help="JSON string of extra kwargs to pass to diffusers pipeline. " help="JSON string of extra kwargs to pass to diffusers pipeline. "
'Example: \'{"output_type": "latent", "clip_skip": 2}\'', 'Example: \'{"output_type": "latent", "clip_skip": 2}\'',
) )
parser.add_argument( add_argument(
"--no-override-protected-fields", "--no-override-protected-fields",
action="store_true", action="store_true",
default=SamplingParams.no_override_protected_fields,
help=( help=(
"If set, disallow user params to override fields defined in subclasses." "If set, disallow user params to override fields defined in subclasses."
), ),
) )
parser.add_argument( add_argument(
"--adjust-frames", "--adjust-frames",
action=StoreBoolean, action=StoreBoolean,
default=SamplingParams.adjust_frames,
help=( help=(
"Enable/disable adjusting num_frames to evenly split latent frames across GPUs " "Enable/disable adjusting num_frames to evenly split latent frames across GPUs "
"and satisfy model temporal constraints. If disabled, tokens might be padded for SP." "and satisfy model temporal constraints. If disabled, tokens might be padded for SP."
"Default: true. Examples: --adjust-frames, --adjust-frames true, --adjust-frames false." "Default: true. Examples: --adjust-frames, --adjust-frames true, --adjust-frames false."
), ),
) )
parser.add_argument( add_argument(
"--return-file-paths-only", "--return-file-paths-only",
action=StoreBoolean, action=StoreBoolean,
default=SamplingParams.return_file_paths_only,
help="If set, output file will be saved early to get a performance boost, while output tensors will not be returned.", help="If set, output file will be saved early to get a performance boost, while output tensors will not be returned.",
) )
parser.add_argument( add_argument(
"--enable-sequence-shard", "--enable-sequence-shard",
action=StoreBoolean, action=StoreBoolean,
default=SamplingParams.enable_sequence_shard,
help="Enable sequence dimension shard with sequence parallelism.", help="Enable sequence dimension shard with sequence parallelism.",
) )
parser.add_argument( add_argument(
"--enable-frame-interpolation", "--enable-frame-interpolation",
action="store_true", action="store_true",
help="Enable post-generation frame interpolation using RIFE 4.22.lite.", help="Enable post-generation frame interpolation using RIFE 4.22.lite.",
) )
parser.add_argument( add_argument(
"--frame-interpolation-exp", "--frame-interpolation-exp",
type=int, type=int,
default=SamplingParams.frame_interpolation_exp,
help="Frame interpolation exponent: 1=2x, 2=4x (default: 1).", help="Frame interpolation exponent: 1=2x, 2=4x (default: 1).",
) )
parser.add_argument( add_argument(
"--frame-interpolation-scale", "--frame-interpolation-scale",
type=float, type=float,
default=SamplingParams.frame_interpolation_scale,
help="RIFE inference scale factor (default: 1.0; use 0.5 for high-res).", help="RIFE inference scale factor (default: 1.0; use 0.5 for high-res).",
) )
parser.add_argument( add_argument(
"--frame-interpolation-model-path", "--frame-interpolation-model-path",
type=str, type=str,
default=SamplingParams.frame_interpolation_model_path,
help="Local directory or HuggingFace repo ID containing RIFE flownet.pkl weights " help="Local directory or HuggingFace repo ID containing RIFE flownet.pkl weights "
"(default: elfgum/RIFE-4.22.lite, downloaded automatically). " "(default: elfgum/RIFE-4.22.lite, downloaded automatically). "
"Only RIFE 4.22.lite architecture is supported; other RIFE versions or " "Only RIFE 4.22.lite architecture is supported; other RIFE versions or "
"frame interpolation models are not compatible.", "frame interpolation models are not compatible.",
) )
parser.add_argument( add_argument(
"--enable-upscaling", "--enable-upscaling",
action="store_true", action="store_true",
help="Enable post-generation upscaling using Real-ESRGAN.", help="Enable post-generation upscaling using Real-ESRGAN.",
) )
parser.add_argument( add_argument(
"--upscaling-model-path", "--upscaling-model-path",
type=str, type=str,
default=SamplingParams.upscaling_model_path,
help="Local .pth file, HuggingFace repo ID, or repo_id:filename for Real-ESRGAN weights " help="Local .pth file, HuggingFace repo ID, or repo_id:filename for Real-ESRGAN weights "
"(default: ai-forever/Real-ESRGAN with RealESRGAN_x4.pth). " "(default: ai-forever/Real-ESRGAN with RealESRGAN_x4.pth). "
"Only RRDBNet (e.g. RealESRGAN_x4plus) and SRVGGNetCompact (e.g. realesr-animevideov3) " "Only RRDBNet (e.g. RealESRGAN_x4plus) and SRVGGNetCompact (e.g. realesr-animevideov3) "
"architectures are supported; other super-resolution models are not compatible. " "architectures are supported; other super-resolution models are not compatible. "
"Use 'repo_id:filename' to specify a custom weight file from a HF repo.", "Use 'repo_id:filename' to specify a custom weight file from a HF repo.",
) )
parser.add_argument( add_argument(
"--upscaling-scale", "--upscaling-scale",
type=int, type=int,
default=SamplingParams.upscaling_scale,
help="Upscaling factor (default: 4).", help="Upscaling factor (default: 4).",
) )
return parser return parser
@@ -6,6 +6,7 @@ from sglang.multimodal_gen.configs.sample.diffusers_generic import (
DiffusersGenericSamplingParams, DiffusersGenericSamplingParams,
) )
from sglang.multimodal_gen.configs.sample.flux import FluxSamplingParams from sglang.multimodal_gen.configs.sample.flux import FluxSamplingParams
from sglang.multimodal_gen.configs.sample.qwenimage import QwenImageSamplingParams
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
@@ -68,81 +69,60 @@ class TestSamplingParamsSubclass(unittest.TestCase):
DiffusersGenericSamplingParams(num_frames=0) DiffusersGenericSamplingParams(num_frames=0)
class TestNegativePromptMerge(unittest.TestCase): class TestSamplingParamsCliArgs(unittest.TestCase):
"""Regression tests for negative_prompt not being passed through CLI""" def _parse_cli_kwargs(self, argv: list[str]) -> dict:
parser = argparse.ArgumentParser()
SamplingParams.add_cli_args(parser)
args = parser.parse_args(argv)
return SamplingParams.get_cli_args(args)
def test_get_cli_args_filters_none(self): def _make_qwen_image_params(self, argv: list[str]) -> QwenImageSamplingParams:
ns = argparse.Namespace(negative_prompt=None, prompt="hello") return QwenImageSamplingParams(**self._parse_cli_kwargs(argv))
result = SamplingParams.get_cli_args(ns)
self.assertNotIn("negative_prompt", result)
self.assertEqual(result["prompt"], "hello")
def test_get_cli_args_keeps_explicit_value(self): def test_get_cli_args_drops_unset_sampling_params(self):
ns = argparse.Namespace(negative_prompt="ugly, blurry") self.assertEqual(self._parse_cli_kwargs([]), {})
result = SamplingParams.get_cli_args(ns)
self.assertEqual(result["negative_prompt"], "ugly, blurry")
def test_merge_preserves_subclass_default_when_not_explicit(self): def test_get_cli_args_keeps_explicit_sampling_params(self):
"""Without explicit_fields, value matching base default is not merged, kwargs = self._parse_cli_kwargs(
so the subclass default (empty string) is preserved.""" [
"--guidance-scale",
str(SamplingParams.guidance_scale),
"--negative-prompt",
SamplingParams.negative_prompt,
"--save-output",
]
)
self.assertEqual(kwargs["guidance_scale"], SamplingParams.guidance_scale)
self.assertEqual(kwargs["negative_prompt"], SamplingParams.negative_prompt)
self.assertTrue(kwargs["save_output"])
def test_qwen_image_cli_path_preserves_model_defaults(self):
params = self._make_qwen_image_params([])
self.assertEqual(params.negative_prompt, " ")
self.assertEqual(params.guidance_scale, 4.0)
def test_qwen_image_cli_path_allows_explicit_override_to_base_defaults(self):
params = self._make_qwen_image_params(
[
"--guidance-scale",
str(SamplingParams.guidance_scale),
"--negative-prompt",
SamplingParams.negative_prompt,
]
)
self.assertEqual(params.guidance_scale, SamplingParams.guidance_scale)
self.assertEqual(params.negative_prompt, SamplingParams.negative_prompt)
def test_merge_allows_explicit_field_matching_base_default(self):
target = DiffusersGenericSamplingParams() target = DiffusersGenericSamplingParams()
self.assertEqual(target.negative_prompt, "") user = SamplingParams(negative_prompt=SamplingParams.negative_prompt)
user = SamplingParams()
target._merge_with_user_params(user)
self.assertEqual(target.negative_prompt, "")
def test_merge_applies_different_negative_prompt(self):
target = DiffusersGenericSamplingParams()
user = SamplingParams(negative_prompt="ugly, blurry")
target._merge_with_user_params(user)
self.assertEqual(target.negative_prompt, "ugly, blurry")
def test_merge_explicit_field_matching_base_default(self):
"""Even when the user value matches the base-class default, it should
still be applied if listed in explicit_fields."""
base_default = SamplingParams.negative_prompt
target = DiffusersGenericSamplingParams()
self.assertEqual(target.negative_prompt, "")
user = SamplingParams(negative_prompt=base_default)
target._merge_with_user_params(user, explicit_fields={"negative_prompt"}) target._merge_with_user_params(user, explicit_fields={"negative_prompt"})
self.assertEqual(target.negative_prompt, base_default)
def test_cli_roundtrip_no_negative_prompt(self): self.assertEqual(target.negative_prompt, SamplingParams.negative_prompt)
"""Simulate CLI without --negative-prompt: subclass default is kept."""
ns = argparse.Namespace(negative_prompt=None, width=512, height=512)
kwargs = SamplingParams.get_cli_args(ns)
self.assertNotIn("negative_prompt", kwargs)
user = SamplingParams(**kwargs)
target = DiffusersGenericSamplingParams()
target._merge_with_user_params(user, explicit_fields=set(kwargs.keys()))
self.assertEqual(target.negative_prompt, "")
def test_cli_roundtrip_with_negative_prompt(self):
"""Simulate CLI with --negative-prompt: user value is applied."""
user_neg = "bad quality, watermark"
ns = argparse.Namespace(negative_prompt=user_neg, width=512, height=512)
kwargs = SamplingParams.get_cli_args(ns)
user = SamplingParams(**kwargs)
target = DiffusersGenericSamplingParams()
target._merge_with_user_params(user, explicit_fields=set(kwargs.keys()))
self.assertEqual(target.negative_prompt, user_neg)
def test_cli_roundtrip_with_base_default_negative_prompt(self):
"""Simulate CLI where --negative-prompt value matches the base default:
user value should still be applied (not dropped)."""
base_default = SamplingParams.negative_prompt
ns = argparse.Namespace(negative_prompt=base_default, width=512, height=512)
kwargs = SamplingParams.get_cli_args(ns)
self.assertIn("negative_prompt", kwargs)
user = SamplingParams(**kwargs)
target = DiffusersGenericSamplingParams()
target._merge_with_user_params(user, explicit_fields=set(kwargs.keys()))
self.assertEqual(target.negative_prompt, base_default)
if __name__ == "__main__": if __name__ == "__main__":