Neko 1.99.7
A portable framework for high-order spectral element flow simulations
Loading...
Searching...
No Matches
gs_tune.f90
Go to the documentation of this file.
1! Copyright (c) 2026, The Neko Authors
2! All rights reserved.
3!
4! Redistribution and use in source and binary forms, with or without
5! modification, are permitted provided that the following conditions
6! are met:
7!
8! * Redistributions of source code must retain the above copyright
9! notice, this list of conditions and the following disclaimer.
10!
11! * Redistributions in binary form must reproduce the above
12! copyright notice, this list of conditions and the following
13! disclaimer in the documentation and/or other materials provided
14! with the distribution.
15!
16! * Neither the name of the authors nor the names of its
17! contributors may be used to endorse or promote products derived
18! from this software without specific prior written permission.
19!
20! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21! "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22! LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23! FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24! COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25! INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26! BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27! LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29! LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30! ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31! POSSIBILITY OF SUCH DAMAGE.
32!
34submodule(gather_scatter) gs_tune
35 use gs_shmem, only : gs_shmem_avail
39 use gs_utofu, only : gs_utofu_avail
40 implicit none
41
45 integer, parameter :: GS_TUNE_BCKND(5) = [gs_comm_mpi, gs_comm_neighbour, &
46 gs_comm_openshmem, gs_comm_caf, gs_comm_utofu]
47
51 logical, parameter :: GS_TUNE_DEFAULT(5) = [.true., .true., .true., &
52 .false., .true.]
53
59 logical, save :: caf_signal_tuned = .false.
60
61contains
62
72 function gs_comm_host_cand() result(cand)
73 integer, allocatable :: cand(:)
74 character(len=LOG_SIZE) :: log_buf
75 character(len=13) :: label
76 logical :: sel(size(GS_TUNE_BCKND)), named(size(GS_TUNE_BCKND))
77 integer :: c(size(GS_TUNE_BCKND)), i, n
78
79 call gs_tune_select(sel, named)
80
81 n = 0
82 do i = 1, size(gs_tune_bcknd)
83 if (.not. sel(i)) cycle
84 if (gs_comm_tunable(gs_tune_bcknd(i))) then
85 n = n + 1
86 c(n) = gs_tune_bcknd(i)
87 else if (named(i)) then
88 ! Asked for by name, but this build or run cannot drive it: say so
89 ! rather than leaving the candidate quietly missing from the
90 ! comparison below
91 label = adjustl(gs_comm_name(gs_tune_bcknd(i)))
92 write(log_buf, '(A,A,A12)') label, ': ', 'unavailable'
93 call neko_log%message(log_buf)
94 end if
95 end do
96
97 allocate(cand(n))
98 cand = c(1:n)
99
100 end function gs_comm_host_cand
101
109 function gs_comm_tunable(comm_bcknd) result(tunable)
110 integer, intent(in) :: comm_bcknd
111 logical :: tunable
112
113 select case (comm_bcknd)
114 case (gs_comm_mpi, gs_comm_neighbour)
115 tunable = .true.
116 case (gs_comm_openshmem)
117 ! The one-sided backends that address their peers by global PE or
118 ! image number (OpenSHMEM PEs, coarray images) are only correct when
119 ! NEKO_COMM spans every process, so skip them when the run has been
120 ! split into several communicators (NEKO_COMM_ID). uTofu exchanges
121 ! its addresses over NEKO_COMM itself and is unaffected.
122 tunable = gs_shmem_avail .and. (pe_size .eq. global_pe_size)
123 case (gs_comm_caf)
124 ! Coarray support at configure time says nothing about the job
125 ! actually running more than one image, see gs_caf_usable
126 tunable = gs_caf_avail .and. gs_caf_usable() .and. &
127 (pe_size .eq. global_pe_size)
128 case (gs_comm_utofu)
129 tunable = gs_utofu_avail
130 case default
131 tunable = .false.
132 end select
133
134 end function gs_comm_tunable
135
157 subroutine gs_tune_select(sel, named)
158 logical, intent(out) :: sel(:), named(:)
159 character(len=255) :: env_val
160 character(len=32) :: tok, name
161 character(len=1) :: op
162 integer :: env_len, i, j, k
163 logical :: delta, first
164
165 sel = gs_tune_default
166 named = .false.
167
168 call get_environment_variable("NEKO_GS_TUNE", env_val, env_len)
169 if (env_len .eq. 0) return
170 ! env_len is the length of the value, which may well exceed the buffer
171 env_len = min(env_len, len(env_val))
172
173 delta = .false.
174 first = .true.
175 i = 1
176 do while (i .le. env_len)
177 if (scan(env_val(i:i), ', ') .ne. 0) then
178 i = i + 1
179 cycle
180 end if
181
182 j = i
183 do while (j .le. env_len)
184 if (scan(env_val(j:j), ', ') .ne. 0) exit
185 j = j + 1
186 end do
187 tok = gs_tune_upcase(env_val(i:j-1))
188 i = j
189
190 op = ' '
191 name = tok
192 if (tok(1:1) .eq. '+' .or. tok(1:1) .eq. '-') then
193 op = tok(1:1)
194 name = tok(2:)
195 end if
196
197 if (first) then
198 ! The first name decides how the list is read; a plain one starts
199 ! the set from nothing rather than from the default
200 delta = (op .ne. ' ')
201 if (.not. delta) sel = .false.
202 first = .false.
203 else if ((op .ne. ' ') .neqv. delta) then
204 call neko_error('NEKO_GS_TUNE: plain backend names and +/- ' // &
205 'prefixed ones cannot be mixed')
206 end if
207
208 k = gs_tune_index(name)
209 if (k .eq. 0) then
210 call neko_error('NEKO_GS_TUNE: not a tunable Gather-scatter ' // &
211 'comm. backend: ' // trim(name))
212 end if
213 sel(k) = (op .ne. '-')
214 named(k) = .true.
215 end do
216
217 end subroutine gs_tune_select
218
225 function gs_tune_index(name) result(idx)
226 character(len=*), intent(in) :: name
227 integer :: idx, bcknd, i
228
229 select case (trim(name))
230 case ('MPI')
231 bcknd = gs_comm_mpi
232 case ('NEIGHBOUR', 'NEIGHBOR')
233 bcknd = gs_comm_neighbour
234 case ('SHMEM', 'OPENSHMEM')
235 bcknd = gs_comm_openshmem
236 case ('CAF')
237 bcknd = gs_comm_caf
238 case ('UTOFU')
239 bcknd = gs_comm_utofu
240 case default
241 bcknd = 0
242 end select
243
244 idx = 0
245 do i = 1, size(gs_tune_bcknd)
246 if (gs_tune_bcknd(i) .eq. bcknd) idx = i
247 end do
248
249 end function gs_tune_index
250
255 function gs_tune_upcase(str) result(upper)
256 character(len=*), intent(in) :: str
257 character(len=len(str)) :: upper
258 integer :: i, c
259
260 do i = 1, len(str)
261 c = iachar(str(i:i))
262 if (c .ge. iachar('a') .and. c .le. iachar('z')) then
263 upper(i:i) = achar(c - (iachar('a') - iachar('A')))
264 else
265 upper(i:i) = str(i:i)
266 end if
267 end do
268
269 end function gs_tune_upcase
270
280 subroutine gs_comm_switch(gs, comm_bcknd)
281 type(gs_t), intent(inout) :: gs
282 integer, intent(in) :: comm_bcknd
283 class(gs_comm_t), allocatable :: comm_new
284
285 call gs_comm_alloc(comm_new, comm_bcknd)
286 call comm_new%take_schedule(gs%comm)
287 call gs%comm%free()
288 deallocate(gs%comm)
289 call move_alloc(comm_new, gs%comm)
290 call gs%comm%init_schedule()
291
292 end subroutine gs_comm_switch
293
303 module function gs_time_ops(gs, u, n, op, ntrials) result(t)
304 type(gs_t), intent(inout) :: gs
305 integer, intent(in) :: n
306 real(kind=rp), dimension(n), intent(inout) :: u
307 integer, intent(in) :: op, ntrials
308 real(kind=dp) :: t
309 integer :: i
310
311 do i = 1, gs_tune_nwarmup
312 call gs_op_vector(gs, u, n, op)
313 end do
314
315 if (neko_bcknd_device .eq. 1) call device_sync
316 call mpi_barrier(neko_comm)
317
318 t = mpi_wtime()
319 do i = 1, ntrials
320 call gs_op_vector(gs, u, n, op)
321 end do
322 if (neko_bcknd_device .eq. 1) call device_sync
323 t = (mpi_wtime() - t) / real(ntrials, dp)
324
325 end function gs_time_ops
326
348 module subroutine gs_tune_comm(gs, n, comm_bcknd)
349 type(gs_t), intent(inout) :: gs
350 integer, intent(in) :: n
351 integer, intent(in) :: comm_bcknd
352 character(len=LOG_SIZE) :: log_buf
353 character(len=13) :: label
354 integer, allocatable :: cand(:)
355 real(kind=dp), allocatable :: cand_time(:)
356 real(kind=rp), allocatable :: tmp(:)
357 type(c_ptr) :: tmp_d
358 integer :: i, best, nmin, cur
359
360 allocate(cand, source = gs_comm_host_cand())
361
362 ! Track what gs is running as the sweep proceeds. The candidate list
363 ! need neither start with the backend the schedule was built with nor
364 ! contain it at all, and switching is what costs, so every candidate is
365 ! switched to exactly once
366 cur = comm_bcknd
367
368 ! A rank without any dofs skips the halo exchange in gs_op_vector, which
369 ! would leave every other rank hanging in a collective based backend.
370 ! Keep the current backend in that case. NEKO_GS_TUNE may also have left
371 ! us with nothing to compare, in which case a lone candidate is simply
372 ! switched to, as if it had been requested with NEKO_GS_COMM.
373 nmin = n
374 call mpi_allreduce(mpi_in_place, nmin, 1, mpi_integer, mpi_min, neko_comm)
375 if (nmin .eq. 0 .or. size(cand) .lt. 2) then
376 call neko_log%message('Comm tuning : skipped')
377 if (nmin .gt. 0 .and. size(cand) .eq. 1) then
378 if (cand(1) .ne. cur) call gs_comm_switch(gs, cand(1))
379 cur = cand(1)
380 end if
381 call neko_log%message('Tuned comm : ' // gs_comm_name(cur))
382 return
383 end if
384
385 allocate(cand_time(size(cand)))
386
387 tmp_d = c_null_ptr
388 allocate(tmp(n))
389 tmp = 1.0_rp
390 if (neko_bcknd_device .eq. 1) then
391 call device_map(tmp, tmp_d, n)
392 call device_memcpy(tmp, tmp_d, n, host_to_device, sync = .false.)
393 end if
394
395 ! GS_OP_MIN leaves the working vector untouched, so the benchmark can
396 ! run for any number of trials without the values growing out of range
397 do i = 1, size(cand)
398 if (cand(i) .eq. gs_comm_caf .and. gs_caf_signal_auto() .and. &
399 .not. caf_signal_tuned) then
400 ! Benchmark the signaling modes as well; this leaves the CAF
401 ! backend allocated in the winning mode
402 cand_time(i) = gs_tune_caf_signal(gs, tmp, n)
403 caf_signal_tuned = .true.
404 else
405 if (cand(i) .ne. cur) call gs_comm_switch(gs, cand(i))
406 cand_time(i) = gs_time_ops(gs, tmp, n, gs_op_min, gs_tune_ntrials)
407 end if
408 cur = cand(i)
409 end do
410
411 if (neko_bcknd_device .eq. 1) call device_unmap(tmp, tmp_d)
412 deallocate(tmp)
413
414 call mpi_allreduce(mpi_in_place, cand_time, size(cand), &
415 mpi_double_precision, mpi_sum, neko_comm)
416 cand_time = cand_time / pe_size
417
418 best = minloc(cand_time, 1)
419
420 do i = 1, size(cand)
421 label = adjustl(gs_comm_name(cand(i)))
422 ! What the coarray backend costs depends on the signaling mode in
423 ! force, so report the timing under the mode it was measured in
424 if (cand(i) .eq. gs_comm_caf .and. gs_caf_mode_get() .ne. 0) then
425 label = 'CAF (' // &
426 trim(adjustl(gs_caf_mode_name(gs_caf_mode_get()))) // ')'
427 end if
428 ! ES10.3 + ' s' fills the same 12 columns as the right-adjusted
429 ! backend names, so the unit lines up with their last letter
430 write(log_buf, '(A,A,ES10.3,A)') label, ': ', cand_time(i), ' s'
431 call neko_log%message(log_buf)
432 end do
433
434 if (cand(best) .ne. cur) call gs_comm_switch(gs, cand(best))
435
436 call neko_log%message('Tuned comm : ' // gs_comm_name(cand(best)))
437
438 deallocate(cand_time)
439
440 end subroutine gs_tune_comm
441
459 function gs_tune_caf_signal(gs, u, n) result(t)
460 type(gs_t), intent(inout) :: gs
461 integer, intent(in) :: n
462 real(kind=rp), dimension(n), intent(inout) :: u
463 real(kind=dp) :: t
464 character(len=LOG_SIZE) :: log_buf
465 character(len=13) :: label
466 integer, allocatable :: mode(:)
467 real(kind=dp), allocatable :: mode_time(:)
468 integer :: i, best
469
470 allocate(mode, source = gs_caf_signal_modes())
471 allocate(mode_time(size(mode)))
472
473 do i = 1, size(mode)
474 ! Bind the mode before building the backend: gs_caf_init sets up its
475 ! per-instance signaling state according to the mode in force
476 call gs_caf_set_mode(mode(i))
477 call gs_comm_switch(gs, gs_comm_caf)
478 mode_time(i) = gs_time_ops(gs, u, n, gs_op_min, gs_tune_ntrials)
479 end do
480
481 call mpi_allreduce(mpi_in_place, mode_time, size(mode), &
482 mpi_double_precision, mpi_sum, neko_comm)
483 mode_time = mode_time / pe_size
484
485 best = minloc(mode_time, 1)
486
487 do i = 1, size(mode)
488 label = 'CAF ' // adjustl(gs_caf_mode_name(mode(i)))
489 write(log_buf, '(A,A,ES10.3,A)') label, ': ', mode_time(i), ' s'
490 call neko_log%message(log_buf)
491 end do
492
493 ! The last mode is the one currently allocated
494 if (best .ne. size(mode)) then
495 call gs_caf_set_mode(mode(best))
496 call gs_comm_switch(gs, gs_comm_caf)
497 end if
498
499 call neko_log%message('Tuned CAF sig: ' // gs_caf_mode_name(mode(best)))
500
501 t = mode_time(best)
502
503 deallocate(mode, mode_time)
504
505 end function gs_tune_caf_signal
506
507end submodule gs_tune
double real
Gather-scatter.
Defines Coarray Fortran gather-scatter communication.
Definition gs_caf.F90:34
integer function, public gs_caf_mode_get()
The signaling mode currently in force, or 0 if none has been bound yet (no gs_caf_t has been initiali...
Definition gs_caf.F90:270
integer function, dimension(:), allocatable, public gs_caf_signal_modes()
The signaling modes this build can run, in the order they should be benchmarked. Events are only avai...
Definition gs_caf.F90:250
logical function, public gs_caf_usable()
Whether the coarray backend can actually run in this job. GS_CAF_AVAIL only says that the compiler ac...
Definition gs_caf.F90:219
subroutine, public gs_caf_set_mode(mode)
Bind the signaling mode shared by every gs_caf_t instance, allocating whatever module-level state the...
Definition gs_caf.F90:309
character(len=12) function, public gs_caf_mode_name(mode)
Name of the signaling mode mode, right-adjusted for the log.
Definition gs_caf.F90:283
logical, parameter, public gs_caf_avail
Whether coarray support was built into this Neko. Lets callers (e.g. the gs comm. autotuner) skip the...
Definition gs_caf.F90:57
logical function, public gs_caf_signal_auto()
Whether the signaling mode should be selected by benchmarking, i.e. NEKO_GS_CAF_SIGNALING=auto....
Definition gs_caf.F90:235
Defines OpenSHMEM gather-scatter communication.
Definition gs_shmem.F90:34
logical, parameter, public gs_shmem_avail
Whether a native OpenSHMEM library was built into this Neko (–with-openshmem). Lets callers (e....
Definition gs_shmem.F90:68
Defines a gather-scatter backend using the native Tofu interconnect (uTofu). Each rank registers its ...
Definition gs_utofu.F90:43
logical, parameter, public gs_utofu_avail
Whether uTofu support was built into this Neko (–with-utofu). Lets callers (e.g. the gs comm....
Definition gs_utofu.F90:65