Skip to content

Fix real-space symmetry filtering in GPU contractions - #16

Merged
PeizeLin merged 11 commits into
abacusmodeling:masterfrom
Stardust0831:fix/gpu-symmetry-upstream
Oct 24, 2026
Merged

PeizeLin merged 11 commits into
abacusmodeling:masterfrom
Stardust0831:fix/gpu-symmetry-upstream

Conversation

@Stardust0831

@Stardust0831 Stardust0831 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

GPU contractions with real-space symmetry can abort on a valid contraction label or lose contributions. GPU batching uses a different loop order from CPU contractions, so the shared symmetry callbacks must identify the actual output atom pair and relative cell at each call site.

The fix addresses three cases:

  • Add the missing GPU callback cases. Defer screening while only an intermediate atom is known, then check the output pair when both atoms are available.
  • Correct the argument order in four a01b01_a2b2 contractions so the callback screens (Aa01, Ab01).
  • In four a01b2_a2b01 contractions, move the check inside the Ab01 loop. Screening intermediate Aa2 as an output atom had discarded valid terms from the sum.

The first two changes backport maki49's initial fix and extension, preserving authorship. The additional intermediate-sum correction has a numerical regression covering all 18 contraction labels and their combined sum, real/complex tensors, periodic cells, restricted/empty output and seeded accumulation. Its reference is an unrestricted CPU contraction with an independent mask on the final output keys.

Both CPU implementations retain their existing filtering rules. For the newly supported label/argument-type combinations, CPU calls different overloads; the symmetry delta leaves the CPU contraction loops unchanged.

This PR includes the accumulator and atom-list corrections from #15. Symmetry changes relative to #15.

The original ABACUS failure was:

terminate called after throwing an instance of 'std::invalid_argument'
  what(): label a0b0_a1b1 error in .../Filter_Atom_Symmetry.h line 49
Signal: Aborted (6)

Original before/after records show four GPU-symmetry ABACUS cases aborting before the fix and passing after it. Among the 18 standalone contractions, the original version had 10 exceptions and four incorrect outputs; the two backports alone left four incorrect sums; the complete fix passes all 18.

The version merged with current master and #15 (7025df6) was validated again:

Check Result
Symmetry regression: CPU-only and GPU-enabled builds, real/complex tensors, 1/4 threads All four runs pass, covering both CPU implementations and GPU execution
Accumulation: float/double, real/complex, CPU-only/GPU-enabled, 1/4 threads 16/16 runs pass
Hartree reference, atom-list regressions, exchange/force/stress comparisons 6/6 runs pass
Complete ABACUS GPU build, including BSE Pass
ABACUS: five solver/RI/symmetry configurations × real/complex × one/two MPI ranks 20/20 calculations pass repository checks and CPU/GPU comparisons

Hardware: NVIDIA V100-SXM2 16 GB GPUs, GCC 13.3/OpenMP and DDLA v0.0.4/CUDA. ABACUS b30b66a5a uses only the test patch removing its GPU-symmetry rejection so execution reaches LibRI; no LibRI patches are applied. The ABACUS fixture has zero forces by symmetry, so it does not establish nonzero-force behavior. MAGMA, BSE numerical results and multi-node execution were not tested.

Complete merged-version build and runtime logs. Standalone reproduction. Related: deepmodeling/abacus-develop#8169.

Stardust0831 and others added 5 commits October 23, 2026 09:54
Apply fac_add_Ds while merging downloaded GPU contributions through the
existing tensor accumulation helper. Remove the unused GPU result map
and locks; the result merge runs outside the OpenMP packing regions.

Add regression coverage for signed, zero and fractional coefficients,
seeded outputs, repeated calls and combined contraction labels.
(cherry picked from commit 2f77684)
(cherry picked from commit 87f521f)
(cherry picked from commit a0db48f)
(cherry picked from commit dc10847)
In the four a01b2_a2b01 contractions, Aa2 is an intermediate atom
summed into the (Aa01, Ab01) output. Applying filter_for2 to
(Aa01, Aa2) drops valid terms. Apply it once Ab01 is available.

Add standalone CPU/DDLA regression coverage against unfiltered CPU
contractions masked independently by output atom pairs and cells.
Cover all 18 labels and their sum, real/complex tensors, empty and
seeded outputs, and signed/zero/fractional increments.

Validated CPU-only and DDLA v0.0.4/CUDA builds on V100 with 1 and 4
OpenMP threads. The two preceding symmetry fixes are cherry-picked
from maki49; this regression additionally catches the four remaining
incorrect contraction paths.

(cherry picked from commit 2781215)
@maki49

maki49 commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Thanks for completing the symmetry fix. The GPU loop-order table below highlights the last four a01b2_a2b01 cases:

Stage-2 loop Output block
Aa01 → Aa2 → Ab01 (Aa01, Ab01)

My backports had mistakenly marked the early filter_for2(Aa01, Aa2) as a no-op. It actually screens the intermediate Aa2 mistakenly. Moving the check inside the Ab01 loop closes this gap. I support this PR and have applied the later fix to my local branch.

The following table shows the behavior of symmetry filters comprehensively for all 18 labels in cal_loop3_GPU.

label output 1st loop 2nd loop 3rd loop
a0b0_a1b1 (Aa2, Ab2) Aa01 → Ab01 → Ab2 ① f1(Aa01) → no-op;② f2(Aa01,Ab01) → no-op;③ f31(Aa01,Ab01,Ab2) → no-op Aa01 → Aa2 → Ab2 ① f1(Aa01) → no-op;② f2(Aa01,Aa2) → no-op;③ f32(Aa01,Aa2,Ab2) → output (Aa2, Ab2) —
a0b1_a1b0 (Aa2, Ab2) Aa01 → Ab01 → Ab2 ① f1(Aa01) → no-op;② f2(Aa01,Ab01) → no-op;③ f31(Aa01,Ab01,Ab2) → no-op Aa01 → Aa2 → Ab2 ① f1(Aa01) → no-op;② f2(Aa01,Aa2) → no-op;③ f32(Aa01,Aa2,Ab2) → output (Aa2, Ab2) —
a0b0_a1b2 (Aa2, Ab01) Ab01 → Ab2 → Aa01 ① f1(Ab01) → second atom Ab01;② f2(Ab01,Ab2) → no-op;③ f31(Aa01,Ab01,Ab2) → no-op Aa01 → Ab01 → Aa2 ① f1(Aa01) → no-op;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Aa2,Ab01) → output (Aa2, Ab01) —
a0b1_a1b2 (Aa2, Ab01) Ab01 → Ab2 → Aa01 ① f1(Ab01) → second atom Ab01;② f2(Ab01,Ab2) → no-op;③ f31(Aa01,Ab01,Ab2) → no-op Aa01 → Ab01 → Aa2 ① f1(Aa01) → no-op;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Aa2,Ab01) → output (Aa2, Ab01) —
a0b2_a1b0 (Aa2, Ab01) Ab01 → Ab2 → Aa01 ① f1(Ab01) → second atom Ab01;② f2(Ab01,Ab2) → no-op;③ f31(Aa01,Ab01,Ab2) → no-op Aa01 → Ab01 → Aa2 ① f1(Aa01) → no-op;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Aa2,Ab01) → output (Aa2, Ab01) —
a0b2_a1b1 (Aa2, Ab01) Ab01 → Ab2 → Aa01 ① f1(Ab01) → second atom Ab01;② f2(Ab01,Ab2) → no-op;③ f31(Aa01,Ab01,Ab2) → no-op Aa01 → Ab01 → Aa2 ① f1(Aa01) → no-op;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Aa2,Ab01) → output (Aa2, Ab01) —
a0b0_a2b1 (Aa01, Ab2) Aa01 → Aa2 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → no-op;③ f31(Aa01,Ab01,Aa2) → no-op Aa01 → Ab01 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Ab01,Ab2) → output (Aa01, Ab2) —
a0b1_a2b0 (Aa01, Ab2) Aa01 → Aa2 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → no-op;③ f31(Aa01,Ab01,Aa2) → no-op Aa01 → Ab01 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Ab01,Ab2) → output (Aa01, Ab2) —
a1b0_a2b1 (Aa01, Ab2) Aa01 → Aa2 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → no-op;③ f31(Aa01,Ab01,Aa2) → no-op Aa01 → Ab01 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Ab01,Ab2) → output (Aa01, Ab2) —
a1b1_a2b0 (Aa01, Ab2) Aa01 → Aa2 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → no-op;③ f31(Aa01,Ab01,Aa2) → no-op Aa01 → Ab01 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Ab01,Ab2) → output (Aa01, Ab2) —
a0b0_a2b2 (Aa01, Ab01) Aa01 → Aa2 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → no-op;③ f31(Aa01,Aa2,Ab2) → no-op Aa01 → Ab01 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Ab2,Ab01) → output (Aa01, Ab01) —
a0b1_a2b2 (Aa01, Ab01) Aa01 → Aa2 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → no-op;③ f31(Aa01,Aa2,Ab2) → no-op Aa01 → Ab01 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Ab2,Ab01) → output (Aa01, Ab01) —
a1b0_a2b2 (Aa01, Ab01) Aa01 → Aa2 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → no-op;③ f31(Aa01,Aa2,Ab2) → no-op Aa01 → Ab01 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Ab2,Ab01) → output (Aa01, Ab01) —
a1b1_a2b2 (Aa01, Ab01) Aa01 → Aa2 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → no-op;③ f31(Aa01,Aa2,Ab2) → no-op Aa01 → Ab01 → Ab2 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → no-op;③ f32(Aa01,Ab2,Ab01) → output (Aa01, Ab01) —
a1b2_a2b1 (Aa01, Ab01) Ab01 → Ab2 → Aa01 ① f1(Ab01) → second atom Ab01;② f2(Ab01,Ab2) → no-op;③ f31(Aa01,Ab01,Ab2) → no-op Aa01 → Aa2 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → mistakenly filtered, deleted in PR Aa2; ③ f2(Aa01,Ab01) → output (Aa01, Ab01), added in PR;f32(Aa01,Ab01,Aa2) → no-op Aa01 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → output (Aa01, Ab01)
a0b2_a2b0 (Aa01, Ab01) Ab01 → Ab2 → Aa01 ① f1(Ab01) → second atom Ab01;② f2(Ab01,Ab2) → no-op;③ f31(Aa01,Ab01,Ab2) → no-op Aa01 → Aa2 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → mistakenly filtered, deleted in PR Aa2; ③ f2(Aa01,Ab01) → output (Aa01, Ab01), added in PR;f32(Aa01,Ab01,Aa2) → no-op Aa01 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → output (Aa01, Ab01)
a0b2_a2b1 (Aa01, Ab01) Ab01 → Ab2 → Aa01 ① f1(Ab01) → second atom Ab01;② f2(Ab01,Ab2) → no-op;③ f31(Aa01,Ab01,Ab2) → no-op Aa01 → Aa2 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → mistakenly filtered, deleted in PR Aa2; ③ f2(Aa01,Ab01) → output (Aa01, Ab01), added in PR;f32(Aa01,Ab01,Aa2) → no-op Aa01 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → output (Aa01, Ab01)
a1b2_a2b0 (Aa01, Ab01) Ab01 → Ab2 → Aa01 ① f1(Ab01) → second atom Ab01;② f2(Ab01,Ab2) → no-op;③ f31(Aa01,Ab01,Ab2) → no-op Aa01 → Aa2 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Aa2) → mistakenly filtered, deleted in PR Aa2; ③ f2(Aa01,Ab01) → output (Aa01, Ab01), added in PR; f32(Aa01,Ab01,Aa2) → no-op Aa01 → Ab01 ① f1(Aa01) → first atom Aa01;② f2(Aa01,Ab01) → output (Aa01, Ab01)

PeizeLin and others added 4 commits October 24, 2026 14:21
(cherry picked from commit 6534722)
Use the right atom list to bound the density loop and the left atom list to select mu in the CVC loop. Add scalar-reference regressions for distinct and unequal atom partitions.
Copy the input when initializing an empty result with unit coefficient. Tensor assignment shares storage, so subsequent in-place additions otherwise change the input and other accumulators. Cover independent outputs and complex accumulation coefficients.
Upstream now contains the original PeizeLin commits already imported here. Keep the independent const-input accumulator and corrected Hartree atom-list indexing. The merged tree is identical to the previously validated PR head.
Integrate the upstream PeizeLin implementation and the validated accumulator and atom-list corrections while retaining all symmetry filtering changes.
@maki49

maki49 commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Why the CPU behavior is unchanged

Filter_Atom_Symmetry is shared by the CPU and GPU paths, but the new cases are selected by both the label and the argument types. The existing CPU loops never call these newly added label/overload combinations.

New cases in Filter_Atom_Symmetry Calls made by the CPU for those labels
Six a01b01_a01b01 / a01b01_a01b2 labels: filter_for1(TA) and filter_for32(TA, TAC, TAC) The CPU starts from Aa2 or Ab01 (TAC), so it calls filter_for1(TAC). Its later filter_for32 call starts with TAC as well.
Four a01b2_a2b01 labels: filter_for1(TAC) The CPU starts from Aa01 (TA) and calls filter_for1(TA).

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.

3 participants