File size: 5,963 Bytes
8d0b310
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/* CUDA-build regression test for the multi-tier CPU-spill refusal path.
 *
 * Half-B (B7) lifted the GPU-only refusal: GPU-only multi-tier placements
 * now run normally. CPU-spill placements (where any layer lands on the
 * CPU tier because GPU VRAM is too tight) continue to refuse with stderr
 * naming the wave-3b follow-up `mgpu-graph-session-cpu-spill`.
 *
 * This test forces CPU spill by configuring two GPUs with deliberately
 * tiny VRAM budgets (1 GiB each, not enough to hold the model), then
 * asserts that engine creation:
 *   - returns nonzero,
 *   - leaves the engine pointer NULL,
 *   - emits stderr containing "multi-GPU layout" (proves the layout
 *     printer ran — init_multi succeeded and placement was computed),
 *   - emits stderr containing "mgpu-graph-session-cpu-spill" (proves
 *     the CPU-spill branch fired with the new wording).
 *
 * Requires DS4_TEST_MODEL to be set to a valid GGUF path. */

#include "ds4.h"
#include "ds4_gpu_mgpu.h"

#include <cuda_runtime.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define CHECK(cond, msg)                                                    \
    do {                                                                    \
        if (!(cond)) {                                                      \
            fprintf(stderr, "FAIL: %s (line %d)\n", (msg), __LINE__);       \
            return 1;                                                       \
        }                                                                   \
    } while (0)

static int read_file_to_buf(const char *path, char **out_buf, long *out_len) {
    FILE *f = fopen(path, "rb");
    if (!f) return 1;
    if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 1; }
    long n = ftell(f);
    if (n < 0) { fclose(f); return 1; }
    if (fseek(f, 0, SEEK_SET) != 0) { fclose(f); return 1; }
    char *buf = (char *)malloc((size_t)n + 1);
    if (!buf) { fclose(f); return 1; }
    size_t r = fread(buf, 1, (size_t)n, f);
    fclose(f);
    buf[r] = '\0';
    *out_buf = buf;
    *out_len = (long)r;
    return 0;
}

int main(void) {
    int dev_count = 0;
    (void)cudaGetDeviceCount(&dev_count);
    fprintf(stderr, "test_engine_mgpu_refusal: %d CUDA devices visible\n", dev_count);
    if (dev_count < 2) {
        fprintf(stderr, "  skipping (need >= 2 devices)\n");
        return 0;
    }

    const char *model_path = getenv("DS4_TEST_MODEL");
    if (!model_path || !model_path[0]) {
        fprintf(stderr, "FAIL: DS4_TEST_MODEL not set\n");
        return 1;
    }

    /* Capture stderr to a temp file. We use a known path so failures are
     * easy to inspect; the file is removed at the end on success. */
    const char *cap_path = "/tmp/ds4_mgpu_refusal_stderr.log";
    (void)unlink(cap_path);
    fflush(stderr);
    int saved_stderr = dup(fileno(stderr));
    CHECK(saved_stderr >= 0, "dup stderr");
    FILE *redir = freopen(cap_path, "w+", stderr);
    CHECK(redir != NULL, "freopen stderr");

    /* Build a 2-GPU config. */
    ds4_gpu_config cfg;
    memset(&cfg, 0, sizeof(cfg));
    cfg.n_gpus = 2;
    cfg.device_indices[0] = 0;
    cfg.device_indices[1] = 1;
    /* Pick small-but-not-too-small budgets:
     *   - each must exceed the per-tier graph overhead (~4 GiB at default
     *     ctx) so the pre-subtract refusal at engine_classify_multi_tier
     *     doesn't fire (that path returns early without printing the
     *     "multi-GPU layout:" header this test asserts on);
     *   - but the combined budget must be far below the model's tensor
     *     bytes so the packer is forced to spill some entries to CPU,
     *     triggering the CPU-spill refusal path this test exercises.
     * 8 GiB per GPU = 16 GiB total: leaves ~4 GiB usable per tier after
     * the pre-subtract — enough for some layer entries — while the
     * ~36 GiB model forces spill. */
    cfg.vram_bytes[0] = (size_t)8ull * 1024u * 1024u * 1024u;   /* 8 GiB */
    cfg.vram_bytes[1] = (size_t)8ull * 1024u * 1024u * 1024u;   /* 8 GiB */
    cfg.safety_margin_bytes = 0;

    ds4_engine_options opt;
    memset(&opt, 0, sizeof(opt));
    opt.model_path = model_path;
    opt.backend = DS4_BACKEND_CUDA;
    opt.n_threads = 1;
    opt.warm_weights = false;
    opt.quality = false;

    ds4_engine *engine = NULL;
    int rc = ds4_engine_create_with_gpu_config(&engine, &opt, &cfg);

    /* Restore stderr so subsequent prints reach the terminal. */
    fflush(stderr);
    FILE *sink = freopen("/dev/null", "w", stderr); /* close redir's FILE* cleanly */
    CHECK(sink != NULL, "freopen stderr sink");
    int err_fd = fileno(stderr);
    if (err_fd >= 0) {
        (void)dup2(saved_stderr, err_fd);
        (void)close(saved_stderr);
    }

    fprintf(stderr, "  engine_create_with_gpu_config -> rc=%d, engine=%p\n",
            rc, (void *)engine);

    /* Read captured stderr. */
    char *cap = NULL; long cap_len = 0;
    int read_rc = read_file_to_buf(cap_path, &cap, &cap_len);
    if (read_rc != 0) {
        fprintf(stderr, "FAIL: could not read captured stderr %s\n", cap_path);
        return 1;
    }
    fprintf(stderr, "  captured stderr (%ld bytes):\n----\n%s\n----\n",
            cap_len, cap);

    CHECK(rc != 0, "engine_create should refuse multi-tier and return nonzero");
    CHECK(engine == NULL, "engine pointer should be NULL on refusal");
    CHECK(strstr(cap, "multi-GPU layout") != NULL,
          "stderr must contain 'multi-GPU layout' (init_multi must have succeeded "
          "and layout must have been printed before refusal)");
    CHECK(strstr(cap, "mgpu-graph-session-cpu-spill") != NULL,
          "stderr must name the wave-3b CPU-spill follow-up task");
    CHECK(strstr(cap, "CPU-spill placement detected") != NULL,
          "stderr must contain the CPU-spill diagnostic line");

    free(cap);
    (void)unlink(cap_path);
    fprintf(stderr, "test_engine_mgpu_refusal PASS\n");
    return 0;
}