This proposal focuses on arith.maxf, arith.minf, vector.reduction <maxf>, and vector.reduction <minf> in MLIR. We have identified certain issues that require attention, which are outlined in the following sections:
Problem 1: Missing Semantics and Confusing Naming
In LLVM, there are two ways to find the minimum and maximum of two floating-point numbers: minnum/maxnum intrinsics and minimum/maximum intrinsics [1, 2]. Each pair exhibits different behavior when dealing with NaNs and +0.0/-0.0. The former returns the non-NaN argument when one of the arguments is NaN and does not distinguish between +0.0 and -0.0, whereas the latter propagates the NaN and does distinguish between +0.0 and -0.0. The same applies to the reduction variants [9, 10].
In MLIR, there is currently a single way to represent the minimum and maximum of two floating-point numbers: arith.minf/arith.maxf. The semantics of these operations adhere to the semantics of minimum/maximum intrinsics in LLVM [3]. The same applies to the reduction variants in the Vector dialect [11]. However, there are no variants in MLIR to model the semantics of LLVM’s minnum/maxnum.
Proposed Solution
We propose to rename arith.minf/arith.maxf to arith.minimum/arith.maximum to align their names with their LLVM counterparts. We would also like to introduce the corresponding arith.minnum and arith.maxnum operations to mirror the semantics of the minnum/maxnum intrinsics in LLVM. The same applies to the vector reductions operations in the Vector dialect: vector.reduction <maxf> and vector.reduction <minf>.
Problem 2: arith.minf and arith.maxf lowering to LLVM
Currently, there is a bug in the lowering of arith.maxf and arith.minf to LLVM. Although their semantics [3, 4] clearly indicate that NaN should be propagated and +0.0/-0.0 should be differentiated, they are lowered to minnum/maxnum intrinsics without the proper handling of the cases described above [5]. Interestingly enough, the lowering of their vector reduction variants seems to have always been aligned with the described semantics [6] even before it was changed to the proper intrinsic lowering.
Proposed Solution
We plan to fix the existing bug by changing the lowering of arith.maxf and arith.minf from the minnum/maxnum intrinsics to the maximum/minimum intrinsics in LLVM.
Problem 3: Vector reductions lowering to SPIR-V
A quite similar bug can be identified in a distinct combination of operations and lowerings: vector reduction operations and their corresponding SPIR-V lowerings. While the lowerings utilize spirv.CL.fmax and spirv.CL.fmin operations, the documentation specifies that they are intended to exhibit behavior similar to the llvm.m**num intrinsics [7]. However, there are no additional operations inserted to rectify the behavior as previously done in similar cases [8].
Proposed Solution
We’re going to fix the bug by adding extra operations to make sure the right meaning is carried through, just like how it was done before for the LLVM lowering [6].
Summary
State before the changes
| Operation | LLVM lowering | SPIR-V lowering |
|---|---|---|
arith.maxf |
llvm.maxnum without enforcing the desired semantic |
|
arith.minf |
llvm.minnum without enforcing the desired semantic |
|
vector.reduction <maxf> |
llvm.vector.reduce.fmaximum |
spirv.CL.fmaxes without enforcing the desired semantic |
vector.reduction <minf> |
llvm.vector.reduce.fminimum |
spirv.CL.fmins without enforcing the desired semantic |
State after the changes
| Operation | Desired LLVM lowering | Desired SPIR-V lowering |
|---|---|---|
arith.maximumf |
llvm.maximum |
Current arith.maxf lowering |
arith.minimumf |
llvm.minimum |
Current arith.minf lowering |
arith.maxnumf |
llvm.maxnum |
spirv.CL.fmax spirv.GL.FMax + additional checks to propagate non-NaN (*) |
arith.minnumf |
llvm.minnum |
spirv.CL.fmin spirv.GL.FMin + additional checks to propagate non-NaN (*) |
vector.reduction <maximumf> |
llvm.vector.reduce.fmaximum |
sequence of spirv.CL.fmaxes + additional checks to propagate NaN |
vector.reduction <minimumf> |
llvm.vector.reduce.fminimum |
sequence of spirv.CL.fmins + additional checks to propagate NaN |
vector.reduction <maxf> |
llvm.vector.reduce.fmax |
sequence of spirv.CL.fmaxes |
vector.reduction <minf> |
llvm.vector.reduce.fmin |
sequence of spirv.CL.fmins |
* A note on SPIR-V lowerings for Arith operations with m**num intrinsics
There are two lowerings from the Arith dialect to SPIR-V: spirv.CL and spirv.GL. The spirv.CL operations behave similarly to the llvm.m**num intrinsics when it comes to handling NaNs. However, the spirv.GL operations have undefined results when one of the operands is NaN. To ensure consistent semantics, additional operations should be inserted in the spirv.GL lowering to enforce the correct behavior, like the current lowerings but with the different intent.
Work breakdown
1 Arith dialect
1.1 Change the lowering to llvm.m **imum intrinsics for arith.m**f operations.
1.2 Rename arith.m**f to arith.m**imumf.
1.3 Add arith.m**numf operations.
1.4 Add arith.m**numf LLVM lowerings.
1.5 Add arith.m**numf SPIR-V lowerings.
2 Vector dialect
2.1 Rename vector.reduction <m**f> to vector.reduction <m**imumf>
2.2 Fix SPIRV lowering for vector.reduction <m**imumf> to propagate NaNs.
2.3 Add vector.reduction <m**f> operations.
2.4 Add vector.reduction <m**f> LLVM lowerings.
2.5 Add vector.reduction <m**f> SPIR-V lowerings.
References
llvm.maxnumintrinsicllvm.maximumintrinsicarith.maxfReferencearith.minfReferencearith.maxfandarith.minfLLVM Conversion tests- Vector Reduction example with explicit enforcement of semantics
spirv.CL.fmaxReference- Vector Reduction operations SPIR-V lowering tests
llvm.vector.reduce.fmaxintrinsicllvm.vector.reduce.fmaximumintrinsic- The implementation of the vector reduction intrinsic has always been aligned with the
llvm.vector.reduce.fm**imumintrinsics [6].
Authors: @dcaballe and @unterumarmung
CC: @kuhar, @mehdi_amini, @nicolasvasilache, @ftynse and @banach-space