Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 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
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
88 changes: 88 additions & 0 deletions .github/workflows/acceptancetest_mg7.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,94 @@ jobs:
export PATH="$HOME/.cache/HEPtools/bin:$PATH"
./tests/test_manager.py test_group_subprocess_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_reproducibility:
needs: build_madspace
# mg7 seeded-generation reproducibility for a full survey + madnis
# training + generate run (p p > t t~, no gridpack export): the same
# run_card seed must give a byte-identical LHE file across independent
# runs, including the training phase itself, a different seed must not.
# Companion to acceptancetest_mg7_gridpack_reproducibility, which only
# covers a frozen gridpack's own regeneration. 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_reproducibility_mg7
run: |
cd $GITHUB_WORKSPACE
export PATH="$HOME/.cache/HEPtools/bin:$PATH"
./tests/test_manager.py test_madnis_reproducibility_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
8 changes: 7 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 Down Expand Up @@ -69,6 +68,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", 0),
help="0 draws a fresh random seed; a positive integer makes the run "
"reproducible at any --cpu_thread_pool_size (for --output_format lhe "
"also set combine_thread_pool_size = 1)"
)
parser.add_argument("--device", type=str, nargs="*")
parser.add_argument(
"--cpu_thread_pool_size", type=int, default=run_args["cpu_thread_pool_size"]
Expand Down Expand Up @@ -173,6 +178,7 @@ def main() -> None:
channels=channel_generators,
status_file=os.path.join(run_path, "info.json"),
config=config,
seed=args.seed,
)

# run generation
Expand Down
48 changes: 35 additions & 13 deletions madgraph/iolibs/template_files/mg7/madevent.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,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 @@ -417,6 +417,7 @@ def build_event_generator(self, phasespaces: list[PhaseSpace]) -> ms.EventGenera
channels=channel_generators,
status_file=os.path.join(self.run_path, "info.json"),
config=self.event_generator_config,
seed=self.run_card["run"]["seed"],
)
unused_globals = (
set(self.contexts[0].global_names()) - event_generator.used_globals()
Expand All @@ -427,44 +428,50 @@ 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 == "multichannel":
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)
elif phasespace_mode == "both":
kept_count = self.run_card["phasespace"]["simplified_channel_count"]
phasespaces_multi = [
subproc.build_multichannel_phasespace()
for subproc in self.subprocesses
]
evgen_multi = self.survey_phasespaces(phasespaces_multi)
evgen_multi = self.survey_phasespaces(phasespaces_multi, 0)

phasespaces_flat = [
subproc.build_flat_phasespace()
if len(subproc.meta["channels"]) > kept_count + 1 else
None
for subproc in self.subprocesses
]
#evgen_flat = self.survey_phasespaces(phasespaces_flat, "flat")
#evgen_flat = self.survey_phasespaces(phasespaces_flat, 1)

channel_status = evgen_multi.channel_status()
cross_sections = []
Expand All @@ -485,7 +492,7 @@ def survey(self) -> None:
self.subprocesses, phasespaces_multi, phasespaces_flat, cross_sections
)
]
self.event_generator = self.survey_phasespaces(self.phasespaces)
self.event_generator = self.survey_phasespaces(self.phasespaces, 2)
else:
raise ValueError("Unknown phasespace mode")

Expand Down Expand Up @@ -550,6 +557,12 @@ def train_madnis(self) -> None:
config=config,
integrands=integrands,
cwnets=cwnets,
# Reuses the run_card's own seed (also used by build_event_generator());
# MultiMadnisTraining derives an independent, non-colliding stream from it
# (see salt::job_kind_madnis_train in random.hpp). Only the single-channel
# CPU sample-generation path is currently seeded -- buffered training and
# GPU multi-channel batches are still non-deterministic.
seed=run_args["seed"],
)
madnis_training.train()
for phasespace, active_channels in zip(
Expand Down Expand Up @@ -1292,6 +1305,12 @@ def simplify_phasespace(

def build_madnis(self, phasespace: PhaseSpace) -> PhaseSpace:
madnis_args = self.process.run_card["madnis"]
# Shared across all networks below: each one's global names are
# already unique (per-channel/per-component prefixes), so
# initialize_globals() derives an independent, non-colliding stream
# per tensor from this one base seed (see global_init_seed in
# random.hpp) -- no extra indices need to be threaded through here.
seed = self.process.run_card["run"]["seed"]
channels = []
for channel_id, channel in enumerate(phasespace.channels):
prefix = f"subproc{self.subproc_id}.channel{channel_id}"
Expand All @@ -1309,7 +1328,7 @@ def build_madnis(self, phasespace: PhaseSpace) -> PhaseSpace:
subnet_layers=madnis_args["discrete_layers"],
subnet_activation=self.activation(madnis_args["discrete_activation"]),
)
discrete_before.initialize_globals(self.process.contexts[0])
discrete_before.initialize_globals(self.process.contexts[0], seed)
cond_dim += perm_count

flow_dim = channel.phasespace_mapping.random_dim()
Expand All @@ -1324,10 +1343,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 @@ -1342,7 +1362,7 @@ def build_madnis(self, phasespace: PhaseSpace) -> PhaseSpace:
subnet_layers=madnis_args["discrete_layers"],
subnet_activation=self.activation(madnis_args["discrete_activation"]),
)
discrete_after.initialize_globals(self.process.contexts[0])
discrete_after.initialize_globals(self.process.contexts[0], seed)

channels.append(Channel(
phasespace_mapping = channel.phasespace_mapping,
Expand Down Expand Up @@ -1411,7 +1431,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_card["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
# 0 draws a fresh random seed each run; a positive integer makes the run
# reproducible: the same (seed, thread-pool sizes) reproduces bit-identically and
# distinct seeds are statistically independent. Generation is reproducible at any
# cpu_thread_pool_size; for output_format = lhe also set combine_thread_pool_size = 1
# (compact_npy and lhe_npy are reproducible at any combine size).
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 @@ -6453,6 +6453,12 @@ def default_setup(self):

# ----------------------------- [run] --------------------------
self.add_toml_param('run', 'run_name', "run", gridpack=True)
self.add_toml_param('run', 'seed', 0, gridpack=True,
comment="0 draws a fresh random seed each run; any positive integer "
"makes the run reproducible: the same (seed, thread-pool sizes) "
"reproduces bit-identically. Generation is reproducible at any "
"cpu_thread_pool_size; for output_format = lhe also set "
"combine_thread_pool_size = 1")
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
4 changes: 4 additions & 0 deletions madspace/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ if(ENABLE_OPENBLAS)
-DBUILD_TESTING=OFF
-DBUILD_WITHOUT_CBLAS=ON
-DUSE_THREAD=OFF
-DUSE_LOCKING=ON
-DDYNAMIC_ARCH=ON
-DCMAKE_POSITION_INDEPENDENT_CODE=1
INSTALL_BYPRODUCTS <INSTALL_DIR>/lib/${openblas_libname}
)
Expand Down Expand Up @@ -178,6 +180,7 @@ add_library(
src/driver/backend.cpp
src/driver/lhe_output.cpp
src/driver/madnis_training.cpp
src/driver/random.cpp
src/compgraphs/type.cpp
src/compgraphs/function.cpp
src/compgraphs/instruction.cpp
Expand Down Expand Up @@ -230,6 +233,7 @@ add_library(
include/madspace/driver/lhe_output.hpp
include/madspace/driver/logger.hpp
include/madspace/driver/madnis_training.hpp
include/madspace/driver/random.hpp
include/madspace/compgraphs.hpp
include/madspace/compgraphs/type.hpp
include/madspace/compgraphs/function.hpp
Expand Down
13 changes: 11 additions & 2 deletions madspace/include/madspace/driver/backend.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#pragma once

#include <cstdint>
#include <optional>

#include "madspace/compgraphs.hpp"
#include "madspace/driver/context.hpp"
#include "madspace/driver/tensor.hpp"
Expand All @@ -9,16 +12,22 @@ namespace madspace {
class Runtime {
public:
virtual ~Runtime() = default;
virtual TensorVec run(const TensorVec& inputs) = 0;
// `seed`, when set, makes this call's random numbers a deterministic function of
// `seed` alone. Backends that can't support this should reject it, not ignore it.
virtual TensorVec
run(const TensorVec& inputs, std::optional<std::uint64_t> seed = std::nullopt) = 0;
virtual std::tuple<TensorVec, TensorVec, std::vector<bool>> run_with_grad(
const TensorVec& inputs, const std::vector<bool>& input_requires_grad
const TensorVec& inputs,
const std::vector<bool>& input_requires_grad,
std::optional<std::uint64_t> seed = std::nullopt
) = 0;
virtual std::pair<TensorVec, TensorVec> run_backward(
const TensorVec& output_grads,
const TensorVec& stored_locals,
const std::vector<bool>& eval_grad,
bool return_contiguous_grads = false
) = 0;

friend std::unique_ptr<Runtime>
build_runtime(const Function& function, ContextPtr context, bool concurrent);

Expand Down
Loading
Loading