From 3d50594a16b413143a57b8b425b398d6a45d8920 Mon Sep 17 00:00:00 2001 From: Yifan Hu <224434986+yhu1729@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:46:52 -0400 Subject: [PATCH 1/3] Support maximum reduction --- .../openmp/libs/pass/CGIntrinsicsOpenMP.cpp | 35 ++++++ .../openmp/libs/pass/CGIntrinsicsOpenMP.h | 30 +++-- src/numba/openmp/omp_grammar.py | 3 +- src/numba/openmp/omp_lower.py | 2 + src/numba/openmp/tests/test_max_reduction.py | 104 ++++++++++++++++++ 5 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 src/numba/openmp/tests/test_max_reduction.py diff --git a/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.cpp b/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.cpp index 489ccb0145c3..95592f686a58 100644 --- a/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.cpp +++ b/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.cpp @@ -314,6 +314,7 @@ OutlinedInfoStruct CGIntrinsicsOpenMP::createOutlinedFunction( case DSA_REDUCTION_ADD: case DSA_REDUCTION_SUB: case DSA_REDUCTION_MUL: + case DSA_REDUCTION_MAX: Reductions.push_back(V); break; default: @@ -490,6 +491,11 @@ OutlinedInfoStruct CGIntrinsicsOpenMP::createOutlinedFunction( OMPBuilder.Builder, AllocaIP, AI, ReductionTy, ReductionInfos, IsGPUTeamsReduction); break; + case DSA_REDUCTION_MAX: + Priv = CGReduction::emitInitAndAppendInfo( + OMPBuilder.Builder, AllocaIP, AI, ReductionTy, ReductionInfos, + IsGPUTeamsReduction); + break; default: FATAL_ERROR("Unsupported reduction"); } @@ -1288,6 +1294,11 @@ void CGIntrinsicsOpenMP::emitLoop(DSAValueMapTy &DSAValueMap, CGReduction::emitInitAndAppendInfo( OMPBuilder.Builder, OMPBuilder.Builder.saveIP(), Orig, VTy, ReductionInfos, false); + } else if (DSA == DSA_REDUCTION_MAX) { + ReplacementValue = + CGReduction::emitInitAndAppendInfo( + OMPBuilder.Builder, OMPBuilder.Builder.saveIP(), Orig, VTy, + ReductionInfos, false); } else FATAL_ERROR("Unsupported privatization"); @@ -1773,6 +1784,7 @@ void CGIntrinsicsOpenMP::emitOMPOffloadingMappings( case DSA_REDUCTION_ADD: case DSA_REDUCTION_SUB: case DSA_REDUCTION_MUL: + case DSA_REDUCTION_MAX: case DSA_MAP_TOFROM: MapType = OMP_TGT_MAPTYPE_TO | OMP_TGT_MAPTYPE_FROM; if (IsTargetRegion) @@ -2917,6 +2929,21 @@ Value *CGReduction::emitOperation(IRBuilderBase &IRB, FATAL_ERROR("Unsupported type for reduction operation"); } +template <> +Value *CGReduction::emitOperation(IRBuilderBase &IRB, + Value *LHS, Value *RHS) { + Type *VTy = RHS->getType(); + if (VTy->isIntegerTy()) { + Value *Cmp = IRB.CreateICmpSGT(LHS, RHS, "red.max.cmp"); + return IRB.CreateSelect(Cmp, LHS, RHS, "red.max"); + } + if (VTy->isFloatTy() || VTy->isDoubleTy()) { + Value *Cmp = IRB.CreateFCmpOGT(LHS, RHS, "red.max.cmp"); + return IRB.CreateSelect(Cmp, LHS, RHS, "red.max"); + } + FATAL_ERROR("Unsupported type for maximum reduction operation"); +} + // OpenMP 5.1, 2.21.5, sub is the same as add. template <> Value *CGReduction::emitOperation(IRBuilderBase &IRB, @@ -2950,3 +2977,11 @@ InsertPointTy CGReduction::emitAtomicOperationRMW( IRBuilderBase &IRB, Value *LHS, Value *Partial) { return emitAtomicOperationRMW(IRB, LHS, Partial); } + +template <> +InsertPointTy CGReduction::emitAtomicOperationRMW( + IRBuilderBase &IRB, Value *LHS, Value *Partial) { + IRB.CreateAtomicRMW(AtomicRMWInst::Max, LHS, Partial, MaybeAlign(), + AtomicOrdering::Monotonic); + return IRB.saveIP(); +} diff --git a/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.h b/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.h index 7d36d4b8848c..77eb5d869a80 100644 --- a/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.h +++ b/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.h @@ -35,6 +35,7 @@ enum DSAType { DSA_REDUCTION_ADD, DSA_REDUCTION_SUB, DSA_REDUCTION_MUL, + DSA_REDUCTION_MAX, DSA_MAP_ALLOC, DSA_MAP_TO, DSA_MAP_FROM, @@ -98,6 +99,7 @@ static const DenseMap StringToDSA = { {"QUAL.OMP.REDUCTION.ADD", DSA_REDUCTION_ADD}, {"QUAL.OMP.REDUCTION.SUB", DSA_REDUCTION_SUB}, {"QUAL.OMP.REDUCTION.MUL", DSA_REDUCTION_MUL}, + {"QUAL.OMP.REDUCTION.MAX", DSA_REDUCTION_MAX}, {"QUAL.OMP.MAP.ALLOC", DSA_MAP_ALLOC}, {"QUAL.OMP.MAP.TO", DSA_MAP_TO}, {"QUAL.OMP.MAP.FROM", DSA_MAP_FROM}, @@ -129,6 +131,8 @@ inline std::string toString(const DSAType &DSA) { return "DSA_REDUCTION_SUB"; case DSA_REDUCTION_MUL: return "DSA_REDUCTION_MUL"; + case DSA_REDUCTION_MAX: + return "DSA_REDUCTION_MAX"; case DSA_MAP_ALLOC: return "DSA_MAP_ALLOC"; case DSA_MAP_TO: @@ -346,6 +350,7 @@ struct CGReduction { switch (ReductionOperator) { case DSA_REDUCTION_ADD: case DSA_REDUCTION_SUB: + case DSA_REDUCTION_MAX: return emitAtomicOperationRMW(Builder, LHS, Partial); break; case DSA_REDUCTION_MUL: @@ -373,13 +378,25 @@ struct CGReduction { Type *ReductionTy, SmallVectorImpl &ReductionInfos, bool IsGPUTeamsReduction) { - auto GetIdentityValue = []() { + auto GetIdentityValue = [ReductionTy]() -> Constant * { switch (ReductionOperator) { case DSA_REDUCTION_ADD: case DSA_REDUCTION_SUB: - return 0; + return Constant::getNullValue(ReductionTy); case DSA_REDUCTION_MUL: - return 1; + if (ReductionTy->isIntegerTy()) + return ConstantInt::get(ReductionTy, 1); + if (ReductionTy->isFloatingPointTy()) + return ConstantFP::get(ReductionTy, 1.0); + FATAL_ERROR("Invalid value type"); + case DSA_REDUCTION_MAX: + if (auto *IntegerTy = dyn_cast(ReductionTy)) { + APInt Lowest = APInt::getSignedMinValue(IntegerTy->getBitWidth()); + return ConstantInt::get(IntegerTy, Lowest); + } + if (ReductionTy->isFloatingPointTy()) + return ConstantFP::getInfinity(ReductionTy, true); + FATAL_ERROR("Invalid value type"); default: FATAL_ERROR("Unknown reduction type"); } @@ -404,12 +421,7 @@ struct CGReduction { IRB.restoreIP(SaveIP); // Store identity value based on operation and type. - if (ReductionTy->isIntegerTy()) { - IRB.CreateStore(ConstantInt::get(ReductionTy, GetIdentityValue()), Priv); - } else if (ReductionTy->isFloatTy() || ReductionTy->isDoubleTy()) { - IRB.CreateStore(ConstantFP::get(ReductionTy, GetIdentityValue()), Priv); - } else - FATAL_ERROR("Unsupported type to init with identity reduction value"); + IRB.CreateStore(GetIdentityValue(), Priv); #if LLVM_VERSION_MAJOR <= 16 ReductionInfos.push_back( diff --git a/src/numba/openmp/omp_grammar.py b/src/numba/openmp/omp_grammar.py index ac4ef021c48f..5ae9b3f89333 100644 --- a/src/numba/openmp/omp_grammar.py +++ b/src/numba/openmp/omp_grammar.py @@ -698,7 +698,8 @@ PLUS: "+" MINUS: "-" STAR: "*" - reduction_operator: PLUS | "\\" | STAR | MINUS | "&" | "^" | "|" | "&&" | "||" + MAX: "max" + reduction_operator: PLUS | "\\" | STAR | MINUS | "&" | "^" | "|" | "&&" | "||" | MAX threadprivate_directive: "threadprivate" "(" var_list ")" cancellation_point_directive: "cancellation point" construct_type_clause construct_type_clause: PARALLEL diff --git a/src/numba/openmp/omp_lower.py b/src/numba/openmp/omp_lower.py index 955fe81e8d7a..022ee09bc4a1 100644 --- a/src/numba/openmp/omp_lower.py +++ b/src/numba/openmp/omp_lower.py @@ -2925,6 +2925,8 @@ def reduction_operator(self, args): return "SUB" elif arg == "*": return "MUL" + elif arg == "max": + return "MAX" assert 0 def threadprivate_directive(self, args): diff --git a/src/numba/openmp/tests/test_max_reduction.py b/src/numba/openmp/tests/test_max_reduction.py new file mode 100644 index 000000000000..56df77c4d840 --- /dev/null +++ b/src/numba/openmp/tests/test_max_reduction.py @@ -0,0 +1,104 @@ +import os + +import numpy as np +import pytest + +from numba.openmp import njit +from numba.openmp import openmp_context as openmp + + +@njit +def parallel_max_float64(array): + result = -np.inf + with openmp("parallel for reduction(max:result)"): + for j in range(array.size): + if array[j] > result: + result = array[j] + return result + + +@njit +def parallel_max_int64(array): + result = np.iinfo(np.int64).min + with openmp("parallel for reduction(max:result)"): + for j in range(array.size): + if array[j] > result: + result = array[j] + return result + + +@njit +def parallel_max_with_initial_value(array): + result = 20.0 + with openmp("parallel for reduction(max:result)"): + for j in range(array.size): + if array[j] > result: + result = array[j] + return result + + +@njit +def target_max_float64(array): + result = -np.inf + with openmp("target map(to:array) map(tofrom:result)"): + with openmp("loop reduction(max:result)"): + for j in range(array.size): + if array[j] > result: + result = array[j] + return result + + +def test_parallel_max_float64_all_negative(): + array = np.array([-4.0, -7.0, -1.5, -9.0, -2.0], dtype=np.float64) + assert parallel_max_float64(array) == -1.5 + + +def test_parallel_max_float64_non_power_of_two(): + array = np.array([-3.0, 7.0, 1.0, 7.0, -2.0, 4.0, 6.0], dtype=np.float64) + assert parallel_max_float64(array) == 7.0 + + +def test_parallel_max_float64_single_element(): + array = np.array([-12.5], dtype=np.float64) + assert parallel_max_float64(array) == -12.5 + + +def test_parallel_max_preserve_original_value(): + array = np.array([1.0, 3.0, 2.0], dtype=np.float64) + assert parallel_max_with_initial_value(array) == 20.0 + + +def test_parallel_max_int64_negative(): + array = np.array( + [np.iinfo(np.int64).min, -100, -2, -50], + dtype=np.int64, + ) + assert parallel_max_int64(array) == -2 + + +def test_parallel_max_matche_numpy(): + rng = np.random.default_rng(12345) + array = rng.standard_normal(100_003).astype(np.float64) + assert parallel_max_float64(array) == np.max(array) + + +def _target_offload_requested(): + return os.environ.get("OMP_TARGET_OFFLOAD", "").upper() == "MANDATORY" + + +@pytest.mark.skipif( + not _target_offload_requested(), + reason="need OMP_TARGET_OFFLOAD=MANDATORY", +) +def test_target_max_float64_all_negative(): + array = np.array([-4.0, -7.0, -1.5, -9.0, -2.0], dtype=np.float64) + assert target_max_float64(array) == -1.5 + + +@pytest.mark.skipif( + not _target_offload_requested(), + reason="need OMP_TARGET_OFFLOAD=MANDATORY", +) +def test_target_max_float64_non_power_of_two(): + array = np.array([-3.0, 7.0, 1.0, 7.0, -2.0, 4.0, 6.0], dtype=np.float64) + assert target_max_float64(array) == 7.0 From acd8c9f61f3db19312ee7a5790587d909c1bd7f5 Mon Sep 17 00:00:00 2001 From: Yifan Hu <224434986+yhu1729@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:39:18 -0400 Subject: [PATCH 2/3] Support more reduction op --- .../openmp/libs/pass/CGIntrinsicsOpenMP.cpp | 101 ++++++++++++++++++ .../openmp/libs/pass/CGIntrinsicsOpenMP.h | 33 ++++++ src/numba/openmp/omp_grammar.py | 3 +- src/numba/openmp/omp_lower.py | 2 + src/numba/openmp/tags.py | 10 ++ ...est_max_reduction.py => test_reduction.py} | 83 ++++++++++++++ 6 files changed, 231 insertions(+), 1 deletion(-) rename src/numba/openmp/tests/{test_max_reduction.py => test_reduction.py} (54%) diff --git a/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.cpp b/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.cpp index 95592f686a58..4e20952e7e4b 100644 --- a/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.cpp +++ b/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.cpp @@ -315,6 +315,9 @@ OutlinedInfoStruct CGIntrinsicsOpenMP::createOutlinedFunction( case DSA_REDUCTION_SUB: case DSA_REDUCTION_MUL: case DSA_REDUCTION_MAX: + case DSA_REDUCTION_UMAX: + case DSA_REDUCTION_MIN: + case DSA_REDUCTION_UMIN: Reductions.push_back(V); break; default: @@ -496,6 +499,21 @@ OutlinedInfoStruct CGIntrinsicsOpenMP::createOutlinedFunction( OMPBuilder.Builder, AllocaIP, AI, ReductionTy, ReductionInfos, IsGPUTeamsReduction); break; + case DSA_REDUCTION_UMAX: + Priv = CGReduction::emitInitAndAppendInfo( + OMPBuilder.Builder, AllocaIP, AI, ReductionTy, ReductionInfos, + IsGPUTeamsReduction); + break; + case DSA_REDUCTION_MIN: + Priv = CGReduction::emitInitAndAppendInfo( + OMPBuilder.Builder, AllocaIP, AI, ReductionTy, ReductionInfos, + IsGPUTeamsReduction); + break; + case DSA_REDUCTION_UMIN: + Priv = CGReduction::emitInitAndAppendInfo( + OMPBuilder.Builder, AllocaIP, AI, ReductionTy, ReductionInfos, + IsGPUTeamsReduction); + break; default: FATAL_ERROR("Unsupported reduction"); } @@ -1299,6 +1317,21 @@ void CGIntrinsicsOpenMP::emitLoop(DSAValueMapTy &DSAValueMap, CGReduction::emitInitAndAppendInfo( OMPBuilder.Builder, OMPBuilder.Builder.saveIP(), Orig, VTy, ReductionInfos, false); + } else if (DSA == DSA_REDUCTION_UMAX) { + ReplacementValue = + CGReduction::emitInitAndAppendInfo( + OMPBuilder.Builder, OMPBuilder.Builder.saveIP(), Orig, VTy, + ReductionInfos, false); + } else if (DSA == DSA_REDUCTION_MIN) { + ReplacementValue = + CGReduction::emitInitAndAppendInfo( + OMPBuilder.Builder, OMPBuilder.Builder.saveIP(), Orig, VTy, + ReductionInfos, false); + } else if (DSA == DSA_REDUCTION_UMIN) { + ReplacementValue = + CGReduction::emitInitAndAppendInfo( + OMPBuilder.Builder, OMPBuilder.Builder.saveIP(), Orig, VTy, + ReductionInfos, false); } else FATAL_ERROR("Unsupported privatization"); @@ -1785,6 +1818,9 @@ void CGIntrinsicsOpenMP::emitOMPOffloadingMappings( case DSA_REDUCTION_SUB: case DSA_REDUCTION_MUL: case DSA_REDUCTION_MAX: + case DSA_REDUCTION_UMAX: + case DSA_REDUCTION_MIN: + case DSA_REDUCTION_UMIN: case DSA_MAP_TOFROM: MapType = OMP_TGT_MAPTYPE_TO | OMP_TGT_MAPTYPE_FROM; if (IsTargetRegion) @@ -1834,6 +1870,10 @@ void CGIntrinsicsOpenMP::emitOMPOffloadingMappings( case DSA_REDUCTION_ADD: case DSA_REDUCTION_SUB: case DSA_REDUCTION_MUL: + case DSA_REDUCTION_MAX: + case DSA_REDUCTION_UMAX: + case DSA_REDUCTION_MIN: + case DSA_REDUCTION_UMIN: Size = ConstantInt::get(OMPBuilder.SizeTy, M.getDataLayout().getTypeAllocSize(V->getType())); EmitMappingEntry(Size, GetMapType(DSA), V, V); @@ -2944,6 +2984,43 @@ Value *CGReduction::emitOperation(IRBuilderBase &IRB, FATAL_ERROR("Unsupported type for maximum reduction operation"); } +template <> +Value *CGReduction::emitOperation(IRBuilderBase &IRB, + Value *LHS, Value *RHS) { + Type *VTy = RHS->getType(); + if (VTy->isIntegerTy()) { + Value *Cmp = IRB.CreateICmpUGT(LHS, RHS, "red.umax.cmp"); + return IRB.CreateSelect(Cmp, LHS, RHS, "red.umax"); + } + FATAL_ERROR("Unsupported type for unsigned maximum reduction operation"); +} + +template <> +Value *CGReduction::emitOperation(IRBuilderBase &IRB, + Value *LHS, Value *RHS) { + Type *VTy = RHS->getType(); + if (VTy->isIntegerTy()) { + Value *Cmp = IRB.CreateICmpSLT(LHS, RHS, "red.min.cmp"); + return IRB.CreateSelect(Cmp, LHS, RHS, "red.min"); + } + if (VTy->isFloatTy() || VTy->isDoubleTy()) { + Value *Cmp = IRB.CreateFCmpOLT(LHS, RHS, "red.min.cmp"); + return IRB.CreateSelect(Cmp, LHS, RHS, "red.min"); + } + FATAL_ERROR("Unsupported type for minimum reduction operation"); +} + +template <> +Value *CGReduction::emitOperation(IRBuilderBase &IRB, + Value *LHS, Value *RHS) { + Type *VTy = RHS->getType(); + if (VTy->isIntegerTy()) { + Value *Cmp = IRB.CreateICmpULT(LHS, RHS, "red.umin.cmp"); + return IRB.CreateSelect(Cmp, LHS, RHS, "red.umin"); + } + FATAL_ERROR("Unsupported type for unsigned minimum reduction operation"); +} + // OpenMP 5.1, 2.21.5, sub is the same as add. template <> Value *CGReduction::emitOperation(IRBuilderBase &IRB, @@ -2985,3 +3062,27 @@ InsertPointTy CGReduction::emitAtomicOperationRMW( AtomicOrdering::Monotonic); return IRB.saveIP(); } + +template <> +InsertPointTy CGReduction::emitAtomicOperationRMW( + IRBuilderBase &IRB, Value *LHS, Value *Partial) { + IRB.CreateAtomicRMW(AtomicRMWInst::UMax, LHS, Partial, MaybeAlign(), + AtomicOrdering::Monotonic); + return IRB.saveIP(); +} + +template <> +InsertPointTy CGReduction::emitAtomicOperationRMW( + IRBuilderBase &IRB, Value *LHS, Value *Partial) { + IRB.CreateAtomicRMW(AtomicRMWInst::Min, LHS, Partial, MaybeAlign(), + AtomicOrdering::Monotonic); + return IRB.saveIP(); +} + +template <> +InsertPointTy CGReduction::emitAtomicOperationRMW( + IRBuilderBase &IRB, Value *LHS, Value *Partial) { + IRB.CreateAtomicRMW(AtomicRMWInst::UMin, LHS, Partial, MaybeAlign(), + AtomicOrdering::Monotonic); + return IRB.saveIP(); +} diff --git a/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.h b/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.h index 77eb5d869a80..11c2abebc2c2 100644 --- a/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.h +++ b/src/numba/openmp/libs/pass/CGIntrinsicsOpenMP.h @@ -36,6 +36,9 @@ enum DSAType { DSA_REDUCTION_SUB, DSA_REDUCTION_MUL, DSA_REDUCTION_MAX, + DSA_REDUCTION_UMAX, + DSA_REDUCTION_MIN, + DSA_REDUCTION_UMIN, DSA_MAP_ALLOC, DSA_MAP_TO, DSA_MAP_FROM, @@ -100,6 +103,9 @@ static const DenseMap StringToDSA = { {"QUAL.OMP.REDUCTION.SUB", DSA_REDUCTION_SUB}, {"QUAL.OMP.REDUCTION.MUL", DSA_REDUCTION_MUL}, {"QUAL.OMP.REDUCTION.MAX", DSA_REDUCTION_MAX}, + {"QUAL.OMP.REDUCTION.UMAX", DSA_REDUCTION_UMAX}, + {"QUAL.OMP.REDUCTION.MIN", DSA_REDUCTION_MIN}, + {"QUAL.OMP.REDUCTION.UMIN", DSA_REDUCTION_UMIN}, {"QUAL.OMP.MAP.ALLOC", DSA_MAP_ALLOC}, {"QUAL.OMP.MAP.TO", DSA_MAP_TO}, {"QUAL.OMP.MAP.FROM", DSA_MAP_FROM}, @@ -133,6 +139,12 @@ inline std::string toString(const DSAType &DSA) { return "DSA_REDUCTION_MUL"; case DSA_REDUCTION_MAX: return "DSA_REDUCTION_MAX"; + case DSA_REDUCTION_UMAX: + return "DSA_REDUCTION_UMAX"; + case DSA_REDUCTION_MIN: + return "DSA_REDUCTION_MIN"; + case DSA_REDUCTION_UMIN: + return "DSA_REDUCTION_UMIN"; case DSA_MAP_ALLOC: return "DSA_MAP_ALLOC"; case DSA_MAP_TO: @@ -351,6 +363,9 @@ struct CGReduction { case DSA_REDUCTION_ADD: case DSA_REDUCTION_SUB: case DSA_REDUCTION_MAX: + case DSA_REDUCTION_UMAX: + case DSA_REDUCTION_MIN: + case DSA_REDUCTION_UMIN: return emitAtomicOperationRMW(Builder, LHS, Partial); break; case DSA_REDUCTION_MUL: @@ -397,6 +412,24 @@ struct CGReduction { if (ReductionTy->isFloatingPointTy()) return ConstantFP::getInfinity(ReductionTy, true); FATAL_ERROR("Invalid value type"); + case DSA_REDUCTION_UMAX: + if (ReductionTy->isIntegerTy()) + return Constant::getNullValue(ReductionTy); + FATAL_ERROR("Invalid value type"); + case DSA_REDUCTION_MIN: + if (auto *IntegerTy = dyn_cast(ReductionTy)) { + APInt Highest = APInt::getSignedMaxValue(IntegerTy->getBitWidth()); + return ConstantInt::get(IntegerTy, Highest); + } + if (ReductionTy->isFloatingPointTy()) + return ConstantFP::getInfinity(ReductionTy); + FATAL_ERROR("Invalid value type"); + case DSA_REDUCTION_UMIN: + if (auto *IntegerTy = dyn_cast(ReductionTy)) { + APInt Highest = APInt::getMaxValue(IntegerTy->getBitWidth()); + return ConstantInt::get(IntegerTy, Highest); + } + FATAL_ERROR("Invalid value type"); default: FATAL_ERROR("Unknown reduction type"); } diff --git a/src/numba/openmp/omp_grammar.py b/src/numba/openmp/omp_grammar.py index 5ae9b3f89333..be91e76dbbdf 100644 --- a/src/numba/openmp/omp_grammar.py +++ b/src/numba/openmp/omp_grammar.py @@ -699,7 +699,8 @@ MINUS: "-" STAR: "*" MAX: "max" - reduction_operator: PLUS | "\\" | STAR | MINUS | "&" | "^" | "|" | "&&" | "||" | MAX + MIN: "min" + reduction_operator: PLUS | "\\" | STAR | MINUS | "&" | "^" | "|" | "&&" | "||" | MAX | MIN threadprivate_directive: "threadprivate" "(" var_list ")" cancellation_point_directive: "cancellation point" construct_type_clause construct_type_clause: PARALLEL diff --git a/src/numba/openmp/omp_lower.py b/src/numba/openmp/omp_lower.py index 022ee09bc4a1..bd3e8cabdcef 100644 --- a/src/numba/openmp/omp_lower.py +++ b/src/numba/openmp/omp_lower.py @@ -2927,6 +2927,8 @@ def reduction_operator(self, args): return "MUL" elif arg == "max": return "MAX" + elif arg == "min": + return "MIN" assert 0 def threadprivate_directive(self, args): diff --git a/src/numba/openmp/tags.py b/src/numba/openmp/tags.py index 60e1d9de8e3e..0c2ce705dcc8 100644 --- a/src/numba/openmp/tags.py +++ b/src/numba/openmp/tags.py @@ -263,6 +263,16 @@ def lower(self, lowerer, debug): name_to_use = self.name + if name_to_use in [ + "QUAL.OMP.REDUCTION.MAX", + "QUAL.OMP.REDUCTION.MIN", + ]: + reduction_type = typemap_lookup(typemap, self.arg) + if isinstance(reduction_type, types.Integer) and not reduction_type.signed: + name_to_use = name_to_use.replace( + "QUAL.OMP.REDUCTION.", "QUAL.OMP.REDUCTION.U" + ) + is_array = self.arg in typemap and isinstance( typemap[self.arg], types.npytypes.Array ) diff --git a/src/numba/openmp/tests/test_max_reduction.py b/src/numba/openmp/tests/test_reduction.py similarity index 54% rename from src/numba/openmp/tests/test_max_reduction.py rename to src/numba/openmp/tests/test_reduction.py index 56df77c4d840..5d083ab0c8aa 100644 --- a/src/numba/openmp/tests/test_max_reduction.py +++ b/src/numba/openmp/tests/test_reduction.py @@ -27,6 +27,16 @@ def parallel_max_int64(array): return result +@njit +def parallel_max_uint64(array): + result = np.uint64(0) + with openmp("parallel for reduction(max:result)"): + for j in range(array.size): + if array[j] > result: + result = array[j] + return result + + @njit def parallel_max_with_initial_value(array): result = 20.0 @@ -37,6 +47,36 @@ def parallel_max_with_initial_value(array): return result +@njit +def parallel_min_float64(array): + result = np.inf + with openmp("parallel for reduction(min:result)"): + for j in range(array.size): + if array[j] < result: + result = array[j] + return result + + +@njit +def parallel_min_int64(array): + result = np.iinfo(np.int64).max + with openmp("parallel for reduction(min:result)"): + for j in range(array.size): + if array[j] < result: + result = array[j] + return result + + +@njit +def parallel_min_uint64(array): + result = np.iinfo(np.uint64).max + with openmp("parallel for reduction(min:result)"): + for j in range(array.size): + if array[j] < result: + result = array[j] + return result + + @njit def target_max_float64(array): result = -np.inf @@ -47,6 +87,9 @@ def target_max_float64(array): result = array[j] return result +def test_parallel_max_float64_all_positive(): + array = np.array([4.0, 7.0, 1.5, 9.0, 2.0], dtype=np.float64) + assert parallel_max_float64(array) == 9.0 def test_parallel_max_float64_all_negative(): array = np.array([-4.0, -7.0, -1.5, -9.0, -2.0], dtype=np.float64) @@ -76,15 +119,55 @@ def test_parallel_max_int64_negative(): assert parallel_max_int64(array) == -2 +def test_parallel_max_uint64_above_signed_range(): + array = np.array( + [1, 2**63 + 5, np.iinfo(np.uint64).max], + dtype=np.uint64, + ) + assert parallel_max_uint64(array) == np.iinfo(np.uint64).max + + def test_parallel_max_matche_numpy(): rng = np.random.default_rng(12345) array = rng.standard_normal(100_003).astype(np.float64) assert parallel_max_float64(array) == np.max(array) +def test_parallel_min_float64_all_positive(): + array = np.array([4.0, 7.0, 1.5, 9.0, 2.0], dtype=np.float64) + assert parallel_min_float64(array) == 1.5 + +def test_parallel_min_float64_all_negative(): + array = np.array([-4.0, -7.0, -1.5, -9.0, -2.0], dtype=np.float64) + assert parallel_min_float64(array) == -9.0 + + +def test_parallel_min_int64_extreme(): + array = np.array( + [np.iinfo(np.int64).max, -100, np.iinfo(np.int64).min, -50], + dtype=np.int64, + ) + assert parallel_min_int64(array) == np.iinfo(np.int64).min + + +def test_parallel_min_uint64_above_signed_range(): + array = np.array( + [np.iinfo(np.uint64).max, 2**63 + 5, 2**63 + 100], + dtype=np.uint64, + ) + assert parallel_min_uint64(array) == 2**63 + 5 + + def _target_offload_requested(): return os.environ.get("OMP_TARGET_OFFLOAD", "").upper() == "MANDATORY" +@pytest.mark.skipif( + not _target_offload_requested(), + reason="need OMP_TARGET_OFFLOAD=MANDATORY", +) +def test_target_max_float64_all_positive(): + array = np.array([4.0, 7.0, 1.5, 9.0, 2.0], dtype=np.float64) + assert target_max_float64(array) == 9.0 @pytest.mark.skipif( not _target_offload_requested(), From b12729b75c75dba69942573828e74f5ef16e44a7 Mon Sep 17 00:00:00 2001 From: Yifan Hu <224434986+yhu1729@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:48:24 -0400 Subject: [PATCH 3/3] Improve code style --- src/numba/openmp/tests/test_reduction.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/numba/openmp/tests/test_reduction.py b/src/numba/openmp/tests/test_reduction.py index 5d083ab0c8aa..c02b6d923859 100644 --- a/src/numba/openmp/tests/test_reduction.py +++ b/src/numba/openmp/tests/test_reduction.py @@ -87,10 +87,12 @@ def target_max_float64(array): result = array[j] return result + def test_parallel_max_float64_all_positive(): array = np.array([4.0, 7.0, 1.5, 9.0, 2.0], dtype=np.float64) assert parallel_max_float64(array) == 9.0 + def test_parallel_max_float64_all_negative(): array = np.array([-4.0, -7.0, -1.5, -9.0, -2.0], dtype=np.float64) assert parallel_max_float64(array) == -1.5 @@ -137,6 +139,7 @@ def test_parallel_min_float64_all_positive(): array = np.array([4.0, 7.0, 1.5, 9.0, 2.0], dtype=np.float64) assert parallel_min_float64(array) == 1.5 + def test_parallel_min_float64_all_negative(): array = np.array([-4.0, -7.0, -1.5, -9.0, -2.0], dtype=np.float64) assert parallel_min_float64(array) == -9.0 @@ -161,6 +164,7 @@ def test_parallel_min_uint64_above_signed_range(): def _target_offload_requested(): return os.environ.get("OMP_TARGET_OFFLOAD", "").upper() == "MANDATORY" + @pytest.mark.skipif( not _target_offload_requested(), reason="need OMP_TARGET_OFFLOAD=MANDATORY", @@ -169,6 +173,7 @@ def test_target_max_float64_all_positive(): array = np.array([4.0, 7.0, 1.5, 9.0, 2.0], dtype=np.float64) assert target_max_float64(array) == 9.0 + @pytest.mark.skipif( not _target_offload_requested(), reason="need OMP_TARGET_OFFLOAD=MANDATORY",