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,257 @@
1
+ import os
2
+ import time
3
+ import textwrap
4
+ from multiprocessing import Process, Queue
5
+ import matplotlib.pyplot as plt
6
+ import psutil
7
+ import yaml
8
+ import lemonade.common.filesystem as fs
9
+ import lemonade.common.printing as printing
10
+ from lemonade.profilers import Profiler
11
+
12
+
13
+ DEFAULT_TRACK_MEMORY_INTERVAL = 0.25
14
+ MEMORY_USAGE_YAML_FILENAME = "memory_usage.yaml"
15
+ MEMORY_USAGE_PNG_FILENAME = "memory_usage.png"
16
+
17
+
18
+ class MemoryTracker(Profiler):
19
+
20
+ unique_name = "memory"
21
+
22
+ @staticmethod
23
+ def add_arguments_to_parser(parser):
24
+ parser.add_argument(
25
+ "-m",
26
+ f"--{MemoryTracker.unique_name}",
27
+ nargs="?",
28
+ metavar="TRACK_INTERVAL",
29
+ type=float,
30
+ default=None,
31
+ const=DEFAULT_TRACK_MEMORY_INTERVAL,
32
+ help="Track memory usage and plot the results. "
33
+ "Optionally, set the tracking interval in seconds "
34
+ f"(default: {DEFAULT_TRACK_MEMORY_INTERVAL})",
35
+ )
36
+
37
+ @staticmethod
38
+ def get_time_mem_list(process):
39
+ return [time.time(), process.memory_info().rss]
40
+
41
+ def __init__(self, parser_arg_value):
42
+ super().__init__()
43
+ self.status_stats += [fs.Keys.MEMORY_USAGE_PLOT]
44
+ self.track_memory_interval = parser_arg_value
45
+ self.process_being_tracked = None
46
+ self.build_dir = None
47
+ self.queue = None
48
+ self.tracker_process = None
49
+ self.tracking_active = False
50
+ self.yaml_path = None
51
+
52
+ def start(self, build_dir):
53
+ if self.tracking_active:
54
+ raise RuntimeError("Cannot start tracking while already tracking")
55
+
56
+ # Save the folder where data and plot will be stored
57
+ self.build_dir = build_dir
58
+
59
+ # Get the process being tracked
60
+ track_pid = os.getpid()
61
+ self.process_being_tracked = psutil.Process(track_pid)
62
+
63
+ # Create queue for passing messages to the tracker
64
+ self.queue = Queue()
65
+
66
+ # The yaml file where the memory usage data will be saved
67
+ self.yaml_path = os.path.join(self.build_dir, MEMORY_USAGE_YAML_FILENAME)
68
+
69
+ # Create process to continuously sample memory usage
70
+ self.tracker_process = Process(
71
+ target=self._memory_tracker_,
72
+ args=(
73
+ track_pid,
74
+ self.queue,
75
+ self.yaml_path,
76
+ self.track_memory_interval,
77
+ ),
78
+ )
79
+ self.tracker_process.start()
80
+ self.tracking_active = True
81
+ self.set_label("start")
82
+ self.sample()
83
+
84
+ def tool_starting(self, tool_name):
85
+ self.set_label(tool_name)
86
+
87
+ def tool_stopping(self):
88
+ self.sample()
89
+
90
+ def set_label(self, label):
91
+ if self.tracking_active:
92
+ self.queue.put(label)
93
+
94
+ def sample(self):
95
+ if self.tracking_active:
96
+ self.queue.put(MemoryTracker.get_time_mem_list(self.process_being_tracked))
97
+
98
+ def stop(self):
99
+ if self.tracking_active:
100
+ self.queue.put(None)
101
+ self.tracking_active = False
102
+
103
+ def generate_results(self, state, timestamp, _):
104
+ if self.tracker_process is None:
105
+ return
106
+
107
+ if self.tracking_active:
108
+ self.stop()
109
+
110
+ # Wait for memory tracker to finish writing yaml data file
111
+ while self.tracker_process.is_alive():
112
+ self.tracker_process.join(timeout=1.0)
113
+
114
+ try:
115
+ with open(self.yaml_path, "r", encoding="utf-8") as f:
116
+ memory_tracks = yaml.safe_load(f)
117
+ except FileNotFoundError as e:
118
+ printing.log_info(
119
+ f"Memory tracker file not found: {e.filename}. No memory usage plot generated"
120
+ )
121
+ state.save_stat(fs.Keys.MEMORY_USAGE_PLOT, None)
122
+ return
123
+
124
+ # Add check to ensure that memory_tracks is not empty or improperly formatted
125
+ if not memory_tracks or not isinstance(memory_tracks, list):
126
+ printing.log_info(
127
+ f"Memory tracker file contains no data or is improperly formatted: {self.yaml_path}"
128
+ )
129
+ state.save_stat(fs.Keys.MEMORY_USAGE_PLOT, None)
130
+ return
131
+
132
+ # Find final time in the start track (first track) to subtract from all other times
133
+ _, track = memory_tracks[0]
134
+ t0 = track[-1][0]
135
+
136
+ # last_t and last_y are used to draw a line between the last point of the prior
137
+ # track and the first point of the current track
138
+ last_t = None
139
+ last_y = None
140
+
141
+ plt.figure()
142
+ for k, v in memory_tracks[1:]:
143
+ if len(v) > 0:
144
+ t = [x[0] - t0 for x in v]
145
+ y = [float(x[1]) / 1024**3 for x in v]
146
+ # draw new memory usage track
147
+ if last_t is not None:
148
+ plt.plot([last_t] + t, [last_y] + y, label=k, marker=".")
149
+ else:
150
+ plt.plot(t, y, label=k, marker=".")
151
+ last_t = t[-1]
152
+ last_y = y[-1]
153
+ plt.xlabel("Time (sec)")
154
+ plt.ylabel("GB")
155
+ title_str = "Physical Memory Usage\n" + "\n".join(
156
+ textwrap.wrap(state.build_name, 60)
157
+ )
158
+ plt.title(title_str)
159
+ plt.legend()
160
+ plt.grid()
161
+ plt.tight_layout()
162
+
163
+ # Save plot to cache and to current folder
164
+ plot_path = os.path.join(
165
+ self.build_dir, f"{timestamp}_{MEMORY_USAGE_PNG_FILENAME}"
166
+ )
167
+ plt.savefig(plot_path)
168
+ plot_path = os.path.join(
169
+ os.getcwd(), f"{timestamp}_{MEMORY_USAGE_PNG_FILENAME}"
170
+ )
171
+ plt.savefig(plot_path)
172
+ state.save_stat(fs.Keys.MEMORY_USAGE_PLOT, plot_path)
173
+
174
+ @staticmethod
175
+ def _memory_tracker_(
176
+ tracked_pid,
177
+ input_queue: Queue,
178
+ yaml_path: str,
179
+ track_memory_interval: float,
180
+ ):
181
+ """
182
+ Tracks memory usage during build and saves to yaml file
183
+ The build communicates with the tracker though the input_queue. It may pass:
184
+ 1) string - This is to indicate that a new track is starting and the string is the label
185
+ for the next segment. The tracker will automatically track memory usage at
186
+ the track_memory_interval once a first track_name is given to it.
187
+ 2) list - A time and a current memory usage value that is added to the current track
188
+ (typically used at the end of a segment to make sure that each segment is
189
+ sampled at least once
190
+ 3) None - This indicates that the tracker should stop tracking, save its data to a file
191
+ and end
192
+ """
193
+ memory_tracks = []
194
+ current_track = []
195
+ track_name = None
196
+ tracker_exit = False
197
+
198
+ try:
199
+ tracked_process = psutil.Process(tracked_pid)
200
+ while (
201
+ not tracker_exit and tracked_process.status() == psutil.STATUS_RUNNING
202
+ ):
203
+
204
+ time.sleep(track_memory_interval)
205
+
206
+ # Read any messages from the parent process
207
+ while not input_queue.empty():
208
+ try:
209
+ message = input_queue.get(timeout=0.001)
210
+ if message is None or isinstance(message, str):
211
+ # Save current track.
212
+ if track_name is not None:
213
+ memory_tracks.append([track_name, current_track])
214
+ track_name = message
215
+ current_track = []
216
+ if message is None:
217
+ # Wrap up
218
+ tracker_exit = True
219
+ break
220
+ elif isinstance(message, list):
221
+ # Add time and memory data to current track
222
+ if track_name is not None:
223
+ current_track.append(message)
224
+ else:
225
+ raise TypeError(
226
+ "Track name must be passed to memory tracker prior to "
227
+ "sending data"
228
+ )
229
+ else:
230
+ raise TypeError(
231
+ "Unrecognized message type in memory_tracker input queue: "
232
+ f"{message}"
233
+ )
234
+
235
+ except input_queue.Empty:
236
+ # input_queue.empty had not been updated
237
+ pass
238
+
239
+ if not tracker_exit and track_name is not None:
240
+ # Save current time and memory usage
241
+ current_track.append(
242
+ MemoryTracker.get_time_mem_list(tracked_process)
243
+ )
244
+
245
+ # Save the collected memory tracks
246
+ with open(yaml_path, "w", encoding="utf-8") as f:
247
+ yaml.dump(memory_tracks, f)
248
+
249
+ except psutil.NoSuchProcess:
250
+ # If the parent process stopped existing, we can
251
+ # safely assume that tracking is no longer needed
252
+ # NOTE: this only seems to be needed on Windows
253
+ pass
254
+
255
+
256
+ # This file was originally licensed under Apache 2.0. It has been modified.
257
+ # Modifications Copyright (c) 2025 AMD
@@ -0,0 +1,55 @@
1
+ import abc
2
+
3
+
4
+ class Profiler(abc.ABC):
5
+
6
+ unique_name: str
7
+
8
+ def __init__(self, parser_arg_value=None):
9
+ self.parser_arg_value = parser_arg_value
10
+ # Statistics that will be displayed to the CLI user
11
+ self.status_stats = []
12
+
13
+ @staticmethod
14
+ @abc.abstractmethod
15
+ def add_arguments_to_parser(parser):
16
+ """
17
+ Adds the argument parsing for this tool to the parser.
18
+ Uses f"--{self.unique_name}" as the argument.
19
+ """
20
+
21
+ @abc.abstractmethod
22
+ def start(self, build_dir):
23
+ """
24
+ This method is called prior to the tool sequence starting.
25
+ This informs the profiler to start gathering data.
26
+ The build directory can be used to store profiling data.
27
+ """
28
+
29
+ def tool_starting(self, tool_name):
30
+ """
31
+ This method is called to inform the profiler of the name of the tool that is about to start.
32
+ """
33
+
34
+ def tool_stopping(self):
35
+ """
36
+ This method is called to inform the profiler that the tool has finished.
37
+ """
38
+
39
+ def stop(self):
40
+ """
41
+ This method is called when the tool sequence has finished.
42
+ This informs the profiler to stop gathering data.
43
+ """
44
+
45
+ @abc.abstractmethod
46
+ def generate_results(self, state, timestamp, start_times):
47
+ """
48
+ This method is called so that the profiler can create its output files.
49
+ The state is passed so that build info can be gathered and stats can be written.
50
+ The timestamp can be used for filename in current working directory.
51
+ The start times contain a list of tools and start times.
52
+ """
53
+
54
+
55
+ # Copyright (c) 2025 AMD
lemonade/sequence.py ADDED
@@ -0,0 +1,363 @@
1
+ import sys
2
+ import time
3
+ import os
4
+ import platform
5
+ import copy
6
+ from datetime import datetime
7
+ from typing import List, Dict, Optional
8
+ import pytz
9
+ import psutil
10
+ import lemonade.common.printing as printing
11
+ import lemonade.common.exceptions as exp
12
+ import lemonade.common.build as build
13
+ from lemonade.common.system_info import get_system_info_dict
14
+ import lemonade.common.filesystem as fs
15
+ import lemonade.common.status as status
16
+ from lemonade.tools.tool import Tool
17
+ from lemonade.profilers.profiler import Profiler
18
+ from lemonade.state import State
19
+
20
+
21
+ def _rewind_stdout(lines: int = 1):
22
+ """
23
+ Helper function for the command line monitor. Moves the cursor up a
24
+ certain number of lines in the terminal, corresponding to the
25
+ status line for a Tool, so that we can update the status of
26
+ that Tool.
27
+ """
28
+ rewind_stdout_one_line = "\033[1A"
29
+ rewind_multiple_lines = rewind_stdout_one_line * lines
30
+ print(rewind_multiple_lines, end="")
31
+ sys.stdout.flush()
32
+
33
+
34
+ class Sequence:
35
+ """
36
+ Helper class to launch and manage build tools.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ tools: Dict[Tool, List[str]],
42
+ profilers: List[Profiler] = None,
43
+ ):
44
+
45
+ self.tools = tools
46
+ self.profilers = [] if profilers is None else profilers
47
+
48
+ # Make sure all the tool names are unique
49
+ self.tool_names = [tool.__class__.unique_name for tool in self.tools.keys()]
50
+
51
+ if len(self.tool_names) != len(set(self.tool_names)):
52
+ msg = f"""
53
+ All tools in a Sequence must have unique unique_names, however Sequence
54
+ received duplicates in the list of names: {self.tool_names}
55
+ """
56
+ raise ValueError(msg)
57
+
58
+ # Save the process (used to get memory usage)
59
+ self.process = psutil.Process()
60
+
61
+ def show_monitor(self, state: State, verbosity: bool):
62
+ """
63
+ Displays the monitor on the terminal. The purpose of the monitor
64
+ is to show the status of each tool (success, failure, not started yet,
65
+ or in-progress).
66
+ """
67
+
68
+ if verbosity:
69
+ print()
70
+
71
+ printing.logn(
72
+ f'Building "{state.build_name}"',
73
+ c=printing.Colors.BOLD,
74
+ )
75
+
76
+ for tool in self.tools:
77
+ tool.status_line(successful=None, verbosity=True)
78
+
79
+ _rewind_stdout(len(self.tools))
80
+
81
+ def _advance_cursor(self, current_tool_name: str):
82
+ # Advance the cursor below the monitor so
83
+ # we can print a message
84
+ tool_depth_in_sequence = len(self.tool_names) - self.tool_names.index(
85
+ current_tool_name
86
+ )
87
+ stdout_lines_to_advance = tool_depth_in_sequence - 2
88
+ cursor_down = "\n" * stdout_lines_to_advance
89
+
90
+ print(cursor_down)
91
+
92
+ def _get_mem_usage_str(self) -> str:
93
+ """
94
+ Returns a string with memory usage for the current process
95
+ (non-swapped physical memory). In Windows OS, the peak memory used in the
96
+ process is also included.
97
+
98
+ Example: '1.100 GB (1.638 GB peak)'
99
+ """
100
+ mem_info = self.process.memory_info()
101
+ mem_info_str = f"{mem_info.rss / 1024 ** 3:,.3f} GB"
102
+ if platform.system() == "Windows":
103
+ mem_info_str += f" ({mem_info.peak_wset / 1024 ** 3:,.3f} GB peak)"
104
+ return mem_info_str
105
+
106
+ def launch(
107
+ self,
108
+ state: State,
109
+ lean_cache: bool = False,
110
+ monitor: Optional[bool] = None,
111
+ stats_to_save: Optional[Dict] = None,
112
+ ) -> State:
113
+ """
114
+ Executes the sequence of tools.
115
+ """
116
+
117
+ current_time = datetime.now()
118
+ timestamp = current_time.strftime("%Y-%m-%d-%H%M%S")
119
+ start_times = {"warmup": time.time()}
120
+
121
+ # Allow monitor to be globally disabled by an environment variable
122
+ if monitor is None:
123
+ if os.environ.get("LEMONADE_BUILD_MONITOR") == "False":
124
+ monitor_setting = False
125
+ else:
126
+ monitor_setting = True
127
+ else:
128
+ monitor_setting = monitor
129
+
130
+ # Create a build directory in the cache
131
+ fs.make_build_dir(state.cache_dir, state.build_name)
132
+
133
+ # Start profilers
134
+ build_dir = build.output_dir(state.cache_dir, state.build_name)
135
+ for profiler in self.profilers:
136
+ profiler.start(build_dir)
137
+
138
+ self.show_monitor(state, monitor_setting)
139
+
140
+ if state.build_status == build.FunctionStatus.SUCCESSFUL:
141
+ msg = """
142
+ build_model() is running a build on a model that already built successfully, which
143
+ should not happen because the build should have loaded from cache or rebuilt from scratch.
144
+ If you are using custom tools and Sequences then you have some debugging to do. Otherwise,
145
+ please file an issue at https://github.com/lemonade-sdk/lemonade/issues
146
+ """
147
+ raise exp.Error(msg)
148
+
149
+ # Keep a copy of any stats we loaded from disk, in case we need to
150
+ # restore them later
151
+ saved_stats = copy.deepcopy(fs.Stats(state.cache_dir, state.build_name).stats)
152
+
153
+ # Save build name to stats so it shows up on reports
154
+ state.save_stat(fs.Keys.BUILD_NAME, state.build_name)
155
+
156
+ # Indicate that the build is running. If the build fails for any reason,
157
+ # we will try to catch the exception and note it in the stats.
158
+ # If a concluded build still has a status of "running", this means
159
+ # there was an uncaught exception.
160
+ state.save_stat(fs.Keys.BUILD_STATUS, build.FunctionStatus.INCOMPLETE)
161
+
162
+ # Save a timestamp so that we know the order of builds within a cache
163
+ pacific_tz = pytz.timezone("America/Los_Angeles")
164
+ state.save_stat(
165
+ fs.Keys.TIMESTAMP,
166
+ datetime.now(pacific_tz),
167
+ )
168
+
169
+ # Save the system information used for this build
170
+ system_info = get_system_info_dict()
171
+ state.save_stat(
172
+ fs.Keys.SYSTEM_INFO,
173
+ system_info,
174
+ )
175
+
176
+ # Collect telemetry for the build
177
+ state.save_stat(
178
+ fs.Keys.SELECTED_SEQUENCE_OF_TOOLS,
179
+ self.tool_names,
180
+ )
181
+
182
+ # At the beginning of a sequence no tool has started
183
+ for tool in self.tools:
184
+ state.save_stat(tool.status_key, build.FunctionStatus.NOT_STARTED)
185
+ state.save_stat(tool.duration_key, "-")
186
+ state.save_stat(tool.memory_key, "-")
187
+
188
+ # Save any additional stats passed in via arguments
189
+ if stats_to_save:
190
+ for stat_key, stat_value in stats_to_save.items():
191
+ state.save_stat(stat_key, stat_value)
192
+
193
+ # Save initial memory as a build statistic
194
+ state.save_stat(f"{fs.Keys.TOOL_MEMORY}:__init__", self._get_mem_usage_str())
195
+
196
+ # Run the build
197
+ saved_exception = None
198
+ for tool, argv in self.tools.items():
199
+
200
+ start_time = time.time()
201
+ start_times[tool.unique_name] = start_time
202
+
203
+ # Inform profiler of name of tool about to start
204
+ for profiler in self.profilers:
205
+ profiler.tool_starting(tool.unique_name)
206
+
207
+ try:
208
+
209
+ # Set status as incomplete, since tool just started
210
+ state.save_stat(tool.status_key, build.FunctionStatus.INCOMPLETE)
211
+
212
+ # Collect telemetry about the tool
213
+ state.current_build_tool = tool.unique_name
214
+
215
+ # Run the tool
216
+ state = tool.parse_and_run(state, argv, monitor_setting)
217
+
218
+ # Save the state so that it can be assessed for a cache hit
219
+ state.save()
220
+
221
+ except exp.SkipBuild as e:
222
+ # SkipBuild is a special exception, which means that a build
223
+ # was loaded from disk, then we realized we want to skip it.
224
+ # In order to preserve the original stats and state of the build,
225
+ # we need to restore the stats file to what it was at the beginning
226
+ # of this function call. We also need to avoid calling state.save().
227
+
228
+ # Restore the prior stats
229
+ fs.save_yaml(
230
+ saved_stats, fs.Stats(state.cache_dir, state.build_name).file
231
+ )
232
+
233
+ # Advance the cursor below the monitor so
234
+ # we can print a message
235
+ self._advance_cursor(tool.unique_name)
236
+ printing.log_warning(str(e))
237
+ return
238
+
239
+ # Broad exception is desirable as we want to capture
240
+ # all exceptions (including those we can't anticipate)
241
+ except Exception as e: # pylint: disable=broad-except
242
+
243
+ if os.environ.get("LEMONADE_DEBUG", "").lower() == "true":
244
+ # It may be useful to raise the exception here, since
245
+ # if any of the subsequent lines of code raise another
246
+ # exception it will be very hard to root cause e.
247
+ raise e
248
+
249
+ # Update tool and build status
250
+ state.save_stat(tool.status_key, build.FunctionStatus.ERROR)
251
+ state.save_stat(fs.Keys.BUILD_STATUS, build.FunctionStatus.ERROR)
252
+
253
+ # Save the log file for the failed tool to stats for easy reference
254
+ stats = fs.Stats(state.cache_dir, state.build_name)
255
+ stats.save_eval_error_log(tool.logfile_path)
256
+
257
+ # Advance the cursor below the monitor so
258
+ # we can print a message
259
+ self._advance_cursor(tool.unique_name)
260
+
261
+ if vars(state).get("invocation_info"):
262
+ state.invocation_info.status_message = f"Error: {e}"
263
+ state.invocation_info.status_message_color = printing.Colors.WARNING
264
+ else:
265
+ printing.log_error(e)
266
+
267
+ # We will raise this exception after we capture as many statistics
268
+ # about the build as possible
269
+ saved_exception = e
270
+
271
+ # Don't run any more tools
272
+ break
273
+
274
+ else:
275
+ # Update tool Status
276
+ state.save_stat(tool.status_key, build.FunctionStatus.SUCCESSFUL)
277
+ state.current_build_tool = None
278
+
279
+ finally:
280
+ # Store tool duration
281
+ execution_time = time.time() - start_time
282
+ state.save_stat(tool.duration_key, execution_time)
283
+
284
+ # Store current memory and peak working memory
285
+ state.save_stat(tool.memory_key, self._get_mem_usage_str())
286
+
287
+ # Inform profilers that tool has finished
288
+ for profiler in self.profilers:
289
+ profiler.tool_stopping()
290
+
291
+ start_times["cool down"] = time.time()
292
+
293
+ # Tell the profilers to stop gathering data
294
+ for profiler in self.profilers:
295
+ profiler.stop()
296
+
297
+ if not saved_exception:
298
+ state.build_status = build.FunctionStatus.SUCCESSFUL
299
+ state.save_stat(fs.Keys.BUILD_STATUS, build.FunctionStatus.SUCCESSFUL)
300
+ if vars(state).get("invocation_info"):
301
+ state.invocation_info.status_message = (
302
+ f"Successful build! {state.invocation_info.extra_status}"
303
+ )
304
+ state.invocation_info.status_message_color = printing.Colors.OKGREEN
305
+
306
+ # Generate profiler output
307
+ for profiler in self.profilers:
308
+ profiler.generate_results(state, timestamp, start_times)
309
+
310
+ if vars(state).get("models_found") and vars(state).get("invocation_info"):
311
+
312
+ # Present status statistics from the tools
313
+ for tool in self.tools:
314
+ state.invocation_info.stats_keys += tool.status_stats
315
+
316
+ # Present status statistics from the profilers
317
+ for profiler in self.profilers:
318
+ state.invocation_info.stats_keys += profiler.status_stats
319
+
320
+ print()
321
+
322
+ status.recursive_print(
323
+ models_found=state.models_found,
324
+ build_name=state.build_name,
325
+ cache_dir=state.cache_dir,
326
+ parent_model_hash=None,
327
+ parent_invocation_hash=None,
328
+ script_names_visited=[],
329
+ )
330
+
331
+ if lean_cache:
332
+ printing.log_info("Removing build artifacts...")
333
+ fs.clean_output_dir(state.cache_dir, state.build_name)
334
+
335
+ state.save()
336
+
337
+ if saved_exception:
338
+ raise saved_exception
339
+
340
+ printing.log_success(
341
+ f"\n Saved to **{build.output_dir(state.cache_dir, state.build_name)}**"
342
+ )
343
+
344
+ return state
345
+
346
+ def status_line(self, verbosity):
347
+ """
348
+ Print a status line in the monitor for every tool in the sequence
349
+ """
350
+ for tool in self.tools:
351
+ tool.status_line(successful=None, verbosity=verbosity)
352
+
353
+ @property
354
+ def info(self) -> Dict[str, Dict]:
355
+ """
356
+ Return a dictionary of tool_name:argv for the sequence
357
+ """
358
+
359
+ return {tool.__class__.unique_name: argv for tool, argv in self.tools.items()}
360
+
361
+
362
+ # This file was originally licensed under Apache 2.0. It has been modified.
363
+ # Modifications Copyright (c) 2025 AMD