fix: use subprocess with transformers 5.x for vision detection

Models like GLM-4.7-Flash have architectures (glm4_moe_lite) that
AutoConfig in the main process (transformers 4.57.x) can't recognize.
Instead of a raw config.json workaround, run the AutoConfig check in
a subprocess with .venv_t5/ activated — same pattern as training and
inference workers. This is more robust and consistent.
This commit is contained in:
Roland Tannous 2026-03-06 04:51:23 +00:00
commit b5cfd0952c

View file

@ -7,6 +7,9 @@ from typing import Optional, Dict, Any
from utils.paths import normalize_path, is_local_path, is_model_cached
from utils.utils import without_hf_auth
import logging
import os
import subprocess
import sys
from pathlib import Path
from typing import List, Tuple
import json
@ -366,74 +369,115 @@ def load_model_config(model_name: str, use_auth: bool = False, token: Optional[s
)
def _fetch_raw_config(model_name: str, token: Optional[str] = None) -> Optional[dict]:
"""Fetch raw config.json without going through AutoConfig.
This bypasses AutoConfig's architecture registry, so it works for model
architectures that the installed transformers version doesn't recognize
(e.g. ``glm4_moe_lite`` from transformers 5.x running with 4.57.x).
Checks local path first, then falls back to HuggingFace Hub download.
"""
# --- Local path ---
local_path = Path(model_name)
config_path = local_path / "config.json"
if config_path.is_file():
try:
with open(config_path) as f:
return json.load(f)
except Exception as exc:
logger.debug("Could not read local config.json for %s: %s", model_name, exc)
# --- HuggingFace Hub ---
try:
from huggingface_hub import hf_hub_download
config_file = hf_hub_download(
repo_id=model_name,
filename="config.json",
token=token,
)
with open(config_file) as f:
return json.load(f)
except Exception as exc:
logger.debug("Could not fetch config.json from Hub for %s: %s", model_name, exc)
return None
# VLM architecture suffixes and known VLM model_type values — shared between
# the AutoConfig path and the raw-config fallback.
# VLM architecture suffixes and known VLM model_type values.
_VLM_ARCH_SUFFIXES = ("ForConditionalGeneration", "ForVisionText2Text")
_VLM_MODEL_TYPES = {
'phi3_v', 'llava', 'llava_next', 'llava_onevision',
'internvl_chat', 'cogvlm2', 'minicpmv',
}
# Pre-computed project root and .venv_t5 path for subprocess version switching.
_PROJECT_ROOT = str(Path(__file__).resolve().parent.parent.parent.parent)
_VENV_T5_DIR = os.path.join(_PROJECT_ROOT, ".venv_t5")
_BACKEND_DIR = os.path.join(_PROJECT_ROOT, "studio", "backend")
def _is_vision_from_raw_config(config: dict) -> bool:
"""Check vision indicators in a raw ``config.json`` dict."""
# Architecture class name patterns
architectures = config.get("architectures", [])
if any(a.endswith(_VLM_ARCH_SUFFIXES) for a in architectures):
return True
# Inline script executed in a subprocess with transformers 5.x activated.
# Receives model_name and token via argv, prints JSON result to stdout.
_VISION_CHECK_SCRIPT = r'''
import sys, os, json
os.environ["TOKENIZERS_PARALLELISM"] = "false"
# Has vision_config (most VLMs: LLaVA, Gemma-3, Qwen2-VL, etc.)
if "vision_config" in config:
return True
# Activate transformers 5.x
venv_t5 = sys.argv[1]
backend_dir = sys.argv[2]
model_name = sys.argv[3]
token = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] != "" else None
# Has img_processor (Phi-3.5 Vision)
if "img_processor" in config:
return True
sys.path.insert(0, venv_t5)
if backend_dir not in sys.path:
sys.path.insert(0, backend_dir)
# Has image_token_index
if "image_token_index" in config:
return True
try:
from transformers import AutoConfig
kwargs = {"trust_remote_code": True}
if token:
kwargs["token"] = token
config = AutoConfig.from_pretrained(model_name, **kwargs)
# Known VLM model_type values
if config.get("model_type") in _VLM_MODEL_TYPES:
return True
is_vlm = False
if hasattr(config, "architectures"):
is_vlm = any(
x.endswith(("ForConditionalGeneration", "ForVisionText2Text"))
for x in config.architectures
)
if not is_vlm and hasattr(config, "vision_config"):
is_vlm = True
if not is_vlm and hasattr(config, "img_processor"):
is_vlm = True
if not is_vlm and hasattr(config, "image_token_index"):
is_vlm = True
if not is_vlm and hasattr(config, "model_type"):
vlm_types = {"phi3_v","llava","llava_next","llava_onevision",
"internvl_chat","cogvlm2","minicpmv"}
if config.model_type in vlm_types:
is_vlm = True
return False
model_type = getattr(config, "model_type", "unknown")
archs = getattr(config, "architectures", [])
print(json.dumps({"is_vision": is_vlm, "model_type": model_type,
"architectures": archs}))
except Exception as exc:
print(json.dumps({"error": str(exc)}))
sys.exit(1)
'''
def _is_vision_model_subprocess(model_name: str, hf_token: Optional[str] = None) -> bool:
"""Run is_vision_model check in a subprocess with transformers 5.x.
Same pattern as training/inference workers: spawn a clean subprocess
with .venv_t5/ prepended to sys.path so AutoConfig recognizes newer
architectures (glm4_moe_lite, etc.).
"""
token_arg = hf_token or ""
try:
result = subprocess.run(
[sys.executable, "-c", _VISION_CHECK_SCRIPT,
_VENV_T5_DIR, _BACKEND_DIR, model_name, token_arg],
capture_output=True, text=True, timeout=60,
)
if result.returncode != 0:
stderr = result.stderr.strip()
logger.warning(
"Vision check subprocess failed for '%s': %s",
model_name, stderr or result.stdout.strip(),
)
return False
data = json.loads(result.stdout.strip())
if "error" in data:
logger.warning(
"Vision check subprocess error for '%s': %s",
model_name, data["error"],
)
return False
is_vlm = data["is_vision"]
logger.info(
"Vision check (subprocess, transformers 5.x) for '%s': "
"model_type=%s, architectures=%s, is_vision=%s",
model_name, data.get("model_type"), data.get("architectures"), is_vlm,
)
return is_vlm
except subprocess.TimeoutExpired:
logger.warning("Vision check subprocess timed out for '%s'", model_name)
return False
except Exception as exc:
logger.warning("Vision check subprocess failed for '%s': %s", model_name, exc)
return False
def is_vision_model(model_name: str, hf_token: Optional[str] = None) -> bool:
@ -441,14 +485,25 @@ def is_vision_model(model_name: str, hf_token: Optional[str] = None) -> bool:
Detect vision-language models (VLMs) by checking architecture in config.
Works for fine-tuned models since they inherit the base architecture.
For models whose architecture is not recognized by the installed
transformers version (e.g. transformers 5.x models running with 4.57.x),
falls back to reading config.json directly.
For models that require transformers 5.x (e.g. GLM-4.7-Flash), the check
runs in a subprocess with .venv_t5/ activated same pattern as the
training and inference workers.
Args:
model_name: Model identifier (HF repo or local path)
hf_token: Optional HF token for accessing gated/private models
"""
# Models that need transformers 5.x must be checked in a subprocess
# because AutoConfig in the main process (transformers 4.57.x) doesn't
# recognize their architectures.
from utils.transformers_version import needs_transformers_5
if needs_transformers_5(model_name):
logger.info(
"Model '%s' needs transformers 5.x — checking vision via subprocess",
model_name,
)
return _is_vision_model_subprocess(model_name, hf_token=hf_token)
try:
config = load_model_config(model_name, use_auth=True, token=hf_token)
@ -486,25 +541,6 @@ def is_vision_model(model_name: str, hf_token: Optional[str] = None) -> bool:
return False
except Exception as e:
# AutoConfig may fail for architectures that the installed transformers
# version doesn't recognize (e.g. glm4_moe_lite needs transformers 5.x
# but we're running 4.57.x in the main process). Fall back to reading
# config.json directly and checking for vision indicators.
logger.debug(
"AutoConfig failed for %s (%s), trying raw config.json fallback",
model_name, e,
)
raw_config = _fetch_raw_config(model_name, token=hf_token)
if raw_config is not None:
is_vlm = _is_vision_from_raw_config(raw_config)
model_type = raw_config.get("model_type", "unknown")
logger.info(
"Raw config fallback for %s: model_type=%s, is_vision=%s",
model_name, model_type, is_vlm,
)
return is_vlm
logger.warning(f"Could not determine if {model_name} is vision model: {e}")
return False