nodeocc 1.0.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.
readers/slurmreader.py ADDED
@@ -0,0 +1,308 @@
1
+ import json
2
+ import os
3
+ import time
4
+ from io import StringIO
5
+
6
+ import pandas as pd
7
+
8
+ from model.infrastructure import Infrastructure, Maintenance, Node
9
+ from model.job import Job, Joblet
10
+ from view.curses_multiwindow import Singleton
11
+
12
+ joblet_node_cache = {}
13
+ joblet_array_cache = {}
14
+
15
+
16
+ def _ungroup_slurm_names(x):
17
+ if '[' in x:
18
+ tokens = x.split('[')[1].split(']')[0].split(',')
19
+ return ','.join([x.split('[')[0].split(',')[-1] + tok + x.split(']')[1].split(',')[0] for tok in tokens] +
20
+ [a for a in x.split(',') if '[' not in a and ']' not in a])
21
+ else:
22
+ return x
23
+
24
+
25
+ def _split_column(df, column):
26
+ # flatten representation
27
+ node_flat = pd.DataFrame([[i, x]
28
+ for i, y in df[column].fillna('').apply(_ungroup_slurm_names).str.split(',').items()
29
+ for x in y], columns=['__I', column]).set_index('__I')
30
+ node_flat[column] = node_flat[column].replace('', float('nan'))
31
+ return node_flat.merge(df[[x for x in df.columns if x != column]], left_index=True, right_index=True).reset_index(drop=True)
32
+
33
+
34
+ def _node_from_sinfo(x):
35
+ n = Node(_node_preproc(x['NODELIST']), x.n_gpu, x.m_gpu, x.reserved, None, x.CPUS, x.MEMORY)
36
+ if x['STATE'] == 'drng' or 'drain' in x['STATE']:
37
+ n.status = 'drain'
38
+ elif x['STATE'].upper() in ('MAINT', 'DOWN', 'DOWN*', 'FAIL', 'FAILING', 'FAIL*'):
39
+ n.status = 'down'
40
+ else:
41
+ n.status = 'ok'
42
+ return n
43
+
44
+
45
+ def _read_nodes(reservations, pending_res):
46
+ sinfo_df = pd.read_csv(StringIO(os.popen(r'sinfo -N -o "%N;%G;%t;%m;%c"').read()), sep=';')
47
+ sinfo_df = _split_column(sinfo_df, 'NODELIST').drop_duplicates()
48
+ sinfo_df['n_gpu'] = sinfo_df['GRES'].apply(lambda x: 0 if x.split('(')[0].split(':')[0] in ('(null)', '') else int(x.split('(')[0].split(':')[-1]))
49
+ sinfo_df['m_gpu'] = sinfo_df['GRES'].str.split(':').apply(lambda x: 'n/a' if x[0] in ('(null)', '') else x[1])
50
+ sinfo_df['reserved'] = 'no'
51
+ if reservations is not None:
52
+ sinfo_df.loc[sinfo_df['NODELIST'].isin(reservations.Nodes.unique()), 'reserved'] = 'yes'
53
+ if pending_res is not None:
54
+ sinfo_df.loc[sinfo_df['NODELIST'].isin(pending_res.Nodes.unique()), 'reserved'] = 'pending'
55
+
56
+ return sinfo_df.apply(_node_from_sinfo, axis=1).tolist()
57
+
58
+
59
+ def _maint_from_reservations(x):
60
+ mts = []
61
+ for st in x.StartTime.unique():
62
+ dt = x[x.StartTime == st]
63
+ mts.append(Maintenance(dt.Nodes.unique().tolist(), st, dt.EndTime.iloc[0]))
64
+ return mts
65
+
66
+
67
+ def _read_maintenances():
68
+ # create dataframe
69
+ reservations = os.popen(r'scontrol show reservation 2>/dev/null').read().split('\n\n')
70
+ reservations = [{c.split('=')[0]: c.split('=')[1] for c in r.split()} for r in reservations if len(r) and '=' in r]
71
+ if not len(reservations):
72
+ return [], None, None
73
+ reservations = pd.DataFrame.from_records(reservations)
74
+ reservations['StartTime'] = str(pd.to_datetime(reservations['StartTime']))
75
+ reservations['EndTime'] = str(pd.to_datetime(reservations['EndTime']))
76
+ reservations = _split_column(reservations, 'Nodes')
77
+ maint_flag = reservations['Flags'].str.contains('MAINT') & \
78
+ reservations['Flags'].str.contains('SPEC_NODES') & \
79
+ reservations['Flags'].str.contains('ALL_NODES')
80
+ # | reservations['ReservationName'].str.contains('limit_temp')
81
+
82
+ maintenances = _maint_from_reservations(reservations[maint_flag])
83
+ return maintenances, reservations[(~maint_flag) & (reservations['State'] == 'ACTIVE')], reservations[(~maint_flag) & (reservations['State'] == 'INACTIVE')]
84
+
85
+
86
+ def _read_limits():
87
+ """
88
+ Read the maximum amount of GPUs allowed
89
+ :returns max_prod_per_user, max_prod_group, max_studentsprod_per_user, max_studentsprod_group
90
+ """
91
+ lims = pd.read_fwf(StringIO(os.popen(r'sacctmgr show qos format=\'name%15,maxtresperuser%35,grptres%25,maxjobsperuser\' 2>/dev/null').read())).loc[1:]
92
+ lims['puG'] = lims['MaxTRESPU'].apply(lambda x: int(x.split('gres/gpu=')[1].split(',')[0]) if isinstance(x, str) and 'gres/gpu' in x else float('nan'))
93
+ lims['grpG'] = lims['GrpTRES'].apply(lambda x: int(x.split('gres/gpu=')[1].split(',')[0]) if isinstance(x, str) and 'gres/gpu' in x else float('nan'))
94
+ lims['puM'] = lims['MaxTRESPU'].apply(lambda x: parse_mem(x.split('mem=')[1].split(',')[0]) if isinstance(x, str) and 'mem=' in x else float('nan'))
95
+ lims['grpM'] = lims['GrpTRES'].apply(lambda x: parse_mem(x.split('mem=')[1].split(',')[0]) if isinstance(x, str) and 'mem=' in x else float('nan'))
96
+ lims['puC'] = lims['MaxTRESPU'].apply(lambda x: int(x.split('cpu=')[1].split(',')[0]) if isinstance(x, str) and 'cpu=' in x else float('nan'))
97
+ lims['grpC'] = lims['GrpTRES'].apply(lambda x: int(x.split('cpu=')[1].split(',')[0]) if isinstance(x, str) and 'cpu=' in x else float('nan'))
98
+ return (lims.loc[lims['Name'] == 'all_usr_prod', 'puG'].iloc[0],
99
+ lims.loc[lims['Name'] == 'all_usr_prod', 'grpG'].iloc[0],
100
+ lims.loc[lims['Name'] == 'all_usr_prod', 'puG'].iloc[0], # students
101
+ lims.loc[lims['Name'] == 'all_usr_prod', 'grpG'].iloc[0], # students
102
+ lims.loc[lims['Name'] == 'all_usr_prod', 'puM'].iloc[0],
103
+ lims.loc[lims['Name'] == 'all_usr_prod', 'grpM'].iloc[0],
104
+ lims.loc[lims['Name'] == 'all_usr_prod', 'puM'].iloc[0], # students
105
+ lims.loc[lims['Name'] == 'all_usr_prod', 'grpM'].iloc[0], # students
106
+ lims.loc[lims['Name'] == 'all_usr_prod', 'puC'].iloc[0],
107
+ lims.loc[lims['Name'] == 'all_usr_prod', 'grpC'].iloc[0],
108
+ lims.loc[lims['Name'] == 'all_usr_prod', 'puC'].iloc[0], # students
109
+ lims.loc[lims['Name'] == 'all_usr_prod', 'grpC'].iloc[0]) # students
110
+
111
+
112
+ def read_infrastructure():
113
+ """
114
+ Get infrastructure status (nodes, reservations,
115
+ maintenances, resource limits, etc)
116
+ """
117
+ maints, reserv, pending_res = _read_maintenances()
118
+ nodes = _read_nodes(reserv, pending_res)
119
+ mpuG, mpugG, mspuG, mspugG, mpuM, mpugM, mspuM, mspugM, mpuC, mpugC, mspuC, mspugC = _read_limits()
120
+ return Infrastructure(maints, nodes, (mpuG, mpugG, mspuG, mspugG), (mpuM, mpugM, mspuM, mspugM), (mpuC, mpugC, mspuC, mspugC))
121
+
122
+
123
+ def _parse_scontrol_joblet_nodes(jobid):
124
+ global joblet_node_cache
125
+ if jobid not in joblet_node_cache:
126
+ jobline = f'scontrol show jobid -d {jobid} | grep \' Nodes=\''
127
+ joblet_id = os.popen(jobline).read().splitlines()
128
+ joblet_node_cache[jobid] = joblet_id
129
+ return joblet_node_cache[jobid]
130
+
131
+
132
+ def _parse_scontrol_joblet_arrays(jobid):
133
+ global joblet_array_cache
134
+ if jobid not in joblet_array_cache:
135
+ jobline = f'scontrol show jobid -d {jobid} 2>/dev/null | grep Array '
136
+ joblet_id = os.popen(jobline).read().splitlines()
137
+ joblet_array_cache[jobid] = joblet_id
138
+ return joblet_array_cache[jobid]
139
+
140
+
141
+ def _purge_cache():
142
+ global joblet_node_cache, joblet_array_cache
143
+ joblet_node_cache = {}
144
+ joblet_array_cache = {}
145
+
146
+
147
+ def _load_joblet_meta(line):
148
+ if line['ST'] != 'PD':
149
+ return [x for x in _parse_scontrol_joblet_nodes(line["JOBID"]) if line["NODELIST"] in x]
150
+ else:
151
+ return '[]'
152
+
153
+
154
+ def _gpus_per_joblet(line):
155
+ if pd.isna(line['gpus_per_job']) and pd.isna(line['gpus_per_node']) and pd.isna(line['gpus_per_task']):
156
+ return 0
157
+ elif not pd.isna(line['gpus_per_node']):
158
+ return line['gpus_per_node'] * (line['NODES'] if pd.isna(line['NODELIST']) else 1)
159
+ else:
160
+ if line['ST'] == 'PD':
161
+ return line['gpus_per_job'] if not pd.isna(line['gpus_per_job']) else line['gpus_per_task']
162
+ gpus_id = line['joblet_meta_str']
163
+ gpus_id = sum([x.split('IDX:')[-1].split(')')[0].split(',') for x in gpus_id if 'gpu' in x], [])
164
+ gpus_n = len(gpus_id) + sum([len(range(int(x.split('-')[1].split()[0]) - int(x.split('-')[0]))) for x in gpus_id if '-' in x])
165
+ return gpus_n
166
+
167
+
168
+ def _cpus_per_joblet(line):
169
+ if line['ST'] == 'PD':
170
+ return line['MIN_CPUS']
171
+ else:
172
+ cpus_id = line['joblet_meta_str']
173
+ cpus = sum([x.split('CPU_IDs=')[-1].split()[0].split(',') for x in cpus_id], [])
174
+ cpus_n = len(cpus) + sum([len(range(int(x.split('-')[1].split()[0]) - int(x.split('-')[0]))) for x in cpus if '-' in x])
175
+ return cpus_n
176
+
177
+
178
+ memunits = {
179
+ 'K': 2**-10,
180
+ 'M': 2**0,
181
+ 'G': 2**10,
182
+ 'T': 2**20
183
+ }
184
+
185
+
186
+ def parse_mem(thing):
187
+ if thing == '0':
188
+ return 0
189
+ if any([l in thing for l in memunits]):
190
+ return float(thing[:-1]) * memunits[thing[-1]]
191
+ else:
192
+ return thing
193
+
194
+
195
+ def _mem_per_joblet(line):
196
+ if line['ST'] == 'PD':
197
+ return parse_mem(line['MIN_MEMORY'])
198
+ else:
199
+ mem_id = line['joblet_meta_str']
200
+ mem = sum([int(x.split('Mem=')[-1].split()[0]) for x in mem_id])
201
+ return mem
202
+
203
+
204
+ def _true_jobid(line):
205
+
206
+ try:
207
+ base_jid = line.split('_')[0]
208
+ tid = line.split('_')[1]
209
+ tj = _parse_scontrol_joblet_arrays(line)
210
+ assert len(tj) == 1
211
+ return tj[0].split('JobId=')[1].split(' ')[0]
212
+ except BaseException:
213
+ return line
214
+
215
+
216
+ def _node_preproc(x):
217
+ return x.replace('aimagelab-srv-', '').replace('ailb-login-', '')
218
+
219
+
220
+ def try_parse_gpu(s: str):
221
+ if 'gpu' in s:
222
+ try:
223
+ return int(s.split(':')[-1].split()[0])
224
+ except BaseException:
225
+ return 1
226
+ else:
227
+ return 1
228
+
229
+
230
+ def get_mem_joblet(tres_str: str):
231
+ try:
232
+ mem = tres_str.split('mem=')[-1].split(',')[0]
233
+ if 'G' in mem:
234
+ return int(mem.split('G')[0]) * 1024
235
+ elif 'M' in mem:
236
+ return int(mem.split('M')[0])
237
+ else:
238
+ return int(mem)
239
+ except BaseException:
240
+ return 0
241
+
242
+
243
+ def read_jobs():
244
+ """
245
+ Get jobs and joblets status
246
+ """
247
+ instance = Singleton.getInstance()
248
+
249
+ squeue_cmd = r'squeue -O jobarrayid:\;,Reason:\;,NodeList:\;,Username:\;,tres-per-job:\;,tres-per-task:\;,tres-per-node:\;,Name:\;,Partition:\;,StateCompact:\;,StartTime:\;,TimeUsed:\;,NumNodes:\;,NumTasks:\;,Reason:\;,MinMemory:\;,MinCpus:\;,Account:\;,PriorityLong:\;,jobid:\;,tres:\;,nice: 2> /dev/null'
250
+ try:
251
+ jobs_list = os.popen(squeue_cmd).read()
252
+ squeue_df = pd.read_csv(StringIO(jobs_list), sep=';')
253
+ except Exception as e:
254
+ instance.err(f"Exception: {e}")
255
+ return [], []
256
+
257
+ instance.timeme(f"\t- squeue and dataframe creation")
258
+
259
+ squeue_df['JOBID'] = squeue_df['JOBID'].apply(str)
260
+ squeue_df = _split_column(squeue_df, 'NODELIST')
261
+ squeue_df['gpus_per_node'] = squeue_df['TRES_PER_NODE'].apply(lambda x: try_parse_gpu(x) if isinstance(x, str) else x)
262
+ squeue_df['gpus_per_job'] = squeue_df['TRES_PER_JOB'].apply(lambda x: try_parse_gpu(x) if isinstance(x, str) else x)
263
+ squeue_df['gpus_per_task'] = squeue_df['TRES_PER_TASK'].apply(lambda x: try_parse_gpu(x) if isinstance(x, str) else x)
264
+
265
+ try:
266
+ squeue_df['joblet_mem'] = squeue_df['TRES_ALLOC'].apply(get_mem_joblet)
267
+ squeue_df['joblet_gpus'] = squeue_df['TRES_ALLOC'].apply(lambda l: str(l).split('gpu=')[-1].split(',')[0] if 'gpu=' in str(l) else 0)
268
+ squeue_df['joblet_cpus'] = squeue_df['TRES_ALLOC'].apply(lambda l: int(str(l).split('cpu=')[-1].split(',')[0]))
269
+ except Exception as e:
270
+ instance.err(f"Exception PD: {e}")
271
+
272
+ instance.timeme(f"\t- re processing")
273
+ squeue_df.rename(columns={'JOBID.1': 'TRUE_JOBID'}, inplace=True)
274
+
275
+ instance.timeme(f"\t- processing")
276
+
277
+ _purge_cache()
278
+ instance.timeme(f"\t- purging cache")
279
+
280
+ if len(squeue_df) == 0:
281
+ return [], []
282
+
283
+ joblets = squeue_df.apply(
284
+ lambda line: Joblet(
285
+ line['JOBID'],
286
+ line['TRUE_JOBID'],
287
+ _node_preproc(
288
+ line['NODELIST']) if not pd.isna(
289
+ line['NODELIST']) else None,
290
+ line['joblet_gpus'],
291
+ line['joblet_cpus'],
292
+ line['joblet_mem']),
293
+ axis=1).tolist()
294
+ jobs = squeue_df.drop_duplicates('JOBID').apply(lambda line: Job(
295
+ line['JOBID'], line['TRUE_JOBID'], line['NAME'], line['USER'],
296
+ line['PARTITION'], line['ST'], line['TIME'], line['REASON'],
297
+ line['ACCOUNT'], line['PRIORITY'], line['TRES_ALLOC'], line['START_TIME'],
298
+ line['NICE']
299
+ ), axis=1).tolist()
300
+
301
+ for j in jobs:
302
+ j.joblets = [x for x in joblets if x.jobid == j.jobid]
303
+ return jobs, joblets
304
+
305
+
306
+ if __name__ == "__main__":
307
+ print(read_infrastructure())
308
+ print('\n'.join([str(x) for x in read_jobs()[0]]))
view/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ import curses
2
+ from . import slurm_list, slurm_viz, styles
3
+
4
+ def update_views(stdscr, instance, filter):
5
+ if instance.jobs is None or instance.inf is None:
6
+ return
7
+
8
+ instance.rens = slurm_list.view_list(instance, instance.jobs, filter, stylefn=styles.crsstyler, width=instance.left_width if hasattr(instance, 'left_width') else 72, jit=instance.job_id_type if hasattr(instance, 'job_id_type') else 'agg')
9
+ instance.timeme(f"- print job list")
10
+
11
+ instance.nocc = slurm_viz.view_viz(instance.inf, instance.jobs, stylefn=styles.crsstyler, mode=instance.view_mode if hasattr(instance, 'view_mode') else 'gpu')
12
+ instance.timeme(f"- view nodeocc")