lemonade-sdk 9.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lemonade/__init__.py +5 -0
- lemonade/api.py +180 -0
- lemonade/cache.py +92 -0
- lemonade/cli.py +173 -0
- lemonade/common/__init__.py +0 -0
- lemonade/common/build.py +176 -0
- lemonade/common/cli_helpers.py +139 -0
- lemonade/common/exceptions.py +98 -0
- lemonade/common/filesystem.py +368 -0
- lemonade/common/inference_engines.py +408 -0
- lemonade/common/network.py +93 -0
- lemonade/common/printing.py +110 -0
- lemonade/common/status.py +471 -0
- lemonade/common/system_info.py +1411 -0
- lemonade/common/test_helpers.py +28 -0
- lemonade/profilers/__init__.py +1 -0
- lemonade/profilers/agt_power.py +437 -0
- lemonade/profilers/hwinfo_power.py +429 -0
- lemonade/profilers/memory_tracker.py +259 -0
- lemonade/profilers/profiler.py +58 -0
- lemonade/sequence.py +363 -0
- lemonade/state.py +159 -0
- lemonade/tools/__init__.py +1 -0
- lemonade/tools/accuracy.py +432 -0
- lemonade/tools/adapter.py +114 -0
- lemonade/tools/bench.py +302 -0
- lemonade/tools/flm/__init__.py +1 -0
- lemonade/tools/flm/utils.py +305 -0
- lemonade/tools/huggingface/bench.py +187 -0
- lemonade/tools/huggingface/load.py +235 -0
- lemonade/tools/huggingface/utils.py +359 -0
- lemonade/tools/humaneval.py +264 -0
- lemonade/tools/llamacpp/bench.py +255 -0
- lemonade/tools/llamacpp/load.py +222 -0
- lemonade/tools/llamacpp/utils.py +1260 -0
- lemonade/tools/management_tools.py +319 -0
- lemonade/tools/mmlu.py +319 -0
- lemonade/tools/oga/__init__.py +0 -0
- lemonade/tools/oga/bench.py +120 -0
- lemonade/tools/oga/load.py +804 -0
- lemonade/tools/oga/migration.py +403 -0
- lemonade/tools/oga/utils.py +462 -0
- lemonade/tools/perplexity.py +147 -0
- lemonade/tools/prompt.py +263 -0
- lemonade/tools/report/__init__.py +0 -0
- lemonade/tools/report/llm_report.py +203 -0
- lemonade/tools/report/table.py +899 -0
- lemonade/tools/server/__init__.py +0 -0
- lemonade/tools/server/flm.py +133 -0
- lemonade/tools/server/llamacpp.py +320 -0
- lemonade/tools/server/serve.py +2123 -0
- lemonade/tools/server/static/favicon.ico +0 -0
- lemonade/tools/server/static/index.html +279 -0
- lemonade/tools/server/static/js/chat.js +1059 -0
- lemonade/tools/server/static/js/model-settings.js +183 -0
- lemonade/tools/server/static/js/models.js +1395 -0
- lemonade/tools/server/static/js/shared.js +556 -0
- lemonade/tools/server/static/logs.html +191 -0
- lemonade/tools/server/static/styles.css +2654 -0
- lemonade/tools/server/static/webapp.html +321 -0
- lemonade/tools/server/tool_calls.py +153 -0
- lemonade/tools/server/tray.py +664 -0
- lemonade/tools/server/utils/macos_tray.py +226 -0
- lemonade/tools/server/utils/port.py +77 -0
- lemonade/tools/server/utils/thread.py +85 -0
- lemonade/tools/server/utils/windows_tray.py +408 -0
- lemonade/tools/server/webapp.py +34 -0
- lemonade/tools/server/wrapped_server.py +559 -0
- lemonade/tools/tool.py +374 -0
- lemonade/version.py +1 -0
- lemonade_install/__init__.py +1 -0
- lemonade_install/install.py +239 -0
- lemonade_sdk-9.1.1.dist-info/METADATA +276 -0
- lemonade_sdk-9.1.1.dist-info/RECORD +84 -0
- lemonade_sdk-9.1.1.dist-info/WHEEL +5 -0
- lemonade_sdk-9.1.1.dist-info/entry_points.txt +5 -0
- lemonade_sdk-9.1.1.dist-info/licenses/LICENSE +201 -0
- lemonade_sdk-9.1.1.dist-info/licenses/NOTICE.md +47 -0
- lemonade_sdk-9.1.1.dist-info/top_level.txt +3 -0
- lemonade_server/cli.py +805 -0
- lemonade_server/model_manager.py +758 -0
- lemonade_server/pydantic_models.py +159 -0
- lemonade_server/server_models.json +643 -0
- lemonade_server/settings.py +39 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This power profiler uses an external tool called HWiNFO.
|
|
3
|
+
# Please see the power profiling documentation for download and install instructions.
|
|
4
|
+
#
|
|
5
|
+
# The power profiling functionality is currently not part of our continuous integration
|
|
6
|
+
# testing framework, primarily due to the setup overhead required from the above three items.
|
|
7
|
+
# We will revisit in the future if we face issues.
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
import ctypes
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
import os
|
|
13
|
+
import platform
|
|
14
|
+
import textwrap
|
|
15
|
+
import time
|
|
16
|
+
import subprocess
|
|
17
|
+
import psutil
|
|
18
|
+
import matplotlib.pyplot as plt
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
import lemonade.common.printing as printing
|
|
22
|
+
from lemonade.profilers import Profiler
|
|
23
|
+
from lemonade.tools.report.table import LemonadePerfTable, DictListStat
|
|
24
|
+
|
|
25
|
+
DEFAULT_TRACK_POWER_INTERVAL_MS = 500
|
|
26
|
+
DEFAULT_TRACK_POWER_WARMUP_PERIOD = 5
|
|
27
|
+
|
|
28
|
+
HWINFO_PATH_ENV_VAR = "HWINFO_PATH"
|
|
29
|
+
DEFAULT_HWINFO_PATH = r"C:\Program Files\HWiNFO64\HWiNFO64.exe"
|
|
30
|
+
POWER_USAGE_CSV_FILENAME = "power_usage_hwinfo.csv"
|
|
31
|
+
POWER_USAGE_PNG_FILENAME = "power_usage_hwinfo.png"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Keys:
|
|
35
|
+
# Path to the file containing the power usage plot
|
|
36
|
+
POWER_USAGE_PLOT = "power_usage_plot_hwinfo"
|
|
37
|
+
# Path to the file containing the power usage plot
|
|
38
|
+
POWER_USAGE_DATA = "power_usage_data_hwinfo"
|
|
39
|
+
# Path to the file containing the power usage plot
|
|
40
|
+
POWER_USAGE_DATA_CSV = "power_usage_data_file_hwinfo"
|
|
41
|
+
# Maximum power consumed by the CPU processor package during the tools sequence
|
|
42
|
+
PEAK_PROCESSOR_PACKAGE_POWER = "peak_processor_package_power_hwinfo"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Add column to the Lemonade performance report table for the power data
|
|
46
|
+
LemonadePerfTable.table_descriptor["stat_columns"].append(
|
|
47
|
+
DictListStat(
|
|
48
|
+
"Power Usage (HWiNFO)",
|
|
49
|
+
Keys.POWER_USAGE_DATA,
|
|
50
|
+
[
|
|
51
|
+
("name", "{0}:"),
|
|
52
|
+
("duration", "{0:.1f}s,"),
|
|
53
|
+
("energy consumed", "{0:.1f} J"),
|
|
54
|
+
],
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_user_admin() -> bool:
|
|
60
|
+
"""Return true if platform is Windows and user is Admin"""
|
|
61
|
+
os_type = platform.system()
|
|
62
|
+
if os_type == "Windows":
|
|
63
|
+
try:
|
|
64
|
+
return ctypes.windll.shell32.IsUserAnAdmin() == 1
|
|
65
|
+
except AttributeError:
|
|
66
|
+
pass
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def is_process_running(executable_name):
|
|
71
|
+
"""Checks if an executable is currently running."""
|
|
72
|
+
executable_name = executable_name.lower()
|
|
73
|
+
for process in psutil.process_iter(["pid", "name"]):
|
|
74
|
+
if process.info["name"].lower() == executable_name:
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def read_data_from_csv(csv_path, columns_dict, encoding="utf-8") -> pd.DataFrame:
|
|
80
|
+
try:
|
|
81
|
+
available_columns = pd.read_csv(csv_path, nrows=0, encoding=encoding).columns
|
|
82
|
+
columns_to_read = list(set(columns_dict.values()) & set(available_columns))
|
|
83
|
+
df = pd.read_csv(csv_path, usecols=columns_to_read, encoding=encoding)
|
|
84
|
+
except FileNotFoundError as e:
|
|
85
|
+
printing.log_info(f"Power profiler file not found: {e.filename}")
|
|
86
|
+
return None
|
|
87
|
+
except ValueError as e:
|
|
88
|
+
printing.log_info(f"Error reading power data from {csv_path}: {e}")
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# Rename columns to simple name
|
|
92
|
+
df.rename(
|
|
93
|
+
columns={v: k for k, v in columns_dict.items() if v in columns_to_read},
|
|
94
|
+
inplace=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return df
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class HWINFOPowerProfiler(Profiler):
|
|
101
|
+
|
|
102
|
+
unique_name = "power-hwinfo"
|
|
103
|
+
|
|
104
|
+
# mapping from short name to full name of the measurement in the CSV file produced by HWiNFO
|
|
105
|
+
columns_dict = {
|
|
106
|
+
"time": "Time",
|
|
107
|
+
"cpu_package_power": "CPU Package Power [W]",
|
|
108
|
+
"npu_clock": "NPU Clock [MHz]",
|
|
109
|
+
"gpu_clock": "GPU Clock [MHz]",
|
|
110
|
+
"total_cpu_usage": "Total CPU Usage [%]",
|
|
111
|
+
"apu_stapm_limit": "APU STAPM Limit [%]",
|
|
112
|
+
"cpu_tdc_limit": "CPU TDC Limit [%]",
|
|
113
|
+
"cpu_edc_limit": "CPU EDC Limit [%]",
|
|
114
|
+
"cpu_ppt_fast_limit": "CPU PPT FAST Limit [%]",
|
|
115
|
+
"cpu_ppt_slow_limit": "CPU PPT SLOW Limit [%]",
|
|
116
|
+
"thermal_limit": "Thermal Limit [%]",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def time_to_seconds(time_str):
|
|
121
|
+
# Parse the time string
|
|
122
|
+
try:
|
|
123
|
+
time_obj = datetime.strptime(time_str, "%H:%M:%S.%f")
|
|
124
|
+
except TypeError:
|
|
125
|
+
raise ValueError(f"Could not parse {time_str}")
|
|
126
|
+
|
|
127
|
+
# Calculate the total seconds
|
|
128
|
+
total_seconds = (
|
|
129
|
+
time_obj.hour * 3600
|
|
130
|
+
+ time_obj.minute * 60
|
|
131
|
+
+ time_obj.second
|
|
132
|
+
+ time_obj.microsecond / 1_000_000
|
|
133
|
+
)
|
|
134
|
+
return total_seconds
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def add_arguments_to_parser(parser):
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
f"--{HWINFOPowerProfiler.unique_name}",
|
|
140
|
+
nargs="?",
|
|
141
|
+
metavar="WARMUP_PERIOD",
|
|
142
|
+
type=int,
|
|
143
|
+
default=None,
|
|
144
|
+
const=DEFAULT_TRACK_POWER_WARMUP_PERIOD,
|
|
145
|
+
help="Track power consumption using the HWiNFO application and plot the results. "
|
|
146
|
+
"HWiNFO is a commercial product from a third party (https://www.hwinfo.com/) "
|
|
147
|
+
"and should be acquired/licensed appropriately. "
|
|
148
|
+
"Optionally, set the warmup period in seconds "
|
|
149
|
+
f"(default: {DEFAULT_TRACK_POWER_WARMUP_PERIOD}). If the application is not "
|
|
150
|
+
f"installed at {DEFAULT_HWINFO_PATH}, set the {HWINFO_PATH_ENV_VAR} environment "
|
|
151
|
+
f"variable to point at it. This is a Windows only feature and Lemonade must be run "
|
|
152
|
+
f"from a CMD window with Administrator privileges.",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def __init__(self, parser_arg_value):
|
|
156
|
+
super().__init__()
|
|
157
|
+
self.warmup_period = parser_arg_value
|
|
158
|
+
self.status_stats += [Keys.PEAK_PROCESSOR_PACKAGE_POWER, Keys.POWER_USAGE_PLOT]
|
|
159
|
+
self.tracking_active = False
|
|
160
|
+
self.build_dir = None
|
|
161
|
+
self.csv_path = None
|
|
162
|
+
self.hwinfo_process = None
|
|
163
|
+
self.data = None
|
|
164
|
+
|
|
165
|
+
def start(self, build_dir):
|
|
166
|
+
if self.tracking_active:
|
|
167
|
+
raise RuntimeError("Cannot start power tracking while already tracking")
|
|
168
|
+
|
|
169
|
+
if platform.system() != "Windows":
|
|
170
|
+
raise RuntimeError("Power usage tracking is only enabled in Windows.")
|
|
171
|
+
|
|
172
|
+
# Check that user as running in Admin mode
|
|
173
|
+
if not is_user_admin():
|
|
174
|
+
raise RuntimeError(
|
|
175
|
+
"For power usage tracking, run Lemonade as an Administrator."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Save the folder where data and plot will be stored
|
|
179
|
+
self.build_dir = build_dir
|
|
180
|
+
|
|
181
|
+
# The csv file where power data will be stored
|
|
182
|
+
self.csv_path = os.path.join(build_dir, POWER_USAGE_CSV_FILENAME)
|
|
183
|
+
if " " in self.csv_path:
|
|
184
|
+
raise RuntimeError(
|
|
185
|
+
"Can't log HWiNFO data to a file with a <space> in the path. "
|
|
186
|
+
"Please use the `-d` flag to specify a Lemonade cache path with no spaces."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# See if the HWINFO_PATH environment variables exists
|
|
190
|
+
# If so, use it instead of the default path
|
|
191
|
+
if HWINFO_PATH_ENV_VAR in os.environ:
|
|
192
|
+
hwinfo_path = os.getenv(HWINFO_PATH_ENV_VAR)
|
|
193
|
+
else:
|
|
194
|
+
hwinfo_path = DEFAULT_HWINFO_PATH
|
|
195
|
+
|
|
196
|
+
# Check the HWINFO executable exists
|
|
197
|
+
if not os.path.isfile(hwinfo_path):
|
|
198
|
+
raise FileNotFoundError(hwinfo_path)
|
|
199
|
+
|
|
200
|
+
# Check that executable is not already running
|
|
201
|
+
executable = hwinfo_path.split(os.sep)[-1]
|
|
202
|
+
if is_process_running(executable):
|
|
203
|
+
raise RuntimeError(
|
|
204
|
+
f"{executable} is already running. Quit it and try again."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Start HWiNFO executable
|
|
208
|
+
try:
|
|
209
|
+
command = [
|
|
210
|
+
hwinfo_path,
|
|
211
|
+
f"-l{self.csv_path}",
|
|
212
|
+
f"-poll_rate={DEFAULT_TRACK_POWER_INTERVAL_MS}",
|
|
213
|
+
]
|
|
214
|
+
self.hwinfo_process = subprocess.Popen(
|
|
215
|
+
command,
|
|
216
|
+
stdin=subprocess.PIPE,
|
|
217
|
+
stderr=subprocess.PIPE,
|
|
218
|
+
)
|
|
219
|
+
except OSError as e:
|
|
220
|
+
if "[WinError 740]" in str(e):
|
|
221
|
+
print(
|
|
222
|
+
"\nTo avoid `requested operation requires elevation` error, please make sure"
|
|
223
|
+
)
|
|
224
|
+
print(
|
|
225
|
+
"HWiNFO.exe has Properties->Compatibility->`Run this program as an "
|
|
226
|
+
"administrator` checked."
|
|
227
|
+
)
|
|
228
|
+
print(
|
|
229
|
+
"You may also need to set Windows User Account Control to `Never notify`.\n"
|
|
230
|
+
)
|
|
231
|
+
raise
|
|
232
|
+
self.tracking_active = True
|
|
233
|
+
time.sleep(self.warmup_period)
|
|
234
|
+
|
|
235
|
+
def stop(self):
|
|
236
|
+
if self.tracking_active:
|
|
237
|
+
self.tracking_active = False
|
|
238
|
+
time.sleep(self.warmup_period)
|
|
239
|
+
self.hwinfo_process.terminate()
|
|
240
|
+
self.hwinfo_process.wait()
|
|
241
|
+
|
|
242
|
+
def generate_results(self, state, timestamp, start_times):
|
|
243
|
+
if self.hwinfo_process is None:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
if self.tracking_active:
|
|
247
|
+
self.stop()
|
|
248
|
+
|
|
249
|
+
df = read_data_from_csv(self.csv_path, self.columns_dict, encoding="latin1")
|
|
250
|
+
if df is None:
|
|
251
|
+
state.save_stat(Keys.POWER_USAGE_PLOT, "NONE")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
# Remap time to seconds from start of profiling data
|
|
255
|
+
# Remap csv data time to elapsed seconds (i.e., substract out initial time)
|
|
256
|
+
try:
|
|
257
|
+
initial_data_time = self.time_to_seconds(df["time"].iloc[0])
|
|
258
|
+
df["time"] = df["time"].apply(
|
|
259
|
+
lambda x: (self.time_to_seconds(x) - initial_data_time)
|
|
260
|
+
)
|
|
261
|
+
except ValueError as e:
|
|
262
|
+
printing.log_info(
|
|
263
|
+
f"Badly formatted time data in {self.csv_path}: {e}. "
|
|
264
|
+
f"HWiNFO may have closed unexpectedly."
|
|
265
|
+
)
|
|
266
|
+
state.save_stat(Keys.POWER_USAGE_PLOT, "NONE")
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
# Make time 0 the time of the first tool starting (after the warmup period)
|
|
270
|
+
if start_times:
|
|
271
|
+
tool_start_times = sorted(start_times.values())
|
|
272
|
+
# First tool after warmup (if no tools, then will be time of start of cool down)
|
|
273
|
+
first_tool_time = tool_start_times[1]
|
|
274
|
+
|
|
275
|
+
# Map the measurement data so that zero in the measurement data aligns with
|
|
276
|
+
# the first_tool_time
|
|
277
|
+
#
|
|
278
|
+
# Find the difference between the timestamp first_tool_time and initial_data_time
|
|
279
|
+
# which is a count of seconds since midnight
|
|
280
|
+
#
|
|
281
|
+
# Find midnight prior to first_tool_time
|
|
282
|
+
t = time.localtime(first_tool_time)
|
|
283
|
+
since_midnight = (
|
|
284
|
+
t.tm_hour * 3600 + t.tm_min * 60 + t.tm_sec + (first_tool_time % 1)
|
|
285
|
+
)
|
|
286
|
+
delta = since_midnight - initial_data_time
|
|
287
|
+
df["time"] = df["time"] - delta
|
|
288
|
+
|
|
289
|
+
peak_power = max(df["cpu_package_power"])
|
|
290
|
+
|
|
291
|
+
# Create a figure
|
|
292
|
+
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(16, 8))
|
|
293
|
+
|
|
294
|
+
if start_times:
|
|
295
|
+
tool_starts = sorted(start_times.items(), key=lambda item: item[1])
|
|
296
|
+
tool_name_list = [item[0] for item in tool_starts]
|
|
297
|
+
|
|
298
|
+
# Adjust to common time frame as power measurements
|
|
299
|
+
tool_start_list = [
|
|
300
|
+
max(df["time"].iloc[0], item[1] - first_tool_time)
|
|
301
|
+
for item in tool_starts
|
|
302
|
+
]
|
|
303
|
+
tool_stop_list = tool_start_list[1:] + [df["time"].values[-1]]
|
|
304
|
+
|
|
305
|
+
# Extract power data time series
|
|
306
|
+
x_time = df["time"].to_numpy()
|
|
307
|
+
y_power = df["cpu_package_power"].to_numpy()
|
|
308
|
+
|
|
309
|
+
# Extract data for each stage in the build
|
|
310
|
+
self.data = []
|
|
311
|
+
for name, t0, tf in zip(tool_name_list, tool_start_list, tool_stop_list):
|
|
312
|
+
x = x_time[(x_time >= t0) * (x_time <= tf)]
|
|
313
|
+
x = np.insert(x, 0, t0)
|
|
314
|
+
x = np.insert(x, len(x), tf)
|
|
315
|
+
y = np.interp(x, x_time, y_power)
|
|
316
|
+
energy = np.trapz(y, x)
|
|
317
|
+
avg_power = energy / (tf - t0)
|
|
318
|
+
stage = {
|
|
319
|
+
"name": name,
|
|
320
|
+
"t": x.tolist(),
|
|
321
|
+
"power": y.tolist(),
|
|
322
|
+
"duration": float(tf - t0),
|
|
323
|
+
"energy consumed": float(energy),
|
|
324
|
+
"average power": float(avg_power),
|
|
325
|
+
}
|
|
326
|
+
self.data.append(stage)
|
|
327
|
+
|
|
328
|
+
for stage in self.data:
|
|
329
|
+
# Plot power usage time series
|
|
330
|
+
p = ax1.plot(
|
|
331
|
+
stage["t"],
|
|
332
|
+
stage["power"],
|
|
333
|
+
label=f"{stage['name']} ({stage['duration']:.1f}s, "
|
|
334
|
+
f"{stage['energy consumed']:0.1f} J)",
|
|
335
|
+
)
|
|
336
|
+
# Add a dashed line to show average power
|
|
337
|
+
ax1.plot(
|
|
338
|
+
[stage["t"][0], stage["t"][-1]],
|
|
339
|
+
[stage["average power"], stage["average power"]],
|
|
340
|
+
linestyle="--",
|
|
341
|
+
c=p[0].get_c(),
|
|
342
|
+
)
|
|
343
|
+
# Add average power text to plot
|
|
344
|
+
ax1.text(
|
|
345
|
+
stage["t"][0],
|
|
346
|
+
stage["average power"],
|
|
347
|
+
f"{stage['average power']:.1f} W ",
|
|
348
|
+
horizontalalignment="right",
|
|
349
|
+
verticalalignment="center",
|
|
350
|
+
c=p[0].get_c(),
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
ax1.plot(
|
|
354
|
+
df["time"],
|
|
355
|
+
df["cpu_package_power"],
|
|
356
|
+
)
|
|
357
|
+
# Add title and labels to plots
|
|
358
|
+
ax1.set_ylabel(self.columns_dict["cpu_package_power"])
|
|
359
|
+
title_str = "HWiNFO Stats\n" + "\n".join(textwrap.wrap(state.build_name, 60))
|
|
360
|
+
ax1.set_title(title_str)
|
|
361
|
+
ax1.legend()
|
|
362
|
+
ax1.grid(True)
|
|
363
|
+
|
|
364
|
+
# Create second plot
|
|
365
|
+
ax2.plot(
|
|
366
|
+
df["time"],
|
|
367
|
+
df["npu_clock"],
|
|
368
|
+
label=self.columns_dict["npu_clock"],
|
|
369
|
+
)
|
|
370
|
+
ax2.plot(
|
|
371
|
+
df["time"],
|
|
372
|
+
df["gpu_clock"],
|
|
373
|
+
label=self.columns_dict["gpu_clock"],
|
|
374
|
+
)
|
|
375
|
+
ax2.set_xlabel("Time [s]")
|
|
376
|
+
ax2.set_ylabel("Clock Frequency [MHz]")
|
|
377
|
+
ax2.legend(loc=2)
|
|
378
|
+
ax2.grid(True)
|
|
379
|
+
# Add second y-axis for %
|
|
380
|
+
ax2_twin = ax2.twinx()
|
|
381
|
+
ax2_twin.plot(
|
|
382
|
+
df["time"],
|
|
383
|
+
df["total_cpu_usage"],
|
|
384
|
+
label=self.columns_dict["total_cpu_usage"],
|
|
385
|
+
c="g",
|
|
386
|
+
)
|
|
387
|
+
ax2_twin.set_ylim([0, 100])
|
|
388
|
+
vals = ax2_twin.get_yticks()
|
|
389
|
+
ax2_twin.set_yticks(vals)
|
|
390
|
+
ax2_twin.set_yticklabels([f"{v:.0f}%" for v in vals])
|
|
391
|
+
ax2_twin.legend(loc=1)
|
|
392
|
+
|
|
393
|
+
# Create third plot (all remaining columns)
|
|
394
|
+
plot3_columns = [
|
|
395
|
+
"apu_stapm_limit",
|
|
396
|
+
"cpu_tdc_limit",
|
|
397
|
+
"cpu_edc_limit",
|
|
398
|
+
"cpu_ppt_fast_limit",
|
|
399
|
+
"cpu_ppt_slow_limit",
|
|
400
|
+
"thermal_limit",
|
|
401
|
+
]
|
|
402
|
+
for col_str in plot3_columns:
|
|
403
|
+
if col_str in df.columns:
|
|
404
|
+
ax3.plot(
|
|
405
|
+
df["time"],
|
|
406
|
+
df[col_str],
|
|
407
|
+
label=self.columns_dict[col_str],
|
|
408
|
+
)
|
|
409
|
+
ax3.set_xlabel("Time [s]")
|
|
410
|
+
ax3.set_ylim([0, 100])
|
|
411
|
+
vals = ax3.get_yticks()
|
|
412
|
+
ax3.set_yticks(vals)
|
|
413
|
+
ax3.set_yticklabels([f"{v:.0f}%" for v in vals])
|
|
414
|
+
if len(ax3.lines):
|
|
415
|
+
ax3.legend()
|
|
416
|
+
ax3.grid(True)
|
|
417
|
+
|
|
418
|
+
# Save plot to current folder AND save to cache
|
|
419
|
+
plot_path = os.path.join(
|
|
420
|
+
self.build_dir, f"{timestamp}_{POWER_USAGE_PNG_FILENAME}"
|
|
421
|
+
)
|
|
422
|
+
fig.savefig(plot_path, dpi=300, bbox_inches="tight")
|
|
423
|
+
plot_path = os.path.join(os.getcwd(), f"{timestamp}_{POWER_USAGE_PNG_FILENAME}")
|
|
424
|
+
fig.savefig(plot_path, dpi=300, bbox_inches="tight")
|
|
425
|
+
|
|
426
|
+
state.save_stat(Keys.POWER_USAGE_PLOT, plot_path)
|
|
427
|
+
state.save_stat(Keys.POWER_USAGE_DATA, self.data)
|
|
428
|
+
state.save_stat(Keys.POWER_USAGE_DATA_CSV, self.csv_path)
|
|
429
|
+
state.save_stat(Keys.PEAK_PROCESSOR_PACKAGE_POWER, f"{peak_power:0.1f} W")
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import textwrap
|
|
4
|
+
from multiprocessing import Process, Queue
|
|
5
|
+
import psutil
|
|
6
|
+
import yaml
|
|
7
|
+
import lemonade.common.filesystem as fs
|
|
8
|
+
import lemonade.common.printing as printing
|
|
9
|
+
from lemonade.profilers import Profiler
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEFAULT_TRACK_MEMORY_INTERVAL = 0.25
|
|
13
|
+
MEMORY_USAGE_YAML_FILENAME = "memory_usage.yaml"
|
|
14
|
+
MEMORY_USAGE_PNG_FILENAME = "memory_usage.png"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MemoryTracker(Profiler):
|
|
18
|
+
|
|
19
|
+
unique_name = "memory"
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def add_arguments_to_parser(parser):
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"-m",
|
|
25
|
+
f"--{MemoryTracker.unique_name}",
|
|
26
|
+
nargs="?",
|
|
27
|
+
metavar="TRACK_INTERVAL",
|
|
28
|
+
type=float,
|
|
29
|
+
default=None,
|
|
30
|
+
const=DEFAULT_TRACK_MEMORY_INTERVAL,
|
|
31
|
+
help="Track memory usage and plot the results. "
|
|
32
|
+
"Optionally, set the tracking interval in seconds "
|
|
33
|
+
f"(default: {DEFAULT_TRACK_MEMORY_INTERVAL})",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def get_time_mem_list(process):
|
|
38
|
+
return [time.time(), process.memory_info().rss]
|
|
39
|
+
|
|
40
|
+
def __init__(self, parser_arg_value):
|
|
41
|
+
super().__init__()
|
|
42
|
+
self.status_stats += [fs.Keys.MEMORY_USAGE_PLOT]
|
|
43
|
+
self.track_memory_interval = parser_arg_value
|
|
44
|
+
self.process_being_tracked = None
|
|
45
|
+
self.build_dir = None
|
|
46
|
+
self.queue = None
|
|
47
|
+
self.tracker_process = None
|
|
48
|
+
self.tracking_active = False
|
|
49
|
+
self.yaml_path = None
|
|
50
|
+
|
|
51
|
+
def start(self, build_dir):
|
|
52
|
+
if self.tracking_active:
|
|
53
|
+
raise RuntimeError("Cannot start tracking while already tracking")
|
|
54
|
+
|
|
55
|
+
# Save the folder where data and plot will be stored
|
|
56
|
+
self.build_dir = build_dir
|
|
57
|
+
|
|
58
|
+
# Get the process being tracked
|
|
59
|
+
track_pid = os.getpid()
|
|
60
|
+
self.process_being_tracked = psutil.Process(track_pid)
|
|
61
|
+
|
|
62
|
+
# Create queue for passing messages to the tracker
|
|
63
|
+
self.queue = Queue()
|
|
64
|
+
|
|
65
|
+
# The yaml file where the memory usage data will be saved
|
|
66
|
+
self.yaml_path = os.path.join(self.build_dir, MEMORY_USAGE_YAML_FILENAME)
|
|
67
|
+
|
|
68
|
+
# Create process to continuously sample memory usage
|
|
69
|
+
self.tracker_process = Process(
|
|
70
|
+
target=self._memory_tracker_,
|
|
71
|
+
args=(
|
|
72
|
+
track_pid,
|
|
73
|
+
self.queue,
|
|
74
|
+
self.yaml_path,
|
|
75
|
+
self.track_memory_interval,
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
self.tracker_process.start()
|
|
79
|
+
self.tracking_active = True
|
|
80
|
+
self.set_label("start")
|
|
81
|
+
self.sample()
|
|
82
|
+
|
|
83
|
+
def tool_starting(self, tool_name):
|
|
84
|
+
self.set_label(tool_name)
|
|
85
|
+
|
|
86
|
+
def tool_stopping(self):
|
|
87
|
+
self.sample()
|
|
88
|
+
|
|
89
|
+
def set_label(self, label):
|
|
90
|
+
if self.tracking_active:
|
|
91
|
+
self.queue.put(label)
|
|
92
|
+
|
|
93
|
+
def sample(self):
|
|
94
|
+
if self.tracking_active:
|
|
95
|
+
self.queue.put(MemoryTracker.get_time_mem_list(self.process_being_tracked))
|
|
96
|
+
|
|
97
|
+
def stop(self):
|
|
98
|
+
if self.tracking_active:
|
|
99
|
+
self.queue.put(None)
|
|
100
|
+
self.tracking_active = False
|
|
101
|
+
|
|
102
|
+
def generate_results(self, state, timestamp, _):
|
|
103
|
+
|
|
104
|
+
import matplotlib.pyplot as plt
|
|
105
|
+
|
|
106
|
+
if self.tracker_process is None:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
if self.tracking_active:
|
|
110
|
+
self.stop()
|
|
111
|
+
|
|
112
|
+
# Wait for memory tracker to finish writing yaml data file
|
|
113
|
+
while self.tracker_process.is_alive():
|
|
114
|
+
self.tracker_process.join(timeout=1.0)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
with open(self.yaml_path, "r", encoding="utf-8") as f:
|
|
118
|
+
memory_tracks = yaml.safe_load(f)
|
|
119
|
+
except FileNotFoundError as e:
|
|
120
|
+
printing.log_info(
|
|
121
|
+
f"Memory tracker file not found: {e.filename}. No memory usage plot generated"
|
|
122
|
+
)
|
|
123
|
+
state.save_stat(fs.Keys.MEMORY_USAGE_PLOT, None)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
# Add check to ensure that memory_tracks is not empty or improperly formatted
|
|
127
|
+
if not memory_tracks or not isinstance(memory_tracks, list):
|
|
128
|
+
printing.log_info(
|
|
129
|
+
f"Memory tracker file contains no data or is improperly formatted: {self.yaml_path}"
|
|
130
|
+
)
|
|
131
|
+
state.save_stat(fs.Keys.MEMORY_USAGE_PLOT, None)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
# Find final time in the start track (first track) to subtract from all other times
|
|
135
|
+
_, track = memory_tracks[0]
|
|
136
|
+
t0 = track[-1][0]
|
|
137
|
+
|
|
138
|
+
# last_t and last_y are used to draw a line between the last point of the prior
|
|
139
|
+
# track and the first point of the current track
|
|
140
|
+
last_t = None
|
|
141
|
+
last_y = None
|
|
142
|
+
|
|
143
|
+
plt.figure()
|
|
144
|
+
for k, v in memory_tracks[1:]:
|
|
145
|
+
if len(v) > 0:
|
|
146
|
+
t = [x[0] - t0 for x in v]
|
|
147
|
+
y = [float(x[1]) / 1024**3 for x in v]
|
|
148
|
+
# draw new memory usage track
|
|
149
|
+
if last_t is not None:
|
|
150
|
+
plt.plot([last_t] + t, [last_y] + y, label=k, marker=".")
|
|
151
|
+
else:
|
|
152
|
+
plt.plot(t, y, label=k, marker=".")
|
|
153
|
+
last_t = t[-1]
|
|
154
|
+
last_y = y[-1]
|
|
155
|
+
plt.xlabel("Time (sec)")
|
|
156
|
+
plt.ylabel("GB")
|
|
157
|
+
title_str = "Physical Memory Usage\n" + "\n".join(
|
|
158
|
+
textwrap.wrap(state.build_name, 60)
|
|
159
|
+
)
|
|
160
|
+
plt.title(title_str)
|
|
161
|
+
plt.legend()
|
|
162
|
+
plt.grid()
|
|
163
|
+
plt.tight_layout()
|
|
164
|
+
|
|
165
|
+
# Save plot to cache and to current folder
|
|
166
|
+
plot_path = os.path.join(
|
|
167
|
+
self.build_dir, f"{timestamp}_{MEMORY_USAGE_PNG_FILENAME}"
|
|
168
|
+
)
|
|
169
|
+
plt.savefig(plot_path)
|
|
170
|
+
plot_path = os.path.join(
|
|
171
|
+
os.getcwd(), f"{timestamp}_{MEMORY_USAGE_PNG_FILENAME}"
|
|
172
|
+
)
|
|
173
|
+
plt.savefig(plot_path)
|
|
174
|
+
state.save_stat(fs.Keys.MEMORY_USAGE_PLOT, plot_path)
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _memory_tracker_(
|
|
178
|
+
tracked_pid,
|
|
179
|
+
input_queue: Queue,
|
|
180
|
+
yaml_path: str,
|
|
181
|
+
track_memory_interval: float,
|
|
182
|
+
):
|
|
183
|
+
"""
|
|
184
|
+
Tracks memory usage during build and saves to yaml file
|
|
185
|
+
The build communicates with the tracker though the input_queue. It may pass:
|
|
186
|
+
1) string - This is to indicate that a new track is starting and the string is the label
|
|
187
|
+
for the next segment. The tracker will automatically track memory usage at
|
|
188
|
+
the track_memory_interval once a first track_name is given to it.
|
|
189
|
+
2) list - A time and a current memory usage value that is added to the current track
|
|
190
|
+
(typically used at the end of a segment to make sure that each segment is
|
|
191
|
+
sampled at least once
|
|
192
|
+
3) None - This indicates that the tracker should stop tracking, save its data to a file
|
|
193
|
+
and end
|
|
194
|
+
"""
|
|
195
|
+
memory_tracks = []
|
|
196
|
+
current_track = []
|
|
197
|
+
track_name = None
|
|
198
|
+
tracker_exit = False
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
tracked_process = psutil.Process(tracked_pid)
|
|
202
|
+
while (
|
|
203
|
+
not tracker_exit and tracked_process.status() == psutil.STATUS_RUNNING
|
|
204
|
+
):
|
|
205
|
+
|
|
206
|
+
time.sleep(track_memory_interval)
|
|
207
|
+
|
|
208
|
+
# Read any messages from the parent process
|
|
209
|
+
while not input_queue.empty():
|
|
210
|
+
try:
|
|
211
|
+
message = input_queue.get(timeout=0.001)
|
|
212
|
+
if message is None or isinstance(message, str):
|
|
213
|
+
# Save current track.
|
|
214
|
+
if track_name is not None:
|
|
215
|
+
memory_tracks.append([track_name, current_track])
|
|
216
|
+
track_name = message
|
|
217
|
+
current_track = []
|
|
218
|
+
if message is None:
|
|
219
|
+
# Wrap up
|
|
220
|
+
tracker_exit = True
|
|
221
|
+
break
|
|
222
|
+
elif isinstance(message, list):
|
|
223
|
+
# Add time and memory data to current track
|
|
224
|
+
if track_name is not None:
|
|
225
|
+
current_track.append(message)
|
|
226
|
+
else:
|
|
227
|
+
raise TypeError(
|
|
228
|
+
"Track name must be passed to memory tracker prior to "
|
|
229
|
+
"sending data"
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
raise TypeError(
|
|
233
|
+
"Unrecognized message type in memory_tracker input queue: "
|
|
234
|
+
f"{message}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
except input_queue.Empty:
|
|
238
|
+
# input_queue.empty had not been updated
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
if not tracker_exit and track_name is not None:
|
|
242
|
+
# Save current time and memory usage
|
|
243
|
+
current_track.append(
|
|
244
|
+
MemoryTracker.get_time_mem_list(tracked_process)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Save the collected memory tracks
|
|
248
|
+
with open(yaml_path, "w", encoding="utf-8") as f:
|
|
249
|
+
yaml.dump(memory_tracks, f)
|
|
250
|
+
|
|
251
|
+
except psutil.NoSuchProcess:
|
|
252
|
+
# If the parent process stopped existing, we can
|
|
253
|
+
# safely assume that tracking is no longer needed
|
|
254
|
+
# NOTE: this only seems to be needed on Windows
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# This file was originally licensed under Apache 2.0. It has been modified.
|
|
259
|
+
# Modifications Copyright (c) 2025 AMD
|