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

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