[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
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
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
|
||||
if model_path in _MODEL_HF_PATH_TO_NAME:
|
||||
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)
|
||||
|
||||
# 2. Partial match: find the best (longest) match against all registered model hf paths.
|
||||
model_name = get_model_short_name(model_path.lower())
|
||||
all_model_hf_paths = sorted(_MODEL_HF_PATH_TO_NAME.keys(), key=len, reverse=True)
|
||||
model_short_name = get_model_short_name(model_path.lower())
|
||||
for registered_model_hf_id in all_model_hf_paths:
|
||||
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(
|
||||
f"Resolved model name '{registered_model_hf_id}' from partial path match."
|
||||
)
|
||||
@@ -310,7 +328,7 @@ class ModelInfo:
|
||||
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.
|
||||
|
||||
@@ -337,6 +355,7 @@ def _get_diffusers_model_info(model_path: str) -> ModelInfo:
|
||||
def get_model_info(
|
||||
model_path: str,
|
||||
backend: Optional[Union[str, "Backend"]] = None,
|
||||
model_id: Optional[str] = None,
|
||||
) -> Optional[ModelInfo]:
|
||||
"""
|
||||
Resolves all necessary classes (pipeline, sampling, config) for a given model path.
|
||||
@@ -365,7 +384,7 @@ def get_model_info(
|
||||
logger.info(
|
||||
"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
|
||||
# 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}")
|
||||
if backend == Backend.AUTO:
|
||||
logger.info("Falling back to diffusers backend")
|
||||
return _get_diffusers_model_info(model_path)
|
||||
return _get_diffusers_model_info()
|
||||
return None
|
||||
|
||||
pipeline_class_name = config.get("_class_name")
|
||||
@@ -399,7 +418,7 @@ def get_model_info(
|
||||
)
|
||||
if backend == Backend.AUTO:
|
||||
logger.info("Falling back to diffusers backend")
|
||||
return _get_diffusers_model_info(model_path)
|
||||
return _get_diffusers_model_info()
|
||||
return None
|
||||
|
||||
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"Falling back to diffusers backend."
|
||||
)
|
||||
return _get_diffusers_model_info(model_path)
|
||||
return _get_diffusers_model_info()
|
||||
else:
|
||||
logger.error(
|
||||
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
|
||||
|
||||
# 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 backend == Backend.AUTO:
|
||||
logger.warning(
|
||||
f"Could not resolve native configuration for model '{model_path}'. "
|
||||
f"Falling back to diffusers backend."
|
||||
)
|
||||
return _get_diffusers_model_info(model_path)
|
||||
return _get_diffusers_model_info()
|
||||
else:
|
||||
logger.error(
|
||||
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
|
||||
|
||||
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 = {
|
||||
"model_path": server_args.model_path,
|
||||
|
||||
@@ -180,7 +180,11 @@ async def available_models():
|
||||
if not server_args:
|
||||
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 = {
|
||||
"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 = {
|
||||
"id": model,
|
||||
|
||||
@@ -66,7 +66,11 @@ def build_pipeline(
|
||||
)
|
||||
else:
|
||||
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
|
||||
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_path: str
|
||||
|
||||
# explicit model ID override (e.g. "Qwen-Image")
|
||||
model_id: str | None = None
|
||||
|
||||
# Model backend (sglang native or diffusers)
|
||||
backend: Backend = Backend.AUTO
|
||||
|
||||
@@ -328,9 +331,13 @@ class ServerArgs:
|
||||
"""
|
||||
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):
|
||||
"""set defaults and normalize values."""
|
||||
self._adjust_offload()
|
||||
self._adjust_path()
|
||||
self._adjust_quant_config()
|
||||
self._adjust_warmup()
|
||||
self._adjust_network_ports()
|
||||
@@ -582,6 +589,17 @@ class ServerArgs:
|
||||
type=str,
|
||||
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
|
||||
parser.add_argument(
|
||||
"--attention-backend",
|
||||
|
||||
@@ -34,6 +34,7 @@ SUITES = {
|
||||
"../unit/test_sampling_params_validate.py",
|
||||
"../unit/test_storage.py",
|
||||
"../unit/test_lora_format_adapter.py",
|
||||
"../unit/test_server_args_unit.py",
|
||||
# add new unit tests here
|
||||
],
|
||||
"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