ALOHA: merge multi-coupling routines, and make FD gauge routines self-contained - #58
Open
oliviermattelaer wants to merge 11 commits into
Open
ALOHA: merge multi-coupling routines, and make FD gauge routines self-contained#58oliviermattelaer wants to merge 11 commits into
oliviermattelaer wants to merge 11 commits into
Conversation
ALOHAWriterForFortran.write_combined used to write FFV2_4_3 & co as a wrapper: one call per Lorentz structure into a <type>tmp, then a component by component accumulation. The routine therefore had no expression of its own -- no contracted temporaries, no propagator DENOM -- which costs one call plus one accumulation loop per use, and leaves nothing for the helicity-recycling batching to hoist out of its loop. The combined routine is now computed like any other one: its body is the sum of the structures, each multiplied by its own coupling, sharing the momenta, the propagator denominator and the contracted temporaries. This re-uses CombineRoutineBuilder, i.e. the mechanism the loop (explicit combine) routines already go through, and the writing goes through the standard write() path so the routine picks up whatever a single coupling routine gets. The wrapper form stays as write_combined_wrapper, used when the merge is not possible (loop routine, unknown Lorentz structure, structures on different spins) and reachable with MG_ALOHA_COMBINE_WRAPPER for A/B checks. Two supporting fixes: - get_coupling_def handles several matrix couplings: each MCOUPi has its own PARTNER map, so the routine gives up only when none of them pairs the flavours, the first coupling fixes the outgoing flavour index and the others are dropped when they disagree with it. Output for a single coupling is unchanged. - CombineRoutineBuilder expands the formfactors of every structure it combines (only those of the first one were, and that expansion was then overwritten). Validated per routine (every component, matching and rejected flavours, FFV/FFS/VVVV groups, C1/P0/P1N tags: max 1e-15 relative, i.e. the reassociation of the sum) and on |M|^2 for 16 process/model combinations including p p > e- ve~ mu+ vm QCD=0, u u~ > w+ w- and p p > z j j. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
In FD gauge an offshell V/S routine ended with
CALL MULTIPLY_PROPAGATOR_FACTOR(V3, M3, V3)
so the wavefunction it returns was only finished by a helper. That call is
opaque: the helicity-recycling batching cannot look inside it, and the
helper recomputes for every helicity the part that only depends on the
momentum (the 5-momentum q, the gauge direction n from define_gauge_dir,
and n.q), even though all the copies of a batched slot share their
momentum.
The factor is now emitted by the writer, split at its natural seam:
- get_fd_gauge_txt: q (its 5th component carries the mass) and n, written
with the momenta, hence loop invariant;
- get_fd_propagator_txt: js1, js2 and w -> w - q*js1 - n*js2, appended to
the expression since it is what the routine builds.
define_gauge_dir stays a call: it is a pure function of the momentum (with
a branch that is not worth generating) and it sits in the invariant part.
The generated statements mirror the helper operation by operation, so the
result is bit identical: checked component by component (5 components,
momenta and flavour index, 20 pseudo-random inputs) on the 48 routines of
w+ w- > w+ w- and e+ e- > e+ e- z, and on |M|^2 for u u~ > w+ w-,
w+ w- > w+ w-, u u~ > z z, p p > w+ w-, e+ e- > e+ e- z and
e+ e- > w+ w- z. Nothing outside FD gauge changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
In FD gauge a massive vector and its Goldstone are the same wavefunction (components 1-4 and 5 of one 5-slot object), so the Lorentz structures of a vertex see a given leg sometimes as V and sometimes as S: the combinations are VVV1_VVS1_VSV2_VSS1, VVS1_VSS1_SVS1_SSS1, VVVV2_SSSS1_VSVS1_VSSV1_SVVS1_SVSV1 ... Structures with different spins can not share one ALOHA expression (a rank 1 and a rank 0 tensor do not add), so every single FD combination was left on the wrapper form -- 7 out of 7 for w+ w- > w+ w-. They do not need one expression though: they read one wavefunction per leg and write into one output, only under different names and into different slots. They are now assembled into a single routine which shares the momenta, the declarations, the contracted temporaries and the FD propagator factor, each structure adding its own coupling times its own expression into the slots it feeds. What this needs from the writer, beyond the assembler itself: - rename_wf: a term is written with the name the assembled routine gives each leg, while the slot stays the one the structure's own spin picks (S1_1 -> V1 % W(5)); - coup_name/combined_part: a term multiplies by its own COUPi and adds up into the output instead of assigning it. The 'is there a coupling to apply' test that used to compare coup_name to 'COUP' now compares it to 1, so that COUP2 & co are not silently dropped. The same TMP is often rebuilt by several structures (they share the contraction cache): duplicate definitions are dropped. w+ w- > w+ w- keeps 17 calls in its DHELAS, all of them the momentum-only DEFINE_GAUGE_DIR, down from 48. Validated component by component against the wrapper form (34 routines, 20 pseudo-random inputs: 4.4e-16, the projector now applied once on the sum) and on |M|^2 (bit identical for u u~ > w+ w-, w+ w- > w+ w-, u u~ > z z, e+ e- > e+ e- z; 1 ulp for e+ e- > w+ w- z). Output outside FD gauge is byte identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The merged multiple coupling routine has always been there for madmatrix: export_cpp builds the ALOHA model with explicit_combine=True, so CombineRoutineBuilder gives it one expression per combination and the wrapper writer is never reached (a non-FD HelAmps_sm.h has 0 scratch wavefunctions and 0 inner calls). FD gauge was the exception, for the same two reasons the fortran output had: - multiply_propagator_factor was called at the end of every offshell V/S routine, recomputing the momentum-only part (the 5-momentum q, the gauge direction n from define_gauge_dir, n.q) for each helicity; - structures acting on different spins (a leg is a vector in one and its Goldstone in the next) can not share an ALOHA expression, so every FD combination was written as a wrapper: 4 to 6 calls, unrolled 5 component accumulations, and an extra scratch wavefunction dragged through the signature and the call site. Both are now written like the fortran ones: the propagator factor is emitted in the routine (q/n/n.q with the momenta, js1/js2 and the update after the components), and the mixed-spin combinations are assembled term by term, sharing the momenta, the declarations, the contracted temporaries and one propagator factor. The scratch wavefunction disappears with the wrapper, so the "FIXME: hack to avoid a bug in the FD code" that fed aloha_obj_tmp/amp_tmp_fp at the call site goes away too. has_fd_propagator and rename_wf move to WriteALOHA (they are language independent) and each assembled term gets its own denom, since the C++ writer declares it const. w+ w- > w+ w- in FD gauge: 75 scratch references and 34 inner calls become 0. |M|^2 from check_sa.exe is bit identical for FD u u~ > w+ w-, w+ w- > w+ w- and e+ e- > w+ w- z, and the non-FD HelAmps_sm.h / CPPProcess.cc are byte identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The inlined FD propagator factor wrote the 5th component of q as -cI * M, which is a scalar complex: it converts to cxtype_sv only when the backend is scalar. With BACKEND=cppsse4 (and any other SIMD backend) cxtype_sv is cxtype_v and the build failed with no viable conversion from 'cxsmpl<double>' to 'cxtype_sv' Build it the way multiply_propagator_factor does, from reals broadcast to the vector type, which is also correct for the scalar backends. cppnone output is unchanged (bit identical |M|^2 for FD u u~ > w+ w-, w+ w- > w+ w- and e+ e- > w+ w- z), cppsse4 now builds and gives, for those same three processes, exactly what the unmodified generator gives under cppsse4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
In FD gauge, the longitudinal polarisation of a massive vector carries a
5th (Goldstone) component. helas_fd.h builds it twice, once for the scalar
backends and once for the SIMD ones, and the two disagreed on its sign:
#ifndef MGONGPU_CPPSIMD
w[4] = static_cast<fptype>(nsv)*cI;
#else
w[4] = -static_cast<fptype>(nsv)*cI;
The minus is the right one: fortran vxxxxx has vc%W(5) = -nsv*ci
(aloha_functions_fd.f) and so does the python one (wavefunctions.py).
So every |M|^2 with a longitudinal massive vector was wrong by ~0.7% with
BACKEND=cppnone -- which is also what cppauto falls back to when no SIMD
is available -- while cppsse4 was right. It is only visible in FD gauge
(no 5th component otherwise) and only for |nhel| != 1, which is why the
backends agree everywhere else.
For the same phase-space point as the fortran standalone,
FD u u~ > w+ w- now gives 5.1093407366301505e-03 with both cppnone and
cppsse4 (it gave 5.0762980332466311e-03 with cppnone before), against
5.1093405664217962e-03 for fortran -- the remaining 3e-8 is -ffast-math.
Same for w+ w- > w+ w- and e+ e- > w+ w- z; non-FD output is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
define_gauge_dir also builds n twice, and the two branches disagreed on n[3] when the propagator has no spatial momentum: -sign(q0) for the scalar backends, +sign(q0) for the SIMD ones. Everything else in the codebase uses -sign(q0): fortran define_gauge_dir n(3) = sign(-1.d0, dble(q(0))) python define_gauge_dir n[3] = sign(-1., q[0].real) helas_fd.h vxxxxx n[3] = -sign(p0), in both of its branches so the SIMD define_gauge_dir was the odd one out and now follows suit. n is the gauge direction, so this changes no |M|^2: the branch is reached (poisoning it gives nan for an s-channel propagator at rest, which is what the check_sa phase-space point produces), but the summed amplitude is invariant under it -- FD u u~ > w+ w-, w+ w- > w+ w- and e+ e- > w+ w- z give bit identical results before and after, with both cppnone and cppsse4. What it removes is a gauge choice that silently depended on the backend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The FD block of vxxxxx built its vector constants with
const fptype_sv zero{0.};
const fptype_sv one{1.};
A braced initialiser sets the first lane and zero fills the others, so
'one' was {1,0,...} instead of a broadcast. Everything the block selects
with it was therefore right in the first lane only: for every other lane
b_A and b_B were both 0, hence n = 0, nk = 0 and -vmass/nk = inf/nan.
check_sa's 'matrix' mode only reads the first event, which is why the
single point agreed with fortran; a real multi event run did not. For
FD u u~ > w+ w- with BACKEND=cppsse4:
MeanMatrixElemValue = ( nan +- 0 ) -> ( 0.339313 +- 0.306598 )
and the multi event mean now agrees between cppnone and cppsse4 (checked
also on w+ w- > w+ w- and e+ e- > w+ w- z, where the two backends give the
same mean to the last printed digit).
Two more things in that block, while there:
- the gauge direction is now selected lane by lane with fpternary, like
define_gauge_dir does, instead of being combined arithmetically as
nA*b_A + nB*b_B. The division is by ppDENOM (already defined above,
=1 where pp==0): a select discards the value of the branch not taken,
an arithmetic combination does not, since nan*0 is nan;
- nhel is a scalar, so the choice between the |nhel|=1 and the
longitudinal case is a plain if, not a cxternary on a bool_v built from
a single value (which, again, would only have set the first lane).
Checked in situ with a driver calling vxxxxx on a register holding a boson
at rest next to a moving one: every lane is finite, and each lane matches
what the scalar backend gives for that momentum. The single point |M|^2 of
the three FD processes is unchanged, and non-FD output is byte identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_short_multiple_lorentz_subset checked FFV1_2C1_0 for the wrapper form (a COMPLEX*16 TMP scratch and a call to each single coupling routine). It is now one merged expression, so the check becomes: no call at all, and both couplings in the same expression. The temporaries are left out of the comparison, since their names depend on what the kernel contracted before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ther There was an FD gauge cross-backend test already (test_standalone_cpp_fd_output_consistency), but it cannot fail on a wrong value: its process gives matrix elements of 1e-15 down to 1e-21 while it asserts with atol=1e-7, so the comparison is satisfied by any pair of numbers, including 0 against anything. That is how three madmatrix FD bugs reached the released templates. Two numerical tests, both verified to fail on the bug they are meant to hold (and to pass once it is fixed): - test_standalone_mg7_fd_vs_fortran: u u~ > w+ w- in FD gauge, madmatrix against the fortran standalone, per flavour, with a relative tolerance and no absolute floor. The W's give a longitudinal polarisation (hence the Goldstone component), the massless initial state makes both drivers evaluate the same RAMBO point, and |M|^2 is O(1e-3). It is run once per madmatrix backend, since helas_fd.h writes the FD wavefunctions once for the scalar types and once for the vector ones and which one a plain 'make' builds depends on the host. The magnitude is asserted too, so the test cannot quietly become vacuous the way the existing one did. Catches the Goldstone sign bug: 0.005076 vs 0.005109. - test_standalone_mg7_fd_simd_lanes: the mean matrix element over a multi-event run must be the same for a scalar and for a vectorised build. 'matrix' mode reports the first event only, so it cannot see a routine that is right in the first lane and wrong in the others; 'perf' mode covers them all. Catches the lane-broadcast bug: 0.131645 vs 0.131656 (it gave nan before the surrounding code used selects). And two IO tests, one per backend, holding the FD output itself: IOTestFDGauge covers the generated DHELAS routines (the verbatim template copy aloha_functions.f is vetoed) and madmatrix's HelAmps_sm.h, which carries both the generated routines and the FD helpers. This is what pins down the third bug, the gauge direction that differed between the scalar and the vector branch: n is a gauge choice, no matrix element can see it, only a reference file can. Verified by reintroducing it: the madmatrix IO test fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FDgauge_standalone_fortran held the whole generated DHELAS, 13 files whose content largely repeats: the FD specifics are the propagator factor written into every offshell routine and the merged/assembled combinations. Keep FFV6M_3.f alone -- it carries both, the inlined propagator factor and FFV6_2M_3, a combination merged into one expression -- and drop the rest. Checked that the reduced reference still moves for an edit to either half: perturbing the fortran FD template and the inlined propagator emitted by the writer both make it fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Merged multi-coupling routines
ALOHAWriterForFortran.write_combinedused to writeFFV2_4_3& co as a wrapper: one call per Lorentz structure into a<type>tmp, then a component-by-component accumulation. Such a routine has no expression of its own — no contracted temporaries, no propagatorDENOM— which costs a call plus an accumulation loop per use, and leaves nothing for the helicity-recycling batching to hoist out of its loop.The combined routine is now computed like any other one: its body is the sum of the structures, each multiplied by its own coupling, sharing the momenta, the propagator denominator and the contracted temporaries. This re-uses
CombineRoutineBuilder, i.e. the mechanism the loop (explicit-combine) routines already go through, and the writing goes through the standardwrite()path. The wrapper stays aswrite_combined_wrapperfor the cases where the merge is not possible, and is reachable withMG_ALOHA_COMBINE_WRAPPERfor A/B checks.madmatrix already merged these (
explicit_combine=Trueinexport_cpp), so this brings the two backends in line.FD gauge
Two things made FD routines the exception, on both backends:
multiply_propagator_factorwas called at the end of every offshell V/S routine, recomputing for each helicity the part that only depends on the momentum (the 5-momentumq, the gauge directionn,n.q);w+ w- > w+ w-).Both are now written like a single-coupling routine: the propagator factor is emitted in the routine (
q/n/n.qwith the momenta,js1/js2and the update after the components), and the mixed-spin combinations are assembled term by term, sharing the momenta, the declarations, the contracted temporaries and one propagator factor.define_gauge_dirstays a call: it is a pure function of the momentum and sits in the invariant part.For madmatrix the scratch wavefunction disappears with the wrapper, so the
FIXME: hack to avoid a bug in the FD codethat fedaloha_obj_tmp/amp_tmp_fpat the call site goes away too.w+ w- > w+ w-in FD keeps 17 calls in its DHELAS (allDEFINE_GAUGE_DIR), down from 48; itsHelAmps_sm.hgoes from 75 scratch references and 34 inner calls to 0.Three pre-existing madmatrix bugs found on the way
Independent of the above, in
aloha/template_files/madmatrix/helas_fd.h:vxxxxx, scalar backends — the FD Goldstone component of a longitudinal massive vector had the wrong sign (+nsv*cIinstead of-nsv*cI, against fortranvc%W(5) = -nsv*ciand the python and SIMD implementations). Every matrix element with a longitudinal massive vector was ~0.7% off withBACKEND=cppnone, which is also whatcppautofalls back to without SIMD. FDu u~ > w+ w-went from5.0762980332466311e-03to5.1093407366301505e-03, against5.1093405664217962e-03for the fortran standalone at the same phase-space point.vxxxxx, SIMD backends — the FD block built its vector constants asconst fptype_sv one{1.}, which sets the first lane and zero-fills the others. Everything selected with it was right in lane 0 only; the other lanes gotn = 0,nk = 0and-vmass/nk= nan.check_sa matrixreads event 0, so single points looked fine, but a real multi-event run did not:MeanMatrixElemValue = ( nan +- 0 )becomes( 0.339313 +- 0.306598 ), and the multi-event mean now agrees betweencppnoneandcppsse4. The gauge direction there is now selected lane by lane withfpternary(dividing by the existingppDENOM) instead of being combined arithmetically, sincenan*0isnan; and the|nhel|=1choice is a plainif,nhelbeing a scalar.define_gauge_dir— the scalar and SIMD branches disagreed onn[3]when the propagator has no spatial momentum.nis a gauge direction, so no matrix element changes (verified), but the choice should not depend on the backend; the SIMD branch now follows fortran, python and the scalar branch.Speed
Fortran standalone, best of 5 runs of
./check 1000 N:p p > e- ve~ mu+ vm QCD=0u u~ > w+ w-e+ e- > e+ e- zg g > t t~ g(control, no combined routine)u u~ > w+ w-w+ w- > w+ w-madmatrix FD
w+ w- > w+ w-(check_sa.exe perf): 24053 to 49487 evt/s, 2.06x. ALOHA generation costs 3-8% more on the processes measured.Validation
FFV2_4,FFV2_5,FFV1_2_4,FFS1_2,VVVV1_3_4(3 couplings), plus theC1,P0andP1Ntags — max 1e-15 relative, i.e. the reassociation of the sum. Same check on the 48 FD routines ofw+ w- > w+ w-ande+ e- > e+ e- z.check_sa, worst 7e-15 — includingp p > z j j(7 subprocesses, 30 flavour matrix elements), MSSM, heft and decay chains. FD: bit-identical foru u~ > w+ w-,w+ w- > w+ w-,u u~ > z z,e+ e- > e+ e- z, 1 ulp fore+ e- > w+ w- z.cppnoneandcppsse4built and compared, inmatrixandperfmode;vxxxxxdriven directly on a SIMD register holding a boson at rest next to a moving one, every lane finite and equal to the scalar backend.p p > e+ e- [QCD]generates and compiles, including the quadruple-precisionMP_FFV2_4_3; loop routines keep their own explicit-combine path.test_madevent_ufo_aloha,test_madevent_ufo_aloha_merged,test_mg7_ufo_aloha,test_standalone_madevent_consistency(9),test_madevent_flavor_zud/zjj,test_madevent_merged_flavor_uq,test_single_qcd_order_mg7,test_group_subprocess_mg7all green. TheFFV2_4_3reference text incheck_aloha_fileis updated to the merged routine.Not covered:
cppavx2/cpp512*and CUDA (no GPU here), and the plainALOHAWriterForCPP(standalone_cpp) backend, which still emits the FD wrapper.🤖 Generated with Claude Code