Pantheon/tests/test_graph_capture_concurre...

157 lines
5.6 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/* test_graph_capture_concurrency.c — regression guard for the GLOBAL stream
* capture bug (task #28, foundry e6af3d4).
*
* WHY THIS EXISTS
* fc_graph_begin_ctx used to open capture in CU_STREAM_CAPTURE_MODE_GLOBAL,
* which makes the driver prohibit "potentially unsafe" CUDA calls across the
* WHOLE PROCESS for the duration of the capture. A serve runs one decode
* thread per batched model plus a connection thread per HTTP request, with no
* GPU-wide lock across models, so a second model's ordinary cuMemAlloc /
* cuStreamSynchronize both failed (CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED,
* 900) and invalidated the other die's open capture (901). Output stayed
* correct - the recorded launches never execute - but the per-layer retry
* latches, so one collision cost that layer its graph for the life of the
* process. It showed up as unexplained 900/901 in the production log.
*
* Measured on the K80 (driver 470.256.02) before the fix: 20 of 300 captures
* destroyed and 21 of 108 of the other thread's allocations rejected. After:
* 300/300 and zero rejections.
*
* WHAT IT ASSERTS
* With a second thread allocating/syncing on a SECOND die, every capture on
* the first die still succeeds, and none of the second thread's allocations
* are refused. Both are exact - any failure at all is the bug returning.
*
* REQUIREMENTS
* Two visible CUDA devices. Self-gates otherwise (prints [SKIP]), so it is
* safe to run anywhere. On the K80 test machine, pin it to an idle die:
* CUDA_VISIBLE_DEVICES=6,7 tests/test_graph_capture_concurrency
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include "cuda/fc_cuda.h"
#define ROUNDS 200
#define LAUNCHES_PER 13 /* about one layer body */
#define SCRATCH_N 4096
static fc_cuda_ctx *ctx_a, *ctx_b;
static volatile int stop_b;
static int cap_ok, cap_fail, launch_fail;
static int b_alloc_ok, b_alloc_fail;
/* Stands in for a second model's connection thread: allocate scratch, wait for
* its die, free. Every one of these is a call GLOBAL capture used to reject. */
static void *interferer(void *unused)
{
(void)unused;
while (!stop_b) {
fc_buffer *t = fc_buffer_alloc_ctx(ctx_b, 1u << 20);
if (!t) { b_alloc_fail++; usleep(200); continue; }
b_alloc_ok++;
fc_sync_ctx(ctx_b);
fc_buffer_free(t);
}
return NULL;
}
static fc_buffer *x, *y, *z;
static void capture_round(void)
{
if (fc_graph_begin_ctx(ctx_a) != 0) { cap_fail++; return; }
for (int i = 0; i < LAUNCHES_PER; i++) {
fc_add_ctx(ctx_a, x, y, z, SCRATCH_N);
fc_mul_ctx(ctx_a, z, y, x, SCRATCH_N);
}
fc_graph *g = fc_graph_end_ctx(ctx_a);
if (!g) { cap_fail++; return; }
if (fc_graph_launch(g) != 0) launch_fail++;
else cap_ok++;
fc_graph_free(g);
}
int main(void)
{
setbuf(stdout, NULL);
ctx_a = fc_cuda_init_device(0);
if (!ctx_a) {
printf("[SKIP] no CUDA device 0\n");
return 0;
}
ctx_b = fc_cuda_init_device(1);
if (!ctx_b) {
printf("[SKIP] needs two visible CUDA devices "
"(try CUDA_VISIBLE_DEVICES=6,7)\n");
fc_cuda_ctx_free(ctx_a);
return 0;
}
printf("capture die=%d interferer die=%d rounds=%d\n",
fc_cuda_ctx_device_id(ctx_a), fc_cuda_ctx_device_id(ctx_b), ROUNDS);
x = fc_buffer_alloc_ctx(ctx_a, SCRATCH_N * sizeof(float));
y = fc_buffer_alloc_ctx(ctx_a, SCRATCH_N * sizeof(float));
z = fc_buffer_alloc_ctx(ctx_a, SCRATCH_N * sizeof(float));
if (!x || !y || !z) {
printf("[SKIP] could not allocate capture scratch on die 0\n");
return 0;
}
/* Baseline: uncontended. If this is not clean the box has a problem that
* has nothing to do with what we are testing. */
for (int i = 0; i < ROUNDS; i++) capture_round();
if (cap_ok != ROUNDS) {
printf("FAIL: uncontended capture is already broken "
"(%d/%d ok, %d failed, %d launch failed)\n",
cap_ok, ROUNDS, cap_fail, launch_fail);
return 1;
}
printf(" uncontended: %d/%d captures OK\n", cap_ok, ROUNDS);
cap_ok = cap_fail = launch_fail = 0;
pthread_t th;
if (pthread_create(&th, NULL, interferer, NULL) != 0) {
printf("[SKIP] could not start interferer thread\n");
return 0;
}
for (int i = 0; i < ROUNDS; i++) capture_round();
stop_b = 1;
pthread_join(th, NULL);
printf(" contended: %d/%d captures OK (%d failed, %d launch failed); "
"interferer allocs %d OK / %d refused\n",
cap_ok, ROUNDS, cap_fail, launch_fail, b_alloc_ok, b_alloc_fail);
int bad = 0;
if (cap_ok != ROUNDS) {
printf("FAIL: a second die's thread destroyed %d of %d captures — "
"stream capture is process-GLOBAL again\n",
ROUNDS - cap_ok, ROUNDS);
bad = 1;
}
if (b_alloc_fail != 0) {
printf("FAIL: %d of the interferer's %d allocations were refused — "
"a capture on another die is blocking unrelated work\n",
b_alloc_fail, b_alloc_ok + b_alloc_fail);
bad = 1;
}
if (b_alloc_ok == 0) {
printf("FAIL: interferer never allocated; the test proved nothing\n");
bad = 1;
}
fc_buffer_free(x); fc_buffer_free(y); fc_buffer_free(z);
fc_cuda_ctx_free(ctx_a);
fc_cuda_ctx_free(ctx_b);
printf(bad ? "[FAIL] graph capture concurrency\n"
: "[PASS] graph capture concurrency\n");
return bad;
}