Skip to content

Commit 7623de1

Browse files
authored
tests: test-backend-ops -j <N> to run tests in parallel (ggml-org#23637)
Create a pool of N threads that grab a chunk of up to 100 tests at a time to iterate through. The number of tests at a time decreases as fewer remain. Each thread uses its own dev and cpu backend, and set_n_threads_fn is not called on the cpu backend. Fix some TSAN issues that arose: - In init_tensor_uniform, don't use static vector of generators. - Replace gmtime with versions that don't use a global variable. - Mutex calls to print_test_result.
1 parent c9d9829 commit 7623de1

1 file changed

Lines changed: 138 additions & 45 deletions

File tree

tests/test-backend-ops.cpp

Lines changed: 138 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <ggml-cpp.h>
2222

2323
#include <algorithm>
24+
#include <atomic>
2425
#include <array>
2526
#include <cfloat>
2627
#include <cinttypes>
@@ -33,6 +34,7 @@
3334
#include <future>
3435
#include <fstream>
3536
#include <memory>
37+
#include <mutex>
3638
#include <random>
3739
#include <regex>
3840
#include <set>
@@ -55,33 +57,24 @@ static void init_tensor_uniform(ggml_tensor * tensor, float min = -1.0f, float m
5557
{
5658
// parallel initialization
5759
static const size_t n_threads = N_THREADS;
58-
// static RNG initialization (revisit if n_threads stops being constant)
59-
static std::vector<std::default_random_engine> generators = []() {
60-
std::random_device rd;
61-
std::vector<std::default_random_engine> vec;
62-
vec.reserve(n_threads);
63-
//for (size_t i = 0; i < n_threads; i++) { vec.emplace_back(1234 + i); } // fixed seed
64-
for (size_t i = 0; i < n_threads; i++) { vec.emplace_back(rd()); }
65-
return vec;
66-
}();
67-
68-
auto init_thread = [&](size_t ith, size_t start, size_t end) {
60+
61+
auto init_thread = [&](size_t start, size_t end) {
62+
thread_local std::default_random_engine gen(std::random_device{}());
6963
std::uniform_real_distribution<float> distribution(min, max);
70-
auto & gen = generators[ith];
7164
for (size_t i = start; i < end; i++) {
7265
data[i] = distribution(gen);
7366
}
7467
};
7568

7669
if (n_threads == 1) {
77-
init_thread(0, 0, nels);
70+
init_thread(0, nels);
7871
} else {
7972
std::vector<std::future<void>> tasks;
8073
tasks.reserve(n_threads);
8174
for (size_t i = 0; i < n_threads; i++) {
8275
size_t start = i*nels/n_threads;
8376
size_t end = (i+1)*nels/n_threads;
84-
tasks.push_back(std::async(std::launch::async, init_thread, i, start, end));
77+
tasks.push_back(std::async(std::launch::async, init_thread, start, end));
8578
}
8679
for (auto & t : tasks) {
8780
t.get();
@@ -516,6 +509,25 @@ static bool output_format_from_str(const std::string & s, output_formats & forma
516509
return true;
517510
}
518511

512+
static std::string test_time_now() {
513+
time_t t = time(NULL);
514+
struct tm tm_buf;
515+
#ifdef _WIN32
516+
if (gmtime_s(&tm_buf, &t) != 0) {
517+
return "";
518+
}
519+
#else
520+
if (gmtime_r(&t, &tm_buf) == nullptr) {
521+
return "";
522+
}
523+
#endif
524+
char buf[32];
525+
if (std::strftime(buf, sizeof(buf), "%FT%TZ", &tm_buf) == 0) {
526+
return "";
527+
}
528+
return buf;
529+
}
530+
519531
// Test result structure for SQL output
520532
struct test_result {
521533
std::string test_time;
@@ -545,11 +557,7 @@ struct test_result {
545557
supported = false;
546558
passed = false;
547559

548-
// Set test time
549-
time_t t = time(NULL);
550-
char buf[32];
551-
std::strftime(buf, sizeof(buf), "%FT%TZ", gmtime(&t));
552-
test_time = buf;
560+
test_time = test_time_now();
553561

554562
// Set build info
555563
build_commit = ggml_commit();
@@ -573,11 +581,7 @@ struct test_result {
573581
n_runs(n_runs),
574582
device_description(device_description),
575583
backend_reg_name(backend_reg_name) {
576-
// Set test time
577-
time_t t = time(NULL);
578-
char buf[32];
579-
std::strftime(buf, sizeof(buf), "%FT%TZ", gmtime(&t));
580-
test_time = buf;
584+
test_time = test_time_now();
581585

582586
// Set build info
583587
build_commit = ggml_commit();
@@ -1110,6 +1114,17 @@ static std::unique_ptr<printer> create_printer(output_formats format) {
11101114
GGML_ABORT("invalid output format");
11111115
}
11121116

1117+
static std::mutex g_test_output_mutex;
1118+
1119+
static void print_test_result_locked(printer * output_printer, const test_result & result) {
1120+
if (output_printer == nullptr) {
1121+
return;
1122+
}
1123+
1124+
std::lock_guard<std::mutex> guard(g_test_output_mutex);
1125+
output_printer->print_test_result(result);
1126+
}
1127+
11131128
struct test_case {
11141129
virtual ~test_case() {}
11151130

@@ -1338,9 +1353,7 @@ struct test_case {
13381353
test_result result(ggml_backend_name(backend1), current_op_name, vars(), "test",
13391354
false, false, "not supported");
13401355

1341-
if (output_printer) {
1342-
output_printer->print_test_result(result);
1343-
}
1356+
print_test_result_locked(output_printer, result);
13441357

13451358
ggml_free(ctx);
13461359
return test_status_t::NOT_SUPPORTED;
@@ -1462,9 +1475,7 @@ struct test_case {
14621475
test_result result(ggml_backend_name(backend1), current_op_name, vars(), "test", supported, test_passed,
14631476
error_msg);
14641477

1465-
if (output_printer) {
1466-
output_printer->print_test_result(result);
1467-
}
1478+
print_test_result_locked(output_printer, result);
14681479

14691480
return test_passed ? test_status_t::OK : test_status_t::FAIL;
14701481
}
@@ -9493,8 +9504,8 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_from_file(const c
94939504
return test_cases;
94949505
}
94959506

9496-
static bool test_backend(ggml_backend_t backend, test_mode mode, const char * op_names_filter, const char * params_filter,
9497-
printer * output_printer, const char * test_file_path) {
9507+
static bool test_backend(ggml_backend_t backend, ggml_backend_dev_t dev, test_mode mode, const char * op_names_filter, const char * params_filter,
9508+
printer * output_printer, const char * test_file_path, int parallel_workers) {
94989509
auto filter_test_cases = [](std::vector<std::unique_ptr<test_case>> & test_cases, const char * params_filter) {
94999510
if (params_filter == nullptr) {
95009511
return;
@@ -9547,21 +9558,90 @@ static bool test_backend(ggml_backend_t backend, test_mode mode, const char * op
95479558
set_use_ref(backend_cpu, true);
95489559
}
95499560

9550-
size_t n_ok = 0;
9551-
size_t tests_run = 0;
9561+
std::atomic<size_t> n_ok = 0;
9562+
std::atomic<size_t> tests_run = 0;
95529563
std::vector<std::string> failed_tests;
9553-
for (auto & test : test_cases) {
9554-
test_status_t status = test->eval(backend, backend_cpu, op_names_filter, output_printer);
9555-
if (status == test_status_t::SKIPPED || status == test_status_t::NOT_SUPPORTED) {
9556-
continue;
9564+
std::mutex failed_tests_mutex;
9565+
9566+
// Each worker grabs a chunk of cases at a time. The chunk shrinks as we
9567+
// run out of work so that a few slow tests at the tail get spread across
9568+
// workers instead of landing on one unlucky thread.
9569+
constexpr size_t MAX_TESTS_PER_ITER = 100;
9570+
std::atomic<size_t> test_idx = 0;
9571+
9572+
const auto & next_chunk = [&](size_t & my_begin, size_t & my_end) {
9573+
const size_t cur = test_idx.load(std::memory_order_relaxed);
9574+
const size_t remaining = cur < test_cases.size() ? test_cases.size() - cur : 0;
9575+
const size_t chunk = std::max<size_t>(1, std::min<size_t>(MAX_TESTS_PER_ITER, remaining / parallel_workers));
9576+
my_begin = test_idx.fetch_add(chunk);
9577+
my_end = std::min(my_begin + chunk, test_cases.size());
9578+
};
9579+
9580+
const auto & run_tests = [&](ggml_backend_t b, ggml_backend_t b_cpu) {
9581+
size_t my_begin, my_end;
9582+
next_chunk(my_begin, my_end);
9583+
while (my_begin < test_cases.size()) {
9584+
for (size_t i = my_begin; i < my_end; ++i) {
9585+
auto & test = test_cases[i];
9586+
test_status_t status = test->eval(b, b_cpu, op_names_filter, output_printer);
9587+
if (status == test_status_t::SKIPPED || status == test_status_t::NOT_SUPPORTED) {
9588+
continue;
9589+
}
9590+
tests_run++;
9591+
if (status == test_status_t::OK) {
9592+
n_ok++;
9593+
} else if (status == test_status_t::FAIL) {
9594+
std::lock_guard<std::mutex> guard(failed_tests_mutex);
9595+
failed_tests.push_back(test->current_op_name + "(" + test->vars() + ")");
9596+
}
9597+
}
9598+
next_chunk(my_begin, my_end);
95579599
}
9558-
tests_run++;
9559-
if (status == test_status_t::OK) {
9560-
n_ok++;
9561-
} else if (status == test_status_t::FAIL) {
9562-
failed_tests.push_back(test->current_op_name + "(" + test->vars() + ")");
9600+
};
9601+
9602+
if (parallel_workers <= 1) {
9603+
// Reuse the outer backend / backend_cpu so we don't pay an
9604+
// extra CPU backend init.
9605+
run_tests(backend, backend_cpu);
9606+
} else {
9607+
std::atomic<size_t> workers_started = 0;
9608+
9609+
const auto & eval_worker = [&]() {
9610+
ggml_backend_t b = ggml_backend_dev_init(dev, NULL);
9611+
if (b == NULL) {
9612+
return;
9613+
}
9614+
9615+
ggml_backend_t b_cpu = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, NULL);
9616+
if (b_cpu == NULL) {
9617+
ggml_backend_free(b);
9618+
return;
9619+
}
9620+
9621+
if (set_use_ref) {
9622+
set_use_ref(b_cpu, true);
9623+
}
9624+
workers_started++;
9625+
run_tests(b, b_cpu);
9626+
ggml_backend_free(b_cpu);
9627+
ggml_backend_free(b);
9628+
};
9629+
9630+
std::vector<std::thread> threads;
9631+
threads.reserve(parallel_workers);
9632+
for (int i = 0; i < parallel_workers; ++i) {
9633+
threads.emplace_back(eval_worker);
9634+
}
9635+
for (auto & t : threads) {
9636+
t.join();
9637+
}
9638+
9639+
if (workers_started == 0 && !test_cases.empty()) {
9640+
ggml_backend_free(backend_cpu);
9641+
return false;
95639642
}
95649643
}
9644+
95659645
output_printer->print_summary(test_summary_info(n_ok, tests_run, false));
95669646
output_printer->print_failed_tests(failed_tests);
95679647

@@ -9709,7 +9789,7 @@ static void show_test_coverage() {
97099789

97109790
static void usage(char ** argv) {
97119791
printf("Usage: %s [mode] [-o <op,..>] [-b <backend>] [-p <params regex>] [--output <console|sql|csv>] [--list-ops]", argv[0]);
9712-
printf(" [--show-coverage] [--test-file <path>]\n");
9792+
printf(" [--show-coverage] [--test-file <path>] [-j <n>]\n");
97139793
printf(" valid modes:\n");
97149794
printf(" - test (default, compare with CPU backend for correctness)\n");
97159795
printf(" - grad (compare gradients from backpropagation with method of finite differences)\n");
@@ -9721,6 +9801,7 @@ static void usage(char ** argv) {
97219801
printf(" --list-ops lists all available GGML operations\n");
97229802
printf(" --show-coverage shows test coverage\n");
97239803
printf(" --test-file reads test operators from a test file generated by llama-export-graph-ops\n");
9804+
printf(" -j <n> runs tests using <n> parallel worker threads (default: 1, test mode only)\n");
97249805
}
97259806

97269807
int main(int argc, char ** argv) {
@@ -9730,6 +9811,7 @@ int main(int argc, char ** argv) {
97309811
const char * backend_filter = nullptr;
97319812
const char * params_filter = nullptr;
97329813
const char * test_file_path = nullptr;
9814+
int parallel_workers = 1;
97339815

97349816
for (int i = 1; i < argc; i++) {
97359817
if (strcmp(argv[i], "test") == 0) {
@@ -9784,6 +9866,17 @@ int main(int argc, char ** argv) {
97849866
usage(argv);
97859867
return 1;
97869868
}
9869+
} else if (strcmp(argv[i], "-j") == 0) {
9870+
if (i + 1 < argc) {
9871+
parallel_workers = atoi(argv[++i]);
9872+
if (parallel_workers < 1) {
9873+
usage(argv);
9874+
return 1;
9875+
}
9876+
} else {
9877+
usage(argv);
9878+
return 1;
9879+
}
97879880
} else {
97889881
usage(argv);
97899882
return 1;
@@ -9836,7 +9929,7 @@ int main(int argc, char ** argv) {
98369929
false, "", ggml_backend_dev_description(dev),
98379930
total / 1024 / 1024, free / 1024 / 1024, true));
98389931

9839-
bool ok = test_backend(backend, mode, op_names_filter, params_filter, output_printer.get(), test_file_path);
9932+
bool ok = test_backend(backend, dev, mode, op_names_filter, params_filter, output_printer.get(), test_file_path, parallel_workers);
98409933

98419934
if (ok) {
98429935
n_ok++;

0 commit comments

Comments
 (0)