Repository navigation
perf: add contiguous fast path to complex add/subtract fallback loop - #278
intel-python-devops wants to merge 2 commits into
Conversation
|
unsure if auto-generated PRs like this should be putting these changes into the CHANGELOG or not. Thoughts? |
Tough call, I would say ideally yes but I would want to make sure we have some kind of anti-AI-slop-text skill or guard before that |
| *oi @OP@= ri; | ||
| return; | ||
| } | ||
| else if (contig) { |
There was a problem hiding this comment.
Wrong results for complex accumulate/cumsum in GCC builds.
What NumPy sends this branch:
np.add.accumulateandnp.cumsumcall the inner loop without = in1 + 1element and all strides contiguous (NumPy v2.5.3 ufunc_object.c L3043–3078).- So contig is
1andcan_vectorizeis 0. That lands in the new branch, where each output depends on the result two scalars earlier.
Why that breaks under GCC:
NPY_PRAGMA_VECTORbecomes#pragma GCC ivdep, which promises the compiler there is no such dependency. GCC then drops its runtime overlap check:-fopt-info-vecshows the new loop vectorized with no versioning, while the old loop was "versioned … because of possible aliasing".- The three existing uses of this pragma (lines 355, 514, 668) all run behind
DISJOINT_OR_SAMEchecks.fast_loop_macros.h:205says ivdep must only be used after that kind of check. The new comment itself says operands may be "overlapping".
The repro: a GCC build of the PR gives mkl_umath.add.accumulate on complex64 [1..8] = [1, 3, -3.62, 0.38, 5, 11, 7, 15]. Expected is [1, 3, 6, 10, …]. The result includes uninitialized output memory, so it's nondeterministic.
Shipped builds use icx, so they're fine today. But meson.build accepts GCC, the README says "a C compiler", and a plain pip install . on Linux would use GCC.
| else if (contig) { | |
| else if (can_vectorize) { |
This matches the other three pragma sites and the overlap guard NumPy itself uses before its fast path. It still covers np.add(Z, C, Z).
There was a problem hiding this comment.
Reproduced on GCC 9.4. Fixed: now gated on can_vectorize, pragma removed. test_accumulate_complex covers it.
| *oi @OP@= ri; | ||
| return; | ||
| } | ||
| else if (contig) { |
There was a problem hiding this comment.
We need a real benchmark numbers in the PR before it merges, including both out-of-place with no overlap and in-place paths
There was a problem hiding this comment.
Added to the description. In-place numbers showed a regression on icx (0.54–0.66x), so in-place calls now get their own loops (1.2–2.1x).
| *oi @OP@= ri; | ||
| return; | ||
| } | ||
| else if (contig) { |
There was a problem hiding this comment.
The PR text and code comment don't match what was measured:
- "Bit-for-bit identical for every stride, overlap…": false.
- "Lets the compiler autovectorize": only true for complex64. For complex128, icx reports "seems inefficient" and only unrolls the loop. This adds 2 new -Wpass-failed warnings for the CDOUBLE add and subtract loops.
- No measurements: the PR gives no benchmark numbers.
np.add(Z, C, Z)isn't an overlap case: the output is the same array as an input, which can_vectorize already allows.
There was a problem hiding this comment.
Agreed. Description and comment are rewritten. Removing the pragma also removes the two -Wpass-failed warnings, and complex128 gains are attributed to unroll + SLP.
| *oi @OP@= ri; | ||
| return; | ||
| } | ||
| else if (contig) { |
There was a problem hiding this comment.
Missing tests coverage for the new branch
There was a problem hiding this comment.
Added test_complex_contig and test_accumulate_complex.
| const @ftype@ *ip1 = (@ftype@ *)args[0]; | ||
| const @ftype@ *ip2 = (@ftype@ *)args[1]; | ||
| @ftype@ *op1 = (@ftype@ *)args[2]; |
There was a problem hiding this comment.
| const @ftype@ *ip1 = (@ftype@ *)args[0]; | |
| const @ftype@ *ip2 = (@ftype@ *)args[1]; | |
| @ftype@ *op1 = (@ftype@ *)args[2]; | |
| const @ftype@ *ip1 = (const @ftype@ *)args[0]; | |
| const @ftype@ *ip2 = (const @ftype@ *)args[1]; | |
| @ftype@ *op1 = (const @ftype@ *)args[2]; |
There was a problem hiding this comment.
Applied to ip1/ip2. op1 stays non-const since it's written through.
| *oi @OP@= ri; | ||
| return; | ||
| } | ||
| else if (contig) { |
There was a problem hiding this comment.
Need to populate the changelog
- Take the flat loop only when operands are disjoint or exactly in-place; accumulate (out == in1 + 1) goes back to the strided loop. Under GCC, NPY_PRAGMA_VECTOR (ivdep) gave wrong accumulate results. - Drop the pragma; it did not change icx codegen and added two -Wpass-failed warnings for CDOUBLE. - Give out == in1 and out == in2 their own loops so the runtime alias check does not send in-place calls to the scalar fallback. - Add tests for the contiguous and accumulate paths, and a changelog entry.
Note
This pull request is AI-generated. Please review thoroughly.
Adds a contiguous fast path to the complex
add/subtractloops (CFLOAT,CDOUBLE) for calls that don't take the oneMKL VM path, i.e. contiguous arrays withn <= VML_ASM_THRESHOLD(100000). Before this change those calls fell through to the stridedBINARY_LOOP.can_vectorize: contiguous, and each input either disjoint from the output or exactly the output. Partially overlapping operands, such asaccumulate/cumsum(out == in1 + 1), still takeBINARY_LOOP.2*nscalars. Since +/- is componentwise and each element is computed independently, results are identical to the previous loop for the layouts this branch accepts.out is in1andout is in2get their own two-pointer loops. With a single three-pointer loop, the compiler's runtime alias check seesout == inas an overlap and runs its scalar fallback, which made in-place calls slower thanmain(icx, complex64: 0.54–0.66x at n = 1k–100k).NPY_PRAGMA_VECTORis#pragma GCC ivdepunder GCC, which is unsafe for partial overlap. Under icx it did not change codegen and added two-Wpass-failedwarnings forCDOUBLE. The icx warning count is now the same asmain(91).Codegen (icx 2026.1.1,
-qopt-report=3, conda-forge CFLAGS-march=nocona -mtune=haswell -O2):Tests:
test_complex_contigcovers out-of-place,out=aandout=bat n = 1, 7, 100, 1001 with a bitwise comparison to NumPy.test_accumulate_complexcovers the partial-overlap path; it fails on a GCC build of the previous revision of this PR (acced6b) and passes with this one.