Foundry/cuda/tests/vram_hot_swap.c

234 lines
8.6 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/*
* vram_hot_swap.c — Test CUDA context survival across bar_swap cycles
*
* The simplest approach to VRAM persistence: keep the CUDA context alive,
* swap the die underneath via bar_swap, swap it back, and check if CUDA
* can still read its data.
*
* Flow:
* 1. Acquire die 0, load nvidia, CUDA alloc + write 0xDEADBEEF
* 2. bar_swap release die 0 (nvidia stays loaded, CUDA context alive)
* 3. bar_swap acquire die 0 again
* 4. CUDA read back — does the data survive?
*
* Usage:
* sudo LD_LIBRARY_PATH=/usr/local/cuda-11.4/lib64 ./cuda/tests/vram_hot_swap
*
* Requires: bar_swap loaded, nvidia loaded, die 0 acquired, root.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <dlfcn.h>
#include <time.h>
#include "barswap/barswap.h"
#define TEST_SIZE (1024 * 1024) /* 1 MB */
#define PATTERN 0xDEADBEEF
typedef int CUresult;
typedef void *CUcontext;
typedef unsigned long long CUdeviceptr;
#define CUDA_SUCCESS 0
static double now_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6;
}
int main(void) {
printf("=== VRAM Hot Swap Test ===\n\n");
int n_words = TEST_SIZE / sizeof(uint32_t);
uint32_t *host_buf = malloc(TEST_SIZE);
/* Load CUDA symbols */
void *libcuda = dlopen("libcuda.so.1", RTLD_LAZY);
if (!libcuda) { fprintf(stderr, "dlopen: %s\n", dlerror()); return 1; }
CUresult (*cuInit)(unsigned int) = dlsym(libcuda, "cuInit");
CUresult (*cuDeviceGet)(int *, int) = dlsym(libcuda, "cuDeviceGet");
CUresult (*cuCtxCreate)(CUcontext *, unsigned int, int) =
dlsym(libcuda, "cuCtxCreate_v2");
CUresult (*cuCtxDestroy)(CUcontext) = dlsym(libcuda, "cuCtxDestroy_v2");
CUresult (*cuMemAlloc)(CUdeviceptr *, size_t) = dlsym(libcuda, "cuMemAlloc_v2");
CUresult (*cuMemFree)(CUdeviceptr) = dlsym(libcuda, "cuMemFree_v2");
CUresult (*cuMemcpyHtoD)(CUdeviceptr, const void *, size_t) =
dlsym(libcuda, "cuMemcpyHtoD_v2");
CUresult (*cuMemcpyDtoH)(void *, CUdeviceptr, size_t) =
dlsym(libcuda, "cuMemcpyDtoH_v2");
CUresult (*cuCtxSynchronize)(void) = dlsym(libcuda, "cuCtxSynchronize");
/* ================================================================
* Step 1: CUDA alloc + write pattern (die 0 must already be acquired)
* ================================================================ */
printf("--- Step 1: CUDA alloc + write ---\n");
CUresult err = cuInit(0);
if (err) { fprintf(stderr, "cuInit: %d\n", err); return 1; }
int dev;
cuDeviceGet(&dev, 0);
CUcontext cuda_ctx;
err = cuCtxCreate(&cuda_ctx, 0, dev);
if (err) { fprintf(stderr, "cuCtxCreate: %d\n", err); return 1; }
printf("CUDA context created.\n");
CUdeviceptr dptr;
err = cuMemAlloc(&dptr, TEST_SIZE);
if (err) { fprintf(stderr, "cuMemAlloc: %d\n", err); return 1; }
printf("Allocated 1 MB at dptr=0x%llx\n", (unsigned long long)dptr);
for (int i = 0; i < n_words; i++) host_buf[i] = PATTERN;
cuMemcpyHtoD(dptr, host_buf, TEST_SIZE);
cuCtxSynchronize();
/* Verify */
memset(host_buf, 0, TEST_SIZE);
cuMemcpyDtoH(host_buf, dptr, TEST_SIZE);
int ok = 1;
for (int i = 0; i < n_words; i++) if (host_buf[i] != PATTERN) { ok = 0; break; }
printf("Write verify: %s\n\n", ok ? "OK" : "FAIL");
if (!ok) { fprintf(stderr, "Write failed, aborting.\n"); return 1; }
/* ================================================================
* Step 2: bar_swap release (CUDA context stays alive!)
* ================================================================ */
printf("--- Step 2: bar_swap release (CUDA stays alive) ---\n");
bs_context *ctx = bs_init();
if (!ctx) { fprintf(stderr, "bs_init failed\n"); return 1; }
/* Release die 0 — this unmaps BAR0/BAR1 from the PCI slot.
* nvidia driver still loaded, CUDA context still exists,
* but the hardware is no longer accessible. */
double t0 = now_ms();
/* We can't use bs_release without a gpu handle from bs_acquire.
* Use a raw ioctl to release die 0. */
/* Actually, let's just use bs_acquire to get a handle first,
* then release it. But bs_acquire will try to mmap BAR1 again...
* Let's use the direct approach: release via acquire of a different
* die (implicit release) then release that die. Or just release
* via raw ioctl. */
/* Simple approach: use the kernel module's implicit release.
* Actually, the cleanest way is to do a raw RELEASE ioctl.
* Let's include the kernel header. */
/* Manually define the release ioctl to avoid header dep issues */
struct release_req { uint32_t die_id; uint32_t flags; };
#define MY_RELEASE _IOW('B', 5, struct release_req)
int bs_fd = open("/dev/bar_swap", O_RDWR);
if (bs_fd < 0) { perror("open /dev/bar_swap"); return 1; }
struct release_req rel = { .die_id = 0, .flags = 0 };
int ret = ioctl(bs_fd, MY_RELEASE, &rel);
double t1 = now_ms();
printf("Released die 0: %s (%.1fms)\n", ret == 0 ? "OK" : "FAIL", t1 - t0);
if (ret != 0) { perror("release ioctl"); }
printf("Die 0 released. CUDA context still alive. nvidia still loaded.\n");
printf("Sleeping 500ms...\n\n");
usleep(500000);
/* ================================================================
* Step 3: bar_swap acquire die 0 again
* ================================================================ */
printf("--- Step 3: bar_swap re-acquire die 0 ---\n");
struct acquire_req {
uint32_t die_id;
uint32_t flags;
uint64_t bar0_addr;
uint64_t bar1_addr;
uint64_t bar1_size;
uint64_t bar3_addr;
};
#define MY_ACQUIRE _IOWR('B', 4, struct acquire_req)
struct acquire_req acq = { .die_id = 0, .flags = 0 };
t0 = now_ms();
ret = ioctl(bs_fd, MY_ACQUIRE, &acq);
t1 = now_ms();
printf("Re-acquired die 0: %s (%.1fms)\n", ret == 0 ? "OK" : "FAIL", t1 - t0);
if (ret == 0) {
printf(" BAR0=0x%llx BAR1=0x%llx (size=0x%llx)\n",
(unsigned long long)acq.bar0_addr,
(unsigned long long)acq.bar1_addr,
(unsigned long long)acq.bar1_size);
} else {
perror("acquire ioctl");
close(bs_fd);
return 1;
}
close(bs_fd);
printf("Sleeping 200ms for hardware settle...\n\n");
usleep(200000);
/* ================================================================
* Step 4: CUDA read back — did the data survive?
* ================================================================ */
printf("--- Step 4: CUDA read back ---\n");
memset(host_buf, 0xAA, TEST_SIZE); /* Fill with sentinel */
err = cuMemcpyDtoH(host_buf, dptr, TEST_SIZE);
printf("cuMemcpyDtoH: %s (err=%d)\n", err == CUDA_SUCCESS ? "OK" : "FAIL", err);
if (err == CUDA_SUCCESS) {
int match = 0, zeros = 0;
uint32_t first_bad = 0;
int first_bad_idx = -1;
for (int i = 0; i < n_words; i++) {
if (host_buf[i] == PATTERN) match++;
else if (host_buf[i] == 0) zeros++;
else if (first_bad_idx < 0) { first_bad_idx = i; first_bad = host_buf[i]; }
}
printf("Results: %d/%d match (%.1f%%), %d zeros",
match, n_words, 100.0f * match / n_words, zeros);
if (first_bad_idx >= 0)
printf(", first bad at [%d]=0x%08X", first_bad_idx, first_bad);
printf("\n");
if (match == n_words) {
printf("\n >>> VRAM SURVIVES HOT SWAP! <<<\n");
printf(" CUDA context + data persists across bar_swap release/acquire.\n");
printf(" Architecture A is VIABLE: pre-load models, swap freely.\n");
} else if (match > n_words / 2) {
printf("\n Partial persistence (%d/%d).\n", match, n_words);
} else {
printf("\n Data did not survive the swap.\n");
}
} else {
printf("CUDA DtoH copy failed — nvidia may have lost the GPU.\n");
printf("Trying cuCtxSynchronize...\n");
err = cuCtxSynchronize();
printf("cuCtxSynchronize: %d\n", err);
}
/* ================================================================
* Cleanup
* ================================================================ */
printf("\nCleaning up...\n");
cuMemFree(dptr);
cuCtxDestroy(cuda_ctx);
bs_free(ctx);
free(host_buf);
dlclose(libcuda);
printf("Done.\n");
return 0;
}