lllm2 0.2.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.
- lllm2/__init__.py +0 -0
- lllm2/__main__.py +3 -0
- lllm2/app.py +299 -0
- lllm2/bench.py +404 -0
- lllm2/cli.py +214 -0
- lllm2/config.py +12 -0
- lllm2/defaults.py +154 -0
- lllm2/discovery.py +205 -0
- lllm2/downloads.py +215 -0
- lllm2/engine.py +283 -0
- lllm2/engine_install.py +197 -0
- lllm2/gguf.py +320 -0
- lllm2/harness.py +104 -0
- lllm2/launch.py +83 -0
- lllm2/models.json +125 -0
- lllm2/recommendations.json +3412 -0
- lllm2/recommendations.py +174 -0
- lllm2/settings.py +330 -0
- lllm2/source_workloads.py +205 -0
- lllm2/static/index.html +73 -0
- lllm2/static/panel.css +1275 -0
- lllm2/static/panel.js +673 -0
- lllm2/store.py +37 -0
- lllm2/templates/qwen3.8-27b.jinja +189 -0
- lllm2/warm.py +197 -0
- lllm2-0.2.0.dist-info/METADATA +38 -0
- lllm2-0.2.0.dist-info/RECORD +31 -0
- lllm2-0.2.0.dist-info/WHEEL +4 -0
- lllm2-0.2.0.dist-info/entry_points.txt +2 -0
- lllm2-0.2.0.dist-info/licenses/LICENSE +201 -0
- lllm2-0.2.0.dist-info/licenses/NOTICE +7 -0
lllm2/__init__.py
ADDED
|
File without changes
|
lllm2/__main__.py
ADDED
lllm2/app.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import fcntl
|
|
3
|
+
import json
|
|
4
|
+
import secrets
|
|
5
|
+
import signal
|
|
6
|
+
import socket
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
from . import config, downloads
|
|
13
|
+
from .bench import Bench, WORKLOADS
|
|
14
|
+
from .discovery import CATALOG, engines, hardware, probe
|
|
15
|
+
from .engine import Cancelled, Engine
|
|
16
|
+
from .settings import Settings, capabilities, launch_args
|
|
17
|
+
from .store import Store
|
|
18
|
+
from .defaults import starting_defaults
|
|
19
|
+
from .recommendations import promotion_provenance, saved_qualifications
|
|
20
|
+
from .launch import installed_models, choose_launch
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class App:
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.store = Store()
|
|
26
|
+
self.engine = Engine()
|
|
27
|
+
self.bench = Bench(self.engine,self.store)
|
|
28
|
+
self.token = secrets.token_urlsafe(32)
|
|
29
|
+
self.start_requests = {}
|
|
30
|
+
|
|
31
|
+
def default_key(self,s):
|
|
32
|
+
return str(Path(s.model).expanduser().resolve()) + '|' + s.backend
|
|
33
|
+
|
|
34
|
+
def action(self,path,data):
|
|
35
|
+
if path == '/api/files':
|
|
36
|
+
requested = data.get('directory')
|
|
37
|
+
directory = Path(requested).expanduser() if requested else config.MODELS_DIR
|
|
38
|
+
if not requested and not directory.is_dir():
|
|
39
|
+
directory = Path.home()
|
|
40
|
+
directory = directory.resolve(strict=True)
|
|
41
|
+
if not directory.is_dir():
|
|
42
|
+
raise ValueError('Choose a directory.')
|
|
43
|
+
entries = []
|
|
44
|
+
for child in directory.iterdir():
|
|
45
|
+
try:
|
|
46
|
+
is_dir = child.is_dir()
|
|
47
|
+
if is_dir or (child.is_file() and child.suffix.lower() == '.gguf'):
|
|
48
|
+
entries.append(dict(name=child.name, path=str(child), directory=is_dir,
|
|
49
|
+
size=None if is_dir else child.stat().st_size))
|
|
50
|
+
except OSError:
|
|
51
|
+
continue
|
|
52
|
+
entries.sort(key=lambda e: (not e['directory'], e['name'].casefold()))
|
|
53
|
+
return dict(directory=str(directory), parent=str(directory.parent), entries=entries,
|
|
54
|
+
home=str(Path.home()), models=str(config.MODELS_DIR))
|
|
55
|
+
if path == '/api/discover':
|
|
56
|
+
return dict(models=installed_models(),engines=engines(),catalog=CATALOG)
|
|
57
|
+
if path == '/api/launch/select':
|
|
58
|
+
return choose_launch(data.get('model', ''), data.get('engine', ''),
|
|
59
|
+
data.get('backend', ''), data.get('device', ''))
|
|
60
|
+
if path == '/api/launch/check':
|
|
61
|
+
s = Settings.parse(data['settings'])
|
|
62
|
+
error = None
|
|
63
|
+
try:
|
|
64
|
+
if not hardware()['gpus']:
|
|
65
|
+
raise ValueError('No NVIDIA GPU detected. Check GPU availability before starting.')
|
|
66
|
+
launch_args(s, config.ENGINE_PORT)
|
|
67
|
+
except (OSError, ValueError) as e:
|
|
68
|
+
error = str(e)
|
|
69
|
+
return dict(valid=error is None, error=error,
|
|
70
|
+
saved_exists=self.store.get('default', self.default_key(s)) is not None)
|
|
71
|
+
if path == '/api/result/preview':
|
|
72
|
+
r = self.store.get('result', data['result_id'])
|
|
73
|
+
if not r or r['status'] != 'complete' or not r['samples']:
|
|
74
|
+
raise ValueError('Choose a completed result with samples.')
|
|
75
|
+
if r.get('measurement_mode') == 'warm-conversation' or r.get('quality_status') == 'failed':
|
|
76
|
+
raise ValueError('This result cannot be used as a general measured configuration.')
|
|
77
|
+
s = Settings.parse(r['settings'])
|
|
78
|
+
if data.get('use_context'):
|
|
79
|
+
if not r.get('recommended_context'):
|
|
80
|
+
raise ValueError('This result has no successful context probe.')
|
|
81
|
+
s.context = r['recommended_context'] * s.slots
|
|
82
|
+
return dict(settings=s.dict(), source='Experiment result · for next launch',
|
|
83
|
+
notes=['Review before starting or saving. Saved preferences are unchanged.'],
|
|
84
|
+
evidence=promotion_provenance(r, s, data.get('use_context')),
|
|
85
|
+
result_id=r['id'], use_context=bool(data.get('use_context')))
|
|
86
|
+
if path == '/api/capabilities':
|
|
87
|
+
s = Settings.parse(data['settings'])
|
|
88
|
+
return dict(features=capabilities(s),engine={k:v for k,v in probe(s.engine).items() if k != 'help'})
|
|
89
|
+
if path == '/api/default/resolve':
|
|
90
|
+
s = Settings.parse(data['settings'])
|
|
91
|
+
source = data.get('source','auto')
|
|
92
|
+
if source not in ['auto','saved','built-in']:
|
|
93
|
+
raise ValueError('Unknown defaults source.')
|
|
94
|
+
saved = self.store.get('default',self.default_key(s))
|
|
95
|
+
if saved and source != 'built-in':
|
|
96
|
+
resolved = Settings.parse(saved)
|
|
97
|
+
resolved.engine, resolved.device = s.engine, s.device
|
|
98
|
+
provenance = self.store.get('default-evidence', self.default_key(s))
|
|
99
|
+
if not provenance or provenance.get('settings') != saved:
|
|
100
|
+
return dict(settings=resolved.dict(), source='Saved defaults · origin unknown',
|
|
101
|
+
notes=['Legacy saved settings preserved; no measurement provenance was recorded.'])
|
|
102
|
+
notes = ['Saved preferences take precedence over built-in recommendations.']
|
|
103
|
+
if provenance['kind'] == 'benchmark':
|
|
104
|
+
notes.append(provenance['note'])
|
|
105
|
+
notes.extend(saved_qualifications(provenance, resolved))
|
|
106
|
+
if provenance['context']['used_headroom_estimate']:
|
|
107
|
+
notes.append('Saved context uses a headroom estimate, not an observed successful allocation.')
|
|
108
|
+
return dict(settings=resolved.dict(), source='Saved defaults · ' +
|
|
109
|
+
('historical benchmark evidence · qualified' if provenance['kind'] == 'benchmark' else 'manual preferences'),
|
|
110
|
+
notes=notes, evidence=provenance if provenance['kind'] == 'benchmark' else None)
|
|
111
|
+
if source == 'saved':
|
|
112
|
+
raise ValueError('No saved settings for this model and backend yet. Load a completed experiment into Launch or edit the draft, then choose “Save my settings”.')
|
|
113
|
+
return starting_defaults(s)
|
|
114
|
+
if path == '/api/default/load':
|
|
115
|
+
s = Settings.parse(data['settings'])
|
|
116
|
+
return self.store.get('default',self.default_key(s))
|
|
117
|
+
if path == '/api/default/save':
|
|
118
|
+
provenance = None
|
|
119
|
+
if data.get('result_id'):
|
|
120
|
+
r = self.store.get('result',data['result_id'])
|
|
121
|
+
if not r or r['status'] != 'complete' or not r['samples']:
|
|
122
|
+
raise ValueError('Only a completed measured configuration can be promoted.')
|
|
123
|
+
if r.get('measurement_mode') == 'warm-conversation':
|
|
124
|
+
raise ValueError('Warm conversation results cannot be promoted as a general cold baseline. Save launch preferences manually if desired.')
|
|
125
|
+
if r.get('quality_status') == 'failed':
|
|
126
|
+
raise ValueError('Source adherence failed; this result cannot be promoted as a measured baseline.')
|
|
127
|
+
s = Settings.parse(r['settings'])
|
|
128
|
+
if data.get('use_context'):
|
|
129
|
+
if not r['recommended_context']:
|
|
130
|
+
raise ValueError('This run has no successful context probe.')
|
|
131
|
+
s.context = r['recommended_context'] * s.slots
|
|
132
|
+
provenance = promotion_provenance(r, s, data.get('use_context'))
|
|
133
|
+
else:
|
|
134
|
+
s = Settings.parse(data['settings'])
|
|
135
|
+
launch_args(s,config.ENGINE_PORT)
|
|
136
|
+
self.store.put('default',self.default_key(s),s.dict())
|
|
137
|
+
self.store.put('default-evidence', self.default_key(s),
|
|
138
|
+
provenance or dict(kind='manual', settings=s.dict()))
|
|
139
|
+
return s.dict()
|
|
140
|
+
if path == '/api/benchmark':
|
|
141
|
+
return self.bench.submit(data)
|
|
142
|
+
if path in ['/api/cancel','/api/stop']:
|
|
143
|
+
with self.bench.lock:
|
|
144
|
+
self.bench.cancel.set()
|
|
145
|
+
self.engine.stop()
|
|
146
|
+
if not self.bench.active:
|
|
147
|
+
self.bench.progress = dict(status='stopped', kind='launch')
|
|
148
|
+
return dict(ok=True)
|
|
149
|
+
if path == '/api/start':
|
|
150
|
+
s = Settings.parse(data['settings'])
|
|
151
|
+
request_id = data.get('request_id')
|
|
152
|
+
if request_id is not None and (not isinstance(request_id, str) or not 1 <= len(request_id) <= 128):
|
|
153
|
+
raise ValueError('Invalid start request identity.')
|
|
154
|
+
launch_args(s,config.ENGINE_PORT)
|
|
155
|
+
with self.bench.lock:
|
|
156
|
+
if request_id in self.start_requests:
|
|
157
|
+
if self.start_requests[request_id] != s.dict():
|
|
158
|
+
raise ValueError('Start request identity already used for different settings.')
|
|
159
|
+
return dict(ok=True)
|
|
160
|
+
if self.bench.active:
|
|
161
|
+
raise ValueError('Wait for the current operation or cancel it first.')
|
|
162
|
+
running = self.engine.state()
|
|
163
|
+
if running.get('ready') and running['settings'] == s.dict():
|
|
164
|
+
return dict(ok=True)
|
|
165
|
+
if running['running']:
|
|
166
|
+
if data.get('replace_running') is not True or data.get('expected_pid') != running['pid']:
|
|
167
|
+
raise ValueError('A model is running or has changed. Refresh status and explicitly switch or restart it.')
|
|
168
|
+
self.bench.active = True
|
|
169
|
+
self.bench.cancel.clear()
|
|
170
|
+
if request_id:
|
|
171
|
+
self.start_requests[request_id] = s.dict()
|
|
172
|
+
if len(self.start_requests) > 128:
|
|
173
|
+
del self.start_requests[next(iter(self.start_requests))]
|
|
174
|
+
self.bench.progress = dict(kind='launch', status='starting', phase='Loading model',
|
|
175
|
+
started_at=time.time(), settings=s.dict(), request_id=request_id)
|
|
176
|
+
def start():
|
|
177
|
+
try:
|
|
178
|
+
self.engine.start(s,self.bench.cancel)
|
|
179
|
+
self.bench.update(status='serving',phase='Ready')
|
|
180
|
+
except Cancelled:
|
|
181
|
+
self.bench.update(status='cancelled')
|
|
182
|
+
except Exception as e:
|
|
183
|
+
self.bench.update(status='failed',error=str(e))
|
|
184
|
+
finally:
|
|
185
|
+
with self.bench.lock:
|
|
186
|
+
self.bench.active = False
|
|
187
|
+
threading.Thread(target=start,daemon=True).start()
|
|
188
|
+
return dict(ok=True)
|
|
189
|
+
if path == '/api/download':
|
|
190
|
+
entry = next((e for e in CATALOG if e['id']==data['id']),None)
|
|
191
|
+
if entry is None:
|
|
192
|
+
raise ValueError('Unknown catalogue model.')
|
|
193
|
+
return downloads.start(entry).as_dict()
|
|
194
|
+
if path == '/api/download/cancel':
|
|
195
|
+
return dict(cancelled=downloads.cancel(data['id']))
|
|
196
|
+
raise ValueError('Unknown action')
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def serve(host='127.0.0.1', port=8082):
|
|
200
|
+
config.STATE_DIR.mkdir(parents=True,exist_ok=True)
|
|
201
|
+
lock = (config.STATE_DIR/'panel.lock').open('w')
|
|
202
|
+
try:
|
|
203
|
+
fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
|
204
|
+
except BlockingIOError as error:
|
|
205
|
+
lock.close()
|
|
206
|
+
raise RuntimeError('Another lllm2 panel owns this state directory.') from error
|
|
207
|
+
app = App()
|
|
208
|
+
hostnames = {'127.0.0.1','localhost',socket.gethostname().lower(),socket.getfqdn().lower()}
|
|
209
|
+
allowed_hosts = {f'{name}:{port}' for name in hostnames}
|
|
210
|
+
|
|
211
|
+
class Handler(BaseHTTPRequestHandler):
|
|
212
|
+
def send(self,status,body,kind='application/json'):
|
|
213
|
+
raw = json.dumps(body).encode() if kind == 'application/json' else body
|
|
214
|
+
self.send_response(status)
|
|
215
|
+
self.send_header('Content-Type',kind)
|
|
216
|
+
self.send_header('Content-Length',str(len(raw)))
|
|
217
|
+
self.send_header('Cache-Control','no-store')
|
|
218
|
+
self.send_header('X-Content-Type-Options','nosniff')
|
|
219
|
+
self.end_headers()
|
|
220
|
+
try:
|
|
221
|
+
self.wfile.write(raw)
|
|
222
|
+
except (BrokenPipeError,ConnectionResetError):
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
def valid_host(self):
|
|
226
|
+
# The accepted socket identifies the local interface used by this
|
|
227
|
+
# request, including LAN addresses on multi-interface workstations.
|
|
228
|
+
local_host = f'{self.connection.getsockname()[0]}:{port}'
|
|
229
|
+
if self.headers.get('Host','').lower() not in allowed_hosts | {local_host}:
|
|
230
|
+
self.send(403,dict(error='Use this workstation’s panel address or hostname.'))
|
|
231
|
+
return False
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
def do_GET(self):
|
|
235
|
+
if not self.valid_host():
|
|
236
|
+
return
|
|
237
|
+
path = urlparse(self.path).path
|
|
238
|
+
if path == '/':
|
|
239
|
+
self.send(200,Path(__file__).with_name('static').joinpath('index.html').read_bytes(),'text/html; charset=utf-8')
|
|
240
|
+
elif path == '/static/panel.js':
|
|
241
|
+
self.send(200,Path(__file__).with_name('static').joinpath('panel.js').read_bytes(),'text/javascript; charset=utf-8')
|
|
242
|
+
elif path == '/static/panel.css':
|
|
243
|
+
self.send(200,Path(__file__).with_name('static').joinpath('panel.css').read_bytes(),'text/css; charset=utf-8')
|
|
244
|
+
elif path == '/api/status':
|
|
245
|
+
self.send(200,dict(token=app.token,engine=app.engine.state(),job=app.bench.snapshot(),hardware=hardware(),
|
|
246
|
+
downloads=downloads.all_downloads(),endpoint=app.engine.base+'/v1',
|
|
247
|
+
paths=dict(models=str(config.MODELS_DIR),engines=[str(p) for p in config.ENGINE_ROOTS]),workloads=WORKLOADS))
|
|
248
|
+
elif path in ['/api/results','/api/results/export']:
|
|
249
|
+
rows = app.store.list('summary' if path == '/api/results' else 'result')
|
|
250
|
+
self.send(200,rows)
|
|
251
|
+
else:
|
|
252
|
+
self.send(404,dict(error='Not found'))
|
|
253
|
+
|
|
254
|
+
def do_POST(self):
|
|
255
|
+
if not self.valid_host():
|
|
256
|
+
return
|
|
257
|
+
origin = self.headers.get('Origin')
|
|
258
|
+
if self.headers.get('X-LLLM2-Token') != app.token or (origin and origin.lower() != f'http://{self.headers.get("Host","").lower()}'):
|
|
259
|
+
self.send(403,dict(error='Reload the panel to renew its session.'))
|
|
260
|
+
return
|
|
261
|
+
try:
|
|
262
|
+
size = int(self.headers.get('Content-Length','0'))
|
|
263
|
+
if not 0<size<=2_000_000:
|
|
264
|
+
raise ValueError('Invalid request size')
|
|
265
|
+
data = json.loads(self.rfile.read(size))
|
|
266
|
+
self.send(200,app.action(urlparse(self.path).path,data))
|
|
267
|
+
except (ValueError,KeyError,TypeError,OSError) as e:
|
|
268
|
+
self.send(400,dict(error=str(e)))
|
|
269
|
+
except Exception as e:
|
|
270
|
+
app.engine.log('Panel error: ' + str(e))
|
|
271
|
+
self.send(500,dict(error=str(e)))
|
|
272
|
+
|
|
273
|
+
def log_message(self,*args):
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
server = ThreadingHTTPServer((host,port),Handler)
|
|
277
|
+
def shutdown(*_):
|
|
278
|
+
threading.Thread(target=server.shutdown,daemon=True).start()
|
|
279
|
+
signal.signal(signal.SIGTERM,shutdown)
|
|
280
|
+
signal.signal(signal.SIGINT,shutdown)
|
|
281
|
+
print(f'lllm2: http://127.0.0.1:{port}',flush=True)
|
|
282
|
+
if host != '127.0.0.1':
|
|
283
|
+
print(f'LAN panel: http://{socket.gethostname() if host == "0.0.0.0" else host}:{port} (listening on {host})',flush=True)
|
|
284
|
+
print('LAN access has no login or TLS: anyone who can reach this port can control the workbench. Use only on a trusted network.',flush=True)
|
|
285
|
+
try:
|
|
286
|
+
server.serve_forever()
|
|
287
|
+
finally:
|
|
288
|
+
app.bench.cancel.set()
|
|
289
|
+
app.engine.stop()
|
|
290
|
+
server.server_close()
|
|
291
|
+
lock.close()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def main(argv=None):
|
|
295
|
+
parser = argparse.ArgumentParser(description='lllm2 local LLM workbench')
|
|
296
|
+
parser.add_argument('--port',type=int,default=8082)
|
|
297
|
+
parser.add_argument('--host',default='127.0.0.1',help='IPv4 bind address (default: localhost; use 0.0.0.0 for trusted-LAN access without authentication or TLS)')
|
|
298
|
+
args = parser.parse_args(argv)
|
|
299
|
+
serve(args.host, args.port)
|