diff options
| author | Joshua Bakita <jbakita@cs.unc.edu> | 2024-12-19 14:20:38 -0500 |
|---|---|---|
| committer | Joshua Bakita <jbakita@cs.unc.edu> | 2024-12-19 14:48:21 -0500 |
| commit | d052c2df34ab41ba285f70965663e5a0832f6ac9 (patch) | |
| tree | 0a761be3f62910275da8a2cad546a8902073b1e9 /libsmctrl.c | |
| parent | aa63a02efa5fc8701f0c3418704bbbc2051c1042 (diff) | |
Bugfix stream-mask override, support old CUDA, and start Hopper support
Use a different callback to intercept the TMD/QMD later in the
launch pipeline.
Major improvements:
- Fix bug with next mask not overriding stream mask on CUDA 11.0+
- Add CUDA 6.5-10.2 support for next- and global-granularity
partitioning masks on x86_64 and aarch64 Jetson
- Remove libdl dependency
- Partially support TMD/QMD Version 4 (Hopper)
Minor improvements:
- Check for sufficient CUDA version before before attempting to
apply a next-granularity partitioning mask
- Only check for sufficient CUDA version on the first call to
`libsmctrl_set_next_mask()` or `libsmctrl_set_global_mask()`,
rather than checking every time (lowers overheads)
- Check that TMD version is sufficient before modifying it
- Improve documentation
Issues:
- Partitioning mask bits have a different meaning in TMD/QMD
Version 4 and require floorsweeping and remapping information to
properly construct. This information will be forthcoming in
future releases of libsmctrl and nvdebug.
Diffstat (limited to 'libsmctrl.c')
| -rw-r--r-- | libsmctrl.c | 247 |
1 files changed, 86 insertions, 161 deletions
diff --git a/libsmctrl.c b/libsmctrl.c index 1018e44..24a3177 100644 --- a/libsmctrl.c +++ b/libsmctrl.c | |||
| @@ -5,31 +5,34 @@ | |||
| 5 | * | 5 | * |
| 6 | * This file implements partitioning via three different mechanisms: | 6 | * This file implements partitioning via three different mechanisms: |
| 7 | * - Modifying the QMD/TMD immediately prior to upload | 7 | * - Modifying the QMD/TMD immediately prior to upload |
| 8 | * - Changing a field in CUDA's global struct that CUDA applies to the QMD/TMD | ||
| 9 | * - Changing a field in CUDA's stream struct that CUDA applies to the QMD/TMD | 8 | * - Changing a field in CUDA's stream struct that CUDA applies to the QMD/TMD |
| 10 | * This table shows the mechanism used with each CUDA version: | 9 | * This table shows the mechanism used with each CUDA version: |
| 11 | * +-----------+---------------+---------------+--------------+ | 10 | * +-----------+---------------+---------------+--------------+ |
| 12 | * | Version | Global Mask | Stream Mask | Next Mask | | 11 | * | Version | Global Mask | Stream Mask | Next Mask | |
| 13 | * +-----------+---------------+---------------+--------------+ | 12 | * +-----------+---------------+---------------+--------------+ |
| 14 | * | 11.0-12.6 | TMD/QMD Hook | stream struct | TMD/QMD Hook | | 13 | * | 8.0-12.6 | TMD/QMD Hook | stream struct | TMD/QMD Hook | |
| 15 | * | 10.2 | global struct | stream struct | N/A | | 14 | * | 6.5-7.5 | TMD/QMD Hook | N/A | TMD/QMD Hook | |
| 16 | * | 8.0-10.1 | N/A | stream struct | N/A | | ||
| 17 | * +-----------+---------------+---------------+--------------+ | 15 | * +-----------+---------------+---------------+--------------+ |
| 18 | * "N/A" indicates that a mask type is unsupported on that CUDA version. | 16 | * "N/A" indicates that a mask type is unsupported on that CUDA version. |
| 19 | * Please contact the authors if support is needed for a particular feature on | 17 | * Please contact the authors if support is needed for a particular feature on |
| 20 | * an older CUDA version. Support for those is unimplemented, not impossible. | 18 | * an older CUDA version. Support for those is unimplemented, not impossible. |
| 19 | * | ||
| 20 | * An old implementation of this file effected the global mask on CUDA 10.2 by | ||
| 21 | * changing a field in CUDA's global struct that CUDA applies to the QMD/TMD. | ||
| 22 | * That implementation was extraordinarily complicated, and was replaced in | ||
| 23 | * 2024 with a more-backward-compatible way of hooking the TMD/QMD. | ||
| 24 | * View the old implementation via Git: `git show aa63a02e:libsmctrl.c`. | ||
| 21 | */ | 25 | */ |
| 22 | #include <cuda.h> | 26 | #include <cuda.h> |
| 23 | 27 | ||
| 24 | #include <errno.h> | 28 | #include <errno.h> |
| 25 | #include <error.h> | 29 | #include <error.h> |
| 26 | #include <fcntl.h> | 30 | #include <fcntl.h> |
| 31 | #include <stdbool.h> | ||
| 27 | #include <stdint.h> | 32 | #include <stdint.h> |
| 28 | #include <stdio.h> | 33 | #include <stdio.h> |
| 29 | #include <unistd.h> | 34 | #include <unistd.h> |
| 30 | 35 | ||
| 31 | #include <dlfcn.h> | ||
| 32 | |||
| 33 | #include "libsmctrl.h" | 36 | #include "libsmctrl.h" |
| 34 | 37 | ||
| 35 | // In functions that do not return an error code, we favor terminating with an | 38 | // In functions that do not return an error code, we favor terminating with an |
| @@ -37,148 +40,66 @@ | |||
| 37 | #define abort(ret, errno, ...) error_at_line(ret, errno, __FILE__, __LINE__, \ | 40 | #define abort(ret, errno, ...) error_at_line(ret, errno, __FILE__, __LINE__, \ |
| 38 | __VA_ARGS__) | 41 | __VA_ARGS__) |
| 39 | 42 | ||
| 40 | // Layout of mask control fields to match CUDA's static global struct | 43 | /*** QMD/TMD-based SM Mask Control via Debug Callback. ***/ |
| 41 | struct global_sm_control { | ||
| 42 | uint32_t enabled; | ||
| 43 | uint64_t mask; | ||
| 44 | } __attribute__((packed)); | ||
| 45 | |||
| 46 | /*** CUDA Globals Manipulation. CUDA 10.2 only ***/ | ||
| 47 | |||
| 48 | // Ends up being 0x7fb7fa3408 in some binaries (CUDA 10.2, Jetson) | ||
| 49 | static struct global_sm_control* g_sm_control = NULL; | ||
| 50 | |||
| 51 | /* Find the location of CUDA's `globals` struct and the SM mask control fields | ||
| 52 | * No symbols are exported from within `globals`, so this has to do a very | ||
| 53 | * messy lookup, following the pattern of the assembly of `cuDeviceGetCount()`. | ||
| 54 | * Don't call this before the CUDA library has been initialized. | ||
| 55 | * (Note that this appears to work, even if built on CUDA > 10.2.) | ||
| 56 | */ | ||
| 57 | static void setup_g_sm_control_10() { | ||
| 58 | if (g_sm_control) | ||
| 59 | return; | ||
| 60 | // The location of the static global struct containing the global SM | ||
| 61 | // mask field will vary depending on where the loader locates the CUDA | ||
| 62 | // library. In order to reliably modify this struct, we must defeat | ||
| 63 | // that relocation by deriving its location relative to a known | ||
| 64 | // reference point. | ||
| 65 | // | ||
| 66 | // == Choosing a Reference Point: | ||
| 67 | // The cudbg* symbols appear to be relocated to a constant offset from | ||
| 68 | // the globals structure, and so we use the address of the symbol | ||
| 69 | // `cudbgReportDriverApiErrorFlags` as our reference point. (This ends | ||
| 70 | // up being the closest to an intermediate table we use as part of our | ||
| 71 | // lookup---process discussed below.) | ||
| 72 | // | ||
| 73 | // Unfortunately, the symbol we reference is errantly omitted from the | ||
| 74 | // libcuda.so stub used by nvcc starting around CUDA 11.8, so we have to | ||
| 75 | // use dlsym to avoid build-time issues. | ||
| 76 | void* hndl = dlopen(NULL, RTLD_LAZY); | ||
| 77 | uint32_t* sym = dlsym(hndl, "cudbgReportDriverApiErrorFlags"); | ||
| 78 | |||
| 79 | // == Deriving Location: | ||
| 80 | // The number of CUDA devices available is co-located in the same CUDA | ||
| 81 | // globals structure that we aim to modify the SM mask field in. The | ||
| 82 | // value in that field can be assigned to a user-controlled pointer via | ||
| 83 | // the cuDeviceGetCount() CUDA Driver Library function. To determine | ||
| 84 | // the location of thu structure, we pass a bad address to the function | ||
| 85 | // and dissasemble the code adjacent to where it segfaults. On the | ||
| 86 | // Jetson Xavier with CUDA 10.2, the assembly is as follows: | ||
| 87 | // (reg x19 contains cuDeviceGetCount()'s user-provided pointer) | ||
| 88 | // ... | ||
| 89 | // 0x0000007fb71454b4: cbz x19, 0x7fb71454d0 // Check ptr non-zero | ||
| 90 | // 0x0000007fb71454b8: adrp x1, 0x7fb7ea6000 // Addr of lookup tbl | ||
| 91 | // 0x0000007fb71454bc: ldr x1, [x1,#3672] // Get addr of globals | ||
| 92 | // 0x0000007fb71454c0: ldr w1, [x1,#904] // Get count from globals | ||
| 93 | // 0x0000007fb71454c4: str w1, [x19] // Store count at user addr | ||
| 94 | // ... | ||
| 95 | // In this assembly, we can identify that CUDA uses an internal lookup | ||
| 96 | // table to identify the location of the globals structure (pointer | ||
| 97 | // 459 in the table; offset 3672). After obtaining this pointer, it | ||
| 98 | // advances to offset 904 in the global structure, dereferences the | ||
| 99 | // value stored there, and then attempts to store it at the user- | ||
| 100 | // -provided address (register x19). This final line will trigger a | ||
| 101 | // segfault if a non-zero bad address is passed to cuDeviceGetCount(). | ||
| 102 | // | ||
| 103 | // On x86_64: | ||
| 104 | // (reg %rbx contains cuDeviceGetCount()'s user-provided pointer) | ||
| 105 | // ... | ||
| 106 | // 0x00007ffff6cac01f: test %rbx,%rbx // Check ptr non-zero | ||
| 107 | // 0x00007ffff6cac022: je 0x7ffff6cac038 // '' | ||
| 108 | // 0x00007ffff6cac024: mov 0x100451d(%rip),%rdx # 0x7ffff7cb0548 // Get globals base address from offset from instruction pointer | ||
| 109 | // 0x00007ffff6cac02b: mov 0x308(%rdx),%edx // Take globals base address, add an offset of 776, and dereference | ||
| 110 | // 0x00007ffff6cac031: mov %edx,(%rbx) // Store count at user addr | ||
| 111 | // ... | ||
| 112 | // Note that this does not use an intermediate lookup table. | ||
| 113 | // | ||
| 114 | // [Aside: cudbgReportDriverApiErrorFlags is currently the closest | ||
| 115 | // symbol to **the lookup table**. cudbgDebuggerInitialized is closer | ||
| 116 | // to the globals struct itself (+7424 == SM mask control), but we | ||
| 117 | // perfer the table lookup approach for now, as that's what | ||
| 118 | // cuDeviceGetCount() does.] | ||
| 119 | |||
| 120 | #if __aarch64__ | ||
| 121 | // In my test binary, the lookup table is at address 0x7fb7ea6000, and | ||
| 122 | // this is 1029868 bytes before the address for | ||
| 123 | // cudbgReportDriverApiErrorFlags. Use this information to derive the | ||
| 124 | // location of the lookup in our binary (defeat relocation). | ||
| 125 | uintptr_t* tbl_base = (uintptr_t*)((uintptr_t)sym - 1029868); | ||
| 126 | // Address of `globals` is at offset 3672 (entry 459?) in the table | ||
| 127 | uintptr_t globals_addr = *(tbl_base + 459); | ||
| 128 | // SM mask control is at offset 4888 in the `globals` struct | ||
| 129 | // [Device count at offset 904 (0x388)] | ||
| 130 | g_sm_control = (struct global_sm_control*)(globals_addr + 4888); | ||
| 131 | #endif // __aarch64__ | ||
| 132 | #if __x86_64__ | ||
| 133 | // In my test binary, globals is at 0x7ffff7cb0548, which is 1103576 | ||
| 134 | // bytes before the address for cudbgReportDriverApiErrorFlags | ||
| 135 | // (0x7ffff7dbdc20). Use this offset to defeat relocation. | ||
| 136 | uintptr_t globals_addr = *(uintptr_t*)((uintptr_t)sym - 1103576); | ||
| 137 | // SM mask control is at offset 4728 in the `globals` struct | ||
| 138 | // [Device count at offset 776 (0x308)] | ||
| 139 | g_sm_control = (struct global_sm_control*)(globals_addr + 4728); | ||
| 140 | #endif // __x86_64__ | ||
| 141 | // SM mask should be empty by default | ||
| 142 | if (g_sm_control->enabled || g_sm_control->mask) | ||
| 143 | fprintf(stderr, "Warning: Found non-empty SM disable mask " | ||
| 144 | "during setup! libsmctrl_set_global_mask() is " | ||
| 145 | "unlikely to work on this platform!\n"); | ||
| 146 | } | ||
| 147 | |||
| 148 | /*** QMD/TMD-based SM Mask Control via Debug Callback. CUDA 11+ ***/ | ||
| 149 | 44 | ||
| 150 | // Tested working on CUDA x86_64 11.0-12.2. | 45 | // Tested working on x86_64 CUDA 6.5, 9.1, and various 10+ versions |
| 151 | // Tested not working on aarch64 or x86_64 10.2 | 46 | // (No testing attempted on pre-CUDA-6.5 versions) |
| 47 | // Values for the following three lines can be extracted by tracing CUPTI as | ||
| 48 | // it interects with libcuda.so to set callbacks. | ||
| 152 | static const CUuuid callback_funcs_id = {0x2c, (char)0x8e, 0x0a, (char)0xd8, 0x07, 0x10, (char)0xab, 0x4e, (char)0x90, (char)0xdd, 0x54, 0x71, (char)0x9f, (char)0xe5, (char)0xf7, 0x4b}; | 49 | static const CUuuid callback_funcs_id = {0x2c, (char)0x8e, 0x0a, (char)0xd8, 0x07, 0x10, (char)0xab, 0x4e, (char)0x90, (char)0xdd, 0x54, 0x71, (char)0x9f, (char)0xe5, (char)0xf7, 0x4b}; |
| 153 | #define LAUNCH_DOMAIN 0x3 | 50 | // These callback descriptors appear to intercept the TMD/QMD late enough that |
| 154 | #define LAUNCH_PRE_UPLOAD 0x3 | 51 | // CUDA has already applied the per-stream mask from its internal data |
| 52 | // structures, allowing us to override it with the next mask. | ||
| 53 | #define QMD_DOMAIN 0xb | ||
| 54 | #define QMD_PRE_UPLOAD 0x1 | ||
| 55 | // Global mask (applies across all threads) | ||
| 155 | static uint64_t g_sm_mask = 0; | 56 | static uint64_t g_sm_mask = 0; |
| 57 | // Next mask (applies per-thread) | ||
| 156 | static __thread uint64_t g_next_sm_mask = 0; | 58 | static __thread uint64_t g_next_sm_mask = 0; |
| 157 | static char sm_control_setup_called = 0; | 59 | // Flag value to indicate if setup has been completed |
| 158 | static void launchCallback(void *ukwn, int domain, int cbid, const void *in_params) { | 60 | static bool sm_control_setup_called = false; |
| 159 | // The third 8-byte element in `in_parms` is a pointer to the stream struct. | 61 | |
| 160 | // This exists even when in_params < 0x50. This could be used to implement | 62 | // v1 has been removed---it intercepted the TMD/QMD too early, making it |
| 161 | // stream masking without the manual offsets specified elsewhere (store a | 63 | // impossible to override the CUDA-injected stream mask with the next mask. |
| 162 | // table of stream pointers to masks and do a lookup here). | 64 | static void control_callback_v2(void *ukwn, int domain, int cbid, const void *in_params) { |
