@tuned-tensor/cli 0.13.1 → 0.15.0
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.
- package/README.md +153 -69
- package/dist/{chunk-CAEBS5HH.js → chunk-YGN26POJ.js} +4 -3
- package/dist/chunk-YGN26POJ.js.map +1 -0
- package/dist/index.js +4405 -201
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.js +124 -33
- package/dist/local-runtime.js.map +1 -1
- package/docs/local-runtime/architecture.md +11 -7
- package/docs/local-runtime/serving.md +171 -0
- package/docs/local-runtime/spark.md +4 -2
- package/package.json +3 -1
- package/training/adapter/src/model_contract.py +1 -1
- package/training/adapter/src/prefetch.py +1 -1
- package/training/adapter/src/serve.py +133 -267
- package/training/adapter/src/train.py +2 -2
- package/training/serving/pyproject.toml +9 -0
- package/training/serving/uv.lock +2785 -0
- package/dist/chunk-CAEBS5HH.js.map +0 -1
|
@@ -1,280 +1,146 @@
|
|
|
1
|
+
"""Artifact preparation only; vLLM owns HTTP, batching, streaming and tools."""
|
|
1
2
|
from __future__ import annotations
|
|
2
3
|
|
|
3
|
-
import hmac
|
|
4
4
|
import json
|
|
5
|
-
import
|
|
5
|
+
import hmac
|
|
6
6
|
import os
|
|
7
|
+
import signal
|
|
8
|
+
import socket
|
|
7
9
|
import sys
|
|
8
|
-
import threading
|
|
9
|
-
import time
|
|
10
|
-
import uuid
|
|
11
|
-
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
12
10
|
from pathlib import Path
|
|
13
11
|
from tempfile import TemporaryDirectory
|
|
14
|
-
from typing import Any
|
|
15
|
-
|
|
16
|
-
from evaluate import (
|
|
17
|
-
configure_hugging_face_cache,
|
|
18
|
-
import_runtime_dependencies,
|
|
19
|
-
load_text_model,
|
|
20
|
-
resolve_adapter_path,
|
|
21
|
-
sampling_kwargs,
|
|
22
|
-
)
|
|
23
|
-
from model_contract import assert_certified_base_model_revision, chat_template_kwargs
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
MODEL_ARTIFACT = os.environ.get("TT_MODEL_ARTIFACT")
|
|
27
|
-
BASE_MODEL = os.environ["TT_BASE_MODEL"]
|
|
28
|
-
MODEL_SOURCE = os.environ.get("TT_MODEL_SOURCE", BASE_MODEL)
|
|
29
|
-
BASE_MODEL_REVISION = os.environ.get("TT_BASE_MODEL_REVISION")
|
|
30
|
-
MODEL_NAME = os.environ.get("TT_MODEL_NAME", "tuned-tensor-local")
|
|
31
|
-
MODEL_LOADER = os.environ.get("TT_MODEL_LOADER", "causal_lm")
|
|
32
|
-
SYSTEM_PROMPT = os.environ.get("TT_SYSTEM_PROMPT", "").strip()
|
|
33
|
-
HOST = os.environ.get("TT_HOST", "127.0.0.1")
|
|
34
|
-
PORT = int(os.environ.get("TT_PORT", "8000"))
|
|
35
|
-
DEVICE_REQUEST = os.environ.get("TT_DEVICE", "cuda")
|
|
36
|
-
DEFAULT_MAX_TOKENS = int(os.environ.get("TT_MAX_TOKENS", "512"))
|
|
37
|
-
DEFAULT_TEMPERATURE = float(os.environ.get("TT_TEMPERATURE", "0"))
|
|
38
|
-
DEFAULT_TOP_P = float(os.environ.get("TT_TOP_P", "1"))
|
|
39
|
-
MAX_REQUEST_BYTES = 2 * 1024 * 1024
|
|
40
|
-
MAX_PROMPT_CHARS = 100_000
|
|
41
|
-
MAX_PROMPT_TOKENS = 16_384
|
|
42
|
-
MAX_MESSAGES = 128
|
|
43
|
-
API_KEY = os.environ.get("TT_API_KEY", "")
|
|
44
|
-
MAX_CONCURRENT_REQUESTS = int(os.environ.get("TT_MAX_CONCURRENT_REQUESTS", "1"))
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if MODEL_LOADER not in ("causal_lm", "image_text_to_text"):
|
|
48
|
-
raise ValueError(
|
|
49
|
-
f"The bundled model server does not support TT_MODEL_LOADER={MODEL_LOADER!r}; "
|
|
50
|
-
"expected causal_lm or image_text_to_text"
|
|
51
|
-
)
|
|
52
|
-
assert_certified_base_model_revision(BASE_MODEL, BASE_MODEL_REVISION, "Serving base model revision")
|
|
53
|
-
configure_hugging_face_cache(os.environ.get("HF_HOME"))
|
|
54
|
-
import_runtime_dependencies()
|
|
55
|
-
TEMP_DIR = TemporaryDirectory(prefix="tt-local-serve-")
|
|
56
|
-
ADAPTER_PATH = (
|
|
57
|
-
resolve_adapter_path(MODEL_ARTIFACT, Path(TEMP_DIR.name))
|
|
58
|
-
if MODEL_ARTIFACT
|
|
59
|
-
else None
|
|
60
|
-
)
|
|
61
|
-
MODEL_PAYLOAD = {
|
|
62
|
-
"base_model": BASE_MODEL,
|
|
63
|
-
"model_source": MODEL_SOURCE,
|
|
64
|
-
"base_model_revision": BASE_MODEL_REVISION,
|
|
65
|
-
"device": DEVICE_REQUEST,
|
|
66
|
-
"model_loader": MODEL_LOADER,
|
|
67
|
-
}
|
|
68
|
-
MODEL, TOKENIZER, DEVICE = load_text_model(MODEL_PAYLOAD, ADAPTER_PATH, MODEL_LOADER)
|
|
69
|
-
GENERATION_LOCK = threading.Lock()
|
|
70
|
-
REQUEST_SLOTS = threading.BoundedSemaphore(MAX_CONCURRENT_REQUESTS)
|
|
71
|
-
|
|
72
12
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
13
|
+
from evaluate import configure_hugging_face_cache, resolve_adapter_path
|
|
14
|
+
from model_contract import assert_certified_base_model_revision, assert_certified_model_config
|
|
15
|
+
|
|
16
|
+
class BearerAuthMiddleware:
|
|
17
|
+
"""Keep TT's auth boundary on ALL routes, including upstream admin APIs.
|
|
18
|
+
|
|
19
|
+
vLLM's default bearer guard only covers selected API prefixes. This is
|
|
20
|
+
transport admission only; requests/responses and generation stay upstream.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, app):
|
|
23
|
+
self.app = app
|
|
24
|
+
self.expected = os.environ["TT_API_KEY"].encode()
|
|
25
|
+
|
|
26
|
+
async def __call__(self, scope, receive, send):
|
|
27
|
+
if scope["type"] in {"http", "websocket"}:
|
|
28
|
+
authorization = dict(scope.get("headers", [])).get(b"authorization", b"")
|
|
29
|
+
scheme, _, token = authorization.partition(b" ")
|
|
30
|
+
if scheme.lower() != b"bearer" or not hmac.compare_digest(token, self.expected):
|
|
31
|
+
if scope["type"] == "websocket":
|
|
32
|
+
await send({"type": "websocket.close", "code": 1008})
|
|
33
|
+
else:
|
|
34
|
+
await send({"type": "http.response.start", "status": 401,
|
|
35
|
+
"headers": [(b"content-type", b"application/json")]})
|
|
36
|
+
await send({"type": "http.response.body", "body": b'{"error":"Unauthorized"}'})
|
|
37
|
+
return
|
|
38
|
+
await self.app(scope, receive, send)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_vllm_args(model_source: str, adapter_path: str | None, temp_dir: Path) -> list[str]:
|
|
42
|
+
args = [
|
|
43
|
+
"serve", model_source,
|
|
44
|
+
"--served-model-name", os.environ["TT_MODEL_NAME"],
|
|
45
|
+
"--host", os.environ.get("TT_HOST", "127.0.0.1"),
|
|
46
|
+
"--port", os.environ.get("TT_PORT", "8000"),
|
|
47
|
+
"--max-model-len", os.environ.get("TT_CONTEXT_LENGTH", "16384"),
|
|
48
|
+
"--max-num-seqs", os.environ.get("TT_MAX_CONCURRENT_REQUESTS", "1"),
|
|
49
|
+
"--gpu-memory-utilization", os.environ.get("TT_GPU_MEMORY_UTILIZATION", "0.8"),
|
|
50
|
+
"--generation-config", "vllm",
|
|
51
|
+
"--override-generation-config", json.dumps({
|
|
52
|
+
"max_new_tokens": int(os.environ.get("TT_MAX_TOKENS", "512")),
|
|
53
|
+
"temperature": float(os.environ.get("TT_TEMPERATURE", "0")),
|
|
54
|
+
"top_p": float(os.environ.get("TT_TOP_P", "1")),
|
|
55
|
+
}),
|
|
56
|
+
"--language-model-only",
|
|
57
|
+
"--no-enable-log-requests", "--no-enable-log-outputs",
|
|
58
|
+
]
|
|
59
|
+
if os.environ.get("TT_API_KEY"):
|
|
60
|
+
args += ["--middleware", "serve.BearerAuthMiddleware"]
|
|
61
|
+
base = os.environ["TT_BASE_MODEL"]
|
|
62
|
+
# Upstream model-specific parsers, not a TT parser or model-size heuristic.
|
|
63
|
+
parser = {
|
|
64
|
+
"Qwen/Qwen3.5-2B": "qwen3_xml",
|
|
65
|
+
"nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16": "qwen3_coder",
|
|
66
|
+
"meta-models/Muse-Glimmer-30B": "muse_glimmer",
|
|
67
|
+
}[base]
|
|
68
|
+
args += ["--enable-auto-tool-choice", "--tool-call-parser", parser]
|
|
69
|
+
if base != "meta-models/Muse-Glimmer-30B":
|
|
70
|
+
args += ["--default-chat-template-kwargs", '{"enable_thinking":false}']
|
|
71
|
+
if adapter_path:
|
|
72
|
+
config = json.loads((Path(adapter_path) / "adapter_config.json").read_text())
|
|
73
|
+
adapter_base = config.get("base_model_name_or_path")
|
|
74
|
+
if adapter_base not in {os.environ["TT_BASE_MODEL"], model_source}:
|
|
75
|
+
raise ValueError("The adapter base does not match the verified serving model.")
|
|
76
|
+
base_name = "base:" + os.environ["TT_BASE_MODEL"]
|
|
77
|
+
args[args.index("--served-model-name") + 1] = base_name
|
|
78
|
+
args += ["--enable-lora", "--max-lora-rank", str(max(8, int(config["r"]))),
|
|
79
|
+
"--lora-modules", json.dumps({"name": os.environ["TT_MODEL_NAME"],
|
|
80
|
+
"path": adapter_path, "base_model_name": base_name})]
|
|
81
|
+
prompt = os.environ.get("TT_SYSTEM_PROMPT", "").strip()
|
|
82
|
+
if prompt:
|
|
83
|
+
source = Path(model_source)
|
|
84
|
+
template_file = source / "chat_template.jinja"
|
|
85
|
+
if template_file.is_file():
|
|
86
|
+
template = template_file.read_text(encoding="utf-8")
|
|
107
87
|
else:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
messages
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def bounded_number(value: Any, default: float, minimum: float, maximum: float) -> float:
|
|
156
|
-
number = float(default if value is None else value)
|
|
157
|
-
if not math.isfinite(number) or number < minimum or number > maximum:
|
|
158
|
-
raise ValueError(f"Generation value must be between {minimum} and {maximum}.")
|
|
159
|
-
return number
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def bounded_integer(value: Any, default: int, minimum: int, maximum: int) -> int:
|
|
163
|
-
number = bounded_number(value, default, minimum, maximum)
|
|
164
|
-
if not number.is_integer():
|
|
165
|
-
raise ValueError(f"Generation value must be an integer between {minimum} and {maximum}.")
|
|
166
|
-
return int(number)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class Handler(BaseHTTPRequestHandler):
|
|
170
|
-
server_version = "TunedTensorLocalServer/0.1"
|
|
171
|
-
|
|
172
|
-
def setup(self) -> None:
|
|
173
|
-
super().setup()
|
|
174
|
-
self.connection.settimeout(30)
|
|
175
|
-
|
|
176
|
-
def authorized(self) -> bool:
|
|
177
|
-
if not API_KEY:
|
|
178
|
-
return True
|
|
179
|
-
supplied = self.headers.get("authorization", "")
|
|
180
|
-
return hmac.compare_digest(supplied, "Bearer " + API_KEY)
|
|
181
|
-
|
|
182
|
-
def send_json(self, status: int, payload: Any) -> None:
|
|
183
|
-
body = json.dumps(payload).encode("utf-8")
|
|
184
|
-
self.send_response(status)
|
|
185
|
-
self.send_header("Content-Type", "application/json")
|
|
186
|
-
self.send_header("Content-Length", str(len(body)))
|
|
187
|
-
self.end_headers()
|
|
188
|
-
self.wfile.write(body)
|
|
189
|
-
|
|
190
|
-
def do_GET(self) -> None: # noqa: N802
|
|
191
|
-
if not self.authorized():
|
|
192
|
-
self.send_json(401, {"error": {"message": "Unauthorized"}})
|
|
193
|
-
return
|
|
194
|
-
if self.path in {"/", "/health"}:
|
|
195
|
-
self.send_json(200, {"status": "ok", "model": MODEL_NAME, "device": DEVICE})
|
|
196
|
-
return
|
|
197
|
-
if self.path == "/v1/models":
|
|
198
|
-
self.send_json(200, {
|
|
199
|
-
"object": "list",
|
|
200
|
-
"data": [{"id": MODEL_NAME, "object": "model", "owned_by": "tuned-tensor-local"}],
|
|
201
|
-
})
|
|
202
|
-
return
|
|
203
|
-
self.send_json(404, {"error": {"message": "Not found"}})
|
|
204
|
-
|
|
205
|
-
def do_POST(self) -> None: # noqa: N802
|
|
206
|
-
if not self.authorized():
|
|
207
|
-
self.send_json(401, {"error": {"message": "Unauthorized"}})
|
|
208
|
-
return
|
|
209
|
-
if self.path not in {"/v1/chat/completions", "/chat/completions"}:
|
|
210
|
-
self.send_json(404, {"error": {"message": "Not found"}})
|
|
211
|
-
return
|
|
88
|
+
template = json.loads((source / "tokenizer_config.json").read_text())["chat_template"]
|
|
89
|
+
if not isinstance(template, str):
|
|
90
|
+
raise ValueError("Serving requires an unambiguous string chat template.")
|
|
91
|
+
# Literal data, never prompt text interpolated as Jinja source. Preserve
|
|
92
|
+
# tool calls/results verbatim while merging leading system context.
|
|
93
|
+
prefix = "{%- set tt = namespace(system=" + json.dumps(prompt) + ", history=[]) -%}"
|
|
94
|
+
prefix += "{%- for message in messages -%}{%- if message.role == 'system' -%}"
|
|
95
|
+
prefix += "{%- set tt.system = tt.system + '\\n\\n' + message.content -%}"
|
|
96
|
+
prefix += "{%- else -%}{%- set tt.history = tt.history + [message] -%}{%- endif -%}{%- endfor -%}"
|
|
97
|
+
prefix += "{%- set messages = [{'role': 'system', 'content': tt.system}] + tt.history -%}"
|
|
98
|
+
path = temp_dir / "chat-template.jinja"
|
|
99
|
+
path.write_text(prefix + template, encoding="utf-8")
|
|
100
|
+
args += ["--chat-template", str(path)]
|
|
101
|
+
return args
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def prepare_model_source() -> str:
|
|
105
|
+
base = os.environ["TT_BASE_MODEL"]
|
|
106
|
+
revision = os.environ.get("TT_BASE_MODEL_REVISION")
|
|
107
|
+
assert_certified_base_model_revision(base, revision, "Serving base model revision")
|
|
108
|
+
configure_hugging_face_cache(os.environ.get("HF_HOME"))
|
|
109
|
+
source = os.environ.get("TT_MODEL_SOURCE", base)
|
|
110
|
+
if not Path(source).is_dir():
|
|
111
|
+
if source != base:
|
|
112
|
+
raise ValueError("Explicit serving snapshot is not an existing directory.")
|
|
113
|
+
from huggingface_hub import snapshot_download
|
|
114
|
+
source = snapshot_download(repo_id=base, revision=revision, local_files_only=True)
|
|
115
|
+
config = json.loads((Path(source) / "config.json").read_text(encoding="utf-8"))
|
|
116
|
+
assert_certified_model_config(config, "Serving model config", base)
|
|
117
|
+
return str(Path(source).resolve())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def run_server() -> None:
|
|
121
|
+
host = os.environ.get("TT_HOST", "127.0.0.1")
|
|
122
|
+
port = int(os.environ.get("TT_PORT", "8000"))
|
|
123
|
+
# Early diagnostic; upstream still owns the real bind and handles races.
|
|
124
|
+
family = socket.AF_INET6 if ":" in host else socket.AF_INET
|
|
125
|
+
with socket.socket(family, socket.SOCK_STREAM) as probe:
|
|
126
|
+
probe.bind((host, port))
|
|
127
|
+
model_source = prepare_model_source()
|
|
128
|
+
if os.environ.get("TT_API_KEY"):
|
|
129
|
+
os.environ["VLLM_API_KEY"] = os.environ["TT_API_KEY"]
|
|
130
|
+
with TemporaryDirectory(prefix="tt-serve-") as temp:
|
|
131
|
+
adapter = resolve_adapter_path(os.environ.get("TT_MODEL_ARTIFACT"), Path(temp))
|
|
132
|
+
args = build_vllm_args(model_source, adapter, Path(temp))
|
|
133
|
+
from vllm.entrypoints.cli.main import main
|
|
134
|
+
previous = sys.argv
|
|
212
135
|
try:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if not isinstance(body, dict):
|
|
218
|
-
raise ValueError("Request body must be a JSON object.")
|
|
219
|
-
if body.get("stream"):
|
|
220
|
-
self.send_json(400, {"error": {"message": "Streaming is not supported yet."}})
|
|
221
|
-
return
|
|
222
|
-
messages = normalize_messages(body.get("messages"))
|
|
223
|
-
generation = {
|
|
224
|
-
"max_new_tokens": bounded_integer(body.get("max_tokens"), DEFAULT_MAX_TOKENS, 1, 8192),
|
|
225
|
-
"temperature": bounded_number(body.get("temperature"), DEFAULT_TEMPERATURE, 0, 5),
|
|
226
|
-
"top_p": bounded_number(body.get("top_p"), DEFAULT_TOP_P, 0, 1),
|
|
227
|
-
}
|
|
228
|
-
if not REQUEST_SLOTS.acquire(blocking=False):
|
|
229
|
-
self.send_json(429, {"error": {"message": "The local model is busy; retry shortly."}})
|
|
230
|
-
return
|
|
231
|
-
started = time.perf_counter()
|
|
232
|
-
try:
|
|
233
|
-
content, prompt_tokens, completion_tokens = generate_text(messages, generation)
|
|
234
|
-
finally:
|
|
235
|
-
REQUEST_SLOTS.release()
|
|
236
|
-
latency_ms = round((time.perf_counter() - started) * 1000)
|
|
237
|
-
self.send_json(200, {
|
|
238
|
-
"id": "chatcmpl-" + uuid.uuid4().hex,
|
|
239
|
-
"object": "chat.completion",
|
|
240
|
-
"created": int(time.time()),
|
|
241
|
-
"model": MODEL_NAME,
|
|
242
|
-
"choices": [{
|
|
243
|
-
"index": 0,
|
|
244
|
-
"message": {"role": "assistant", "content": content},
|
|
245
|
-
"finish_reason": "stop",
|
|
246
|
-
}],
|
|
247
|
-
"usage": {
|
|
248
|
-
"prompt_tokens": prompt_tokens,
|
|
249
|
-
"completion_tokens": completion_tokens,
|
|
250
|
-
"total_tokens": prompt_tokens + completion_tokens,
|
|
251
|
-
},
|
|
252
|
-
"tt_local": {"latency_ms": latency_ms, "device": DEVICE},
|
|
253
|
-
})
|
|
254
|
-
except (ValueError, TypeError, json.JSONDecodeError) as exc:
|
|
255
|
-
self.send_json(400, {"error": {"message": str(exc)}})
|
|
256
|
-
except Exception as exc:
|
|
257
|
-
print(f"Model server error: {type(exc).__name__}: {exc}", file=sys.stderr, flush=True)
|
|
258
|
-
self.send_json(500, {"error": {"message": "Internal model server error."}})
|
|
259
|
-
|
|
260
|
-
def log_message(self, fmt: str, *args: Any) -> None:
|
|
261
|
-
print("%s - %s" % (self.address_string(), fmt % args), flush=True)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def main() -> None:
|
|
265
|
-
print(f"Serving {MODEL_NAME} on http://{HOST}:{PORT}", flush=True)
|
|
266
|
-
print(f"OpenAI-compatible endpoint: http://{HOST}:{PORT}/v1/chat/completions", flush=True)
|
|
267
|
-
print(f"Device: {DEVICE}", flush=True)
|
|
268
|
-
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
|
269
|
-
server.daemon_threads = True
|
|
270
|
-
server.request_queue_size = max(2, MAX_CONCURRENT_REQUESTS * 2)
|
|
271
|
-
try:
|
|
272
|
-
server.serve_forever()
|
|
273
|
-
except KeyboardInterrupt:
|
|
274
|
-
print("Model server stopped.", flush=True)
|
|
275
|
-
finally:
|
|
276
|
-
server.server_close()
|
|
136
|
+
sys.argv = ["vllm", *args]
|
|
137
|
+
main()
|
|
138
|
+
finally:
|
|
139
|
+
sys.argv = previous
|
|
277
140
|
|
|
278
141
|
|
|
279
142
|
if __name__ == "__main__":
|
|
280
|
-
|
|
143
|
+
# Ensure temporary artifacts unwind if stopped before upstream installs its
|
|
144
|
+
# own graceful shutdown handlers. Node forwards signals to the whole group.
|
|
145
|
+
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
|
146
|
+
run_server()
|
|
@@ -92,8 +92,8 @@ def assert_certified_request() -> None:
|
|
|
92
92
|
def require_cuda() -> dict[str, Any]:
|
|
93
93
|
if not torch.cuda.is_available():
|
|
94
94
|
raise RuntimeError(
|
|
95
|
-
"TT
|
|
96
|
-
"Run this workflow on
|
|
95
|
+
"TT local training requires an NVIDIA CUDA GPU. "
|
|
96
|
+
"Run this workflow on a supported CUDA host and verify it first with tt doctor."
|
|
97
97
|
)
|
|
98
98
|
device_index = torch.cuda.current_device()
|
|
99
99
|
properties = torch.cuda.get_device_properties(device_index)
|