diff --git a/.github/fabric-consumers.json b/.github/fabric-consumers.json index 586c63e578..80957073c0 100644 --- a/.github/fabric-consumers.json +++ b/.github/fabric-consumers.json @@ -1,5 +1,6 @@ { "npm_runtime": [ - "classification-ggml" + "classification-ggml", + "vla-ggml" ] } diff --git a/.github/workflows/cpp-tests-vla.yml b/.github/workflows/cpp-tests-vla.yml index c741c6e2a8..ead188fe63 100644 --- a/.github/workflows/cpp-tests-vla.yml +++ b/.github/workflows/cpp-tests-vla.yml @@ -15,6 +15,13 @@ on: type: string required: false default: "packages/vla-ggml" + fabric-overlay-artifact: + description: >- + Workflow-run artifact name for PR-built @qvac/fabric prebuilds. Empty + skips the overlay (published npm baseline). + type: string + required: false + default: "" workflow_dispatch: inputs: ref: @@ -125,6 +132,22 @@ jobs: run: | npm install + - name: Download fabric-prebuilds for overlay + if: ${{ inputs.fabric-overlay-artifact != '' }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1 + with: + name: ${{ inputs.fabric-overlay-artifact }} + path: ${{ runner.temp }}/fabric-prebuilds + + - name: Overlay PR @qvac/fabric prebuilds + if: ${{ inputs.fabric-overlay-artifact != '' }} + uses: tetherto/qvac/.github/actions/overlay-local-fabric@6620d695d3a07cafe3931182e7844dde376a23f8 + with: + workdir: ${{ env.WORKDIR }} + prebuilds-root: ${{ runner.temp }}/fabric-prebuilds + platform: ${{ matrix.platform }} + arch: ${{ matrix.arch }} + - name: Install bare tooling run: npm install -g --force bare bare-make @@ -133,12 +156,6 @@ jobs: shell: bash run: echo "VULKAN_SDK=/opt/vulkansdk/x86_64" >> $GITHUB_ENV - # vla requests qvac-fabric[hip-backend] (linux-x64), which requires ROCm at - # build time — install it so the addon's C++ tests build with HIP. - - if: ${{ matrix.os == 'ubuntu-24.04' }} - name: Setup ROCm SDK for HIP cross-compile - uses: ./.github/actions/setup-rocm - - name: Setup vcpkg uses: tetherto/qvac/.github/actions/setup-vcpkg@98990f3d11736a1ae88feddc514d14d78d7abdc7 env: @@ -292,12 +309,12 @@ jobs: exit 1 fi - cd build/test/unit/ - # Capture test output to a file for later parsing (embodiment sweep results) - ./addon-test --gtest_output=xml:cpp-test-results.xml 2>&1 | tee test-output.log + # test:cpp:run applies fabric-boundary ASAN_OPTIONS via scripts/run-cpp-tests.js. + # pipefail: a failing addon-test exit code propagates through tee. + npm run test:cpp:run 2>&1 | tee build/test/unit/test-output.log - if [ -f cpp-test-results.xml ]; then - test_count=$(grep -c '- + Workflow-run artifact name for PR-built @qvac/fabric prebuilds. Empty + skips the overlay (published npm baseline). + type: string + required: false + default: "" workflow_dispatch: inputs: ref: @@ -153,6 +160,21 @@ jobs: run: | npm install + - name: Download fabric-prebuilds for overlay + if: ${{ inputs.fabric-overlay-artifact != '' }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1 + with: + name: ${{ inputs.fabric-overlay-artifact }} + path: ${{ runner.temp }}/fabric-prebuilds + + - name: Overlay PR @qvac/fabric prebuilds + if: ${{ inputs.fabric-overlay-artifact != '' }} + uses: tetherto/qvac/.github/actions/overlay-local-fabric@6620d695d3a07cafe3931182e7844dde376a23f8 + with: + workdir: ${{ env.WORKDIR }} + prebuilds-root: ${{ runner.temp }}/fabric-prebuilds + platform: ${{ matrix.platform }} + arch: ${{ matrix.arch }} - name: Install bare tooling run: npm install -g --force bare bare-make bare-runtime bare-https brittle diff --git a/.github/workflows/on-pr-vla.yml b/.github/workflows/on-pr-vla.yml index c1284f84f7..76d6de269e 100644 --- a/.github/workflows/on-pr-vla.yml +++ b/.github/workflows/on-pr-vla.yml @@ -16,6 +16,8 @@ on: paths: - "packages/vla-ggml/**" - ".github/workflows/*-vla.yml" + - "packages/fabric/vcpkg.json" + - "packages/fabric/vcpkg-configuration.json" workflow_dispatch: workflow_call: @@ -86,24 +88,62 @@ jobs: with: github-token: ${{ github.token }} - verify-fabric-lockstep: + detect-fabric-stack: if: needs.authorize.outputs.allowed == 'true' - needs: [fork-approval, authorize] + needs: + - fork-approval + - authorize + - ci-router runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + outputs: + fabric_stack: ${{ steps.detect.outputs.fabric_stack }} + needs_fabric_artifact: ${{ steps.detect.outputs.fabric_stack == 'true' && needs.ci-router.outputs.run_prebuilds == 'true' && github.event_name == 'pull_request_target' }} steps: - - name: Checkout code + - name: Checkout repository (default branch) uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 + with: + ref: ${{ github.event.repository.default_branch }} + - name: Detect fabric-stack PR + id: detect + uses: tetherto/qvac/.github/actions/detect-fabric-stack@6620d695d3a07cafe3931182e7844dde376a23f8 + - name: Warn when a stack PR builds against released fabric + if: steps.detect.outputs.fabric_stack == 'true' && needs.ci-router.outputs.run_prebuilds != 'true' + shell: bash + run: | + echo "::warning::This PR changes packages/fabric, but no prebuilds label is set, so on-pr-fabric publishes nothing. Native jobs here build against the released @qvac/fabric, not this PR's. Add the 'prebuilds' label to exercise the stack." - - name: Verify qvac-fabric versions are lockstep - id: lockstep - uses: ./.github/actions/verify-qvac-fabric-lockstep - - - name: Report verified version - run: 'echo "Verified qvac-fabric version: ${{ steps.lockstep.outputs.version }}"' + resolve-fabric-prebuilds: + if: needs.detect-fabric-stack.outputs.needs_fabric_artifact == 'true' && needs.authorize.outputs.allowed == 'true' + needs: + - fork-approval + - authorize + - detect-fabric-stack + runs-on: ubuntu-latest + timeout-minutes: 130 + permissions: + contents: read + actions: read + steps: + - name: Wait for fabric-prebuilds from on-pr-fabric + uses: tetherto/qvac/.github/actions/wait-and-download-fabric-prebuilds@6620d695d3a07cafe3931182e7844dde376a23f8 + with: + github-token: ${{ github.token }} + head-sha: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Re-publish fabric-prebuilds for downstream jobs + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0 + with: + name: fabric-prebuilds + path: fabric-prebuilds-staging sanity-checks: - if: always() && !cancelled() && needs.ci-router.outputs.run_verified_checks == 'true' && needs.authorize.outputs.allowed == 'true' - needs: [authorize, verify-fabric-lockstep, ci-router, fork-approval] + if: needs.ci-router.outputs.run_verified_checks == 'true' && needs.authorize.outputs.allowed == 'true' + needs: + - fork-approval + - authorize + - ci-router runs-on: ubuntu-latest steps: - name: Checkout code @@ -119,21 +159,17 @@ jobs: workdir: packages/vla-ggml cpp-lint: - # Runs on PRs (base SHA diff target) and on manual workflow_dispatch, where - # cpp-lint.yaml's Resolve refs falls back to HEAD~1 for the format/tidy diff - # base. Manual dispatch pointing at a branch lets us validate workflow - # changes (e.g. the hip-backend ROCm setup) before they land on the base. - if: always() && !cancelled() && needs.ci-router.outputs.run_verified_checks == 'true' && needs.authorize.outputs.allowed == 'true' && (github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch') - needs: [authorize, ci-router, fork-approval] + if: needs.ci-router.outputs.run_verified_checks == 'true' && needs.authorize.outputs.allowed == 'true' && (github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch') + needs: + - fork-approval + - authorize + - ci-router uses: ./.github/workflows/cpp-lint.yaml secrets: inherit with: - sha: ${{ github.event.pull_request.base.sha }} - pr_head_sha: ${{ github.event.pull_request.head.sha }} + sha: ${{ github.event.pull_request.base.sha || github.event.before || 'HEAD~1' }} + pr_head_sha: ${{ github.event.pull_request.head.sha || github.sha }} workdir: packages/vla-ggml - # vla pulls qvac-fabric[hip-backend]; ggml-config find_dependency(hip) - # needs ROCm on the lint runner at configure time. - include-rocm-sdk: true cpp-tests: permissions: @@ -141,17 +177,33 @@ jobs: packages: read pull-requests: write id-token: write # OIDC to pull the GR00T parity model from S3 (cpp-tests-vla) - if: always() && !cancelled() && needs.ci-router.outputs.run_cpp_tests == 'true' && needs.authorize.outputs.allowed == 'true' - needs: [authorize, sanity-checks, ci-router, fork-approval] + if: | + always() && + needs.sanity-checks.result == 'success' && + needs.detect-fabric-stack.result == 'success' && + needs.ci-router.outputs.run_cpp_tests == 'true' && + needs.authorize.outputs.allowed == 'true' && + (needs.detect-fabric-stack.outputs.needs_fabric_artifact != 'true' || needs.resolve-fabric-prebuilds.result == 'success') + needs: + - fork-approval + - authorize + - sanity-checks + - ci-router + - detect-fabric-stack + - resolve-fabric-prebuilds uses: ./.github/workflows/cpp-tests-vla.yml secrets: inherit with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.event.pull_request.head.sha || github.ref }} + fabric-overlay-artifact: ${{ needs.detect-fabric-stack.outputs.needs_fabric_artifact == 'true' && 'fabric-prebuilds' || '' }} ts-checks: - if: needs.authorize.outputs.allowed == 'true' - needs: [authorize, fork-approval] + if: needs.ci-router.outputs.run_verified_checks == 'true' && needs.authorize.outputs.allowed == 'true' + needs: + - fork-approval + - authorize + - ci-router runs-on: ubuntu-latest environment: release steps: @@ -165,7 +217,7 @@ jobs: - name: Install dependencies working-directory: packages/vla-ggml - run: npm install + run: npm install --ignore-scripts - name: Type declaration check working-directory: packages/vla-ggml @@ -202,8 +254,14 @@ jobs: workdir: packages/vla-ggml prebuild-artifact-reuse: - if: needs.detect-native-changes.outputs.native_changed == 'false' - needs: [detect-native-changes, sanity-checks, fork-approval] + if: | + needs.detect-native-changes.outputs.native_changed == 'false' && + needs.detect-fabric-stack.outputs.fabric_stack != 'true' + needs: + - fork-approval + - detect-native-changes + - detect-fabric-stack + - sanity-checks runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -233,19 +291,29 @@ jobs: packages: write pull-requests: write id-token: write + needs: + - fork-approval + - authorize + - ci-router + - sanity-checks + - detect-native-changes + - detect-fabric-stack + - resolve-fabric-prebuilds + - prebuild-artifact-reuse if: | always() && - !cancelled() && needs.sanity-checks.result == 'success' && + needs.detect-fabric-stack.result == 'success' && needs.ci-router.outputs.run_prebuilds == 'true' && needs.authorize.outputs.allowed == 'true' && + (needs.detect-fabric-stack.outputs.needs_fabric_artifact != 'true' || needs.resolve-fabric-prebuilds.result == 'success') && (needs.detect-native-changes.outputs.native_changed == 'true' || needs.prebuild-artifact-reuse.outputs.reuse_hit != 'true') - needs: [authorize, ci-router, sanity-checks, detect-native-changes, prebuild-artifact-reuse, fork-approval] uses: ./.github/workflows/prebuilds-vla.yml secrets: inherit with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.event.pull_request.head.sha || github.ref }} + fabric-overlay-artifact: ${{ needs.detect-fabric-stack.outputs.needs_fabric_artifact == 'true' && 'fabric-prebuilds' || '' }} prebuild-artifact-save: if: always() && !cancelled() && needs.prebuild.result == 'success' @@ -268,8 +336,21 @@ jobs: pr-number: ${{ github.event.pull_request.number }} integration-tests: - needs: [authorize, prebuild, prebuild-artifact-reuse, ci-router, fork-approval] - if: always() && !cancelled() && needs.ci-router.outputs.run_desktop == 'true' && needs.authorize.outputs.allowed == 'true' && (needs.prebuild.result == 'success' || needs.prebuild-artifact-reuse.outputs.reuse_hit == 'true') + needs: + - fork-approval + - authorize + - prebuild + - prebuild-artifact-reuse + - ci-router + - detect-fabric-stack + - resolve-fabric-prebuilds + if: | + always() && + needs.detect-fabric-stack.result == 'success' && + needs.ci-router.outputs.run_desktop == 'true' && + needs.authorize.outputs.allowed == 'true' && + (needs.detect-fabric-stack.outputs.needs_fabric_artifact != 'true' || needs.resolve-fabric-prebuilds.result == 'success') && + (needs.prebuild.result == 'success' || needs.prebuild-artifact-reuse.outputs.reuse_hit == 'true') permissions: contents: read packages: read @@ -277,8 +358,9 @@ jobs: uses: ./.github/workflows/integration-test-vla.yml secrets: inherit with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.event.pull_request.head.sha || github.ref }} + fabric-overlay-artifact: ${{ needs.detect-fabric-stack.outputs.needs_fabric_artifact == 'true' && 'fabric-prebuilds' || '' }} # Mobile integration tests are no longer run on PRs. They run on demand via # `integration-mobile-test-vla.yml` (workflow_dispatch), where the dispatcher @@ -324,9 +406,30 @@ jobs: merge-guard: # run-mobile-integration-tests removed — mobile is on-demand only now. - needs: [authorize, verify-fabric-lockstep, sanity-checks, cpp-lint, cpp-tests, prebuild, prebuild-artifact-reuse, prebuild-artifact-save, detect-native-changes, integration-tests, ts-checks, ci-router] + needs: + [ + authorize, + ci-router, + sanity-checks, + ts-checks, + cpp-lint, + cpp-tests, + detect-fabric-stack, + resolve-fabric-prebuilds, + detect-native-changes, + prebuild-artifact-reuse, + prebuild, + prebuild-artifact-save, + integration-tests, + publish-prebuild-status, + ] if: always() && !cancelled() uses: ./.github/workflows/public-pr.yml + permissions: + contents: read + pull-requests: write + packages: read with: - sanity-checks-status: ${{ needs.verify-fabric-lockstep.result == 'success' && needs.sanity-checks.result == 'success' && needs.ts-checks.result == 'success' }} - build-status: ${{ needs.prebuild.result == 'success' || needs.prebuild.result == 'skipped' || needs.prebuild-artifact-reuse.outputs.reuse_hit == 'true' }} + sanity-checks-status: ${{ needs.detect-fabric-stack.result != 'failure' && (needs.detect-fabric-stack.outputs.needs_fabric_artifact != 'true' || needs.resolve-fabric-prebuilds.result == 'success') && needs.sanity-checks.result == 'success' && needs.ts-checks.result == 'success' && (needs.cpp-lint.result == 'success' || needs.cpp-lint.result == 'skipped') && (needs.cpp-tests.result == 'success' || needs.cpp-tests.result == 'skipped') }} + build-status: ${{ needs.detect-fabric-stack.result != 'failure' && (needs.detect-fabric-stack.outputs.needs_fabric_artifact != 'true' || needs.resolve-fabric-prebuilds.result == 'success') && (needs.prebuild.result == 'success' || needs.prebuild.result == 'skipped' || needs.prebuild-artifact-reuse.outputs.reuse_hit == 'true') }} + integration-tests-status: ${{ needs.detect-fabric-stack.result != 'failure' && (needs.detect-fabric-stack.outputs.needs_fabric_artifact != 'true' || needs.resolve-fabric-prebuilds.result == 'success') && (needs.integration-tests.result == 'success' || needs.integration-tests.result == 'skipped') }} diff --git a/.github/workflows/prebuilds-vla.yml b/.github/workflows/prebuilds-vla.yml index e161486523..dbccb8129d 100644 --- a/.github/workflows/prebuilds-vla.yml +++ b/.github/workflows/prebuilds-vla.yml @@ -31,6 +31,13 @@ on: required: false type: string default: "packages/vla-ggml" + fabric-overlay-artifact: + description: >- + Workflow-run artifact name for PR-built @qvac/fabric prebuilds. Empty + skips the overlay (published npm baseline). + type: string + required: false + default: "" permissions: contents: read @@ -49,7 +56,5 @@ jobs: artifact-name-prefix: vla- linux-extra-packages: libxi-dev libxtst-dev libxrandr-dev include-vulkan-sdk: true - # Cross-compile the ROCm/HIP backend (gfx1151) into the linux-x64 prebuild. - # No AMD GPU needed on the runner; fail-safe if ROCm install/feature is absent. - include-rocm: true + fabric-overlay-artifact: ${{ inputs.fabric-overlay-artifact }} secrets: inherit diff --git a/packages/fabric/vcpkg-configuration.json b/packages/fabric/vcpkg-configuration.json index 39265c0d89..976ad6a46b 100644 --- a/packages/fabric/vcpkg-configuration.json +++ b/packages/fabric/vcpkg-configuration.json @@ -1,4 +1,7 @@ { + "overlay-ports": [ + "../../vcpkg-overlays/ports" + ], "default-registry": { "kind": "git", "baseline": "c57eec31bdc37ce3a98537b312f5a366b554db6a", diff --git a/packages/vla-ggml/CHANGELOG.md b/packages/vla-ggml/CHANGELOG.md index ddbee535b5..562682eb8f 100644 --- a/packages/vla-ggml/CHANGELOG.md +++ b/packages/vla-ggml/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [0.23.0] - 2026-08-21 + +### Changed + +- Migrated the ggml runtime off the static `qvac-fabric` vcpkg port and onto the + shared `@qvac/fabric` npm dependency (`^0.6.0`). The addon no longer bundles + ggml or its compute backends; on desktop it resolves the single `@qvac/fabric` + install and loads backend modules from + `node_modules/@qvac/fabric/prebuilds//qvac__fabric/`, falling back to + this addon's own `prebuilds/` on mobile (where the package tree isn't + resolvable from the packed worklet bundle). Run `npm install` so `@qvac/fabric` + is present before `bare-make generate`/`build`, and ensure the dependency isn't + pruned at runtime. +- The `qvac-fabric[hip-backend]` vcpkg feature is removed from this package; ROCm/HIP + backends are provided by `@qvac/fabric` when present in its prebuilds. +- `@qvac/fabric` dependency carries `qvac-fabric` `10069.2.0` (TurboVec CPU + support from the shared runtime; no API change for this package). + ## [0.22.0] - 2026-08-20 ### Changed diff --git a/packages/vla-ggml/CMakeLists.txt b/packages/vla-ggml/CMakeLists.txt index 0397d3b6ab..6d52b30f4d 100644 --- a/packages/vla-ggml/CMakeLists.txt +++ b/packages/vla-ggml/CMakeLists.txt @@ -1,83 +1,36 @@ cmake_minimum_required(VERSION 3.25) -option(ANDROID_STL "Android STL linkage" c++_shared) -option(BUILD_TESTING "Build tests" OFF) -option(ENABLE_COVERAGE "Enable coverage instrumentation for unit tests" OFF) -if(BUILD_TESTING) - list(APPEND VCPKG_MANIFEST_FEATURES "tests") -endif() - -find_package(cmake-bare REQUIRED PATHS node_modules/cmake-bare) -find_package(cmake-vcpkg REQUIRED PATHS node_modules/cmake-vcpkg) - -set(VCPKG_OVERLAY_TRIPLETS "${CMAKE_CURRENT_SOURCE_DIR}/../../vcpkg-overlays/triplets;${VCPKG_OVERLAY_TRIPLETS}") - -project(qvac-lib-infer-vla C CXX) - -if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - add_compile_options(-stdlib=libc++) - add_link_options(-stdlib=libc++ -static-libstdc++) -endif() - -find_path(VCPKG_INSTALLED_PATH share/lint-cpp/.clang-format REQUIRED) -configure_file(${VCPKG_INSTALLED_PATH}/share/lint-cpp/.clang-format - ${CMAKE_CURRENT_SOURCE_DIR}/.clang-format COPYONLY) -configure_file(${VCPKG_INSTALLED_PATH}/share/lint-cpp/.clang-tidy - ${CMAKE_CURRENT_SOURCE_DIR}/.clang-tidy COPYONLY) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_POSITION_INDEPENDENT_CODE ON) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +include(${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/qvac-addon/qvac-addon.cmake) +qvac_addon_preproject() -find_path(QVAC_LIB_INFERENCE_ADDON_CPP_INCLUDE_DIRS "inference-addon-cpp/JsInterface.hpp" REQUIRED) +project(qvac-lib-infer-vla LANGUAGES C CXX) -# ggml is provided by the qvac-fabric vcpkg port (same port that ships -# llama.cpp for the LLM addon). Backends (Vulkan, Metal, …) are loaded at -# runtime via ggml_backend_load_all_from_path; here we just link the core -# libraries and install each available backend as a shared library. -find_package(ggml CONFIG REQUIRED) +qvac_addon_project_setup() -if(WIN32) - add_definitions(-DNOMINMAX -DWIN32_LEAN_AND_MEAN -DNOGDI) -endif() - -# Compute the per-addon "backends" directory that the module will ship; the -# C++ code uses this path at runtime to discover backend plugins (mirrors the -# LLM addon). -bare_target(bare_target_value) -bare_module_target("." unused_target NAME module_name VERSION unused_version) -set(BACKENDS_SUBDIR_VALUE "${bare_target_value}/${module_name}") -message("Building qvac-lib-infer-vla with BACKENDS_SUBDIR='${BACKENDS_SUBDIR_VALUE}'") +find_path(QVAC_LIB_INFERENCE_ADDON_CPP_INCLUDE_DIRS + "inference-addon-cpp/JsInterface.hpp" REQUIRED) -# Collect backend shared libraries to export alongside the .bare module. -# On Android/Linux (and Windows if applicable) each ggml backend is a real -# shared library that must ship with the prebuild. On macOS/iOS the Metal -# backend is compiled into ggml itself, so there's nothing extra to copy. -set(BACKEND_DL_LIBS "") -if((ANDROID OR UNIX) AND NOT APPLE) - foreach(_backend ${GGML_AVAILABLE_BACKENDS}) - list(APPEND BACKEND_DL_LIBS INSTALL TARGET ggml::${_backend}) - endforeach() -endif() +# ggml runtime is the shared @qvac/fabric prebuild (dynamically linked, loaded +# once per process); sets qvac_fabric_target + BACKENDS_SUBDIR_VALUE. +qvac_addon_use_fabric() -add_bare_module(qvac-lib-infer-vla EXPORTS ${BACKEND_DL_LIBS}) +add_bare_module(qvac-lib-infer-vla EXPORTS) -if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - target_link_options(${qvac-lib-infer-vla}_module PRIVATE -Wl,--exclude-libs,ALL) -endif() +set(ADDON_SOURCES + ${PROJECT_SOURCE_DIR}/addon/src/js-interface/binding.cpp + ${PROJECT_SOURCE_DIR}/addon/src/model-interface/smolvla.cpp + ${PROJECT_SOURCE_DIR}/addon/src/model-interface/smolvla_adapter.cpp + ${PROJECT_SOURCE_DIR}/addon/src/model-interface/pi05.cpp + ${PROJECT_SOURCE_DIR}/addon/src/model-interface/groot.cpp + ${PROJECT_SOURCE_DIR}/addon/src/model-interface/model_factory.cpp + ${PROJECT_SOURCE_DIR}/addon/src/utils/BackendSelection.cpp + ${PROJECT_SOURCE_DIR}/addon/src/utils/LoggingMacros.cpp +) target_sources( ${qvac-lib-infer-vla} PRIVATE - ${PROJECT_SOURCE_DIR}/addon/src/js-interface/binding.cpp - ${PROJECT_SOURCE_DIR}/addon/src/model-interface/smolvla.cpp - ${PROJECT_SOURCE_DIR}/addon/src/model-interface/smolvla_adapter.cpp - ${PROJECT_SOURCE_DIR}/addon/src/model-interface/pi05.cpp - ${PROJECT_SOURCE_DIR}/addon/src/model-interface/groot.cpp - ${PROJECT_SOURCE_DIR}/addon/src/model-interface/model_factory.cpp - ${PROJECT_SOURCE_DIR}/addon/src/utils/BackendSelection.cpp - ${PROJECT_SOURCE_DIR}/addon/src/utils/LoggingMacros.cpp + ${ADDON_SOURCES} ) target_include_directories( @@ -87,28 +40,13 @@ target_include_directories( ${PROJECT_SOURCE_DIR}/addon/src ) -target_link_libraries(${qvac-lib-infer-vla} PRIVATE ggml::ggml) +qvac_addon_link_fabric(${qvac-lib-infer-vla} ${qvac_fabric_target}) -# Route QLOG calls into the JS-side logger installed via setLogger() (matches -# diffusion-cpp). Public so unit-test targets that link the same translation -# units pick the same code path. -target_compile_definitions(${qvac-lib-infer-vla} PUBLIC JS_LOGGER) +qvac_addon_finalize(${qvac-lib-infer-vla} SUBDIR "${BACKENDS_SUBDIR_VALUE}") -target_compile_definitions(${qvac-lib-infer-vla} - PRIVATE - BACKENDS_SUBDIR="${BACKENDS_SUBDIR_VALUE}") - -# Mirror the diffusion addon: when ggml is built with GGML_BACKEND_DL=ON -# (Android), tell the addon's translation units so any conditional code -# (e.g. ggml_backend_load_all_from_path call sites) compiles consistently -# with the headers and the bundled backend modules. -if(GGML_BACKEND_DL) - target_compile_definitions(${qvac-lib-infer-vla} PRIVATE GGML_BACKEND_DL) -endif() if(BUILD_TESTING) find_package(GTest CONFIG REQUIRED) include(GoogleTest) enable_testing() - add_subdirectory(test/unit) endif() diff --git a/packages/vla-ggml/binding.js b/packages/vla-ggml/binding.js index cea46308c0..581704c8ad 100644 --- a/packages/vla-ggml/binding.js +++ b/packages/vla-ggml/binding.js @@ -1 +1,6 @@ +// Pre-load @qvac/fabric so its shared .bare module (the llama.cpp + ggml +// runtime) is registered with the bare runtime before our addon triggers +// resolution of its DT_NEEDED dependency qvac__fabric@0.bare. +require('@qvac/fabric') + module.exports = require.addon() diff --git a/packages/vla-ggml/index.js b/packages/vla-ggml/index.js index 9779912a4e..909ffeaa4b 100644 --- a/packages/vla-ggml/index.js +++ b/packages/vla-ggml/index.js @@ -14,6 +14,23 @@ const errorModule = require("./lib/error"); const infer_base_1 = require("@qvac/infer-base"); const { DEFAULT_IMAGE_SIZE } = addonModule; const { QvacErrorAddonVla, ERR_CODES } = errorModule; +// The ggml compute backends (GGML_BACKEND_DL modules) ship exactly once, in the +// @qvac/fabric dependency (prebuilds//qvac__fabric). On desktop, resolve +// the single @qvac/fabric install and load the backends from there. On mobile +// the package tree isn't resolvable at runtime (the worklet runs from a packed +// bundle), so fall back to this addon's own prebuilds, where the mobile +// packaging stages the backends. The native side appends BACKENDS_SUBDIR +// ("/qvac__fabric") to whichever root we return. +function resolveBackendsDir() { + try { + const fabricPkg = require.resolve("@qvac/fabric/package"); + const fabricPrebuilds = path.join(path.dirname(fabricPkg), "prebuilds"); + if (fs.existsSync(fabricPrebuilds)) + return fabricPrebuilds; + } + catch { } + return path.join(__dirname, "prebuilds"); +} // eslint-disable-next-line @typescript-eslint/no-require-imports -- native binding is resolved lazily from package prebuilds. const binding = require("./binding"); // Maps the C++ Priority enum (0=ERROR, 1=WARNING, 2=INFO, 3=DEBUG) to the @@ -509,7 +526,7 @@ class VlaModel { // JobRunner thread consumes runJob() and feeds the outputCb. const backendsDir = this._config.backendsDir ? this._config.backendsDir - : path.join(__dirname, "prebuilds"); + : resolveBackendsDir(); this._handle = binding.createInstance(this, { ggufPath, backend, diff --git a/packages/vla-ggml/package.json b/packages/vla-ggml/package.json index 0f251b29b9..39c6abf9ca 100644 --- a/packages/vla-ggml/package.json +++ b/packages/vla-ggml/package.json @@ -1,6 +1,6 @@ { "name": "@qvac/vla-ggml", - "version": "0.22.0", + "version": "0.23.0", "description": "VLA vision-language-action inference addon for QVAC (ggml backend)", "addon": true, "engines": { @@ -26,8 +26,9 @@ "test:unit:generate": "brittle -r test/unit/all.js test/unit/*.test.js", "test:unit": "npm run build:ts && npm run test:unit:generate && bare test/unit/all.js --exit && npm run test:prestage", "test:prestage": "node --test scripts/__tests__/generate-prestage-block.test.js", + "test:scripts": "node --test scripts/__tests__/run-cpp-tests.test.js", "test:cpp:build": "bare-make generate -D BUILD_TESTING=ON && bare-make build --target addon-test", - "test:cpp:run": "cd build/test/unit/ && ./addon-test --gtest_output=xml:cpp-test-results.xml", + "test:cpp:run": "npm run test:scripts && node scripts/run-cpp-tests.js", "test:cpp": "npm run test:cpp:build && npm run test:cpp:run", "coverage:cpp:build": "bare-make generate -D BUILD_TESTING=ON -D ENABLE_COVERAGE=ON && bare-make build --target addon-test", "coverage:cpp:run": "cd build/test/unit/ && LLVM_PROFILE_FILE=default.profraw ./addon-test --gtest_output=xml:cpp-test-results.xml", @@ -62,6 +63,7 @@ "bugs": "https://github.com/tetherto/qvac/issues", "homepage": "https://qvac.tether.io", "dependencies": { + "@qvac/fabric": "^0.6.0", "@qvac/error": "^0.1.0", "@qvac/infer-base": "^0.6.2", "@qvac/logging": "^0.1.0", diff --git a/packages/vla-ggml/scripts/__tests__/run-cpp-tests.test.js b/packages/vla-ggml/scripts/__tests__/run-cpp-tests.test.js new file mode 100644 index 0000000000..2ce8224ef9 --- /dev/null +++ b/packages/vla-ggml/scripts/__tests__/run-cpp-tests.test.js @@ -0,0 +1,81 @@ +'use strict' + +/** + * Regression tests for scripts/run-cpp-tests.js runner semantics. + * + * Run locally: + * npm run test:scripts + */ + +const test = require('node:test') +const assert = require('node:assert/strict') + +const { + DEFAULT_ASAN_OPTIONS, + buildRunnerEnv, + resolveExitCode, + resolveBuildDir +} = require('../run-cpp-tests') + +test('resolveExitCode maps a normal gtest failure to a non-zero exit', () => { + assert.equal(resolveExitCode({ status: 1 }), 1) +}) + +test('resolveExitCode maps success to exit 0', () => { + assert.equal(resolveExitCode({ status: 0 }), 0) +}) + +test('resolveExitCode maps ASan SIGABRT (null status) to failure, not exit 0', () => { + assert.equal(resolveExitCode({ signal: 'SIGABRT', status: null }), 1) +}) + +test('resolveExitCode maps null status without a signal to failure', () => { + assert.equal(resolveExitCode({ status: null }), 1) +}) + +test('buildRunnerEnv applies DEFAULT_ASAN_OPTIONS when unset', () => { + const env = buildRunnerEnv({}) + assert.equal(env.ASAN_OPTIONS, DEFAULT_ASAN_OPTIONS) +}) + +test('buildRunnerEnv replaces rather than merges explicit ASAN_OPTIONS', () => { + const override = 'abort_on_error=0' + const env = buildRunnerEnv({ ASAN_OPTIONS: override }) + assert.equal(env.ASAN_OPTIONS, override) + assert.notEqual(env.ASAN_OPTIONS, DEFAULT_ASAN_OPTIONS) +}) + +test('buildRunnerEnv preserves an explicit empty ASAN_OPTIONS', () => { + const env = buildRunnerEnv({ ASAN_OPTIONS: '' }) + assert.equal(env.ASAN_OPTIONS, '') +}) + +test('resolveBuildDir defaults to build', () => { + assert.equal(resolveBuildDir([], {}), 'build') +}) + +test('resolveBuildDir honours CPP_BUILD_DIR env', () => { + assert.equal(resolveBuildDir([], { CPP_BUILD_DIR: 'build-fuzz' }), 'build-fuzz') +}) + +test('resolveBuildDir --build-dir flag wins over env', () => { + assert.equal( + resolveBuildDir(['--build-dir', 'build-fuzz'], { CPP_BUILD_DIR: 'other' }), + 'build-fuzz' + ) + assert.equal(resolveBuildDir(['--build-dir=build-fuzz'], {}), 'build-fuzz') +}) + +test('resolveBuildDir rejects --build-dir without a value instead of using the default', () => { + assert.throws( + () => resolveBuildDir(['--build-dir'], { CPP_BUILD_DIR: 'other' }), + /--build-dir requires/ + ) +}) + +test('resolveBuildDir rejects a --build-dir that would swallow a following flag', () => { + assert.throws( + () => resolveBuildDir(['--build-dir', '--gtest_filter=Foo'], {}), + /--build-dir requires/ + ) +}) diff --git a/packages/vla-ggml/scripts/run-cpp-tests.js b/packages/vla-ggml/scripts/run-cpp-tests.js new file mode 100644 index 0000000000..7c3f211386 --- /dev/null +++ b/packages/vla-ggml/scripts/run-cpp-tests.js @@ -0,0 +1,95 @@ +'use strict' + +const path = require('path') +const os = require('os') +const { spawnSync } = require('child_process') + +// addon-test links AddressSanitizer but dynamically loads the non-ASan, +// -static-libstdc++ @qvac/fabric prebuild. Objects that cross that module +// boundary trip alloc-dealloc-mismatch, and fabric's long-lived runtime globals +// plus its dlopen'd ggml backends look like leaks at exit -- both fire after +// every test has already passed. Relax exactly those two checks, matching +// .github/workflows/cpp-tests-vla.yml. See test/unit/CMakeLists.txt for the +// full rationale. +const DEFAULT_ASAN_OPTIONS = 'alloc_dealloc_mismatch=0:detect_leaks=0:abort_on_error=1' + +/** + * Build the child-process env for addon-test. When ASAN_OPTIONS is absent, apply + * DEFAULT_ASAN_OPTIONS for local runs. When it is present — including an explicit + * empty string (ASAN_OPTIONS=) — that value is used as-is; we do not merge with or + * patch the default string. Setting only ASAN_OPTIONS=abort_on_error=0 drops + * alloc_dealloc_mismatch=0 and detect_leaks=0 unless you include them yourself. + */ +function buildRunnerEnv(processEnv) { + return { + ...processEnv, + ASAN_OPTIONS: 'ASAN_OPTIONS' in processEnv ? processEnv.ASAN_OPTIONS : DEFAULT_ASAN_OPTIONS + } +} + +/** + * Map spawnSync() output to the runner's process exit code. ASan with + * abort_on_error=1 terminates via SIGABRT (status null); that must not be + * treated as success. + */ +function resolveExitCode(result) { + if (result.signal) { + return 1 + } + return result.status ?? 1 +} + +/** + * Resolve which build tree holds addon-test. Defaults to build/, which is where + * the tests-only configure lands. A `--build-dir ` (or `--build-dir=`) + * flag wins over the CPP_BUILD_DIR env var, which wins over the default. + */ +function resolveBuildDir(argv, processEnv) { + const eq = argv.find((a) => a.startsWith('--build-dir=')) + if (eq) { + return eq.slice('--build-dir='.length) + } + const idx = argv.indexOf('--build-dir') + if (idx !== -1) { + const next = argv[idx + 1] + if (next === undefined || next.startsWith('--')) { + throw new Error('--build-dir requires a directory (e.g. --build-dir build-fuzz)') + } + return next + } + return processEnv.CPP_BUILD_DIR || 'build' +} + +function main() { + const binary = os.platform() === 'win32' ? 'addon-test.exe' : './addon-test' + const buildDir = resolveBuildDir(process.argv.slice(2), process.env) + const cwd = path.resolve(__dirname, '..', buildDir, 'test', 'unit') + + const result = spawnSync(binary, ['--gtest_output=xml:cpp-test-results.xml'], { + cwd, + stdio: 'inherit', + shell: false, + env: buildRunnerEnv(process.env) + }) + + if (result.error) { + throw result.error + } + + if (result.signal) { + console.error(`addon-test terminated by signal ${result.signal}`) + } + + process.exit(resolveExitCode(result)) +} + +if (require.main === module) { + main() +} + +module.exports = { + DEFAULT_ASAN_OPTIONS, + buildRunnerEnv, + resolveExitCode, + resolveBuildDir +} diff --git a/packages/vla-ggml/src/bare-modules.d.ts b/packages/vla-ggml/src/bare-modules.d.ts index da619e1759..542ed444e8 100644 --- a/packages/vla-ggml/src/bare-modules.d.ts +++ b/packages/vla-ggml/src/bare-modules.d.ts @@ -4,5 +4,6 @@ declare module "bare-fs" { declare module "bare-path" { export function join(...paths: string[]): string; + export function dirname(path: string): string; export function isAbsolute(path: string): boolean; } diff --git a/packages/vla-ggml/src/index.ts b/packages/vla-ggml/src/index.ts index 7074cb01fa..9c18766362 100644 --- a/packages/vla-ggml/src/index.ts +++ b/packages/vla-ggml/src/index.ts @@ -20,6 +20,22 @@ import { const { DEFAULT_IMAGE_SIZE } = addonModule; const { QvacErrorAddonVla, ERR_CODES } = errorModule; +// The ggml compute backends (GGML_BACKEND_DL modules) ship exactly once, in the +// @qvac/fabric dependency (prebuilds//qvac__fabric). On desktop, resolve +// the single @qvac/fabric install and load the backends from there. On mobile +// the package tree isn't resolvable at runtime (the worklet runs from a packed +// bundle), so fall back to this addon's own prebuilds, where the mobile +// packaging stages the backends. The native side appends BACKENDS_SUBDIR +// ("/qvac__fabric") to whichever root we return. +function resolveBackendsDir(): string { + try { + const fabricPkg = require.resolve("@qvac/fabric/package"); + const fabricPrebuilds = path.join(path.dirname(fabricPkg), "prebuilds"); + if (fs.existsSync(fabricPrebuilds)) return fabricPrebuilds; + } catch {} + return path.join(__dirname, "prebuilds"); +} + interface VlaConfig { verbosity?: number; backendsDir?: string; @@ -661,7 +677,7 @@ class VlaModel { // JobRunner thread consumes runJob() and feeds the outputCb. const backendsDir = this._config.backendsDir ? this._config.backendsDir - : path.join(__dirname, "prebuilds"); + : resolveBackendsDir(); this._handle = binding.createInstance( this, { diff --git a/packages/vla-ggml/test/unit/CMakeLists.txt b/packages/vla-ggml/test/unit/CMakeLists.txt index f22ef4bfbb..fbe8b03a7f 100644 --- a/packages/vla-ggml/test/unit/CMakeLists.txt +++ b/packages/vla-ggml/test/unit/CMakeLists.txt @@ -60,24 +60,35 @@ target_include_directories( target_compile_features(addon-test PRIVATE cxx_std_20) -# Under GGML_BACKEND_DL the CPU/GPU backends are separate libqvac-ggml-*.so -# modules co-installed with the ggml core lib. Production loads them from the -# addon's prebuilds dir; the unit-test env (backend_env.cpp) loads them from -# here. There's no reliable runtime handle to that directory ($ORIGIN RUNPATH -# leaves dladdr with a relative path), so pass the absolute ggml lib dir in. -target_compile_definitions( - addon-test - PRIVATE GGML_BACKEND_DIR="$") +# BackendSelection.cpp references BACKENDS_SUBDIR on its DL load path. The addon +# target defines it, but addon-test compiles that TU separately. Tests preload +# backends via backend_env.cpp from GGML_BACKEND_DIR, so the value is unused at +# runtime — a placeholder just lets the TU compile. +target_compile_definitions(addon-test PRIVATE BACKENDS_SUBDIR="qvac-lib-infer-vla") target_link_libraries( addon-test PRIVATE - ggml::ggml + qvac-fabric::headers + ${qvac_fabric_target}_module GTest::gtest GTest::gtest_main GTest::gmock_main ) +# GGML_BACKEND_DL/DIR, copy qvac__fabric@0.bare + backends next to the binary, +# rpath, and the win32 delay-load helper. See qvac-addon.cmake for the details. +qvac_addon_stage_fabric_for_test(addon-test ${qvac_fabric_target}) + +# AddressSanitizer on the test executable, kept in a deliberately LIMITED form. +# addon-test dynamically links the shared @qvac/fabric runtime +# (qvac__fabric@0.bare), and the shipped fabric prebuild is built with +# -static-libstdc++. See packages/classification-ggml/test/unit/CMakeLists.txt +# for the full rationale. scripts/run-cpp-tests.js sets ASAN_OPTIONS for local/CI runs. +if(NOT WIN32) + target_link_libraries(addon-test PRIVATE -fsanitize=address) +endif() + add_test(NAME VlaGgmlUnit COMMAND addon-test) set_tests_properties(VlaGgmlUnit PROPERTIES WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} diff --git a/packages/vla-ggml/test/unit/backend_env.cpp b/packages/vla-ggml/test/unit/backend_env.cpp index 18ec8edd8c..00ab76bde4 100644 --- a/packages/vla-ggml/test/unit/backend_env.cpp +++ b/packages/vla-ggml/test/unit/backend_env.cpp @@ -1,63 +1,36 @@ -// Global test environment: load the ggml backends once before any test runs. +// Global test environment: preload the ggml DL backend modules before any test. // -// In production the addon loads its backends at init via -// vla_backend_selection::loadBackendsOnce(backendsDir), where backendsDir is -// the prebuilds folder holding the DL backend modules. The unit tests link the -// addon objects directly and the factory test passes backendsDir="" — fine in a -// static build (CPU is linked in), but under GGML_BACKEND_DL=ON it means no -// backend gets loaded, so pi05LoadModel throws "no CPU backend available". -// -// The DL modules (libqvac-ggml-cpu-*.so) are installed next to the ggml core -// library this test binary links. Resolve that directory at runtime via -// dladdr() and hand it to loadBackendsOnce() — the same code path production -// uses, just with the test's own lib dir. loadBackendsOnce is std::call_once, -// so this first load wins and the factory test's later loadBackendsOnce("") -// becomes a no-op. - -#include -#include +// Under GGML_BACKEND_DL the backends are separate libqvac-ggml-*.so modules +// staged next to the test binary by qvac_addon_stage_fabric_for_test(). Without +// this no backend (not even CPU) is registered, and tests that construct a +// backend dereference null and abort. #include #include #include "utils/BackendSelection.hpp" -// dladdr/dlfcn is the POSIX fallback used only when CMake didn't inject -// GGML_BACKEND_DIR. It doesn't exist on Windows (a static, non-DL build where -// GGML_BACKEND_DIR is always defined), so keep the include behind the guard. -#ifndef GGML_BACKEND_DIR -#include -#endif +// The ggml Vulkan backend leaks a small, one-time allocation while enumerating +// devices at registration — a known upstream ggml issue, benign (one-time, +// non-growing). Suppress that specific third-party leak so LeakSanitizer +// doesn't fail the run when ASan is enabled on addon-test. +extern "C" const char* __lsan_default_suppressions() { + return "leak:ggml_backend_vk_reg_get_device\n"; +} namespace { -// Directory of the ggml core library linked into this test binary, where the -// DL backend modules are co-installed. CMake injects the absolute path via -// GGML_BACKEND_DIR; we fall back to dladdr (and then the default search) only -// if it isn't defined. -std::string ggmlLibDir() { -#ifdef GGML_BACKEND_DIR - return GGML_BACKEND_DIR; -#else - Dl_info info{}; - if (dladdr(reinterpret_cast(&ggml_backend_load_all), &info) != - 0 && - info.dli_fname != nullptr) { - return std::filesystem::path(info.dli_fname).parent_path().string(); - } - return ""; -#endif -} - class BackendEnvironment : public ::testing::Environment { public: void SetUp() override { - vla_backend_selection::loadBackendsOnce(ggmlLibDir()); +#ifdef GGML_BACKEND_DIR + ggml_backend_load_all_from_path(GGML_BACKEND_DIR); +#else + vla_backend_selection::loadBackendsOnce(""); +#endif } }; -// Registered at static-init (before main), so gtest_main runs SetUp() ahead of -// RUN_ALL_TESTS. The returned pointer is owned by gtest. const ::testing::Environment* const kBackendEnvironment = ::testing::AddGlobalTestEnvironment(new BackendEnvironment); diff --git a/packages/vla-ggml/vcpkg.json b/packages/vla-ggml/vcpkg.json index d21fb148aa..6f0ffe33e7 100644 --- a/packages/vla-ggml/vcpkg.json +++ b/packages/vla-ggml/vcpkg.json @@ -4,20 +4,13 @@ "name": "opencl", "platform": "android" }, - { - "name": "qvac-fabric", - "version>=": "10069.2.0", - "features": [ - "hip-backend" - ] - }, { "name": "qvac-lib-inference-addon-cpp", "version>=": "1.3.3" }, { "name": "qvac-lint-cpp", - "version>=": "1.4.4#2" + "version>=": "1.4.4#3" } ], "features": { diff --git a/vcpkg-overlays/ports/qvac-fabric/portfile.cmake b/vcpkg-overlays/ports/qvac-fabric/portfile.cmake new file mode 100644 index 0000000000..c0a666fa27 --- /dev/null +++ b/vcpkg-overlays/ports/qvac-fabric/portfile.cmake @@ -0,0 +1,234 @@ +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO tetherto/qvac-fabric-llm.cpp + REF v${VERSION} + SHA512 f50e9de12d9e5c37b9a4435badb18e60f10fd2be448d9f05fda60ae56469ff33d91677f3e7ba2f00bc3bea421f9aa6169509eb71a9268e7825e512e1bbd275fe +) + +# Upstream CMake options only — passed through to vcpkg_cmake_configure. +vcpkg_check_features( + OUT_FEATURE_OPTIONS FEATURE_OPTIONS + FEATURES + force-profiler FORCE_GGML_VK_PERF_LOGGER + llama BUILD_LLAMA + vector-index GGML_VECTOR_INDEX +) + +# Portfile-only feature flags (drive PLATFORM_OPTIONS; not upstream cache vars). +vcpkg_check_features( + OUT_FEATURE_OPTIONS _PORTFILE_FEATURE_OPTIONS + FEATURES + gpu-backends BUILD_GPU_BACKENDS + kleidiai BUILD_KLEIDIAI + openmp BUILD_OPENMP + hip-backend BUILD_HIP_BACKEND +) + +# gpu-backends is default-on via default-features in vcpkg.json. CPU-only +# consumers (e.g. @qvac/classification-ggml) disable it with +# default-features:false (and re-add 'llama' if needed). +if(NOT BUILD_GPU_BACKENDS) + message(STATUS "qvac-fabric: gpu-backends feature OFF — building CPU-only ggml (no Metal/Vulkan/CUDA/OpenCL)") +endif() + +set(PLATFORM_OPTIONS) + +if (VCPKG_TARGET_IS_ANDROID AND BUILD_GPU_BACKENDS) + # The Android NDK ships only the C Vulkan headers; the ggml Vulkan backend + # additionally needs the C++ bindings (vulkan.hpp) and SPIRV-Headers, which as + # of b9840 ggml fetches itself via FetchContent (ggml/src/ggml-vulkan/CMakeLists.txt, + # `if (ANDROID)` block). The registry vcpkg-cmake sets FETCHCONTENT_FULLY_DISCONNECTED=ON + # globally, so allow the fetch here (same as the kleidiai path below). + list(APPEND PLATFORM_OPTIONS -DFETCHCONTENT_FULLY_DISCONNECTED=OFF) +endif() + +if(NOT BUILD_GPU_BACKENDS) + # Force every GPU backend off explicitly, in case upstream defaults change. + list(APPEND PLATFORM_OPTIONS + -DGGML_METAL=OFF + -DGGML_VULKAN=OFF + -DGGML_CUDA=OFF + -DGGML_OPENCL=OFF + ) + if (VCPKG_TARGET_IS_IOS) + # Same iOS BLAS/Accelerate gating as the GPU-on path; unrelated to the + # CPU-vs-GPU split, an iOS-toolchain workaround for missing frameworks. + list(APPEND PLATFORM_OPTIONS -DGGML_BLAS=OFF -DGGML_ACCELERATE=OFF) + endif() +elseif (VCPKG_TARGET_IS_OSX OR VCPKG_TARGET_IS_IOS) + list(APPEND PLATFORM_OPTIONS -DGGML_METAL=ON) + if (VCPKG_TARGET_IS_IOS) + list(APPEND PLATFORM_OPTIONS -DGGML_BLAS=OFF -DGGML_ACCELERATE=OFF) + endif() +else() + list(APPEND PLATFORM_OPTIONS -DGGML_VULKAN=ON) +endif() + +# Android: always build CPU variants (NEON_DOTPROD, NEON_I8MM, etc.) and CPU +# repacking. These are CPU-only runtime optimizations selected based on the +# device's SIMD capabilities at load time, completely orthogonal to the GPU +# backends. Bundling them is essential for good CPU inference performance on +# the wide range of arm64 devices the addons ship to. Requires GGML_BACKEND_DL +# to dispatch the variants at runtime; the existing #ifdef guard around +# `ggml_backend_load_all_from_path()` in ggml-backend-reg.cpp keeps the search +# scoped to the consumer's own prebuilds dir. +if(VCPKG_TARGET_IS_ANDROID OR (VCPKG_TARGET_IS_LINUX AND BUILD_GPU_BACKENDS)) + # Desktop Linux also needs GGML_BACKEND_DL=ON so that multiple GPU backends + # (Vulkan + HIP/ROCm) can coexist as separately-loaded modules, the same way + # Android dispatches CPU variants at runtime. Without DL the Linux build links + # a single static GPU backend and a second one (HIP) cannot be stacked. + # GGML_NATIVE is incompatible with DL, so CPU variants are dispatched via + # GGML_CPU_ALL_VARIANTS instead. Consumers must ship the core ggml/llama libs + # alongside their backend modules so the dynamically-linked .bare can resolve + # them at load time. + set(DL_BACKENDS ON) + list(APPEND PLATFORM_OPTIONS + -DGGML_BACKEND_DL=ON + -DGGML_CPU_ALL_VARIANTS=ON + -DGGML_CPU_REPACK=ON) +else() + set(DL_BACKENDS OFF) +endif() + +# HIP/ROCm backend — opt-in via the 'hip-backend' feature (Linux + AMD only). +# Only @qvac/vla-ggml requests it, so every other consumer builds with no HIP +# and gains no ROCm dependency. Builds libqvac-ggml-hip.so as a standalone DL +# module alongside Vulkan (GGML_BACKEND_DL is already ON above), so the addon +# dlopen's whichever GPU backend BackendSelection picks at runtime. The `hip` +# feature-dependency port forwards the system ROCm's find_package() configs. +# +# FAIL-SAFE: enable GGML_HIP only when a ROCm SDK is actually present. On a build +# host without ROCm we skip HIP and build Vulkan/CPU only — the build never +# hard-fails, and at runtime a missing HIP module just isn't loaded (the DL +# loader skips it) so BackendSelection falls back to Vulkan/CPU. Targets gfx1151 +# (Strix Halo / Radeon 8060S); the HIP compiler + ROCM_PATH come from the build env. +# linux-x64 only: AMD GPU hosts (Strix Halo / gfx1151) are x86_64, and the ROCm +# dist is x64. On other arches (e.g. linux-arm64) HIP is skipped even if the +# feature is requested — no ROCm requirement, no build break. +if(VCPKG_TARGET_IS_LINUX AND VCPKG_TARGET_ARCHITECTURE STREQUAL "x64" AND BUILD_GPU_BACKENDS AND BUILD_HIP_BACKEND) + # DETERMINISTIC: requesting hip-backend REQUIRES a ROCm SDK at build time. We + # must NOT silently skip when ROCm is absent — a host-dependent skip yields a + # no-HIP package with the SAME vcpkg ABI as a real HIP build, which the binary + # cache then conflates (cache poisoning: a no-ROCm build caches a no-HIP + # package that ROCm-equipped builds then restore). So ROCm present => HIP; + # ROCm absent => hard error (don't request hip-backend on a host without ROCm). + # The RUNTIME fail-safe is unchanged: an absent HIP module / non-AMD target is + # simply not loaded and BackendSelection falls back to Vulkan/CPU. + if(NOT (DEFINED ENV{ROCM_PATH} AND EXISTS "$ENV{ROCM_PATH}/lib/cmake/hip/hip-config.cmake")) + message(FATAL_ERROR "qvac-fabric: hip-backend feature requires a ROCm SDK — set ROCM_PATH to a ROCm/TheRock install containing lib/cmake/hip/hip-config.cmake. Do not request hip-backend on a host without ROCm.") + endif() + message(STATUS "qvac-fabric: hip-backend ON — building GGML_HIP (gfx1151)") + list(APPEND PLATFORM_OPTIONS + -DGGML_HIP=ON + -DAMDGPU_TARGETS=gfx1151 + -DCMAKE_HIP_ARCHITECTURES=gfx1151) +endif() + +if(VCPKG_TARGET_IS_ANDROID AND BUILD_KLEIDIAI) + message(STATUS "qvac-fabric: kleidiai feature ON — building with ARM KleidiAI optimized kernels") + # ggml only vendors KleidiAI via FetchContent; registry vcpkg-cmake sets + # FETCHCONTENT_FULLY_DISCONNECTED=ON globally, so allow the download here. + list(APPEND PLATFORM_OPTIONS + -DGGML_CPU_KLEIDIAI=ON + -DFETCHCONTENT_FULLY_DISCONNECTED=OFF + ) +endif() + +if(VCPKG_TARGET_IS_ANDROID AND BUILD_OPENMP) + message(STATUS "qvac-fabric: OpenMP for Android enabled") + list(APPEND PLATFORM_OPTIONS -DGGML_OPENMP=ON) +else() + message(STATUS "qvac-fabric: OpenMP Disabled") + list(APPEND PLATFORM_OPTIONS -DGGML_OPENMP=OFF) +endif() + +if (VCPKG_TARGET_IS_ANDROID AND BUILD_GPU_BACKENDS) + list(APPEND PLATFORM_OPTIONS -DGGML_OPENCL=ON) +endif() + +if(BUILD_GPU_BACKENDS AND NOT VCPKG_TARGET_IS_OSX AND NOT VCPKG_TARGET_IS_IOS) + if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) + string(APPEND VCPKG_C_FLAGS " /I${CURRENT_INSTALLED_DIR}/include") + string(APPEND VCPKG_CXX_FLAGS " /I${CURRENT_INSTALLED_DIR}/include") + else() + string(APPEND VCPKG_C_FLAGS " -isystem ${CURRENT_INSTALLED_DIR}/include") + string(APPEND VCPKG_CXX_FLAGS " -isystem ${CURRENT_INSTALLED_DIR}/include") + endif() +endif() + +# Under GGML_BACKEND_DL the per-microarch backends ship as standalone +# libqvac-ggml-*.so modules that the consumer dlopen's at runtime. Built with +# -stdlib=libc++ they otherwise carry a runtime NEEDED dependency on the system +# libc++.so.1 / libc++abi.so.1, so they silently fail to dlopen on any target +# without libc++ installed (e.g. stock ubuntu-24.04 — no CPU backend registers, +# inference aborts). Statically link the C++ runtime into the modules so they +# are self-contained, matching how the addons link themselves. The module<->addon +# boundary is the C ggml-backend ABI, so per-module libc++ copies never exchange +# C++ objects. Linux only: Apple/iOS use Metal frameworks, Android ships +# libc++_shared via the NDK STL, Windows uses the MSVC runtime. +if(VCPKG_TARGET_IS_LINUX AND DL_BACKENDS) + string(APPEND VCPKG_LINKER_FLAGS " -static-libstdc++") +endif() + +set(LLAMA_OPTIONS) +if("llama" IN_LIST FEATURES) + list(APPEND LLAMA_OPTIONS -DLLAMA_MTMD=ON) +else() + list(APPEND LLAMA_OPTIONS + -DLLAMA_MTMD=OFF + -DLLAMA_BUILD_COMMON=OFF + ) +endif() + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + DISABLE_PARALLEL_CONFIGURE + OPTIONS + -DGGML_NATIVE=OFF + -DGGML_CCACHE=OFF + -DGGML_LLAMAFILE=OFF + -DLLAMA_CURL=OFF + -DLLAMA_BUILD_TESTS=OFF + -DLLAMA_BUILD_TOOLS=OFF + -DLLAMA_BUILD_EXAMPLES=OFF + -DLLAMA_BUILD_SERVER=OFF + -DLLAMA_BUILD_APP=OFF + -DMTMD_VIDEO=OFF + -DLLAMA_ALL_WARNINGS=OFF + ${LLAMA_OPTIONS} + ${PLATFORM_OPTIONS} + ${FEATURE_OPTIONS} +) + +vcpkg_cmake_install() +vcpkg_cmake_config_fixup( + PACKAGE_NAME ggml) + +if(BUILD_LLAMA) + vcpkg_cmake_config_fixup(PACKAGE_NAME llama) +endif() + +vcpkg_copy_pdbs() +vcpkg_fixup_pkgconfig() + + +if(BUILD_LLAMA) + file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/tools/${PORT}") + file(RENAME "${CURRENT_PACKAGES_DIR}/bin/convert_hf_to_gguf.py" "${CURRENT_PACKAGES_DIR}/tools/${PORT}/convert-hf-to-gguf.py") + file(INSTALL "${SOURCE_PATH}/gguf-py" DESTINATION "${CURRENT_PACKAGES_DIR}/tools/${PORT}") + file(RENAME "${CURRENT_PACKAGES_DIR}/bin/vulkan_profiling_analyzer.py" "${CURRENT_PACKAGES_DIR}/tools/${PORT}/vulkan_profiling_analyzer.py") +endif() + +if (NOT VCPKG_BUILD_TYPE) + file(REMOVE "${CURRENT_PACKAGES_DIR}/debug/bin/convert_hf_to_gguf.py") +endif() + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share") + +if (VCPKG_LIBRARY_LINKAGE MATCHES "static") + file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/bin") + file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/bin") +endif() + +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") diff --git a/vcpkg-overlays/ports/qvac-fabric/vcpkg.json b/vcpkg-overlays/ports/qvac-fabric/vcpkg.json new file mode 100644 index 0000000000..9647d18a6c --- /dev/null +++ b/vcpkg-overlays/ports/qvac-fabric/vcpkg.json @@ -0,0 +1,61 @@ +{ + "name": "qvac-fabric", + "version": "10069.2.0", + "description": "LLM inference in C/C++", + "homepage": "https://github.com/tetherto/qvac-fabric-llm.cpp", + "license": "MIT", + "dependencies": [ + { + "name": "opencl", + "platform": "android" + }, + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ], + "default-features": [ + "gpu-backends", + "llama" + ], + "features": { + "force-profiler": { + "description": "Force vk performance logging in ggml" + }, + "gpu-backends": { + "description": "Build the GPU backends ggml ships per platform: Metal on Apple, Vulkan on Linux/Windows/Android, plus the Android backend-DL hybrid mode and OpenCL. Default-on so existing consumers (llamacpp-llm, llamacpp-embed, nmtcpp, diffusion-cpp) keep their current behaviour with default features. Disable to produce a CPU-only ggml build (useful for consumers like @qvac/classification-ggml that don't need GPU paths and want to skip the vulkan-sdk / metal / opencl build cost). Orthogonal to the 'llama' feature.", + "dependencies": [ + { + "name": "spirv-headers", + "platform": "!osx & !ios", + "version>=": "1.4.341.0" + } + ] + }, + "hip-backend": { + "description": "Build the ROCm/HIP GPU backend (libqvac-ggml-hip.so) for AMD GPUs on Linux as a DL module alongside Vulkan. Opt-in (only @qvac/vla-ggml requests it). Fail-safe: if no ROCm SDK is present at build time the HIP backend is skipped (Vulkan/CPU only). Targets gfx1151 (Strix Halo). The 'hip' dependency forwards the system ROCm find_package configs.", + "dependencies": [ + { + "name": "hip", + "platform": "linux & x64" + } + ] + }, + "kleidiai": { + "description": "Enable ARM KleidiAI optimized kernels on Android." + }, + "llama": { + "description": "Build llama components." + }, + "openmp": { + "description": "Enable openmp on Android." + }, + "vector-index": { + "description": "Build and install the ggml vector index library." + } + } +}