blob: 74bdf1cc32c4c812937ac48bc56a36b75c17bf58 [file] [edit]
/*****************************************************************************\
* gres_tpu.c - Support TPUs as a generic resource.
*****************************************************************************
* Copyright (C) 2026 Google LLC.
*
* 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 <dirent.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "slurm/slurm.h"
#include "slurm/slurm_errno.h"
#include "src/common/slurm_xlator.h"
#include "src/common/bitstring.h"
#include "src/common/env.h"
#include "src/common/list.h"
#include "src/common/read_config.h"
#include "src/common/run_in_daemon.h"
#include "src/common/tpu_conf.h"
#include "src/common/xstring.h"
#include "src/interfaces/gres.h"
#include "../common/gres_common.h"
/* Required Slurm plugin symbols: */
const char plugin_name[] = "Gres TPU plugin";
const char plugin_type[] = "gres/tpu";
const uint32_t plugin_version = SLURM_VERSION_NUMBER;
#define DEFAULT_SYSFS_PATH "/sys/bus/pci/devices"
/*
* Maximum PCI functions per device slot (PCI standard allows up to 8;
* multi-core TPU chips expose 2 functions per device).
*/
#define TPU_MAX_CORES 8
static list_t *gres_devices = NULL;
/*
* One physical accelerator chip, assembled from the PCI functions that share
* its base address. A chip is NOT a PCI device: tpu7x presents two functions
* per chip, each with its own IOMMU group and its own VFIO node.
*/
typedef struct {
char *base_addr; /* "0000:00:05", i.e. domain:bus:device */
char *cpus; /* local_cpulist of the lowest-numbered core */
const tpu_type_entry_t *entry; /* family this chip belongs to */
bool shared; /* shares an IOMMU group with another chip */
long sort_key; /* IOMMU group of the lowest-numbered core */
char *vfio[TPU_MAX_CORES]; /* "/dev/vfio/<group>" per core */
} tpu_chip_t;
typedef struct {
uint32_t chip_cnt;
const tpu_type_entry_t *entry;
list_t *gres_list;
node_config_load_t *node_config;
} build_args_t;
typedef struct {
tpu_chip_t *chip;
const char *vfio;
} find_vfio_args_t;
static void _destroy_tpu_chip(void *x)
{
tpu_chip_t *chip = x;
if (!chip)
return;
xfree(chip->base_addr);
xfree(chip->cpus);
for (int i = 0; i < TPU_MAX_CORES; i++)
xfree(chip->vfio[i]);
xfree(chip);
}
/*
* Read the first line of a sysfs attribute.
* RET xmalloc()'d contents with the trailing newline removed, or NULL.
*/
static char *_read_attr(const char *dir, const char *name, const char *attr)
{
char *path = NULL, *line = NULL, *value = NULL;
size_t size = 0;
FILE *fp;
xstrfmtcat(path, "%s/%s/%s", dir, name, attr);
fp = fopen(path, "r");
xfree(path);
if (!fp)
return NULL;
/* local_cpulist of a large node can be longer than any fixed buffer */
if (getline(&line, &size, fp) > 0) {
line[strcspn(line, "\r\n")] = '\0';
if (line[0])
value = xstrdup(line);
}
fclose(fp);
free(line);
return value;
}
/*
* Resolve the IOMMU group of a PCI device.
* RET group number, or -1 if the device is not bound to one. A TPU with no
* IOMMU group has no /dev/vfio node and cannot be handed to a job, so it
* is not a usable resource.
*/
static long _iommu_group(const char *dir, const char *name)
{
char *path = NULL, *base, *end = NULL;
char target[PATH_MAX];
ssize_t len;
long group;
xstrfmtcat(path, "%s/%s/iommu_group", dir, name);
len = readlink(path, target, sizeof(target) - 1);
xfree(path);
if (len <= 0)
return -1;
target[len] = '\0';
if (!(base = xstrrchr(target, '/')) || (base[1] < '0') ||
(base[1] > '9'))
return -1;
group = strtol(base + 1, &end, 10);
if ((group < 0) || !end || *end)
return -1;
return group;
}
static int _find_chip(void *x, void *key)
{
tpu_chip_t *chip = x;
return !xstrcmp(chip->base_addr, (char *) key);
}
/*
* Order chips by the IOMMU group of their first core, numerically.
*
* The GRES index a task sees is this list's order, so it has to be stable and
* it has to match what every other tool on the node reports. Comparing the
* device paths as strings would put /dev/vfio/10 before /dev/vfio/9 and
* silently bind tasks to the wrong chips on any node with ten or more groups.
*/
static int _sort_chips(void *x, void *y)
{
tpu_chip_t *a = *(tpu_chip_t **) x;
tpu_chip_t *b = *(tpu_chip_t **) y;
if (a->sort_key < b->sort_key)
return -1;
if (a->sort_key > b->sort_key)
return 1;
return 0;
}
/* Mark a chip other than args->chip that has args->vfio, and args->chip */
static int _mark_vfio_shared(void *x, void *arg)
{
tpu_chip_t *chip = x;
find_vfio_args_t *args = arg;
if (chip == args->chip)
return 0;
for (int i = 0; i < TPU_MAX_CORES; i++) {
if (!xstrcmp(chip->vfio[i], args->vfio)) {
chip->shared = true;
args->chip->shared = true;
}
}
return 0;
}
static int _mark_shared(void *x, void *arg)
{
tpu_chip_t *chip = x;
list_t *chips = arg;
find_vfio_args_t args = {
.chip = chip,
};
for (int i = 0; i < TPU_MAX_CORES; i++) {
if (!chip->vfio[i])
continue;
args.vfio = chip->vfio[i];
(void) list_for_each_ro(chips, _mark_vfio_shared, &args);
}
return 0;
}
static int _is_shared(void *x, void *key)
{
tpu_chip_t *chip = x;
if (!chip->shared)
return 0;
error("chip %s shares an IOMMU group with another chip, so it cannot be given to a job on its own and will be ignored. Enable ACS on the PCI bridge above it to give each chip its own group.",
chip->base_addr);
return 1;
}
/*
* Walk sysfs and assemble the chips physically present on this node.
*
* RET list of tpu_chip_t*, sorted. Never NULL, may be empty.
*/
static list_t *_scan_sysfs(tpu_conf_t *tpu_conf, const char *sysfs_path)
{
list_t *chips = list_create(_destroy_tpu_chip);
DIR *dir;
struct dirent *ent;
if (!(dir = opendir(sysfs_path))) {
debug("cannot open %s, no TPUs will be detected",
sysfs_path);
return chips;
}
while ((ent = readdir(dir))) {
char *vendor, *device, *base_addr, *dot, *end = NULL;
const tpu_type_entry_t *entry;
tpu_chip_t *chip;
long group, core;
if (ent->d_name[0] == '.')
continue;
/*
* Vendor 0x1ae0 also covers non-TPU virtual devices (e.g.
* gVNIC), so match both PCI vendor and device ID against
* families configured in tpu.yaml.
*/
vendor = _read_attr(sysfs_path, ent->d_name, "vendor");
device = _read_attr(sysfs_path, ent->d_name, "device");
entry = tpu_conf_find_pci_id(tpu_conf, vendor, device);
if (!entry) {
debug2("ignoring %s, PCI %s:%s is not configured in %s",
ent->d_name,
vendor ? vendor : "(none)",
device ? device : "(none)", TPU_CONF_FILE);
xfree(vendor);
xfree(device);
continue;
}
xfree(vendor);
xfree(device);
if ((group = _iommu_group(sysfs_path, ent->d_name)) < 0) {
error("%s matches a configured TPU but has no IOMMU group; it cannot be isolated and will be ignored. Check that the host booted with an IOMMU enabled.",
ent->d_name);
continue;
}
/* "0000:00:05.1" -> base "0000:00:05", core 1 */
base_addr = xstrdup(ent->d_name);
if (!(dot = xstrrchr(base_addr, '.')) || !dot[1]) {
error("PCI address %s has no function number",
ent->d_name);
xfree(base_addr);
continue;
}
*dot = '\0';
core = strtol(dot + 1, &end, 16);
if ((core < 0) || (core >= TPU_MAX_CORES) || !end || *end) {
error("PCI address %s has invalid function number",
ent->d_name);
xfree(base_addr);
continue;
}
if (!(chip = list_find_first(chips, _find_chip, base_addr))) {
chip = xmalloc(sizeof(*chip));
chip->base_addr = xstrdup(base_addr);
chip->entry = entry;
chip->sort_key = group;
list_append(chips, chip);
} else if (chip->entry != entry) {
error("PCI device %s reports a different TPU family than its sibling functions; ignoring",
ent->d_name);
xfree(base_addr);
continue;
}
if (chip->vfio[core]) {
error("duplicate PCI function %s",
ent->d_name);
xfree(base_addr);
continue;
}
xstrfmtcat(chip->vfio[core], "/dev/vfio/%ld", group);
/*
* Cores of one chip hang off the same physical device, so any
* of them gives the same CPU affinity. Keep the lowest one so
* the result does not depend on readdir() order.
*/
if ((group < chip->sort_key) || !chip->cpus) {
if (group < chip->sort_key)
chip->sort_key = group;
xfree(chip->cpus);
chip->cpus = _read_attr(sysfs_path, ent->d_name,
"local_cpulist");
}
xfree(base_addr);
}
closedir(dir);
/*
* VFIO hands out an IOMMU group as a whole. Without ACS on the PCI
* bridge above them, several chips can land in one group, and then no
* job can get one of them without the others, so leave them all out.
*/
(void) list_for_each_ro(chips, _mark_shared, chips);
(void) list_delete_all(chips, _is_shared, NULL);
list_sort(chips, _sort_chips);
return chips;
}
/* Join a chip's VFIO nodes in core order, e.g. "/dev/vfio/8,/dev/vfio/9". */
static char *_chip_file_str(tpu_chip_t *chip, int *core_cnt)
{
char *files = NULL;
*core_cnt = 0;
for (int i = 0; i < TPU_MAX_CORES; i++) {
if (!chip->vfio[i])
continue;
xstrfmtcat(files, "%s%s", (files ? "," : ""), chip->vfio[i]);
(*core_cnt)++;
}
return files;
}
static int _build_gres_record(void *x, void *arg)
{
tpu_chip_t *chip = x;
build_args_t *args = arg;
gres_slurmd_conf_t gres_slurmd_conf = { 0 };
char *abs_cpus = NULL;
int core_cnt = 0;
gres_slurmd_conf.file = _chip_file_str(chip, &core_cnt);
if (!gres_slurmd_conf.file) {
error("chip at %s has no usable cores",
chip->base_addr);
return SLURM_SUCCESS;
}
/*
* sysfs reports machine CPU numbering; Slurm records store both the
* abstract range string (cpus) and the machine-formatted bitmap
* (cpus_bitmap). Affinity is an optimization, so a failed conversion
* degrades to no affinity rather than dropping the chip.
*/
if (chip->cpus && args->node_config->xcpuinfo_mac_to_abs) {
if (args->node_config->xcpuinfo_mac_to_abs(chip->cpus,
&abs_cpus)) {
abs_cpus = NULL;
} else {
gres_slurmd_conf.cpus_bitmap =
bit_alloc(args->node_config->cpu_cnt);
if (!bit_size(gres_slurmd_conf.cpus_bitmap) ||
bit_unfmt(gres_slurmd_conf.cpus_bitmap,
chip->cpus)) {
FREE_NULL_BITMAP(gres_slurmd_conf.cpus_bitmap);
xfree(abs_cpus);
}
}
}
/*
* One record per chip, whatever its core count. add_gres_to_list()
* derives GRES_CONF_HAS_FILE and GRES_CONF_HAS_MULT from the file
* list, so a multi-core chip becomes a MultipleFiles GRES on its own.
*/
gres_slurmd_conf.config_flags = GRES_CONF_AUTODETECT;
gres_slurmd_conf.count = 1;
gres_slurmd_conf.cpu_cnt = args->node_config->cpu_cnt;
gres_slurmd_conf.cpus = abs_cpus;
gres_slurmd_conf.name = "tpu";
gres_slurmd_conf.type_name = chip->entry->type;
add_gres_to_list(args->gres_list, &gres_slurmd_conf);
debug("chip %s (%s) with %d core%s: %s",
chip->base_addr, chip->entry->type, core_cnt,
(core_cnt == 1 ? "" : "s"), gres_slurmd_conf.file);
args->chip_cnt++;
args->entry = chip->entry;
FREE_NULL_BITMAP(gres_slurmd_conf.cpus_bitmap);
xfree(abs_cpus);
xfree(gres_slurmd_conf.file);
return SLURM_SUCCESS;
}
/*
* Build the GRES records for the TPUs attached to this node.
* RET list of gres_slurmd_conf_t*, or NULL if no TPUs were found.
*/
static list_t *_get_system_tpu_list(node_config_load_t *node_config)
{
const char *sysfs_path;
list_t *chips, *gres_list;
build_args_t args = { 0 };
tpu_conf_t *tpu_conf = NULL;
int rc;
/*
* slurmctld also loads the GRES of cloud and external nodes, but their
* TPUs are on the nodes, not on the controller's host. Only probe real
* hardware from slurmd, as gres/gpu does. A mock sysfs tree given for
* testing is read anywhere.
*/
if (!node_config->in_slurmd && !getenv("SLURM_TPU_SYSFS_PATH"))
return NULL;
/*
* An operator who says AutoDetect=off has taken responsibility for
* gres.conf, so leave it alone. Every other setting, including no
* setting at all, discovers: a node with no TPUs finds nothing and
* loses nothing, so there is no reason to make the common case opt in.
* AutoDetect=tpu is accepted and means the same thing, spelled out.
*/
if (gres_get_autodetect_flags() & GRES_AUTODETECT_GPU_OFF) {
debug("AutoDetect=off, not scanning for TPUs");
return NULL;
}
/*
* Overridable via SLURM_TPU_SYSFS_PATH for testing against mock sysfs
* trees.
*/
if (!(sysfs_path = getenv("SLURM_TPU_SYSFS_PATH")))
sysfs_path = DEFAULT_SYSFS_PATH;
/*
* A missing tpu.yaml is not fatal: without it no device id maps to a
* family, so no TPUs are detected and the node simply has none. That
* keeps a cluster with the plugin enabled but no accelerators working.
*/
if ((rc = tpu_conf_load(&tpu_conf)) != SLURM_SUCCESS) {
debug("no usable %s (%s); TPU autodetection is disabled",
TPU_CONF_FILE, slurm_strerror(rc));
return NULL;
}
chips = _scan_sysfs(tpu_conf, sysfs_path);
if (!list_count(chips)) {
FREE_NULL_LIST(chips);
tpu_conf_destroy(tpu_conf);
return NULL;
}
gres_list = list_create(destroy_gres_slurmd_conf);
args.gres_list = gres_list;
args.node_config = node_config;
(void) list_for_each(chips, _build_gres_record, &args);
FREE_NULL_LIST(chips);
if (!list_count(gres_list)) {
FREE_NULL_LIST(gres_list);
tpu_conf_destroy(tpu_conf);
return NULL;
}
/*
* A node that reports a different chip count than its family's
* shape_per_node implies is misconfigured. Sizing decisions upstream
* are made from the shape, so surface it loudly rather than
* registering a count that will not add up.
*/
if (args.entry && (args.chip_cnt != args.entry->chips_per_node))
error("found %u %s chips but %s declares shape_per_node %s, which is %u chips per node. Jobs sized from --layout will not fit this node.",
args.chip_cnt, args.entry->type,
TPU_CONF_FILE, args.entry->shape_per_node,
args.entry->chips_per_node);
tpu_conf_destroy(tpu_conf);
return gres_list;
}
static int _is_tpu_gres(void *x, void *arg)
{
gres_slurmd_conf_t *gres_slurmd_conf = x;
return !xstrcasecmp(gres_slurmd_conf->name, "tpu");
}
extern int init(void)
{
debug("%s: %s loaded", __func__, plugin_name);
return SLURM_SUCCESS;
}
extern void fini(void)
{
debug("%s: unloading %s", __func__, plugin_name);
FREE_NULL_LIST(gres_devices);
}
extern int gres_p_node_config_load(list_t *gres_conf_list,
node_config_load_t *node_config)
{
int rc;
list_t *gres_list_system;
/* Assume this state is caused by an scontrol reconfigure */
if (gres_devices) {
debug("%s: Resetting gres_devices", plugin_name);
FREE_NULL_LIST(gres_devices);
}
if ((gres_list_system = _get_system_tpu_list(node_config))) {
debug("%s: replacing configured TPU GRES with detected TPUs",
plugin_name);
/*
* Autodetection is authoritative for TPUs. gres.conf cannot
* name a VFIO node usefully, because IOMMU group numbers are
* assigned at boot and are not stable across reboots, so any
* hand-written File= would be wrong sooner or later. Drop
* any configured TPU records while preserving other GRES types
* on the node, and transfer the detected TPU chips in.
*/
if (gres_conf_list)
(void) list_delete_all(gres_conf_list, _is_tpu_gres,
NULL);
list_transfer(gres_conf_list, gres_list_system);
FREE_NULL_LIST(gres_list_system);
if (slurm_conf.debug_flags & DEBUG_FLAG_GRES)
print_gres_list(gres_conf_list, LOG_LEVEL_VERBOSE);
}
rc = gres_node_config_load(gres_conf_list, node_config, &gres_devices);
if (rc != SLURM_SUCCESS) {
/*
* Cloud and external nodes are validated from slurmctld;
* fatal() there would kill the whole controller over one
* node's bad config, so return the error and let the node be
* rejected instead, as gres/gpu does.
*/
if (running_in_slurmctld())
return rc;
fatal("%s failed to load configuration", plugin_name);
}
return rc;
}
static void _set_env(common_gres_env_t *gres_env)
{
char *slurm_env_var;
if (gres_env->is_job)
slurm_env_var = "SLURM_JOB_TPUS";
else
slurm_env_var = "SLURM_STEP_TPUS";
/*
* Unlike gres/nic, which names devices after the file (mlx4_0), a TPU
* chip has no meaningful name of its own. Its VFIO file is named for
* an IOMMU group number assigned at boot, so dev_num would export an
* unstable value that means nothing to the task. Leave use_dev_num
* false so the env carries the chip's relative index on the node,
* matching both gres/gpu and the order libtpu enumerates chips in.
*/
gres_env->prefix = "";
common_gres_set_env(gres_env);
/*
* Unset rather than leave stale when nothing is allocated, so a step
* requesting --gres=none inside a TPU allocation does not inherit the
* job's device list.
*/
if (gres_env->global_list) {
env_array_overwrite(gres_env->env_ptr, slurm_env_var,
gres_env->global_list);
xfree(gres_env->global_list);
} else {
unsetenvp(*gres_env->env_ptr, slurm_env_var);
}
/* No TPU environment variable is built from the per-device lists */
xfree(gres_env->local_list);
xfree(gres_env->local_list_uuid);
}
extern void gres_p_job_set_env(char ***job_env_ptr, bitstr_t *gres_bit_alloc,
uint64_t gres_cnt, uint64_t *gres_per_bit,
gres_internal_flags_t flags)
{
common_gres_env_t gres_env = {
.bit_alloc = gres_bit_alloc,
.env_ptr = job_env_ptr,
.flags = flags,
.gres_cnt = gres_cnt,
.gres_devices = gres_devices,
.gres_per_bit = gres_per_bit,
.is_job = true,
};
_set_env(&gres_env);
}
extern void gres_p_step_set_env(char ***step_env_ptr, bitstr_t *gres_bit_alloc,
uint64_t gres_cnt, uint64_t *gres_per_bit,
gres_internal_flags_t flags)
{
common_gres_env_t gres_env = {
.bit_alloc = gres_bit_alloc,
.env_ptr = step_env_ptr,
.flags = flags,
.gres_cnt = gres_cnt,
.gres_devices = gres_devices,
.gres_per_bit = gres_per_bit,
};
_set_env(&gres_env);
}
extern void gres_p_task_set_env(char ***task_env_ptr, bitstr_t *gres_bit_alloc,
uint64_t gres_cnt, uint64_t *gres_per_bit,
bitstr_t *usable_gres,
gres_internal_flags_t flags)
{
common_gres_env_t gres_env = {
.bit_alloc = gres_bit_alloc,
.env_ptr = task_env_ptr,
.flags = flags,
.gres_cnt = gres_cnt,
.gres_devices = gres_devices,
.gres_per_bit = gres_per_bit,
.is_task = true,
.usable_gres = usable_gres,
};
_set_env(&gres_env);
}
extern void gres_p_send_stepd(buf_t *buffer)
{
gres_send_stepd(buffer, gres_devices);
}
extern void gres_p_recv_stepd(buf_t *buffer)
{
gres_recv_stepd(buffer, &gres_devices);
}
/*
* Return the list of gres_device_t of this plugin. The plugin owns the list
* and frees it in fini(), so the caller must not free it.
*/
extern list_t *gres_p_get_devices(void)
{
return gres_devices;
}
extern list_t *gres_p_get_dmem_devices(void)
{
return NULL;
}
extern void gres_p_step_hardware_init(bitstr_t *usable_gres, char *settings)
{
return;
}
extern void gres_p_step_hardware_fini(void)
{
return;
}
extern gres_prep_t *gres_p_prep_build_env(gres_job_state_t *gres_js)
{
return NULL;
}
extern void gres_p_prep_set_env(char ***prep_env_ptr, gres_prep_t *gres_prep,
int node_inx)
{
return;
}