diff --git a/common_thread.h b/common_thread.h index e9b76a8805..cbe7e133d8 100644 --- a/common_thread.h +++ b/common_thread.h @@ -143,17 +143,41 @@ extern int blas_is_num_threads_set_explicitly; static __inline int num_cpu_avail(int level) { #ifdef USE_OPENMP - /* If the user explicitly called openblas_set_num_threads(), - respect that setting instead of overriding it with - `omp_get_max_threads()` below (which is to get a default - in case the user hasn't made an explicit choice). */ - if (blas_is_num_threads_set_explicitly) { - return blas_cpu_number; + int in_parallel = omp_in_parallel(); + int openmp_nthreads; + + /* + * An active outer OpenMP region takes precedence and uses the local + * setting, which defaults to one. Outside a region, an explicit + * openblas_set_num_threads() setting takes precedence over the OpenMP + * runtime default. Without either, keep following omp_get_max_threads(). + */ + if (in_parallel) + openmp_nthreads = blas_omp_threads_local; + else if (blas_is_num_threads_set_explicitly) + openmp_nthreads = blas_cpu_number; + else + openmp_nthreads = omp_get_max_threads(); + + if (openmp_nthreads < 1) + openmp_nthreads = 1; + if (openmp_nthreads > blas_omp_number_max && blas_omp_number_max > 0) { +#ifdef DEBUG + fprintf(stderr, + "WARNING - more OpenMP threads requested (%d) than available (%d)\n", + openmp_nthreads, blas_omp_number_max); +#endif + openmp_nthreads = blas_omp_number_max; } - -int openmp_nthreads; - openmp_nthreads=omp_get_max_threads(); - if (omp_in_parallel()) openmp_nthreads = blas_omp_threads_local; + if (openmp_nthreads > MAX_CPU_NUMBER) + openmp_nthreads = MAX_CPU_NUMBER; + + /* + * The nested count is per-call policy. Do not make a serialized nested + * call overwrite the durable global setting. + */ + if (in_parallel) + return openmp_nthreads; #endif #ifndef USE_OPENMP @@ -164,18 +188,14 @@ int openmp_nthreads; ) return 1; #ifdef USE_OPENMP - if (openmp_nthreads > blas_omp_number_max){ -#ifdef DEBUG - fprintf(stderr,"WARNING - more OpenMP threads requested (%d) than available (%d)\n",openmp_nthreads,blas_omp_number_max); -#endif - openmp_nthreads = blas_omp_number_max; - } - if (blas_cpu_number != openmp_nthreads) { - goto_set_num_threads(openmp_nthreads); + if (!blas_is_num_threads_set_explicitly && + blas_cpu_number != openmp_nthreads) { + goto_set_num_threads(openmp_nthreads); } -#endif - + return openmp_nthreads; +#else return blas_cpu_number; +#endif } diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 1551cefda0..3b3bfc7dbe 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -144,6 +144,24 @@ add_executable(${OpenBLAS_utest_ext_bin} ${OpenBLAS_utest_ext_src}) target_link_libraries(${OpenBLAS_utest_bin} ${OpenBLAS_LIBNAME}) target_link_libraries(${OpenBLAS_utest_ext_bin} ${OpenBLAS_LIBNAME}) +if (USE_OPENMP AND NOT NO_CBLAS) +set(OpenBLAS_utest_openmp_bin openblas_utest_openmp) +add_executable(${OpenBLAS_utest_openmp_bin} test_openmp_thread_selection.c) +target_link_libraries(${OpenBLAS_utest_openmp_bin} + ${OpenBLAS_LIBNAME} + OpenMP::OpenMP_C + ) +set_target_properties(${OpenBLAS_utest_openmp_bin} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) +add_test(${OpenBLAS_utest_openmp_bin} + ${CMAKE_CURRENT_BINARY_DIR}/${OpenBLAS_utest_openmp_bin} + ) +set_tests_properties(${OpenBLAS_utest_openmp_bin} PROPERTIES + ENVIRONMENT "OMP_NUM_THREADS=2" + ) +endif() + if(${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD" OR ${CMAKE_SYSTEM_NAME} MATCHES "QNX" ) target_link_libraries(${OpenBLAS_utest_bin} m) endif() diff --git a/utest/Makefile b/utest/Makefile index 07de0c2fc0..c83e0a997f 100644 --- a/utest/Makefile +++ b/utest/Makefile @@ -7,12 +7,19 @@ override TARGET_MACH= UTESTBIN=openblas_utest$(EXE) UTESTEXTBIN=openblas_utest_ext$(EXE) +UTESTOPENMPBIN=openblas_utest_openmp$(EXE) .PHONY : all -.NOTPARALLEL : all run_test $(UTESTBIN) $(UTESTEXTBIN) +.NOTPARALLEL : all run_test $(UTESTBIN) $(UTESTEXTBIN) $(UTESTOPENMPBIN) include $(TOPDIR)/Makefile.system +ifeq ($(USE_OPENMP), 1) +ifneq ($(NO_CBLAS), 1) +OPENMP_TEST_TARGET=$(UTESTOPENMPBIN) +endif +endif + OBJS=utest_main.o test_min.o test_amax.o test_ismin.o test_rotmg.o test_axpy.o test_dotu.o test_dsdot.o test_swap.o test_rot.o test_dnrm2.o test_zscal.o \ test_amin.o test_axpby.o test_gemv.o #test_rot.o test_swap.o test_axpy.o test_dotu.o test_dsdot.o test_fork.o @@ -84,14 +91,20 @@ $(UTESTEXTBIN): $(OBJS_EXT) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ ../$(LIBNAME) $(EXTRALIB) $(FEXTRALIB) endif -run_test: $(UTESTBIN) $(UTESTEXTBIN) +$(UTESTOPENMPBIN): test_openmp_thread_selection.o + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ ../$(LIBNAME) $(EXTRALIB) $(FEXTRALIB) + +run_test: $(UTESTBIN) $(UTESTEXTBIN) $(OPENMP_TEST_TARGET) ifneq ($(CROSS), 1) ./$(UTESTBIN) ./$(UTESTEXTBIN) +ifneq ($(OPENMP_TEST_TARGET),) + OMP_NUM_THREADS=2 ./$(UTESTOPENMPBIN) +endif endif clean: - -rm -f *.o $(UTESTBIN) $(UTESTEXTBIN) + -rm -f *.o $(UTESTBIN) $(UTESTEXTBIN) $(UTESTOPENMPBIN) -rm -f $(DIR_EXT)/*.o libs: diff --git a/utest/test_openmp_thread_selection.c b/utest/test_openmp_thread_selection.c new file mode 100644 index 0000000000..d51b9f3161 --- /dev/null +++ b/utest/test_openmp_thread_selection.c @@ -0,0 +1,189 @@ +/***************************************************************************** +Copyright (c) 2026, The OpenBLAS Project +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. Neither the name of the OpenBLAS project nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE OPENBLAS PROJECT OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*****************************************************************************/ + +#include +#include +#include +#include +#include + +static int callback_calls; +static int callback_numjobs = 1; + +static void test_threads_callback(int sync, openblas_dojob_callback dojob, + int numjobs, size_t jobdata_elsize, + void *jobdata, int dojob_data) +{ + int i; + + (void)sync; + callback_calls++; + callback_numjobs = numjobs; + if (omp_in_parallel()) { + /* + * Use tasks from the existing team. DGEMM jobs exchange packed blocks, + * so they must be allowed to make progress concurrently. + */ + for (i = 0; i < numjobs; i++) { +#pragma omp task firstprivate(i, dojob, jobdata_elsize, jobdata, dojob_data) + dojob(i, (char *)jobdata + (size_t)i * jobdata_elsize, dojob_data); + } +#pragma omp taskwait + } else { +#pragma omp parallel for num_threads(numjobs) schedule(static, 1) \ + firstprivate(dojob, jobdata_elsize, jobdata, dojob_data) + for (i = 0; i < numjobs; i++) + dojob(i, (char *)jobdata + (size_t)i * jobdata_elsize, dojob_data); + } +} + +static int check_dgemm(const char *label, int expected_jobs, blasint n, + const double *a, const double *b, double *c) +{ + size_t i; + size_t elements = (size_t)n * (size_t)n; + int failed = 0; + + memset(c, 0, elements * sizeof(*c)); + callback_calls = 0; + callback_numjobs = 1; + + printf("Running %s\n", label); + fflush(stdout); + + /* + * TN avoids architecture-specific small-matrix kernels that would bypass + * the threaded DGEMM dispatcher at this deliberately small test size. + */ + cblas_dgemm(CblasColMajor, CblasTrans, CblasNoTrans, n, n, n, + 1.0, a, n, b, n, 0.0, c, n); + + if (callback_numjobs != expected_jobs) { + fprintf(stderr, "%s: expected %d logical job(s), observed %d\n", + label, expected_jobs, callback_numjobs); + failed = 1; + } + if (expected_jobs > 1 && callback_calls == 0) { + fprintf(stderr, "%s: threaded callback was not invoked\n", label); + failed = 1; + } + + for (i = 0; i < elements; i++) { + if (c[i] != (double)n) { + fprintf(stderr, + "%s: incorrect DGEMM result at element %zu: %.17g != %d\n", + label, i, c[i], (int)n); + failed = 1; + break; + } + } + + return failed; +} + +int main(void) +{ + const double two_thread_work = + 2.0 * 65536.0 * (double)GEMM_MULTITHREAD_THRESHOLD; + blasint n = 1; + size_t elements; + double *a; + double *b; + double *c; + int failed = 0; + int nested_failed = 0; + int outer_team_size = 0; + size_t i; + + /* + * This must be the first OpenBLAS call. The callback records the logical + * job count and runs the jobs without creating a nested OpenMP team. + */ + openblas_set_threads_callback_function(test_threads_callback); + + omp_set_dynamic(0); + while ((double)n * (double)n * (double)n <= two_thread_work) + n++; + n++; /* Keep the test just beyond the threshold boundary. */ + + elements = (size_t)n * (size_t)n; + a = malloc(elements * sizeof(*a)); + b = malloc(elements * sizeof(*b)); + c = malloc(elements * sizeof(*c)); + if (a == NULL || b == NULL || c == NULL) { + fprintf(stderr, "failed to allocate DGEMM test matrices\n"); + free(c); + free(b); + free(a); + return 1; + } + + for (i = 0; i < elements; i++) + a[i] = b[i] = 1.0; + + openblas_set_num_threads(1); + failed |= check_dgemm("global count 1 outside OpenMP", 1, n, a, b, c); + + openblas_set_num_threads(2); + failed |= check_dgemm("global count 2 outside OpenMP", 2, n, a, b, c); + +#pragma omp parallel num_threads(2) shared(outer_team_size, nested_failed) + { +#pragma omp single + { + outer_team_size = omp_get_num_threads(); + if (outer_team_size == 2) { +#pragma omp task + nested_failed = + check_dgemm("global count 2 inside OpenMP task", 1, n, a, b, c); +#pragma omp taskwait + } + } + } + + if (outer_team_size == 2) + failed |= nested_failed; + else + printf("SKIP: OpenMP runtime formed an outer team of %d thread(s)\n", + outer_team_size); + + failed |= check_dgemm("global count 2 after OpenMP region", 2, n, a, b, c); + + free(c); + free(b); + free(a); + + if (failed == 0) + printf("OpenMP thread selection test passed (DGEMM dimension %d)\n", + (int)n); + return failed; +}