diff options
Diffstat (limited to 'nvtaskset.c')
| -rw-r--r-- | nvtaskset.c | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/nvtaskset.c b/nvtaskset.c new file mode 100644 index 0000000..b8873ab --- /dev/null +++ b/nvtaskset.c | |||
| @@ -0,0 +1,210 @@ | |||
| 1 | // Copyright 2025 Joshua Bakita | ||
| 2 | // taskset-like utility for the GPU | ||
| 3 | #define _GNU_SOURCE // For program_invocation_name | ||
| 4 | #include <argp.h> | ||
| 5 | #include <errno.h> | ||
| 6 | #include <error.h> | ||
| 7 | #include <stdint.h> | ||
| 8 | #include <stdio.h> | ||
| 9 | #include <stdlib.h> | ||
| 10 | #include <string.h> | ||
| 11 | #include <sys/ipc.h> | ||
| 12 | #include <sys/shm.h> | ||
| 13 | #include <sys/types.h> | ||
| 14 | #include <unistd.h> | ||
| 15 | |||
| 16 | #include <cuda.h> // To help with getting GPC info | ||
| 17 | |||
| 18 | #include "libsmctrl.h" | ||
| 19 | |||
| 20 | const char* maintainer = "<jbakita@cs.unc.edu>"; | ||
| 21 | const char* version = "nvtaskset 2025.03"; | ||
| 22 | const char* desc = "taskset-like utility for NVIDIA GPUs."; | ||
| 23 | |||
| 24 | const struct argp_option opts[] = { | ||
| 25 | {0} | ||
| 26 | }; | ||
| 27 | |||
| 28 | unsigned __int128 strtou128(const char *nptr, char **endptr, int base) { | ||
| 29 | unsigned __int128 result = 0; | ||
| 30 | if (base != 16) | ||
| 31 | error(1, EINVAL, "Internal error"); | ||
| 32 | // Skip a "0x" prefix. Safe due to early evaluation | ||
| 33 | if (*nptr == '0' && (*(nptr + 1) == 'x' || *(nptr + 1) == 'X')) | ||
| 34 | nptr += 2; | ||
| 35 | // Until hitting an invalid character | ||
| 36 | while (1) { | ||
| 37 | if (*nptr >= 'a' && *nptr <= 'f') | ||
| 38 | result = result << 4 | (*nptr - 'a' + 10); | ||
| 39 | else if (*nptr >= 'A' && *nptr <= 'F') | ||
| 40 | result = result << 4 | (*nptr - 'A' + 10); | ||
| 41 | else if (*nptr >= '0' && *nptr <= '9') | ||
| 42 | result = result << 4 | (*nptr - '0'); | ||
| 43 | else | ||
| 44 | break; | ||
| 45 | nptr++; | ||
| 46 | } | ||
| 47 | if (endptr) | ||
| 48 | *endptr = (char*)nptr; | ||
| 49 | return result; | ||
| 50 | } | ||
| 51 | |||
| 52 | void libsmctrl_get_gpc_info_ext_easy(uint32_t* num_gpcs, uint128_t** masks, int gpu_id) { | ||
| 53 | int res; | ||
| 54 | CUcontext ctx; | ||
| 55 | // XXX: Copied from libsmctrl_test_gpc_info | ||
| 56 | // Tell CUDA to use PCI device id ordering (to match nvdebug) | ||
| 57 | putenv((char*)"CUDA_DEVICE_ORDER=PCI_BUS_ID"); | ||
| 58 | // A CUDA context is required before reading the topology information | ||
| 59 | if ((res = cuInit(0))) { | ||
| 60 | const char* name; | ||
| 61 | cuGetErrorName(res, &name); | ||
| 62 | fprintf(stderr, "%s: Unable to initialize CUDA, error %s\n", program_invocation_name, name); | ||
| 63 | exit(1); | ||
| 64 | } | ||
| 65 | if ((res = cuCtxCreate(&ctx, 0, 0))) { | ||
| 66 | const char* name; | ||
| 67 | cuGetErrorName(res, &name); | ||
| 68 | fprintf(stderr, "%s: Unable to create a CUDA context, error %s\n", program_invocation_name, name); | ||
| 69 | exit(1); | ||
| 70 | } | ||
| 71 | // Pull topology information from libsmctrl | ||
| 72 | if ((res = libsmctrl_get_gpc_info_ext(num_gpcs, masks, gpu_id)) != 0) { | ||
| 73 | error(0, res, "libsmctrl_get_gpc_info() failed"); | ||
| 74 | if (res == ENOENT) | ||
| 75 | fprintf(stderr, "%s: Is the nvdebug kernel module loaded?\n", program_invocation_name); | ||
| 76 | if (res == EIO) | ||
| 77 | fprintf(stderr, "%s: Is the GPU powered on, i.e., is there an active context?\n", program_invocation_name); | ||
| 78 | exit(1); | ||
| 79 | } | ||
| 80 | // Not copied | ||
| 81 | unsetenv("CUDA_DEVICE_ORDER"); | ||
| 82 | } | ||
| 83 | |||
| 84 | int main(int argc, char **argv) { | ||
| 85 | if (argc < 3) { | ||
| 86 | fprintf(stderr, "Usage: %s -p <hex mask> <pid>\n", argv[0]); | ||
| 87 | fprintf(stderr, " %s <hex mask> <command> <argument...>\n", argv[0]); | ||
| 88 | fprintf(stderr, " %s --gpc-list <gpc list> <command> <argument...>\n", argv[0]); | ||
| 89 | fprintf(stderr, " <hex mask> has a bit set for each TPC to be enabled\n"); | ||
| 90 | return 1; | ||
| 91 | } | ||
| 92 | // TODO: Use a proper argument parser | ||
| 93 | if (strcmp("-p", argv[1]) == 0) { // Setting mask on running task | ||
| 94 | char *end; | ||
| 95 | pid_t target_pid = strtoul(argv[2], &end, 10); | ||
| 96 | // strtoul stores a pointer to the first invalid character in `end` | ||
| 97 | if (*end != '\0') { | ||
| 98 | fprintf(stderr, "Invalid character \"%c\" in PID argument.\n", *end); | ||
| 99 | return 1; | ||
| 100 | } | ||
| 101 | unsigned __int128 mask = strtou128(argv[3], &end, 16); | ||
| 102 | if (*end != '\0') { | ||
| 103 | fprintf(stderr, "Invalid character \"%c\" in mask argument.\n", *end); | ||
| 104 | return 1; | ||
| 105 | } | ||
| 106 | // The shared memory lookup key is the lower 16-bits of the PID | "sm" | ||
| 107 | key_t shm_key = target_pid << 16 | (int)'s' << 8 | (int) 'm'; | ||
| 108 | // Get a handle to the 128-bit shared memory region | ||
| 109 | int shmid = shmget(shm_key, 16, 0); | ||
| 110 | if (shmid == -1) | ||
| 111 | error(1, errno, "Unable to find control region for PID %d", target_pid); | ||
| 112 | // Open the shared memory region | ||
| 113 | unsigned __int128 *supreme_mask = shmat(shmid, NULL, 0); | ||
| 114 | if (supreme_mask == (void*)-1) | ||
| 115 | error(1, errno, "Unable to open control region for PID %d", target_pid); | ||
| 116 | // Write the requested mask into the shared memory region | ||
| 117 | *supreme_mask = mask; | ||
| 118 | } else { // Starting a new task with a mask | ||
| 119 | // TODO: Check other locations for nvidia-cuda-mps-control if its not on the path | ||
| 120 | // TODO: Use dup2() to redirect MPS startup messages | ||
| 121 | int ret = system("echo -n | nvidia-cuda-mps-control"); | ||
| 122 | if (ret == -1) | ||
| 123 | error(1, errno, "Unable to run subshell to check MPS status"); | ||
| 124 | if (ret != 0) { // Control deamon not yet started | ||
| 125 | fprintf(stderr, "nvtaskset: MPS control deamon does not appear to be running. Automatically starting...\n"); | ||
| 126 | ret = system("nvidia-cuda-mps-control -d"); | ||
| 127 | if (ret == -1) | ||
| 128 | error(1, errno, "Unable to run subshell to start MPS"); | ||
| 129 | if (ret == 1) { | ||
| 130 | fprintf(stderr, "nvtaskset: Error starting MPS control deamon. Terminating...\n"); | ||
| 131 | return 1; | ||
| 132 | } | ||
| 133 | fprintf(stderr, "nvtaskset: Done. Use \"echo quit | nvidia-cuda-mps-control\" to terminate it later as desired.\n"); | ||
| 134 | } | ||
| 135 | // Tell loader to initialize libsmctrl.so first | ||
| 136 | // TODO: Append, rather than overwrite LD_PRELOAD | ||
| 137 | setenv("LD_PRELOAD", "./libsmctrl.so", 1); | ||
| 138 | // Explictly set the number of channels, otherwise CUDA will only use two | ||
| 139 | // (see paper for why that causes problems) | ||
| 140 | setenv("CUDA_DEVICE_MAX_CONNECTIONS", "8", 1); | ||
| 141 | // Check if a mask, or a list of GPCs is being provided | ||
| 142 | if (strcmp(argv[1], "--gpc-list") == 0) { | ||
| 143 | // TODO: Support the full syntax that taskset supports | ||
| 144 | // We just support X,Y,Z for now | ||
| 145 | uint32_t num_gpcs = 0; | ||
| 146 | uint128_t* masks = NULL; | ||
| 147 | // TODO: Allow specifying GPU ID, rather than assuming 0! | ||
| 148 | libsmctrl_get_gpc_info_ext_easy(&num_gpcs, &masks, 0); | ||
| 149 | uint128_t mask = 0; | ||
| 150 | int range_start_gpc = -1; | ||
| 151 | char* start = argv[2]; | ||
| 152 | int len = strlen(argv[2]); | ||
| 153 | // TODO: Handle invalid input cleanly. | ||
| 154 | // Convert comma-seperated GPC list into a mask | ||
| 155 | for (int i = 0; i < len + 1; i++) { | ||
| 156 | if (argv[2][i] == ',' || argv[2][i] == '\0') { | ||
| 157 | argv[2][i] = '\0'; | ||
| 158 | int gpc = atoi(start); | ||
| 159 | if (gpc > num_gpcs - 1) { | ||
| 160 | fprintf(stderr, "Invalid GPC ID '%s'!\n", start); | ||
| 161 | return 1; | ||
| 162 | } | ||
| 163 | // Handle ranges | ||
| 164 | if (range_start_gpc != -1) { | ||
| 165 | if (range_start_gpc >= gpc) { | ||
| 166 | fprintf(stderr, "Invalid GPC range!\n"); | ||
| 167 | return 1; | ||
| 168 | } | ||
| 169 | while (range_start_gpc <= gpc) { | ||
| 170 | //printf("gpc %i\n", range_start_gpc); | ||
| 171 | mask |= masks[range_start_gpc]; | ||
| 172 | range_start_gpc++; | ||
| 173 | } | ||
| 174 | range_start_gpc = -1; | ||
| 175 | } else { | ||
| 176 | //printf("gpc %i\n", gpc); | ||
| 177 | mask |= masks[gpc]; | ||
| 178 | } | ||
| 179 | start = argv[2] + i + 1; | ||
| 180 | } | ||
| 181 | // Range start | ||
| 182 | if (argv[2][i] == '-') { | ||
| 183 | argv[2][i] = '\0'; | ||
| 184 | range_start_gpc = atoi(start); | ||
| 185 | start = argv[2] + i + 1; | ||
| 186 | } | ||
| 187 | } | ||
| 188 | // Convert to string, prefix with ~, and set env var | ||
| 189 | char mask_str[32+3+1]; // 32 hexits, "~0x", and '\0' | ||
| 190 | snprintf(mask_str, 36, "~0x%lx%016lx", (uint64_t)(mask >> 64), (uint64_t)mask); | ||
| 191 | //printf("nvtaskset: Using mask string %s\n", mask_str); | ||
| 192 | setenv("LIBSMCTRL_MASK", mask_str, 1); | ||
| 193 | // Start task | ||
| 194 | execvp(argv[3], argv+3); | ||
| 195 | error(1, errno, "Unable to launch task '%s'", argv[3]); | ||
| 196 | } else { | ||
| 197 | // Tell libsmctrl what mask to use | ||
| 198 | char* mask = malloc(strlen(argv[1]) + 2); | ||
| 199 | mask[0] = '~'; // Make an enable mask | ||
| 200 | strcpy(mask+1, argv[1]); | ||
| 201 | setenv("LIBSMCTRL_MASK", mask, 1); | ||
| 202 | free(mask); // setenv() made a copy | ||
| 203 | // Start task | ||
| 204 | execvp(argv[2], argv+2); | ||
| 205 | error(1, errno, "Unable to launch task '%s'", argv[2]); | ||
| 206 | } | ||
| 207 | } | ||
| 208 | fprintf(stderr, "Invalid arguments\n"); | ||
| 209 | return 1; | ||
| 210 | } | ||
