Repository navigation
Java: scope the OpenMP thread count to each build; fix builds without index parameters #2844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
589d784
06c6125
ff2181e
6a3982c
cd9be4e
41fb40f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,8 +119,6 @@ private IndexReference build( | |
|
|
||
| MemorySegment datasetMemSegment = dataset.memorySegment(); | ||
|
|
||
| omp_set_num_threads(bruteForceIndexParams.getNumWriterThreads()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I can see, OpenMP is not used here. I think that was added by copy/paste bug. We can just remove it here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, nothing on the brute-force build path uses OpenMP. Removed in 06c6125. |
||
|
|
||
| long datasetBytes = C_FLOAT_BYTE_SIZE * rows * cols; | ||
| var index = createBruteForceIndex(); | ||
|
|
||
|
|
@@ -152,8 +150,6 @@ private IndexReference build( | |
| tensorDataArena, | ||
| index); | ||
| } | ||
| } finally { | ||
| omp_set_num_threads(1); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| package com.nvidia.cuvs.internal.common; | ||
|
|
||
| import static com.nvidia.cuvs.internal.panama.headers_h.omp_get_max_threads; | ||
| import static com.nvidia.cuvs.internal.panama.headers_h.omp_set_num_threads; | ||
|
|
||
| /** | ||
| * Sets the OpenMP thread count for the native work done by the current thread, and restores the | ||
| * previous value on {@link #close()}. | ||
| * | ||
| * <p>OpenMP's {@code nthreads-var} is per thread: {@code omp_set_num_threads} only affects parallel | ||
| * regions started by the calling thread, so concurrent builds on other threads keep their own | ||
| * values. What must not happen is leaving this thread's value changed after the call returns, which | ||
| * is why the previous value is restored rather than reset to a fixed number. | ||
| */ | ||
| public final class OmpThreadScope implements AutoCloseable { | ||
| private final int previous; | ||
|
|
||
| private OmpThreadScope(int previous) { | ||
| this.previous = previous; | ||
| } | ||
|
|
||
| /** | ||
| * Applies {@code numThreads} for the current thread until the returned scope is closed. A value | ||
| * of zero or less leaves the current setting untouched, so native code uses the OpenMP default. | ||
| */ | ||
| public static OmpThreadScope of(int numThreads) { | ||
| if (numThreads <= 0) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This changes what withNumWriterThreads(0) (or a negative value) means. On main it reached omp_set_num_threads(0), which libgomp clamps to 1, so the build was single-threaded. Here it skips the call, so the build uses the OpenMP default (all cores). I think it should be at least documented in CagraIndexParams.Builder.withNumWriterThreads.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, this is a behaviour change. Documented in cd9be4e on |
||
| return new OmpThreadScope(0); | ||
| } | ||
| int previous = omp_get_max_threads(); | ||
| omp_set_num_threads(numThreads); | ||
| return new OmpThreadScope(previous); | ||
| } | ||
|
|
||
| @Override | ||
| public void close() { | ||
| if (previous > 0) { | ||
| omp_set_num_threads(previous); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| package com.nvidia.cuvs; | ||
|
|
||
| import static com.carrotsearch.randomizedtesting.RandomizedTest.assumeTrue; | ||
| import static org.junit.Assert.assertEquals; | ||
|
|
||
| import com.carrotsearch.randomizedtesting.RandomizedRunner; | ||
| import java.lang.foreign.Arena; | ||
| import java.lang.foreign.FunctionDescriptor; | ||
| import java.lang.foreign.Linker; | ||
| import java.lang.foreign.SymbolLookup; | ||
| import java.lang.foreign.ValueLayout; | ||
| import java.lang.invoke.MethodHandle; | ||
| import org.junit.After; | ||
| import org.junit.Before; | ||
| import org.junit.Test; | ||
| import org.junit.runner.RunWith; | ||
|
|
||
| /** A build must not leave the calling thread's OpenMP thread count changed. */ | ||
| @RunWith(RandomizedRunner.class) | ||
| public class OmpThreadsIT extends CuVSTestCase { | ||
|
|
||
| // Differs from both the old hard-coded reset (1) and the numWriterThreads the tests request (3). | ||
| private static final int BASELINE = 5; | ||
|
|
||
| private MethodHandle ompGetMaxThreads; | ||
| private MethodHandle ompSetNumThreads; | ||
| private int original; | ||
|
|
||
| @Before | ||
| public void setup() throws Throwable { | ||
| assumeTrue("not supported on " + System.getProperty("os.name"), isLinuxSupportedArch()); | ||
| initializeRandom(); | ||
| var libgomp = SymbolLookup.libraryLookup("libgomp.so.1", Arena.global()); | ||
| var linker = Linker.nativeLinker(); | ||
| ompGetMaxThreads = | ||
| linker.downcallHandle( | ||
| libgomp.find("omp_get_max_threads").orElseThrow(), | ||
| FunctionDescriptor.of(ValueLayout.JAVA_INT)); | ||
| ompSetNumThreads = | ||
| linker.downcallHandle( | ||
| libgomp.find("omp_set_num_threads").orElseThrow(), | ||
| FunctionDescriptor.ofVoid(ValueLayout.JAVA_INT)); | ||
| original = ompMaxThreads(); | ||
| setOmpThreads(BASELINE); | ||
| } | ||
|
|
||
| @After | ||
| public void restore() throws Throwable { | ||
| if (ompSetNumThreads != null) { | ||
| setOmpThreads(original); | ||
| } | ||
| } | ||
|
|
||
| private int ompMaxThreads() throws Throwable { | ||
| return (int) ompGetMaxThreads.invokeExact(); | ||
| } | ||
|
|
||
| private void setOmpThreads(int n) throws Throwable { | ||
| ompSetNumThreads.invokeExact(n); | ||
| } | ||
|
|
||
| @Test | ||
| public void testCagraBuildRestoresOmpThreads() throws Throwable { | ||
| var params = | ||
| new CagraIndexParams.Builder() | ||
| .withCagraGraphBuildAlgo(CagraIndexParams.CagraGraphBuildAlgo.NN_DESCENT) | ||
| .withNumWriterThreads(3) | ||
| .build(); | ||
| try (var resources = CheckedCuVSResources.create(); | ||
| var dataset = CuVSMatrix.ofArray(createFloatMatrix(2000, 32))) { | ||
| try (var index = | ||
| CagraIndex.newBuilder(resources).withDataset(dataset).withIndexParams(params).build()) { | ||
| assertEquals(BASELINE, ompMaxThreads()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testCagraBuildWithoutParamsLeavesOmpThreads() throws Throwable { | ||
| try (var resources = CheckedCuVSResources.create(); | ||
| var dataset = CuVSMatrix.ofArray(createFloatMatrix(2000, 32))) { | ||
| try (var index = CagraIndex.newBuilder(resources).withDataset(dataset).build()) { | ||
| assertEquals(BASELINE, ompMaxThreads()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testCagraMergeRestoresOmpThreads() throws Throwable { | ||
| var params = | ||
| new CagraIndexParams.Builder() | ||
| .withCagraGraphBuildAlgo(CagraIndexParams.CagraGraphBuildAlgo.NN_DESCENT) | ||
| .withNumWriterThreads(3) | ||
| .build(); | ||
| try (var resources = CheckedCuVSResources.create(); | ||
| var dataset1 = CuVSMatrix.ofArray(createFloatMatrix(2000, 32)).toDevice(resources); | ||
| var dataset2 = CuVSMatrix.ofArray(createFloatMatrix(2000, 32)).toDevice(resources); | ||
| var index1 = | ||
| CagraIndex.newBuilder(resources).withDataset(dataset1).withIndexParams(params).build(); | ||
| var index2 = | ||
| CagraIndex.newBuilder(resources) | ||
| .withDataset(dataset2) | ||
| .withIndexParams(params) | ||
| .build()) { | ||
| setOmpThreads(BASELINE); | ||
| try (var merged = CagraIndex.merge(new CagraIndex[] {index1, index2}, params)) { | ||
| assertEquals(BASELINE, ompMaxThreads()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testBruteForceBuildWithoutParams() throws Throwable { | ||
| try (var resources = CheckedCuVSResources.create(); | ||
| var dataset = CuVSMatrix.ofArray(createFloatMatrix(500, 16))) { | ||
| try (var index = BruteForceIndex.newBuilder(resources).withDataset(dataset).build()) { | ||
| assertEquals(BASELINE, ompMaxThreads()); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. | ||
| * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
|
|
@@ -17,3 +17,8 @@ void omp_set_num_threads(int n_writer_threads); | |
| * @brief function signature for getting omp threads | ||
| */ | ||
| int omp_get_num_threads(void); | ||
|
|
||
| /** | ||
| * @brief function signature for getting the calling thread's OpenMP thread count | ||
| */ | ||
| int omp_get_max_threads(void); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please run pre-commit so this file's copyright header would be updated.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, and it also fixed the header on the new |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Describe the retained thread count, not an OpenMP default.
If the calling thread already has an OpenMP count of 1,
withNumWriterThreads(0)keeps that count; it does not select one thread per core. State that a nonpositive value retains the calling thread’s current setting. Make the same correction inCagraIndexParams.java. As per path instructions, documentation changes must meet “Accuracy” and “Consistency” requirements.Source: Path instructions