Skip to content

Restore vectorized PRelu for per-channel slopes on CPU - #33996

Open
Chinmay Shrivastava (JonSnow1807) wants to merge 2 commits into
microsoft:mainfrom
JonSnow1807:fix-prelu-per-channel-perf-33831
Open

Chinmay Shrivastava (JonSnow1807) wants to merge 2 commits into
microsoft:mainfrom
JonSnow1807:fix-prelu-per-channel-perf-33831

Conversation

@JonSnow1807

Copy link
Copy Markdown

Description

Fixes #33831. Since #28750, the float PRelu span functors use a per-element ternary to keep x * slope from turning ±inf into NaN. The ternary doesn’t vectorize, so PRelu with a per-channel slope became ~21x slower. This replaces both functors with Eigen select(), which keeps #28750’s inf/NaN behaviour and vectorizes again.

Motivation and Context

Measured with the issue’s repro (4-D input, per-channel slope), one thread, taskset on one core, median of 9 fresh processes, GCC 14, x86-64: 1345 µs → 65 µs. The same-shape slope case goes 1349 µs → 63 µs. Output is bit-exact against numpy.where(x >= 0, x, slope * x) including inf, NaN and -0. New test ActivationOpTest.PRelu_MultiChannel4D_SpecialValues; the PRelu, activation and broadcast tests pass, 548 tests, 0 failures. MSVC was not measured.

AI usage: code drafted by both human and AI, and reviewed by human.

microsoft#28750 replaced the branch-free PRelu formula with a per-element scalar
ternary loop to stop +/-inf inputs producing NaN. The loop is not
vectorized, which made per-channel PRelu (for example X [1,64,56,56] with
slope [64,1,1]) roughly 20x slower on the CPU EP.

Express the same per-lane selection with Eigen select() in the two span
functors. select() evaluates both arms and keeps one, never adding them, so
a non-finite value in the discarded arm cannot leak into the result and the
inf/NaN behavior from microsoft#28750 is unchanged.

Add a per-channel test with spans longer than a SIMD packet and inf, NaN and
signed-zero inputs.

Fixes microsoft#33831
Copilot AI balanced review requested due to automatic review settings November 9, 2026 02:50
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The signed-zero tests can miss incorrect output signs because they use EXPECT_NEAR and only positive slopes, requiring explicit signbit checks and a negative-slope case.

1 open issue
What changed in this PR

Restores vectorized CPU float PRelu processing to address #33831’s performance regression while preserving the infinity and NaN handling introduced by #28750.

Changes:

  • Replaces scalar loops with Eigen select() for scalar-slope and elementwise-slope spans.
  • Adds a multi-channel 4-D regression test with finite values, infinities, NaNs, and signed zeros.
File Description
onnxruntime/​test/​providers/​cpu/​activation/​activation_op_test.cc Adds per-channel special-value coverage across vector-length spans.
onnxruntime/​core/​providers/​cpu/​math/​element_wise_ops.cc Uses Eigen selection to restore vectorization in two PRelu broadcast paths.

Review effort: Balanced

Comment on lines +686 to +701
const std::vector<float> slopes{0.5f, 2.0f};

std::vector<float> inputs;
std::vector<float> outputs;
const float specials[] = {pos_inf, neg_inf, nan_val, 0.0f, -0.0f, 3.0f, -3.0f};
for (int i = 0; i < 50; ++i) {
const float x = (i % 3 == 0) ? specials[(i / 3) % 7] : static_cast<float>(i) - 25.0f;
inputs.push_back(x);
// NaN compares false, so it takes the multiply branch and stays NaN.
outputs.push_back(x >= 0.0f ? x : x * slopes[i / 25]);
}

OpTester test("PRelu");
test.AddInput<float>("X", x_dims, inputs);
test.AddInput<float>("slope", slope_dims, slopes);
test.AddOutput<float>("Y", x_dims, outputs);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the float checker treats +0 and -0 as equal and both slopes were positive. The second slope is now negative, and a custom verifier checks every value (NaN, exact infinity, the rest with EXPECT_FLOAT_EQ) plus, on the CPU EP, the sign of each zero output. The sign check is CPU-only because CUDA's PRelu (a > 0 ? a : a * b) gives -0 for a +0 input with a negative slope. I checked that the test fails when the kernel gets the sign of zero wrong and passes with this PR.

@JonSnow1807

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

The default float checker compares with EXPECT_NEAR, so +0 and -0 are
equal and a kernel that multiplies a zero input by the slope passed the
test. Make the second slope negative so such a multiply flips the sign,
and verify the output in a custom verifier: shape, NaN, exact infinity,
the other values with EXPECT_FLOAT_EQ, and on CPU the sign of each zero.

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.

[Performance] PRelu (CPU EP, float, per-channel slope) is ~21x slower since 1.27.0 (#28750)

2 participants