From b16d8a54b04da6284f19be64cc36ae682743d9f7 Mon Sep 17 00:00:00 2001 From: eunchae Date: Thu, 6 Aug 2026 17:35:53 +0900 Subject: [PATCH] fix(sdk): make the install tree usable `cmake --install` produced a tree that could not build or run a generated model. - Generated code includes `visp/nn.h` and helpers from `src/util/`, neither of which was installed. - There was no runner source to compile a generated `.cpp` against, so consumers copied one out of the source tree by hand. - The installed `libvisioncpp.so` records `libggml.so.0` in DT_NEEDED but had no RUNPATH. Linking succeeded and startup failed with "libggml.so.0: cannot open shared object file" -- the linker resolves the path CMake gives it, the loader searches again at run time and had nothing to go on. Every consumer worked around it with LD_LIBRARY_PATH. Installs the missing headers, installs the existing generic runner `tools/verify/backbone/run_dump.cpp` as `VISP_RUNNER` in the package config, and sets `CMAKE_INSTALL_RPATH` to `$ORIGIN`. ggml installs next to libvisioncpp, so the loader finds it with no environment variable. The runner gains two switches it needs to be useful outside this tree: `VISP_BACKEND=cpu` forces the CPU device, because `backend_init()` picks an accelerator backend that a build may include but a machine may not have; and `VISP_DUMP_NODES=` writes every named intermediate, because comparing only the final output says that something diverged, not where. Verified by building a generated model against the install tree with nothing but find_package(visioncpp): it links, runs, and writes its output. Co-Authored-By: Claude Opus 5 --- CMakeLists.txt | 18 ++++++++++++- scripts/cmake/visioncpp-config.cmake.in | 5 ++++ tools/verify/backbone/run_dump.cpp | 34 ++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 88ce3aa..ffe5795 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,10 +17,19 @@ include(GNUInstallDirs) set(VISP_INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Location of header files") set(VISP_LIB_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Location of library files") set(VISP_BIN_INSTALL_DIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Location of binary files") +set(VISP_SHARE_INSTALL_DIR "${CMAKE_INSTALL_DATAROOTDIR}/visioncpp" CACHE PATH "Location of SDK data (generic runner source)") if(VISP_INSTALL_MODELS) set(VISP_MODEL_INSTALL_DIR "${CMAKE_INSTALL_DATAROOTDIR}/visioncpp" CACHE PATH "Directory to install default models to") endif() +# The installed libvisioncpp.so records libggml.so.0 in DT_NEEDED. Without a RUNPATH a +# consumer links successfully and then fails at startup with +# "libggml.so.0: cannot open shared object file" -- the linker resolves the path CMake gives +# it, and the loader searches again at run time with nothing to go on. +# $ORIGIN is the directory holding the .so itself; ggml installs into the same lib/, so this +# one line removes the need for LD_LIBRARY_PATH. +set(CMAKE_INSTALL_RPATH "$ORIGIN") + if(PROJECT_IS_TOP_LEVEL) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) @@ -169,6 +178,13 @@ install(TARGETS visioncpp LIBRARY DESTINATION ${VISP_LIB_INSTALL_DIR} ARCHIVE DESTINATION ${VISP_LIB_INSTALL_DIR}) install(DIRECTORY include/ DESTINATION ${VISP_INCLUDE_INSTALL_DIR}) +# Internal headers that generated code includes; consumers of the install tree need them. +install(FILES src/visp/nn.h DESTINATION ${VISP_INCLUDE_INSTALL_DIR}/visp) +install(DIRECTORY src/util/ DESTINATION ${VISP_INCLUDE_INSTALL_DIR}/util + FILES_MATCHING PATTERN "*.h") + +# Generic runner source. Consumers compile it together with their generated .cpp. +install(FILES tools/verify/backbone/run_dump.cpp DESTINATION ${VISP_SHARE_INSTALL_DIR}) if(PROJECT_IS_TOP_LEVEL) install(FILES README.md LICENSE DESTINATION .) endif() @@ -190,7 +206,7 @@ configure_package_config_file( scripts/cmake/visioncpp-config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/visioncpp-config.cmake INSTALL_DESTINATION ${VISP_LIB_INSTALL_DIR}/cmake/visioncpp - PATH_VARS VISP_INCLUDE_INSTALL_DIR VISP_LIB_INSTALL_DIR VISP_BIN_INSTALL_DIR + PATH_VARS VISP_INCLUDE_INSTALL_DIR VISP_LIB_INSTALL_DIR VISP_BIN_INSTALL_DIR VISP_SHARE_INSTALL_DIR ) write_basic_package_version_file( visioncpp-version.cmake diff --git a/scripts/cmake/visioncpp-config.cmake.in b/scripts/cmake/visioncpp-config.cmake.in index 2904aa9..44d1425 100644 --- a/scripts/cmake/visioncpp-config.cmake.in +++ b/scripts/cmake/visioncpp-config.cmake.in @@ -2,6 +2,11 @@ set_and_check(VISP_INCLUDE_DIR "@PACKAGE_VISP_INCLUDE_INSTALL_DIR@") set_and_check(VISP_LIB_DIR "@PACKAGE_VISP_LIB_INSTALL_DIR@") +set_and_check(VISP_SHARE_DIR "@PACKAGE_VISP_SHARE_INSTALL_DIR@") + +# Generic runner source for generated models. For example: +# add_executable(run ${VISP_RUNNER} ${GEN}/${ARCH}.cpp) +set(VISP_RUNNER "${VISP_SHARE_DIR}/run_dump.cpp") include(CMakeFindDependencyMacro) find_dependency(ggml) diff --git a/tools/verify/backbone/run_dump.cpp b/tools/verify/backbone/run_dump.cpp index f4a9a07..5a95ed0 100755 --- a/tools/verify/backbone/run_dump.cpp +++ b/tools/verify/backbone/run_dump.cpp @@ -36,7 +36,13 @@ int main(int argc, char** argv) { std::string pref = argv[3]; const int SZ = argc > 4 ? atoi(argv[4]) : 512; - backend_device backend = backend_init(); + // backend_init() picks the best device it can find. When a build includes an + // accelerator backend that is not actually present, that choice fails late and + // unhelpfully, and numeric comparison wants the CPU anyway. + const char* backend_env = std::getenv("VISP_BACKEND"); + backend_device backend = (backend_env && std::string(backend_env) == "cpu") + ? backend_init(backend_type::cpu) + : backend_init(); model_file file = model_load(gguf); model_weights weights = model_init(file.n_tensors()); model_transfer(file, weights, backend, backend.preferred_float_type(), file.tensor_layout()); @@ -50,6 +56,15 @@ int main(int argc, char** argv) { tensor last = FWD(m, input, p); // 내부에서 compute_graph_output 이 out_i 를 그래프에 등록 ggml_build_forward_expand(graph, last); + // VISP_DUMP_NODES= writes every named intermediate as .bin. Comparing only + // the final output says that something diverged, not where. + const char* dump_dir = std::getenv("VISP_DUMP_NODES"); + if (dump_dir) { + for (int i = 0; i < ggml_graph_n_nodes(graph.graph); ++i) { + ggml_set_output(ggml_graph_node(graph.graph, i)); + } + } + compute_graph_allocate(graph, backend); auto in = load_bin(inb, (size_t)3 * SZ * SZ); transfer_to_backend(input, std::span(in.data(), in.size())); @@ -71,5 +86,22 @@ int main(int argc, char** argv) { (long long)t->ne[0], (long long)t->ne[1], (long long)t->ne[2], (long long)t->ne[3]); } printf("dumped %d outputs → %s.out.*.bin\n", n, pref.c_str()); + + if (dump_dir) { + int written = 0; + for (int i = 0; i < ggml_graph_n_nodes(graph.graph); ++i) { + tensor t = ggml_graph_node(graph.graph, i); + const char* nm = ggml_get_name(t); + if (!nm || !nm[0] || t->type != GGML_TYPE_F32) { + continue; + } + std::vector d(ggml_nelements(t)); + transfer_from_backend(t, std::span(d.data(), d.size())); + std::ofstream of(std::string(dump_dir) + "/" + nm + ".bin", std::ios::binary); + of.write(reinterpret_cast(d.data()), d.size() * sizeof(float)); + ++written; + } + printf("- VISP_DUMP_NODES: %d intermediates → %s\n", written, dump_dir); + } return 0; }