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.
Files changed (84) hide show
  1. lemonade/__init__.py +5 -0
  2. lemonade/api.py +180 -0
  3. lemonade/cache.py +92 -0
  4. lemonade/cli.py +173 -0
  5. lemonade/common/__init__.py +0 -0
  6. lemonade/common/build.py +176 -0
  7. lemonade/common/cli_helpers.py +139 -0
  8. lemonade/common/exceptions.py +98 -0
  9. lemonade/common/filesystem.py +368 -0
  10. lemonade/common/inference_engines.py +408 -0
  11. lemonade/common/network.py +93 -0
  12. lemonade/common/printing.py +110 -0
  13. lemonade/common/status.py +471 -0
  14. lemonade/common/system_info.py +1411 -0
  15. lemonade/common/test_helpers.py +28 -0
  16. lemonade/profilers/__init__.py +1 -0
  17. lemonade/profilers/agt_power.py +437 -0
  18. lemonade/profilers/hwinfo_power.py +429 -0
  19. lemonade/profilers/memory_tracker.py +259 -0
  20. lemonade/profilers/profiler.py +58 -0
  21. lemonade/sequence.py +363 -0
  22. lemonade/state.py +159 -0
  23. lemonade/tools/__init__.py +1 -0
  24. lemonade/tools/accuracy.py +432 -0
  25. lemonade/tools/adapter.py +114 -0
  26. lemonade/tools/bench.py +302 -0
  27. lemonade/tools/flm/__init__.py +1 -0
  28. lemonade/tools/flm/utils.py +305 -0
  29. lemonade/tools/huggingface/bench.py +187 -0
  30. lemonade/tools/huggingface/load.py +235 -0
  31. lemonade/tools/huggingface/utils.py +359 -0
  32. lemonade/tools/humaneval.py +264 -0
  33. lemonade/tools/llamacpp/bench.py +255 -0
  34. lemonade/tools/llamacpp/load.py +222 -0
  35. lemonade/tools/llamacpp/utils.py +1260 -0
  36. lemonade/tools/management_tools.py +319 -0
  37. lemonade/tools/mmlu.py +319 -0
  38. lemonade/tools/oga/__init__.py +0 -0
  39. lemonade/tools/oga/bench.py +120 -0
  40. lemonade/tools/oga/load.py +804 -0
  41. lemonade/tools/oga/migration.py +403 -0
  42. lemonade/tools/oga/utils.py +462 -0
  43. lemonade/tools/perplexity.py +147 -0
  44. lemonade/tools/prompt.py +263 -0
  45. lemonade/tools/report/__init__.py +0 -0
  46. lemonade/tools/report/llm_report.py +203 -0
  47. lemonade/tools/report/table.py +899 -0
  48. lemonade/tools/server/__init__.py +0 -0
  49. lemonade/tools/server/flm.py +133 -0
  50. lemonade/tools/server/llamacpp.py +320 -0
  51. lemonade/tools/server/serve.py +2123 -0
  52. lemonade/tools/server/static/favicon.ico +0 -0
  53. lemonade/tools/server/static/index.html +279 -0
  54. lemonade/tools/server/static/js/chat.js +1059 -0
  55. lemonade/tools/server/static/js/model-settings.js +183 -0
  56. lemonade/tools/server/static/js/models.js +1395 -0
  57. lemonade/tools/server/static/js/shared.js +556 -0
  58. lemonade/tools/server/static/logs.html +191 -0
  59. lemonade/tools/server/static/styles.css +2654 -0
  60. lemonade/tools/server/static/webapp.html +321 -0
  61. lemonade/tools/server/tool_calls.py +153 -0
  62. lemonade/tools/server/tray.py +664 -0
  63. lemonade/tools/server/utils/macos_tray.py +226 -0
  64. lemonade/tools/server/utils/port.py +77 -0
  65. lemonade/tools/server/utils/thread.py +85 -0
  66. lemonade/tools/server/utils/windows_tray.py +408 -0
  67. lemonade/tools/server/webapp.py +34 -0
  68. lemonade/tools/server/wrapped_server.py +559 -0
  69. lemonade/tools/tool.py +374 -0
  70. lemonade/version.py +1 -0
  71. lemonade_install/__init__.py +1 -0
  72. lemonade_install/install.py +239 -0
  73. lemonade_sdk-9.1.1.dist-info/METADATA +276 -0
  74. lemonade_sdk-9.1.1.dist-info/RECORD +84 -0
  75. lemonade_sdk-9.1.1.dist-info/WHEEL +5 -0
  76. lemonade_sdk-9.1.1.dist-info/entry_points.txt +5 -0
  77. lemonade_sdk-9.1.1.dist-info/licenses/LICENSE +201 -0
  78. lemonade_sdk-9.1.1.dist-info/licenses/NOTICE.md +47 -0
  79. lemonade_sdk-9.1.1.dist-info/top_level.txt +3 -0
  80. lemonade_server/cli.py +805 -0
  81. lemonade_server/model_manager.py +758 -0
  82. lemonade_server/pydantic_models.py +159 -0
  83. lemonade_server/server_models.json +643 -0
  84. 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