Add SLURM resources to form template
This commit is contained in:
parent
b8efa00a05
commit
11d878cecf
7 changed files with 506 additions and 112 deletions
|
@ -24,7 +24,7 @@ poetry run ruff format
|
|||
|
||||
To render the form template (with live reload):
|
||||
```bash
|
||||
poetry run python -m render
|
||||
poetry run python -m render [--single-cluster]
|
||||
```
|
||||
|
||||
To activate the virtual environement globally:
|
||||
|
|
|
@ -3,12 +3,16 @@
|
|||
Usage: `python -m render`
|
||||
|
||||
"""
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
from traceback import format_exc
|
||||
|
||||
from flask import Flask, render_template, request
|
||||
from glicid_spawner.form import TEMPLATES, options_from_form
|
||||
from glicid_spawner.micromamba import MicromambaEnv
|
||||
from glicid_spawner.resources import CPU, GPU, RAM
|
||||
from glicid_spawner.resources import CPU, MEMORY, gpu_max_duration
|
||||
from glicid_spawner.slurm import gres, sinfo_from_file
|
||||
from livereload import Server
|
||||
|
||||
# Dummy username and python environments
|
||||
|
@ -18,15 +22,30 @@ ENVS = [
|
|||
MicromambaEnv('USER', 'bar', f'/{USERNAME}/envs/bar'),
|
||||
MicromambaEnv('GLOBAL', 'baz', '/global/envs/baz'),
|
||||
]
|
||||
|
||||
# Dummy SLURM config
|
||||
DATA = Path(__file__).parent / '..' / 'tests' / 'data'
|
||||
SINFO = sinfo_from_file(
|
||||
DATA / 'sinfo.txt', with_states=('idle', 'mixed', 'allocated', 'completing', 'planned')
|
||||
)
|
||||
|
||||
# Single vs. multi-cluster implementation
|
||||
SLURM_SINGLE_CLUSTER = {'N/A': SINFO.pop('N/A')}
|
||||
SLURM_MULTI_CLUSTER = SINFO
|
||||
GPU_SINGLE_CLUSTER = gpu_max_duration(gres(SLURM_SINGLE_CLUSTER))
|
||||
GPU_MULTI_CLUSTER = gpu_max_duration(gres(SLURM_MULTI_CLUSTER))
|
||||
|
||||
# Format dummy options
|
||||
OPTIONS = {
|
||||
'username': USERNAME,
|
||||
'envs': ENVS,
|
||||
'cpus': CPU,
|
||||
'rams': RAM,
|
||||
'gpus': GPU,
|
||||
'mems': MEMORY,
|
||||
# Multi-cluster by default. See `--single-cluster` flag in CLI for single cluster config.
|
||||
'gpus': GPU_MULTI_CLUSTER,
|
||||
'sinfo': SLURM_MULTI_CLUSTER,
|
||||
}
|
||||
|
||||
|
||||
# Flask app
|
||||
app = Flask(__name__)
|
||||
app.debug = True
|
||||
|
@ -60,11 +79,35 @@ def submit():
|
|||
return render_template('options.html', formdata=formdata, err=format_exc())
|
||||
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
"""Dummy favicon."""
|
||||
return ''
|
||||
|
||||
|
||||
def server_autoreload():
|
||||
"""Start auto-reload server."""
|
||||
server = Server(app.wsgi_app)
|
||||
server.serve()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def cli(argv=None):
|
||||
"""Command line interface."""
|
||||
parser = ArgumentParser('Spawner form render')
|
||||
parser.add_argument(
|
||||
'--single-cluster',
|
||||
action='store_true',
|
||||
help='Toggle SLURM config to single cluster configuration.',
|
||||
)
|
||||
|
||||
args, _ = parser.parse_known_args(argv)
|
||||
|
||||
if args.single_cluster:
|
||||
OPTIONS['gpus'] = GPU_SINGLE_CLUSTER
|
||||
OPTIONS['sinfo'] = SLURM_SINGLE_CLUSTER
|
||||
|
||||
server_autoreload()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli(sys.argv)
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
|
||||
from .micromamba import get_envs
|
||||
from .resources import CPU, GPU, RAM
|
||||
from .resources import CPU, GPU_DEFAULTS, MEMORY, gpu_max_duration
|
||||
from .slurm import gres, sinfo
|
||||
|
||||
TEMPLATES = Environment(
|
||||
loader=PackageLoader('glicid_spawner'),
|
||||
|
@ -13,12 +14,18 @@ TEMPLATES = Environment(
|
|||
|
||||
def options_attrs(username: str) -> dict:
|
||||
"""Form options attributes."""
|
||||
slurm_sinfo = sinfo(username)
|
||||
|
||||
# Allocated 1h to any SLURM GPU resources not listed in GPUS
|
||||
gpus = gpu_max_duration(gres(slurm_sinfo), unknown_default=1)
|
||||
|
||||
return {
|
||||
'username': username,
|
||||
'envs': get_envs(username),
|
||||
'cpus': CPU,
|
||||
'rams': RAM,
|
||||
'gpus': GPU,
|
||||
'mems': MEMORY,
|
||||
'gpus': gpus,
|
||||
'sinfo': slurm_sinfo,
|
||||
}
|
||||
|
||||
|
||||
|
@ -30,26 +37,37 @@ def options_form(username: str) -> str:
|
|||
|
||||
def options_from_form(formdata) -> dict:
|
||||
"""Export options from default form."""
|
||||
# Resources choices indexes
|
||||
i_cpu = int(formdata['cpu'][0])
|
||||
i_ram = int(formdata['ram'][0])
|
||||
i_gpu = int(formdata['gpu'][0])
|
||||
# Parse form data response
|
||||
env = formdata['python-env'][0]
|
||||
cpu = int(formdata['cpu'][0])
|
||||
mem = int(formdata['mem'][0])
|
||||
gpu = formdata['gpu'][0]
|
||||
cluster = formdata.get('cluster', [None])[0]
|
||||
partition = formdata.get('partition', [None])[0]
|
||||
node = formdata.get('node', [None])[0]
|
||||
|
||||
duration = min(
|
||||
CPU[i_cpu].max_duration,
|
||||
RAM[i_ram].max_duration,
|
||||
GPU[i_gpu].max_duration,
|
||||
)
|
||||
# Compute max duration
|
||||
# If the value provided is not in the original list, runtime = 0 (except unknown GPU = 1h)
|
||||
runtime = min(CPU.get(cpu, 0), MEMORY.get(mem, 0), GPU_DEFAULTS.get(gpu, 1))
|
||||
|
||||
# Export options
|
||||
options = {
|
||||
'pyenv': formdata['python-env'][0],
|
||||
'nprocs': int(CPU[i_cpu].description),
|
||||
'memory': RAM[i_ram].description.replace(' ', ''),
|
||||
'runtime': f'{duration:02d}:00:00',
|
||||
'pyenv': env,
|
||||
'nprocs': cpu,
|
||||
'memory': f'{mem}GB',
|
||||
'runtime': f'{runtime:02d}:00:00',
|
||||
}
|
||||
|
||||
if i_gpu:
|
||||
options['gres'] = 'gpu:' + GPU[i_gpu].description.lower()
|
||||
if gpu != 'None':
|
||||
options['gres'] = f'gpu:{gpu.lower()}'
|
||||
|
||||
if cluster:
|
||||
options['cluster'] = cluster
|
||||
|
||||
if partition:
|
||||
options['partition'] = partition
|
||||
|
||||
if node:
|
||||
options['node'] = node
|
||||
|
||||
return options
|
||||
|
|
|
@ -15,10 +15,29 @@ input[type=radio]:checked+label {
|
|||
|
||||
.panel-heading .panel-title-toggle:before {
|
||||
font-family: 'FontAwesome';
|
||||
content: "\\f078";
|
||||
content: "\f078";
|
||||
color: lightgrey;
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
.panel-heading .panel-title-toggle.collapsed:before {
|
||||
content: "\\f054";
|
||||
content: "\f054";
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.flex-item-2 {
|
||||
width: 49%;
|
||||
}
|
||||
.flex-item-4 {
|
||||
width: 24%;
|
||||
}
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
<div class="form-group">
|
||||
<label for="cpu" class="col-sm-3 control-label">CPU:</label>
|
||||
<div class="col-sm-9">
|
||||
{%- for cpu in cpus -%}
|
||||
{%- for cpu, max_duration in cpus.items() -%}
|
||||
<div class="col-sm-2">
|
||||
<input type="radio" name="cpu" id="cpu_{{loop.index0}}" value="{{loop.index0}}"
|
||||
data-max-duration="{{cpu.max_duration}}"{% if loop.first %} checked{% endif %}>
|
||||
<label for="cpu_{{loop.index0}}" class="btn btn-default btn-block">
|
||||
{{ cpu.description }}
|
||||
<input type="radio" name="cpu" id="cpu_{{cpu}}" value="{{cpu}}"
|
||||
data-max-duration="{{max_duration}}"
|
||||
{%- if loop.first %} checked{% endif %}>
|
||||
<label for="cpu_{{cpu}}" class="btn btn-default btn-block">
|
||||
{{ cpu }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor -%}
|
||||
|
@ -16,14 +17,15 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ram" class="col-sm-3 control-label">Memory:</label>
|
||||
<label for="mem" class="col-sm-3 control-label">Memory:</label>
|
||||
<div class="col-sm-9">
|
||||
{%- for ram in rams -%}
|
||||
{%- for mem, max_duration in mems.items() -%}
|
||||
<div class="col-sm-2">
|
||||
<input type="radio" name="ram" id="ram_{{loop.index0}}" value="{{loop.index0}}"
|
||||
data-max-duration="{{ram.max_duration}}"{% if loop.first %} checked{% endif %}>
|
||||
<label for="ram_{{loop.index0}}" class="btn btn-default btn-block">
|
||||
{{ ram.description }}
|
||||
<input type="radio" name="mem" id="mem_{{mem}}" value="{{mem}}"
|
||||
data-max-duration="{{max_duration}}"
|
||||
{%- if loop.first %} checked{% endif %}>
|
||||
<label for="mem_{{mem}}" class="btn btn-default btn-block">
|
||||
{{ mem }} GB
|
||||
</label>
|
||||
</div>
|
||||
{% endfor -%}
|
||||
|
@ -33,17 +35,19 @@
|
|||
<div class="form-group">
|
||||
<label for="gpu" class="col-sm-3 control-label">GPU:</label>
|
||||
<div class="col-sm-9">
|
||||
{%- for gpu in gpus -%}
|
||||
{%- for gpu, max_duration in gpus.items() -%}
|
||||
<div class="col-sm-2">
|
||||
<input type="radio" name="gpu" id="gpu_{{loop.index0}}" value="{{loop.index0}}"
|
||||
data-max-duration="{{gpu.max_duration}}"{% if loop.first %} checked{% endif %}>
|
||||
<label for="gpu_{{loop.index0}}" class="btn btn-default btn-block">
|
||||
{{ gpu.description }}
|
||||
<input type="radio" name="gpu" id="gpu_{{gpu}}" value="{{gpu}}"
|
||||
data-max-duration="{{max_duration}}"
|
||||
{%- if loop.first %} checked{% endif %}>
|
||||
<label for="gpu_{{gpu}}" class="btn btn-default btn-block">
|
||||
{{ gpu }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
@ -59,9 +63,9 @@
|
|||
|
||||
$('.resources input[type=radio]').change(function () {
|
||||
var cpu = $('input[name=cpu]:checked').data('max-duration');
|
||||
var ram = $('input[name=ram]:checked').data('max-duration');
|
||||
var mem = $('input[name=mem]:checked').data('max-duration');
|
||||
var gpu = $('input[name=gpu]:checked').data('max-duration');
|
||||
|
||||
$reservations_dropdown.text(Math.min(cpu, ram, gpu));
|
||||
$reservations_dropdown.text(Math.min(cpu, mem, gpu));
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
<div class="panel-group" id="advanced-config" role="tablist" aria-multiselectable="true">
|
||||
<div id="cluster-config" class="panel-group" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="heading">
|
||||
<h4 class="panel-title">
|
||||
<a class="panel-title-toggle collapsed" role="button" data-toggle="collapse"
|
||||
data-parent="#advanced-config" href="#advanced-config-collapse" aria-expanded="true" aria-controls="advanced-config-collapse">
|
||||
Advanced configuration
|
||||
data-parent="#cluster-config" href="#cluster-config-collapse" aria-expanded="true" aria-controls="cluster-config-collapse">
|
||||
Advanced cluster configuration
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="advanced-config-collapse" class="panel-collapse {# collapse #}" role="tabpanel" aria-labelledby="heading">
|
||||
<div id="cluster-config-collapse" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading">
|
||||
<div class="panel-body">
|
||||
<p>You can specify on which cluster/partion/node you want to start your Jupyter server:</p>
|
||||
|
||||
{# {% if slurm is not 'None' %} #}
|
||||
<div class="form-group">
|
||||
<label for="cluster" class="col-sm-3 control-label">Clusters:</label>
|
||||
<div class="col-sm-9">
|
||||
{%- for cluster in slurm -%}
|
||||
<div class="col-sm-6">
|
||||
<input type="radio" name="cluster" id="cluster_{{cluster}}" class="slurm_cluster"
|
||||
value="{{cluster}}">
|
||||
{% if 'N/A' not in sinfo %}
|
||||
<div class="form-group clusters">
|
||||
<label for="cluster" class="col-sm-3 control-label">Cluster:</label>
|
||||
<div class="col-sm-9 flex-container">
|
||||
{%- for cluster in sinfo.values() -%}
|
||||
<div class="flex-item-2 slurm-cluster" data-cluster="{{cluster}}">
|
||||
<input type="radio" name="cluster" id="cluster_{{cluster}}" value="{{cluster}}">
|
||||
<label for="cluster_{{cluster}}" class="btn btn-default btn-block">
|
||||
{{ cluster | capitalize }}
|
||||
</label>
|
||||
|
@ -27,40 +25,42 @@
|
|||
{% endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
{# {% endif %} #}
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="partition" class="col-sm-3 control-label">Partitions:</label>
|
||||
<div class="col-sm-9" id="partitions-list"></div>
|
||||
<div class="form-group partitions{% if 'N/A' not in sinfo %} hidden{% endif %}">
|
||||
<label for="partition" class="col-sm-3 control-label">Partition:</label>
|
||||
<div class="col-sm-9 flex-container">
|
||||
{%- for cluster in sinfo.values() -%}
|
||||
{%- for partition in cluster -%}
|
||||
<div class="flex-item-4 slurm-partition"
|
||||
data-cluster="{{cluster}}" data-partition="{{partition}}"
|
||||
data-cpu="{{partition.max_idle_cpu}}" data-mem="{{partition.max_mem}}" data-gpu="{{partition.gpus}}">
|
||||
<input type="radio" name="partition" id="partition_{{cluster}}_{{partition}}" value="{{partition}}">
|
||||
<label for="partition_{{cluster}}_{{partition}}" class="btn btn-default btn-block">
|
||||
{{ partition | capitalize }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="node" class="col-sm-3 control-label">Nodes:</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="col-sm-3">
|
||||
<input type="radio" name="node" id="node_0" value="0" checked>
|
||||
<label for="node_0" class="btn btn-default btn-block">
|
||||
cribbar033
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input type="radio" name="node" id="node_1" value="1">
|
||||
<label for="node_1" class="btn btn-default btn-block">
|
||||
cribbar034
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input type="radio" name="node" id="node_2" value="2">
|
||||
<label for="node_2" class="btn btn-default btn-block">
|
||||
cribbar035
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input type="radio" name="node" id="node_3" value="3">
|
||||
<label for="node_3" class="btn btn-default btn-block">
|
||||
cribbar036
|
||||
<div class="form-group nodes hidden">
|
||||
<label for="node" class="col-sm-3 control-label">Node:</label>
|
||||
<div class="col-sm-9 flex-container">
|
||||
{%- for cluster in sinfo.values() -%}
|
||||
{%- for partition in cluster -%}
|
||||
{%- for node in partition -%}
|
||||
<div class="flex-item-4 slurm-node" data-cluster="{{cluster}}" data-partition="{{partition}}"
|
||||
data-node="{{node}}" data-cpu="{{node.cpu.idle}}" data-mem="{{node.mem}}" data-gpu="{{node.gpu}}">
|
||||
<input type="radio" name="node" id="node_{{cluster}}_{{partition}}_{{node}}" value="{{node}}">
|
||||
<label for="node_{{cluster}}_{{partition}}_{{node}}" class="btn btn-default btn-block">
|
||||
{{ node | capitalize }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -68,3 +68,98 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="not-enough-resources" class="panel panel-danger hidden">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">⚠️ Not enough ressources</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
Please change your resources request or retry later…
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var $form = $('form');
|
||||
var $clusters = $('.slurm-cluster');
|
||||
var $partitions = $('.slurm-partition');
|
||||
var $nodes = $('.slurm-node');
|
||||
|
||||
get_config = function(){
|
||||
// Convert form into object (https://stackoverflow.com/a/17784656)
|
||||
return $form
|
||||
.serializeArray()
|
||||
.reduce(function (data, o) {
|
||||
data[o.name] = o.value;
|
||||
return data;
|
||||
}, {});
|
||||
}
|
||||
|
||||
_toggle = function (el, cpu, mem, gpu, cluster, partition) {
|
||||
if (
|
||||
(parseInt(cpu) > parseInt(el.dataset.cpu)) |
|
||||
(parseInt(mem) > parseInt(el.dataset.mem)) |
|
||||
(!el.dataset.gpu.includes(gpu)) |
|
||||
(cluster !== undefined & cluster != el.dataset.cluster) |
|
||||
(partition !== undefined & partition != el.dataset.partition)
|
||||
) {
|
||||
el.classList.add('hidden');
|
||||
el.querySelector('input').checked = false;
|
||||
} else {
|
||||
el.classList.remove('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
not_enough_resources = function (err) {
|
||||
var $err = $('#not-enough-resources');
|
||||
var $submit = $('input[type=submit]');
|
||||
|
||||
if (err) {
|
||||
$submit.addClass('hidden');
|
||||
$err.removeClass('hidden');
|
||||
} else {
|
||||
$submit.removeClass('hidden');
|
||||
$err.addClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
toggle_config = function() {
|
||||
var config = get_config();
|
||||
|
||||
if (config['cluster'] !== undefined) {
|
||||
$('.partitions').removeClass('hidden');
|
||||
}
|
||||
if (config['partition'] !== undefined) {
|
||||
$('.nodes').removeClass('hidden');
|
||||
}
|
||||
|
||||
$partitions.each(function(_, el){
|
||||
_toggle(el, config['cpu'], config['mem'], config['gpu'], config['cluster']);
|
||||
})
|
||||
|
||||
$nodes.each(function(_, el){
|
||||
_toggle(el, config['cpu'], config['mem'], config['gpu'], config['cluster'], config['partition']);
|
||||
})
|
||||
|
||||
if ($partitions.not('.hidden').length == 0) {
|
||||
$('.partitions').addClass('hidden');
|
||||
} else {
|
||||
$('.partitions').removeClass('hidden');
|
||||
}
|
||||
|
||||
if (config['partition'] === undefined | $nodes.not('.hidden').length == 0) {
|
||||
$('.nodes').addClass('hidden');
|
||||
} else {
|
||||
$('.nodes').removeClass('hidden');
|
||||
}
|
||||
|
||||
if ($partitions.not('.hidden').length == 0 & $nodes.not('.hidden').length == 0) {
|
||||
not_enough_resources(true);
|
||||
} else {
|
||||
not_enough_resources(false);
|
||||
}
|
||||
}
|
||||
|
||||
toggle_config();
|
||||
$form.change(toggle_config);
|
||||
|
||||
</script>
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
"""Test form templates module."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from glicid_spawner import form
|
||||
from glicid_spawner.form import options_form, options_from_form
|
||||
from glicid_spawner.form import options_attrs, options_form, options_from_form
|
||||
from glicid_spawner.micromamba import MicromambaEnv
|
||||
from glicid_spawner.slurm import sinfo_from_file
|
||||
from pytest import fixture
|
||||
|
||||
DATA = Path(__file__).parent / 'data'
|
||||
SINFO = sinfo_from_file(DATA / 'sinfo.txt')
|
||||
|
||||
SLURM_SINGLE_CLUSTER = {'N/A': SINFO.pop('N/A')}
|
||||
SLURM_MULTI_CLUSTER = SINFO
|
||||
|
||||
|
||||
def test_options_form(monkeypatch):
|
||||
"""Test options form render."""
|
||||
@fixture
|
||||
def mock_python_envs(monkeypatch):
|
||||
"""Mock python environments list."""
|
||||
monkeypatch.setattr(
|
||||
form,
|
||||
'get_envs',
|
||||
|
@ -19,6 +29,80 @@ def test_options_form(monkeypatch):
|
|||
],
|
||||
)
|
||||
|
||||
|
||||
@fixture
|
||||
def mock_cluster(monkeypatch, mock_python_envs):
|
||||
"""Mock multi cluster configuration (default)."""
|
||||
monkeypatch.setattr(form, 'sinfo', lambda _: SLURM_MULTI_CLUSTER)
|
||||
|
||||
|
||||
@fixture
|
||||
def mock_single_cluster(monkeypatch, mock_python_envs):
|
||||
"""Mock multi cluster configuration."""
|
||||
monkeypatch.setattr(form, 'sinfo', lambda _: SLURM_SINGLE_CLUSTER)
|
||||
|
||||
|
||||
def test_options_attrs(mock_cluster):
|
||||
"""Test form options attributes."""
|
||||
options = options_attrs('john-doe')
|
||||
|
||||
assert options['username'] == 'john-doe'
|
||||
|
||||
assert [env.path for env in options['envs']] == [
|
||||
'/john-doe/envs/foo',
|
||||
'/john-doe/envs/bar',
|
||||
'/global/envs/baz',
|
||||
]
|
||||
|
||||
cpu = options['cpus']
|
||||
|
||||
assert cpu[1] == 24
|
||||
assert cpu[24] == 1
|
||||
|
||||
mem = options['mems']
|
||||
|
||||
assert mem[4] == 24
|
||||
assert mem[96] == 1
|
||||
|
||||
gpu = options['gpus']
|
||||
|
||||
assert gpu['None'] == 24
|
||||
assert gpu['A100'] == 1
|
||||
|
||||
# Multi cluster configuration (default)
|
||||
sinfo = options['sinfo']
|
||||
|
||||
assert 'N/A' not in sinfo
|
||||
assert 'nautilus' in sinfo
|
||||
assert 'waves' in sinfo
|
||||
|
||||
node = sinfo['nautilus']['gpu']['gnode1']
|
||||
|
||||
assert node == 'gnode1'
|
||||
assert node.cpu.idle == 92
|
||||
assert node.gpu.name == 'A100'
|
||||
|
||||
|
||||
def test_options_attrs_single_cluster(mock_single_cluster):
|
||||
"""Test form options attributes in single cluster configuration."""
|
||||
options = options_attrs('john-doe')
|
||||
|
||||
# Single cluster configuration
|
||||
sinfo = options['sinfo']
|
||||
|
||||
assert 'N/A' in sinfo
|
||||
assert 'nautilus' not in sinfo
|
||||
assert 'waves' not in sinfo
|
||||
|
||||
node = sinfo['N/A']['Devel']['nazare001']
|
||||
|
||||
assert node == 'nazare001'
|
||||
assert node.cpu.idle == 20
|
||||
assert node.gpu.name == 'None'
|
||||
|
||||
|
||||
def test_options_form_resources(mock_cluster):
|
||||
"""Test options form render."""
|
||||
html = re.sub(r'\n\s+', ' ', options_form('john-doe')) # trim line breaks
|
||||
|
||||
# Username
|
||||
|
@ -31,61 +115,192 @@ def test_options_form(monkeypatch):
|
|||
|
||||
# CPU
|
||||
assert (
|
||||
'<input type="radio" name="cpu" id="cpu_0" value="0" data-max-duration="24" checked>'
|
||||
'<input type="radio" name="cpu" id="cpu_1" value="1" data-max-duration="24" checked>'
|
||||
in html
|
||||
)
|
||||
assert '<label for="cpu_0" class="btn btn-default btn-block"> 1 </label>' in html
|
||||
assert '<input type="radio" name="cpu" id="cpu_5" value="5" data-max-duration="1">' in html
|
||||
assert '<label for="cpu_5" class="btn btn-default btn-block"> 24 </label>' in html
|
||||
assert '<label for="cpu_1" class="btn btn-default btn-block"> 1 </label>' in html
|
||||
assert '<input type="radio" name="cpu" id="cpu_24" value="24" data-max-duration="1">' in html
|
||||
assert '<label for="cpu_24" class="btn btn-default btn-block"> 24 </label>' in html
|
||||
|
||||
# Memory
|
||||
assert (
|
||||
'<input type="radio" name="ram" id="ram_0" value="0" data-max-duration="24" checked>'
|
||||
'<input type="radio" name="mem" id="mem_4" value="4" data-max-duration="24" checked>'
|
||||
in html
|
||||
)
|
||||
assert '<label for="ram_0" class="btn btn-default btn-block"> 4 GB </label>' in html
|
||||
assert '<input type="radio" name="ram" id="ram_5" value="5" data-max-duration="1">' in html
|
||||
assert '<label for="ram_5" class="btn btn-default btn-block"> 96 GB </label>' in html
|
||||
assert '<label for="mem_4" class="btn btn-default btn-block"> 4 GB </label>' in html
|
||||
assert '<input type="radio" name="mem" id="mem_96" value="96" data-max-duration="1">' in html
|
||||
assert '<label for="mem_96" class="btn btn-default btn-block"> 96 GB </label>' in html
|
||||
|
||||
# GPU
|
||||
assert (
|
||||
'<input type="radio" name="gpu" id="gpu_0" value="0" data-max-duration="24" checked>'
|
||||
'<input type="radio" name="gpu" id="gpu_None" value="None" data-max-duration="24" checked>'
|
||||
in html
|
||||
)
|
||||
assert '<label for="gpu_None" class="btn btn-default btn-block"> None </label>' in html
|
||||
assert (
|
||||
'<input type="radio" name="gpu" id="gpu_A100" value="A100" data-max-duration="1">' in html
|
||||
)
|
||||
assert '<label for="gpu_A100" class="btn btn-default btn-block"> A100 </label>' in html
|
||||
|
||||
|
||||
def test_options_form_slurm(mock_cluster):
|
||||
"""Test options form render."""
|
||||
html = re.sub(r'\n\s+', ' ', options_form('john-doe')) # trim line breaks
|
||||
|
||||
# Cluster (multi-cluster by default)
|
||||
assert '<label for="cluster" class="col-sm-3 control-label">Cluster:</label>' in html
|
||||
|
||||
assert '<div class="flex-item-2 slurm-cluster" data-cluster="nautilus">' in html
|
||||
assert '<div class="flex-item-2 slurm-cluster" data-cluster="waves">' in html
|
||||
|
||||
assert '<input type="radio" name="cluster" id="cluster_nautilus" value="nautilus">' in html
|
||||
assert (
|
||||
'<label for="cluster_nautilus" class="btn btn-default btn-block"> Nautilus </label>' in html
|
||||
)
|
||||
|
||||
# Partitions (hidden by default for multi-cluster)
|
||||
assert '<div class="form-group partitions hidden">' in html
|
||||
assert '<label for="partition" class="col-sm-3 control-label">Partition:</label>' in html
|
||||
|
||||
assert (
|
||||
'<div class="flex-item-4 slurm-partition" '
|
||||
'data-cluster="nautilus" data-partition="gpu" '
|
||||
'data-cpu="96" data-mem="768" data-gpu="A100">' in html
|
||||
)
|
||||
|
||||
assert '<input type="radio" name="partition" id="partition_nautilus_gpu" value="gpu">' in html
|
||||
assert (
|
||||
'<label for="partition_nautilus_gpu" class="btn btn-default btn-block"> Gpu </label>'
|
||||
in html
|
||||
)
|
||||
|
||||
# Nodes (hidden by default)
|
||||
assert '<div class="form-group nodes hidden">' in html
|
||||
assert '<label for="node" class="col-sm-3 control-label">Node:</label>' in html
|
||||
|
||||
assert (
|
||||
'<div class="flex-item-4 slurm-node" '
|
||||
'data-cluster="nautilus" data-partition="gpu" data-node="gnode1" '
|
||||
'data-cpu="92" data-mem="768" data-gpu="A100">' in html
|
||||
)
|
||||
|
||||
assert '<input type="radio" name="node" id="node_nautilus_gpu_gnode1" value="gnode1">' in html
|
||||
assert (
|
||||
'<label for="node_nautilus_gpu_gnode1" class="btn btn-default btn-block"> Gnode1 </label>'
|
||||
in html
|
||||
)
|
||||
|
||||
|
||||
def test_options_form_slurm_single_cluster(mock_single_cluster):
|
||||
"""Test options form render."""
|
||||
html = re.sub(r'\n\s+', ' ', options_form('john-doe')) # trim line breaks
|
||||
|
||||
# No cluster
|
||||
assert '<label for="cluster" class="col-sm-3 control-label">Cluster:</label>' not in html
|
||||
|
||||
# Partitions (not hidden by default for single-cluster)
|
||||
assert '<div class="form-group partitions">' in html
|
||||
assert '<label for="partition" class="col-sm-3 control-label">Partition:</label>' in html
|
||||
|
||||
assert (
|
||||
'<div class="flex-item-4 slurm-partition" '
|
||||
'data-cluster="N/A" data-partition="GPU-short" '
|
||||
'data-cpu="20" data-mem="184" data-gpu="T4">' in html
|
||||
)
|
||||
|
||||
assert (
|
||||
'<input type="radio" name="partition" id="partition_N/A_GPU-short" value="GPU-short">'
|
||||
in html
|
||||
)
|
||||
assert (
|
||||
'<label for="partition_N/A_GPU-short" class="btn btn-default btn-block"> Gpu-short </label>'
|
||||
in html
|
||||
)
|
||||
|
||||
# Nodes (hidden by default)
|
||||
assert (
|
||||
'<div class="flex-item-4 slurm-node" '
|
||||
'data-cluster="N/A" data-partition="GPU-short" data-node="budbud001" '
|
||||
'data-cpu="20" data-mem="184" data-gpu="T4">' in html
|
||||
)
|
||||
|
||||
assert (
|
||||
'<input type="radio" name="node" id="node_N/A_GPU-short_budbud001" value="budbud001">'
|
||||
in html
|
||||
)
|
||||
assert (
|
||||
'<label for="node_N/A_GPU-short_budbud001" class="btn btn-default btn-block"> Budbud001 </label>'
|
||||
in html
|
||||
)
|
||||
assert '<label for="gpu_0" class="btn btn-default btn-block"> No </label>' in html
|
||||
assert '<input type="radio" name="gpu" id="gpu_1" value="1" data-max-duration="1">' in html
|
||||
assert '<label for="gpu_1" class="btn btn-default btn-block"> A100 </label>' in html
|
||||
|
||||
|
||||
def test_options_from_form():
|
||||
"""Test options from form parser."""
|
||||
# No GPU
|
||||
formdata = {
|
||||
'python-env': ['/john-doe/envs/bar'],
|
||||
'python-env': ['/john-doe/envs/foo'],
|
||||
'cpu': ['1'],
|
||||
'ram': ['2'],
|
||||
'gpu': ['0'],
|
||||
'mem': ['4'],
|
||||
'gpu': ['None'],
|
||||
}
|
||||
|
||||
assert options_from_form(formdata) == {
|
||||
'pyenv': '/john-doe/envs/foo',
|
||||
'nprocs': 1,
|
||||
'memory': '4GB',
|
||||
'runtime': '24:00:00',
|
||||
}
|
||||
|
||||
# With GPU (in defaults list)
|
||||
formdata = {
|
||||
'python-env': ['/john-doe/envs/bar'],
|
||||
'cpu': ['2'],
|
||||
'mem': ['16'],
|
||||
'gpu': ['A40'], # -> 2 h
|
||||
'cluster': ['nautilus'],
|
||||
}
|
||||
|
||||
assert options_from_form(formdata) == {
|
||||
'pyenv': '/john-doe/envs/bar',
|
||||
'nprocs': 2,
|
||||
'memory': '16GB',
|
||||
'runtime': '06:00:00',
|
||||
'runtime': '02:00:00',
|
||||
'gres': 'gpu:a40',
|
||||
'cluster': 'nautilus',
|
||||
}
|
||||
|
||||
# No GPU
|
||||
# With unknown GPU (default 1h allocation)
|
||||
formdata = {
|
||||
'python-env': ['/global/envs/baz'],
|
||||
'cpu': ['0'],
|
||||
'ram': ['0'],
|
||||
'gpu': ['1'],
|
||||
'cpu': ['8'],
|
||||
'mem': ['16'],
|
||||
'gpu': ['T4'],
|
||||
'partition': ['GPU-short'],
|
||||
}
|
||||
|
||||
assert options_from_form(formdata) == {
|
||||
'pyenv': '/global/envs/baz',
|
||||
'nprocs': 1,
|
||||
'memory': '4GB',
|
||||
'nprocs': 8,
|
||||
'memory': '16GB',
|
||||
'runtime': '01:00:00',
|
||||
'gres': 'gpu:a100',
|
||||
'gres': 'gpu:t4',
|
||||
'partition': 'GPU-short',
|
||||
}
|
||||
|
||||
# Invalid CPU request (0h allocated)
|
||||
formdata = {
|
||||
'python-env': ['/global/envs/qux'],
|
||||
'cpu': ['128'],
|
||||
'mem': ['4096'],
|
||||
'gpu': ['A100'],
|
||||
'node': ['cribbar001'],
|
||||
}
|
||||
|
||||
assert options_from_form(formdata) == {
|
||||
'pyenv': '/global/envs/qux',
|
||||
'nprocs': 128,
|
||||
'memory': '4096GB',
|
||||
'runtime': '00:00:00',
|
||||
'gres': 'gpu:a100',
|
||||
'node': 'cribbar001',
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue