lemonade-sdk 9.1.1__py3-none-any.whl

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