Download tests/test_layer_pack.c from julianmb/ds4fa: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/julianmb/ds4fa/resolve/main/tests/test_layer_pack.c
- Command line
-
hf download hf://julianmb/ds4fa/tests/test_layer_pack.c
-
curl -L -o test_layer_pack.c https://huggingface.co/julianmb/ds4fa/resolve/main/tests/test_layer_pack.c
12.2 kB
| /* Unit tests for the monotonic-contiguous layer placement packer. | |
| * | |
| * Pure C99: no CUDA, no Metal. Builds and runs on any host. */ | |
| static int g_failed = 0; | |
| static int g_total = 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; | |
| } | |