flyteplugins-llamacpp 2.7.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.
- flyteplugins/llamacpp/__init__.py +4 -0
- flyteplugins/llamacpp/_app_environment.py +269 -0
- flyteplugins/llamacpp/_constants.py +24 -0
- flyteplugins/llamacpp/_image.py +153 -0
- flyteplugins/llamacpp/_server.py +73 -0
- flyteplugins_llamacpp-2.7.0.dist-info/METADATA +128 -0
- flyteplugins_llamacpp-2.7.0.dist-info/RECORD +10 -0
- flyteplugins_llamacpp-2.7.0.dist-info/WHEEL +5 -0
- flyteplugins_llamacpp-2.7.0.dist-info/entry_points.txt +2 -0
- flyteplugins_llamacpp-2.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
from dataclasses import dataclass, field, replace
|
|
5
|
+
from typing import Any, Literal, Optional, Union
|
|
6
|
+
|
|
7
|
+
import flyte.app
|
|
8
|
+
import rich.repr
|
|
9
|
+
from flyte import Environment, Image, Resources, SecretRequest
|
|
10
|
+
from flyte.app import ArtifactValue, Parameter, RunOutput
|
|
11
|
+
from flyte.app._types import Port
|
|
12
|
+
from flyte.models import SerializationContext
|
|
13
|
+
|
|
14
|
+
from flyteplugins.llamacpp._image import DEFAULT_LLAMA_CPP_IMAGE
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _shell_safe(args: list[str]) -> list[str]:
|
|
18
|
+
"""Quote args that have to survive a trip through a shell.
|
|
19
|
+
|
|
20
|
+
`fserve` runs the app with `Popen(" ".join(args), shell=True)` (flyte/_bin/serve.py), so
|
|
21
|
+
any token carrying spaces or quotes -- a `--chat-template` blob, say -- reaches the server
|
|
22
|
+
mangled unless it is quoted here. `shlex.quote` is the identity function for ordinary
|
|
23
|
+
tokens, so this is a no-op for everything else.
|
|
24
|
+
|
|
25
|
+
Tokens starting with `$` are left alone: `fserve` expands those against the container
|
|
26
|
+
environment *before* joining, and quoting would turn the marker into a literal.
|
|
27
|
+
"""
|
|
28
|
+
return [arg if arg.startswith("$") else shlex.quote(arg) for arg in args]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@rich.repr.auto
|
|
32
|
+
@dataclass(kw_only=True, repr=True)
|
|
33
|
+
class LlamaCppAppEnvironment(flyte.app.AppEnvironment):
|
|
34
|
+
"""
|
|
35
|
+
App environment backed by llama.cpp (llama-server) for serving GGUF models.
|
|
36
|
+
|
|
37
|
+
This environment serves an OpenAI-compatible endpoint (under `/v1`) plus the llama.cpp
|
|
38
|
+
Web UI, with the specified GGUF model and configuration. llama.cpp shines where vLLM and
|
|
39
|
+
SGLang don't fit: quantized GGUF weights, partial CPU offload of models larger than VRAM,
|
|
40
|
+
and CPU-only serving.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
name: The name of the application.
|
|
44
|
+
port: Port the application listens on. Defaults to 8080.
|
|
45
|
+
requests: Compute resource requests for application.
|
|
46
|
+
secrets: Secrets that are requested for application.
|
|
47
|
+
limits: Compute resource limits for application.
|
|
48
|
+
env_vars: Environment variables to set for the application.
|
|
49
|
+
scaling: Scaling configuration for the app environment.
|
|
50
|
+
domain: Domain to use for the app.
|
|
51
|
+
cluster_pool: The target cluster_pool where the app should be deployed.
|
|
52
|
+
requires_auth: Whether the public URL requires authentication.
|
|
53
|
+
type: Type of app.
|
|
54
|
+
extra_args: Extra args to pass to `llama-server`, e.g. `"--ctx-size 32768 --jinja"`.
|
|
55
|
+
Run `llama-server --help` or see
|
|
56
|
+
https://github.com/ggml-org/llama.cpp/tree/master/tools/server for details.
|
|
57
|
+
model_path: Remote path to the GGUF weights -- a directory containing `.gguf` file(s)
|
|
58
|
+
or a direct path to one (e.g. s3://bucket/path/to/model), or a
|
|
59
|
+
`RunOutput`/`ArtifactValue` resolved at deploy time. The weights are downloaded
|
|
60
|
+
into the container and the served `.gguf` is located at startup (for sharded
|
|
61
|
+
models, the `-00001-of-` shard is picked; llama-server finds the rest).
|
|
62
|
+
model_hf_path: Hugging Face GGUF repo, optionally with a quant tag (e.g.
|
|
63
|
+
`ggml-org/gemma-3-4b-it-GGUF:Q4_K_M`). Passed to llama-server as `--hf-repo`,
|
|
64
|
+
which downloads the weights at startup.
|
|
65
|
+
model_id: Model id exposed by the server (llama-server's `--alias`).
|
|
66
|
+
draft_model_path: Remote path to the draft model GGUF used for speculative decoding,
|
|
67
|
+
or a `RunOutput`/`ArtifactValue` resolved at deploy time. Downloaded alongside the
|
|
68
|
+
target model and passed as `--model-draft`. Tune the speculation via `extra_args`
|
|
69
|
+
(`--draft-max`, `--draft-min`, `--gpu-layers-draft`, ...).
|
|
70
|
+
draft_model_hf_path: Hugging Face GGUF repo for the draft model, as an alternative to
|
|
71
|
+
`draft_model_path`. Passed as `--hf-repo-draft`.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
port: int | Port = 8080
|
|
75
|
+
type: str = "llama.cpp"
|
|
76
|
+
extra_args: str | list[str] = ""
|
|
77
|
+
model_path: str | RunOutput | ArtifactValue = ""
|
|
78
|
+
model_hf_path: str = ""
|
|
79
|
+
model_id: str = ""
|
|
80
|
+
draft_model_path: str | RunOutput | ArtifactValue = ""
|
|
81
|
+
draft_model_hf_path: str = ""
|
|
82
|
+
image: str | Image | Literal["auto"] = DEFAULT_LLAMA_CPP_IMAGE
|
|
83
|
+
# Under /tmp, and that is not cosmetic: ``fserve`` materializes each mounted Parameter
|
|
84
|
+
# through ``_ensure_dest_writable``, which needs the *image's* user to be able to create the
|
|
85
|
+
# parent directory. The released Flyte base image runs non-root, so a mount at the
|
|
86
|
+
# filesystem root -- or under /root -- fails with "Permission denied" before the engine ever
|
|
87
|
+
# starts. /tmp is writable for any user and lives on the same overlay filesystem the weights
|
|
88
|
+
# are already budgeted against by ``disk=``.
|
|
89
|
+
_model_mount_path: str = field(default="/tmp/flyte/model", init=False)
|
|
90
|
+
_draft_model_mount_path: str = field(default="/tmp/flyte/draft-model", init=False)
|
|
91
|
+
|
|
92
|
+
def __post_init__(self):
|
|
93
|
+
if self.env_vars is None:
|
|
94
|
+
self.env_vars = {}
|
|
95
|
+
|
|
96
|
+
if self._server is not None:
|
|
97
|
+
raise ValueError("server function cannot be set for LlamaCppAppEnvironment")
|
|
98
|
+
|
|
99
|
+
if self._on_startup is not None:
|
|
100
|
+
raise ValueError("on_startup function cannot be set for LlamaCppAppEnvironment")
|
|
101
|
+
|
|
102
|
+
if self._on_shutdown is not None:
|
|
103
|
+
raise ValueError("on_shutdown function cannot be set for LlamaCppAppEnvironment")
|
|
104
|
+
|
|
105
|
+
if self.model_id == "":
|
|
106
|
+
raise ValueError("model_id must be defined")
|
|
107
|
+
|
|
108
|
+
if self.model_path == "" and self.model_hf_path == "":
|
|
109
|
+
raise ValueError("model_path or model_hf_path must be defined")
|
|
110
|
+
if self.model_path != "" and self.model_hf_path != "":
|
|
111
|
+
raise ValueError("model_path and model_hf_path cannot be set at the same time")
|
|
112
|
+
|
|
113
|
+
if self.draft_model_path != "" and self.draft_model_hf_path != "":
|
|
114
|
+
raise ValueError("draft_model_path and draft_model_hf_path cannot be set at the same time")
|
|
115
|
+
|
|
116
|
+
if self.args:
|
|
117
|
+
raise ValueError("args cannot be set for LlamaCppAppEnvironment. Use `extra_args` to add extra arguments.")
|
|
118
|
+
|
|
119
|
+
if isinstance(self.extra_args, str):
|
|
120
|
+
extra_args = shlex.split(self.extra_args)
|
|
121
|
+
else:
|
|
122
|
+
extra_args = list(self.extra_args)
|
|
123
|
+
|
|
124
|
+
# The GGUF filename inside a mounted directory is unknown at deploy time, so mounted
|
|
125
|
+
# weights go through the `llama-cpp-fserve` shim, which resolves `--model-dir` /
|
|
126
|
+
# `--draft-model-dir` to concrete .gguf paths and execs llama-server.
|
|
127
|
+
if self.model_path:
|
|
128
|
+
model_args = ["--model-dir", self._model_mount_path]
|
|
129
|
+
else:
|
|
130
|
+
model_args = ["--hf-repo", self.model_hf_path]
|
|
131
|
+
|
|
132
|
+
draft_args: list[str] = []
|
|
133
|
+
if self.draft_model_path:
|
|
134
|
+
draft_args = ["--draft-model-dir", self._draft_model_mount_path]
|
|
135
|
+
elif self.draft_model_hf_path:
|
|
136
|
+
draft_args = ["--hf-repo-draft", self.draft_model_hf_path]
|
|
137
|
+
|
|
138
|
+
# llama-server binds 127.0.0.1 by default, which is unreachable from outside the
|
|
139
|
+
# container.
|
|
140
|
+
host_args = [] if "--host" in extra_args else ["--host", "0.0.0.0"]
|
|
141
|
+
|
|
142
|
+
self.args = _shell_safe(
|
|
143
|
+
[
|
|
144
|
+
"llama-cpp-fserve",
|
|
145
|
+
*model_args,
|
|
146
|
+
"--alias",
|
|
147
|
+
self.model_id,
|
|
148
|
+
*host_args,
|
|
149
|
+
"--port",
|
|
150
|
+
str(self.get_port().port),
|
|
151
|
+
*draft_args,
|
|
152
|
+
*extra_args,
|
|
153
|
+
]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if self.parameters:
|
|
157
|
+
raise ValueError("parameters cannot be set for LlamaCppAppEnvironment")
|
|
158
|
+
|
|
159
|
+
parameters: list[Parameter] = []
|
|
160
|
+
if self.model_path:
|
|
161
|
+
parameters.append(
|
|
162
|
+
Parameter(
|
|
163
|
+
name="model_path",
|
|
164
|
+
value=self.model_path,
|
|
165
|
+
download=True,
|
|
166
|
+
mount=self._model_mount_path,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
if self.draft_model_path:
|
|
170
|
+
parameters.append(
|
|
171
|
+
Parameter(
|
|
172
|
+
name="draft_model_path",
|
|
173
|
+
value=self.draft_model_path,
|
|
174
|
+
download=True,
|
|
175
|
+
mount=self._draft_model_mount_path,
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
if parameters:
|
|
179
|
+
self.parameters = parameters
|
|
180
|
+
|
|
181
|
+
self.links = [flyte.app.Link(path="/", title="llama.cpp Web UI", is_relative=True), *self.links]
|
|
182
|
+
|
|
183
|
+
if self.image is None or self.image == "auto":
|
|
184
|
+
self.image = DEFAULT_LLAMA_CPP_IMAGE
|
|
185
|
+
|
|
186
|
+
super().__post_init__()
|
|
187
|
+
|
|
188
|
+
def container_args(self, serialization_context: SerializationContext) -> list[str]:
|
|
189
|
+
"""Return the container arguments for llama.cpp."""
|
|
190
|
+
if isinstance(self.args, str):
|
|
191
|
+
return shlex.split(self.args)
|
|
192
|
+
return self.args or []
|
|
193
|
+
|
|
194
|
+
def clone_with(
|
|
195
|
+
self,
|
|
196
|
+
name: str,
|
|
197
|
+
image: Optional[Union[str, Image, Literal["auto"]]] = None,
|
|
198
|
+
resources: Optional[Resources] = None,
|
|
199
|
+
env_vars: Optional[dict[str, str]] = None,
|
|
200
|
+
secrets: Optional[SecretRequest] = None,
|
|
201
|
+
depends_on: Optional[list[Environment]] = None,
|
|
202
|
+
description: Optional[str] = None,
|
|
203
|
+
interruptible: Optional[bool] = None,
|
|
204
|
+
**kwargs: Any,
|
|
205
|
+
) -> LlamaCppAppEnvironment:
|
|
206
|
+
port = kwargs.pop("port", None)
|
|
207
|
+
extra_args = kwargs.pop("extra_args", None)
|
|
208
|
+
if "model_path" in kwargs:
|
|
209
|
+
set_model_path = True
|
|
210
|
+
model_path = kwargs.pop("model_path", "") or ""
|
|
211
|
+
else:
|
|
212
|
+
set_model_path = False
|
|
213
|
+
model_path = self.model_path
|
|
214
|
+
if "model_hf_path" in kwargs:
|
|
215
|
+
set_model_hf_path = True
|
|
216
|
+
model_hf_path = kwargs.pop("model_hf_path", "") or ""
|
|
217
|
+
else:
|
|
218
|
+
set_model_hf_path = False
|
|
219
|
+
model_hf_path = self.model_hf_path
|
|
220
|
+
if "draft_model_path" in kwargs:
|
|
221
|
+
set_draft_model_path = True
|
|
222
|
+
draft_model_path = kwargs.pop("draft_model_path", "") or ""
|
|
223
|
+
else:
|
|
224
|
+
set_draft_model_path = False
|
|
225
|
+
draft_model_path = self.draft_model_path
|
|
226
|
+
if "draft_model_hf_path" in kwargs:
|
|
227
|
+
set_draft_model_hf_path = True
|
|
228
|
+
draft_model_hf_path = kwargs.pop("draft_model_hf_path", "") or ""
|
|
229
|
+
else:
|
|
230
|
+
set_draft_model_hf_path = False
|
|
231
|
+
draft_model_hf_path = self.draft_model_hf_path
|
|
232
|
+
model_id = kwargs.pop("model_id", None)
|
|
233
|
+
|
|
234
|
+
if kwargs:
|
|
235
|
+
raise TypeError(f"Unexpected keyword arguments: {list(kwargs.keys())}")
|
|
236
|
+
|
|
237
|
+
kwargs = self._get_kwargs()
|
|
238
|
+
kwargs["name"] = name
|
|
239
|
+
kwargs["args"] = None
|
|
240
|
+
kwargs["parameters"] = None
|
|
241
|
+
if image is not None:
|
|
242
|
+
kwargs["image"] = image
|
|
243
|
+
if resources is not None:
|
|
244
|
+
kwargs["resources"] = resources
|
|
245
|
+
if env_vars is not None:
|
|
246
|
+
kwargs["env_vars"] = env_vars
|
|
247
|
+
if secrets is not None:
|
|
248
|
+
kwargs["secrets"] = secrets
|
|
249
|
+
if depends_on is not None:
|
|
250
|
+
kwargs["depends_on"] = depends_on
|
|
251
|
+
if description is not None:
|
|
252
|
+
kwargs["description"] = description
|
|
253
|
+
if interruptible is not None:
|
|
254
|
+
kwargs["interruptible"] = interruptible
|
|
255
|
+
if port is not None:
|
|
256
|
+
kwargs["port"] = port
|
|
257
|
+
if extra_args is not None:
|
|
258
|
+
kwargs["extra_args"] = extra_args
|
|
259
|
+
if set_model_path:
|
|
260
|
+
kwargs["model_path"] = model_path
|
|
261
|
+
if set_model_hf_path:
|
|
262
|
+
kwargs["model_hf_path"] = model_hf_path
|
|
263
|
+
if set_draft_model_path:
|
|
264
|
+
kwargs["draft_model_path"] = draft_model_path
|
|
265
|
+
if set_draft_model_hf_path:
|
|
266
|
+
kwargs["draft_model_hf_path"] = draft_model_hf_path
|
|
267
|
+
if model_id is not None:
|
|
268
|
+
kwargs["model_id"] = model_id
|
|
269
|
+
return replace(self, **kwargs)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Constants shared by the llama.cpp image build and server shim."""
|
|
2
|
+
|
|
3
|
+
LLAMA_CPP_REPO = "https://github.com/ggml-org/llama.cpp"
|
|
4
|
+
LLAMA_CPP_INSTALL_DIR = "/opt/llama.cpp"
|
|
5
|
+
LLAMA_SERVER_BINARY = f"{LLAMA_CPP_INSTALL_DIR}/build/bin/llama-server"
|
|
6
|
+
|
|
7
|
+
CUDA_HOME = "/usr/local/cuda-12.8"
|
|
8
|
+
CUDA_TOOLKIT_PACKAGE = "cuda-toolkit-12-8"
|
|
9
|
+
# CUDA stubs let the linker resolve libcuda.so on GPU-less build machines; the real
|
|
10
|
+
# driver library is injected by the container runtime on the serving node.
|
|
11
|
+
CUDA_STUB_LIB = f"{CUDA_HOME}/lib64/stubs"
|
|
12
|
+
|
|
13
|
+
# Default target arch: 89 = Ada Lovelace (L4/L40S). Image builds run on CPU-only
|
|
14
|
+
# nodes, so CMAKE_CUDA_ARCHITECTURES=native is out. Pass a ";"-separated list to
|
|
15
|
+
# ``build_llama_cpp_image(cuda_arch=...)`` to produce a fat binary, e.g. "80;86;89;90"
|
|
16
|
+
# to also cover A100 (80), A10 (86), and H100 (90).
|
|
17
|
+
DEFAULT_CUDA_ARCH = "89"
|
|
18
|
+
|
|
19
|
+
# Node is required to build llama-server's embedded Web UI from source. The UI is a
|
|
20
|
+
# Vite 7 / SvelteKit app (needs Node >=22.12); without Node, cmake's llama-ui-assets
|
|
21
|
+
# target skips the npm build and falls back to a prebuilt UI download that is
|
|
22
|
+
# version-mismatched against embed.cpp and fails the build.
|
|
23
|
+
NODE_VERSION = "22.12.0"
|
|
24
|
+
NODE_HOME = "/opt/node"
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Container image with llama.cpp built from source (CUDA-enabled by default)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
|
|
7
|
+
import flyte
|
|
8
|
+
|
|
9
|
+
from flyteplugins.llamacpp._constants import (
|
|
10
|
+
CUDA_HOME,
|
|
11
|
+
CUDA_STUB_LIB,
|
|
12
|
+
CUDA_TOOLKIT_PACKAGE,
|
|
13
|
+
DEFAULT_CUDA_ARCH,
|
|
14
|
+
LLAMA_CPP_INSTALL_DIR,
|
|
15
|
+
LLAMA_CPP_REPO,
|
|
16
|
+
NODE_HOME,
|
|
17
|
+
NODE_VERSION,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_NODE_TARBALL = f"node-v{NODE_VERSION}-linux-x64"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _node_install_commands() -> list[str]:
|
|
24
|
+
return [
|
|
25
|
+
f"wget -q https://nodejs.org/dist/v{NODE_VERSION}/{_NODE_TARBALL}.tar.xz -O /tmp/node.tar.xz",
|
|
26
|
+
f"mkdir -p {NODE_HOME} && tar -xJf /tmp/node.tar.xz -C {NODE_HOME} --strip-components=1 && rm /tmp/node.tar.xz",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _run_script(script: str) -> str:
|
|
31
|
+
"""Serialize a shell script for an image-builder RUN step without quoting hazards.
|
|
32
|
+
|
|
33
|
+
The remote image builder mangles embedded quoting: double quotes inside a command
|
|
34
|
+
are dropped, so a fat-binary `-DCMAKE_CUDA_ARCHITECTURES="80;86;89"` reaches `sh`
|
|
35
|
+
as `80;86;89` and the `;` splits it into separate commands ("sh: 1: 86: not").
|
|
36
|
+
Base64-encoding the whole script and decoding it into `sh` at build time keeps
|
|
37
|
+
every character intact regardless of how the builder serializes RUN commands.
|
|
38
|
+
"""
|
|
39
|
+
encoded = base64.b64encode(script.encode()).decode()
|
|
40
|
+
return f"echo {encoded} | base64 -d | sh"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _clone_commands(repo: str, ref: str | None) -> list[str]:
|
|
44
|
+
commands = [f"git clone {repo} {LLAMA_CPP_INSTALL_DIR}"]
|
|
45
|
+
if ref is not None:
|
|
46
|
+
commands.append(f"cd {LLAMA_CPP_INSTALL_DIR} && git checkout {ref}")
|
|
47
|
+
return commands
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_llama_cpp_image(
|
|
51
|
+
*,
|
|
52
|
+
name: str = "llama-cpp-app-image",
|
|
53
|
+
cuda: bool = True,
|
|
54
|
+
cuda_arch: str = DEFAULT_CUDA_ARCH,
|
|
55
|
+
repo: str = LLAMA_CPP_REPO,
|
|
56
|
+
ref: str | None = None,
|
|
57
|
+
) -> flyte.Image:
|
|
58
|
+
"""Build a Debian image with llama-server compiled from source.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
name: Name of the image.
|
|
62
|
+
cuda: Build with CUDA support (GGML_CUDA=ON). Set to False for a CPU-only image.
|
|
63
|
+
cuda_arch: Target CUDA architecture(s) for the kernel build, as a ";"-separated
|
|
64
|
+
list of compute capabilities (e.g. "89" for L4/L40S, "80;86;89;90" for a fat
|
|
65
|
+
binary that also covers A100/A10/H100). Ignored when `cuda=False`.
|
|
66
|
+
repo: Git repository to build llama.cpp from.
|
|
67
|
+
ref: Git ref (tag, branch, or commit) to check out. None builds the default
|
|
68
|
+
branch tip; pin a release tag (e.g. "b6148") for reproducible builds.
|
|
69
|
+
"""
|
|
70
|
+
image = flyte.Image.from_debian_base(name=name).with_apt_packages(
|
|
71
|
+
"git",
|
|
72
|
+
"build-essential",
|
|
73
|
+
"cmake",
|
|
74
|
+
"pkg-config",
|
|
75
|
+
"wget",
|
|
76
|
+
"curl",
|
|
77
|
+
"ca-certificates",
|
|
78
|
+
"libnuma-dev",
|
|
79
|
+
"libssl-dev",
|
|
80
|
+
"pciutils",
|
|
81
|
+
"libcurl4-openssl-dev",
|
|
82
|
+
"xz-utils",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if cuda:
|
|
86
|
+
image = image.with_commands(
|
|
87
|
+
[
|
|
88
|
+
(
|
|
89
|
+
"wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/"
|
|
90
|
+
"cuda-keyring_1.1-1_all.deb"
|
|
91
|
+
),
|
|
92
|
+
"dpkg -i cuda-keyring_1.1-1_all.deb",
|
|
93
|
+
"apt-get update",
|
|
94
|
+
f"apt-get install -y {CUDA_TOOLKIT_PACKAGE}",
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
cmake_configure = (
|
|
98
|
+
f"LIBRARY_PATH={CUDA_STUB_LIB}:$LIBRARY_PATH "
|
|
99
|
+
f"cmake -S {LLAMA_CPP_INSTALL_DIR} -B {LLAMA_CPP_INSTALL_DIR}/build "
|
|
100
|
+
"-DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=ON "
|
|
101
|
+
"-DCMAKE_BUILD_TYPE=Release "
|
|
102
|
+
f'-DCMAKE_CUDA_ARCHITECTURES="{cuda_arch}" '
|
|
103
|
+
f'-DCMAKE_EXE_LINKER_FLAGS="-Wl,-rpath-link,{CUDA_STUB_LIB}" '
|
|
104
|
+
f'-DCMAKE_SHARED_LINKER_FLAGS="-Wl,-rpath-link,{CUDA_STUB_LIB}"'
|
|
105
|
+
)
|
|
106
|
+
cmake_build_prefix = f"LIBRARY_PATH={CUDA_STUB_LIB}:$LIBRARY_PATH "
|
|
107
|
+
stub_commands = [f"ln -sf {CUDA_STUB_LIB}/libcuda.so {CUDA_STUB_LIB}/libcuda.so.1"]
|
|
108
|
+
else:
|
|
109
|
+
cmake_configure = (
|
|
110
|
+
f"cmake -S {LLAMA_CPP_INSTALL_DIR} -B {LLAMA_CPP_INSTALL_DIR}/build "
|
|
111
|
+
"-DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release"
|
|
112
|
+
)
|
|
113
|
+
cmake_build_prefix = ""
|
|
114
|
+
stub_commands = []
|
|
115
|
+
|
|
116
|
+
image = image.with_commands(
|
|
117
|
+
_node_install_commands()
|
|
118
|
+
+ _clone_commands(repo, ref)
|
|
119
|
+
+ stub_commands
|
|
120
|
+
+ [
|
|
121
|
+
# The cmake configure carries a ";"-separated CMAKE_CUDA_ARCHITECTURES;
|
|
122
|
+
# ship it via _run_script so the ";" and quotes survive the builder.
|
|
123
|
+
_run_script(cmake_configure),
|
|
124
|
+
# Build llama-server. The llama-ui-assets target runs an npm build of
|
|
125
|
+
# tools/ui to embed the Web UI; put node/npm on PATH. The UI's .npmrc
|
|
126
|
+
# sets engine-strict=true, which makes a transitive dep's Node bound a
|
|
127
|
+
# fatal EBADENGINE; override it so install proceeds (Node here satisfies
|
|
128
|
+
# the UI's real Vite requirement).
|
|
129
|
+
_run_script(
|
|
130
|
+
f"PATH={NODE_HOME}/bin:$PATH npm_config_engine_strict=false "
|
|
131
|
+
f"{cmake_build_prefix}"
|
|
132
|
+
f"cmake --build {LLAMA_CPP_INSTALL_DIR}/build --config Release "
|
|
133
|
+
"-j $(nproc) --target llama-server"
|
|
134
|
+
),
|
|
135
|
+
]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
env_vars = {
|
|
139
|
+
"PATH": f"{LLAMA_CPP_INSTALL_DIR}/build/bin:{CUDA_HOME}/bin:$PATH"
|
|
140
|
+
if cuda
|
|
141
|
+
else (f"{LLAMA_CPP_INSTALL_DIR}/build/bin:$PATH"),
|
|
142
|
+
# Under /tmp so `-hf`-style downloads work with the non-root user the
|
|
143
|
+
# released Flyte base image runs as.
|
|
144
|
+
"LLAMA_CACHE": "/tmp/llama.cpp/cache",
|
|
145
|
+
}
|
|
146
|
+
if cuda:
|
|
147
|
+
env_vars["CUDA_HOME"] = CUDA_HOME
|
|
148
|
+
|
|
149
|
+
# The plugin itself provides the `llama-cpp-fserve` entrypoint (and pulls in flyte).
|
|
150
|
+
return image.with_env_vars(env_vars).with_pip_packages("flyteplugins-llamacpp", pre=True)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
DEFAULT_LLAMA_CPP_IMAGE = build_llama_cpp_image()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""`llama-cpp-fserve`: resolve mounted GGUF weights, then exec llama-server.
|
|
2
|
+
|
|
3
|
+
The app environment mounts model weights as a *directory* (the GGUF filename inside a
|
|
4
|
+
`RunOutput`/blob-store directory is unknown at deploy time), but llama-server takes a
|
|
5
|
+
path to a concrete `.gguf` file. This shim bridges the two: it rewrites
|
|
6
|
+
|
|
7
|
+
- `--model-dir <dir>` -> `--model <resolved .gguf>`
|
|
8
|
+
- `--draft-model-dir <dir>` -> `--model-draft <resolved .gguf>`
|
|
9
|
+
|
|
10
|
+
leaving every other argument untouched, and then replaces itself with llama-server.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import glob
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from flyteplugins.llamacpp._constants import LLAMA_SERVER_BINARY
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
_DIR_FLAG_TO_MODEL_FLAG = {
|
|
27
|
+
"--model-dir": "--model",
|
|
28
|
+
"--draft-model-dir": "--model-draft",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_gguf(path: str) -> str:
|
|
33
|
+
"""Resolve the GGUF file to serve from a mounted file or directory.
|
|
34
|
+
|
|
35
|
+
For sharded models only the first shard is passed to llama-server (it discovers
|
|
36
|
+
the rest itself), so `*-00001-of-*.gguf` wins over other matches.
|
|
37
|
+
"""
|
|
38
|
+
if os.path.isfile(path):
|
|
39
|
+
return path
|
|
40
|
+
matches = sorted(glob.glob(os.path.join(path, "**", "*.gguf"), recursive=True))
|
|
41
|
+
if not matches:
|
|
42
|
+
raise FileNotFoundError(f"No .gguf files found under {path!r}")
|
|
43
|
+
first_shards = [m for m in matches if "-00001-of-" in Path(m).name]
|
|
44
|
+
return first_shards[0] if first_shards else matches[0]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_command(argv: list[str]) -> list[str]:
|
|
48
|
+
"""Build the llama-server argv, resolving `--model-dir`/`--draft-model-dir`."""
|
|
49
|
+
server = shutil.which("llama-server") or LLAMA_SERVER_BINARY
|
|
50
|
+
cmd = [server]
|
|
51
|
+
i = 0
|
|
52
|
+
while i < len(argv):
|
|
53
|
+
arg = argv[i]
|
|
54
|
+
model_flag = _DIR_FLAG_TO_MODEL_FLAG.get(arg)
|
|
55
|
+
if model_flag is not None:
|
|
56
|
+
if i + 1 >= len(argv):
|
|
57
|
+
raise ValueError(f"{arg} requires a value")
|
|
58
|
+
cmd.extend([model_flag, find_gguf(argv[i + 1])])
|
|
59
|
+
i += 2
|
|
60
|
+
else:
|
|
61
|
+
cmd.append(arg)
|
|
62
|
+
i += 1
|
|
63
|
+
return cmd
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main() -> None:
|
|
67
|
+
logging.basicConfig(
|
|
68
|
+
level=logging.INFO,
|
|
69
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
70
|
+
)
|
|
71
|
+
cmd = build_command(sys.argv[1:])
|
|
72
|
+
logger.info("Starting llama-server: %s", " ".join(cmd))
|
|
73
|
+
os.execv(cmd[0], cmd)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flyteplugins-llamacpp
|
|
3
|
+
Version: 2.7.0
|
|
4
|
+
Summary: llama.cpp plugin for flyte
|
|
5
|
+
Author-email: Niels Bantilan <cosmicbboy@users.noreply.github.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: flyte>=2.0.0b43
|
|
9
|
+
|
|
10
|
+
# Union llama.cpp Plugin
|
|
11
|
+
|
|
12
|
+
Serve GGUF models with [llama.cpp](https://github.com/ggml-org/llama.cpp)'s `llama-server` behind Flyte Apps.
|
|
13
|
+
|
|
14
|
+
This plugin provides the `LlamaCppAppEnvironment` class for deploying quantized (GGUF) LLMs
|
|
15
|
+
with an OpenAI-compatible API (under `/v1`) and the built-in llama.cpp Web UI. llama.cpp shines
|
|
16
|
+
where vLLM and SGLang don't fit: quantized GGUF weights, partial CPU offload of models larger
|
|
17
|
+
than VRAM, and CPU-only serving.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install --pre flyteplugins-llamacpp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import flyte
|
|
29
|
+
import flyte.app
|
|
30
|
+
from flyteplugins.llamacpp import LlamaCppAppEnvironment
|
|
31
|
+
|
|
32
|
+
llama_app = LlamaCppAppEnvironment(
|
|
33
|
+
name="my-llm-app",
|
|
34
|
+
# A directory (or direct path) of GGUF weights in object storage...
|
|
35
|
+
model_path="s3://your-bucket/models/your-model-gguf",
|
|
36
|
+
model_id="your-model-id",
|
|
37
|
+
resources=flyte.Resources(cpu="4", memory="32Gi", gpu="L40s:1", disk="100Gi"),
|
|
38
|
+
scaling=flyte.app.Scaling(replicas=(0, 1), scaledown_after=300),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
flyte.init_from_config()
|
|
43
|
+
app = flyte.serve(llama_app)
|
|
44
|
+
print(f"Deployed llama.cpp app: {app.url}")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`model_path` accepts a remote directory or file path, a `RunOutput` (e.g. from a prefetch task
|
|
48
|
+
that downloaded the GGUF), or an `ArtifactValue`. The weights are downloaded into the container
|
|
49
|
+
and the served `.gguf` is located at startup; for sharded models the `-00001-of-` shard is
|
|
50
|
+
selected and llama-server discovers the rest.
|
|
51
|
+
|
|
52
|
+
Alternatively, point directly at a Hugging Face GGUF repo (with an optional quant tag) and let
|
|
53
|
+
llama-server download it at startup:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
llama_app = LlamaCppAppEnvironment(
|
|
57
|
+
name="gemma-app",
|
|
58
|
+
model_hf_path="ggml-org/gemma-3-4b-it-GGUF:Q4_K_M",
|
|
59
|
+
model_id="gemma-3-4b-it",
|
|
60
|
+
resources=flyte.Resources(cpu="4", memory="16Gi", gpu="L4:1", disk="50Gi"),
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## The default image
|
|
65
|
+
|
|
66
|
+
llama.cpp ships no GPU pip wheel, so the default image compiles `llama-server` from source with
|
|
67
|
+
CUDA enabled (plus the embedded Web UI). The default targets compute capability 8.9 (L4/L40S);
|
|
68
|
+
use `build_llama_cpp_image` to target other GPUs, pin a llama.cpp release for reproducible
|
|
69
|
+
builds, or build a CPU-only image:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from flyteplugins.llamacpp import LlamaCppAppEnvironment, build_llama_cpp_image
|
|
73
|
+
|
|
74
|
+
llama_app = LlamaCppAppEnvironment(
|
|
75
|
+
name="my-llm-app",
|
|
76
|
+
image=build_llama_cpp_image(
|
|
77
|
+
cuda_arch="80;86;89;90", # fat binary: A100, A10, L4/L40S, H100
|
|
78
|
+
ref="b6148", # pin a llama.cpp release tag
|
|
79
|
+
),
|
|
80
|
+
...
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`build_llama_cpp_image(cuda=False)` produces a CPU-only image for serving small quantized
|
|
85
|
+
models without a GPU.
|
|
86
|
+
|
|
87
|
+
## Speculative decoding
|
|
88
|
+
|
|
89
|
+
Point `draft_model_path` (object storage, `RunOutput`, or `ArtifactValue`) or
|
|
90
|
+
`draft_model_hf_path` at a small draft GGUF and it is passed to llama-server as
|
|
91
|
+
`--model-draft` / `--hf-repo-draft`. Tune the speculation via `extra_args`:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
llama_app = LlamaCppAppEnvironment(
|
|
95
|
+
name="qwen3-spec",
|
|
96
|
+
model_path="s3://your-bucket/models/qwen3-32b-gguf",
|
|
97
|
+
model_id="qwen3-32b",
|
|
98
|
+
draft_model_hf_path="ggml-org/Qwen3-0.6B-GGUF:Q8_0",
|
|
99
|
+
extra_args="--draft-max 16 --draft-min 1 --gpu-layers-draft 99",
|
|
100
|
+
resources=flyte.Resources(cpu="8", memory="64Gi", gpu="L40s:1", disk="120Gi"),
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Extra arguments
|
|
105
|
+
|
|
106
|
+
`extra_args` is appended to `llama-server`, as either a string or a list:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
llama_app = LlamaCppAppEnvironment(
|
|
110
|
+
name="my-llm-app",
|
|
111
|
+
model_path="s3://your-bucket/models/your-model-gguf",
|
|
112
|
+
model_id="your-model-id",
|
|
113
|
+
extra_args="--ctx-size 32768 --parallel 4 --jinja",
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Useful flags: `--ctx-size` (context length), `--parallel` (concurrent request slots),
|
|
118
|
+
`--jinja` (enable the model's chat template, needed for tool calling), `--n-gpu-layers`
|
|
119
|
+
(limit GPU offload for models larger than VRAM; recent llama.cpp offloads everything by
|
|
120
|
+
default), `--cache-type-k/--cache-type-v` (quantized KV cache), `--flash-attn`.
|
|
121
|
+
|
|
122
|
+
Arguments are quoted before they reach the server, so values containing spaces or JSON survive
|
|
123
|
+
intact. Arguments of the form `$MY_VAR` are left unquoted so that Flyte still expands them from
|
|
124
|
+
the app's environment.
|
|
125
|
+
|
|
126
|
+
Run `llama-server --help` or see the
|
|
127
|
+
[llama-server docs](https://github.com/ggml-org/llama.cpp/tree/master/tools/server)
|
|
128
|
+
for all options.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
flyteplugins/llamacpp/__init__.py,sha256=zVnwPFuug96hLpbR0VsgkdBtDUTyJV8FAAdg3ke_KGE,252
|
|
2
|
+
flyteplugins/llamacpp/_app_environment.py,sha256=NdvoJ3UNipZ2HMIsd_r1KwUwO4xL0iiqPGb4mALhp1k,11723
|
|
3
|
+
flyteplugins/llamacpp/_constants.py,sha256=W_EfK5ITz8-7Sq56MAAA1l6MXH1agk4YO_LIRzN6fUY,1205
|
|
4
|
+
flyteplugins/llamacpp/_image.py,sha256=fDmug_s_QBAJ03gWS1BNyUx1Okm3CUjYg_lUW3Lxrg0,5838
|
|
5
|
+
flyteplugins/llamacpp/_server.py,sha256=c3qI3zrTr_qnoc9njMCZoZJkYYfAdd2wbezH1tBeF0c,2377
|
|
6
|
+
flyteplugins_llamacpp-2.7.0.dist-info/METADATA,sha256=VlALvkkc_GCNFwuQKzWDA0gni9tvBdx6KnpR9yCFSOs,4548
|
|
7
|
+
flyteplugins_llamacpp-2.7.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
flyteplugins_llamacpp-2.7.0.dist-info/entry_points.txt,sha256=40N9KHFJ9tN29aXctZzU1VBcCBPAuI4B25xJ6SgFFko,72
|
|
9
|
+
flyteplugins_llamacpp-2.7.0.dist-info/top_level.txt,sha256=cgd779rPu9EsvdtuYgUxNHHgElaQvPn74KhB5XSeMBE,13
|
|
10
|
+
flyteplugins_llamacpp-2.7.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flyteplugins
|