ramllm 0.1.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.
- ramllm/__init__.py +3 -0
- ramllm/__main__.py +6 -0
- ramllm/api/__init__.py +5 -0
- ramllm/api/server.py +306 -0
- ramllm/backends/__init__.py +21 -0
- ramllm/backends/base.py +98 -0
- ramllm/backends/llamacpp/__init__.py +7 -0
- ramllm/backends/llamacpp/adapter.py +75 -0
- ramllm/backends/llamacpp/flags.py +78 -0
- ramllm/backends/llamacpp/install.py +75 -0
- ramllm/backends/llamacpp/launcher.py +130 -0
- ramllm/backends/mock.py +119 -0
- ramllm/cli/__init__.py +14 -0
- ramllm/cli/app.py +714 -0
- ramllm/cli/chat/__init__.py +12 -0
- ramllm/cli/chat/client.py +199 -0
- ramllm/cli/chat/history.py +16 -0
- ramllm/cli/chat/render.py +69 -0
- ramllm/cli/chat/repl.py +449 -0
- ramllm/cli/chat/slash.py +381 -0
- ramllm/cli/chat/toolbar.py +58 -0
- ramllm/cli/chat/transcripts.py +117 -0
- ramllm/cli/commands/backend_cmd.py +132 -0
- ramllm/cli/commands/chat_cmd.py +327 -0
- ramllm/cli/commands/clean_cmd.py +168 -0
- ramllm/cli/commands/completion_cmd.py +107 -0
- ramllm/cli/commands/config_cmd.py +114 -0
- ramllm/cli/commands/dashboard_cmd.py +83 -0
- ramllm/cli/commands/doctor_cmd.py +73 -0
- ramllm/cli/commands/explain_cmd.py +43 -0
- ramllm/cli/commands/models_cmd.py +396 -0
- ramllm/cli/commands/process_cmd.py +322 -0
- ramllm/cli/commands/serve_cmd.py +310 -0
- ramllm/cli/commands/setup_cmd.py +26 -0
- ramllm/cli/commands/tour_cmd.py +87 -0
- ramllm/cli/commands/update_cmd.py +74 -0
- ramllm/cli/commands/version_cmd.py +25 -0
- ramllm/cli/dashboard/__init__.py +5 -0
- ramllm/cli/dashboard/app.py +181 -0
- ramllm/cli/dashboard/widgets.py +147 -0
- ramllm/cli/home/menu.py +126 -0
- ramllm/cli/main.py +11 -0
- ramllm/cli/setup/state.py +58 -0
- ramllm/cli/setup/wizard.py +276 -0
- ramllm/cli/typo.py +25 -0
- ramllm/cli/ui/console.py +53 -0
- ramllm/cli/ui/errors.py +280 -0
- ramllm/cli/ui/glossary.py +80 -0
- ramllm/cli/ui/theme.py +44 -0
- ramllm/core/__init__.py +1 -0
- ramllm/core/analyzer/__init__.py +12 -0
- ramllm/core/analyzer/gguf.py +147 -0
- ramllm/core/analyzer/kv.py +56 -0
- ramllm/core/analyzer/moe.py +51 -0
- ramllm/core/governor/__init__.py +18 -0
- ramllm/core/governor/enforcement/__init__.py +20 -0
- ramllm/core/governor/enforcement/base.py +42 -0
- ramllm/core/governor/enforcement/cgroups.py +123 -0
- ramllm/core/governor/enforcement/darwin.py +37 -0
- ramllm/core/governor/enforcement/factory.py +27 -0
- ramllm/core/governor/enforcement/windows.py +138 -0
- ramllm/core/governor/monitor.py +224 -0
- ramllm/core/governor/recovery.py +211 -0
- ramllm/core/planner/__init__.py +17 -0
- ramllm/core/planner/candidates.py +726 -0
- ramllm/core/planner/cost_model.py +106 -0
- ramllm/core/planner/explain.py +75 -0
- ramllm/core/planner/feasibility.py +104 -0
- ramllm/core/planner/planner.py +123 -0
- ramllm/core/planner/recommend.py +134 -0
- ramllm/core/planner/reduction.py +360 -0
- ramllm/core/profiler/__init__.py +5 -0
- ramllm/core/profiler/cpu.py +61 -0
- ramllm/core/profiler/disk.py +85 -0
- ramllm/core/profiler/gpu_amd.py +51 -0
- ramllm/core/profiler/gpu_apple.py +69 -0
- ramllm/core/profiler/gpu_nvidia.py +131 -0
- ramllm/core/profiler/gpu_vulkan.py +46 -0
- ramllm/core/profiler/memory.py +58 -0
- ramllm/core/profiler/pcie.py +31 -0
- ramllm/core/profiler/prober.py +119 -0
- ramllm/core/schemas.py +158 -0
- ramllm/data/models_index.json +1098 -0
- ramllm/registry/__init__.py +26 -0
- ramllm/registry/catalog.py +182 -0
- ramllm/registry/download.py +137 -0
- ramllm/services/__init__.py +5 -0
- ramllm/services/backend_manager.py +558 -0
- ramllm/services/config_service.py +125 -0
- ramllm/services/doctor_service.py +272 -0
- ramllm/services/model_resolver.py +259 -0
- ramllm/services/model_service.py +219 -0
- ramllm/services/paths.py +73 -0
- ramllm/services/plan_service.py +209 -0
- ramllm/services/process_registry.py +171 -0
- ramllm/services/upgrade_service.py +81 -0
- ramllm/telemetry/__init__.py +13 -0
- ramllm/telemetry/bench.py +121 -0
- ramllm/telemetry/calibration.py +73 -0
- ramllm/util/parsing.py +61 -0
- ramllm-0.1.0.dist-info/METADATA +276 -0
- ramllm-0.1.0.dist-info/RECORD +105 -0
- ramllm-0.1.0.dist-info/WHEEL +4 -0
- ramllm-0.1.0.dist-info/entry_points.txt +2 -0
- ramllm-0.1.0.dist-info/licenses/LICENSE +17 -0
ramllm/__init__.py
ADDED
ramllm/__main__.py
ADDED
ramllm/api/__init__.py
ADDED
ramllm/api/server.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""FastAPI server providing OpenAI-compatible endpoints and RAMLLM extensions."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from fastapi import FastAPI, HTTPException, Request, Response
|
|
10
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from ramllm.backends import LlamaCppAdapter
|
|
14
|
+
from ramllm.backends.base import BackendAdapter, BackendProcess
|
|
15
|
+
from ramllm.core.analyzer.gguf import analyze_gguf
|
|
16
|
+
from ramllm.core.governor.monitor import MemoryGovernor, TierMetrics
|
|
17
|
+
from ramllm.core.governor.recovery import RecoveryExhaustedError, launch_with_recovery
|
|
18
|
+
from ramllm.core.planner import create_execution_plans
|
|
19
|
+
from ramllm.core.profiler import probe_hardware
|
|
20
|
+
from ramllm.core.schemas import ExecutionPlan, HardwareProfile, ModelInfo, UserRequest
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ReplanRequest(BaseModel):
|
|
24
|
+
"""Payload for dynamic runtime replanning."""
|
|
25
|
+
|
|
26
|
+
vram_budget_bytes: dict[int, int] | None = None
|
|
27
|
+
ram_budget_bytes: int | None = None
|
|
28
|
+
context: int | None = None
|
|
29
|
+
gpu_indices: list[int] | None = None
|
|
30
|
+
allow_streaming: bool | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ServerState:
|
|
34
|
+
"""Shared state for the RAMLLM API server."""
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self.model_path: str = ""
|
|
38
|
+
self.model_info: ModelInfo | None = None
|
|
39
|
+
self.hw_profile: HardwareProfile | None = None
|
|
40
|
+
self.user_request: UserRequest | None = None
|
|
41
|
+
self.active_plan: ExecutionPlan | None = None
|
|
42
|
+
self.backend_process: BackendProcess | None = None
|
|
43
|
+
self.governor: MemoryGovernor | None = None
|
|
44
|
+
self.internal_port: int = 18080
|
|
45
|
+
self.adapter: BackendAdapter = LlamaCppAdapter()
|
|
46
|
+
self.lock: asyncio.Lock = asyncio.Lock()
|
|
47
|
+
self.replan_pending: bool = False
|
|
48
|
+
self.auto_retry: bool = True
|
|
49
|
+
self.max_retries: int = 3
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
state = ServerState()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _handle_server_memory_pressure(metrics: TierMetrics) -> None:
|
|
56
|
+
"""Invoked when memory governor detects external memory pressure during server runtime."""
|
|
57
|
+
state.replan_pending = True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def launch_active_session(auto_retry: bool | None = None) -> None:
|
|
61
|
+
"""Plan and launch the backend session with reactive recovery and memory governor."""
|
|
62
|
+
if not state.model_info or not state.hw_profile or not state.user_request:
|
|
63
|
+
raise RuntimeError("Server state not initialized.")
|
|
64
|
+
|
|
65
|
+
plans = create_execution_plans(state.model_info, state.hw_profile, state.user_request)
|
|
66
|
+
chosen = plans[0]
|
|
67
|
+
if chosen.verdict == "INFEASIBLE":
|
|
68
|
+
raise ValueError(f"Plan is INFEASIBLE: {'; '.join(chosen.warnings)}")
|
|
69
|
+
|
|
70
|
+
use_auto_retry = state.auto_retry if auto_retry is None else auto_retry
|
|
71
|
+
|
|
72
|
+
proc, final_plan, _ = launch_with_recovery(
|
|
73
|
+
adapter=state.adapter,
|
|
74
|
+
plan=chosen,
|
|
75
|
+
model=state.model_info,
|
|
76
|
+
hw=state.hw_profile,
|
|
77
|
+
req=state.user_request,
|
|
78
|
+
port=state.internal_port,
|
|
79
|
+
timeout_s=40.0,
|
|
80
|
+
max_retries=state.max_retries,
|
|
81
|
+
auto_retry=use_auto_retry,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
state.active_plan = final_plan
|
|
85
|
+
state.backend_process = proc
|
|
86
|
+
state.governor = MemoryGovernor(
|
|
87
|
+
proc,
|
|
88
|
+
state.hw_profile,
|
|
89
|
+
on_pressure=_handle_server_memory_pressure,
|
|
90
|
+
)
|
|
91
|
+
state.governor.start()
|
|
92
|
+
state.replan_pending = False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def shutdown_active_session() -> None:
|
|
96
|
+
"""Stop memory governor and terminate backend engine."""
|
|
97
|
+
if state.governor:
|
|
98
|
+
state.governor.stop()
|
|
99
|
+
state.governor = None
|
|
100
|
+
if state.backend_process:
|
|
101
|
+
state.adapter.stop(state.backend_process)
|
|
102
|
+
state.backend_process = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@asynccontextmanager
|
|
106
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
107
|
+
"""FastAPI application lifespan managing backend lifecycle."""
|
|
108
|
+
if state.model_path:
|
|
109
|
+
launch_active_session()
|
|
110
|
+
yield
|
|
111
|
+
shutdown_active_session()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
api_app = FastAPI(
|
|
115
|
+
title="RAMLLM Server",
|
|
116
|
+
description="OpenAI-compatible LLM inference server with strict RAM/VRAM budget governor.",
|
|
117
|
+
version="0.1.0",
|
|
118
|
+
lifespan=lifespan,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@api_app.get("/healthz")
|
|
123
|
+
async def healthz() -> dict[str, str]:
|
|
124
|
+
"""Liveness check endpoint."""
|
|
125
|
+
return {"status": "ok"}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@api_app.get("/ramllm/plan")
|
|
129
|
+
async def get_plan() -> ExecutionPlan:
|
|
130
|
+
"""Return the currently executing ExecutionPlan."""
|
|
131
|
+
if not state.active_plan:
|
|
132
|
+
raise HTTPException(status_code=503, detail="No active plan scheduled.")
|
|
133
|
+
return state.active_plan
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@api_app.get("/ramllm/metrics")
|
|
137
|
+
async def get_metrics() -> TierMetrics:
|
|
138
|
+
"""Return real-time per-tier memory metrics and governor warnings."""
|
|
139
|
+
if not state.governor:
|
|
140
|
+
raise HTTPException(status_code=503, detail="Governor not running.")
|
|
141
|
+
return state.governor.get_latest_metrics()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@api_app.post("/ramllm/replan")
|
|
145
|
+
async def replan(req: ReplanRequest) -> ExecutionPlan:
|
|
146
|
+
"""Change memory budgets or context at runtime and replan execution."""
|
|
147
|
+
async with state.lock:
|
|
148
|
+
if not state.user_request or not state.model_info or not state.hw_profile:
|
|
149
|
+
raise HTTPException(status_code=503, detail="Server not initialized with model.")
|
|
150
|
+
|
|
151
|
+
shutdown_active_session()
|
|
152
|
+
|
|
153
|
+
# Update request
|
|
154
|
+
ctx = req.context or state.user_request.min_context
|
|
155
|
+
vram_dict = (
|
|
156
|
+
req.vram_budget_bytes
|
|
157
|
+
if req.vram_budget_bytes is not None
|
|
158
|
+
else state.user_request.vram_budget_bytes
|
|
159
|
+
)
|
|
160
|
+
ram_bytes = (
|
|
161
|
+
req.ram_budget_bytes
|
|
162
|
+
if req.ram_budget_bytes is not None
|
|
163
|
+
else state.user_request.ram_budget_bytes
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
gpu_idxs = (
|
|
167
|
+
req.gpu_indices if req.gpu_indices is not None else state.user_request.gpu_indices
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
allow_streaming = (
|
|
171
|
+
req.allow_streaming
|
|
172
|
+
if req.allow_streaming is not None
|
|
173
|
+
else state.user_request.allow_streaming
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
state.user_request = UserRequest(
|
|
177
|
+
model=state.model_path,
|
|
178
|
+
vram_budget_bytes=vram_dict,
|
|
179
|
+
ram_budget_bytes=ram_bytes,
|
|
180
|
+
min_context=ctx,
|
|
181
|
+
target_context=ctx,
|
|
182
|
+
gpu_indices=gpu_idxs,
|
|
183
|
+
allow_streaming=allow_streaming,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
launch_active_session()
|
|
188
|
+
except (ValueError, RecoveryExhaustedError) as err:
|
|
189
|
+
raise HTTPException(
|
|
190
|
+
status_code=422,
|
|
191
|
+
detail=f"Cannot replan under requested constraints: {err}",
|
|
192
|
+
) from err
|
|
193
|
+
except Exception as err:
|
|
194
|
+
raise HTTPException(
|
|
195
|
+
status_code=500,
|
|
196
|
+
detail=f"Replan failed unexpectedly: {err}",
|
|
197
|
+
) from err
|
|
198
|
+
|
|
199
|
+
if not state.active_plan:
|
|
200
|
+
raise HTTPException(status_code=500, detail="Failed to establish new plan.")
|
|
201
|
+
return state.active_plan
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@api_app.get("/v1/models")
|
|
205
|
+
async def list_models_api() -> dict[str, Any]:
|
|
206
|
+
"""OpenAI-compatible models list endpoint."""
|
|
207
|
+
model_name = state.model_info.arch if state.model_info else "ramllm-model"
|
|
208
|
+
return {
|
|
209
|
+
"object": "list",
|
|
210
|
+
"data": [
|
|
211
|
+
{
|
|
212
|
+
"id": model_name,
|
|
213
|
+
"object": "model",
|
|
214
|
+
"created": 1700000000,
|
|
215
|
+
"owned_by": "ramllm",
|
|
216
|
+
}
|
|
217
|
+
],
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@api_app.post("/v1/chat/completions")
|
|
222
|
+
async def chat_completions(request: Request) -> Response:
|
|
223
|
+
"""OpenAI-compatible chat completions proxy with streaming SSE support."""
|
|
224
|
+
body = await request.json()
|
|
225
|
+
is_stream = body.get("stream", False)
|
|
226
|
+
target_url = f"http://127.0.0.1:{state.internal_port}/v1/chat/completions"
|
|
227
|
+
|
|
228
|
+
client = httpx.AsyncClient(timeout=120.0)
|
|
229
|
+
|
|
230
|
+
if is_stream:
|
|
231
|
+
|
|
232
|
+
async def event_generator() -> AsyncGenerator[bytes, None]:
|
|
233
|
+
try:
|
|
234
|
+
async with client.stream("POST", target_url, json=body) as resp:
|
|
235
|
+
async for chunk in resp.aiter_bytes():
|
|
236
|
+
yield chunk
|
|
237
|
+
finally:
|
|
238
|
+
await client.aclose()
|
|
239
|
+
|
|
240
|
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
241
|
+
else:
|
|
242
|
+
try:
|
|
243
|
+
resp = await client.post(target_url, json=body)
|
|
244
|
+
return JSONResponse(status_code=resp.status_code, content=resp.json())
|
|
245
|
+
finally:
|
|
246
|
+
await client.aclose()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@api_app.post("/v1/completions")
|
|
250
|
+
async def completions(request: Request) -> Response:
|
|
251
|
+
"""OpenAI-compatible text completions proxy."""
|
|
252
|
+
body = await request.json()
|
|
253
|
+
is_stream = body.get("stream", False)
|
|
254
|
+
target_url = f"http://127.0.0.1:{state.internal_port}/v1/completions"
|
|
255
|
+
|
|
256
|
+
client = httpx.AsyncClient(timeout=120.0)
|
|
257
|
+
|
|
258
|
+
if is_stream:
|
|
259
|
+
|
|
260
|
+
async def event_generator() -> AsyncGenerator[bytes, None]:
|
|
261
|
+
try:
|
|
262
|
+
async with client.stream("POST", target_url, json=body) as resp:
|
|
263
|
+
async for chunk in resp.aiter_bytes():
|
|
264
|
+
yield chunk
|
|
265
|
+
finally:
|
|
266
|
+
await client.aclose()
|
|
267
|
+
|
|
268
|
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
269
|
+
else:
|
|
270
|
+
try:
|
|
271
|
+
resp = await client.post(target_url, json=body)
|
|
272
|
+
return JSONResponse(status_code=resp.status_code, content=resp.json())
|
|
273
|
+
finally:
|
|
274
|
+
await client.aclose()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def create_app(
|
|
278
|
+
model_path: str,
|
|
279
|
+
vram_budget_bytes: dict[int, int] | None = None,
|
|
280
|
+
ram_budget_bytes: int | None = None,
|
|
281
|
+
context: int = 4096,
|
|
282
|
+
gpu_indices: list[int] | None = None,
|
|
283
|
+
internal_port: int = 18080,
|
|
284
|
+
adapter: BackendAdapter | None = None,
|
|
285
|
+
hw_profile: HardwareProfile | None = None,
|
|
286
|
+
auto_retry: bool = True,
|
|
287
|
+
max_retries: int = 3,
|
|
288
|
+
) -> FastAPI:
|
|
289
|
+
"""Configure server state and return FastAPI application instance."""
|
|
290
|
+
state.model_path = model_path
|
|
291
|
+
state.internal_port = internal_port
|
|
292
|
+
state.auto_retry = auto_retry
|
|
293
|
+
state.max_retries = max_retries
|
|
294
|
+
if adapter:
|
|
295
|
+
state.adapter = adapter
|
|
296
|
+
state.model_info = analyze_gguf(model_path)
|
|
297
|
+
state.hw_profile = hw_profile or probe_hardware(quick=True)
|
|
298
|
+
state.user_request = UserRequest(
|
|
299
|
+
model=model_path,
|
|
300
|
+
vram_budget_bytes=vram_budget_bytes,
|
|
301
|
+
ram_budget_bytes=ram_budget_bytes,
|
|
302
|
+
min_context=context,
|
|
303
|
+
target_context=context,
|
|
304
|
+
gpu_indices=gpu_indices,
|
|
305
|
+
)
|
|
306
|
+
return api_app
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Backends package."""
|
|
2
|
+
|
|
3
|
+
from ramllm.backends.base import (
|
|
4
|
+
BackendAdapter,
|
|
5
|
+
BackendInstall,
|
|
6
|
+
BackendProcess,
|
|
7
|
+
Capabilities,
|
|
8
|
+
MemoryReport,
|
|
9
|
+
)
|
|
10
|
+
from ramllm.backends.llamacpp import LlamaCppAdapter
|
|
11
|
+
from ramllm.backends.mock import MockBackendAdapter
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BackendAdapter",
|
|
15
|
+
"BackendInstall",
|
|
16
|
+
"BackendProcess",
|
|
17
|
+
"Capabilities",
|
|
18
|
+
"MemoryReport",
|
|
19
|
+
"LlamaCppAdapter",
|
|
20
|
+
"MockBackendAdapter",
|
|
21
|
+
]
|
ramllm/backends/base.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Abstract backend interface, capabilities, and process models."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
from ramllm.core.schemas import ExecutionPlan
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Capabilities(BaseModel):
|
|
12
|
+
"""Features supported by a specific backend installation."""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(frozen=True)
|
|
15
|
+
|
|
16
|
+
partial_gpu_offload: bool = True
|
|
17
|
+
per_tensor_placement: bool = False
|
|
18
|
+
kv_on_gpu_or_cpu_choice: bool = True
|
|
19
|
+
kv_quantization: bool = True
|
|
20
|
+
multi_gpu_layer_split: bool = True
|
|
21
|
+
multi_gpu_tensor_split: bool = False
|
|
22
|
+
flash_attention: bool = True
|
|
23
|
+
mmap_control: bool = True
|
|
24
|
+
async_weight_streaming: bool = False
|
|
25
|
+
expert_level_eviction: bool = False
|
|
26
|
+
apis: list[str] = Field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BackendInstall(BaseModel):
|
|
30
|
+
"""Metadata describing a discovered backend binary on the system."""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(frozen=True)
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
binary_path: str
|
|
36
|
+
version: str | None = None
|
|
37
|
+
build_info: str | None = None
|
|
38
|
+
capabilities: Capabilities = Field(default_factory=Capabilities)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MemoryReport(BaseModel):
|
|
42
|
+
"""Reported peak and current memory usage from runtime process sampling and logs."""
|
|
43
|
+
|
|
44
|
+
vram_used_bytes: dict[int, int] = Field(default_factory=dict)
|
|
45
|
+
ram_rss_bytes: int = 0
|
|
46
|
+
model_buffer_bytes: int = 0
|
|
47
|
+
kv_buffer_bytes: int = 0
|
|
48
|
+
compute_buffer_bytes: int = 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BackendProcess(BaseModel):
|
|
52
|
+
"""Handle to a running backend server process."""
|
|
53
|
+
|
|
54
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
55
|
+
|
|
56
|
+
pid: int
|
|
57
|
+
port: int
|
|
58
|
+
process: subprocess.Popen[str] | None = None
|
|
59
|
+
plan: ExecutionPlan
|
|
60
|
+
initial_memory_report: MemoryReport = Field(default_factory=MemoryReport)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BackendAdapter(ABC):
|
|
64
|
+
"""Abstract interface for all RAMLLM execution engines."""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def detect(self) -> BackendInstall | None:
|
|
70
|
+
"""Discover backend binary on system PATH or configured locations."""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def capabilities(self, install: BackendInstall) -> Capabilities:
|
|
75
|
+
"""Inspect backend capabilities from its binary help output."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def build_args(self, plan: ExecutionPlan, port: int = 8080) -> list[str]:
|
|
80
|
+
"""Convert an ExecutionPlan into concrete command-line arguments."""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def launch(
|
|
85
|
+
self, plan: ExecutionPlan, port: int = 8080, timeout_s: float = 30.0
|
|
86
|
+
) -> BackendProcess:
|
|
87
|
+
"""Launch the backend process, await healthiness, and extract parsed buffer sizes."""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def memory_report(self, proc: BackendProcess) -> MemoryReport:
|
|
92
|
+
"""Collect current memory consumption across hardware tiers."""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def stop(self, proc: BackendProcess) -> None:
|
|
97
|
+
"""Gracefully terminate the backend process."""
|
|
98
|
+
...
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""llama.cpp backend package."""
|
|
2
|
+
|
|
3
|
+
from ramllm.backends.llamacpp.adapter import LlamaCppAdapter
|
|
4
|
+
from ramllm.backends.llamacpp.flags import build_llamacpp_args
|
|
5
|
+
from ramllm.backends.llamacpp.install import detect_llamacpp
|
|
6
|
+
|
|
7
|
+
__all__ = ["LlamaCppAdapter", "detect_llamacpp", "build_llamacpp_args"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Llama.cpp Backend Adapter implementation."""
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
|
|
5
|
+
from ramllm.backends.base import (
|
|
6
|
+
BackendAdapter,
|
|
7
|
+
BackendInstall,
|
|
8
|
+
BackendProcess,
|
|
9
|
+
Capabilities,
|
|
10
|
+
MemoryReport,
|
|
11
|
+
)
|
|
12
|
+
from ramllm.backends.llamacpp.flags import build_llamacpp_args
|
|
13
|
+
from ramllm.backends.llamacpp.install import detect_llamacpp
|
|
14
|
+
from ramllm.backends.llamacpp.launcher import launch_llamacpp_server
|
|
15
|
+
from ramllm.core.schemas import ExecutionPlan
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LlamaCppAdapter(BackendAdapter):
|
|
19
|
+
"""Execution engine adapter for llama.cpp via managed llama-server subprocess."""
|
|
20
|
+
|
|
21
|
+
name = "llamacpp"
|
|
22
|
+
|
|
23
|
+
def __init__(self, binary_path: str | None = None):
|
|
24
|
+
self.binary_path = binary_path
|
|
25
|
+
self._install: BackendInstall | None = None
|
|
26
|
+
|
|
27
|
+
def detect(self) -> BackendInstall | None:
|
|
28
|
+
if self.binary_path:
|
|
29
|
+
self._install = detect_llamacpp(self.binary_path)
|
|
30
|
+
else:
|
|
31
|
+
from ramllm.services.backend_manager import discover_backend
|
|
32
|
+
|
|
33
|
+
res = discover_backend()
|
|
34
|
+
self._install = res.install if res else None
|
|
35
|
+
return self._install
|
|
36
|
+
|
|
37
|
+
def capabilities(self, install: BackendInstall) -> Capabilities:
|
|
38
|
+
return install.capabilities
|
|
39
|
+
|
|
40
|
+
def build_args(self, plan: ExecutionPlan, port: int = 8080) -> list[str]:
|
|
41
|
+
if not self._install:
|
|
42
|
+
self.detect()
|
|
43
|
+
return build_llamacpp_args(plan, install=self._install, port=port)
|
|
44
|
+
|
|
45
|
+
def launch(
|
|
46
|
+
self, plan: ExecutionPlan, port: int = 8080, timeout_s: float = 30.0
|
|
47
|
+
) -> BackendProcess:
|
|
48
|
+
cmd = self.build_args(plan, port=port)
|
|
49
|
+
return launch_llamacpp_server(cmd, plan=plan, port=port, timeout_s=timeout_s)
|
|
50
|
+
|
|
51
|
+
def memory_report(self, proc: BackendProcess) -> MemoryReport:
|
|
52
|
+
rss = 0
|
|
53
|
+
try:
|
|
54
|
+
p = psutil.Process(proc.pid)
|
|
55
|
+
rss = p.memory_info().rss
|
|
56
|
+
for child in p.children(recursive=True):
|
|
57
|
+
rss += child.memory_info().rss
|
|
58
|
+
except Exception:
|
|
59
|
+
rss = proc.initial_memory_report.ram_rss_bytes
|
|
60
|
+
|
|
61
|
+
return MemoryReport(
|
|
62
|
+
vram_used_bytes=proc.plan.memory.vram_peak_bytes,
|
|
63
|
+
ram_rss_bytes=rss,
|
|
64
|
+
model_buffer_bytes=proc.initial_memory_report.model_buffer_bytes,
|
|
65
|
+
kv_buffer_bytes=proc.initial_memory_report.kv_buffer_bytes,
|
|
66
|
+
compute_buffer_bytes=proc.initial_memory_report.compute_buffer_bytes,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def stop(self, proc: BackendProcess) -> None:
|
|
70
|
+
if proc.process and proc.process.poll() is None:
|
|
71
|
+
proc.process.terminate()
|
|
72
|
+
try:
|
|
73
|
+
proc.process.wait(timeout=3)
|
|
74
|
+
except Exception:
|
|
75
|
+
proc.process.kill()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Flag translation from ExecutionPlan to llama-server CLI arguments."""
|
|
2
|
+
|
|
3
|
+
from ramllm.backends.base import BackendInstall
|
|
4
|
+
from ramllm.core.schemas import ExecutionPlan
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_llamacpp_args(
|
|
8
|
+
plan: ExecutionPlan,
|
|
9
|
+
install: BackendInstall | None = None,
|
|
10
|
+
host: str = "127.0.0.1",
|
|
11
|
+
port: int = 8080,
|
|
12
|
+
) -> list[str]:
|
|
13
|
+
"""Map an ExecutionPlan to concrete arguments for llama-server."""
|
|
14
|
+
binary = install.binary_path if install else "llama-server"
|
|
15
|
+
args = [binary, "--host", host, "--port", str(port)]
|
|
16
|
+
|
|
17
|
+
# Extract model path from backend_args in plan
|
|
18
|
+
model_path = ""
|
|
19
|
+
for i, a in enumerate(plan.backend_args):
|
|
20
|
+
if a in ("-m", "--model") and i + 1 < len(plan.backend_args):
|
|
21
|
+
model_path = plan.backend_args[i + 1]
|
|
22
|
+
break
|
|
23
|
+
|
|
24
|
+
if model_path:
|
|
25
|
+
args.extend(["-m", model_path])
|
|
26
|
+
|
|
27
|
+
# GPU layers
|
|
28
|
+
gpu_layer_count = sum(len(layer_list) for layer_list in plan.placement.gpu_layers.values())
|
|
29
|
+
args.extend(["-ngl", str(gpu_layer_count)])
|
|
30
|
+
|
|
31
|
+
# Context size
|
|
32
|
+
args.extend(["-c", str(plan.kv.context)])
|
|
33
|
+
|
|
34
|
+
# Predictable single-slot memory usage
|
|
35
|
+
args.extend(["-np", "1"])
|
|
36
|
+
|
|
37
|
+
# KV quantization
|
|
38
|
+
if plan.kv.type_k != "f16":
|
|
39
|
+
args.extend(["-ctk", plan.kv.type_k])
|
|
40
|
+
if plan.kv.type_v != "f16":
|
|
41
|
+
args.extend(["-ctv", plan.kv.type_v])
|
|
42
|
+
|
|
43
|
+
# KV offload to CPU if requested
|
|
44
|
+
if plan.kv.location == "cpu":
|
|
45
|
+
args.append("--no-kv-offload")
|
|
46
|
+
|
|
47
|
+
# Multi-GPU split
|
|
48
|
+
if len(plan.placement.gpu_layers) > 1:
|
|
49
|
+
total_offloaded = sum(len(layers) for layers in plan.placement.gpu_layers.values())
|
|
50
|
+
if total_offloaded > 0:
|
|
51
|
+
gpu_indices = sorted(plan.placement.gpu_layers.keys())
|
|
52
|
+
max_gpu_idx = max(gpu_indices)
|
|
53
|
+
fractions = [
|
|
54
|
+
f"{len(plan.placement.gpu_layers.get(idx, [])) / total_offloaded:.2f}"
|
|
55
|
+
for idx in range(max_gpu_idx + 1)
|
|
56
|
+
]
|
|
57
|
+
main_gpu = gpu_indices[0]
|
|
58
|
+
for i, a in enumerate(plan.backend_args):
|
|
59
|
+
if a in ("-mg", "--main-gpu") and i + 1 < len(plan.backend_args):
|
|
60
|
+
main_gpu = int(plan.backend_args[i + 1])
|
|
61
|
+
break
|
|
62
|
+
args.extend(["-sm", "layer", "-ts", ",".join(fractions), "-mg", str(main_gpu)])
|
|
63
|
+
|
|
64
|
+
# MoE CPU offload flags
|
|
65
|
+
if plan.placement.expert_placement == "cpu":
|
|
66
|
+
args.append("-cmoe")
|
|
67
|
+
|
|
68
|
+
# Partial MoE CPU offload (-ncmoe)
|
|
69
|
+
for i, a in enumerate(plan.backend_args):
|
|
70
|
+
if a in ("-ncmoe", "--n-cpu-moe") and i + 1 < len(plan.backend_args):
|
|
71
|
+
args.extend(["-ncmoe", plan.backend_args[i + 1]])
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
# Disk streaming / no-mmap
|
|
75
|
+
if plan.placement.disk_streamed_layers:
|
|
76
|
+
args.extend(["-lm", "none"])
|
|
77
|
+
|
|
78
|
+
return args
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Discovery and capability introspection for llama.cpp installations."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from ramllm.backends.base import BackendInstall, Capabilities
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def detect_llamacpp(custom_path: str | None = None) -> BackendInstall | None:
|
|
10
|
+
"""Discover llama-server binary and introspect its capabilities.
|
|
11
|
+
|
|
12
|
+
Returns None if no working llama-server executable is found.
|
|
13
|
+
"""
|
|
14
|
+
binary = custom_path or shutil.which("llama-server") or shutil.which("llama-cli")
|
|
15
|
+
if not binary:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
# Check version
|
|
19
|
+
version_str: str | None = None
|
|
20
|
+
build_info: str | None = None
|
|
21
|
+
try:
|
|
22
|
+
ver_res = subprocess.run([binary, "--version"], capture_output=True, text=True, timeout=5)
|
|
23
|
+
out = ver_res.stdout + ver_res.stderr
|
|
24
|
+
for line in out.splitlines():
|
|
25
|
+
if "version:" in line.lower() or "build" in line.lower():
|
|
26
|
+
version_str = line.strip()
|
|
27
|
+
if "built with" in line.lower():
|
|
28
|
+
build_info = line.strip()
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
# Check help output for supported flags
|
|
33
|
+
help_text = ""
|
|
34
|
+
try:
|
|
35
|
+
help_res = subprocess.run([binary, "--help"], capture_output=True, text=True, timeout=5)
|
|
36
|
+
help_text = help_res.stdout + help_res.stderr
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
# Derive capabilities from help text
|
|
41
|
+
has_ngl = "-ngl" in help_text or "--gpu-layers" in help_text
|
|
42
|
+
has_cpu_moe = "-cmoe" in help_text or "--cpu-moe" in help_text
|
|
43
|
+
has_kv_quant = "-ctk" in help_text or "--cache-type-k" in help_text
|
|
44
|
+
has_override_tensor = "-ot" in help_text or "--override-tensor" in help_text
|
|
45
|
+
has_split_mode = "-sm" in help_text or "--split-mode" in help_text
|
|
46
|
+
has_flash_attn = "-fa" in help_text or "--flash-attn" in help_text
|
|
47
|
+
|
|
48
|
+
apis: list[str] = []
|
|
49
|
+
if "metal" in help_text.lower() or "metal" in (build_info or "").lower():
|
|
50
|
+
apis.append("metal")
|
|
51
|
+
if "cuda" in help_text.lower() or "cuda" in (build_info or "").lower():
|
|
52
|
+
apis.append("cuda")
|
|
53
|
+
if "vulkan" in help_text.lower():
|
|
54
|
+
apis.append("vulkan")
|
|
55
|
+
if "rocm" in help_text.lower():
|
|
56
|
+
apis.append("rocm")
|
|
57
|
+
|
|
58
|
+
caps = Capabilities(
|
|
59
|
+
partial_gpu_offload=has_ngl,
|
|
60
|
+
per_tensor_placement=has_override_tensor or has_cpu_moe,
|
|
61
|
+
kv_on_gpu_or_cpu_choice=True,
|
|
62
|
+
kv_quantization=has_kv_quant,
|
|
63
|
+
multi_gpu_layer_split=has_split_mode,
|
|
64
|
+
flash_attention=has_flash_attn,
|
|
65
|
+
mmap_control=True,
|
|
66
|
+
apis=apis,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return BackendInstall(
|
|
70
|
+
name="llamacpp",
|
|
71
|
+
binary_path=binary,
|
|
72
|
+
version=version_str,
|
|
73
|
+
build_info=build_info,
|
|
74
|
+
capabilities=caps,
|
|
75
|
+
)
|