blob: 1433c14a06ce008d34f11e3ea17c8927c49db053 [file] [edit]
/*****************************************************************************\
* gres_chunks.c - GRES device chunk filtering and selection
*****************************************************************************
* Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
*
* This file is part of Slurm, a resource management program.
* For details, see <https://slurm.schedmd.com/>.
* Please also read the included file: DISCLAIMER.
*
* Slurm is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* In addition, as a special exception, the copyright holders give permission
* to link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two. You must obey the GNU
* General Public License in all respects for all of the code used other than
* OpenSSL. If you modify file(s) with this exception, you may extend this
* exception to your version of the file(s), but you are not obligated to do
* so. If you do not wish to do so, delete this exception statement from your
* version. If you delete this exception statement from all source files in
* the program, then also delete it here.
*
* Slurm is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with Slurm; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
\*****************************************************************************/
#include "src/common/slurm_xlator.h"
#include "gres_chunks.h"
/*
* Find the chunk domain a request draws from. A typed request draws from its
* Type's domain. An untyped request draws from every device, so it only has a
* domain when all of the node's devices are of one chunked Type.
* IN gres_js - job state for the request
* IN gres_ns - node state for the GRES
* RET the domain, or NULL if none
*/
static gres_chunk_domain_t *_find_domain(gres_job_state_t *gres_js,
gres_node_state_t *gres_ns)
{
/* Job did not specify type in request */
if (!gres_js->type_id) {
/*
* GRES chunks can only be used for untyped job requests on
* nodes with exactly one chunk domain.
*/
if (gres_ns->chunk_domain_cnt != 1)
return NULL;
/*
* Make sure all devices on the node are the same type as the
* single chunk domain on the node.
*/
for (int i = 0; i < gres_ns->topo_cnt; i++) {
if (gres_ns->topo_type_id[i] !=
gres_ns->chunk_domains[0].type_id)
return NULL;
}
return &gres_ns->chunk_domains[0];
}
for (int i = 0; i < gres_ns->chunk_domain_cnt; i++) {
/* Find the node's chunk domain for the job's type */
if (gres_ns->chunk_domains[i].type_id == gres_js->type_id)
return &gres_ns->chunk_domains[i];
}
return NULL;
}
/* Test whether cnt is exactly one of the configured chunk sizes */
static bool _is_chunk_size(gres_chunk_domain_t *dom, uint64_t cnt)
{
for (int i = 0; i < dom->sizes_cnt; i++) {
if (dom->sizes[i] == cnt)
return true;
}
return false;
}
/*
* Test whether this node's per device bitmaps are available. Only
* _build_sock_gres_by_topo() fills them in. A node that has not registered
* since the controller started falls back to a builder that leaves them NULL,
* and the chunk layout cannot be checked without them.
* RET true if the chunk rules can be evaluated on this node
*/
static bool _have_dev_bitmaps(sock_gres_t *sock_gres)
{
if (sock_gres->bits_any_sock)
return true;
if (!sock_gres->bits_by_sock)
return false;
for (int s = 0; s < sock_gres->sock_cnt; s++) {
if (sock_gres->bits_by_sock[s])
return true;
}
return false;
}
/*
* Build the devices this job could use on this node: those on sockets
* with usable cores plus those tied to no socket, less what is allocated.
* IN sock_gres - job/node GRES details
* IN gres_ns - node state for the GRES
* IN used_cores_on_sock - cores the job holds per socket, NULL to ignore
* RET bitmap, must be freed by caller. NULL if none.
*/
static bitstr_t *_build_usable_dev_bitmap(sock_gres_t *sock_gres,
gres_node_state_t *gres_ns,
uint32_t *used_cores_on_sock)
{
bitstr_t *usable = NULL;
if (sock_gres->bits_any_sock)
usable = bit_copy(sock_gres->bits_any_sock);
if (sock_gres->bits_by_sock && sock_gres->cnt_by_sock) {
for (int s = 0; s < sock_gres->sock_cnt; s++) {
if (!sock_gres->cnt_by_sock[s] ||
!sock_gres->bits_by_sock[s])
continue;
/*
* Under enforce-binding the selection below only picks
* from sockets the job holds cores on, so a chunk
* chosen elsewhere can never be filled.
*/
if (used_cores_on_sock && !used_cores_on_sock[s])
continue;
if (!usable)
usable = bit_copy(sock_gres->bits_by_sock[s]);
else
bit_or(usable, sock_gres->bits_by_sock[s]);
}
}
if (!usable)
return NULL;
/* Only consider free devices unless testing against all GRES */
if (!sock_gres->use_total_gres && gres_ns->gres_bit_alloc)
bit_and_not(usable, gres_ns->gres_bit_alloc);
return usable;
}
/*
* Find the largest chunk size that still has a free aligned chunk.
* RET that size, or 0 if no chunk is free
*/
static uint64_t _largest_free_chunk(sock_gres_t *sock_gres,
gres_node_state_t *gres_ns,
gres_chunk_domain_t *dom)
{
bitstr_t *usable = NULL;
uint64_t best = 0;
/*
* No core binding here: this runs before the job is placed, so the
* sockets it will hold cores on are not known yet.
*/
if (!(usable = _build_usable_dev_bitmap(sock_gres, gres_ns, NULL)))
return 0;
/* Sizes are ascending, so walk them largest first */
for (int i = (dom->sizes_cnt - 1); i >= 0; i--) {
int size = dom->sizes[i];
/* Chunks of this size start at every multiple of size */
for (int start = 0; (start + size) <= dom->order_len;
start += size) {
int j;
/* Stop at the first device in this chunk not free */
for (j = 0; j < size; j++) {
if (!bit_test(usable, dom->order[start + j]))
break;
}
/* Every device is free, so this is the largest size */
if (j == size) {
best = size;
goto fini;
}
}
}
fini:
FREE_NULL_BITMAP(usable);
return best;
}
/*
* Pick a free aligned chunk. Best fit: among the free chunks, take the one
* whose next larger chunk has the fewest free devices, so larger free chunks
* stay whole for later jobs.
* IN usable - devices that may be picked
* IN dom - the request's chunk domain
* IN gres_needed - a configured chunk size, or every device in the domain
* RET bitmap of the chunk's devices, must be freed by caller. NULL if none.
*/
static bitstr_t *_pick_chunk(bitstr_t *usable, gres_chunk_domain_t *dom,
int gres_needed)
{
bitstr_t *picked = NULL;
int best_pos = -1, best_score = -1, parent_size = 0;
/* The next size up bounds how much free space a pick can break up */
for (int i = 0; i < dom->sizes_cnt; i++) {
if ((int) dom->sizes[i] > gres_needed) {
parent_size = dom->sizes[i];
break;
}
}
if (!parent_size)
parent_size = gres_needed;
/* Chunks of this size start at every multiple of gres_needed */
for (int start = 0; (start + gres_needed) <= dom->order_len;
start += gres_needed) {
int score = 0, j, parent;
/* Skip this chunk if any of its devices is not free */
for (j = 0; j < gres_needed; j++) {
if (!bit_test(usable, dom->order[start + j]))
break;
}
if (j < gres_needed)
continue;
/*
* Score this chunk by the free devices in the next larger
* chunk holding it. Each size divides the next, so the parent
* stays in range.
*/
parent = (start / parent_size) * parent_size;
for (j = 0; j < parent_size; j++) {
if (bit_test(usable, dom->order[parent + j]))
score++;
}
/* Keep the lowest score, ties go to the first in ChunkOrder */
if ((best_pos < 0) || (score < best_score)) {
best_pos = start;
best_score = score;
}
}
if (best_pos < 0)
return NULL;
picked = bit_alloc(bit_size(usable));
for (int j = 0; j < gres_needed; j++)
bit_set(picked, dom->order[best_pos + j]);
return picked;
}
/*
* Pick a free aligned chunk and narrow selection on this node to it.
* IN/OUT sock_gres - job/node GRES details, narrowed to the chunk
* IN gres_ns - node state for the GRES
* IN dom - the request's chunk domain
* IN gres_needed - a configured chunk size, or every device in the domain
* IN used_cores_on_sock - cores the job holds per socket, NULL to ignore
* RET SLURM_SUCCESS, or ESLURM_INVALID_GRES if no chunk is free
*/
static int _select_chunk(sock_gres_t *sock_gres, gres_node_state_t *gres_ns,
gres_chunk_domain_t *dom, int gres_needed,
uint32_t *used_cores_on_sock)
{
bitstr_t *usable = NULL, *picked = NULL;
if (!(usable = _build_usable_dev_bitmap(sock_gres, gres_ns,
used_cores_on_sock)))
return ESLURM_INVALID_GRES;
picked = _pick_chunk(usable, dom, gres_needed);
FREE_NULL_BITMAP(usable);
if (!picked)
return ESLURM_INVALID_GRES;
/*
* Narrow to the chunk so the selection below cannot pick outside it.
* Counts are only lowered, so a socket remove_unusable zeroed for core
* binding stays zeroed.
*/
if (sock_gres->bits_any_sock) {
bit_and(sock_gres->bits_any_sock, picked);
sock_gres->cnt_any_sock =
MIN(sock_gres->cnt_any_sock,
bit_set_count(sock_gres->bits_any_sock));
}
if (sock_gres->bits_by_sock) {
for (int s = 0; s < sock_gres->sock_cnt; s++) {
if (!sock_gres->bits_by_sock[s])
continue;
bit_and(sock_gres->bits_by_sock[s], picked);
if (sock_gres->cnt_by_sock) {
int cnt = 0;
cnt = bit_set_count(sock_gres->bits_by_sock[s]);
sock_gres->cnt_by_sock[s] =
MIN(sock_gres->cnt_by_sock[s], cnt);
}
}
}
sock_gres->total_cnt = MIN(sock_gres->total_cnt, gres_needed);
FREE_NULL_BITMAP(picked);
return SLURM_SUCCESS;
}
/*
* Test whether the chunk rules govern this request. A typed request is
* governed by its Type's domain, if it has one. An untyped request can draw
* from any device on the node, so any domain on the node governs it, even when
* no single domain holds every device and gres_chunks_request_validate()
* refuses it.
* IN gres_js - job state for the request
* IN gres_ns - node state for the GRES
* RET true if the chunk rules apply
*/
extern bool gres_chunks_apply(gres_job_state_t *gres_js,
gres_node_state_t *gres_ns)
{
if (!gres_ns->chunk_domain_cnt)
return false;
if (!gres_js->type_id)
return true;
if (!_find_domain(gres_js, gres_ns))
return false;
return true;
}
/* Reject request forms that cannot describe a per-node chunk. */
extern int gres_chunks_request_validate(sock_gres_t *sock_gres, bool whole_node)
{
gres_job_state_t *gres_js = sock_gres->gres_state_job->gres_data;
gres_node_state_t *gres_ns = sock_gres->gres_state_node->gres_data;
/*
* An untyped request on a node with several Types could take chunked
* devices without following their chunks. A whole node job takes every
* device on the node, so it never has to fit a chunk.
*/
if (!whole_node && !gres_js->type_id && gres_ns->chunk_domain_cnt &&
!_find_domain(gres_js, gres_ns)) {
log_flag(SELECT_TYPE, "%s: %s uses ChunkSizes on a node with several Types, so a request must name a Type",
__func__, sock_gres->gres_state_node->gres_name);
return SLURM_ERROR;
}
/*
* Chunks need a per-node count. gres_per_job leaves the split to the
* scheduler and gres_per_socket asks for a socket shape. gres_per_job
* is also derived from a per task request, so only trust it when
* nothing else pins the count down.
*/
if (!whole_node && gres_chunks_apply(gres_js, gres_ns) &&
gres_js->gres_per_socket) {
log_flag(SELECT_TYPE, "%s: %s uses ChunkSizes, which does not support a per socket request",
__func__, sock_gres->gres_state_node->gres_name);
return SLURM_ERROR;
}
if (!whole_node && gres_chunks_apply(gres_js, gres_ns) &&
gres_js->gres_per_job && !gres_js->gres_per_node &&
!gres_js->gres_per_task) {
log_flag(SELECT_TYPE, "%s: %s uses ChunkSizes, which does not support a per job request",
__func__, sock_gres->gres_state_node->gres_name);
return SLURM_ERROR;
}
return SLURM_SUCCESS;
}
/* Count the devices in bits that are part of picked, 0 if either is NULL */
static uint64_t _picked_cnt(bitstr_t *bits, bitstr_t *picked)
{
if (!bits || !picked)
return 0;
return bit_overlap(bits, picked);
}
/*
* Only one chunk is handed out, so the largest free one bounds this node.
* IN/OUT sock_gres - availability, capped to the largest free chunk
* IN gres_ns - node state for the GRES
* IN dom - the request's chunk domain
* IN chunk_gres - devices the job needs on this node, 0 if not fixed
* IN enforce_binding - GRES must be co-allocated with cores
* IN/OUT near_gres_cnt - available GRES near usable cores
*/
static void _cap_at_free_chunk(sock_gres_t *sock_gres,
gres_node_state_t *gres_ns,
gres_chunk_domain_t *dom, uint64_t chunk_gres,
bool enforce_binding, uint64_t *near_gres_cnt)
{
uint64_t chunk_max = _largest_free_chunk(sock_gres, gres_ns, dom);
bitstr_t *picked = NULL, *usable = NULL;
bool by_pick = enforce_binding && chunk_gres;
/*
* total_cnt can end up below the sum of the per-socket counts, so
* gres_filter_sock_core() must recompute it from the cap when it drops
* a socket, rather than subtract the socket's count from it.
*/
sock_gres->total_cnt = MIN(sock_gres->total_cnt, chunk_max);
sock_gres->links_cap = chunk_max;
/*
* Under enforce-binding, devices only come from sockets the job holds
* cores on, and the counts below decide where its cores land. Count
* only the devices of one free chunk of the size needed, so its cores
* land on every socket that chunk needs. Capping each socket on its
* own would let a socket with too few free devices draw the cores, or
* spread them over sockets that no single chunk fills.
*/
if (by_pick &&
(usable = _build_usable_dev_bitmap(sock_gres, gres_ns, NULL))) {
picked = _pick_chunk(usable, dom, chunk_gres);
FREE_NULL_BITMAP(usable);
}
/*
* gres_filter_sock_core() runs before selection and counts per socket,
* so leaving these uncapped lets it commit cores on sockets the chosen
* chunk does not reach.
*/
if (by_pick)
sock_gres->cnt_any_sock =
MIN(sock_gres->cnt_any_sock,
_picked_cnt(sock_gres->bits_any_sock, picked));
else
sock_gres->cnt_any_sock =
MIN(sock_gres->cnt_any_sock, chunk_max);
if (sock_gres->cnt_by_sock) {
for (int s = 0; s < sock_gres->sock_cnt; s++) {
bitstr_t *sock_bits = NULL;
uint64_t sock_max = chunk_max;
if (sock_gres->bits_by_sock)
sock_bits = sock_gres->bits_by_sock[s];
if (by_pick)
sock_max = _picked_cnt(sock_bits, picked);
sock_gres->cnt_by_sock[s] =
MIN(sock_gres->cnt_by_sock[s], sock_max);
}
}
FREE_NULL_BITMAP(picked);
/*
* Taken from total_cnt above, so without this the node reports more
* near GPUs than available ones.
*/
*near_gres_cnt = MIN(*near_gres_cnt, sock_gres->total_cnt);
}
/*
* Apply chunk size rules and cap availability at a free aligned chunk, so an
* impossible node fails here, not late in gres_select_filter_select_and_set().
*/
extern int gres_chunks_filter(sock_gres_t *sock_gres, bool whole_node,
bool single_node, uint16_t task_per_node,
bool enforce_binding, uint64_t *min_gres,
uint64_t *near_gres_cnt)
{
gres_job_state_t *gres_js = sock_gres->gres_state_job->gres_data;
gres_node_state_t *gres_ns = sock_gres->gres_state_node->gres_data;
gres_chunk_domain_t *dom = _find_domain(gres_js, gres_ns);
uint64_t dev_cnt, chunk_gres = 0;
/* No ChunkSizes apply to this request */
if (!dom)
return SLURM_SUCCESS;
/*
* A whole node job takes every device on the node, so it already holds
* the whole chunk domain and the size rules do not apply to it.
*/
if (whole_node)
return SLURM_SUCCESS;
dev_cnt = dom->order_len;
/*
* A chunk needs a definite per node count. gres_per_node is one, and a
* per task request becomes one once the tasks per node are fixed, by
* ntasks-per-node or a one node job.
*/
if (gres_js->gres_per_node) {
chunk_gres = gres_js->gres_per_node;
} else if (gres_js->gres_per_task && task_per_node &&
(task_per_node != NO_VAL16)) {
chunk_gres = gres_js->gres_per_task * task_per_node;
} else if (gres_js->gres_per_task && single_node) {
chunk_gres = gres_js->gres_per_job;
}
/*
* Guessing it from the total instead only works when the job happens
* to land the way the guess assumed, which fails late in
* gres_select_filter_select_and_set() and repeats forever.
*/
if (!chunk_gres && gres_js->gres_per_task) {
log_flag(SELECT_TYPE, "%s: %s uses ChunkSizes, so a per task request must fix the count per node, with --ntasks-per-node or a one node allocation",
__func__, sock_gres->gres_state_node->gres_name);
return SLURM_ERROR;
}
if (chunk_gres) {
/* A job spanning nodes must take each whole domain */
if (!single_node && (chunk_gres != dev_cnt)) {
log_flag(SELECT_TYPE, "%s: %s may span nodes, so it must request all %"PRIu64" devices per node rather than %"PRIu64,
__func__,
sock_gres->gres_state_node->gres_name,
dev_cnt, chunk_gres);
return SLURM_ERROR;
}
/* Only a partial request has to fit one chunk */
if ((chunk_gres != dev_cnt) &&
!_is_chunk_size(dom, chunk_gres)) {
log_flag(SELECT_TYPE, "%s: %s needs %"PRIu64" devices, which is not one of the configured ChunkSizes",
__func__,
sock_gres->gres_state_node->gres_name,
chunk_gres);
return SLURM_ERROR;
}
*min_gres = MAX(*min_gres, chunk_gres);
}
/*
* Skip the cap when the device bitmaps are missing, or a node that has
* not registered yet reads as having no free chunk and is rejected as
* never runnable.
*/
if ((chunk_gres != dev_cnt) && _have_dev_bitmaps(sock_gres))
_cap_at_free_chunk(sock_gres, gres_ns, dom, chunk_gres,
enforce_binding, near_gres_cnt);
return SLURM_SUCCESS;
}
/* Narrow final selection to one chunk before per-request device assignment. */
extern int gres_chunks_select(sock_gres_t *sock_gres, job_record_t *job_ptr,
node_record_t *node_ptr, uint32_t task_cnt,
uint32_t *used_cores_on_sock)
{
gres_job_state_t *gres_js = sock_gres->gres_state_job->gres_data;
gres_node_state_t *gres_ns = sock_gres->gres_state_node->gres_data;
gres_chunk_domain_t *dom = _find_domain(gres_js, gres_ns);
int gres_needed;
int rc = ESLURM_INVALID_GRES;
/* No ChunkSizes apply to this request */
if (!dom)
return SLURM_SUCCESS;
/*
* Skip nodes without the device bitmaps, matching the cap in
* _foreach_remove_unusable(). A node that has not registered since the
* controller started has none, and would fail here every cycle.
*/
if (!_have_dev_bitmaps(sock_gres))
return SLURM_SUCCESS;
/* Shared GRES are slices of one device, not chunks of devices */
if (gres_id_shared(sock_gres->gres_state_job->config_flags))
return SLURM_SUCCESS;
/*
* A whole node job takes every device on the node, which is the whole
* chunk domain and more, so there is no chunk to pick for it.
*/
if (job_ptr->details &&
(job_ptr->details->whole_node & WHOLE_NODE_REQUIRED))
return SLURM_SUCCESS;
gres_needed = dom->order_len;
if (gres_js->gres_per_node) {
gres_needed = gres_js->gres_per_node;
} else if (gres_js->gres_per_task) {
gres_needed = gres_js->gres_per_task * task_cnt;
}
/*
* Recheck the chunk rules at final selection using the actual node and
* task counts. A job spanning nodes must take every device on each.
*/
if ((bit_set_count(job_ptr->job_resrcs->node_bitmap) > 1) &&
(gres_needed != dom->order_len)) {
log_flag(SELECT_TYPE, "%s: %pJ spans nodes, so it must request all %d %s devices on node %s rather than %d",
__func__, job_ptr, dom->order_len,
sock_gres->gres_state_node->gres_name, node_ptr->name,
gres_needed);
return ESLURM_INVALID_GRES;
}
/* Otherwise it must take every device or exactly one chunk size */
if ((gres_needed != dom->order_len) &&
!_is_chunk_size(dom, gres_needed)) {
log_flag(SELECT_TYPE, "%s: %pJ needs %d %s devices on node %s, which is not one of the configured ChunkSizes",
__func__, job_ptr, gres_needed,
sock_gres->gres_state_node->gres_name, node_ptr->name);
return ESLURM_INVALID_GRES;
}
/*
* Commit to one chunk and narrow the socket bitmaps to it, so the
* selection that follows works unchanged and cannot reach outside it.
* Under enforce-binding _set_node_bits() only fills from sockets the
* job holds cores on, so a chunk chosen anywhere else fails every
* cycle. Keep the pick inside those sockets.
*/
rc = _select_chunk(sock_gres, gres_ns, dom, gres_needed,
(job_ptr->bit_flags & GRES_ENFORCE_BIND) ?
used_cores_on_sock :
NULL);
if (rc != SLURM_SUCCESS) {
log_flag(SELECT_TYPE, "%s: %pJ found no free chunk of %d %s devices on node %s",
__func__, job_ptr, gres_needed,
sock_gres->gres_state_node->gres_name, node_ptr->name);
return rc;
}
return SLURM_SUCCESS;
}