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 |
Follows the semantics |
arith.minf |
llvm.minnum without enforcing the desired semantic |
Follows the semantics |
vector.reduction <maxf> |
llvm.vector.reduce.fmaximum |
sequence of spirv.CL.fmax es without enforcing the desired semantic |
vector.reduction <minf> |
llvm.vector.reduce.fminimum |
sequence of spirv.CL.fmin s 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.fmax es + additional checks to propagate NaN |
vector.reduction <minimumf> |
llvm.vector.reduce.fminimum |
sequence of spirv.CL.fmin s + additional checks to propagate NaN |
vector.reduction <maxf> |
llvm.vector.reduce.fmax |
sequence of spirv.CL.fmax es |
vector.reduction <minf> |
llvm.vector.reduce.fmin |
sequence of spirv.CL.fmin s |
* 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.maxnum
intrinsicllvm.maximum
intrinsicarith.maxf
Referencearith.minf
Referencearith.maxf
andarith.minf
LLVM Conversion tests- Vector Reduction example with explicit enforcement of semantics
spirv.CL.fmax
Reference- Vector Reduction operations SPIR-V lowering tests
llvm.vector.reduce.fmax
intrinsicllvm.vector.reduce.fmaximum
intrinsic- The implementation of the vector reduction intrinsic has always been aligned with the
llvm.vector.reduce.fm**imum
intrinsics [6].
Authors: @dcaballe and @unterumarmung
CC: @kuhar, @mehdi_amini, @nicolasvasilache, @ftynse and @banach-space