[diffusion] feat: Add --model-id for config resolution; deprecate model_detectors (#19607)

This commit is contained in:
Mick
2026-03-02 16:39:53 +08:00
committed by GitHub
parent f2c5503542
commit 2e15c015c0
7 changed files with 115 additions and 15 deletions

View File

@@ -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}'. "

View File

@@ -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,

View File

@@ -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,

View File

@@ -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__}")

View File

@@ -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",

View File

@@ -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": [

View File

@@ -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()