Skip to content

madspace: multi-step re- and unweighting (surrogates, color reweighting) - #285

Draft
theoheimel wants to merge 41 commits into
mainfrom
feat-multi-step-unweighting
Draft

theoheimel wants to merge 41 commits into
mainfrom
feat-multi-step-unweighting

Conversation

@theoheimel

Copy link
Copy Markdown
Contributor

Multi-step re- and unweighting for the madspace EventGenerator.

Groups of channels can get a sequence of re- or unweighting steps, each with its own matrix element. Main use cases are fast surrogates of the matrix element (NN surrogates) and leading-to-full colour reweighting.

Depends on #264 (custom UMAMI functions) and #253 (event streaming). This branch merges both, so the diff against main includes them. The commits of this PR start at the merge commit 301c66d.

Weight definitions

  • A step multiplies the weight by the ratio r_k = |M_k|² / |M_(k-1)|². Channel weights, PDFs, flux, Jacobian and bias are those of the first step and cancel in the ratio.
  • Unweighting step: unweights u_k = o_prev · Π r, the overweight carried from the last unweighting step times the ratios since. It has its own maximum weight, truncation and freeze, per channel and step. Raising any maximum only lowers acceptances, so the final rescan (unweight_all) stays rejection-only.
  • Reweighting step: keeps r_k in the weight. A following unweighting step absorbs it.
  • Importance sampling is optimized with respect to the first step. Integrals, event counts and the sharing of events between channels refer to the last step.

Implementation

  • Event flow:
    • Events passing the first unweighting stay on the device.
    • They are collected per (context, group) until a batch is full; partial batches run at round barriers.
    • Only the per-event weights and matrix elements go to the host between steps.
    • Full events are copied to the host once, after the last step.
  • Reproducibility:
    • Decisions are made per channel in job order.
    • Random numbers are drawn per event at the first unweighting.
    • Results therefore do not depend on batching or thread count.
  • Matrix-element outputs: only |M|² is required. diagram_amp2 is needed only for diagram-based channel weights. Colour, helicity and diagram indices come from the first step that returns them, or from later steps with override_outputs.
  • Gridpacks and streaming: groups serialize to JSON, and step maxima are stored in the channel JSON. fix_max_weights and EventStream support steps.
  • Without groups, generation is unchanged: byte-identical events, LHE and status files compared to the previous build.

Fixes found along the way

Testing

  • madspace/tests/test_weighting_steps.py:
    • With a deformed surrogate followed by the true ME, cross sections and distributions agree with runs using the true ME, for unweight, reweight and reweight→unweight chains.
    • Identical events across thread counts, with grouped and ungrouped channels.
    • fix_max_weights, then a reload from JSON.
    • EventStream reproducibility and cross section.
  • madspace/tests/test_integrand_me_outputs.py: optional ME outputs.
  • Full madspace suite passes, as do the gridpack and custom-function acceptance tests.

Not yet done

  • mg7 run card and launch.py integration, including requesting diagram_amp2 only for the diagram SDE strategy.
  • Not tested on GPU.
  • Validation with a madmatrix surrogate (modified model parameters) through mg7.

theoheimel and others added 30 commits October 9, 2026 11:03
After splitting the backends code, the subfolder "backend" was added to
the process folder. This folder should be copied when the full source
gridpack is asked.
…_max_weight gridpacks

New madspace EventStream: a coordinator thread keeps generation jobs running,
buffers each channel's unweighted events in launch order and picks the channel
of every event from the current cross-section estimates. Reproducible from the
seed, nothing written to disk. Shared helpers factored out of EventGenerator
(event_utils) and gridpack.py (gridpack_setup.py).
…(valid .pyi stub), gridpack section in launch test run card
…t-streaming-event-gen

# Conflicts:
#	madgraph/interface/tutorials/mg7.py
#	madgraph/iolibs/template_files/mg7/run_card.toml
# Conflicts:
#	madgraph/iolibs/template_files/mg7/gridpack.py
…as functions

Add UMAMI input keys for particle count, PDG ids, momentum fraction, scale,
PDG id and beam index, and output keys CUT, SCALE, PDF and BIAS. A new
CustomFunction generator calls a loaded UMAMI library on the event momenta.
Cuts, EnergyScale, PartonDensity (via CustomPdf) and Integrand accept these
custom functions.
bin/init_custom_func writes custom_<function>/ with a UMAMI wrapper, a
function body for one phase-space point (C, C++, Fortran, CUDA or HIP), the
UMAMI header and a Makefile that builds lib/custom_<function>.so. Options are
taken from the command line or asked interactively. The run card gets a
[custom_functions] section with the library paths.
The libraries are built from custom_<kind>/ if present, and loaded before the
matrix elements in every context and gridpack. Custom cuts and scales get the
PDG ids of the first flavor, the bias those of the sampled flavor. A custom
PDF replaces the PDF grid (alpha_s still comes from the PDF set) and disables
the systematics.
…eights

The integrand weight includes the bias b, so adaptation, unweighting and the
channel allocation see |w| b. The signed running integral, which gives the
reported cross section, accumulates w / b, and the event records carry b so
that the combined events are written with weight w_unw / b.
test_mg7_custom_functions.py compares a custom cut, scale, PDF and bias with
the equivalent built-in settings on p p > t t~, and builds and evaluates every
function in every language with a toolchain. A new Usage page describes
bin/init_custom_func and the function signatures.
UmamiFunctions holds one std::function per UMAMI entry point. The dlopen path
fills it from the library symbols, and Context::add_matrix_element takes it
directly, so matrix elements can be implemented without a shared library.
Worker threads can then call back into Python. The abort check and the DLPack
deleters of Python-owned tensors take the GIL themselves.
ms.PythonFunction calls a Python function on numpy arrays or torch tensors with
selectable UMAMI inputs and outputs, and Context.add_matrix_element adds it to a
context. The inputs are zero-copy views of the madspace buffers. The results are
checked for type, dtype, shape and device before they are copied back. torch
functions run on the stream of the GPU runtime.
_madspace_py_loader imports it, so pybind11-stubgen failed to import the
staged package in the ENABLE_DOCS builds of CI.
[custom_functions] takes a .so library or a .py file. custom_function_loader
loads both, for bin/generate_events and gridpacks. bin/init_custom_func gains
the python-numpy and python-torch languages, which need no build. torch is
limited to one thread on the CPU, where madspace already splits the batches.
…eat-multi-step-unweighting

The bias of the custom functions is now masked in combined_event_layout and
divided out in copy_channel_event, so EventStream handles it as well. Custom
functions of a gridpack are loaded in gridpack_setup.load_context_data.
The Integrand looks up the matrix-element outputs by name. Only |M|^2 is
required: diagram_amp2 only for diagram-based channel weights, and missing
color, helicity and diagram indices are written as -1. With
return_matrix_element, DifferentialCrossSection also returns |M|^2 itself,
which the Integrand passes on as the matrix_element output.
With return_matrix_element, the Integrand also returns the flavor index passed
to the matrix element and, with several differential cross sections, which
one was evaluated, so that a later step can evaluate its own matrix element
on the same events.
Groups of channels can be given a sequence of WeightingSteps, each with its
own matrix element, for example a fast surrogate in the integrand followed by
the full matrix element. A step multiplies the weights by the ratio of its
matrix element to the previous one. An unweighting step then unweights the
ratios since the last unweighting step, times the overweight carried from it,
with its own maximum weight, truncation and freeze. A reweighting step keeps
the ratio in the weight.

The events that pass the first unweighting stay on the device and are
collected per group and context until a batch is full, or until the end of a
round. The decisions are made per channel in job order, with random numbers
drawn per event when the events are first unweighted, so the results do not
depend on the batching or the number of threads. Integrals, event counts and
the sharing of the events between channels refer to the last step, the
importance sampling to the first. The color, helicity and diagram indices are
taken from the first step that returns them, or replaced with
override_outputs. Without groups the generation is unchanged.
Like survey and generate: with matrix elements or custom functions in Python,
the worker threads otherwise wait for the GIL held by the calling thread.
weighting_groups_to_json/from_json store the groups with their matrix
elements by index, like the channel functions store theirs, so that a
gridpack can restore them. With include_estimates, the channel JSON also holds
the maximum weights of the steps, which stay fixed when the channel is loaded
again. Tested with fix_max_weights() and a reload into a fresh context.
Destroying the stream waits for its running jobs, which can need the GIL, for
example with custom functions or matrix elements in Python. Destroyed from
Python with the GIL held, this deadlocked.
The EventStream takes the weighting groups like the EventGenerator and runs
the events of their channels through the same pipeline, which now hands the
events after the last step to a sink: the EventGenerator writes them to the
channel files, the stream buffers them as finished jobs. All maxima have to
be fixed, so every decision only depends on the event itself. As for the
first step, the integral after the steps only includes the jobs whose events
were requested, so the stream stays a pure function of the seed.

Also passes the biases of the jobs on to their integration, which the stream
dropped before.
The default mode of the WeightingStep constructor made pybind11-stubgen fail
the wheel build.
A surrogate of the matrix element replaces it in the integrand, and the full
matrix element unweights or reweights the events in a second pass, as one
weighting step per subprocess. The surrogate is a .so library with the inputs
of the madmatrix libraries, loaded per subprocess with {subprocess} and
{device} in its path, or a Python file defining
custom_surrogate(momenta, pdg_ids, alpha_s). In unweight mode, the
first_pass_* settings apply to the unweighting with the surrogate and those of
[generation] to the second pass. Needs the denominators sde_strategy; no
gridpacks yet.
With weighting steps, the run summary shows the efficiency of the first
unweighting, before and after cuts, and of each step relative to the one
before, next to the overall one, their product. The channels keep showing the
overall efficiency. The accepted counts of the steps are now rescaled like
the final count when a maximum of an earlier step rises, and reweighting
steps count their events too.
UMAMI 1.2 adds the output UMAMI_OUT_MATRIX_ELEMENT_RATIO, the ratio of the
matrix element to a reference defined by the caller. A weighting step whose
matrix elements return it (MatrixElement::matrix_element_ratio_out) uses the
ratio directly, and the ratio times the previous matrix element is the
reference of the next step. Integrands reject such matrix elements.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants