Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
bf3e1ac
Add run_card seed for reproducible mg7 CPU event generation
oliviermattelaer Jul 23, 2026
fbd1856
Merge origin/main into claude/run-card-seed-independence-6c1362
oliviermattelaer Jul 23, 2026
930fd33
Wire gridpack --seed through to the madspace context
oliviermattelaer Jul 23, 2026
dd4d1fa
some refactoring of the deterministic RNG system
theoheimel Jul 24, 2026
cc3e28a
fix race condition from trailing jobs
theoheimel Jul 24, 2026
217ff03
make seeding independent of context index
theoheimel Jul 24, 2026
a69289b
make reproducibility independent of thread pool size
theoheimel Jul 24, 2026
5604439
only enforce ordering per channel, perform synchronization of integra…
theoheimel Jul 24, 2026
c39d755
reproducible lhe output
theoheimel Jul 24, 2026
2b0e631
move unweighting step back into worker thread
theoheimel Jul 24, 2026
df98036
desloppification and code quality improvements
theoheimel Jul 25, 2026
528c8a9
Merge branch 'main' into feat-reproducibility
theoheimel Jul 25, 2026
a38059b
add reproducibility test
theoheimel Jul 25, 2026
f874000
bugfix in the final unweighting pass if called repeatedly
theoheimel Jul 25, 2026
77be66c
openblas thread safety bugfix
theoheimel Jul 25, 2026
24a0453
build openblas in dynamic_arch mode
theoheimel Jul 25, 2026
63ca530
store seed in info.json file
theoheimel Jul 25, 2026
92e0243
fix concurrent writes and non-deterministic ordering in cpu backward …
theoheimel Jul 25, 2026
eca2ea0
first steps toward madnis reproducibility. backward pass bugfixes
theoheimel Jul 26, 2026
8bf182a
reproducible generation job scheduling during madnis training
theoheimel Jul 26, 2026
4cf1105
only test madnis reproducibility in reproducible mode
theoheimel Jul 26, 2026
c89badf
extend to GPU madnis training
theoheimel Jul 26, 2026
ab7d1a8
Merge branch 'main' into feat-reproducibility
theoheimel Jul 29, 2026
cc9bfec
add code for MIXMAX random number generator from https://github.com/k…
theoheimel Jul 29, 2026
1e9f342
start implementing new seed derivation system
theoheimel Jul 29, 2026
607c42b
replace mt19937 with MIXMAX, implement new seed system
theoheimel Jul 29, 2026
f5c0d38
update cpu runtime to use mixmax
theoheimel Jul 29, 2026
7761280
madnis training and RNG bugfixes
theoheimel Jul 30, 2026
7b11407
Merge branch 'main' into feat-reproducibility
theoheimel Aug 20, 2026
e33c267
smarter job scheduling using integration error
theoheimel Aug 21, 2026
c66eea6
improvements and bugfixes of job scheduling
theoheimel Aug 21, 2026
aad4f49
Merge branch 'main' into feat-reproducibility
theoheimel Aug 21, 2026
4408b19
remove non-reproducible code path for event generation
theoheimel Aug 21, 2026
aa4ba47
extend mixmax rng to gpu
theoheimel Aug 25, 2026
fbf7a30
per-thread resources shared across runtimes
theoheimel Aug 25, 2026
ec6b454
support negative integrands
theoheimel Aug 25, 2026
934b8df
fix bug in sde strategy 2
theoheimel Aug 26, 2026
ded6062
reproducible madnis training by default
theoheimel Aug 26, 2026
b0379ed
more efficient mixmax seeding
theoheimel Aug 29, 2026
77a03ec
Merge branch 'feat-reproducibility' of github.com:MadGraphTeam/MadGra…
theoheimel Aug 30, 2026
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
86 changes: 86 additions & 0 deletions .github/workflows/acceptancetest_mg7.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,92 @@ jobs:
export PATH="$HOME/.cache/HEPtools/bin:$PATH"
./tests/test_manager.py test_flavor_grouping_consistency_mg7 -pA -t0 -l INFO

acceptancetest_mg7_vegas_reproducibility:
needs: build_madspace
# mg7 seeded-generation reproducibility (p p > t t~, plain VEGAS-optimized
# run): the same run_card seed must give a byte-identical LHE file across
# independent runs, a different seed must not. Self-skips if the madspace
# + LHAPDF(NNPDF23) stack is unavailable.
runs-on: ubuntu-24.04
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/checkout_mg5
- uses: ./.github/actions/install_madspace
- uses: ./.github/actions/restore-pip-cache
- uses: ./.github/actions/restore_heptools
- name: test one of the test test_vegas_reproducibility_mg7
run: |
cd $GITHUB_WORKSPACE
export PATH="$HOME/.cache/HEPtools/bin:$PATH"
./tests/test_manager.py test_vegas_reproducibility_mg7 -pA -t0 -l INFO

acceptancetest_mg7_madnis_reproducible_mode:
needs: build_madspace
# mg7 madnis training (p p > t t~): training itself must be byte-identical
# for the same seed independent of the cpu thread pool size, both with
# online-only training and with buffered (off-policy replay) training
# enabled. Self-skips if the madspace + LHAPDF(NNPDF23) stack is
# unavailable.
runs-on: ubuntu-24.04
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/checkout_mg5
- uses: ./.github/actions/install_madspace
- uses: ./.github/actions/restore-pip-cache
- uses: ./.github/actions/restore_heptools
- name: test one of the test test_madnis_reproducible_mode_mg7
run: |
cd $GITHUB_WORKSPACE
export PATH="$HOME/.cache/HEPtools/bin:$PATH"
./tests/test_manager.py test_madnis_reproducible_mode_mg7 -pA -t0 -l INFO

acceptancetest_mg7_gridpack_reproducibility:
needs: build_madspace
# mg7 seeded-generation reproducibility for a gridpack trained with
# madnis (p p > t t~): the gridpack's own bin/generate_events --seed must
# give a byte-identical LHE file across independent runs with the same
# seed, and a different result for a different seed. Training itself is
# not required to be deterministic. Self-skips if the madspace +
# LHAPDF(NNPDF23) stack is unavailable.
runs-on: ubuntu-24.04
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/checkout_mg5
- uses: ./.github/actions/install_madspace
- uses: ./.github/actions/restore-pip-cache
- uses: ./.github/actions/restore_heptools
- name: test one of the test test_gridpack_reproducibility_mg7
run: |
cd $GITHUB_WORKSPACE
export PATH="$HOME/.cache/HEPtools/bin:$PATH"
./tests/test_manager.py test_gridpack_reproducibility_mg7 -pA -t0 -l INFO

acceptancetest_mg7_gridpack_reproducibility_vegas:
needs: build_madspace
# mg7 seeded-generation reproducibility for a plain VEGAS-optimized
# gridpack (p p > t t~, no madnis training): the gridpack's own
# bin/generate_events --seed must give a byte-identical LHE file across
# independent runs with the same seed, and a different result for a
# different seed. Companion to acceptancetest_mg7_gridpack_reproducibility,
# which covers the madnis-trained case. Self-skips if the madspace +
# LHAPDF(NNPDF23) stack is unavailable.
runs-on: ubuntu-24.04
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/checkout_mg5
- uses: ./.github/actions/install_madspace
- uses: ./.github/actions/restore-pip-cache
- uses: ./.github/actions/restore_heptools
- name: test one of the test test_gridpack_reproducibility_vegas_mg7
run: |
cd $GITHUB_WORKSPACE
export PATH="$HOME/.cache/HEPtools/bin:$PATH"
./tests/test_manager.py test_gridpack_reproducibility_vegas_mg7 -pA -t0 -l INFO

acceptancetest_mg7_merged_flavor_uq:
needs: build_madspace
# mg7 cross-section for the merged-flavor u q > u q (q = u d), pinned to the
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/madspace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,16 @@ jobs:
yum -y install openblas-devel yum-utils &&
yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/x86_64/cuda-rhel8.repo &&
yum-config-manager --add-repo /project/.github/workflows/rocm.repo &&
yum -y install cuda-nvcc-12-8 cuda-cudart-devel-12-8 cuda-thrust-12-8 libcublas-devel-12-8 libcurand-devel-12-8 rocm-hip-runtime-devel rocblas-devel rocrand-devel rocthrust-devel hipcub-devel
yum -y install cuda-nvcc-12-8 cuda-cudart-devel-12-8 cuda-thrust-12-8 libcublas-devel-12-8 rocm-hip-runtime-devel rocblas-devel rocthrust-devel hipcub-devel
CIBW_REPAIR_WHEEL_COMMAND: >
auditwheel repair
--exclude libmadspace.so
--exclude 'libcublas.so.*'
--exclude 'libcurand.so.*'
--exclude 'libcublasLt.so.*'
--exclude 'libcudart.so.*'
--exclude 'libamdhip64.so.*'
--exclude 'libhipblaslt.so.*'
--exclude 'librocblas.so.*'
--exclude 'librocrand.so.*'
--exclude 'libroctx64.so.*'
-w {dest_dir} {wheel}
CIBW_ENVIRONMENT: >
Expand Down
17 changes: 16 additions & 1 deletion madgraph/iolibs/template_files/mg7/gridpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import tomllib
import argparse


def resolve_verbosity(verbosity: str) -> str:
"""Resolve the run_card "auto" verbosity to "pretty"/"log" depending on
whether stdout is attached to a terminal; other values pass through
Expand All @@ -41,6 +40,14 @@ def resolve_verbosity(verbosity: str) -> str:
return verbosity


def resolve_seed(seed: int) -> int:
"""Resolve the run_card "seed": -1 draws a fresh 64-bit seed via
os.urandom, any other value is used as-is."""
if seed == -1:
return int.from_bytes(os.urandom(8), "big")
return seed


def main() -> None:
# load run card and metadata. Use the RunCardMG7 representation when the
# madgraph package is importable; gridpacks are meant to be portable, so
Expand Down Expand Up @@ -69,6 +76,12 @@ def main() -> None:
# parse command line arguments
parser = argparse.ArgumentParser()
parser.add_argument("--run_name", type=str, default=run_args["run_name"])
parser.add_argument(
"--seed", type=int, default=run_args.get("seed", -1),
help="every run is reproducible from its seed; -1 draws a fresh random "
"seed each run instead of fixing one here (still recorded in the "
"run's info.json)"
)
parser.add_argument("--device", type=str, nargs="*")
parser.add_argument(
"--cpu_thread_pool_size", type=int, default=run_args["cpu_thread_pool_size"]
Expand All @@ -94,6 +107,7 @@ def main() -> None:
parser.add_argument("--cpu_batch_size", type=int, default=gen_args["cpu_batch_size"])
parser.add_argument("--gpu_batch_size", type=int, default=gen_args["gpu_batch_size"])
args = parser.parse_args()
seed = resolve_seed(args.seed)

# initialize event directory
run_name = args.run_name
Expand Down Expand Up @@ -173,6 +187,7 @@ def main() -> None:
channels=channel_generators,
status_file=ms.StatusFile(os.path.join(run_path, "info.json")),
config=config,
seed=seed,
)

# run generation
Expand Down
62 changes: 49 additions & 13 deletions madgraph/iolibs/template_files/mg7/madevent.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ def resolve_verbosity(verbosity: str) -> str:
return verbosity


def resolve_seed(seed: int) -> int:
"""Resolve the run_card "seed": -1 draws a fresh 64-bit seed via
os.urandom, any other value is used as-is."""
if seed == -1:
return int.from_bytes(os.urandom(8), "big")
return seed


def resolve_cppauto_backend(build_path: str) -> str:
"""Ask the matrix-element Makefile to resolve ``cppauto``.

Expand Down Expand Up @@ -230,6 +238,10 @@ def __init__(self):

def load_cards(self) -> None:
self.run_card = RunCardMG7(os.path.join("Cards", "run_card.toml"))
# Resolved once so every generator built during this run shares the same
# seed; the concrete value (even if randomly drawn) is recorded in each
# generator's info.json.
self.run_seed = resolve_seed(self.run_card["run"]["seed"])
self.param_card_path = os.path.join("Cards", "param_card.dat")
self.param_card = ParamCard(self.param_card_path)
with open(os.path.join("SubProcesses", "subprocesses.json")) as f:
Expand Down Expand Up @@ -269,7 +281,7 @@ def init_context(self) -> None:
self.device_types = []
self.devices = []
self.pool_sizes = []
for i, device_name in enumerate(device_names):
for device_name in device_names:
if ":" in device_name:
device_type, device_index_str = device_name.split(":")
device_index = int(device_index_str)
Expand Down Expand Up @@ -566,6 +578,7 @@ def build_event_generator(self, phasespaces: list[PhaseSpace]) -> ms.EventGenera
channels=channel_generators,
status_file=self.status_file,
config=self.event_generator_config,
seed=self.run_seed,
)
unused_globals = (
set(self.contexts[0].global_names()) - event_generator.used_globals()
Expand All @@ -576,29 +589,35 @@ def build_event_generator(self, phasespaces: list[PhaseSpace]) -> ms.EventGenera
return event_generator

def survey_phasespaces(
self, phasespaces: list[PhaseSpace | None]
self, phasespaces: list[PhaseSpace | None], survey_pass: int = 0
) -> ms.EventGenerator | None:
ps_filtered = [ps for ps in phasespaces if ps is not None]
if len(ps_filtered) == 0:
return None
event_generator = self.build_event_generator(ps_filtered)
event_generator.survey()
event_generator.survey(survey_pass)
return event_generator

def survey(self) -> None:
# survey_pass distinguishes the survey() calls below: "both" mode can
# re-survey a channel carried over unchanged from the multichannel pass
# into the final (simplified) pass, and both passes schedule jobs on the
# same underlying ChannelEventGenerator. The explicit pass index keeps each
# pass's job seeds independent of the other passes' job counts, rather than
# depending on call history.
phasespace_mode = self.run_card["phasespace"]["mode"]
if phasespace_mode in ["multichannel", "both", "auto"]:
self.phasespaces = [
subproc.build_multichannel_phasespace()
for subproc in self.subprocesses
]
self.event_generator = self.survey_phasespaces(self.phasespaces)
self.event_generator = self.survey_phasespaces(self.phasespaces, 0)
elif phasespace_mode == "flat":
self.phasespaces = [
subproc.build_flat_phasespace()
for subproc in self.subprocesses
]
self.event_generator = self.survey_phasespaces(self.phasespaces)
self.event_generator = self.survey_phasespaces(self.phasespaces, 0)
else:
raise ValueError("Unknown phasespace mode")

Expand All @@ -610,8 +629,8 @@ def survey(self) -> None:
variance = 0.
count_opt = 0
for status in channel_status[chan_offset:chan_offset + len(ps.channels)]:
mean += status.mean
variance += status.error**2
mean += status.mean_abs
variance += status.error_abs**2
count_opt += status.count_opt
rsd = (variance * count_opt)**0.5 / mean
subproc.set_madnis_auto_settings(rsd)
Expand Down Expand Up @@ -644,7 +663,10 @@ def survey(self) -> None:
ps_multi is not ps_both
for ps_multi, ps_both in zip(phasespaces_multi, self.phasespaces)
):
self.event_generator = self.survey_phasespaces(self.phasespaces)
# distinct survey_pass: a channel carried over unchanged from the
# multichannel pass (pass 0) into this resurvey must not share its
# seed stream with that earlier pass.
self.event_generator = self.survey_phasespaces(self.phasespaces, 1)

def train_madnis(self) -> None:
madnis_args = self.run_card["madnis"]
Expand Down Expand Up @@ -715,6 +737,11 @@ def train_madnis(self) -> None:
training_args=training_args,
verbosity=verbosity,
status_file=self.status_file,
# Reuses the run's resolved seed (also used by build_event_generator()).
# Only the single-channel CPU sample-generation path is currently seeded
# -- buffered training and GPU multi-channel batches are still
# non-deterministic.
seed=self.run_seed,
)
madnis_training.train()
for phasespace, active_channels in zip(
Expand Down Expand Up @@ -918,6 +945,9 @@ def build_lhe_meta(self):
headers.append(ms.LHEHeader(name="MG5ProcCard", content=f.read()))
headers.append(ms.LHEHeader(name="slha", content=param_text))
headers.append(ms.LHEHeader(name="MG7RunCard", content=run_text))
# The resolved seed (even when the run_card requested a random one via
# seed = -1), so the run can be reproduced from the LHE file alone.
headers.append(ms.LHEHeader(name="MG7Seed", content=str(self.run_seed)))
return ms.LHEMeta(
beam1_pdg_id=beam_pdgs[0], beam2_pdg_id=beam_pdgs[1],
beam1_energy=energies[0], beam2_energy=energies[1],
Expand Down Expand Up @@ -1678,6 +1708,9 @@ def set_madnis_auto_settings(self, rsd: float):

def build_madnis(self, phasespace: PhaseSpace) -> PhaseSpace:
madnis_args = self.process.run_card["madnis"]
# Shared across all networks below: initialize_globals() derives an
# independent, non-colliding stream per tensor from this one base seed.
seed = self.process.run_seed
channels = []
for channel_id, channel in enumerate(phasespace.channels):
prefix = f"subproc{self.subproc_id}.channel{channel_id}"
Expand All @@ -1695,10 +1728,11 @@ def build_madnis(self, phasespace: PhaseSpace) -> PhaseSpace:
invert_spline=madnis_args["flow_invert_spline"],
)
if channel.adaptive_mapping is None:
flow.initialize_globals(self.process.contexts[0])
flow.initialize_globals(self.process.contexts[0], seed)
else:
flow.initialize_from_vegas(
self.process.contexts[0], channel.adaptive_mapping.grid_name()
self.process.contexts[0], channel.adaptive_mapping.grid_name(),
seed
)
cond_dim += flow_dim

Expand All @@ -1715,7 +1749,7 @@ def build_madnis(self, phasespace: PhaseSpace) -> PhaseSpace:
subnet_layers=madnis_args["discrete_layers"],
subnet_activation=self.activation(madnis_args["discrete_activation"]),
)
discrete_sym.initialize_globals(self.process.contexts[0])
discrete_sym.initialize_globals(self.process.contexts[0], seed)
cond_dim += perm_count

discrete_flavor = channel.discrete_flavor
Expand All @@ -1729,7 +1763,7 @@ def build_madnis(self, phasespace: PhaseSpace) -> PhaseSpace:
subnet_layers=madnis_args["discrete_layers"],
subnet_activation=self.activation(madnis_args["discrete_activation"]),
)
discrete_flavor.initialize_globals(self.process.contexts[0])
discrete_flavor.initialize_globals(self.process.contexts[0], seed)

channels.append(Channel(
phasespace_mapping = channel.phasespace_mapping,
Expand Down Expand Up @@ -1803,7 +1837,9 @@ def build_cwnet(self, channel_count: int) -> ms.ChannelWeightNetwork:
activation=self.activation(madnis_args["cwnet_activation"]),
prefix=f"subproc{self.subproc_id}.cwnet",
)
cwnet.initialize_globals(self.process.contexts[0])
cwnet.initialize_globals(
self.process.contexts[0], self.process.run_seed
)
return cwnet

def t_channel_mode(self, name: str) -> ms.PhaseSpaceMapping.TChannelMode:
Expand Down
6 changes: 6 additions & 0 deletions madgraph/iolibs/template_files/mg7/run_card.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
[run]
run_name = %(run.run_name)s
# every run is reproducible: the same seed reproduces the run bit-identically,
# and distinct seeds are statistically independent. -1 draws a fresh random
# seed each run instead of fixing one here; the seed actually used is still
# recorded (the MG7Seed tag in the LHE file, or the info.json status file), so
# the run can be reproduced later.
seed = %(run.seed)s
# options: cuda, hip, cpp, cppnone, cppsse4, cppavx2, cpp512y, cpp512z, cppauto
devices = %(run.devices)s
# options:
Expand Down
6 changes: 6 additions & 0 deletions madgraph/various/banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6463,6 +6463,12 @@ def default_setup(self):

# ----------------------------- [run] --------------------------
self.add_toml_param('run', 'run_name', "run", gridpack=True)
self.add_toml_param('run', 'seed', -1, gridpack=True,
comment="every run is reproducible: the same seed reproduces the run "
"bit-identically. -1 draws a fresh random seed each run instead "
"of fixing one here; the seed actually used is still recorded "
"(the MG7Seed tag in the LHE file, or the info.json status "
"file), so the run can be reproduced later")
self.add_toml_param('run', 'devices', ["cppnone"], typelist=str, gridpack=True,
comment="options: cuda, hip, cpp, cppnone, cppsse4, cppavx2, cpp512y, cpp512z, cppauto")
self.add_toml_param('run', 'simd_vector_size', -1,
Expand Down
2 changes: 2 additions & 0 deletions madspace/.clang-format-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ src/cpu/runtime_mixin.inc
src/gpu/runtime_mixin.inc
src/cpu/runtime_backward_mixin.inc
src/gpu/runtime_backward_mixin.inc
include/madspace/mixmax/mixmax.hpp
include/madspace/mixmax/mixmax_skip_N17.c
Loading
Loading