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,899 @@
1
+ from abc import ABC, abstractmethod
2
+ from datetime import datetime, timezone
3
+ import re
4
+ from typing import Tuple, Dict, List
5
+ import textwrap
6
+ from tabulate import tabulate
7
+ import lemonade.common.build as build
8
+ import lemonade.common.filesystem as fs
9
+ from lemonade.cache import Keys
10
+ from lemonade.tools.accuracy import LMEvalHarness
11
+ from lemonade.tools.huggingface.bench import HuggingfaceBench
12
+ from lemonade.tools.llamacpp.bench import LlamaCppBench
13
+ from lemonade.tools.mmlu import AccuracyMMLU
14
+ from lemonade.tools.oga.bench import OgaBench
15
+
16
+ # List of python packages for which to log the version
17
+ PYTHON_PACKAGES = ["onnxruntime", "transformers", "lemonade-sdk", "voe"]
18
+
19
+ # Key value in local build data dict
20
+ SW_VERSIONS = "sw_versions"
21
+
22
+ # Map datatype aliases to common names
23
+ dtype_aliases = {
24
+ "fp32": "float32",
25
+ "fp16": "float16",
26
+ }
27
+
28
+
29
+ ################################################################################
30
+ # HELPER FUNCTIONS
31
+ ################################################################################
32
+
33
+
34
+ def _to_list(x) -> List:
35
+ """Puts item in list if it is not already a list"""
36
+ if isinstance(x, list):
37
+ return x
38
+ return [x]
39
+
40
+
41
+ def _wrap(text: str, width: int) -> str:
42
+ """Wraps text cleanly to specified width"""
43
+ return "\n".join(textwrap.wrap(text, width=width))
44
+
45
+
46
+ def _merge_join(str1, str2) -> str:
47
+ """Joins a pair of strings with \n as long as both are non-empty, else skips the \n"""
48
+ return str1 + ("\n" if str1 and str2 else "") + str2
49
+
50
+
51
+ def _window_sum(data: list, n_windows: int) -> list:
52
+ """Sums data into n_windows windows"""
53
+ if n_windows <= 0:
54
+ return data
55
+ window_size = max(1, len(data) // n_windows)
56
+ summed_data = []
57
+ for i in range(0, len(data), window_size):
58
+ window_sum = sum(data[i : i + window_size])
59
+ summed_data.append(window_sum)
60
+ return summed_data
61
+
62
+
63
+ ################################################################################
64
+ # CLASSES THAT DESCRIBE TEXT TABLE COLUMNS
65
+ ################################################################################
66
+
67
+
68
+ # Table entry types
69
+ class TableColumn(ABC):
70
+
71
+ default_wrap = 80
72
+
73
+ @abstractmethod
74
+ def get_str(self, build_stats: dict, lean=False) -> str:
75
+ """Method used to return the string that goes in the table column for this build"""
76
+
77
+
78
+ class SimpleStat(TableColumn):
79
+ """These are for statistics already declared by the tool or basic build stats"""
80
+
81
+ def __init__(
82
+ self,
83
+ column_header,
84
+ stat,
85
+ format_str,
86
+ align="center",
87
+ omit_if_lean=False,
88
+ wrap=None,
89
+ stat_fn=None,
90
+ ):
91
+ self.column_header = column_header
92
+ self.stat = stat
93
+ self.format_str = format_str
94
+ self.align = align
95
+ self.omit_if_lean = omit_if_lean
96
+ self.wrap = wrap or self.default_wrap
97
+ self.stat_fn = stat_fn
98
+
99
+ def get_str(self, build_stats, lean=False):
100
+ if lean and self.omit_if_lean:
101
+ return None
102
+ data = build_stats.get(self.stat, None)
103
+ if data is None or (data == []):
104
+ return "-"
105
+ if self.stat_fn:
106
+ data = self.stat_fn(data)
107
+ cell_str = "\n".join(
108
+ [
109
+ _wrap("-" if x is None else f"{x:{self.format_str}}", self.wrap)
110
+ for x in _to_list(data)
111
+ ]
112
+ )
113
+ return cell_str
114
+
115
+
116
+ class DependentStat(TableColumn):
117
+ """
118
+ These are for statistics already declared by the tool or basic build stats that
119
+ rely on one or more additional stats to compute their value. The dependency is
120
+ embodied by the stat_fn function.
121
+ """
122
+
123
+ def __init__(
124
+ self,
125
+ column_header,
126
+ stats,
127
+ format_str,
128
+ align="center",
129
+ omit_if_lean=False,
130
+ wrap=None,
131
+ stat_fn=None,
132
+ ):
133
+ self.column_header = column_header
134
+ self.stats = stats
135
+ self.format_str = format_str
136
+ self.align = align
137
+ self.omit_if_lean = omit_if_lean
138
+ self.wrap = wrap or self.default_wrap
139
+ self.stat_fn = stat_fn
140
+
141
+ def get_str(self, build_stats, lean=False):
142
+ if lean and self.omit_if_lean:
143
+ return None
144
+ stats_data = [build_stats.get(stat, None) for stat in self.stats]
145
+ if self.stat_fn:
146
+ data = self.stat_fn(stats_data)
147
+ else:
148
+ data = stats_data[0]
149
+ if data is None or (data == []):
150
+ return "-"
151
+ cell_str = "\n".join(
152
+ [_wrap(f"{x:{self.format_str}}", self.wrap) for x in _to_list(data)]
153
+ )
154
+ return cell_str
155
+
156
+
157
+ class TimestampStat(SimpleStat):
158
+ """These are for timestamp statistics already declared by the tool or basic build stats"""
159
+
160
+ def get_str(self, build_stats, lean=False):
161
+ if lean and self.omit_if_lean:
162
+ return None
163
+ data = build_stats.get(self.stat, None)
164
+ if data is None:
165
+ return "-"
166
+ cell_str = data.strftime(self.format_str)
167
+ return cell_str
168
+
169
+
170
+ class MultiStat(TableColumn):
171
+ """
172
+ These are for string-values statistics already declared by the tool or basic build stats.
173
+ One or more stats will be put in the same cell.
174
+ """
175
+
176
+ def __init__(
177
+ self,
178
+ column_header,
179
+ stats,
180
+ format_str,
181
+ align="center",
182
+ omit_if_lean=False,
183
+ wrap=None,
184
+ ):
185
+ self.column_header = column_header
186
+ self.stats = _to_list(stats)
187
+ self.format_str = format_str
188
+ self.align = align
189
+ self.omit_if_lean = omit_if_lean
190
+ self.wrap = wrap or self.default_wrap
191
+
192
+ def get_str(self, build_stats, lean=False):
193
+ if lean and self.omit_if_lean:
194
+ return None
195
+ cell_str_list = []
196
+ for stat in self.stats:
197
+ if stat is None:
198
+ cell_str_list.append("") # will be a blank line
199
+ else:
200
+ data = build_stats.get(stat, None)
201
+ if data is None:
202
+ cell_str_list.append("-") # missing value
203
+ else:
204
+ cell_str_list.append(_wrap(f"{data:{self.format_str}}", self.wrap))
205
+ cell_str = "\n".join(cell_str_list)
206
+ return cell_str
207
+
208
+
209
+ class StatWithSD(TableColumn):
210
+ """These are for statistics already declared by the tool that have an
211
+ accompanying standard deviation statistic"""
212
+
213
+ def __init__(
214
+ self,
215
+ column_header,
216
+ stat,
217
+ sd_stat,
218
+ format_str,
219
+ align="center",
220
+ omit_if_lean=False,
221
+ ):
222
+ self.column_header = column_header
223
+ self.stat = stat
224
+ self.sd_stat = sd_stat
225
+ self.format_str = format_str
226
+ self.align = align
227
+ self.omit_if_lean = omit_if_lean
228
+
229
+ def get_str(self, build_stats, lean=False):
230
+ if lean and self.omit_if_lean:
231
+ return None
232
+ if not self.stat in build_stats:
233
+ return ""
234
+ data = build_stats[self.stat]
235
+ sd_data = build_stats.get(self.sd_stat, None)
236
+ if sd_data is None:
237
+ data = _to_list(data)
238
+ sd_data = [None] * len(data)
239
+ cell_str = "\n".join(
240
+ [
241
+ (
242
+ f"{x:{self.format_str}} +/- {sd_x:{self.format_str}}"
243
+ if not sd_x is None
244
+ else f"{x:{self.format_str}}"
245
+ )
246
+ for x, sd_x in zip(_to_list(data), _to_list(sd_data))
247
+ ]
248
+ )
249
+ return cell_str
250
+
251
+
252
+ class AdditionalStat(TableColumn):
253
+ """These are for statistics not declared by the tool. A regular expression is defined
254
+ and all statistics matching the regular expression will be put in the cell"""
255
+
256
+ def __init__(
257
+ self,
258
+ column_header,
259
+ regexp,
260
+ lean_regexp,
261
+ format_str,
262
+ align="center",
263
+ omit_if_lean=False,
264
+ wrap=None,
265
+ ):
266
+ self.column_header = column_header
267
+ self.regexp = regexp
268
+ self.lean_regexp = lean_regexp or regexp
269
+ self.format_str = format_str
270
+ self.align = align
271
+ self.omit_if_lean = omit_if_lean
272
+ self.wrap = wrap or self.default_wrap
273
+
274
+ def get_str(self, build_stats, lean=False):
275
+ if lean and self.omit_if_lean:
276
+ return None
277
+ # Find stats in build_stats that match the regexp for this column
278
+ regexp = self.lean_regexp if lean else self.regexp
279
+ stats = []
280
+ for stat in build_stats.keys():
281
+ if re.match(regexp, stat):
282
+ stats.append(stat)
283
+ # Construct the cell entry
284
+ cell_entry = []
285
+ for stat in stats:
286
+ if stat.endswith("_units"):
287
+ continue
288
+ units = build_stats.get(stat + "_units", None)
289
+ value = f"{build_stats[stat]:{self.format_str}}" + (
290
+ " " + units if not units is None else ""
291
+ )
292
+ cell_entry += [_wrap(stat, self.wrap), value]
293
+ return "\n".join(cell_entry)
294
+
295
+
296
+ class DictListStat(TableColumn):
297
+ """
298
+ A statistic that is a list of dicts and values from a given list of keys will be
299
+ pulled out of each dict and placed in the cell
300
+ """
301
+
302
+ def __init__(
303
+ self,
304
+ column_header,
305
+ statistic_name,
306
+ key_format_list,
307
+ align="center",
308
+ omit_if_lean=False,
309
+ wrap=None,
310
+ ):
311
+ self.column_header = column_header
312
+ self.statistic_name = statistic_name
313
+ self.key_format_list = key_format_list
314
+ self.align = align
315
+ self.omit_if_lean = omit_if_lean
316
+ self.wrap = wrap or self.default_wrap
317
+
318
+ def get_str(self, build_stats, lean=False):
319
+ if lean and self.omit_if_lean:
320
+ return None
321
+ stat = build_stats.get(self.statistic_name, None)
322
+ if not stat:
323
+ return ""
324
+ cell_entry = []
325
+ for stat_dict in stat:
326
+ line = [
327
+ format_str.format(stat_dict[key])
328
+ for key, format_str in self.key_format_list
329
+ ]
330
+ cell_entry.append(" ".join(line))
331
+ return "\n".join(cell_entry)
332
+
333
+ def get_keys(self):
334
+ return [self.statistic_name]
335
+
336
+
337
+ ################################################################################
338
+ # ABSTRACT BASE CLASS FOR DEFINING A TABLE
339
+ ################################################################################
340
+
341
+
342
+ class Table(ABC):
343
+
344
+ table_descriptor = {}
345
+
346
+ def __init__(self):
347
+ self.all_builds = []
348
+ self.all_stats = []
349
+ self.lean = False
350
+ self.tools = None
351
+ self.merge_test_fn = lambda b1, b2: False
352
+
353
+ def find_builds(self, cache_dirs: List[str], model: str = None):
354
+ """
355
+ Finds all the folder names of all the builds in the given list of cache directories.
356
+ Each (cache_dir, build_folder) tuple is appended to the all_builds list attribute.
357
+ If a model string is given, then builds will only be saved if the build folder name
358
+ contains the model string (case-insensitive).
359
+ """
360
+ self.all_builds = []
361
+ for cache_dir in cache_dirs:
362
+ # Get all of the directories within the build cache
363
+ builds = fs.get_available_builds(cache_dir)
364
+
365
+ # Filter out any that don't have the right model in the name
366
+ if model is not None:
367
+ builds = [
368
+ (cache_dir, build_name)
369
+ for build_name in builds
370
+ if model.lower() in build_name.lower()
371
+ ]
372
+ else:
373
+ builds = [(cache_dir, build_name) for build_name in builds]
374
+
375
+ self.all_builds += builds
376
+
377
+ def load_stats(self):
378
+ """
379
+ Loads the build stats dict from each build into the all_stats list attribute,
380
+ one dict per build.
381
+ """
382
+ self.all_stats = []
383
+
384
+ # Add results from all user-provided cache folders
385
+ for cache_dir, build_name in self.all_builds:
386
+ model_stats = fs.Stats(cache_dir, build_name).stats
387
+
388
+ if self.include_stats(model_stats):
389
+ self.post_process_stats(model_stats)
390
+
391
+ def include_stats(self, _model_stats: Dict) -> bool:
392
+ """
393
+ Returns True if stats from this model build should be part of the table
394
+ """
395
+ return True
396
+
397
+ def post_process_stats(self, model_stats: Dict) -> Dict:
398
+ """
399
+ Create a dict of stats from these model stats and append to the all_stats list attribute.
400
+ Make any necessary modifications along the way
401
+ """
402
+ evaluation_stats = {}
403
+ for key, value in model_stats.items():
404
+ # If a build or benchmark is still marked as "incomplete" at
405
+ # reporting time, it must have been killed by a timeout,
406
+ # out-of-memory (OOM), or some other uncaught exception
407
+ if (
408
+ key == fs.Keys.BUILD_STATUS or fs.Keys.TOOL_STATUS in key
409
+ ) and value == build.FunctionStatus.INCOMPLETE:
410
+ value = build.FunctionStatus.KILLED
411
+
412
+ # Add stats ensuring that those are all in lower case
413
+ evaluation_stats[key.lower()] = value
414
+
415
+ self.all_stats.append(evaluation_stats)
416
+
417
+ def sort_stats(self):
418
+ """Sorts the stats list used the class sort_key key function"""
419
+ self.all_stats.sort(key=self.sort_key)
420
+
421
+ def __str__(self) -> str:
422
+ """Returns table as a string"""
423
+ #
424
+ # Construct headers and column alignment lists
425
+ #
426
+ headers = []
427
+ col_align = []
428
+
429
+ # First headers
430
+ first_columns = self.table_descriptor.get("first_columns", [])
431
+ for column in first_columns:
432
+ if not (self.lean and column.omit_if_lean):
433
+ headers.append(column.column_header)
434
+ col_align += (column.align,)
435
+
436
+ # Per tool headers
437
+ tool_columns = self.table_descriptor.get("tool_columns", {})
438
+ tools = self.tools or []
439
+ for tool in tools:
440
+
441
+ # Don't duplicate columns if tool has an alternate tool listed
442
+ if isinstance(tool_columns[tool], type):
443
+ referenced_tool = tool_columns[tool]
444
+ if referenced_tool in tools:
445
+ continue
446
+ # Use the column specification of the referenced tool
447
+ tool = referenced_tool
448
+
449
+ for column in tool_columns[tool]:
450
+ if not (self.lean and column.omit_if_lean):
451
+ headers.append(column.column_header)
452
+ col_align += (column.align,)
453
+
454
+ # Stat column headers
455
+ stat_columns = self.table_descriptor.get("stat_columns", [])
456
+ stat_columns_include = []
457
+ for column in stat_columns:
458
+ # Check to see that at least one build has data for the column
459
+ keep_column = False
460
+ if not (self.lean and column.omit_if_lean):
461
+ keys = column.get_keys()
462
+ for build_stats in self.all_stats:
463
+ found = [(key in build_stats) for key in keys]
464
+ if any(found):
465
+ keep_column = True
466
+ headers.append(column.column_header)
467
+ col_align += (column.align,)
468
+ break
469
+ stat_columns_include.append(keep_column)
470
+ stat_columns = [
471
+ column
472
+ for column, include in zip(stat_columns, stat_columns_include)
473
+ if include
474
+ ]
475
+
476
+ # Final headers
477
+ last_columns = self.table_descriptor.get("last_columns", [])
478
+ for column in last_columns:
479
+ if not (self.lean and column.omit_if_lean):
480
+ headers.append(column.column_header)
481
+ col_align += (column.align,)
482
+
483
+ #
484
+ # Construct table rows
485
+ #
486
+ rows = []
487
+ last_row = None
488
+ last_build_stats = None
489
+ for build_stats in self.all_stats:
490
+ row = []
491
+
492
+ # First columns
493
+ first_columns_count = 0
494
+ for entry in first_columns:
495
+ entry_str = entry.get_str(build_stats, self.lean)
496
+ if entry_str is not None:
497
+ row.append(entry_str)
498
+ first_columns_count += 1
499
+
500
+ # Per tool columns
501
+ for tool in tools:
502
+
503
+ if not isinstance(tool_columns[tool], list):
504
+ referenced_tool = tool_columns[tool]
505
+ if referenced_tool in tools:
506
+ continue
507
+ tool = referenced_tool
508
+
509
+ for entry in tool_columns[tool]:
510
+ entry_str = entry.get_str(build_stats, self.lean)
511
+ if entry_str is not None:
512
+ row.append(entry_str)
513
+
514
+ # Per stat columns
515
+ for entry in stat_columns:
516
+ entry_str = entry.get_str(build_stats, self.lean)
517
+ if entry_str is not None:
518
+ row.append(entry_str)
519
+
520
+ # Final columns
521
+ last_columns_count = 0
522
+ for entry in last_columns:
523
+ entry_str = entry.get_str(build_stats, self.lean)
524
+ if entry_str is not None:
525
+ row.append(entry_str)
526
+ last_columns_count += 1
527
+
528
+ # See if this row should be merged with the last row
529
+ if last_build_stats and self.merge_test_fn(last_build_stats, build_stats):
530
+ # Merge with last row
531
+ for col in range(0, first_columns_count):
532
+ # If identical, don't duplicate
533
+ if last_row[col] != row[col]:
534
+ last_row[col] = _merge_join(last_row[col], row[col])
535
+ for col in range(first_columns_count, len(row) - last_columns_count):
536
+ # Allow duplicates
537
+ last_row[col] = _merge_join(last_row[col], row[col])
538
+ for col in range(len(row) - last_columns_count, len(row)):
539
+ # If identical, don't duplicate
540
+ if last_row[col] != row[col]:
541
+ last_row[col] = _merge_join(last_row[col], row[col])
542
+ else:
543
+ rows.append(row)
544
+ last_row = row
545
+ last_build_stats = build_stats
546
+
547
+ if not rows:
548
+ rows = [["NO DATA"] + [" "] * (len(headers) - 1)]
549
+
550
+ return tabulate(rows, headers=headers, tablefmt="grid", colalign=col_align)
551
+
552
+ def create_csv_report(self):
553
+
554
+ # Find all keys and use as column headers
555
+ column_headers = set()
556
+ for build_stats in self.all_stats:
557
+ column_headers |= set(build_stats.keys())
558
+
559
+ # Sort all columns alphabetically
560
+ column_headers = sorted(column_headers)
561
+
562
+ # Fill in blanks for each build
563
+ report: List[Dict] = []
564
+ for build_stats in self.all_stats:
565
+ result = {k: "-" for k in column_headers}
566
+ for k, v in build_stats.items():
567
+ result[k] = v
568
+ report.append(result)
569
+
570
+ return report, column_headers
571
+
572
+ @staticmethod
573
+ def sort_key(build_stats: dict) -> Tuple:
574
+ """Sort by timestamp. If timestamp is missing, then list first."""
575
+ return (
576
+ build_stats[fs.Keys.TIMESTAMP].strftime("%Y%m%d%H:%M:%S")
577
+ if fs.Keys.TIMESTAMP in build_stats
578
+ else "-"
579
+ )
580
+
581
+ @staticmethod
582
+ def get_report_name(prefix: str = "") -> str:
583
+ """
584
+ Returns the name of the .csv report
585
+ """
586
+ day = datetime.now().day
587
+ month = datetime.now().month
588
+ year = datetime.now().year
589
+ date_key = f"{year}-{str(month).zfill(2)}-{str(day).zfill(2)}"
590
+ return f"{prefix}{date_key}.csv"
591
+
592
+
593
+ ################################################################################
594
+ # TABLE CLASS FOR BASIC LEMONADE REPORT
595
+ ################################################################################
596
+
597
+
598
+ class LemonadeTable(Table):
599
+
600
+ table_descriptor = {
601
+ "first_columns": [
602
+ TimestampStat("Timestamp", fs.Keys.TIMESTAMP, "%Y-%m-%d\n%H:%M:%S"),
603
+ SimpleStat("Build Name", fs.Keys.BUILD_NAME, "s"),
604
+ SimpleStat("Tools\nSequence", fs.Keys.SELECTED_SEQUENCE_OF_TOOLS, "s"),
605
+ SimpleStat("Build\nStatus", fs.Keys.BUILD_STATUS, "s"),
606
+ ],
607
+ }
608
+
609
+
610
+ ################################################################################
611
+ # TABLE CLASS FOR LEMONADE PERFORMANCE REPORT
612
+ ################################################################################
613
+
614
+
615
+ class LemonadePerfTable(Table):
616
+
617
+ table_descriptor = {
618
+ "first_columns": [
619
+ TimestampStat("Timestamp", fs.Keys.TIMESTAMP, "%Y-%m-%d\n%H:%M:%S"),
620
+ # SimpleStat("Timestamp", fs.Keys.TIMESTAMP, "s"),
621
+ MultiStat(
622
+ "Model\n\nDevice\nData Type",
623
+ [
624
+ Keys.CHECKPOINT,
625
+ None,
626
+ Keys.DEVICE,
627
+ Keys.DTYPE,
628
+ None,
629
+ Keys.LOCAL_MODEL_FOLDER,
630
+ ],
631
+ "s",
632
+ wrap=35,
633
+ ),
634
+ ],
635
+ "tool_columns": {
636
+ OgaBench: [
637
+ SimpleStat(_wrap("Prompt Len (Tokens)", 8), Keys.PROMPT_TOKENS, "d"),
638
+ StatWithSD(
639
+ _wrap("Time to First Token (sec)", 8),
640
+ Keys.SECONDS_TO_FIRST_TOKEN,
641
+ Keys.STD_DEV_SECONDS_TO_FIRST_TOKEN,
642
+ ".2f",
643
+ ),
644
+ StatWithSD(
645
+ _wrap("Prefill Tokens per Second", 8),
646
+ Keys.PREFILL_TOKENS_PER_SECOND,
647
+ Keys.STD_DEV_PREFILL_TOKENS_PER_SECOND,
648
+ ".2f",
649
+ ),
650
+ StatWithSD(
651
+ _wrap("Tokens per Second", 8),
652
+ Keys.TOKEN_GENERATION_TOKENS_PER_SECOND,
653
+ Keys.STD_DEV_TOKENS_PER_SECOND,
654
+ ".2f",
655
+ ),
656
+ DependentStat(
657
+ _wrap("Total Generated Tokens", 9),
658
+ [Keys.RESPONSE_TOKENS, Keys.PROMPT_TOKENS],
659
+ "d",
660
+ stat_fn=lambda x: _window_sum(
661
+ _to_list(x[0]), n_windows=len(_to_list(x[1]))
662
+ ),
663
+ ),
664
+ SimpleStat(
665
+ _wrap("Memory Used (GB)", 8), Keys.MAX_MEMORY_USED_GBYTE, ".3f"
666
+ ),
667
+ ],
668
+ HuggingfaceBench: OgaBench,
669
+ LlamaCppBench: OgaBench,
670
+ AccuracyMMLU: [
671
+ AdditionalStat(
672
+ "MMLU",
673
+ fs.Keys.AVERAGE_MMLU_ACCURACY + "|^mmlu_",
674
+ fs.Keys.AVERAGE_MMLU_ACCURACY,
675
+ ".2f",
676
+ )
677
+ ],
678
+ LMEvalHarness: [
679
+ AdditionalStat(
680
+ "EleutherAI\nLM Evaluation",
681
+ "^lm_eval_",
682
+ "^lm_eval_",
683
+ ".1f",
684
+ )
685
+ ],
686
+ },
687
+ "stat_columns": [],
688
+ "last_columns": [
689
+ SimpleStat(
690
+ "System Info",
691
+ fs.Keys.SYSTEM_INFO,
692
+ "s",
693
+ "left",
694
+ omit_if_lean=True,
695
+ wrap=50,
696
+ ),
697
+ SimpleStat(
698
+ "Software Versions",
699
+ SW_VERSIONS,
700
+ "s",
701
+ "left",
702
+ omit_if_lean=True,
703
+ wrap=45,
704
+ ),
705
+ ],
706
+ }
707
+
708
+ basic_build_stats = [
709
+ Keys.CHECKPOINT,
710
+ Keys.DEVICE,
711
+ Keys.DTYPE,
712
+ Keys.RYZEN_AI_VERSION_INFO,
713
+ fs.Keys.TIMESTAMP,
714
+ fs.Keys.SYSTEM_INFO,
715
+ ]
716
+
717
+ def __init__(
718
+ self,
719
+ device: str = None,
720
+ dtype: str = None,
721
+ model: str = None,
722
+ days: int = None,
723
+ merge: bool = False,
724
+ lean: bool = False,
725
+ ):
726
+ super().__init__()
727
+ self.device = device
728
+ if dtype:
729
+ dtype = dtype.lower()
730
+ dtype = dtype_aliases.get(dtype, dtype)
731
+ self.dtype = dtype
732
+ self.model = model
733
+ self.days = days
734
+ self.lean = lean
735
+ self.tools_found_set = set()
736
+ self.include_stats_filter = {
737
+ Keys.DEVICE: self.device,
738
+ Keys.CHECKPOINT: self.model,
739
+ }
740
+ if merge:
741
+ self.merge_test_fn = LemonadePerfTable.matching_builds
742
+
743
+ def load_stats(self):
744
+ super().load_stats()
745
+ self.tools = list(self.tools_found_set)
746
+ self.tools.sort(key=lambda tool_class: tool_class.unique_name)
747
+
748
+ def include_stats(self, model_stats) -> bool:
749
+ """
750
+ Returns True if the build was successful and matches
751
+ the criteria specified, else returns False.
752
+ """
753
+ # Filter out builds that are incomplete
754
+ if (
755
+ not model_stats.get(fs.Keys.BUILD_STATUS, None)
756
+ == build.FunctionStatus.SUCCESSFUL
757
+ ):
758
+ return False
759
+
760
+ # Filter out build if it doesn't match specified dtype
761
+ if self.dtype:
762
+ build_dtype = model_stats.get(Keys.DTYPE, "").lower()
763
+ build_dtype = dtype_aliases.get(build_dtype, build_dtype)
764
+ if self.dtype not in build_dtype:
765
+ return False
766
+
767
+ # Filter out build if it doesn't match specified device or model
768
+ for key, value in self.include_stats_filter.items():
769
+ if value is not None:
770
+ model_value = model_stats.get(key, "")
771
+ if model_value is None or value.lower() not in model_value.lower():
772
+ return False
773
+
774
+ # Filter out build if it is too old
775
+ if not self.days is None:
776
+ build_day = model_stats[fs.Keys.TIMESTAMP]
777
+ today = datetime.now(timezone.utc)
778
+ delta = today - build_day
779
+ if delta.days > self.days:
780
+ return False
781
+
782
+ # All tests passed
783
+ return True
784
+
785
+ def post_process_stats(self, model_stats) -> bool:
786
+ tool_columns = self.table_descriptor["tool_columns"]
787
+ data = {}
788
+
789
+ for tool in tool_columns.keys():
790
+ tool_status_key = fs.Keys.TOOL_STATUS + ":" + tool.unique_name
791
+ if (
792
+ model_stats.get(tool_status_key, None)
793
+ == build.FunctionStatus.SUCCESSFUL
794
+ ):
795
+ # Successful build of this tool, so remember this tool
796
+ self.tools_found_set.add(tool)
797
+
798
+ # Extract the declared tool stats
799
+ data = data | {
800
+ stat: model_stats.get(stat, None) for stat in tool().status_stats
801
+ }
802
+
803
+ # Find if there are any additional stats for this tool
804
+ # First see if this tool refers to another tool
805
+ if not isinstance(tool_columns[tool], list):
806
+ tool = tool_columns[tool]
807
+ regexp_list = [
808
+ stat.regexp
809
+ for stat in tool_columns[tool]
810
+ if isinstance(stat, AdditionalStat)
811
+ ]
812
+ match_expr = "(?:% s)" % "|".join(regexp_list)
813
+ additional_stats = [
814
+ stat for stat in model_stats.keys() if re.match(match_expr, stat)
815
+ ]
816
+ data = data | {stat: model_stats[stat] for stat in additional_stats}
817
+
818
+ if not data:
819
+ # No matching tools successfully completed in this build
820
+ return
821
+
822
+ #
823
+ # Add basic build stats
824
+ #
825
+ for key in self.basic_build_stats:
826
+ data[key] = model_stats.get(key, "")
827
+
828
+ # Create a new entry with Driver Versions and relevant Python Packages
829
+ sw_versions = []
830
+ if "Driver Versions" in data[fs.Keys.SYSTEM_INFO]:
831
+ sw_versions += [
832
+ key + ": " + value
833
+ for key, value in data[fs.Keys.SYSTEM_INFO]["Driver Versions"].items()
834
+ ]
835
+ if "Python Packages" in data[fs.Keys.SYSTEM_INFO]:
836
+ sw_versions += [
837
+ pkg
838
+ for pkg in data[fs.Keys.SYSTEM_INFO]["Python Packages"]
839
+ if any(name in pkg for name in PYTHON_PACKAGES)
840
+ ]
841
+ if isinstance(data[Keys.RYZEN_AI_VERSION_INFO], dict):
842
+ sw_versions += [
843
+ "Ryzen AI: " + value
844
+ for key, value in data[Keys.RYZEN_AI_VERSION_INFO].items()
845
+ if value is not None and "artifacts" in value
846
+ ]
847
+ data[SW_VERSIONS] = sw_versions
848
+
849
+ # Exclude Python Packages and Driver Versions from System Info
850
+ system_info = [
851
+ key + ": " + str(value)
852
+ for key, value in data[fs.Keys.SYSTEM_INFO].items()
853
+ if key not in ["Python Packages", "Driver Versions"]
854
+ ]
855
+ data[fs.Keys.SYSTEM_INFO] = system_info
856
+
857
+ self.all_stats.append(data)
858
+
859
+ @staticmethod
860
+ def sort_key(build_stats: dict) -> Tuple:
861
+ return tuple(
862
+ build_stats[key]
863
+ for key in [
864
+ Keys.CHECKPOINT,
865
+ Keys.DEVICE,
866
+ Keys.DTYPE,
867
+ fs.Keys.SYSTEM_INFO,
868
+ SW_VERSIONS,
869
+ fs.Keys.TIMESTAMP,
870
+ ]
871
+ )
872
+
873
+ @staticmethod
874
+ def matching_builds(build_stats_1: Dict, build_stats_2: Dict) -> bool:
875
+ """
876
+ Returns true if the two builds have matching model, device, datatype,
877
+ system info and SW versions
878
+ """
879
+ merge_key_list = [
880
+ Keys.CHECKPOINT,
881
+ Keys.DEVICE,
882
+ Keys.DTYPE,
883
+ fs.Keys.SYSTEM_INFO,
884
+ SW_VERSIONS,
885
+ ]
886
+ dict_1 = {key: build_stats_1[key] for key in merge_key_list}
887
+ dict_2 = {key: build_stats_2[key] for key in merge_key_list}
888
+
889
+ return dict_1 == dict_2
890
+
891
+ @staticmethod
892
+ def get_report_name() -> str:
893
+ current_time = datetime.now(timezone.utc)
894
+ timestamp = current_time.strftime("%Y-%m-%d-%H%M%S")
895
+ return f"{timestamp}_perf.csv"
896
+
897
+
898
+ # This file was originally licensed under Apache 2.0. It has been modified.
899
+ # Modifications Copyright (c) 2025 AMD