lemonade-sdk 7.0.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.

Potentially problematic release.


This version of lemonade-sdk might be problematic. Click here for more details.

Files changed (61) hide show
  1. lemonade/__init__.py +5 -0
  2. lemonade/api.py +125 -0
  3. lemonade/cache.py +85 -0
  4. lemonade/cli.py +135 -0
  5. lemonade/common/__init__.py +0 -0
  6. lemonade/common/analyze_model.py +26 -0
  7. lemonade/common/build.py +223 -0
  8. lemonade/common/cli_helpers.py +139 -0
  9. lemonade/common/exceptions.py +98 -0
  10. lemonade/common/filesystem.py +368 -0
  11. lemonade/common/labels.py +61 -0
  12. lemonade/common/onnx_helpers.py +176 -0
  13. lemonade/common/plugins.py +10 -0
  14. lemonade/common/printing.py +110 -0
  15. lemonade/common/status.py +490 -0
  16. lemonade/common/system_info.py +390 -0
  17. lemonade/common/tensor_helpers.py +83 -0
  18. lemonade/common/test_helpers.py +28 -0
  19. lemonade/profilers/__init__.py +1 -0
  20. lemonade/profilers/memory_tracker.py +257 -0
  21. lemonade/profilers/profiler.py +55 -0
  22. lemonade/sequence.py +363 -0
  23. lemonade/state.py +159 -0
  24. lemonade/tools/__init__.py +1 -0
  25. lemonade/tools/adapter.py +104 -0
  26. lemonade/tools/bench.py +284 -0
  27. lemonade/tools/huggingface_bench.py +267 -0
  28. lemonade/tools/huggingface_load.py +520 -0
  29. lemonade/tools/humaneval.py +258 -0
  30. lemonade/tools/llamacpp.py +261 -0
  31. lemonade/tools/llamacpp_bench.py +154 -0
  32. lemonade/tools/management_tools.py +273 -0
  33. lemonade/tools/mmlu.py +327 -0
  34. lemonade/tools/ort_genai/__init__.py +0 -0
  35. lemonade/tools/ort_genai/oga.py +1129 -0
  36. lemonade/tools/ort_genai/oga_bench.py +142 -0
  37. lemonade/tools/perplexity.py +146 -0
  38. lemonade/tools/prompt.py +228 -0
  39. lemonade/tools/quark/__init__.py +0 -0
  40. lemonade/tools/quark/quark_load.py +172 -0
  41. lemonade/tools/quark/quark_quantize.py +439 -0
  42. lemonade/tools/report/__init__.py +0 -0
  43. lemonade/tools/report/llm_report.py +203 -0
  44. lemonade/tools/report/table.py +739 -0
  45. lemonade/tools/server/__init__.py +0 -0
  46. lemonade/tools/server/serve.py +1354 -0
  47. lemonade/tools/server/tool_calls.py +146 -0
  48. lemonade/tools/tool.py +374 -0
  49. lemonade/version.py +1 -0
  50. lemonade_install/__init__.py +1 -0
  51. lemonade_install/install.py +774 -0
  52. lemonade_sdk-7.0.0.dist-info/METADATA +116 -0
  53. lemonade_sdk-7.0.0.dist-info/RECORD +61 -0
  54. lemonade_sdk-7.0.0.dist-info/WHEEL +5 -0
  55. lemonade_sdk-7.0.0.dist-info/entry_points.txt +4 -0
  56. lemonade_sdk-7.0.0.dist-info/licenses/LICENSE +201 -0
  57. lemonade_sdk-7.0.0.dist-info/licenses/NOTICE.md +21 -0
  58. lemonade_sdk-7.0.0.dist-info/top_level.txt +3 -0
  59. lemonade_server/cli.py +260 -0
  60. lemonade_server/model_manager.py +98 -0
  61. lemonade_server/server_models.json +142 -0
@@ -0,0 +1,139 @@
1
+ import argparse
2
+ import sys
3
+ from typing import List, Dict, Tuple, Any
4
+ from lemonade.tools import Tool, FirstTool
5
+ import lemonade.common.printing as printing
6
+ from lemonade.tools.management_tools import ManagementTool
7
+
8
+
9
+ class CustomArgumentParser(argparse.ArgumentParser):
10
+
11
+ def error(self, message):
12
+ self.print_usage()
13
+ printing.log_error(message)
14
+ self.exit(2)
15
+
16
+
17
+ def _tool_list_help(tools: List[Tool], subclass, exclude=None) -> str:
18
+ help = ""
19
+
20
+ for tool_class in tools:
21
+ if exclude and issubclass(tool_class, exclude):
22
+ continue
23
+ if issubclass(tool_class, subclass):
24
+ help = (
25
+ help
26
+ + f" * {tool_class.unique_name}: {tool_class.parser().short_description}\n"
27
+ )
28
+
29
+ return help
30
+
31
+
32
+ def parse_tools(
33
+ parser: argparse.ArgumentParser, supported_tools: List[Tool], cli_name="lemonade"
34
+ ) -> Tuple[Dict[str, Any], Dict[Tool, List[str]], List[str]]:
35
+ """
36
+ Add the help for parsing tools and their args to an ArgumentParser.
37
+
38
+ Then, perform the task of parsing a full CLI command including
39
+ teasing apart the global arguments and separate tool invocations.
40
+ """
41
+
42
+ tool_parsers = {tool.unique_name: tool.parser() for tool in supported_tools}
43
+ tool_classes = {tool.unique_name: tool for tool in supported_tools}
44
+
45
+ # Sort tools into categories and format for the help menu
46
+ first_tool_choices = _tool_list_help(supported_tools, FirstTool)
47
+ eval_tool_choices = _tool_list_help(supported_tools, Tool, exclude=FirstTool)
48
+ mgmt_tool_choices = _tool_list_help(supported_tools, ManagementTool)
49
+
50
+ tools_action = parser.add_argument(
51
+ "tools",
52
+ metavar="tool --tool-args [tool --tool-args...]",
53
+ nargs="?",
54
+ help=f"""\
55
+ Run `{cli_name} TOOL -h` to learn more about each tool.
56
+
57
+ Tools that can start a sequence:
58
+ {first_tool_choices}
59
+ Tools that go into a sequence:
60
+ {eval_tool_choices}
61
+ Management tools:
62
+ {mgmt_tool_choices}""",
63
+ choices=tool_parsers.keys(),
64
+ )
65
+
66
+ # run as if "-h" was passed if no parameters are passed
67
+ if len(sys.argv) == 1:
68
+ sys.argv.append("-h")
69
+
70
+ # Break sys.argv into categories based on which tools were invoked
71
+ # Arguments that are passed prior to invoking a tool are categorized as
72
+ # global arguments that should be used to initialize the state.
73
+ current_tool = "globals"
74
+ tools_invoked = {current_tool: []}
75
+ cmd = sys.argv[1:]
76
+ while len(cmd):
77
+ if cmd[0] in tool_parsers.keys():
78
+ # Make sure each tool was only called once
79
+ if cmd[0] in tools_invoked.keys():
80
+ parser.error(
81
+ "A single call to lemonade can only invoke each tool once, "
82
+ f"however this call invokes tool {cmd[0]} multiple times."
83
+ )
84
+ current_tool = cmd.pop(0)
85
+ tools_invoked[current_tool] = []
86
+ else:
87
+ tools_invoked[current_tool].append(cmd.pop(0))
88
+
89
+ # Trick argparse into thinking tools was not a positional argument
90
+ # this helps to avoid an error where an incorrect arg/value pair
91
+ # can be misinterpreted as the tools positional argument
92
+ tools_action.option_strings = ["--tools"]
93
+
94
+ # Do one pass of parsing to figure out if -h was used
95
+ global_args = vars(parser.parse_args(tools_invoked["globals"]))
96
+
97
+ # Remove "tools" from global args because it was just there
98
+ # as a placeholder
99
+ global_args.pop("tools")
100
+
101
+ # Remove globals from the list since its already been parsed
102
+ tools_invoked.pop("globals")
103
+ evaluation_tools = []
104
+ management_tools = []
105
+ for cmd, argv in tools_invoked.items():
106
+ tool_parsers[cmd].parse_args(argv)
107
+
108
+ # Keep track of whether the tools are ManagementTool or not,
109
+ # since ManagementTools are mutually exclusive with evaluation
110
+ # tools
111
+ if issubclass(tool_classes[cmd], ManagementTool):
112
+ management_tools.append(cmd)
113
+ else:
114
+ evaluation_tools.append(cmd)
115
+
116
+ if len(management_tools) > 0 and len(evaluation_tools) > 0:
117
+ parser.error(
118
+ "This call to lemonade invoked both management and "
119
+ "evaluation tools, however each call to lemonade "
120
+ "is only allowed to invoke one or the other. "
121
+ f"Management tools: {management_tools};"
122
+ f"Evaluation tools: {evaluation_tools}."
123
+ )
124
+
125
+ if len(management_tools) == 0 and len(evaluation_tools) == 0:
126
+ parser.error(
127
+ "Calls to lemonade are required to call at least "
128
+ "one tool or management tool."
129
+ )
130
+
131
+ # Convert tool names into Tool instances
132
+ tool_instances = {tool_classes[cmd](): argv for cmd, argv in tools_invoked.items()}
133
+ evaluation_tools = [tool_classes[cmd] for cmd in evaluation_tools]
134
+
135
+ return global_args, tool_instances, evaluation_tools
136
+
137
+
138
+ # This file was originally licensed under Apache 2.0. It has been modified.
139
+ # Modifications Copyright (c) 2025 AMD
@@ -0,0 +1,98 @@
1
+ import lemonade.common.printing as printing
2
+
3
+
4
+ class Error(Exception):
5
+ """
6
+ Indicates something has gone wrong while running the tools
7
+ """
8
+
9
+ def __init__(self, msg):
10
+ super().__init__(msg)
11
+ printing.log_error(msg)
12
+
13
+
14
+ class CacheError(Error):
15
+ """
16
+ Indicates ambiguous behavior from when a build already exists in the cache,
17
+ but the model, inputs, or args have changed thereby invalidating
18
+ the cached copy of the model.
19
+ """
20
+
21
+
22
+ class EnvError(Error):
23
+ """
24
+ Indicates to the user that the required tools are not
25
+ available on their PATH.
26
+ """
27
+
28
+
29
+ class ArgError(Error):
30
+ """
31
+ Indicates to the user that they provided invalid arguments
32
+ """
33
+
34
+
35
+ class ToolError(Exception):
36
+ """
37
+ Let the user know that something went wrong while
38
+ running a tool.
39
+
40
+ Note: not overloading __init__() so that the
41
+ attempt to print to stdout isn't captured into
42
+ the Tool's log file.
43
+ """
44
+
45
+
46
+ class StateError(Exception):
47
+ """
48
+ Raised when something goes wrong with State
49
+ """
50
+
51
+
52
+ class IntakeError(Exception):
53
+ """
54
+ Let the user know that something went wrong during the
55
+ initial intake process of analyzing a model.
56
+ """
57
+
58
+
59
+ class IOError(Error):
60
+ """
61
+ Indicates to the user that an input/output operation failed,
62
+ such trying to open a file.
63
+ """
64
+
65
+
66
+ class ModelArgError(Error):
67
+ """
68
+ Indicates to the user that values provided to a Model instance method
69
+ were not allowed.
70
+ """
71
+
72
+
73
+ class ModelRuntimeError(Error):
74
+ """
75
+ Indicates to the user that attempting to invoke a Model instance failed.
76
+ """
77
+
78
+
79
+ class BenchmarkException(Exception):
80
+ """
81
+ Indicates a failure during benchmarking
82
+ """
83
+
84
+
85
+ class HardwareError(Error):
86
+ """
87
+ Indicates that the hardware used is faulty or unavailable.
88
+ """
89
+
90
+
91
+ class SkipBuild(Exception):
92
+ """
93
+ Indicates that an exception is deliberately being raised to skip a build
94
+ """
95
+
96
+
97
+ # This file was originally licensed under Apache 2.0. It has been modified.
98
+ # Modifications Copyright (c) 2025 AMD
@@ -0,0 +1,368 @@
1
+ import os
2
+ import shutil
3
+ import glob
4
+ import pathlib
5
+ from typing import Dict, List, Optional
6
+ import yaml
7
+ import lemonade.common.printing as printing
8
+ import lemonade.common.build as build
9
+ import lemonade.common.exceptions as exp
10
+
11
+ CACHE_MARKER = ".lemonadecache"
12
+ BUILD_MARKER = ".lemonadebuild"
13
+
14
+
15
+ def rmdir(folder, excludes: Optional[List[str]] = None):
16
+ """
17
+ Remove the contents of a directory from the filesystem.
18
+ If `<name>` is in `excludes`, the directory itself and the file named <name>
19
+ are kept. Otherwise, the entire directory is removed.
20
+ """
21
+
22
+ # Use an empty list by default
23
+ if excludes:
24
+ excludes_to_use = excludes
25
+ else:
26
+ excludes_to_use = []
27
+
28
+ if os.path.isdir(folder):
29
+ for filename in os.listdir(folder):
30
+ file_path = os.path.join(folder, filename)
31
+ if file_path not in excludes_to_use:
32
+ if os.path.isfile(file_path) or os.path.islink(file_path):
33
+ os.unlink(file_path)
34
+ elif os.path.isdir(file_path):
35
+ shutil.rmtree(file_path)
36
+
37
+ if excludes is None:
38
+ shutil.rmtree(folder)
39
+
40
+ return True
41
+
42
+ else:
43
+ return False
44
+
45
+
46
+ def get_all(path, exclude_path=False, file_type=build.state_file_name, recursive=True):
47
+ if recursive:
48
+ files = [
49
+ os.path.join(dp, f)
50
+ for dp, dn, filenames in os.walk(path)
51
+ for f in filenames
52
+ if file_type in f
53
+ ]
54
+ else:
55
+ files = []
56
+ dp, _, filenames = os.walk(path)
57
+ for f in filenames:
58
+ if file_type in f:
59
+ files.append(os.path.join(dp, f))
60
+
61
+ if exclude_path:
62
+ files = [os.path.basename(f) for f in files]
63
+
64
+ return files
65
+
66
+
67
+ def clean_file_name(script_path: str) -> str:
68
+ """
69
+ Trim the ".py" / ".onnx" if present.
70
+
71
+ If its a state.yaml file, trim the "_state.yaml"
72
+ """
73
+
74
+ if script_path.endswith("_" + build.state_file_name):
75
+ return pathlib.Path(script_path).stem.replace(
76
+ "_" + os.path.splitext(build.state_file_name)[0], ""
77
+ )
78
+ else:
79
+ return pathlib.Path(script_path).stem
80
+
81
+
82
+ class CacheError(exp.Error):
83
+ """
84
+ Raise this exception when the cache is being accessed incorrectly
85
+ """
86
+
87
+
88
+ def _load_yaml(file) -> Dict:
89
+ if os.path.isfile(file):
90
+ with open(file, "r", encoding="utf8") as stream:
91
+ return yaml.load(stream, Loader=yaml.FullLoader)
92
+ else:
93
+ return {}
94
+
95
+
96
+ def save_yaml(dict: Dict, file):
97
+ with open(file, "w", encoding="utf8") as outfile:
98
+ yaml.dump(dict, outfile)
99
+
100
+
101
+ def print_yaml_file(file_path, description):
102
+ if os.path.exists(file_path):
103
+ with open(file_path, "r", encoding="utf-8") as file:
104
+ printing.log_info(f"The {description} for {file_path} are:")
105
+ print(file.read())
106
+ else:
107
+ raise CacheError(
108
+ f"No {description} found at {file_path}. "
109
+ "Try running `lemonade cache --list` to see the builds in your build cache."
110
+ )
111
+
112
+
113
+ def make_cache_dir(cache_dir: str):
114
+ """
115
+ Create the build and cache directories, and put hidden files in them
116
+ to mark them as such.
117
+ """
118
+
119
+ os.makedirs(cache_dir, exist_ok=True)
120
+
121
+ # File that indicates that the directory is a cache directory
122
+ cache_file_path = os.path.join(cache_dir, CACHE_MARKER)
123
+ open(cache_file_path, mode="w", encoding="utf").close()
124
+
125
+
126
+ def make_build_dir(cache_dir: str, build_name: str):
127
+ """
128
+ Create the build and cache directories, and put hidden files in them
129
+ to mark them as such.
130
+ """
131
+ make_cache_dir(cache_dir)
132
+
133
+ build_dir = build.output_dir(cache_dir, build_name)
134
+ os.makedirs(build_dir, exist_ok=True)
135
+
136
+ # File that indicates that the directory is a build directory
137
+ build_file_path = os.path.join(build_dir, BUILD_MARKER)
138
+ open(build_file_path, mode="w", encoding="utf").close()
139
+
140
+
141
+ def check_cache_dir(cache_dir: str):
142
+ cache_file_path = os.path.join(cache_dir, CACHE_MARKER)
143
+ if not os.path.isfile(cache_file_path):
144
+ raise CacheError(
145
+ f"{cache_dir} is not a cache directory generated by Lemonade. "
146
+ "You can only clean, delete and generate reports for directories that "
147
+ "have been generated by Lemonade. Set a different --cache-dir before "
148
+ "trying again."
149
+ )
150
+
151
+
152
+ def is_build_dir(cache_dir: str, build_name: str):
153
+ build_dir = build.output_dir(cache_dir, build_name)
154
+ build_file_path = os.path.join(build_dir, BUILD_MARKER)
155
+ return os.path.isfile(build_file_path)
156
+
157
+
158
+ def clean_output_dir(cache_dir: str, build_name: str) -> None:
159
+ """
160
+ Delete all elements of the output directory that are not human readable
161
+ """
162
+ output_dir = build.output_dir(cache_dir, build_name)
163
+ if os.path.isdir(output_dir) and is_build_dir(cache_dir, build_name):
164
+ output_dir = os.path.expanduser(output_dir)
165
+ else:
166
+ raise CacheError(f"No build found at {output_dir}")
167
+
168
+ # Remove files that do not have an allowed extension
169
+ allowed_extensions = (".txt", ".out", ".yaml", ".json", ".png")
170
+ all_paths = glob.glob(f"{output_dir}/**/*", recursive=True)
171
+ for path in all_paths:
172
+ if os.path.isfile(path) and not path.endswith(allowed_extensions):
173
+ os.remove(path)
174
+
175
+ # Remove all empty folders
176
+ for path in all_paths:
177
+ if os.path.isdir(path):
178
+ if len(os.listdir(path)) == 0:
179
+ shutil.rmtree(path)
180
+
181
+
182
+ def get_available_builds(cache_dir):
183
+ """
184
+ Get all of the build directories within the build cache
185
+ located at `cache_dir`
186
+ """
187
+
188
+ check_cache_dir(cache_dir)
189
+
190
+ builds = [
191
+ pathlib.PurePath(build_name).name
192
+ for build_name in os.listdir(os.path.abspath(build.builds_dir(cache_dir)))
193
+ if os.path.isdir(build.output_dir(cache_dir, build_name))
194
+ and is_build_dir(cache_dir, build_name)
195
+ ]
196
+ builds.sort()
197
+
198
+ return builds
199
+
200
+
201
+ class Keys:
202
+ # Number of parameters in the model
203
+ PARAMETERS = "parameters"
204
+ # List of all build tools in the Sequence
205
+ SELECTED_SEQUENCE_OF_TOOLS = "selected_sequence_of_tools"
206
+ # MeasuredPerformance data for a benchmarked workload
207
+ PERFORMANCE = "performance"
208
+ # Runtime used for the benchmark
209
+ RUNTIME = "runtime"
210
+ # Type of device used for the benchmark (e.g., "x86")
211
+ DEVICE_TYPE = "device_type"
212
+ # Specific device used for the benchmark
213
+ DEVICE = "device"
214
+ # Name of the model
215
+ MODEL_NAME = "model_name"
216
+ # Number of iterations used in benchmarking
217
+ ITERATIONS = "iterations"
218
+ # System information to keep track of DUT
219
+ SYSTEM_INFO = "system_info"
220
+ # Indicates status of the most recent build tool run: FunctionStatus
221
+ BUILD_STATUS = "build_status"
222
+ # Prefix for reporting the execution duration of a tool
223
+ # In the report this will look like tool_duration:TOOL_NAME
224
+ TOOL_DURATION = "tool_duration"
225
+ # Prefix for reporting the peak working memory in the build through this tool
226
+ # In the report this will look like tool_memory:TOOL_NAME
227
+ TOOL_MEMORY = "tool_memory"
228
+ # Prefix for reporting the execution status of a tool
229
+ # In the report this will look like tool_status:TOOL_NAME
230
+ TOOL_STATUS = "tool_status"
231
+ # Records the date and time of the evaluation after analysis but before
232
+ # build and benchmark
233
+ TIMESTAMP = "timestamp"
234
+ # Records the logfile of any failed tool/benchmark
235
+ ERROR_LOG = "error_log"
236
+ # Name of the build in the cache
237
+ BUILD_NAME = "build_name"
238
+ # Sequence of tools used for this build, along with their args
239
+ SEQUENCE_INFO = "sequence_info"
240
+ # Version of Lemonade used for the build
241
+ LEMONADE_VERSION = "lemonade_version"
242
+ # Unique ID for this build
243
+ UID = "uid"
244
+ # Directory where the lemonade build cache is stored
245
+ CACHE_DIR = "cache_dir"
246
+ # Example inputs to the model
247
+ INPUTS = "inputs"
248
+ # Path to the file containing the memory usage plot
249
+ MEMORY_USAGE_PLOT = "memory_usage_plot"
250
+ # Average of all tested MMLU subject scores
251
+ AVERAGE_MMLU_ACCURACY = "average_mmlu_accuracy"
252
+
253
+
254
+ def _clean_logfile(logfile_lines: List[str]) -> List[str]:
255
+ """
256
+ Remove the whitespace and empty lines from an array of logfile lines
257
+ """
258
+ return "\n".join([line.rstrip() for line in logfile_lines if line.rstrip()])
259
+
260
+
261
+ def stats_file(cache_dir: str, build_name: str):
262
+ """
263
+ Returns the expected location of the lemonade stats file
264
+ """
265
+ dir = build.output_dir(cache_dir, build_name)
266
+ return os.path.join(dir, "lemonade_stats.yaml")
267
+
268
+
269
+ class Stats:
270
+ def __init__(self, cache_dir: str, build_name: str):
271
+ self.file = stats_file(cache_dir, build_name)
272
+
273
+ os.makedirs(os.path.dirname(self.file), exist_ok=True)
274
+ if not os.path.exists(self.file):
275
+ # Start an empty stats file
276
+ save_yaml({}, self.file)
277
+
278
+ @property
279
+ def stats(self):
280
+ return _load_yaml(self.file)
281
+
282
+ def _set_key(self, dict, keys: List["str"], value):
283
+ """
284
+ Recursive approach to safely setting a key within any level of hierarchy
285
+ in a dictionary. If a parent key of the desired key does not exist, create
286
+ it and set it with an empty dictionary before proceeding.
287
+
288
+ The end result is: dict[keys[0]][keys[1]]...[keys[-1]] = value
289
+ """
290
+ if len(keys) == 1:
291
+ dict[keys[0]] = value
292
+
293
+ else:
294
+ if keys[0] not in dict.keys():
295
+ dict[keys[0]] = {}
296
+
297
+ self._set_key(dict[keys[0]], keys[1:], value)
298
+
299
+ def save_stat(self, key: str, value):
300
+ """
301
+ Save statistics to an yaml file in the build directory
302
+ """
303
+
304
+ stats_dict = self.stats
305
+
306
+ self._set_key(stats_dict, [key], value)
307
+
308
+ save_yaml(stats_dict, self.file)
309
+
310
+ def save_sub_stat(self, parent_key: str, key: str, value):
311
+ stats_dict = self.stats
312
+
313
+ self._set_key(stats_dict, [parent_key, key], value)
314
+
315
+ save_yaml(stats_dict, self.file)
316
+
317
+ def save_eval_error_log(self, logfile_path):
318
+ if logfile_path is None:
319
+ # Avoid an error in the situation where we crashed before
320
+ # initializing the tool (in which case it has no logfile path yet)
321
+ return
322
+ if os.path.exists(logfile_path):
323
+ with open(logfile_path, "r", encoding="utf-8") as f:
324
+ full_log = f.readlines()
325
+
326
+ # Log files can be quite large, so we will just record the beginning
327
+ # and ending lines. Users can always open the log file if they
328
+ # want to see the full log.
329
+ start_cutoff = 5
330
+ end_cutoff = -30
331
+ max_full_length = start_cutoff + abs(end_cutoff)
332
+
333
+ if len(full_log) > max_full_length:
334
+ log_start = _clean_logfile(full_log[:start_cutoff])
335
+ log_end = _clean_logfile(full_log[end_cutoff:])
336
+ truncation_notice = (
337
+ "NOTICE: This copy of the log has been truncated to the first "
338
+ f"{start_cutoff} and last {abs(end_cutoff)} lines "
339
+ f"to save space. Please see {logfile_path} "
340
+ "to see the full log.\n"
341
+ )
342
+
343
+ stats_log = log_start + truncation_notice + log_end
344
+ else:
345
+ stats_log = _clean_logfile(full_log)
346
+
347
+ self.save_stat(Keys.ERROR_LOG, stats_log)
348
+
349
+
350
+ def expand_inputs(input_paths: List[str]) -> List[str]:
351
+ """
352
+ Convert regular expressions in input paths
353
+ into full file/dir paths (e.g., [*.py] -> [a.py, b.py] )
354
+
355
+ This makes up for Windows not resolving wildcards on the command line
356
+ """
357
+ input_paths_expanded = sum(
358
+ [glob.glob(f) for f in input_paths if "::" not in f], []
359
+ ) + [f for f in input_paths if "::" in f]
360
+
361
+ if not input_paths_expanded:
362
+ raise exp.ArgError("No files that match your inputs could be found.")
363
+
364
+ return input_paths_expanded
365
+
366
+
367
+ # This file was originally licensed under Apache 2.0. It has been modified.
368
+ # Modifications Copyright (c) 2025 AMD
@@ -0,0 +1,61 @@
1
+ from typing import Dict, List
2
+ import turnkeyml.common.printing as printing
3
+
4
+
5
+ def to_dict(label_list: List[str]) -> Dict[str, List[str]]:
6
+ """
7
+ Convert label list into a dictionary of labels
8
+ """
9
+ label_dict = {}
10
+ for item in label_list:
11
+ try:
12
+ label_key, label_value = item.split("::")
13
+ label_value = label_value.split(",")
14
+ label_dict[label_key] = label_value
15
+ except ValueError:
16
+ printing.log_warning(
17
+ (
18
+ f"Malformed label {item} found. "
19
+ "Each label must have the format key::value1,value2,... "
20
+ )
21
+ )
22
+ return label_dict
23
+
24
+
25
+ def load_from_file(file_path: str) -> Dict[str, List[str]]:
26
+ """
27
+ This function extracts labels from a Python file.
28
+ Labels must be in the first line of a Python file and start with "# labels: "
29
+ Each label must have the format "key::value1,value2,..."
30
+
31
+ Example:
32
+ "# labels: author::google test_group::daily,monthly"
33
+ """
34
+ # Open file
35
+ with open(file_path, encoding="utf-8") as f:
36
+ first_line = f.readline()
37
+
38
+ # Return label dict
39
+ if "# labels:" in first_line:
40
+ label_list = first_line.replace("\n", "").split(" ")[2:]
41
+ return to_dict(label_list)
42
+ else:
43
+ return {}
44
+
45
+
46
+ def is_subset(label_dict_a: Dict[str, List[str]], label_dict_b: Dict[str, List[str]]):
47
+ """
48
+ This function returns True if label_dict_a is a subset of label_dict_b.
49
+ More specifically, we return True if:
50
+ * All keys of label_dict_a are also keys of label_dict_b AND,
51
+ * All values of label_dict_a[key] are values of label_dict_b[key]
52
+ """
53
+ for key in label_dict_a:
54
+ # Skip benchmarking if the label_dict_a key is not a key of label_dict_b
55
+ if key not in label_dict_b:
56
+ return False
57
+ # A label key may point to multiple label values
58
+ # Skip if not all values of label_dict_a[key] are in label_dict_b[key]
59
+ elif not all(elem in label_dict_a[key] for elem in label_dict_b[key]):
60
+ return False
61
+ return True