mship 0.7.3__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 +186 -0
- modelship/deploy/config.py +109 -0
- modelship/deploy/effective_config.py +101 -0
- modelship/deploy/serve_utils.py +468 -0
- modelship/deploy/strategy.py +244 -0
- modelship/driver.py +366 -0
- modelship/infer/base_infer.py +467 -0
- modelship/infer/base_serving.py +21 -0
- modelship/infer/custom/custom_infer.py +89 -0
- modelship/infer/custom/openai/serving_speech.py +56 -0
- modelship/infer/custom/openai/serving_transcription.py +83 -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 +621 -0
- modelship/infer/llama_server/llama_server_infer.py +854 -0
- modelship/infer/model_deployment.py +454 -0
- modelship/infer/model_resolver.py +249 -0
- modelship/infer/replica_coordinator.py +158 -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 +632 -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 +863 -0
- modelship/launcher.py +163 -0
- modelship/logging.py +229 -0
- modelship/metrics.py +409 -0
- modelship/openai/api.py +1074 -0
- modelship/openai/auth.py +186 -0
- modelship/openai/compaction_crypto.py +99 -0
- modelship/openai/protocol/__init__.py +169 -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/raw.py +57 -0
- modelship/openai/protocol/responses/__init__.py +73 -0
- modelship/openai/protocol/responses/adapter.py +361 -0
- modelship/openai/protocol/responses/schemas.py +227 -0
- modelship/openai/protocol/responses/streaming.py +393 -0
- modelship/openai/protocol/usage.py +26 -0
- modelship/openai/state/__init__.py +22 -0
- modelship/openai/state/responses.py +119 -0
- modelship/openai/utils/__init__.py +4 -0
- modelship/openai/utils/chat.py +293 -0
- modelship/openai/utils/responses.py +333 -0
- modelship/plugins/base_plugin.py +147 -0
- modelship/preflight/__init__.py +38 -0
- modelship/preflight/base.py +501 -0
- modelship/preflight/llama_cpp.py +531 -0
- modelship/preflight/stable_diffusion_cpp.py +42 -0
- modelship/preflight/vllm.py +896 -0
- modelship/state/__init__.py +161 -0
- modelship/state/base.py +83 -0
- modelship/state/memory.py +208 -0
- modelship/state/redis.py +129 -0
- modelship/utils/__init__.py +113 -0
- modelship/utils/accelerator.py +29 -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-0.7.3.dist-info/METADATA +319 -0
- mship-0.7.3.dist-info/RECORD +75 -0
- mship-0.7.3.dist-info/WHEEL +4 -0
- mship-0.7.3.dist-info/entry_points.txt +3 -0
modelship/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Ray Serve actor option construction for model deployments.
|
|
2
|
+
|
|
3
|
+
Centralises the GPU-allocation decisions and the plugin-wheel runtime_env
|
|
4
|
+
injection for custom-loader models. Multi-slot vLLM deploys always use a
|
|
5
|
+
Ray Serve placement group (one whole-GPU bundle per slot) that vLLM
|
|
6
|
+
inherits via its ray distributed executor.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from modelship.infer.infer_config import ModelLoader, ModelshipModelConfig
|
|
16
|
+
from modelship.logging import get_logger
|
|
17
|
+
from modelship.utils.cache import resolve_cache_root
|
|
18
|
+
|
|
19
|
+
logger = get_logger("startup")
|
|
20
|
+
|
|
21
|
+
# Forwarded from the driver to each replica's runtime_env: logging vars, the gateway
|
|
22
|
+
# name (metrics.py stamps every metric with it), MSHIP_METRICS so --no-metrics on
|
|
23
|
+
# the driver also disables metrics in the replicas (else they'd default to on),
|
|
24
|
+
# MSHIP_PREFLIGHT so --no-preflight on the driver also disables it in the replicas
|
|
25
|
+
# (preflight runs inside each loader's actor __init__, not on the driver), and the
|
|
26
|
+
# /v1/responses state-store tuning read inside the gateway replica's own process
|
|
27
|
+
# (state.responses.ttl_seconds / state.memory._sweep_interval_s), not the driver's.
|
|
28
|
+
_PASSTHROUGH_ENV_VARS = (
|
|
29
|
+
"MSHIP_LOG_LEVEL",
|
|
30
|
+
"MSHIP_LOG_FORMAT",
|
|
31
|
+
"MSHIP_LOG_TARGET",
|
|
32
|
+
"MSHIP_GATEWAY_NAME",
|
|
33
|
+
"MSHIP_METRICS",
|
|
34
|
+
"MSHIP_PREFLIGHT",
|
|
35
|
+
"MSHIP_RESPONSES_TTL_S",
|
|
36
|
+
"MSHIP_STATE_SWEEP_INTERVAL_S",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_passthrough_env_vars() -> dict[str, str]:
|
|
41
|
+
"""Driver→replica env vars (logging, gateway name, metrics) read off the
|
|
42
|
+
driver's environment. Shared by model and gateway deployments so both
|
|
43
|
+
replicas inherit the same logging/metrics config."""
|
|
44
|
+
return {var: os.environ[var] for var in _PASSTHROUGH_ENV_VARS if os.environ.get(var) is not None}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_cache_env_vars() -> dict[str, str]:
|
|
48
|
+
"""Resolve HF / vLLM / FlashInfer cache dirs, all rooted at MSHIP_CACHE_DIR.
|
|
49
|
+
|
|
50
|
+
Also forwards HF_TOKEN/HF_HUB_OFFLINE when set on the driver, so an actor
|
|
51
|
+
downloading a gated/offline model has the same auth."""
|
|
52
|
+
base_cache = resolve_cache_root()
|
|
53
|
+
env_vars = {
|
|
54
|
+
"HF_HOME": os.environ.get("HF_HOME", f"{base_cache}/huggingface"),
|
|
55
|
+
"VLLM_CACHE_ROOT": os.environ.get("VLLM_CACHE_ROOT", f"{base_cache}/vllm"),
|
|
56
|
+
"FLASHINFER_CACHE_DIR": os.environ.get("FLASHINFER_CACHE_DIR", f"{base_cache}/flashinfer"),
|
|
57
|
+
# Triton JITs kernels at import for some archs
|
|
58
|
+
"TRITON_CACHE_DIR": os.environ.get("TRITON_CACHE_DIR", f"{base_cache}/triton"),
|
|
59
|
+
# vLLM's usage-stats thread writes usage_stats.json/do_not_track here
|
|
60
|
+
"VLLM_CONFIG_ROOT": os.environ.get("VLLM_CONFIG_ROOT", f"{base_cache}/vllm-config"),
|
|
61
|
+
}
|
|
62
|
+
for var in ("HF_TOKEN", "HF_HUB_OFFLINE"):
|
|
63
|
+
if os.environ.get(var) is not None:
|
|
64
|
+
env_vars[var] = os.environ[var]
|
|
65
|
+
return env_vars
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _plugin_wheel_dir() -> Path:
|
|
69
|
+
return Path(os.environ.get("MSHIP_PLUGIN_WHEEL_DIR", ".build/plugin-wheels"))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve_plugin_wheel(plugin: str) -> Path:
|
|
73
|
+
wheel_dir = _plugin_wheel_dir()
|
|
74
|
+
normalized_name = plugin.replace("-", "_")
|
|
75
|
+
wheels = sorted(wheel_dir.glob(f"{normalized_name}-*.whl"))
|
|
76
|
+
if not wheels:
|
|
77
|
+
raise RuntimeError(
|
|
78
|
+
f"No wheel found for plugin '{plugin}' (normalized: '{normalized_name}') in {wheel_dir}. "
|
|
79
|
+
f"Build wheels with `make plugin-wheels` (or rebuild the Docker image), "
|
|
80
|
+
f"or set MSHIP_PLUGIN_WHEEL_DIR to the directory containing them."
|
|
81
|
+
)
|
|
82
|
+
# Absolute path required: Ray workers run with a different cwd
|
|
83
|
+
# (/tmp/ray/session_*/runtime_resources/.../exec_cwd), so a relative wheel
|
|
84
|
+
# path in runtime_env.pip would fail to resolve on the worker.
|
|
85
|
+
return wheels[-1].resolve()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _world_size(config: ModelshipModelConfig) -> int:
|
|
89
|
+
if config.loader != ModelLoader.vllm:
|
|
90
|
+
return 1
|
|
91
|
+
tp = config.vllm_engine_kwargs.tensor_parallel_size
|
|
92
|
+
pp = config.vllm_engine_kwargs.pipeline_parallel_size
|
|
93
|
+
return tp * pp
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def total_gpu_reservation(deploy_opts: dict) -> float:
|
|
97
|
+
"""Sum the GPU units this deployment (actor + any PG bundles) will consume.
|
|
98
|
+
|
|
99
|
+
Used by the coordinator's resource tracker, which can't read the PG
|
|
100
|
+
bundle list as a single scalar.
|
|
101
|
+
"""
|
|
102
|
+
return _total_reservation(deploy_opts, "GPU", "num_gpus")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def total_cpu_reservation(deploy_opts: dict) -> float:
|
|
106
|
+
"""Sum the CPU units this deployment (actor + any PG bundles) will consume.
|
|
107
|
+
|
|
108
|
+
For multi-slot deploys the outer actor sits in bundle 0 and its CPU
|
|
109
|
+
request is satisfied from that bundle's reservation, so summing the
|
|
110
|
+
bundles gives the correct total — same shape as the GPU helper.
|
|
111
|
+
"""
|
|
112
|
+
return _total_reservation(deploy_opts, "CPU", "num_cpus")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _total_reservation(deploy_opts: dict, bundle_key: str, actor_key: str) -> float:
|
|
116
|
+
if "placement_group_bundles" in deploy_opts:
|
|
117
|
+
return float(sum(b.get(bundle_key, 0) for b in deploy_opts["placement_group_bundles"]))
|
|
118
|
+
return float(deploy_opts.get("ray_actor_options", {}).get(actor_key, 0) or 0)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def build_deployment_options(config: ModelshipModelConfig, plugin_wheel: Path | None = None) -> dict:
|
|
122
|
+
"""Return a kwargs dict for `Deployment.options(**...)`.
|
|
123
|
+
|
|
124
|
+
Always contains ``ray_actor_options``; for multi-slot vLLM deploys also
|
|
125
|
+
contains ``placement_group_bundles`` and ``placement_group_strategy`` so
|
|
126
|
+
Ray Serve allocates one whole-GPU bundle per slot and vLLM's ray executor
|
|
127
|
+
inherits the PG. When the model config sets ``max_ongoing_requests`` it is
|
|
128
|
+
forwarded as the per-replica Ray Serve concurrency cap.
|
|
129
|
+
"""
|
|
130
|
+
env_vars = build_cache_env_vars()
|
|
131
|
+
env_vars.update(build_passthrough_env_vars())
|
|
132
|
+
|
|
133
|
+
runtime_env: dict = {"env_vars": env_vars}
|
|
134
|
+
if plugin_wheel is not None:
|
|
135
|
+
# Ship the plugin to the Ray worker via runtime_env. Ray content-hashes
|
|
136
|
+
# and caches the resulting per-job venv, so repeat deploys of the same
|
|
137
|
+
# wheel reuse the install.
|
|
138
|
+
runtime_env["pip"] = [str(plugin_wheel)]
|
|
139
|
+
|
|
140
|
+
if config.loader == ModelLoader.stable_diffusion_cpp and platform.system() != "Darwin":
|
|
141
|
+
# Off Darwin the loader's ggml backend genuinely is CPU-only. On Darwin,
|
|
142
|
+
# ggml's runtime device registry picks up Metal automatically — forcing
|
|
143
|
+
# 0 here wouldn't stop the actor from using the GPU, it would just make
|
|
144
|
+
# Ray believe it isn't, letting it co-schedule another GPU actor onto it.
|
|
145
|
+
if config.num_gpus > 0:
|
|
146
|
+
logger.warning(
|
|
147
|
+
"num_gpus=%s is ignored for model '%s': stable_diffusion_cpp loader only supports GPU on Metal.",
|
|
148
|
+
config.num_gpus,
|
|
149
|
+
config.name,
|
|
150
|
+
)
|
|
151
|
+
opts: dict = {"ray_actor_options": {"num_gpus": 0, "num_cpus": config.num_cpus, "runtime_env": runtime_env}}
|
|
152
|
+
else:
|
|
153
|
+
world_size = _world_size(config)
|
|
154
|
+
if world_size == 1:
|
|
155
|
+
# Single slot: scalar Ray allocation. Fractional num_gpus (0 < n < 1)
|
|
156
|
+
# lets Ray pack other actors onto the same physical GPU.
|
|
157
|
+
opts = {
|
|
158
|
+
"ray_actor_options": {
|
|
159
|
+
"num_gpus": config.num_gpus,
|
|
160
|
+
"num_cpus": config.num_cpus,
|
|
161
|
+
"runtime_env": runtime_env,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else:
|
|
165
|
+
# Multi-slot: one PG bundle per slot, STRICT_PACK keeps them on the
|
|
166
|
+
# same node (NVLink). Outer actor sits in bundle 0 with 0 GPU; vLLM's
|
|
167
|
+
# ray executor reuses the PG via get_current_placement_group() and
|
|
168
|
+
# pins each worker actor to its bundle. Each bundle requests a whole
|
|
169
|
+
# GPU, so Ray spreads across distinct physical GPUs.
|
|
170
|
+
bundles = [{"GPU": 1, "CPU": config.num_cpus} for _ in range(world_size)]
|
|
171
|
+
opts = {
|
|
172
|
+
"ray_actor_options": {"num_gpus": 0, "num_cpus": config.num_cpus, "runtime_env": runtime_env},
|
|
173
|
+
"placement_group_bundles": bundles,
|
|
174
|
+
"placement_group_strategy": "STRICT_PACK",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Per-model Ray Serve concurrency cap; only override the default when set.
|
|
178
|
+
# The reservation helpers read only the GPU/CPU keys, so this is inert there.
|
|
179
|
+
max_ongoing = config.max_ongoing_requests
|
|
180
|
+
if max_ongoing is None and config.loader == ModelLoader.llama_server:
|
|
181
|
+
parallel = config.llama_server_config.parallel if config.llama_server_config else 1
|
|
182
|
+
max_ongoing = parallel
|
|
183
|
+
|
|
184
|
+
if max_ongoing is not None:
|
|
185
|
+
opts["max_ongoing_requests"] = max_ongoing
|
|
186
|
+
return opts
|
|
@@ -0,0 +1,109 @@
|
|
|
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.deploy.actor_options import resolve_plugin_wheel
|
|
8
|
+
from modelship.infer.infer_config import ModelLoader, ModelshipConfig
|
|
9
|
+
from modelship.infer.model_resolver import check_model_source
|
|
10
|
+
from modelship.logging import get_logger
|
|
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 and validates only the merged result."""
|
|
51
|
+
with open(resolve_config_path(arg_path)) as f:
|
|
52
|
+
data = yaml.safe_load(f) or {}
|
|
53
|
+
if not isinstance(data, dict):
|
|
54
|
+
raise ValueError("models.yaml: top-level document must be a mapping with a 'models' key.")
|
|
55
|
+
models = data.get("models", [])
|
|
56
|
+
if not isinstance(models, list):
|
|
57
|
+
raise ValueError("models.yaml: 'models' must be a list.")
|
|
58
|
+
return models
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolve_all_plugin_wheels(yml_conf: ModelshipConfig) -> dict[str, Path]:
|
|
62
|
+
"""Pre-flight: resolve every referenced plugin wheel up front so a missing
|
|
63
|
+
wheel fails the whole startup before any Ray deploy is attempted."""
|
|
64
|
+
wheels: dict[str, Path] = {}
|
|
65
|
+
for cfg in yml_conf.models:
|
|
66
|
+
if cfg.loader == ModelLoader.custom and cfg.plugin and cfg.plugin not in wheels:
|
|
67
|
+
wheels[cfg.plugin] = resolve_plugin_wheel(cfg.plugin)
|
|
68
|
+
return wheels
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve_all_model_sources(yml_conf: ModelshipConfig) -> None:
|
|
72
|
+
"""Pre-flight: check every built-in-loader model's source, without
|
|
73
|
+
downloading any weight bytes.
|
|
74
|
+
|
|
75
|
+
Populates `_pinned_source` (and, for llama_server, the mmproj pin) on each
|
|
76
|
+
config in place; actual download happens per-replica in
|
|
77
|
+
`BaseInfer.ensure_downloaded`. Raises on the first failure (auth,
|
|
78
|
+
missing repo, missing file, glob-no-match) so the operator sees it before
|
|
79
|
+
any Ray actor spins up.
|
|
80
|
+
|
|
81
|
+
Plugins (`loader=custom`) are skipped — they manage their own download.
|
|
82
|
+
|
|
83
|
+
Note: HF_HOME / VLLM_CACHE_ROOT / FLASHINFER_CACHE_DIR are set at module
|
|
84
|
+
load time in mship_deploy.py — `huggingface_hub.HF_HOME` is latched at
|
|
85
|
+
import, so setting them later doesn't take effect.
|
|
86
|
+
"""
|
|
87
|
+
for cfg in yml_conf.models:
|
|
88
|
+
if cfg.loader == ModelLoader.custom:
|
|
89
|
+
continue
|
|
90
|
+
assert cfg.model is not None # validator guarantees this for built-in loaders
|
|
91
|
+
trust_remote_code = bool(cfg.vllm_engine_kwargs and cfg.vllm_engine_kwargs.trust_remote_code)
|
|
92
|
+
logger.info("Checking model source for '%s': %s", cfg.name, cfg.model)
|
|
93
|
+
cfg._pinned_source = check_model_source(cfg.model, trust_remote_code=trust_remote_code)
|
|
94
|
+
logger.info("Checked '%s' (revision=%s)", cfg.name, cfg._pinned_source.revision or "local")
|
|
95
|
+
|
|
96
|
+
if cfg.loader == ModelLoader.llama_server and cfg.llama_server_config and cfg.llama_server_config.mmproj:
|
|
97
|
+
logger.info("Checking mmproj source for '%s': %s", cfg.name, cfg.llama_server_config.mmproj)
|
|
98
|
+
cfg.llama_server_config._pinned_mmproj = check_model_source(
|
|
99
|
+
cfg.llama_server_config.mmproj, trust_remote_code=trust_remote_code
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# GGUF is not supported on the vllm loader (vLLM 0.24 dropped in-tree
|
|
103
|
+
# GGUF). Reject early using the listed filename, before any download.
|
|
104
|
+
if cfg.loader == ModelLoader.vllm and cfg._pinned_source.resolves_to_gguf:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Model '{cfg.name}' resolves to a GGUF file, which the vllm loader does not support "
|
|
107
|
+
f"(vLLM 0.24 dropped in-tree GGUF). Use `loader: llama_server` for GGUF models, or point "
|
|
108
|
+
f"the vllm loader at a non-GGUF checkpoint (safetensors, or an AWQ/GPTQ/FP8 quant)."
|
|
109
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
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 merge(
|
|
46
|
+
effective_raw: list[dict],
|
|
47
|
+
input_raw: list[dict],
|
|
48
|
+
gateway_name: str,
|
|
49
|
+
mode: DeployMode,
|
|
50
|
+
) -> list[dict]:
|
|
51
|
+
"""Fold the user's input into the effective raw model set under *mode*.
|
|
52
|
+
|
|
53
|
+
- additive: union — append input dicts whose deployment name isn't already
|
|
54
|
+
present (identical config = idempotent skip; same name + different config =
|
|
55
|
+
a distinct deployment the gateway round-robins, preserved as today).
|
|
56
|
+
- reconcile: input replaces the effective set entirely.
|
|
57
|
+
"""
|
|
58
|
+
if mode == "reconcile":
|
|
59
|
+
return list(input_raw)
|
|
60
|
+
|
|
61
|
+
present = {_deployment_name(d, gateway_name) for d in effective_raw}
|
|
62
|
+
merged = list(effective_raw)
|
|
63
|
+
for d in input_raw:
|
|
64
|
+
name = _deployment_name(d, gateway_name)
|
|
65
|
+
if name not in present:
|
|
66
|
+
merged.append(d)
|
|
67
|
+
present.add(name)
|
|
68
|
+
return merged
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def deployment_names(raw_models: list[dict], gateway_name: str) -> set[str]:
|
|
72
|
+
"""The deployment-name set for raw model dicts — the identity set of what's
|
|
73
|
+
under this gateway's effective management. Passed to the deploy plan so a
|
|
74
|
+
reconcile only removes deployments that WERE effective-managed (never legacy /
|
|
75
|
+
un-tracked deployments or another gateway's apps). Relies on the effective
|
|
76
|
+
config being per-gateway and the gateway being folded into each fingerprint."""
|
|
77
|
+
return {_deployment_name(d, gateway_name) for d in raw_models}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def to_config(raw_models: list[dict]) -> ModelshipConfig:
|
|
81
|
+
"""Validate raw model dicts into a ModelshipConfig for the deploy path."""
|
|
82
|
+
return ModelshipConfig.model_validate({"models": raw_models})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def read_effective(store: StateStore, gateway_name: str) -> list[dict]:
|
|
86
|
+
"""Return the persisted effective raw model set for *gateway_name* (empty if
|
|
87
|
+
none yet)."""
|
|
88
|
+
data = store.get(f"{_NAMESPACE}/{gateway_name}")
|
|
89
|
+
if not isinstance(data, dict):
|
|
90
|
+
return []
|
|
91
|
+
models = data.get("models", [])
|
|
92
|
+
if not isinstance(models, list):
|
|
93
|
+
logger.warning("Effective config for gateway %r has non-list 'models'; treating as empty.", gateway_name)
|
|
94
|
+
return []
|
|
95
|
+
return models
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def write_effective(store: StateStore, gateway_name: str, raw_models: list[dict]) -> None:
|
|
99
|
+
"""Persist the effective raw model set for *gateway_name*."""
|
|
100
|
+
store.set(f"{_NAMESPACE}/{gateway_name}", {"models": raw_models})
|
|
101
|
+
logger.info("Effective config for gateway %r now has %d model(s).", gateway_name, len(raw_models))
|