gpu4agents 0.5.0__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.
- gpu4agents/__init__.py +5 -0
- gpu4agents/__main__.py +4 -0
- gpu4agents/agent/__init__.py +1 -0
- gpu4agents/agent/install.py +173 -0
- gpu4agents/agent/jobs.py +193 -0
- gpu4agents/agent/mcp.py +646 -0
- gpu4agents/agent/skill/SKILL.md +68 -0
- gpu4agents/cli/__init__.py +0 -0
- gpu4agents/cli/_common.py +131 -0
- gpu4agents/cli/account.py +330 -0
- gpu4agents/cli/agent.py +74 -0
- gpu4agents/cli/inspect.py +207 -0
- gpu4agents/cli/lifecycle.py +127 -0
- gpu4agents/cli/main.py +163 -0
- gpu4agents/cli/providers.py +96 -0
- gpu4agents/cli/run.py +38 -0
- gpu4agents/cli/setup.py +164 -0
- gpu4agents/cli/status.py +224 -0
- gpu4agents/cli/timer.py +143 -0
- gpu4agents/cli/validate.py +57 -0
- gpu4agents/core/__init__.py +0 -0
- gpu4agents/core/beacon.py +133 -0
- gpu4agents/core/config.py +647 -0
- gpu4agents/core/credentials.py +95 -0
- gpu4agents/core/durations.py +47 -0
- gpu4agents/core/engine.py +1335 -0
- gpu4agents/core/errors.py +94 -0
- gpu4agents/core/events.py +117 -0
- gpu4agents/core/execution.py +169 -0
- gpu4agents/core/health.py +62 -0
- gpu4agents/core/hosts.py +58 -0
- gpu4agents/core/model_inspect.py +368 -0
- gpu4agents/core/models.py +313 -0
- gpu4agents/core/paths.py +42 -0
- gpu4agents/core/policy.py +264 -0
- gpu4agents/core/state.py +406 -0
- gpu4agents/core/sweep.py +478 -0
- gpu4agents/core/tool_parsers.py +63 -0
- gpu4agents/providers/__init__.py +4 -0
- gpu4agents/providers/base.py +137 -0
- gpu4agents/providers/registry.py +63 -0
- gpu4agents/providers/vast/__init__.py +3 -0
- gpu4agents/providers/vast/client.py +255 -0
- gpu4agents/providers/vast/config.py +56 -0
- gpu4agents/providers/vast/mapper.py +292 -0
- gpu4agents/providers/vast/provider.py +438 -0
- gpu4agents/runtime/__init__.py +6 -0
- gpu4agents/runtime/base.py +184 -0
- gpu4agents/runtime/vllm.py +295 -0
- gpu4agents/watchdog/__init__.py +3 -0
- gpu4agents/watchdog/agent_body.py +959 -0
- gpu4agents/watchdog/builder.py +248 -0
- gpu4agents-0.5.0.dist-info/METADATA +157 -0
- gpu4agents-0.5.0.dist-info/RECORD +59 -0
- gpu4agents-0.5.0.dist-info/WHEEL +4 -0
- gpu4agents-0.5.0.dist-info/entry_points.txt +2 -0
- gpu4agents-0.5.0.dist-info/licenses/LICENSE +21 -0
- jambu_gpu/__init__.py +9 -0
- jambu_gpu/__main__.py +4 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""vLLM OpenAI-compatible server runtime.
|
|
2
|
+
|
|
3
|
+
This is where "does it support agentic flows?" gets answered: vLLM's server
|
|
4
|
+
already implements both ``/v1/chat/completions`` (with tool/function calling)
|
|
5
|
+
and ``/v1/responses`` (the OpenAI Responses API, MCP-tool-routing included).
|
|
6
|
+
gpu does not implement any of that - it only turns on the right vLLM
|
|
7
|
+
flags and forwards the same endpoint/credentials it always has, so any
|
|
8
|
+
OpenAI-SDK-compatible agent framework (LangChain, LangGraph, CrewAI, the
|
|
9
|
+
`openai` SDK's own `responses.create`, ...) can talk to the instance with no
|
|
10
|
+
extra layer in between.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import shlex
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
from ..core.models import ValidationResult
|
|
21
|
+
from ..core.tool_parsers import guess_tool_call_parser # noqa: F401 (re-exported)
|
|
22
|
+
from .base import ModelRuntime, cuda_from_tag, image_repo, image_tag
|
|
23
|
+
|
|
24
|
+
# vLLM flags gpu already manages explicitly - collided with via
|
|
25
|
+
# runtime.vllm_args, these would fight the config fields meant to control
|
|
26
|
+
# them (compute.gpu, model.id, lifecycle port wiring, ...). Not a hard block
|
|
27
|
+
# (power users can still reach them through raw extra_args if they really
|
|
28
|
+
# need to), just something validate() warns about.
|
|
29
|
+
_MANAGED_VLLM_FLAGS = frozenset(
|
|
30
|
+
{
|
|
31
|
+
"model",
|
|
32
|
+
"host",
|
|
33
|
+
"port",
|
|
34
|
+
"served-model-name",
|
|
35
|
+
"served_model_name",
|
|
36
|
+
"revision",
|
|
37
|
+
"trust-remote-code",
|
|
38
|
+
"trust_remote_code",
|
|
39
|
+
"enable-auto-tool-choice",
|
|
40
|
+
"enable_auto_tool_choice",
|
|
41
|
+
"tool-call-parser",
|
|
42
|
+
"tool_call_parser",
|
|
43
|
+
"tool-server",
|
|
44
|
+
"tool_server",
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def flatten_vllm_args(mapping: dict[str, Any]) -> list[str]:
|
|
50
|
+
"""Turn a ``{flag: value}`` mapping into vLLM CLI argv.
|
|
51
|
+
|
|
52
|
+
Rules, matched to how vLLM's own argparse-based CLI takes flags:
|
|
53
|
+
- key: underscores become dashes (``max_num_seqs`` -> ``--max-num-seqs``),
|
|
54
|
+
already-dashed keys pass through unchanged. A leading ``--`` in the
|
|
55
|
+
key is tolerated (stripped) so either spelling works.
|
|
56
|
+
- ``True`` -> a bare flag (``--enable-prefix-caching``)
|
|
57
|
+
- ``False`` -> the negated flag (``--no-enable-prefix-caching``), matching
|
|
58
|
+
vLLM's own store_true/store_false flag-pair convention
|
|
59
|
+
- ``None`` -> the flag is skipped entirely (lets a value come from
|
|
60
|
+
elsewhere, e.g. an env var, without an empty CLI arg)
|
|
61
|
+
- dict/list -> JSON-encoded (several vLLM flags, e.g.
|
|
62
|
+
``--limit-mm-per-prompt``, take a JSON object as their value)
|
|
63
|
+
- anything else -> ``str(value)``
|
|
64
|
+
"""
|
|
65
|
+
args: list[str] = []
|
|
66
|
+
for raw_key, value in mapping.items():
|
|
67
|
+
key = raw_key.lstrip("-").replace("_", "-")
|
|
68
|
+
flag = f"--{key}"
|
|
69
|
+
if value is None:
|
|
70
|
+
continue
|
|
71
|
+
if value is True:
|
|
72
|
+
args.append(flag)
|
|
73
|
+
elif value is False:
|
|
74
|
+
args.append(f"--no-{key}")
|
|
75
|
+
elif isinstance(value, (dict, list)):
|
|
76
|
+
args += [flag, json.dumps(value)]
|
|
77
|
+
else:
|
|
78
|
+
args += [flag, str(value)]
|
|
79
|
+
return args
|
|
80
|
+
|
|
81
|
+
LOG_PATH = "/var/log/jambu/vllm.log"
|
|
82
|
+
PID_PATH = "/var/run/jambu/vllm.pid"
|
|
83
|
+
|
|
84
|
+
# Pinned, never `latest`: it moved to CUDA 13 under us and landed on a host
|
|
85
|
+
# whose driver tops out at 12.2. The main build, not a `-cuNNN` variant: the
|
|
86
|
+
# v0.32.0-cu129 variant ships a torchcodec that cannot load (seen live).
|
|
87
|
+
DEFAULT_IMAGE = "vllm/vllm-openai:v0.32.0"
|
|
88
|
+
VLLM_REPO = "vllm/vllm-openai"
|
|
89
|
+
|
|
90
|
+
# CUDA behind the bare `vX.Y.Z` / `latest` tags (the image's NVIDIA_REQUIRE_CUDA),
|
|
91
|
+
# from the first release that requires it. Older bare tags are left unknown.
|
|
92
|
+
_BARE_TAG_CUDA = [((0, 32), 13.0)]
|
|
93
|
+
_LATEST_CUDA = 13.0
|
|
94
|
+
_VERSION_TAG = re.compile(r"^v?(\d+)\.(\d+)(?:\.\d+)?$")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def vllm_image_cuda(image: str) -> Optional[float]:
|
|
98
|
+
"""CUDA a vLLM image needs: the ``-cuNNN`` suffix, else the known bare tags."""
|
|
99
|
+
tag = image_tag(image)
|
|
100
|
+
suffixed = cuda_from_tag(tag)
|
|
101
|
+
if suffixed is not None:
|
|
102
|
+
return suffixed
|
|
103
|
+
if not image_repo(image).endswith(VLLM_REPO):
|
|
104
|
+
return None
|
|
105
|
+
if tag == "latest":
|
|
106
|
+
return _LATEST_CUDA
|
|
107
|
+
match = _VERSION_TAG.match(tag)
|
|
108
|
+
if not match:
|
|
109
|
+
return None
|
|
110
|
+
version = (int(match.group(1)), int(match.group(2)))
|
|
111
|
+
found = None
|
|
112
|
+
for since, cuda in _BARE_TAG_CUDA:
|
|
113
|
+
if version >= since:
|
|
114
|
+
found = cuda
|
|
115
|
+
return found
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class VllmRuntime(ModelRuntime):
|
|
119
|
+
engine = "vllm"
|
|
120
|
+
default_image = DEFAULT_IMAGE
|
|
121
|
+
api_key_env = "VLLM_API_KEY"
|
|
122
|
+
|
|
123
|
+
def required_cuda(self) -> Optional[float]:
|
|
124
|
+
return vllm_image_cuda(self.image())
|
|
125
|
+
|
|
126
|
+
def required_compute_cap(self) -> Optional[float]:
|
|
127
|
+
# vLLM wheels built for CUDA 13+ drop Volta (sm_70) and need Ampere
|
|
128
|
+
# (8.0) or newer; those crash at engine-core init on a V100 or older.
|
|
129
|
+
# CUDA 12.x wheels still run Volta. Pascal (GTX 10xx, 6.1) is below
|
|
130
|
+
# vLLM's floor on any build.
|
|
131
|
+
cuda = self.required_cuda()
|
|
132
|
+
if cuda is None:
|
|
133
|
+
return None
|
|
134
|
+
return 8.0 if cuda >= 13.0 else 7.0
|
|
135
|
+
|
|
136
|
+
def runtime_pid_path(self) -> str:
|
|
137
|
+
return PID_PATH
|
|
138
|
+
|
|
139
|
+
def runtime_log_path(self) -> str:
|
|
140
|
+
return LOG_PATH
|
|
141
|
+
|
|
142
|
+
def env(self) -> dict[str, str]:
|
|
143
|
+
env: dict[str, str] = {
|
|
144
|
+
"HF_HOME": "/workspace/hf",
|
|
145
|
+
"HUGGING_FACE_HUB_CACHE": "/workspace/hf/hub",
|
|
146
|
+
"VLLM_LOGGING_LEVEL": "INFO",
|
|
147
|
+
}
|
|
148
|
+
if self.config.model.trust_remote_code:
|
|
149
|
+
env["VLLM_TRUST_REMOTE_CODE"] = "1"
|
|
150
|
+
return env
|
|
151
|
+
|
|
152
|
+
def resolve_tool_call_parser(self) -> "str | None":
|
|
153
|
+
cfg = self.config.runtime
|
|
154
|
+
if cfg.tool_calling == "off":
|
|
155
|
+
return None
|
|
156
|
+
if cfg.tool_call_parser:
|
|
157
|
+
return cfg.tool_call_parser
|
|
158
|
+
return guess_tool_call_parser(self.config.model.id)
|
|
159
|
+
|
|
160
|
+
def server_args(self) -> list[str]:
|
|
161
|
+
cfg = self.config
|
|
162
|
+
args: list[str] = [
|
|
163
|
+
"--model",
|
|
164
|
+
cfg.model.id,
|
|
165
|
+
"--host",
|
|
166
|
+
"0.0.0.0",
|
|
167
|
+
"--port",
|
|
168
|
+
str(cfg.runtime.port),
|
|
169
|
+
"--dtype",
|
|
170
|
+
cfg.runtime.dtype,
|
|
171
|
+
"--gpu-memory-utilization",
|
|
172
|
+
str(cfg.runtime.gpu_memory_utilization),
|
|
173
|
+
]
|
|
174
|
+
if cfg.model.revision:
|
|
175
|
+
args += ["--revision", cfg.model.revision]
|
|
176
|
+
if cfg.model.served_name:
|
|
177
|
+
args += ["--served-model-name", cfg.model.served_name]
|
|
178
|
+
if cfg.runtime.context_length:
|
|
179
|
+
args += ["--max-model-len", str(cfg.runtime.context_length)]
|
|
180
|
+
tp = cfg.runtime.tensor_parallel_size or cfg.compute.gpu.count
|
|
181
|
+
if tp and tp > 1:
|
|
182
|
+
args += ["--tensor-parallel-size", str(tp)]
|
|
183
|
+
if cfg.model.trust_remote_code:
|
|
184
|
+
args.append("--trust-remote-code")
|
|
185
|
+
if cfg.runtime.api_key:
|
|
186
|
+
args += ["--api-key", cfg.runtime.api_key]
|
|
187
|
+
|
|
188
|
+
# Agentic flows: /v1/chat/completions and /v1/responses both need the
|
|
189
|
+
# model to actually be able to emit tool calls, which needs a parser
|
|
190
|
+
# matched to the model family.
|
|
191
|
+
parser = self.resolve_tool_call_parser()
|
|
192
|
+
if parser:
|
|
193
|
+
args += ["--enable-auto-tool-choice", "--tool-call-parser", parser]
|
|
194
|
+
if cfg.runtime.tool_server:
|
|
195
|
+
# Lets /v1/responses execute MCP tools server-side instead of the
|
|
196
|
+
# calling agent framework doing it client-side.
|
|
197
|
+
args += ["--tool-server", cfg.runtime.tool_server]
|
|
198
|
+
|
|
199
|
+
# Escape hatch for vLLM flags this schema doesn't model explicitly -
|
|
200
|
+
# structured form first, then the raw form, each able to override
|
|
201
|
+
# what came before it (vLLM's argparse takes the last occurrence of
|
|
202
|
+
# a repeated flag).
|
|
203
|
+
args += flatten_vllm_args(cfg.runtime.vllm_args)
|
|
204
|
+
args += list(cfg.runtime.extra_args)
|
|
205
|
+
return args
|
|
206
|
+
|
|
207
|
+
def start_command(self) -> str:
|
|
208
|
+
args = " ".join(shlex.quote(a) for a in self.server_args())
|
|
209
|
+
# The interpreter that can import vllm: images that put a Python shim
|
|
210
|
+
# first on PATH (vastai/vllm) keep vllm under /usr/bin/python3.
|
|
211
|
+
find_python = (
|
|
212
|
+
"PY=python3; for p in /usr/bin/python3 python3 python; do "
|
|
213
|
+
"$p -c 'import vllm' 2>/dev/null && PY=$p && break; done"
|
|
214
|
+
)
|
|
215
|
+
return (
|
|
216
|
+
f"mkdir -p $(dirname {LOG_PATH}) && {find_python}; "
|
|
217
|
+
f"nohup $PY -m vllm.entrypoints.openai.api_server {args} "
|
|
218
|
+
f">> {LOG_PATH} 2>&1 & echo $! > {PID_PATH}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def activity_probe(self) -> dict:
|
|
222
|
+
# The watchdog runs in the same container, so loopback reaches vLLM
|
|
223
|
+
# without the public port. /metrics is served even with --api-key.
|
|
224
|
+
return {
|
|
225
|
+
"kind": "prometheus",
|
|
226
|
+
"url": f"http://127.0.0.1:{self.config.runtime.port}/metrics",
|
|
227
|
+
"gauges": ["vllm:num_requests_running", "vllm:num_requests_waiting"],
|
|
228
|
+
"counters": ["vllm:request_success_total", "vllm:prompt_tokens_total"],
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
def validate(self) -> ValidationResult:
|
|
232
|
+
result = ValidationResult()
|
|
233
|
+
cfg = self.config
|
|
234
|
+
if not (0.1 <= cfg.runtime.gpu_memory_utilization <= 1.0):
|
|
235
|
+
result.error(
|
|
236
|
+
"runtime.gpu_memory_utilization must be between 0.1 and 1.0",
|
|
237
|
+
"runtime.gpu_memory_utilization",
|
|
238
|
+
)
|
|
239
|
+
tp = cfg.runtime.tensor_parallel_size
|
|
240
|
+
if tp and tp > cfg.compute.gpu.count:
|
|
241
|
+
result.error(
|
|
242
|
+
f"runtime.tensor_parallel_size ({tp}) exceeds compute.gpu.count "
|
|
243
|
+
f"({cfg.compute.gpu.count})",
|
|
244
|
+
"runtime.tensor_parallel_size",
|
|
245
|
+
)
|
|
246
|
+
if cfg.runtime.context_length and cfg.runtime.context_length > 131072:
|
|
247
|
+
result.warn(
|
|
248
|
+
"runtime.context_length above 128k needs a lot of KV cache VRAM",
|
|
249
|
+
"runtime.context_length",
|
|
250
|
+
)
|
|
251
|
+
if "/" not in cfg.model.id:
|
|
252
|
+
result.warn(
|
|
253
|
+
f"model.id '{cfg.model.id}' does not look like a HuggingFace repo id",
|
|
254
|
+
"model.id",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
collisions = {
|
|
258
|
+
key.lstrip("-").replace("_", "-") for key in cfg.runtime.vllm_args
|
|
259
|
+
} & _MANAGED_VLLM_FLAGS
|
|
260
|
+
if collisions:
|
|
261
|
+
result.warn(
|
|
262
|
+
f"runtime.vllm_args sets {sorted(collisions)}, which gpu "
|
|
263
|
+
"already manages via other config fields (compute.gpu, model.id, "
|
|
264
|
+
"runtime.tool_calling, ...) - it will still be passed through and "
|
|
265
|
+
"win (last flag wins), but the field meant to control it won't "
|
|
266
|
+
"reflect what's actually running",
|
|
267
|
+
"runtime.vllm_args",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if cfg.runtime.tool_calling == "on" and not self.resolve_tool_call_parser():
|
|
271
|
+
result.error(
|
|
272
|
+
"runtime.tool_calling is 'on' but no --tool-call-parser could be "
|
|
273
|
+
f"resolved for '{cfg.model.id}'. Set runtime.tool_call_parser "
|
|
274
|
+
"explicitly (see docs/CONFIG.md for the list vLLM supports).",
|
|
275
|
+
"runtime.tool_call_parser",
|
|
276
|
+
)
|
|
277
|
+
elif cfg.runtime.tool_calling == "auto" and not self.resolve_tool_call_parser():
|
|
278
|
+
result.warn(
|
|
279
|
+
f"could not guess a --tool-call-parser for '{cfg.model.id}'; "
|
|
280
|
+
"tool/function calling will be OFF on both /v1/chat/completions "
|
|
281
|
+
"and /v1/responses. Set runtime.tool_call_parser to turn it on.",
|
|
282
|
+
"runtime.tool_call_parser",
|
|
283
|
+
)
|
|
284
|
+
if cfg.runtime.tool_server and not self.resolve_tool_call_parser():
|
|
285
|
+
result.warn(
|
|
286
|
+
"runtime.tool_server is set but no tool-call parser is active; "
|
|
287
|
+
"the model will not be able to invoke it",
|
|
288
|
+
"runtime.tool_server",
|
|
289
|
+
)
|
|
290
|
+
return result
|
|
291
|
+
|
|
292
|
+
def describe(self) -> str:
|
|
293
|
+
parser = self.resolve_tool_call_parser()
|
|
294
|
+
tools = f", tools={parser}" if parser else ""
|
|
295
|
+
return f"vLLM :{self.config.runtime.port} ({self.config.model.id}{tools})"
|