299 lines
12 KiB
C
299 lines
12 KiB
C
/* 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;
|
|
}
|