[chore] harden checkpoint quantization metadata parsing (#36922)
This commit is contained in:
@@ -4,6 +4,7 @@ from unittest import mock
|
|||||||
|
|
||||||
import torch
|
import torch
|
||||||
from torch import nn
|
from torch import nn
|
||||||
|
from transformers import PretrainedConfig
|
||||||
|
|
||||||
from sglang.multimodal_gen.configs.models.encoders.clip import CLIPVisionConfig
|
from sglang.multimodal_gen.configs.models.encoders.clip import CLIPVisionConfig
|
||||||
from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import (
|
from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import (
|
||||||
@@ -135,7 +136,7 @@ class TestImageEncoderQuantizationAdmission(unittest.TestCase):
|
|||||||
|
|
||||||
class TestImageEncoderNativeLoading(unittest.TestCase):
|
class TestImageEncoderNativeLoading(unittest.TestCase):
|
||||||
def test_bnb4_uses_shared_transformers_path_and_image_precision(self):
|
def test_bnb4_uses_shared_transformers_path_and_image_precision(self):
|
||||||
component_config = SimpleNamespace(
|
component_config = PretrainedConfig(
|
||||||
is_encoder_decoder=False,
|
is_encoder_decoder=False,
|
||||||
architectures=["CLIPVisionModelWithProjection"],
|
architectures=["CLIPVisionModelWithProjection"],
|
||||||
quantization_config={
|
quantization_config={
|
||||||
@@ -193,7 +194,7 @@ class TestImageEncoderNativeLoading(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_explicit_offload_is_rejected_before_transformers_load(self):
|
def test_explicit_offload_is_rejected_before_transformers_load(self):
|
||||||
component_config = SimpleNamespace(
|
component_config = PretrainedConfig(
|
||||||
is_encoder_decoder=False,
|
is_encoder_decoder=False,
|
||||||
architectures=["ThirdPartyVisionModel"],
|
architectures=["ThirdPartyVisionModel"],
|
||||||
quantization_config={"quant_method": "fp8"},
|
quantization_config={"quant_method": "fp8"},
|
||||||
@@ -236,7 +237,7 @@ class TestImageEncoderNativeLoading(unittest.TestCase):
|
|||||||
def to(self, *args, **kwargs):
|
def to(self, *args, **kwargs):
|
||||||
raise AssertionError("quantized component must not be moved again")
|
raise AssertionError("quantized component must not be moved again")
|
||||||
|
|
||||||
component_config = SimpleNamespace(
|
component_config = PretrainedConfig(
|
||||||
is_encoder_decoder=False,
|
is_encoder_decoder=False,
|
||||||
architectures=["ThirdPartyVisionModel"],
|
architectures=["ThirdPartyVisionModel"],
|
||||||
quantization_config={"quant_method": "fp8"},
|
quantization_config={"quant_method": "fp8"},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Pure-data helpers for quantization metadata in Hugging Face configs."""
|
"""Read quantization metadata declared by Hugging Face configurations."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -8,6 +8,8 @@ from copy import deepcopy
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Literal, Mapping, TypeAlias
|
from typing import Any, Literal, Mapping, TypeAlias
|
||||||
|
|
||||||
|
from transformers import PretrainedConfig
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CheckpointQuantSpec",
|
"CheckpointQuantSpec",
|
||||||
"QuantMetadataSource",
|
"QuantMetadataSource",
|
||||||
@@ -20,11 +22,12 @@ QuantMetadataSource: TypeAlias = Literal[
|
|||||||
"text_config.quantization_config",
|
"text_config.quantization_config",
|
||||||
"compression_config",
|
"compression_config",
|
||||||
]
|
]
|
||||||
|
ConfigMapping: TypeAlias = Mapping[str, Any] | PretrainedConfig
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class CheckpointQuantSpec:
|
class CheckpointQuantSpec:
|
||||||
"""Quantization metadata declared by a checkpoint.
|
"""Quantization metadata declared by a checkpoint configuration.
|
||||||
|
|
||||||
``declared_method`` preserves ``quant_method`` verbatim and is never inferred
|
``declared_method`` preserves ``quant_method`` verbatim and is never inferred
|
||||||
from backend-specific fields. This intentionally contains no runtime
|
from backend-specific fields. This intentionally contains no runtime
|
||||||
@@ -36,54 +39,48 @@ class CheckpointQuantSpec:
|
|||||||
source: QuantMetadataSource
|
source: QuantMetadataSource
|
||||||
|
|
||||||
|
|
||||||
def _get_field(config: object, name: str) -> Any:
|
def _as_config_mapping(value: ConfigMapping, source: str) -> Mapping[str, Any]:
|
||||||
if isinstance(config, Mapping):
|
|
||||||
return config.get(name)
|
|
||||||
return getattr(config, name, None)
|
|
||||||
|
|
||||||
|
|
||||||
def _to_metadata_dict(value: object, source: QuantMetadataSource) -> dict[str, Any]:
|
|
||||||
if isinstance(value, Mapping):
|
if isinstance(value, Mapping):
|
||||||
return deepcopy(dict(value))
|
return value
|
||||||
|
if isinstance(value, PretrainedConfig):
|
||||||
to_dict = getattr(value, "to_dict", None)
|
return value.to_dict()
|
||||||
if callable(to_dict):
|
|
||||||
metadata = to_dict()
|
|
||||||
if isinstance(metadata, Mapping):
|
|
||||||
return deepcopy(dict(metadata))
|
|
||||||
|
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"{source} must be a mapping or expose to_dict(), "
|
f"{source} must be a mapping or transformers.PretrainedConfig, "
|
||||||
f"got {type(value).__name__}"
|
f"got {type(value).__name__}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _select_hf_quant_metadata(
|
def _select_hf_quant_metadata(
|
||||||
hf_config: object,
|
hf_config: ConfigMapping,
|
||||||
) -> tuple[QuantMetadataSource, object] | None:
|
) -> tuple[QuantMetadataSource, object] | None:
|
||||||
value = _get_field(hf_config, "quantization_config")
|
config = _as_config_mapping(hf_config, "HF config")
|
||||||
|
value = config.get("quantization_config")
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return "quantization_config", value
|
return "quantization_config", value
|
||||||
|
|
||||||
text_config = _get_field(hf_config, "text_config")
|
text_config = config.get("text_config")
|
||||||
value = _get_field(text_config, "quantization_config")
|
if text_config is not None:
|
||||||
|
text_config_mapping = _as_config_mapping(text_config, "text_config")
|
||||||
|
value = text_config_mapping.get("quantization_config")
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return "text_config.quantization_config", value
|
return "text_config.quantization_config", value
|
||||||
|
|
||||||
value = _get_field(hf_config, "compression_config")
|
value = config.get("compression_config")
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return "compression_config", value
|
return "compression_config", value
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def resolve_checkpoint_quant_spec(hf_config: object) -> CheckpointQuantSpec | None:
|
def resolve_checkpoint_quant_spec(
|
||||||
"""Resolve checkpoint quantization metadata from an HF config.
|
hf_config: ConfigMapping,
|
||||||
|
) -> CheckpointQuantSpec | None:
|
||||||
|
"""Resolve quantization metadata from an HF configuration.
|
||||||
|
|
||||||
The lookup order matches SRT's checkpoint loader: top-level
|
The lookup order matches both serving runtimes: top-level
|
||||||
``quantization_config``, the text sub-config used by some multimodal
|
``quantization_config``, the text sub-config used by some multimodal
|
||||||
checkpoints, then ``compression_config``. The returned metadata is deep-copied
|
checkpoints, then ``compression_config``. Returned metadata is deep-copied
|
||||||
so callers can attach runtime-only fields without mutating the HF config.
|
so callers can attach runtime-only fields without mutating the source config.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
selected = _select_hf_quant_metadata(hf_config)
|
selected = _select_hf_quant_metadata(hf_config)
|
||||||
@@ -91,10 +88,11 @@ def resolve_checkpoint_quant_spec(hf_config: object) -> CheckpointQuantSpec | No
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
source, value = selected
|
source, value = selected
|
||||||
config = _to_metadata_dict(value, source)
|
config = _as_config_mapping(value, source)
|
||||||
declared_method = config.get("quant_method")
|
copied_config = deepcopy(dict(config))
|
||||||
|
declared_method = copied_config.get("quant_method")
|
||||||
return CheckpointQuantSpec(
|
return CheckpointQuantSpec(
|
||||||
declared_method=(declared_method if isinstance(declared_method, str) else None),
|
declared_method=(declared_method if isinstance(declared_method, str) else None),
|
||||||
config=config,
|
config=copied_config,
|
||||||
source=source,
|
source=source,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
import torch
|
import torch
|
||||||
import torch.nn as nn
|
import torch.nn as nn
|
||||||
|
from transformers import PretrainedConfig
|
||||||
|
|
||||||
from sglang.srt.configs.device_config import DeviceConfig
|
from sglang.srt.configs.device_config import DeviceConfig
|
||||||
from sglang.srt.configs.load_config import LoadConfig
|
from sglang.srt.configs.load_config import LoadConfig
|
||||||
@@ -657,7 +658,7 @@ class TestModelOptFp4LoaderSelection(CustomTestCase):
|
|||||||
quantization="modelopt_fp4",
|
quantization="modelopt_fp4",
|
||||||
is_draft_model=True,
|
is_draft_model=True,
|
||||||
is_draft_quantization_explicit=is_explicit,
|
is_draft_quantization_explicit=is_explicit,
|
||||||
hf_config=SimpleNamespace(
|
hf_config=PretrainedConfig(
|
||||||
quantization_config={
|
quantization_config={
|
||||||
"quant_algo": "NVFP4",
|
"quant_algo": "NVFP4",
|
||||||
"group_size": 16,
|
"group_size": 16,
|
||||||
@@ -757,7 +758,7 @@ class TestModelOptMixedPrecisionConfig(CustomTestCase):
|
|||||||
with self.subTest(inline_config=inline_config):
|
with self.subTest(inline_config=inline_config):
|
||||||
model_config = SimpleNamespace(
|
model_config = SimpleNamespace(
|
||||||
quantization="modelopt_mixed",
|
quantization="modelopt_mixed",
|
||||||
hf_config=SimpleNamespace(
|
hf_config=PretrainedConfig(
|
||||||
quantization_config=inline_config,
|
quantization_config=inline_config,
|
||||||
),
|
),
|
||||||
model_path=model_path,
|
model_path=model_path,
|
||||||
@@ -787,7 +788,7 @@ class TestModelOptMixedPrecisionConfig(CustomTestCase):
|
|||||||
}
|
}
|
||||||
model_config = SimpleNamespace(
|
model_config = SimpleNamespace(
|
||||||
quantization="modelopt_mixed",
|
quantization="modelopt_mixed",
|
||||||
hf_config=SimpleNamespace(
|
hf_config=PretrainedConfig(
|
||||||
quantization_config={
|
quantization_config={
|
||||||
"quant_method": "modelopt_mixed",
|
"quant_method": "modelopt_mixed",
|
||||||
"quant_algo": "MIXED_PRECISION",
|
"quant_algo": "MIXED_PRECISION",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from transformers import PretrainedConfig
|
||||||
|
|
||||||
from sglang.srt.layers.modelopt_utils import canonicalize_modelopt_quant_algo
|
from sglang.srt.layers.modelopt_utils import canonicalize_modelopt_quant_algo
|
||||||
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
||||||
from sglang.srt.model_loader.checkpoint_quantization import (
|
from sglang.srt.model_loader.checkpoint_quantization import (
|
||||||
@@ -14,19 +16,6 @@ from sglang.test.test_utils import CustomTestCase
|
|||||||
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
|
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
|
||||||
|
|
||||||
|
|
||||||
class _ConfigObject:
|
|
||||||
def __init__(self, **values):
|
|
||||||
self.__dict__.update(values)
|
|
||||||
|
|
||||||
|
|
||||||
class _QuantConfigObject:
|
|
||||||
def __init__(self, values):
|
|
||||||
self._values = values
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return self._values
|
|
||||||
|
|
||||||
|
|
||||||
class TestResolveCheckpointQuantSpec(CustomTestCase):
|
class TestResolveCheckpointQuantSpec(CustomTestCase):
|
||||||
def test_modelopt_quant_algo_canonicalization(self):
|
def test_modelopt_quant_algo_canonicalization(self):
|
||||||
cases = {
|
cases = {
|
||||||
@@ -75,9 +64,9 @@ class TestResolveCheckpointQuantSpec(CustomTestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_text_config_fallback_supports_config_objects(self):
|
def test_text_config_fallback_supports_pretrained_configs(self):
|
||||||
config = _ConfigObject(
|
config = PretrainedConfig(
|
||||||
text_config=_ConfigObject(
|
text_config=PretrainedConfig(
|
||||||
quantization_config={"quant_method": "gptq", "bits": 4}
|
quantization_config={"quant_method": "gptq", "bits": 4}
|
||||||
),
|
),
|
||||||
compression_config={"quant_method": "compressed-tensors"},
|
compression_config={"quant_method": "compressed-tensors"},
|
||||||
@@ -90,7 +79,7 @@ class TestResolveCheckpointQuantSpec(CustomTestCase):
|
|||||||
self.assertEqual(spec.source, "text_config.quantization_config")
|
self.assertEqual(spec.source, "text_config.quantization_config")
|
||||||
|
|
||||||
def test_compression_config_fallback(self):
|
def test_compression_config_fallback(self):
|
||||||
config = _ConfigObject(
|
config = PretrainedConfig(
|
||||||
compression_config={"quant_method": "compressed-tensors"}
|
compression_config={"quant_method": "compressed-tensors"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,18 +103,6 @@ class TestResolveCheckpointQuantSpec(CustomTestCase):
|
|||||||
self.assertIsNone(spec.declared_method)
|
self.assertIsNone(spec.declared_method)
|
||||||
self.assertEqual(spec.config["quant_algo"], "FP8")
|
self.assertEqual(spec.config["quant_algo"], "FP8")
|
||||||
|
|
||||||
def test_quant_config_object_is_converted(self):
|
|
||||||
config = _ConfigObject(
|
|
||||||
quantization_config=_QuantConfigObject(
|
|
||||||
{"quant_method": "bitsandbytes", "load_in_4bit": True}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
spec = resolve_checkpoint_quant_spec(config)
|
|
||||||
|
|
||||||
self.assertIsNotNone(spec)
|
|
||||||
self.assertEqual(spec.config["load_in_4bit"], True)
|
|
||||||
|
|
||||||
def test_lookup_priority_matches_srt_loader(self):
|
def test_lookup_priority_matches_srt_loader(self):
|
||||||
config = {
|
config = {
|
||||||
"quantization_config": {},
|
"quantization_config": {},
|
||||||
|
|||||||
Reference in New Issue
Block a user