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.
- lemonade/__init__.py +5 -0
- lemonade/api.py +125 -0
- lemonade/cache.py +85 -0
- lemonade/cli.py +135 -0
- lemonade/common/__init__.py +0 -0
- lemonade/common/analyze_model.py +26 -0
- lemonade/common/build.py +223 -0
- lemonade/common/cli_helpers.py +139 -0
- lemonade/common/exceptions.py +98 -0
- lemonade/common/filesystem.py +368 -0
- lemonade/common/labels.py +61 -0
- lemonade/common/onnx_helpers.py +176 -0
- lemonade/common/plugins.py +10 -0
- lemonade/common/printing.py +110 -0
- lemonade/common/status.py +490 -0
- lemonade/common/system_info.py +390 -0
- lemonade/common/tensor_helpers.py +83 -0
- lemonade/common/test_helpers.py +28 -0
- lemonade/profilers/__init__.py +1 -0
- lemonade/profilers/memory_tracker.py +257 -0
- lemonade/profilers/profiler.py +55 -0
- lemonade/sequence.py +363 -0
- lemonade/state.py +159 -0
- lemonade/tools/__init__.py +1 -0
- lemonade/tools/adapter.py +104 -0
- lemonade/tools/bench.py +284 -0
- lemonade/tools/huggingface_bench.py +267 -0
- lemonade/tools/huggingface_load.py +520 -0
- lemonade/tools/humaneval.py +258 -0
- lemonade/tools/llamacpp.py +261 -0
- lemonade/tools/llamacpp_bench.py +154 -0
- lemonade/tools/management_tools.py +273 -0
- lemonade/tools/mmlu.py +327 -0
- lemonade/tools/ort_genai/__init__.py +0 -0
- lemonade/tools/ort_genai/oga.py +1129 -0
- lemonade/tools/ort_genai/oga_bench.py +142 -0
- lemonade/tools/perplexity.py +146 -0
- lemonade/tools/prompt.py +228 -0
- lemonade/tools/quark/__init__.py +0 -0
- lemonade/tools/quark/quark_load.py +172 -0
- lemonade/tools/quark/quark_quantize.py +439 -0
- lemonade/tools/report/__init__.py +0 -0
- lemonade/tools/report/llm_report.py +203 -0
- lemonade/tools/report/table.py +739 -0
- lemonade/tools/server/__init__.py +0 -0
- lemonade/tools/server/serve.py +1354 -0
- lemonade/tools/server/tool_calls.py +146 -0
- lemonade/tools/tool.py +374 -0
- lemonade/version.py +1 -0
- lemonade_install/__init__.py +1 -0
- lemonade_install/install.py +774 -0
- lemonade_sdk-7.0.0.dist-info/METADATA +116 -0
- lemonade_sdk-7.0.0.dist-info/RECORD +61 -0
- lemonade_sdk-7.0.0.dist-info/WHEEL +5 -0
- lemonade_sdk-7.0.0.dist-info/entry_points.txt +4 -0
- lemonade_sdk-7.0.0.dist-info/licenses/LICENSE +201 -0
- lemonade_sdk-7.0.0.dist-info/licenses/NOTICE.md +21 -0
- lemonade_sdk-7.0.0.dist-info/top_level.txt +3 -0
- lemonade_server/cli.py +260 -0
- lemonade_server/model_manager.py +98 -0
- 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
|