mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 09:57:57 -04:00
feat: materialize Hugging Face model artifacts (#10825)
* feat(config): add model artifact source contract Assisted-by: Codex:GPT-5 [Codex] * feat(downloader): add authenticated raw-byte progress Assisted-by: Codex:GPT-5 [Codex] * feat(huggingface): resolve immutable snapshot manifests Assisted-by: Codex:GPT-5 [Codex] * feat(models): add artifact storage primitives Assisted-by: Codex:GPT-5 [Codex] * feat(models): materialize pinned Hugging Face snapshots Assisted-by: Codex:GPT-5 [Codex] * feat(models): bind managed snapshots at runtime Assisted-by: Codex:GPT-5 [Codex] * feat(gallery): materialize model artifacts during install Assisted-by: Codex:GPT-5 [Codex] * feat(gallery): declare managed Hugging Face artifacts Assisted-by: Codex:GPT-5 [Codex] * feat(models): preload managed model artifacts Assisted-by: Codex:GPT-5 [Codex] * fix(gallery): retain shared artifact caches on delete Assisted-by: Codex:GPT-5 [Codex] * feat(models): report artifact acquisition progress Assisted-by: Codex:GPT-5 [Codex] * refactor(backends): load managed models from ModelFile Assisted-by: Codex:GPT-5 [Codex] * refactor(backends): load staged speech model snapshots Assisted-by: Codex:GPT-5 [Codex] * refactor(backends): use staged snapshots in engine backends Assisted-by: Codex:GPT-5 [Codex] * test(distributed): cover staged artifact snapshots Assisted-by: Codex:GPT-5 [Codex] * docs: explain managed model artifacts Assisted-by: Codex:GPT-5 [Codex] * docs: add product design context Assisted-by: Codex:GPT-5 [Codex] * feat(ui): show model artifact download progress Assisted-by: Codex:GPT-5 [Codex] * Eagerly materialize Hugging Face artifacts Materialize HF-backed model references as managed GGUF artifacts during load, with lazy download retained only as fallback. Assisted-by: Codex:GPT-5 [shell] * Refactor HF downloads through a shared executor Assisted-by: Codex:GPT-5 [shell] * drop Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
28
backend/python/common/model_utils.py
Normal file
28
backend/python/common/model_utils.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import os
|
||||
|
||||
|
||||
def resolve_model_reference(request, default=""):
|
||||
model_file = (getattr(request, "ModelFile", "") or "").strip()
|
||||
if model_file and os.path.exists(model_file):
|
||||
return model_file, True
|
||||
model = (getattr(request, "Model", "") or "").strip()
|
||||
return model or default, False
|
||||
|
||||
|
||||
def require_snapshot_file(model_ref, suffix):
|
||||
if os.path.isfile(model_ref) and model_ref.endswith(suffix):
|
||||
return model_ref
|
||||
if not os.path.isdir(model_ref):
|
||||
raise ValueError(f"model snapshot does not exist: {model_ref}")
|
||||
|
||||
matches = []
|
||||
for root, directories, files in os.walk(model_ref):
|
||||
directories.sort()
|
||||
for name in sorted(files):
|
||||
if name.endswith(suffix):
|
||||
matches.append(os.path.join(root, name))
|
||||
if len(matches) != 1:
|
||||
raise ValueError(
|
||||
f"model snapshot must contain exactly one {suffix} file; found {len(matches)}"
|
||||
)
|
||||
return matches[0]
|
||||
81
backend/python/common/model_utils_test.py
Normal file
81
backend/python/common/model_utils_test.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import ast
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from model_utils import require_snapshot_file, resolve_model_reference
|
||||
|
||||
|
||||
class ResolveModelReferenceTest(unittest.TestCase):
|
||||
def test_existing_model_file_wins(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
request = SimpleNamespace(Model="owner/repo", ModelFile=directory)
|
||||
self.assertEqual(resolve_model_reference(request), (directory, True))
|
||||
|
||||
def test_missing_model_file_preserves_legacy_repo_fallback(self):
|
||||
request = SimpleNamespace(Model=" owner/repo ", ModelFile="/does/not/exist")
|
||||
self.assertEqual(resolve_model_reference(request), ("owner/repo", False))
|
||||
|
||||
def test_empty_request_uses_explicit_default(self):
|
||||
request = SimpleNamespace(Model=" ", ModelFile="")
|
||||
self.assertEqual(
|
||||
resolve_model_reference(request, "default/repo"),
|
||||
("default/repo", False),
|
||||
)
|
||||
|
||||
def test_requires_the_only_matching_snapshot_file(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
checkpoint = os.path.join(directory, "model.nemo")
|
||||
with open(checkpoint, "wb"):
|
||||
pass
|
||||
self.assertEqual(require_snapshot_file(directory, ".nemo"), checkpoint)
|
||||
|
||||
def test_rejects_ambiguous_packaged_files(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
for name in ("first.nemo", "second.nemo"):
|
||||
with open(os.path.join(directory, name), "wb"):
|
||||
pass
|
||||
with self.assertRaisesRegex(ValueError, "exactly one"):
|
||||
require_snapshot_file(directory, ".nemo")
|
||||
|
||||
|
||||
class MigratedBackendSourceTest(unittest.TestCase):
|
||||
def test_general_backends_use_shared_resolution(self):
|
||||
common_dir = os.path.dirname(__file__)
|
||||
python_root = os.path.dirname(common_dir)
|
||||
for backend in (
|
||||
"transformers",
|
||||
"diffusers",
|
||||
"qwen-asr",
|
||||
"fish-speech",
|
||||
"nemo",
|
||||
"voxcpm",
|
||||
"qwen-tts",
|
||||
"liquid-audio",
|
||||
"vllm",
|
||||
"vllm-omni",
|
||||
"sglang",
|
||||
):
|
||||
with self.subTest(backend=backend):
|
||||
with open(
|
||||
os.path.join(python_root, backend, "backend.py"), encoding="utf-8"
|
||||
) as backend_file:
|
||||
tree = ast.parse(backend_file.read())
|
||||
imports = {
|
||||
alias.name
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module == "model_utils"
|
||||
for alias in node.names
|
||||
}
|
||||
calls = {
|
||||
node.func.id
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
|
||||
}
|
||||
self.assertIn("resolve_model_reference", imports)
|
||||
self.assertIn("resolve_model_reference", calls)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -25,6 +25,7 @@ import grpc
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
|
||||
from grpc_auth import get_auth_interceptors
|
||||
from model_utils import resolve_model_reference
|
||||
|
||||
|
||||
# Import dynamic loader for pipeline discovery
|
||||
@@ -215,7 +216,7 @@ def get_scheduler(name: str, config: dict = {}):
|
||||
# Implement the BackendServicer class with the service methods
|
||||
class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
|
||||
def _load_pipeline(self, request, modelFile, fromSingleFile, torchType, variant, device_map=None):
|
||||
def _load_pipeline(self, request, model_ref, from_single_file, local_only, torchType, variant, device_map=None):
|
||||
"""
|
||||
Load a diffusers pipeline dynamically using the dynamic loader.
|
||||
|
||||
@@ -225,8 +226,9 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
|
||||
Args:
|
||||
request: The gRPC request containing pipeline configuration
|
||||
modelFile: Path to the model file (for single file loading)
|
||||
fromSingleFile: Whether to use from_single_file() vs from_pretrained()
|
||||
model_ref: Repository ID or local model file/directory
|
||||
from_single_file: Whether to use from_single_file() vs from_pretrained()
|
||||
local_only: Whether Hub-backed loaders must remain offline
|
||||
torchType: The torch dtype to use
|
||||
variant: Model variant (e.g., "fp16")
|
||||
device_map: Device mapping strategy (e.g., "auto" for multi-GPU)
|
||||
@@ -251,7 +253,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
dtype = torch.bfloat16
|
||||
bfl_repo = os.environ.get("BFL_REPO", "ChuckMcSneed/FLUX.1-dev")
|
||||
|
||||
transformer = FluxTransformer2DModel.from_single_file(modelFile, torch_dtype=dtype, device_map=device_map)
|
||||
transformer = FluxTransformer2DModel.from_single_file(model_ref, torch_dtype=dtype, device_map=device_map)
|
||||
quantize(transformer, weights=qfloat8)
|
||||
freeze(transformer)
|
||||
text_encoder_2 = T5EncoderModel.from_pretrained(bfl_repo, subfolder="text_encoder_2", torch_dtype=dtype, device_map=device_map)
|
||||
@@ -269,17 +271,19 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
# WanPipeline - requires special VAE with float32 dtype
|
||||
if pipeline_type == "WanPipeline":
|
||||
vae = AutoencoderKLWan.from_pretrained(
|
||||
request.Model,
|
||||
model_ref,
|
||||
subfolder="vae",
|
||||
torch_dtype=torch.float32,
|
||||
device_map=device_map
|
||||
device_map=device_map,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
pipe = load_diffusers_pipeline(
|
||||
class_name="WanPipeline",
|
||||
model_id=request.Model,
|
||||
model_id=model_ref,
|
||||
vae=vae,
|
||||
torch_dtype=torchType,
|
||||
device_map=device_map
|
||||
device_map=device_map,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
self.txt2vid = True
|
||||
return pipe
|
||||
@@ -287,17 +291,19 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
# WanImageToVideoPipeline - requires special VAE with float32 dtype
|
||||
if pipeline_type == "WanImageToVideoPipeline":
|
||||
vae = AutoencoderKLWan.from_pretrained(
|
||||
request.Model,
|
||||
model_ref,
|
||||
subfolder="vae",
|
||||
torch_dtype=torch.float32,
|
||||
device_map=device_map
|
||||
device_map=device_map,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
pipe = load_diffusers_pipeline(
|
||||
class_name="WanImageToVideoPipeline",
|
||||
model_id=request.Model,
|
||||
model_id=model_ref,
|
||||
vae=vae,
|
||||
torch_dtype=torchType,
|
||||
device_map=device_map
|
||||
device_map=device_map,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
self.img2vid = True
|
||||
return pipe
|
||||
@@ -306,10 +312,11 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
if pipeline_type == "SanaPipeline":
|
||||
pipe = load_diffusers_pipeline(
|
||||
class_name="SanaPipeline",
|
||||
model_id=request.Model,
|
||||
model_id=model_ref,
|
||||
variant="bf16",
|
||||
torch_dtype=torch.bfloat16,
|
||||
device_map=device_map
|
||||
device_map=device_map,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
pipe.vae.to(torch.bfloat16)
|
||||
pipe.text_encoder.to(torch.bfloat16)
|
||||
@@ -320,9 +327,10 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
self.txt2vid = True
|
||||
pipe = load_diffusers_pipeline(
|
||||
class_name="DiffusionPipeline",
|
||||
model_id=request.Model,
|
||||
model_id=model_ref,
|
||||
torch_dtype=torchType,
|
||||
device_map=device_map
|
||||
device_map=device_map,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
return pipe
|
||||
|
||||
@@ -331,10 +339,11 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
self.img2vid = True
|
||||
pipe = load_diffusers_pipeline(
|
||||
class_name="StableVideoDiffusionPipeline",
|
||||
model_id=request.Model,
|
||||
model_id=model_ref,
|
||||
torch_dtype=torchType,
|
||||
variant=variant,
|
||||
device_map=device_map
|
||||
device_map=device_map,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
if not DISABLE_CPU_OFFLOAD:
|
||||
pipe.enable_model_cpu_offload()
|
||||
@@ -346,8 +355,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
self.ltx2_pipeline = True
|
||||
|
||||
# Check if loading from single file (GGUF)
|
||||
if fromSingleFile and LTX2VideoTransformer3DModel is not None:
|
||||
_, single_file_ext = os.path.splitext(modelFile)
|
||||
if from_single_file and LTX2VideoTransformer3DModel is not None:
|
||||
_, single_file_ext = os.path.splitext(model_ref)
|
||||
if single_file_ext == ".gguf":
|
||||
# Load transformer from single GGUF file with quantization
|
||||
transformer_kwargs = {}
|
||||
@@ -355,7 +364,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
transformer_kwargs["quantization_config"] = quantization_config
|
||||
|
||||
transformer = LTX2VideoTransformer3DModel.from_single_file(
|
||||
modelFile,
|
||||
model_ref,
|
||||
config=request.Model, # Use request.Model as the config/model_id
|
||||
subfolder="transformer",
|
||||
device_map=device_map,
|
||||
@@ -374,7 +383,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
# Single file but not GGUF - use standard single file loading
|
||||
pipe = load_diffusers_pipeline(
|
||||
class_name="LTX2ImageToVideoPipeline",
|
||||
model_id=modelFile,
|
||||
model_id=model_ref,
|
||||
from_single_file=True,
|
||||
torch_dtype=torchType,
|
||||
device_map=device_map,
|
||||
@@ -383,10 +392,11 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
# Standard loading from pretrained
|
||||
pipe = load_diffusers_pipeline(
|
||||
class_name="LTX2ImageToVideoPipeline",
|
||||
model_id=request.Model,
|
||||
model_id=model_ref,
|
||||
torch_dtype=torchType,
|
||||
variant=variant,
|
||||
device_map=device_map
|
||||
device_map=device_map,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
|
||||
if not DISABLE_CPU_OFFLOAD:
|
||||
@@ -399,8 +409,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
self.ltx2_pipeline = True
|
||||
|
||||
# Check if loading from single file (GGUF)
|
||||
if fromSingleFile and LTX2VideoTransformer3DModel is not None:
|
||||
_, single_file_ext = os.path.splitext(modelFile)
|
||||
if from_single_file and LTX2VideoTransformer3DModel is not None:
|
||||
_, single_file_ext = os.path.splitext(model_ref)
|
||||
if single_file_ext == ".gguf":
|
||||
# Load transformer from single GGUF file with quantization
|
||||
transformer_kwargs = {}
|
||||
@@ -408,7 +418,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
transformer_kwargs["quantization_config"] = quantization_config
|
||||
|
||||
transformer = LTX2VideoTransformer3DModel.from_single_file(
|
||||
modelFile,
|
||||
model_ref,
|
||||
config=request.Model, # Use request.Model as the config/model_id
|
||||
subfolder="transformer",
|
||||
device_map=device_map,
|
||||
@@ -427,7 +437,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
# Single file but not GGUF - use standard single file loading
|
||||
pipe = load_diffusers_pipeline(
|
||||
class_name="LTX2Pipeline",
|
||||
model_id=modelFile,
|
||||
model_id=model_ref,
|
||||
from_single_file=True,
|
||||
torch_dtype=torchType,
|
||||
device_map=device_map,
|
||||
@@ -436,10 +446,11 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
# Standard loading from pretrained
|
||||
pipe = load_diffusers_pipeline(
|
||||
class_name="LTX2Pipeline",
|
||||
model_id=request.Model,
|
||||
model_id=model_ref,
|
||||
torch_dtype=torchType,
|
||||
variant=variant,
|
||||
device_map=device_map
|
||||
device_map=device_map,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
|
||||
if not DISABLE_CPU_OFFLOAD:
|
||||
@@ -455,12 +466,13 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
load_kwargs = {"torch_dtype": torchType}
|
||||
|
||||
# Add variant if not loading from single file
|
||||
if not fromSingleFile and variant:
|
||||
if not from_single_file and variant:
|
||||
load_kwargs["variant"] = variant
|
||||
|
||||
# Add use_safetensors for from_pretrained
|
||||
if not fromSingleFile:
|
||||
if not from_single_file:
|
||||
load_kwargs["use_safetensors"] = SAFETENSORS
|
||||
load_kwargs["local_files_only"] = local_only
|
||||
|
||||
# Add device_map for multi-GPU support (when TensorParallelSize > 1)
|
||||
if device_map:
|
||||
@@ -473,8 +485,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
try:
|
||||
pipe = load_diffusers_pipeline(
|
||||
class_name=effective_pipeline_type,
|
||||
model_id=modelFile if fromSingleFile else request.Model,
|
||||
from_single_file=fromSingleFile,
|
||||
model_id=model_ref,
|
||||
from_single_file=from_single_file,
|
||||
**load_kwargs
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -539,8 +551,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
|
||||
print(f"Options: {self.options}", file=sys.stderr)
|
||||
|
||||
local = False
|
||||
modelFile = request.Model
|
||||
model_ref, local_only = resolve_model_reference(request)
|
||||
|
||||
self.cfg_scale = 7
|
||||
self.PipelineType = request.PipelineType
|
||||
@@ -555,13 +566,10 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
if request.CLIPSubfolder != "":
|
||||
clipsubfolder = request.CLIPSubfolder
|
||||
|
||||
# Check if ModelFile exists
|
||||
if request.ModelFile != "":
|
||||
if os.path.exists(request.ModelFile):
|
||||
local = True
|
||||
modelFile = request.ModelFile
|
||||
|
||||
fromSingleFile = request.Model.startswith("http") or request.Model.startswith("/") or local
|
||||
from_single_file = os.path.isfile(model_ref) or (
|
||||
not local_only
|
||||
and (request.Model.startswith("http") or request.Model.startswith("/"))
|
||||
)
|
||||
self.img2vid = False
|
||||
self.txt2vid = False
|
||||
self.ltx2_pipeline = False
|
||||
@@ -579,8 +587,9 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
# Special cases that require custom initialization are handled first
|
||||
self.pipe = self._load_pipeline(
|
||||
request=request,
|
||||
modelFile=modelFile,
|
||||
fromSingleFile=fromSingleFile,
|
||||
model_ref=model_ref,
|
||||
from_single_file=from_single_file,
|
||||
local_only=local_only,
|
||||
torchType=torchType,
|
||||
variant=variant,
|
||||
device_map=device_map
|
||||
|
||||
@@ -22,6 +22,7 @@ import grpc
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
|
||||
from grpc_auth import get_auth_interceptors
|
||||
from model_utils import resolve_model_reference
|
||||
|
||||
|
||||
|
||||
@@ -169,22 +170,17 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
else None
|
||||
)
|
||||
|
||||
# Get model path from request
|
||||
model_path = request.Model
|
||||
if not model_path:
|
||||
model_path = "fishaudio/s2-pro"
|
||||
|
||||
# If model_path looks like a HuggingFace repo ID (e.g. "fishaudio/fish-speech-1.5"),
|
||||
# download it locally first since fish-speech expects a local directory
|
||||
if "/" in model_path and not os.path.exists(model_path):
|
||||
model_path, local_only = resolve_model_reference(
|
||||
request, "fishaudio/s2-pro"
|
||||
)
|
||||
if not local_only and "/" in model_path:
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
print(
|
||||
f"Downloading model from HuggingFace: {model_path}",
|
||||
f"Downloading legacy unmanaged model from HuggingFace: {model_path}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
model_path = snapshot_download(repo_id=model_path)
|
||||
print(f"Model downloaded to: {model_path}", file=sys.stderr)
|
||||
|
||||
# Determine precision
|
||||
if device in ("mps", "cpu"):
|
||||
|
||||
@@ -23,6 +23,7 @@ import grpc
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
|
||||
from grpc_auth import get_auth_interceptors # noqa: E402
|
||||
from model_utils import resolve_model_reference # noqa: E402
|
||||
from python_utils import parse_options # noqa: E402
|
||||
|
||||
import backend_pb2 # noqa: E402
|
||||
@@ -172,19 +173,14 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
"fp32": torch.float32,
|
||||
}.get(dtype_name, torch.bfloat16)
|
||||
|
||||
# request.Model holds the raw `parameters.model` value (an HF
|
||||
# repo id like "LiquidAI/LFM2.5-Audio-1.5B"); request.ModelFile
|
||||
# is LocalAI's ModelPath-prefixed local copy that exists only
|
||||
# when the gallery supplied a `files:` list. Mirror the
|
||||
# transformers/vibevoice convention: prefer the repo id and
|
||||
# only switch to the local path if it's been staged on disk.
|
||||
model_id = request.Model
|
||||
if not model_id:
|
||||
model_id = request.ModelFile
|
||||
# `_local_only` distinguishes a staged snapshot from the legacy
|
||||
# repository fallback. liquid_audio has no matching Hub keyword,
|
||||
# so the compatibility shim below enforces local loading instead.
|
||||
model_id, _local_only = resolve_model_reference(
|
||||
request, "LiquidAI/LFM2.5-Audio-1.5B"
|
||||
)
|
||||
if not model_id:
|
||||
return backend_pb2.Result(success=False, message="No model identifier provided")
|
||||
if request.ModelFile and os.path.isdir(request.ModelFile):
|
||||
model_id = request.ModelFile
|
||||
self.model_id = model_id
|
||||
|
||||
# Pure fine-tune jobs don't need an in-memory inference model — the
|
||||
|
||||
@@ -17,6 +17,7 @@ import grpc
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
|
||||
from grpc_auth import get_auth_interceptors
|
||||
from model_utils import require_snapshot_file, resolve_model_reference
|
||||
|
||||
|
||||
|
||||
@@ -70,11 +71,21 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
value = value.lower() == "true"
|
||||
self.options[key] = value
|
||||
|
||||
model_name = request.Model or "nvidia/parakeet-tdt-0.6b-v3"
|
||||
model_ref, local_only = resolve_model_reference(
|
||||
request, "nvidia/parakeet-tdt-0.6b-v3"
|
||||
)
|
||||
|
||||
try:
|
||||
print(f"Loading NEMO ASR model from {model_name}", file=sys.stderr)
|
||||
self.model = nemo_asr.models.ASRModel.from_pretrained(model_name=model_name)
|
||||
print(f"Loading NEMO ASR model from {model_ref}", file=sys.stderr)
|
||||
if local_only:
|
||||
checkpoint = require_snapshot_file(model_ref, ".nemo")
|
||||
self.model = nemo_asr.models.ASRModel.restore_from(
|
||||
restore_path=checkpoint
|
||||
)
|
||||
else:
|
||||
self.model = nemo_asr.models.ASRModel.from_pretrained(
|
||||
model_name=model_ref
|
||||
)
|
||||
print("NEMO ASR model loaded successfully", file=sys.stderr)
|
||||
except Exception as err:
|
||||
print(f"[ERROR] LoadModel failed: {err}", file=sys.stderr)
|
||||
|
||||
@@ -17,6 +17,7 @@ import grpc
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
|
||||
from grpc_auth import get_auth_interceptors
|
||||
from model_utils import resolve_model_reference
|
||||
|
||||
|
||||
|
||||
@@ -70,7 +71,9 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
value = value.lower() == "true"
|
||||
self.options[key] = value
|
||||
|
||||
model_path = request.Model or "Qwen/Qwen3-ASR-1.7B"
|
||||
model_path, local_only = resolve_model_reference(
|
||||
request, "Qwen/Qwen3-ASR-1.7B"
|
||||
)
|
||||
default_dtype = torch.bfloat16 if self.device == "cuda" else torch.float32
|
||||
load_dtype = default_dtype
|
||||
if "torch_dtype" in self.options:
|
||||
@@ -117,6 +120,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
if attn_implementation:
|
||||
forced_aligner_kwargs["attn_implementation"] = attn_implementation
|
||||
load_kwargs["forced_aligner_kwargs"] = forced_aligner_kwargs
|
||||
load_kwargs["local_files_only"] = local_only
|
||||
|
||||
try:
|
||||
print(f"Loading Qwen3-ASR from {model_path}", file=sys.stderr)
|
||||
|
||||
@@ -26,6 +26,7 @@ import grpc
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
|
||||
from grpc_auth import get_auth_interceptors
|
||||
from model_utils import resolve_model_reference
|
||||
|
||||
|
||||
|
||||
@@ -190,19 +191,23 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
)
|
||||
print(traceback.format_exc(), file=sys.stderr)
|
||||
|
||||
# Get model path from request
|
||||
model_path = request.Model
|
||||
if not model_path:
|
||||
model_path = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"
|
||||
model_path, _local_only = resolve_model_reference(
|
||||
request, "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"
|
||||
)
|
||||
detection_ref = request.Model or model_path
|
||||
|
||||
# Determine model type from model path or options
|
||||
self.model_type = self.options.get("model_type", None)
|
||||
if not self.model_type:
|
||||
if "CustomVoice" in model_path:
|
||||
if "CustomVoice" in detection_ref:
|
||||
self.model_type = "CustomVoice"
|
||||
elif "VoiceDesign" in model_path:
|
||||
elif "VoiceDesign" in detection_ref:
|
||||
self.model_type = "VoiceDesign"
|
||||
elif "Base" in model_path or "0.6B" in model_path or "1.7B" in model_path:
|
||||
elif (
|
||||
"Base" in detection_ref
|
||||
or "0.6B" in detection_ref
|
||||
or "1.7B" in detection_ref
|
||||
):
|
||||
self.model_type = "Base" # VoiceClone model
|
||||
else:
|
||||
# Default to CustomVoice
|
||||
|
||||
@@ -41,6 +41,7 @@ import grpc
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
|
||||
from grpc_auth import get_auth_interceptors
|
||||
from model_utils import resolve_model_reference
|
||||
|
||||
# sglang imports. Engine is the stable public entry point; parser modules
|
||||
# are wrapped in try/except so older / leaner installs that omit them
|
||||
@@ -173,7 +174,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
return backend_pb2.Reply(message=bytes("OK", 'utf-8'))
|
||||
|
||||
async def LoadModel(self, request, context):
|
||||
engine_kwargs = {"model_path": request.Model}
|
||||
model_ref, local_only = resolve_model_reference(request)
|
||||
engine_kwargs = {"model_path": model_ref}
|
||||
|
||||
if request.Quantization:
|
||||
engine_kwargs["quantization"] = request.Quantization
|
||||
@@ -228,8 +230,9 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
if HAS_TRANSFORMERS:
|
||||
try:
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(
|
||||
request.Model,
|
||||
model_ref,
|
||||
trust_remote_code=bool(request.TrustRemoteCode),
|
||||
local_files_only=local_only,
|
||||
)
|
||||
except Exception as err:
|
||||
print(f"AutoTokenizer load failed (non-fatal): {err!r}", file=sys.stderr)
|
||||
|
||||
@@ -19,6 +19,7 @@ import grpc
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
|
||||
from grpc_auth import get_auth_interceptors
|
||||
from model_utils import resolve_model_reference
|
||||
|
||||
import torch
|
||||
import torch.cuda
|
||||
@@ -61,11 +62,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
return backend_pb2.Reply(message=bytes("OK", 'utf-8'))
|
||||
|
||||
def LoadModel(self, request, context):
|
||||
model_name = request.Model
|
||||
|
||||
# Check to see if the Model exists in the filesystem already.
|
||||
if os.path.exists(request.ModelFile):
|
||||
model_name = request.ModelFile
|
||||
model_name, local_only = resolve_model_reference(request)
|
||||
|
||||
compute = torch.float16
|
||||
if request.F16Memory == True:
|
||||
@@ -150,7 +147,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
device_map=device_map,
|
||||
load_in_4bit=xpu_4bit,
|
||||
load_in_8bit=xpu_8bit,
|
||||
torch_dtype=compute)
|
||||
torch_dtype=compute,
|
||||
local_files_only=local_only)
|
||||
elif request.Type == "OVModelForCausalLM":
|
||||
from optimum.intel.openvino import OVModelForCausalLM
|
||||
from openvino.runtime import Core
|
||||
@@ -171,7 +169,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
compile=True,
|
||||
trust_remote_code=request.TrustRemoteCode,
|
||||
ov_config=ovconfig,
|
||||
device=device_map)
|
||||
device=device_map,
|
||||
local_files_only=local_only)
|
||||
self.OV = True
|
||||
elif request.Type == "OVModelForFeatureExtraction":
|
||||
from optimum.intel.openvino import OVModelForFeatureExtraction
|
||||
@@ -194,11 +193,16 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
trust_remote_code=request.TrustRemoteCode,
|
||||
ov_config=ovconfig,
|
||||
export=True,
|
||||
device=device_map)
|
||||
device=device_map,
|
||||
local_files_only=local_only)
|
||||
self.OV = True
|
||||
elif request.Type == "SentenceTransformer":
|
||||
autoTokenizer = False
|
||||
self.model = SentenceTransformer(model_name, trust_remote_code=request.TrustRemoteCode)
|
||||
self.model = SentenceTransformer(
|
||||
model_name,
|
||||
trust_remote_code=request.TrustRemoteCode,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
self.SentenceTransformer = True
|
||||
elif request.Type == "TokenClassification":
|
||||
# NER / PII tagging via HuggingFace's token-classification
|
||||
@@ -213,6 +217,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
aggregation_strategy="simple",
|
||||
device=0 if self.CUDA else -1,
|
||||
trust_remote_code=request.TrustRemoteCode,
|
||||
model_kwargs={"local_files_only": local_only},
|
||||
)
|
||||
self.TokenClassification = True
|
||||
else:
|
||||
@@ -231,6 +236,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
quantization_config=quantization,
|
||||
device_map=device_map,
|
||||
torch_dtype=compute,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
|
||||
# Try to load a processor (needed for TTS/audio models)
|
||||
@@ -238,6 +244,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
self.processor = AutoProcessor.from_pretrained(
|
||||
model_name,
|
||||
trust_remote_code=request.TrustRemoteCode,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
self.GenericTTS = True
|
||||
print(f"Loaded processor for {model_name}", file=sys.stderr)
|
||||
@@ -252,7 +259,10 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
self.max_tokens = self.options.get("max_new_tokens", 512)
|
||||
|
||||
if autoTokenizer:
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(
|
||||
model_name,
|
||||
local_files_only=local_only,
|
||||
)
|
||||
self.XPU = False
|
||||
|
||||
if XPU and self.OV == False:
|
||||
|
||||
@@ -33,6 +33,7 @@ import grpc
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
|
||||
from grpc_auth import get_auth_interceptors
|
||||
from model_utils import resolve_model_reference
|
||||
from vllm_utils import parse_options, messages_to_dicts, setup_parsers
|
||||
|
||||
|
||||
@@ -166,12 +167,16 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
# Parse options from request.Options using shared helper
|
||||
self.options = parse_options(request.Options)
|
||||
opts = self.options
|
||||
model_ref, _local_only = resolve_model_reference(request)
|
||||
detection_ref = request.Model or model_ref
|
||||
|
||||
print(f"Options: {self.options}", file=sys.stderr)
|
||||
|
||||
# Detect model type
|
||||
self.model_name = request.Model
|
||||
self.model_type = request.Type if request.Type else self._detect_model_type(request.Model)
|
||||
self.model_name = model_ref
|
||||
self.model_type = (
|
||||
request.Type if request.Type else self._detect_model_type(detection_ref)
|
||||
)
|
||||
print(f"Detected model type: {self.model_type}", file=sys.stderr)
|
||||
|
||||
# Build DiffusionParallelConfig if diffusion model (image or video)
|
||||
@@ -205,9 +210,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
}
|
||||
|
||||
# Base Omni initialization parameters
|
||||
omni_kwargs = {
|
||||
"model": request.Model,
|
||||
}
|
||||
omni_kwargs = {"model": model_ref}
|
||||
|
||||
# Add diffusion-specific parameters (image/video models)
|
||||
if self.model_type in ["image", "video"]:
|
||||
@@ -251,7 +254,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
try:
|
||||
from vllm.transformers_utils.tokenizer import get_tokenizer
|
||||
self.tokenizer = get_tokenizer(
|
||||
request.Model,
|
||||
model_ref,
|
||||
trust_remote_code=opts.get("trust_remote_code", False),
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
@@ -21,6 +21,7 @@ import grpc
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
|
||||
from grpc_auth import get_auth_interceptors
|
||||
from model_utils import resolve_model_reference
|
||||
|
||||
from vllm.engine.arg_utils import AsyncEngineArgs
|
||||
from vllm.engine.async_llm_engine import AsyncLLMEngine
|
||||
@@ -197,9 +198,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
Returns:
|
||||
backend_pb2.Result: The load model result.
|
||||
"""
|
||||
engine_args = AsyncEngineArgs(
|
||||
model=request.Model,
|
||||
)
|
||||
model_ref, _local_only = resolve_model_reference(request)
|
||||
engine_args = AsyncEngineArgs(model=model_ref)
|
||||
|
||||
if request.Quantization != "":
|
||||
engine_args.quantization = request.Quantization
|
||||
@@ -260,7 +260,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
tokenizer = self.llm.tokenizer
|
||||
if tokenizer is None:
|
||||
tokenizer = get_tokenizer(
|
||||
request.Model,
|
||||
model_ref,
|
||||
trust_remote_code=bool(request.TrustRemoteCode),
|
||||
truncation_side="left",
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ import grpc
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
|
||||
from grpc_auth import get_auth_interceptors
|
||||
from model_utils import resolve_model_reference
|
||||
|
||||
|
||||
def is_float(s):
|
||||
@@ -99,10 +100,9 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
value = value.lower() == "true"
|
||||
self.options[key] = value
|
||||
|
||||
# Get model path from request
|
||||
model_path = request.Model
|
||||
if not model_path:
|
||||
model_path = "openbmb/VoxCPM1.5"
|
||||
model_path, _local_only = resolve_model_reference(
|
||||
request, "openbmb/VoxCPM1.5"
|
||||
)
|
||||
|
||||
try:
|
||||
print(f"Loading model from {model_path}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user