From 6c0826a9e42663adfd16abd8ee4df6b8a55d56a5 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 1 Apr 2026 01:38:09 -0700 Subject: [PATCH] Fix Windows local GGUF model loading crash (#4730) * Fix Windows "Non-relative patterns are unsupported" when loading local GGUF models When a user loads a GGUF model from a local Windows path (e.g. C:\Users\danie\.lmstudio\models\unsloth\functiongemma-270m-it-GGUF), the model identifier contains backslashes and a drive letter. Both load_model_defaults() and _has_specific_yaml() constructed a YAML filename from the full absolute path and passed it to Path.rglob(), which rejects non-relative patterns on Windows. Fixed by detecting Windows-style paths (drive letters, UNC paths, backslashes) in addition to Unix-style paths, and using only the directory basename for the YAML filename lookup when the identifier is a local filesystem path. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Refactor: reuse is_local_path helper, fix case-sensitive suffix lookup - Replace inline local-path detection in model_config.py and inference_config.py with the existing is_local_path() from utils.paths, which already handles Unix, Windows drive-letter, UNC, and backslash paths - Fix case-sensitive suffix lookup in load_model_defaults(): the _REVERSE_MODEL_MAPPING is lowercase-keyed, so suffix comparisons must use .lower() to match paths like /path/to/Spark-TTS-0.5B/LLM * Fix WSL path parsing and _has_specific_yaml suffix lookup - Use normalize_path() before Path() operations so backslash Windows paths (e.g. C:\Users\...\model) are correctly split on POSIX/WSL hosts where pathlib treats backslashes as literal characters - Add suffix-based (2-component and 1-component) lookup to _has_specific_yaml() so it matches the same resolution rules as load_model_defaults(), fixing wrong inference params for local suffix-mapped models like Spark-TTS-0.5B/LLM --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../utils/inference/inference_config.py | 25 +++++++++++++++-- studio/backend/utils/models/model_config.py | 27 ++++++++++++------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/studio/backend/utils/inference/inference_config.py b/studio/backend/utils/inference/inference_config.py index a0377f9869..9efc281b0b 100644 --- a/studio/backend/utils/inference/inference_config.py +++ b/studio/backend/utils/inference/inference_config.py @@ -17,6 +17,7 @@ import structlog from loggers import get_logger from utils.models.model_config import load_model_defaults +from utils.paths import is_local_path, normalize_path logger = get_logger(__name__) @@ -93,8 +94,28 @@ def _has_specific_yaml(model_identifier: str) -> bool: if model_identifier.lower() in _REVERSE_MODEL_MAPPING: return True - # Check for exact filename match - model_filename = model_identifier.replace("/", "_") + ".yaml" + # For local filesystem paths (e.g. C:\Users\...\model on Windows), + # normalize backslashes so Path().parts splits correctly on POSIX/WSL, + # then try matching the last 1-2 path components against the registry + # (mirrors the logic in load_model_defaults). + _is_local = is_local_path(model_identifier) + _normalized = normalize_path(model_identifier) if _is_local else model_identifier + + if _is_local: + parts = Path(_normalized).parts + for depth in (2, 1): + if len(parts) >= depth: + suffix = "/".join(parts[-depth:]) + if suffix.lower() in _REVERSE_MODEL_MAPPING: + return True + _lookup = Path(_normalized).name + else: + _lookup = model_identifier + + # Check for exact filename match (basename for local paths to avoid + # passing absolute paths into rglob which raises + # "Non-relative patterns are unsupported" on Windows). + model_filename = _lookup.replace("/", "_") + ".yaml" for config_path in defaults_dir.rglob(model_filename): if config_path.is_file(): return True diff --git a/studio/backend/utils/models/model_config.py b/studio/backend/utils/models/model_config.py index 5de3fd2cf9..f7d9b33542 100644 --- a/studio/backend/utils/models/model_config.py +++ b/studio/backend/utils/models/model_config.py @@ -1420,17 +1420,20 @@ def load_model_defaults(model_name: str) -> Dict[str, Any]: return config # If model_name is a local path (e.g. /home/.../Spark-TTS-0.5B/LLM from - # adapter_config.json), try matching the last 1-2 path components against - # the registry (e.g. "Spark-TTS-0.5B/LLM"). - if model_name not in _REVERSE_MODEL_MAPPING and ( - model_name.startswith("/") or model_name.startswith(".") - ): - parts = Path(model_name).parts + # adapter_config.json, or C:\Users\...\model on Windows), try matching + # the last 1-2 path components against the registry + # (e.g. "Spark-TTS-0.5B/LLM"). + _is_local_path = is_local_path(model_name) + # Normalize Windows backslash paths so Path().parts splits correctly + # on POSIX/WSL hosts (pathlib treats backslashes as literals on Linux). + _normalized = normalize_path(model_name) if _is_local_path else model_name + if model_name.lower() not in _REVERSE_MODEL_MAPPING and _is_local_path: + parts = Path(_normalized).parts for depth in [2, 1]: if len(parts) >= depth: suffix = "/".join(parts[-depth:]) - if suffix in _REVERSE_MODEL_MAPPING: - canonical_file = _REVERSE_MODEL_MAPPING[suffix] + if suffix.lower() in _REVERSE_MODEL_MAPPING: + canonical_file = _REVERSE_MODEL_MAPPING[suffix.lower()] for config_path in defaults_dir.rglob(canonical_file): if config_path.is_file(): with open(config_path, "r", encoding = "utf-8") as f: @@ -1440,8 +1443,12 @@ def load_model_defaults(model_name: str) -> Dict[str, Any]: ) return config - # Try exact model name match (for backward compatibility) - model_filename = model_name.replace("/", "_") + ".yaml" + # Try exact model name match (for backward compatibility). + # For local filesystem paths, use only the directory basename to + # avoid passing absolute paths (e.g. C:\...) into rglob which + # raises "Non-relative patterns are unsupported" on Windows. + _lookup_name = Path(_normalized).name if _is_local_path else model_name + model_filename = _lookup_name.replace("/", "_") + ".yaml" # Search in subfolders and root for config_path in defaults_dir.rglob(model_filename): if config_path.is_file():