From ee58808919cbadccd8f0ad5205e030d16b046712 Mon Sep 17 00:00:00 2001 From: zhenghaoz Date: Sat, 7 Nov 2026 08:10:15 +0800 Subject: [PATCH 1/3] fix: default CPU inference to physical core count --- CMakeLists.txt | 11 ++++ README.md | 11 +++- node/README.md | 2 +- node/test/clap.test.js | 2 +- python/README.md | 2 +- python/tests/test_clap.py | 2 +- src/CMakeLists.txt | 1 + src/clap.cpp | 4 +- src/cpu-threads.cpp | 102 +++++++++++++++++++++++++++++ src/cpu-threads.h | 15 +++++ src/graph.cpp | 5 +- tests/cpu-threads-runtime-test.cpp | 63 ++++++++++++++++++ tests/cpu-threads-test.cpp | 64 ++++++++++++++++++ 13 files changed, 272 insertions(+), 12 deletions(-) create mode 100644 src/cpu-threads.cpp create mode 100644 src/cpu-threads.h create mode 100644 tests/cpu-threads-runtime-test.cpp create mode 100644 tests/cpu-threads-test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 03af389..153e7b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,17 @@ if(BUILD_TESTING) target_link_libraries(clap-model-test PRIVATE clap nlohmann_json::nlohmann_json) target_include_directories(clap-model-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") add_test(NAME clap_model_unit COMMAND clap-model-test) + add_executable(cpu-threads-test tests/cpu-threads-test.cpp src/cpu-threads.cpp) + target_include_directories(cpu-threads-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") + add_test(NAME clap_cpu_threads COMMAND cpu-threads-test "${CMAKE_CURRENT_BINARY_DIR}/cpu-topology-fixture") + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND NOT BUILD_SHARED_LIBS) + add_executable(cpu-threads-runtime-test tests/cpu-threads-runtime-test.cpp) + target_include_directories(cpu-threads-runtime-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") + target_link_libraries(cpu-threads-runtime-test PRIVATE clap ggml-cpu) + target_link_options(cpu-threads-runtime-test PRIVATE "-Wl,--wrap=ggml_backend_cpu_set_n_threads") + add_test(NAME clap_cpu_threads_runtime COMMAND cpu-threads-runtime-test) + set_tests_properties(clap_cpu_threads_runtime PROPERTIES SKIP_RETURN_CODE 77) + endif() add_executable(graph-pipeline-test tests/graph-pipeline-test.cpp) target_include_directories(graph-pipeline-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") target_link_libraries(graph-pipeline-test PRIVATE clap) diff --git a/README.md b/README.md index 156e9e0..8a76a96 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,18 @@ GGUF models are available at [Audio Embedding GGUF](https://huggingface.co/colle ## Usage +CPU inference defaults to the detected physical core count on Linux, macOS and +Windows. A positive `--threads` value overrides it; `0` selects automatic detection. +If physical topology is unavailable, it falls back to the logical CPU count (or +one thread if that is also unavailable), without a fixed eight-thread cap. + ### Embeddings command Load a converted GGUF and generate real native text or audio embeddings: ```sh -clap embedding --model larger_clap_general-q4_k_m.gguf --threads 4 --text 'Sound of a dog' -clap embedding --model larger_clap_general-q4_k_m.gguf --threads 4 --audio dog.wav +clap embedding --model larger_clap_general-q4_k_m.gguf --text 'Sound of a dog' +clap embedding --model larger_clap_general-q4_k_m.gguf --audio dog.wav ``` ### Embeddings server @@ -50,7 +55,7 @@ Start the server with a GGUF supporting the requested modality, such as CLAP, CLaMP3 or MuQ-MuLan (default listen address: `127.0.0.1:8080`): ```sh -clap serve --model larger_clap_general-q4_k_m.gguf --threads 4 --host 127.0.0.1 --port 8080 +clap serve --model larger_clap_general-q4_k_m.gguf --host 127.0.0.1 --port 8080 ``` The model is loaded once at startup. Its API identifier is the GGUF **filename** diff --git a/node/README.md b/node/README.md index 13c6fd8..295a3d6 100644 --- a/node/README.md +++ b/node/README.md @@ -17,7 +17,7 @@ npm install clap-cpp ```js const { CLAP } = require('clap-cpp'); -const model = new CLAP('larger_clap_general-q4_k_m.gguf', 4); +const model = new CLAP('larger_clap_general-q4_k_m.gguf'); try { const textEmbedding = model.encodeText('Sound of a dog'); const audioEmbedding = model.encodeAudio('dog.wav'); diff --git a/node/test/clap.test.js b/node/test/clap.test.js index f37cbbd..d9f9836 100644 --- a/node/test/clap.test.js +++ b/node/test/clap.test.js @@ -41,7 +41,7 @@ test('CLAP', async (t) => { await pipeline(file.stream(), createWriteStream(modelPath)); } - const clap = new CLAP(modelPath, 1); + const clap = new CLAP(modelPath); t.after(() => clap.close()); const audioEmbedding = clap.encodeAudio(path.join(__dirname, '..', '..', 'assets', 'dog.wav'), 0); const textEmbedding1 = clap.encodeText('Sound of a dog'); diff --git a/python/README.md b/python/README.md index 547c2e9..8fb2c62 100644 --- a/python/README.md +++ b/python/README.md @@ -26,7 +26,7 @@ pip install "clap-cpp[vulkan]" # Vulkan ```python from clap_cpp import CLAP -with CLAP("larger_clap_general-q4_k_m.gguf", threads=4) as model: +with CLAP("larger_clap_general-q4_k_m.gguf") as model: text_embedding = model.encode_text("Sound of a dog") audio_embedding = model.encode_audio("dog.wav") ``` diff --git a/python/tests/test_clap.py b/python/tests/test_clap.py index 735d6f9..b690e3a 100644 --- a/python/tests/test_clap.py +++ b/python/tests/test_clap.py @@ -33,7 +33,7 @@ def test_clap(): ) audio_path = Path(__file__).resolve().parents[2] / "assets" / "dog.wav" - with CLAP(model_path, threads=1) as clap: + with CLAP(model_path) as clap: audio_embedding = clap.encode_audio(audio_path, 0) text_embedding_1 = clap.encode_text("Sound of a dog") text_embedding_2 = clap.encode_text("Sound of vaccum cleaner") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 76c9b1e..db587cb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ set(CLAP_CORE_SOURCES model.cpp model-loader.cpp graph.cpp + cpu-threads.cpp tensor-data.cpp audio.cpp vocab.cpp) diff --git a/src/clap.cpp b/src/clap.cpp index f4d5bfa..a5531ae 100644 --- a/src/clap.cpp +++ b/src/clap.cpp @@ -22,7 +22,7 @@ int main(int argc, char **argv) { auto *devices = app.add_subcommand("devices", "List available GGML devices"); auto *serve = app.add_subcommand("serve", "Start the embeddings HTTP server"); serve->add_option("-m,--model", server_model, "Converted model GGUF")->required()->check(CLI::ExistingFile); - serve->add_option("-t,--threads", server_threads, "ggml CPU threads (0: automatic)") + serve->add_option("-t,--threads", server_threads, "ggml CPU threads (0: detected physical cores)") ->check(CLI::NonNegativeNumber) ->capture_default_str(); serve->add_option("--device", server_device, "GGML device name, auto or cpu (default: CLAP_DEVICE or auto)"); @@ -35,7 +35,7 @@ int main(int argc, char **argv) { embedding->add_option("-m,--model", options.model_path, "Converted CLAP/CLaMP3/MERT/MERT2/MuQ/MuLan GGUF") ->required() ->check(CLI::ExistingFile); - embedding->add_option("-t,--threads", options.threads, "ggml CPU threads (0: automatic)") + embedding->add_option("-t,--threads", options.threads, "ggml CPU threads (0: detected physical cores)") ->check(CLI::NonNegativeNumber) ->capture_default_str(); embedding->add_option("--device", options.device, "GGML device name, auto or cpu (default: CLAP_DEVICE or auto)"); diff --git a/src/cpu-threads.cpp b/src/cpu-threads.cpp new file mode 100644 index 0000000..044b78c --- /dev/null +++ b/src/cpu-threads.cpp @@ -0,0 +1,102 @@ +#include "cpu-threads.h" +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#include +#elif defined(__APPLE__) +#include +#elif defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif + +namespace clap::detail { +int select_cpu_threads(int requested, int physical, unsigned logical) { + if (requested > 0) + return requested; + if (physical > 0) + return physical; + return int(std::max(1u, std::min(logical, unsigned(std::numeric_limits::max())))); +} + +#if defined(__linux__) +int linux_physical_cores(const std::filesystem::path &cpu_root) { + std::set> cores; + std::error_code error; + std::filesystem::directory_iterator it(cpu_root, error), end; + for (; !error && it != end; it.increment(error)) { + const auto name = it->path().filename().string(); + if (name.size() <= 3 || name.compare(0, 3, "cpu") != 0 || + name.find_first_not_of("0123456789", 3) != std::string::npos) + continue; + int online = 1; + std::ifstream online_file(it->path() / "online"); + if (online_file.is_open() && (!(online_file >> online) || (online != 0 && online != 1))) + return 0; + if (!online) + continue; + int package = -1, core = -1; + const bool have_package = bool(std::ifstream(it->path() / "topology/physical_package_id") >> package); + const bool have_core = bool(std::ifstream(it->path() / "topology/core_id") >> core); + // Never return a partial count if the kernel does not expose topology. + if (!have_package || !have_core || package < 0 || core < 0) + return 0; + cores.emplace(package, core); + } + return error ? 0 : int(cores.size()); +} +#endif + +static int physical_cores() { +#if defined(__linux__) + return linux_physical_cores("/sys/devices/system/cpu"); +#elif defined(__APPLE__) + int cores = 0; + size_t size = sizeof(cores); + return sysctlbyname("hw.physicalcpu", &cores, &size, nullptr, 0) == 0 && size == sizeof(cores) ? cores : 0; +#elif defined(_WIN32) + DWORD bytes = 0; + GetLogicalProcessorInformationEx(RelationProcessorCore, nullptr, &bytes); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || !bytes) + return 0; + // The API returns variable-sized records, one per core across all groups. + std::vector buffer(bytes); + if (!GetLogicalProcessorInformationEx( + RelationProcessorCore, reinterpret_cast(buffer.data()), &bytes)) + return 0; + int cores = 0; + for (size_t offset = 0; offset < bytes;) { + if (bytes - offset < offsetof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, Processor)) + return 0; + const auto *info = reinterpret_cast(buffer.data() + offset); + if (info->Size < offsetof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, Processor) || info->Size > bytes - offset) + return 0; + if (info->Relationship == RelationProcessorCore) + ++cores; + offset += info->Size; + } + return cores; +#else + return 0; +#endif +} + +int default_cpu_threads() { + int physical = 0; + try { + physical = physical_cores(); + } catch (const std::exception &) { + // Missing topology, filesystem failures or allocation failure: retain a + // usable automatic setting even when physical detection is unavailable. + } + return select_cpu_threads(0, physical, std::thread::hardware_concurrency()); +} +} // namespace clap::detail diff --git a/src/cpu-threads.h b/src/cpu-threads.h new file mode 100644 index 0000000..7bbedac --- /dev/null +++ b/src/cpu-threads.h @@ -0,0 +1,15 @@ +#pragma once + +#if defined(__linux__) +#include +#endif + +namespace clap::detail { +// A failed physical-core query returns zero; selection then uses logical CPUs, +// or one thread if neither source is available. Positive overrides are exact. +int select_cpu_threads(int requested, int physical, unsigned logical); +int default_cpu_threads(); +#if defined(__linux__) +int linux_physical_cores(const std::filesystem::path &cpu_root); +#endif +} // namespace clap::detail diff --git a/src/graph.cpp b/src/graph.cpp index a3fd0be..5e62c02 100644 --- a/src/graph.cpp +++ b/src/graph.cpp @@ -1,11 +1,11 @@ #include "graph.h" +#include "cpu-threads.h" #include #include #include #include #include #include -#include namespace { void load_backends() { static std::once_flag once; @@ -79,8 +79,7 @@ struct GraphRuntime::State { cpu.reset(ggml_backend_cpu_init()); if (!cpu) throw std::runtime_error("CLAP: CPU backend initialization failed"); - ggml_backend_cpu_set_n_threads( - cpu.get(), threads > 0 ? threads : int(std::max(1u, std::min(8u, std::thread::hardware_concurrency())))); + ggml_backend_cpu_set_n_threads(cpu.get(), threads > 0 ? threads : default_cpu_threads()); if (requested == "auto") { for (size_t i = 0; i < ggml_backend_dev_count() && !gpu; ++i) { auto device = ggml_backend_dev_get(i); diff --git a/tests/cpu-threads-runtime-test.cpp b/tests/cpu-threads-runtime-test.cpp new file mode 100644 index 0000000..3af02c7 --- /dev/null +++ b/tests/cpu-threads-runtime-test.cpp @@ -0,0 +1,63 @@ +#include "cpu-threads.h" +#include "graph.h" +#include +#include +#include +#include +#include +#include + +// Observe the actual backend configuration without exposing a public API getter. +static int configured_threads = 0; +extern "C" void __real_ggml_backend_cpu_set_n_threads(ggml_backend_t, int); +extern "C" void __wrap_ggml_backend_cpu_set_n_threads(ggml_backend_t backend, int threads) { + configured_threads = threads; + __real_ggml_backend_cpu_set_n_threads(backend, threads); +} + +int main() { + try { + // Independent Linux oracle: package/core pairs in /proc, not sysfs. + std::ifstream input("/proc/cpuinfo"); + std::set> cores; + int package = -1, core = -1; + std::string line; + auto record = [&] { + if (package >= 0 && core >= 0) + cores.emplace(package, core); + package = core = -1; + }; + while (std::getline(input, line)) { + if (line.empty()) { + record(); + continue; + } + const auto colon = line.find(':'); + if (colon == std::string::npos) + continue; + std::istringstream value(line.substr(colon + 1)); + if (line.compare(0, 11, "physical id") == 0) + value >> package; + if (line.compare(0, 7, "core id") == 0) + value >> core; + } + record(); + if (cores.empty() || clap::detail::linux_physical_cores("/sys/devices/system/cpu") == 0) { + std::cout << "Physical topology or independent /proc oracle unavailable; skipped\n"; + return 77; + } + clap::detail::ModelLoader loader(clap::RuntimeOptions{0, "cpu"}); + for (int requested : {0, -1, 1, 3, 129}) { + clap::detail::GraphRuntime runtime(loader, requested); + const int expected = requested > 0 ? requested : int(cores.size()); + std::cout << "requested=" << requested << " configured=" << configured_threads << " expected=" << expected + << '\n'; + if (configured_threads != expected) + throw std::runtime_error("backend CPU threads must use physical cores unless explicitly overridden"); + } + return 0; + } catch (const std::exception &error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/tests/cpu-threads-test.cpp b/tests/cpu-threads-test.cpp new file mode 100644 index 0000000..827bf9c --- /dev/null +++ b/tests/cpu-threads-test.cpp @@ -0,0 +1,64 @@ +#include "cpu-threads.h" +#include +#include +#include +#if defined(__linux__) +#include +#endif + +static void require(bool condition, const char *message) { + if (!condition) + throw std::runtime_error(message); +} +int main(int argc, char **argv) { + try { + using namespace clap::detail; + require(select_cpu_threads(0, 24, 48) == 24, "physical count must not be capped at eight"); + require(select_cpu_threads(-1, 24, 48) == 24, "nonpositive threads select automatic"); + for (int threads : {1, 3, 129}) + require(select_cpu_threads(threads, 24, 48) == threads, "explicit threads must be preserved"); + require(select_cpu_threads(0, 0, 48) == 48, "missing topology must use logical count without a cap"); + require(select_cpu_threads(0, -1, 0) == 1, "unavailable CPU counts must use one thread"); + require(select_cpu_threads(0, 0, std::numeric_limits::max()) == std::numeric_limits::max(), + "logical fallback must fit a positive int"); + require(default_cpu_threads() > 0, "host detection must return a usable count"); +#if defined(__linux__) + require(argc == 2, "cpu-threads-test FIXTURE_DIR"); + const std::filesystem::path root(argv[1]); + std::filesystem::remove_all(root); + auto cpu = [&](int id, int package, int core) { + const auto dir = root / ("cpu" + std::to_string(id)) / "topology"; + std::filesystem::create_directories(dir); + std::ofstream(dir / "physical_package_id") << package; + std::ofstream(dir / "core_id") << core; + }; + require(linux_physical_cores(root) == 0, "missing sysfs must signal fallback"); + for (int i = 0; i < 48; ++i) + cpu(i, i / 24, (i % 24) / 2); + require(linux_physical_cores(root) == 24, "deduplicate SMT siblings, not equal core IDs across sockets"); + cpu(48, 2, 0); + std::ofstream(root / "cpu48/online") << 0; + require(linux_physical_cores(root) == 24, "offline CPUs must not be counted"); + std::ofstream(root / "cpu48/online") << "invalid"; + require(linux_physical_cores(root) == 0, "malformed online status must signal fallback"); + std::ofstream(root / "cpu48/online") << 0; + std::filesystem::create_directories(root / "cpufreq"); + require(linux_physical_cores(root) == 24, "ignore non-CPU entries"); + std::ofstream(root / "cpu0/topology/core_id") << "invalid"; + require(linux_physical_cores(root) == 0, "malformed topology must not return a partial count"); + cpu(0, 0, -1); + require(linux_physical_cores(root) == 0, "unknown topology must signal fallback"); + std::filesystem::remove(root / "cpu0/topology/core_id"); + require(linux_physical_cores(root) == 0, "missing core ID must signal fallback"); + std::filesystem::remove_all(root); +#else + (void)argc; + (void)argv; +#endif + std::cout << "CPU physical-core detection, overrides and fallbacks passed\n"; + return 0; + } catch (const std::exception &error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 272eadce931910937719299e1aad3b7194fd0463 Mon Sep 17 00:00:00 2001 From: zhenghaoz Date: Sat, 7 Nov 2026 08:24:58 +0800 Subject: [PATCH 2/3] fix: use hardware concurrency for automatic CPU threads --- CMakeLists.txt | 4 -- README.md | 5 -- src/CMakeLists.txt | 1 - src/clap.cpp | 4 +- src/cpu-threads.cpp | 102 ----------------------------- src/cpu-threads.h | 15 ----- src/graph.cpp | 5 +- tests/cpu-threads-runtime-test.cpp | 41 ++---------- tests/cpu-threads-test.cpp | 64 ------------------ 9 files changed, 11 insertions(+), 230 deletions(-) delete mode 100644 src/cpu-threads.cpp delete mode 100644 src/cpu-threads.h delete mode 100644 tests/cpu-threads-test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 153e7b7..708f6c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,16 +172,12 @@ if(BUILD_TESTING) target_link_libraries(clap-model-test PRIVATE clap nlohmann_json::nlohmann_json) target_include_directories(clap-model-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") add_test(NAME clap_model_unit COMMAND clap-model-test) - add_executable(cpu-threads-test tests/cpu-threads-test.cpp src/cpu-threads.cpp) - target_include_directories(cpu-threads-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") - add_test(NAME clap_cpu_threads COMMAND cpu-threads-test "${CMAKE_CURRENT_BINARY_DIR}/cpu-topology-fixture") if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND NOT BUILD_SHARED_LIBS) add_executable(cpu-threads-runtime-test tests/cpu-threads-runtime-test.cpp) target_include_directories(cpu-threads-runtime-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") target_link_libraries(cpu-threads-runtime-test PRIVATE clap ggml-cpu) target_link_options(cpu-threads-runtime-test PRIVATE "-Wl,--wrap=ggml_backend_cpu_set_n_threads") add_test(NAME clap_cpu_threads_runtime COMMAND cpu-threads-runtime-test) - set_tests_properties(clap_cpu_threads_runtime PROPERTIES SKIP_RETURN_CODE 77) endif() add_executable(graph-pipeline-test tests/graph-pipeline-test.cpp) target_include_directories(graph-pipeline-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") diff --git a/README.md b/README.md index 8a76a96..710051d 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,6 @@ GGUF models are available at [Audio Embedding GGUF](https://huggingface.co/colle ## Usage -CPU inference defaults to the detected physical core count on Linux, macOS and -Windows. A positive `--threads` value overrides it; `0` selects automatic detection. -If physical topology is unavailable, it falls back to the logical CPU count (or -one thread if that is also unavailable), without a fixed eight-thread cap. - ### Embeddings command Load a converted GGUF and generate real native text or audio embeddings: diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index db587cb..76c9b1e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,7 +8,6 @@ set(CLAP_CORE_SOURCES model.cpp model-loader.cpp graph.cpp - cpu-threads.cpp tensor-data.cpp audio.cpp vocab.cpp) diff --git a/src/clap.cpp b/src/clap.cpp index a5531ae..ccca04a 100644 --- a/src/clap.cpp +++ b/src/clap.cpp @@ -22,7 +22,7 @@ int main(int argc, char **argv) { auto *devices = app.add_subcommand("devices", "List available GGML devices"); auto *serve = app.add_subcommand("serve", "Start the embeddings HTTP server"); serve->add_option("-m,--model", server_model, "Converted model GGUF")->required()->check(CLI::ExistingFile); - serve->add_option("-t,--threads", server_threads, "ggml CPU threads (0: detected physical cores)") + serve->add_option("-t,--threads", server_threads, "ggml CPU threads (0: hardware concurrency)") ->check(CLI::NonNegativeNumber) ->capture_default_str(); serve->add_option("--device", server_device, "GGML device name, auto or cpu (default: CLAP_DEVICE or auto)"); @@ -35,7 +35,7 @@ int main(int argc, char **argv) { embedding->add_option("-m,--model", options.model_path, "Converted CLAP/CLaMP3/MERT/MERT2/MuQ/MuLan GGUF") ->required() ->check(CLI::ExistingFile); - embedding->add_option("-t,--threads", options.threads, "ggml CPU threads (0: detected physical cores)") + embedding->add_option("-t,--threads", options.threads, "ggml CPU threads (0: hardware concurrency)") ->check(CLI::NonNegativeNumber) ->capture_default_str(); embedding->add_option("--device", options.device, "GGML device name, auto or cpu (default: CLAP_DEVICE or auto)"); diff --git a/src/cpu-threads.cpp b/src/cpu-threads.cpp deleted file mode 100644 index 044b78c..0000000 --- a/src/cpu-threads.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "cpu-threads.h" -#include -#include -#include -#include -#include - -#if defined(__linux__) -#include -#include -#elif defined(__APPLE__) -#include -#elif defined(_WIN32) -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#include -#endif - -namespace clap::detail { -int select_cpu_threads(int requested, int physical, unsigned logical) { - if (requested > 0) - return requested; - if (physical > 0) - return physical; - return int(std::max(1u, std::min(logical, unsigned(std::numeric_limits::max())))); -} - -#if defined(__linux__) -int linux_physical_cores(const std::filesystem::path &cpu_root) { - std::set> cores; - std::error_code error; - std::filesystem::directory_iterator it(cpu_root, error), end; - for (; !error && it != end; it.increment(error)) { - const auto name = it->path().filename().string(); - if (name.size() <= 3 || name.compare(0, 3, "cpu") != 0 || - name.find_first_not_of("0123456789", 3) != std::string::npos) - continue; - int online = 1; - std::ifstream online_file(it->path() / "online"); - if (online_file.is_open() && (!(online_file >> online) || (online != 0 && online != 1))) - return 0; - if (!online) - continue; - int package = -1, core = -1; - const bool have_package = bool(std::ifstream(it->path() / "topology/physical_package_id") >> package); - const bool have_core = bool(std::ifstream(it->path() / "topology/core_id") >> core); - // Never return a partial count if the kernel does not expose topology. - if (!have_package || !have_core || package < 0 || core < 0) - return 0; - cores.emplace(package, core); - } - return error ? 0 : int(cores.size()); -} -#endif - -static int physical_cores() { -#if defined(__linux__) - return linux_physical_cores("/sys/devices/system/cpu"); -#elif defined(__APPLE__) - int cores = 0; - size_t size = sizeof(cores); - return sysctlbyname("hw.physicalcpu", &cores, &size, nullptr, 0) == 0 && size == sizeof(cores) ? cores : 0; -#elif defined(_WIN32) - DWORD bytes = 0; - GetLogicalProcessorInformationEx(RelationProcessorCore, nullptr, &bytes); - if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || !bytes) - return 0; - // The API returns variable-sized records, one per core across all groups. - std::vector buffer(bytes); - if (!GetLogicalProcessorInformationEx( - RelationProcessorCore, reinterpret_cast(buffer.data()), &bytes)) - return 0; - int cores = 0; - for (size_t offset = 0; offset < bytes;) { - if (bytes - offset < offsetof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, Processor)) - return 0; - const auto *info = reinterpret_cast(buffer.data() + offset); - if (info->Size < offsetof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, Processor) || info->Size > bytes - offset) - return 0; - if (info->Relationship == RelationProcessorCore) - ++cores; - offset += info->Size; - } - return cores; -#else - return 0; -#endif -} - -int default_cpu_threads() { - int physical = 0; - try { - physical = physical_cores(); - } catch (const std::exception &) { - // Missing topology, filesystem failures or allocation failure: retain a - // usable automatic setting even when physical detection is unavailable. - } - return select_cpu_threads(0, physical, std::thread::hardware_concurrency()); -} -} // namespace clap::detail diff --git a/src/cpu-threads.h b/src/cpu-threads.h deleted file mode 100644 index 7bbedac..0000000 --- a/src/cpu-threads.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#if defined(__linux__) -#include -#endif - -namespace clap::detail { -// A failed physical-core query returns zero; selection then uses logical CPUs, -// or one thread if neither source is available. Positive overrides are exact. -int select_cpu_threads(int requested, int physical, unsigned logical); -int default_cpu_threads(); -#if defined(__linux__) -int linux_physical_cores(const std::filesystem::path &cpu_root); -#endif -} // namespace clap::detail diff --git a/src/graph.cpp b/src/graph.cpp index 5e62c02..719dd8e 100644 --- a/src/graph.cpp +++ b/src/graph.cpp @@ -1,11 +1,11 @@ #include "graph.h" -#include "cpu-threads.h" #include #include #include #include #include #include +#include namespace { void load_backends() { static std::once_flag once; @@ -79,7 +79,8 @@ struct GraphRuntime::State { cpu.reset(ggml_backend_cpu_init()); if (!cpu) throw std::runtime_error("CLAP: CPU backend initialization failed"); - ggml_backend_cpu_set_n_threads(cpu.get(), threads > 0 ? threads : default_cpu_threads()); + ggml_backend_cpu_set_n_threads(cpu.get(), + threads > 0 ? threads : int(std::max(1u, std::thread::hardware_concurrency()))); if (requested == "auto") { for (size_t i = 0; i < ggml_backend_dev_count() && !gpu; ++i) { auto device = ggml_backend_dev_get(i); diff --git a/tests/cpu-threads-runtime-test.cpp b/tests/cpu-threads-runtime-test.cpp index 3af02c7..dab4cfe 100644 --- a/tests/cpu-threads-runtime-test.cpp +++ b/tests/cpu-threads-runtime-test.cpp @@ -1,11 +1,9 @@ -#include "cpu-threads.h" #include "graph.h" #include -#include +#include #include -#include -#include #include +#include // Observe the actual backend configuration without exposing a public API getter. static int configured_threads = 0; @@ -17,43 +15,16 @@ extern "C" void __wrap_ggml_backend_cpu_set_n_threads(ggml_backend_t backend, in int main() { try { - // Independent Linux oracle: package/core pairs in /proc, not sysfs. - std::ifstream input("/proc/cpuinfo"); - std::set> cores; - int package = -1, core = -1; - std::string line; - auto record = [&] { - if (package >= 0 && core >= 0) - cores.emplace(package, core); - package = core = -1; - }; - while (std::getline(input, line)) { - if (line.empty()) { - record(); - continue; - } - const auto colon = line.find(':'); - if (colon == std::string::npos) - continue; - std::istringstream value(line.substr(colon + 1)); - if (line.compare(0, 11, "physical id") == 0) - value >> package; - if (line.compare(0, 7, "core id") == 0) - value >> core; - } - record(); - if (cores.empty() || clap::detail::linux_physical_cores("/sys/devices/system/cpu") == 0) { - std::cout << "Physical topology or independent /proc oracle unavailable; skipped\n"; - return 77; - } + const int automatic = int(std::max(1u, std::thread::hardware_concurrency())); clap::detail::ModelLoader loader(clap::RuntimeOptions{0, "cpu"}); for (int requested : {0, -1, 1, 3, 129}) { clap::detail::GraphRuntime runtime(loader, requested); - const int expected = requested > 0 ? requested : int(cores.size()); + const int expected = requested > 0 ? requested : automatic; std::cout << "requested=" << requested << " configured=" << configured_threads << " expected=" << expected << '\n'; if (configured_threads != expected) - throw std::runtime_error("backend CPU threads must use physical cores unless explicitly overridden"); + throw std::runtime_error( + "backend CPU threads must use hardware concurrency unless explicitly overridden"); } return 0; } catch (const std::exception &error) { diff --git a/tests/cpu-threads-test.cpp b/tests/cpu-threads-test.cpp deleted file mode 100644 index 827bf9c..0000000 --- a/tests/cpu-threads-test.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "cpu-threads.h" -#include -#include -#include -#if defined(__linux__) -#include -#endif - -static void require(bool condition, const char *message) { - if (!condition) - throw std::runtime_error(message); -} -int main(int argc, char **argv) { - try { - using namespace clap::detail; - require(select_cpu_threads(0, 24, 48) == 24, "physical count must not be capped at eight"); - require(select_cpu_threads(-1, 24, 48) == 24, "nonpositive threads select automatic"); - for (int threads : {1, 3, 129}) - require(select_cpu_threads(threads, 24, 48) == threads, "explicit threads must be preserved"); - require(select_cpu_threads(0, 0, 48) == 48, "missing topology must use logical count without a cap"); - require(select_cpu_threads(0, -1, 0) == 1, "unavailable CPU counts must use one thread"); - require(select_cpu_threads(0, 0, std::numeric_limits::max()) == std::numeric_limits::max(), - "logical fallback must fit a positive int"); - require(default_cpu_threads() > 0, "host detection must return a usable count"); -#if defined(__linux__) - require(argc == 2, "cpu-threads-test FIXTURE_DIR"); - const std::filesystem::path root(argv[1]); - std::filesystem::remove_all(root); - auto cpu = [&](int id, int package, int core) { - const auto dir = root / ("cpu" + std::to_string(id)) / "topology"; - std::filesystem::create_directories(dir); - std::ofstream(dir / "physical_package_id") << package; - std::ofstream(dir / "core_id") << core; - }; - require(linux_physical_cores(root) == 0, "missing sysfs must signal fallback"); - for (int i = 0; i < 48; ++i) - cpu(i, i / 24, (i % 24) / 2); - require(linux_physical_cores(root) == 24, "deduplicate SMT siblings, not equal core IDs across sockets"); - cpu(48, 2, 0); - std::ofstream(root / "cpu48/online") << 0; - require(linux_physical_cores(root) == 24, "offline CPUs must not be counted"); - std::ofstream(root / "cpu48/online") << "invalid"; - require(linux_physical_cores(root) == 0, "malformed online status must signal fallback"); - std::ofstream(root / "cpu48/online") << 0; - std::filesystem::create_directories(root / "cpufreq"); - require(linux_physical_cores(root) == 24, "ignore non-CPU entries"); - std::ofstream(root / "cpu0/topology/core_id") << "invalid"; - require(linux_physical_cores(root) == 0, "malformed topology must not return a partial count"); - cpu(0, 0, -1); - require(linux_physical_cores(root) == 0, "unknown topology must signal fallback"); - std::filesystem::remove(root / "cpu0/topology/core_id"); - require(linux_physical_cores(root) == 0, "missing core ID must signal fallback"); - std::filesystem::remove_all(root); -#else - (void)argc; - (void)argv; -#endif - std::cout << "CPU physical-core detection, overrides and fallbacks passed\n"; - return 0; - } catch (const std::exception &error) { - std::cerr << error.what() << '\n'; - return 1; - } -} From f625f55b5228452584e21fe08d43b75ccb332d97 Mon Sep 17 00:00:00 2001 From: zhenghaoz Date: Sat, 7 Nov 2026 08:45:31 +0800 Subject: [PATCH 3/3] test: remove CPU thread runtime regression --- CMakeLists.txt | 7 ------ tests/cpu-threads-runtime-test.cpp | 34 ------------------------------ 2 files changed, 41 deletions(-) delete mode 100644 tests/cpu-threads-runtime-test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 708f6c7..03af389 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,13 +172,6 @@ if(BUILD_TESTING) target_link_libraries(clap-model-test PRIVATE clap nlohmann_json::nlohmann_json) target_include_directories(clap-model-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") add_test(NAME clap_model_unit COMMAND clap-model-test) - if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND NOT BUILD_SHARED_LIBS) - add_executable(cpu-threads-runtime-test tests/cpu-threads-runtime-test.cpp) - target_include_directories(cpu-threads-runtime-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") - target_link_libraries(cpu-threads-runtime-test PRIVATE clap ggml-cpu) - target_link_options(cpu-threads-runtime-test PRIVATE "-Wl,--wrap=ggml_backend_cpu_set_n_threads") - add_test(NAME clap_cpu_threads_runtime COMMAND cpu-threads-runtime-test) - endif() add_executable(graph-pipeline-test tests/graph-pipeline-test.cpp) target_include_directories(graph-pipeline-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") target_link_libraries(graph-pipeline-test PRIVATE clap) diff --git a/tests/cpu-threads-runtime-test.cpp b/tests/cpu-threads-runtime-test.cpp deleted file mode 100644 index dab4cfe..0000000 --- a/tests/cpu-threads-runtime-test.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "graph.h" -#include -#include -#include -#include -#include - -// Observe the actual backend configuration without exposing a public API getter. -static int configured_threads = 0; -extern "C" void __real_ggml_backend_cpu_set_n_threads(ggml_backend_t, int); -extern "C" void __wrap_ggml_backend_cpu_set_n_threads(ggml_backend_t backend, int threads) { - configured_threads = threads; - __real_ggml_backend_cpu_set_n_threads(backend, threads); -} - -int main() { - try { - const int automatic = int(std::max(1u, std::thread::hardware_concurrency())); - clap::detail::ModelLoader loader(clap::RuntimeOptions{0, "cpu"}); - for (int requested : {0, -1, 1, 3, 129}) { - clap::detail::GraphRuntime runtime(loader, requested); - const int expected = requested > 0 ? requested : automatic; - std::cout << "requested=" << requested << " configured=" << configured_threads << " expected=" << expected - << '\n'; - if (configured_threads != expected) - throw std::runtime_error( - "backend CPU threads must use hardware concurrency unless explicitly overridden"); - } - return 0; - } catch (const std::exception &error) { - std::cerr << error.what() << '\n'; - return 1; - } -}