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.
- __init__.py +1 -0
- controller/controller.py +255 -0
- model/infrastructure.py +95 -0
- model/job.py +75 -0
- nodeocc-1.0.1.dist-info/METADATA +33 -0
- nodeocc-1.0.1.dist-info/RECORD +17 -0
- nodeocc-1.0.1.dist-info/WHEEL +5 -0
- nodeocc-1.0.1.dist-info/entry_points.txt +2 -0
- nodeocc-1.0.1.dist-info/top_level.txt +5 -0
- readers/compute_wait_time.py +28 -0
- readers/slurmreader.py +308 -0
- view/__init__.py +12 -0
- view/curses_multiwindow.py +580 -0
- view/slurm_list.py +148 -0
- view/slurm_viz.py +494 -0
- view/styles.py +76 -0
- view/utils.py +60 -0
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")
|