[diffusion] feat: add per-component quantization overrides (#36084)
This commit is contained in:
@@ -130,6 +130,9 @@ class ComponentLoader(ABC):
|
|||||||
# components may fall back when that global choice is incompatible; an
|
# components may fall back when that global choice is incompatible; an
|
||||||
# explicit --component-attention-backends entry remains strict.
|
# explicit --component-attention-backends entry remains strict.
|
||||||
allow_global_attention_backend_fallback = True
|
allow_global_attention_backend_fallback = True
|
||||||
|
# Gates only --component-quantizations.<name>. Quantization declared by a
|
||||||
|
# checkpoint is discovered and admitted by the component's normal loader.
|
||||||
|
supports_online_quantization_override = False
|
||||||
|
|
||||||
_loaders_registered = False
|
_loaders_registered = False
|
||||||
|
|
||||||
@@ -227,6 +230,17 @@ class ComponentLoader(ABC):
|
|||||||
If all of the above methods failed, an error will be thrown
|
If all of the above methods failed, an error will be thrown
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
component_quantization = server_args.component_quantizations.get(component_name)
|
||||||
|
if (
|
||||||
|
component_quantization is not None
|
||||||
|
and not self.supports_online_quantization_override
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f"{component_name!r} does not support an explicit quantization "
|
||||||
|
"override; "
|
||||||
|
"use a self-describing quantized component checkpoint when supported"
|
||||||
|
)
|
||||||
|
|
||||||
gpu_mem_before_loading = current_platform.get_available_gpu_memory()
|
gpu_mem_before_loading = current_platform.get_available_gpu_memory()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Loading %s from %s. avail mem: %.2f GB",
|
"Loading %s from %s. avail mem: %.2f GB",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class ImageEncoderLoader(TextEncoderLoader):
|
|||||||
component_model_path,
|
component_model_path,
|
||||||
component_weights_path,
|
component_weights_path,
|
||||||
component_name,
|
component_name,
|
||||||
|
server_args.component_quantizations.get(component_name),
|
||||||
)
|
)
|
||||||
# real dims are populated now; resolve fold vs replicate
|
# real dims are populated now; resolve fold vs replicate
|
||||||
finalize_encoder_folding(
|
finalize_encoder_folding(
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ from sglang.srt.environ import envs
|
|||||||
|
|
||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
_ONLINE_ENCODER_QUANTIZATIONS = frozenset({"fp8", "kitchen_int8", "mxfp4"})
|
||||||
|
|
||||||
_TRANSFORMERS_ENCODER_ONLY_CLASSES = {
|
_TRANSFORMERS_ENCODER_ONLY_CLASSES = {
|
||||||
"T5EncoderModel": transformers.T5EncoderModel,
|
"T5EncoderModel": transformers.T5EncoderModel,
|
||||||
"T5Model": transformers.T5EncoderModel,
|
"T5Model": transformers.T5EncoderModel,
|
||||||
@@ -159,8 +161,14 @@ def _configure_encoder_quantization(
|
|||||||
component_model_path: str,
|
component_model_path: str,
|
||||||
component_weights_path: str,
|
component_weights_path: str,
|
||||||
component_name: str,
|
component_name: str,
|
||||||
|
explicit_quantization: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if getattr(model_cls, "manages_checkpoint_quantization", False):
|
if getattr(model_cls, "manages_checkpoint_quantization", False):
|
||||||
|
if explicit_quantization is not None:
|
||||||
|
raise ComponentCheckpointUnsupportedError(
|
||||||
|
f"{component_name!r} manages its own checkpoint quantization and "
|
||||||
|
"does not support an online quantization override"
|
||||||
|
)
|
||||||
# Preserve model-owned formats such as Ideogram's bitsandbytes state.
|
# Preserve model-owned formats such as Ideogram's bitsandbytes state.
|
||||||
# Those models parse metadata, construct layers, and attach quant states
|
# Those models parse metadata, construct layers, and attach quant states
|
||||||
# themselves; running the generic lifecycle as well would process twice.
|
# themselves; running the generic lifecycle as well would process twice.
|
||||||
@@ -182,6 +190,24 @@ def _configure_encoder_quantization(
|
|||||||
f"Cannot configure checkpoint quantization for {component_name!r}: {error}"
|
f"Cannot configure checkpoint quantization for {component_name!r}: {error}"
|
||||||
) from error
|
) from error
|
||||||
model_config.quant_config = quant_config
|
model_config.quant_config = quant_config
|
||||||
|
if explicit_quantization is not None:
|
||||||
|
if quant_config is not None:
|
||||||
|
raise ComponentCheckpointUnsupportedError(
|
||||||
|
f"{component_name!r} already declares checkpoint quantization; "
|
||||||
|
"drop the explicit online quantization override"
|
||||||
|
)
|
||||||
|
if explicit_quantization not in _ONLINE_ENCODER_QUANTIZATIONS:
|
||||||
|
raise ComponentCheckpointUnsupportedError(
|
||||||
|
f"Online quantization {explicit_quantization!r} is not supported "
|
||||||
|
f"for native encoders; choose one of "
|
||||||
|
f"{sorted(_ONLINE_ENCODER_QUANTIZATIONS)}"
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.layers.quantization import (
|
||||||
|
get_quantization_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
model_config.quant_config = get_quantization_config(explicit_quantization)()
|
||||||
|
quant_config = model_config.quant_config
|
||||||
if quant_config is None:
|
if quant_config is None:
|
||||||
return
|
return
|
||||||
if not issubclass(model_cls, EncoderTensorParallelMixin):
|
if not issubclass(model_cls, EncoderTensorParallelMixin):
|
||||||
@@ -198,6 +224,7 @@ def _resolve_and_configure_encoder_quantization(
|
|||||||
component_model_path: str,
|
component_model_path: str,
|
||||||
component_weights_path: str,
|
component_weights_path: str,
|
||||||
component_name: str,
|
component_name: str,
|
||||||
|
explicit_quantization: str | None = None,
|
||||||
) -> type[nn.Module]:
|
) -> type[nn.Module]:
|
||||||
architectures = getattr(model_config, "architectures", [])
|
architectures = getattr(model_config, "architectures", [])
|
||||||
try:
|
try:
|
||||||
@@ -218,6 +245,11 @@ def _resolve_and_configure_encoder_quantization(
|
|||||||
f"Cannot parse checkpoint quantization for {component_name!r}: "
|
f"Cannot parse checkpoint quantization for {component_name!r}: "
|
||||||
f"{quantization_error}"
|
f"{quantization_error}"
|
||||||
) from quantization_error
|
) from quantization_error
|
||||||
|
if explicit_quantization is not None and quant_config is None:
|
||||||
|
raise ComponentCheckpointUnsupportedError(
|
||||||
|
f"Online quantization for {component_name!r} requires an in-tree "
|
||||||
|
f"native encoder; unsupported architectures: {architectures}"
|
||||||
|
) from resolution_error
|
||||||
if quant_config is None:
|
if quant_config is None:
|
||||||
raise
|
raise
|
||||||
raise ComponentCheckpointUnsupportedError(
|
raise ComponentCheckpointUnsupportedError(
|
||||||
@@ -232,6 +264,7 @@ def _resolve_and_configure_encoder_quantization(
|
|||||||
component_model_path,
|
component_model_path,
|
||||||
component_weights_path,
|
component_weights_path,
|
||||||
component_name,
|
component_name,
|
||||||
|
explicit_quantization,
|
||||||
)
|
)
|
||||||
return model_cls
|
return model_cls
|
||||||
|
|
||||||
@@ -356,6 +389,15 @@ class TextEncoderLoader(ComponentLoader):
|
|||||||
|
|
||||||
component_names = ["text_encoder"]
|
component_names = ["text_encoder"]
|
||||||
expected_library = "transformers"
|
expected_library = "transformers"
|
||||||
|
supports_online_quantization_override = True
|
||||||
|
|
||||||
|
def should_raise_customized_load_error(
|
||||||
|
self, server_args: ServerArgs, component_name: str
|
||||||
|
) -> bool:
|
||||||
|
return (
|
||||||
|
super().should_raise_customized_load_error(server_args, component_name)
|
||||||
|
or component_name in server_args.component_quantizations
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_model_weights_path(
|
def resolve_model_weights_path(
|
||||||
@@ -602,6 +644,7 @@ class TextEncoderLoader(ComponentLoader):
|
|||||||
component_model_path,
|
component_model_path,
|
||||||
component_weights_path,
|
component_weights_path,
|
||||||
component_name,
|
component_name,
|
||||||
|
server_args.component_quantizations.get(component_name),
|
||||||
)
|
)
|
||||||
if issubclass(model_cls, EncoderTensorParallelMixin):
|
if issubclass(model_cls, EncoderTensorParallelMixin):
|
||||||
model_cls.configure_component_paths(
|
model_cls.configure_component_paths(
|
||||||
|
|||||||
+11
-1
@@ -100,8 +100,10 @@ def _server_args_for_transformer_component(
|
|||||||
) -> ServerArgs:
|
) -> ServerArgs:
|
||||||
"""Mask global quantized override flags for secondary transformer components."""
|
"""Mask global quantized override flags for secondary transformer components."""
|
||||||
component_weights_path = server_args.component_weights_paths.get(component_name)
|
component_weights_path = server_args.component_weights_paths.get(component_name)
|
||||||
if component_weights_path is not None:
|
component_quantization = server_args.component_quantizations.get(component_name)
|
||||||
|
if component_weights_path is not None or component_quantization is not None:
|
||||||
component_server_args = copy.copy(server_args)
|
component_server_args = copy.copy(server_args)
|
||||||
|
if component_weights_path is not None:
|
||||||
component_server_args.transformer_weights_path = component_weights_path
|
component_server_args.transformer_weights_path = component_weights_path
|
||||||
component_server_args.nunchaku_config = None
|
component_server_args.nunchaku_config = None
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -109,6 +111,13 @@ def _server_args_for_transformer_component(
|
|||||||
component_name,
|
component_name,
|
||||||
component_weights_path,
|
component_weights_path,
|
||||||
)
|
)
|
||||||
|
if component_quantization is not None:
|
||||||
|
component_server_args.quantization = component_quantization
|
||||||
|
logger.info(
|
||||||
|
"Using quantization override %s for %s",
|
||||||
|
component_quantization,
|
||||||
|
component_name,
|
||||||
|
)
|
||||||
return component_server_args
|
return component_server_args
|
||||||
|
|
||||||
if component_name not in ("transformer_2", "unconditional_transformer"):
|
if component_name not in ("transformer_2", "unconditional_transformer"):
|
||||||
@@ -135,6 +144,7 @@ class TransformerLoader(ComponentLoader):
|
|||||||
"""Shared loader for (video/audio) DiT transformers."""
|
"""Shared loader for (video/audio) DiT transformers."""
|
||||||
|
|
||||||
allow_global_attention_backend_fallback = False
|
allow_global_attention_backend_fallback = False
|
||||||
|
supports_online_quantization_override = True
|
||||||
|
|
||||||
component_names = [
|
component_names = [
|
||||||
"transformer",
|
"transformer",
|
||||||
|
|||||||
@@ -306,6 +306,9 @@ class ServerArgs(DisaggServerArgsMixin):
|
|||||||
component_paths: dict[str, str] = field(default_factory=dict)
|
component_paths: dict[str, str] = field(default_factory=dict)
|
||||||
# Exact weight-file overrides retain the base component configuration.
|
# Exact weight-file overrides retain the base component configuration.
|
||||||
component_weights_paths: dict[str, str] = field(default_factory=dict)
|
component_weights_paths: dict[str, str] = field(default_factory=dict)
|
||||||
|
# Explicit quantization override for one component. Self-describing
|
||||||
|
# checkpoints remain auto-detected and do not need this override.
|
||||||
|
component_quantizations: dict[str, str] = field(default_factory=dict)
|
||||||
# Optional LTX-2.5 decoder is large enough to load only when requested.
|
# Optional LTX-2.5 decoder is large enough to load only when requested.
|
||||||
load_diffusion_decoder: bool = False
|
load_diffusion_decoder: bool = False
|
||||||
|
|
||||||
@@ -1722,6 +1725,22 @@ class ServerArgs(DisaggServerArgsMixin):
|
|||||||
component_weights_paths[component] = path
|
component_weights_paths[component] = path
|
||||||
self.component_paths = component_paths
|
self.component_paths = component_paths
|
||||||
self.component_weights_paths = component_weights_paths
|
self.component_weights_paths = component_weights_paths
|
||||||
|
normalized_quantizations: dict[str, str] = {}
|
||||||
|
for component, quantization in self.component_quantizations.items():
|
||||||
|
component = str(component).strip().replace("-", "_")
|
||||||
|
quantization = str(quantization).strip().lower()
|
||||||
|
if not component or not quantization:
|
||||||
|
raise ValueError(
|
||||||
|
"Component quantization entries require a component and method"
|
||||||
|
)
|
||||||
|
previous = normalized_quantizations.get(component)
|
||||||
|
if previous is not None and previous != quantization:
|
||||||
|
raise ValueError(
|
||||||
|
f"Conflicting quantization overrides for {component!r}: "
|
||||||
|
f"{previous!r} and {quantization!r}"
|
||||||
|
)
|
||||||
|
normalized_quantizations[component] = quantization
|
||||||
|
self.component_quantizations = normalized_quantizations
|
||||||
|
|
||||||
# Convert string disagg_role to enum (from CLI/config)
|
# Convert string disagg_role to enum (from CLI/config)
|
||||||
if isinstance(self.disagg_role, str):
|
if isinstance(self.disagg_role, str):
|
||||||
@@ -2833,6 +2852,21 @@ class ServerArgs(DisaggServerArgsMixin):
|
|||||||
alias_suffix="-weights-path",
|
alias_suffix="-weights-path",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_component_quantizations(
|
||||||
|
cls,
|
||||||
|
unknown_args: list[str],
|
||||||
|
) -> tuple[dict[str, str], list[str]]:
|
||||||
|
"""Extract explicit per-component quantization methods."""
|
||||||
|
return cls._extract_dynamic_component_map(
|
||||||
|
unknown_args,
|
||||||
|
option_prefixes=(
|
||||||
|
"--component-quantizations.",
|
||||||
|
"--component_quantizations.",
|
||||||
|
),
|
||||||
|
alias_suffix="-quantization",
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_component_attention_backends(
|
def _extract_component_attention_backends(
|
||||||
unknown_args: list[str],
|
unknown_args: list[str],
|
||||||
@@ -2880,9 +2914,12 @@ class ServerArgs(DisaggServerArgsMixin):
|
|||||||
if unknown_args is None:
|
if unknown_args is None:
|
||||||
unknown_args = []
|
unknown_args = []
|
||||||
|
|
||||||
|
dynamic_quantizations, remaining = cls._extract_component_quantizations(
|
||||||
|
unknown_args
|
||||||
|
)
|
||||||
# Extract the more specific weights suffix before the generic path alias.
|
# Extract the more specific weights suffix before the generic path alias.
|
||||||
dynamic_weights_paths, remaining = cls._extract_component_weights_paths(
|
dynamic_weights_paths, remaining = cls._extract_component_weights_paths(
|
||||||
unknown_args
|
remaining
|
||||||
)
|
)
|
||||||
dynamic_paths, remaining = cls._extract_component_paths(remaining)
|
dynamic_paths, remaining = cls._extract_component_paths(remaining)
|
||||||
dynamic_attention_backends, remaining = (
|
dynamic_attention_backends, remaining = (
|
||||||
@@ -2915,6 +2952,11 @@ class ServerArgs(DisaggServerArgsMixin):
|
|||||||
existing.update(dynamic_weights_paths)
|
existing.update(dynamic_weights_paths)
|
||||||
provided_args["component_weights_paths"] = existing
|
provided_args["component_weights_paths"] = existing
|
||||||
explicit_arg_names.add("component_weights_paths")
|
explicit_arg_names.add("component_weights_paths")
|
||||||
|
if dynamic_quantizations:
|
||||||
|
existing = dict(provided_args.get("component_quantizations") or {})
|
||||||
|
existing.update(dynamic_quantizations)
|
||||||
|
provided_args["component_quantizations"] = existing
|
||||||
|
explicit_arg_names.add("component_quantizations")
|
||||||
if dynamic_attention_backends:
|
if dynamic_attention_backends:
|
||||||
existing = cls._parse_component_attention_backend_map(
|
existing = cls._parse_component_attention_backend_map(
|
||||||
provided_args.get("component_attention_backends")
|
provided_args.get("component_attention_backends")
|
||||||
|
|||||||
@@ -510,6 +510,7 @@ class TestIdeogram4(unittest.TestCase):
|
|||||||
transformer_weights_path="/unused/override.safetensors",
|
transformer_weights_path="/unused/override.safetensors",
|
||||||
nunchaku_config={"enabled": True},
|
nunchaku_config={"enabled": True},
|
||||||
component_weights_paths={},
|
component_weights_paths={},
|
||||||
|
component_quantizations={},
|
||||||
)
|
)
|
||||||
component_args = _server_args_for_transformer_component(
|
component_args = _server_args_for_transformer_component(
|
||||||
server_args, "unconditional_transformer"
|
server_args, "unconditional_transformer"
|
||||||
@@ -530,6 +531,7 @@ class TestIdeogram4(unittest.TestCase):
|
|||||||
"ideogram4_unconditional_nvfp4_mixed.safetensors"
|
"ideogram4_unconditional_nvfp4_mixed.safetensors"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
component_quantizations={"unconditional_transformer": "fp8"},
|
||||||
)
|
)
|
||||||
|
|
||||||
component_args = _server_args_for_transformer_component(
|
component_args = _server_args_for_transformer_component(
|
||||||
@@ -543,6 +545,7 @@ class TestIdeogram4(unittest.TestCase):
|
|||||||
"/ckpt/diffusion_models/ideogram4_unconditional_nvfp4_mixed.safetensors",
|
"/ckpt/diffusion_models/ideogram4_unconditional_nvfp4_mixed.safetensors",
|
||||||
)
|
)
|
||||||
self.assertIsNone(component_args.nunchaku_config)
|
self.assertIsNone(component_args.nunchaku_config)
|
||||||
|
self.assertEqual(component_args.quantization, "fp8")
|
||||||
|
|
||||||
def test_ideogram_nvfp4_unconditional_transformer_path_uses_sibling_file(self):
|
def test_ideogram_nvfp4_unconditional_transformer_path_uses_sibling_file(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class TestImageEncoderQuantizationAdmission(unittest.TestCase):
|
|||||||
native_only_components=(),
|
native_only_components=(),
|
||||||
),
|
),
|
||||||
component_weights_paths={},
|
component_weights_paths={},
|
||||||
|
component_quantizations={},
|
||||||
encoder_parallel="replicate",
|
encoder_parallel="replicate",
|
||||||
resolve_component_attention_backend=lambda _name: (None, None),
|
resolve_component_attention_backend=lambda _name: (None, None),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -494,6 +494,9 @@ class TestServerArgsPathExpansion(unittest.TestCase):
|
|||||||
"--component-weights-paths.text_encoder",
|
"--component-weights-paths.text_encoder",
|
||||||
"owner/repo/text_encoder.safetensors",
|
"owner/repo/text_encoder.safetensors",
|
||||||
"--image-encoder-weights-path=/custom/image_encoder.safetensors",
|
"--image-encoder-weights-path=/custom/image_encoder.safetensors",
|
||||||
|
"--component-quantizations.text_encoder",
|
||||||
|
"kitchen_int8",
|
||||||
|
"--transformer-quantization=fp8",
|
||||||
"--component-attention-backends.transformer",
|
"--component-attention-backends.transformer",
|
||||||
"fa3",
|
"fa3",
|
||||||
]
|
]
|
||||||
@@ -536,6 +539,10 @@ class TestServerArgsPathExpansion(unittest.TestCase):
|
|||||||
{"transformer": "fa"},
|
{"transformer": "fa"},
|
||||||
server_args.component_attention_backends,
|
server_args.component_attention_backends,
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{"text_encoder": "kitchen_int8", "transformer": "fp8"},
|
||||||
|
server_args.component_quantizations,
|
||||||
|
)
|
||||||
|
|
||||||
def test_serve_cli_defaults_warmup_on(self):
|
def test_serve_cli_defaults_warmup_on(self):
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.cli.serve import (
|
from sglang.multimodal_gen.runtime.entrypoints.cli.serve import (
|
||||||
|
|||||||
@@ -334,6 +334,23 @@ class TestTextEncoderQuantization(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertIs(model_config.quant_config, self.serialized)
|
self.assertIs(model_config.quant_config, self.serialized)
|
||||||
|
|
||||||
|
def test_explicit_online_quantization_configures_native_encoder(self):
|
||||||
|
model_config = SimpleNamespace(quant_config=None)
|
||||||
|
self.get_quant_config.return_value = None
|
||||||
|
|
||||||
|
_configure_encoder_quantization(
|
||||||
|
model_config,
|
||||||
|
TextEncoder,
|
||||||
|
{},
|
||||||
|
"/model/text_encoder",
|
||||||
|
"/model/text_encoder",
|
||||||
|
"text_encoder",
|
||||||
|
explicit_quantization="kitchen_int8",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsInstance(model_config.quant_config, KitchenInt8Config)
|
||||||
|
self.assertFalse(model_config.quant_config.is_checkpoint_int8_serialized)
|
||||||
|
|
||||||
def test_weight_file_metadata_configures_native_encoder(self):
|
def test_weight_file_metadata_configures_native_encoder(self):
|
||||||
model_config = SimpleNamespace(quant_config=None)
|
model_config = SimpleNamespace(quant_config=None)
|
||||||
self.get_quant_config.return_value = None
|
self.get_quant_config.return_value = None
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class _FakeServerArgs:
|
|||||||
self.revision = "test-revision"
|
self.revision = "test-revision"
|
||||||
self.trust_remote_code = True
|
self.trust_remote_code = True
|
||||||
self.layerwise_components = set()
|
self.layerwise_components = set()
|
||||||
|
self.component_quantizations = {}
|
||||||
|
|
||||||
def resolve_component_attention_backend(self, _component_name):
|
def resolve_component_attention_backend(self, _component_name):
|
||||||
return None, None
|
return None, None
|
||||||
|
|||||||
Reference in New Issue
Block a user