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 ADDED
@@ -0,0 +1 @@
1
+ name = 'nodeocc'
@@ -0,0 +1,255 @@
1
+ import sys
2
+ import os
3
+ from pathlib import Path
4
+
5
+
6
+ import numpy as np
7
+
8
+ conf_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9
+ print(conf_path)
10
+ sys.path.append(conf_path)
11
+ from curses import wrapper
12
+ from view.curses_multiwindow import main, Singleton, try_open_socket_as_slave
13
+
14
+ from readers.compute_wait_time import get_wait_time
15
+ from importlib import reload
16
+ import readers.slurmreader
17
+ import view.slurm_list
18
+ import view.slurm_viz
19
+ import view.styles
20
+
21
+ import time
22
+ import json
23
+ import argparse
24
+ import traceback
25
+ from model.infrastructure import Infrastructure
26
+ from model.job import Job
27
+ import asyncio
28
+ import importlib.metadata
29
+
30
+ program_name = 'nodeocc'
31
+ version_number = importlib.metadata.version(program_name)
32
+
33
+ # get newest version from pip
34
+ try:
35
+ url = "https://pypi.python.org/pypi/nodeocc/json"
36
+ import requests
37
+ newest_version = json.loads(requests.get(url).text)['info']['version']
38
+ if newest_version.split('.')[0] > version_number.split('.')[0]:
39
+ print(f"New major version available: {newest_version} (current: {version_number})")
40
+ for _ in range(5):
41
+ print('.', end='')
42
+ time.sleep(0.5)
43
+ elif newest_version.split('.')[1] > version_number.split('.')[1]:
44
+ print(f"New minor version available: {newest_version} (current: {version_number})")
45
+ for _ in range(3):
46
+ print('.', end='')
47
+ time.sleep(0.5)
48
+ elif newest_version.split('.')[2] > version_number.split('.')[2]:
49
+ print(f"New patch version available: {newest_version} (current: {version_number})")
50
+ time.sleep(0.5)
51
+ except Exception as e:
52
+ print(f"Could not get newest version from pip, check if {program_name} is up to date")
53
+ time.sleep(1)
54
+
55
+
56
+ BEGIN_DELIM = "!{$"
57
+ END_DELIM_ENCODED = "!}$".encode('utf-8')
58
+ MAX_BUF = 65535
59
+ MAX_MSG_LEN = 1024 * 1024 * 1024
60
+
61
+ parser = argparse.ArgumentParser(description='Visualize slurm jobs')
62
+ parser.add_argument('--debug', action='store_true', help='Enable logging')
63
+ parser.add_argument('--master', action='store_true', help='Start master process')
64
+ parser.add_argument('--daemon_only', action='store_true', help='Disable all prints - only run in background')
65
+ parser.add_argument('--force_override', action='store_true', help='Force override of port file, kill previous instance')
66
+ parser.add_argument('--basepath', type=str, default='/nas/softechict-nas-2/mboschini/cool_scripts/new_nodeocc/', help='Base path for nodeocc. Must be readable by all users')
67
+ args = parser.parse_args()
68
+
69
+ # check basepath is readable
70
+ assert os.access(args.basepath, os.R_OK), f"Base path {args.basepath} not readable"
71
+
72
+ # export args
73
+ Singleton.getInstance(args)
74
+
75
+ last_update = os.path.getmtime(conf_path + "/view/slurm_viz.py")
76
+ if os.path.getmtime(conf_path + "/view/slurm_list.py") > last_update:
77
+ last_update = os.path.getmtime(conf_path + "/view/slurm_list.py")
78
+ if os.path.getmtime(conf_path + "/view/styles.py") > last_update:
79
+ last_update = os.path.getmtime(conf_path + "/view/styles.py")
80
+ if os.path.getmtime(conf_path + "/readers/slurmreader.py") > last_update:
81
+ last_update = os.path.getmtime(conf_path + "/readers/slurmreader.py")
82
+
83
+
84
+ def get_avg_wait_time(instance: Singleton):
85
+ try:
86
+ raise Exception("Not implemented")
87
+ avg_wait_time = get_wait_time('prod' if instance.cur_partition == 'prod' else 'students-prod')
88
+ h = avg_wait_time // 3600
89
+ m = (avg_wait_time % 3600) // 60
90
+ time_str = f"{int(h)}h{int(m)}m"
91
+ return time_str
92
+ except Exception as e:
93
+ instance.err(f"Could not get wait times")
94
+ instance.err(traceback.format_exc())
95
+ return 'err', 'err'
96
+
97
+
98
+ def update_data_master(instance):
99
+ if not instance.check_port_file_master():
100
+ instance.log(f"Port file dead, killing current instance")
101
+ instance.sock.close()
102
+ del instance
103
+ exit(0)
104
+
105
+ inf = readers.slurmreader.read_infrastructure()
106
+ instance.timeme(f"- infrastructure load")
107
+
108
+ jobs, _ = readers.slurmreader.read_jobs()
109
+ instance.timeme(f"- jobs list read")
110
+
111
+ # send data to clients (if any)
112
+ running_jobs = [j.to_nested_dict() for j in jobs if j.state == 'CG' or j.state == 'R']
113
+ queue_jobs = [j.to_nested_dict() for j in jobs if j.state != 'R' and j.state != 'CG']
114
+ queue_jobs = sorted(queue_jobs, key=lambda x: x['priority'], reverse=True)
115
+ maxlen = min(120, len(running_jobs) + len(queue_jobs))
116
+
117
+ msg = json.dumps({'inf': inf.to_nested_dict(), 'jobs': running_jobs + queue_jobs[:maxlen]})
118
+
119
+ # msg to bytes
120
+ msg = msg.encode('utf-8')
121
+ msg_len = len(msg)
122
+ msg = (str(msg_len) + BEGIN_DELIM).encode('utf-8') + msg + END_DELIM_ENCODED
123
+ instance.timeme(f"- msg encoded with len {msg_len}")
124
+
125
+ instance.sock.sendto(msg, ('<broadcast>', instance.port))
126
+ instance.timeme(f"- broadcast")
127
+
128
+ return inf, jobs
129
+
130
+
131
+ async def get_data_slave(instance):
132
+ inf, jobs = None, None
133
+ try:
134
+ # listen for data from master
135
+ # instance.sock.settimeout(6.5)
136
+ data, addr = instance.sock.recvfrom(MAX_BUF)
137
+
138
+ if BEGIN_DELIM in data.decode('utf-8'):
139
+ msg_len = int(data.decode('utf-8').split(BEGIN_DELIM)[0])
140
+ msg_len += len(str(msg_len)) + 2
141
+ instance.timeme(f"about to receive {msg_len} bytes")
142
+ while END_DELIM_ENCODED not in data:
143
+ buf_len = min(MAX_BUF, msg_len - len(data))
144
+ instance.timeme(f"bytes remaining {msg_len - len(data)} - receiving")
145
+ data += instance.sock.recvfrom(buf_len)[0]
146
+
147
+ if len(data) > MAX_MSG_LEN:
148
+ instance.err(f"Message too long, aborting")
149
+ return
150
+
151
+ data = data.split(END_DELIM_ENCODED)[0]
152
+ # data = data[:msg_len]
153
+ data = data.decode('utf-8')
154
+ data = data.split(BEGIN_DELIM)[1]
155
+
156
+ msg = json.loads(data)
157
+ inf = Infrastructure.from_dict(msg['inf'])
158
+ jobs = [Job.from_dict(j) for j in msg['jobs']]
159
+ avg_wait_time = "N/A" # get_avg_wait_time(instance)
160
+ instance.timeme(f"- receive")
161
+ else:
162
+ instance.timeme(f"- no data")
163
+ return None, None, None, None
164
+
165
+ except BlockingIOError as e:
166
+ pass
167
+
168
+ except TimeoutError as e:
169
+ instance.log(f"TIMEOUT")
170
+ try_open_socket_as_slave(instance)
171
+
172
+ return inf, jobs, avg_wait_time
173
+
174
+
175
+ async def get_all():
176
+ global last_update
177
+ # watch for reload
178
+ updt = os.path.getmtime(conf_path + "/view/slurm_viz.py")
179
+ updt = max(updt, os.path.getmtime(conf_path + "/view/slurm_list.py"))
180
+ updt = max(updt, os.path.getmtime(conf_path + "/view/styles.py"))
181
+ updt = max(updt, os.path.getmtime(conf_path + "/readers/slurmreader.py"))
182
+
183
+ instance = Singleton.getInstance()
184
+ instance.timeme(f"Starting update")
185
+
186
+ if updt > last_update:
187
+ reload(readers.slurmreader)
188
+ reload(view.slurm_list)
189
+ reload(view.slurm_viz)
190
+ reload(view.styles)
191
+ last_update = updt
192
+
193
+ instance.timeme(f"- reload")
194
+
195
+ inf, jobs, avg_wait_time = None, None, None
196
+ try:
197
+ if args.master:
198
+ inf, jobs = update_data_master(instance)
199
+ else:
200
+ # loop = asyncio.get_event_loop()
201
+ instance.timeme(f"- listening for data")
202
+
203
+ # wait for data from master but async update the view
204
+ inf, jobs, avg_wait_time = await get_data_slave(instance)
205
+ except Exception as e:
206
+ instance.err(f"Exception: {e}")
207
+ instance.err(traceback.format_exc())
208
+ # instance.rens = 'Something went wrong'
209
+ # instance.nocc = ':('
210
+
211
+ return inf, jobs, avg_wait_time
212
+
213
+
214
+ def display_main(stdscr):
215
+ return asyncio.run(main(stdscr))
216
+
217
+
218
+ def _main():
219
+ if args.daemon_only:
220
+ assert args.master, "Daemon mode only available for master"
221
+ instance = Singleton.getInstance()
222
+
223
+ instance.log(f"Starting master daemon")
224
+ # register atexit
225
+ import atexit
226
+
227
+ def exit_handler():
228
+ instance.log(f"Exiting...")
229
+ instance.sock.close()
230
+ # remove .port file
231
+ if instance.port_file_exists():
232
+ Path(instance.get_port_file_name()[1]).unlink()
233
+ atexit.register(exit_handler)
234
+
235
+ while True:
236
+ instance.timeme(f"Updating...")
237
+ try:
238
+ update_data_master(instance)
239
+ except Exception as e:
240
+ instance.err(f"Exception: {e}")
241
+ instance.err(traceback.format_exc())
242
+
243
+ time.sleep(5)
244
+
245
+ else:
246
+ # configure singleton
247
+
248
+ Singleton.getInstance().signature = f"{program_name} v{version_number}"
249
+ Singleton.getInstance().fetch_fn = get_all
250
+
251
+ wrapper(display_main)
252
+
253
+
254
+ if __name__ == '__main__':
255
+ _main()
@@ -0,0 +1,95 @@
1
+ from typing import List
2
+
3
+
4
+ class Node:
5
+ """
6
+ A simple class modelling a node within
7
+ the SLURM infrastructure
8
+ """
9
+
10
+ def __init__(self, name, n_gpus, gpu_model, reserved, status, cpus, mem) -> None:
11
+ self.name = name
12
+ self.n_gpus = n_gpus
13
+ self.gpu_model = gpu_model
14
+ self.reserved = reserved
15
+ assert reserved in ('no', 'yes', 'pending')
16
+ self.status = status
17
+ self.cpus = cpus
18
+ self.mem = mem
19
+
20
+ def __repr__(self):
21
+ return f'NODE {self.name} - {self.n_gpus}x{self.gpu_model} [{self.cpus}/{self.mem/1000}] - {self.status} ({self.reserved} reserv)'
22
+
23
+
24
+ class Maintenance:
25
+ """
26
+ A simple class modelling a pending
27
+ SLURM maintenance
28
+ """
29
+
30
+ def __init__(self, nodes, start_time, end_time) -> None:
31
+ self.nodes = nodes
32
+ self.start_time = start_time
33
+ self.end_time = end_time
34
+
35
+ def __repr__(self):
36
+ return f'RESERVATION {self.start_time} - {self.end_time} on {",".join(self.nodes)}'
37
+
38
+ @staticmethod
39
+ def from_dict(d):
40
+ m = Maintenance(None, None, None)
41
+ for k, v in d.items():
42
+ setattr(m, k, v)
43
+ return m
44
+
45
+ def to_nested_dict(self):
46
+ return {
47
+ k: v for k, v in self.__dict__.items()
48
+ }
49
+
50
+
51
+ class Infrastructure:
52
+ def __init__(self, maintenances: List[Maintenance], nodes, gpu_lims, ram_lims, cpu_lims) -> None:
53
+ self.maintenances = maintenances
54
+ self.nodes = nodes
55
+ gpu_limit_pu, gpu_limit_grp, gpu_limit_stu, gpu_limit_stugrp = gpu_lims
56
+ ram_limit_pu, ram_limit_grp, ram_limit_stu, ram_limit_stugrp = ram_lims
57
+ cpu_limit_pu, cpu_limit_grp, cpu_limit_stu, cpu_limit_stugrp = cpu_lims
58
+ self.gpu_limit_pu = gpu_limit_pu
59
+ self.gpu_limit_grp = gpu_limit_grp
60
+ self.gpu_limit_stu = gpu_limit_stu
61
+ self.gpu_limit_stugrp = gpu_limit_stugrp
62
+ self.ram_limit_pu = ram_limit_pu
63
+ self.ram_limit_grp = ram_limit_grp
64
+ self.ram_limit_stu = ram_limit_stu
65
+ self.ram_limit_stugrp = ram_limit_stugrp
66
+ self.cpu_limit_pu = cpu_limit_pu
67
+ self.cpu_limit_grp = cpu_limit_grp
68
+ self.cpu_limit_stu = cpu_limit_stu
69
+ self.cpu_limit_stugrp = cpu_limit_stugrp
70
+ self.prior = ['RTX6000', '2080', 'V100', 'RTXA5000', 'RTX5000', '1080', 'P100', 'n/a', None, 'K80']
71
+
72
+ @staticmethod
73
+ def from_dict(d):
74
+ i = Infrastructure(None, None, [None] * 4, [None] * 4, [None] * 4)
75
+ for k, v in d.items():
76
+ if k == 'nodes':
77
+ setattr(i, k, [Node(**n) for n in v])
78
+ else:
79
+ setattr(i, k, v)
80
+ return i
81
+
82
+ def to_nested_dict(self):
83
+ return {
84
+ k: ([n.__dict__ for n in self.nodes] if k == 'nodes' else
85
+ ([n.to_nested_dict() for n in self.maintenances] if k == 'maintenances' else v))
86
+ for k, v in self.__dict__.items()
87
+ }
88
+
89
+ def __repr__(self):
90
+ return "INFRASTRUCTURE\n" + "\n".join([str(x) for x in self.maintenances]) + "\n\n" + "\n".join([str(x) for x in self.nodes]) + \
91
+ f"\n\nlimits:({self.gpu_limit_pu}:{self.gpu_limit_grp}),({self.gpu_limit_stu}:{self.gpu_limit_stugrp})"
92
+
93
+ def get_sorted_nodes(self):
94
+
95
+ return sorted(self.nodes, key=lambda x: self.prior.index(x.gpu_model) if x.gpu_model in self.prior else -1)
model/job.py ADDED
@@ -0,0 +1,75 @@
1
+ import grp
2
+ import os
3
+ from pwd import getpwnam
4
+
5
+ from view.curses_multiwindow import Singleton
6
+
7
+
8
+ class Job:
9
+ """
10
+ A simple class modelling a SLURM job
11
+ """
12
+
13
+ def __init__(self, jobid, true_jobid, name, user, partition, state, runtime, reason, account, priority, tres, start_time, nice) -> None:
14
+ self.jobid = jobid
15
+ self.true_jobid = str(true_jobid)
16
+ self.name = name
17
+ self.user = user
18
+ self.partition = partition
19
+ self.state = state
20
+ self.runtime = runtime
21
+ self.joblets = []
22
+ self.reason = reason
23
+ self.account = account
24
+ self.priority = priority
25
+ self.tres = tres
26
+ self.starttime = start_time
27
+ self.nice = nice
28
+ self.user_group = grp.getgrgid(getpwnam(self.user)[3]).gr_name if self.user is not None else None
29
+
30
+ def is_student(self):
31
+ return self.user_group in ['studenti', 'tesisti']
32
+
33
+ @staticmethod
34
+ def from_dict(d):
35
+ j = Job(None, None, None, None, None, None, None, None, None, None, None, None, None)
36
+ j.joblets = []
37
+ for k, v in d.items():
38
+ if k == 'joblets':
39
+ setattr(j, k, [Joblet(**jblet) for jblet in v])
40
+ else:
41
+ setattr(j, k, v)
42
+ return j
43
+
44
+ def to_nested_dict(self):
45
+ return {
46
+ k: (v if k != 'joblets' else [n.__dict__ for n in self.joblets]) for k, v in self.__dict__.items()
47
+ }
48
+
49
+ def __repr__(self):
50
+ return f"JOB: {self.jobid} - {self.name} ({self.user}) [{self.reason}]\n" + \
51
+ '\n'.join(['\t' + str(x) for x in self.joblets])
52
+
53
+
54
+ class Joblet:
55
+ """
56
+ A simple class modelling the portion
57
+ of a SLURM job running on a given node
58
+ """
59
+
60
+ def __init__(self, jobid, true_jobid, node, n_gpus, cpus, mem) -> None:
61
+ self.jobid = jobid
62
+ self.true_jobid = true_jobid
63
+ self.node = node.replace('ailb-login-02', '0💀') if node is not None else None
64
+ self.n_gpus = int(n_gpus)
65
+ self.mem = mem
66
+ self.cpus = cpus
67
+
68
+ def __repr__(self):
69
+ try:
70
+ if isinstance(self.mem, str):
71
+ retstr = f"JOBLET: {self.jobid} on {self.node} ({self.n_gpus} gpus, {self.cpus} cpus, {self.mem} Mmem)"
72
+ retstr = f"JOBLET: {self.jobid} on {self.node} ({self.n_gpus} gpus, {self.cpus} cpus, {self.mem/1024} Gmem)"
73
+ except Exception as e:
74
+ Singleton.getInstance().err(f"Exception in joblet repr: {e}")
75
+ return retstr
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.1
2
+ Name: nodeocc
3
+ Version: 1.0.1
4
+ Summary: TUI application for viewing the status of GPU allocations on a Slurm cluster
5
+ Author: Matteo Boschini
6
+ Maintainer: Lorenzo Bonicelli, Aniello Panariello
7
+ License: MIT
8
+ Project-URL: Repository, https://github.com/mbosc/slurm_nodeocc
9
+ Keywords: Slurm,GPU,TUI,CLI,HPC,cluster,allocations,nodeocc
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console :: Curses
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: pandas
16
+ Requires-Dist: numpy
17
+
18
+ # Slurm NodeOCC
19
+ ![[screenshot](https://github.com/apanariello4/slurm_nodeocc/blob/master/sshot.jpg?raw=true)](sshot.jpg)
20
+
21
+ TUI application for viewing the status of GPU allocations on a Slurm cluster
22
+
23
+ Contents:
24
+
25
+ + `model/`: folder containing the fundamental classes definitions to model the Slurm cluster, its jobs and their portions;
26
+ + `readers/`: folder containing the functions to parse Slurm infrastructure and jobs status from standard unix commands;
27
+ + `view/slurm_list.py`: a simple list-based viewer of queued and allocated jobs;
28
+ + `view/slurm_viz.py`: a graphic-based viewer of allocated jobs across Slurm topology;
29
+ + `controller/controller.py`: the main entrypoint for the curses-based interactive TUI application.
30
+
31
+ Run it with: `python controller/controller.py`
32
+
33
+ Requirements: pandas, pycurses.
@@ -0,0 +1,17 @@
1
+ __init__.py,sha256=LlgHaFKDsLYKxyTe8BOiLhyt4Id6f6TG9KDiSrpMU5c,17
2
+ controller/controller.py,sha256=kewivhyKPoPfwWY3-qj_fmFyev6UrOHe0GFPwRNkyq4,8746
3
+ model/infrastructure.py,sha256=65iMo7ajIC42S-3nhVfR7_eEbXsRSqfUz-dMdNquwWk,3339
4
+ model/job.py,sha256=xBJd1W1ngnUZkZezEtsVSQfKbEI91e5v2kd9MLp99h0,2482
5
+ readers/compute_wait_time.py,sha256=sUH-VKceMninrj1keQ0vSE4z_qHhDMlurdtQIdZ4bDU,804
6
+ readers/slurmreader.py,sha256=Jc-XFylsCKIdIavmnTnKLyys6Syowl6NR328h4O5Ylw,12485
7
+ view/__init__.py,sha256=Q-1NQu1KuD4mHNnyMxMTlhj3O7z7d8vGu7-uD_XlYsU,660
8
+ view/curses_multiwindow.py,sha256=d_YK-W33EsRdrG1avuJUvgEgx77-aFnEiR4NeTu5kYs,21257
9
+ view/slurm_list.py,sha256=f_sGMtIpMp5h10KO7MTyrd48qh2Abukr8I0rYFez7Iw,5657
10
+ view/slurm_viz.py,sha256=E_FzP8D3u6fNQLOisXEg9eEfxFrjBWtD6DFHaMHFhZE,24809
11
+ view/styles.py,sha256=nYRtfVQXwowGk5AdIFP3_zD1NT1fBMQ8_GM_j0SdxaQ,2812
12
+ view/utils.py,sha256=Ze8WVi7OWjTJ1FTHy66Zg5eKnlBE72Pxirv5vC8aXeI,1834
13
+ nodeocc-1.0.1.dist-info/METADATA,sha256=LFW8a-XlJegL9KxJw-1TObO5bAg2YLseSPLy23JNyUY,1395
14
+ nodeocc-1.0.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
15
+ nodeocc-1.0.1.dist-info/entry_points.txt,sha256=7dTNBQ0rS2Weve3sFUIWaQksCsIJxHVX3ey2M7mCCwg,56
16
+ nodeocc-1.0.1.dist-info/top_level.txt,sha256=Kth-SFY-5rHDYYus3pnUwfB9CoHeREptSPhVLJLvRaY,39
17
+ nodeocc-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nodeocc = controller.controller:_main
@@ -0,0 +1,5 @@
1
+ __init__
2
+ controller
3
+ model
4
+ readers
5
+ view
@@ -0,0 +1,28 @@
1
+ import os
2
+ import numpy as np
3
+
4
+ def get_sec(time_str):
5
+ days_chks = time_str.split('-')
6
+ if len(days_chks) > 1:
7
+ days = days_chks[0]
8
+ time_str = days_chks[1]
9
+ else:
10
+ days = 0
11
+ h, m, s = time_str.split(':')
12
+ return int(days) * 86400 + int(h) * 3600 + int(m) * 60 + int(s)
13
+
14
+ def get_wait_time(partition):
15
+ assert partition in ['prod', 'students-prod']
16
+ cmd = 'sacct --noheader -X -a --partition=%s --start=now-1weeks --format="Reserved"' % partition
17
+
18
+ output = os.popen(cmd).readlines()
19
+ wait_times = []
20
+
21
+ for line in output:
22
+ if ':' not in line:
23
+ continue
24
+ wait_times.append(get_sec(line.strip()))
25
+
26
+ wait_times = np.asarray(wait_times)
27
+ avg_wait_time = np.mean(wait_times) if len(wait_times)>0 else -1
28
+ return avg_wait_time