Skip to content

Commit cd42cfd

Browse files
Wire reproducible seed into native build (#196)
1 parent a6b967f commit cd42cfd

3 files changed

Lines changed: 63 additions & 25 deletions

File tree

native/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This is epic #165 phase 1. It extends sccn/amica PR #53's vendor-neutral recipe:
1212
| Intel MKL | `-cpp` skips the `#ifdef MKL` branches | (build flags) |
1313
| AMD LibM (`vrda_exp`/`vrda_log`) | libm exp/log loops | `vmath_shim.c` |
1414
| Open MPI runtime | single-rank MPI shim | `mpi_single.f90` + `mpi_single.c` |
15-
| ifort-only `random_seed` | portable full-size seed array (PR #53) | `patch_sources.py` |
15+
| ifort-only `random_seed` | portable full-size seed array + reproducible `seed` param (PR #54, supersedes #53) | `patch_sources.py` |
1616

1717
LAPACK/BLAS remains, satisfied by Apple's Accelerate framework on macOS (a system
1818
framework, always present) and static reference LAPACK/OpenBLAS on Linux/Windows.

native/build.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env bash
22
# Build a dependency-free native amica15 (epic #165, phase 1).
33
#
4-
# Extends sccn/amica PR #53's vendor-neutral recipe (gfortran, no MKL, portable
5-
# random_seed, vmath_shim for vrda_exp/vrda_log) with a single-rank MPI shim
4+
# Extends sccn/amica PR #54's vendor-neutral recipe (gfortran, no MKL, portable
5+
# + reproducible random_seed, vmath_shim for vrda_exp/vrda_log) with a single-rank MPI shim
66
# (mpi_single.f90 + mpi_single.c), so the binary links NO MPI runtime and is
77
# self-contained. Runtime dependencies after this are only the C/Fortran runtime
88
# and LAPACK/BLAS, which we static-link where possible.

native/patch_sources.py

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,66 @@
11
#!/usr/bin/env python3
2-
"""Patch the build copy of amica15 for a portable gfortran build (epic #165).
2+
"""Patch the build copy of amica15 for a portable, seedable gfortran build.
33
4-
Applies sccn/amica PR #53's portable random_seed fix: the tracked source passes a
5-
fixed size-2 PUT array (only ifort accepts that); gfortran needs an array of its
6-
own SIZE. Idempotent and verified -- if the anchor lines are absent (source
7-
drift), it fails loudly rather than building a subtly wrong binary.
4+
Applies sccn/amica PR #54 (supersedes #53): (1) size the ``random_seed(PUT=...)``
5+
array via ``random_seed(SIZE=...)`` so it builds under gfortran, not only ifort;
6+
(2) wire a ``seed`` ``input.param`` option so a run's random initialization is
7+
reproducible when set, and clock-random otherwise. Idempotent and verified -- if
8+
an anchor is absent (source drift) it fails loudly rather than building a subtly
9+
wrong binary.
810
9-
Only the build copy is patched; the tracked amica15.f90 stays the read-only
10-
parity reference.
11+
Only the build copy is patched; the tracked ``amica15.f90`` stays the read-only
12+
parity reference (== sccn/amica master until PR #54 merges).
1113
"""
1214

1315
import sys
1416

17+
# (1) header: SIZE-based seed array + the `seed` param state.
18+
HDR_ANCHOR = "integer :: ii, jj, kk, c0, c1, c2"
19+
HDR_ADD = (
20+
HDR_ANCHOR
21+
+ "\nINTEGER :: nseed, input_seed = 0"
22+
+ "\nLOGICAL :: use_seed = .false."
23+
+ "\nINTEGER, ALLOCATABLE :: seedvec(:)"
24+
)
25+
26+
# (2) portable + reproducible seeding.
1527
OLD_SEED = "call random_seed(PUT = c1 * (myrank+1) * (seed+myrank+1))"
1628
NEW_SEED = """\
17-
! Portable RNG seeding (sccn/amica PR #53): gfortran's random_seed(PUT=...)
18-
! requires an array of the compiler-defined size (query with SIZE=); the original
19-
! passed a fixed size-2 array, which only compiles with ifort. Fill the full-size
20-
! array from the clock, rank, and fixed seed so per-rank streams still differ.
29+
! Portable + reproducible RNG seeding (sccn/amica PR #54, supersedes #53):
30+
! random_seed(PUT=...) needs an array of the compiler SIZE (query with SIZE=);
31+
! the original size-2 array only compiled under ifort. With `seed` set in the
32+
! param file, seed deterministically (reproducible); otherwise clock-seed.
2133
call random_seed(size = nseed)
2234
allocate(seedvec(nseed))
23-
do jj = 1, nseed
24-
seedvec(jj) = c1 + (jj-1) + (myrank+1)*(seed(mod(jj-1,2)+1) + myrank + 1)
25-
end do
35+
if (use_seed) then
36+
do jj = 1, nseed
37+
seedvec(jj) = input_seed + 1009*myrank + 37*(jj-1)
38+
end do
39+
else
40+
do jj = 1, nseed
41+
seedvec(jj) = c1 + 1009*(myrank+1)*jj + 37*(jj-1)
42+
end do
43+
end if
2644
call random_seed(PUT = seedvec)
2745
deallocate(seedvec)"""
2846

29-
HDR_ANCHOR = "integer :: ii, jj, kk, c0, c1, c2"
30-
HDR_ADD = HDR_ANCHOR + "\nINTEGER :: nseed\nINTEGER, ALLOCATABLE :: seedvec(:)"
47+
# (3) broadcast the new param state to all ranks.
48+
OLD_BCAST = "call MPI_BCAST(rholratefact,1,MPI_DOUBLE_PRECISION,0,seg_comm,ierr)"
49+
NEW_BCAST = (
50+
OLD_BCAST
51+
+ "\ncall MPI_BCAST(input_seed,1,MPI_INTEGER,0,seg_comm,ierr)"
52+
+ "\ncall MPI_BCAST(use_seed,1,MPI_LOGICAL,0,seg_comm,ierr)"
53+
)
54+
55+
# (4) parse the `seed` option from input.param (appended after the `indir` case).
56+
OLD_PARSE = " case('indir')\n read(tmparg,'(a)') indirparam"
57+
NEW_PARSE = (
58+
OLD_PARSE
59+
+ "\n case('seed')"
60+
+ "\n read(tmparg,'(i12)') input_seed"
61+
+ "\n use_seed = .true."
62+
+ "\n print *, 'seed = ', input_seed; call flush(6)"
63+
)
3164

3265

3366
def patch(path, old, new, marker, label):
@@ -50,17 +83,22 @@ def main():
5083

5184
new_seed = NEW_SEED
5285
if pin_seed:
53-
# Determinism for validate_shim.sh: drop the clock term (c1) so the shim
54-
# and mpif90 builds seed identically and can be compared bit-for-bit.
55-
# Scoped to the seed formula only -- the clock is still read for timing.
56-
if "c1 +" not in NEW_SEED:
57-
sys.exit("ERROR: --pin-seed anchor 'c1 +' not found in NEW_SEED")
86+
# Determinism for validate_shim.sh: drop the clock term (c1) from the
87+
# default (unseeded) branch so the shim and mpif90 builds seed identically
88+
# and can be compared bit-for-bit. Scoped to that one formula.
89+
if "seedvec(jj) = c1 +" not in NEW_SEED:
90+
sys.exit(
91+
"ERROR: --pin-seed anchor 'seedvec(jj) = c1 +' not found in NEW_SEED"
92+
)
5893
new_seed = NEW_SEED.replace("seedvec(jj) = c1 +", "seedvec(jj) = 987654321 +")
5994

6095
patch(header, HDR_ANCHOR, HDR_ADD, "INTEGER :: nseed", "header seedvec decl")
6196
patch(amica15, OLD_SEED, new_seed, "call random_seed(size = nseed)", "random_seed")
97+
patch(amica15, OLD_BCAST, NEW_BCAST, "MPI_BCAST(use_seed", "seed BCAST")
98+
patch(amica15, OLD_PARSE, NEW_PARSE, "case('seed')", "seed param parser")
6299
print(
63-
f"patched: portable random_seed + seedvec declarations{' (pinned)' if pin_seed else ''}"
100+
"patched: portable+reproducible random_seed, seed param, BCAST"
101+
f"{' (pinned)' if pin_seed else ''}"
64102
)
65103

66104

0 commit comments

Comments
 (0)