jax-hpc-profiler 0.2.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.
- jax_hpc_profiler/__init__.py +9 -0
- jax_hpc_profiler/create_argparse.py +158 -0
- jax_hpc_profiler/main.py +57 -0
- jax_hpc_profiler/plotting.py +214 -0
- jax_hpc_profiler/timer.py +185 -0
- jax_hpc_profiler/utils.py +396 -0
- jax_hpc_profiler-0.2.0.dist-info/LICENSE +674 -0
- jax_hpc_profiler-0.2.0.dist-info/METADATA +847 -0
- jax_hpc_profiler-0.2.0.dist-info/RECORD +12 -0
- jax_hpc_profiler-0.2.0.dist-info/WHEEL +5 -0
- jax_hpc_profiler-0.2.0.dist-info/entry_points.txt +2 -0
- jax_hpc_profiler-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict, List, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from matplotlib.axes import Axes
|
|
8
|
+
|
|
9
|
+
def inspect_data(dataframes: Dict[str, pd.DataFrame]):
|
|
10
|
+
"""
|
|
11
|
+
Inspect the dataframes.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
dataframes : Dict[str, pd.DataFrame]
|
|
16
|
+
Dictionary of method names to dataframes.
|
|
17
|
+
"""
|
|
18
|
+
print("=" * 80)
|
|
19
|
+
print("Inspecting dataframes...")
|
|
20
|
+
print("=" * 80)
|
|
21
|
+
for method, df in dataframes.items():
|
|
22
|
+
print(f"Method: {method}")
|
|
23
|
+
inspect_df(df)
|
|
24
|
+
print("=" * 80)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def inspect_df(df: pd.DataFrame):
|
|
28
|
+
"""
|
|
29
|
+
Inspect the dataframe.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
df : pd.DataFrame
|
|
34
|
+
The dataframe to inspect.
|
|
35
|
+
"""
|
|
36
|
+
print(df.to_markdown())
|
|
37
|
+
print("-" * 80)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
params_dict = {
|
|
41
|
+
"%pn%": "%plot_name%",
|
|
42
|
+
"%m%": "%method_name%",
|
|
43
|
+
"%n%": "%node%",
|
|
44
|
+
"%b%": "%backend%",
|
|
45
|
+
"%f%": "%function%",
|
|
46
|
+
"%cn%": "%column_name%",
|
|
47
|
+
"%pr%": "%precision%",
|
|
48
|
+
"%p%": "%decomposition%",
|
|
49
|
+
"%d%": "%data_size%",
|
|
50
|
+
"%g%": "%nb_gpu%"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def expand_label(label_template: str, params: dict) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Expand the label template with the provided parameters.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
label_template : str
|
|
61
|
+
The label template with placeholders.
|
|
62
|
+
params : dict
|
|
63
|
+
The dictionary with actual values to replace placeholders.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
str
|
|
68
|
+
The expanded label.
|
|
69
|
+
"""
|
|
70
|
+
for key, value in params_dict.items():
|
|
71
|
+
label_template = label_template.replace(key, value)
|
|
72
|
+
|
|
73
|
+
for key, value in params.items():
|
|
74
|
+
label_template = label_template.replace(f"%{key}%", str(value))
|
|
75
|
+
return label_template
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def plot_with_pdims_strategy(ax: Axes, df: pd.DataFrame, method: str,
|
|
79
|
+
pdims_strategy: List[str],
|
|
80
|
+
print_decompositions: bool, x_col: str,
|
|
81
|
+
y_col: str, label_template: str):
|
|
82
|
+
"""
|
|
83
|
+
Plot the data based on the pdims strategy.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
ax : Axes
|
|
88
|
+
The axes to plot on.
|
|
89
|
+
df : pd.DataFrame
|
|
90
|
+
The dataframe to plot.
|
|
91
|
+
method : str
|
|
92
|
+
The method name.
|
|
93
|
+
backend : str
|
|
94
|
+
The backend name.
|
|
95
|
+
nodes_in_label : bool
|
|
96
|
+
Whether to include node names in labels.
|
|
97
|
+
pdims_strategy : List[str]
|
|
98
|
+
Strategy for plotting pdims.
|
|
99
|
+
print_decompositions : bool
|
|
100
|
+
Whether to print decompositions on the plot.
|
|
101
|
+
x_col : str
|
|
102
|
+
The column name for the x-axis values.
|
|
103
|
+
x_label : str
|
|
104
|
+
The label for the x-axis.
|
|
105
|
+
y_label : str
|
|
106
|
+
The label for the y-axis.
|
|
107
|
+
label_template : str
|
|
108
|
+
Template for plot labels with placeholders.
|
|
109
|
+
"""
|
|
110
|
+
label_params = {
|
|
111
|
+
"plot_name": y_col,
|
|
112
|
+
"method_name": method,
|
|
113
|
+
"backend": df['backend'].values[0],
|
|
114
|
+
"node": df['nodes'].values[0],
|
|
115
|
+
"precision": df['precision'].values[0],
|
|
116
|
+
"function": df['function'].values[0],
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if 'plot_fastest' in pdims_strategy:
|
|
120
|
+
df_decomp = df.groupby([x_col])
|
|
121
|
+
|
|
122
|
+
# Sort all and keep fastest
|
|
123
|
+
sorted_dfs = []
|
|
124
|
+
for _, group in df_decomp:
|
|
125
|
+
group.sort_values(by=[y_col], inplace=True, ascending=False)
|
|
126
|
+
sorted_dfs.append(group.iloc[0])
|
|
127
|
+
sorted_df = pd.DataFrame(sorted_dfs)
|
|
128
|
+
label_params.update({
|
|
129
|
+
"decomposition":
|
|
130
|
+
f"{group['px'].values[0]}x{group['py'].values[0]}"
|
|
131
|
+
})
|
|
132
|
+
label = expand_label(label_template, label_params)
|
|
133
|
+
ax.plot(sorted_df[x_col].values,
|
|
134
|
+
sorted_df[y_col],
|
|
135
|
+
marker='o',
|
|
136
|
+
linestyle='-',
|
|
137
|
+
label=label)
|
|
138
|
+
# TODO(wassim) : this is not working very well
|
|
139
|
+
if print_decompositions:
|
|
140
|
+
for j, (px, py) in enumerate(zip(sorted_df['px'],
|
|
141
|
+
sorted_df['py'])):
|
|
142
|
+
ax.annotate(
|
|
143
|
+
f"{px}x{py}",
|
|
144
|
+
(sorted_df[x_col].values[j], sorted_df[y_col].values[j]),
|
|
145
|
+
textcoords="offset points",
|
|
146
|
+
xytext=(0, 10),
|
|
147
|
+
ha='center',
|
|
148
|
+
color='red' if j == 0 else 'white')
|
|
149
|
+
return sorted_df[x_col].values, sorted_df[y_col].values
|
|
150
|
+
|
|
151
|
+
elif any(strategy in pdims_strategy
|
|
152
|
+
for strategy in ['plot_all', 'slab_yz', 'slab_xy', 'pencils']):
|
|
153
|
+
df_decomp = df.groupby(['decomp'])
|
|
154
|
+
x_values = []
|
|
155
|
+
y_values = []
|
|
156
|
+
for _, group in df_decomp:
|
|
157
|
+
group.drop_duplicates(subset=[x_col, 'decomp'],
|
|
158
|
+
keep='last',
|
|
159
|
+
inplace=True)
|
|
160
|
+
group.sort_values(by=[x_col], inplace=True, ascending=False)
|
|
161
|
+
# filter decomp based on pdims_strategy
|
|
162
|
+
if 'plot_all' not in pdims_strategy and group['decomp'].values[
|
|
163
|
+
0] not in pdims_strategy:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
label_params.update({"decomposition": group['decomp'].values[0]})
|
|
167
|
+
label = expand_label(label_template, label_params)
|
|
168
|
+
ax.plot(group[x_col].values,
|
|
169
|
+
group[y_col],
|
|
170
|
+
marker='o',
|
|
171
|
+
linestyle='-',
|
|
172
|
+
label=label)
|
|
173
|
+
x_values.extend(group[x_col].values)
|
|
174
|
+
y_values.extend(group[y_col].values)
|
|
175
|
+
return x_values, y_values
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def concatenate_csvs(root_dir: str, output_dir: str):
|
|
179
|
+
"""
|
|
180
|
+
Concatenate CSV files and remove duplicates by GPU type.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
root_dir : str
|
|
185
|
+
Root directory containing CSV files.
|
|
186
|
+
output_dir : str
|
|
187
|
+
Output directory to save concatenated CSV files.
|
|
188
|
+
"""
|
|
189
|
+
# Iterate over each GPU type directory
|
|
190
|
+
for gpu in os.listdir(root_dir):
|
|
191
|
+
gpu_dir = os.path.join(root_dir, gpu)
|
|
192
|
+
|
|
193
|
+
# Check if the GPU directory exists and is a directory
|
|
194
|
+
if not os.path.isdir(gpu_dir):
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Dictionary to hold combined dataframes for each CSV file name
|
|
198
|
+
combined_dfs = {}
|
|
199
|
+
|
|
200
|
+
# List CSV in directory and subdirectories
|
|
201
|
+
for root, dirs, files in os.walk(gpu_dir):
|
|
202
|
+
for file in files:
|
|
203
|
+
if file.endswith('.csv'):
|
|
204
|
+
csv_file_path = os.path.join(root, file)
|
|
205
|
+
print(f'Concatenating {csv_file_path}...')
|
|
206
|
+
df = pd.read_csv(
|
|
207
|
+
csv_file_path,
|
|
208
|
+
header=None,
|
|
209
|
+
names=[
|
|
210
|
+
"function", "precision", "x", "y", "z", "px",
|
|
211
|
+
"py", "backend", "nodes", "jit_time", "min_time",
|
|
212
|
+
"max_time", "mean_time", "std_div", "last_time",
|
|
213
|
+
"generated_code", "argument_size", "output_size",
|
|
214
|
+
"temp_size", "flops"
|
|
215
|
+
],
|
|
216
|
+
index_col=False)
|
|
217
|
+
if file not in combined_dfs:
|
|
218
|
+
combined_dfs[file] = df
|
|
219
|
+
else:
|
|
220
|
+
combined_dfs[file] = pd.concat(
|
|
221
|
+
[combined_dfs[file], df], ignore_index=True)
|
|
222
|
+
|
|
223
|
+
# Remove duplicates based on specified columns and save
|
|
224
|
+
for file_name, combined_df in combined_dfs.items():
|
|
225
|
+
combined_df.drop_duplicates(subset=[
|
|
226
|
+
"function", "precision", "x", "y", "z", "px", "py", "backend",
|
|
227
|
+
"nodes"
|
|
228
|
+
],
|
|
229
|
+
keep='last',
|
|
230
|
+
inplace=True)
|
|
231
|
+
|
|
232
|
+
gpu_output_dir = os.path.join(output_dir, gpu)
|
|
233
|
+
if not os.path.exists(gpu_output_dir):
|
|
234
|
+
print(f"Creating directory {gpu_output_dir}")
|
|
235
|
+
os.makedirs(gpu_output_dir)
|
|
236
|
+
|
|
237
|
+
output_file = os.path.join(gpu_output_dir, file_name)
|
|
238
|
+
print(f"Writing file to {output_file}...")
|
|
239
|
+
combined_df.to_csv(output_file, index=False)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def clean_up_csv(
|
|
243
|
+
csv_files: List[str],
|
|
244
|
+
precisions: Optional[List[str]] = None,
|
|
245
|
+
function_names: Optional[List[str]] = None,
|
|
246
|
+
gpus: Optional[List[int]] = None,
|
|
247
|
+
data_sizes: Optional[List[int]] = None,
|
|
248
|
+
pdims: Optional[List[str]] = None,
|
|
249
|
+
pdims_strategy: List[str] = ['plot_fastest'],
|
|
250
|
+
backends: List[str] = ['MPI', 'NCCL', 'MPI4JAX'],
|
|
251
|
+
memory_units: str = 'KB',
|
|
252
|
+
) -> Tuple[Dict[str, pd.DataFrame], set, set]:
|
|
253
|
+
"""
|
|
254
|
+
Clean up and aggregate data from CSV files.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
csv_files : List[str]
|
|
259
|
+
List of CSV files to process.
|
|
260
|
+
precisions : Optional[List[str]], optional
|
|
261
|
+
Precisions to filter by, by default None.
|
|
262
|
+
function_names : Optional[List[str]], optional
|
|
263
|
+
Function names to filter by, by default None.
|
|
264
|
+
gpus : Optional[List[int]], optional
|
|
265
|
+
List of GPU counts to filter by, by default None.
|
|
266
|
+
data_sizes : Optional[List[int]], optional
|
|
267
|
+
List of data sizes to filter by, by default None.
|
|
268
|
+
pdims : Optional[List[str]], optional
|
|
269
|
+
List of pdims to filter by, by default None.
|
|
270
|
+
pdims_strategy : List[str], optional
|
|
271
|
+
Strategy for plotting pdims, by default ['plot_fastest'].
|
|
272
|
+
backends : List[str], optional
|
|
273
|
+
List of backends to filter by, by default ['MPI', 'NCCL', 'MPI4JAX'].
|
|
274
|
+
time_columns : List[str], optional
|
|
275
|
+
Time columns to use for aggregation, by default ['mean_time'].
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
Dict[str, pd.DataFrame]
|
|
280
|
+
Dictionary of method names to aggregated dataframes.
|
|
281
|
+
"""
|
|
282
|
+
dataframes = {}
|
|
283
|
+
available_gpu_counts = set()
|
|
284
|
+
available_data_sizes = set()
|
|
285
|
+
for csv_file in csv_files:
|
|
286
|
+
file_name = os.path.splitext(os.path.basename(csv_file))[0]
|
|
287
|
+
ext = os.path.splitext(os.path.basename(csv_file))[1]
|
|
288
|
+
if ext != '.csv':
|
|
289
|
+
print(f"Ignoring {csv_file} as it is not a CSV file")
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
df = pd.read_csv(csv_file,
|
|
293
|
+
header=None,
|
|
294
|
+
skiprows=1,
|
|
295
|
+
names=[
|
|
296
|
+
"function", "precision", "x", "y", "z", "px",
|
|
297
|
+
"py", "backend", "nodes", "jit_time", "min_time",
|
|
298
|
+
"max_time", "mean_time", "std_div", "last_time",
|
|
299
|
+
"generated_code", "argument_size", "output_size",
|
|
300
|
+
"temp_size", "flops"
|
|
301
|
+
],
|
|
302
|
+
dtype={
|
|
303
|
+
"function": str,
|
|
304
|
+
"precision": str,
|
|
305
|
+
"x": int,
|
|
306
|
+
"y": int,
|
|
307
|
+
"z": int,
|
|
308
|
+
"px": int,
|
|
309
|
+
"py": int,
|
|
310
|
+
"backend": str,
|
|
311
|
+
"nodes": int,
|
|
312
|
+
"jit_time": float,
|
|
313
|
+
"min_time": float,
|
|
314
|
+
"max_time": float,
|
|
315
|
+
"mean_time": float,
|
|
316
|
+
"std_div": float,
|
|
317
|
+
"last_time": float,
|
|
318
|
+
"generated_code": float,
|
|
319
|
+
"argument_size": float,
|
|
320
|
+
"output_size": float,
|
|
321
|
+
"temp_size": float,
|
|
322
|
+
"flops": float
|
|
323
|
+
},
|
|
324
|
+
index_col=False)
|
|
325
|
+
|
|
326
|
+
# Filter precisions
|
|
327
|
+
if precisions:
|
|
328
|
+
df = df[df['precision'].isin(precisions)]
|
|
329
|
+
# Filter function names
|
|
330
|
+
if function_names:
|
|
331
|
+
df = df[df['function'].isin(function_names)]
|
|
332
|
+
# Filter backends
|
|
333
|
+
df = df[df['backend'].isin(backends)]
|
|
334
|
+
|
|
335
|
+
# Filter data sizes
|
|
336
|
+
if data_sizes:
|
|
337
|
+
df = df[df['x'].isin(data_sizes)]
|
|
338
|
+
|
|
339
|
+
# Filter pdims
|
|
340
|
+
if pdims:
|
|
341
|
+
px_list, py_list = zip(*[map(int, p.split('x')) for p in pdims])
|
|
342
|
+
df = df[(df['px'].isin(px_list)) & (df['py'].isin(py_list))]
|
|
343
|
+
|
|
344
|
+
# convert memory units columns to remquested memory_units
|
|
345
|
+
match memory_units:
|
|
346
|
+
case 'KB':
|
|
347
|
+
factor = 1024
|
|
348
|
+
case 'MB':
|
|
349
|
+
factor = 1024**2
|
|
350
|
+
case 'GB':
|
|
351
|
+
factor = 1024**3
|
|
352
|
+
case 'TB':
|
|
353
|
+
factor = 1024**4
|
|
354
|
+
case _:
|
|
355
|
+
factor = 1
|
|
356
|
+
|
|
357
|
+
df['generated_code'] = df['generated_code'] / factor
|
|
358
|
+
df['argument_size'] = df['argument_size'] / factor
|
|
359
|
+
df['output_size'] = df['output_size'] / factor
|
|
360
|
+
df['temp_size'] = df['temp_size'] / factor
|
|
361
|
+
# in case of the same test is run multiple times, keep the last one
|
|
362
|
+
df = df.drop_duplicates(subset=[
|
|
363
|
+
"function", "precision", "x", "y", "z", "px", "py", "backend",
|
|
364
|
+
"nodes"
|
|
365
|
+
],
|
|
366
|
+
keep='last')
|
|
367
|
+
|
|
368
|
+
df['gpus'] = df['px'] * df['py']
|
|
369
|
+
|
|
370
|
+
if gpus:
|
|
371
|
+
df = df[df['gpus'].isin(gpus)]
|
|
372
|
+
|
|
373
|
+
if 'plot_all' in pdims_strategy or 'slab_yz' in pdims_strategy or 'slab_xy' in pdims_strategy or 'pencils' in pdims_strategy:
|
|
374
|
+
|
|
375
|
+
def get_decomp_from_px_py(row):
|
|
376
|
+
if row['px'] > 1 and row['py'] == 1:
|
|
377
|
+
return 'slab_yz'
|
|
378
|
+
elif row['px'] == 1 and row['py'] > 1:
|
|
379
|
+
return 'slab_xy'
|
|
380
|
+
else:
|
|
381
|
+
return 'pencils'
|
|
382
|
+
|
|
383
|
+
df['decomp'] = df.apply(get_decomp_from_px_py, axis=1)
|
|
384
|
+
df.drop(columns=['px', 'py'], inplace=True)
|
|
385
|
+
if not 'plot_all' in pdims_strategy:
|
|
386
|
+
df = df[df['decomp'].isin(pdims_strategy)]
|
|
387
|
+
# check available gpus in dataset
|
|
388
|
+
available_gpu_counts.update(df['gpus'].unique())
|
|
389
|
+
available_data_sizes.update(df['x'].unique())
|
|
390
|
+
|
|
391
|
+
if dataframes.get(file_name) is None:
|
|
392
|
+
dataframes[file_name] = df
|
|
393
|
+
else:
|
|
394
|
+
dataframes[file_name] = pd.concat([dataframes[file_name], df])
|
|
395
|
+
|
|
396
|
+
return dataframes, available_gpu_counts, available_data_sizes
|