Repository navigation
Interference in mg7: madevent helicity convention, IDWTUP -4, signed-weight fixes - #244
Merged
oliviermattelaer merged 18 commits intoOct 10, 2026
Conversation
With squared split orders that keep an interference (nampso > 1), a helicity can contribute negatively. The event-by-event helicity choice compared the random number with the signed running sum over the good helicities, divided by the signed total: with mixed signs that is not a distribution, and a negative total turns it upside down. The |M|^2 and the cross sections were right, but the helicity written to the events was not (Z/photon interference in mg7: per-helicity sigma off by up to 16 sigma from madevent). Follow madevent (matrix_madevent_group_v4.inc) instead: pick helicity i with probability |T_i| / sum|T| and return sign(T_i) * sum|T|, whose average over the choice is sum T. select_helicity_signed in the cpu and simd SigmaKin.cc, compiled in only when nampso > 1. It is switched on by a new trailing sigmaKin argument (default false), which umami sets when the caller gives a random helicity number, i.e. event generation; the callers without one (systematics re-evaluation, check_sa, fbridge) keep the helicity sum. The GPU backend still refuses nampso > 1. Checked on heft g g > t t~ HIG^2==1 (mH = 500 GeV): the per-helicity contributions drawn at five mixed-sign points equal Fortran SMATRIXHEL to the grid step. New acceptance test test_standalone_interference_helicity_choice drives the library through umami (ctypes) on u u~ > t t~ g QED^2==2; it fails with the old choice.
madspace already integrates and unweights signed weights correctly; what an interference (negative total) still got wrong around it: - LHE <init>: IDWTUP was hard-coded to +3 and XMAXUP to the signed cross section. A sample with negative weights now declares IDWTUP = -4, as madevent does, and XMAXUP is the unit weight sigma_abs (launch.py and gridpack.py). -4, not -3: Pythia8 8.317 reads -3 as |XSECUP| times the mean sign (-6405 pb for a -9572 pb sample); with -4 the showered cross section is -9678(75) pb. Positive samples are unchanged. - systematics_summary: percentages against |nominal| / |central| (the box printed "Scale variation: +-24.1% --30%"). - channel pruning (simplify_phasespace) ranks the channels by the integral of |w| rather than |signed integral|, and survives a zero total. - MadNIS BufferUnweighter: max weight from the quantile of |w|. - the relative error is printed against |mean|; the weight histogram is in units of sigma_abs, with a symmetric range for processes with a squared-order constraint; a plot with a negative bin keeps a linear axis. - test_check_xsec_processes_mg7: relative difference against |reference| (a negative reference always passed). New acceptance test test_mg7_interference.py: p p > u u~ QCD^2==2 against -12253(10) pb (three mg7 seeds of 200k events; madevent -12340(42) pb), the <init> block and the signed events.
Audit of the squared-order, [treextree] and [LIxtree] interference support in madmatrix and madspace on main, the measurements, the defects found and the phased plan (phase 1 done).
BufferUnweighter (the partial unweighting of the MadNIS replay buffer) took its maximum weight from the quantile of the signed weights while it accepts on |w|; f8d5c18 made it the quantile of |w|. Expose it to Python and test it on synthetic batches, the event index carried along so that the per-event relations are checked exactly: signs kept, weight max(|w|, w_max), w_max the |w| quantile, adaptive_prob rescaled by the acceptance probability times N / N_kept; the kept count and the estimator of the mean weight to 5 sigma. With the signed quantile an all-negative batch kept every event at its full weight and a mostly negative one 94% (the positive equivalent keeps 28.5%): those two tests fail on it. The buffer stayed unbiased either way, and on interference samples the effect on the training is not measurable (MadNIS forced on, 4000 batches: p p > u u~ QCD^2==2 RSD 0.1081 vs 0.1085, heft g g > t t~ HIG^2==1 0.0886 vs 0.0880); with the default card the buffer is not used at all (buffer_skip_batches = train_batches = 1000). The class comment described an event-file unweighter against a running maximum; it now says what the class does.
4 tasks
… CI job The GPU backend refused nampso > 1 at compile time: the device jamp buffers held one ncolor vector per helicity, while the generated diagram code (shared with the CPU backends) offsets each amplitude onto the vector of its amplitude split order. Port the CPU implementation: - calculate_jamps keeps njampso = ncolor * nampso jamps; DeviceAccessJamp, the device jamp buffers (MatrixElementKernelDevice, umami) and the memset use njampso as the length of the real and imaginary planes (== ncolor without split orders, so nothing changes for the other processes); - color_sum_kernel pairs them as color_sum_cpu_splitorders does: all ORDERED pairs of amplitude orders, masked on sqSoIndex/chosenSqso, the color contraction triangular, in the color algebra precision; - sqSoIndex and chosenSqso are generated __device__ constexpr (as icolamp; an empty macro on the CPU backends) so that the kernel can read them; - the runtime cuBLAS color sum (CUDACPP_RUNTIME_BLASCOLORSUM) cannot pair split orders: refused with a warning, and asserted in color_sum_gpu; - add_and_select_hel chooses the helicity of an interference as madevent does, behind the sigmaKin flag the CPU backends have (umami sets it when it is given a random helicity number); - GPU umami filters the helicities in the per-helicity jamp buffer rather than the ncolor_flow one, which could hold at most 8 amplitude orders. Not compiled here (no CUDA/HIP toolchain): .github/gpu_ci/interference_gpu.sh and interference_checks.py add an `interference` job to gpu_runner_ci.yml, on by default for "Run workflow" on manneback (CUDA) and lemaitre4 (HIP), off on pushes to main until validated. It checks the standalone u u~ > u u~ squared-order components (the Fortran interference, the QED=0+2+4 sum rule, FPTYPE=m), the helicity choice through umami on u u~ > t t~ g QED^2==2 (device memory through the CUDA/HIP runtime with ctypes) and mg7 p p > u u~ QCD^2==2 against the CPU reference. The script passes on a CPU backend (--backend simd_128).
oliviermattelaer
temporarily deployed
to
ceci-gpu
October 9, 2026 09:50 — with
GitHub Actions
Inactive
- start_runner runs on [self-hosted, vmadgraph, vmadgraph-<cluster>], so a run waiting in one cluster's Slurm queue does not block the other cluster - ceci_ssh: the key-check error file was the fixed /tmp/keycheck, which another Unix user on the same host had created, so the check failed and was reported as a bad key - README: start_runner runs on vmadgraph, not on GitHub-hosted
…adgraph runner per cluster)
oliviermattelaer
temporarily deployed
to
ceci-gpu
October 9, 2026 10:13 — with
GitHub Actions
Inactive
…the color sum
First GPU CI run of the split orders (manneback, NVIDIA A10): the interference,
the umami helicity choice and mg7 p p > u u~ QCD^2==2 passed, but
u u~ > u u~ QED^2==4 did not compile: with flavor grouping all its couplings
are flavor couplings (nIPC = 0) and none depends on alpha_s (ndcoup = 0), so
calculate_jamps declared allCOUPs[0] and COUPs[0], which nvcc refuses in device
code ("zero-sized variable ... is not allowed"; the host compilers take it as
an extension). Size them at least 1, as cIPD/cIPC already are. Not specific to
split orders: any such process failed to compile on the GPU backends.
color_sum_kernel: the one-order color sum is the else branch of the split-order
one rather than code after a return, which nvcc reported as unreachable
(warning #128) for the split-order processes.
oliviermattelaer
temporarily deployed
to
ceci-gpu
October 9, 2026 10:24 — with
GitHub Actions
Inactive
…U mg7 check, no zero-sized coupling arrays on CPU
- test_standalone_interference_helicity_choice (and the GPU CI checker) loaded
libmadmatrix_common with RTLD_GLOBAL before the process library. In the shared
acceptance-test process that leaks this process's symbols into every library
loaded afterwards (an in-process mg7 launch of another output). The process
library finds its common library through its own loader-relative rpath: load
it alone, and free the umami handle.
- The GPU interference job failed its mg7 check whenever the workflow's pdf_set
input was not the reference's NNPDF23_lo_as_0130_qed. The check now always
runs with the PDF set of its reference: interference_checks.py names it
(--print-reference-pdf) and interference_gpu.sh downloads it into the cache;
the job no longer reads PDF_SET.
- backend/{cpu,simd}/SigmaKin.cc: allCOUPs/COUPs sized at least 1, as the GPU
backend now is. With nxcoup == 0 (u u~ > u u~ QED^2==4 with flavor grouping)
they were zero-sized arrays, a compiler extension only.
…es 9 when summed The madevent convention for the helicity of an interference (helicity i drawn on |T_i|, weight sign(T_i) * sum|T|) gives exact helicities at the cost of a larger sum|w|, about twice the events for the same precision. Make it a choice: - run card [generation] interference_helicity: "exact" (the default, as before) or "summed", which keeps the helicity sum as the weight; - "summed" is passed to the matrix-element libraries as the umami parameter interference_helicity_summed (cpu, simd and gpu umami.cc, per instance), only when asked for: a library made before it refuses an unknown parameter; - the helicities of those events then mean nothing: export_mg7 flags the subprocesses that keep an interference (squared split orders dropping a component, subprocesses.json "interference"), and launch.lhe_helicities gives the LHE completer a table of 9 for them, so every particle line has helicity 9 (also through npy_to_lhe and the gridpack, which reuse the completer). The run says so when it starts. export_mg7 had no module logger, so its debug messages in the except branches would have raised NameError: define it. Tests: unit test_mg7_interference_helicity (card default, me_parameters, lhe_helicities, round trip); the umami helicity test and the GPU CI checker set the parameter and check that every draw returns the helicity sum; acceptance test_interference_summed_helicity_mg7 (same cross section, every helicity 9), and the exact run checks that its helicities are not 9.
oliviermattelaer
temporarily deployed
to
ceci-gpu
October 9, 2026 13:50 — with
GitHub Actions
Inactive
…omputation-madmatrix-madspace-6423f4
oliviermattelaer
temporarily deployed
to
ceci-gpu
October 9, 2026 17:26 — with
GitHub Actions
Inactive
… error)
First HIP run (lemaitre4, MI300A): every process with a single amplitude order
(p p > t t~, u u~ > u u~ QED^2==0) failed to link color_sum.o:
lld: error: undefined hidden symbol: double& KernelAccessHelper<
madmatrix::MemoryAccessMatrixElementsBase, true, double>::kernelAccessField<>(double*)
color_sum_kernel stored deltaMEs with E_ACCESS::kernelAccess in both branches of
the `if constexpr( nampso > 1 ) ... else`, and for nampso == 1 the first one, in
the discarded branch, is what clang/HIP saw first: the template was then never
instantiated for the device although the live branch uses it (nvcc is fine).
Accumulate deltaMEs in both branches and store it once, after them. The split
order builds (u u~ > t t~ g QED^2==2: the umami helicity checks) already passed
on HIP.
oliviermattelaer
temporarily deployed
to
ceci-gpu
October 9, 2026 17:34 — with
GitHub Actions
Inactive
oliviermattelaer
temporarily deployed
to
ceci-gpu
October 9, 2026 17:34 — with
GitHub Actions
Inactive
…rence run interference_gpu.sh also runs interference_diagnose.py: the failing mg7 run with serialized kernels and HIP logging, and the variants split orders with positive weights, a single flavor, the summed helicity with a dynamical scale, and the CPU of the node. Results as diag_* lines in summary.txt, log tails as diag_*.log.
oliviermattelaer
temporarily deployed
to
ceci-gpu
October 9, 2026 18:15 — with
GitHub Actions
Inactive
… interference run" This reverts commit 43f864d.
…omputation-madmatrix-madspace-6423f4 # Conflicts: # .github/workflows/gpu_runner_ci.yml
oliviermattelaer
temporarily deployed
to
ceci-gpu
October 10, 2026 05:15 — with
GitHub Actions
Inactive
Validated on both clusters (manneback CUDA run 37967270581, lemaitre4 HIP run 38026859945, the whole lemaitre4 allocation in 9 minutes, inside the 20 minute limit of a push run). On "Run workflow" the input stays, ticked by default.
…omputation-madmatrix-madspace-6423f4 # Conflicts: # madgraph/iolibs/template_files/mg7/run_card.toml # madgraph/various/banner.py
oliviermattelaer
deleted the
claude/interference-computation-madmatrix-madspace-6423f4
branch
October 10, 2026 20:05
oliviermattelaer
added a commit
that referenced
this pull request
Oct 10, 2026
…alone Since PR #244 (now in main) the madmatrix color sum pairs the amplitude split orders on every backend, CPU and GPU, and madspace integrates and unweights the signed weights of an interference. Allow 'output' (mg7) and 'output standalone' for treextree; madevent and standalone_fortran stay, and LIxtree still needs a loop backend (madevent, standalone_fortran). The madmatrix message announcing CPU-only builds comes corrected with the merge of main. Checked: - u u~ > z > e+ e- [treextree] u u~ > a > e+ e-, C++ standalone built in double precision: equal to |Z+a|^2 - |Z|^2 - |a|^2 from the same code to 2e-16, and to the Fortran value to 1e-15 (new acceptance test; the default mixed-precision build agrees to 7e-8 only); - mg7, p p > e+ e-, m(ee) > 200 GeV: interference 0.0650(9) pb over four seeds, |Z+a|^2 - |Z|^2 - |a|^2 0.0649(9) pb; the Z+a, Z and a cross sections agree with madevent. madevent's interference comes out 7.5% lower (0.0601(17) pb, 2.6 sigma), its known iteration-combination bias on sign-changing integrands; same matrix element (agrees to 1e-15). Full unit suite 2499 OK, interference acceptance tests OK.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 of
doc/interference_madmatrix_madspace_plan.md.Where things stood
copysign.p p > u u~ QCD^2==2: −9481(84) vs −9532(63) pb;p p > j j QCD^2==2: 5.283(87)e4 vs 5.263(53)e4 pb.Changes
madmatrix: helicity choice.
nampso > 1, so every other process compiles exactly as before.check_saand fbridge keep the helicity sum ΣT.Opt-out:
interference_helicity. The run card[generation] interference_helicity = "exact" | "summed"controls the helicity choice; the default,"exact", is the madevent convention above."summed"keeps the helicity sum as the weight, which means lower variance; on the heft sample it brings σ_abs from 5.57 back to 4.16 pb.interference_helicity_summed, sent only when asked for, because older libraries refuse unknown parameters.LHE
<init>. Negative-weight samples now writeIDWTUP = -4, as madevent does, withXMAXUP = σ_abs. Previously it was +3 with XMAXUP = σ. It has to be −4 and not −3: Pythia8 8.317 reads −3 as |XSECUP|·⟨sign⟩.p p > u u~ QCD^2==2Positive samples are unchanged.
Smaller fixes:
BufferUnweightertakes the quantile of |w| (see the buffer section below);Validation
g g > t t~ HIG^2==1(m_H = 500 GeV), at five fixed points with mixed-sign helicities, the per-helicity values drawn equal FortranSMATRIXHELto the grid step, with the same labels and signs.test_cmd.py::test_standalone_interference_helicity_choice(u u~ > t t~ g QED^2==2). It drives the library through umami via ctypes, and fails with the old choice.test_mg7_interference.py(p p > u u~ QCD^2==2). It checks the cross section against −12253(10) pb, the<init>block and the signed events.ppjjj,ttx1j) pass.MadNIS buffer
BufferUnweighter, the partial unweighting of the MadNIS replay buffer, accepts on |w| but took its maximum weight from the quantile of the signed weights.Synthetic batches. It now has a Python binding and
madspace/tests/test_buffer_unweighter.py. The table gives the fraction of events kept:The all-negative and mixed tests fail on the old code. The buffer stayed unbiased either way, because the rescaled
adaptive_probholds for any maximum weight.Effect on training. It is not measurable on our samples. With MadNIS forced on for 4000 batches, the buffers reach capacity within about 1200 batches either way:
p p > u u~ QCD^2==2g g > t t~ HIG^2==1With the default card the buffer is never used:
buffer_skip_batchesequalstrain_batches, both 1000.GPU (phase 4): validated on CUDA (manneback)
The GPU backend refused
nampso > 1at compile time. It now follows the CPU implementation:calculate_jampskeepsnjampso = ncolor × nampsojamps.DeviceAccessJampand every device jamp buffer usenjampsoas the plane length, so nothing changes without split orders.color_sum_kernelpairs the jamps over all ordered pairs of amplitude orders, ascolor_sum_cpu_splitordersdoes.sqSoIndexandchosenSqsoare generated__device__ constexpr, which is an empty macro on CPU.add_and_select_heluses the madevent helicity convention, behind the same flag as on CPU.Validation on CUDA. Run 37917456030, on an NVIDIA A10 through the vmadgraph runner. This branch includes
claude/vmadgraph-parallel-tasks-7fe363.u u~ > u u~ QED^2==2QED^2<=4FPTYPE=mp p > u u~ QCD^2==2IDWTUP −4, 4191 of 5000 events negativePre-existing GPU bug, fixed here. The first run found it. A process whose couplings are all flavour couplings, with none depending on α_s, declared zero-sized coupling arrays, which nvcc refuses in device code. One example is
u u~ > u u~ QED^2==4with flavour grouping. This was not specific to split orders.lemaitre4 (HIP) has not been run yet.
CI job. The new
interferencejob ofgpu_runner_ci.yml(.github/gpu_ci/interference_gpu.shandinterference_checks.py) runs on "Run workflow" on manneback and lemaitre4. It is off on pushes to main until validated. It checks:u u~ > u u~: the Fortran interference, the QED=0+2+4 sum rule, andFPTYPE=m;p p > u u~ QCD^2==2against the CPU reference.On a CPU backend the script passes every check.
Not in this PR
qcd_power = -1forces the systematics to re-evaluate the matrix element.[treextree]on mg7, which needs Interference processes: [LIxtree=QCD] and [treextree] left/right syntax #243.