File size: 12,224 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 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | /* Unit tests for the monotonic-contiguous layer placement packer.
*
* Pure C99: no CUDA, no Metal. Builds and runs on any host. */
#include "ds4_layer_pack.h"
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_failed = 0;
static int g_total = 0;
#define CHECK(cond, msg) do { \
g_total++; \
if (!(cond)) { \
fprintf(stderr, " FAIL: %s (line %d)\n", (msg), __LINE__); \
g_failed++; \
} \
} while (0)
#define RUN(fn) do { \
fprintf(stderr, "RUN: %s\n", #fn); \
int _before = g_failed; \
(fn)(); \
fprintf(stderr, " %s\n", (_before == g_failed) ? "ok" : "FAIL"); \
} while (0)
/* Monotonicity invariant: tier(e) <= tier(e+1) where CPU is the max tier. */
static int check_monotonic(const int *dev, int n) {
int prev_rank = -1;
for (int i = 0; i < n; i++) {
int rank = (dev[i] == DS4_LAYER_PACK_CPU) ? 999999 : dev[i];
if (rank < prev_rank) return 0;
prev_rank = rank;
}
return 1;
}
/* ----- scenarios ----- */
static void test_all_fit_n1(void) {
const size_t entries[] = {10, 10, 10, 10, 10, 10};
ds4_layer_pack_config cfg = { .n_gpus = 1 };
cfg.gpu_budget_bytes[0] = 100;
int dev[6];
CHECK(ds4_compute_layer_placement(entries, 6, &cfg, dev) == 0, "rc 0");
for (int i = 0; i < 6; i++) CHECK(dev[i] == 0, "all on dev 0");
CHECK(check_monotonic(dev, 6), "monotonic");
}
static void test_all_fit_n2(void) {
/* Budgets 50/50, entries {20,20,20,20}. Greedy: dev0 takes 20+20=40;
* next 20 β 40+20=60 > 50, advance to dev1. dev1 takes 20+20=40.
* Expected: {0,0,1,1}. */
const size_t entries[] = {20, 20, 20, 20};
ds4_layer_pack_config cfg = { .n_gpus = 2 };
cfg.gpu_budget_bytes[0] = 50;
cfg.gpu_budget_bytes[1] = 50;
int dev[4];
CHECK(ds4_compute_layer_placement(entries, 4, &cfg, dev) == 0, "rc 0");
CHECK(dev[0] == 0 && dev[1] == 0, "entries 0,1 on dev 0");
CHECK(dev[2] == 1 && dev[3] == 1, "entries 2,3 on dev 1");
CHECK(check_monotonic(dev, 4), "monotonic");
}
static void test_partial_spill_n2(void) {
/* Budgets 30/30, entries {15,15,15,15,15}. Sum 75 > 60 β tail to CPU.
* dev0 takes 15+15=30; next 15 β 45 > 30, advance.
* dev1 takes 15+15=30; next 15 β 45 > 30, advance β CPU. */
const size_t entries[] = {15, 15, 15, 15, 15};
ds4_layer_pack_config cfg = { .n_gpus = 2 };
cfg.gpu_budget_bytes[0] = 30;
cfg.gpu_budget_bytes[1] = 30;
int dev[5];
CHECK(ds4_compute_layer_placement(entries, 5, &cfg, dev) == 0, "rc 0");
CHECK(dev[0] == 0 && dev[1] == 0, "0,1 on dev 0");
CHECK(dev[2] == 1 && dev[3] == 1, "2,3 on dev 1");
CHECK(dev[4] == DS4_LAYER_PACK_CPU, "4 on CPU");
CHECK(check_monotonic(dev, 5), "monotonic");
}
static void test_zero_budget_one_gpu_n2(void) {
/* Budgets {50, 0}, entries {10,10,10,10,10,10}. dev0 holds five (50/50).
* Next 10 β 60 > 50, advance to dev1; dev1 budget 0, 10 > 0, advance β
* CPU. The zero-budget device is effectively skipped. */
const size_t entries[] = {10, 10, 10, 10, 10, 10};
ds4_layer_pack_config cfg = { .n_gpus = 2 };
cfg.gpu_budget_bytes[0] = 50;
cfg.gpu_budget_bytes[1] = 0;
int dev[6];
CHECK(ds4_compute_layer_placement(entries, 6, &cfg, dev) == 0, "rc 0");
for (int i = 0; i < 5; i++) CHECK(dev[i] == 0, "first five on dev 0");
CHECK(dev[5] == DS4_LAYER_PACK_CPU, "sixth on CPU (dev 1 skipped)");
for (int i = 0; i < 6; i++) CHECK(dev[i] != 1, "nothing on dev 1");
CHECK(check_monotonic(dev, 6), "monotonic");
}
static void test_oversized_entry_n2(void) {
/* Entry too large for any device β CPU, and the rule propagates. */
const size_t entries[] = {10, 40, 10};
ds4_layer_pack_config cfg = { .n_gpus = 2 };
cfg.gpu_budget_bytes[0] = 30;
cfg.gpu_budget_bytes[1] = 30;
int dev[3];
CHECK(ds4_compute_layer_placement(entries, 3, &cfg, dev) == 0, "rc 0");
CHECK(dev[0] == 0, "small first on dev 0");
CHECK(dev[1] == DS4_LAYER_PACK_CPU, "oversized on CPU");
CHECK(dev[2] == DS4_LAYER_PACK_CPU, "post-oversized stays CPU");
CHECK(check_monotonic(dev, 3), "monotonic");
}
static void test_mixed_n8(void) {
/* Eight GPUs with VARIED budgets and VARIED entry sizes. Verify exact
* placements under greedy semantics so a buggy implementation that
* placed everything on the last device cannot pass. */
const size_t entries[] = {
/* idx: 0 1 2 3 4 5 6 7 8 9 10 11 12 */
30, 60, 40, 50, 80, 20, 20, 10, 50, 70, 90, 10, 10
};
const int n_entries = 13;
ds4_layer_pack_config cfg = { .n_gpus = 8 };
/* Budgets vary intentionally β small / medium / large mix. */
cfg.gpu_budget_bytes[0] = 100;
cfg.gpu_budget_bytes[1] = 80;
cfg.gpu_budget_bytes[2] = 50;
cfg.gpu_budget_bytes[3] = 90;
cfg.gpu_budget_bytes[4] = 60;
cfg.gpu_budget_bytes[5] = 30;
cfg.gpu_budget_bytes[6] = 80;
cfg.gpu_budget_bytes[7] = 40;
int dev[13];
CHECK(ds4_compute_layer_placement(entries, n_entries, &cfg, dev) == 0, "rc 0");
/* Hand-trace under strict greedy fill (advance once entry > current budget):
*
* d=0 budget=100
* e0 30 -> 30 fits (rem 70) ; dev[0]=0
* e1 60 -> 60 fits (rem 10) ; dev[1]=0
* e2 40 -> 40 > 10, advance to d=1 (rem 80)
* d=1 budget=80
* e2 40 -> 40 fits (rem 40) ; dev[2]=1
* e3 50 -> 50 > 40, advance to d=2 (rem 50)
* d=2 budget=50
* e3 50 -> 50 fits exactly (rem 0) ; dev[3]=2
* e4 80 -> 80 > 0, advance to d=3 (rem 90)
* d=3 budget=90
* e4 80 -> 80 fits (rem 10) ; dev[4]=3
* e5 20 -> 20 > 10, advance to d=4 (rem 60)
* d=4 budget=60
* e5 20 -> 20 fits (rem 40) ; dev[5]=4
* e6 20 -> 20 fits (rem 20) ; dev[6]=4
* e7 10 -> 10 fits (rem 10) ; dev[7]=4
* e8 50 -> 50 > 10, advance to d=5 (rem 30)
* d=5 budget=30
* e8 50 -> 50 > 30, advance to d=6 (rem 80)
* d=6 budget=80
* e8 50 -> 50 fits (rem 30) ; dev[8]=6
* e9 70 -> 70 > 30, advance to d=7 (rem 40)
* d=7 budget=40
* e9 70 -> 70 > 40, advance to d=8 (out of GPUs)
* e9..e12 all CPU
*/
const int expected[13] = {0,0,1,2,3,4,4,4,6,DS4_LAYER_PACK_CPU,DS4_LAYER_PACK_CPU,DS4_LAYER_PACK_CPU,DS4_LAYER_PACK_CPU};
for (int i = 0; i < n_entries; i++) {
if (dev[i] != expected[i]) {
fprintf(stderr, " mismatch e%d: got %d expected %d\n", i, dev[i], expected[i]);
}
CHECK(dev[i] == expected[i], "exact placement");
}
CHECK(check_monotonic(dev, n_entries), "monotonic");
}
static void test_n16_stress(void) {
/* 16 GPUs each budget 100; 32 entries of 50 each β {0,0,1,1,...,15,15}. */
ds4_layer_pack_config cfg = { .n_gpus = 16 };
for (int d = 0; d < 16; d++) cfg.gpu_budget_bytes[d] = 100;
size_t entries[32];
for (int i = 0; i < 32; i++) entries[i] = 50;
int dev[32];
CHECK(ds4_compute_layer_placement(entries, 32, &cfg, dev) == 0, "rc 0");
for (int i = 0; i < 32; i++) {
CHECK(dev[i] == i / 2, "pair per device");
}
CHECK(check_monotonic(dev, 32), "monotonic");
}
static void test_pseudo_layers(void) {
/* 43 layers + 2 pseudo. Embedding small, output head small, layers
* medium-sized. Three 60-byte budgets. */
const int n_layers = 43;
const int n_entries = n_layers + 2;
size_t entries[45];
entries[0] = 5; /* embedding */
for (int i = 1; i <= n_layers; i++) entries[i] = 10;
entries[n_layers + 1] = 7; /* output head */
ds4_layer_pack_config cfg = { .n_gpus = 3 };
cfg.gpu_budget_bytes[0] = 60;
cfg.gpu_budget_bytes[1] = 60;
cfg.gpu_budget_bytes[2] = 60;
int dev[45];
CHECK(ds4_compute_layer_placement(entries, n_entries, &cfg, dev) == 0, "rc 0");
/* Embedding placed first β on dev 0 (5 bytes, plenty of room). */
CHECK(dev[0] == 0, "embedding on dev 0");
/* Total = 5 + 43*10 + 7 = 442. 3 budgets of 60 = 180. Mostly CPU.
* The output head lands wherever the chain ends. With 5 + 10*5 = 55 on
* dev 0 (5 embedding + 5 layers), next 10 β 65 > 60 β advance to dev 1.
* Dev 1 holds 6 layers (60). Dev 2 holds 6 layers (60). After that
* everything is CPU. Output head ends up on CPU. */
CHECK(dev[n_layers + 1] == DS4_LAYER_PACK_CPU, "output head on CPU");
CHECK(check_monotonic(dev, n_entries), "monotonic");
}
static void test_null_inputs(void) {
size_t one = 0;
int dev = 0;
ds4_layer_pack_config cfg = { .n_gpus = 1 };
cfg.gpu_budget_bytes[0] = 100;
CHECK(ds4_compute_layer_placement(NULL, 1, &cfg, &dev) != 0, "null entries");
CHECK(ds4_compute_layer_placement(&one, 1, NULL, &dev) != 0, "null cfg");
CHECK(ds4_compute_layer_placement(&one, 1, &cfg, NULL) != 0, "null out");
ds4_layer_pack_config bad = { .n_gpus = -1 };
CHECK(ds4_compute_layer_placement(&one, 1, &bad, &dev) != 0, "n_gpus<0");
bad.n_gpus = DS4_LAYER_PACK_MAX_GPUS + 1;
CHECK(ds4_compute_layer_placement(&one, 1, &bad, &dev) != 0, "n_gpus too big");
}
/* Golden-string print test.
*
* Setup: n_layers=4 (so entries are indices 0..5), with placement
* entry 0 (embedding) -> dev 0
* entry 1 (layer 0) -> dev 0
* entry 2 (layer 1) -> dev 0
* entry 3 (layer 2) -> dev 1
* entry 4 (layer 3) -> CPU
* entry 5 (output head) -> CPU
*
* Used/budget GB chosen so the printed numbers are stable (no rounding
* ambiguity): used = 10 GiB, budget = 20 GiB β "10.0 / 20.0 GB" on each
* device. GiB = 1073741824. */
static void test_print_golden(void) {
setlocale(LC_ALL, "C");
const int n_layers = 4;
const int n_entries = n_layers + 2;
int dev[6] = { 0, 0, 0, 1, DS4_LAYER_PACK_CPU, DS4_LAYER_PACK_CPU };
size_t entry_bytes[6] = {0, 0, 0, 0, 0, 0};
size_t used[2] = {10ull * 1073741824ull, 10ull * 1073741824ull};
size_t budget[2] = {20ull * 1073741824ull, 20ull * 1073741824ull};
/* Portable golden capture: tmpfile() + rewind + fread. fmemopen is
* a glibc extension and may not be available on every host that
* builds the CPU-only target. */
char buf[1024];
memset(buf, 0, sizeof(buf));
FILE *f = tmpfile();
CHECK(f != NULL, "tmpfile");
if (!f) return;
ds4_layer_pack_print(f, dev, n_entries, n_layers, entry_bytes,
used, budget, 2);
fflush(f);
rewind(f);
size_t n_read = fread(buf, 1, sizeof(buf) - 1, f);
buf[n_read] = '\0';
fclose(f);
const char *expected =
"multi-GPU layout:\n"
" GPU0: layers 0-1 + embedding (10.0 / 20.0 GB)\n"
" GPU1: layer 2 (10.0 / 20.0 GB)\n"
" CPU : layer 3 + output head\n";
if (strcmp(buf, expected) != 0) {
fprintf(stderr, " -- expected --\n%s", expected);
fprintf(stderr, " -- got --\n%s", buf);
}
CHECK(strcmp(buf, expected) == 0, "golden layout");
CHECK(check_monotonic(dev, n_entries), "golden monotonic");
}
int main(void) {
setlocale(LC_ALL, "C");
RUN(test_all_fit_n1);
RUN(test_all_fit_n2);
RUN(test_partial_spill_n2);
RUN(test_zero_budget_one_gpu_n2);
RUN(test_oversized_entry_n2);
RUN(test_mixed_n8);
RUN(test_n16_stress);
RUN(test_pseudo_layers);
RUN(test_null_inputs);
RUN(test_print_golden);
fprintf(stderr, "\ntest_layer_pack: %d/%d checks passed (%d failed)\n",
g_total - g_failed, g_total, g_failed);
return g_failed == 0 ? 0 : 1;
}
|