Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions cime_config/config_component.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,21 @@
<desc>pes or cores used relative to MAX_MPITASKS_PER_NODE for accounting (0 means TOTALPES is valid)</desc>
</entry>

<entry id="BFBFLAG">
<type>logical</type>
<valid_values>TRUE,FALSE</valid_values>
<default_value>FALSE</default_value>
<group>run_flags</group>
<file>env_run.xml</file>
<desc>
If TRUE, perform some calculations (in CMEPS and elsewhere) such as global sums
using a less efficient algorithm that is independent of processor count. This allows
for Bit-For-Bit (BFB) reproducibility when changing PE layouts, for many compsets.
However, setting this flag does NOT guarantee that all calculations throughout all
model configurations are BFB independent of processor count.
</desc>
</entry>

<!-- ===================================================================== -->
<!-- definitions grid -->
<!-- ===================================================================== -->
Expand Down Expand Up @@ -2515,21 +2530,6 @@
<desc>add aoflux calculation to runseq</desc>
</entry>

<!-- ===================================================================== -->
<!-- TODO: the following should be deleted. They are still used by -->
<!-- the component testing but are not doing anything in cmeps -->
<!-- ===================================================================== -->

<entry id="BFBFLAG">
<type>logical</type>
<valid_values>TRUE,FALSE</valid_values>
<default_value>FALSE</default_value>
<group>run_flags</group>
<file>env_run.xml</file>
<desc>turns on coupler bit-for-bit reproducibility with varying pe counts</desc>
</entry>


<help>
=========================================
Notes:
Expand Down
20 changes: 20 additions & 0 deletions cime_config/namelist_definition_drv.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2676,6 +2676,26 @@
</values>
</entry>

<!-- Note: the reprosum_* attributes above are DRIVER_attributes, since they configure
shr_reprosum_mod itself, which is done in the driver. bfbflag is in
ALLCOMP_attributes because it is read by multiple components that perform global
sums. -->
<entry id="bfbflag" modify_via_xml="BFBFLAG">
<type>logical</type>
<category>reprosum</category>
<group>ALLCOMP_attributes</group>
<desc>
If true, perform some calculations (in CMEPS and elsewhere) such as global sums
using a less efficient algorithm that is independent of processor count. This allows
for Bit-For-Bit (BFB) reproducibility when changing PE layouts, for many compsets.
However, setting this flag does NOT guarantee that all calculations throughout all
model configurations are BFB independent of processor count.
</desc>
<values>
<value>$BFBFLAG</value>
</values>
</entry>

<!-- =========================== -->
<!-- CLOCK Attributes -->
<!-- =========================== -->
Expand Down
1 change: 1 addition & 0 deletions mediator/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ set(SRCFILES esmFldsExchange_cesm_mod.F90 med_fraction_mod.F90
esmFldsExchange_ufs_mod.F90 med_io_mod.F90
med_phases_history_mod.F90 med_phases_prep_ocn_mod.F90
med_utils_mod.F90 esmFlds.F90 med_kind_mod.F90
med_global_sums_mod.F90
med_phases_prep_rof_mod.F90
med_constants_mod.F90 med_map_mod.F90
med_phases_prep_atm_mod.F90 med_phases_prep_wav_mod.F90
Expand Down
157 changes: 157 additions & 0 deletions mediator/med_global_sums_mod.F90
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
module med_global_sums_mod

!-----------------------------------------------------------------------------
! Computation of global sums, optionally in a manner that is independent of processor count.
!
! NOTE: This is deliberately duplicated from dshr_global_sums in
! cdeps/dshr/dshr_global_sums_mod.F90. The duplication is accepted because neither CMEPS nor
! CDEPS is available in all of the builds that need this functionality, and putting it in the
! CESM share code would require a separate, nearly complete reimplementation for builds that
! do not have access to that code. If you change this module, make the corresponding change
! in CDEPS.
!-----------------------------------------------------------------------------

use med_kind_mod, only : r8=>SHR_KIND_R8, cs=>SHR_KIND_CS
use ESMF , only : ESMF_GridComp, ESMF_GridCompGet, ESMF_VM, ESMF_VMGet
use ESMF , only : ESMF_VMAllreduce, ESMF_REDUCE_SUM, ESMF_SUCCESS
use NUOPC , only : NUOPC_CompAttributeGet
use shr_log_mod, only : shr_log_error
use med_utils_mod, only : chkerr => med_utils_ChkErr
#ifdef CESMCOUPLED
use shr_reprosum_mod, only : shr_reprosum_calc
#endif

implicit none
private

public :: med_global_sums

character(*), parameter :: u_FILE_u = &
__FILE__

! med_global_sums is generic over the number of fields being summed: the 1fld version takes
! summands for a single field and returns a scalar sum; the nflds version takes summands for
! an arbitrary number of fields and returns an array of sums.
interface med_global_sums
module procedure med_global_sums_1fld
module procedure med_global_sums_nflds
end interface med_global_sums

!===============================================================================
contains
!===============================================================================

subroutine med_global_sums_nflds(gcomp, local_summands, global_sums, rc)

! Compute global sums of the given local summands.
!
! local_summands has dimensions [number of local grid cells, number of fields];
! global_sums has dimensions [number of fields].
!
! If the bfbflag attribute is true, the sums are computed in a manner that is
! independent of processor count; otherwise a cheaper, processor-count-dependent
! algorithm is used. Note that, for the sums to truly be independent of processor
! count, the caller needs to provide the individual summands here rather than
! pre-summing them locally.

! input/output variables
type(ESMF_GridComp), intent(in) :: gcomp
real(r8), intent(in) :: local_summands(:,:)
real(r8), intent(out) :: global_sums(:)
integer , intent(out) :: rc

! local variables
type(ESMF_VM) :: vm
integer :: nsummands ! number of local grid cells
integer :: nflds ! number of fields
integer :: n, nf
#ifdef CESMCOUPLED
integer :: mpicom ! MPI communicator of this component
#endif
character(len=CS) :: cvalue
logical :: isPresent, isSet
logical :: bfbflag ! value of the bfbflag attribute
real(r8) :: local_sums(size(local_summands, 2)) ! local sums over grid cells, for each field

character(len=*), parameter :: subname = '(med_global_sums_nflds)'
!---------------------------------------------------------------

rc = ESMF_SUCCESS

call NUOPC_CompAttributeGet(gcomp, name='bfbflag', value=cvalue, &
isPresent=isPresent, isSet=isSet, rc=rc)
if (chkerr(rc,__LINE__,u_FILE_u)) return
bfbflag = .false.
if (isPresent .and. isSet) then
read(cvalue,*) bfbflag
end if

nsummands = size(local_summands, 1)
nflds = size(local_summands, 2)

if (size(global_sums) /= nflds) then
call shr_log_error(subname//' ERROR: size of global_sums must agree with the '// &
'second dimension of local_summands', line=__LINE__, file=u_FILE_u, rc=rc)
return
end if

call ESMF_GridCompGet(gcomp, vm=vm, rc=rc)
if (chkerr(rc,__LINE__,u_FILE_u)) return

if (bfbflag) then
#ifdef CESMCOUPLED
call ESMF_VMGet(vm, mpiCommunicator=mpicom, rc=rc)
if (chkerr(rc,__LINE__,u_FILE_u)) return
call shr_reprosum_calc(arr=local_summands, arr_gsum=global_sums, &
nsummands=nsummands, dsummands=nsummands, nflds=nflds, commid=mpicom)
#else
call shr_log_error(subname//' ERROR: bfbflag is true, but this build does not have '// &
'access to the shr_reprosum module needed for bfbflag to be true', &
line=__LINE__, file=u_FILE_u, rc=rc)
return
#endif
else
do nf = 1, nflds
local_sums(nf) = 0.0_r8
do n = 1, nsummands
local_sums(nf) = local_sums(nf) + local_summands(n,nf)
end do
end do
call ESMF_VMAllreduce(vm, senddata=local_sums, recvdata=global_sums, count=nflds, &
reduceflag=ESMF_REDUCE_SUM, rc=rc)
if (chkerr(rc,__LINE__,u_FILE_u)) return
end if

end subroutine med_global_sums_nflds

!===============================================================================

subroutine med_global_sums_1fld(gcomp, local_summands, global_sum, rc)

! Compute the global sum of the given local summands, for a single field.
!
! This is a convenience wrapper around med_global_sums_nflds for the common case of a
! single field; see that routine for details.

! input/output variables
type(ESMF_GridComp), intent(in) :: gcomp
real(r8) , intent(in) :: local_summands(:)
real(r8) , intent(out) :: global_sum
integer , intent(out) :: rc

! local variables
real(r8), allocatable :: summands_2d(:,:)
real(r8) :: global_sums(1)
!---------------------------------------------------------------

rc = ESMF_SUCCESS

allocate(summands_2d(size(local_summands), 1))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I was feeling like a check was needed here, and I figured out what I was thinking is adding a return stat in the allocate call and checking to make sure it was successful. Like:
`allocate(summands_2d(size(local_summands), 1), stat=ierr)

if (ierr /= 0) then
errstring='Error allocating summands_2d'`

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize that feelings differ across CSEG on this point (with CAM apparently preferring a stat check). I personally prefer to not have an explicit check of the stat of an allocate: if you don't query the stat, then the code will abort if there's an error in allocation; if you do query the stat, then it's up to you to handle the error. This leaves open the possibility that you'll make an error in the error handling itself, accidentally letting an allocation error slip through without aborting. I think I've seen this kind of bug in the error handling of an allocate in a recent code review. I realize that catching the error can allow for a more informative error message, but my feeling is that it's not worth the extra code and the possibility of inserting a bug in the error-handling code.

I'm open to having my mind changed on this point, but I think this is a bigger discussion that should be taken outside of this PR review: the vast majority of allocates in CMEPS do not check the stat, so I prefer to stay consistent with the pre-existing pattern until we have a discussion where the consensus is that we want to add this pattern.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Bill, no worries. I think the fact that this is in-line with CMEPS standards is good enough. I've had too many reviews where CAM SEs (no need to name names!) would flag all of mine, so I just want to make sure we at least consider the choice.

summands_2d(:,1) = local_summands(:)
call med_global_sums_nflds(gcomp, summands_2d, global_sums, rc)
if (chkerr(rc,__LINE__,u_FILE_u)) return
global_sum = global_sums(1)

end subroutine med_global_sums_1fld

end module med_global_sums_mod
71 changes: 38 additions & 33 deletions mediator/med_phases_post_rof_mod.F90
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ module med_phases_post_rof_mod
use NUOPC , only : NUOPC_CompAttributeGet
use ESMF , only : ESMF_Clock, ESMF_ClockIsCreated
use ESMF , only : ESMF_LogWrite, ESMF_LOGMSG_INFO, ESMF_SUCCESS
use ESMF , only : ESMF_GridComp, ESMF_GridCompGet
use ESMF , only : ESMF_GridComp
use ESMF , only : ESMF_Mesh, ESMF_MESHLOC_ELEMENT, ESMF_TYPEKIND_R8
use ESMF , only : ESMF_Field
use ESMF , only : ESMF_FieldBundle, ESMF_FieldBundleCreate
use ESMF , only : ESMF_FieldBundleGet, ESMF_FieldBundleAdd
use ESMF , only : ESMF_VM, ESMF_VMAllreduce, ESMF_REDUCE_SUM
use med_kind_mod , only : CX=>SHR_KIND_CX, CS=>SHR_KIND_CS, CL=>SHR_KIND_CL, R8=>SHR_KIND_R8
use med_internalstate_mod , only : complnd, compocn, compice, comprof
use med_internalstate_mod , only : InternalState, maintask, logunit
use med_utils_mod , only : chkerr => med_utils_ChkErr
use med_global_sums_mod , only : med_global_sums
use med_constants_mod , only : dbug_flag => med_constants_dbug_flag
use med_phases_history_mod, only : med_phases_history_write_comp
use med_map_mod , only : med_map_field_packed
Expand Down Expand Up @@ -323,15 +323,18 @@ subroutine med_phases_post_rof_remove_negative_runoff(gcomp, field_name, rc)

! local variables
type(InternalState) :: is_local
type(ESMF_VM) :: vm
real(r8), pointer :: runoff_flux(:) ! temporary 1d pointer
real(r8), pointer :: areas(:)
real(r8) :: local_positive(1), global_positive(1)
real(r8) :: local_negative(1), global_negative(1)
! The local_* variables below hold per-grid-cell contributions to the corresponding global
! sums. These are kept per grid cell, rather than being summed up locally here, so that the
! global sums can be computed in a manner that is independent of processor count if bfbflag
! is set.
real(r8), allocatable :: local_positive(:), local_negative(:)
real(r8) :: global_positive, global_negative
real(r8) :: global_sum
real(r8) :: multiplier
real(r8) :: local_positive_final(1), global_positive_final(1)
real(r8) :: local_negative_final(1), global_negative_final(1)
real(r8), allocatable :: local_positive_final(:), local_negative_final(:)
real(r8) :: global_positive_final, global_negative_final
real(r8) :: global_sum_final
integer :: n

Expand All @@ -358,36 +361,36 @@ subroutine med_phases_post_rof_remove_negative_runoff(gcomp, field_name, rc)
call fldbun_getdata1d(FBrof_r, trim(field_name), runoff_flux, rc=rc)
if (ChkErr(rc,__LINE__,u_FILE_u)) return

local_positive(1) = 0.0_r8
local_negative(1) = 0.0_r8
allocate(local_positive(size(runoff_flux)))
allocate(local_negative(size(runoff_flux)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a check for error status on the allocate?

local_positive(:) = 0.0_r8
local_negative(:) = 0.0_r8
do n = 1, size(runoff_flux)
if (runoff_flux(n) >= 0.0_r8) then
local_positive(1) = local_positive(1) + areas(n) * runoff_flux(n)
local_positive(n) = areas(n) * runoff_flux(n)
else
local_negative(1) = local_negative(1) + areas(n) * runoff_flux(n)
local_negative(n) = areas(n) * runoff_flux(n)
end if
end do

call ESMF_GridCompGet(gcomp, vm=vm, rc=rc)
call med_global_sums(gcomp, local_positive, global_positive, rc)
if (ChkErr(rc,__LINE__,u_FILE_u)) return
call ESMF_VMAllreduce(vm, senddata=local_positive, recvdata=global_positive, count=1, &
reduceflag=ESMF_REDUCE_SUM, rc=rc)
call med_global_sums(gcomp, local_negative, global_negative, rc)
if (ChkErr(rc,__LINE__,u_FILE_u)) return
call ESMF_VMAllreduce(vm, senddata=local_negative, recvdata=global_negative, count=1, &
reduceflag=ESMF_REDUCE_SUM, rc=rc)
if (ChkErr(rc,__LINE__,u_FILE_u)) return
global_sum = global_positive(1) + global_negative(1)
deallocate(local_positive)
deallocate(local_negative)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a check to make sure it's allocated before deallocating?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a check to make sure it's allocated before deallocating?

This is another conversation that I think should be taken outside this PR review if you think it's important: From my quick look, it looks like we rarely check the allocated status of arrays before deallocating in CMEPS, and it seems unnecessary here because they are allocated unconditionally. (I have a feeling that the places that do check the allocation status are handling situations where the array may truly be unallocated at that point - for example, I see this in a "clean" routine that I think I may have written, which is handling the possibility that "clean" is called - e.g., from a unit test - despite the array not having been allocated.) So this would be a broader discussion on the preferred pattern in general.

Actually, I think it would be safe to completely remove these deallocate statements, because since Fortran 2003 allocatables are automatically deallocated when they go out of scope. I thought about doing that, and would personally prefer removing them completely over adding a conditional check around the deallocate.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, TIL. Well, most of my Fortran has this check in it, both CISM and CAM. Mostly in case the code changes in a way that could result in the array being unallocated when the deallocate is called. I agree it's not super necessary here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, TIL. Well, most of my Fortran has this check in it, both CISM and CAM. Mostly in case the code changes in a way that could result in the array being unallocated when the deallocate is called. I agree it's not super necessary here.

Fair enough. I see that rationale. Personally, I think I'd look at that and say, "why is this only conditionally deallocated? what is the situation where it wouldn't have been allocated?" But I certainly see your perspective on this, too, and it's a good one.

global_sum = global_positive + global_negative
if (maintask .and. dbug_flag > dbug_threshold) then
write(logunit,'(a)') subname//' Before correction: '//trim(field_name)
write(logunit,'(a,e27.17)') subname//' global_positive = ', global_positive(1)
write(logunit,'(a,e27.17)') subname//' global_negative = ', global_negative(1)
write(logunit,'(a,e27.17)') subname//' global_positive = ', global_positive
write(logunit,'(a,e27.17)') subname//' global_negative = ', global_negative
write(logunit,'(a,e27.17)') subname//' global_sum = ', global_sum
end if

if (global_sum > 0.0_r8) then
! There is enough positive runoff to absorb all of the negative runoff; so set
! negative runoff to 0 and downweight positive runoff to conserve.
multiplier = global_sum/global_positive(1)
multiplier = global_sum/global_positive
do n = 1, size(runoff_flux)
if (runoff_flux(n) > 0.0_r8) then
runoff_flux(n) = runoff_flux(n) * multiplier
Expand All @@ -399,7 +402,7 @@ subroutine med_phases_post_rof_remove_negative_runoff(gcomp, field_name, rc)
! There is more negative than positive runoff. Hopefully this happens rarely, if
! ever; so set positive runoff to 0 and downweight negative runoff to minimize
! negative runoff and conserve.
multiplier = global_sum/global_negative(1)
multiplier = global_sum/global_negative
do n = 1, size(runoff_flux)
if (runoff_flux(n) < 0.0_r8) then
runoff_flux(n) = runoff_flux(n) * multiplier
Expand All @@ -417,26 +420,28 @@ subroutine med_phases_post_rof_remove_negative_runoff(gcomp, field_name, rc)

if (dbug_flag > dbug_threshold) then
! Recompute positives, negatives and total sum for output diagnostic purposes
local_positive_final(1) = 0.0_r8
local_negative_final(1) = 0.0_r8
allocate(local_positive_final(size(runoff_flux)))
allocate(local_negative_final(size(runoff_flux)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safety checks on these allocates?

local_positive_final(:) = 0.0_r8
local_negative_final(:) = 0.0_r8
do n = 1, size(runoff_flux)
if (runoff_flux(n) >= 0.0_r8) then
local_positive_final(1) = local_positive_final(1) + areas(n) * runoff_flux(n)
local_positive_final(n) = areas(n) * runoff_flux(n)
else
local_negative_final(1) = local_negative_final(1) + areas(n) * runoff_flux(n)
local_negative_final(n) = areas(n) * runoff_flux(n)
end if
end do
call ESMF_VMAllreduce(vm, senddata=local_positive_final, recvdata=global_positive_final, count=1, &
reduceflag=ESMF_REDUCE_SUM, rc=rc)
call med_global_sums(gcomp, local_positive_final, global_positive_final, rc)
if (ChkErr(rc,__LINE__,u_FILE_u)) return
call ESMF_VMAllreduce(vm, senddata=local_negative_final, recvdata=global_negative_final, count=1, &
reduceflag=ESMF_REDUCE_SUM, rc=rc)
call med_global_sums(gcomp, local_negative_final, global_negative_final, rc)
if (ChkErr(rc,__LINE__,u_FILE_u)) return
global_sum_final = global_positive_final(1) + global_negative_final(1)
deallocate(local_positive_final)
deallocate(local_negative_final)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And checks here as well

global_sum_final = global_positive_final + global_negative_final
if (maintask) then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, outside the scope of these changes, but the writes above included "dbug_flag > dbug_threshold" in this check and I think that might be good here to reduce log output.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole block of code is already inside a conditional on dbug_flag > dbug_threshold.

write(logunit,'(a)') subname//' After correction: '//trim(field_name)
write(logunit,'(a,e27.17)') subname//' global_positive_final = ', global_positive_final(1)
write(logunit,'(a,e27.17)') subname//' global_negative_final = ', global_negative_final(1)
write(logunit,'(a,e27.17)') subname//' global_positive_final = ', global_positive_final
write(logunit,'(a,e27.17)') subname//' global_negative_final = ', global_negative_final
write(logunit,'(a,e27.17)') subname//' global_sum_final = ', global_sum_final
end if
end if
Expand Down
Loading
Loading