[diffusion] feat: Add --model-id for config resolution; deprecate model_detectors (#19607)
This commit is contained in:
@@ -245,10 +245,29 @@ def get_model_short_name(model_id: str) -> str:
|
|||||||
return model_id
|
return model_id
|
||||||
|
|
||||||
|
|
||||||
def _get_config_info(model_path: str) -> Optional[ConfigInfo]:
|
@lru_cache(maxsize=1)
|
||||||
|
def _get_config_info(
|
||||||
|
model_path: str, model_id: Optional[str] = None
|
||||||
|
) -> Optional[ConfigInfo]:
|
||||||
"""
|
"""
|
||||||
Gets the ConfigInfo for a given model path using mappings and detectors.
|
Gets the ConfigInfo for a given model path using mappings and detectors.
|
||||||
"""
|
"""
|
||||||
|
all_model_hf_paths = sorted(_MODEL_HF_PATH_TO_NAME.keys(), key=len, reverse=True)
|
||||||
|
|
||||||
|
# 0. Explicit model_id override: match by short name
|
||||||
|
if model_id is not None:
|
||||||
|
model_id_lower = model_id.lower()
|
||||||
|
for registered_hf_id in all_model_hf_paths:
|
||||||
|
if get_model_short_name(registered_hf_id).lower() == model_id_lower:
|
||||||
|
logger.debug(
|
||||||
|
f"Resolved model via explicit --model-id '{model_id}' → '{registered_hf_id}'."
|
||||||
|
)
|
||||||
|
return _CONFIG_REGISTRY.get(_MODEL_HF_PATH_TO_NAME[registered_hf_id])
|
||||||
|
logger.warning(
|
||||||
|
f"--model-id '{model_id}' did not match any registered model; "
|
||||||
|
"falling back to automatic detection."
|
||||||
|
)
|
||||||
|
|
||||||
# 1. Exact match
|
# 1. Exact match
|
||||||
if model_path in _MODEL_HF_PATH_TO_NAME:
|
if model_path in _MODEL_HF_PATH_TO_NAME:
|
||||||
model_id = _MODEL_HF_PATH_TO_NAME[model_path]
|
model_id = _MODEL_HF_PATH_TO_NAME[model_path]
|
||||||
@@ -256,12 +275,11 @@ def _get_config_info(model_path: str) -> Optional[ConfigInfo]:
|
|||||||
return _CONFIG_REGISTRY.get(model_id)
|
return _CONFIG_REGISTRY.get(model_id)
|
||||||
|
|
||||||
# 2. Partial match: find the best (longest) match against all registered model hf paths.
|
# 2. Partial match: find the best (longest) match against all registered model hf paths.
|
||||||
model_name = get_model_short_name(model_path.lower())
|
model_short_name = get_model_short_name(model_path.lower())
|
||||||
all_model_hf_paths = sorted(_MODEL_HF_PATH_TO_NAME.keys(), key=len, reverse=True)
|
|
||||||
for registered_model_hf_id in all_model_hf_paths:
|
for registered_model_hf_id in all_model_hf_paths:
|
||||||
registered_model_name = get_model_short_name(registered_model_hf_id.lower())
|
registered_model_name = get_model_short_name(registered_model_hf_id.lower())
|
||||||
|
|
||||||
if registered_model_name == model_name:
|
if registered_model_name == model_short_name:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Resolved model name '{registered_model_hf_id}' from partial path match."
|
f"Resolved model name '{registered_model_hf_id}' from partial path match."
|
||||||
)
|
)
|
||||||
@@ -310,7 +328,7 @@ class ModelInfo:
|
|||||||
pipeline_config_cls: Type[PipelineConfig]
|
pipeline_config_cls: Type[PipelineConfig]
|
||||||
|
|
||||||
|
|
||||||
def _get_diffusers_model_info(model_path: str) -> ModelInfo:
|
def _get_diffusers_model_info() -> ModelInfo:
|
||||||
"""
|
"""
|
||||||
Get model info for diffusers backend.
|
Get model info for diffusers backend.
|
||||||
|
|
||||||
@@ -337,6 +355,7 @@ def _get_diffusers_model_info(model_path: str) -> ModelInfo:
|
|||||||
def get_model_info(
|
def get_model_info(
|
||||||
model_path: str,
|
model_path: str,
|
||||||
backend: Optional[Union[str, "Backend"]] = None,
|
backend: Optional[Union[str, "Backend"]] = None,
|
||||||
|
model_id: Optional[str] = None,
|
||||||
) -> Optional[ModelInfo]:
|
) -> Optional[ModelInfo]:
|
||||||
"""
|
"""
|
||||||
Resolves all necessary classes (pipeline, sampling, config) for a given model path.
|
Resolves all necessary classes (pipeline, sampling, config) for a given model path.
|
||||||
@@ -365,7 +384,7 @@ def get_model_info(
|
|||||||
logger.info(
|
logger.info(
|
||||||
"Using diffusers backend for model '%s' (explicitly requested)", model_path
|
"Using diffusers backend for model '%s' (explicitly requested)", model_path
|
||||||
)
|
)
|
||||||
return _get_diffusers_model_info(model_path)
|
return _get_diffusers_model_info()
|
||||||
|
|
||||||
# For AUTO or SGLANG backend, try native implementation first
|
# For AUTO or SGLANG backend, try native implementation first
|
||||||
# 1. Discover all available pipeline classes and cache them
|
# 1. Discover all available pipeline classes and cache them
|
||||||
@@ -389,7 +408,7 @@ def get_model_info(
|
|||||||
logger.error(f"Could not read model config for '{model_path}': {e}")
|
logger.error(f"Could not read model config for '{model_path}': {e}")
|
||||||
if backend == Backend.AUTO:
|
if backend == Backend.AUTO:
|
||||||
logger.info("Falling back to diffusers backend")
|
logger.info("Falling back to diffusers backend")
|
||||||
return _get_diffusers_model_info(model_path)
|
return _get_diffusers_model_info()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
pipeline_class_name = config.get("_class_name")
|
pipeline_class_name = config.get("_class_name")
|
||||||
@@ -399,7 +418,7 @@ def get_model_info(
|
|||||||
)
|
)
|
||||||
if backend == Backend.AUTO:
|
if backend == Backend.AUTO:
|
||||||
logger.info("Falling back to diffusers backend")
|
logger.info("Falling back to diffusers backend")
|
||||||
return _get_diffusers_model_info(model_path)
|
return _get_diffusers_model_info()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
pipeline_cls = _PIPELINE_REGISTRY.get(pipeline_class_name)
|
pipeline_cls = _PIPELINE_REGISTRY.get(pipeline_class_name)
|
||||||
@@ -409,7 +428,7 @@ def get_model_info(
|
|||||||
f"Pipeline class '{pipeline_class_name}' specified in '{model_path}' has no native sglang support. "
|
f"Pipeline class '{pipeline_class_name}' specified in '{model_path}' has no native sglang support. "
|
||||||
f"Falling back to diffusers backend."
|
f"Falling back to diffusers backend."
|
||||||
)
|
)
|
||||||
return _get_diffusers_model_info(model_path)
|
return _get_diffusers_model_info()
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Pipeline class '{pipeline_class_name}' specified in '{model_path}' is not a registered EntryClass in the framework. "
|
f"Pipeline class '{pipeline_class_name}' specified in '{model_path}' is not a registered EntryClass in the framework. "
|
||||||
@@ -419,14 +438,14 @@ def get_model_info(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 3. Get configuration classes (sampling, pipeline config)
|
# 3. Get configuration classes (sampling, pipeline config)
|
||||||
config_info = _get_config_info(model_path)
|
config_info = _get_config_info(model_path, model_id=model_id)
|
||||||
if not config_info:
|
if not config_info:
|
||||||
if backend == Backend.AUTO:
|
if backend == Backend.AUTO:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Could not resolve native configuration for model '{model_path}'. "
|
f"Could not resolve native configuration for model '{model_path}'. "
|
||||||
f"Falling back to diffusers backend."
|
f"Falling back to diffusers backend."
|
||||||
)
|
)
|
||||||
return _get_diffusers_model_info(model_path)
|
return _get_diffusers_model_info()
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Could not resolve configuration for model '{model_path}'. "
|
f"Could not resolve configuration for model '{model_path}'. "
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ async def get_models(request: Request):
|
|||||||
from sglang.multimodal_gen.registry import get_model_info
|
from sglang.multimodal_gen.registry import get_model_info
|
||||||
|
|
||||||
server_args: ServerArgs = request.app.state.server_args
|
server_args: ServerArgs = request.app.state.server_args
|
||||||
model_info = get_model_info(server_args.model_path)
|
model_info = get_model_info(server_args.model_path, model_id=server_args.model_id)
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
"model_path": server_args.model_path,
|
"model_path": server_args.model_path,
|
||||||
|
|||||||
@@ -180,7 +180,11 @@ async def available_models():
|
|||||||
if not server_args:
|
if not server_args:
|
||||||
raise HTTPException(status_code=500, detail="Server args not initialized")
|
raise HTTPException(status_code=500, detail="Server args not initialized")
|
||||||
|
|
||||||
model_info = get_model_info(server_args.model_path, backend=server_args.backend)
|
model_info = get_model_info(
|
||||||
|
server_args.model_path,
|
||||||
|
backend=server_args.backend,
|
||||||
|
model_id=server_args.model_id,
|
||||||
|
)
|
||||||
|
|
||||||
card_kwargs = {
|
card_kwargs = {
|
||||||
"id": server_args.model_path,
|
"id": server_args.model_path,
|
||||||
@@ -222,7 +226,11 @@ async def retrieve_model(model: str):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
model_info = get_model_info(server_args.model_path, backend=server_args.backend)
|
model_info = get_model_info(
|
||||||
|
server_args.model_path,
|
||||||
|
backend=server_args.backend,
|
||||||
|
model_id=server_args.model_id,
|
||||||
|
)
|
||||||
|
|
||||||
card_kwargs = {
|
card_kwargs = {
|
||||||
"id": model,
|
"id": model,
|
||||||
|
|||||||
@@ -66,7 +66,11 @@ def build_pipeline(
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("No pipeline_class_name specified, using model_index.json")
|
logger.info("No pipeline_class_name specified, using model_index.json")
|
||||||
model_info = get_model_info(model_path, backend=server_args.backend)
|
model_info = get_model_info(
|
||||||
|
model_path,
|
||||||
|
backend=server_args.backend,
|
||||||
|
model_id=server_args.model_id,
|
||||||
|
)
|
||||||
pipeline_cls = model_info.pipeline_cls
|
pipeline_cls = model_info.pipeline_cls
|
||||||
logger.info(f"Using pipeline from model_index.json: {pipeline_cls.__name__}")
|
logger.info(f"Using pipeline from model_index.json: {pipeline_cls.__name__}")
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,9 @@ class ServerArgs:
|
|||||||
# Model and path configuration (for convenience)
|
# Model and path configuration (for convenience)
|
||||||
model_path: str
|
model_path: str
|
||||||
|
|
||||||
|
# explicit model ID override (e.g. "Qwen-Image")
|
||||||
|
model_id: str | None = None
|
||||||
|
|
||||||
# Model backend (sglang native or diffusers)
|
# Model backend (sglang native or diffusers)
|
||||||
backend: Backend = Backend.AUTO
|
backend: Backend = Backend.AUTO
|
||||||
|
|
||||||
@@ -328,9 +331,13 @@ class ServerArgs:
|
|||||||
"""
|
"""
|
||||||
return self.host is None or self.port is None
|
return self.host is None or self.port is None
|
||||||
|
|
||||||
|
def _adjust_path(self):
|
||||||
|
self.model_path = os.path.expanduser(self.model_path)
|
||||||
|
|
||||||
def _adjust_parameters(self):
|
def _adjust_parameters(self):
|
||||||
"""set defaults and normalize values."""
|
"""set defaults and normalize values."""
|
||||||
self._adjust_offload()
|
self._adjust_offload()
|
||||||
|
self._adjust_path()
|
||||||
self._adjust_quant_config()
|
self._adjust_quant_config()
|
||||||
self._adjust_warmup()
|
self._adjust_warmup()
|
||||||
self._adjust_network_ports()
|
self._adjust_network_ports()
|
||||||
@@ -582,6 +589,17 @@ class ServerArgs:
|
|||||||
type=str,
|
type=str,
|
||||||
help="The path of the model weights. This can be a local folder or a Hugging Face repo ID.",
|
help="The path of the model weights. This can be a local folder or a Hugging Face repo ID.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--model-id",
|
||||||
|
type=str,
|
||||||
|
default=ServerArgs.model_id,
|
||||||
|
help=(
|
||||||
|
"Override the model ID used for config resolution. "
|
||||||
|
"Useful when --model-path is a local directory whose name does not match "
|
||||||
|
"any registered HF repo name. Should be the repo name portion of the HF ID "
|
||||||
|
"(e.g. 'Qwen-Image' for 'Qwen/Qwen-Image')."
|
||||||
|
),
|
||||||
|
)
|
||||||
# attention
|
# attention
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--attention-backend",
|
"--attention-backend",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ SUITES = {
|
|||||||
"../unit/test_sampling_params_validate.py",
|
"../unit/test_sampling_params_validate.py",
|
||||||
"../unit/test_storage.py",
|
"../unit/test_storage.py",
|
||||||
"../unit/test_lora_format_adapter.py",
|
"../unit/test_lora_format_adapter.py",
|
||||||
|
"../unit/test_server_args_unit.py",
|
||||||
# add new unit tests here
|
# add new unit tests here
|
||||||
],
|
],
|
||||||
"1-gpu": [
|
"1-gpu": [
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.registry import _get_config_info
|
||||||
|
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||||
|
|
||||||
|
|
||||||
|
class TestServerArgsPathExpansion(unittest.TestCase):
|
||||||
|
def test_tilde_model_path_is_expanded(self):
|
||||||
|
args = ServerArgs.from_dict({"model_path": "~/fake/local/model"})
|
||||||
|
expected = os.path.expanduser("~/fake/local/model")
|
||||||
|
self.assertEqual(args.model_path, expected)
|
||||||
|
self.assertFalse(args.model_path.startswith("~"))
|
||||||
|
|
||||||
|
def test_absolute_path_is_unchanged(self):
|
||||||
|
args = ServerArgs.from_dict({"model_path": "/data/my-model"})
|
||||||
|
self.assertEqual(args.model_path, "/data/my-model")
|
||||||
|
|
||||||
|
|
||||||
|
class TestModelIdResolution(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
_get_config_info.cache_clear()
|
||||||
|
|
||||||
|
def test_model_id_overrides_arbitrary_local_path(self):
|
||||||
|
# a local path whose directory name does not match any HF repo name;
|
||||||
|
# --model-id tells the engine which config to use
|
||||||
|
info = _get_config_info("/data/my-custom-qwen", model_id="Qwen-Image")
|
||||||
|
self.assertIsNotNone(info)
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.qwen_image import (
|
||||||
|
QwenImagePipelineConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(info.pipeline_config_cls, QwenImagePipelineConfig)
|
||||||
|
|
||||||
|
def test_model_id_works_after_tilde_expansion(self):
|
||||||
|
# simulate the full flow: user passes ~/..., engine expands and resolves
|
||||||
|
expanded = os.path.expanduser("~/.cache/huggingface/hub/bbb/snapshots/ccc")
|
||||||
|
_get_config_info.cache_clear()
|
||||||
|
info = _get_config_info(expanded, model_id="Qwen-Image")
|
||||||
|
self.assertIsNotNone(info)
|
||||||
|
|
||||||
|
def test_model_id_unknown_falls_back_without_crash(self):
|
||||||
|
# unrecognized model_id: should warn and fall back to path-based detection
|
||||||
|
# with an unresolvable path, expect RuntimeError from the detector step
|
||||||
|
with self.assertRaises((RuntimeError, Exception)):
|
||||||
|
_get_config_info("/data/no-such-model", model_id="NonExistentModelXYZ")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user