2121#include < ggml-cpp.h>
2222
2323#include < algorithm>
24+ #include < atomic>
2425#include < array>
2526#include < cfloat>
2627#include < cinttypes>
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
520532struct 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+
11131128struct 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
97109790static 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
97269807int 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