| /*****************************************************************************\ | |
| * gpu_amd.c - Support for AMD GPUs through the kernel sysfs interface. | |
| ***************************************************************************** | |
| * 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. | |
| \*****************************************************************************/ | |
| #define _GNU_SOURCE | |
| #include "../common/gpu_common.h" | |
| /* | |
| * gpu/amd: reads GPU information from the amdgpu kernel driver via sysfs, with | |
| * no vendor library. | |
| */ | |
| #define GPU_AMD_DEV_DIR "/dev/dri" | |
| #define GPU_AMD_DRM_DIR "/sys/class/drm" | |
| #define GPU_AMD_LINK_SELF (-1) | |
| #define GPU_AMD_MAX_XGMI_NODES 64 /* AMDGPU_XGMI_MAX_CONNECTED_NODES */ | |
| #define GPU_AMD_PCI_NAME_MAX 16 /* at most %04x:%02x:%02x.%d */ | |
| #define GPU_AMD_PCI_SLOT_NAME_SCAN \ | |
| "PCI_SLOT_NAME=%" XSTRINGIFY(GPU_AMD_PCI_NAME_MAX) "s" | |
| #define GPU_AMD_SYSFS_BUF 256 | |
| /* The slurmd configuration, defined for real by slurmd and slurmstepd. */ | |
| #if defined(__APPLE__) | |
| extern slurmd_conf_t *conf __attribute__((weak_import)); | |
| #else | |
| slurmd_conf_t *conf = NULL; | |
| #endif | |
| /* A card kept by discovery. */ | |
| typedef struct { | |
| int card; /* DRM card number */ | |
| char *name; /* lowercased GPU model, owned and freed per element */ | |
| char *pci_address; /* PCI slot name, owned and freed per element */ | |
| int render_minor; /* DRM render node minor */ | |
| unsigned long long xgmi_id; /* XGMI node ID, valid when xgmi_ok */ | |
| bool xgmi_ok; /* whether the XGMI node ID was read */ | |
| } amd_card_t; | |
| /* One XGMI hive node, read from a device/xgmi_hive_info/nodeN symlink. */ | |
| typedef struct { | |
| int node_num; /* N in the nodeN symlink name */ | |
| unsigned long long xgmi_id; /* the hive node's XGMI node ID */ | |
| } amd_xgmi_node_t; | |
| /* Required Slurm plugin symbols: */ | |
| const char plugin_name[] = "GPU AMD plugin"; | |
| const char plugin_type[] = "gpu/amd"; | |
| const uint32_t plugin_version = SLURM_VERSION_NUMBER; | |
| /* | |
| * The cards discovery emits a GRES for, in GRES index order, cached for the | |
| * process lifetime. Freed in fini(). | |
| */ | |
| static amd_card_t *amdgpu_cards = NULL; | |
| static int amdgpu_ncards = 0; | |
| /* Whether this load logged its discovery messages. */ | |
| static bool discovery_logged = false; | |
| /* | |
| * Build the sysfs path GPU_AMD_DRM_DIR/card<num>/device/<attr>. | |
| * | |
| * card_num (IN) The DRM card number. | |
| * attr (IN) The attribute name under the card's device directory. | |
| * | |
| * Returns an xmalloc'd path (caller frees). | |
| */ | |
| static char *_card_device_path(int card_num, const char *attr) | |
| { | |
| xassert(attr); | |
| return xstrdup_printf("%s/card%d/device/%s", GPU_AMD_DRM_DIR, card_num, | |
| attr); | |
| } | |
| /* | |
| * Check whether a DRM card is bound to the amdgpu kernel driver. | |
| * | |
| * card_num (IN) The DRM card number. | |
| * | |
| * Returns true if the card is an amdgpu device, false otherwise. | |
| */ | |
| static bool _is_amdgpu_card(int card_num) | |
| { | |
| char *path = NULL; | |
| char link[PATH_MAX] = { 0 }; | |
| ssize_t len = 0; | |
| path = _card_device_path(card_num, "driver"); | |
| len = readlink(path, link, sizeof(link) - 1); | |
| xfree(path); | |
| /* | |
| * readlink() neither NUL-terminates nor signals truncation, so a full | |
| * buffer may hold a truncated target. Fail rather than compare it. | |
| */ | |
| if ((len < 0) || (len >= (sizeof(link) - 1))) | |
| return false; | |
| link[len] = '\0'; | |
| return !xstrcmp(xbasename(link), "amdgpu"); | |
| } | |
| /* | |
| * Read the first line of a sysfs file, stripping the line terminator. | |
| * | |
| * path (IN) Full path of the sysfs file to read. | |
| * | |
| * Returns the xmalloc'd line (caller frees), NULL when the file cannot be read | |
| * or the line is empty. | |
| */ | |
| static char *_read_sysfs_line(const char *path) | |
| { | |
| FILE *fp = NULL; | |
| char *line = NULL, *value = NULL; | |
| size_t size = 0; | |
| xassert(path); | |
| if (!(fp = fopen(path, "r"))) | |
| return NULL; | |
| if (getline(&line, &size, fp) > 0) { | |
| line[strcspn(line, "\n")] = '\0'; | |
| if (line[0]) | |
| value = xstrdup(line); | |
| } | |
| fclose(fp); | |
| free(line); | |
| return value; | |
| } | |
| /* | |
| * Collect the amdgpu card numbers on the node, ordered ascending so the cache | |
| * fill walks them deterministically. | |
| * | |
| * cards (OUT) An xmalloc'd array of the card numbers (caller frees), NULL when | |
| * none was found. | |
| * | |
| * Returns the number of cards found. | |
| */ | |
| static int _list_amdgpu_cards(int **cards) | |
| { | |
| DIR *dr = NULL; | |
| struct dirent *de = NULL; | |
| int found = 0; | |
| int *found_cards = NULL; | |
| xassert(cards); | |
| *cards = NULL; | |
| if (!(dr = opendir(GPU_AMD_DRM_DIR))) | |
| return 0; | |
| while ((de = readdir(dr))) { | |
| int card_num = 0; | |
| char tail = '\0'; | |
| /* | |
| * Connector entries (card0-DP-1) share the prefix, so any | |
| * character after the number rejects the entry. | |
| */ | |
| if (sscanf(de->d_name, "card%d%c", &card_num, &tail) != 1) | |
| continue; | |
| if (!_is_amdgpu_card(card_num)) | |
| continue; | |
| xrecalloc(found_cards, (found + 1), sizeof(*found_cards)); | |
| found_cards[found++] = card_num; | |
| } | |
| closedir(dr); | |
| if (found) | |
| qsort(found_cards, found, sizeof(*found_cards), | |
| slurm_sort_int_list_asc); | |
| *cards = found_cards; | |
| return found; | |
| } | |
| /* | |
| * Get a card's DRM render minor, naming its /dev/dri/renderD<minor> file. | |
| * | |
| * card_num (IN) The DRM card number. | |
| * | |
| * Returns the renderD minor number, -1 when the card has no render node. | |
| */ | |
| static int _get_render_minor(int card_num) | |
| { | |
| char *dir_path = NULL; | |
| DIR *dr = NULL; | |
| struct dirent *de = NULL; | |
| int minor = -1; | |
| dir_path = _card_device_path(card_num, "drm"); | |
| dr = opendir(dir_path); | |
| xfree(dir_path); | |
| if (!dr) | |
| return -1; | |
| while ((de = readdir(dr))) { | |
| if (sscanf(de->d_name, "renderD%d", &minor) == 1) | |
| break; | |
| } | |
| closedir(dr); | |
| return minor; | |
| } | |
| /* | |
| * Read a card's PCI address from the PCI_SLOT_NAME line of its device uevent. | |
| * | |
| * card_num (IN) The DRM card number. | |
| * | |
| * Returns the xmalloc'd address (caller frees), NULL when none was read. | |
| */ | |
| static char *_read_pci_address(int card_num) | |
| { | |
| char *path = NULL, *pci_address = NULL; | |
| FILE *fp = NULL; | |
| char line[GPU_AMD_SYSFS_BUF] = { 0 }; | |
| char addr[GPU_AMD_PCI_NAME_MAX + 1] = { 0 }; | |
| path = _card_device_path(card_num, "uevent"); | |
| fp = fopen(path, "r"); | |
| xfree(path); | |
| if (!fp) | |
| return NULL; | |
| while (!pci_address && fgets(line, sizeof(line), fp)) { | |
| if (sscanf(line, GPU_AMD_PCI_SLOT_NAME_SCAN, addr) == 1) | |
| pci_address = xstrdup(addr); | |
| } | |
| fclose(fp); | |
| return pci_address; | |
| } | |
| /* | |
| * Read one line of a card's device/<attr> sysfs attribute. | |
| * | |
| * card_num (IN) The DRM card number. | |
| * attr (IN) The attribute name under the card's device directory. | |
| * | |
| * Returns the xmalloc'd line (caller frees), NULL when the attribute cannot be | |
| * read or is empty. | |
| */ | |
| static char *_read_card_attr(int card_num, const char *attr) | |
| { | |
| char *path = NULL, *line = NULL; | |
| xassert(attr); | |
| path = _card_device_path(card_num, attr); | |
| line = _read_sysfs_line(path); | |
| xfree(path); | |
| return line; | |
| } | |
| /* | |
| * Read a card's model name, lower-cased and underscorified to serve as a GRES | |
| * type name. product_name exists only where the driver read a board FRU | |
| * EEPROM, so fall back to the PCI device ID (device/device). | |
| * | |
| * card_num (IN) The DRM card number. | |
| * | |
| * Returns the xmalloc'd model name (caller frees), NULL when none is read. | |
| */ | |
| static char *_read_card_model(int card_num) | |
| { | |
| char *name = NULL; | |
| if (!(name = _read_card_attr(card_num, "product_name"))) | |
| name = _read_card_attr(card_num, "device"); | |
| if (name) | |
| gpu_common_underscorify_tolower(name); | |
| return name; | |
| } | |
| /* | |
| * Read a card's CPU affinity from sysfs (device/local_cpulist) and convert it | |
| * from machine format to Slurm's abstract format. | |
| * | |
| * xcpuinfo_mac_to_abs (IN) Converts a machine CPU range to abstract format. | |
| * card_num (IN) The DRM card number. | |
| * enabled_cpus (IN) The CPUs Slurm schedules, masking the list, or NULL to | |
| * keep every listed CPU. | |
| * cores (OUT) The abstract core range (caller frees), NULL when the affinity | |
| * could not be resolved. | |
| * | |
| * Returns the machine CPU affinity bitmap (caller frees), NULL when the | |
| * affinity could not be resolved. | |
| */ | |
| static bitstr_t *_set_cpu_affinity(int (*xcpuinfo_mac_to_abs)(char *mac, | |
| char **abs), | |
| int card_num, bitstr_t *enabled_cpus, | |
| char **cores) | |
| { | |
| char *path = NULL, *line = NULL, *mac_range = NULL; | |
| bitstr_t *cpus_bitmap = NULL; | |
| xassert(xcpuinfo_mac_to_abs); | |
| xassert(cores); | |
| *cores = NULL; | |
| path = _card_device_path(card_num, "local_cpulist"); | |
| if (!(line = _read_sysfs_line(path))) { | |
| error("%s: unable to read %s", __func__, path); | |
| xfree(path); | |
| return NULL; | |
| } | |
| cpus_bitmap = bit_alloc(MAX_CPUS); | |
| if (bit_unfmt(cpus_bitmap, line)) { | |
| error("%s: unable to parse CPU list \"%s\" from %s", | |
| __func__, line, path); | |
| FREE_NULL_BITMAP(cpus_bitmap); | |
| xfree(line); | |
| xfree(path); | |
| return NULL; | |
| } | |
| xfree(line); | |
| xfree(path); | |
| if (enabled_cpus) | |
| bit_and(cpus_bitmap, enabled_cpus); | |
| mac_range = bit_fmt_full(cpus_bitmap); | |
| if (xcpuinfo_mac_to_abs(mac_range, cores)) { | |
| error("%s: machine to abstract conversion failed", __func__); | |
| FREE_NULL_BITMAP(cpus_bitmap); | |
| } | |
| xfree(mac_range); | |
| return cpus_bitmap; | |
| } | |
| /* | |
| * Emit one GRES from its already-resolved fields. The caller keeps ownership | |
| * of every pointer argument. | |
| * | |
| * cpu_cnt (IN) The node's CPU count, recorded on the GRES. | |
| * gres_list (IN/OUT) The system GRES list to append to. | |
| * file (IN) The GRES device file. | |
| * type_name (IN) The GRES type name, or NULL to emit an untyped GRES. | |
| * unique_id (IN) The ROCR GPU-<hex> token, or NULL for selection by index. | |
| * cores (IN) The abstract core affinity range, or NULL when unresolved. | |
| * cpus_bitmap (IN) The machine CPU affinity bitmap, or NULL when unresolved. | |
| * links (IN) The GRES Links vector. | |
| */ | |
| static void _emit_gpu_gres(uint32_t cpu_cnt, list_t *gres_list, char *file, | |
| char *type_name, char *unique_id, char *cores, | |
| bitstr_t *cpus_bitmap, char *links) | |
| { | |
| gres_slurmd_conf_t gres_slurmd_conf = { | |
| .config_flags = (GRES_CONF_ENV_RSMI | GRES_CONF_AUTODETECT), | |
| .count = 1, | |
| .cpu_cnt = cpu_cnt, | |
| /* The GRES cpus field carries the abstract core range. */ | |
| .cpus = cores, | |
| .cpus_bitmap = cpus_bitmap, | |
| .file = file, | |
| .links = links, | |
| .name = "gpu", | |
| .type_name = type_name, | |
| .unique_id = unique_id, | |
| }; | |
| xassert(gres_list); | |
| xassert(file); | |
| xassert(links); | |
| add_gres_to_list(gres_list, &gres_slurmd_conf); | |
| } | |
| /* | |
| * Read a card's XGMI node ID from device/xgmi_device_id. | |
| * | |
| * card_num (IN) The DRM card number. | |
| * xgmi_id (OUT) The XGMI node id. | |
| * | |
| * Returns true if the card exposes an XGMI node ID, false otherwise. | |
| */ | |
| static bool _read_xgmi_node_id(int card_num, unsigned long long *xgmi_id) | |
| { | |
| char *line = NULL; | |
| bool ok = false; | |
| xassert(xgmi_id); | |
| if (!(line = _read_card_attr(card_num, "xgmi_device_id"))) | |
| return false; | |
| ok = (sscanf(line, "%llu", xgmi_id) == 1); | |
| xfree(line); | |
| return ok; | |
| } | |
| /* | |
| * Order XGMI hive nodes by hive node number. The kernel hands the XGMI | |
| * firmware the hive node list in that order and copies its answer back by | |
| * position, so the xgmi_num_links columns follow it, the same alignment the | |
| * kernel's own xgmi_port_num attribute relies on. | |
| * | |
| * a (IN) First hive node to compare (amd_xgmi_node_t). | |
| * b (IN) Second hive node to compare (amd_xgmi_node_t). | |
| * | |
| * Returns negative, zero, or positive as a sorts before, at, or after b. | |
| */ | |
| static int _sort_xgmi_node(const void *a, const void *b) | |
| { | |
| const amd_xgmi_node_t *na = a, *nb = b; | |
| xassert(a); | |
| xassert(b); | |
| return slurm_sort_int_list_asc(&na->node_num, &nb->node_num); | |
| } | |
| /* | |
| * Map each xgmi_num_links column to the XGMI node ID of its hive node by | |
| * reading the nodeN symlinks (device/xgmi_hive_info/nodeN/xgmi_device_id) in | |
| * hive node number order. A column count that differs from the hive node | |
| * count makes the caller emit the placeholder Links instead of a guessed | |
| * pairing. | |
| * | |
| * card_num (IN) The DRM card number. | |
| * col_xgmi_id (OUT) XGMI node ID of each column, GPU_AMD_MAX_XGMI_NODES | |
| * entries. | |
| * | |
| * Returns the number of columns mapped, or 0 on error. | |
| */ | |
| static int _xgmi_hive_node_ids(int card_num, unsigned long long *col_xgmi_id) | |
| { | |
| char *dir_path = NULL; | |
| DIR *dr = NULL; | |
| struct dirent *de = NULL; | |
| int ncolumns = 0; | |
| amd_xgmi_node_t hive_nodes[GPU_AMD_MAX_XGMI_NODES] = { { 0 } }; | |
| xassert(col_xgmi_id); | |
| dir_path = _card_device_path(card_num, "xgmi_hive_info"); | |
| if (!(dr = opendir(dir_path))) { | |
| log_flag(GRES, "%s: card%d: cannot open xgmi_hive_info, using empty links", | |
| __func__, card_num); | |
| xfree(dir_path); | |
| return 0; | |
| } | |
| while ((ncolumns < GPU_AMD_MAX_XGMI_NODES) && (de = readdir(dr))) { | |
| char *path = NULL, *line = NULL; | |
| if (sscanf(de->d_name, "node%d", | |
| &hive_nodes[ncolumns].node_num) != 1) | |
| continue; | |
| path = xstrdup_printf("%s/%s/xgmi_device_id", dir_path, | |
| de->d_name); | |
| line = _read_sysfs_line(path); | |
| xfree(path); | |
| if (!line) | |
| continue; | |
| if (sscanf(line, "%llu", &hive_nodes[ncolumns].xgmi_id) == 1) | |
| ncolumns++; | |
| xfree(line); | |
| } | |
| closedir(dr); | |
| xfree(dir_path); | |
| if (!ncolumns) { | |
| log_flag(GRES, "%s: card%d: xgmi_hive_info has no readable hive nodes, using empty links", | |
| __func__, card_num); | |
| return 0; | |
| } | |
| /* Order by hive node number so columns line up with xgmi_num_links */ | |
| qsort(hive_nodes, ncolumns, sizeof(*hive_nodes), _sort_xgmi_node); | |
| for (int i = 0; i < ncolumns; i++) | |
| col_xgmi_id[i] = hive_nodes[i].xgmi_id; | |
| return ncolumns; | |
| } | |
| /* | |
| * Build the GRES Links string for the GPU at gpu_index: a comma-separated link | |
| * count to each GPU, -1 for itself, 0 for no connection. xgmi_num_links gives | |
| * the count per hive node, whose XGMI node IDs map the columns to GPU indexes. | |
| * | |
| * gpu_index (IN) The GPU whose links to build. | |
| * | |
| * Returns the links string, or NULL when there is no XGMI hive or the topology | |
| * is inconsistent. | |
| */ | |
| static char *_xgmi_links_create(int gpu_index) | |
| { | |
| const amd_card_t *self = &amdgpu_cards[gpu_index]; | |
| const char *sep = ""; | |
| char *line = NULL, *tok = NULL, *save = NULL, *links_str = NULL; | |
| int ncols = 0, nhive_nodes = 0; | |
| uint8_t link_cnt[GPU_AMD_MAX_XGMI_NODES] = { 0 }; | |
| unsigned long long col_xgmi_id[GPU_AMD_MAX_XGMI_NODES] = { 0 }; | |
| int *links = NULL; | |
| if (!self->xgmi_ok) | |
| return NULL; | |
| if (!(line = _read_card_attr(self->card, "xgmi_num_links"))) | |
| return NULL; | |
| /* uint8_t num_links columns, "%02x " (amdgpu_xgmi_show_num_links()) */ | |
| while ((ncols < GPU_AMD_MAX_XGMI_NODES) && | |
| (tok = xstrtoken(line, " ", &save))) { | |
| unsigned int num_links = 0; | |
| if (sscanf(tok, "%x", &num_links) != 1) | |
| break; | |
| link_cnt[ncols++] = (uint8_t) num_links; | |
| } | |
| xfree(line); | |
| /* The columns and the hive node list must line up one-to-one */ | |
| if (!(nhive_nodes = _xgmi_hive_node_ids(self->card, col_xgmi_id))) | |
| return NULL; | |
| if (nhive_nodes != ncols) { | |
| log_flag(GRES, "%s: card%d: xgmi_num_links columns (%d) do not line up with hive nodes (%d), using empty links", | |
| __func__, self->card, ncols, nhive_nodes); | |
| return NULL; | |
| } | |
| links = xcalloc(amdgpu_ncards, sizeof(*links)); | |
| links[gpu_index] = GPU_AMD_LINK_SELF; | |
| /* | |
| * Match each hive column to a kept card by XGMI node id. The hive lists | |
| * only local GPUs, so an unmatched column is a local hive node the | |
| * plugin did not keep and stays out of the GPU-to-GPU Links. | |
| */ | |
| for (int i = 0; i < ncols; i++) { | |
| bool is_gpu_peer = false; | |
| /* Self column */ | |
| if (col_xgmi_id[i] == self->xgmi_id) | |
| continue; | |
| for (int j = 0; j < amdgpu_ncards; j++) { | |
| if (!amdgpu_cards[j].xgmi_ok || | |
| (amdgpu_cards[j].xgmi_id != col_xgmi_id[i])) | |
| continue; | |
| links[j] = link_cnt[i]; | |
| is_gpu_peer = true; | |
| break; | |
| } | |
| if (!is_gpu_peer) | |
| log_flag(GRES, "%s: card%d: XGMI node ID %llu is a local hive node not among the matched cards, skipping", | |
| __func__, self->card, col_xgmi_id[i]); | |
| } | |
| for (int i = 0; i < amdgpu_ncards; i++) { | |
| xstrfmtcat(links_str, "%s%d", sep, links[i]); | |
| sep = ","; | |
| } | |
| xfree(links); | |
| return links_str; | |
| } | |
| /* | |
| * Log the node's GPU capabilities at info, after discovery: the detected | |
| * count and how many GPUs expose each capability the plugin reads. | |
| */ | |
| static void _log_capabilities(void) | |
| { | |
| int xgmi_gpus = 0; | |
| info("%d GPU system device(s) detected", amdgpu_ncards); | |
| for (int i = 0; i < amdgpu_ncards; i++) { | |
| if (amdgpu_cards[i].xgmi_ok) | |
| xgmi_gpus++; | |
| } | |
| info(" inter-GPU XGMI links: %d/%d", xgmi_gpus, amdgpu_ncards); | |
| } | |
| /* | |
| * Order cached cards by PCI address, the device numbering gres.conf documents. | |
| * The kernel zero pads the address fields, so text order is numeric order. | |
| * | |
| * a (IN) First card to compare (amd_card_t). | |
| * b (IN) Second card to compare (amd_card_t). | |
| * | |
| * Returns negative, zero, or positive as a sorts before, at, or after b. | |
| */ | |
| static int _sort_by_pci_address(const void *a, const void *b) | |
| { | |
| const amd_card_t *ca = a, *cb = b; | |
| xassert(a); | |
| xassert(b); | |
| return xstrcmp(ca->pci_address, cb->pci_address); | |
| } | |
| /* | |
| * Discover the GPUs on the first call, caching them ordered by PCI address, | |
| * packed densely so entry i is the card behind GRES index i. A failed listing | |
| * stays uncached for a later retry. | |
| * | |
| * Returns the number of cached cards, 0 when none could be cached. | |
| */ | |
| static int _discover_gpus(void) | |
| { | |
| int ncards = 0; | |
| int *cards = NULL; | |
| if (amdgpu_cards) | |
| return amdgpu_ncards; | |
| if (!(ncards = _list_amdgpu_cards(&cards))) | |
| return 0; | |
| /* | |
| * Keep only cards exposing a render node and a readable model, caching | |
| * them so the emit loop needs no second read. The kept list is dense, | |
| * so every Links vector discovery sizes to it is exact (an over-wide | |
| * one drains the node). | |
| */ | |
| amdgpu_cards = xcalloc(ncards, sizeof(*amdgpu_cards)); | |
| for (int i = 0; i < ncards; i++) { | |
| amd_card_t *kept = &amdgpu_cards[amdgpu_ncards]; | |
| char *mode = NULL; | |
| /* Skip: a fileless GRES cannot be device-cgroup isolated. */ | |
| if ((kept->render_minor = _get_render_minor(cards[i])) < 0) { | |
| error("%s: card%d: no render node, no GRES emitted", | |
| __func__, cards[i]); | |
| continue; | |
| } | |
| if (!(kept->pci_address = _read_pci_address(cards[i]))) { | |
| error("%s: card%d: unreadable PCI address, no GRES emitted", | |
| __func__, cards[i]); | |
| continue; | |
| } | |
| if (!(kept->name = _read_card_model(cards[i]))) { | |
| error("%s: card%d: unreadable model, no GRES emitted", | |
| __func__, cards[i]); | |
| xfree(kept->pci_address); | |
| continue; | |
| } | |
| /* | |
| * A compute partition mode other than SPX splits the GPU into | |
| * several KFD devices this DRM scan does not see; the GRES | |
| * stays the whole GPU. | |
| */ | |
| if ((mode = _read_card_attr(cards[i], | |
| "current_compute_partition"))) { | |
| if (xstrcasecmp(mode, "SPX")) | |
| error("%s: card%d: compute partition mode %s is not supported, only SPX; the whole GPU is one GRES", | |
| __func__, cards[i], mode); | |
| xfree(mode); | |
| } | |
| kept->card = cards[i]; | |
| kept->xgmi_ok = _read_xgmi_node_id(cards[i], &kept->xgmi_id); | |
| amdgpu_ncards++; | |
| } | |
| xfree(cards); | |
| /* The array, not the count, marks a fill, keeping failure retryable. */ | |
| if (!amdgpu_ncards) { | |
| xfree(amdgpu_cards); | |
| return 0; | |
| } | |
| /* | |
| * Order the cache by PCI address, so GRES index i is the GPU with the | |
| * i-th smallest address, the device numbering gres.conf documents. | |
| */ | |
| qsort(amdgpu_cards, amdgpu_ncards, sizeof(*amdgpu_cards), | |
| _sort_by_pci_address); | |
| return amdgpu_ncards; | |
| } | |
| /* | |
| * Build the GRES conf list of the AMD GPUs detected on the node. | |
| * | |
| * node_config (IN) The node configuration. | |
| * | |
| * Returns the GRES conf list (caller frees), or NULL if no GPUs are found or | |
| * on error. | |
| */ | |
| static list_t *_get_system_gpu_list_amd(const node_config_load_t *node_config) | |
| { | |
| list_t *gres_list_system = NULL; | |
| bitstr_t *enabled_cpus = NULL; | |
| int nuuid = 0; | |
| xassert(node_config); | |
| if (!_discover_gpus()) | |
| return NULL; | |
| gres_list_system = list_create(destroy_gres_slurmd_conf); | |
| /* Mask out E-cores, which local_cpulist lists but slurmd may not. */ | |
| enabled_cpus = gpu_common_get_enabled_cpus(); | |
| for (int i = 0; i < amdgpu_ncards; i++) { | |
| char *file = NULL, *uuid = NULL, *unique_id = NULL; | |
| char *cores = NULL, *links = NULL; | |
| bitstr_t *cpus_bitmap = NULL; | |
| debug2("GPU(amd) card%d:", amdgpu_cards[i].card); | |
| debug2(" Name: %s", amdgpu_cards[i].name); | |
| debug2(" PCI Address: %s", amdgpu_cards[i].pci_address); | |
| /* | |
| * Both DRM nodes as one MultipleFiles group, so the device | |
| * cgroup confines the card node too: a card node left | |
| * accessible hands its first opener the DRM master role with | |
| * the full API. | |
| */ | |
| file = xstrdup_printf("%s/renderD%d,%s/card%d", GPU_AMD_DEV_DIR, | |
| amdgpu_cards[i].render_minor, | |
| GPU_AMD_DEV_DIR, amdgpu_cards[i].card); | |
| debug2(" Device File: %s", file); | |
| /* | |
| * amdgpu exports 16 hex ID digits only for GPUs carrying a | |
| * hardware ID, leaving the attribute absent or empty on the | |
| * rest. ROCR reports the same digits as "GPU-<hex>" and | |
| * matches a ROCR_VISIBLE_DEVICES token regardless of case. Under | |
| * Flags=env_uuid a job allocated a GPU with no ID gets numeric | |
| * indices for all of its GPUs. | |
| */ | |
| if ((uuid = _read_card_attr(amdgpu_cards[i].card, | |
| "unique_id"))) { | |
| unique_id = xstrdup_printf("GPU-%s", uuid); | |
| xfree(uuid); | |
| nuuid++; | |
| } | |
| debug2(" UUID: %s", unique_id); | |
| cpus_bitmap = | |
| _set_cpu_affinity(node_config->xcpuinfo_mac_to_abs, | |
| amdgpu_cards[i].card, enabled_cpus, | |
| &cores); | |
| if (cpus_bitmap) { | |
| char *mac_range = bit_fmt_full(cpus_bitmap); | |
| debug2(" CPU Affinity Range - Machine: %s", mac_range); | |
| xfree(mac_range); | |
| } | |
| debug2(" Core Affinity Range - Abstract: %s", cores); | |
| /* Per-peer XGMI link counts, or a device-order placeholder. */ | |
| if (!(links = _xgmi_links_create(i))) | |
| links = gres_links_create_empty(i, amdgpu_ncards); | |
| debug2(" Links: %s", links); | |
| _emit_gpu_gres(node_config->cpu_cnt, gres_list_system, file, | |
| amdgpu_cards[i].name, unique_id, cores, | |
| cpus_bitmap, links); | |
| xfree(unique_id); | |
| xfree(cores); | |
| FREE_NULL_BITMAP(cpus_bitmap); | |
| xfree(file); | |
| xfree(links); | |
| } | |
| FREE_NULL_BITMAP(enabled_cpus); | |
| /* | |
| * Log once per process: with AutoDetect=full the plugin probe and the | |
| * GRES load both run this discovery. | |
| */ | |
| if (!discovery_logged) { | |
| if (nuuid && (nuuid < amdgpu_ncards)) | |
| warning("gpu/amd: node has GPUs both with and without a UUID. With Flags=env_uuid, a job allocated a GPU without a UUID gets numeric indices for all of its GPUs."); | |
| _log_capabilities(); | |
| discovery_logged = true; | |
| } | |
| return gres_list_system; | |
| } | |
| /* | |
| * Initialize the plugin on load. | |
| * | |
| * Returns SLURM_SUCCESS. | |
| */ | |
| extern int init(void) | |
| { | |
| debug("%s loaded", plugin_name); | |
| return SLURM_SUCCESS; | |
| } | |
| /* | |
| * Free plugin state on unload. | |
| */ | |
| extern void fini(void) | |
| { | |
| for (int i = 0; i < amdgpu_ncards; i++) { | |
| xfree(amdgpu_cards[i].name); | |
| xfree(amdgpu_cards[i].pci_address); | |
| } | |
| xfree(amdgpu_cards); | |
| amdgpu_ncards = 0; | |
| discovery_logged = false; | |
| debug("unloading %s", plugin_name); | |
| } | |
| /* | |
| * Detect the node's AMD GPUs via sysfs and return them as a GRES conf list. | |
| * | |
| * node_config (IN) The node configuration. | |
| * | |
| * Returns the GRES conf list (the caller frees it), or NULL on error. | |
| */ | |
| extern list_t *gpu_p_get_system_gpu_list(node_config_load_t *node_config) | |
| { | |
| list_t *gres_list_system = NULL; | |
| xassert(node_config); | |
| if (!(gres_list_system = _get_system_gpu_list_amd(node_config))) | |
| error("System GPU detection failed"); | |
| return gres_list_system; | |
| } | |
| /* | |
| * At step start (in slurmstepd), report a requested GPU frequency as not | |
| * controlled, like gpu/generic does. This plugin does not drive GPU clocks. | |
| * | |
| * usable_gpus (IN) Bitmap of the GPUs allocated to the step. | |
| * tres_freq (IN) The step's TRES frequency specification. | |
| */ | |
| extern void gpu_p_step_hardware_init(bitstr_t *usable_gpus, char *tres_freq) | |
| { | |
| xassert(tres_freq); | |
| xassert(usable_gpus); | |
| if (!usable_gpus || !tres_freq) | |
| return; | |
| /* No GPU frequency spec */ | |
| if (!xstrstr(tres_freq, "gpu:")) | |
| return; | |
| fprintf(stderr, "GpuFreq=control_disabled\n"); | |
| } | |
| /* | |
| * At step end (in slurmstepd), reset each card gpu_p_step_hardware_init() | |
| * drove back to automatic performance management. | |
| */ | |
| extern void gpu_p_step_hardware_fini(void) | |
| { | |
| return; | |
| } | |
| /* | |
| * Testsuite CPU range conversion hook, not implemented by this plugin. | |
| * | |
| * cpu_range (IN) The CPU range string to convert. | |
| * | |
| * Returns NULL. | |
| */ | |
| extern char *gpu_p_test_cpu_conv(char *cpu_range) | |
| { | |
| return NULL; | |
| } | |
| /* | |
| * Read the current power draw of a GPU for energy accounting. | |
| * | |
| * dv_ind (IN) The GPU index. | |
| * gpu (IN/OUT) The GPU status to update with the power reading. | |
| * | |
| * Returns SLURM_SUCCESS. | |
| */ | |
| extern int gpu_p_energy_read(uint32_t dv_ind, gpu_status_t *gpu) | |
| { | |
| xassert(gpu); | |
| return SLURM_SUCCESS; | |
| } | |
| /* | |
| * Count the node's amdgpu GPUs, that is the GPUs discovery emits a GRES for. | |
| * | |
| * device_count (OUT) Set to the number of AMD GPUs found, or 0 on error. | |
| */ | |
| extern void gpu_p_get_device_count(uint32_t *device_count) | |
| { | |
| xassert(device_count); | |
| *device_count = _discover_gpus(); | |
| } | |
| /* | |
| * Read a process's GPU memory and compute-unit usage for job accounting. | |
| * | |
| * pid (IN) The process ID to read GPU usage for. | |
| * data (IN/OUT) The accounting array to fill with the process's GPU usage. | |
| * | |
| * Returns SLURM_SUCCESS. | |
| */ | |
| extern int gpu_p_usage_read(pid_t pid, acct_gather_data_t *data) | |
| { | |
| xassert(data); | |
| return SLURM_SUCCESS; | |
| } |