lemonade-sdk 9.1.1__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.
- lemonade/__init__.py +5 -0
- lemonade/api.py +180 -0
- lemonade/cache.py +92 -0
- lemonade/cli.py +173 -0
- lemonade/common/__init__.py +0 -0
- lemonade/common/build.py +176 -0
- lemonade/common/cli_helpers.py +139 -0
- lemonade/common/exceptions.py +98 -0
- lemonade/common/filesystem.py +368 -0
- lemonade/common/inference_engines.py +408 -0
- lemonade/common/network.py +93 -0
- lemonade/common/printing.py +110 -0
- lemonade/common/status.py +471 -0
- lemonade/common/system_info.py +1411 -0
- lemonade/common/test_helpers.py +28 -0
- lemonade/profilers/__init__.py +1 -0
- lemonade/profilers/agt_power.py +437 -0
- lemonade/profilers/hwinfo_power.py +429 -0
- lemonade/profilers/memory_tracker.py +259 -0
- lemonade/profilers/profiler.py +58 -0
- lemonade/sequence.py +363 -0
- lemonade/state.py +159 -0
- lemonade/tools/__init__.py +1 -0
- lemonade/tools/accuracy.py +432 -0
- lemonade/tools/adapter.py +114 -0
- lemonade/tools/bench.py +302 -0
- lemonade/tools/flm/__init__.py +1 -0
- lemonade/tools/flm/utils.py +305 -0
- lemonade/tools/huggingface/bench.py +187 -0
- lemonade/tools/huggingface/load.py +235 -0
- lemonade/tools/huggingface/utils.py +359 -0
- lemonade/tools/humaneval.py +264 -0
- lemonade/tools/llamacpp/bench.py +255 -0
- lemonade/tools/llamacpp/load.py +222 -0
- lemonade/tools/llamacpp/utils.py +1260 -0
- lemonade/tools/management_tools.py +319 -0
- lemonade/tools/mmlu.py +319 -0
- lemonade/tools/oga/__init__.py +0 -0
- lemonade/tools/oga/bench.py +120 -0
- lemonade/tools/oga/load.py +804 -0
- lemonade/tools/oga/migration.py +403 -0
- lemonade/tools/oga/utils.py +462 -0
- lemonade/tools/perplexity.py +147 -0
- lemonade/tools/prompt.py +263 -0
- lemonade/tools/report/__init__.py +0 -0
- lemonade/tools/report/llm_report.py +203 -0
- lemonade/tools/report/table.py +899 -0
- lemonade/tools/server/__init__.py +0 -0
- lemonade/tools/server/flm.py +133 -0
- lemonade/tools/server/llamacpp.py +320 -0
- lemonade/tools/server/serve.py +2123 -0
- lemonade/tools/server/static/favicon.ico +0 -0
- lemonade/tools/server/static/index.html +279 -0
- lemonade/tools/server/static/js/chat.js +1059 -0
- lemonade/tools/server/static/js/model-settings.js +183 -0
- lemonade/tools/server/static/js/models.js +1395 -0
- lemonade/tools/server/static/js/shared.js +556 -0
- lemonade/tools/server/static/logs.html +191 -0
- lemonade/tools/server/static/styles.css +2654 -0
- lemonade/tools/server/static/webapp.html +321 -0
- lemonade/tools/server/tool_calls.py +153 -0
- lemonade/tools/server/tray.py +664 -0
- lemonade/tools/server/utils/macos_tray.py +226 -0
- lemonade/tools/server/utils/port.py +77 -0
- lemonade/tools/server/utils/thread.py +85 -0
- lemonade/tools/server/utils/windows_tray.py +408 -0
- lemonade/tools/server/webapp.py +34 -0
- lemonade/tools/server/wrapped_server.py +559 -0
- lemonade/tools/tool.py +374 -0
- lemonade/version.py +1 -0
- lemonade_install/__init__.py +1 -0
- lemonade_install/install.py +239 -0
- lemonade_sdk-9.1.1.dist-info/METADATA +276 -0
- lemonade_sdk-9.1.1.dist-info/RECORD +84 -0
- lemonade_sdk-9.1.1.dist-info/WHEEL +5 -0
- lemonade_sdk-9.1.1.dist-info/entry_points.txt +5 -0
- lemonade_sdk-9.1.1.dist-info/licenses/LICENSE +201 -0
- lemonade_sdk-9.1.1.dist-info/licenses/NOTICE.md +47 -0
- lemonade_sdk-9.1.1.dist-info/top_level.txt +3 -0
- lemonade_server/cli.py +805 -0
- lemonade_server/model_manager.py +758 -0
- lemonade_server/pydantic_models.py +159 -0
- lemonade_server/server_models.json +643 -0
- lemonade_server/settings.py +39 -0
lemonade/tools/tool.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import os
|
|
5
|
+
import argparse
|
|
6
|
+
import textwrap as _textwrap
|
|
7
|
+
import re
|
|
8
|
+
from typing import Tuple, Dict
|
|
9
|
+
from multiprocessing import Process, Queue
|
|
10
|
+
import psutil
|
|
11
|
+
import lemonade.common.printing as printing
|
|
12
|
+
import lemonade.common.exceptions as exp
|
|
13
|
+
import lemonade.common.build as build
|
|
14
|
+
import lemonade.common.filesystem as fs
|
|
15
|
+
from lemonade.state import State
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _spinner(message, q: Queue):
|
|
19
|
+
"""
|
|
20
|
+
Displays a moving "..." indicator so that the user knows that the
|
|
21
|
+
Tool is still working. Tools can optionally use a multiprocessing
|
|
22
|
+
Queue to display the percent progress of the Tool.
|
|
23
|
+
"""
|
|
24
|
+
percent_complete = None
|
|
25
|
+
# Get sleep time from environment variable, default to 0.5s if not set
|
|
26
|
+
try:
|
|
27
|
+
sleep_time = float(os.getenv("LEMONADE_BUILD_MONITOR_FREQUENCY", "0.5"))
|
|
28
|
+
except ValueError:
|
|
29
|
+
sleep_time = 0.5
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
parent_process = psutil.Process(pid=os.getppid())
|
|
33
|
+
while parent_process.status() == psutil.STATUS_RUNNING:
|
|
34
|
+
for cursor in [" ", ". ", ".. ", "..."]:
|
|
35
|
+
time.sleep(sleep_time)
|
|
36
|
+
while not q.empty():
|
|
37
|
+
percent_complete = q.get()
|
|
38
|
+
if percent_complete is not None:
|
|
39
|
+
status = f" {message} ({percent_complete:.1f}%){cursor}\r"
|
|
40
|
+
else:
|
|
41
|
+
status = f" {message}{cursor} \r"
|
|
42
|
+
sys.stdout.write(status)
|
|
43
|
+
sys.stdout.flush()
|
|
44
|
+
except psutil.NoSuchProcess:
|
|
45
|
+
# If the parent process stopped existing, we can
|
|
46
|
+
# safely assume the spinner no longer needs to spin
|
|
47
|
+
# NOTE: this only seems to be needed on Windows
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _name_is_file_safe(name: str):
|
|
52
|
+
"""
|
|
53
|
+
Make sure the name can be used in a filename
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
allowed_in_unique_name = set(
|
|
57
|
+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if len(name) == 0:
|
|
61
|
+
msg = """
|
|
62
|
+
Tool __init__() was passed a unique_name with no length. A
|
|
63
|
+
uniquely identifying unique_name is required.
|
|
64
|
+
"""
|
|
65
|
+
raise ValueError(msg)
|
|
66
|
+
|
|
67
|
+
for char in name:
|
|
68
|
+
if char not in allowed_in_unique_name:
|
|
69
|
+
msg = f"""
|
|
70
|
+
Tool __init__() was passed a unique_name:
|
|
71
|
+
{name}
|
|
72
|
+
with illegal characters. The unique_name must be safe to
|
|
73
|
+
use in a filename, meaning it can only use characters: {allowed_in_unique_name}
|
|
74
|
+
"""
|
|
75
|
+
raise ValueError(msg)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class NiceHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|
79
|
+
def __add_whitespace(self, idx, amount, text):
|
|
80
|
+
if idx == 0:
|
|
81
|
+
return text
|
|
82
|
+
return (" " * amount) + text
|
|
83
|
+
|
|
84
|
+
def _split_lines(self, text, width):
|
|
85
|
+
textRows = text.splitlines()
|
|
86
|
+
for idx, line in enumerate(textRows):
|
|
87
|
+
search = re.search(r"\s*[0-9\-]{0,}\.?\s*", line)
|
|
88
|
+
if line.strip() == "":
|
|
89
|
+
textRows[idx] = " "
|
|
90
|
+
elif search:
|
|
91
|
+
whitespace_needed = search.end()
|
|
92
|
+
lines = [
|
|
93
|
+
self.__add_whitespace(i, whitespace_needed, x)
|
|
94
|
+
for i, x in enumerate(_textwrap.wrap(line, width))
|
|
95
|
+
]
|
|
96
|
+
textRows[idx] = lines
|
|
97
|
+
|
|
98
|
+
return [item for sublist in textRows for item in sublist]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ToolParser(argparse.ArgumentParser):
|
|
102
|
+
|
|
103
|
+
def error(self, message):
|
|
104
|
+
if message.startswith("unrecognized arguments"):
|
|
105
|
+
unrecognized = message.split(": ")[1]
|
|
106
|
+
if not unrecognized.startswith("-"):
|
|
107
|
+
# This was probably a misspelled tool name
|
|
108
|
+
message = message + (
|
|
109
|
+
f". If `{unrecognized}` was intended to invoke "
|
|
110
|
+
"a tool, please run `lemonade -h` and check the spelling and "
|
|
111
|
+
"availability of that tool."
|
|
112
|
+
)
|
|
113
|
+
self.print_usage()
|
|
114
|
+
printing.log_error(message)
|
|
115
|
+
self.exit(2)
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self, short_description: str, description: str, prog: str, epilog: str, **kwargs
|
|
119
|
+
):
|
|
120
|
+
super().__init__(
|
|
121
|
+
description=description,
|
|
122
|
+
prog=prog,
|
|
123
|
+
epilog=epilog,
|
|
124
|
+
formatter_class=NiceHelpFormatter,
|
|
125
|
+
**kwargs,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.short_description = short_description
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Tool(abc.ABC):
|
|
132
|
+
|
|
133
|
+
unique_name: str
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def helpful_parser(cls, short_description: str, **kwargs):
|
|
137
|
+
epilog = (
|
|
138
|
+
f"`{cls.unique_name}` is a Tool. It is intended to be invoked as "
|
|
139
|
+
"part of a sequence of Tools, for example: `lemonade -i INPUTS tool-one "
|
|
140
|
+
"tool-two tool-three`. Tools communicate data to each other via State. "
|
|
141
|
+
"You can learn more at "
|
|
142
|
+
"https://github.com/lemonade-sdk/lemonade/blob/main/docs/README.md"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return ToolParser(
|
|
146
|
+
prog=f"lemonade {cls.unique_name}",
|
|
147
|
+
short_description=short_description,
|
|
148
|
+
description=cls.__doc__,
|
|
149
|
+
epilog=epilog,
|
|
150
|
+
**kwargs,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def status_line(self, successful, verbosity):
|
|
154
|
+
"""
|
|
155
|
+
Print a line of status information for this Tool into the monitor.
|
|
156
|
+
"""
|
|
157
|
+
if verbosity:
|
|
158
|
+
# Only use special characters when the terminal encoding supports it
|
|
159
|
+
if sys.stdout.encoding == "utf-8":
|
|
160
|
+
success_tick = "✓"
|
|
161
|
+
fail_tick = "×"
|
|
162
|
+
else:
|
|
163
|
+
success_tick = "+"
|
|
164
|
+
fail_tick = "x"
|
|
165
|
+
|
|
166
|
+
if self.percent_progress is None:
|
|
167
|
+
progress_indicator = ""
|
|
168
|
+
else:
|
|
169
|
+
progress_indicator = f" ({self.percent_progress:.1f}%)"
|
|
170
|
+
|
|
171
|
+
if successful is None:
|
|
172
|
+
# Initialize the message
|
|
173
|
+
printing.logn(f" {self.monitor_message} ")
|
|
174
|
+
elif successful:
|
|
175
|
+
# Print success message
|
|
176
|
+
printing.log(f" {success_tick} ", c=printing.Colors.OKGREEN)
|
|
177
|
+
printing.logn(
|
|
178
|
+
self.monitor_message + progress_indicator + " "
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
# successful == False, print failure message
|
|
182
|
+
printing.log(f" {fail_tick} ", c=printing.Colors.FAIL)
|
|
183
|
+
printing.logn(
|
|
184
|
+
self.monitor_message + progress_indicator + " "
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def __init__(
|
|
188
|
+
self,
|
|
189
|
+
monitor_message,
|
|
190
|
+
enable_logger=True,
|
|
191
|
+
):
|
|
192
|
+
_name_is_file_safe(self.__class__.unique_name)
|
|
193
|
+
|
|
194
|
+
self.status_key = f"{fs.Keys.TOOL_STATUS}:{self.__class__.unique_name}"
|
|
195
|
+
self.duration_key = f"{fs.Keys.TOOL_DURATION}:{self.__class__.unique_name}"
|
|
196
|
+
self.memory_key = f"{fs.Keys.TOOL_MEMORY}:{self.__class__.unique_name}"
|
|
197
|
+
self.monitor_message = monitor_message
|
|
198
|
+
self.progress = None
|
|
199
|
+
self.progress_queue = None
|
|
200
|
+
self.percent_progress = None
|
|
201
|
+
self.logfile_path = None
|
|
202
|
+
# Tools can disable build.Logger, which captures all stdout and stderr from
|
|
203
|
+
# the Tool, by setting enable_logger=False
|
|
204
|
+
self.enable_logger = enable_logger
|
|
205
|
+
# Tools can provide a list of keys that can be found in
|
|
206
|
+
# evaluation stats. Those key:value pairs will be presented
|
|
207
|
+
# in the status at the end of the build.
|
|
208
|
+
self.status_stats = []
|
|
209
|
+
|
|
210
|
+
@abc.abstractmethod
|
|
211
|
+
def run(self, state: State) -> State:
|
|
212
|
+
"""
|
|
213
|
+
Execute the functionality of the Tool by acting on the state.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
@abc.abstractmethod
|
|
218
|
+
def parser() -> argparse.ArgumentParser:
|
|
219
|
+
"""
|
|
220
|
+
Static method that returns an ArgumentParser that defines the command
|
|
221
|
+
line interface for this Tool.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def set_percent_progress(self, percent_progress: float):
|
|
225
|
+
"""
|
|
226
|
+
Update the progress monitor with a percent progress to let the user
|
|
227
|
+
know how much progress the Tool has made.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
if percent_progress is not None and not isinstance(percent_progress, float):
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"Input argument must be a float or None, got {percent_progress}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if self.progress_queue:
|
|
236
|
+
self.progress_queue.put(percent_progress)
|
|
237
|
+
self.percent_progress = percent_progress
|
|
238
|
+
|
|
239
|
+
# pylint: disable=unused-argument
|
|
240
|
+
def parse(self, state: State, args, known_only=True) -> argparse.Namespace:
|
|
241
|
+
"""
|
|
242
|
+
Run the parser and return a Namespace of keyword arguments that the user
|
|
243
|
+
passed to the Tool via the command line.
|
|
244
|
+
|
|
245
|
+
Tools should extend this function only if they require specific parsing
|
|
246
|
+
logic, for example decoding the name of a data type into a data type class.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
state: the same state passed into the run method of the Tool, useful if
|
|
250
|
+
the parse decoding logic needs to take the state into account.
|
|
251
|
+
args: command line arguments passed from the CLI.
|
|
252
|
+
known_only: this argument allows the CLI framework to
|
|
253
|
+
incrementally parse complex commands.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
if known_only:
|
|
257
|
+
parsed_args = self.__class__.parser().parse_args(args)
|
|
258
|
+
else:
|
|
259
|
+
parsed_args, _ = self.__class__.parser().parse_known_args(args)
|
|
260
|
+
|
|
261
|
+
return parsed_args
|
|
262
|
+
|
|
263
|
+
def parse_and_run(
|
|
264
|
+
self,
|
|
265
|
+
state: State,
|
|
266
|
+
args,
|
|
267
|
+
monitor: bool = False,
|
|
268
|
+
known_only=True,
|
|
269
|
+
) -> Dict:
|
|
270
|
+
"""
|
|
271
|
+
Helper function to parse CLI arguments into the args expected
|
|
272
|
+
by run(), and then forward them into the run() method.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
parsed_args = self.parse(state, args, known_only)
|
|
276
|
+
return self.run_helper(state, monitor, **parsed_args.__dict__)
|
|
277
|
+
|
|
278
|
+
def run_helper(
|
|
279
|
+
self, state: State, monitor: bool = False, **kwargs
|
|
280
|
+
) -> Tuple[State, int]:
|
|
281
|
+
"""
|
|
282
|
+
Wraps the developer-defined .run() method with helper functionality.
|
|
283
|
+
Specifically:
|
|
284
|
+
- Provides a path to a log file
|
|
285
|
+
- Redirects the stdout of the tool to that log file
|
|
286
|
+
- Monitors the progress of the tool on the command line,
|
|
287
|
+
including in the event of an exception
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
# Set the build status to INCOMPLETE to indicate that a Tool
|
|
291
|
+
# started running. This allows us to test whether the Tool exited
|
|
292
|
+
# unexpectedly, before it was able to set ERROR
|
|
293
|
+
state.build_status = build.FunctionStatus.INCOMPLETE
|
|
294
|
+
|
|
295
|
+
self.logfile_path = os.path.join(
|
|
296
|
+
build.output_dir(state.cache_dir, state.build_name),
|
|
297
|
+
f"log_{self.unique_name}.txt",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if monitor:
|
|
301
|
+
self.progress_queue = Queue()
|
|
302
|
+
self.progress = Process(
|
|
303
|
+
target=_spinner, args=(self.monitor_message, self.progress_queue)
|
|
304
|
+
)
|
|
305
|
+
self.progress.start()
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
# Execute the build tool
|
|
309
|
+
|
|
310
|
+
if self.enable_logger:
|
|
311
|
+
with build.Logger(self.monitor_message, self.logfile_path):
|
|
312
|
+
state = self.run(state, **kwargs)
|
|
313
|
+
else:
|
|
314
|
+
state = self.run(state, **kwargs)
|
|
315
|
+
|
|
316
|
+
except Exception: # pylint: disable=broad-except
|
|
317
|
+
self.status_line(
|
|
318
|
+
successful=False,
|
|
319
|
+
verbosity=monitor,
|
|
320
|
+
)
|
|
321
|
+
state.build_status = build.FunctionStatus.ERROR
|
|
322
|
+
raise
|
|
323
|
+
|
|
324
|
+
else:
|
|
325
|
+
self.status_line(successful=True, verbosity=monitor)
|
|
326
|
+
|
|
327
|
+
# Tools should not set build.FunctionStatus.SUCCESSFUL for the whole build,
|
|
328
|
+
# as that is reserved for Sequence.launch()
|
|
329
|
+
if state.build_status == build.FunctionStatus.SUCCESSFUL:
|
|
330
|
+
raise exp.ToolError(
|
|
331
|
+
"Lemonade Tools are not allowed to set "
|
|
332
|
+
"`state.build_status == build.FunctionStatus.SUCCESSFUL`, "
|
|
333
|
+
"however that has happened. If you are a plugin developer, "
|
|
334
|
+
"do not do this. If you are a user, please file an issue at "
|
|
335
|
+
"https://github.com/lemonade-sdk/lemonade/issues."
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
finally:
|
|
339
|
+
if monitor:
|
|
340
|
+
self.progress.terminate()
|
|
341
|
+
|
|
342
|
+
return state
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class FirstTool(Tool):
|
|
346
|
+
"""
|
|
347
|
+
Provides extra features for Tools that are meant to be the first Tool
|
|
348
|
+
in the sequence.
|
|
349
|
+
|
|
350
|
+
Specifically:
|
|
351
|
+
- FirstTools should not have any expectations of State.result, since
|
|
352
|
+
they populate State with an initial result.
|
|
353
|
+
- All FirstTools implicitly take an `input` argument that points to
|
|
354
|
+
the input to that Tool, for example an ONNX file or PyTorch script.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
def helpful_parser(cls, short_description: str, **kwargs):
|
|
359
|
+
parser = super().helpful_parser(short_description, **kwargs)
|
|
360
|
+
|
|
361
|
+
# Argument required for any tool that starts a sequence
|
|
362
|
+
parser.add_argument("--input", help=argparse.SUPPRESS)
|
|
363
|
+
|
|
364
|
+
return parser
|
|
365
|
+
|
|
366
|
+
@abc.abstractmethod
|
|
367
|
+
def run(self, state: State, input=None) -> State:
|
|
368
|
+
"""
|
|
369
|
+
The run() method of any FirstTool must accept the `input` argument
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# This file was originally licensed under Apache 2.0. It has been modified.
|
|
374
|
+
# Modifications Copyright (c) 2025 AMD
|
lemonade/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "9.1.1"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .install import main as installcli
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Utility that helps users install software. It is structured like a
|
|
2
|
+
# ManagementTool, however it is not a ManagementTool because it cannot
|
|
3
|
+
# import any lemonade modules in order to avoid any installation
|
|
4
|
+
# collisions on imported modules.
|
|
5
|
+
#
|
|
6
|
+
# This tool can install llama.cpp and FLM (FastFlowLM) for local LLM inference.
|
|
7
|
+
# For RyzenAI support, use PyPI installation:
|
|
8
|
+
# pip install lemonade-sdk[oga-ryzenai] --extra-index-url https://pypi.amd.com/simple
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
# NPU Driver configuration
|
|
18
|
+
NPU_DRIVER_DOWNLOAD_URL = (
|
|
19
|
+
"https://account.amd.com/en/forms/downloads/"
|
|
20
|
+
"ryzenai-eula-public-xef.html?filename=NPU_RAI1.5_280_WHQL.zip"
|
|
21
|
+
)
|
|
22
|
+
REQUIRED_NPU_DRIVER_VERSION = "32.0.203.280"
|
|
23
|
+
|
|
24
|
+
# List of supported Ryzen AI processor series (can be extended in the future)
|
|
25
|
+
SUPPORTED_RYZEN_AI_SERIES = ["300"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_ryzenai_version_info(device=None):
|
|
29
|
+
"""
|
|
30
|
+
Centralized version detection for RyzenAI installations.
|
|
31
|
+
Uses lazy imports to avoid import errors when OGA is not installed.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
# Lazy import to avoid errors when OGA is not installed
|
|
35
|
+
from packaging.version import Version
|
|
36
|
+
|
|
37
|
+
# For embedded Python on Windows, add DLL directory before importing onnxruntime_genai
|
|
38
|
+
# This is required to find DirectML.dll and other dependencies
|
|
39
|
+
if sys.platform.startswith("win"):
|
|
40
|
+
import site
|
|
41
|
+
|
|
42
|
+
site_packages = site.getsitepackages()
|
|
43
|
+
for sp in site_packages:
|
|
44
|
+
oga_dir = os.path.join(sp, "onnxruntime_genai")
|
|
45
|
+
if os.path.exists(oga_dir):
|
|
46
|
+
# Add the onnxruntime_genai directory to DLL search path
|
|
47
|
+
# This ensures DirectML.dll and onnxruntime.dll can be found
|
|
48
|
+
os.add_dll_directory(oga_dir)
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
import onnxruntime_genai as og
|
|
52
|
+
|
|
53
|
+
if Version(og.__version__) >= Version("0.7.0"):
|
|
54
|
+
oga_path = os.path.dirname(og.__file__)
|
|
55
|
+
if og.__version__ in ("0.9.2", "0.9.2.1"):
|
|
56
|
+
return "1.6.0", oga_path
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Unsupported onnxruntime-genai-directml-ryzenai version: {og.__version__}\n"
|
|
60
|
+
"Only RyzenAI 1.6.0 is currently supported. Please upgrade:\n"
|
|
61
|
+
"pip install --upgrade lemonade-sdk[oga-ryzenai] --extra-index-url https://pypi.amd.com/simple"
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
# Legacy lemonade-install approach is no longer supported
|
|
65
|
+
raise ValueError(
|
|
66
|
+
"Legacy RyzenAI installation detected (version < 0.7.0).\n"
|
|
67
|
+
"RyzenAI 1.4.0 and 1.5.0 are no longer supported. Please upgrade to 1.6.0:\n"
|
|
68
|
+
"pip install lemonade-sdk[oga-ryzenai] --extra-index-url https://pypi.amd.com/simple"
|
|
69
|
+
)
|
|
70
|
+
except ImportError as e:
|
|
71
|
+
raise ImportError(
|
|
72
|
+
f"{e}\n Please install lemonade-sdk with "
|
|
73
|
+
"one of the oga extras, for example:\n"
|
|
74
|
+
"pip install lemonade-sdk[dev,oga-cpu]\n"
|
|
75
|
+
"See https://lemonade-server.ai/install_options.html for details"
|
|
76
|
+
) from e
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def check_ryzen_ai_processor():
|
|
80
|
+
"""
|
|
81
|
+
Checks if the current system has a supported Ryzen AI processor.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
UnsupportedPlatformError: If the processor is not a supported Ryzen AI models.
|
|
85
|
+
"""
|
|
86
|
+
if not sys.platform.startswith("win"):
|
|
87
|
+
raise UnsupportedPlatformError(
|
|
88
|
+
"Ryzen AI installation is only supported on Windows."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
skip_check = os.getenv("RYZENAI_SKIP_PROCESSOR_CHECK", "").lower() in {
|
|
92
|
+
"1",
|
|
93
|
+
"true",
|
|
94
|
+
"yes",
|
|
95
|
+
}
|
|
96
|
+
if skip_check:
|
|
97
|
+
print("[WARNING]: Processor check skipped.")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
is_supported = False
|
|
101
|
+
cpu_name = ""
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# Use PowerShell command to get processor name
|
|
105
|
+
powershell_cmd = [
|
|
106
|
+
"powershell",
|
|
107
|
+
"-ExecutionPolicy",
|
|
108
|
+
"Bypass",
|
|
109
|
+
"-Command",
|
|
110
|
+
"Get-WmiObject -Class Win32_Processor | Select-Object -ExpandProperty Name",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
result = subprocess.run(
|
|
114
|
+
powershell_cmd,
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
check=True,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Extract the CPU name from PowerShell output
|
|
121
|
+
cpu_name = result.stdout.strip()
|
|
122
|
+
if not cpu_name:
|
|
123
|
+
print(
|
|
124
|
+
"[WARNING]: Could not detect processor name. Proceeding with installation."
|
|
125
|
+
)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
# Check for any supported series
|
|
129
|
+
for series in SUPPORTED_RYZEN_AI_SERIES:
|
|
130
|
+
# Look for the series number pattern - matches any processor in the supported series
|
|
131
|
+
pattern = rf"ryzen ai.*\b{series[0]}\d{{2}}\b"
|
|
132
|
+
match = re.search(pattern, cpu_name.lower(), re.IGNORECASE)
|
|
133
|
+
|
|
134
|
+
if match:
|
|
135
|
+
is_supported = True
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
if not is_supported:
|
|
139
|
+
print(
|
|
140
|
+
f"[WARNING]: Processor '{cpu_name}' may not be officially supported for Ryzen AI hybrid execution."
|
|
141
|
+
)
|
|
142
|
+
print(
|
|
143
|
+
"[WARNING]: Installation will proceed, but hybrid features may not work correctly."
|
|
144
|
+
)
|
|
145
|
+
print("[WARNING]: Officially supported processors: Ryzen AI 300-series")
|
|
146
|
+
|
|
147
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
148
|
+
print(
|
|
149
|
+
f"[WARNING]: Could not detect processor ({e}). Proceeding with installation."
|
|
150
|
+
)
|
|
151
|
+
print("[WARNING]: Hybrid features may not work if processor is not supported.")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class UnsupportedPlatformError(Exception):
|
|
155
|
+
"""
|
|
156
|
+
Raise an exception if the hardware is not supported.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Install:
|
|
161
|
+
"""
|
|
162
|
+
Installs the necessary software for specific lemonade features.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def parser() -> argparse.ArgumentParser:
|
|
167
|
+
parser = argparse.ArgumentParser(
|
|
168
|
+
description="Installs the necessary software for specific lemonade features",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
parser.add_argument(
|
|
172
|
+
"--llamacpp",
|
|
173
|
+
help="Install llama.cpp binaries with specified backend",
|
|
174
|
+
choices=["rocm", "vulkan", "cpu"],
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
parser.add_argument(
|
|
178
|
+
"--flm",
|
|
179
|
+
action="store_true",
|
|
180
|
+
help="Install FLM (FastFlowLM) for running local language models",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return parser
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _install_llamacpp(backend):
|
|
187
|
+
"""
|
|
188
|
+
Install llama.cpp binaries with the specified backend.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
backend: The backend to use ('rocm' or 'vulkan' or 'cpu').
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
from lemonade.tools.llamacpp.utils import install_llamacpp
|
|
195
|
+
|
|
196
|
+
install_llamacpp(backend)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _install_flm():
|
|
200
|
+
"""
|
|
201
|
+
Install FLM (FastFlowLM) for running local language models.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
# Check if the processor is supported before proceeding
|
|
205
|
+
check_ryzen_ai_processor()
|
|
206
|
+
|
|
207
|
+
from lemonade.tools.flm.utils import install_flm
|
|
208
|
+
|
|
209
|
+
install_flm()
|
|
210
|
+
|
|
211
|
+
def run(
|
|
212
|
+
self,
|
|
213
|
+
llamacpp: Optional[str] = None,
|
|
214
|
+
flm: Optional[bool] = None,
|
|
215
|
+
):
|
|
216
|
+
if llamacpp is None and flm is None:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
"You must select something to install, "
|
|
219
|
+
"for example `--llamacpp` or `--flm`"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if llamacpp is not None:
|
|
223
|
+
self._install_llamacpp(llamacpp)
|
|
224
|
+
|
|
225
|
+
if flm:
|
|
226
|
+
self._install_flm()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def main():
|
|
230
|
+
installer = Install()
|
|
231
|
+
args = installer.parser().parse_args()
|
|
232
|
+
installer.run(**args.__dict__)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
main()
|
|
237
|
+
|
|
238
|
+
# This file was originally licensed under Apache 2.0. It has been modified.
|
|
239
|
+
# Modifications Copyright (c) 2025 AMD
|