Add SLURM resources to form template

This commit is contained in:
Benoît Seignovert 2024-02-20 15:13:14 +01:00
parent b8efa00a05
commit 11d878cecf
Signed by: Benoît Seignovert
GPG key ID: F5D8895227D18A0B
7 changed files with 506 additions and 112 deletions

View file

@ -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:

View file

@ -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)

View file

@ -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

View file

@ -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%;
}

View file

@ -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>

View file

@ -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&hellip;
</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>

View file

@ -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',
}