/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ #define _GNU_SOURCE /* * roundrobin_persist.c — No-teardown round-robin multi-die persistence test * * Proves the production switch mechanism for the K80 barswap rig: nvidia is * loaded EXACTLY ONCE and never rmmod'd. Weights stay resident in each die's * VRAM, and we move between dies purely by: * * 1. cuCtxDestroy — drop the current die's CUDA context (driver stays) * 2. PCI unbind — write the old device's BDF to .../nvidia/unbind * 3. bs_acquire(die) — barswap maps the next die into the single MMIO slot * 4. driver_override — clear it on the target, "none" on every other GK210 * 5. PCI bind — write the new device's BDF to .../nvidia/bind * (or, on the very first switch only, modprobe once) * 6. cuCtxCreate — fresh context on device 0 (== the slot die) * * This is the no-teardown analogue of xcard_persist.c. xcard_persist proves * persistence across a full driver unload/reload between dies; this proves the * SAME persistence while the nvidia module never leaves the kernel — only the * single active PCI function is rebound. That is the mechanism we actually ship. * * Per-die payload is a distinct pattern seeded from a real SmolLM2 weight chunk * (per-die file offset) so a stale read of the wrong die's VRAM cannot pass. * * Persistence is verified the vram_persist.c way (lines 270-315): after * re-establishing a context on the die, allocate same-size buffers in the SAME * order as Phase 1 so they land on the same physical VRAM, read back WITHOUT * writing, and compare to the recorded expected pattern + compute result. * * Reuses (verbatim where noted): * - test_swap.c : discover_gk210s, read_bar0, find_acquired_bdf, * set_driver_override, block_all_except, unblock_all * - compute_persist.c : the dlopen CUDA scaffolding + the slow_add PTX kernel * - vram_persist.c : the same-size-realloc persistence-verify trick * * Usage (on a K80 host, as root): * make roundrobin-persist * sudo ./barswap/tests/roundrobin_persist ... * e.g. sudo ./barswap/tests/roundrobin_persist 0 2 4 6 8 10 12 14 * * SmolLM2 payload path: argv has priority via none (it's all dies); the path * comes from $SMOL_GGUF, else the SMOL_GGUF_DEFAULT #define. Missing file -> * synthetic per-die ramp (noted in output). * * On a host with no barswap device (e.g. the Mac) it prints [SKIP] and exits 0, * so it compiles, links, and runs everywhere. Shells out to modprobe ONCE and * to sysfs bind/unbind — must run as root. Calls _Exit() at the end because the * rig's CUDA atexit teardown hangs after manual swaps. * * @status Builds clean on Mac (SKIP path). Not yet run on hardware. * @issues None * @todo None */ #include #include #include #include #include #include #include #include #include "barswap/barswap.h" #define COLOR_GREEN "\033[92m" #define COLOR_RED "\033[31m" #define COLOR_YELLOW "\033[93m" #define COLOR_RESET "\033[0m" #define MAX_DIES BS_MAX_DIES /* Default SmolLM2 weights path; override with $SMOL_GGUF. */ #define SMOL_GGUF_DEFAULT "/root/n2-test/models/SmolLM2-135M-Instruct-Q8_0.gguf" /* ================================================================ * GK210 enumeration + driver_override protocol (from test_swap.c) * ================================================================ */ /* BDF strings for all GK210 devices found in sysfs */ static char gk210_bdf[MAX_DIES][16]; static int n_gk210; /* * Discover GK210 device BDFs from sysfs (vendor 0x10de, device 0x102d). * Verbatim port of test_swap.c:discover_gk210s(). */ static int discover_gk210s(void) { FILE *fp; char path[512], buf[16]; DIR *dir; struct dirent *ent; char *bdfs[MAX_DIES]; int count = 0; dir = opendir("/sys/bus/pci/devices"); if (!dir) return 0; while ((ent = readdir(dir)) != NULL) { if (ent->d_name[0] == '.') continue; snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/vendor", ent->d_name); fp = fopen(path, "r"); if (!fp) continue; if (!fgets(buf, sizeof(buf), fp)) { fclose(fp); continue; } fclose(fp); if (strncmp(buf, "0x10de", 6) != 0) continue; snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/device", ent->d_name); fp = fopen(path, "r"); if (!fp) continue; if (!fgets(buf, sizeof(buf), fp)) { fclose(fp); continue; } fclose(fp); if (strncmp(buf, "0x102d", 6) != 0) continue; if (count < MAX_DIES) { bdfs[count] = strdup(ent->d_name); count++; } } closedir(dir); /* Sort by BDF name */ for (int i = 0; i < count - 1; i++) for (int j = i + 1; j < count; j++) if (strcmp(bdfs[i], bdfs[j]) > 0) { char *tmp = bdfs[i]; bdfs[i] = bdfs[j]; bdfs[j] = tmp; } for (int i = 0; i < count; i++) { snprintf(gk210_bdf[i], sizeof(gk210_bdf[i]), "%s", bdfs[i]); free(bdfs[i]); } n_gk210 = count; return count; } /* * Read BAR 0 value from sysfs for a PCI device. * Returns the resource start address, or 0 if unreadable/unmapped. * Verbatim port of test_swap.c:read_bar0(). */ static uint64_t read_bar0(const char *bdf) { char path[512], buf[256]; uint64_t start = 0; snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/resource", bdf); FILE *fp = fopen(path, "r"); if (!fp) return 0; /* First line of 'resource' file is BAR 0: start end flags */ if (fgets(buf, sizeof(buf), fp)) start = strtoull(buf, NULL, 16); fclose(fp); return start; } /* * Find which GK210 BDF currently has BARs programmed into the MMIO slot. * Only the acquired die has a non-zero BAR 0 (the rest are zeroed on release). * Mirrors test_swap.c:find_acquired_bdf(), keyed on "non-zero" since the * barswap lib's bs_gpu does not expose bar0_addr. * Returns the index into gk210_bdf[], or -1 if none found. */ static int find_acquired_bdf(void) { for (int i = 0; i < n_gk210; i++) { if (read_bar0(gk210_bdf[i]) != 0) return i; } return -1; } /* * Write a NUL-terminated string to a sysfs attribute (O_WRONLY, write, close). * Returns 0 on success, -1 on error (open or write failure), reporting via * stderr. The trailing newline expected by the PCI bind/unbind attributes must * be included by the caller. */ static int sysfs_write(const char *path, const char *str) { int fd = open(path, O_WRONLY); if (fd < 0) { fprintf(stderr, " sysfs_write: open(%s) failed\n", path); return -1; } ssize_t want = (ssize_t)strlen(str); ssize_t got = write(fd, str, (size_t)want); close(fd); if (got != want) { fprintf(stderr, " sysfs_write: write(%s, \"%s\") failed (%zd/%zd)\n", path, str, got, want); return -1; } return 0; } static void set_driver_override(const char *bdf, const char *value) { char path[512]; int fd; snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/driver_override", bdf); fd = open(path, O_WRONLY); if (fd < 0) return; if (*value == '\0') { /* Write newline to clear override — 0-byte write is ignored by VFS */ if (write(fd, "\n", 1) < 0) { /* best-effort */ } } else { if (write(fd, value, strlen(value)) < 0) { /* best-effort */ } } close(fd); } /* * Block all GK210 devices except the one at bdf_index, so nvidia only binds * the acquired die. Verbatim port of test_swap.c:block_all_except(). */ static void block_all_except(int bdf_index) { for (int i = 0; i < n_gk210; i++) { if (i == bdf_index) set_driver_override(gk210_bdf[i], ""); /* clear — allow nvidia */ else set_driver_override(gk210_bdf[i], "none"); /* block */ } } static void unblock_all(void) { for (int i = 0; i < n_gk210; i++) set_driver_override(gk210_bdf[i], ""); } /* * Load nvidia ONCE for the whole run. Mirrors compute_persist.c:load_nvidia(). * After this returns the module stays resident — it is never unloaded. */ static int load_nvidia_once(void) { int ret; /* Each module loaded separately with --ignore-install: "install nvidia * /bin/false" in modprobe.d otherwise blocks dependency resolution. */ ret = system("modprobe --ignore-install nvidia 2>/dev/null"); if (ret != 0) return -1; sleep(1); /* nvidia_uvm is required for cuInit to succeed */ ret = system("modprobe --ignore-install nvidia_uvm 2>/dev/null"); if (ret != 0) return -1; sleep(1); return 0; } /* * Confirm the nvidia module is still resident in the kernel. Reads * /proc/modules and looks for the "nvidia " entry. Returns 1 if present. * Used at the end to assert the module never left. */ static int nvidia_module_present(void) { FILE *fp = fopen("/proc/modules", "r"); if (!fp) return 0; char line[256]; int found = 0; while (fgets(line, sizeof(line), fp)) { if (strncmp(line, "nvidia ", 7) == 0) { found = 1; break; } } fclose(fp); return found; } /* ================================================================ * CUDA driver API via dlopen (from compute_persist.c + vram_persist.c) * ================================================================ */ typedef int CUresult; typedef int CUdevice; typedef void *CUcontext; typedef void *CUmodule; typedef void *CUfunction; typedef void *CUstream; typedef unsigned long long CUdeviceptr; #define CUDA_SUCCESS 0 typedef CUresult (*pfn_cuInit)(unsigned int); typedef CUresult (*pfn_cuDeviceGet)(CUdevice *, int); typedef CUresult (*pfn_cuCtxCreate)(CUcontext *, unsigned int, CUdevice); typedef CUresult (*pfn_cuCtxDestroy)(CUcontext); typedef CUresult (*pfn_cuCtxSynchronize)(void); typedef CUresult (*pfn_cuMemAlloc)(CUdeviceptr *, size_t); typedef CUresult (*pfn_cuMemFree)(CUdeviceptr); typedef CUresult (*pfn_cuMemcpyHtoD)(CUdeviceptr, const void *, size_t); typedef CUresult (*pfn_cuMemcpyDtoH)(void *, CUdeviceptr, size_t); typedef CUresult (*pfn_cuModuleLoadData)(CUmodule *, const void *); typedef CUresult (*pfn_cuModuleGetFunction)(CUfunction *, CUmodule, const char *); typedef CUresult (*pfn_cuLaunchKernel)(CUfunction, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, CUstream, void **, void **); static void *libcuda = NULL; static pfn_cuInit p_cuInit; static pfn_cuDeviceGet p_cuDeviceGet; static pfn_cuCtxCreate p_cuCtxCreate; static pfn_cuCtxDestroy p_cuCtxDestroy; static pfn_cuCtxSynchronize p_cuCtxSynchronize; static pfn_cuMemAlloc p_cuMemAlloc; static pfn_cuMemFree p_cuMemFree; static pfn_cuMemcpyHtoD p_cuMemcpyHtoD; static pfn_cuMemcpyDtoH p_cuMemcpyDtoH; static pfn_cuModuleLoadData p_cuModuleLoadData; static pfn_cuModuleGetFunction p_cuModuleGetFunction; static pfn_cuLaunchKernel p_cuLaunchKernel; /* * Embedded PTX kernel — adds 1.0 to each element `iters` times. * Verbatim port of compute_persist.c:slow_add_ptx (entry "slow_add"). * out[i] = in[i] + iters, exact in float32 while values stay under 2^24. */ static const char *slow_add_ptx = ".version 5.0\n" ".target sm_37\n" ".address_size 64\n" "\n" ".visible .entry slow_add(\n" " .param .u64 p_out,\n" " .param .u64 p_in,\n" " .param .u32 p_n,\n" " .param .u32 p_iters\n" ") {\n" " .reg .u32 %r<10>;\n" " .reg .u64 %rd<10>;\n" " .reg .f32 %f<4>;\n" " .reg .pred %p1;\n" "\n" " mov.u32 %r0, %tid.x;\n" " mov.u32 %r1, %ctaid.x;\n" " mov.u32 %r2, %ntid.x;\n" " mad.lo.u32 %r0, %r1, %r2, %r0;\n" "\n" " ld.param.u32 %r3, [p_n];\n" " setp.ge.u32 %p1, %r0, %r3;\n" " @%p1 bra $L_done;\n" "\n" " ld.param.u64 %rd0, [p_in];\n" " cvt.u64.u32 %rd1, %r0;\n" " shl.b64 %rd1, %rd1, 2;\n" " add.u64 %rd0, %rd0, %rd1;\n" " ld.global.f32 %f0, [%rd0];\n" "\n" " ld.param.u32 %r4, [p_iters];\n" " mov.u32 %r5, 0;\n" " mov.f32 %f1, 0f3F800000;\n" /* 1.0f */ "\n" "$L_loop:\n" " add.f32 %f0, %f0, %f1;\n" " add.u32 %r5, %r5, 1;\n" " setp.lt.u32 %p1, %r5, %r4;\n" " @%p1 bra $L_loop;\n" "\n" " ld.param.u64 %rd2, [p_out];\n" " cvt.u64.u32 %rd3, %r0;\n" " shl.b64 %rd3, %rd3, 2;\n" " add.u64 %rd2, %rd2, %rd3;\n" " st.global.f32 [%rd2], %f0;\n" "\n" "$L_done:\n" " ret;\n" "}\n"; static int load_cuda_symbols(void) { libcuda = dlopen("libcuda.so.1", RTLD_LAZY); if (!libcuda) libcuda = dlopen("libcuda.so", RTLD_LAZY); if (!libcuda) { fprintf(stderr, " Cannot dlopen libcuda: %s\n", dlerror()); return -1; } /* _v2 symbols where needed (legacy v1 ctx is incompatible). Mirrors * compute_persist.c:load_cuda_symbols(). */ #define LOAD(name, sym) do { \ p_##name = (pfn_##name)dlsym(libcuda, sym); \ if (!p_##name) { \ fprintf(stderr, " dlsym(%s) failed: %s\n", sym, dlerror()); \ return -1; \ } \ } while (0) LOAD(cuInit, "cuInit"); LOAD(cuDeviceGet, "cuDeviceGet"); LOAD(cuCtxCreate, "cuCtxCreate_v2"); LOAD(cuCtxDestroy, "cuCtxDestroy_v2"); LOAD(cuCtxSynchronize, "cuCtxSynchronize"); LOAD(cuMemAlloc, "cuMemAlloc_v2"); LOAD(cuMemFree, "cuMemFree_v2"); LOAD(cuMemcpyHtoD, "cuMemcpyHtoD_v2"); LOAD(cuMemcpyDtoH, "cuMemcpyDtoH_v2"); LOAD(cuModuleLoadData, "cuModuleLoadData"); LOAD(cuModuleGetFunction, "cuModuleGetFunction"); LOAD(cuLaunchKernel, "cuLaunchKernel"); #undef LOAD return 0; } /* ================================================================ * SmolLM2 per-die payload * ================================================================ */ #define N_ELEMENTS (1024 * 1024) /* 1M floats = 4 MB per buffer */ #define N_ITERS 1000 /* 1000 adds per element */ #define BLOCK_SIZE 256 /* Per-die file stride so each die reads a different SmolLM2 region. */ #define SMOL_DIE_STRIDE_BYTES (1024 * 1024) /* 1 MiB apart per die index */ static const char *smol_path; /* resolved once in main() */ static int smol_available; /* 1 if the file opened, 0 -> synthetic fallback */ /* * Per-die expectation, recorded in Phase 1 and re-checked in Phase 2. * Each die gets a DISTINCT input pattern so a stale read of the wrong die's * VRAM cannot masquerade as a pass. */ typedef struct die_expectation { int die_id; int list_index; /* position in the argv die list (drives offset) */ uint32_t seed; /* distinct per-die seed (from SmolLM2 or synthetic)*/ int bdf_idx; /* gk210_bdf index this die mapped to (-1 if none) */ int phase1_ok; /* Phase 1 wrote + computed successfully */ int vram_pass; /* Phase 2: input pattern survived */ int compute_pass; /* Phase 2: compute result survived */ } die_expectation; /* * Derive a distinct 32-bit seed for a die by reading a SmolLM2 weight chunk at * a per-die file offset and folding its bytes. Falls back to a synthetic seed * if the file can't be read at that offset. The seed makes the resident pattern * unmistakably die-specific. */ static uint32_t derive_die_seed(int list_index) { uint32_t synthetic = 0x9E3779B9u * (uint32_t)(list_index + 1); if (!smol_available) return synthetic; FILE *fp = fopen(smol_path, "rb"); if (!fp) return synthetic; off_t offset = (off_t)list_index * SMOL_DIE_STRIDE_BYTES; unsigned char chunk[4096]; if (fseeko(fp, offset, SEEK_SET) != 0) { fclose(fp); return synthetic; } size_t got = fread(chunk, 1, sizeof(chunk), fp); fclose(fp); if (got == 0) return synthetic; /* FNV-1a fold of the chunk -> distinct seed tied to real weight bytes */ uint32_t h = 2166136261u; for (size_t i = 0; i < got; i++) { h ^= chunk[i]; h *= 16777619u; } return h ? h : synthetic; } /* * Fill a host buffer with a distinct, deterministic per-die pattern derived * from the die's seed. Values are kept small (0..4095) so that out = in + N_ITERS * stays exact in float32 (well under 2^24), making the compute check reliable * regardless of the raw SmolLM2 byte magnitudes. */ static void fill_die_pattern(float *host_buf, uint32_t seed) { for (int i = 0; i < N_ELEMENTS; i++) { uint32_t v = (seed + (uint32_t)i * 2654435761u) & 0xFFFu; /* 0..4095 */ host_buf[i] = (float)v; } } /* Host-side expected compute result for element i: in[i] + N_ITERS. */ static float expected_compute_at(uint32_t seed, int i) { uint32_t v = (seed + (uint32_t)i * 2654435761u) & 0xFFFu; return (float)v + (float)N_ITERS; } /* ================================================================ * The switch primitive — THE point of this test * ================================================================ */ static bs_context *g_bs; static int active_bdf_idx = -1; /* gk210_bdf index currently nvidia-bound */ static int nvidia_loaded; /* set after the single modprobe */ static bs_gpu *current_gpu; /* die currently in the MMIO slot */ static CUcontext current_ctx; /* live ctx on the slot die (resident) */ /* * Unbind a single PCI device from the nvidia driver via sysfs. * Writes "\n" to /sys/bus/pci/drivers/nvidia/unbind. */ static int pci_unbind(const char *bdf) { char val[32]; snprintf(val, sizeof(val), "%s\n", bdf); return sysfs_write("/sys/bus/pci/drivers/nvidia/unbind", val); } /* * Bind a single PCI device to the nvidia driver via sysfs (re-probe just it). * Writes "\n" to /sys/bus/pci/drivers/nvidia/bind. */ static int pci_bind(const char *bdf) { char val[32]; snprintf(val, sizeof(val), "%s\n", bdf); return sysfs_write("/sys/bus/pci/drivers/nvidia/bind", val); } /* * switch_to — move the active GPU to `die`, NO driver teardown. * * Drops the current context and PCI-unbinds the old device, acquires the new * die into the MMIO slot, points driver_override at it, and either loads nvidia * (first switch ONLY) or PCI-binds just this device. Leaves a fresh CUDA context * live in current_ctx and the handle in current_gpu. Returns 0 on success. * * The nvidia module is loaded exactly once across the whole run; every later * switch is cuCtxDestroy -> unbind -> acquire -> bind. nvidia is NEVER rmmod'd. */ static int switch_to(int die) { CUresult err; CUdevice dev; /* 1. Drop the previous die's context + unbind its PCI function (driver * stays resident). */ if (active_bdf_idx >= 0) { if (current_ctx) { p_cuCtxDestroy(current_ctx); current_ctx = NULL; } printf(" switch: unbinding old device %s\n", gk210_bdf[active_bdf_idx]); if (pci_unbind(gk210_bdf[active_bdf_idx]) != 0) fprintf(stderr, " warning: unbind of %s failed\n", gk210_bdf[active_bdf_idx]); } /* Release the old slot mapping so the next acquire is clean. */ if (current_gpu) { bs_release(g_bs, current_gpu); current_gpu = NULL; } /* 2. Map the target die into the single MMIO slot. */ current_gpu = bs_acquire(g_bs, die); if (!current_gpu) { fprintf(stderr, " switch: bs_acquire(%d) failed\n", die); return -1; } /* 3. Identify which GK210 now occupies the slot. */ int idx = find_acquired_bdf(); if (idx < 0) { fprintf(stderr, " switch: could not find acquired BDF for die %d\n", die); return -1; } active_bdf_idx = idx; printf(" switch: target die %d -> acquired GK210 %s, active BDF %s\n", die, gk210_bdf[idx], gk210_bdf[idx]); /* 4. Allow nvidia to bind only the acquired die. */ block_all_except(idx); /* 5. Load nvidia ONCE, ever; afterwards just re-probe this one device. */ if (!nvidia_loaded) { printf(" switch: loading nvidia (first switch only)\n"); if (load_nvidia_once() != 0) { fprintf(stderr, " switch: nvidia load failed\n"); return -1; } nvidia_loaded = 1; /* First modprobe probes the only allowed device, binding it for us. */ } else { printf(" switch: binding device %s\n", gk210_bdf[idx]); if (pci_bind(gk210_bdf[idx]) != 0) fprintf(stderr, " warning: bind of %s failed\n", gk210_bdf[idx]); } /* 6. Fresh context on device 0 (== the slot die). */ if ((err = p_cuInit(0)) != CUDA_SUCCESS) { fprintf(stderr, " switch: cuInit failed: %d\n", err); return -1; } if ((err = p_cuDeviceGet(&dev, 0)) != CUDA_SUCCESS) { fprintf(stderr, " switch: cuDeviceGet failed: %d\n", err); return -1; } if ((err = p_cuCtxCreate(¤t_ctx, 0, dev)) != CUDA_SUCCESS) { fprintf(stderr, " switch: cuCtxCreate failed: %d\n", err); current_ctx = NULL; return -1; } return 0; } /* ================================================================ * Phases * ================================================================ */ /* * Phase 1 for one die: switch_to (NO teardown), allocate d_in/d_out, upload the * die's distinct pattern, launch the kernel (out = in + N_ITERS), sync, and * self-verify. Buffers and the context are LEFT resident — the next switch_to * handles cuCtxDestroy + PCI unbind. Returns 0 on success. */ static int phase1_load_resident(die_expectation *exp) { CUresult err; CUdeviceptr d_in = 0, d_out = 0; CUmodule mod; CUfunction kern; float *h_in = NULL, *h_check = NULL; int rc = -1; printf(COLOR_YELLOW "\n[Phase 1] die %d — load resident (no teardown)" COLOR_RESET "\n", exp->die_id); if (switch_to(exp->die_id) != 0) return -1; exp->bdf_idx = active_bdf_idx; size_t buf_bytes = (size_t)N_ELEMENTS * sizeof(float); if ((err = p_cuMemAlloc(&d_in, buf_bytes)) != CUDA_SUCCESS) { fprintf(stderr, " cuMemAlloc(in) failed: %d\n", err); goto done; } if ((err = p_cuMemAlloc(&d_out, buf_bytes)) != CUDA_SUCCESS) { fprintf(stderr, " cuMemAlloc(out) failed: %d\n", err); goto done; } h_in = (float *)malloc(buf_bytes); fill_die_pattern(h_in, exp->seed); if ((err = p_cuMemcpyHtoD(d_in, h_in, buf_bytes)) != CUDA_SUCCESS) { fprintf(stderr, " cuMemcpyHtoD failed: %d\n", err); goto done; } if ((err = p_cuModuleLoadData(&mod, slow_add_ptx)) != CUDA_SUCCESS) { fprintf(stderr, " cuModuleLoadData failed: %d\n", err); goto done; } if ((err = p_cuModuleGetFunction(&kern, mod, "slow_add")) != CUDA_SUCCESS) { fprintf(stderr, " cuModuleGetFunction failed: %d\n", err); goto done; } unsigned int n = N_ELEMENTS, iters = N_ITERS; unsigned int grid = (N_ELEMENTS + BLOCK_SIZE - 1) / BLOCK_SIZE; void *args[] = { &d_out, &d_in, &n, &iters }; if ((err = p_cuLaunchKernel(kern, grid, 1, 1, BLOCK_SIZE, 1, 1, 0, NULL, args, NULL)) != CUDA_SUCCESS) { fprintf(stderr, " cuLaunchKernel failed: %d\n", err); goto done; } if ((err = p_cuCtxSynchronize()) != CUDA_SUCCESS) { fprintf(stderr, " cuCtxSynchronize failed: %d\n", err); goto done; } /* In-die self-check before we ever leave this die. */ h_check = (float *)malloc(buf_bytes); p_cuMemcpyDtoH(h_check, d_out, buf_bytes); int ok = 1; for (int i = 0; i < N_ELEMENTS; i++) if (h_check[i] != expected_compute_at(exp->seed, i)) { ok = 0; break; } if (!ok) { fprintf(stderr, " Phase 1 self-check failed for die %d\n", exp->die_id); goto done; } printf(" die %d: seed=0x%08x, pattern uploaded + compute (out=in+%d), verify OK\n", exp->die_id, exp->seed, N_ITERS); printf(" die %d: buffers LEFT resident, ctx LEFT live (next switch tears down)\n", exp->die_id); exp->phase1_ok = 1; rc = 0; done: /* Free host buffers only. GPU buffers stay allocated; the context stays * live — both are intentionally left resident across the switch. */ free(h_in); free(h_check); return rc; } /* * Phase 2 for one die: switch_to, re-allocate the SAME two buffers in the SAME * order (in then out) so they land on the same physical VRAM, read back WITHOUT * writing, and compare to the recorded pattern + compute result. * Verification mirrors vram_persist.c lines 278 / 290-292 / 304-315. * Returns 0 if the check ran, -1 if it could not run. */ static int phase2_verify(die_expectation *exp) { CUresult err; CUdeviceptr d_in = 0, d_out = 0; float *r_in = NULL, *r_out = NULL; int rc = -1; printf(COLOR_YELLOW "\n[Phase 2] die %d — verify resident (no teardown)" COLOR_RESET "\n", exp->die_id); if (switch_to(exp->die_id) != 0) return -1; size_t buf_bytes = (size_t)N_ELEMENTS * sizeof(float); /* Same-size, same-order allocations as Phase 1 -> same physical VRAM. * (vram_persist.c:278) */ if ((err = p_cuMemAlloc(&d_in, buf_bytes)) != CUDA_SUCCESS) { fprintf(stderr, " cuMemAlloc(in) failed: %d\n", err); goto done; } if ((err = p_cuMemAlloc(&d_out, buf_bytes)) != CUDA_SUCCESS) { fprintf(stderr, " cuMemAlloc(out) failed: %d\n", err); goto done; } /* Read back without writing — checking for residual data. * (vram_persist.c:290-292) */ r_in = (float *)malloc(buf_bytes); r_out = (float *)malloc(buf_bytes); memset(r_in, 0, buf_bytes); memset(r_out, 0, buf_bytes); p_cuMemcpyDtoH(r_in, d_in, buf_bytes); p_cuMemcpyDtoH(r_out, d_out, buf_bytes); /* Tally per-element matches (vram_persist.c:304-315). */ int in_match = 0, out_match = 0; for (int i = 0; i < N_ELEMENTS; i++) { uint32_t v = (exp->seed + (uint32_t)i * 2654435761u) & 0xFFFu; if (r_in[i] == (float)v) in_match++; if (r_out[i] == (float)v + (float)N_ITERS) out_match++; } exp->vram_pass = (in_match == N_ELEMENTS); exp->compute_pass = (out_match == N_ELEMENTS); printf(" die %d: vram pattern %d/%d, compute result %d/%d\n", exp->die_id, in_match, N_ELEMENTS, out_match, N_ELEMENTS); rc = 0; done: if (d_in) p_cuMemFree(d_in); if (d_out) p_cuMemFree(d_out); free(r_in); free(r_out); return rc; } int main(int argc, char *argv[]) { setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); printf("=== Round-Robin No-Teardown Persistence Test ===\n\n"); /* Gate on barswap availability so this compiles+runs everywhere. */ g_bs = bs_init(); if (!g_bs) { printf("[SKIP] no barswap (bs_init failed — not on a K80 host)\n"); return 0; } if (argc < 2) { fprintf(stderr, "usage: %s ... (e.g. %s 0 2 4 6 8 10 12 14)\n", argv[0], argv[0]); bs_free(g_bs); return 1; } if (getuid() != 0) { fprintf(stderr, "error: must run as root (sudo) — needs modprobe + sysfs\n"); bs_free(g_bs); return 1; } /* Resolve SmolLM2 payload source: $SMOL_GGUF, else the default path. */ smol_path = getenv("SMOL_GGUF"); if (!smol_path || !*smol_path) smol_path = SMOL_GGUF_DEFAULT; FILE *probe = fopen(smol_path, "rb"); smol_available = (probe != NULL); if (probe) fclose(probe); int n_dies = argc - 1; if (n_dies > MAX_DIES) n_dies = MAX_DIES; die_expectation dies[MAX_DIES] = {0}; for (int i = 0; i < n_dies; i++) { dies[i].die_id = atoi(argv[i + 1]); dies[i].list_index = i; dies[i].seed = derive_die_seed(i); dies[i].bdf_idx = -1; } discover_gk210s(); /* Load CUDA symbols once; the nvidia module itself is loaded lazily by the * first switch_to and then never unloaded. */ if (load_cuda_symbols() != 0) { bs_free(g_bs); return 1; } printf("barswap: %u dies available, %d GK210s in sysfs\n", bs_num_dies(g_bs), n_gk210); printf("SmolLM2 payload: %s (%s)\n", smol_path, smol_available ? "found — per-die file offsets" : "MISSING — synthetic ramp"); printf("testing %d dies (no driver teardown between dies):", n_dies); for (int i = 0; i < n_dies; i++) printf(" %d", dies[i].die_id); printf("\n"); /* Phase 1: load each die's resident pattern, leaving ctx + buffers live. * Between dies there is NO rmmod — only cuCtxDestroy + PCI unbind/bind. */ int phase1_failures = 0; for (int i = 0; i < n_dies; i++) if (phase1_load_resident(&dies[i]) != 0) phase1_failures++; /* Phase 2: re-verify each die retained VRAM + compute result. */ int phase2_failures = 0; for (int i = 0; i < n_dies; i++) if (phase2_verify(&dies[i]) != 0) phase2_failures++; /* Final cleanup of the last resident context + slot mapping. nvidia stays. */ if (current_ctx) { p_cuCtxDestroy(current_ctx); current_ctx = NULL; } if (current_gpu) { bs_release(g_bs, current_gpu); current_gpu = NULL; } unblock_all(); /* Report */ printf("\n=== Results ===\n"); int all_pass = 1; for (int i = 0; i < n_dies; i++) { die_expectation *d = &dies[i]; int pass = d->phase1_ok && d->vram_pass && d->compute_pass; if (!pass) all_pass = 0; printf("%s die %d (%s):" COLOR_RESET " vram=%s compute=%s\n", pass ? COLOR_GREEN "[PASS]" : COLOR_RED "[FAIL]", d->die_id, d->bdf_idx >= 0 ? gk210_bdf[d->bdf_idx] : "bdf n/a", d->vram_pass ? "ok" : "LOST", d->compute_pass ? "ok" : "LOST"); } if (phase1_failures || phase2_failures) all_pass = 0; int still_loaded = nvidia_module_present(); if (!still_loaded) all_pass = 0; printf("\nnvidia stayed loaded the whole run: %s\n", still_loaded ? COLOR_GREEN "YES" COLOR_RESET : COLOR_RED "NO (module left the kernel!)" COLOR_RESET); printf("\n%s round-robin no-teardown persistence (%d dies)" COLOR_RESET "\n", all_pass ? COLOR_GREEN "[PASS]" : COLOR_RED "[FAIL]", n_dies); bs_free(g_bs); /* The rig's CUDA atexit teardown hangs after manual swaps — hard-exit. */ _Exit(all_pass ? 0 : 1); }