nodeocc 1.0.1__tar.gz
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.
- nodeocc-1.0.1/PKG-INFO +33 -0
- nodeocc-1.0.1/README.md +16 -0
- nodeocc-1.0.1/pyproject.toml +36 -0
- nodeocc-1.0.1/setup.cfg +4 -0
- nodeocc-1.0.1/setup.py +3 -0
- nodeocc-1.0.1/src/__init__.py +1 -0
- nodeocc-1.0.1/src/controller/controller.py +255 -0
- nodeocc-1.0.1/src/model/infrastructure.py +95 -0
- nodeocc-1.0.1/src/model/job.py +75 -0
- nodeocc-1.0.1/src/nodeocc.egg-info/PKG-INFO +33 -0
- nodeocc-1.0.1/src/nodeocc.egg-info/SOURCES.txt +21 -0
- nodeocc-1.0.1/src/nodeocc.egg-info/dependency_links.txt +1 -0
- nodeocc-1.0.1/src/nodeocc.egg-info/entry_points.txt +2 -0
- nodeocc-1.0.1/src/nodeocc.egg-info/requires.txt +2 -0
- nodeocc-1.0.1/src/nodeocc.egg-info/top_level.txt +5 -0
- nodeocc-1.0.1/src/readers/compute_wait_time.py +28 -0
- nodeocc-1.0.1/src/readers/slurmreader.py +308 -0
- nodeocc-1.0.1/src/view/__init__.py +12 -0
- nodeocc-1.0.1/src/view/curses_multiwindow.py +580 -0
- nodeocc-1.0.1/src/view/slurm_list.py +148 -0
- nodeocc-1.0.1/src/view/slurm_viz.py +494 -0
- nodeocc-1.0.1/src/view/styles.py +76 -0
- nodeocc-1.0.1/src/view/utils.py +60 -0
nodeocc-1.0.1/PKG-INFO
ADDED
|
@@ -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
|
+
](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.
|
nodeocc-1.0.1/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Slurm NodeOCC
|
|
2
|
+
](sshot.jpg)
|
|
3
|
+
|
|
4
|
+
TUI application for viewing the status of GPU allocations on a Slurm cluster
|
|
5
|
+
|
|
6
|
+
Contents:
|
|
7
|
+
|
|
8
|
+
+ `model/`: folder containing the fundamental classes definitions to model the Slurm cluster, its jobs and their portions;
|
|
9
|
+
+ `readers/`: folder containing the functions to parse Slurm infrastructure and jobs status from standard unix commands;
|
|
10
|
+
+ `view/slurm_list.py`: a simple list-based viewer of queued and allocated jobs;
|
|
11
|
+
+ `view/slurm_viz.py`: a graphic-based viewer of allocated jobs across Slurm topology;
|
|
12
|
+
+ `controller/controller.py`: the main entrypoint for the curses-based interactive TUI application.
|
|
13
|
+
|
|
14
|
+
Run it with: `python controller/controller.py`
|
|
15
|
+
|
|
16
|
+
Requirements: pandas, pycurses.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools >= 65.5.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nodeocc"
|
|
7
|
+
version = "1.0.1"
|
|
8
|
+
authors = [{ name = "Matteo Boschini" }]
|
|
9
|
+
maintainers = [{ name = "Lorenzo Bonicelli" }, { name = "Aniello Panariello" }]
|
|
10
|
+
description = "TUI application for viewing the status of GPU allocations on a Slurm cluster"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
keywords = [
|
|
14
|
+
"Slurm",
|
|
15
|
+
"GPU",
|
|
16
|
+
"TUI",
|
|
17
|
+
"CLI",
|
|
18
|
+
"HPC",
|
|
19
|
+
"cluster",
|
|
20
|
+
"allocations",
|
|
21
|
+
"nodeocc",
|
|
22
|
+
]
|
|
23
|
+
license = { text = "MIT" }
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
|
+
"Environment :: Console :: Curses",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
dependencies = ["pandas", "numpy"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Repository = "https://github.com/mbosc/slurm_nodeocc"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
nodeocc = "controller.controller:_main"
|
nodeocc-1.0.1/setup.cfg
ADDED
nodeocc-1.0.1/setup.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)
|
|
@@ -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
|
+
](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,21 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
src/__init__.py
|
|
5
|
+
src/controller/controller.py
|
|
6
|
+
src/model/infrastructure.py
|
|
7
|
+
src/model/job.py
|
|
8
|
+
src/nodeocc.egg-info/PKG-INFO
|
|
9
|
+
src/nodeocc.egg-info/SOURCES.txt
|
|
10
|
+
src/nodeocc.egg-info/dependency_links.txt
|
|
11
|
+
src/nodeocc.egg-info/entry_points.txt
|
|
12
|
+
src/nodeocc.egg-info/requires.txt
|
|
13
|
+
src/nodeocc.egg-info/top_level.txt
|
|
14
|
+
src/readers/compute_wait_time.py
|
|
15
|
+
src/readers/slurmreader.py
|
|
16
|
+
src/view/__init__.py
|
|
17
|
+
src/view/curses_multiwindow.py
|
|
18
|
+
src/view/slurm_list.py
|
|
19
|
+
src/view/slurm_viz.py
|
|
20
|
+
src/view/styles.py
|
|
21
|
+
src/view/utils.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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
|