mship-engine 0.7.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modelship/__init__.py +0 -0
- modelship/deploy/__init__.py +0 -0
- modelship/deploy/actor_options.py +178 -0
- modelship/deploy/capabilities.py +84 -0
- modelship/deploy/config.py +131 -0
- modelship/deploy/effective_config.py +131 -0
- modelship/deploy/serve_utils.py +484 -0
- modelship/deploy/strategy.py +243 -0
- modelship/driver.py +377 -0
- modelship/infer/base_infer.py +473 -0
- modelship/infer/base_serving.py +21 -0
- modelship/infer/deploy_coordinator.py +204 -0
- modelship/infer/diffusers/diffusers_infer.py +171 -0
- modelship/infer/diffusers/openai/serving_image.py +230 -0
- modelship/infer/image_serving_common.py +83 -0
- modelship/infer/infer_config.py +649 -0
- modelship/infer/llama_server/llama_server_infer.py +885 -0
- modelship/infer/model_deployment.py +459 -0
- modelship/infer/model_resolver.py +271 -0
- modelship/infer/replica_coordinator.py +170 -0
- modelship/infer/sherpa_onnx/bundle.py +76 -0
- modelship/infer/sherpa_onnx/registry.py +138 -0
- modelship/infer/sherpa_onnx/sherpa_onnx_infer.py +154 -0
- modelship/infer/stable_diffusion_cpp/openai/serving_image.py +181 -0
- modelship/infer/stable_diffusion_cpp/stable_diffusion_cpp_infer.py +138 -0
- modelship/infer/vllm/capabilities.py +20 -0
- modelship/infer/vllm/engine_ops.py +646 -0
- modelship/infer/vllm/openai/serving_speech.py +6 -0
- modelship/infer/vllm/parsing/__init__.py +6 -0
- modelship/infer/vllm/parsing/detect.py +252 -0
- modelship/infer/vllm/vllm_infer.py +884 -0
- modelship/infer/whispercpp/whispercpp_infer.py +322 -0
- modelship/launcher.py +114 -0
- modelship/logging.py +243 -0
- modelship/metrics.py +409 -0
- modelship/openai/api.py +1151 -0
- modelship/openai/auth.py +186 -0
- modelship/openai/compaction_crypto.py +99 -0
- modelship/openai/mcp/__init__.py +5 -0
- modelship/openai/mcp/client.py +108 -0
- modelship/openai/mcp/egress.py +60 -0
- modelship/openai/mcp/loop.py +706 -0
- modelship/openai/mcp/spec.py +199 -0
- modelship/openai/protocol/__init__.py +163 -0
- modelship/openai/protocol/audio.py +170 -0
- modelship/openai/protocol/base.py +21 -0
- modelship/openai/protocol/chat.py +194 -0
- modelship/openai/protocol/embeddings.py +43 -0
- modelship/openai/protocol/error.py +67 -0
- modelship/openai/protocol/images.py +115 -0
- modelship/openai/protocol/responses/__init__.py +83 -0
- modelship/openai/protocol/responses/adapter.py +452 -0
- modelship/openai/protocol/responses/schemas.py +275 -0
- modelship/openai/protocol/responses/streaming.py +395 -0
- modelship/openai/protocol/usage.py +26 -0
- modelship/openai/state/__init__.py +22 -0
- modelship/openai/state/responses.py +322 -0
- modelship/openai/utils/__init__.py +4 -0
- modelship/openai/utils/audio.py +16 -0
- modelship/openai/utils/chat.py +293 -0
- modelship/openai/utils/responses.py +691 -0
- modelship/preflight/__init__.py +42 -0
- modelship/preflight/base.py +584 -0
- modelship/preflight/llama_cpp.py +536 -0
- modelship/preflight/stable_diffusion_cpp.py +42 -0
- modelship/preflight/vllm.py +822 -0
- modelship/state/__init__.py +169 -0
- modelship/state/base.py +97 -0
- modelship/state/memory.py +254 -0
- modelship/state/redis.py +154 -0
- modelship/utils/__init__.py +157 -0
- modelship/utils/accelerator.py +46 -0
- modelship/utils/audio.py +69 -0
- modelship/utils/cache.py +18 -0
- modelship/utils/cli.py +250 -0
- modelship/utils/ray_auth.py +25 -0
- modelship/utils/request_id.py +23 -0
- mship_engine-0.7.12.dist-info/METADATA +312 -0
- mship_engine-0.7.12.dist-info/RECORD +80 -0
- mship_engine-0.7.12.dist-info/WHEEL +4 -0
modelship/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Ray Serve actor option construction for model deployments.
|
|
2
|
+
|
|
3
|
+
Centralises the GPU-allocation decisions for model deployments. Multi-slot
|
|
4
|
+
vLLM deploys always use a Ray Serve placement group (one whole-GPU bundle
|
|
5
|
+
per slot) that vLLM inherits via its ray distributed executor.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
|
|
13
|
+
from modelship.deploy.capabilities import deployment_capability_resources
|
|
14
|
+
from modelship.infer.infer_config import ModelLoader, ModelshipModelConfig
|
|
15
|
+
from modelship.logging import get_logger
|
|
16
|
+
from modelship.utils.cache import resolve_cache_root
|
|
17
|
+
|
|
18
|
+
logger = get_logger("startup")
|
|
19
|
+
|
|
20
|
+
# Forwarded from the driver to each replica's runtime_env: logging vars, the gateway
|
|
21
|
+
# name (metrics.py stamps every metric with it), MSHIP_METRICS so --no-metrics on
|
|
22
|
+
# the driver also disables metrics in the replicas (else they'd default to on),
|
|
23
|
+
# MSHIP_PREFLIGHT so --no-preflight on the driver also disables it in the replicas
|
|
24
|
+
# (preflight runs inside each loader's actor __init__, not on the driver), and the
|
|
25
|
+
# /v1/responses state-store tuning read inside the gateway replica's own process
|
|
26
|
+
# (state.responses.ttl_seconds / state.memory._sweep_interval_s), not the driver's.
|
|
27
|
+
_PASSTHROUGH_ENV_VARS = (
|
|
28
|
+
"MSHIP_LOG_LEVEL",
|
|
29
|
+
"MSHIP_LOG_FORMAT",
|
|
30
|
+
"MSHIP_LOG_TARGET",
|
|
31
|
+
"MSHIP_GATEWAY_NAME",
|
|
32
|
+
"MSHIP_METRICS",
|
|
33
|
+
"MSHIP_PREFLIGHT",
|
|
34
|
+
"MSHIP_RESPONSES_TTL_S",
|
|
35
|
+
"MSHIP_STATE_SWEEP_INTERVAL_S",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_passthrough_env_vars() -> dict[str, str]:
|
|
40
|
+
"""Driver→replica env vars (logging, gateway name, metrics) read off the
|
|
41
|
+
driver's environment. Shared by model and gateway deployments so both
|
|
42
|
+
replicas inherit the same logging/metrics config."""
|
|
43
|
+
return {var: os.environ[var] for var in _PASSTHROUGH_ENV_VARS if os.environ.get(var) is not None}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_cache_env_vars() -> dict[str, str]:
|
|
47
|
+
"""Resolve HF / vLLM / FlashInfer cache dirs, all rooted at MSHIP_CACHE_DIR.
|
|
48
|
+
|
|
49
|
+
Also forwards HF_TOKEN/HF_HUB_OFFLINE when set on the driver, so an actor
|
|
50
|
+
downloading a gated/offline model has the same auth."""
|
|
51
|
+
base_cache = resolve_cache_root()
|
|
52
|
+
env_vars = {
|
|
53
|
+
"HF_HOME": os.environ.get("HF_HOME", f"{base_cache}/huggingface"),
|
|
54
|
+
"VLLM_CACHE_ROOT": os.environ.get("VLLM_CACHE_ROOT", f"{base_cache}/vllm"),
|
|
55
|
+
"FLASHINFER_CACHE_DIR": os.environ.get("FLASHINFER_CACHE_DIR", f"{base_cache}/flashinfer"),
|
|
56
|
+
# Triton JITs kernels at import for some archs
|
|
57
|
+
"TRITON_CACHE_DIR": os.environ.get("TRITON_CACHE_DIR", f"{base_cache}/triton"),
|
|
58
|
+
# vLLM's usage-stats thread writes usage_stats.json/do_not_track here
|
|
59
|
+
"VLLM_CONFIG_ROOT": os.environ.get("VLLM_CONFIG_ROOT", f"{base_cache}/vllm-config"),
|
|
60
|
+
# Default download dir for the whispercpp loader's pywhispercpp-managed
|
|
61
|
+
# built-in model names (a bare `model:` like `base.en`).
|
|
62
|
+
"MSHIP_WHISPERCPP_CACHE_DIR": os.environ.get("MSHIP_WHISPERCPP_CACHE_DIR", f"{base_cache}/whispercpp"),
|
|
63
|
+
}
|
|
64
|
+
for var in ("HF_TOKEN", "HF_HUB_OFFLINE"):
|
|
65
|
+
if os.environ.get(var) is not None:
|
|
66
|
+
env_vars[var] = os.environ[var]
|
|
67
|
+
return env_vars
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _world_size(config: ModelshipModelConfig) -> int:
|
|
71
|
+
if config.loader != ModelLoader.vllm:
|
|
72
|
+
return 1
|
|
73
|
+
tp = config.vllm_engine_kwargs.tensor_parallel_size
|
|
74
|
+
pp = config.vllm_engine_kwargs.pipeline_parallel_size
|
|
75
|
+
return tp * pp
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def total_gpu_reservation(deploy_opts: dict) -> float:
|
|
79
|
+
"""Sum the GPU units this deployment (actor + any PG bundles) will consume.
|
|
80
|
+
|
|
81
|
+
Used by the coordinator's resource tracker, which can't read the PG
|
|
82
|
+
bundle list as a single scalar.
|
|
83
|
+
"""
|
|
84
|
+
return _total_reservation(deploy_opts, "GPU", "num_gpus")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def total_cpu_reservation(deploy_opts: dict) -> float:
|
|
88
|
+
"""Sum the CPU units this deployment (actor + any PG bundles) will consume.
|
|
89
|
+
|
|
90
|
+
For multi-slot deploys the outer actor sits in bundle 0 and its CPU
|
|
91
|
+
request is satisfied from that bundle's reservation, so summing the
|
|
92
|
+
bundles gives the correct total — same shape as the GPU helper.
|
|
93
|
+
"""
|
|
94
|
+
return _total_reservation(deploy_opts, "CPU", "num_cpus")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _total_reservation(deploy_opts: dict, bundle_key: str, actor_key: str) -> float:
|
|
98
|
+
if "placement_group_bundles" in deploy_opts:
|
|
99
|
+
return float(sum(b.get(bundle_key, 0) for b in deploy_opts["placement_group_bundles"]))
|
|
100
|
+
return float(deploy_opts.get("ray_actor_options", {}).get(actor_key, 0) or 0)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def build_deployment_options(config: ModelshipModelConfig) -> dict:
|
|
104
|
+
"""Return a kwargs dict for `Deployment.options(**...)`.
|
|
105
|
+
|
|
106
|
+
Always contains ``ray_actor_options``; for multi-slot vLLM deploys also
|
|
107
|
+
contains ``placement_group_bundles`` and ``placement_group_strategy`` so
|
|
108
|
+
Ray Serve allocates one whole-GPU bundle per slot and vLLM's ray executor
|
|
109
|
+
inherits the PG. When the model config sets ``max_ongoing_requests`` it is
|
|
110
|
+
forwarded as the per-replica Ray Serve concurrency cap.
|
|
111
|
+
"""
|
|
112
|
+
env_vars = build_cache_env_vars()
|
|
113
|
+
env_vars.update(build_passthrough_env_vars())
|
|
114
|
+
|
|
115
|
+
runtime_env: dict = {"env_vars": env_vars}
|
|
116
|
+
|
|
117
|
+
capability_resources = deployment_capability_resources(config)
|
|
118
|
+
|
|
119
|
+
# sherpa_onnx never touches CUDA or CoreML (CPU only); ggml-backed loaders are
|
|
120
|
+
# CPU-only off Darwin, where forcing 0 would mislead Ray into co-scheduling
|
|
121
|
+
# another GPU actor onto the device Metal is actually using.
|
|
122
|
+
force_zero_gpu = config.loader == ModelLoader.sherpa_onnx or (
|
|
123
|
+
config.loader in (ModelLoader.stable_diffusion_cpp, ModelLoader.whispercpp) and platform.system() != "Darwin"
|
|
124
|
+
)
|
|
125
|
+
if force_zero_gpu:
|
|
126
|
+
if config.num_gpus > 0:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"num_gpus=%s is ignored for model '%s': %s loader has no GPU backend here.",
|
|
129
|
+
config.num_gpus,
|
|
130
|
+
config.name,
|
|
131
|
+
config.loader.value,
|
|
132
|
+
)
|
|
133
|
+
opts: dict = {
|
|
134
|
+
"ray_actor_options": {
|
|
135
|
+
"num_gpus": 0,
|
|
136
|
+
"num_cpus": config.num_cpus,
|
|
137
|
+
"runtime_env": runtime_env,
|
|
138
|
+
"resources": capability_resources,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else:
|
|
142
|
+
world_size = _world_size(config)
|
|
143
|
+
if world_size == 1:
|
|
144
|
+
# Single slot: scalar Ray allocation. Fractional num_gpus (0 < n < 1)
|
|
145
|
+
# lets Ray pack other actors onto the same physical GPU.
|
|
146
|
+
opts = {
|
|
147
|
+
"ray_actor_options": {
|
|
148
|
+
"num_gpus": config.num_gpus,
|
|
149
|
+
"num_cpus": config.num_cpus,
|
|
150
|
+
"runtime_env": runtime_env,
|
|
151
|
+
"resources": capability_resources,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else:
|
|
155
|
+
# Multi-slot: one PG bundle per slot, STRICT_PACK keeps them on the
|
|
156
|
+
# same node (NVLink). Outer actor sits in bundle 0 with 0 GPU; vLLM's
|
|
157
|
+
# ray executor reuses the PG via get_current_placement_group() and
|
|
158
|
+
# pins each worker actor to its bundle. Each bundle requests a whole
|
|
159
|
+
# GPU, so Ray spreads across distinct physical GPUs. The capability
|
|
160
|
+
# resource is requested on every bundle (not the outer actor) since
|
|
161
|
+
# the bundles are what pin the deploy to a capable node.
|
|
162
|
+
bundles = [{"GPU": 1, "CPU": config.num_cpus, **capability_resources} for _ in range(world_size)]
|
|
163
|
+
opts = {
|
|
164
|
+
"ray_actor_options": {"num_gpus": 0, "num_cpus": config.num_cpus, "runtime_env": runtime_env},
|
|
165
|
+
"placement_group_bundles": bundles,
|
|
166
|
+
"placement_group_strategy": "STRICT_PACK",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Per-model Ray Serve concurrency cap; only override the default when set.
|
|
170
|
+
# The reservation helpers read only the GPU/CPU keys, so this is inert there.
|
|
171
|
+
max_ongoing = config.max_ongoing_requests
|
|
172
|
+
if max_ongoing is None and config.loader == ModelLoader.llama_server:
|
|
173
|
+
parallel = config.llama_server_config.parallel if config.llama_server_config else 1
|
|
174
|
+
max_ongoing = parallel
|
|
175
|
+
|
|
176
|
+
if max_ongoing is not None:
|
|
177
|
+
opts["max_ongoing_requests"] = max_ongoing
|
|
178
|
+
return opts
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Node/deployment capability resources: `mship_<loader>` alongside Ray's native
|
|
2
|
+
`GPU`, so a deploy only schedules onto a node with that loader installed.
|
|
3
|
+
|
|
4
|
+
Must stay ray-free at import time: modelship/launcher.py imports LOADER_MODULES
|
|
5
|
+
from here before resolve_ray_auth_env runs, so the ModelshipModelConfig annotation
|
|
6
|
+
below is deferred behind `if TYPE_CHECKING` rather than imported for real.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.util
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from modelship.infer.infer_config import ModelshipModelConfig
|
|
19
|
+
|
|
20
|
+
RESOURCE_PREFIX = "mship_"
|
|
21
|
+
|
|
22
|
+
# loader name -> the module find_spec() probes for it. llama_server is handled
|
|
23
|
+
# separately below (a subprocess binary, not an importable module).
|
|
24
|
+
LOADER_MODULES = {
|
|
25
|
+
"vllm": "vllm",
|
|
26
|
+
"diffusers": "diffusers",
|
|
27
|
+
"stable_diffusion_cpp": "stable_diffusion_cpp",
|
|
28
|
+
"whispercpp": "pywhispercpp",
|
|
29
|
+
"sherpa_onnx": "sherpa_onnx",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_LLAMA_SERVER_LOADER = "llama_server"
|
|
33
|
+
|
|
34
|
+
# Every loader that gets a capability resource, including llama_server (probed via a
|
|
35
|
+
# binary check below, not find_spec like the LOADER_MODULES entries).
|
|
36
|
+
ALL_CAPABILITY_LOADERS = frozenset({*LOADER_MODULES, _LLAMA_SERVER_LOADER})
|
|
37
|
+
|
|
38
|
+
# Matches a wrapper script's `exec <target> ...` line, quoted or not (Dockerfile's
|
|
39
|
+
# llama-server.sh emits it unquoted; launcher._write_wrapper quotes it).
|
|
40
|
+
_WRAPPER_EXEC_RE = re.compile(r'exec\s+"?([^"\s]+)"?')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def node_capability_resources() -> dict[str, float]:
|
|
44
|
+
"""{"mship_vllm": 1, ...} for every loader this node can run. MSHIP_NODE_CAPABILITIES
|
|
45
|
+
(a JSON object) overrides the probe wholesale."""
|
|
46
|
+
override = os.environ.get("MSHIP_NODE_CAPABILITIES")
|
|
47
|
+
if override:
|
|
48
|
+
return {str(name): float(qty) for name, qty in json.loads(override).items()}
|
|
49
|
+
|
|
50
|
+
resources: dict[str, float] = {}
|
|
51
|
+
for loader, module in LOADER_MODULES.items():
|
|
52
|
+
if importlib.util.find_spec(module) is not None:
|
|
53
|
+
resources[f"{RESOURCE_PREFIX}{loader}"] = 1
|
|
54
|
+
if _llama_server_available():
|
|
55
|
+
resources[f"{RESOURCE_PREFIX}{_LLAMA_SERVER_LOADER}"] = 1
|
|
56
|
+
return resources
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def deployment_capability_resources(config: ModelshipModelConfig) -> dict[str, float]:
|
|
60
|
+
"""{"mship_vllm": 0.001} for the config's loader."""
|
|
61
|
+
return {f"{RESOURCE_PREFIX}{config.loader!s}": 0.001}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _llama_server_available() -> bool:
|
|
65
|
+
"""`thin` bakes the wrapper script unconditionally but ships no binary behind
|
|
66
|
+
it, so a bare existence check on MSHIP_LLAMA_SERVER_BIN can't tell `thin`
|
|
67
|
+
apart from `cpu`/`cuda`/`metal` — resolve the wrapper's own exec target."""
|
|
68
|
+
bin_path = os.environ.get("MSHIP_LLAMA_SERVER_BIN")
|
|
69
|
+
if not bin_path or not os.path.isfile(bin_path):
|
|
70
|
+
return False
|
|
71
|
+
target = _wrapper_exec_target(bin_path)
|
|
72
|
+
if target is None:
|
|
73
|
+
return True # not a recognized wrapper — the existence check above suffices
|
|
74
|
+
return os.path.isfile(target)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _wrapper_exec_target(path: str) -> str | None:
|
|
78
|
+
try:
|
|
79
|
+
with open(path) as f:
|
|
80
|
+
content = f.read()
|
|
81
|
+
except OSError:
|
|
82
|
+
return None
|
|
83
|
+
match = _WRAPPER_EXEC_RE.search(content)
|
|
84
|
+
return match.group(1) if match else None
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from pydantic_yaml import parse_yaml_raw_as
|
|
6
|
+
|
|
7
|
+
from modelship.infer.infer_config import ModelLoader, ModelshipConfig
|
|
8
|
+
from modelship.infer.model_resolver import check_model_source
|
|
9
|
+
from modelship.logging import get_logger
|
|
10
|
+
from modelship.utils import is_pathy
|
|
11
|
+
|
|
12
|
+
logger = get_logger("startup")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def default_config_path(config_dir: Path | None = None) -> Path:
|
|
16
|
+
"""The default config/models.yaml path used absent an explicit --config."""
|
|
17
|
+
config_dir = config_dir or Path(__file__).resolve().parent.parent.parent / "config"
|
|
18
|
+
return config_dir / "models.yaml"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_config_path(arg_path: str | None, config_dir: Path | None = None) -> str:
|
|
22
|
+
"""Resolve the models.yaml to deploy.
|
|
23
|
+
|
|
24
|
+
Precedence:
|
|
25
|
+
1. An explicit ``--config`` path always wins (most specific signal); it must exist.
|
|
26
|
+
2. Otherwise the default ``config/models.yaml`` must exist.
|
|
27
|
+
"""
|
|
28
|
+
if arg_path:
|
|
29
|
+
if not os.path.exists(arg_path):
|
|
30
|
+
raise FileNotFoundError(f"--config {arg_path} not found.")
|
|
31
|
+
return arg_path
|
|
32
|
+
|
|
33
|
+
default = default_config_path(config_dir)
|
|
34
|
+
if default.exists():
|
|
35
|
+
return str(default)
|
|
36
|
+
|
|
37
|
+
raise FileNotFoundError(f"{default} not found. Copy an example config from config/examples/ to config/models.yaml.")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_yaml_config(arg_path: str | None) -> ModelshipConfig:
|
|
41
|
+
with open(resolve_config_path(arg_path)) as f:
|
|
42
|
+
return parse_yaml_raw_as(ModelshipConfig, f)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_raw_models(arg_path: str | None) -> list[dict]:
|
|
46
|
+
"""Read the user's models.yaml as raw, pre-validation dicts.
|
|
47
|
+
|
|
48
|
+
The effective-config store keeps raw dicts (not validated configs, which don't
|
|
49
|
+
round-trip through num_gpus/tp normalization), so the deploy path merges at the
|
|
50
|
+
raw-dict level; ``merge()`` validates this input before folding it in, and the
|
|
51
|
+
merged result is validated again before deploy."""
|
|
52
|
+
with open(resolve_config_path(arg_path)) as f:
|
|
53
|
+
data = yaml.safe_load(f) or {}
|
|
54
|
+
if not isinstance(data, dict):
|
|
55
|
+
raise ValueError("models.yaml: top-level document must be a mapping with a 'models' key.")
|
|
56
|
+
models = data.get("models", [])
|
|
57
|
+
if not isinstance(models, list):
|
|
58
|
+
raise ValueError("models.yaml: 'models' must be a list.")
|
|
59
|
+
return models
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_whispercpp_builtin_ref(model: str) -> bool:
|
|
63
|
+
"""A bare pywhispercpp built-in name (e.g. `base.en`) — no repo or path
|
|
64
|
+
separator. Validated in the actor; the driver may lack pywhispercpp."""
|
|
65
|
+
return "/" not in model and not os.path.exists(model)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _resolve_sherpa_onnx_source(cfg) -> None:
|
|
69
|
+
"""Not an HF repo, so no `check_model_source`. A local-directory `model:` is
|
|
70
|
+
validated here at driver preflight; a bare registry name has nothing to pin
|
|
71
|
+
since the tarball fetch happens in the actor."""
|
|
72
|
+
from modelship.infer.sherpa_onnx.bundle import validate_bundle
|
|
73
|
+
from modelship.infer.sherpa_onnx.registry import REGISTRY
|
|
74
|
+
|
|
75
|
+
model = cfg.model
|
|
76
|
+
assert model is not None # validator guarantees this for built-in loaders
|
|
77
|
+
if is_pathy(model):
|
|
78
|
+
path = os.path.expanduser(model)
|
|
79
|
+
name = os.path.basename(path.rstrip("/"))
|
|
80
|
+
entry = REGISTRY[name] # config validation already guarantees this key exists
|
|
81
|
+
logger.info("Checking sherpa_onnx bundle for '%s': %s", cfg.name, path)
|
|
82
|
+
validate_bundle(path, entry)
|
|
83
|
+
logger.info("Checked '%s' (local bundle, no download)", cfg.name)
|
|
84
|
+
else:
|
|
85
|
+
logger.info(
|
|
86
|
+
"Skipping source check for '%s': sherpa_onnx registry model %r is fetched by the actor", cfg.name, model
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def resolve_all_model_sources(yml_conf: ModelshipConfig) -> None:
|
|
91
|
+
"""Pre-flight: check every built-in-loader model's source, without
|
|
92
|
+
downloading any weight bytes.
|
|
93
|
+
|
|
94
|
+
Populates `_pinned_source` (and, for llama_server, the mmproj pin) on each
|
|
95
|
+
config in place; actual download happens per-replica in
|
|
96
|
+
`BaseInfer.ensure_downloaded`. Raises on the first failure (auth,
|
|
97
|
+
missing repo, missing file, glob-no-match) so the operator sees it before
|
|
98
|
+
any Ray actor spins up.
|
|
99
|
+
|
|
100
|
+
Note: HF_HOME / VLLM_CACHE_ROOT / FLASHINFER_CACHE_DIR are set at module
|
|
101
|
+
load time in mship_deploy.py — `huggingface_hub.HF_HOME` is latched at
|
|
102
|
+
import, so setting them later doesn't take effect.
|
|
103
|
+
"""
|
|
104
|
+
for cfg in yml_conf.models:
|
|
105
|
+
if cfg.loader == ModelLoader.whispercpp and cfg.model and _is_whispercpp_builtin_ref(cfg.model):
|
|
106
|
+
# pywhispercpp resolves/downloads its own built-in models; nothing to pin here.
|
|
107
|
+
logger.info("Skipping source check for '%s': pywhispercpp built-in model %r", cfg.name, cfg.model)
|
|
108
|
+
continue
|
|
109
|
+
if cfg.loader == ModelLoader.sherpa_onnx:
|
|
110
|
+
_resolve_sherpa_onnx_source(cfg)
|
|
111
|
+
continue
|
|
112
|
+
assert cfg.model is not None # validator guarantees this for built-in loaders
|
|
113
|
+
trust_remote_code = bool(cfg.vllm_engine_kwargs and cfg.vllm_engine_kwargs.trust_remote_code)
|
|
114
|
+
logger.info("Checking model source for '%s': %s", cfg.name, cfg.model)
|
|
115
|
+
cfg._pinned_source = check_model_source(cfg.model, trust_remote_code=trust_remote_code)
|
|
116
|
+
logger.info("Checked '%s' (revision=%s)", cfg.name, cfg._pinned_source.revision or "local")
|
|
117
|
+
|
|
118
|
+
if cfg.loader == ModelLoader.llama_server and cfg.llama_server_config and cfg.llama_server_config.mmproj:
|
|
119
|
+
logger.info("Checking mmproj source for '%s': %s", cfg.name, cfg.llama_server_config.mmproj)
|
|
120
|
+
cfg.llama_server_config._pinned_mmproj = check_model_source(
|
|
121
|
+
cfg.llama_server_config.mmproj, trust_remote_code=trust_remote_code
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# GGUF is not supported on the vllm loader (vLLM 0.24 dropped in-tree
|
|
125
|
+
# GGUF). Reject early using the listed filename, before any download.
|
|
126
|
+
if cfg.loader == ModelLoader.vllm and cfg._pinned_source.resolves_to_gguf:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"Model '{cfg.name}' resolves to a GGUF file, which the vllm loader does not support "
|
|
129
|
+
f"(vLLM 0.24 dropped in-tree GGUF). Use `loader: llama_server` for GGUF models, or point "
|
|
130
|
+
f"the vllm loader at a non-GGUF checkpoint (safetensors, or an AWQ/GPTQ/FP8 quant)."
|
|
131
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Per-gateway *effective config* — the durable desired-state for deploys.
|
|
2
|
+
|
|
3
|
+
Every ``mship_deploy`` invocation, whatever its mode, folds the user's input into
|
|
4
|
+
the gateway's effective set (additive = union; reconcile = replace), then
|
|
5
|
+
the deploy ALWAYS reconciles the live cluster to that effective set. Self-heal is
|
|
6
|
+
then just "re-run the deploy": it reads the persisted effective set and reconciles
|
|
7
|
+
onto an empty cluster, restoring the TRUE live set after the cluster dies — not
|
|
8
|
+
just whatever the last user input happened to contain.
|
|
9
|
+
|
|
10
|
+
The store holds **raw, user-equivalent model dicts**, NOT serialized validated
|
|
11
|
+
configs: ``ModelshipModelConfig``'s ``num_gpus``/``tensor_parallel_size``
|
|
12
|
+
normalization is not idempotent, so a dumped validated config fails (or silently
|
|
13
|
+
mutates its fingerprint) on reload. Raw input dicts reload exactly as written.
|
|
14
|
+
|
|
15
|
+
This is the deploy-domain layer over the generic ``modelship.state`` store.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Literal
|
|
19
|
+
|
|
20
|
+
from modelship.infer.infer_config import ModelshipConfig, ModelshipModelConfig
|
|
21
|
+
from modelship.logging import get_logger
|
|
22
|
+
from modelship.state import StateStore
|
|
23
|
+
|
|
24
|
+
logger = get_logger("startup")
|
|
25
|
+
|
|
26
|
+
DeployMode = Literal["additive", "reconcile"]
|
|
27
|
+
|
|
28
|
+
# State-store namespace; one key per gateway: "effective/<gateway-name>".
|
|
29
|
+
_NAMESPACE = "effective"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_mode(*, reconcile: bool) -> DeployMode:
|
|
33
|
+
"""Map the CLI flags to the effective-config merge verb."""
|
|
34
|
+
return "reconcile" if reconcile else "additive"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _deployment_name(raw: dict, gateway_name: str) -> str:
|
|
38
|
+
"""Deployment name (name + fingerprint) for a raw model dict — the identity
|
|
39
|
+
key for additive de-dup and fatal-failure eviction. Validates the dict
|
|
40
|
+
(running normalization) so two raw dicts that normalize identically map to the
|
|
41
|
+
same deployment."""
|
|
42
|
+
return ModelshipModelConfig.model_validate(raw).deployment_name(gateway_name)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _model_name(raw: dict) -> str:
|
|
46
|
+
"""Human-facing model name for a raw model dict."""
|
|
47
|
+
return ModelshipModelConfig.model_validate(raw).name
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _identity(raw: dict, gateway_name: str) -> tuple[str, str]:
|
|
51
|
+
"""(deployment_name, model_name) for a raw model dict from one validation pass."""
|
|
52
|
+
cfg = ModelshipModelConfig.model_validate(raw)
|
|
53
|
+
return cfg.deployment_name(gateway_name), cfg.name
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def merge(
|
|
57
|
+
effective_raw: list[dict],
|
|
58
|
+
input_raw: list[dict],
|
|
59
|
+
gateway_name: str,
|
|
60
|
+
mode: DeployMode,
|
|
61
|
+
) -> list[dict]:
|
|
62
|
+
"""Fold the user's input into the effective raw model set under *mode*.
|
|
63
|
+
|
|
64
|
+
- additive: replace-by-name — identical config (same deployment_name) is an
|
|
65
|
+
idempotent skip; a different config sharing a model name replaces the
|
|
66
|
+
existing entry for that name rather than joining it.
|
|
67
|
+
- reconcile: input replaces the effective set entirely.
|
|
68
|
+
|
|
69
|
+
Validates *input_raw* alone (not the merged result) via ModelshipConfig, so a
|
|
70
|
+
model name reused with a different config in this file is rejected before it
|
|
71
|
+
ever reaches the persisted effective set; pre-existing effective state from
|
|
72
|
+
before this rule existed is left alone.
|
|
73
|
+
"""
|
|
74
|
+
to_config(input_raw)
|
|
75
|
+
if mode == "reconcile":
|
|
76
|
+
return list(input_raw)
|
|
77
|
+
|
|
78
|
+
# dep_name -> raw dict, and model_name -> its current dep_name, both built in
|
|
79
|
+
# one validation pass per dict so lookups below are O(1) instead of rescanning
|
|
80
|
+
# (and re-validating) the whole accumulated set per input entry.
|
|
81
|
+
merged: dict[str, dict] = {}
|
|
82
|
+
dep_name_by_model_name: dict[str, str] = {}
|
|
83
|
+
for m in effective_raw:
|
|
84
|
+
dep_name, model_name = _identity(m, gateway_name)
|
|
85
|
+
merged[dep_name] = m
|
|
86
|
+
dep_name_by_model_name[model_name] = dep_name
|
|
87
|
+
|
|
88
|
+
for d in input_raw:
|
|
89
|
+
dep_name, model_name = _identity(d, gateway_name)
|
|
90
|
+
if dep_name in merged:
|
|
91
|
+
continue
|
|
92
|
+
prior_dep_name = dep_name_by_model_name.get(model_name)
|
|
93
|
+
if prior_dep_name is not None:
|
|
94
|
+
del merged[prior_dep_name]
|
|
95
|
+
merged[dep_name] = d
|
|
96
|
+
dep_name_by_model_name[model_name] = dep_name
|
|
97
|
+
|
|
98
|
+
return list(merged.values())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def deployment_names(raw_models: list[dict], gateway_name: str) -> set[str]:
|
|
102
|
+
"""The deployment-name set for raw model dicts — the identity set of what's
|
|
103
|
+
under this gateway's effective management. Passed to the deploy plan so a
|
|
104
|
+
reconcile only removes deployments that WERE effective-managed (never legacy /
|
|
105
|
+
un-tracked deployments or another gateway's apps). Relies on the effective
|
|
106
|
+
config being per-gateway and the gateway being folded into each fingerprint."""
|
|
107
|
+
return {_deployment_name(d, gateway_name) for d in raw_models}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def to_config(raw_models: list[dict]) -> ModelshipConfig:
|
|
111
|
+
"""Validate raw model dicts into a ModelshipConfig for the deploy path."""
|
|
112
|
+
return ModelshipConfig.model_validate({"models": raw_models})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def read_effective(store: StateStore, gateway_name: str) -> list[dict]:
|
|
116
|
+
"""Return the persisted effective raw model set for *gateway_name* (empty if
|
|
117
|
+
none yet)."""
|
|
118
|
+
data = store.get(f"{_NAMESPACE}/{gateway_name}")
|
|
119
|
+
if not isinstance(data, dict):
|
|
120
|
+
return []
|
|
121
|
+
models = data.get("models", [])
|
|
122
|
+
if not isinstance(models, list):
|
|
123
|
+
logger.warning("Effective config for gateway %r has non-list 'models'; treating as empty.", gateway_name)
|
|
124
|
+
return []
|
|
125
|
+
return models
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def write_effective(store: StateStore, gateway_name: str, raw_models: list[dict]) -> None:
|
|
129
|
+
"""Persist the effective raw model set for *gateway_name*."""
|
|
130
|
+
store.set(f"{_NAMESPACE}/{gateway_name}", {"models": raw_models})
|
|
131
|
+
logger.info("Effective config for gateway %r now has %d model(s).", gateway_name, len(raw_models))
|