diff --git a/.ecrc b/.ecrc index c68877ec211f..8204ad174269 100644 --- a/.ecrc +++ b/.ecrc @@ -1,5 +1,5 @@ { - "Exclude": ["^\\.gitmodules$", "stb_image\\.h"], + "Exclude": ["^\\.gitmodules$", "stb_image\\.h", "^vendor/"], "Disable": { "IndentSize": true } diff --git a/.flake8 b/.flake8 index 669d231f1f63..16de5486797d 100644 --- a/.flake8 +++ b/.flake8 @@ -15,4 +15,5 @@ exclude = build, # This contains builds that we don't want to check dist # This is generated with `python build .` for package releases + scripts/tune # max-complexity = 10 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000000..fc7b35173a93 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @tetherto/ai-runtime-bk-models diff --git a/.github/workflows/approval-check-worker.yml b/.github/workflows/approval-check-worker.yml new file mode 100644 index 000000000000..3a60b0bff53f --- /dev/null +++ b/.github/workflows/approval-check-worker.yml @@ -0,0 +1,19 @@ +name: Approval Check Worker + +on: + pull_request_review: + types: [submitted, dismissed] + +jobs: + check-approvals: + permissions: + contents: write + pull-requests: write + statuses: write + issues: write + + uses: tetherto/qvac-devops/.github/workflows/approval-check-worker.yml@production-workflows-tag + secrets: inherit + with: + pr_number: ${{ github.event.pull_request.number }} + pr_sha: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/build-cmake-pkg.yml b/.github/workflows/build-cmake-pkg.yml index fee2ab96bd0e..fa604bb6f47b 100644 --- a/.github/workflows/build-cmake-pkg.yml +++ b/.github/workflows/build-cmake-pkg.yml @@ -25,7 +25,7 @@ jobs: cmake --build build --config Release cmake --install build --prefix "$PREFIX" --config Release - export LLAMA_CONFIG="$PREFIX"/lib/cmake/llama/llama-config.cmake + export LLAMA_CONFIG="$PREFIX"/share/llama/llama-config.cmake tclsh <<'EOF' set build(commit) [string trim [exec git rev-parse --short HEAD]] set build(number) [string trim [exec git rev-list --count HEAD]] @@ -47,5 +47,5 @@ jobs: EOF cd examples/simple-cmake-pkg - cmake -S . -B build -DCMAKE_PREFIX_PATH="$PREFIX"/lib/cmake + cmake -S . -B build -DCMAKE_PREFIX_PATH="$PREFIX"/share cmake --build build diff --git a/.github/workflows/build-linux-cross.yml b/.github/workflows/build-linux-cross.yml index 36201281f005..421442e313fe 100644 --- a/.github/workflows/build-linux-cross.yml +++ b/.github/workflows/build-linux-cross.yml @@ -4,6 +4,7 @@ on: workflow_call: jobs: + # Disabled. Fails to install some dependencies from arch-specific repositories. # ubuntu-24-riscv64-cpu-cross: # runs-on: ubuntu-24.04 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49e836d9b202..0702b8d0a33c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,9 @@ on: paths: [ '.github/workflows/build.yml', '.github/workflows/build-linux-cross.yml', - '.github/workflows/build-cmake-pkg.yml', + # Disable. There are some modifications in the fork + # so that ggml dynamic builds work with vcpkg. + # '.github/workflows/build-cmake-pkg.yml', '**/CMakeLists.txt', '**/.cmake', '**/*.h', @@ -28,7 +30,9 @@ on: paths: [ '.github/workflows/build.yml', '.github/workflows/build-linux-cross.yml', - '.github/workflows/build-cmake-pkg.yml', + # Disable. There are some modifications in the fork + # so that ggml dynamic builds work with vcpkg. + # '.github/workflows/build-cmake-pkg.yml', '**/CMakeLists.txt', '**/.cmake', '**/*.h', @@ -117,8 +121,9 @@ jobs: -DLLAMA_CURL=OFF \ -DLLAMA_BUILD_BORINGSSL=ON \ -DGGML_METAL=OFF \ - -DGGML_RPC=ON \ - -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3 + -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3 \ + -DGGML_BLAS=OFF \ + -DGGML_RPC=ON cmake --build build --config Release -j $(sysctl -n hw.logicalcpu) - name: Test @@ -269,11 +274,12 @@ jobs: id: checkout uses: actions/checkout@v4 - - name: ccache - uses: ggml-org/ccache-action@v1.2.16 - with: - key: ubuntu-latest-cmake-sanitizer-${{ matrix.sanitizer }} - evict-old-files: 1d + # ccache disabled for sanitizer builds to ensure clean builds with correct sanitizer flags + # - name: ccache + # uses: ggml-org/ccache-action@v1.2.16 + # with: + # key: ubuntu-latest-cmake-sanitizer-${{ matrix.sanitizer }} + # evict-old-files: 1d - name: Dependencies id: depends @@ -308,6 +314,16 @@ jobs: - name: Test id: cmake_test + env: + # AddressSanitizer options + ASAN_OPTIONS: "verbosity=1:abort_on_error=1:print_stats=1:check_initialization_order=1:strict_init_order=1:detect_stack_use_after_return=1:print_summary=1:print_scariness=1:print_legend=1" + # ThreadSanitizer options + TSAN_OPTIONS: "verbosity=1:abort_on_error=1:print_stats=1:print_summary=1:print_legend=1" + # UndefinedBehaviorSanitizer options + # Note: abort_on_error=0 allows UBSAN to print full diagnostics before aborting + UBSAN_OPTIONS: "verbosity=2:abort_on_error=1:print_stacktrace=1:print_summary=1:halt_on_error=1:report_error_type=1:silence_unsigned_overflow=0" + # Common options for all sanitizers + MSAN_OPTIONS: "verbosity=1:abort_on_error=1:print_stats=1" run: | cd build ctest -L main --verbose --timeout 900 @@ -354,6 +370,7 @@ jobs: id: checkout uses: actions/checkout@v4 + # Reduce false pass/failure on CI # - name: ccache # uses: ggml-org/ccache-action@v1.2.16 # with: @@ -389,11 +406,12 @@ jobs: id: checkout uses: actions/checkout@v4 - - name: ccache - uses: ggml-org/ccache-action@v1.2.16 - with: - key: ubuntu-24-cmake-vulkan-deb - evict-old-files: 1d + # Reduce false pass/failure on CI + # - name: ccache + # uses: ggml-org/ccache-action@v1.2.16 + # with: + # key: ubuntu-24-cmake-vulkan + # evict-old-files: 1d - name: Dependencies id: depends @@ -417,7 +435,7 @@ jobs: cmake --build build -j $(nproc) ubuntu-24-cmake-vulkan: - runs-on: ubuntu-24.04 + runs-on: ai-run-linux-gpu steps: - name: Clone @@ -435,7 +453,7 @@ jobs: run: | sudo add-apt-repository -y ppa:kisak/kisak-mesa sudo apt-get update -y - sudo apt-get install -y build-essential mesa-vulkan-drivers libxcb-xinput0 libxcb-xinerama0 libxcb-cursor-dev libssl-dev + sudo apt-get install -y build-essential mesa-vulkan-drivers libxcb-xinput0 libxcb-xinerama0 libxcb-cursor-dev libssl-dev cmake - name: Get latest Vulkan SDK version id: vulkan_sdk_version @@ -483,11 +501,12 @@ jobs: id: checkout uses: actions/checkout@v4 - - name: ccache - uses: ggml-org/ccache-action@v1.2.16 - with: - key: ubuntu-24-cmake-webgpu - evict-old-files: 1d + # Reduce false pass/failure on CI + # - name: ccache + # uses: ggml-org/ccache-action@v1.2.16 + # with: + # key: ubuntu-24-cmake-webgpu + # evict-old-files: 1d - name: Dependencies id: depends @@ -602,11 +621,12 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential git cmake rocblas-dev hipblas-dev libssl-dev rocwmma-dev - - name: ccache - uses: ggml-org/ccache-action@v1.2.16 - with: - key: ubuntu-22-cmake-hip - evict-old-files: 1d + # Reduce false pass/failure on CI + # - name: ccache + # uses: ggml-org/ccache-action@v1.2.16 + # with: + # key: ubuntu-22-cmake-hip + # evict-old-files: 1d - name: Build with native CMake HIP support id: cmake_build @@ -753,8 +773,10 @@ jobs: build-linux-cross: uses: ./.github/workflows/build-linux-cross.yml - build-cmake-pkg: - uses: ./.github/workflows/build-cmake-pkg.yml + # Disable. There are some modifications in the fork + # so that ggml dynamic builds work with vcpkg. + # build-cmake-pkg: + # uses: ./.github/workflows/build-cmake-pkg.yml macOS-latest-cmake-ios: runs-on: macos-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 03cd41c57026..a0e03b5bf936 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ concurrency: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - CMAKE_ARGS: "-DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_TOOLS=ON -DLLAMA_BUILD_SERVER=ON -DGGML_RPC=ON" + CMAKE_ARGS: "-DLLAMA_BUILD_EXAMPLES=ON -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_TOOLS=ON -DLLAMA_BUILD_SERVER=ON -DGGML_RPC=ON" jobs: macOS-arm64: @@ -148,8 +148,6 @@ jobs: include: - build: 'x64' os: ubuntu-22.04 - - build: 's390x' - os: ubuntu-24.04-s390x # GGML_BACKEND_DL and GGML_CPU_ALL_VARIANTS are not currently supported on arm # - build: 'arm64' # os: ubuntu-22.04-arm @@ -348,10 +346,6 @@ jobs: arch: 'x64' defines: '-DGGML_VULKAN=ON' target: 'ggml-vulkan' - - backend: 'opencl-adreno' - arch: 'arm64' - defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake -DCMAKE_PREFIX_PATH="$env:RUNNER_TEMP/opencl-arm64-release" -DGGML_OPENCL=ON -DGGML_OPENCL_USE_ADRENO_KERNELS=ON' - target: 'ggml-opencl' steps: - name: Clone @@ -379,26 +373,6 @@ jobs: run: | choco install ninja - - name: Install OpenCL Headers and Libs - id: install_opencl - if: ${{ matrix.backend == 'opencl-adreno' && matrix.arch == 'arm64' }} - run: | - git clone https://github.com/KhronosGroup/OpenCL-Headers - cd OpenCL-Headers - cmake -B build ` - -DBUILD_TESTING=OFF ` - -DOPENCL_HEADERS_BUILD_TESTING=OFF ` - -DOPENCL_HEADERS_BUILD_CXX_TESTS=OFF ` - -DCMAKE_INSTALL_PREFIX="$env:RUNNER_TEMP/opencl-arm64-release" - cmake --build build --target install - git clone https://github.com/KhronosGroup/OpenCL-ICD-Loader - cd OpenCL-ICD-Loader - cmake -B build-arm64-release ` - -A arm64 ` - -DCMAKE_PREFIX_PATH="$env:RUNNER_TEMP/opencl-arm64-release" ` - -DCMAKE_INSTALL_PREFIX="$env:RUNNER_TEMP/opencl-arm64-release" - cmake --build build-arm64-release --target install --config release - - name: Build id: cmake_build run: | @@ -408,7 +382,7 @@ jobs: - name: Pack artifacts id: pack_artifacts run: | - 7z a -snl llama-bin-win-${{ matrix.backend }}-${{ matrix.arch }}.zip .\build\bin\Release\${{ matrix.target }}.dll + 7z a -snl llama-bin-win-${{ matrix.backend }}-${{ matrix.arch }}.zip .\build\bin\Release\qvac-${{ matrix.target }}.dll - name: Upload artifacts uses: actions/upload-artifact@v4 @@ -416,74 +390,6 @@ jobs: path: llama-bin-win-${{ matrix.backend }}-${{ matrix.arch }}.zip name: llama-bin-win-${{ matrix.backend }}-${{ matrix.arch }}.zip - windows-cuda: - runs-on: windows-2022 - - strategy: - matrix: - cuda: ['12.4'] - - steps: - - name: Clone - id: checkout - uses: actions/checkout@v4 - - - name: Install ccache - uses: ggml-org/ccache-action@v1.2.16 - with: - key: windows-cuda-${{ matrix.cuda }} - variant: ccache - evict-old-files: 1d - - - name: Install Cuda Toolkit - uses: ./.github/actions/windows-setup-cuda - with: - cuda_version: ${{ matrix.cuda }} - - - name: Install Ninja - id: install_ninja - run: | - choco install ninja - - - name: Build - id: cmake_build - shell: cmd - run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - cmake -S . -B build -G "Ninja Multi-Config" ^ - -DGGML_BACKEND_DL=ON ^ - -DGGML_NATIVE=OFF ^ - -DGGML_CPU=OFF ^ - -DGGML_CUDA=ON ^ - -DLLAMA_CURL=OFF - set /A NINJA_JOBS=%NUMBER_OF_PROCESSORS%-1 - cmake --build build --config Release -j %NINJA_JOBS% --target ggml-cuda - - - name: Pack artifacts - id: pack_artifacts - run: | - 7z a -snl llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip .\build\bin\Release\ggml-cuda.dll - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - path: llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip - name: llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip - - - name: Copy and pack Cuda runtime - run: | - echo "Cuda install location: ${{ env.CUDA_PATH }}" - $dst='.\build\bin\cudart\' - robocopy "${{env.CUDA_PATH}}\bin" $dst cudart64_*.dll cublas64_*.dll cublasLt64_*.dll - robocopy "${{env.CUDA_PATH}}\lib" $dst cudart64_*.dll cublas64_*.dll cublasLt64_*.dll - 7z a cudart-llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip $dst\* - - - name: Upload Cuda runtime - uses: actions/upload-artifact@v4 - with: - path: cudart-llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip - name: cudart-llama-bin-win-cuda-${{ matrix.cuda }}-x64.zip - windows-sycl: runs-on: windows-2022 @@ -780,6 +686,47 @@ jobs: path: llama-${{ steps.tag.outputs.name }}-bin-${{ matrix.chip_type }}-openEuler-${{ matrix.arch }}.tar.gz name: llama-bin-${{ matrix.chip_type }}-openEuler-${{ matrix.arch }}.tar.gz + android-build: + runs-on: ubuntu-latest + + steps: + - name: Clone + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: zulu + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + with: + log-accepted-android-sdk-licenses: false + + - name: Build + run: | + cd examples/llama.android + ./gradlew build --no-daemon + + - name: Determine tag name + id: tag + uses: ./.github/actions/get-tag-name + + - name: Pack artifacts + id: pack_artifacts + run: | + cp LICENSE ./examples/llama.android/app/build/ + zip -r llama-${{ steps.tag.outputs.name }}-bin-android.zip ./examples/llama.android/app/build/* + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + path: llama-${{ steps.tag.outputs.name }}-bin-android.zip + name: llama-bin-android.zip + release: if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }} @@ -793,7 +740,6 @@ jobs: needs: - windows - windows-cpu - - windows-cuda - windows-sycl - windows-hip - ubuntu-22-cpu @@ -802,6 +748,7 @@ jobs: - macOS-x64 - ios-xcode-build - openEuler-cann + - android-build steps: - name: Clone @@ -883,16 +830,17 @@ jobs: **Linux:** - [Ubuntu x64 (CPU)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-x64.tar.gz) - [Ubuntu x64 (Vulkan)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-vulkan-x64.tar.gz) - - [Ubuntu s390x (CPU)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-s390x.tar.gz) **Windows:** - [Windows x64 (CPU)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-cpu-x64.zip) - [Windows arm64 (CPU)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-cpu-arm64.zip) - - [Windows x64 (CUDA)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-cuda-12.4-x64.zip) - [Windows x64 (Vulkan)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-vulkan-x64.zip) - [Windows x64 (SYCL)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-sycl-x64.zip) - [Windows x64 (HIP)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-hip-radeon-x64.zip) + **Android:** + - [Android](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-android.zip) + **openEuler:** - [openEuler x86 (310p)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-310p-openEuler-x86.tar.gz) - [openEuler x86 (910b)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-910b-openEuler-x86.tar.gz) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1085b545a96e..0d6f0c43285e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,12 +104,18 @@ option(LLAMA_BUILD_EXAMPLES "llama: build examples" ${LLAMA_STANDALONE}) option(LLAMA_BUILD_SERVER "llama: build server example" ${LLAMA_STANDALONE}) option(LLAMA_TOOLS_INSTALL "llama: install tools" ${LLAMA_TOOLS_INSTALL_DEFAULT}) +# specific extras +option(LLAMA_MTMD "llama: multimodal support" ${LLAMA_BUILD_TOOLS}) + # 3rd party libs option(LLAMA_CURL "llama: use libcurl to download model from an URL" ON) option(LLAMA_HTTPLIB "llama: if libcurl is disabled, use httplib to download model from an URL" ON) option(LLAMA_OPENSSL "llama: use openssl to support HTTPS" OFF) option(LLAMA_LLGUIDANCE "llama-common: include LLGuidance library for structured output in common utils" OFF) +# profiling +option(FORCE_GGML_VK_PERF_LOGGER "Force vk performance logging in ggml" OFF) + # Required for relocatable CMake package include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/build-info.cmake) include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/common.cmake) @@ -220,6 +226,10 @@ if (LLAMA_BUILD_COMMON) endif() endif() +if(LLAMA_BUILD_EXAMPLES OR LLAMA_BUILD_TESTS) + add_subdirectory(common_test) +endif() + if (LLAMA_BUILD_COMMON AND LLAMA_BUILD_TESTS AND NOT CMAKE_JS_VERSION) include(CTest) add_subdirectory(tests) @@ -232,6 +242,8 @@ endif() if (LLAMA_BUILD_COMMON AND LLAMA_BUILD_TOOLS) add_subdirectory(tools) +elseif (LLAMA_MTMD) + add_subdirectory(tools/mtmd) endif() # @@ -241,7 +253,7 @@ endif() include(GNUInstallDirs) include(CMakePackageConfigHelpers) -set(LLAMA_INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Location of header files") +set(LLAMA_INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR}/llama CACHE PATH "Location of header files") set(LLAMA_LIB_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Location of library files") set(LLAMA_BIN_INSTALL_DIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Location of binary files") @@ -253,15 +265,52 @@ set_target_properties(llama PROPERTIES PUBLIC_HEADER "${LLAMA_PUBLIC_HEADERS}") -install(TARGETS llama LIBRARY PUBLIC_HEADER) +install( + TARGETS llama + EXPORT llama-targets + PUBLIC_HEADER + DESTINATION ${LLAMA_INCLUDE_INSTALL_DIR}) + +if (LLAMA_BUILD_COMMON) + + if (NOT LLAMA_CURL AND LLAMA_HTTPLIB) + install( + TARGETS cpp-httplib + EXPORT llama-targets) + endif() + + install( + TARGETS common build_info + EXPORT llama-targets + PUBLIC_HEADER + DESTINATION ${LLAMA_INCLUDE_INSTALL_DIR}/common) + +endif() + +if (LLAMA_MTMD AND TARGET mtmd) + + install( + TARGETS mtmd + EXPORT llama-targets + PUBLIC_HEADER + DESTINATION ${LLAMA_INCLUDE_INSTALL_DIR}/mtmd) + +endif() + +install( + EXPORT llama-targets + FILE llama-targets.cmake + NAMESPACE llama:: + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/llama) + +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/llama-config.cmake + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/llama) configure_package_config_file( - ${CMAKE_CURRENT_SOURCE_DIR}/cmake/llama-config.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/llama-config.cmake - INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/llama - PATH_VARS LLAMA_INCLUDE_INSTALL_DIR - LLAMA_LIB_INSTALL_DIR - LLAMA_BIN_INSTALL_DIR ) + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/llama-config.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/llama-config.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/llama) write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/llama-version.cmake @@ -270,7 +319,7 @@ write_basic_package_version_file( install(FILES ${CMAKE_CURRENT_BINARY_DIR}/llama-config.cmake ${CMAKE_CURRENT_BINARY_DIR}/llama-version.cmake - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/llama) + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/llama) install( FILES convert_hf_to_gguf.py @@ -284,6 +333,10 @@ install( WORLD_EXECUTE DESTINATION ${CMAKE_INSTALL_BINDIR}) +install( + PROGRAMS vulkan_profiling_analyzer.py + DESTINATION ${CMAKE_INSTALL_BINDIR}) + configure_file(cmake/llama.pc.in "${CMAKE_CURRENT_BINARY_DIR}/llama.pc" @ONLY) diff --git a/README.md b/README.md index 2e44ae7d0c79..077454f781d3 100644 --- a/README.md +++ b/README.md @@ -1,608 +1,156 @@ -# llama.cpp +# qvac-fabric-llm.cpp -![llama](https://user-images.githubusercontent.com/1991296/230134379-7181e485-c521-4d23-a0d6-f7b3b61ba524.png) +**AI inference and training engine for desktop and mobile platforms.** [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) -[![Release](https://img.shields.io/github/v/release/ggml-org/llama.cpp)](https://github.com/ggml-org/llama.cpp/releases) -[![Server](https://github.com/ggml-org/llama.cpp/actions/workflows/server.yml/badge.svg)](https://github.com/ggml-org/llama.cpp/actions/workflows/server.yml) +[![Based on llama.cpp](https://img.shields.io/badge/based%20on-llama.cpp%20b7248-orange.svg)](https://github.com/ggml-org/llama.cpp) -[Manifesto](https://github.com/ggml-org/llama.cpp/discussions/205) / [ggml](https://github.com/ggml-org/ggml) / [ops](https://github.com/ggml-org/llama.cpp/blob/master/docs/ops.md) +`qvac-fabric-llm.cpp` is a specialized fork of [llama.cpp](https://github.com/ggml-org/llama.cpp) optimized for embedded systems, mobile devices, and enterprise deployment scenarios. It extends the excellent foundation of llama.cpp with additional capabilities focused on memory-based model loading, mobile GPU optimization, and flexible integration patterns. -LLM inference in C/C++ -## Recent API changes +## Key Features -- [Changelog for `libllama` API](https://github.com/ggml-org/llama.cpp/issues/9289) -- [Changelog for `llama-server` REST API](https://github.com/ggml-org/llama.cpp/issues/9291) +The following capabilities are developed and maintained as part of qvac-fabric-llm.cpp. Features marked as *exclusive* are not available in upstream llama.cpp. -## Hot topics +### LoRA Fine-Tuning *(exclusive)* -- **[guide : using the new WebUI of llama.cpp](https://github.com/ggml-org/llama.cpp/discussions/16938)** -- [guide : running gpt-oss with llama.cpp](https://github.com/ggml-org/llama.cpp/discussions/15396) -- [[FEEDBACK] Better packaging for llama.cpp to support downstream consumers 🤗](https://github.com/ggml-org/llama.cpp/discussions/15313) -- Support for the `gpt-oss` model with native MXFP4 format has been added | [PR](https://github.com/ggml-org/llama.cpp/pull/15091) | [Collaboration with NVIDIA](https://blogs.nvidia.com/blog/rtx-ai-garage-openai-oss) | [Comment](https://github.com/ggml-org/llama.cpp/discussions/15095) -- Multimodal support arrived in `llama-server`: [#12898](https://github.com/ggml-org/llama.cpp/pull/12898) | [documentation](./docs/multimodal.md) -- VS Code extension for FIM completions: https://github.com/ggml-org/llama.vscode -- Vim/Neovim plugin for FIM completions: https://github.com/ggml-org/llama.vim -- Hugging Face Inference Endpoints now support GGUF out of the box! https://github.com/ggml-org/llama.cpp/discussions/9669 -- Hugging Face GGUF editor: [discussion](https://github.com/ggml-org/llama.cpp/discussions/9268) | [tool](https://huggingface.co/spaces/CISCai/gguf-editor) +`qvac-fabric-llm.cpp` provides native [LoRA](https://arxiv.org/abs/2106.09685) (Low-Rank Adaptation) fine-tuning across CPU, Vulkan, and Metal backends. The training pipeline runs directly on consumer hardware, including mobile phones and integrated GPUs. ----- +- Multi-backend GPU training (NVIDIA, AMD, Intel, Apple, Mali, Adreno) +- FP32, FP16, Q8, and Q4 training paths +- Supervised instruction-tuning via assistant-only masked loss (SFT) +- Checkpoint saving and resumable training +- Learning rate schedulers (constant, cosine, linear) with warmup support +- LoRA adapter merging into base models via **llama-export-lora** +- Verified compatibility with Qwen3 and Gemma3 model architectures -## Quick start +For usage details and CLI reference, see the [Finetuning Guide](examples/training/README.md). -Getting started with llama.cpp is straightforward. Here are several ways to install it on your machine: +### BitNet Inference and Fine-Tuning *(exclusive)* -- Install `llama.cpp` using [brew, nix or winget](docs/install.md) -- Run with Docker - see our [Docker documentation](docs/docker.md) -- Download pre-built binaries from the [releases page](https://github.com/ggml-org/llama.cpp/releases) -- Build from source by cloning this repository - check out [our build guide](docs/build.md) +Native support for [BitNet](https://arxiv.org/abs/2402.17764) ternary quantized models via the TQ2_0 data type, enabling efficient inference and LoRA fine-tuning of models such as [bitnet_b1_58-xl](https://huggingface.co/qvac/fabric-llm-finetune-bitnet) on resource-constrained devices. -Once installed, you'll need a model to work with. Head to the [Obtaining and quantizing models](#obtaining-and-quantizing-models) section to learn more. +The official [microsoft/BitNet](https://github.com/microsoft/BitNet) inference framework provides optimized CPU kernels and GPU support limited to CUDA. `qvac-fabric-llm.cpp` **extends** BitNet to all major GPU backends -- Vulkan, Metal, and CPU -- bringing cross-platform GPU-accelerated BitNet inference and on-device fine-tuning to hardware not covered by the upstream framework, including Apple Silicon, mobile GPUs (Adreno, Mali), and AMD/Intel discrete GPUs. Compatible with models such as [bitnet_b1_58-3B](https://huggingface.co/1bitLLM/bitnet_b1_58-3B). -Example command: +- **Backends**: Vulkan, Metal and CPU TQ2_0 quantization support +- **Training**: LoRA fine-tuning of BitNet models on Vulkan, Metal, and CPU backends +- **Conversion**: HuggingFace-to-GGUF conversion for BitNet model architectures +- Cooperative matrix (coopmat) support for Vulkan devices that expose the extension -```sh -# Use a local model file -llama-cli -m my_model.gguf +### Memory-Based Model Loading *(exclusive)* -# Or download and run a model directly from Hugging Face -llama-cli -hf ggml-org/gemma-3-1b-it-GGUF +Load models directly from memory buffers instead of the filesystem, enabling deployment in environments where disk access is restricted or unavailable. -# Launch OpenAI-compatible API server -llama-server -hf ggml-org/gemma-3-1b-it-GGUF -``` - -## Description - -The main goal of `llama.cpp` is to enable LLM inference with minimal setup and state-of-the-art performance on a wide -range of hardware - locally and in the cloud. - -- Plain C/C++ implementation without any dependencies -- Apple silicon is a first-class citizen - optimized via ARM NEON, Accelerate and Metal frameworks -- AVX, AVX2, AVX512 and AMX support for x86 architectures -- RVV, ZVFH, ZFH and ZICBOP support for RISC-V architectures -- 1.5-bit, 2-bit, 3-bit, 4-bit, 5-bit, 6-bit, and 8-bit integer quantization for faster inference and reduced memory use -- Custom CUDA kernels for running LLMs on NVIDIA GPUs (support for AMD GPUs via HIP and Moore Threads GPUs via MUSA) -- Vulkan and SYCL backend support -- CPU+GPU hybrid inference to partially accelerate models larger than the total VRAM capacity - -The `llama.cpp` project is the main playground for developing new features for the [ggml](https://github.com/ggml-org/ggml) library. - -
-Models - -Typically finetunes of the base models below are supported as well. - -Instructions for adding support for new models: [HOWTO-add-model.md](docs/development/HOWTO-add-model.md) - -#### Text-only - -- [X] LLaMA 🦙 -- [x] LLaMA 2 🦙🦙 -- [x] LLaMA 3 🦙🦙🦙 -- [X] [Mistral 7B](https://huggingface.co/mistralai/Mistral-7B-v0.1) -- [x] [Mixtral MoE](https://huggingface.co/models?search=mistral-ai/Mixtral) -- [x] [DBRX](https://huggingface.co/databricks/dbrx-instruct) -- [x] [Jamba](https://huggingface.co/ai21labs) -- [X] [Falcon](https://huggingface.co/models?search=tiiuae/falcon) -- [X] [Chinese LLaMA / Alpaca](https://github.com/ymcui/Chinese-LLaMA-Alpaca) and [Chinese LLaMA-2 / Alpaca-2](https://github.com/ymcui/Chinese-LLaMA-Alpaca-2) -- [X] [Vigogne (French)](https://github.com/bofenghuang/vigogne) -- [X] [BERT](https://github.com/ggml-org/llama.cpp/pull/5423) -- [X] [Koala](https://bair.berkeley.edu/blog/2023/04/03/koala/) -- [X] [Baichuan 1 & 2](https://huggingface.co/models?search=baichuan-inc/Baichuan) + [derivations](https://huggingface.co/hiyouga/baichuan-7b-sft) -- [X] [Aquila 1 & 2](https://huggingface.co/models?search=BAAI/Aquila) -- [X] [Starcoder models](https://github.com/ggml-org/llama.cpp/pull/3187) -- [X] [Refact](https://huggingface.co/smallcloudai/Refact-1_6B-fim) -- [X] [MPT](https://github.com/ggml-org/llama.cpp/pull/3417) -- [X] [Bloom](https://github.com/ggml-org/llama.cpp/pull/3553) -- [x] [Yi models](https://huggingface.co/models?search=01-ai/Yi) -- [X] [StableLM models](https://huggingface.co/stabilityai) -- [x] [Deepseek models](https://huggingface.co/models?search=deepseek-ai/deepseek) -- [x] [Qwen models](https://huggingface.co/models?search=Qwen/Qwen) -- [x] [PLaMo-13B](https://github.com/ggml-org/llama.cpp/pull/3557) -- [x] [Phi models](https://huggingface.co/models?search=microsoft/phi) -- [x] [PhiMoE](https://github.com/ggml-org/llama.cpp/pull/11003) -- [x] [GPT-2](https://huggingface.co/gpt2) -- [x] [Orion 14B](https://github.com/ggml-org/llama.cpp/pull/5118) -- [x] [InternLM2](https://huggingface.co/models?search=internlm2) -- [x] [CodeShell](https://github.com/WisdomShell/codeshell) -- [x] [Gemma](https://ai.google.dev/gemma) -- [x] [Mamba](https://github.com/state-spaces/mamba) -- [x] [Grok-1](https://huggingface.co/keyfan/grok-1-hf) -- [x] [Xverse](https://huggingface.co/models?search=xverse) -- [x] [Command-R models](https://huggingface.co/models?search=CohereForAI/c4ai-command-r) -- [x] [SEA-LION](https://huggingface.co/models?search=sea-lion) -- [x] [GritLM-7B](https://huggingface.co/GritLM/GritLM-7B) + [GritLM-8x7B](https://huggingface.co/GritLM/GritLM-8x7B) -- [x] [OLMo](https://allenai.org/olmo) -- [x] [OLMo 2](https://allenai.org/olmo) -- [x] [OLMoE](https://huggingface.co/allenai/OLMoE-1B-7B-0924) -- [x] [Granite models](https://huggingface.co/collections/ibm-granite/granite-code-models-6624c5cec322e4c148c8b330) -- [x] [GPT-NeoX](https://github.com/EleutherAI/gpt-neox) + [Pythia](https://github.com/EleutherAI/pythia) -- [x] [Snowflake-Arctic MoE](https://huggingface.co/collections/Snowflake/arctic-66290090abe542894a5ac520) -- [x] [Smaug](https://huggingface.co/models?search=Smaug) -- [x] [Poro 34B](https://huggingface.co/LumiOpen/Poro-34B) -- [x] [Bitnet b1.58 models](https://huggingface.co/1bitLLM) -- [x] [Flan T5](https://huggingface.co/models?search=flan-t5) -- [x] [Open Elm models](https://huggingface.co/collections/apple/openelm-instruct-models-6619ad295d7ae9f868b759ca) -- [x] [ChatGLM3-6b](https://huggingface.co/THUDM/chatglm3-6b) + [ChatGLM4-9b](https://huggingface.co/THUDM/glm-4-9b) + [GLMEdge-1.5b](https://huggingface.co/THUDM/glm-edge-1.5b-chat) + [GLMEdge-4b](https://huggingface.co/THUDM/glm-edge-4b-chat) -- [x] [GLM-4-0414](https://huggingface.co/collections/THUDM/glm-4-0414-67f3cbcb34dd9d252707cb2e) -- [x] [SmolLM](https://huggingface.co/collections/HuggingFaceTB/smollm-6695016cad7167254ce15966) -- [x] [EXAONE-3.0-7.8B-Instruct](https://huggingface.co/LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct) -- [x] [FalconMamba Models](https://huggingface.co/collections/tiiuae/falconmamba-7b-66b9a580324dd1598b0f6d4a) -- [x] [Jais](https://huggingface.co/inceptionai/jais-13b-chat) -- [x] [Bielik-11B-v2.3](https://huggingface.co/collections/speakleash/bielik-11b-v23-66ee813238d9b526a072408a) -- [x] [RWKV-6](https://github.com/BlinkDL/RWKV-LM) -- [x] [QRWKV-6](https://huggingface.co/recursal/QRWKV6-32B-Instruct-Preview-v0.1) -- [x] [GigaChat-20B-A3B](https://huggingface.co/ai-sage/GigaChat-20B-A3B-instruct) -- [X] [Trillion-7B-preview](https://huggingface.co/trillionlabs/Trillion-7B-preview) -- [x] [Ling models](https://huggingface.co/collections/inclusionAI/ling-67c51c85b34a7ea0aba94c32) -- [x] [LFM2 models](https://huggingface.co/collections/LiquidAI/lfm2-686d721927015b2ad73eaa38) -- [x] [Hunyuan models](https://huggingface.co/collections/tencent/hunyuan-dense-model-6890632cda26b19119c9c5e7) -- [x] [BailingMoeV2 (Ring/Ling 2.0) models](https://huggingface.co/collections/inclusionAI/ling-v2-68bf1dd2fc34c306c1fa6f86) - -#### Multimodal - -- [x] [LLaVA 1.5 models](https://huggingface.co/collections/liuhaotian/llava-15-653aac15d994e992e2677a7e), [LLaVA 1.6 models](https://huggingface.co/collections/liuhaotian/llava-16-65b9e40155f60fd046a5ccf2) -- [x] [BakLLaVA](https://huggingface.co/models?search=SkunkworksAI/Bakllava) -- [x] [Obsidian](https://huggingface.co/NousResearch/Obsidian-3B-V0.5) -- [x] [ShareGPT4V](https://huggingface.co/models?search=Lin-Chen/ShareGPT4V) -- [x] [MobileVLM 1.7B/3B models](https://huggingface.co/models?search=mobileVLM) -- [x] [Yi-VL](https://huggingface.co/models?search=Yi-VL) -- [x] [Mini CPM](https://huggingface.co/models?search=MiniCPM) -- [x] [Moondream](https://huggingface.co/vikhyatk/moondream2) -- [x] [Bunny](https://github.com/BAAI-DCAI/Bunny) -- [x] [GLM-EDGE](https://huggingface.co/models?search=glm-edge) -- [x] [Qwen2-VL](https://huggingface.co/collections/Qwen/qwen2-vl-66cee7455501d7126940800d) -- [x] [LFM2-VL](https://huggingface.co/collections/LiquidAI/lfm2-vl-68963bbc84a610f7638d5ffa) - -
- -
-Bindings - -- Python: [ddh0/easy-llama](https://github.com/ddh0/easy-llama) -- Python: [abetlen/llama-cpp-python](https://github.com/abetlen/llama-cpp-python) -- Go: [go-skynet/go-llama.cpp](https://github.com/go-skynet/go-llama.cpp) -- Node.js: [withcatai/node-llama-cpp](https://github.com/withcatai/node-llama-cpp) -- JS/TS (llama.cpp server client): [lgrammel/modelfusion](https://modelfusion.dev/integration/model-provider/llamacpp) -- JS/TS (Programmable Prompt Engine CLI): [offline-ai/cli](https://github.com/offline-ai/cli) -- JavaScript/Wasm (works in browser): [tangledgroup/llama-cpp-wasm](https://github.com/tangledgroup/llama-cpp-wasm) -- Typescript/Wasm (nicer API, available on npm): [ngxson/wllama](https://github.com/ngxson/wllama) -- Ruby: [yoshoku/llama_cpp.rb](https://github.com/yoshoku/llama_cpp.rb) -- Rust (more features): [edgenai/llama_cpp-rs](https://github.com/edgenai/llama_cpp-rs) -- Rust (nicer API): [mdrokz/rust-llama.cpp](https://github.com/mdrokz/rust-llama.cpp) -- Rust (more direct bindings): [utilityai/llama-cpp-rs](https://github.com/utilityai/llama-cpp-rs) -- Rust (automated build from crates.io): [ShelbyJenkins/llm_client](https://github.com/ShelbyJenkins/llm_client) -- C#/.NET: [SciSharp/LLamaSharp](https://github.com/SciSharp/LLamaSharp) -- C#/VB.NET (more features - community license): [LM-Kit.NET](https://docs.lm-kit.com/lm-kit-net/index.html) -- Scala 3: [donderom/llm4s](https://github.com/donderom/llm4s) -- Clojure: [phronmophobic/llama.clj](https://github.com/phronmophobic/llama.clj) -- React Native: [mybigday/llama.rn](https://github.com/mybigday/llama.rn) -- Java: [kherud/java-llama.cpp](https://github.com/kherud/java-llama.cpp) -- Java: [QuasarByte/llama-cpp-jna](https://github.com/QuasarByte/llama-cpp-jna) -- Zig: [deins/llama.cpp.zig](https://github.com/Deins/llama.cpp.zig) -- Flutter/Dart: [netdur/llama_cpp_dart](https://github.com/netdur/llama_cpp_dart) -- Flutter: [xuegao-tzx/Fllama](https://github.com/xuegao-tzx/Fllama) -- PHP (API bindings and features built on top of llama.cpp): [distantmagic/resonance](https://github.com/distantmagic/resonance) [(more info)](https://github.com/ggml-org/llama.cpp/pull/6326) -- Guile Scheme: [guile_llama_cpp](https://savannah.nongnu.org/projects/guile-llama-cpp) -- Swift [srgtuszy/llama-cpp-swift](https://github.com/srgtuszy/llama-cpp-swift) -- Swift [ShenghaiWang/SwiftLlama](https://github.com/ShenghaiWang/SwiftLlama) -- Delphi [Embarcadero/llama-cpp-delphi](https://github.com/Embarcadero/llama-cpp-delphi) -- Go (no CGo needed): [hybridgroup/yzma](https://github.com/hybridgroup/yzma) - -
- -
-UIs - -*(to have a project listed here, it should clearly state that it depends on `llama.cpp`)* - -- [AI Sublime Text plugin](https://github.com/yaroslavyaroslav/OpenAI-sublime-text) (MIT) -- [cztomsik/ava](https://github.com/cztomsik/ava) (MIT) -- [Dot](https://github.com/alexpinel/Dot) (GPL) -- [eva](https://github.com/ylsdamxssjxxdd/eva) (MIT) -- [iohub/collama](https://github.com/iohub/coLLaMA) (Apache-2.0) -- [janhq/jan](https://github.com/janhq/jan) (AGPL) -- [johnbean393/Sidekick](https://github.com/johnbean393/Sidekick) (MIT) -- [KanTV](https://github.com/zhouwg/kantv?tab=readme-ov-file) (Apache-2.0) -- [KodiBot](https://github.com/firatkiral/kodibot) (GPL) -- [llama.vim](https://github.com/ggml-org/llama.vim) (MIT) -- [LARS](https://github.com/abgulati/LARS) (AGPL) -- [Llama Assistant](https://github.com/vietanhdev/llama-assistant) (GPL) -- [LLMFarm](https://github.com/guinmoon/LLMFarm?tab=readme-ov-file) (MIT) -- [LLMUnity](https://github.com/undreamai/LLMUnity) (MIT) -- [LMStudio](https://lmstudio.ai/) (proprietary) -- [LocalAI](https://github.com/mudler/LocalAI) (MIT) -- [LostRuins/koboldcpp](https://github.com/LostRuins/koboldcpp) (AGPL) -- [MindMac](https://mindmac.app) (proprietary) -- [MindWorkAI/AI-Studio](https://github.com/MindWorkAI/AI-Studio) (FSL-1.1-MIT) -- [Mobile-Artificial-Intelligence/maid](https://github.com/Mobile-Artificial-Intelligence/maid) (MIT) -- [Mozilla-Ocho/llamafile](https://github.com/Mozilla-Ocho/llamafile) (Apache-2.0) -- [nat/openplayground](https://github.com/nat/openplayground) (MIT) -- [nomic-ai/gpt4all](https://github.com/nomic-ai/gpt4all) (MIT) -- [ollama/ollama](https://github.com/ollama/ollama) (MIT) -- [oobabooga/text-generation-webui](https://github.com/oobabooga/text-generation-webui) (AGPL) -- [PocketPal AI](https://github.com/a-ghorbani/pocketpal-ai) (MIT) -- [psugihara/FreeChat](https://github.com/psugihara/FreeChat) (MIT) -- [ptsochantaris/emeltal](https://github.com/ptsochantaris/emeltal) (MIT) -- [pythops/tenere](https://github.com/pythops/tenere) (AGPL) -- [ramalama](https://github.com/containers/ramalama) (MIT) -- [semperai/amica](https://github.com/semperai/amica) (MIT) -- [withcatai/catai](https://github.com/withcatai/catai) (MIT) -- [Autopen](https://github.com/blackhole89/autopen) (GPL) - -
- -
-Tools - -- [akx/ggify](https://github.com/akx/ggify) – download PyTorch models from HuggingFace Hub and convert them to GGML -- [akx/ollama-dl](https://github.com/akx/ollama-dl) – download models from the Ollama library to be used directly with llama.cpp -- [crashr/gppm](https://github.com/crashr/gppm) – launch llama.cpp instances utilizing NVIDIA Tesla P40 or P100 GPUs with reduced idle power consumption -- [gpustack/gguf-parser](https://github.com/gpustack/gguf-parser-go/tree/main/cmd/gguf-parser) - review/check the GGUF file and estimate the memory usage -- [Styled Lines](https://marketplace.unity.com/packages/tools/generative-ai/styled-lines-llama-cpp-model-292902) (proprietary licensed, async wrapper of inference part for game development in Unity3d with pre-built Mobile and Web platform wrappers and a model example) -- [unslothai/unsloth](https://github.com/unslothai/unsloth) – 🦥 exports/saves fine-tuned and trained models to GGUF (Apache-2.0) - -
- -
-Infrastructure - -- [Paddler](https://github.com/intentee/paddler) - Open-source LLMOps platform for hosting and scaling AI in your own infrastructure -- [GPUStack](https://github.com/gpustack/gpustack) - Manage GPU clusters for running LLMs -- [llama_cpp_canister](https://github.com/onicai/llama_cpp_canister) - llama.cpp as a smart contract on the Internet Computer, using WebAssembly -- [llama-swap](https://github.com/mostlygeek/llama-swap) - transparent proxy that adds automatic model switching with llama-server -- [Kalavai](https://github.com/kalavai-net/kalavai-client) - Crowdsource end to end LLM deployment at any scale -- [llmaz](https://github.com/InftyAI/llmaz) - ☸️ Easy, advanced inference platform for large language models on Kubernetes. -
- -
-Games - -- [Lucy's Labyrinth](https://github.com/MorganRO8/Lucys_Labyrinth) - A simple maze game where agents controlled by an AI model will try to trick you. - -
- - -## Supported backends - -| Backend | Target devices | -| --- | --- | -| [Metal](docs/build.md#metal-build) | Apple Silicon | -| [BLAS](docs/build.md#blas-build) | All | -| [BLIS](docs/backend/BLIS.md) | All | -| [SYCL](docs/backend/SYCL.md) | Intel and Nvidia GPU | -| [MUSA](docs/build.md#musa) | Moore Threads GPU | -| [CUDA](docs/build.md#cuda) | Nvidia GPU | -| [HIP](docs/build.md#hip) | AMD GPU | -| [Vulkan](docs/build.md#vulkan) | GPU | -| [CANN](docs/build.md#cann) | Ascend NPU | -| [OpenCL](docs/backend/OPENCL.md) | Adreno GPU | -| [IBM zDNN](docs/backend/zDNN.md) | IBM Z & LinuxONE | -| [WebGPU [In Progress]](docs/build.md#webgpu) | All | -| [RPC](https://github.com/ggml-org/llama.cpp/tree/master/tools/rpc) | All | -| [Hexagon [In Progress]](docs/backend/hexagon/README.md) | Snapdragon | - -## Obtaining and quantizing models - -The [Hugging Face](https://huggingface.co) platform hosts a [number of LLMs](https://huggingface.co/models?library=gguf&sort=trending) compatible with `llama.cpp`: - -- [Trending](https://huggingface.co/models?library=gguf&sort=trending) -- [LLaMA](https://huggingface.co/models?sort=trending&search=llama+gguf) - -You can either manually download the GGUF file or directly use any `llama.cpp`-compatible models from [Hugging Face](https://huggingface.co/) or other model hosting sites, such as [ModelScope](https://modelscope.cn/), by using this CLI argument: `-hf /[:quant]`. For example: - -```sh -llama-cli -hf ggml-org/gemma-3-1b-it-GGUF -``` - -By default, the CLI would download from Hugging Face, you can switch to other options with the environment variable `MODEL_ENDPOINT`. For example, you may opt to downloading model checkpoints from ModelScope or other model sharing communities by setting the environment variable, e.g. `MODEL_ENDPOINT=https://www.modelscope.cn/`. - -After downloading a model, use the CLI tools to run it locally - see below. - -`llama.cpp` requires the model to be stored in the [GGUF](https://github.com/ggml-org/ggml/blob/master/docs/gguf.md) file format. Models in other data formats can be converted to GGUF using the `convert_*.py` Python scripts in this repo. - -The Hugging Face platform provides a variety of online tools for converting, quantizing and hosting models with `llama.cpp`: - -- Use the [GGUF-my-repo space](https://huggingface.co/spaces/ggml-org/gguf-my-repo) to convert to GGUF format and quantize model weights to smaller sizes -- Use the [GGUF-my-LoRA space](https://huggingface.co/spaces/ggml-org/gguf-my-lora) to convert LoRA adapters to GGUF format (more info: https://github.com/ggml-org/llama.cpp/discussions/10123) -- Use the [GGUF-editor space](https://huggingface.co/spaces/CISCai/gguf-editor) to edit GGUF meta data in the browser (more info: https://github.com/ggml-org/llama.cpp/discussions/9268) -- Use the [Inference Endpoints](https://ui.endpoints.huggingface.co/) to directly host `llama.cpp` in the cloud (more info: https://github.com/ggml-org/llama.cpp/discussions/9669) - -To learn more about model quantization, [read this documentation](tools/quantize/README.md) - -## [`llama-cli`](tools/main) - -#### A CLI tool for accessing and experimenting with most of `llama.cpp`'s functionality. - --
- Run in conversation mode - - Models with a built-in chat template will automatically activate conversation mode. If this doesn't occur, you can manually enable it by adding `-cnv` and specifying a suitable chat template with `--chat-template NAME` - - ```bash - llama-cli -m model.gguf - - # > hi, who are you? - # Hi there! I'm your helpful assistant! I'm an AI-powered chatbot designed to assist and provide information to users like you. I'm here to help answer your questions, provide guidance, and offer support on a wide range of topics. I'm a friendly and knowledgeable AI, and I'm always happy to help with anything you need. What's on your mind, and how can I assist you today? - # - # > what is 1+1? - # Easy peasy! The answer to 1+1 is... 2! - ``` - -
- --
- Run in conversation mode with custom chat template - - ```bash - # use the "chatml" template (use -h to see the list of supported templates) - llama-cli -m model.gguf -cnv --chat-template chatml - - # use a custom template - llama-cli -m model.gguf -cnv --in-prefix 'User: ' --reverse-prompt 'User:' - ``` - -
- --
- Run simple text completion - - To disable conversation mode explicitly, use `-no-cnv` - - ```bash - llama-cli -m model.gguf -p "I believe the meaning of life is" -n 128 -no-cnv - - # I believe the meaning of life is to find your own truth and to live in accordance with it. For me, this means being true to myself and following my passions, even if they don't align with societal expectations. I think that's what I love about yoga – it's not just a physical practice, but a spiritual one too. It's about connecting with yourself, listening to your inner voice, and honoring your own unique journey. - ``` - -
- --
- Constrain the output with a custom grammar - - ```bash - llama-cli -m model.gguf -n 256 --grammar-file grammars/json.gbnf -p 'Request: schedule a call at 8pm; Command:' - - # {"appointmentTime": "8pm", "appointmentDetails": "schedule a a call"} - ``` - - The [grammars/](grammars/) folder contains a handful of sample grammars. To write your own, check out the [GBNF Guide](grammars/README.md). - - For authoring more complex JSON grammars, check out https://grammar.intrinsiclabs.ai/ +- Embedded systems with limited or no filesystem access +- WebAssembly deployments where models are fetched over the network +- Encrypted model storage where models are decrypted in memory +- Streaming scenarios where models arrive over network connections -
+```cpp +#include "llama-cpp.h" +std::vector model_data = /* load from network, decrypt, etc. */; +auto model = llama_model_load_from_buffer(std::move(model_data), params); -## [`llama-server`](tools/server) - -#### A lightweight, [OpenAI API](https://github.com/openai/openai-openapi) compatible, HTTP server for serving LLMs. - --
- Start a local HTTP server with default configuration on port 8080 - - ```bash - llama-server -m model.gguf --port 8080 - - # Basic web UI can be accessed via browser: http://localhost:8080 - # Chat completion endpoint: http://localhost:8080/v1/chat/completions - ``` - -
- --
- Support multiple-users and parallel decoding - - ```bash - # up to 4 concurrent requests, each with 4096 max context - llama-server -m model.gguf -c 16384 -np 4 - ``` - -
- --
- Enable speculative decoding - - ```bash - # the draft.gguf model should be a small variant of the target model.gguf - llama-server -m model.gguf -md draft.gguf - ``` - -
- --
- Serve an embedding model - - ```bash - # use the /embedding endpoint - llama-server -m model.gguf --embedding --pooling cls -ub 8192 - ``` - -
- --
- Serve a reranking model - - ```bash - # use the /reranking endpoint - llama-server -m model.gguf --reranking - ``` - -
- --
- Constrain all outputs with a grammar - - ```bash - # custom grammar - llama-server -m model.gguf --grammar-file grammar.gbnf +auto model = llama_model_load_from_split_futures(paths, n_paths, context, tensor_list, params); +llama_model_load_fulfill_split_future(path, context, std::move(streambuf)); +``` - # JSON - llama-server -m model.gguf --grammar-file grammars/json.gbnf - ``` +### Mobile GPU Optimization *(exclusive)* -
+Enhanced GPU support with targeted optimizations for Qualcomm Adreno GPUs. +- **Vulkan on Adreno 800+**: quantized inference (Q4_0, Q8) and LoRA fine-tuning for Gemma3, Qwen3, and BitNet (TQ2_0) model architectures. +- **OpenCL on Adreno**: inference support for all other model architectures. +- Adreno-specific Vulkan shader variants for improved throughput. +- Vulkan Memory Allocator (VMA) integration for efficient GPU memory management. -## [`llama-perplexity`](tools/perplexity) -#### A tool for measuring the [perplexity](tools/perplexity/README.md) [^1] (and other quality metrics) of a model over a given text. +## Quick Start --
- Measure the perplexity over a text file +### Building from Source - ```bash - llama-perplexity -m model.gguf -f file.txt +```bash +git clone https://github.com/tetherto/qvac-fabric-llm.cpp.git +cd qvac-fabric-llm.cpp - # [1]15.2701,[2]5.4007,[3]5.3073,[4]6.2965,[5]5.8940,[6]5.6096,[7]5.7942,[8]4.9297, ... - # Final estimate: PPL = 5.4007 +/- 0.67339 - ``` +# Standard build +cmake -B build +cmake --build build --config Release -
+# With Vulkan support (Android, Windows, Linux) +cmake -B build -DGGML_VULKAN=ON +cmake --build build --config Release --
- Measure KL divergence +# With Metal support (macOS, iOS) +cmake -B build -DGGML_METAL=ON +cmake --build build --config Release +``` - ```bash - # TODO - ``` +For more detailed build instructions, see [docs/build.md](docs/build.md). -
+### Running a Model -[^1]: [https://huggingface.co/docs/transformers/perplexity](https://huggingface.co/docs/transformers/perplexity) +```bash +# Run a model with Vulkan GPU acceleration, 4096 context length, and a prompt +./build/bin/llama-cli -m model.gguf -ngl 99 -c 4096 -p "Explain quantum computing in simple terms" +``` -## [`llama-bench`](tools/llama-bench) -#### Benchmark the performance of the inference for various parameters. +## Supported Platforms --
- Run default benchmark +| Platform | Backend | Status | +|----------|---------|--------| +| Linux (x86_64, ARM64) | CPU, Vulkan, CUDA | ✅ Full support | +| macOS (Intel, Apple Silicon) | CPU, Metal | ✅ Full support | +| Windows (x86_64) | CPU, Vulkan, CUDA | ✅ Full support | +| Android (ARM64) | CPU, Vulkan, OpenCL | ✅ Full support | +| iOS | CPU, Metal | ✅ Full support | - ```bash - llama-bench -m model.gguf - # Output: - # | model | size | params | backend | threads | test | t/s | - # | ------------------- | ---------: | ---------: | ---------- | ------: | ------------: | -------------------: | - # | qwen2 1.5B Q4_0 | 885.97 MiB | 1.54 B | Metal,BLAS | 16 | pp512 | 5765.41 ± 20.55 | - # | qwen2 1.5B Q4_0 | 885.97 MiB | 1.54 B | Metal,BLAS | 16 | tg128 | 197.71 ± 0.81 | - # - # build: 3e0ba0e60 (4229) - ``` +## Relationship with llama.cpp -
+qvac-fabric-llm.cpp is a maintained fork of [llama.cpp](https://github.com/ggml-org/llama.cpp). The project regularly synchronizes with upstream releases to incorporate improvements, bug fixes, and new model support, while extending the engine with capabilities not present in the upstream project. -## [`llama-run`](tools/run) +**Current upstream baseline:** llama.cpp b7248 -#### A comprehensive example for running `llama.cpp` models. Useful for inferencing. Used with RamaLama [^3]. +### Exclusive Features --
- Run a model with a specific prompt (by default it's pulled from Ollama registry) +The following features are developed in qvac-fabric-llm.cpp and are not available in upstream llama.cpp: - ```bash - llama-run granite-code - ``` +| Feature | Description | +|---------|-------------| +| LoRA fine-tuning | On-device training across CPU, Vulkan, and Metal with SFT, checkpointing, and LR scheduling | +| BitNet inference and training | TQ2_0 quantization on Vulkan, Metal, and CPU for inference and LoRA fine-tuning; extends [microsoft/BitNet](https://github.com/microsoft/BitNet) beyond its CUDA-only GPU support | +| Memory-based model loading | Load models from in-memory buffers with split-model and async fulfillment support | +| Mobile GPU optimization | Adreno 800+ quantized inference (Q4_0, Q8), Adreno-specific Vulkan shader variants, VMA integration | -
+### Upstream Compatibility -[^3]: [RamaLama](https://github.com/containers/ramalama) +All standard llama.cpp functionality, models, and APIs remain fully compatible. -## [`llama-simple`](examples/simple) +- Any GGUF model supported by llama.cpp is supported by qvac-fabric-llm.cpp +- Existing llama.cpp documentation applies to all non-exclusive features -#### A minimal example for implementing apps with `llama.cpp`. Useful for developers. --
- Basic text completion +## Contributing - ```bash - llama-simple -m model.gguf +We welcome contributions! Please see our development workflow: - # Hello my name is Kaitlyn and I am a 16 year old girl. I am a junior in high school and I am currently taking a class called "The Art of - ``` +1. Fork the repository +2. Create a feature branch from `master` +3. Submit a pull request -
+## License -## Contributing +MIT License - see [LICENSE](LICENSE) for details. -- Contributors can open PRs -- Collaborators will be invited based on contributions -- Maintainers can push to branches in the `llama.cpp` repo and merge PRs into the `master` branch -- Any help with managing issues, PRs and projects is very appreciated! -- See [good first issues](https://github.com/ggml-org/llama.cpp/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) for tasks suitable for first contributions -- Read the [CONTRIBUTING.md](CONTRIBUTING.md) for more information -- Make sure to read this: [Inference at the edge](https://github.com/ggml-org/llama.cpp/discussions/205) -- A bit of backstory for those who are interested: [Changelog podcast](https://changelog.com/podcast/532) - -## Other documentation - -- [main (cli)](tools/main/README.md) -- [server](tools/server/README.md) -- [GBNF grammars](grammars/README.md) - -#### Development documentation - -- [How to build](docs/build.md) -- [Running on Docker](docs/docker.md) -- [Build on Android](docs/android.md) -- [Performance troubleshooting](docs/development/token_generation_performance_tips.md) -- [GGML tips & tricks](https://github.com/ggml-org/llama.cpp/wiki/GGML-Tips-&-Tricks) - -#### Seminal papers and background on the models - -If your issue is with model generation quality, then please at least scan the following links and papers to understand the limitations of LLaMA models. This is especially important when choosing an appropriate model size and appreciating both the significant and subtle differences between LLaMA models and ChatGPT: -- LLaMA: - - [Introducing LLaMA: A foundational, 65-billion-parameter large language model](https://ai.facebook.com/blog/large-language-model-llama-meta-ai/) - - [LLaMA: Open and Efficient Foundation Language Models](https://arxiv.org/abs/2302.13971) -- GPT-3 - - [Language Models are Few-Shot Learners](https://arxiv.org/abs/2005.14165) -- GPT-3.5 / InstructGPT / ChatGPT: - - [Aligning language models to follow instructions](https://openai.com/research/instruction-following) - - [Training language models to follow instructions with human feedback](https://arxiv.org/abs/2203.02155) - -## XCFramework -The XCFramework is a precompiled version of the library for iOS, visionOS, tvOS, -and macOS. It can be used in Swift projects without the need to compile the -library from source. For example: -```swift -// swift-tools-version: 5.10 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "MyLlamaPackage", - targets: [ - .executableTarget( - name: "MyLlamaPackage", - dependencies: [ - "LlamaFramework" - ]), - .binaryTarget( - name: "LlamaFramework", - url: "https://github.com/ggml-org/llama.cpp/releases/download/b5046/llama-b5046-xcframework.zip", - checksum: "c19be78b5f00d8d29a25da41042cb7afa094cbf6280a225abe614b03b20029ab" - ) - ] -) -``` -The above example is using an intermediate build `b5046` of the library. This can be modified -to use a different version by changing the URL and checksum. +qvac-fabric-llm.cpp is built on [llama.cpp](https://github.com/ggml-org/llama.cpp) by Georgi Gerganov and contributors. -## Completions -Command-line completion is available for some environments. +--- -#### Bash Completion -```bash -$ build/bin/llama-cli --completion-bash > ~/.llama-completion.bash -$ source ~/.llama-completion.bash -``` -Optionally this can be added to your `.bashrc` or `.bash_profile` to load it -automatically. For example: -```console -$ echo "source ~/.llama-completion.bash" >> ~/.bashrc -``` +For additional documentation, refer to the [llama.cpp documentation](https://github.com/ggml-org/llama.cpp/tree/master/docs). ## Dependencies @@ -613,4 +161,3 @@ $ echo "source ~/.llama-completion.bash" >> ~/.bashrc - [linenoise.cpp](./tools/run/linenoise.cpp/linenoise.cpp) - C++ library that provides readline-like line editing capabilities, used by `llama-run` - BSD 2-Clause License - [curl](https://curl.se/) - Client-side URL transfer library, used by various tools/examples - [CURL License](https://curl.se/docs/copyright.html) - [miniaudio.h](https://github.com/mackron/miniaudio) - Single-header audio format decoder, used by multimodal subsystem - Public domain -- [subprocess.h](https://github.com/sheredom/subprocess.h) - Single-header process launching solution for C and C++ - Public domain diff --git a/ci/run.sh b/ci/run.sh index 83b2603e8210..7fa469b14b57 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -236,6 +236,11 @@ function gg_run_ctest_release { # Check cmake, make and ctest are installed gg_check_build_requirements + # Disable native CPU optimizations for low-perf builds to ensure compatibility + if [ ! -z ${GG_BUILD_LOW_PERF} ]; then + CMAKE_EXTRA="${CMAKE_EXTRA} -DGGML_NATIVE=OFF" + fi + (time cmake -DCMAKE_BUILD_TYPE=Release ${CMAKE_EXTRA} .. ) 2>&1 | tee -a $OUT/${ci}-cmake.log (time make -j$(nproc) ) 2>&1 | tee -a $OUT/${ci}-make.log diff --git a/cmake/llama-config.cmake.in b/cmake/llama-config.cmake.in index 90cbec5b6f13..5a1d6a8a33b8 100644 --- a/cmake/llama-config.cmake.in +++ b/cmake/llama-config.cmake.in @@ -5,26 +5,9 @@ set(LLAMA_SHARED_LIB @BUILD_SHARED_LIBS@) @PACKAGE_INIT@ -set_and_check(LLAMA_INCLUDE_DIR "@PACKAGE_LLAMA_INCLUDE_INSTALL_DIR@") -set_and_check(LLAMA_LIB_DIR "@PACKAGE_LLAMA_LIB_INSTALL_DIR@") -set_and_check(LLAMA_BIN_DIR "@PACKAGE_LLAMA_BIN_INSTALL_DIR@") +include(CMakeFindDependencyMacro) +find_dependency(ggml CONFIG REQUIRED) -find_package(ggml REQUIRED HINTS ${LLAMA_LIB_DIR}/cmake) +include("${CMAKE_CURRENT_LIST_DIR}/llama-targets.cmake") -find_library(llama_LIBRARY llama - REQUIRED - HINTS ${LLAMA_LIB_DIR} - NO_CMAKE_FIND_ROOT_PATH -) - -add_library(llama UNKNOWN IMPORTED) -set_target_properties(llama - PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${LLAMA_INCLUDE_DIR}" - INTERFACE_LINK_LIBRARIES "ggml::ggml;ggml::ggml-base;" - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - IMPORTED_LOCATION "${llama_LIBRARY}" - INTERFACE_COMPILE_FEATURES c_std_90 - POSITION_INDEPENDENT_CODE ON) - -check_required_components(Llama) +check_required_components(llama) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index bb168e8358aa..c72b54b27b0a 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -44,39 +44,50 @@ endif() set(TARGET common) +set(${TARGET}_HEADERS + arg.h + base64.hpp + chat-parser.h + chat.h + common.h + console.h + json-partial.h + json-schema-to-grammar.h + log.h + ngram-cache.h + regex-partial.h + sampling.h + speculative.h +) + +list(TRANSFORM ${TARGET}_HEADERS PREPEND ${CMAKE_CURRENT_SOURCE_DIR}/) + add_library(${TARGET} STATIC arg.cpp - arg.h - base64.hpp chat-parser.cpp chat-parser.h chat-parser-xml-toolcall.h chat-parser-xml-toolcall.cpp chat.cpp - chat.h common.cpp - common.h console.cpp console.h download.cpp download.h http.h json-partial.cpp - json-partial.h json-schema-to-grammar.cpp llguidance.cpp log.cpp - log.h ngram-cache.cpp - ngram-cache.h regex-partial.cpp - regex-partial.h sampling.cpp - sampling.h speculative.cpp - speculative.h + ${${TARGET}_HEADERS} ) +set_target_properties(${TARGET} PROPERTIES PUBLIC_HEADER "${${TARGET}_HEADERS}") + if (BUILD_SHARED_LIBS) set_target_properties(${TARGET} PROPERTIES POSITION_INDEPENDENT_CODE ON) endif() @@ -143,7 +154,12 @@ if (LLAMA_LLGUIDANCE) set(LLAMA_COMMON_EXTRA_LIBS ${LLAMA_COMMON_EXTRA_LIBS} llguidance ${LLGUIDANCE_PLATFORM_LIBS}) endif () -target_include_directories(${TARGET} PUBLIC . ../vendor) +target_include_directories( + ${TARGET} + PUBLIC + $${CMAKE_SOURCE_DIR}/vendor> + $) + target_compile_features (${TARGET} PUBLIC cxx_std_17) target_link_libraries (${TARGET} PRIVATE ${LLAMA_COMMON_EXTRA_LIBS} PUBLIC llama Threads::Threads) diff --git a/common/arg.cpp b/common/arg.cpp index 45c0d1a72609..8c37edff6843 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -1811,6 +1811,14 @@ common_params_context common_params_parser_init(common_params & params, llama_ex params.mmproj_use_gpu = false; } ).set_examples(mmproj_examples).set_env("LLAMA_ARG_NO_MMPROJ_OFFLOAD")); + add_opt(common_arg( + {"--mmproj-backend"}, "NAME", + "GPU backend for multimodal projector (e.g. CUDA, Metal, Vulkan)\n" + "if not specified, will use MTMD_BACKEND_DEVICE env var or default GPU backend", + [](common_params & params, const std::string & value) { + params.mmproj_backend = value; + } + ).set_examples({LLAMA_EXAMPLE_MTMD, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_MAIN})); add_opt(common_arg( {"--image", "--audio"}, "FILE", "path to an image or audio file. use with multimodal models, can be repeated if you have multiple files\n", diff --git a/common/common.cpp b/common/common.cpp index f07af1d86255..01b86c98a313 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -9,6 +9,8 @@ #include "log.h" #include "llama.h" #include "sampling.h" +#include "chat.h" +#include #include #include @@ -1021,16 +1023,7 @@ static inline void common_init_sampler_from_model( get_float(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_MIROSTAT_ETA), sparams.mirostat_eta, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_MIROSTAT_ETA); } -struct common_init_result common_init_from_params(common_params & params) { - common_init_result iparams; - auto mparams = common_model_params_to_llama(params); - - llama_model * model = llama_model_load_from_file(params.model.path.c_str(), mparams); - if (model == NULL) { - LOG_ERR("%s: failed to load model '%s', try reducing --n-gpu-layers if you're running out of VRAM\n", - __func__, params.model.path.c_str()); - return iparams; - } +struct common_init_result common_init_from_model_and_params(llama_model* model, common_init_result iparams, common_params & params) { common_init_sampler_from_model(model, params.sampling); @@ -1203,6 +1196,19 @@ struct common_init_result common_init_from_params(common_params & params) { return iparams; } +struct common_init_result common_init_from_params(common_params & params) { + common_init_result iparams; + auto mparams = common_model_params_to_llama(params); + + llama_model * model = llama_model_load_from_file(params.model.path.c_str(), mparams); + if (model == NULL) { + LOG_ERR("%s: failed to load model '%s'\n", __func__, params.model.path.c_str()); + return iparams; + } + + return common_init_from_model_and_params(model, std::move(iparams), params); +} + std::string get_model_endpoint() { const char * model_endpoint_env = getenv("MODEL_ENDPOINT"); // We still respect the use of environment-variable "HF_ENDPOINT" for backward-compatibility. @@ -1293,6 +1299,7 @@ struct llama_context_params common_context_params_to_llama(const common_params & cparams.no_perf = params.no_perf; cparams.op_offload = !params.no_op_offload; cparams.swa_full = params.swa_full; + cparams.training = params.training; cparams.kv_unified = params.kv_unified; cparams.type_k = params.cache_type_k; @@ -1738,3 +1745,333 @@ float lr_opt::get_lr(float epoch) const { LOG_INF("epoch %.2g lr=%.2g\n", epoch, r); return r; } + +ggml_opt_dataset_t common_opt_sft_dataset_init( + struct llama_context * ctx, + const std::string & json_content, + int64_t stride, + const std::string & chat_template_path) { + using json = nlohmann::json; + + const llama_vocab * vocab = llama_model_get_vocab(llama_get_model(ctx)); + common_chat_templates_ptr chat_templates; + std::string chat_template_source; + if (!chat_template_path.empty()) { + std::ifstream tmpl_file(chat_template_path); + if (!tmpl_file.is_open()) { + LOG_ERR("Warning: Failed to open chat template file: %s\n", chat_template_path.c_str()); + } else { + chat_template_source.assign(std::istreambuf_iterator(tmpl_file), std::istreambuf_iterator()); + tmpl_file.close(); + } + } + + try { + chat_templates = common_chat_templates_init(llama_get_model(ctx), chat_template_source); + if (chat_template_source.empty()) { + LOG_INF("Using model's built-in chat template\n"); + } else { + LOG_INF("Using custom chat template from: %s\n", chat_template_path.c_str()); + } + } catch (const std::exception & e) { + if (!chat_template_path.empty()) { + LOG_ERR("Warning: Failed to parse chat template '%s': %s\n", chat_template_path.c_str(), e.what()); + } else { + LOG_ERR("Warning: Failed to initialize chat template: %s\n", e.what()); + } + } + + std::vector conversations; + std::istringstream content_stream(json_content); + + std::string line; + while (std::getline(content_stream, line)) { + if (line.empty() || line[0] == '#') continue; + try { + json conv = json::parse(line); + if (conv.contains("messages") && conv["messages"].is_array()) { + conversations.push_back(conv); + } + } catch (const json::exception & e) { + LOG_DBG("Warning: Failed to parse JSON line: %s\n", e.what()); + } + } + + if (conversations.empty()) { + LOG_ERR("Error: No valid conversations found\n"); + return nullptr; + } + LOG_INF("Loaded %zu conversations\n", conversations.size()); + + const int64_t ne_datapoint = llama_n_ctx(ctx); + if (stride <= 0) stride = ne_datapoint; + if (stride > ne_datapoint) stride = ne_datapoint; + + std::vector> all_tokenized_data; + std::vector> all_assistant_masks; + + auto token_count_prefix = [&](const std::string & render, size_t char_count) -> size_t { + std::string prefix = render.substr(0, char_count); + auto t = common_tokenize(ctx, prefix, /*add_special=*/false, /*parse_special=*/true); + return t.size(); + }; + + // TODO: The masking logic currently relies on string searching for specific role tags. + // While chat templates render appropriately, we must parse the rendered string to find + // the boundaries of the "assistant" role to calculate loss gracefully. + // Currently, this only supports ChatML (Qwen, etc.) and Gemma formats. + // A more reliable, model-agnostic method would require common_chat_templates_apply to + // return token role spans directly, which is not yet supported in common/chat.cpp. + const std::string START_TAG = "<|im_start|>"; + const std::string START_SYS = "<|im_start|>system\n"; + const std::string START_USR = "<|im_start|>user\n"; + const std::string START_AST = "<|im_start|>assistant\n"; + const std::string END_TAG = "<|im_end|>"; + const std::string NL = "\n"; + + for (size_t i = 0; i < conversations.size(); ++i) { + const auto & messages = conversations[i]["messages"]; + if (!messages.is_array() || messages.empty()) continue; + + std::string render; + + if (chat_templates) { + std::vector chat_msgs; + chat_msgs.reserve(messages.size()); + for (const auto & msg : messages) { + if (!msg.contains("role") || !msg.contains("content")) { + continue; + } + common_chat_msg chat_msg; + chat_msg.role = msg["role"].get(); + chat_msg.content = msg["content"].get(); + chat_msgs.push_back(std::move(chat_msg)); + } + + if (!chat_msgs.empty()) { + common_chat_templates_inputs inputs; + inputs.messages = std::move(chat_msgs); + inputs.add_generation_prompt = false; + inputs.use_jinja = true; + try { + render = common_chat_templates_apply(chat_templates.get(), inputs).prompt; + + size_t last_im_end = render.rfind("<|im_end|>"); + if (last_im_end != std::string::npos) { + size_t end_pos = last_im_end + 10; // length of "<|im_end|>" + // Remove any trailing whitespace/newlines after the final <|im_end|> + while (end_pos < render.size() && (render[end_pos] == '\n' || render[end_pos] == '\r' || render[end_pos] == ' ')) { + end_pos++; + } + if (end_pos < render.size()) { + render = render.substr(0, last_im_end + 10); // Keep only up to + } + } + } catch (const std::exception & e) { + LOG_WRN("Warning: chat template rendering failed for conversation %zu: %s. Falling back to default ChatML rendering.\n", + i, e.what()); + } + } + } + + if (render.empty()) { + render.reserve(4096); + for (const auto & msg : messages) { + if (!msg.contains("role") || !msg.contains("content")) continue; + const std::string role = msg["role"].get(); + const std::string content = msg["content"].get(); + + if (role == "system") { + render += START_SYS; render += content; render += END_TAG + NL; + } else if (role == "user") { + render += START_USR; render += content; render += END_TAG + NL; + } else if (role == "assistant") { + render += START_AST; render += content; render += END_TAG + NL; + } + } + } + + if (render.empty()) { + continue; + } + + struct Span { size_t lo, hi; }; + std::vector assistant_spans; + + { + bool is_gemma = render.find("model\n") != std::string::npos; + + if (is_gemma) { + const std::string GEMMA_START = "model\n"; + const std::string GEMMA_END = ""; + + size_t from = 0; + while (true) { + size_t open = render.find(GEMMA_START, from); + if (open == std::string::npos) break; + size_t lo = open; + size_t close = render.find(GEMMA_END, lo); + if (close == std::string::npos) { + assistant_spans.push_back({lo, render.size()}); + break; + } + + size_t hi = close + GEMMA_END.size(); + if (hi < render.size() && render[hi] == '\n') { + hi++; + } + assistant_spans.push_back({lo, std::min(hi, render.size())}); + + from = hi; + } + } else { + size_t from = 0; + while (true) { + size_t open = render.find(START_AST, from); + if (open == std::string::npos) break; + + // Include the role token ("assistant") and everything through the closing tag/newlines + size_t lo = open + START_TAG.size(); + if (lo > render.size()) { + lo = render.size(); + } + + size_t close = render.find(END_TAG, open + START_AST.size()); + if (close == std::string::npos) { + assistant_spans.push_back({lo, render.size()}); + break; + } + + size_t hi = close + END_TAG.size(); + if (hi <= lo) { + lo = open; + hi = close + END_TAG.size(); + } + + assistant_spans.push_back({lo, std::min(hi, render.size())}); + + size_t next_from = hi; + from = next_from; + } + } + } + + if (assistant_spans.empty()) { + LOG_WRN("Conversation %zu has no assistant spans\n", i); + continue; + } + + auto tokens_full = common_tokenize(ctx, render, /*add_special=*/false, /*parse_special=*/true); + if (tokens_full.empty()) continue; + + std::vector assistant_mask(tokens_full.size(), 0); + size_t assistant_token_count = 0; + + for (const auto & sp : assistant_spans) { + size_t t_lo = token_count_prefix(render, sp.lo); + size_t t_hi = token_count_prefix(render, sp.hi); + if (t_lo > tokens_full.size()) t_lo = tokens_full.size(); + if (t_hi > tokens_full.size()) t_hi = tokens_full.size(); + + + for (size_t t = t_lo; t < t_hi; ++t) { + assistant_mask[t] = 1; + ++assistant_token_count; + } + } + + if (assistant_token_count == 0) { + LOG_WRN("Warning: Conversation %zu has zero assistant tokens after masking\n", i); + continue; + } + + all_tokenized_data.push_back(tokens_full); + all_assistant_masks.push_back(assistant_mask); + } + + if (all_tokenized_data.empty()) { + LOG_ERR("ERROR: No valid training samples generated after processing %zu conversations\n", conversations.size()); + return nullptr; + } + + std::vector> final_samples; + std::vector> final_masks; + + llama_token pad_token = llama_vocab_pad(vocab); + if (pad_token == LLAMA_TOKEN_NULL) { + pad_token = llama_vocab_eos(vocab); + } + + for (size_t i = 0; i < all_tokenized_data.size(); ++i) { + const auto& conv_tokens = all_tokenized_data[i]; + const auto& conv_mask = all_assistant_masks[i]; + + if ((int64_t)conv_tokens.size() > ne_datapoint) { + LOG_WRN("Skipping conversation %zu: too long (%zu tokens > %lld)\n", i, conv_tokens.size(), (long long)ne_datapoint); + continue; + } + + size_t conv_assistant_tokens = 0; + for (int32_t mask_val : conv_mask) { + if (mask_val == 1) conv_assistant_tokens++; + } + + if (conv_assistant_tokens == 0) { + LOG_WRN("Skipping conversation %zu: no assistant tokens\n", i); + continue; + } + + std::vector sample_tokens = conv_tokens; + std::vector sample_mask = conv_mask; + + sample_tokens.resize(ne_datapoint, pad_token); + sample_mask.resize(ne_datapoint, 0); // Padding tokens are not trained on + + final_samples.push_back(sample_tokens); + final_masks.push_back(sample_mask); + } + + all_tokenized_data = std::move(final_samples); + all_assistant_masks = std::move(final_masks); + + const int64_t ndata = all_tokenized_data.size(); + + ggml_opt_dataset_t result = ggml_opt_dataset_init_with_masks( + GGML_TYPE_I32, GGML_TYPE_I32, GGML_TYPE_I32, + /*ne_datapoint=*/ne_datapoint, /*ne_label=*/ne_datapoint, /*ne_mask=*/ne_datapoint, + /*ndata=*/ndata, /*ndata_shard=*/1); + + if (result == nullptr) { + return nullptr; + } + + int32_t * data = (int32_t *) ggml_opt_dataset_data(result)->data; + int32_t * labels = (int32_t *) ggml_opt_dataset_labels(result)->data; + int32_t * masks = (int32_t *) ggml_opt_dataset_masks(result)->data; + + for (int64_t idata = 0; idata < ndata; ++idata) { + const auto & sample_tokens = all_tokenized_data[idata]; + const auto & sample_mask = all_assistant_masks[idata]; + + // inputs + for (int64_t i = 0; i < ne_datapoint; ++i) { + data[idata * ne_datapoint + i] = sample_tokens[i]; + } + + // labels: Set actual next tokens for ALL positions (masked cross-entropy needs real tokens) + for (int64_t i = 0; i < ne_datapoint - 1; ++i) { + // Always set the actual next token - masking is handled separately + labels[idata * ne_datapoint + i] = sample_tokens[i + 1]; + } + labels[idata * ne_datapoint + (ne_datapoint - 1)] = sample_tokens[ne_datapoint - 1]; // last token predicts itself (will be masked) + + // masks: indicate which preds should be trained on (shifted by 1 from sample_mask) + // Since we predict token[i+1] from token[i], we train when token[i+1] is assistant + for (int64_t i = 0; i < ne_datapoint - 1; ++i) { + masks[idata * ne_datapoint + i] = (i + 1 < ne_datapoint && sample_mask[i + 1] == 1) ? 1 : 0; + } + masks[idata * ne_datapoint + (ne_datapoint - 1)] = 0; + } + + return result; +} diff --git a/common/common.h b/common/common.h index 6e6b2c1cab69..1fc687eab8e6 100644 --- a/common/common.h +++ b/common/common.h @@ -415,6 +415,7 @@ struct common_params { bool warmup = true; // warmup run bool check_tensors = false; // validate tensor data bool no_op_offload = false; // globally disable offload host tensor operations to device + bool training = false; // enable training mode (affects LoRA K/V gradient flow) bool no_extra_bufts = false; // disable extra buffer types (used for weight repacking) bool no_host = false; // bypass host buffer allowing extra buffers to be used @@ -428,6 +429,7 @@ struct common_params { // multimodal models (see tools/mtmd) struct common_params_model mmproj; bool mmproj_use_gpu = true; // use GPU for multimodal model + std::string mmproj_backend = ""; // GPU backend for multimodal model (e.g. "CUDA", "Metal", "Vulkan") bool no_mmproj = false; // explicitly disable multimodal model std::vector image; // path to image file(s) int image_min_tokens = -1; @@ -664,6 +666,8 @@ struct common_init_result { }; struct common_init_result common_init_from_params(common_params & params); +struct common_init_result common_init_from_model_and_params(llama_model * model, common_init_result iparams, + common_params & params); struct llama_model_params common_model_params_to_llama ( common_params & params); struct llama_context_params common_context_params_to_llama(const common_params & params); @@ -804,3 +808,8 @@ ggml_opt_dataset_t common_opt_dataset_init(struct llama_context * ctx, const std // "adamw" or "sgd" (case insensitive) enum ggml_opt_optimizer_type common_opt_get_optimizer(const char *); +ggml_opt_dataset_t common_opt_sft_dataset_init( + struct llama_context * ctx, + const std::string & json_content, + int64_t stride, + const std::string & chat_template_path = ""); diff --git a/common/log.cpp b/common/log.cpp index b6c9ff79a43d..ef244bddd321 100644 --- a/common/log.cpp +++ b/common/log.cpp @@ -92,7 +92,14 @@ struct common_log_entry { // signals the worker thread to stop bool is_end; - void print(FILE * file = nullptr) const { + void print(FILE * file = nullptr, ggml_log_callback callback = nullptr, void * callback_user_data = nullptr) const { + // if callback is provided, use it instead of printing + if (callback != nullptr) { + callback(level, msg.data(), callback_user_data); + return; + } + + FILE * fcur = file; if (!fcur) { // stderr displays DBG messages only when their verbosity level is not higher than the threshold @@ -150,6 +157,8 @@ struct common_log { timestamps = false; running = false; t_start = t_us(); + callback = nullptr; + callback_user_data = nullptr; // initial message size - will be expanded if longer messages arrive entries.resize(capacity); @@ -191,6 +200,10 @@ struct common_log { // worker thread copies into this common_log_entry cur; + // custom callback for log messages + ggml_log_callback callback; + void * callback_user_data; + public: void add(enum ggml_log_level level, const char * fmt, va_list args) { std::lock_guard lock(mtx); @@ -281,11 +294,15 @@ struct common_log { thrd = std::thread([this]() { while (true) { + ggml_log_callback cb = nullptr; + void * cb_user_data = nullptr; { std::unique_lock lock(mtx); cv.wait(lock, [this]() { return head != tail; }); cur = entries[head]; + cb = callback; + cb_user_data = callback_user_data; head = (head + 1) % entries.size(); } @@ -294,7 +311,7 @@ struct common_log { break; } - cur.print(); // stdout and stderr + cur.print(nullptr, cb, cb_user_data); // stdout and stderr or callback if (file) { cur.print(file); @@ -376,6 +393,15 @@ struct common_log { this->timestamps = timestamps; } + + void set_callback(ggml_log_callback cb, void * user_data) { + pause(); + + this->callback = cb; + this->callback_user_data = user_data; + + resume(); + } }; // @@ -462,3 +488,7 @@ void common_log_default_callback(enum ggml_log_level level, const char * text, v common_log_add(common_log_main(), level, "%s", text); } } + +void common_log_set_callback(struct common_log * log, ggml_log_callback callback, void * user_data) { + log->set_callback(callback, user_data); +} diff --git a/common/log.h b/common/log.h index b24f5f000a6e..8b338d24ae18 100644 --- a/common/log.h +++ b/common/log.h @@ -85,6 +85,11 @@ void common_log_set_colors (struct common_log * log, log_colors colors); // n void common_log_set_prefix (struct common_log * log, bool prefix); // whether to output prefix to each log void common_log_set_timestamps(struct common_log * log, bool timestamps); // whether to output timestamps in the prefix +// set a custom callback to handle log messages instead of printing to stdout/stderr +// if callback is NULL, reverts to default printing behavior +// note: the callback will be called from the worker thread +void common_log_set_callback(struct common_log * log, ggml_log_callback callback, void * user_data); // not thread-safe + // helper macros for logging // use these to avoid computing log arguments if the verbosity of the log is higher than the threshold // diff --git a/common_test/CMakeLists.txt b/common_test/CMakeLists.txt new file mode 100644 index 000000000000..44903612e534 --- /dev/null +++ b/common_test/CMakeLists.txt @@ -0,0 +1,15 @@ +# common_test library for load_into_memory.h and uint8-buff-stream.h + +set(TARGET llama-common-test) + +add_library(${TARGET} INTERFACE) + +target_include_directories(${TARGET} INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(${TARGET} INTERFACE LLAMA_COMMON_TEST_HEADERS) + +target_compile_features(${TARGET} INTERFACE cxx_std_17) + +target_link_libraries(${TARGET} INTERFACE common) diff --git a/common_test/load_into_memory.h b/common_test/load_into_memory.h new file mode 100644 index 000000000000..1c84e84cce82 --- /dev/null +++ b/common_test/load_into_memory.h @@ -0,0 +1,220 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// header-only utilities to showcase how to directly load a model from memory +#include "uint8-buff-stream-wrapper.h" + +namespace { +bool is_split_file(const char * const model_path) { + if (!model_path) { + fprintf(stderr, "No model file provided\n"); + exit(EXIT_FAILURE); + } + + std::string path(model_path); + return path.find("-of-") != std::string::npos; +} + +std::vector load_file_into_buffer(const char * const model_path) { + std::ifstream file_stream(model_path, std::ios::binary | std::ios::ate); + if (!file_stream) { + fprintf(stderr, "Failed to open file %s for reading into streambuf\n", model_path); + exit(EXIT_FAILURE); + } + + const size_t file_size = file_stream.tellg(); + file_stream.seekg(0, std::ios::beg); + + static_assert(sizeof(std::uint8_t) == sizeof(char), "uint8_t must be same size as char"); + std::vector buffer(file_size); + if (!file_stream.read((char *) buffer.data(), file_size)) { + fprintf(stderr, "Failed to read entire file into buffer\n"); + exit(EXIT_FAILURE); + } + + return buffer; +} + +std::unique_ptr> load_file_into_streambuf(const char * const model_path) { + return std::make_unique(load_file_into_buffer(model_path)); +} + +struct file_entry { + std::string path; + std::unique_ptr> streambuf; +}; + +std::vector load_files_into_streambuf(const char * const model_path) { + std::vector files; + + // Extract pattern from first file path + std::string path(model_path); + + // Split by '-' + std::vector parts; + std::stringstream ss(path); + std::string item; + while (std::getline(ss, item, '-')) { + parts.push_back(item); + } + + // Split the last part by '.' + std::string last_part = parts.back(); + parts.pop_back(); + size_t dot_pos = last_part.find('.'); + if (dot_pos != std::string::npos) { + parts.push_back(last_part.substr(0, dot_pos)); + parts.push_back(last_part.substr(dot_pos + 1)); // extension + } else { + parts.push_back(last_part); + } + + // Check if we have enough parts + if (parts.size() < 4) { + fprintf(stderr, "Model path does not contain expected pattern\n"); + exit(EXIT_FAILURE); + } + + // Get total files from [-2] position (before the extension) + int total_files = std::stoi(parts[parts.size() - 2]); + + // Get base path by joining all parts except -start-of-end.gguf + std::string base_path; + for (size_t i = 0; i < parts.size() - 4; i++) { + if (i > 0) { + base_path += "-"; + } + base_path += parts[i]; + } + + for (int i = 1; i <= total_files; i++) { + char numbered_path[1024]; + snprintf(numbered_path, sizeof(numbered_path), "%s-%05d-of-%05d.gguf", base_path.c_str(), i, total_files); + + files.push_back({ numbered_path, load_file_into_streambuf(numbered_path) }); + } + + return files; +} + +file_entry load_tensor_list_file(const char * const model_path) { + std::string path(model_path); + + // Split by '-' + std::vector parts; + std::stringstream ss(path); + std::string item; + while (std::getline(ss, item, '-')) { + parts.push_back(item); + } + + // Split the last part by '.' + std::string last_part = parts.back(); + parts.pop_back(); + size_t dot_pos = last_part.find('.'); + if (dot_pos != std::string::npos) { + parts.push_back(last_part.substr(0, dot_pos)); + parts.push_back(last_part.substr(dot_pos + 1)); // extension + } else { + parts.push_back(last_part); + } + + // Check if we have enough parts + if (parts.size() < 4) { + fprintf(stderr, "Model path does not contain expected pattern\n"); + exit(EXIT_FAILURE); + } + + // Get base path by joining all parts except -start-of-end.gguf + std::string base_path; + for (size_t i = 0; i < parts.size() - 4; i++) { + if (i > 0) { + base_path += "-"; + } + base_path += parts[i]; + } + + // Construct tensor list file path + std::string tensor_list_path = base_path + ".tensors.txt"; + + printf("Loading tensor list file: %s\n", tensor_list_path.c_str()); + return { tensor_list_path, load_file_into_streambuf(tensor_list_path.c_str()) }; +} + +llama_model * load_model_from_memory_configuration(const char * model_path, llama_model_params & model_params) { + llama_model * model; + std::chrono::steady_clock::time_point load_start_time; + if (getenv("LLAMA_EXAMPLE_MEMORY_BUFFER")) { + std::vector buffer = load_file_into_buffer(model_path); + fprintf(stdout, "%s: loading model from memory buffer\n", __func__); + load_start_time = std::chrono::steady_clock::now(); + model = llama_model_load_from_buffer(std::move(buffer), model_params); + } else if (getenv("LLAMA_EXAMPLE_MEMORY_BUFFER_SPLIT")) { + file_entry tensor_list_file = load_tensor_list_file(model_path); + std::vector files = load_files_into_streambuf(model_path); + fprintf(stdout, "%s: loading model from %zu file streambufs\n", __func__, files.size()); + + std::vector file_paths; + for (const auto & file : files) { + printf("Found file %s with streambuf\n", file.path.c_str()); + file_paths.push_back(file.path.c_str()); + } + + load_start_time = std::chrono::steady_clock::now(); + const char * async_load_context = "test-model-load"; + std::thread fulfill_thread([&files, &tensor_list_file, &async_load_context]() { + const bool success = llama_model_load_fulfill_split_future( + tensor_list_file.path.c_str(), async_load_context, std::move(tensor_list_file.streambuf)); + printf("Fulfilling tensor list file %s: %s\n", tensor_list_file.path.c_str(), + success ? "success" : "failure"); + if (!success) { + exit(EXIT_FAILURE); + } + + for (auto & file : files) { + const bool success = llama_model_load_fulfill_split_future(file.path.c_str(), async_load_context, + std::move(file.streambuf)); + printf("Fulfilling file %s with streambuf: %s\n", file.path.c_str(), success ? "success" : "failure"); + if (!success) { + exit(EXIT_FAILURE); + } + } + }); + fprintf(stderr, "Loading model from splits\n"); + model = llama_model_load_from_split_futures(file_paths.data(), file_paths.size(), async_load_context, + tensor_list_file.path.c_str(), model_params); + fulfill_thread.join(); + } else if (getenv("LLAMA_EXAMPLE_FROM_FILE")) { + load_start_time = std::chrono::steady_clock::now(); + model = llama_model_load_from_file(model_path, model_params); + } else { + return nullptr; + } + + if (model == NULL) { + fprintf(stderr, "%s: error: unable to load model\n", __func__); + exit(1); + } + std::chrono::steady_clock::time_point load_end_time = std::chrono::steady_clock::now(); + std::chrono::duration load_duration = load_end_time - load_start_time; + fprintf(stdout, "%s: loading model took %f seconds\n", __func__, load_duration.count()); + return model; +} + +bool memory_configuration_env_is_set() { + return getenv("LLAMA_EXAMPLE_MEMORY_BUFFER") || getenv("LLAMA_EXAMPLE_MEMORY_BUFFER_SPLIT") || + getenv("LLAMA_EXAMPLE_FROM_FILE"); +} +} // namespace diff --git a/common_test/uint8-buff-stream-wrapper.h b/common_test/uint8-buff-stream-wrapper.h new file mode 100644 index 000000000000..3a03721b98c0 --- /dev/null +++ b/common_test/uint8-buff-stream-wrapper.h @@ -0,0 +1,5 @@ +#pragma once + +// Wrapper to include the specific header from src +#include "uint8-buff-stream.h" + diff --git a/convert_hf_to_gguf.py b/convert_hf_to_gguf.py index 8ddb6d04cd99..307866857c8a 100755 --- a/convert_hf_to_gguf.py +++ b/convert_hf_to_gguf.py @@ -3029,18 +3029,47 @@ def prepare_tensors(self): super().prepare_tensors() -@ModelBase.register("BitnetForCausalLM") +@ModelBase.register("BitnetForCausalLM", "BitNetForCausalLM") class BitnetModel(TextModel): model_arch = gguf.MODEL_ARCH.BITNET + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._bitnet_weight_scales: dict[str, torch.Tensor] = {} + def set_vocab(self): - self._set_vocab_sentencepiece() + if (self.dir_model / "tokenizer.model").is_file(): + self._set_vocab_sentencepiece() + else: + self._set_vocab_gpt2() def set_gguf_parameters(self): super().set_gguf_parameters() self.gguf_writer.add_rope_scaling_type(gguf.RopeScalingType.LINEAR) self.gguf_writer.add_rope_scaling_factor(1.0) + @staticmethod + def _unpack_bitnet_weights(packed: torch.Tensor) -> torch.Tensor: + if packed.dtype != torch.uint8: + raise ValueError(f"Expected packed BitNet weights to be torch.uint8, got {packed.dtype}") + + values_per_item = 4 + rows = packed.shape[0] + rest = packed.shape[1:] + + unpacked_chunks: list[torch.Tensor] = [] + mapping = torch.tensor([-1.0, 0.0, 1.0, 0.0], dtype=torch.float32, device=packed.device) + + for i in range(values_per_item): + chunk = (packed >> (2 * i)) & 0x03 + chunk = mapping[chunk.long()].reshape((rows, *rest)) + unpacked_chunks.append(chunk) + + if not unpacked_chunks: + raise ValueError("Failed to unpack BitNet weights: no chunks produced") + + return torch.cat(unpacked_chunks, dim=0) + def weight_quant(self, weight: Tensor) -> Tensor: dtype = weight.dtype weight = weight.float() @@ -3053,8 +3082,37 @@ def weight_quant(self, weight: Tensor) -> Tensor: return result.type(dtype) def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]: + weight_scale_suffix = ".weight_scale" + if name.endswith(weight_scale_suffix): + weight_name = name[:-len(weight_scale_suffix)] + ".weight" + mapped_weight_name = self.map_tensor_name(weight_name) + if isinstance(data_torch, LazyTorchTensor): + data_torch = LazyTorchTensor.to_eager(data_torch) + + scale_tensor = data_torch.to(torch.float32) + self._bitnet_weight_scales[mapped_weight_name] = scale_tensor + return [] + new_name = self.map_tensor_name(name) + ternary_weight = False + + if name.endswith(".weight"): + scale_tensor = self._bitnet_weight_scales.pop(new_name, None) + if scale_tensor is not None: + scale_tensor = scale_tensor.to(torch.float32) + if scale_tensor.numel() != 1: + raise ValueError(f"Expected scalar weight_scale for '{name}', got shape {tuple(scale_tensor.shape)}") + + if isinstance(data_torch, LazyTorchTensor): + data_torch = LazyTorchTensor.to_eager(data_torch) + + packed = data_torch.to(torch.uint8) + unpacked = self._unpack_bitnet_weights(packed) + scale_value = scale_tensor.reshape(-1)[0].item() + data_torch = unpacked * scale_value + ternary_weight = True + if any(self.match_model_tensor_name(new_name, key, bid) for key in [ gguf.MODEL_TENSOR.ATTN_Q, gguf.MODEL_TENSOR.ATTN_K, @@ -3063,7 +3121,7 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter gguf.MODEL_TENSOR.FFN_UP, gguf.MODEL_TENSOR.FFN_DOWN, gguf.MODEL_TENSOR.FFN_GATE, - ]): + ]) and not ternary_weight: # transform weight into 1/0/-1 (in fp32) data_torch = self.weight_quant(data_torch) diff --git a/docs/backend/hexagon/CMakeUserPresets.json b/docs/backend/hexagon/CMakeUserPresets.json index e0b19db0f5a2..8828ea4aa88a 100644 --- a/docs/backend/hexagon/CMakeUserPresets.json +++ b/docs/backend/hexagon/CMakeUserPresets.json @@ -1,4 +1,4 @@ -{ +{ "version": 4, "configurePresets": [ { diff --git a/examples/embedding/CMakeLists.txt b/examples/embedding/CMakeLists.txt index 809040307d2c..be3d7b17e157 100644 --- a/examples/embedding/CMakeLists.txt +++ b/examples/embedding/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET llama-embedding) add_executable(${TARGET} embedding.cpp) install(TARGETS ${TARGET} RUNTIME) -target_link_libraries(${TARGET} PRIVATE common llama ${CMAKE_THREAD_LIBS_INIT}) +target_link_libraries(${TARGET} PRIVATE common llama llama-common-test ${CMAKE_THREAD_LIBS_INIT}) target_compile_features(${TARGET} PRIVATE cxx_std_17) diff --git a/examples/embedding/embedding.cpp b/examples/embedding/embedding.cpp index fe91b308cdc0..8f7899aa2f5b 100644 --- a/examples/embedding/embedding.cpp +++ b/examples/embedding/embedding.cpp @@ -1,15 +1,25 @@ +#include +#include +#include +#include +#include +#include +#include +#include + #include "arg.h" #include "common.h" +#include "llama-cpp.h" #include "log.h" -#include "llama.h" - -#include -#include #if defined(_MSC_VER) #pragma warning(disable: 4244 4267) // possible loss of data #endif +#ifdef LLAMA_COMMON_TEST_HEADERS +#include "load_into_memory.h" +#endif + static std::vector split_lines(const std::string & s, const std::string & separator = "\n") { std::vector lines; size_t start = 0; @@ -131,7 +141,20 @@ int main(int argc, char ** argv) { llama_numa_init(params.numa); // load the model - common_init_result llama_init = common_init_from_params(params); + common_init_result llama_init; + +#ifdef LLAMA_COMMON_TEST_HEADERS + if (memory_configuration_env_is_set()) { + llama_model_params mparams = common_model_params_to_llama(params); + common_init_result iparams; + llama_model * model = load_model_from_memory_configuration(params.model.path.c_str(), mparams); + llama_init = common_init_from_model_and_params(model, std::move(iparams), params); + } else { + llama_init = common_init_from_params(params); + } +#else + llama_init = common_init_from_params(params); +#endif llama_model * model = llama_init.model.get(); llama_context * ctx = llama_init.context.get(); diff --git a/examples/llama.android/llama/src/main/cpp/CMakeLists.txt b/examples/llama.android/llama/src/main/cpp/CMakeLists.txt index 6119fe09b0cb..572eb9436dc3 100644 --- a/examples/llama.android/llama/src/main/cpp/CMakeLists.txt +++ b/examples/llama.android/llama/src/main/cpp/CMakeLists.txt @@ -21,6 +21,12 @@ project("llama-android") # Also provides "common" #FetchContent_MakeAvailable(llama) +set(LLAMA_ROOT_DIR "../../../../../..") + +# Set global include directories that will be used by all targets +include_directories(${LLAMA_ROOT_DIR}/include) +include_directories(${LLAMA_ROOT_DIR}/vendor) + # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. @@ -32,7 +38,7 @@ project("llama-android") # #load local llama.cpp -add_subdirectory(../../../../../../ build-llama) +add_subdirectory(${LLAMA_ROOT_DIR}/ build-llama) # In order to load a library into your app from Java/Kotlin, you must call # System.loadLibrary() and pass the name of the library defined here; @@ -51,3 +57,9 @@ target_link_libraries(${CMAKE_PROJECT_NAME} common android log) + +# Add include directories for llama.h and common.h +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE + ${LLAMA_ROOT_DIR}/include + ${LLAMA_ROOT_DIR}/src + ${LLAMA_ROOT_DIR}/common) diff --git a/examples/llama.swiftui/llama.cpp.swift/LibLlama.swift b/examples/llama.swiftui/llama.cpp.swift/LibLlama.swift index dc2bafc88b17..7d78eaaa10f2 100644 --- a/examples/llama.swiftui/llama.cpp.swift/LibLlama.swift +++ b/examples/llama.swiftui/llama.cpp.swift/LibLlama.swift @@ -5,6 +5,16 @@ enum LlamaError: Error { case couldNotInitializeContext } +struct LlamaRuntimeOptions { + var contextLength: Int32 + var nGpuLayers: Int32 + var seed: UInt32 + var temperature: Float + var topP: Float + var topK: Int32 + var flashAttention: Bool +} + func llama_batch_clear(_ batch: inout llama_batch) { batch.n_tokens = 0 } @@ -25,9 +35,10 @@ actor LlamaContext { private var model: OpaquePointer private var context: OpaquePointer private var vocab: OpaquePointer - private var sampling: UnsafeMutablePointer + private var sampling: UnsafeMutablePointer? private var batch: llama_batch private var tokens_list: [llama_token] + private var runtimeOptions: LlamaRuntimeOptions var is_done: Bool = false /// This variable is used to store temporarily invalid cchars @@ -38,34 +49,80 @@ actor LlamaContext { var n_decode: Int32 = 0 - init(model: OpaquePointer, context: OpaquePointer) { + init(model: OpaquePointer, context: OpaquePointer, options: LlamaRuntimeOptions) { self.model = model self.context = context self.tokens_list = [] - self.batch = llama_batch_init(512, 0, 1) + self.batch = llama_batch_init(max(Int32(512), options.contextLength), 0, 1) self.temporary_invalid_cchars = [] - let sparams = llama_sampler_chain_default_params() - self.sampling = llama_sampler_chain_init(sparams) - llama_sampler_chain_add(self.sampling, llama_sampler_init_temp(0.4)) - llama_sampler_chain_add(self.sampling, llama_sampler_init_dist(1234)) + self.runtimeOptions = options + self.n_len = options.contextLength vocab = llama_model_get_vocab(model) + + let chainParams = llama_sampler_chain_default_params() + let initialChain = llama_sampler_chain_init(chainParams) + + if options.topK > 0 { + llama_sampler_chain_add(initialChain, llama_sampler_init_top_k(options.topK)) + } + + let clampedTopP = max(0.0, min(Double(options.topP), 1.0)) + llama_sampler_chain_add(initialChain, llama_sampler_init_top_p(Float(clampedTopP), 1)) + + let clampedTemp = max(0.0, Double(options.temperature)) + llama_sampler_chain_add(initialChain, llama_sampler_init_temp(Float(clampedTemp))) + + let seed = options.seed == 0 ? UInt32.max : options.seed + llama_sampler_chain_add(initialChain, llama_sampler_init_dist(seed)) + + sampling = initialChain } deinit { - llama_sampler_free(sampling) + if let sampling { + llama_sampler_free(sampling) + } llama_batch_free(batch) llama_model_free(model) llama_free(context) llama_backend_free() } - static func create_context(path: String) throws -> LlamaContext { + private func rebuildSamplerChain() { + let chainParams = llama_sampler_chain_default_params() + let newChain = llama_sampler_chain_init(chainParams) + + if runtimeOptions.topK > 0 { + llama_sampler_chain_add(newChain, llama_sampler_init_top_k(runtimeOptions.topK)) + } + + let clampedTopP = max(0.0, min(runtimeOptions.topP, 1.0)) + llama_sampler_chain_add(newChain, llama_sampler_init_top_p(clampedTopP, 1)) + + let clampedTemp = max(0.0, Double(runtimeOptions.temperature)) + llama_sampler_chain_add(newChain, llama_sampler_init_temp(Float(clampedTemp))) + + let seed = runtimeOptions.seed == 0 ? UInt32.max : runtimeOptions.seed + llama_sampler_chain_add(newChain, llama_sampler_init_dist(seed)) + + if let sampling { + llama_sampler_free(sampling) + } + + sampling = newChain + } + + static func create_context(path: String, options: LlamaRuntimeOptions) throws -> LlamaContext { llama_backend_init() var model_params = llama_model_default_params() #if targetEnvironment(simulator) model_params.n_gpu_layers = 0 print("Running on simulator, force use n_gpu_layers = 0") +#else + if options.nGpuLayers >= 0 { + model_params.n_gpu_layers = options.nGpuLayers + } #endif let model = llama_model_load_from_file(path, model_params) guard let model else { @@ -77,9 +134,10 @@ actor LlamaContext { print("Using \(n_threads) threads") var ctx_params = llama_context_default_params() - ctx_params.n_ctx = 2048 + ctx_params.n_ctx = UInt32(options.contextLength) ctx_params.n_threads = Int32(n_threads) ctx_params.n_threads_batch = Int32(n_threads) + ctx_params.flash_attn_type = options.flashAttention ? LLAMA_FLASH_ATTN_TYPE_ENABLED : LLAMA_FLASH_ATTN_TYPE_DISABLED let context = llama_init_from_model(model, ctx_params) guard let context else { @@ -87,7 +145,13 @@ actor LlamaContext { throw LlamaError.couldNotInitializeContext } - return LlamaContext(model: model, context: context) + return LlamaContext(model: model, context: context, options: options) + } + + func updateSampler(options: LlamaRuntimeOptions) { + runtimeOptions = options + n_len = options.contextLength + rebuildSamplerChain() } func model_info() -> String { @@ -151,6 +215,10 @@ actor LlamaContext { func completion_loop() -> String { var new_token_id: llama_token = 0 + guard let sampling else { + return "" + } + new_token_id = llama_sampler_sample(sampling, context, batch.n_tokens - 1) if llama_vocab_is_eog(vocab, new_token_id) || n_cur == n_len { diff --git a/examples/llama.swiftui/llama.swiftui.xcodeproj/project.pbxproj b/examples/llama.swiftui/llama.swiftui.xcodeproj/project.pbxproj index 6f08fe220a9d..401a9371f293 100644 --- a/examples/llama.swiftui/llama.swiftui.xcodeproj/project.pbxproj +++ b/examples/llama.swiftui/llama.swiftui.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ DD84C9FD2D747FED007778EC /* llama.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD84C9FC2D747FED007778EC /* llama.xcframework */; }; DD84C9FE2D747FED007778EC /* llama.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DD84C9FC2D747FED007778EC /* llama.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F1FE20E22B465ECA00B45541 /* LoadCustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FE20E12B465EC900B45541 /* LoadCustomButton.swift */; }; + D1A2F0012E0C8E8B00A1B1C0 /* FinetuneBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = D1A2F0002E0C8E8B00A1B1C0 /* FinetuneBridge.mm */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -51,6 +52,9 @@ DD84C9FC2D747FED007778EC /* llama.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = llama.xcframework; path = "../../build-apple/llama.xcframework"; sourceTree = ""; }; DF2D2FE72B4A59BE00FCB72D /* llama.cpp */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = llama.cpp; path = ../..; sourceTree = ""; }; F1FE20E12B465EC900B45541 /* LoadCustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadCustomButton.swift; sourceTree = ""; }; + D1A2F0002E0C8E8B00A1B1C0 /* FinetuneBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FinetuneBridge.mm; sourceTree = ""; }; + D1A2F0022E0C8E8B00A1B1C0 /* FinetuneBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FinetuneBridge.h; sourceTree = ""; }; + D1A2F0032E0C8E8B00A1B1C0 /* llama_swiftui-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "llama_swiftui-Bridging-Header.h"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -93,6 +97,7 @@ 8A3F84102AC4BD85005E2EE8 /* Resources */, 8A9F7C4B2AC332DC008AE1EA /* Models */, 8A9F7C4A2AC332BF008AE1EA /* UI */, + D1A2F0062E0C8E8B00A1B1C0 /* Bridging */, 8A1C83762AC328BD0096AF73 /* llama_swiftuiApp.swift */, 8A1C837A2AC328BE0096AF73 /* Assets.xcassets */, ); @@ -109,19 +114,19 @@ name = Frameworks; sourceTree = ""; }; - 8A3F84102AC4BD85005E2EE8 /* Resources */ = { + 8A3F84112AC4BD8C005E2EE8 /* models */ = { isa = PBXGroup; children = ( - 8A3F84112AC4BD8C005E2EE8 /* models */, ); - path = Resources; + path = models; sourceTree = ""; }; - 8A3F84112AC4BD8C005E2EE8 /* models */ = { + 8A3F84102AC4BD85005E2EE8 /* Resources */ = { isa = PBXGroup; children = ( + 8A3F84112AC4BD8C005E2EE8 /* models */, ); - path = models; + path = Resources; sourceTree = ""; }; 8A907F312AC7134E006146EA /* llama.cpp.swift */ = { @@ -132,6 +137,16 @@ path = llama.cpp.swift; sourceTree = ""; }; + D1A2F0062E0C8E8B00A1B1C0 /* Bridging */ = { + isa = PBXGroup; + children = ( + D1A2F0022E0C8E8B00A1B1C0 /* FinetuneBridge.h */, + D1A2F0032E0C8E8B00A1B1C0 /* llama_swiftui-Bridging-Header.h */, + D1A2F0002E0C8E8B00A1B1C0 /* FinetuneBridge.mm */, + ); + path = Bridging; + sourceTree = ""; + }; 8A9F7C4A2AC332BF008AE1EA /* UI */ = { isa = PBXGroup; children = ( @@ -230,6 +245,7 @@ F1FE20E22B465ECA00B45541 /* LoadCustomButton.swift in Sources */, 8A907F332AC7138A006146EA /* LibLlama.swift in Sources */, 8A9F7C4D2AC332EE008AE1EA /* LlamaState.swift in Sources */, + D1A2F0012E0C8E8B00A1B1C0 /* FinetuneBridge.mm in Sources */, 8A1C83792AC328BD0096AF73 /* ContentView.swift in Sources */, 8A1C83772AC328BD0096AF73 /* llama_swiftuiApp.swift in Sources */, 7FA3D2B32B2EA2F600543F92 /* DownloadButton.swift in Sources */, @@ -364,9 +380,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = K5UQJPP73A; + DEVELOPMENT_TEAM = 3LT8Z8ZRCG; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -380,14 +397,16 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.bachittle.llama-swift"; + PRODUCT_BUNDLE_IDENTIFIER = "llama-collabora"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; + SWIFT_OBJC_BRIDGING_HEADER = "llama.swiftui/Bridging/llama_swiftui-Bridging-Header.h"; }; name = Debug; }; @@ -396,9 +415,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = K5UQJPP73A; + DEVELOPMENT_TEAM = 3LT8Z8ZRCG; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -412,13 +432,15 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.bachittle.llama-swift"; + PRODUCT_BUNDLE_IDENTIFIER = "llama-collabora"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; + SWIFT_OBJC_BRIDGING_HEADER = "llama.swiftui/Bridging/llama_swiftui-Bridging-Header.h"; }; name = Release; }; diff --git a/examples/llama.swiftui/llama.swiftui/Bridging/FinetuneBridge.h b/examples/llama.swiftui/llama.swiftui/Bridging/FinetuneBridge.h new file mode 100644 index 000000000000..ff0d469e58bb --- /dev/null +++ b/examples/llama.swiftui/llama.swiftui/Bridging/FinetuneBridge.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Error codes returned by llama_swift_run_lora_finetune +enum llama_swift_finetune_error { + LLAMA_SWIFT_FINETUNE_OK = 0, + LLAMA_SWIFT_FINETUNE_ERROR_INVALID_ARGUMENT = 1, + LLAMA_SWIFT_FINETUNE_ERROR_MODEL_LOAD = 2, + LLAMA_SWIFT_FINETUNE_ERROR_CONTEXT_CREATE = 3, + LLAMA_SWIFT_FINETUNE_ERROR_DATASET = 4, + LLAMA_SWIFT_FINETUNE_ERROR_TRAINING_INIT = 5, + LLAMA_SWIFT_FINETUNE_ERROR_SAVE = 6, +}; + +struct llama_swift_finetune_options { + int32_t n_ctx; + int32_t n_threads; + int32_t n_batch; + int32_t n_ubatch; + int32_t epochs; + int32_t lora_rank; + float lora_alpha; + float learning_rate; + float val_split; + uint32_t target_modules; + int32_t seed; + bool flash_attn; + int32_t n_gpu_layers; + + int32_t checkpoint_save_steps; + const char* checkpoint_save_dir; + const char* resume_from_checkpoint; + bool auto_resume; +}; + +typedef void (*llama_swift_finetune_log_callback)(const char * message, void * user_data); + +enum llama_swift_finetune_error llama_swift_run_lora_finetune( + const char * model_path, + const char * dataset_path, + const char * output_adapter_path, + const struct llama_swift_finetune_options * options, + llama_swift_finetune_log_callback logger, + void * user_data); + +#ifdef __cplusplus +} +#endif diff --git a/examples/llama.swiftui/llama.swiftui/Bridging/FinetuneBridge.mm b/examples/llama.swiftui/llama.swiftui/Bridging/FinetuneBridge.mm new file mode 100644 index 000000000000..ee15350adff4 --- /dev/null +++ b/examples/llama.swiftui/llama.swiftui/Bridging/FinetuneBridge.mm @@ -0,0 +1,861 @@ +#import "FinetuneBridge.h" + +#import +#import +#import +#import + +#if defined(__APPLE__) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE +constexpr bool kIsAppleMobileGPU = true; +#else +constexpr bool kIsAppleMobileGPU = false; +#endif + +struct Hms { + int64_t hours; + int64_t minutes; + int64_t seconds; +}; + +Hms microseconds_to_hms(int64_t microseconds) { + if (microseconds < 0) { + microseconds = 0; + } + + int64_t total_seconds = microseconds / 1000000; + Hms result{}; + result.hours = total_seconds / 3600; + total_seconds -= result.hours * 3600; + result.minutes = total_seconds / 60; + total_seconds -= result.minutes * 60; + result.seconds = total_seconds; + return result; +} + +struct Logger { + llama_swift_finetune_log_callback callback; + void * user; + + void log(const std::string & message) const { + if (callback) { + callback(message.c_str(), user); + } + } + + void logf(const char * fmt, ...) const { + if (!callback) { + return; + } + + va_list args; + va_start(args, fmt); + std::array stack_buffer; + int written = vsnprintf(stack_buffer.data(), stack_buffer.size(), fmt, args); + va_end(args); + + if (written < 0) { + return; + } + + if (written < static_cast(stack_buffer.size())) { + callback(stack_buffer.data(), user); + return; + } + + std::vector heap_buffer(static_cast(written + 1)); + va_start(args, fmt); + vsnprintf(heap_buffer.data(), heap_buffer.size(), fmt, args); + va_end(args); + + callback(heap_buffer.data(), user); + } +}; + +struct checkpoint_metadata { + int32_t epoch; + int32_t lora_rank; + float lora_alpha; + uint32_t target_modules; +}; + +struct checkpoint_callback_data { + llama_context* ctx; + llama_adapter_lora* adapter; + int32_t checkpoint_save_steps; + std::string checkpoint_save_dir; + int64_t global_step; + int64_t initial_step; + int32_t current_epoch; + int32_t lora_rank; + float lora_alpha; + uint32_t target_modules; + const Logger* logger; +}; + +static checkpoint_callback_data* g_checkpoint_data = nullptr; + +static std::string get_checkpoint_filename(const std::string& checkpoint_dir, int64_t step) { + std::ostringstream oss; + oss << checkpoint_dir << "/checkpoint_step_" << std::setfill('0') << std::setw(8) << step; + return oss.str(); +} + +static std::string find_latest_checkpoint(const std::string& checkpoint_dir) { + if (!std::filesystem::exists(checkpoint_dir)) { + return ""; + } + + std::string latest_checkpoint; + int64_t latest_step = -1; + + for (const auto& entry : std::filesystem::directory_iterator(checkpoint_dir)) { + if (entry.is_directory()) { + std::string dirname = entry.path().filename().string(); + if (dirname.find("checkpoint_step_") == 0 && dirname.size() >= 16) { + std::string step_str = dirname.substr(16, 8); + try { + int64_t step = std::stoll(step_str); + if (step > latest_step) { + latest_step = step; + latest_checkpoint = entry.path().string(); + } + } catch (const std::exception&) { + continue; + } + } + } + } + + return latest_checkpoint; +} + +static bool save_checkpoint( + llama_context* ctx, + llama_adapter_lora* adapter, + const checkpoint_metadata& meta, + const std::string& checkpoint_dir, + const Logger& logger) { + + try { + std::filesystem::create_directories(checkpoint_dir); + } catch (const std::exception& e) { + logger.logf("Failed to create checkpoint directory: %s\n", e.what()); + return false; + } + + std::string adapter_path = checkpoint_dir + "/model.gguf"; + if (!llama_lora_save_adapter(adapter, adapter_path.c_str(), llama_get_model(ctx))) { + logger.logf("Failed to save LoRA adapter to %s\n", adapter_path.c_str()); + return false; + } + + std::string optimizer_path = checkpoint_dir + "/optimizer.gguf"; + if (!llama_opt_save_state(ctx, optimizer_path.c_str())) { + logger.logf("Failed to save optimizer state to %s\n", optimizer_path.c_str()); + return false; + } + + std::string meta_path = checkpoint_dir + "/metadata.txt"; + std::ofstream meta_file(meta_path); + if (meta_file.is_open()) { + meta_file << "epoch=" << meta.epoch << "\n"; + meta_file << "lora_rank=" << meta.lora_rank << "\n"; + meta_file << "lora_alpha=" << meta.lora_alpha << "\n"; + meta_file << "target_modules=" << meta.target_modules << "\n"; + meta_file.close(); + } else { + logger.logf("Failed to save checkpoint metadata to %s\n", meta_path.c_str()); + return false; + } + + logger.logf("Checkpoint saved to %s\n", checkpoint_dir.c_str()); + return true; +} + +static bool load_checkpoint_metadata(const std::string& checkpoint_dir, checkpoint_metadata& metadata, const Logger& logger) { + std::string meta_path = checkpoint_dir + "/metadata.txt"; + + if (std::filesystem::exists(meta_path)) { + std::ifstream meta_file(meta_path); + if (meta_file.is_open()) { + std::string line; + while (std::getline(meta_file, line)) { + size_t eq_pos = line.find('='); + if (eq_pos != std::string::npos) { + std::string key = line.substr(0, eq_pos); + std::string value = line.substr(eq_pos + 1); + + if (key == "epoch") { + metadata.epoch = std::stoi(value); + } else if (key == "lora_rank") { + metadata.lora_rank = std::stoi(value); + } else if (key == "lora_alpha") { + metadata.lora_alpha = std::stof(value); + } else if (key == "target_modules") { + metadata.target_modules = std::stoul(value); + } + } + } + meta_file.close(); + logger.logf("Checkpoint metadata loaded successfully\n"); + return true; + } else { + logger.logf("Failed to open checkpoint metadata file\n"); + return false; + } + } else { + logger.logf("Checkpoint metadata file not found: %s\n", meta_path.c_str()); + return false; + } +} + +std::optional read_file(const char * path, std::string & error_message) { + std::ifstream stream(path, std::ios::binary); + if (!stream) { + error_message = "Unable to open dataset file"; + return std::nullopt; + } + + std::ostringstream buffer; + buffer << stream.rdbuf(); + if (stream.fail() && !stream.eof()) { + error_message = "Failed reading dataset file"; + return std::nullopt; + } + + return buffer.str(); +} + +bool tokenize_dataset(const llama_model * model, const std::string & text, std::vector & tokens, std::string & error_message) { + const llama_vocab * vocab = llama_model_get_vocab(model); + if (!vocab) { + error_message = "Model vocabulary unavailable"; + return false; + } + + const int32_t text_len = static_cast(text.size()); + int32_t capacity = text_len + 8; + capacity = std::max(capacity, 8); + + tokens.resize(capacity); + int32_t n_tokens = llama_tokenize(vocab, text.c_str(), text_len, tokens.data(), static_cast(tokens.size()), /*add_bos=*/true, /*parse_special=*/false); + if (n_tokens == std::numeric_limits::min()) { + error_message = "Dataset too large to tokenize"; + return false; + } + if (n_tokens < 0) { + tokens.resize(static_cast(-n_tokens)); + n_tokens = llama_tokenize(vocab, text.c_str(), text_len, tokens.data(), static_cast(tokens.size()), /*add_bos=*/true, /*parse_special=*/false); + } else { + tokens.resize(static_cast(n_tokens)); + } + + if (n_tokens <= 0) { + error_message = "Dataset produced no tokens"; + return false; + } + + return true; +} + +int32_t default_thread_count() { + unsigned int hw = std::thread::hardware_concurrency(); + if (hw == 0) { + return 4; + } + if (hw <= 2) { + return 1; + } + return static_cast(hw - 1); +} + +struct EpochLoggerState { + const Logger * logger = nullptr; + int32_t epoch_index = 0; + int32_t epoch_total = 0; + int64_t last_logged_train = -1; + int64_t last_logged_eval = -1; +}; + +static thread_local EpochLoggerState g_epoch_logger_state; + +struct EpochLoggerScope { + EpochLoggerScope(const Logger & logger, int32_t epoch_index, int32_t epoch_total) { + g_epoch_logger_state.logger = &logger; + g_epoch_logger_state.epoch_index = epoch_index; + g_epoch_logger_state.epoch_total = epoch_total; + g_epoch_logger_state.last_logged_train = -1; + g_epoch_logger_state.last_logged_eval = -1; + } + + ~EpochLoggerScope() { + g_epoch_logger_state.logger = nullptr; + } +}; + +static void finetune_epoch_progress_callback( + bool train, + ggml_opt_context_t opt_ctx, + ggml_opt_dataset_t, + ggml_opt_result_t result, + int64_t ibatch, + int64_t ibatch_max, + int64_t t_start_us) { + + EpochLoggerState & state = g_epoch_logger_state; + if (!state.logger || ibatch_max <= 0) { + return; + } + + int64_t & last_logged = train ? state.last_logged_train : state.last_logged_eval; + if (ibatch <= last_logged) { + return; + } + last_logged = ibatch; + + const int64_t total_batches = std::max(ibatch_max, int64_t(1)); + const int64_t clamped_batch = std::clamp(ibatch, int64_t(1), total_batches); + + const ggml_tensor * input_tensor = ggml_opt_inputs(opt_ctx); + const int64_t batch_size = input_tensor ? input_tensor->ne[1] : 0; + const int64_t data_processed = batch_size > 0 ? clamped_batch * batch_size : clamped_batch; + const int64_t data_total = batch_size > 0 ? total_batches * batch_size : total_batches; + + double loss = 0.0; + double loss_unc = 0.0; + ggml_opt_result_loss(result, &loss, &loss_unc); + if (!std::isfinite(loss)) { + loss = 0.0; + } + if (!std::isfinite(loss_unc)) { + loss_unc = 0.0; + } + + double accuracy = 0.0; + double accuracy_unc = 0.0; + ggml_opt_result_accuracy(result, &accuracy, &accuracy_unc); + if (!std::isfinite(accuracy)) { + accuracy = 0.0; + } + if (!std::isfinite(accuracy_unc)) { + accuracy_unc = 0.0; + } + + int64_t elapsed_us = 0; + if (t_start_us > 0) { + const int64_t now_us = ggml_time_us(); + if (now_us >= t_start_us) { + elapsed_us = now_us - t_start_us; + } + } + + int64_t eta_us = 0; + if (clamped_batch > 0 && total_batches > clamped_batch) { + eta_us = (elapsed_us * (total_batches - clamped_batch)) / clamped_batch; + } + + const Hms elapsed_hms = microseconds_to_hms(elapsed_us); + const Hms eta_hms = microseconds_to_hms(eta_us); + + const double progress = (static_cast(clamped_batch) * 100.0) / static_cast(total_batches); + + state.logger->logf( + " [epoch %d/%d][%s] step %lld/%lld data=%lld/%lld loss=%.6f±%.6f acc=%.2f±%.2f%% " + "t=%02lld:%02lld:%02lld ETA=%02lld:%02lld:%02lld (%.1f%%)\n", + state.epoch_index + 1, + state.epoch_total, + train ? "train" : "eval", + static_cast(clamped_batch), + static_cast(total_batches), + static_cast(data_processed), + static_cast(data_total), + loss, + loss_unc, + 100.0 * accuracy, + 100.0 * accuracy_unc, + static_cast(elapsed_hms.hours), + static_cast(elapsed_hms.minutes), + static_cast(elapsed_hms.seconds), + static_cast(eta_hms.hours), + static_cast(eta_hms.minutes), + static_cast(eta_hms.seconds), + progress); +} + +static void checkpoint_progress_callback( + bool train, + ggml_opt_context_t opt_ctx, + ggml_opt_dataset_t dataset, + ggml_opt_result_t result, + int64_t ibatch, + int64_t ibatch_max, + int64_t t_start_us) { + + finetune_epoch_progress_callback(train, opt_ctx, dataset, result, ibatch, ibatch_max, t_start_us); + + if (!train) { + return; + } + + checkpoint_callback_data* cb_data = g_checkpoint_data; + if (!cb_data) { + return; + } + + if (cb_data->checkpoint_save_steps <= 0) { + return; + } + + cb_data->global_step++; + + if (cb_data->global_step % cb_data->checkpoint_save_steps == 0) { + if (!cb_data->ctx) { + if (cb_data->logger) { + cb_data->logger->logf("ERROR: Context is null in checkpoint callback!\n"); + } + return; + } + + if (!cb_data->adapter) { + if (cb_data->logger) { + cb_data->logger->logf("ERROR: LoRA adapter is null in checkpoint callback!\n"); + } + return; + } + + checkpoint_metadata meta = { + /*epoch =*/ cb_data->current_epoch, + /*lora_rank =*/ cb_data->lora_rank, + /*lora_alpha =*/ cb_data->lora_alpha, + /*target_modules =*/ cb_data->target_modules, + }; + + std::string checkpoint_path = get_checkpoint_filename(cb_data->checkpoint_save_dir, cb_data->global_step); + + if (cb_data->logger) { + cb_data->logger->logf("Saving checkpoint at step %lld to %s\n", + (long long)cb_data->global_step, checkpoint_path.c_str()); + } + + if (!save_checkpoint(cb_data->ctx, cb_data->adapter, meta, checkpoint_path, *cb_data->logger)) { + if (cb_data->logger) { + cb_data->logger->logf("ERROR: Failed to save checkpoint at step %lld\n", + (long long)cb_data->global_step); + } + } + } +} + +} // namespace + +extern "C" enum llama_swift_finetune_error llama_swift_run_lora_finetune( + const char * model_path, + const char * dataset_path, + const char * output_adapter_path, + const struct llama_swift_finetune_options * options, + llama_swift_finetune_log_callback logger_callback, + void * user_data) { + + Logger logger{logger_callback, user_data}; + + if (!model_path || !dataset_path || !output_adapter_path) { + logger.log("Invalid arguments supplied to finetune request\n"); + return LLAMA_SWIFT_FINETUNE_ERROR_INVALID_ARGUMENT; + } + + llama_swift_finetune_options opts{}; + if (options) { + opts = *options; + } + + if (opts.n_ctx <= 0) { + opts.n_ctx = 256; + } + if (opts.n_threads <= 0) { + opts.n_threads = default_thread_count(); + } + if (opts.n_batch <= 0) { + opts.n_batch = 256; + } + if (opts.n_ubatch <= 0) { + opts.n_ubatch = opts.n_batch; + } + if (opts.epochs <= 0) { + opts.epochs = 2; + } + if (opts.lora_rank <= 0) { + opts.lora_rank = 8; + } + if (opts.lora_alpha <= 0.0f) { + opts.lora_alpha = 16.0f; + } + if (opts.learning_rate <= 0.0f) { + opts.learning_rate = 1e-5f; + } + if (opts.val_split < 0.0f) { + opts.val_split = 0.0f; + } + if (opts.val_split >= 1.0f) { + opts.val_split = 0.0f; + } + if (opts.target_modules == 0) { + opts.target_modules = LLAMA_LORA_TARGET_ATTN_Q | + LLAMA_LORA_TARGET_ATTN_K | + LLAMA_LORA_TARGET_ATTN_V | + LLAMA_LORA_TARGET_ATTN_O; + } + if (opts.n_gpu_layers < 0) { + const char * env_ngl = std::getenv("LLAMA_SWIFT_FINETUNE_NGL"); + if (env_ngl && *env_ngl) { + char * endptr = nullptr; + long parsed = std::strtol(env_ngl, &endptr, 10); + if (endptr != env_ngl && parsed >= 0 && parsed <= std::numeric_limits::max()) { + opts.n_gpu_layers = static_cast(parsed); + logger.logf("Environment override: using n_gpu_layers=%d\n", opts.n_gpu_layers); + } else { + logger.logf("Ignoring invalid LLAMA_SWIFT_FINETUNE_NGL value '%s'\n", env_ngl); + opts.n_gpu_layers = 999; + } + } else { + opts.n_gpu_layers = 999; + } + } + + // opts.n_gpu_layers = 0; + + const bool gpu_available = opts.n_gpu_layers > 0 && llama_supports_gpu_offload(); + if (gpu_available) { + const int32_t max_gpu_batch = kIsAppleMobileGPU ? 64 : 256; + const int32_t max_gpu_ubatch = kIsAppleMobileGPU ? 32 : 128; + + if (opts.n_batch > max_gpu_batch) { + logger.logf("Clamping batch size from %d to %d for on-device GPU stability\n", opts.n_batch, max_gpu_batch); + opts.n_batch = max_gpu_batch; + } + + if (opts.n_ubatch > max_gpu_ubatch) { + logger.logf("Clamping micro-batch size from %d to %d for on-device GPU stability\n", opts.n_ubatch, max_gpu_ubatch); + opts.n_ubatch = max_gpu_ubatch; + } + + if (opts.n_ubatch > opts.n_batch) { + logger.logf("Adjusting micro-batch size to not exceed batch size (%d -> %d)\n", opts.n_ubatch, opts.n_batch); + opts.n_ubatch = opts.n_batch; + } + } + + llama_backend_init(); + + llama_model_params model_params = llama_model_default_params(); + model_params.n_gpu_layers = opts.n_gpu_layers; + model_params.use_mmap = false; + model_params.use_mlock = false; + + logger.logf("Loading model from %s\n", model_path); + llama_model * model = llama_model_load_from_file(model_path, model_params); + if (!model) { + logger.log("Failed to load model for finetuning\n"); + return LLAMA_SWIFT_FINETUNE_ERROR_MODEL_LOAD; + } + + llama_context_params ctx_params = llama_context_default_params(); + ctx_params.n_ctx = opts.n_ctx; + ctx_params.n_batch = opts.n_batch; + ctx_params.n_ubatch = opts.n_ubatch; + ctx_params.n_threads = opts.n_threads; + ctx_params.n_threads_batch = opts.n_threads; + ctx_params.flash_attn_type = opts.flash_attn ? LLAMA_FLASH_ATTN_TYPE_ENABLED : LLAMA_FLASH_ATTN_TYPE_DISABLED; + + if (opts.seed != 0) { + logger.log("Warning: custom RNG seed requested but not supported by bundled llama.framework.\n"); + } + + logger.logf("Creating training context (n_ctx=%d, n_threads=%d, batch=%d, ubatch=%d, ngl=%d)\n", + ctx_params.n_ctx, ctx_params.n_threads, ctx_params.n_batch, ctx_params.n_ubatch, opts.n_gpu_layers); + + llama_context * ctx = llama_init_from_model(model, ctx_params); + if (!ctx) { + logger.log("Failed to create llama context for finetuning\n"); + llama_model_free(model); + return LLAMA_SWIFT_FINETUNE_ERROR_CONTEXT_CREATE; + } + + std::string error_message; + auto dataset_text = read_file(dataset_path, error_message); + if (!dataset_text.has_value()) { + logger.logf("%s: %s\n", error_message.c_str(), dataset_path); + llama_free(ctx); + llama_model_free(model); + return LLAMA_SWIFT_FINETUNE_ERROR_DATASET; + } + + std::vector tokens; + if (!tokenize_dataset(model, dataset_text.value(), tokens, error_message)) { + logger.logf("Tokenization failed: %s\n", error_message.c_str()); + llama_free(ctx); + llama_model_free(model); + return LLAMA_SWIFT_FINETUNE_ERROR_DATASET; + } + + const int64_t ne_datapoint = llama_n_ctx(ctx); + const int64_t stride = std::max(1, ne_datapoint / 2); + if (tokens.size() < static_cast(ne_datapoint + 1)) { + logger.log("Dataset is too short for the selected context size\n"); + llama_free(ctx); + llama_model_free(model); + return LLAMA_SWIFT_FINETUNE_ERROR_DATASET; + } + + const int64_t ndata = (static_cast(tokens.size()) - ne_datapoint - 1) / stride; + if (ndata <= 0) { + logger.log("Tokenized dataset produced no training samples\n"); + llama_free(ctx); + llama_model_free(model); + return LLAMA_SWIFT_FINETUNE_ERROR_DATASET; + } + + ggml_opt_dataset_t dataset = ggml_opt_dataset_init( + GGML_TYPE_I32, + GGML_TYPE_I32, + ne_datapoint, + ne_datapoint, + ndata, + /*ndata_shard=*/1); + + llama_token * data_ptr = reinterpret_cast(ggml_opt_dataset_data(dataset)->data); + llama_token * labels_ptr = reinterpret_cast(ggml_opt_dataset_labels(dataset)->data); + + for (int64_t i = 0; i < ndata; ++i) { + std::memcpy(data_ptr + i * ne_datapoint, tokens.data() + i * stride, static_cast(ne_datapoint) * sizeof(llama_token)); + std::memcpy(labels_ptr + i * ne_datapoint, tokens.data() + i * stride + 1, static_cast(ne_datapoint) * sizeof(llama_token)); + } + + logger.logf("Prepared dataset with %lld sequences (stride=%lld)\n", static_cast(ndata), static_cast(stride)); + + llama_lora_training_params lora_params{ + /*target_modules =*/ opts.target_modules, + /*rank =*/ opts.lora_rank, + /*alpha =*/ opts.lora_alpha, + /*dropout =*/ 0.0f, + /*init_std =*/ 0.02f, + }; + + llama_adapter_lora * adapter = llama_lora_training_init(ctx, model, &lora_params); + if (!adapter) { + logger.log("Failed to initialize LoRA training\n"); + ggml_opt_dataset_free(dataset); + llama_free(ctx); + llama_model_free(model); + return LLAMA_SWIFT_FINETUNE_ERROR_TRAINING_INIT; + } + + bool checkpoint_loaded = false; + int32_t start_epoch = 0; + int64_t start_step = 0; + std::string checkpoint_dir = opts.checkpoint_save_dir ? opts.checkpoint_save_dir : "./checkpoints"; + std::string resume_from_checkpoint; + + if (opts.auto_resume) { + logger.logf("Auto-resume enabled, searching for latest checkpoint in %s\n", checkpoint_dir.c_str()); + resume_from_checkpoint = find_latest_checkpoint(checkpoint_dir); + if (!resume_from_checkpoint.empty()) { + logger.logf("Found latest checkpoint: %s\n", resume_from_checkpoint.c_str()); + } else { + logger.logf("No existing checkpoint found, starting from scratch\n"); + } + } else if (opts.resume_from_checkpoint && opts.resume_from_checkpoint[0] != '\0') { + resume_from_checkpoint = opts.resume_from_checkpoint; + logger.logf("Resuming from specified checkpoint: %s\n", resume_from_checkpoint.c_str()); + } + + if (!resume_from_checkpoint.empty()) { + checkpoint_metadata checkpoint_meta{}; + if (load_checkpoint_metadata(resume_from_checkpoint, checkpoint_meta, logger)) { + if (checkpoint_meta.lora_rank != opts.lora_rank) { + logger.logf("Warning: Checkpoint LoRA rank (%d) doesn't match current rank (%d)\n", + checkpoint_meta.lora_rank, opts.lora_rank); + } + if (checkpoint_meta.lora_alpha != opts.lora_alpha) { + logger.logf("Warning: Checkpoint LoRA alpha (%.3f) doesn't match current alpha (%.3f)\n", + checkpoint_meta.lora_alpha, opts.lora_alpha); + } + if (checkpoint_meta.target_modules != opts.target_modules) { + logger.logf("Warning: Checkpoint target_modules doesn't match current target_modules\n"); + } + + start_epoch = checkpoint_meta.epoch; + checkpoint_loaded = true; + } else { + logger.logf("Failed to load checkpoint metadata, starting from scratch\n"); + } + } + + std::string optimizer_checkpoint_path; + if (checkpoint_loaded && !resume_from_checkpoint.empty()) { + optimizer_checkpoint_path = resume_from_checkpoint + "/optimizer.gguf"; + } + + ggml_opt_optimizer_params optimizer_params = ggml_opt_get_default_optimizer_params(nullptr); + optimizer_params.adamw.alpha = opts.learning_rate; + + llama_opt_params opt_params{ + /*n_ctx_train =*/ 0, + /*param_filter =*/ llama_opt_param_filter_lora, + /*param_filter_ud =*/ nullptr, + /*get_opt_pars =*/ ggml_opt_get_constant_optimizer_params, + /*get_opt_pars_ud =*/ &optimizer_params, + /*optimizer_type =*/ GGML_OPT_OPTIMIZER_TYPE_ADAMW, + /*checkpoint_path =*/ checkpoint_loaded ? optimizer_checkpoint_path.c_str() : nullptr, + /*load_optimizer_state =*/ checkpoint_loaded, + /*assistant_loss_only =*/ false, + }; + + llama_opt_init(ctx, model, opt_params); + + ggml_opt_result_t result_train = ggml_opt_result_init(); + ggml_opt_result_t result_eval = ggml_opt_result_init(); + + const int64_t total_samples = ggml_opt_dataset_ndata(dataset); + int64_t idata_split = total_samples; + if (opts.val_split > 0.0f && opts.val_split < 1.0f) { + const double train_fraction = std::clamp(1.0 - static_cast(opts.val_split), 0.0, 1.0); + int64_t proposed = static_cast(std::llround(train_fraction * static_cast(total_samples))); + proposed = std::clamp(proposed, 1, std::max(total_samples - 1, 1)); + idata_split = proposed; + } + + checkpoint_callback_data cb_data{}; + cb_data.ctx = ctx; + cb_data.adapter = adapter; + cb_data.checkpoint_save_steps = opts.checkpoint_save_steps; + cb_data.checkpoint_save_dir = checkpoint_dir; + cb_data.global_step = start_step; + cb_data.initial_step = start_step; + cb_data.current_epoch = start_epoch; + cb_data.lora_rank = opts.lora_rank; + cb_data.lora_alpha = opts.lora_alpha; + cb_data.target_modules = opts.target_modules; + cb_data.logger = &logger; + + g_checkpoint_data = &cb_data; + + ggml_opt_epoch_callback train_callback = opts.checkpoint_save_steps > 0 + ? checkpoint_progress_callback + : finetune_epoch_progress_callback; + + ggml_opt_epoch_callback eval_callback = (idata_split < total_samples) + ? finetune_epoch_progress_callback + : nullptr; + + if (opts.checkpoint_save_steps > 0) { + logger.logf("Checkpointing enabled, saving every %d steps to %s\n", + opts.checkpoint_save_steps, checkpoint_dir.c_str()); + } else { + logger.logf("Checkpointing disabled\n"); + } + + logger.logf("Starting LoRA finetuning for %d epoch(s)\n", opts.epochs); + + for (int32_t epoch = start_epoch; epoch < opts.epochs; ++epoch) { + logger.logf("Epoch %d/%d\n", epoch + 1, opts.epochs); + EpochLoggerScope epoch_scope(logger, epoch, opts.epochs); + + cb_data.current_epoch = epoch; + + int64_t resume_batch = 0; + + llama_opt_epoch_resume(ctx, + dataset, + result_train, + result_eval, + idata_split, + train_callback, + eval_callback, + resume_batch); + + double train_loss = 0.0; + double train_unc = 0.0; + ggml_opt_result_loss(result_train, &train_loss, &train_unc); + ggml_opt_result_reset(result_train); + + if (idata_split < total_samples) { + double val_loss = 0.0; + double val_unc = 0.0; + ggml_opt_result_loss(result_eval, &val_loss, &val_unc); + ggml_opt_result_reset(result_eval); + logger.logf(" train loss: %.6f val loss: %.6f\n", train_loss, val_loss); + } else { + logger.logf(" train loss: %.6f\n", train_loss); + } + } + + g_checkpoint_data = nullptr; + + std::filesystem::path output_path(output_adapter_path); + if (output_path.has_parent_path()) { + std::error_code ec; + std::filesystem::create_directories(output_path.parent_path(), ec); + if (ec) { + logger.logf("Failed to create output directory: %s (%d)\n", ec.message().c_str(), static_cast(ec.value())); + ggml_opt_result_free(result_train); + ggml_opt_result_free(result_eval); + ggml_opt_dataset_free(dataset); + llama_adapter_lora_free(adapter); + llama_free(ctx); + llama_model_free(model); + return LLAMA_SWIFT_FINETUNE_ERROR_SAVE; + } + } + + logger.logf("Saving LoRA adapter to %s\n", output_adapter_path); + if (!llama_lora_save_adapter(adapter, output_adapter_path, model)) { + logger.log("Failed to save LoRA adapter\n"); + ggml_opt_result_free(result_train); + ggml_opt_result_free(result_eval); + ggml_opt_dataset_free(dataset); + llama_adapter_lora_free(adapter); + llama_free(ctx); + llama_model_free(model); + return LLAMA_SWIFT_FINETUNE_ERROR_SAVE; + } + + std::error_code size_ec; + const auto file_size = std::filesystem::file_size(output_path, size_ec); + if (!size_ec) { + logger.logf("Saved adapter size: %.2f MB\n", static_cast(file_size) / (1024.0 * 1024.0)); + } + + ggml_opt_result_free(result_train); + ggml_opt_result_free(result_eval); + ggml_opt_dataset_free(dataset); + llama_adapter_lora_free(adapter); + llama_free(ctx); + llama_model_free(model); + + logger.log("LoRA finetuning completed successfully\n"); + return LLAMA_SWIFT_FINETUNE_OK; +} diff --git a/examples/llama.swiftui/llama.swiftui/Bridging/llama_swiftui-Bridging-Header.h b/examples/llama.swiftui/llama.swiftui/Bridging/llama_swiftui-Bridging-Header.h new file mode 100644 index 000000000000..74ef699ec2e0 --- /dev/null +++ b/examples/llama.swiftui/llama.swiftui/Bridging/llama_swiftui-Bridging-Header.h @@ -0,0 +1 @@ +#import "FinetuneBridge.h" diff --git a/examples/llama.swiftui/llama.swiftui/Models/LlamaState.swift b/examples/llama.swiftui/llama.swiftui/Models/LlamaState.swift index b8f6a31d582c..111945eaf816 100644 --- a/examples/llama.swiftui/llama.swiftui/Models/LlamaState.swift +++ b/examples/llama.swiftui/llama.swiftui/Models/LlamaState.swift @@ -8,33 +8,235 @@ struct Model: Identifiable { var status: String? } +struct Dataset: Identifiable, Equatable { + var id = UUID() + var name: String + var url: String + var filename: String + var status: String? +} + @MainActor class LlamaState: ObservableObject { @Published var messageLog = "" @Published var cacheCleared = false @Published var downloadedModels: [Model] = [] @Published var undownloadedModels: [Model] = [] + @Published var selectedModelFilename: String? + @Published var isFinetuning = false + @Published var downloadedDatasets: [Dataset] = [] + @Published var availableDatasets: [Dataset] = [] + @Published var selectedDatasetFilename: String? + @Published var optionNGpuLayers: Int = -1 { + didSet { + let clamped = max(-1, min(optionNGpuLayers, 256)) + if optionNGpuLayers != clamped { + optionNGpuLayers = clamped + return + } + UserDefaults.standard.set(optionNGpuLayers, forKey: runtimeNglKey) + if optionNGpuLayers != oldValue { + reloadModelWithCurrentOptionsIfPossible() + } + } + } + + @Published var optionContextLength: Int = 2048 { + didSet { + let clamped = max(32, min(optionContextLength, 8192)) + if optionContextLength != clamped { + optionContextLength = clamped + return + } + UserDefaults.standard.set(optionContextLength, forKey: runtimeContextKey) + if optionContextLength != oldValue { + reloadModelWithCurrentOptionsIfPossible() + } + } + } + + @Published var optionSeed: Int = 42 { + didSet { + let clamped = max(0, min(optionSeed, 1_000_000)) + if optionSeed != clamped { + optionSeed = clamped + return + } + UserDefaults.standard.set(optionSeed, forKey: runtimeSeedKey) + if optionSeed != oldValue { + applySamplerUpdate() + } + } + } + + @Published var optionTemperature: Double = 0.0 { + didSet { + let clamped = min(max(optionTemperature, 0.0), 0.0) + if abs(optionTemperature - clamped) > 0.0001 { + optionTemperature = clamped + return + } + UserDefaults.standard.set(optionTemperature, forKey: runtimeTempKey) + if abs(optionTemperature - oldValue) > 0.0001 { + applySamplerUpdate() + } + } + } + + @Published var optionTopP: Double = 0.95 { + didSet { + let clamped = min(max(optionTopP, 0.01), 1.0) + if abs(optionTopP - clamped) > 0.0001 { + optionTopP = clamped + return + } + UserDefaults.standard.set(optionTopP, forKey: runtimeTopPKey) + if abs(optionTopP - oldValue) > 0.0001 { + applySamplerUpdate() + } + } + } + + @Published var optionTopK: Int = 40 { + didSet { + let clamped = max(1, min(optionTopK, 500)) + if optionTopK != clamped { + optionTopK = clamped + return + } + UserDefaults.standard.set(optionTopK, forKey: runtimeTopKKey) + if optionTopK != oldValue { + applySamplerUpdate() + } + } + } + + @Published var optionFlashAttention: Bool = false { + didSet { + UserDefaults.standard.set(optionFlashAttention, forKey: runtimeFlashAttnKey) + if optionFlashAttention != oldValue { + reloadModelWithCurrentOptionsIfPossible() + } + } + } + + @Published var optionSingleTurn: Bool = true { + didSet { + UserDefaults.standard.set(optionSingleTurn, forKey: runtimeSingleTurnKey) + } + } + let NS_PER_S = 1_000_000_000.0 private var llamaContext: LlamaContext? + private var currentModelURL: URL? + private let modelSelectionKey = "llama_swiftui_selected_model" + private let datasetSelectionKey = "llama_swiftui_selected_dataset" + private let allowedModelExtensions: Set = ["gguf", "ggml", "bin"] + private let runtimeNglKey = "llama_swiftui_runtime_ngl" + private let runtimeContextKey = "llama_swiftui_runtime_ctx" + private let runtimeSeedKey = "llama_swiftui_runtime_seed" + private let runtimeTempKey = "llama_swiftui_runtime_temp" + private let runtimeTopPKey = "llama_swiftui_runtime_top_p" + private let runtimeTopKKey = "llama_swiftui_runtime_top_k" + private let runtimeFlashAttnKey = "llama_swiftui_runtime_flash_attn" + private let runtimeSingleTurnKey = "llama_swiftui_runtime_single_turn" + + private typealias FinetuneLogCallback = @convention(c) (UnsafePointer?, UnsafeMutableRawPointer?) -> Void + + private static let finetuneLogCallback: FinetuneLogCallback = { messagePtr, userData in + guard let userData else { return } + let state = Unmanaged.fromOpaque(userData).takeUnretainedValue() + guard let messagePtr else { return } + let text = String(cString: messagePtr) + Task { @MainActor in + state.messageLog += text + } + } private var defaultModelUrl: URL? { Bundle.main.url(forResource: "ggml-model", withExtension: "gguf", subdirectory: "models") // Bundle.main.url(forResource: "llama-2-7b-chat", withExtension: "Q2_K.gguf", subdirectory: "models") } + private let finetuneDatasetFilename = "trump.txt" + private let defaultFinetuneDatasetURL = URL(string: "https://github.com/user-attachments/files/21859494/trump.txt") + private let rockTweetsDatasetFilename = "the-rock-tweets.txt" + private let rockTweetsDatasetURL = URL(string: "https://gist.githubusercontent.com/zoq/774ab751bb3599835362bd6b5b604044/raw/a0d58f5d4bcd46621f9a3112c6fee2c3142fc8a0/the-rock-tweets.txt") + + private lazy var defaultDatasets: [Dataset] = { + var defaults: [Dataset] = [] + if let url = defaultFinetuneDatasetURL { + defaults.append(Dataset(name: "Sample Trump Interview", url: url.absoluteString, filename: finetuneDatasetFilename, status: "download")) + } + if let rockTweetsURL = rockTweetsDatasetURL { + defaults.append(Dataset(name: "Sample The Rock Tweets", url: rockTweetsURL.absoluteString, filename: rockTweetsDatasetFilename, status: "download")) + } + return defaults + }() + + private let disallowedDatasetExtensions: Set = ["gguf", "ggml", "bin"] + init() { + let defaults = UserDefaults.standard + if let stored = defaults.object(forKey: runtimeNglKey) as? Int { + optionNGpuLayers = max(-1, min(stored, 256)) + } + if let stored = defaults.object(forKey: runtimeContextKey) as? Int, stored >= 32 { + optionContextLength = min(stored, 8192) + } + if let stored = defaults.object(forKey: runtimeSeedKey) as? Int, stored >= 0 { + optionSeed = min(stored, 1_000_000) + } + if let stored = defaults.object(forKey: runtimeTempKey) as? Double { + optionTemperature = min(max(stored, 0.0), 0.0) + } + if let stored = defaults.object(forKey: runtimeTopPKey) as? Double { + optionTopP = min(max(stored, 0.01), 1.0) + } + if let stored = defaults.object(forKey: runtimeTopKKey) as? Int, stored >= 1 { + optionTopK = min(stored, 500) + } + if let stored = defaults.object(forKey: runtimeFlashAttnKey) as? Bool { + optionFlashAttention = stored + } + if let stored = defaults.object(forKey: runtimeSingleTurnKey) as? Bool { + optionSingleTurn = stored + } + loadModelsFromDisk() loadDefaultModels() + selectedModelFilename = UserDefaults.standard.string(forKey: modelSelectionKey) + ensureSelectedModelIsValid() + loadDatasetsFromDisk() + loadDefaultDatasets() + selectedDatasetFilename = UserDefaults.standard.string(forKey: datasetSelectionKey) + ensureSelectedDatasetIsValid() } private func loadModelsFromDisk() { + downloadedModels.removeAll { model in + let fileURL = modelFileURL(for: model.filename) + return !FileManager.default.fileExists(atPath: fileURL.path) + } + do { let documentsURL = getDocumentsDirectory() - let modelURLs = try FileManager.default.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]) - for modelURL in modelURLs { - let modelName = modelURL.deletingPathExtension().lastPathComponent - downloadedModels.append(Model(name: modelName, url: "", filename: modelURL.lastPathComponent, status: "downloaded")) + let modelURLs = try FileManager.default.contentsOfDirectory( + at: documentsURL, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants] + ) + + for modelURL in modelURLs where !modelURL.hasDirectoryPath { + let ext = modelURL.pathExtension.lowercased() + guard allowedModelExtensions.contains(ext) else { continue } + let filename = modelURL.lastPathComponent + guard !downloadedModels.contains(where: { $0.filename == filename }) else { continue } + + let displayName = modelURL.deletingPathExtension().lastPathComponent + downloadedModels.append(Model(name: displayName, url: "", filename: filename, status: "downloaded")) } + downloadedModels.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } catch { print("Error loading models from disk: \(error)") } @@ -48,14 +250,217 @@ class LlamaState: ObservableObject { } for model in defaultModels { - let fileURL = getDocumentsDirectory().appendingPathComponent(model.filename) + let fileURL = modelFileURL(for: model.filename) if FileManager.default.fileExists(atPath: fileURL.path) { + continue + } + guard !undownloadedModels.contains(where: { $0.filename == model.filename }) else { continue } + + var undownloadedModel = model + undownloadedModel.status = "download" + undownloadedModels.append(undownloadedModel) + } + + undownloadedModels.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + } - } else { - var undownloadedModel = model - undownloadedModel.status = "download" - undownloadedModels.append(undownloadedModel) + private func ensureSelectedModelIsValid() { + guard let selectedFilename = selectedModelFilename else { return } + let fileURL = modelFileURL(for: selectedFilename) + let bundleMatch = defaultModelUrl?.lastPathComponent == selectedFilename + if FileManager.default.fileExists(atPath: fileURL.path) || bundleMatch { + return + } + updateSelectedModel(filename: nil, name: nil, announce: false) + } + + private func updateSelectedModel(filename: String?, name: String?, announce: Bool) { + if let filename { + selectedModelFilename = filename + UserDefaults.standard.set(filename, forKey: modelSelectionKey) + if announce { + let displayName = name ?? filename + messageLog += "Ready to use model \(displayName).\n" } + } else { + selectedModelFilename = nil + UserDefaults.standard.removeObject(forKey: modelSelectionKey) + } + } + + private func runtimeOptions() -> LlamaRuntimeOptions { + let context = Int32(optionContextLength) + let ngl = optionNGpuLayers >= 0 ? Int32(optionNGpuLayers) : -1 + let seedValue = optionSeed < 0 ? 0 : UInt32(optionSeed) + let temperature = Float(min(max(optionTemperature, 0.0), 0.0)) + let topP = Float(min(max(optionTopP, 0.01), 1.0)) + let topK = Int32(max(1, optionTopK)) + + return LlamaRuntimeOptions( + contextLength: context, + nGpuLayers: ngl, + seed: seedValue, + temperature: temperature, + topP: topP, + topK: topK, + flashAttention: optionFlashAttention + ) + } + + private func reloadModelWithCurrentOptionsIfPossible() { + guard !isFinetuning else { + messageLog += "Finetuning in progress; runtime option will apply after the current run completes.\n" + return + } + guard let currentModelURL else { return } + do { + try loadModel(modelUrl: currentModelURL) + } catch { + messageLog += "Failed to reload model with new options: \(error.localizedDescription).\n" + } + } + + private func applySamplerUpdate() { + guard let context = llamaContext else { return } + let options = runtimeOptions() + Task { + await context.updateSampler(options: options) + } + } + + func selectDownloadedModel(_ model: Model) { + let fileURL = modelFileURL(for: model.filename) + guard FileManager.default.fileExists(atPath: fileURL.path) else { + messageLog += "Model \(model.filename) no longer exists on disk.\n" + return + } + + do { + try loadModel(modelUrl: fileURL) + updateSelectedModel(filename: model.filename, name: model.name, announce: true) + } catch { + messageLog += "Failed to load model \(model.filename): \(error.localizedDescription).\n" + } + } + + func registerDownloadedModel(_ model: Model) { + var downloaded = model + downloaded.status = "downloaded" + + if let index = downloadedModels.firstIndex(where: { $0.filename == downloaded.filename }) { + downloadedModels[index] = downloaded + } else { + downloadedModels.append(downloaded) + } + + undownloadedModels.removeAll { $0.filename == downloaded.filename } + downloadedModels.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + messageLog += "Downloaded model \(downloaded.name) -> \(downloaded.filename).\n" + } + + func deleteModel(_ model: Model) { + let fileURL = modelFileURL(for: model.filename) + if FileManager.default.fileExists(atPath: fileURL.path) { + do { + try FileManager.default.removeItem(at: fileURL) + messageLog += "Deleted model \(model.filename).\n" + } catch { + messageLog += "Failed to delete model \(model.filename): \(error.localizedDescription).\n" + } + } + + downloadedModels.removeAll { $0.filename == model.filename } + + if !model.url.isEmpty && !undownloadedModels.contains(where: { $0.filename == model.filename }) { + var restorable = model + restorable.status = "download" + undownloadedModels.append(restorable) + undownloadedModels.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + } + + if selectedModelFilename == model.filename { + updateSelectedModel(filename: nil, name: nil, announce: false) + } + } + + func deleteDownloadedModels(at offsets: IndexSet) { + let targets = offsets.compactMap { index in + downloadedModels.indices.contains(index) ? downloadedModels[index] : nil + } + + for model in targets { + deleteModel(model) + } + } + + private func loadDatasetsFromDisk() { + downloadedDatasets.removeAll { !isDatasetFilename($0.filename) || isModelFilename($0.filename) } + + do { + let documentsURL = getDocumentsDirectory() + let datasetURLs = try FileManager.default.contentsOfDirectory( + at: documentsURL, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants] + ) + + for datasetURL in datasetURLs where !datasetURL.hasDirectoryPath { + if !isDatasetFile(datasetURL) || isModelFilename(datasetURL.lastPathComponent) { + continue + } + let rawName = datasetURL.deletingPathExtension().lastPathComponent + let displayName = (rawName.removingPercentEncoding ?? rawName) + .replacingOccurrences(of: "_", with: " ") + .replacingOccurrences(of: "-", with: " ") + let dataset = Dataset( + name: displayName.isEmpty ? datasetURL.lastPathComponent : displayName, + url: "", + filename: datasetURL.lastPathComponent, + status: "downloaded" + ) + + if !downloadedDatasets.contains(where: { $0.filename == dataset.filename }) { + downloadedDatasets.append(dataset) + } + } + downloadedDatasets.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + } catch { + print("Error loading datasets from disk: \(error)") + } + } + + private func loadDefaultDatasets() { + for dataset in defaultDatasets { + let fileURL = datasetFileURL(for: dataset.filename) + if FileManager.default.fileExists(atPath: fileURL.path) { + if !downloadedDatasets.contains(where: { $0.filename == dataset.filename }) { + var downloaded = dataset + downloaded.status = "downloaded" + downloadedDatasets.append(downloaded) + } + } else if !availableDatasets.contains(where: { $0.filename == dataset.filename }) { + availableDatasets.append(dataset) + } + } + + downloadedDatasets.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + availableDatasets.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + } + + private func ensureSelectedDatasetIsValid() { + if let selectedFilename = selectedDatasetFilename { + let selectedURL = datasetFileURL(for: selectedFilename) + if isDatasetFilename(selectedFilename) + && !isModelFilename(selectedFilename) + && FileManager.default.fileExists(atPath: selectedURL.path) { + return + } + } + + if let firstDataset = downloadedDatasets.first { + updateSelectedDataset(filename: firstDataset.filename, name: firstDataset.name, announce: false) + } else { + updateSelectedDataset(filename: nil, name: nil, announce: false) } } @@ -63,32 +468,139 @@ class LlamaState: ObservableObject { let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return paths[0] } + + func datasetFileURL(for filename: String) -> URL { + getDocumentsDirectory().appendingPathComponent(filename) + } + + func modelFileURL(for filename: String) -> URL { + getDocumentsDirectory().appendingPathComponent(filename) + } + + private func isDatasetFilename(_ filename: String) -> Bool { + let ext = URL(fileURLWithPath: filename).pathExtension.lowercased() + if ext.isEmpty { + return true + } + return !disallowedDatasetExtensions.contains(ext) + } + + private func isDatasetFile(_ url: URL) -> Bool { + isDatasetFilename(url.lastPathComponent) + } + + private func updateSelectedDataset(filename: String?, name: String?, announce: Bool) { + if let filename { + selectedDatasetFilename = filename + UserDefaults.standard.set(filename, forKey: datasetSelectionKey) + if announce { + let displayName = name ?? filename + messageLog += "Using dataset \(displayName) for finetuning.\n" + } + } else { + selectedDatasetFilename = nil + UserDefaults.standard.removeObject(forKey: datasetSelectionKey) + } + } + + func selectDataset(_ dataset: Dataset) { + updateSelectedDataset(filename: dataset.filename, name: dataset.name, announce: true) + } + + func registerDownloadedDataset(_ dataset: Dataset) { + guard isDatasetFilename(dataset.filename), !isModelFilename(dataset.filename) else { + let fileURL = datasetFileURL(for: dataset.filename) + if FileManager.default.fileExists(atPath: fileURL.path) { + do { + try FileManager.default.removeItem(at: fileURL) + messageLog += "Removed non-dataset file \(dataset.filename) from datasets folder.\n" + } catch { + messageLog += "Failed to remove non-dataset file \(dataset.filename): \(error.localizedDescription).\n" + } + } + messageLog += "Ignoring \(dataset.filename) because it is not a dataset file.\n" + return + } + var downloaded = dataset + downloaded.status = "downloaded" + + if let index = downloadedDatasets.firstIndex(where: { $0.filename == downloaded.filename }) { + downloadedDatasets[index] = downloaded + } else { + downloadedDatasets.append(downloaded) + } + + availableDatasets.removeAll { $0.filename == downloaded.filename } + downloadedDatasets.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + messageLog += "Downloaded dataset \(downloaded.name) -> \(downloaded.filename).\n" + updateSelectedDataset(filename: downloaded.filename, name: downloaded.name, announce: true) + } + + private func isModelFilename(_ filename: String) -> Bool { + let ext = URL(fileURLWithPath: filename).pathExtension.lowercased() + if allowedModelExtensions.contains(ext) { + return true + } + return downloadedModels.contains { $0.filename == filename } + } + + func deleteDataset(_ dataset: Dataset) { + let fileURL = datasetFileURL(for: dataset.filename) + if FileManager.default.fileExists(atPath: fileURL.path) { + do { + try FileManager.default.removeItem(at: fileURL) + messageLog += "Deleted dataset \(dataset.filename).\n" + } catch { + messageLog += "Failed to delete dataset \(dataset.filename): \(error.localizedDescription).\n" + } + } + + downloadedDatasets.removeAll { $0.filename == dataset.filename } + + if !dataset.url.isEmpty && !availableDatasets.contains(where: { $0.filename == dataset.filename }) { + var restorable = dataset + restorable.status = "download" + availableDatasets.append(restorable) + availableDatasets.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + } + + if selectedDatasetFilename == dataset.filename { + updateSelectedDataset(filename: nil, name: nil, announce: false) + ensureSelectedDatasetIsValid() + } + } + + func deleteDownloadedDatasets(at offsets: IndexSet) { + let targets = offsets.compactMap { index in + downloadedDatasets.indices.contains(index) ? downloadedDatasets[index] : nil + } + + for dataset in targets { + deleteDataset(dataset) + } + } private let defaultModels: [Model] = [ - Model(name: "TinyLlama-1.1B (Q4_0, 0.6 GiB)",url: "https://huggingface.co/TheBloke/TinyLlama-1.1B-1T-OpenOrca-GGUF/resolve/main/tinyllama-1.1b-1t-openorca.Q4_0.gguf?download=true",filename: "tinyllama-1.1b-1t-openorca.Q4_0.gguf", status: "download"), + Model(name: "TinyLlama-1.1B (Q4_0, 0.6 GiB)", url: "https://huggingface.co/TheBloke/TinyLlama-1.1B-1T-OpenOrca-GGUF/resolve/main/tinyllama-1.1b-1t-openorca.Q4_0.gguf?download=true", filename: "tinyllama-1.1b-1t-openorca.Q4_0.gguf", status: "download"), Model( name: "TinyLlama-1.1B Chat (Q8_0, 1.1 GiB)", url: "https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q8_0.gguf?download=true", filename: "tinyllama-1.1b-chat-v1.0.Q8_0.gguf", status: "download" ), - Model( name: "TinyLlama-1.1B (F16, 2.2 GiB)", url: "https://huggingface.co/ggml-org/models/resolve/main/tinyllama-1.1b/ggml-model-f16.gguf?download=true", filename: "tinyllama-1.1b-f16.gguf", status: "download" ), - Model( name: "Phi-2.7B (Q4_0, 1.6 GiB)", url: "https://huggingface.co/ggml-org/models/resolve/main/phi-2/ggml-model-q4_0.gguf?download=true", filename: "phi-2-q4_0.gguf", status: "download" ), - Model( name: "Phi-2.7B (Q8_0, 2.8 GiB)", url: "https://huggingface.co/ggml-org/models/resolve/main/phi-2/ggml-model-q8_0.gguf?download=true", filename: "phi-2-q8_0.gguf", status: "download" ), - Model( name: "Mistral-7B-v0.1 (Q4_0, 3.8 GiB)", url: "https://huggingface.co/TheBloke/Mistral-7B-v0.1-GGUF/resolve/main/mistral-7b-v0.1.Q4_0.gguf?download=true", @@ -98,24 +610,133 @@ class LlamaState: ObservableObject { name: "OpenHermes-2.5-Mistral-7B (Q3_K_M, 3.52 GiB)", url: "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q3_K_M.gguf?download=true", filename: "openhermes-2.5-mistral-7b.Q3_K_M.gguf", status: "download" + ), + // Qwen3 0.6B family + Model( + name: "Qwen3-0.6B (Q4_0)", + url: "https://huggingface.co/medel/Qwen3-0.6B-q4_0.gguf/resolve/main/Qwen3-0.6B-q4_0.gguf", + filename: "Qwen3-0.6B-q4_0.gguf", status: "download" + ), + Model( + name: "Qwen3-0.6B (Q8_0)", + url: "https://huggingface.co/prithivMLmods/Qwen3-0.6B-GGUF/resolve/main/Qwen3_0.6B.Q8_0.gguf", + filename: "Qwen3_0.6B.Q8_0.gguf", status: "download" + ), + Model( + name: "Qwen3-0.6B (F16)", + url: "https://huggingface.co/medel/models-sink/resolve/main/Qwen3-0.6B-f16.gguf", + filename: "Qwen3-0.6B-f16.gguf", status: "download" + ), + Model( + name: "Qwen3-0.6B (F32)", + url: "https://huggingface.co/medel/models-sink/resolve/main/Qwen3-0.6B-f32.gguf", + filename: "Qwen3-0.6B-f32.gguf", status: "download" + ), + // Qwen3 1.7B family + Model( + name: "Qwen3-1.7B (Q4_0)", + url: "https://huggingface.co/medel/models-sink/resolve/main/Qwen3-1.7B-Q4_0.gguf", + filename: "Qwen3-1.7B-Q4_0.gguf", status: "download" + ), + Model( + name: "Qwen3-1.7B (Q8_0)", + url: "https://huggingface.co/Qwen/Qwen3-1.7B-GGUF/resolve/main/Qwen3-1.7B-Q8_0.gguf", + filename: "Qwen3-1.7B-Q8_0.gguf", status: "download" + ), + Model( + name: "Qwen3-1.7B (F16)", + url: "https://huggingface.co/medel/models-sink/resolve/main/Qwen3-1.7B-f16.gguf", + filename: "Qwen3-1.7B-f16.gguf", status: "download" + ), + Model( + name: "Qwen3-1.7B (F32)", + url: "https://huggingface.co/medel/models-sink/resolve/main/Qwen3-1.7B-f32.gguf", + filename: "Qwen3-1.7B-f32.gguf", status: "download" + ), + // Qwen3 4B family + Model( + name: "Qwen3-4B (Q4_0)", + url: "https://huggingface.co/unsloth/Qwen3-4B-GGUF/resolve/main/Qwen3-4B-Q4_0.gguf", + filename: "Qwen3-4B-Q4_0.gguf", status: "download" + ), + Model( + name: "Qwen3-4B (Q8_0)", + url: "https://huggingface.co/unsloth/Qwen3-4B-GGUF/resolve/main/Qwen3-4B-Q8_0.gguf", + filename: "Qwen3-4B-Q8_0.gguf", status: "download" + ), + Model( + name: "Qwen3-4B (F16)", + url: "https://huggingface.co/medel/models-sink/resolve/main/Qwen3-4B-f16.gguf", + filename: "Qwen3-4B-f16.gguf", status: "download" + ), + Model( + name: "Qwen3-4B (F32)", + url: "https://huggingface.co/medel/models-sink/resolve/main/Qwen3-4B-f32.gguf", + filename: "Qwen3-4B-f32.gguf", status: "download" + ), + // Gemma 3 1B family + Model( + name: "Gemma3-1B Instruct (Q4_0)", + url: "https://huggingface.co/unsloth/gemma-3-1b-it-GGUF/resolve/main/gemma-3-1b-it-Q4_0.gguf", + filename: "gemma-3-1b-it-Q4_0.gguf", status: "download" + ), + Model( + name: "Gemma3-1B Instruct (Q8_0)", + url: "https://huggingface.co/unsloth/gemma-3-1b-it-GGUF/resolve/main/gemma-3-1b-it-Q8_0.gguf", + filename: "gemma-3-1b-it-Q8_0.gguf", status: "download" + ), + Model( + name: "Gemma3-1B Instruct (F16)", + url: "https://huggingface.co/gguf-org/gemma-3-1b-it-gguf/resolve/main/gemma-3-1b-it-f16.gguf", + filename: "gemma-3-1b-it-f16.gguf", status: "download" + ), + Model( + name: "Gemma3-1B Instruct (F32)", + url: "https://huggingface.co/gguf-org/gemma-3-1b-it-gguf/resolve/main/gemma-3-1b-it-f32.gguf", + filename: "gemma-3-1b-it-f32.gguf", status: "download" + ), + // Gemma 3 4B family + Model( + name: "Gemma3-4B Instruct (Q4_0)", + url: "https://huggingface.co/unsloth/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it-Q4_0.gguf", + filename: "gemma-3-4b-it-Q4_0.gguf", status: "download" + ), + Model( + name: "Gemma3-4B Instruct (Q8_0)", + url: "https://huggingface.co/ggml-org/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it-Q8_0.gguf", + filename: "gemma-3-4b-it-Q8_0.gguf", status: "download" + ), + Model( + name: "Gemma3-4B Instruct (F16)", + url: "https://huggingface.co/ggml-org/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it-f16.gguf", + filename: "gemma-3-4b-it-f16.gguf", status: "download" + ), + Model( + name: "Gemma3-4B Instruct (F32)", + url: "https://huggingface.co/ggml-org/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it-f32.gguf", + filename: "gemma-3-4b-it-f32.gguf", status: "download" ) ] func loadModel(modelUrl: URL?) throws { - if let modelUrl { - messageLog += "Loading model...\n" - llamaContext = try LlamaContext.create_context(path: modelUrl.path()) - messageLog += "Loaded model \(modelUrl.lastPathComponent)\n" - - // Assuming that the model is successfully loaded, update the downloaded models - updateDownloadedModels(modelName: modelUrl.lastPathComponent, status: "downloaded") - } else { + guard let modelUrl else { messageLog += "Load a model from the list below\n" + return } + + messageLog += "Loading model...\n" + llamaContext = try LlamaContext.create_context(path: modelUrl.path(), options: runtimeOptions()) + messageLog += "Loaded model \(modelUrl.lastPathComponent)\n" + currentModelURL = modelUrl + + let filename = modelUrl.lastPathComponent + updateDownloadedModels(filename: filename) + let displayName = modelUrl.deletingPathExtension().lastPathComponent + updateSelectedModel(filename: filename, name: displayName, announce: false) } - private func updateDownloadedModels(modelName: String, status: String) { - undownloadedModels.removeAll { $0.name == modelName } + private func updateDownloadedModels(filename: String) { + undownloadedModels.removeAll { $0.filename == filename } } @@ -124,6 +745,12 @@ class LlamaState: ObservableObject { return } + await llamaContext.updateSampler(options: runtimeOptions()) + + if optionSingleTurn { + messageLog = "" + } + let t_start = DispatchTime.now().uptimeNanoseconds await llamaContext.completion_init(text: text) let t_heat_end = DispatchTime.now().uptimeNanoseconds @@ -185,6 +812,130 @@ class LlamaState: ObservableObject { messageLog += "\n" } + func finetune() async { + if isFinetuning { + messageLog += "A finetuning run is already in progress.\n" + return + } + + guard let modelURL = currentModelURL else { + messageLog += "Load a model before starting finetuning.\n" + return + } + + guard let datasetURL = locateFinetuneDataset() else { + messageLog += "Unable to locate a finetuning dataset. Download one from Settings first.\n" + return + } + + let outputURL = getDocumentsDirectory().appendingPathComponent("finetuned-\(modelURL.deletingPathExtension().lastPathComponent).gguf") + let outputFilename = outputURL.lastPathComponent + + let ngl = optionNGpuLayers >= 0 ? Int32(optionNGpuLayers) : -1 + let options = llama_swift_finetune_options( + n_ctx: Int32(optionContextLength), + n_threads: Int32(max(1, ProcessInfo.processInfo.processorCount - 1)), + n_batch: 256, + n_ubatch: 256, + epochs: 2, + lora_rank: 8, + lora_alpha: 16, + learning_rate: 1e-5, + val_split: 0.05, + target_modules: 0, + seed: Int32(optionSeed), + flash_attn: optionFlashAttention, + n_gpu_layers: ngl, + checkpoint_save_steps: 0, + checkpoint_save_dir: nil, + resume_from_checkpoint: nil, + auto_resume: false + ) + + isFinetuning = true + messageLog += "Starting on-device LoRA finetuning for \(modelURL.lastPathComponent).\n" + + let modelPath = modelURL.path + let datasetPath = datasetURL.path + let outputPath = outputURL.path + let statePointer = Unmanaged.passUnretained(self).toOpaque() + let callback = LlamaState.finetuneLogCallback + + Task.detached(priority: .userInitiated) { + var opts = options + let result: llama_swift_finetune_error = modelPath.withCString { modelC in + datasetPath.withCString { datasetC in + outputPath.withCString { outputC in + llama_swift_run_lora_finetune(modelC, datasetC, outputC, &opts, callback, statePointer) + } + } + } + + Task { @MainActor in + let state = Unmanaged.fromOpaque(statePointer).takeUnretainedValue() + state.isFinetuning = false + if result == LLAMA_SWIFT_FINETUNE_OK { + state.messageLog += "Finetuning finished successfully. Adapter saved as \(outputFilename).\n" + } else { + state.messageLog += "Finetuning failed: \(state.describeFinetuneError(result)).\n" + } + } + } + } + + private func locateFinetuneDataset() -> URL? { + let fileManager = FileManager.default + let environment = ProcessInfo.processInfo.environment + + if let overridePath = environment["LLAMA_FINETUNE_DATASET"], !overridePath.isEmpty { + let overrideURL = URL(fileURLWithPath: overridePath) + if fileManager.fileExists(atPath: overrideURL.path) { + return overrideURL + } + } + + if let selectedFilename = selectedDatasetFilename { + let selectedURL = datasetFileURL(for: selectedFilename) + if fileManager.fileExists(atPath: selectedURL.path) { + return selectedURL + } + } + + for dataset in downloadedDatasets { + let candidate = datasetFileURL(for: dataset.filename) + if fileManager.fileExists(atPath: candidate.path) { + updateSelectedDataset(filename: dataset.filename, name: dataset.name, announce: false) + return candidate + } + } + + let workingDirectoryCandidate = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent(finetuneDatasetFilename) + if fileManager.fileExists(atPath: workingDirectoryCandidate.path) { + return workingDirectoryCandidate + } + + return nil + } + + private func describeFinetuneError(_ code: llama_swift_finetune_error) -> String { + switch code { + case LLAMA_SWIFT_FINETUNE_ERROR_INVALID_ARGUMENT: + return "invalid configuration" + case LLAMA_SWIFT_FINETUNE_ERROR_MODEL_LOAD: + return "failed to load model" + case LLAMA_SWIFT_FINETUNE_ERROR_CONTEXT_CREATE: + return "failed to create training context" + case LLAMA_SWIFT_FINETUNE_ERROR_DATASET: + return "dataset preparation failed" + case LLAMA_SWIFT_FINETUNE_ERROR_TRAINING_INIT: + return "training initialization failed" + case LLAMA_SWIFT_FINETUNE_ERROR_SAVE: + return "failed to save adapter" + default: + return "unexpected error (\(code.rawValue))" + } + } + func clear() async { guard let llamaContext else { return @@ -193,4 +944,5 @@ class LlamaState: ObservableObject { await llamaContext.clear() messageLog = "" } + } diff --git a/examples/llama.swiftui/llama.swiftui/UI/ContentView.swift b/examples/llama.swiftui/llama.swiftui/UI/ContentView.swift index 1c3cd9d2efc7..497095055daf 100644 --- a/examples/llama.swiftui/llama.swiftui/UI/ContentView.swift +++ b/examples/llama.swiftui/llama.swiftui/UI/ContentView.swift @@ -32,6 +32,11 @@ struct ContentView: View { bench() } + Button(llamaState.isFinetuning ? "Finetuning..." : "Finetune") { + finetune() + } + .disabled(llamaState.isFinetuning) + Button("Clear") { clear() } @@ -43,14 +48,14 @@ struct ContentView: View { .buttonStyle(.bordered) .padding() - NavigationLink(destination: DrawerView(llamaState: llamaState)) { - Text("View Models") + NavigationLink(destination: SettingsView(llamaState: llamaState)) { + Text("Settings") } .padding() } .padding() - .navigationBarTitle("Model Settings", displayMode: .inline) + .navigationBarTitle("Settings", displayMode: .inline) } } @@ -68,56 +73,184 @@ struct ContentView: View { } } + func finetune() { + Task { + await llamaState.finetune() + } + } + func clear() { Task { await llamaState.clear() } } - struct DrawerView: View { + struct SettingsView: View { @ObservedObject var llamaState: LlamaState @State private var showingHelp = false - func delete(at offsets: IndexSet) { - offsets.forEach { offset in - let model = llamaState.downloadedModels[offset] - let fileURL = getDocumentsDirectory().appendingPathComponent(model.filename) - do { - try FileManager.default.removeItem(at: fileURL) - } catch { - print("Error deleting file: \(error)") + var body: some View { + List { + Section(header: Text("Dataset for Finetuning")) { + if let selectedFilename = llamaState.selectedDatasetFilename, + let dataset = llamaState.downloadedDatasets.first(where: { $0.filename == selectedFilename }) { + VStack(alignment: .leading, spacing: 4) { + Text(dataset.name) + .font(.body) + Text(dataset.filename) + .font(.caption) + .foregroundColor(.secondary) + } + } else { + Text("No dataset selected. Download or choose one below.") + .font(.footnote) + .foregroundColor(.secondary) + } } - } - // Remove models from downloadedModels array - llamaState.downloadedModels.remove(atOffsets: offsets) - } + if !llamaState.downloadedDatasets.isEmpty { + Section(header: Text("Downloaded Datasets")) { + ForEach(llamaState.downloadedDatasets) { dataset in + DatasetDownloadedRow(llamaState: llamaState, dataset: dataset) + } + .onDelete { offsets in + llamaState.deleteDownloadedDatasets(at: offsets) + } + } + } - func getDocumentsDirectory() -> URL { - let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - return paths[0] - } - var body: some View { - List { - Section(header: Text("Download Models From Hugging Face")) { - HStack { - InputButton(llamaState: llamaState) + if !llamaState.availableDatasets.isEmpty { + Section(header: Text("Available Datasets")) { + ForEach(llamaState.availableDatasets) { dataset in + DatasetAvailableRow(llamaState: llamaState, dataset: dataset) + } } } - Section(header: Text("Downloaded Models")) { - ForEach(llamaState.downloadedModels) { model in - DownloadButton(llamaState: llamaState, modelName: model.name, modelUrl: model.url, filename: model.filename) + + Section(header: Text("Download Dataset From URL")) { + DatasetURLInputRow(llamaState: llamaState) + } + Section(header: Text("Model for Inference")) { + if let selectedFilename = llamaState.selectedModelFilename, + let model = llamaState.downloadedModels.first(where: { $0.filename == selectedFilename }) { + VStack(alignment: .leading, spacing: 4) { + Text(model.name) + .font(.body) + Text(model.filename) + .font(.caption) + .foregroundColor(.secondary) + } + } else if let selectedFilename = llamaState.selectedModelFilename { + VStack(alignment: .leading, spacing: 4) { + Text(selectedFilename) + .font(.body) + Text("Bundle resource") + .font(.caption) + .foregroundColor(.secondary) + } + } else { + Text("No model loaded. Download or choose one below.") + .font(.footnote) + .foregroundColor(.secondary) } - .onDelete(perform: delete) } - Section(header: Text("Default Models")) { - ForEach(llamaState.undownloadedModels) { model in - DownloadButton(llamaState: llamaState, modelName: model.name, modelUrl: model.url, filename: model.filename) + + if !llamaState.downloadedModels.isEmpty { + Section(header: Text("Downloaded Models")) { + ForEach(llamaState.downloadedModels) { model in + ModelDownloadedRow(llamaState: llamaState, model: model) + } + .onDelete { offsets in + llamaState.deleteDownloadedModels(at: offsets) + } + } + } + + if !llamaState.undownloadedModels.isEmpty { + Section(header: Text("Available Models")) { + ForEach(llamaState.undownloadedModels) { model in + ModelAvailableRow(llamaState: llamaState, model: model) + } + } + } + + Section(header: Text("Download Model From URL")) { + ModelURLInputRow(llamaState: llamaState) + } + + Section(header: Text("Finetuning & Runtime Options")) { + Stepper(value: $llamaState.optionNGpuLayers, in: -1...256, step: 1) { + HStack { + Text("n-gpu-layers (ngl)") + Spacer() + if llamaState.optionNGpuLayers >= 0 { + Text("\(llamaState.optionNGpuLayers)") + .foregroundColor(.secondary) + } else { + Text("Auto") + .foregroundColor(.secondary) + } + } + } + + Stepper(value: $llamaState.optionContextLength, in: 32...8192, step: 64) { + HStack { + Text("Context size (c)") + Spacer() + Text("\(llamaState.optionContextLength)") + .foregroundColor(.secondary) + } + } + + Stepper(value: $llamaState.optionSeed, in: 0...1_000_000, step: 1) { + HStack { + Text("Seed (s)") + Spacer() + Text("\(llamaState.optionSeed)") + .foregroundColor(.secondary) + } + } + + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Temperature (temp)") + Spacer() + Text(String(format: "%.2f", llamaState.optionTemperature)) + .foregroundColor(.secondary) + } + Slider(value: $llamaState.optionTemperature, in: 0...0) + } + + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Top-p") + Spacer() + Text(String(format: "%.2f", llamaState.optionTopP)) + .foregroundColor(.secondary) + } + Slider(value: $llamaState.optionTopP, in: 0.01...1.0, step: 0.01) + } + + Stepper(value: $llamaState.optionTopK, in: 1...500, step: 1) { + HStack { + Text("Top-k") + Spacer() + Text("\(llamaState.optionTopK)") + .foregroundColor(.secondary) + } + } + + Toggle(isOn: $llamaState.optionFlashAttention) { + Text("Flash Attention") + } + + Toggle(isOn: $llamaState.optionSingleTurn) { + Text("Single Turn (st)") } } } .listStyle(GroupedListStyle()) - .navigationBarTitle("Model Settings", displayMode: .inline).toolbar { + .navigationBarTitle("Settings", displayMode: .inline).toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Help") { showingHelp = true @@ -149,6 +282,486 @@ struct ContentView: View { } } +struct DatasetDownloadedRow: View { + @ObservedObject var llamaState: LlamaState + let dataset: Dataset + + private var isSelected: Bool { + llamaState.selectedDatasetFilename == dataset.filename + } + + var body: some View { + Button { + llamaState.selectDataset(dataset) + } label: { + HStack(alignment: .center, spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text(dataset.name) + .foregroundColor(.primary) + Text(dataset.filename) + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + if isSelected { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) + } + } + .padding(.vertical, 4) + } + .buttonStyle(.plain) + } +} + +struct DatasetAvailableRow: View { + @ObservedObject var llamaState: LlamaState + let dataset: Dataset + @State private var status: Status = .idle + + private enum Status: Equatable { + case idle + case downloading + case failed(String) + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(dataset.name) + .font(.body) + Text(dataset.url) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(2) + + switch status { + case .idle: + Button("Download") { + startDownload() + } + .buttonStyle(.borderedProminent) + case .downloading: + HStack(spacing: 8) { + ProgressView() + Text("Downloading…") + .font(.footnote) + .foregroundColor(.secondary) + } + case .failed(let message): + VStack(alignment: .leading, spacing: 4) { + Text("Download failed: \(message)") + .font(.caption) + .foregroundColor(.red) + Button("Retry") { + status = .idle + startDownload() + } + } + } + } + .padding(.vertical, 4) + } + + private func startDownload() { + guard status != .downloading else { return } + guard let downloadURL = URL(string: dataset.url) else { + status = .failed("Invalid URL") + return + } + + status = .downloading + + Task { + do { + let (temporaryURL, response) = try await URLSession.shared.download(from: downloadURL) + guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { + await MainActor.run { + status = .failed("Server error") + } + return + } + + let destination = await MainActor.run { + llamaState.datasetFileURL(for: dataset.filename) + } + + let fileManager = FileManager.default + if fileManager.fileExists(atPath: destination.path) { + try fileManager.removeItem(at: destination) + } + try fileManager.moveItem(at: temporaryURL, to: destination) + + await MainActor.run { + llamaState.registerDownloadedDataset(dataset) + status = .idle + } + } catch { + if Task.isCancelled { return } + await MainActor.run { + status = .failed(error.localizedDescription) + } + } + } + } +} + +struct DatasetURLInputRow: View { + @ObservedObject var llamaState: LlamaState + @State private var urlText: String = "" + @State private var status: Status = .idle + + private enum Status: Equatable { + case idle + case downloading + case failed(String) + case success + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + TextField("https://example.com/dataset.jsonl", text: $urlText) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + + switch status { + case .failed(let message): + Text(message) + .font(.caption) + .foregroundColor(.red) + case .success: + Text("Download complete!") + .font(.caption) + .foregroundColor(.green) + default: + EmptyView() + } + + Button(action: startDownload) { + if status == .downloading { + HStack { + ProgressView() + Text("Downloading…") + } + } else { + Text("Download Dataset") + } + } + .buttonStyle(.borderedProminent) + .disabled(status == .downloading || urlText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + .padding(.vertical, 4) + } + + private func startDownload() { + guard status != .downloading else { return } + let trimmed = urlText.trimmingCharacters(in: .whitespacesAndNewlines) + guard let downloadURL = URL(string: trimmed), !trimmed.isEmpty else { + status = .failed("Enter a valid dataset URL") + return + } + + let filename = deriveFilename(from: downloadURL) + let displayName = deriveDisplayName(from: filename) + let dataset = Dataset(name: displayName, url: trimmed, filename: filename, status: "download") + + status = .downloading + + Task { + do { + let (temporaryURL, response) = try await URLSession.shared.download(from: downloadURL) + guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { + await MainActor.run { + status = .failed("Server error") + } + return + } + + let destination = await MainActor.run { + llamaState.datasetFileURL(for: dataset.filename) + } + + let fileManager = FileManager.default + if fileManager.fileExists(atPath: destination.path) { + try fileManager.removeItem(at: destination) + } + try fileManager.moveItem(at: temporaryURL, to: destination) + + await MainActor.run { + llamaState.registerDownloadedDataset(dataset) + status = .success + urlText = "" + } + } catch { + if Task.isCancelled { return } + await MainActor.run { + status = .failed(error.localizedDescription) + } + } + } + } + + private func deriveFilename(from url: URL) -> String { + let candidate = url.lastPathComponent + if candidate.isEmpty { + return "dataset-\(UUID().uuidString).txt" + } + return candidate + } + + private func deriveDisplayName(from filename: String) -> String { + let decoded = filename.removingPercentEncoding ?? filename + let baseName = (decoded as NSString).deletingPathExtension + let cleaned = baseName.replacingOccurrences(of: "_", with: " ") + .replacingOccurrences(of: "-", with: " ") + let trimmed = cleaned.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { + return "Custom Dataset" + } + return trimmed.capitalized + } +} + +struct ModelDownloadedRow: View { + @ObservedObject var llamaState: LlamaState + let model: Model + + private var isSelected: Bool { + llamaState.selectedModelFilename == model.filename + } + + var body: some View { + Button { + llamaState.selectDownloadedModel(model) + } label: { + HStack(alignment: .center, spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text(model.name) + .foregroundColor(.primary) + Text(model.filename) + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + if isSelected { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) + } + } + .padding(.vertical, 4) + } + .buttonStyle(.plain) + } +} + +struct ModelAvailableRow: View { + @ObservedObject var llamaState: LlamaState + let model: Model + @State private var status: Status = .idle + + private enum Status: Equatable { + case idle + case downloading + case failed(String) + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(model.name) + .font(.body) + Text(model.url) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(2) + + switch status { + case .idle: + Button("Download") { + startDownload() + } + .buttonStyle(.borderedProminent) + case .downloading: + HStack(spacing: 8) { + ProgressView() + Text("Downloading…") + .font(.footnote) + .foregroundColor(.secondary) + } + case .failed(let message): + VStack(alignment: .leading, spacing: 4) { + Text("Download failed: \(message)") + .font(.caption) + .foregroundColor(.red) + Button("Retry") { + status = .idle + startDownload() + } + } + } + } + .padding(.vertical, 4) + } + + private func startDownload() { + guard status != .downloading else { return } + guard let downloadURL = URL(string: model.url) else { + status = .failed("Invalid URL") + return + } + + status = .downloading + + Task { + do { + let (temporaryURL, response) = try await URLSession.shared.download(from: downloadURL) + guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { + await MainActor.run { + status = .failed("Server error") + } + return + } + + let destination = await MainActor.run { + llamaState.modelFileURL(for: model.filename) + } + + let fileManager = FileManager.default + if fileManager.fileExists(atPath: destination.path) { + try fileManager.removeItem(at: destination) + } + try fileManager.moveItem(at: temporaryURL, to: destination) + + await MainActor.run { + llamaState.registerDownloadedModel(model) + status = .idle + } + } catch { + if Task.isCancelled { return } + await MainActor.run { + status = .failed(error.localizedDescription) + } + } + } + } +} + +struct ModelURLInputRow: View { + @ObservedObject var llamaState: LlamaState + @State private var urlText: String = "" + @State private var status: Status = .idle + + private enum Status: Equatable { + case idle + case downloading + case failed(String) + case success + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + TextField("https://example.com/model.gguf", text: $urlText) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + + switch status { + case .failed(let message): + Text(message) + .font(.caption) + .foregroundColor(.red) + case .success: + Text("Download complete!") + .font(.caption) + .foregroundColor(.green) + default: + EmptyView() + } + + Button(action: startDownload) { + if status == .downloading { + HStack { + ProgressView() + Text("Downloading…") + } + } else { + Text("Download Model") + } + } + .buttonStyle(.borderedProminent) + .disabled(status == .downloading || urlText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + .padding(.vertical, 4) + } + + private func startDownload() { + guard status != .downloading else { return } + let trimmed = urlText.trimmingCharacters(in: .whitespacesAndNewlines) + guard let downloadURL = URL(string: trimmed), !trimmed.isEmpty else { + status = .failed("Enter a valid model URL") + return + } + + let filename = deriveFilename(from: downloadURL) + let displayName = deriveDisplayName(from: filename) + let model = Model(name: displayName, url: trimmed, filename: filename, status: "download") + + status = .downloading + + Task { + do { + let (temporaryURL, response) = try await URLSession.shared.download(from: downloadURL) + guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { + await MainActor.run { + status = .failed("Server error") + } + return + } + + let destination = await MainActor.run { + llamaState.modelFileURL(for: model.filename) + } + + let fileManager = FileManager.default + if fileManager.fileExists(atPath: destination.path) { + try fileManager.removeItem(at: destination) + } + try fileManager.moveItem(at: temporaryURL, to: destination) + + await MainActor.run { + llamaState.registerDownloadedModel(model) + status = .success + urlText = "" + } + } catch { + if Task.isCancelled { return } + await MainActor.run { + status = .failed(error.localizedDescription) + } + } + } + } + + private func deriveFilename(from url: URL) -> String { + let candidate = url.lastPathComponent + if candidate.isEmpty { + return "model-\(UUID().uuidString).gguf" + } + return candidate + } + + private func deriveDisplayName(from filename: String) -> String { + let decoded = filename.removingPercentEncoding ?? filename + let baseName = (decoded as NSString).deletingPathExtension + let cleaned = baseName.replacingOccurrences(of: "_", with: " ") + .replacingOccurrences(of: "-", with: " ") + let trimmed = cleaned.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { + return "Custom Model" + } + return trimmed.capitalized + } +} + struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() diff --git a/examples/llama.swiftui/llama.swiftui/UI/DownloadButton.swift b/examples/llama.swiftui/llama.swiftui/UI/DownloadButton.swift index 4584d6eaa3d3..631ce50c5019 100644 --- a/examples/llama.swiftui/llama.swiftui/UI/DownloadButton.swift +++ b/examples/llama.swiftui/llama.swiftui/UI/DownloadButton.swift @@ -1,3 +1,4 @@ +#if false import SwiftUI struct DownloadButton: View { @@ -113,12 +114,13 @@ struct DownloadButton: View { } } } +#endif -// #Preview { -// DownloadButton( -// llamaState: LlamaState(), -// modelName: "TheBloke / TinyLlama-1.1B-1T-OpenOrca-GGUF (Q4_0)", -// modelUrl: "https://huggingface.co/TheBloke/TinyLlama-1.1B-1T-OpenOrca-GGUF/resolve/main/tinyllama-1.1b-1t-openorca.Q4_0.gguf?download=true", -// filename: "tinyllama-1.1b-1t-openorca.Q4_0.gguf" -// ) -// } +import SwiftUI + +@available(*, deprecated, message: "Use ModelAvailableRow and ModelDownloadedRow instead.") +struct DownloadButton: View { + var body: some View { + EmptyView() + } +} diff --git a/examples/llama.swiftui/llama.swiftui/UI/InputButton.swift b/examples/llama.swiftui/llama.swiftui/UI/InputButton.swift index c5ffbad4ec33..60bf1d7b0fb9 100644 --- a/examples/llama.swiftui/llama.swiftui/UI/InputButton.swift +++ b/examples/llama.swiftui/llama.swiftui/UI/InputButton.swift @@ -1,3 +1,4 @@ +#if false import SwiftUI struct InputButton: View { @@ -129,3 +130,13 @@ struct InputButton: View { } } } +#endif + +import SwiftUI + +@available(*, deprecated, message: "Use ModelURLInputRow instead.") +struct InputButton: View { + var body: some View { + EmptyView() + } +} diff --git a/examples/simple-cmake-pkg/CMakeLists.txt b/examples/simple-cmake-pkg/CMakeLists.txt index 128e38c8f2dc..06e2692a1d85 100644 --- a/examples/simple-cmake-pkg/CMakeLists.txt +++ b/examples/simple-cmake-pkg/CMakeLists.txt @@ -7,5 +7,5 @@ find_package(Llama REQUIRED) add_executable(${TARGET} ${CMAKE_CURRENT_LIST_DIR}/../simple/simple.cpp) install(TARGETS ${TARGET} RUNTIME) -target_link_libraries(${TARGET} PRIVATE llama ggml::all ${CMAKE_THREAD_LIBS_INIT}) +target_link_libraries(${TARGET} PRIVATE llama::llama ggml::all ${CMAKE_THREAD_LIBS_INIT}) target_compile_features(${TARGET} PRIVATE cxx_std_17) diff --git a/examples/simple/CMakeLists.txt b/examples/simple/CMakeLists.txt index 104ecabfd723..5ada3fdd3de6 100644 --- a/examples/simple/CMakeLists.txt +++ b/examples/simple/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET llama-simple) add_executable(${TARGET} simple.cpp) install(TARGETS ${TARGET} RUNTIME) -target_link_libraries(${TARGET} PRIVATE llama ${CMAKE_THREAD_LIBS_INIT}) +target_link_libraries(${TARGET} PRIVATE llama llama-common-test ${CMAKE_THREAD_LIBS_INIT}) target_compile_features(${TARGET} PRIVATE cxx_std_17) diff --git a/examples/simple/simple.cpp b/examples/simple/simple.cpp index d09771d10457..0268f503fe2b 100644 --- a/examples/simple/simple.cpp +++ b/examples/simple/simple.cpp @@ -1,15 +1,20 @@ +#include "llama-cpp.h" #include "llama.h" #include #include #include -#include static void print_usage(int, char ** argv) { printf("\nexample usage:\n"); printf("\n %s -m model.gguf [-n n_predict] [-ngl n_gpu_layers] [prompt]\n", argv[0]); + printf("\n Optional environment variables: LLAMA_EXAMPLE_MEMORY_BUFFER LLAMA_EXAMPLE_MEMORY_BUFFER_SPLIT"); printf("\n"); } +#ifdef LLAMA_COMMON_TEST_HEADERS +#include "load_into_memory.h" +#endif + int main(int argc, char ** argv) { // path to the model gguf file std::string model_path; @@ -83,12 +88,13 @@ int main(int argc, char ** argv) { llama_model_params model_params = llama_model_default_params(); model_params.n_gpu_layers = ngl; +#ifdef LLAMA_COMMON_TEST_HEADERS + llama_model * model = memory_configuration_env_is_set() ? + load_model_from_memory_configuration(model_path.c_str(), model_params) : + llama_model_load_from_file(model_path.c_str(), model_params); +#else llama_model * model = llama_model_load_from_file(model_path.c_str(), model_params); - - if (model == NULL) { - fprintf(stderr , "%s: error: unable to load model\n" , __func__); - return 1; - } +#endif const llama_vocab * vocab = llama_model_get_vocab(model); // tokenize the prompt diff --git a/examples/training/CMakeLists.txt b/examples/training/CMakeLists.txt index 64afe6ddc647..c11e981dbf3b 100644 --- a/examples/training/CMakeLists.txt +++ b/examples/training/CMakeLists.txt @@ -3,3 +3,9 @@ add_executable(${TARGET} finetune.cpp) install(TARGETS ${TARGET} RUNTIME) target_link_libraries(${TARGET} PRIVATE common llama ${CMAKE_THREAD_LIBS_INIT}) target_compile_features(${TARGET} PRIVATE cxx_std_11) + +set(TARGET llama-finetune-lora) +add_executable(${TARGET} finetune-lora.cpp) +install(TARGETS ${TARGET} RUNTIME) +target_link_libraries(${TARGET} PRIVATE common llama ${CMAKE_THREAD_LIBS_INIT}) +target_compile_features(${TARGET} PRIVATE cxx_std_11) diff --git a/examples/training/README.md b/examples/training/README.md index df425279266e..3f46b50fd9c5 100644 --- a/examples/training/README.md +++ b/examples/training/README.md @@ -1,5 +1,6 @@ # llama.cpp/examples/training +## finetune This directory contains examples related to language model training using llama.cpp/GGML. So far finetuning is technically functional (for FP32 models and limited hardware setups) but the code is very much WIP. Finetuning of Stories 260K and LLaMA 3.2 1b seems to work with 24 GB of memory. @@ -15,3 +16,266 @@ export model_name=llama_3.2-1b && export quantization=f32 ``` The perplexity value of the finetuned model should be lower after training on the test set for 2 epochs. + + +## finetune-lora + +LoRA (Low-Rank Adaptation) fine-tuning for efficient model training. This approach trains only a small set of additional parameters while keeping +the base model frozen, making it memory-efficient. + +### Basic Usage + +```sh +# Create new LoRA adapter with default settings (rank=8, alpha=16, attention modules) +./build/bin/llama-finetune-lora -m model.gguf -f dataset.txt -ngl 999 -c 512 -b 512 -ub 512 -fa off + +# Custom LoRA parameters(creates new lora adapter and trains it from scratch) +./build/bin/llama-finetune-lora -m model.gguf -f dataset.txt -ngl 999 -c 512 -b 512 -ub 512 \ + --lora-rank 16 --lora-alpha 32 --lora-modules "attn_q,attn_k,attn_v,attn_o" -fa off + +# Fine-tune existing LoRA adapter +./build/bin/llama-finetune-lora -m base_model.gguf -f dataset.txt --lora existing_adapter.gguf \ + --output-adapter improved_adapter.gguf -ngl 999 -c 512 -b 512 -ub 512 + +# Training with checkpointing +./build/bin/llama-finetune-lora -m model.gguf -f dataset.txt -ngl 999 -c 512 -b 512 -ub 512 \ + --checkpoint-save-steps 50 --checkpoint-save-dir "./lora_checkpoints" + +# Resume training from checkpoint +./build/bin/llama-finetune-lora -m model.gguf -f dataset.txt -ngl 999 -c 512 -b 512 -ub 512 \ + --resume-from "./lora_checkpoints/checkpoint_step_00000150/" + --output-adapter improved_adapter.gguf -ngl 999 -c 512 -b 512 -ub 512 -fa off + +# Supervised FineTuning with Assistant only loss +./build/bin/llama-finetune-lora -m model.gguf -f dataset.jsonl -ngl 999 -c 512 -b 512 -ub 512 \ + --lora-modules "attn_q,attn_k,attn_v,attn_o" --assistant-loss-only -fa off +``` + +### SFT(Instruction Fine Tuning) with Assistant Only Loss +- Masks the system and user tokens and only computes loss on assistant tokens +- Requires the dataset to be in json format just like huggingface with `role` and `content` for each role +- Allows users to optionally pass a jinja chat template with `--chat-template chat-ml-template.jinja` + +### Parameters + +#### LoRA Configuration +- `--lora-rank N` - LoRA rank (default: 8) + - Lower rank = smaller adapter, less capacity + - Higher rank = larger adapter, more capacity +- `--lora-alpha N` - LoRA alpha scaling factor (default: 16.0) + - Controls adaptation strength + - Common rule: alpha = 2 × rank +- `--lora-modules MODULES` - Target modules as comma-separated list + - Available: `attn_q`, `attn_k`, `attn_v`, `attn_o`, `ffn_gate`, `ffn_up`, `ffn_down`, `embed`, `output`, `all` + - Default: `attn_q,attn_k,attn_v,attn_o` (attention modules) +- `--output-adapter PATH` - Output adapter filename (default: auto-generated) +- `--lora-seed N` - Seed for reproducible LoRA weight initialization (default: 0 = non-deterministic) + +#### Checkpointing +- `--checkpoint-save-steps N` - Save checkpoint every N training steps (default: 100) +- `--checkpoint-save-dir PATH` - Directory for checkpoints (default: `./checkpoints`) +- `--resume-from PATH` - Resume training from specific checkpoint directory +- `--auto-resume` - Automatically resume from latest checkpoint in save directory + +#### Standard Parameters +- `-m MODEL` - Base model file (.gguf) +- `-f FILE` - Training dataset +- `-ngl N` - GPU layers (use 999 for full GPU training) +- `-c N` - Context length (512 recommended for mobile) +- `--assistant-loss-only` - Trains only on assistant tokens +- `--chat-template` - Jinja chat template for chat ML formatting to train on assistant tokens only +- `--learning-rate` - AdamW learning rate (default: 1e-5) +- `--weight-decay` - AdamW weight decay (default: 1e-2) +- `--lr-scheduler` - Learning rate scheduler: constant, cosine, linear (default: constant) +- `--lr-min` - Minimum LR for cosine/linear schedulers (default: 0) +- `--warmup-ratio` - Fraction of total steps for LR warmup (default: 0.0) +- `--warmup-steps` - Explicit warmup steps (overrides warmup-ratio) + + +### Using Trained Adapters + +After training, you'll get a small adapter file. Use it with the original base model: + +```sh +./build/bin/llama-cli -m base_model.gguf --lora trained_adapter.gguf -ngl 999 +``` + +### Checkpointing + +The LoRA fine-tuning supports automatic checkpointing to save and resume training progress: +- **Automatic saving**: Model and optimizer state saved every N training steps +- **Complete state**: Includes LoRA weights, optimizer momentum, and training metadata +- **Resume capability**: Continue training from exact step with full optimizer state +- **Auto-resume**: Automatically find and resume from latest checkpoint + +#### Checkpoint Structure +Each checkpoint directory contains: +- `model.gguf` - LoRA adapter weights +- `optimizer.gguf` - Optimizer state (momentum, variance, iteration) +- `metadata.txt` - Training parameters and step information + +### Architecture Overview + +This section explains how LoRA fine-tuning is implemented in llama.cpp: + +**LoRA Adapter Management (`src/llama-lora-training.cpp`):** +This file manages the complete lifecycle of LoRA adapters: + +1. **Adapter Creation (`llama_lora_create_adapter()`):** + - Iterates through all model tensors to find target modules + - Creates low-rank matrix pairs (A, B) for each selected module + - Tensor naming: `blk.{layer}.{module}.lora_a` and `blk.{layer}.{module}.lora_b` + - Dimensions: `A ∈ R^(d×r)`, `B ∈ R^(r×k)` where r is the rank + +2. **Weight Initialization (`llama_lora_init_tensor_weights()`):** + - Matrix A: Initialized with Gaussian distribution N(0, init_std) + - Matrix B: Initialized to zeros + - This ensures ΔW = BA starts at zero (no adaptation initially) + - Supports both CPU and GPU tensors via `ggml_backend_tensor_set()` + - Use `--lora-seed N` for reproducible initialization (0 = non-deterministic) + +3. **Buffer Allocation (`llama_lora_allocate_buffers()`):** + - Auto-detects backend from base model (CPU/CUDA/Vulkan) + - Allocates LoRA tensors on same device as model layers + - Uses `ggml_backend_alloc_ctx_tensors_from_buft()` for optimal placement + +4. **Module Selection:** + - Scans tensor names for patterns: `attn_q`, `attn_k`, `attn_v`, `attn_output` + - FFN modules: `ffn_gate`, `ffn_up`, `ffn_down` + - Controlled by `target_modules` bitmask (lines 194-211) + +5. **Optimizer Integration (`llama_opt_param_filter_lora()`):** + - Filter function for `ggml-opt` to identify trainable parameters + - Returns `true` only for tensors with `.lora_a` or `.lora_b` suffix + - Ensures base model weights are excluded from gradient computation + +6. **Checkpointing (`llama_lora_save_checkpoint()`):** + - Creates checkpoint directory structure + - Saves `model.gguf` (LoRA weights via `llama_lora_save_adapter()`) + - Saves `optimizer.gguf` (optimizer state via `ctx->opt_save_state()`) + - Both files required for resuming training + +**Forward Pass (`ggml-opt.cpp:ggml_opt_forward()`):** +1. Input batch flows through base model with LoRA injections +2. Loss computation uses only trainable LoRA parameters, we mark only the lora-parameters as trainable with `llama_opt_param_filter_lora` +3. For instruction tuning with `--assistant-loss-only`, loss masking is applied to system/user tokens + +**Backward Pass (`ggml-opt.cpp:ggml_opt_backward()`):** +1. Gradients computed only for LoRA adapters (matrices A and B) +2. Base model weights are excluded from gradient computation +3. Memory efficient: only stores gradients for low-rank matrices + +**Optimizer State (`ggml-opt.cpp`):** +- Uses AdamW optimizer by default +- Maintains first moment (momentum) and second moment (variance) for each LoRA parameter +- State tensors: `opt_state_m` and `opt_state_v` for each adapter matrix +- Checkpoint format includes full optimizer state for seamless resumption + +### Adding Support for a New Model Architecture in llama.cpp + +Supporting a new Transformer model in llama.cpp is not automatic — it requires +implementing missing operators, adding tokenizer + prompt formatting, and +validating training/inference parity with a reference framework. The process is +repeatable and model families like Gemma, Qwen, Mistral, Grok, Phi can be +onboarded by following a consistent workflow. + +Below is a generalized end-to-end porting procedure, derived from our work enabling +Gemma/Qwen inference and LoRA finetuning across CPU, Vulkan, Metal, and CUDA backends. + +#### Step 1 — Analyze the Architecture + +| Component | Example Differences Across Models | +| ------------------- | ----------------------------------------------------- | +| Attention structure | Grouped-QKV, Multi-query, Sliding-window, Flash-attn | +| FFN type | SiLU-MLP (LLaMA), GEGLU (Gemma), SWIGLU/MoE (Mixtral) | +| Positional encoding | RoPE, XPos, ALiBi | +| Tokenizer format | BPE, SentencePiece, Unigram | +| Chat/Prompt style | ChatML, Gemini-style blocks, Turn-format | + +If the model deviates from LLaMA in any FFN or attention component — new ops will be required. + +#### Step 2 — Implement Missing Operators + +Almost all new model bring at least one of these requirements: + +| Op Type | Purpose | Required for | +| ------------------------ | ------------------------------- | ---------------------- | +| Feed-forward activations | GEGLU, SWIGLU, MoE dispatch | inference + training | +| Loss + grad kernels | CROSS_ENTROPY_BACK, MASKED_LOSS | LoRA/SFT training | +| Matrix update ops | OUT_PROD, OUT_PROD_BACK | LoRA parameter updates | + +#### Step 3 — Backend Kernel Extension + +Each operator must exist in at least one backend to work — but training must support +Vulkan, CUDA, CPU, optionally Metal. + +| Backend | Required For | +| ------- | ---------------------------------------------------------- | +| CPU | reference correctness + unit tests | +| Vulkan | cross-platform inference + LoRA (Adreno, Mali, AMD, Intel) | +| CUDA | high throughput training | +| Metal | iOS / Apple Silicon finetuning | + +Special attention for mobile Vulkan: + +- operators must tile to fit GPU SSBO memory windows +- OUT_PROD + MUL_MAT need dynamic splitting +- quantized INT4/INT8 variants reduce VRAM footprint dramatically + +#### Step 4 — Add Tokenizer, Prompt Format, Chat Template + +Even if inference works, instruction finetuning will fail without chat formatting. + +You must implement: + +``` +tokenizer.json / spm.model: convert to tokenizer.gguf +default chat.jinja: system/user/assistant roles +assistant-only masking: loss applies only to assistant tokens +``` + +Then train: + +```bash +./llama-finetune-lora -m newmodel.gguf -f data.jsonl \ + --assistant-loss-only --chat-template template.jinja \ + --lora-rank 16 -ngl 999 +``` + +#### Step 5 — Validation Workflow + +Before claiming model support: + +| Test | Pass Criteria | +| ------------------------- | ----------------------------------------------- | +| Generate text | No NaNs, stable token distribution | +| Forward-only parity | Output ≈ PyTorch within float tolerance | +| 50–200 step LoRA run | Loss decreases consistently | +| Merge-adapter → inference | identical behavior to runtime adapter injection | +| Finetune resumption | checkpoint restore reproducible | + +If inference works but training diverges, most of the time it's a missing backward op. + +#### Example — Adding Support for Gemma (Inference + LoRA) + +Gemma is a non-LLaMA architecture requiring GEGLU feed-forward layers, which means +new forward + backward operators must be implemented before LoRA finetuning becomes +functional. The reference implementation for this exists in the [Gemma integration +PR](https://github.com/tetherto/qvac-ext-lib-llama.cpp/pull/63). + +This PR demonstrates a complete integration path: inference, instruction fine-tuning, +adapter merging, making it an ideal template when porting additional architectures. + +### Troubleshooting + +- **Out of memory**: Reduce context length (`-c 256`), lower rank, or use fewer target modules +- **Poor quality**: Increase rank, add more target modules, or train longer +- **Large adapter**: Reduce rank or limit target modules +- **Checkpoint issues**: Ensure checkpoint directory contains all required files (`model.gguf`, `optimizer.gguf`, `metadata.txt`) + +### Help + +Run with `--help` or `-h` to see all available parameters: +```sh +./build/bin/llama-finetune-lora --help +``` diff --git a/examples/training/finetune-lora.cpp b/examples/training/finetune-lora.cpp new file mode 100644 index 000000000000..40430d1a7a09 --- /dev/null +++ b/examples/training/finetune-lora.cpp @@ -0,0 +1,993 @@ +#include "arg.h" +#include "common.h" +#include "log.h" +#include "llama.h" +#include "ggml-backend.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) +#pragma warning(disable: 4244 4267) // possible loss of data +#endif + + +struct checkpoint_callback_data; +static checkpoint_callback_data* g_checkpoint_data = nullptr; + +enum class lora_lr_schedule_type : std::uint8_t { + CONSTANT, + COSINE, + LINEAR, +}; + +// TODO: Ideally, training configuration variables should be added to common.h and +// parsed using the existing common_params_parse (or loaded from a config file) +// to reuse the existing parser and reduce boilerplate CLI parsing code. +struct lora_lr_scheduler_state { + lora_lr_schedule_type schedule = lora_lr_schedule_type::CONSTANT; + float lr_init = 1e-5f; + float lr_min = 0.0f; + float weight_decay = 0.0f; + int64_t total_steps = 0; + int64_t current_step = 0; + float last_lr = 0.0f; + float warmup_ratio = 0.0f; + int64_t warmup_steps = 0; +}; + +static bool lora_lr_scheduler_type_from_string(const std::string & name, lora_lr_schedule_type & out) { + auto equals = [](const std::string & lhs, const char * rhs) { + const size_t rhs_len = std::strlen(rhs); + if (lhs.size() != rhs_len) { + return false; + } + for (size_t i = 0; i < rhs_len; ++i) { + if (std::tolower(static_cast(lhs[i])) != + std::tolower(static_cast(rhs[i]))) { + return false; + } + } + return true; + }; + + if (equals(name, "constant")) { + out = lora_lr_schedule_type::CONSTANT; + return true; + } + if (equals(name, "cosine")) { + out = lora_lr_schedule_type::COSINE; + return true; + } + if (equals(name, "linear")) { + out = lora_lr_schedule_type::LINEAR; + return true; + } + return false; +} + +static const char * lora_lr_scheduler_type_to_cstr(lora_lr_schedule_type type) { + switch (type) { + case lora_lr_schedule_type::LINEAR: return "linear"; + case lora_lr_schedule_type::COSINE: return "cosine"; + case lora_lr_schedule_type::CONSTANT: return "constant"; + } + return "constant"; +} + +static float lora_scheduler_lr_for_step(const lora_lr_scheduler_state & state, int64_t step) { + + if (state.total_steps <= 0) { + return std::max(state.lr_init, 0.0f); + } + + const int64_t clamped_step = std::min(std::max(step, 0), state.total_steps); + const int64_t warmup_steps = std::min(std::max(state.warmup_steps, 0), state.total_steps); + + if (warmup_steps > 0 && clamped_step < warmup_steps) { + const float warmup_progress = static_cast(clamped_step) / static_cast(warmup_steps); + const float lr = state.lr_init * warmup_progress; + return std::max(lr, 0.0f); + } + + const int64_t adjusted_step = clamped_step - warmup_steps; + int64_t remaining_steps = state.total_steps - warmup_steps; + if (remaining_steps <= 0) { + remaining_steps = 1; + } + + const float progress = std::min(static_cast(adjusted_step) / static_cast(remaining_steps), 1.0f); + float lr = state.lr_init; + + switch (state.schedule) { + case lora_lr_schedule_type::CONSTANT: + lr = state.lr_init; + break; + case lora_lr_schedule_type::COSINE: { + constexpr float kPi = 3.14159265358979323846f; + const float cosine = 0.5f * (1.0f + std::cos(progress * kPi)); + lr = state.lr_min + (state.lr_init - state.lr_min) * cosine; + break; + } + case lora_lr_schedule_type::LINEAR: { + lr = state.lr_init + (state.lr_min - state.lr_init) * progress; + break; + } + } + + return std::max(lr, 0.0f); +} + +static struct ggml_opt_optimizer_params lora_scheduler_get_optimizer_params(void * userdata) { + auto * scheduler = static_cast(userdata); + struct ggml_opt_optimizer_params params = ggml_opt_get_default_optimizer_params(nullptr); + + if (!scheduler) { + return params; + } + + const float lr = lora_scheduler_lr_for_step(*scheduler, scheduler->current_step+1); + scheduler->last_lr = lr; + + params.adamw.alpha = lr; + params.adamw.wd = scheduler->weight_decay; + + params.sgd.alpha = lr; + params.sgd.wd = scheduler->weight_decay; + + if (scheduler->current_step < scheduler->total_steps) { + scheduler->current_step++; + } + + return params; +} + +static uint32_t parse_lora_modules(const std::string& modules_str) { + if (modules_str.empty()) { + return LLAMA_LORA_TARGET_ATTN_Q | LLAMA_LORA_TARGET_ATTN_K | LLAMA_LORA_TARGET_ATTN_V | LLAMA_LORA_TARGET_ATTN_O; + } + + static const std::map module_map = { + {"attn_q", LLAMA_LORA_TARGET_ATTN_Q}, + {"attn_k", LLAMA_LORA_TARGET_ATTN_K}, + {"attn_v", LLAMA_LORA_TARGET_ATTN_V}, + {"attn_o", LLAMA_LORA_TARGET_ATTN_O}, + {"ffn_gate", LLAMA_LORA_TARGET_FFN_GATE}, + {"ffn_up", LLAMA_LORA_TARGET_FFN_UP}, + {"ffn_down", LLAMA_LORA_TARGET_FFN_DOWN}, + {"output", LLAMA_LORA_TARGET_OUTPUT}, + {"all", LLAMA_LORA_TARGET_ALL} + }; + + uint32_t target_modules = 0; + std::stringstream ss(modules_str); + std::string module; + + while (std::getline(ss, module, ',')) { + module.erase(0, module.find_first_not_of(" \t")); + module.erase(module.find_last_not_of(" \t") + 1); + + auto it = module_map.find(module); + if (it != module_map.end()) { + target_modules |= it->second; + LOG_INF("Added target module: %s\n", module.c_str()); + } else { + LOG_ERR("Unknown LoRA target module: %s\n", module.c_str()); + LOG_ERR("Available modules: attn_q, attn_k, attn_v, attn_o, ffn_gate, ffn_up, ffn_down, output, all\n"); + return 0; + } + } + + return target_modules; +} + +static bool training_supports_out_prod_f16(const common_params & params) { + std::vector devices; + + if (!params.devices.empty()) { + devices.assign(params.devices.begin(), params.devices.end()); + } else { + ggml_backend_dev_t gpu = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_GPU); + if (gpu) { + devices.push_back(gpu); + } + } + + if (devices.empty()) { + return true; + } + + constexpr int64_t ne0 = 4; + constexpr int64_t ne1 = 3; + constexpr int64_t k = 2; + + struct ggml_tensor src0 = {}; + struct ggml_tensor src1 = {}; + struct ggml_tensor dst = {}; + + src0.type = GGML_TYPE_F16; + src1.type = GGML_TYPE_F32; + dst.type = GGML_TYPE_F32; + + src0.ne[0] = ne0; src0.ne[1] = k; src0.ne[2] = 1; src0.ne[3] = 1; + src1.ne[0] = ne1; src1.ne[1] = k; src1.ne[2] = 1; src1.ne[3] = 1; + dst.ne [0] = ne0; dst.ne [1] = ne1; dst.ne [2] = 1; dst.ne [3] = 1; + + src0.nb[0] = sizeof(ggml_fp16_t); + src0.nb[1] = src0.nb[0] * ne0; + src0.nb[2] = src0.nb[1] * k; + src0.nb[3] = src0.nb[2] * 1; + + src1.nb[0] = sizeof(float); + src1.nb[1] = src1.nb[0] * ne1; + src1.nb[2] = src1.nb[1] * k; + src1.nb[3] = src1.nb[2] * 1; + + dst.nb[0] = sizeof(float); + dst.nb[1] = dst.nb[0] * ne0; + dst.nb[2] = dst.nb[1] * ne1; + dst.nb[3] = dst.nb[2] * 1; + + dst.op = GGML_OP_OUT_PROD; + dst.src[0] = &src0; + dst.src[1] = &src1; + + for (ggml_backend_dev_t dev : devices) { + if (dev == nullptr) { + continue; + } + if (ggml_backend_dev_type(dev) != GGML_BACKEND_DEVICE_TYPE_GPU) { + continue; + } + if (!ggml_backend_dev_supports_op(dev, &dst)) { + return false; + } + } + + return true; +} + +static void print_lora_usage() { + printf("\n----- LoRA Fine-tuning Parameters -----\n"); + printf(" --lora-rank N LoRA rank (default: 8, range: 1-512)\n"); + printf(" --lora-alpha N LoRA alpha scaling factor (default: 16.0, range: 0.1-1000.0)\n"); + printf(" --lora-modules MODULES Target modules as comma-separated list (default: attn_q,attn_k,attn_v,attn_o)\n"); + printf(" Available modules: attn_q, attn_k, attn_v, attn_o, ffn_gate, ffn_up, ffn_down, output, all\n"); + printf(" Examples: \"attn_q,attn_v\" or \"all\" or \"attn_q,attn_k,attn_v,attn_o,ffn_gate,ffn_up,ffn_down\"\n"); + printf(" --output-adapter PATH Output path for trained adapter (default: auto-generated)\n"); + printf("\nTraining Options:\n"); + printf(" --num-epochs N Number of training epochs (default: 1)\n"); + printf(" --assistant-loss-only Use JSON dataset format with masked loss (ChatML/conversation format)\n"); + printf(" Only computes loss on assistant responses, not system/user prompts\n"); + printf(" --chat-template PATH Optional Jinja chat template to render JSON dataset (matches HF apply_chat_template)\n"); + printf(" --learning-rate F AdamW learning rate (default: 1e-5)\n"); + printf(" --weight-decay F AdamW weight decay (default: 1e-2)\n"); + printf(" --lr-scheduler TYPE Learning rate scheduler: constant, cosine, linear (default: constant)\n"); + printf(" --lr-min F Minimum LR for cosine/linear schedulers (default: 0)\n"); + printf(" --warmup-ratio F Fraction of total steps for LR warmup (default: 0.0)\n"); + printf(" --warmup-steps N Explicit warmup steps (overrides warmup-ratio)\n"); + printf("\nCheckpointing Options:\n"); + printf(" --checkpoint-save-steps N Save checkpoint every N training steps (default: 100)\n"); + printf(" --checkpoint-save-dir PATH Directory for checkpoints (default: ./checkpoints)\n"); + printf(" --resume-from PATH Resume training from specific checkpoint file\n"); + printf(" --auto-resume Automatically resume from latest checkpoint in save dir\n"); + printf("\nExamples:\n"); + printf(" # Train with rank=16, alpha=32, all attention modules\n"); + printf(" %s -m model.gguf -f dataset.txt --lora-rank 16 --lora-alpha 32 --lora-modules attn_q,attn_k,attn_v,attn_o\n", "finetune-lora"); + printf("\n # Fine-tune existing adapter with all modules\n"); + printf(" %s -m model.gguf -f dataset.txt --lora existing.gguf --output-adapter improved.gguf\n", "finetune-lora"); + printf("\n # Instruction fine-tuning with ChatML format\n"); + printf(" %s -m model.gguf -f conversations.jsonl --assistant-loss-only --lora-rank 16\n", "finetune-lora"); + printf("\n"); +} + +struct checkpoint_metadata { + int32_t epoch; + int32_t lora_rank; + float lora_alpha; + uint32_t target_modules; +}; + +static std::string get_checkpoint_filename(const std::string& checkpoint_dir, int64_t step) { + std::ostringstream oss; + oss << checkpoint_dir << "/checkpoint_step_" << std::setfill('0') << std::setw(8) << step; + return oss.str(); +} + +static std::string find_latest_checkpoint(const std::string& checkpoint_dir) { + if (!std::filesystem::exists(checkpoint_dir)) { + return ""; + } + + std::string latest_checkpoint; + int64_t latest_step = -1; + + for (const auto& entry : std::filesystem::directory_iterator(checkpoint_dir)) { + if (entry.is_directory()) { + std::string dirname = entry.path().filename().string(); + if (dirname.find("checkpoint_step_") == 0 && dirname.size() >= 16) { + std::string step_str = dirname.substr(16, 8); + try { + int64_t step = std::stoll(step_str); + if (step > latest_step) { + latest_step = step; + latest_checkpoint = entry.path().string(); + } + } catch (const std::exception&) { + continue; + } + } + } + } + + return latest_checkpoint; +} + +static bool save_checkpoint(llama_context* ctx, llama_adapter_lora* adapter, const checkpoint_metadata& metadata, const std::string& checkpoint_dir) { + if (!std::filesystem::exists(checkpoint_dir)) { + if (!std::filesystem::create_directories(checkpoint_dir)) { + LOG_ERR("Failed to create checkpoint directory: %s\n", checkpoint_dir.c_str()); + return false; + } + } + + if (!llama_lora_save_checkpoint(adapter, checkpoint_dir.c_str(), llama_get_model(ctx), ctx)) { + LOG_ERR("Failed to save LoRA checkpoint\n"); + return false; + } + + std::string meta_path = checkpoint_dir + "/metadata.txt"; + std::ofstream meta_file(meta_path); + if (meta_file.is_open()) { + meta_file << "epoch=" << metadata.epoch << "\n"; + meta_file << "lora_rank=" << metadata.lora_rank << "\n"; + meta_file << "lora_alpha=" << metadata.lora_alpha << "\n"; + meta_file << "target_modules=" << metadata.target_modules << "\n"; + meta_file.close(); + } else { + LOG_ERR("Failed to save checkpoint metadata\n"); + return false; + } + + LOG_INF("Checkpoint saved successfully to %s\n", checkpoint_dir.c_str()); + return true; +} + +static bool validate_checkpoint_metadata(const std::string& checkpoint_path, checkpoint_metadata& metadata) { + std::string checkpoint_dir = checkpoint_path; + + if (!std::filesystem::exists(checkpoint_dir)) { + LOG_ERR("Checkpoint directory does not exist: %s\n", checkpoint_dir.c_str()); + return false; + } + + LOG_INF("Loading checkpoint from: %s\n", checkpoint_dir.c_str()); + + std::string meta_path = checkpoint_dir + "/metadata.txt"; + if (std::filesystem::exists(meta_path)) { + std::ifstream meta_file(meta_path); + if (meta_file.is_open()) { + std::string line; + while (std::getline(meta_file, line)) { + size_t eq_pos = line.find('='); + if (eq_pos != std::string::npos) { + std::string key = line.substr(0, eq_pos); + std::string value = line.substr(eq_pos + 1); + + if (key == "epoch") { + metadata.epoch = std::stoi(value); + } else if (key == "lora_rank") { + metadata.lora_rank = std::stoi(value); + } else if (key == "lora_alpha") { + metadata.lora_alpha = std::stof(value); + } else if (key == "target_modules") { + metadata.target_modules = std::stoul(value); + } + } + } + meta_file.close(); + } else { + LOG_ERR("Failed to open checkpoint metadata file\n"); + return false; + } + } else { + LOG_ERR("Checkpoint metadata file not found: %s\n", meta_path.c_str()); + return false; + } + + LOG_INF("Checkpoint loaded successfully\n"); + return true; +} + + +struct checkpoint_callback_data { + llama_context* ctx; + llama_adapter_lora* adapter; + int32_t checkpoint_save_steps; + std::string checkpoint_save_dir; + int64_t global_step; + int64_t initial_step; + int32_t current_epoch; + int32_t lora_rank; + float lora_alpha; + uint32_t target_modules; + float learning_rate; + lora_lr_scheduler_state * lr_scheduler; + std::string model_path; + std::string dataset_path; +}; + +static void checkpoint_progress_callback( + bool train, + ggml_opt_context_t opt_ctx, + ggml_opt_dataset_t dataset, + ggml_opt_result_t result, + int64_t ibatch, + int64_t ibatch_max, + int64_t t_start_us) { + ggml_opt_epoch_callback_progress_bar(train, opt_ctx, dataset, result, ibatch, ibatch_max, t_start_us); + + if (!train) { + return; + } + + checkpoint_callback_data* cb_data = g_checkpoint_data; + + if (!cb_data) { + LOG_ERR("Checkpoint callback data is null!\n"); + return; + } + + if (cb_data->lr_scheduler) { + cb_data->learning_rate = lora_scheduler_lr_for_step(*cb_data->lr_scheduler, cb_data->lr_scheduler->current_step+1); + } + + if (cb_data->checkpoint_save_steps <= 0) { + return; + } + + cb_data->global_step++; + + if (cb_data->global_step % cb_data->checkpoint_save_steps == 0) { + if (!cb_data->ctx) { + LOG_ERR("Context is null in checkpoint callback!\n"); + return; + } + + if (!cb_data->adapter) { + LOG_ERR("LoRA adapter is null in checkpoint callback!\n"); + return; + } + + checkpoint_metadata meta = { + /*epoch =*/ cb_data->current_epoch, + /*lora_rank =*/ cb_data->lora_rank, + /*lora_alpha =*/ cb_data->lora_alpha, + /*target_modules =*/ cb_data->target_modules, + }; + + std::string checkpoint_path = get_checkpoint_filename(cb_data->checkpoint_save_dir, cb_data->global_step); + + if (!save_checkpoint(cb_data->ctx, cb_data->adapter, meta, checkpoint_path)) { + LOG_ERR("Failed to save checkpoint at step %" PRId64 "\n", cb_data->global_step); + } + } +} + +struct finetune_params { + int32_t lora_rank = 8; + float lora_alpha = 16.0f; + std::string lora_modules_str; + std::string output_adapter_path; + + int32_t num_epochs = 1; + float learning_rate = 1e-5f; + float lr_min = 0.0f; + float weight_decay = 0.01f; + std::string lr_scheduler = "constant"; + float warmup_ratio = 0.0f; + int64_t warmup_steps = 0; + bool warmup_ratio_set = false; + bool warmup_steps_set = false; + + int32_t checkpoint_save_steps = 100; + std::string checkpoint_save_dir = "./checkpoints"; + std::string resume_from_checkpoint; + bool auto_resume = false; + std::string chat_template_path; + bool assistant_loss_only = false; + uint32_t lora_seed = 42; +}; + +static bool parse_finetune_args(int& argc, char** argv, finetune_params& ft_params) { + auto remove_arg_pair = [&](int i) { + for (int j = i; j < argc - 2; j++) { + argv[j] = argv[j + 2]; + } + argc -= 2; + }; + + auto remove_arg_single = [&](int i) { + for (int j = i; j < argc - 1; j++) { + argv[j] = argv[j + 1]; + } + argc -= 1; + }; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--assistant-loss-only") == 0) { + ft_params.assistant_loss_only = true; + remove_arg_single(i); + i--; + } + } + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--lora-rank") == 0 && i + 1 < argc) { + ft_params.lora_rank = std::atoi(argv[i + 1]); + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--lora-alpha") == 0 && i + 1 < argc) { + ft_params.lora_alpha = std::atof(argv[i + 1]); + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--lora-seed") == 0 && i + 1 < argc) { + ft_params.lora_seed = std::strtoul(argv[i + 1], nullptr, 10); + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--lora-modules") == 0 && i + 1 < argc) { + ft_params.lora_modules_str = argv[i + 1]; + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--output-adapter") == 0 && i + 1 < argc) { + ft_params.output_adapter_path = argv[i + 1]; + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--num-epochs") == 0 && i + 1 < argc) { + ft_params.num_epochs = std::atoi(argv[i + 1]); + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--learning-rate") == 0 && i + 1 < argc) { + ft_params.learning_rate = std::atof(argv[i + 1]); + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--weight-decay") == 0 && i + 1 < argc) { + ft_params.weight_decay = std::atof(argv[i + 1]); + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--lr-scheduler") == 0 && i + 1 < argc) { + ft_params.lr_scheduler = argv[i + 1]; + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--lr-min") == 0 && i + 1 < argc) { + ft_params.lr_min = std::atof(argv[i + 1]); + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--warmup-ratio") == 0 && i + 1 < argc) { + ft_params.warmup_ratio = std::atof(argv[i + 1]); + ft_params.warmup_ratio_set = true; + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--warmup-steps") == 0 && i + 1 < argc) { + ft_params.warmup_steps = std::atoll(argv[i + 1]); + ft_params.warmup_steps_set = true; + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--checkpoint-save-steps") == 0 && i + 1 < argc) { + ft_params.checkpoint_save_steps = std::atoi(argv[i + 1]); + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--checkpoint-save-dir") == 0 && i + 1 < argc) { + ft_params.checkpoint_save_dir = argv[i + 1]; + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--resume-from") == 0 && i + 1 < argc) { + ft_params.resume_from_checkpoint = argv[i + 1]; + remove_arg_pair(i); + i--; + } else if (strcmp(argv[i], "--auto-resume") == 0) { + ft_params.auto_resume = true; + for (int j = i; j < argc - 1; j++) { + argv[j] = argv[j + 1]; + } + argc--; + i--; + } else if (strcmp(argv[i], "--chat-template") == 0 && i + 1 < argc) { + ft_params.chat_template_path = argv[i + 1]; + remove_arg_pair(i); + i--; + } + } + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_lora_usage(); + } + } + + return true; +} + +int main(int argc, char ** argv) { + common_params params; + finetune_params ft_params; + + params.escape = false; + parse_finetune_args(argc, argv, ft_params); + + if (!common_params_parse(argc, argv, params, LLAMA_EXAMPLE_PERPLEXITY)) { + return 1; + } + + lora_lr_schedule_type scheduler_type; + if (!lora_lr_scheduler_type_from_string(ft_params.lr_scheduler, scheduler_type)) { + LOG_ERR("Unknown learning rate scheduler: %s (expected: constant, cosine, linear)\n", ft_params.lr_scheduler.c_str()); + return 1; + } + + if (ft_params.num_epochs <= 0) { + LOG_ERR("Number of epochs must be > 0, got %d\n", ft_params.num_epochs); + return 1; + } + if (ft_params.learning_rate <= 0.0f) { + LOG_ERR("Learning rate must be > 0, got %.4e\n", ft_params.learning_rate); + return 1; + } + if (ft_params.weight_decay < 0.0f) { + LOG_ERR("Weight decay must be >= 0, got %.4e\n", ft_params.weight_decay); + return 1; + } + if (ft_params.lr_min < 0.0f) { + LOG_ERR("Minimum learning rate must be >= 0, got %.4e\n", ft_params.lr_min); + return 1; + } + const bool scheduler_uses_lr_min = scheduler_type == lora_lr_schedule_type::COSINE || + scheduler_type == lora_lr_schedule_type::LINEAR; + if (scheduler_uses_lr_min && ft_params.lr_min > ft_params.learning_rate) { + LOG_ERR("For %s scheduler lr-min (%.4e) cannot exceed learning-rate (%.4e)\n", + lora_lr_scheduler_type_to_cstr(scheduler_type), ft_params.lr_min, ft_params.learning_rate); + return 1; + } + + LOG_INF("Using LoRA parameters: rank=%d, alpha=%.1f, seed=%u\n", ft_params.lora_rank, ft_params.lora_alpha, ft_params.lora_seed); + LOG_INF("Training for %d epochs\n", ft_params.num_epochs); + + // Handle checkpoint auto-resume before model initialization + if (ft_params.auto_resume && ft_params.resume_from_checkpoint.empty()) { + std::string latest_checkpoint = find_latest_checkpoint(ft_params.checkpoint_save_dir); + if (!latest_checkpoint.empty()) { + ft_params.resume_from_checkpoint = latest_checkpoint; + LOG_INF("Auto-resume: found checkpoint %s\n", ft_params.resume_from_checkpoint.c_str()); + } + } + + if (!ft_params.resume_from_checkpoint.empty()) { + params.warmup = false; + } + + // Load checkpoint LoRA adapter from directory structure (model.gguf) + if (!ft_params.resume_from_checkpoint.empty()) { + std::filesystem::path checkpoint_dir(ft_params.resume_from_checkpoint); + std::filesystem::path model_path = checkpoint_dir / "model.gguf"; + + LOG_INF("Loading checkpoint LoRA adapter: %s\n", model_path.c_str()); + common_adapter_lora_info lora_adapter; + lora_adapter.path = model_path.string(); + lora_adapter.scale = 1.0f; + lora_adapter.ptr = nullptr; + params.lora_adapters.clear(); // Remove any existing adapters + params.lora_adapters.push_back(lora_adapter); + LOG_INF("Checkpoint LoRA adapter added to params\n"); + } + + if (params.use_mmap) { + LOG_INF("%s: force disabling memory mapping because it would result in-read-only pointers to the weights\n", __func__); + params.use_mmap = false; + } + const bool supports_out_prod_f16 = training_supports_out_prod_f16(params); + if (!supports_out_prod_f16) { + if (params.cache_type_k != GGML_TYPE_F32) { + LOG_INF("%s: force changing k cache type to f32 due to a lack of f16 support for OUT_PROD\n", __func__); + params.cache_type_k = GGML_TYPE_F32; + } + if (params.cache_type_v != GGML_TYPE_F32) { + LOG_INF("%s: force changing v cache type to f32 due to a lack of f16 support for OUT_PROD\n", __func__); + params.cache_type_v = GGML_TYPE_F32; + } + } + + common_init(); + llama_backend_init(); + llama_numa_init(params.numa); + params.training = true; + + common_init_result llama_init = common_init_from_params(params); + llama_model_ptr & model = llama_init.model; + llama_context_ptr & ctx = llama_init.context; + + if (model == NULL) { + LOG_ERR("%s: unable to load model\n", __func__); + return 1; + } + + { + LOG_INF("\n"); + LOG_INF("%s\n", common_params_get_system_info(params).c_str()); + } + + uint32_t target_modules = parse_lora_modules(ft_params.lora_modules_str); + if (target_modules == 0) { + return 1; + } + + struct llama_lora_training_params lora_params = { + /*target_modules =*/ target_modules, + /*rank =*/ ft_params.lora_rank, + /*alpha =*/ ft_params.lora_alpha, + /*dropout =*/ 0.0f, + /*init_std =*/ 0.02f, + /*seed =*/ ft_params.lora_seed, + }; + + bool has_existing_lora = !params.lora_adapters.empty(); + struct llama_adapter_lora * trained_adapter = nullptr; + + if (has_existing_lora) { + LOG_INF("Finetuning existing LoRA adapters\n"); + LOG_INF("Found %zu existing LoRA adapters to train\n", params.lora_adapters.size()); + trained_adapter = params.lora_adapters[0].ptr; + if (!trained_adapter) { + LOG_ERR("Existing LoRA adapter is null\n"); + return 1; + } + } else { + LOG_INF("Target modules: Q=%s, K=%s, V=%s, O=%s, GATE=%s, UP=%s, DOWN=%s, OUTPUT=%s\n", + (lora_params.target_modules & LLAMA_LORA_TARGET_ATTN_Q) ? "yes" : "no", + (lora_params.target_modules & LLAMA_LORA_TARGET_ATTN_K) ? "yes" : "no", + (lora_params.target_modules & LLAMA_LORA_TARGET_ATTN_V) ? "yes" : "no", + (lora_params.target_modules & LLAMA_LORA_TARGET_ATTN_O) ? "yes" : "no", + (lora_params.target_modules & LLAMA_LORA_TARGET_FFN_GATE) ? "yes" : "no", + (lora_params.target_modules & LLAMA_LORA_TARGET_FFN_UP) ? "yes" : "no", + (lora_params.target_modules & LLAMA_LORA_TARGET_FFN_DOWN) ? "yes" : "no", + (lora_params.target_modules & LLAMA_LORA_TARGET_OUTPUT) ? "yes" : "no"); + + LOG_INF("LoRA configuration: rank=%d, alpha=%.1f (scaling=%.3f)\n", + lora_params.rank, lora_params.alpha, lora_params.alpha / lora_params.rank); + + trained_adapter = llama_lora_training_init(ctx.get(), model.get(), &lora_params); + if (!trained_adapter) { + LOG_ERR("%s: LoRA training initialization failed\n", __func__); + return 1; + } + } + + constexpr float val_split = 0.05f; + + ggml_opt_dataset_t dataset; + + if (ft_params.assistant_loss_only) { + LOG_INF("Using JSON dataset with chat template and assistant-only loss\n"); + dataset = common_opt_sft_dataset_init(ctx.get(), params.prompt, llama_n_ctx(ctx.get())/2, ft_params.chat_template_path); + } else { + std::vector tokens = common_tokenize(ctx.get(), params.prompt, true); + LOG_INF("Using standard next-token prediction mode\n"); + dataset = common_opt_dataset_init(ctx.get(), tokens, llama_n_ctx(ctx.get())/2); + } + + if (dataset == nullptr) { + LOG_ERR("Failed to create dataset. Please check your input file and parameters.\n"); + return 1; + } + + const int64_t total_datapoints = ggml_opt_dataset_ndata(dataset); + const int64_t idata_split = static_cast(total_datapoints * (1.0f - val_split)); + const int64_t training_batches_per_epoch = idata_split; + + if (training_batches_per_epoch <= 0) { + LOG_ERR("Training split is empty. Adjust --val-split or dataset size.\n"); + return 1; + } + + lora_lr_scheduler_state lr_scheduler; + lr_scheduler.schedule = scheduler_type; + lr_scheduler.lr_init = ft_params.learning_rate; + lr_scheduler.lr_min = (scheduler_type == lora_lr_schedule_type::CONSTANT) ? ft_params.learning_rate : ft_params.lr_min; + lr_scheduler.weight_decay = ft_params.weight_decay; + lr_scheduler.total_steps = std::max(1, static_cast(ft_params.num_epochs) * training_batches_per_epoch); + if (ft_params.warmup_steps_set) { + lr_scheduler.warmup_steps = std::min(ft_params.warmup_steps, lr_scheduler.total_steps); + } else if (ft_params.warmup_ratio_set) { + const double warmup_from_ratio = static_cast(lr_scheduler.total_steps) * static_cast(ft_params.warmup_ratio); + lr_scheduler.warmup_steps = std::min(static_cast(warmup_from_ratio), lr_scheduler.total_steps); + } else { + lr_scheduler.warmup_steps = 0; + } + lr_scheduler.warmup_steps = std::max(lr_scheduler.warmup_steps, 0); + if (lr_scheduler.total_steps > 0) { + lr_scheduler.warmup_ratio = static_cast(lr_scheduler.warmup_steps) / static_cast(lr_scheduler.total_steps); + } else { + lr_scheduler.warmup_ratio = 0.0f; + } + lr_scheduler.current_step = 0; + lr_scheduler.last_lr = lora_scheduler_lr_for_step(lr_scheduler, lr_scheduler.current_step); + + LOG_INF("Training split: datapoints=%lld, batches_per_epoch=%lld\n", + (long long) total_datapoints, (long long) training_batches_per_epoch); + LOG_INF("Optimizer: adamw scheduler=%s lr=%.4e wd=%.4e total_steps=%lld\n", + lora_lr_scheduler_type_to_cstr(lr_scheduler.schedule), lr_scheduler.lr_init, + lr_scheduler.weight_decay, (long long) lr_scheduler.total_steps); + if (lr_scheduler.schedule == lora_lr_schedule_type::COSINE) { + LOG_INF("Cosine scheduler: lr-min=%.4e\n", lr_scheduler.lr_min); + } else if (lr_scheduler.schedule == lora_lr_schedule_type::LINEAR) { + LOG_INF("Linear scheduler: lr-min=%.4e\n", lr_scheduler.lr_min); + } + if (lr_scheduler.warmup_steps > 0) { + LOG_INF("Warmup: steps=%lld ratio=%.4f\n", (long long) lr_scheduler.warmup_steps, lr_scheduler.warmup_ratio); + } else if (ft_params.warmup_ratio_set) { + LOG_WRN("Warmup ratio %.4f produced 0 warmup steps (total_steps=%lld); no warmup applied\n", + ft_params.warmup_ratio, (long long) lr_scheduler.total_steps); + } + + int start_epoch = 0; + int64_t start_step = 0; + checkpoint_metadata checkpoint_meta = {}; + bool checkpoint_loaded = false; + + if (!ft_params.resume_from_checkpoint.empty()) { + if (validate_checkpoint_metadata(ft_params.resume_from_checkpoint, checkpoint_meta)) { + start_epoch = checkpoint_meta.epoch; + checkpoint_loaded = true; + + if (checkpoint_meta.lora_rank != ft_params.lora_rank) { + LOG_ERR("Checkpoint LoRA rank (%d) doesn't match current rank (%d). Use --resume-from to manually specify a compatible checkpoint.\n", + checkpoint_meta.lora_rank, ft_params.lora_rank); + return 1; + } + if (checkpoint_meta.lora_alpha != ft_params.lora_alpha) { + LOG_ERR("Checkpoint LoRA alpha (%.3f) doesn't match current alpha (%.3f)\n", + checkpoint_meta.lora_alpha, ft_params.lora_alpha); + return 1; + } + if (checkpoint_meta.target_modules != target_modules) { + LOG_ERR("Checkpoint target_modules doesn't match current target_modules\n"); + return 1; + } + + } else { + LOG_ERR("Failed to load checkpoint, starting from scratch\n"); + } + } + + std::string optimizer_checkpoint_path; + if (checkpoint_loaded && !ft_params.resume_from_checkpoint.empty()) { + std::filesystem::path checkpoint_dir(ft_params.resume_from_checkpoint); + optimizer_checkpoint_path = (checkpoint_dir / "optimizer.gguf").string(); + } + + struct llama_opt_params lopt_params = llama_opt_default_params(); + lopt_params.param_filter = llama_opt_param_filter_lora; + lopt_params.get_opt_pars = lora_scheduler_get_optimizer_params; + lopt_params.get_opt_pars_ud = &lr_scheduler; + lopt_params.checkpoint_path = checkpoint_loaded ? optimizer_checkpoint_path.c_str() : nullptr; + lopt_params.load_optimizer_state = checkpoint_loaded; + lopt_params.assistant_loss_only = ft_params.assistant_loss_only; + + llama_opt_init(ctx.get(), model.get(), lopt_params); + + if (checkpoint_loaded) { + start_step = llama_opt_get_iter(ctx.get()); + } + + lr_scheduler.current_step = std::min(start_step, lr_scheduler.total_steps); + lr_scheduler.last_lr = lora_scheduler_lr_for_step(lr_scheduler, lr_scheduler.current_step); + + if (!trained_adapter) { + LOG_ERR("No trained adapter available for checkpointing\n"); + return 1; + } + + if (start_step > 0) { + int64_t completed_epochs = start_step / training_batches_per_epoch; + start_epoch = (int)completed_epochs; + LOG_INF("Resuming training from global step %lld (lr=%.4e)\n", + (long long) start_step, lr_scheduler.last_lr); + } + + checkpoint_callback_data cb_data = { + /*ctx =*/ ctx.get(), + /*adapter =*/ trained_adapter, + /*checkpoint_save_steps =*/ ft_params.checkpoint_save_steps, + /*checkpoint_save_dir =*/ ft_params.checkpoint_save_dir, + /*global_step =*/ start_step, + /*initial_step =*/ start_step, + /*current_epoch =*/ start_epoch, + /*lora_rank =*/ ft_params.lora_rank, + /*lora_alpha =*/ ft_params.lora_alpha, + /*target_modules =*/ target_modules, + /*learning_rate =*/ lr_scheduler.last_lr, + /*lr_scheduler =*/ &lr_scheduler, + /*model_path =*/ params.model.path, + /*dataset_path =*/ params.prompt_file, + }; + g_checkpoint_data = &cb_data; + + ggml_opt_result_t result_train = ggml_opt_result_init(); + ggml_opt_result_t result_eval = ggml_opt_result_init(); + + for (int epoch = start_epoch; epoch < ft_params.num_epochs; ++epoch) { + if (cb_data.lr_scheduler) { + cb_data.learning_rate = lora_scheduler_lr_for_step(*cb_data.lr_scheduler, cb_data.lr_scheduler->current_step+1); + } + LOG_INF("Starting epoch %d (step %lld, lr=%.4e)\n", epoch, (long long)cb_data.global_step, cb_data.learning_rate); + cb_data.current_epoch = epoch; + + int64_t resume_batch = 0; + if (start_step > 0 && epoch == start_epoch) { + resume_batch = start_step % training_batches_per_epoch; + } + + ggml_opt_epoch_callback train_callback = (ft_params.checkpoint_save_steps <= 0) ? + ggml_opt_epoch_callback_progress_bar : checkpoint_progress_callback; + ggml_opt_epoch_callback eval_callback = (ft_params.checkpoint_save_steps <= 0) ? + ggml_opt_epoch_callback_progress_bar : checkpoint_progress_callback; + + if (resume_batch > 0) { + LOG_INF("Resuming training from epoch %d, step %" PRId64 " \n", epoch, resume_batch); + } else if (ft_params.checkpoint_save_steps > 0) { + LOG_INF("Checkpointing enabled, saving every %d steps\n", ft_params.checkpoint_save_steps); + } else { + LOG_INF("Checkpointing disabled, using standard progress callback\n"); + } + + llama_opt_epoch_resume(ctx.get(), dataset, result_train, result_eval, idata_split, + train_callback, eval_callback, resume_batch); + fprintf(stderr, "\n"); + + ggml_opt_result_reset(result_train); + ggml_opt_result_reset(result_eval); + } + + g_checkpoint_data = nullptr; + ggml_opt_result_free(result_train); + ggml_opt_result_free(result_eval); + + std::string adapter_filename; + if (!ft_params.output_adapter_path.empty()) { + adapter_filename = ft_params.output_adapter_path; + } else if (has_existing_lora) { + adapter_filename = "finetuned-lora-adapter.gguf"; + LOG_INF("Finetuned existing lora adapter, saving as: %s\n", adapter_filename.c_str()); + } else { + adapter_filename = "trained-lora-adapter.gguf"; + LOG_INF("Saving new lora adapter: %s\n", adapter_filename.c_str()); + } + + if (trained_adapter) { + if (llama_lora_save_adapter(trained_adapter, adapter_filename.c_str(), model.get())) { + std::ifstream adapter_file(adapter_filename, std::ios::binary | std::ios::ate); + if (adapter_file.is_open()) { + std::streamsize adapter_size = adapter_file.tellg(); + LOG_INF("LoRA adapter saved: %s (%.2f MB)\n", + adapter_filename.c_str(), adapter_size / (1024.0 * 1024.0)); + adapter_file.close(); + } + } else { + LOG_ERR("Failed to save LoRA adapter\n"); + } + } else { + LOG_ERR("No trained adapter available for saving\n"); + } + + llama_backend_free(); + + return 0; +} diff --git a/examples/training/finetune.cpp b/examples/training/finetune.cpp index 416d8d8f6c8f..a7bd01e1575d 100644 --- a/examples/training/finetune.cpp +++ b/examples/training/finetune.cpp @@ -2,6 +2,7 @@ #include "common.h" #include "log.h" #include "llama.h" +#include "ggml-backend.h" #include #include @@ -13,6 +14,72 @@ #pragma warning(disable: 4244 4267) // possible loss of data #endif +static bool training_supports_out_prod_f16(const common_params & params) { + std::vector devices; + + if (!params.devices.empty()) { + devices.assign(params.devices.begin(), params.devices.end()); + } else { + ggml_backend_dev_t gpu = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_GPU); + if (gpu) { + devices.push_back(gpu); + } + } + + if (devices.empty()) { + return true; + } + + constexpr int64_t ne0 = 4; + constexpr int64_t ne1 = 3; + constexpr int64_t k = 2; + + struct ggml_tensor src0 = {}; + struct ggml_tensor src1 = {}; + struct ggml_tensor dst = {}; + + src0.type = GGML_TYPE_F16; + src1.type = GGML_TYPE_F32; + dst.type = GGML_TYPE_F32; + + src0.ne[0] = ne0; src0.ne[1] = k; src0.ne[2] = 1; src0.ne[3] = 1; + src1.ne[0] = ne1; src1.ne[1] = k; src1.ne[2] = 1; src1.ne[3] = 1; + dst.ne [0] = ne0; dst.ne [1] = ne1; dst.ne [2] = 1; dst.ne [3] = 1; + + src0.nb[0] = sizeof(ggml_fp16_t); + src0.nb[1] = src0.nb[0] * ne0; + src0.nb[2] = src0.nb[1] * k; + src0.nb[3] = src0.nb[2] * 1; + + src1.nb[0] = sizeof(float); + src1.nb[1] = src1.nb[0] * ne1; + src1.nb[2] = src1.nb[1] * k; + src1.nb[3] = src1.nb[2] * 1; + + dst.nb[0] = sizeof(float); + dst.nb[1] = dst.nb[0] * ne0; + dst.nb[2] = dst.nb[1] * ne1; + dst.nb[3] = dst.nb[2] * 1; + + dst.op = GGML_OP_OUT_PROD; + dst.src[0] = &src0; + dst.src[1] = &src1; + + for (ggml_backend_dev_t dev : devices) { + if (dev == nullptr) { + continue; + } + if (ggml_backend_dev_type(dev) != GGML_BACKEND_DEVICE_TYPE_GPU) { + continue; + } + if (!ggml_backend_dev_supports_op(dev, &dst)) { + return false; + } + } + + return true; +} + int main(int argc, char ** argv) { common_params params; params.escape = false; @@ -26,13 +93,16 @@ int main(int argc, char ** argv) { __func__); params.use_mmap = false; } - if (params.cache_type_k != GGML_TYPE_F32) { - LOG_INF("%s: force changing k cache type to f32 due to a lack of f16 support for OUT_PROD\n", __func__); - params.cache_type_k = GGML_TYPE_F32; - } - if (params.cache_type_v != GGML_TYPE_F32) { - LOG_INF("%s: force changing v cache type to f32 due to a lack of f16 support for OUT_PROD\n", __func__); - params.cache_type_v = GGML_TYPE_F32; + const bool supports_out_prod_f16 = training_supports_out_prod_f16(params); + if (!supports_out_prod_f16) { + if (params.cache_type_k != GGML_TYPE_F32) { + LOG_INF("%s: force changing k cache type to f32 due to a lack of f16 support for OUT_PROD\n", __func__); + params.cache_type_k = GGML_TYPE_F32; + } + if (params.cache_type_v != GGML_TYPE_F32) { + LOG_INF("%s: force changing v cache type to f32 due to a lack of f16 support for OUT_PROD\n", __func__); + params.cache_type_v = GGML_TYPE_F32; + } } common_init(); @@ -54,22 +124,18 @@ int main(int argc, char ** argv) { LOG_INF("%s\n", common_params_get_system_info(params).c_str()); } - std::vector tokens = common_tokenize(ctx.get(), params.prompt, true); - ggml_opt_dataset_t dataset = common_opt_dataset_init(ctx.get(), tokens, llama_n_ctx(ctx.get()) / 2); + std::vector tokens = common_tokenize(ctx.get(), params.prompt, true); + ggml_opt_dataset_t dataset = common_opt_dataset_init(ctx.get(), tokens, llama_n_ctx(ctx.get())/2); struct lr_opt & lr = params.lr; LOG_INF("-optimizer %s -lr0 %.2g -wd %.2g -lr-min %.2g -min-epochs %.2g -epochs %d -period %.2g -val %.2g\n", ggml_opt_optimizer_name(params.optimizer), (double) lr.lr0, (double) lr.wd, (double) lr.lr_min, (double) lr.decay_epochs, (unsigned) lr.epochs, (double) params.n_batch / params.n_ubatch, (double) params.val_split); - struct llama_opt_params lopt_params{ - /*n_ctx_train =*/0, - /*param_filter =*/llama_opt_param_filter_all, - /*param_filter_ud =*/nullptr, - /*get_opt_pars =*/common_opt_lr_pars, - /*get_opt_pars_ud =*/¶ms.lr, - /*optimizer_type =*/params.optimizer, - }; + struct llama_opt_params lopt_params = llama_opt_default_params(); + lopt_params.get_opt_pars = common_opt_lr_pars; + lopt_params.get_opt_pars_ud = ¶ms.lr; + lopt_params.optimizer_type = params.optimizer; llama_opt_init(ctx.get(), model.get(), lopt_params); const int64_t idata_split = ggml_opt_dataset_ndata(dataset) * (1.0f - params.val_split); @@ -79,7 +145,7 @@ int main(int argc, char ** argv) { for (lr.epoch = 0; lr.epoch < lr.epochs; ++lr.epoch) { llama_opt_epoch(ctx.get(), dataset, result_train, result_eval, idata_split, - ggml_opt_epoch_callback_progress_bar, ggml_opt_epoch_callback_progress_bar); + ggml_opt_epoch_callback_progress_bar, ggml_opt_epoch_callback_progress_bar); fprintf(stderr, "\n"); ggml_opt_result_reset(result_train); diff --git a/ggml/CMakeLists.txt b/ggml/CMakeLists.txt index db47ae8dff2f..7d3723e7cf69 100644 --- a/ggml/CMakeLists.txt +++ b/ggml/CMakeLists.txt @@ -216,6 +216,7 @@ option(GGML_HIP_EXPORT_METRICS "ggml: enable kernel perf metrics ou option(GGML_MUSA_GRAPHS "ggml: use MUSA graph, experimental, unstable" OFF) option(GGML_MUSA_MUDNN_COPY "ggml: enable muDNN for accelerated copy" OFF) option(GGML_VULKAN "ggml: use Vulkan" OFF) +option(GGML_VULKAN_BUILD_ADRENO_SHADERS "ggml: build Adreno-supported shader variants" ON) option(GGML_VULKAN_CHECK_RESULTS "ggml: run Vulkan op checks" OFF) option(GGML_VULKAN_DEBUG "ggml: enable Vulkan debug output" OFF) option(GGML_VULKAN_MEMORY_DEBUG "ggml: enable Vulkan memory debug output" OFF) @@ -325,8 +326,15 @@ set_target_properties(ggml PROPERTIES PUBLIC_HEADER "${GGML_PUBLIC_HEADERS}") #if (GGML_METAL) # set_target_properties(ggml PROPERTIES RESOURCE "${CMAKE_CURRENT_SOURCE_DIR}/src/ggml-metal.metal") #endif() -install(TARGETS ggml LIBRARY PUBLIC_HEADER) -install(TARGETS ggml-base LIBRARY) +install( + TARGETS ggml ggml-base + EXPORT ggml-targets) + +install( + EXPORT ggml-targets + FILE ggml-targets.cmake + NAMESPACE ggml:: + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/ggml) if (GGML_STANDALONE) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ggml.pc.in @@ -377,7 +385,7 @@ set(GGML_BIN_INSTALL_DIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Location of configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/cmake/ggml-config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/ggml-config.cmake - INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ggml + INSTALL_DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/ggml PATH_VARS GGML_INCLUDE_INSTALL_DIR GGML_LIB_INSTALL_DIR GGML_BIN_INSTALL_DIR) @@ -396,8 +404,7 @@ message(STATUS "ggml commit: ${GGML_BUILD_COMMIT}") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ggml-config.cmake ${CMAKE_CURRENT_BINARY_DIR}/ggml-version.cmake - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ggml) - + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/ggml) if (MSVC) set(MSVC_WARNING_FLAGS /wd4005 # Macro redefinition diff --git a/ggml/cmake/ggml-config.cmake.in b/ggml/cmake/ggml-config.cmake.in index 91c9d5cd3434..97e44a000fc1 100644 --- a/ggml/cmake/ggml-config.cmake.in +++ b/ggml/cmake/ggml-config.cmake.in @@ -95,12 +95,22 @@ if (NOT GGML_SHARED_LIB) list(APPEND GGML_SYCL_INTERFACE_LINK_LIBRARIES IntelSYCL::SYCL_CXX MKL::MKL MKL::MKL_SYCL) endif() endif() + + if (GGML_OPENCL) + find_package(PkgConfig REQUIRED) + pkg_check_modules(OpenCL REQUIRED IMPORTED_TARGET OpenCL) + list(APPEND GGML_OPENCL_INTERFACE_LINK_LIBRARIES PkgConfig::OpenCL) + endif() + endif() set_and_check(GGML_INCLUDE_DIR "@PACKAGE_GGML_INCLUDE_INSTALL_DIR@") set_and_check(GGML_LIB_DIR "@PACKAGE_GGML_LIB_INSTALL_DIR@") #set_and_check(GGML_BIN_DIR "@PACKAGE_GGML_BIN_INSTALL_DIR@") +# Include the exported targets file +include("${CMAKE_CURRENT_LIST_DIR}/ggml-targets.cmake") + if(NOT TARGET ggml::ggml) find_package(Threads REQUIRED) diff --git a/ggml/include/ggml-opt.h b/ggml/include/ggml-opt.h index 4703a05afe19..8ee2dc97b6e8 100644 --- a/ggml/include/ggml-opt.h +++ b/ggml/include/ggml-opt.h @@ -31,6 +31,7 @@ extern "C" { GGML_OPT_LOSS_TYPE_MEAN, GGML_OPT_LOSS_TYPE_SUM, GGML_OPT_LOSS_TYPE_CROSS_ENTROPY, + GGML_OPT_LOSS_TYPE_CROSS_ENTROPY_MASKED, GGML_OPT_LOSS_TYPE_MEAN_SQUARED_ERROR, }; @@ -43,12 +44,23 @@ extern "C" { int64_t ne_label, // number of elements per label int64_t ndata, // total number of datapoints/labels int64_t ndata_shard); // number of datapoints/labels per shard (unit at which the dataset is shuffled/copied) + + GGML_API ggml_opt_dataset_t ggml_opt_dataset_init_with_masks( + enum ggml_type type_data, // the type for the internal data tensor + enum ggml_type type_label, // the type for the internal labels tensor + enum ggml_type type_mask, // the type for the internal masks tensor + int64_t ne_datapoint, // number of elements per datapoint + int64_t ne_label, // number of elements per label + int64_t ne_mask, // number of elements per mask + int64_t ndata, // total number of datapoints/labels + int64_t ndata_shard); // number of datapoints/labels per shard GGML_API void ggml_opt_dataset_free(ggml_opt_dataset_t dataset); // get underlying tensors that store the data GGML_API int64_t ggml_opt_dataset_ndata (ggml_opt_dataset_t dataset); GGML_API struct ggml_tensor * ggml_opt_dataset_data (ggml_opt_dataset_t dataset); // shape = [ne_datapoint, ndata] GGML_API struct ggml_tensor * ggml_opt_dataset_labels(ggml_opt_dataset_t dataset); // shape = [nd_label, ndata] + GGML_API struct ggml_tensor * ggml_opt_dataset_masks (ggml_opt_dataset_t dataset); // shape = [ne_datapoint, ndata], can be null // shuffle idata first datapoints from dataset with RNG from opt_ctx, shuffle all datapoints if idata is negative GGML_API void ggml_opt_dataset_shuffle(ggml_opt_context_t opt_ctx, ggml_opt_dataset_t dataset, int64_t idata); @@ -65,6 +77,13 @@ extern "C" { size_t nb_data_batch, void * labels_batch, int64_t ibatch); + GGML_API void ggml_opt_dataset_get_batch_host_with_masks( + ggml_opt_dataset_t dataset, + void * data_batch, + size_t nb_data_batch, + void * labels_batch, + void * masks_batch, + int64_t ibatch); // ====== Model / Context ====== @@ -148,6 +167,7 @@ extern "C" { GGML_API struct ggml_tensor * ggml_opt_inputs( ggml_opt_context_t opt_ctx); // forward graph input tensor GGML_API struct ggml_tensor * ggml_opt_outputs( ggml_opt_context_t opt_ctx); // forward graph output tensor GGML_API struct ggml_tensor * ggml_opt_labels( ggml_opt_context_t opt_ctx); // labels to compare outputs against + GGML_API struct ggml_tensor * ggml_opt_masks( ggml_opt_context_t opt_ctx); // assistant masks for instruction tuning, can be null GGML_API struct ggml_tensor * ggml_opt_loss( ggml_opt_context_t opt_ctx); // scalar tensor that contains the loss GGML_API struct ggml_tensor * ggml_opt_pred( ggml_opt_context_t opt_ctx); // predictions made by outputs GGML_API struct ggml_tensor * ggml_opt_ncorrect(ggml_opt_context_t opt_ctx); // number of matching predictions between outputs and labels @@ -155,6 +175,19 @@ extern "C" { // get the gradient accumulator for a node from the forward graph GGML_API struct ggml_tensor * ggml_opt_grad_acc(ggml_opt_context_t opt_ctx, struct ggml_tensor * node); + // get optimizer state tensors (momentum and variance for AdamW) + GGML_API int64_t ggml_opt_get_iter(ggml_opt_context_t opt_ctx); + GGML_API void ggml_opt_set_iter(ggml_opt_context_t opt_ctx, int64_t iter); + GGML_API int32_t ggml_opt_get_nparams(ggml_opt_context_t opt_ctx); + GGML_API struct ggml_tensor * ggml_opt_get_grad_m(ggml_opt_context_t opt_ctx, int32_t index); + GGML_API struct ggml_tensor * ggml_opt_get_grad_v(ggml_opt_context_t opt_ctx, int32_t index); + + // ====== Optimizer State Persistence ====== + + GGML_API bool ggml_opt_save_state(ggml_opt_context_t opt_ctx, const char* filename); + GGML_API bool ggml_opt_load_state(ggml_opt_context_t opt_ctx, const char* filename); + GGML_API bool ggml_opt_load_tensors(ggml_opt_context_t opt_ctx, const char* filename); + GGML_API enum ggml_opt_optimizer_type ggml_opt_context_optimizer_type(ggml_opt_context_t); //TODO consistent naming scheme GGML_API const char * ggml_opt_optimizer_name(enum ggml_opt_optimizer_type); diff --git a/ggml/include/ggml.h b/ggml/include/ggml.h index 48da68fe7e3e..d8526feaa712 100644 --- a/ggml/include/ggml.h +++ b/ggml/include/ggml.h @@ -479,10 +479,12 @@ extern "C" { GGML_OP_MEAN, GGML_OP_ARGMAX, GGML_OP_COUNT_EQUAL, + GGML_OP_COUNT_EQUAL_MASKED, GGML_OP_REPEAT, GGML_OP_REPEAT_BACK, GGML_OP_CONCAT, GGML_OP_SILU_BACK, + GGML_OP_GEGLU_BACK, GGML_OP_NORM, // normalize GGML_OP_RMS_NORM, GGML_OP_RMS_NORM_BACK, @@ -558,6 +560,8 @@ extern "C" { GGML_OP_CROSS_ENTROPY_LOSS, GGML_OP_CROSS_ENTROPY_LOSS_BACK, + GGML_OP_CROSS_ENTROPY_LOSS_MASKED, + GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK, GGML_OP_OPT_STEP_ADAMW, GGML_OP_OPT_STEP_SGD, @@ -1033,6 +1037,13 @@ extern "C" { struct ggml_tensor * a, struct ggml_tensor * b); + // count number of equal elements in a and b, but only where mask=1 + GGML_API struct ggml_tensor * ggml_count_equal_masked( + struct ggml_context * ctx, + struct ggml_tensor * a, // predictions + struct ggml_tensor * b, // targets + struct ggml_tensor * c); // mask (1 for positions to count, 0 to skip) + // if a is the same shape as b, and a is not parameter, return a // otherwise, return a new tensor: repeat(a) to fit in b GGML_API struct ggml_tensor * ggml_repeat( @@ -1172,6 +1183,10 @@ extern "C" { struct ggml_tensor * a, struct ggml_tensor * b); + GGML_API struct ggml_tensor * ggml_geglu_back( + struct ggml_context * ctx, + struct ggml_tensor * grad, + struct ggml_tensor * g); // hardswish(x) = x * relu6(x + 3) / 6 GGML_API struct ggml_tensor * ggml_hardswish( struct ggml_context * ctx, @@ -2530,6 +2545,19 @@ extern "C" { struct ggml_tensor * b, // labels struct ggml_tensor * c); // gradients of cross_entropy_loss result + // Masked cross-entropy loss for instruction fine-tuning (assistant-only loss) + GGML_API struct ggml_tensor * ggml_cross_entropy_loss_masked( + struct ggml_context * ctx, + struct ggml_tensor * a, // logits + struct ggml_tensor * b, // labels + struct ggml_tensor * c); // mask (1 for assistant tokens, 0 for masked) + GGML_API struct ggml_tensor * ggml_cross_entropy_loss_masked_back( + struct ggml_context * ctx, + struct ggml_tensor * a, // logits + struct ggml_tensor * b, // labels + struct ggml_tensor * c, // mask + struct ggml_tensor * d); // gradients of cross_entropy_loss result + // AdamW optimizer step // Paper: https://arxiv.org/pdf/1711.05101v3.pdf // PyTorch: https://pytorch.org/docs/stable/generated/torch.optim.AdamW.html diff --git a/ggml/include/gguf.h b/ggml/include/gguf.h index 79ee202062b0..db16883c0536 100644 --- a/ggml/include/gguf.h +++ b/ggml/include/gguf.h @@ -74,11 +74,18 @@ extern "C" { // if not NULL, create a ggml_context and allocate the tensor data in it struct ggml_context ** ctx; + + // if true, stop parsing immediately after the KV pairs and skip tensor info entirely; + // ctx is ignored when this flag is set +#ifdef __cplusplus + bool kv_only = false; +#else + bool kv_only; +#endif }; GGML_API struct gguf_context * gguf_init_empty(void); GGML_API struct gguf_context * gguf_init_from_file(const char * fname, struct gguf_init_params params); - //GGML_API struct gguf_context * gguf_init_from_buffer(..); GGML_API void gguf_free(struct gguf_context * ctx); @@ -200,3 +207,8 @@ extern "C" { #ifdef __cplusplus } #endif + +#if defined(__cplusplus) +#include +GGML_API struct gguf_context * gguf_init_from_buffer(std::basic_streambuf& streambuf, struct gguf_init_params params); +#endif diff --git a/ggml/include/uint8-buff-stream.h b/ggml/include/uint8-buff-stream.h new file mode 100644 index 000000000000..e372f917b68e --- /dev/null +++ b/ggml/include/uint8-buff-stream.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +#include + +#ifdef GGML_SHARED +# if defined(_WIN32) && !defined(__MINGW32__) +# ifdef GGML_BUILD +# define GGML_CLASS_API __declspec(dllexport) +# else +# define GGML_CLASS_API __declspec(dllimport) +# endif +# else +# define GGML_CLASS_API __attribute__((visibility("default"))) +# endif +#else +# define GGML_CLASS_API +#endif + +/// @brief Custom basic_streambuf for uint8_t input data, that owns the underlying data. +/// @note basic_streambuf has more support on different platforms than basic_streambuf +/// which is missing on some platforms (e.g. MacOS, newer NDKs). C++ 17 provides additional guarantees for char. +class GGML_CLASS_API Uint8BufferStreamBuf : public std::basic_streambuf { + public: + Uint8BufferStreamBuf(std::vector && _data); + + protected: + int_type underflow() override; + + /// @brief Efficient bulk reading. The standard implementation specifies that this function can be overridden + /// to provide a more efficient implementation: sgetn will call this function if it is overridden. + std::streamsize xsgetn(char_type * s, std::streamsize n) override; + + pos_type seekoff(off_type off, std::ios_base::seekdir dir, + std::ios_base::openmode which = std::ios_base::in) override; + + pos_type seekpos(pos_type pos, std::ios_base::openmode which = std::ios_base::in) override; + + private: + std::vector data; +}; diff --git a/ggml/src/CMakeLists.txt b/ggml/src/CMakeLists.txt index d93664b8b58b..f0aee7765a14 100644 --- a/ggml/src/CMakeLists.txt +++ b/ggml/src/CMakeLists.txt @@ -189,10 +189,6 @@ endif() # ggml -if (GGML_BACKEND_DL AND NOT BUILD_SHARED_LIBS) - message(FATAL_ERROR "GGML_BACKEND_DL requires BUILD_SHARED_LIBS") -endif() - add_library(ggml-base ../include/ggml.h ../include/ggml-alloc.h @@ -200,6 +196,7 @@ add_library(ggml-base ../include/ggml-cpp.h ../include/ggml-opt.h ../include/gguf.h + ../include/uint8-buff-stream.h ggml.c ggml.cpp ggml-alloc.c @@ -209,7 +206,8 @@ add_library(ggml-base ggml-threading.h ggml-quants.c ggml-quants.h - gguf.cpp) + gguf.cpp + uint8-buff-stream.cpp) set_target_properties(ggml-base PROPERTIES VERSION ${GGML_VERSION} @@ -251,24 +249,37 @@ function(ggml_add_backend_library backend) if (GGML_BACKEND_DL) add_library(${backend} MODULE ${ARGN}) # write the shared library to the output directory - set_target_properties(${backend} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + # Set output name with qvac- prefix to match backend_filename_prefix() in ggml-backend-reg.cpp + set_target_properties(${backend} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + OUTPUT_NAME "qvac-${backend}") target_compile_definitions(${backend} PRIVATE GGML_BACKEND_DL) - add_dependencies(ggml ${backend}) + # Do not add dependency, the User will have to explicitely build and install + # the available `ggml::ggml-*` backend targets. This is for better integration + # with cmake-bare + # add_dependencies(ggml ${backend}) + if (GGML_BACKEND_DIR) - install(TARGETS ${backend} LIBRARY DESTINATION ${GGML_BACKEND_DIR}) + install(TARGETS ${backend} + EXPORT ggml-targets + LIBRARY DESTINATION ${GGML_BACKEND_DIR} + RUNTIME DESTINATION ${GGML_BACKEND_DIR}) else() - install(TARGETS ${backend} LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}) + install(TARGETS ${backend} + EXPORT ggml-targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() else() add_library(${backend} ${ARGN}) target_link_libraries(ggml PUBLIC ${backend}) - install(TARGETS ${backend} LIBRARY) + install(TARGETS ${backend} EXPORT ggml-targets) endif() target_link_libraries(${backend} PRIVATE ggml-base) target_include_directories(${backend} PRIVATE ..) - if (${BUILD_SHARED_LIBS}) + if (${BUILD_SHARED_LIBS} OR GGML_BACKEND_DL) target_compile_definitions(${backend} PRIVATE GGML_BACKEND_BUILD) target_compile_definitions(${backend} PUBLIC GGML_BACKEND_SHARED) endif() @@ -467,7 +478,7 @@ if(CMAKE_SYSTEM_NAME MATCHES "visionOS") target_compile_definitions(ggml-base PUBLIC _DARWIN_C_SOURCE) endif() -if (BUILD_SHARED_LIBS) +if (BUILD_SHARED_LIBS OR GGML_BACKEND_DL) foreach (target ggml-base ggml) set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE ON) target_compile_definitions(${target} PRIVATE GGML_BUILD) diff --git a/ggml/src/ggml-backend-reg.cpp b/ggml/src/ggml-backend-reg.cpp index e96b5c403dd3..bbe8983243b3 100644 --- a/ggml/src/ggml-backend-reg.cpp +++ b/ggml/src/ggml-backend-reg.cpp @@ -4,11 +4,13 @@ #include #include #include +#include #include #include #include #include #include +#include #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN @@ -498,9 +500,9 @@ static fs::path get_executable_path() { static fs::path backend_filename_prefix() { #ifdef _WIN32 - return fs::u8path("ggml-"); + return fs::u8path("qvac-ggml-"); #else - return fs::u8path("libggml-"); + return fs::u8path("libqvac-ggml-"); #endif } @@ -515,7 +517,7 @@ static fs::path backend_filename_extension() { static ggml_backend_reg_t ggml_backend_load_best(const char * name, bool silent, const char * user_search_path) { // enumerate all the files that match [lib]ggml-name-*.[so|dll] in the search paths const fs::path name_path = fs::u8path(name); - const fs::path file_prefix = backend_filename_prefix().native() + name_path.native() + fs::u8path("-").native(); + const fs::path file_prefix = backend_filename_prefix().native() + name_path.native(); const fs::path file_extension = backend_filename_extension(); std::vector search_paths; @@ -526,6 +528,9 @@ static ggml_backend_reg_t ggml_backend_load_best(const char * name, bool silent, // default search paths: executable directory, current directory search_paths.push_back(get_executable_path()); search_paths.push_back(fs::current_path()); + + // Android does not require prepending path, the .apk will have embedded the dynamic .so, only the name is needed for dlopen + // TODO add here prebuild/ search patch for Desktop platforms where we want to support dynamic loading } else { search_paths.push_back(fs::u8path(user_search_path)); } @@ -533,38 +538,41 @@ static ggml_backend_reg_t ggml_backend_load_best(const char * name, bool silent, int best_score = 0; fs::path best_path; + auto tryEntryWithScore = [&best_score, &best_path, silent, _func = __func__](const fs::path & entryPath, + int scoreOffset = 1) { + dl_handle_ptr handle{ dl_load_library(entryPath) }; + if (!handle && !silent) { + GGML_LOG_ERROR("%s: failed to load %s: %s\n", _func, path_str(entryPath).c_str(), dl_error()); + } + if (handle) { + auto score_fn = (ggml_backend_score_t) dl_get_sym(handle.get(), "ggml_backend_score"); + int s = 1; + if (score_fn) { + s = score_fn() + scoreOffset; + } +#ifdef NDEBUG + GGML_LOG_DEBUG("%s: %s score: %d\n", _func, path_str(entryPath).c_str(), s); +#endif + if (s > best_score) { + best_score = s; + best_path = entryPath; + } + } + }; + for (const auto & search_path : search_paths) { if (!fs::exists(search_path)) { GGML_LOG_DEBUG("%s: search path %s does not exist\n", __func__, path_str(search_path).c_str()); continue; } + GGML_LOG_INFO("%s: searching for %s in %s\n", __func__, path_str(name_path).c_str(), path_str(search_path).c_str()); fs::directory_iterator dir_it(search_path, fs::directory_options::skip_permission_denied); for (const auto & entry : dir_it) { if (entry.is_regular_file()) { auto filename = entry.path().filename(); auto ext = entry.path().extension(); if (filename.native().find(file_prefix) == 0 && ext == file_extension) { - dl_handle_ptr handle { dl_load_library(entry) }; - if (!handle && !silent) { - GGML_LOG_ERROR("%s: failed to load %s: %s\n", __func__, path_str(entry.path()).c_str(), dl_error()); - } - if (handle) { - auto score_fn = (ggml_backend_score_t) dl_get_sym(handle.get(), "ggml_backend_score"); - if (score_fn) { - int s = score_fn(); -#ifndef NDEBUG - GGML_LOG_DEBUG("%s: %s score: %d\n", __func__, path_str(entry.path()).c_str(), s); -#endif - if (s > best_score) { - best_score = s; - best_path = entry.path(); - } - } else { - if (!silent) { - GGML_LOG_INFO("%s: failed to find ggml_backend_score in %s\n", __func__, path_str(entry.path()).c_str()); - } - } - } + tryEntryWithScore(entry.path()); } } } @@ -579,7 +587,27 @@ static ggml_backend_reg_t ggml_backend_load_best(const char * name, bool silent, return get_reg().load_backend(path, silent); } } - return nullptr; + } + + // In the case of Android, we can load with just the library filename, without pre-pending any path + if (best_path.empty()) { + // From worst to best + std::vector names = { name_path }; +#ifdef __ANDROID__ + if (strcmp(name, "cpu") == 0) { + names.emplace_back("cpu-android_armv8.0_1"); + names.emplace_back("cpu-android_armv8.2_1"); + names.emplace_back("cpu-android_armv8.2_2"); + names.emplace_back("cpu-android_armv8.6_1"); + } +#endif + for (size_t scoreOffset = 0; scoreOffset < names.size(); ++scoreOffset) { + const auto & loopNamePath = names[scoreOffset]; + // Try loading backend with just the library name, leave to dlopen path resolution. + fs::path filename = backend_filename_prefix().native() + loopNamePath.native() + + backend_filename_extension().native(); + tryEntryWithScore(filename, 1+scoreOffset); + } } return get_reg().load_backend(best_path, silent); @@ -589,7 +617,54 @@ void ggml_backend_load_all() { ggml_backend_load_all_from_path(nullptr); } +#ifdef __ANDROID__ +namespace { +// Parses adreno version from gpu description or returns -1 if its not Adreno GPU or -3 if failed to parse the version +int adrenoVersion(const std::string & gpuDescription) { + std::regex adrenoRegex(R"((\d+))"); + std::smatch matches; + if (gpuDescription.find("dreno") != std::string::npos && std::regex_search(gpuDescription, matches, adrenoRegex) && matches.size() > 1) { + try { + int adrenoVersion = std::stoi(matches[1].str()); + return adrenoVersion; + } catch (std::invalid_argument & e) { + GGML_LOG_ERROR("%s: failed to parse adreno version from %s: %s\n", __func__, gpuDescription.c_str(), + e.what()); + return -3; + } + } + return -1; +} + +// Returns smallest Adreno version among GPU devices or -1 if there is no adreno GPU +int minAdrenoVersion(ggml_backend_reg_t vulkanBackend) { + if (!vulkanBackend) { + return -2; + } + int minFoundVersion = std::numeric_limits::max(); + for (size_t i = 0; i < vulkanBackend->iface.get_device_count(vulkanBackend); i++) { + ggml_backend_dev_t dev = vulkanBackend->iface.get_device(vulkanBackend, i); + if (!dev) { + continue; + } + auto description = std::string(dev->iface.get_description(dev)); + GGML_LOG_INFO("%s: found device description: %s\n", __func__, description.c_str()); + int devAdrenoVersion = adrenoVersion(description); + if (devAdrenoVersion > 0) { + minFoundVersion = std::min(minFoundVersion, devAdrenoVersion); + } + } + if (minFoundVersion < std::numeric_limits::max()) { + return minFoundVersion; + } + return -1; +} +} // namespace +#endif + void ggml_backend_load_all_from_path(const char * dir_path) { +#ifdef GGML_BACKEND_DL + // Only attempt to dlopen backends when built with dynamic backend support #ifdef NDEBUG bool silent = true; #else @@ -604,7 +679,34 @@ void ggml_backend_load_all_from_path(const char * dir_path) { ggml_backend_load_best("rpc", silent, dir_path); ggml_backend_load_best("sycl", silent, dir_path); ggml_backend_load_best("vulkan", silent, dir_path); - ggml_backend_load_best("opencl", silent, dir_path); + + bool useOpencl = true; + +#ifdef __ANDROID__ + // Logic for buggy backends on Adreno GPUs + // Use Vulkan backend to obtain GPU information + ggml_backend_reg_t vulkanBackend = ggml_backend_reg_by_name("vulkan"); + int devicesMinAdrenoVersion = minAdrenoVersion(vulkanBackend); + if (devicesMinAdrenoVersion <= 0) { + GGML_LOG_INFO( + "%s: no adreno GPU version found (%d) removing OpenCL backend (if any) to rely on Vulkan/cpu only\n", + __func__, devicesMinAdrenoVersion); + useOpencl = false; + } else if (devicesMinAdrenoVersion > 700) { + GGML_LOG_INFO("%s: Adreno GPU version %d found keeping OpenCL backend\n", __func__, devicesMinAdrenoVersion); + } else if (devicesMinAdrenoVersion > 600) { + GGML_LOG_INFO("%s: Adreno GPU version %d should rely on cpu only\n", __func__, devicesMinAdrenoVersion); + if (vulkanBackend) { + ggml_backend_unload(vulkanBackend); + GGML_LOG_INFO("%s: Vulkan backend removed\n", __func__); + } + useOpencl = false; + } +#endif + + if(useOpencl) { + ggml_backend_load_best("opencl", silent, dir_path); + } ggml_backend_load_best("hexagon", silent, dir_path); ggml_backend_load_best("musa", silent, dir_path); ggml_backend_load_best("cpu", silent, dir_path); @@ -613,4 +715,9 @@ void ggml_backend_load_all_from_path(const char * dir_path) { if (backend_path) { ggml_backend_load(backend_path); } +#else + // When built without GGML_BACKEND_DL, backends are statically linked + // No dynamic loading needed - avoids potential conflicts with system libraries + GGML_UNUSED(dir_path); +#endif } diff --git a/ggml/src/ggml-backend.cpp b/ggml/src/ggml-backend.cpp index 08681f35e3f9..29b8281f3e9a 100644 --- a/ggml/src/ggml-backend.cpp +++ b/ggml/src/ggml-backend.cpp @@ -1,5 +1,6 @@ // Note: porting this file to C++ is a work in progress +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #ifndef NOMINMAX @@ -1634,7 +1635,11 @@ ggml_backend_sched_t ggml_backend_sched_new( struct ggml_backend_sched * sched = (ggml_backend_sched *) calloc(1, sizeof(struct ggml_backend_sched)); +#ifndef FORCE_GGML_VK_PERF_LOGGER const char * GGML_SCHED_DEBUG = getenv("GGML_SCHED_DEBUG"); +#else + GGML_SCHED_DEBUG = "2"; +#endif sched->debug = GGML_SCHED_DEBUG ? atoi(GGML_SCHED_DEBUG) : 0; sched->debug_realloc = 0; diff --git a/ggml/src/ggml-cpu/arch-fallback.h b/ggml/src/ggml-cpu/arch-fallback.h index 0775c87f98b3..d72e14244a8c 100644 --- a/ggml/src/ggml-cpu/arch-fallback.h +++ b/ggml/src/ggml-cpu/arch-fallback.h @@ -13,9 +13,12 @@ #define ggml_vec_dot_q5_0_q8_0_generic ggml_vec_dot_q5_0_q8_0 #define ggml_vec_dot_q5_1_q8_1_generic ggml_vec_dot_q5_1_q8_1 #define ggml_vec_dot_q8_0_q8_0_generic ggml_vec_dot_q8_0_q8_0 +#define ggml_vec_dot_q8_1_q8_1_generic ggml_vec_dot_q8_1_q8_1 #define ggml_vec_dot_mxfp4_q8_0_generic ggml_vec_dot_mxfp4_q8_0 #define ggml_vec_dot_tq1_0_q8_K_generic ggml_vec_dot_tq1_0_q8_K #define ggml_vec_dot_tq2_0_q8_K_generic ggml_vec_dot_tq2_0_q8_K +#define ggml_vec_dot_tq2_0_q8_0_generic ggml_vec_dot_tq2_0_q8_0 +#define ggml_vec_dot_tq2_0_q8_1_generic ggml_vec_dot_tq2_0_q8_1 #define ggml_vec_dot_q2_K_q8_K_generic ggml_vec_dot_q2_K_q8_K #define ggml_vec_dot_q3_K_q8_K_generic ggml_vec_dot_q3_K_q8_K #define ggml_vec_dot_q4_K_q8_K_generic ggml_vec_dot_q4_K_q8_K @@ -59,6 +62,7 @@ #define ggml_gemv_q2_K_8x8_q8_K_generic ggml_gemv_q2_K_8x8_q8_K #define ggml_gemm_iq4_nl_8x8_q8_0_generic ggml_gemm_iq4_nl_8x8_q8_0 #define ggml_gemm_q2_K_8x8_q8_K_generic ggml_gemm_q2_K_8x8_q8_K +#define ggml_vec_dot_q8_1_q8_1_generic ggml_vec_dot_q8_1_q8_1 #elif defined(__x86_64__) || defined(__i386__) || defined(_M_IX86) || defined(_M_X64) // repack.cpp #define ggml_quantize_mat_q8_0_4x4_generic ggml_quantize_mat_q8_0_4x4 @@ -71,13 +75,17 @@ #define ggml_gemm_q4_0_4x8_q8_0_generic ggml_gemm_q4_0_4x8_q8_0 #define ggml_gemm_q4_K_8x4_q8_K_generic ggml_gemm_q4_K_8x4_q8_K #define ggml_gemm_iq4_nl_4x4_q8_0_generic ggml_gemm_iq4_nl_4x4_q8_0 +#define ggml_vec_dot_q8_1_q8_1_generic ggml_vec_dot_q8_1_q8_1 #elif defined(__POWERPC__) || defined(__powerpc__) // ref: https://github.com/ggml-org/llama.cpp/pull/14146#issuecomment-2972561679 // quants.c #define quantize_row_q8_K_generic quantize_row_q8_K #define ggml_vec_dot_tq1_0_q8_K_generic ggml_vec_dot_tq1_0_q8_K #define ggml_vec_dot_tq2_0_q8_K_generic ggml_vec_dot_tq2_0_q8_K +#define ggml_vec_dot_tq2_0_q8_0_generic ggml_vec_dot_tq2_0_q8_0 +#define ggml_vec_dot_tq2_0_q8_1_generic ggml_vec_dot_tq2_0_q8_1 #define ggml_vec_dot_iq1_m_q8_K_generic ggml_vec_dot_iq1_m_q8_K +#define ggml_vec_dot_q8_1_q8_1_generic ggml_vec_dot_q8_1_q8_1 // repack.cpp #define ggml_quantize_mat_q8_0_4x4_generic ggml_quantize_mat_q8_0_4x4 #define ggml_quantize_mat_q8_0_4x8_generic ggml_quantize_mat_q8_0_4x8 @@ -104,8 +112,11 @@ #define quantize_row_q8_K_generic quantize_row_q8_K #define ggml_vec_dot_tq1_0_q8_K_generic ggml_vec_dot_tq1_0_q8_K #define ggml_vec_dot_tq2_0_q8_K_generic ggml_vec_dot_tq2_0_q8_K +#define ggml_vec_dot_tq2_0_q8_0_generic ggml_vec_dot_tq2_0_q8_0 +#define ggml_vec_dot_tq2_0_q8_1_generic ggml_vec_dot_tq2_0_q8_1 #define ggml_vec_dot_iq1_m_q8_K_generic ggml_vec_dot_iq1_m_q8_K #define ggml_vec_dot_mxfp4_q8_0_generic ggml_vec_dot_mxfp4_q8_0 +#define ggml_vec_dot_q8_1_q8_1_generic ggml_vec_dot_q8_1_q8_1 // repack.cpp #define ggml_quantize_mat_q8_0_4x4_generic ggml_quantize_mat_q8_0_4x4 #define ggml_quantize_mat_q8_0_4x8_generic ggml_quantize_mat_q8_0_4x8 @@ -132,6 +143,8 @@ #define quantize_row_q8_K_generic quantize_row_q8_K #define ggml_vec_dot_tq1_0_q8_K_generic ggml_vec_dot_tq1_0_q8_K #define ggml_vec_dot_tq2_0_q8_K_generic ggml_vec_dot_tq2_0_q8_K +#define ggml_vec_dot_tq2_0_q8_0_generic ggml_vec_dot_tq2_0_q8_0 +#define ggml_vec_dot_tq2_0_q8_1_generic ggml_vec_dot_tq2_0_q8_1 #define ggml_vec_dot_iq2_xxs_q8_K_generic ggml_vec_dot_iq2_xxs_q8_K #define ggml_vec_dot_iq2_xs_q8_K_generic ggml_vec_dot_iq2_xs_q8_K #define ggml_vec_dot_iq2_s_q8_K_generic ggml_vec_dot_iq2_s_q8_K @@ -142,6 +155,7 @@ #define ggml_vec_dot_iq4_nl_q8_0_generic ggml_vec_dot_iq4_nl_q8_0 #define ggml_vec_dot_iq4_xs_q8_K_generic ggml_vec_dot_iq4_xs_q8_K #define ggml_vec_dot_mxfp4_q8_0_generic ggml_vec_dot_mxfp4_q8_0 +#define ggml_vec_dot_q8_1_q8_1_generic ggml_vec_dot_q8_1_q8_1 // repack.cpp #define ggml_quantize_mat_q8_0_4x4_generic ggml_quantize_mat_q8_0_4x4 #define ggml_quantize_mat_q8_0_4x8_generic ggml_quantize_mat_q8_0_4x8 @@ -166,6 +180,7 @@ #define quantize_row_q8_K_generic quantize_row_q8_K #define ggml_vec_dot_tq1_0_q8_K_generic ggml_vec_dot_tq1_0_q8_K #define ggml_vec_dot_tq2_0_q8_K_generic ggml_vec_dot_tq2_0_q8_K +#define ggml_vec_dot_tq2_0_q8_1_generic ggml_vec_dot_tq2_0_q8_1 #define ggml_vec_dot_q2_K_q8_K_generic ggml_vec_dot_q2_K_q8_K #define ggml_vec_dot_iq2_xxs_q8_K_generic ggml_vec_dot_iq2_xxs_q8_K #define ggml_vec_dot_iq2_xs_q8_K_generic ggml_vec_dot_iq2_xs_q8_K @@ -174,6 +189,8 @@ #define ggml_vec_dot_iq3_s_q8_K_generic ggml_vec_dot_iq3_s_q8_K #define ggml_vec_dot_iq1_s_q8_K_generic ggml_vec_dot_iq1_s_q8_K #define ggml_vec_dot_iq1_m_q8_K_generic ggml_vec_dot_iq1_m_q8_K +#define ggml_vec_dot_mxfp4_q8_0_generic ggml_vec_dot_mxfp4_q8_0 +#define ggml_vec_dot_q8_1_q8_1_generic ggml_vec_dot_q8_1_q8_1 // repack.cpp #define ggml_quantize_mat_q8_0_4x4_generic ggml_quantize_mat_q8_0_4x4 #define ggml_quantize_mat_q8_0_4x8_generic ggml_quantize_mat_q8_0_4x8 @@ -200,6 +217,7 @@ #define ggml_vec_dot_q4_1_q8_1_generic ggml_vec_dot_q4_1_q8_1 #define ggml_vec_dot_tq1_0_q8_K_generic ggml_vec_dot_tq1_0_q8_K #define ggml_vec_dot_tq2_0_q8_K_generic ggml_vec_dot_tq2_0_q8_K +#define ggml_vec_dot_tq2_0_q8_1_generic ggml_vec_dot_tq2_0_q8_1 #define ggml_vec_dot_iq2_xxs_q8_K_generic ggml_vec_dot_iq2_xxs_q8_K #define ggml_vec_dot_iq2_xs_q8_K_generic ggml_vec_dot_iq2_xs_q8_K #define ggml_vec_dot_iq2_s_q8_K_generic ggml_vec_dot_iq2_s_q8_K @@ -210,6 +228,7 @@ #define ggml_vec_dot_iq4_nl_q8_0_generic ggml_vec_dot_iq4_nl_q8_0 #define ggml_vec_dot_iq4_xs_q8_K_generic ggml_vec_dot_iq4_xs_q8_K #define ggml_vec_dot_mxfp4_q8_0_generic ggml_vec_dot_mxfp4_q8_0 +#define ggml_vec_dot_q8_1_q8_1_generic ggml_vec_dot_q8_1_q8_1 // repack.cpp #define ggml_quantize_mat_q8_0_4x4_generic ggml_quantize_mat_q8_0_4x4 #define ggml_quantize_mat_q8_0_4x8_generic ggml_quantize_mat_q8_0_4x8 diff --git a/ggml/src/ggml-cpu/arch/arm/quants.c b/ggml/src/ggml-cpu/arch/arm/quants.c index b390ab61c785..72a7840bc27a 100644 --- a/ggml/src/ggml-cpu/arch/arm/quants.c +++ b/ggml/src/ggml-cpu/arch/arm/quants.c @@ -1415,6 +1415,14 @@ void ggml_vec_dot_tq2_0_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const vo #endif } +void ggml_vec_dot_tq2_0_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { + ggml_vec_dot_tq2_0_q8_0_generic(n, s, bs, vx, bx, vy, by, nrc); +} + +void ggml_vec_dot_tq2_0_q8_1(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { + ggml_vec_dot_tq2_0_q8_1_generic(n, s, bs, vx, bx, vy, by, nrc); +} + void ggml_vec_dot_q2_K_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { assert(nrc == 1); UNUSED(nrc); diff --git a/ggml/src/ggml-cpu/arch/x86/quants.c b/ggml/src/ggml-cpu/arch/x86/quants.c index cb49320a67f1..20c7c3726674 100644 --- a/ggml/src/ggml-cpu/arch/x86/quants.c +++ b/ggml/src/ggml-cpu/arch/x86/quants.c @@ -1274,6 +1274,14 @@ void ggml_vec_dot_tq2_0_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const vo #endif } +void ggml_vec_dot_tq2_0_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { + ggml_vec_dot_tq2_0_q8_0_generic(n, s, bs, vx, bx, vy, by, nrc); +} + +void ggml_vec_dot_tq2_0_q8_1(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { + ggml_vec_dot_tq2_0_q8_1_generic(n, s, bs, vx, bx, vy, by, nrc); +} + void ggml_vec_dot_q2_K_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { assert(nrc == 1); UNUSED(nrc); diff --git a/ggml/src/ggml-cpu/ggml-cpu.c b/ggml/src/ggml-cpu/ggml-cpu.c index 8507557267a3..04ce9d8c37be 100644 --- a/ggml/src/ggml-cpu/ggml-cpu.c +++ b/ggml/src/ggml-cpu/ggml-cpu.c @@ -250,6 +250,7 @@ static const struct ggml_type_traits_cpu type_traits_cpu[GGML_TYPE_COUNT] = { }, [GGML_TYPE_Q8_1] = { .from_float = quantize_row_q8_1, + .vec_dot = ggml_vec_dot_q8_1_q8_1, .vec_dot_type = GGML_TYPE_Q8_1, .nrows = 1, }, @@ -382,6 +383,33 @@ const struct ggml_type_traits_cpu * ggml_get_type_traits_cpu(enum ggml_type type return &type_traits_cpu[type]; } +struct vec_dot_selection { + ggml_vec_dot_t vec_dot; + enum ggml_type vec_dot_type; +}; + +static struct vec_dot_selection ggml_cpu_select_vec_dot( + const struct ggml_tensor * src0, + const struct ggml_tensor * src1) { + + struct vec_dot_selection selection = { + .vec_dot = type_traits_cpu[src0->type].vec_dot, + .vec_dot_type = type_traits_cpu[src0->type].vec_dot_type, + }; + + if (src0->type == GGML_TYPE_TQ2_0 && src1 != NULL) { + if (src1->type == GGML_TYPE_Q8_1) { + selection.vec_dot = ggml_vec_dot_tq2_0_q8_1; + selection.vec_dot_type = GGML_TYPE_Q8_1; + } else if (src1->type == GGML_TYPE_Q8_0) { + selection.vec_dot = ggml_vec_dot_tq2_0_q8_0; + selection.vec_dot_type = GGML_TYPE_Q8_0; + } + } + + return selection; +} + // // Threading defs // @@ -1112,7 +1140,8 @@ void ggml_set_f32_nd(const struct ggml_tensor * tensor, int i0, int i1, int i2, static void ggml_compute_forward_mul_mat_one_chunk( const struct ggml_compute_params * params, struct ggml_tensor * dst, - const enum ggml_type type, + ggml_vec_dot_t vec_dot, + enum ggml_type vec_dot_type, const int64_t num_rows_per_vec_dot, const int64_t ir0_start, const int64_t ir0_end, @@ -1125,10 +1154,6 @@ static void ggml_compute_forward_mul_mat_one_chunk( GGML_TENSOR_BINARY_OP_LOCALS const bool src1_cont = ggml_is_contiguous(src1); - - ggml_vec_dot_t const vec_dot = type_traits_cpu[type].vec_dot; - enum ggml_type const vec_dot_type = type_traits_cpu[type].vec_dot_type; - // broadcast factors const int64_t r2 = ne12 / ne02; const int64_t r3 = ne13 / ne03; @@ -1211,9 +1236,11 @@ void ggml_compute_forward_mul_mat( const int ith = params->ith; const int nth = params->nth; - enum ggml_type const vec_dot_type = type_traits_cpu[src0->type].vec_dot_type; - ggml_from_float_t const from_float = type_traits_cpu[vec_dot_type].from_float; - int64_t const vec_dot_num_rows = type_traits_cpu[src0->type].nrows; + struct vec_dot_selection vds = ggml_cpu_select_vec_dot(src0, src1); + ggml_vec_dot_t vec_dot = vds.vec_dot; + enum ggml_type vec_dot_type = vds.vec_dot_type; + ggml_from_float_t const from_float = type_traits_cpu[vec_dot_type].from_float; + int64_t const vec_dot_num_rows = type_traits_cpu[src0->type].nrows; GGML_ASSERT(ne0 == ne01); GGML_ASSERT(ne1 == ne11); @@ -1383,7 +1410,7 @@ UseGgmlGemm2:; if ((nr0 % 2 != 0) || (ne11 % 2 != 0) || ((ir0_end - ir0_start) % 2 != 0) || ((ir1_end - ir1_start) % 2 != 0)) { num_rows_per_vec_dot = 1; } - ggml_compute_forward_mul_mat_one_chunk(params, dst, src0->type, num_rows_per_vec_dot, ir0_start, ir0_end, ir1_start, ir1_end); + ggml_compute_forward_mul_mat_one_chunk(params, dst, vec_dot, vec_dot_type, num_rows_per_vec_dot, ir0_start, ir0_end, ir1_start, ir1_end); if (nth >= nchunk0 * nchunk1) { break; @@ -1406,6 +1433,8 @@ static void ggml_compute_forward_mul_mat_id_one_chunk( struct ggml_tensor * dst, const struct ggml_tensor * src0, const struct ggml_tensor * src1, + ggml_vec_dot_t vec_dot, + enum ggml_type vec_dot_type, const struct ggml_tensor * ids, const int64_t cur_a, const int64_t ir0_start, @@ -1420,11 +1449,6 @@ static void ggml_compute_forward_mul_mat_id_one_chunk( GGML_TENSOR_BINARY_OP_LOCALS - const enum ggml_type type = src0->type; - - ggml_vec_dot_t const vec_dot = type_traits_cpu[type].vec_dot; - enum ggml_type const vec_dot_type = type_traits_cpu[type].vec_dot_type; - const int64_t blck_0 = 16; const int64_t blck_1 = 16; @@ -1490,7 +1514,13 @@ static void ggml_compute_forward_mul_mat_id( const bool src1_cont = ggml_is_contiguous(src1); - enum ggml_type const vec_dot_type = type_traits_cpu[type].vec_dot_type; + ggml_vec_dot_t vec_dot; + enum ggml_type vec_dot_type; + { + struct vec_dot_selection vds = ggml_cpu_select_vec_dot(src0, src1); + vec_dot = vds.vec_dot; + vec_dot_type = vds.vec_dot_type; + } ggml_from_float_t const from_float = type_traits_cpu[vec_dot_type].from_float; // we don't support permuted src0 or src1 @@ -1634,7 +1664,7 @@ static void ggml_compute_forward_mul_mat_id( const int64_t ir1_end = MIN(ir1_start + dr1, nr1); ggml_compute_forward_mul_mat_id_one_chunk( - dst, src0, src1, ids, cur_a, + dst, src0, src1, vec_dot, vec_dot_type, ids, cur_a, ir0_start, ir0_end, ir1_start, ir1_end, src0_cur, matrix_rows, row_size, src1_cont, wdata ); @@ -1739,6 +1769,10 @@ static void ggml_compute_forward(struct ggml_compute_params * params, struct ggm { ggml_compute_forward_count_equal(params, tensor); } break; + case GGML_OP_COUNT_EQUAL_MASKED: + { + ggml_compute_forward_count_equal_masked(params, tensor); + } break; case GGML_OP_REPEAT: { ggml_compute_forward_repeat(params, tensor); @@ -1755,6 +1789,10 @@ static void ggml_compute_forward(struct ggml_compute_params * params, struct ggm { ggml_compute_forward_silu_back(params, tensor); } break; + case GGML_OP_GEGLU_BACK: + { + ggml_compute_forward_geglu_back(params, tensor); + } break; case GGML_OP_NORM: { ggml_compute_forward_norm(params, tensor); @@ -2024,6 +2062,16 @@ static void ggml_compute_forward(struct ggml_compute_params * params, struct ggm ggml_compute_forward_cross_entropy_loss_back(params, tensor); } break; + case GGML_OP_CROSS_ENTROPY_LOSS_MASKED: + { + ggml_compute_forward_cross_entropy_loss_masked(params, tensor); + } + break; + case GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK: + { + ggml_compute_forward_cross_entropy_loss_masked_back(params, tensor); + } + break; case GGML_OP_OPT_STEP_ADAMW: { ggml_compute_forward_opt_step_adamw(params, tensor); @@ -2173,6 +2221,7 @@ static int ggml_get_n_tasks(struct ggml_tensor * node, int n_threads) { } break; case GGML_OP_COUNT_EQUAL: case GGML_OP_SOLVE_TRI: + case GGML_OP_COUNT_EQUAL_MASKED: { n_tasks = n_threads; } break; @@ -2233,6 +2282,7 @@ static int ggml_get_n_tasks(struct ggml_tensor * node, int n_threads) { } break; case GGML_OP_SILU_BACK: + case GGML_OP_GEGLU_BACK: case GGML_OP_MUL: case GGML_OP_DIV: case GGML_OP_NORM: @@ -2366,6 +2416,8 @@ static int ggml_get_n_tasks(struct ggml_tensor * node, int n_threads) { } break; case GGML_OP_CROSS_ENTROPY_LOSS: case GGML_OP_CROSS_ENTROPY_LOSS_BACK: + case GGML_OP_CROSS_ENTROPY_LOSS_MASKED: + case GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK: case GGML_OP_OPT_STEP_ADAMW: case GGML_OP_OPT_STEP_SGD: { @@ -2750,12 +2802,14 @@ struct ggml_cplan ggml_graph_plan( } } break; case GGML_OP_COUNT_EQUAL: + case GGML_OP_COUNT_EQUAL_MASKED: { cur = ggml_type_size(node->type)*n_tasks; } break; case GGML_OP_MUL_MAT: { - const enum ggml_type vec_dot_type = type_traits_cpu[node->src[0]->type].vec_dot_type; + struct vec_dot_selection vds = ggml_cpu_select_vec_dot(node->src[0], node->src[1]); + enum ggml_type vec_dot_type = vds.vec_dot_type; if (node->src[1]->type != vec_dot_type) { cur = ggml_row_size(vec_dot_type, ggml_nelements(node->src[1])); @@ -2767,7 +2821,8 @@ struct ggml_cplan ggml_graph_plan( const struct ggml_tensor * src0 = node->src[0]; const struct ggml_tensor * src1 = node->src[1]; const struct ggml_tensor * ids = node->src[2]; - const enum ggml_type vec_dot_type = type_traits_cpu[src0->type].vec_dot_type; + struct vec_dot_selection vds = ggml_cpu_select_vec_dot(src0, src1); + enum ggml_type vec_dot_type = vds.vec_dot_type; const int n_as = src0->ne[2]; // src1 if (src1->type != vec_dot_type) { @@ -2868,6 +2923,11 @@ struct ggml_cplan ggml_graph_plan( { cur = ggml_type_size(node->type)*(n_tasks + node->src[0]->ne[0]*n_tasks); } break; + case GGML_OP_CROSS_ENTROPY_LOSS_MASKED: + case GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK: + { + cur = ggml_type_size(node->type)*(n_tasks + node->src[0]->ne[0]*n_tasks) + sizeof(int64_t)*n_tasks; + } break; case GGML_OP_COUNT: { GGML_ABORT("fatal error"); diff --git a/ggml/src/ggml-cpu/ggml-cpu.cpp b/ggml/src/ggml-cpu/ggml-cpu.cpp index 3191faaa4cd9..eb7d4e738c9d 100644 --- a/ggml/src/ggml-cpu/ggml-cpu.cpp +++ b/ggml/src/ggml-cpu/ggml-cpu.cpp @@ -436,7 +436,11 @@ static bool ggml_backend_cpu_device_supports_op(ggml_backend_dev_t dev, const st op->type != GGML_TYPE_IQ1_S && op->type != GGML_TYPE_IQ1_M; // missing type_traits.from_float case GGML_OP_MUL_MAT: - return src1->type == GGML_TYPE_F32 || src1->type == ggml_get_type_traits_cpu(src0->type)->vec_dot_type; + return + src1->type == GGML_TYPE_F32 || + src1->type == ggml_get_type_traits_cpu(src0->type)->vec_dot_type || + (src0->type == GGML_TYPE_TQ2_0 && + (src1->type == GGML_TYPE_Q8_1 || src1->type == GGML_TYPE_Q8_0)); case GGML_OP_SOFT_MAX_BACK: { if (op->src[0]->type != GGML_TYPE_F32 || op->src[1]->type != GGML_TYPE_F32) { return false; @@ -452,7 +456,7 @@ static bool ggml_backend_cpu_device_supports_op(ggml_backend_dev_t dev, const st case GGML_OP_GET_ROWS_BACK: return src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16; case GGML_OP_OUT_PROD: - return (src0->type == GGML_TYPE_F32 || (ggml_is_quantized(src0->type) && src0->ne[2] == src1->ne[2] && src0->ne[3] == src1->ne[3])) && + return (src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16 || (ggml_is_quantized(src0->type) && src0->ne[2] == src1->ne[2] && src0->ne[3] == src1->ne[3])) && src1->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32; default: return true; diff --git a/ggml/src/ggml-cpu/ops.cpp b/ggml/src/ggml-cpu/ops.cpp index 608e82af69f4..96949f2bfd20 100644 --- a/ggml/src/ggml-cpu/ops.cpp +++ b/ggml/src/ggml-cpu/ops.cpp @@ -1684,6 +1684,100 @@ void ggml_compute_forward_count_equal( } } +// ggml_compute_forward_count_equal_masked + +static void ggml_compute_forward_count_equal_masked_i32( + const ggml_compute_params * params, + ggml_tensor * dst) { + + const ggml_tensor * src0 = dst->src[0]; + const ggml_tensor * src1 = dst->src[1]; + const ggml_tensor * src2 = dst->src[2]; + + GGML_TENSOR_BINARY_OP_LOCALS; + + GGML_ASSERT(src0->type == GGML_TYPE_I32); + GGML_ASSERT(src1->type == GGML_TYPE_I32); + GGML_ASSERT(src2->type == GGML_TYPE_F32); + GGML_ASSERT(ggml_are_same_shape(src0, src1)); + GGML_ASSERT(src2->ne[1] == src0->ne[0] || ggml_are_same_shape(src0, src2)); + GGML_ASSERT(ggml_is_scalar(dst)); + GGML_ASSERT(dst->type == GGML_TYPE_I64); + + const int64_t nr = ggml_nrows(src0); + + const int ith = params->ith; + const int nth = params->nth; + + int64_t * sums = (int64_t *) params->wdata; + int64_t sum_thread = 0; + + const int64_t dr = (nr + nth - 1)/nth; + + const int64_t ir0 = dr*ith; + const int64_t ir1 = MIN(ir0 + dr, nr); + + for (int64_t ir = ir0; ir < ir1; ++ir) { + const int64_t i03 = ir / (ne02*ne01); + const int64_t i02 = (ir - i03*ne03) / ne01; + const int64_t i01 = ir - i03*ne03 - i02*ne02; + + const char * data0 = (const char *) src0->data + i03*nb03 + i02*nb02 + i01*nb01; + const char * data1 = (const char *) src1->data + i03*nb13 + i02*nb12 + i01*nb11; + const char * data2 = (const char *) src2->data + i03*src2->nb[3] + i02*src2->nb[2] + i01*src2->nb[1]; + + for (int64_t i00 = 0; i00 < ne00; ++i00) { + const int32_t val0 = *((const int32_t *) (data0 + i00*nb00)); + const int32_t val1 = *((const int32_t *) (data1 + i00*nb10)); + + float mask_val; + if (ggml_are_same_shape(src0, src2)) { + mask_val = *((const float *) (data2 + i00*src2->nb[0])); + } else { + const char * mask_ptr = (const char *) src2->data + 0*src2->nb[0] + i00*src2->nb[1] + 0*src2->nb[2]; + mask_val = *((const float *) mask_ptr); + } + + const bool mask = mask_val > 0.5f; + + if (mask == 1) { + sum_thread += val0 == val1; + } + } + } + if (ith != 0) { + sums[ith] = sum_thread; + } + ggml_barrier(params->threadpool); + + if (ith != 0) { + return; + } + + for (int ith_other = 1; ith_other < nth; ++ith_other) { + sum_thread += sums[ith_other]; + } + *((int64_t *) dst->data) = sum_thread; +} + +void ggml_compute_forward_count_equal_masked( + const ggml_compute_params * params, + ggml_tensor * dst) { + + const ggml_tensor * src0 = dst->src[0]; + + switch (src0->type) { + case GGML_TYPE_I32: + { + ggml_compute_forward_count_equal_masked_i32(params, dst); + } break; + default: + { + GGML_ABORT("fatal error"); + } + } +} + // ggml_compute_forward_repeat static void ggml_compute_forward_repeat_f32( @@ -2772,6 +2866,57 @@ void ggml_compute_forward_silu_back( } } +static void ggml_compute_forward_geglu_back_f32( + const ggml_compute_params * params, + const struct ggml_tensor * grad, + const struct ggml_tensor * g, + struct ggml_tensor * dst) { + + GGML_ASSERT(ggml_can_repeat(grad, dst)); + GGML_ASSERT(ggml_are_same_shape(g, dst)); + GGML_ASSERT(grad->type == GGML_TYPE_F32); + GGML_ASSERT(g->type == GGML_TYPE_F32); + GGML_ASSERT(dst->type == GGML_TYPE_F32); + + const int ith = params->ith; + const int nth = params->nth; + + const int nc = dst->ne[0]; + + const size_t nb1 = dst->nb[1]; + const size_t nb2 = dst->nb[2]; + const size_t nb3 = dst->nb[3]; + + for (int i3 = 0; i3 < dst->ne[3]; i3++) { + for (int i2 = 0; i2 < dst->ne[2]; i2++) { + for (int i1 = ith; i1 < dst->ne[1]; i1 += nth) { + float * dst_ptr = (float *)((char *) dst->data + i3*nb3 + i2*nb2 + i1*nb1); + const float * grad_ptr = (const float *)((char *) grad->data + i3*grad->nb[3] + i2*grad->nb[2] + i1*grad->nb[1]); + const float * g_ptr = (const float *)((char *) g->data + i3*g->nb[3] + i2*g->nb[2] + i1*g->nb[1]); + ggml_vec_gelu_backward_f32(nc, dst_ptr, g_ptr, grad_ptr); + } + } + } +} + +void ggml_compute_forward_geglu_back( + const ggml_compute_params * params, + ggml_tensor * dst) { + + const struct ggml_tensor * grad = dst->src[0]; + const struct ggml_tensor * g = dst->src[1]; + + switch (dst->type) { + case GGML_TYPE_F32: + { + ggml_compute_forward_geglu_back_f32(params, grad, g, dst); + } break; + default: + { + GGML_ABORT("fatal error"); + } + } +} // ggml_compute_forward_reglu static void ggml_compute_forward_reglu_f32( @@ -4168,6 +4313,107 @@ static void ggml_compute_forward_out_prod_f32( } } +static void ggml_compute_forward_out_prod_f16_f32( + const ggml_compute_params * params, + ggml_tensor * dst) { + + const ggml_tensor * src0 = dst->src[0]; + const ggml_tensor * src1 = dst->src[1]; + + GGML_TENSOR_BINARY_OP_LOCALS + + GGML_ASSERT(dst->type == GGML_TYPE_F32); + GGML_ASSERT(src0->type == GGML_TYPE_F16); + GGML_ASSERT(src1->type == GGML_TYPE_F32); + + const int ith = params->ith; + const int nth = params->nth; + + GGML_ASSERT(ne0 == ne00); + GGML_ASSERT(ne1 == ne10); + GGML_ASSERT(ne2 == ne12); + GGML_ASSERT(ne3 == ne13); + + GGML_ASSERT(ne2 % ne02 == 0); + GGML_ASSERT(ne3 % ne03 == 0); + + // we don't support permuted src0 or src1 + GGML_ASSERT(nb00 == sizeof(ggml_fp16_t)); + + // dst cannot be transposed or permuted + GGML_ASSERT(nb0 == sizeof(float)); + // GGML_ASSERT(nb0 <= nb1); + // GGML_ASSERT(nb1 <= nb2); + // GGML_ASSERT(nb2 <= nb3); + + // nb01 >= nb00 - src0 is not transposed + // compute by src0 rows + + if (ith == 0) { + ggml_vec_set_f32(ne0*ne1*ne2*ne3, (float *)dst->data, 0); + } + ggml_barrier(params->threadpool); + + // dst[:,:,:,:] = 0 + // for i2,i3: + // for i1: + // for i01: + // for i0: + // dst[i0,i1,i2,i3] += src0[i0,i01,i2,i3] * src1[i1,i01,i2,i3] + + // parallelize by last three dimensions + + // total rows in dst + const int64_t nr = ne1*ne2*ne3; + + // rows per thread + const int64_t dr = (nr + nth - 1)/nth; + + // row range for this thread + const int64_t ir0 = dr*ith; + const int64_t ir1 = MIN(ir0 + dr, nr); + + // block-tiling attempt + const int64_t blck_0 = MAX(GGML_VEC_MAD_UNROLL, 32); + const int64_t blck_1 = 16; + + // dps == dst per src0, used for group query attention + const int64_t dps2 = ne2 / ne02; + const int64_t dps3 = ne3 / ne03; + + for (int64_t bir = ir0; bir < ir1; bir += blck_1) { + const int64_t bir1 = MIN(bir + blck_1, ir1); + for (int64_t bi01 = 0; bi01 < ne01; bi01 += blck_0) { + const int64_t bne01 = MIN(bi01 + blck_0, ne01); + for (int64_t ir = bir; ir < bir1; ++ir) { + // dst indices + const int64_t i3 = ir/(ne2*ne1); + const int64_t i2 = (ir - i3*ne2*ne1)/ne1; + const int64_t i1 = (ir - i3*ne2*ne1 - i2*ne1); + + const int64_t i02 = i2 / dps2; + const int64_t i03 = i3 / dps3; + + //const int64_t i10 = i1; + const int64_t i12 = i2; + const int64_t i13 = i3; + + for (int64_t i01 = bi01; i01 < bne01; ++i01) { + const int64_t i11 = i01; + + ggml_fp16_t * s0 = (ggml_fp16_t *) ((char *) src0->data + (i01*nb01 + i02*nb02 + i03*nb03)); + float * s1 = (float *) ((char *) src1->data + (i1*nb10 + i11*nb11 + i12*nb12 + i13*nb13)); + float * d = (float *) ((char *) dst->data + ( i1*nb1 + i2*nb2 + i3*nb3)); + + for (int i = 0; i < ne0; ++i) { + d[i] += GGML_CPU_FP16_TO_FP32(s0[i])*(*s1); + } + } + } + } + } +} + static void ggml_compute_forward_out_prod_q_f32( const ggml_compute_params * params, ggml_tensor * dst) { @@ -4291,9 +4537,8 @@ void ggml_compute_forward_out_prod( } break; case GGML_TYPE_F16: { - GGML_ABORT("fatal error"); // todo - // ggml_compute_forward_out_prod_f16_f32(params, dst); - } + ggml_compute_forward_out_prod_f16_f32(params, dst); + } break; case GGML_TYPE_F32: { ggml_compute_forward_out_prod_f32(params, dst); @@ -10300,6 +10545,196 @@ void ggml_compute_forward_cross_entropy_loss_back( } } +// ggml_compute_forward_cross_entropy_loss_masked_f32 + +static void ggml_compute_forward_cross_entropy_loss_masked_f32( + const ggml_compute_params * params, + ggml_tensor * dst) { + + const ggml_tensor * src0 = dst->src[0]; // logits + const ggml_tensor * src1 = dst->src[1]; // targets + const ggml_tensor * src2 = dst->src[2]; // mask (1 for assistant tokens, 0 for masked) + + GGML_ASSERT(src0->type == GGML_TYPE_F32); + GGML_ASSERT(src1->type == GGML_TYPE_F32); + GGML_ASSERT(src2->type == GGML_TYPE_F32); + GGML_ASSERT(src0->nb[0] == ggml_type_size(src0->type)); + GGML_ASSERT(src1->nb[0] == ggml_type_size(src1->type)); + GGML_ASSERT(src2->nb[0] == ggml_type_size(src2->type)); + GGML_ASSERT(ggml_are_same_shape(src0, src1)); + GGML_ASSERT(ggml_is_scalar(dst)); + GGML_ASSERT(dst->type == GGML_TYPE_F32); + + const int64_t nc = src0->ne[0]; + const int64_t nr = ggml_nrows(src0); + + const int ith = params->ith; + const int nth = params->nth; + + float * sums = (float *) params->wdata; + float * st = ((float *) params->wdata) + nth + ith*nc; + float sum_thread = 0.0f; + int64_t valid_tokens_thread = 0; + + GGML_ASSERT(params->wsize >= sizeof(float) * (nth + nth * nc) + sizeof(int64_t) * nth); + int64_t * valid_counts = (int64_t *)(((float *) params->wdata) + nth + nth * nc); + + const int64_t dr = (nr + nth - 1)/nth; + + const int64_t ir0 = dr*ith; + const int64_t ir1 = MIN(ir0 + dr, nr); + + for (int64_t i1 = ir0; i1 < ir1; ++i1) { + const float * s0 = (const float *)((const char *) src0->data + i1*src0->nb[1]); + const float * s1 = (const float *)((const char *) src1->data + i1*src1->nb[1]); + const float * mask_row = (const float *)((const char *) src2->data + i1*src2->nb[1]); + const float mask_value = mask_row[0]; + + if (mask_value <= 0.5f) continue; + + float max = -INFINITY; + ggml_vec_max_f32(nc, &max, s0); + + const ggml_float sum_softmax = ggml_vec_log_soft_max_f32(nc, st, s0, max); + assert(sum_softmax >= 0.0); + + ggml_vec_add1_f32(nc, st, st, -sum_softmax); + + float sum_st = 0.0f; + for (int64_t i = 0; i < nc; i++) { + sum_st += st[i] * s1[i]; + } + + sum_thread += sum_st; + valid_tokens_thread++; + } + + sums[ith] = sum_thread; + valid_counts[ith] = valid_tokens_thread; + ggml_barrier(params->threadpool); + + if (ith == 0) { + float total_loss = 0.0f; + int64_t total_valid = 0; + + for (int i = 0; i < nth; i++) { + total_loss += sums[i]; + total_valid += valid_counts[i]; + } + + float * dp = (float *) dst->data; + if (total_valid > 0) { + float final_loss = -total_loss / (float)total_valid; + dp[0] = final_loss; + } else { + dp[0] = 0.0f; + } + } +} + +// ggml_compute_forward_cross_entropy_loss_masked_back_f32 + +static void ggml_compute_forward_cross_entropy_loss_masked_back_f32( + const ggml_compute_params * params, + ggml_tensor * dst) { + const ggml_tensor * grad = dst->src[0]; + const ggml_tensor * src0f = dst->src[1]; + const ggml_tensor * src1f = dst->src[2]; + const ggml_tensor * src2f = dst->src[3]; + + GGML_ASSERT(ggml_is_contiguous(dst)); + GGML_ASSERT(ggml_is_contiguous(src0f)); + GGML_ASSERT(ggml_is_contiguous(src1f)); + GGML_ASSERT(ggml_is_contiguous(src2f)); + GGML_ASSERT(ggml_is_contiguous(grad)); + GGML_ASSERT(ggml_are_same_shape(src0f, src1f) && ggml_are_same_shape(src0f, dst)); + + const int64_t ith = params->ith; + const int64_t nth = params->nth; + + const int64_t nc = src0f->ne[0]; + const int64_t nr = ggml_nrows(src0f); + + int64_t total_valid = 0; + for (int64_t i1 = 0; i1 < nr; i1++) { + const float * mask_row = (const float *)((const char *) src2f->data + i1*src2f->nb[1]); + const float mask_value = mask_row[0]; + if (mask_value > 0.5f) { + total_valid++; + } + } + + const int64_t dr = (nr + nth - 1)/nth; + + const int64_t ir0 = dr*ith; + const int64_t ir1 = MIN(ir0 + dr, nr); + + const float upstream_grad = ((const float *) grad->data)[0]; + + float d_scale = 0.0f; + if (total_valid > 0) { + d_scale = upstream_grad / (float) total_valid; + } + + for (int64_t i1 = ir0; i1 < ir1; i1++) { + float * ds0 = (float *)((char *) dst->data + i1*dst->nb[1]); + const float * s0 = (const float *)((const char *) src0f->data + i1*src0f->nb[1]); + const float * s1 = (const float *)((const char *) src1f->data + i1*src1f->nb[1]); + const float * mask_row = (const float *)((const char *) src2f->data + i1*src2f->nb[1]); + const float mask_value = mask_row[0]; + + if (mask_value > 0.5f) { + float max = -INFINITY; + ggml_vec_max_f32(nc, &max, s0); + const ggml_float sum = ggml_vec_soft_max_f32(nc, ds0, s0, max); + assert(sum > 0.0); + ggml_vec_scale_f32(nc, ds0, 1.0/sum); + + ggml_vec_sub_f32(nc, ds0, ds0, s1); + ggml_vec_scale_f32(nc, ds0, d_scale); + } else { + ggml_vec_set_f32(nc, ds0, 0.0f); + + } + } +} + +void ggml_compute_forward_cross_entropy_loss_masked( + const ggml_compute_params * params, + ggml_tensor * dst) { + + const ggml_tensor * src0 = dst->src[0]; + + switch (src0->type) { + case GGML_TYPE_F32: + { + ggml_compute_forward_cross_entropy_loss_masked_f32(params, dst); + } break; + default: + { + GGML_ABORT("fatal error"); + } + } +} + +void ggml_compute_forward_cross_entropy_loss_masked_back( + const ggml_compute_params * params, + ggml_tensor * dst) { + + const ggml_tensor * src0 = dst->src[0]; + + switch (src0->type) { + case GGML_TYPE_F32: + { + ggml_compute_forward_cross_entropy_loss_masked_back_f32(params, dst); + } break; + default: + { + GGML_ABORT("fatal error"); + } + } +} + static void ggml_compute_forward_opt_step_adamw_f32( const ggml_compute_params * params, ggml_tensor * dst) { diff --git a/ggml/src/ggml-cpu/ops.h b/ggml/src/ggml-cpu/ops.h index 0fdfee79766e..d762ac2c0a6a 100644 --- a/ggml/src/ggml-cpu/ops.h +++ b/ggml/src/ggml-cpu/ops.h @@ -38,10 +38,12 @@ void ggml_compute_forward_cumsum(const struct ggml_compute_params * params, stru void ggml_compute_forward_mean(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_argmax(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_count_equal(const struct ggml_compute_params * params, struct ggml_tensor * dst); +void ggml_compute_forward_count_equal_masked(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_repeat(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_repeat_back(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_concat(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_silu_back(const struct ggml_compute_params * params, struct ggml_tensor * dst); +void ggml_compute_forward_geglu_back(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_norm(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_rms_norm(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_rms_norm_back(const struct ggml_compute_params * params, struct ggml_tensor * dst); @@ -108,6 +110,8 @@ void ggml_compute_forward_map_custom3(const struct ggml_compute_params * params, void ggml_compute_forward_custom(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_cross_entropy_loss(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_cross_entropy_loss_back(const struct ggml_compute_params * params, struct ggml_tensor * dst); +void ggml_compute_forward_cross_entropy_loss_masked(const struct ggml_compute_params * params, struct ggml_tensor * dst); +void ggml_compute_forward_cross_entropy_loss_masked_back(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_opt_step_adamw(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_mul_mat(const struct ggml_compute_params * params, struct ggml_tensor * dst); void ggml_compute_forward_opt_step_sgd(const struct ggml_compute_params * params, struct ggml_tensor * dst); diff --git a/ggml/src/ggml-cpu/quants.c b/ggml/src/ggml-cpu/quants.c index 365cb36d2d76..b39278f1b8e5 100644 --- a/ggml/src/ggml-cpu/quants.c +++ b/ggml/src/ggml-cpu/quants.c @@ -332,6 +332,35 @@ void ggml_vec_dot_q8_0_q8_0_generic(int n, float * GGML_RESTRICT s, size_t bs, c *s = sumf; } +void ggml_vec_dot_q8_1_q8_1_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { + const int qk = QK8_1; + const int nb = n / qk; + + assert(n % qk == 0); + assert(nrc == 1); + UNUSED(nrc); + UNUSED(bx); + UNUSED(by); + UNUSED(bs); + + const block_q8_1 * GGML_RESTRICT x = vx; + const block_q8_1 * GGML_RESTRICT y = vy; + + float sumf = 0; + + for (int ib = 0; ib < nb; ++ib) { + int sumi = 0; + + for (int j = 0; j < qk; ++j) { + sumi += x[ib].qs[j]*y[ib].qs[j]; + } + + sumf += sumi*(GGML_CPU_FP16_TO_FP32(x[ib].d)*GGML_CPU_FP16_TO_FP32(y[ib].d)); + } + + *s = sumf; +} + void ggml_vec_dot_tq1_0_q8_K_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { assert(nrc == 1); UNUSED(nrc); @@ -416,6 +445,90 @@ void ggml_vec_dot_tq2_0_q8_K_generic(int n, float * GGML_RESTRICT s, size_t bs, *s = sumf; } +void ggml_vec_dot_tq2_0_q8_0_generic( + int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, + const void * GGML_RESTRICT vy, size_t by, int nrc) { + assert(nrc == 1); + UNUSED(nrc); + UNUSED(bx); + UNUSED(by); + UNUSED(bs); + + GGML_ASSERT(n % QK_K == 0); + const int nb = n / QK_K; + const int q8_blocks = QK_K / QK8_0; + + const block_tq2_0 * GGML_RESTRICT x = vx; + const block_q8_0 * GGML_RESTRICT y = vy; + + float sumf = 0.0f; + + for (int ib = 0; ib < nb; ++ib) { + const float dx = GGML_CPU_FP16_TO_FP32(x[ib].d); + const block_q8_0 * y_blocks = y + ib * q8_blocks; + + size_t block_index = 0; + for (size_t j = 0; j < sizeof(x[ib].qs); j += 32) { + for (int l = 0; l < 4; ++l) { + const block_q8_0 * yb = &y_blocks[block_index++]; + const float dy = GGML_CPU_FP16_TO_FP32(yb->d); + + int32_t sumi = 0; + for (int k = 0; k < QK8_0; ++k) { + const int8_t xv = ((x[ib].qs[j + k] >> (l*2)) & 3) - 1; + sumi += xv * yb->qs[k]; + } + + sumf += (float) sumi * dx * dy; + } + } + } + + *s = sumf; +} + +void ggml_vec_dot_tq2_0_q8_1_generic( + int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, + const void * GGML_RESTRICT vy, size_t by, int nrc) { + assert(nrc == 1); + UNUSED(nrc); + UNUSED(bx); + UNUSED(by); + UNUSED(bs); + + GGML_ASSERT(n % QK_K == 0); + const int nb = n / QK_K; + const int q8_blocks = QK_K / QK8_1; + + const block_tq2_0 * GGML_RESTRICT x = vx; + const block_q8_1 * GGML_RESTRICT y = vy; + + float sumf = 0.0f; + + for (int ib = 0; ib < nb; ++ib) { + const float dx = GGML_CPU_FP16_TO_FP32(x[ib].d); + const block_q8_1 * y_blocks = y + ib * q8_blocks; + + size_t block_index = 0; + for (size_t j = 0; j < sizeof(x[ib].qs); j += 32) { + for (int l = 0; l < 4; ++l) { + const block_q8_1 * yb = &y_blocks[block_index++]; + const float dy = GGML_CPU_FP16_TO_FP32(yb->d); + + int32_t sumi = 0; + for (int k = 0; k < QK8_1; ++k) { + const int8_t xv = ((x[ib].qs[j + k] >> (l*2)) & 3) - 1; + sumi += xv * yb->qs[k]; + } + + sumf += (float) sumi * dx * dy; + } + } + } + + *s = sumf; +} + void ggml_vec_dot_q2_K_q8_K_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { assert(nrc == 1); UNUSED(nrc); diff --git a/ggml/src/ggml-cpu/quants.h b/ggml/src/ggml-cpu/quants.h index d83eb1b144d4..d859bf0234d7 100644 --- a/ggml/src/ggml-cpu/quants.h +++ b/ggml/src/ggml-cpu/quants.h @@ -40,6 +40,7 @@ void ggml_vec_dot_q4_1_q8_1(int n, float * GGML_RESTRICT s, size_t bs, const voi void ggml_vec_dot_q5_0_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_q5_1_q8_1(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_q8_0_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); +void ggml_vec_dot_q8_1_q8_1(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_mxfp4_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); @@ -51,6 +52,8 @@ void ggml_vec_dot_q6_K_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const voi void ggml_vec_dot_tq1_0_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_tq2_0_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); +void ggml_vec_dot_tq2_0_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); +void ggml_vec_dot_tq2_0_q8_1(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_iq2_xxs_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_iq2_xs_q8_K (int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); @@ -71,11 +74,14 @@ void ggml_vec_dot_q4_1_q8_1_generic(int n, float * GGML_RESTRICT s, size_t bs, c void ggml_vec_dot_q5_0_q8_0_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_q5_1_q8_1_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_q8_0_q8_0_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); +void ggml_vec_dot_q8_1_q8_1_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_mxfp4_q8_0_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_tq1_0_q8_K_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_tq2_0_q8_K_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); +void ggml_vec_dot_tq2_0_q8_0_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); +void ggml_vec_dot_tq2_0_q8_1_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_q2_K_q8_K_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); void ggml_vec_dot_q3_K_q8_K_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); diff --git a/ggml/src/ggml-cpu/vec.h b/ggml/src/ggml-cpu/vec.h index bd80805fdc55..a2c6be8b3200 100644 --- a/ggml/src/ggml-cpu/vec.h +++ b/ggml/src/ggml-cpu/vec.h @@ -1310,6 +1310,32 @@ inline static void ggml_vec_silu_backward_f16(const int n, ggml_fp16_t * dx, con } } +inline static float ggml_gelu_backward_f32(float x, float dy) { + const float tanh_arg = SQRT_2_OVER_PI * x * (1.0f + GELU_COEF_A * x * x); + const float tanh_val = tanhf(tanh_arg); + const float sech2_val = 1.0f - tanh_val * tanh_val; + const float dtanh_dx = SQRT_2_OVER_PI * (1.0f + 3.0f * GELU_COEF_A * x * x) * sech2_val; + return dy * 0.5f * (1.0f + tanh_val + x * dtanh_dx); +} + +inline static void ggml_vec_gelu_backward_f32(const int n, float * dx, const float * x, const float * dy) { + for (int i = 0; i < n; ++i) { + dx[i] = ggml_gelu_backward_f32(x[i], dy[i]); + } +} + +inline static void ggml_vec_gelu_backward_f16(const int n, ggml_fp16_t * dx, const ggml_fp16_t * x, const ggml_fp16_t * dy) { + for (int i = 0; i < n; ++i) { + float xi = GGML_CPU_FP16_TO_FP32(x[i]); + float tanh_arg = SQRT_2_OVER_PI * xi * (1.0f + GELU_COEF_A * xi * xi); + float tanh_val = tanhf(tanh_arg); + float sech2_val = 1.0f - tanh_val * tanh_val; + float dtanh_dx = SQRT_2_OVER_PI * (1.0f + 3.0f * GELU_COEF_A * xi * xi) * sech2_val; + + dx[i] = GGML_CPU_FP32_TO_FP16(GGML_CPU_FP16_TO_FP32(dy[i]) * 0.5f * (1.0f + tanh_val + xi * dtanh_dx)); + } +} + inline static void ggml_vec_reglu_f32 (const int n, float * y, const float * x, const float * g) { for (int i = 0; i < n; ++i) { y[i] = (x[i] > 0.f) ? x[i] * g[i] : 0.f; diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 88352a92aa28..21301eae8d69 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -4385,7 +4385,15 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g } } break; case GGML_OP_OUT_PROD: - return op->type == GGML_TYPE_F32 && op->src[0]->type == GGML_TYPE_F32 && op->src[1]->type == GGML_TYPE_F32; + { + const ggml_tensor * src0 = op->src[0]; + const ggml_tensor * src1 = op->src[1]; + const bool src0_supported = src0 != nullptr && + (src0->type == GGML_TYPE_F32 || ggml_get_to_fp32_cuda(src0->type) != nullptr); + const bool src1_supported = src1 != nullptr && + (src1->type == GGML_TYPE_F32 || ggml_get_to_fp32_cuda(src1->type) != nullptr); + return op->type == GGML_TYPE_F32 && src0_supported && src1_supported; + } break; case GGML_OP_GET_ROWS: { switch (op->src[0]->type) { diff --git a/ggml/src/ggml-cuda/out-prod.cu b/ggml/src/ggml-cuda/out-prod.cu index c9b2b699c6a5..2a0305c2db31 100644 --- a/ggml/src/ggml-cuda/out-prod.cu +++ b/ggml/src/ggml-cuda/out-prod.cu @@ -1,4 +1,5 @@ #include "out-prod.cuh" +#include "convert.cuh" #include @@ -8,10 +9,55 @@ void ggml_cuda_out_prod(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { GGML_TENSOR_BINARY_OP_LOCALS - GGML_ASSERT(src0->type == GGML_TYPE_F32); - GGML_ASSERT(src1->type == GGML_TYPE_F32); + const bool src0_requires_fp32 = src0->type != GGML_TYPE_F32; + const bool src1_requires_fp32 = src1->type != GGML_TYPE_F32; + GGML_ASSERT(dst->type == GGML_TYPE_F32); + cudaStream_t stream = ctx.stream(); + ggml_cuda_pool & pool = ctx.pool(); + + // temp buffers + float * src0_f32 = nullptr; + float * src1_f32 = nullptr; + bool allocated_src0 = false; + bool allocated_src1 = false; + ggml_cuda_pool_alloc src0_alloc(pool); + ggml_cuda_pool_alloc src1_alloc(pool); + + if (src0_requires_fp32) { + const size_t src0_size = ggml_nelements(src0); + src0_alloc.alloc(src0_size); + src0_f32 = src0_alloc.ptr; + allocated_src0 = true; + + const to_fp32_cuda_t to_fp32_cuda = ggml_get_to_fp32_cuda(src0->type); + if (to_fp32_cuda != nullptr) { + to_fp32_cuda(src0->data, src0_f32, ggml_nelements(src0), stream); + } else { + GGML_ABORT("Unsupported type for src0 in ggml_cuda_out_prod"); + } + } else { + src0_f32 = (float *) src0->data; + } + + if (src1_requires_fp32) { + const size_t src1_size = ggml_nelements(src1); + src1_alloc.alloc(src1_size); + src1_f32 = src1_alloc.ptr; + allocated_src1 = true; + + const to_fp32_cuda_t to_fp32_cuda = ggml_get_to_fp32_cuda(src1->type); + if (to_fp32_cuda != nullptr) { + to_fp32_cuda(src1->data, src1_f32, ggml_nelements(src1), stream); + } else { + GGML_ABORT("Unsupported type for src1 in ggml_cuda_out_prod"); + } + } else { + src1_f32 = (float *) src1->data; + } + + GGML_ASSERT(ne01 == ne11); GGML_ASSERT(ne0 == ne00); GGML_ASSERT(ne1 == ne10); @@ -22,11 +68,11 @@ void ggml_cuda_out_prod(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { GGML_ASSERT(ne2 == src1->ne[2]); GGML_ASSERT(ne3 == src1->ne[3]); - const float * src0_d = (const float *) src0->data; - const float * src1_d = (const float *) src1->data; + // Use dequantized data + const float * src0_d = src0_f32; + const float * src1_d = src1_f32; float * dst_d = (float *) dst->data; - cudaStream_t stream = ctx.stream(); cublasHandle_t handle = ctx.cublas_handle(); const float alpha = 1.0f; @@ -34,19 +80,25 @@ void ggml_cuda_out_prod(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { CUBLAS_CHECK(cublasSetStream(handle, stream)); - const int64_t lda = nb01 / sizeof(float); + const int64_t lda = allocated_src0 ? ne00 : (nb01 / sizeof(float)); const int64_t ldc = nb1 / sizeof(float); const bool src1_T = ggml_is_transposed(src1); const cublasOperation_t src1_cublas_op = src1_T ? CUBLAS_OP_N : CUBLAS_OP_T; - const int64_t ldb = (src1_T ? nb10 : nb11) / sizeof(float); - GGML_ASSERT( (src1_T ? nb11 : nb10) == sizeof(float)); + const int64_t ldb = allocated_src1 ? + (src1_T ? ne10 : ne11) : + ((src1_T ? nb10 : nb11) / sizeof(float)); + + // Only assert for non dequantized src1 + if (!allocated_src1) { + GGML_ASSERT((src1_T ? nb11 : nb10) == sizeof(float)); + } // data strides in dimensions 2/3 - const size_t s02 = nb02 / sizeof(float); - const size_t s03 = nb03 / sizeof(float); - const size_t s12 = nb12 / sizeof(float); - const size_t s13 = nb13 / sizeof(float); + const size_t s02 = allocated_src0 ? (ne00 * ne01) : nb02 / sizeof(float); + const size_t s03 = allocated_src0 ? (ne00 * ne01 * ne02): nb03 / sizeof(float); + const size_t s12 = allocated_src1 ? (ne10 * ne11) : nb12 / sizeof(float); + const size_t s13 = allocated_src1 ? (ne10 * ne11 * ne12) : nb13 / sizeof(float); const size_t s2 = nb2 / sizeof(float); const size_t s3 = nb3 / sizeof(float); @@ -65,4 +117,5 @@ void ggml_cuda_out_prod(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { &beta, dst_d + i3 *s3 + i2 *s2, ldc)); } } + } diff --git a/ggml/src/ggml-metal/ggml-metal-context.m b/ggml/src/ggml-metal/ggml-metal-context.m index e66646284dbc..8587317c2e0f 100644 --- a/ggml/src/ggml-metal/ggml-metal-context.m +++ b/ggml/src/ggml-metal/ggml-metal-context.m @@ -8,6 +8,7 @@ #import "ggml-metal-ops.h" #import +#import #import @@ -127,6 +128,12 @@ ggml_metal_t ggml_metal_init(ggml_metal_device_t dev) { res->use_fusion = getenv("GGML_METAL_FUSION_DISABLE") == nil; res->use_concurrency = getenv("GGML_METAL_CONCURRENCY_DISABLE") == nil; +#if TARGET_OS_IPHONE + if (getenv("GGML_METAL_CONCURRENCY_DISABLE") == nil) { + res->use_concurrency = false; + } +#endif + { const char * val = getenv("GGML_METAL_GRAPH_DEBUG"); res->debug_graph = val ? atoi(val) : 0; @@ -536,6 +543,7 @@ void ggml_metal_set_n_cb(ggml_metal_t ctx, int n_cb) { } ctx->encode_async = Block_copy(^(size_t iter) { + @autoreleasepool { const int cb_idx = iter; const int n_cb_l = ctx->n_cb; @@ -580,6 +588,7 @@ void ggml_metal_set_n_cb(ggml_metal_t ctx, int n_cb) { if (cb_idx < 2 || ctx->abort_callback == NULL) { [cmd_buf commit]; } + } // @autoreleasepool }); } diff --git a/ggml/src/ggml-metal/ggml-metal-device.cpp b/ggml/src/ggml-metal/ggml-metal-device.cpp index 329500a03e0d..112afd32c29a 100644 --- a/ggml/src/ggml-metal/ggml-metal-device.cpp +++ b/ggml/src/ggml-metal/ggml-metal-device.cpp @@ -597,6 +597,12 @@ ggml_metal_pipeline_t ggml_metal_library_get_pipeline_mul_mv(ggml_metal_library_ nr0 = N_R0_Q8_0; smem = 32*sizeof(float)*N_R0_Q8_0; } break; + case GGML_TYPE_TQ2_0: + { + nsg = N_SG_TQ2_0; + nr0 = N_R0_TQ2_0; + smem = 32*sizeof(float)*N_R0_TQ2_0; + } break; case GGML_TYPE_MXFP4: { nsg = N_SG_MXFP4; @@ -817,6 +823,12 @@ ggml_metal_pipeline_t ggml_metal_library_get_pipeline_mul_mv_id(ggml_metal_libra nr0 = N_R0_Q8_0; smem = 32*sizeof(float)*N_R0_Q8_0; } break; + case GGML_TYPE_TQ2_0: + { + nsg = N_SG_TQ2_0; + nr0 = N_R0_TQ2_0; + smem = 32*sizeof(float)*N_R0_TQ2_0; + } break; case GGML_TYPE_MXFP4: { nsg = N_SG_MXFP4; @@ -1681,6 +1693,111 @@ ggml_metal_pipeline_t ggml_metal_library_get_pipeline_timestep_embedding(ggml_me return res; } +ggml_metal_pipeline_t ggml_metal_library_get_pipeline_out_prod(ggml_metal_library_t lib, const ggml_tensor * op) { + assert(op->op == GGML_OP_OUT_PROD); + assert(op->type == GGML_TYPE_F32); + + char base[256]; + char name[256]; + + const ggml_type src0t = op->src[0]->type; + const ggml_type src1t = op->src[1]->type; + + if (src0t == GGML_TYPE_Q8_0) { + snprintf(base, 256, "kernel_out_prod_q8_0_%s", ggml_type_name(src1t)); + } else if (src0t == GGML_TYPE_Q4_0) { + snprintf(base, 256, "kernel_out_prod_q4_0_%s", ggml_type_name(src1t)); + } else if (src0t == GGML_TYPE_F16 && src1t == GGML_TYPE_F32) { + snprintf(base, 256, "kernel_out_prod_f16_f32"); + } else if (src0t == GGML_TYPE_F32 && src1t == GGML_TYPE_F32) { + snprintf(base, 256, "kernel_out_prod_f32"); + } else if (src0t == GGML_TYPE_F32 && src1t == GGML_TYPE_F16) { + snprintf(base, 256, "kernel_out_prod_f32_f16"); + } else if (src0t == GGML_TYPE_F16 && src1t == GGML_TYPE_F16) { + snprintf(base, 256, "kernel_out_prod_f16"); + } else { + GGML_ABORT("fatal error"); + } + + snprintf(name, 256, "%s", base); + + ggml_metal_pipeline_t res = ggml_metal_library_get_pipeline(lib, name); + if (res) { + return res; + } + + res = ggml_metal_library_compile_pipeline(lib, base, name, nullptr); + + return res; +} + +ggml_metal_pipeline_t ggml_metal_library_get_pipeline_silu_back(ggml_metal_library_t lib, const ggml_tensor * op) { + assert(op->op == GGML_OP_SILU_BACK); + + char base[256]; + char name[256]; + + const int64_t n = ggml_nelements(op); + const char * suffix = (n % 4 == 0) ? "_4" : ""; + + snprintf(base, 256, "kernel_silu_back%s", suffix); + snprintf(name, 256, "%s", base); + + ggml_metal_pipeline_t res = ggml_metal_library_get_pipeline(lib, name); + if (res) { + return res; + } + + res = ggml_metal_library_compile_pipeline(lib, base, name, nullptr); + + return res; +} + +ggml_metal_pipeline_t ggml_metal_library_get_pipeline_soft_max_back(ggml_metal_library_t lib, const ggml_tensor * op) { + assert(op->op == GGML_OP_SOFT_MAX_BACK); + + char base[256]; + char name[256]; + + const char * suffix = (op->src[0]->ne[0] % 4 == 0) ? "_4" : ""; + + snprintf(base, 256, "kernel_soft_max_back%s", suffix); + snprintf(name, 256, "%s", base); + + ggml_metal_pipeline_t res = ggml_metal_library_get_pipeline(lib, name); + if (res) { + return res; + } + + res = ggml_metal_library_compile_pipeline(lib, base, name, nullptr); + + ggml_metal_pipeline_set_smem(res, 32*sizeof(float)); + + return res; +} + +ggml_metal_pipeline_t ggml_metal_library_get_pipeline_rms_norm_back(ggml_metal_library_t lib, const ggml_tensor * op) { + assert(op->op == GGML_OP_RMS_NORM_BACK); + GGML_UNUSED(op); + + char base[256]; + char name[256]; + + snprintf(base, 256, "kernel_rms_norm_back"); + snprintf(name, 256, "%s", base); + + ggml_metal_pipeline_t res = ggml_metal_library_get_pipeline(lib, name); + if (res) { + return res; + } + + res = ggml_metal_library_compile_pipeline(lib, base, name, nullptr); + + ggml_metal_pipeline_set_smem(res, 2*32*sizeof(float)); + + return res; +} + ggml_metal_pipeline_t ggml_metal_library_get_pipeline_opt_step_adamw(ggml_metal_library_t lib, const ggml_tensor * op) { assert(op->op == GGML_OP_OPT_STEP_ADAMW); diff --git a/ggml/src/ggml-metal/ggml-metal-device.h b/ggml/src/ggml-metal/ggml-metal-device.h index 3976e622b9b9..d1dde6fd4325 100644 --- a/ggml/src/ggml-metal/ggml-metal-device.h +++ b/ggml/src/ggml-metal/ggml-metal-device.h @@ -144,8 +144,12 @@ ggml_metal_pipeline_t ggml_metal_library_get_pipeline_pad (ggml_me ggml_metal_pipeline_t ggml_metal_library_get_pipeline_pad_reflect_1d (ggml_metal_library_t lib, const struct ggml_tensor * op); ggml_metal_pipeline_t ggml_metal_library_get_pipeline_arange (ggml_metal_library_t lib, const struct ggml_tensor * op); ggml_metal_pipeline_t ggml_metal_library_get_pipeline_timestep_embedding(ggml_metal_library_t lib, const struct ggml_tensor * op); -ggml_metal_pipeline_t ggml_metal_library_get_pipeline_opt_step_adamw (ggml_metal_library_t lib, const struct ggml_tensor * op); -ggml_metal_pipeline_t ggml_metal_library_get_pipeline_opt_step_sgd (ggml_metal_library_t lib, const struct ggml_tensor * op); +ggml_metal_pipeline_t ggml_metal_library_get_pipeline_out_prod (ggml_metal_library_t lib, const struct ggml_tensor * op); +ggml_metal_pipeline_t ggml_metal_library_get_pipeline_silu_back (ggml_metal_library_t lib, const struct ggml_tensor * op); +ggml_metal_pipeline_t ggml_metal_library_get_pipeline_soft_max_back (ggml_metal_library_t lib, const struct ggml_tensor * op); +ggml_metal_pipeline_t ggml_metal_library_get_pipeline_rms_norm_back (ggml_metal_library_t lib, const struct ggml_tensor * op); +ggml_metal_pipeline_t ggml_metal_library_get_pipeline_opt_step_adamw (ggml_metal_library_t lib, const struct ggml_tensor * op); +ggml_metal_pipeline_t ggml_metal_library_get_pipeline_opt_step_sgd (ggml_metal_library_t lib, const struct ggml_tensor * op); ggml_metal_pipeline_t ggml_metal_library_get_pipeline_flash_attn_ext_pad( ggml_metal_library_t lib, diff --git a/ggml/src/ggml-metal/ggml-metal-device.m b/ggml/src/ggml-metal/ggml-metal-device.m index 62bc4ba45fcc..15a453ea3571 100644 --- a/ggml/src/ggml-metal/ggml-metal-device.m +++ b/ggml/src/ggml-metal/ggml-metal-device.m @@ -849,6 +849,31 @@ bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_te case GGML_OP_DIV: case GGML_OP_ADD_ID: return op->src[0]->type == GGML_TYPE_F32; + case GGML_OP_OUT_PROD: + if (op->type != GGML_TYPE_F32) { + return false; + } + { + const enum ggml_type src0_type = op->src[0]->type; + const enum ggml_type src1_type = op->src[1]->type; + + if (src0_type == GGML_TYPE_F16 && src1_type == GGML_TYPE_F32) { + return true; + } + if (src0_type == GGML_TYPE_F32 && (src1_type == GGML_TYPE_F32 || src1_type == GGML_TYPE_F16)) { + return true; + } + if (src0_type == GGML_TYPE_F16 && src1_type == GGML_TYPE_F16) { + return true; + } + if (src0_type == GGML_TYPE_Q8_0 && (src1_type == GGML_TYPE_F32 || src1_type == GGML_TYPE_F16)) { + return true; + } + if (src0_type == GGML_TYPE_Q4_0 && (src1_type == GGML_TYPE_F32 || src1_type == GGML_TYPE_F16)) { + return true; + } + } + return false; case GGML_OP_ACC: case GGML_OP_REPEAT: case GGML_OP_SCALE: @@ -861,6 +886,16 @@ bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_te op->type == GGML_TYPE_F32; case GGML_OP_CLAMP: return op->src[0]->type == GGML_TYPE_F32; + case GGML_OP_SILU_BACK: + return op->type == GGML_TYPE_F32 && + op->src[0] != NULL && op->src[1] != NULL && + op->src[0]->type == GGML_TYPE_F32 && + op->src[1]->type == GGML_TYPE_F32 && + ggml_is_contiguous_1(op->src[0]) && + ggml_is_contiguous_1(op->src[1]) && + ggml_is_contiguous_1(op) && + ggml_are_same_shape(op, op->src[0]) && + ggml_are_same_shape(op, op->src[1]); case GGML_OP_SQR: case GGML_OP_SQRT: case GGML_OP_SIN: @@ -875,6 +910,29 @@ bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_te case GGML_OP_SOFT_MAX: case GGML_OP_GROUP_NORM: return has_simdgroup_reduction && ggml_is_contiguous_rows(op->src[0]); + case GGML_OP_SOFT_MAX_BACK: + { + if (!has_simdgroup_reduction || + op->type != GGML_TYPE_F32 || + op->src[0] == NULL || op->src[1] == NULL || + op->src[0]->type != GGML_TYPE_F32 || + op->src[1]->type != GGML_TYPE_F32 || + !ggml_is_contiguous_1(op->src[0]) || + !ggml_is_contiguous_1(op->src[1]) || + !ggml_is_contiguous_1(op) || + !ggml_are_same_shape(op, op->src[0]) || + !ggml_are_same_shape(op, op->src[1])) { + return false; + } + + float max_bias = 0.0f; + memcpy(&max_bias, ((const float *) op->op_params) + 1, sizeof(float)); + if (max_bias != 0.0f) { + return false; + } + + return true; + } case GGML_OP_L2_NORM: return has_simdgroup_reduction && (op->ne[0] % 4 == 0 && ggml_is_contiguous_1(op->src[0])); case GGML_OP_ARGMAX: @@ -882,6 +940,18 @@ bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_te case GGML_OP_NORM: case GGML_OP_RMS_NORM: return has_simdgroup_reduction && (ggml_is_contiguous_rows(op->src[0])); + case GGML_OP_RMS_NORM_BACK: + return has_simdgroup_reduction && + op->type == GGML_TYPE_F32 && + op->src[0] != NULL && op->src[1] != NULL && + op->src[0]->type == GGML_TYPE_F32 && + op->src[1]->type == GGML_TYPE_F32 && + op->ne[0] % 4 == 0 && + ggml_is_contiguous_1(op->src[0]) && + ggml_is_contiguous_1(op->src[1]) && + ggml_is_contiguous_1(op) && + ggml_are_same_shape(op, op->src[0]) && + ggml_are_same_shape(op, op->src[1]); case GGML_OP_ROPE: return true; case GGML_OP_IM2COL: @@ -940,7 +1010,7 @@ bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_te return true; case GGML_OP_MUL_MAT: case GGML_OP_MUL_MAT_ID: - return has_simdgroup_reduction; + return op->src[0]->type != GGML_TYPE_TQ1_0 && has_simdgroup_reduction; case GGML_OP_CPY: case GGML_OP_DUP: case GGML_OP_CONT: @@ -997,7 +1067,7 @@ bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_te }; } case GGML_OP_GET_ROWS: - return true; + return op->src[0]->type != GGML_TYPE_TQ1_0; case GGML_OP_SET_ROWS: { if (op->src[0]->type != GGML_TYPE_F32) { diff --git a/ggml/src/ggml-metal/ggml-metal-impl.h b/ggml/src/ggml-metal/ggml-metal-impl.h index 342dc4f8c378..e63e95d856fe 100644 --- a/ggml/src/ggml-metal/ggml-metal-impl.h +++ b/ggml/src/ggml-metal/ggml-metal-impl.h @@ -23,6 +23,9 @@ #define N_R0_Q8_0 2 #define N_SG_Q8_0 4 +#define N_R0_Q8_1 2 +#define N_SG_Q8_1 4 + #define N_R0_MXFP4 2 #define N_SG_MXFP4 2 @@ -41,6 +44,9 @@ #define N_R0_Q6_K 2 #define N_SG_Q6_K 2 +#define N_R0_TQ2_0 4 +#define N_SG_TQ2_0 2 + #define N_R0_IQ1_S 4 #define N_SG_IQ1_S 2 @@ -207,6 +213,33 @@ typedef struct { uint64_t nb3; } ggml_metal_kargs_cpy; +typedef struct { + int32_t ne00; + int32_t ne01; + int32_t ne02; + int32_t ne03; + uint64_t nb00; + uint64_t nb01; + uint64_t nb02; + uint64_t nb03; + int32_t ne10; + int32_t ne11; + int32_t ne12; + int32_t ne13; + uint64_t nb10; + uint64_t nb11; + uint64_t nb12; + uint64_t nb13; + int32_t ne0; + int32_t ne1; + int32_t ne2; + int32_t ne3; + uint64_t nb0; + uint64_t nb1; + uint64_t nb2; + uint64_t nb3; +} ggml_metal_kargs_out_prod; + typedef struct { int64_t ne10; int64_t ne11; @@ -253,6 +286,7 @@ typedef struct { int32_t sect_2; int32_t sect_3; bool src2; + float sin_sign; } ggml_metal_kargs_rope; typedef struct { @@ -488,6 +522,21 @@ typedef struct { uint64_t nbf3[3]; } ggml_metal_kargs_norm; +typedef struct { + int32_t ne00; + int32_t ne00_4; + uint64_t nb01; + uint64_t nb02; + uint64_t nb03; + uint64_t nb11; + uint64_t nb12; + uint64_t nb13; + uint64_t nb1; + uint64_t nb2; + uint64_t nb3; + float eps; +} ggml_metal_kargs_rms_norm_back; + typedef struct { int32_t ne00; int32_t ne00_4; @@ -674,6 +723,21 @@ typedef struct { int32_t n_head_log2; } ggml_metal_kargs_soft_max; +typedef struct { + int32_t ne00; + int32_t ne00_4; + uint64_t nb01; + uint64_t nb02; + uint64_t nb03; + uint64_t nb11; + uint64_t nb12; + uint64_t nb13; + uint64_t nb1; + uint64_t nb2; + uint64_t nb3; + float scale; +} ggml_metal_kargs_soft_max_back; + typedef struct { int64_t ne00; int64_t ne01; diff --git a/ggml/src/ggml-metal/ggml-metal-ops.cpp b/ggml/src/ggml-metal/ggml-metal-ops.cpp index 9871e976f23d..479948f809ab 100644 --- a/ggml/src/ggml-metal/ggml-metal-ops.cpp +++ b/ggml/src/ggml-metal/ggml-metal-ops.cpp @@ -278,6 +278,10 @@ static int ggml_metal_op_encode_impl(ggml_metal_op_t ctx, int idx) { { n_fuse = ggml_metal_op_repeat(ctx, idx); } break; + case GGML_OP_OUT_PROD: + { + n_fuse = ggml_metal_op_out_prod(ctx, idx); + } break; case GGML_OP_ACC: { n_fuse = ggml_metal_op_acc(ctx, idx); @@ -299,6 +303,10 @@ static int ggml_metal_op_encode_impl(ggml_metal_op_t ctx, int idx) { { n_fuse = ggml_metal_op_unary(ctx, idx); } break; + case GGML_OP_SILU_BACK: + { + n_fuse = ggml_metal_op_silu_back(ctx, idx); + } break; case GGML_OP_GLU: { n_fuse = ggml_metal_op_glu(ctx, idx); @@ -320,6 +328,10 @@ static int ggml_metal_op_encode_impl(ggml_metal_op_t ctx, int idx) { { n_fuse = ggml_metal_op_soft_max(ctx, idx); } break; + case GGML_OP_SOFT_MAX_BACK: + { + n_fuse = ggml_metal_op_soft_max_back(ctx, idx); + } break; case GGML_OP_SSM_CONV: { n_fuse = ggml_metal_op_ssm_conv(ctx, idx); @@ -362,6 +374,10 @@ static int ggml_metal_op_encode_impl(ggml_metal_op_t ctx, int idx) { { n_fuse = ggml_metal_op_norm(ctx, idx); } break; + case GGML_OP_RMS_NORM_BACK: + { + n_fuse = ggml_metal_op_rms_norm_back(ctx, idx); + } break; case GGML_OP_ROPE: { n_fuse = ggml_metal_op_rope(ctx, idx); @@ -3125,6 +3141,7 @@ int ggml_metal_op_rope(ggml_metal_op_t ctx, int idx) { /* sect_2 =*/ sect_2, /* sect_3 =*/ sect_3, /* src2 =*/ op->src[2] != nullptr, + /* sin_sign =*/ 1.0f, }; ggml_metal_pipeline_t pipeline = ggml_metal_library_get_pipeline_rope(lib, op); @@ -3899,6 +3916,218 @@ int ggml_metal_op_leaky_relu(ggml_metal_op_t ctx, int idx) { return 1; } +int ggml_metal_op_out_prod(ggml_metal_op_t ctx, int idx) { + ggml_tensor * op = ctx->node(idx); + + ggml_metal_library_t lib = ctx->lib; + ggml_metal_encoder_t enc = ctx->enc; + + GGML_TENSOR_LOCALS( int32_t, ne0, op->src[0], ne); + GGML_TENSOR_LOCALS(uint64_t, nb0, op->src[0], nb); + GGML_TENSOR_LOCALS( int32_t, ne1, op->src[1], ne); + GGML_TENSOR_LOCALS(uint64_t, nb1, op->src[1], nb); + GGML_TENSOR_LOCALS( int32_t, ne, op, ne); + GGML_TENSOR_LOCALS(uint64_t, nb, op, nb); + + ggml_metal_kargs_out_prod args = { + /*.ne00 =*/ ne00, + /*.ne01 =*/ ne01, + /*.ne02 =*/ ne02, + /*.ne03 =*/ ne03, + /*.nb00 =*/ nb00, + /*.nb01 =*/ nb01, + /*.nb02 =*/ nb02, + /*.nb03 =*/ nb03, + /*.ne10 =*/ ne10, + /*.ne11 =*/ ne11, + /*.ne12 =*/ ne12, + /*.ne13 =*/ ne13, + /*.nb10 =*/ nb10, + /*.nb11 =*/ nb11, + /*.nb12 =*/ nb12, + /*.nb13 =*/ nb13, + /*.ne0 =*/ ne0, + /*.ne1 =*/ ne1, + /*.ne2 =*/ ne2, + /*.ne3 =*/ ne3, + /*.nb0 =*/ nb0, + /*.nb1 =*/ nb1, + /*.nb2 =*/ nb2, + /*.nb3 =*/ nb3, + }; + + ggml_metal_pipeline_t pipeline = ggml_metal_library_get_pipeline_out_prod(lib, op); + + const int threads = ne0 < 1 ? 1 : (int) ne0; + const int nth = std::min(ggml_metal_pipeline_max_theads_per_threadgroup(pipeline), threads); + + ggml_metal_encoder_set_pipeline(enc, pipeline); + ggml_metal_encoder_set_bytes (enc, &args, sizeof(args), 0); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[0]), 1); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[1]), 2); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op), 3); + + ggml_metal_encoder_dispatch_threadgroups(enc, ne1, ne2, ne3, nth, 1, 1); + + return 1; +} + +int ggml_metal_op_silu_back(ggml_metal_op_t ctx, int idx) { + ggml_tensor * op = ctx->node(idx); + + ggml_metal_library_t lib = ctx->lib; + ggml_metal_encoder_t enc = ctx->enc; + + ggml_metal_pipeline_t pipeline = ggml_metal_library_get_pipeline_silu_back(lib, op); + + int64_t n = ggml_nelements(op); + + if (n % 4 == 0) { + n /= 4; + } + + ggml_metal_encoder_set_pipeline(enc, pipeline); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[0]), 0); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[1]), 1); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op), 2); + + ggml_metal_encoder_dispatch_threadgroups(enc, n, 1, 1, 1, 1, 1); + + return 1; +} + +int ggml_metal_op_soft_max_back(ggml_metal_op_t ctx, int idx) { + ggml_tensor * op = ctx->node(idx); + + ggml_metal_library_t lib = ctx->lib; + ggml_metal_encoder_t enc = ctx->enc; + + GGML_TENSOR_LOCALS( int32_t, ne0, op->src[0], ne); + GGML_TENSOR_LOCALS(uint64_t, nb0, op->src[0], nb); + GGML_TENSOR_LOCALS( int32_t, ne1, op->src[1], ne); + GGML_TENSOR_LOCALS(uint64_t, nb1, op->src[1], nb); + GGML_TENSOR_LOCALS( int32_t, ne, op, ne); + GGML_TENSOR_LOCALS(uint64_t, nb, op, nb); + + float scale = 1.0f; + float max_bias = 0.0f; + + memcpy(&scale, ((const int32_t *) op->op_params) + 0, sizeof(scale)); + memcpy(&max_bias, ((const int32_t *) op->op_params) + 1, sizeof(max_bias)); + + GGML_ASSERT(max_bias == 0.0f); + + const bool use_vec4 = (ne00 % 4) == 0; + + ggml_metal_kargs_soft_max_back args = { + /*.ne00 =*/ ne00, + /*.ne00_4 =*/ ne00/4, + /*.nb01 =*/ nb01, + /*.nb02 =*/ nb02, + /*.nb03 =*/ nb03, + /*.nb11 =*/ nb11, + /*.nb12 =*/ nb12, + /*.nb13 =*/ nb13, + /*.nb1 =*/ nb1, + /*.nb2 =*/ nb2, + /*.nb3 =*/ nb3, + /*.scale =*/ scale, + }; + + ggml_metal_pipeline_t pipeline = ggml_metal_library_get_pipeline_soft_max_back(lib, op); + + int nth = 32; // SIMD width + + if (use_vec4) { + const int ne00_4 = ne00/4; + while (nth < ne00_4 && nth < ggml_metal_pipeline_max_theads_per_threadgroup(pipeline)) { + nth *= 2; + } + nth = std::min(nth, ne00_4); + } else { + while (nth < ne00 && nth < ggml_metal_pipeline_max_theads_per_threadgroup(pipeline)) { + nth *= 2; + } + nth = std::min(nth, ne00); + } + + nth = std::max(1, nth); + nth = std::min(nth, ggml_metal_pipeline_max_theads_per_threadgroup(pipeline)); + + const size_t smem = ggml_metal_pipeline_get_smem(pipeline); + + ggml_metal_encoder_set_pipeline(enc, pipeline); + ggml_metal_encoder_set_bytes (enc, &args, sizeof(args), 0); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[0]), 1); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[1]), 2); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op), 3); + + ggml_metal_encoder_set_threadgroup_memory_size(enc, smem, 0); + + ggml_metal_encoder_dispatch_threadgroups(enc, ne01, ne02, ne03, nth, 1, 1); + + return 1; +} + +int ggml_metal_op_rms_norm_back(ggml_metal_op_t ctx, int idx) { + ggml_tensor * op = ctx->node(idx); + + ggml_metal_library_t lib = ctx->lib; + ggml_metal_encoder_t enc = ctx->enc; + + GGML_TENSOR_LOCALS( int32_t, ne0, op->src[0], ne); + GGML_TENSOR_LOCALS(uint64_t, nb0, op->src[0], nb); + GGML_TENSOR_LOCALS( int32_t, ne1, op->src[1], ne); + GGML_TENSOR_LOCALS(uint64_t, nb1, op->src[1], nb); + GGML_TENSOR_LOCALS( int32_t, ne, op, ne); + GGML_TENSOR_LOCALS(uint64_t, nb, op, nb); + + GGML_ASSERT(ne00 % 4 == 0); + + float eps; + memcpy(&eps, op->op_params, sizeof(float)); + + ggml_metal_kargs_rms_norm_back args = { + /*.ne00 =*/ ne00, + /*.ne00_4 =*/ ne00/4, + /*.nb01 =*/ nb01, + /*.nb02 =*/ nb02, + /*.nb03 =*/ nb03, + /*.nb11 =*/ nb11, + /*.nb12 =*/ nb12, + /*.nb13 =*/ nb13, + /*.nb1 =*/ nb1, + /*.nb2 =*/ nb2, + /*.nb3 =*/ nb3, + /*.eps =*/ eps, + }; + + ggml_metal_pipeline_t pipeline = ggml_metal_library_get_pipeline_rms_norm_back(lib, op); + + int nth = 32; // SIMD width + + while (nth < ne00/4 && nth < ggml_metal_pipeline_max_theads_per_threadgroup(pipeline)) { + nth *= 2; + } + + nth = std::min(nth, ggml_metal_pipeline_max_theads_per_threadgroup(pipeline)); + nth = std::min(nth, ne00/4); + + const size_t smem = ggml_metal_pipeline_get_smem(pipeline); + + ggml_metal_encoder_set_pipeline(enc, pipeline); + ggml_metal_encoder_set_bytes (enc, &args, sizeof(args), 0); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[0]), 1); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[1]), 2); + ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op), 3); + + ggml_metal_encoder_set_threadgroup_memory_size(enc, smem, 0); + + ggml_metal_encoder_dispatch_threadgroups(enc, ne01, ne02, ne03, nth, 1, 1); + + return 1; +} + int ggml_metal_op_opt_step_adamw(ggml_metal_op_t ctx, int idx) { ggml_tensor * op = ctx->node(idx); diff --git a/ggml/src/ggml-metal/ggml-metal-ops.h b/ggml/src/ggml-metal/ggml-metal-ops.h index b5546146e13d..423b5bf654c5 100644 --- a/ggml/src/ggml-metal/ggml-metal-ops.h +++ b/ggml/src/ggml-metal/ggml-metal-ops.h @@ -83,8 +83,12 @@ int ggml_metal_op_argmax (ggml_metal_op_t ctx, int idx); int ggml_metal_op_argsort (ggml_metal_op_t ctx, int idx); int ggml_metal_op_top_k (ggml_metal_op_t ctx, int idx); int ggml_metal_op_leaky_relu (ggml_metal_op_t ctx, int idx); -int ggml_metal_op_opt_step_adamw (ggml_metal_op_t ctx, int idx); -int ggml_metal_op_opt_step_sgd (ggml_metal_op_t ctx, int idx); +int ggml_metal_op_out_prod (ggml_metal_op_t ctx, int idx); +int ggml_metal_op_silu_back (ggml_metal_op_t ctx, int idx); +int ggml_metal_op_soft_max_back (ggml_metal_op_t ctx, int idx); +int ggml_metal_op_rms_norm_back (ggml_metal_op_t ctx, int idx); +int ggml_metal_op_opt_step_adamw (ggml_metal_op_t ctx, int idx); +int ggml_metal_op_opt_step_sgd (ggml_metal_op_t ctx, int idx); #ifdef __cplusplus } diff --git a/ggml/src/ggml-metal/ggml-metal.cpp b/ggml/src/ggml-metal/ggml-metal.cpp index 70bf6f3d981f..3dfc115d95bb 100644 --- a/ggml/src/ggml-metal/ggml-metal.cpp +++ b/ggml/src/ggml-metal/ggml-metal.cpp @@ -464,6 +464,20 @@ static ggml_guid_t ggml_backend_metal_guid(void) { return &guid; } +static int ggml_backend_metal_default_n_cb(void) { + const char * env_n_cb = getenv("GGML_METAL_N_CB"); + if (env_n_cb != nullptr) { + const int n_cb = atoi(env_n_cb); + return n_cb > 0 ? n_cb : 1; + } + +#if defined(__APPLE__) && defined(__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__) + return 2; +#else + return 1; +#endif +} + ggml_backend_t ggml_backend_metal_init(void) { ggml_backend_dev_t dev = ggml_backend_reg_dev_get(ggml_backend_metal_reg(), 0); ggml_metal_device_t ctx_dev = (ggml_metal_device_t)dev->context; @@ -483,7 +497,7 @@ ggml_backend_t ggml_backend_metal_init(void) { /* .context = */ ctx, }; - ggml_backend_metal_set_n_cb(backend, 1); + ggml_backend_metal_set_n_cb(backend, ggml_backend_metal_default_n_cb()); return backend; } @@ -575,7 +589,7 @@ static ggml_backend_t ggml_backend_metal_device_init(ggml_backend_dev_t dev, con /* .context = */ ctx, }; - ggml_backend_metal_set_n_cb(backend, 1); + ggml_backend_metal_set_n_cb(backend, ggml_backend_metal_default_n_cb()); return backend; diff --git a/ggml/src/ggml-metal/ggml-metal.metal b/ggml/src/ggml-metal/ggml-metal.metal index 3ca8d9b322b0..cf94c12f5fe8 100644 --- a/ggml/src/ggml-metal/ggml-metal.metal +++ b/ggml/src/ggml-metal/ggml-metal.metal @@ -129,6 +129,126 @@ void dequantize_q4_0(device const block_q4_0 * xb, short il, thread type4x4 & re reg = (type4x4) reg_f; } +template +void dequantize_tq2_0(device const block_tq2_0 * xb, short il, thread type4x4 & reg) { + const half d = xb->d; + device const uint8_t * q = (device const uint8_t *) xb->qs + 32*(il/8) + 16*(il & 1); + + il = (il/2)%4; + + const half coef_lut[4] = { 1.h, 1/4.h, 1/16.h, 1/64.h }; + const uchar mask_lut[4] = { 3, 12, 48, 192 }; + half coef = coef_lut[il]; + uchar mask = mask_lut[il]; + const half dl = d * coef; + for (int i = 0; i < 16; ++i) { + reg[i/4][i%4] = dl * (q[i] & mask) - d; + } +} + +template +void kernel_mul_mv_tq2_0_f32_impl( + args_t args, + device const char * src0, + device const char * src1, + device char * dst, + threadgroup char * shmem, + uint3 tgpig, + ushort tiisg, + ushort sgitg) { + + const int nb = args.ne00/QK_K; + const int r0 = tgpig.x; + const int r1 = tgpig.y; + const int im = tgpig.z; + + const int first_row = (r0 * N_SG_TQ2_0 + sgitg) * N_R0_TQ2_0; + + const uint i12 = im%args.ne12; + const uint i13 = im/args.ne12; + + const uint64_t offset0 = first_row*args.nb01 + (i12/args.r2)*args.nb02 + (i13/args.r3)*args.nb03; + const uint64_t offset1 = r1*args.nb11 + (i12 )*args.nb12 + (i13 )*args.nb13; + + device const block_tq2_0 * x = (device const block_tq2_0 *) (src0 + offset0); + device const float * y = (device const float *) (src1 + offset1); + + float yl[32]; + float sumf[N_R0_TQ2_0] = {0.f}; + float all_sum; + + const int step = sizeof(block_tq2_0) * nb / 2; + + const int ix = tiisg/8; // 0...3 + const int it = tiisg%8; // 0...7 + const int iq = it/4; // 0 or 1 + const int ir = it%4; // 0...3 + + device const float * y4 = y + ix * QK_K + 128 * iq + 8 * ir; + + for (int ib = ix; ib < nb; ib += 4) { + + float sumy = 0.f; + for (int i = 0; i < 8; ++i) { + yl[i+ 0] = y4[i+ 0]; sumy += yl[i+ 0]; + yl[i+ 8] = y4[i+32]; sumy += yl[i+ 8]; + yl[i+16] = y4[i+64]; sumy += yl[i+16]; + yl[i+24] = y4[i+96]; sumy += yl[i+24]; + } + + device const half * dh = &x[ib].d; + device const uint16_t * qs = (device const uint16_t *) x[ib].qs + 16 * iq + 4 * ir; + + for (int row = 0; row < N_R0_TQ2_0; row++) { + + float4 acc1 = {0.f, 0.f, 0.f, 0.f}; + float4 acc2 = {0.f, 0.f, 0.f, 0.f}; + for (int i = 0; i < 8; i += 2) { + acc1[0] += yl[i+ 0] * (qs[i/2] & 0x0003); + acc2[0] += yl[i+ 1] * (qs[i/2] & 0x0300); + acc1[1] += yl[i+ 8] * (qs[i/2] & 0x000c); + acc2[1] += yl[i+ 9] * (qs[i/2] & 0x0c00); + acc1[2] += yl[i+16] * (qs[i/2] & 0x0030); + acc2[2] += yl[i+17] * (qs[i/2] & 0x3000); + acc1[3] += yl[i+24] * (qs[i/2] & 0x00c0); + acc2[3] += yl[i+25] * (qs[i/2] & 0xc000); + } + float dall = dh[0]; + sumf[row] += dall * ((acc1[0] + 1.f/256.f * acc2[0]) * 1.f/ 1.f + + (acc1[1] + 1.f/256.f * acc2[1]) * 1.f/ 4.f + + (acc1[2] + 1.f/256.f * acc2[2]) * 1.f/16.f + + (acc1[3] + 1.f/256.f * acc2[3]) * 1.f/64.f - sumy); + + qs += step; + dh += step; + } + + y4 += 4 * QK_K; + } + + device float * dst_f32 = (device float *) dst + (uint64_t) im*args.ne0*args.ne1 + (uint64_t) r1*args.ne0; + + for (int row = 0; row < N_R0_TQ2_0 && first_row + row < args.ne0; ++row) { + all_sum = simd_sum(sumf[row]); + if (tiisg == 0) { + dst_f32[first_row + row] = all_sum; + } + } +} + +[[host_name("kernel_mul_mv_tq2_0_f32")]] +kernel void kernel_mul_mv_tq2_0_f32( + constant ggml_metal_kargs_mul_mv & args, + device const char * src0, + device const char * src1, + device char * dst, + uint3 tgpig[[threadgroup_position_in_grid]], + ushort tiisg[[thread_index_in_simdgroup]], + ushort sgitg[[simdgroup_index_in_threadgroup]]) { + + kernel_mul_mv_tq2_0_f32_impl(args, src0, src1, dst, nullptr, tgpig, tiisg, sgitg); +} + template void dequantize_q4_0_t4(device const block_q4_0 * xb, short il, thread type4 & reg) { device const uint16_t * qs = ((device const uint16_t *)xb + 1); @@ -1098,6 +1218,162 @@ template [[host_name("kernel_repeat_f16")]] kernel kernel_repeat_t kernel_repeat template [[host_name("kernel_repeat_i32")]] kernel kernel_repeat_t kernel_repeat; template [[host_name("kernel_repeat_i16")]] kernel kernel_repeat_t kernel_repeat; +template +kernel void kernel_out_prod_impl( + constant ggml_metal_kargs_out_prod & args, + device const char * src0, + device const char * src1, + device char * dst, + uint3 tgpig[[threadgroup_position_in_grid]], + ushort3 tpitg[[thread_position_in_threadgroup]], + ushort3 ntg[[threads_per_threadgroup]]) { + const int i3 = tgpig.z; + const int i2 = tgpig.y; + const int i1 = tgpig.x; + + const int dps2 = args.ne02 > 0 ? args.ne2 / args.ne02 : 1; + const int dps3 = args.ne03 > 0 ? args.ne3 / args.ne03 : 1; + + const int i02 = args.ne02 > 0 ? i2 / dps2 : 0; + const int i03 = args.ne03 > 0 ? i3 / dps3 : 0; + + device const char * src0_base = src0 + i02*args.nb02 + i03*args.nb03; + device const char * src1_base = src1 + i1*args.nb10 + i2*args.nb12 + i3*args.nb13; + device char * dst_base = dst + i1*args.nb1 + i2*args.nb2 + i3*args.nb3; + + for (int i0 = tpitg.x; i0 < args.ne0; i0 += ntg.x) { + float acc = 0.0f; + + for (int i01 = 0; i01 < args.ne01; ++i01) { + device const char * src0_row = src0_base + i01*args.nb01; + const float v0 = (float) *((device const src0_t *)(src0_row + i0*args.nb00)); + const float v1 = (float) *((device const src1_t *)(src1_base + i01*args.nb11)); + + acc += v0 * v1; + } + + *((device float *)(dst_base + i0*args.nb0)) = acc; + } +} + +typedef decltype(kernel_out_prod_impl) kernel_out_prod_f32_t; +typedef decltype(kernel_out_prod_impl) kernel_out_prod_f16_f32_t; +typedef decltype(kernel_out_prod_impl) kernel_out_prod_f32_f16_t; +typedef decltype(kernel_out_prod_impl) kernel_out_prod_f16_t; + +template [[host_name("kernel_out_prod_f32")]] kernel kernel_out_prod_f32_t kernel_out_prod_impl; +template [[host_name("kernel_out_prod_f16_f32")]] kernel kernel_out_prod_f16_f32_t kernel_out_prod_impl; +template [[host_name("kernel_out_prod_f32_f16")]] kernel kernel_out_prod_f32_f16_t kernel_out_prod_impl; +template [[host_name("kernel_out_prod_f16")]] kernel kernel_out_prod_f16_t kernel_out_prod_impl; + +template +kernel void kernel_out_prod_q8_0_impl( + constant ggml_metal_kargs_out_prod & args, + device const char * src0, + device const char * src1, + device char * dst, + uint3 tgpig[[threadgroup_position_in_grid]], + ushort3 tpitg[[thread_position_in_threadgroup]], + ushort3 ntg[[threads_per_threadgroup]]) { + const int i3 = tgpig.z; + const int i2 = tgpig.y; + const int i1 = tgpig.x; + + const int dps2 = args.ne02 > 0 ? args.ne2 / args.ne02 : 1; + const int dps3 = args.ne03 > 0 ? args.ne3 / args.ne03 : 1; + + const int i02 = args.ne02 > 0 ? i2 / dps2 : 0; + const int i03 = args.ne03 > 0 ? i3 / dps3 : 0; + + device const char * src0_base = src0 + i02*args.nb02 + i03*args.nb03; + device const char * src1_base = src1 + i1*args.nb10 + i2*args.nb12 + i3*args.nb13; + device char * dst_base = dst + i1*args.nb1 + i2*args.nb2 + i3*args.nb3; + + for (int i0 = tpitg.x; i0 < args.ne0; i0 += ntg.x) { + const int ib = i0 / QK8_0; + const int ix = i0 % QK8_0; + + float acc = 0.0f; + + for (int i01 = 0; i01 < args.ne01; ++i01) { + device const char * src0_row_char = src0_base + i01*args.nb01; + device const block_q8_0 * src0_row = (device const block_q8_0 *) src0_row_char; + const block_q8_0 blk = src0_row[ib]; + + const float v0 = (float) blk.d * (float) blk.qs[ix]; + + device const src1_t * src1_row = (device const src1_t *)(src1_base + i01*args.nb11); + const float v1 = (float) src1_row[0]; + + acc += v0 * v1; + } + + *((device float *)(dst_base + i0*args.nb0)) = acc; + } +} + +typedef decltype(kernel_out_prod_q8_0_impl) kernel_out_prod_q8_0_f32_t; +typedef decltype(kernel_out_prod_q8_0_impl) kernel_out_prod_q8_0_f16_t; + +template [[host_name("kernel_out_prod_q8_0_f32")]] kernel kernel_out_prod_q8_0_f32_t kernel_out_prod_q8_0_impl; +template [[host_name("kernel_out_prod_q8_0_f16")]] kernel kernel_out_prod_q8_0_f16_t kernel_out_prod_q8_0_impl; + +template +kernel void kernel_out_prod_q4_0_impl( + constant ggml_metal_kargs_out_prod & args, + device const char * src0, + device const char * src1, + device char * dst, + uint3 tgpig[[threadgroup_position_in_grid]], + ushort3 tpitg[[thread_position_in_threadgroup]], + ushort3 ntg[[threads_per_threadgroup]]) { + const int i3 = tgpig.z; + const int i2 = tgpig.y; + const int i1 = tgpig.x; + + const int dps2 = args.ne02 > 0 ? args.ne2 / args.ne02 : 1; + const int dps3 = args.ne03 > 0 ? args.ne3 / args.ne03 : 1; + + const int i02 = args.ne02 > 0 ? i2 / dps2 : 0; + const int i03 = args.ne03 > 0 ? i3 / dps3 : 0; + + device const char * src0_base = src0 + i02*args.nb02 + i03*args.nb03; + device const char * src1_base = src1 + i1*args.nb10 + i2*args.nb12 + i3*args.nb13; + device char * dst_base = dst + i1*args.nb1 + i2*args.nb2 + i3*args.nb3; + + for (int i0 = tpitg.x; i0 < args.ne0; i0 += ntg.x) { + const int ib = i0 / QK4_0; + const int ix = i0 % QK4_0; + const int iq = ix % (QK4_0 / 2); + const bool upper = ix >= (QK4_0 / 2); + + float acc = 0.0f; + + for (int i01 = 0; i01 < args.ne01; ++i01) { + device const char * src0_row_char = src0_base + i01*args.nb01; + device const block_q4_0 * src0_row = (device const block_q4_0 *) src0_row_char; + const block_q4_0 blk = src0_row[ib]; + + const uint8_t q = blk.qs[iq]; + const int nibble = upper ? (q >> 4) : (q & 0x0F); + const float v0 = ((float) blk.d) * ((float) nibble - 8.0f); + + device const src1_t * src1_row = (device const src1_t *)(src1_base + i01*args.nb11); + const float v1 = (float) src1_row[0]; + + acc += v0 * v1; + } + + *((device float *)(dst_base + i0*args.nb0)) = acc; + } +} + +typedef decltype(kernel_out_prod_q4_0_impl) kernel_out_prod_q4_0_f32_t; +typedef decltype(kernel_out_prod_q4_0_impl) kernel_out_prod_q4_0_f16_t; + +template [[host_name("kernel_out_prod_q4_0_f32")]] kernel kernel_out_prod_q4_0_f32_t kernel_out_prod_q4_0_impl; +template [[host_name("kernel_out_prod_q4_0_f16")]] kernel kernel_out_prod_q4_0_f16_t kernel_out_prod_q4_0_impl; + // assumption: src1 is a row // broadcast src1 into src0 template @@ -1404,6 +1680,28 @@ kernel void kernel_silu_f32_4( dst[tpig] = x / (1.0f + exp(-x)); } +kernel void kernel_silu_back( + device const float * grad, + device const float * src1, + device float * dst, + uint tpig[[thread_position_in_grid]]) { + const float dy = grad[tpig]; + const float x = src1[tpig]; + const float s = 1.0f/(1.0f + exp(-x)); + dst[tpig] = dy*s*(1.0f + x*(1.0f - s)); +} + +kernel void kernel_silu_back_4( + device const float4 * grad, + device const float4 * src1, + device float4 * dst, + uint tpig[[thread_position_in_grid]]) { + const float4 dy = grad[tpig]; + const float4 x = src1[tpig]; + const float4 s = 1.0f/(1.0f + exp(-x)); + dst[tpig] = dy*s*(1.0f + x*(1.0f - s)); +} + kernel void kernel_elu_f32( device const float * src0, device float * dst, @@ -2165,6 +2463,107 @@ template [[host_name("kernel_soft_max_f32")]] kernel kernel_soft_max_t kerne template [[host_name("kernel_soft_max_f16_4")]] kernel kernel_soft_max_4_t kernel_soft_max_4; template [[host_name("kernel_soft_max_f32_4")]] kernel kernel_soft_max_4_t kernel_soft_max_4; +[[host_name("kernel_soft_max_back")]] +kernel void kernel_soft_max_back( + constant ggml_metal_kargs_soft_max_back & args, + device const char * src0, + device const char * src1, + device char * dst, + threadgroup float * shmem_f32 [[threadgroup(0)]], + uint3 tgpig[[threadgroup_position_in_grid]], + ushort3 tpitg[[thread_position_in_threadgroup]], + ushort sgitg[[simdgroup_index_in_threadgroup]], + ushort tiisg[[thread_index_in_simdgroup]], + ushort3 ntg[[threads_per_threadgroup]]) { + if (sgitg == 0) { + shmem_f32[tiisg] = 0.0f; + } + + const int i01 = tgpig.x; + const int i02 = tgpig.y; + const int i03 = tgpig.z; + + device const float * dy = (device const float *) (src0 + i03*args.nb03 + i02*args.nb02 + i01*args.nb01); + device const float * y = (device const float *) (src1 + i03*args.nb13 + i02*args.nb12 + i01*args.nb11); + device float * dx = (device float *) (dst + i03*args.nb3 + i02*args.nb2 + i01*args.nb1); + + float sum = 0.0f; + for (int i00 = tpitg.x; i00 < args.ne00; i00 += ntg.x) { + sum += dy[i00] * y[i00]; + } + + sum = simd_sum(sum); + + threadgroup_barrier(mem_flags::mem_threadgroup); + + if (tiisg == 0) { + shmem_f32[sgitg] = sum; + } + + threadgroup_barrier(mem_flags::mem_threadgroup); + + sum = shmem_f32[tiisg]; + sum = simd_sum(sum); + + const float scale = args.scale; + + for (int i00 = tpitg.x; i00 < args.ne00; i00 += ntg.x) { + dx[i00] = (dy[i00] - sum) * y[i00] * scale; + } +} + +[[host_name("kernel_soft_max_back_4")]] +kernel void kernel_soft_max_back_4( + constant ggml_metal_kargs_soft_max_back & args, + device const char * src0, + device const char * src1, + device char * dst, + threadgroup float * shmem_f32 [[threadgroup(0)]], + uint3 tgpig[[threadgroup_position_in_grid]], + ushort3 tpitg[[thread_position_in_threadgroup]], + ushort sgitg[[simdgroup_index_in_threadgroup]], + ushort tiisg[[thread_index_in_simdgroup]], + ushort3 ntg[[threads_per_threadgroup]]) { + if (sgitg == 0) { + shmem_f32[tiisg] = 0.0f; + } + + const int i01 = tgpig.x; + const int i02 = tgpig.y; + const int i03 = tgpig.z; + + device const float4 * dy = (device const float4 *) (src0 + i03*args.nb03 + i02*args.nb02 + i01*args.nb01); + device const float4 * y = (device const float4 *) (src1 + i03*args.nb13 + i02*args.nb12 + i01*args.nb11); + device float4 * dx = (device float4 *) (dst + i03*args.nb3 + i02*args.nb2 + i01*args.nb1); + + float sum = 0.0f; + for (int i00 = tpitg.x; i00 < args.ne00_4; i00 += ntg.x) { + sum += dot(dy[i00], y[i00]); + } + + sum = simd_sum(sum); + + threadgroup_barrier(mem_flags::mem_threadgroup); + + if (tiisg == 0) { + shmem_f32[sgitg] = sum; + } + + threadgroup_barrier(mem_flags::mem_threadgroup); + + sum = shmem_f32[tiisg]; + sum = simd_sum(sum); + + const float scale = args.scale; + const float4 sum4 = float4(sum); + + for (int i00 = tpitg.x; i00 < args.ne00_4; i00 += ntg.x) { + const float4 dy4 = dy[i00]; + const float4 y4 = y[i00]; + dx[i00] = (dy4 - sum4) * y4 * scale; + } +} + // ref: ggml.c:ggml_compute_forward_ssm_conv_f32 kernel void kernel_ssm_conv_f32_f32( constant ggml_metal_kargs_ssm_conv & args, @@ -2736,6 +3135,73 @@ template [[host_name("kernel_rms_norm_f32_4")]] kernel kernel_rms_norm_f template [[host_name("kernel_rms_norm_mul_f32_4")]] kernel kernel_rms_norm_fuse_t kernel_rms_norm_fuse_impl; template [[host_name("kernel_rms_norm_mul_add_f32_4")]] kernel kernel_rms_norm_fuse_t kernel_rms_norm_fuse_impl; +kernel void kernel_rms_norm_back( + constant ggml_metal_kargs_rms_norm_back & args, + device const char * src0, + device const char * src1, + device char * dst, + threadgroup float * shmem_f32 [[threadgroup(0)]], + uint3 tgpig[[threadgroup_position_in_grid]], + ushort3 tpitg[[thread_position_in_threadgroup]], + ushort sgitg[[simdgroup_index_in_threadgroup]], + ushort tiisg[[thread_index_in_simdgroup]], + ushort3 ntg[[threads_per_threadgroup]]) { + threadgroup float * shmem_xx = shmem_f32; + threadgroup float * shmem_xdz = shmem_f32 + 32; + + if (sgitg == 0) { + shmem_xx[tiisg] = 0.0f; + shmem_xdz[tiisg] = 0.0f; + } + + const int i01 = tgpig.x; + const int i02 = tgpig.y; + const int i03 = tgpig.z; + + device const float4 * dz = (device const float4 *) (src0 + i03*args.nb03 + i02*args.nb02 + i01*args.nb01); + device const float4 * x = (device const float4 *) (src1 + i03*args.nb13 + i02*args.nb12 + i01*args.nb11); + + float sum_xx = 0.0f; + float sum_xdz = 0.0f; + + for (int i00 = tpitg.x; i00 < args.ne00_4; i00 += ntg.x) { + const float4 x4 = x[i00]; + const float4 dz4 = dz[i00]; + sum_xx += dot(x4, x4); + sum_xdz += dot(x4, dz4); + } + + sum_xx = simd_sum(sum_xx); + sum_xdz = simd_sum(sum_xdz); + + threadgroup_barrier(mem_flags::mem_threadgroup); + + if (tiisg == 0) { + shmem_xx[sgitg] = sum_xx; + shmem_xdz[sgitg] = sum_xdz; + } + + threadgroup_barrier(mem_flags::mem_threadgroup); + + sum_xx = shmem_xx[tiisg]; + sum_xdz = shmem_xdz[tiisg]; + + sum_xx = simd_sum(sum_xx); + sum_xdz = simd_sum(sum_xdz); + + const float mean_eps = sum_xx/args.ne00 + args.eps; + const float rrms = rsqrt(mean_eps); + const float sum_eps = sum_xx + args.eps*args.ne00; + const float scale = -sum_xdz/sum_eps; + + device float4 * y = (device float4 *) (dst + i03*args.nb3 + i02*args.nb2 + i01*args.nb1); + + for (int i00 = tpitg.x; i00 < args.ne00_4; i00 += ntg.x) { + float4 dx = dz[i00] + x[i00]*scale; + y[i00] = dx*rrms; + } +} + kernel void kernel_l2_norm_f32( constant ggml_metal_kargs_l2_norm & args, device const char * src0, @@ -3216,6 +3682,184 @@ kernel void kernel_mul_mv_q8_0_f32( kernel_mul_mv_q8_0_f32_impl(args, src0, src1, dst, shmem, tgpig, tiisg, sgitg); } +template +void kernel_mul_mv_tq2_0_q8_1_f32_impl( + args_t args, + device const char * src0, + device const char * src1, + device char * dst, + threadgroup char * shmem, + uint3 tgpig, + ushort tiisg, + ushort sgitg) { + + const int nb = args.ne00/QK_K; + const int q8_blocks = QK_K/QK8_1; + const int total_q8_blocks = nb*q8_blocks; + + const int r0 = tgpig.x*NR0; + const int r1 = tgpig.y; + const int im = tgpig.z; + + const uint i12 = im%args.ne12; + const uint i13 = im/args.ne12; + + const uint64_t offset1 = r1*args.nb11 + (i12 )*args.nb12 + (i13 )*args.nb13; + + device const block_q8_1 * y = (device const block_q8_1 *) (src1 + offset1); + + device const block_tq2_0 * ax[NR0]; + FOR_UNROLL (short row = 0; row < NR0; ++row) { + if (r0 + row < args.ne01) { + const uint64_t offset0 = (r0 + row)*args.nb01 + (i12/args.r2)*args.nb02 + (i13/args.r3)*args.nb03; + ax[row] = (device const block_tq2_0 *) ((device char *) src0 + offset0); + } else { + ax[row] = (device const block_tq2_0 *) src0; + } + } + + float sumf[NR0] = { 0.f }; + + const int lane = sgitg*NW + tiisg; + const int lane_stride = NSG*NW; + + for (int block_index = lane; block_index < total_q8_blocks; block_index += lane_stride) { + const int ib = block_index / q8_blocks; + const int sub = block_index % q8_blocks; + const int chunk = sub/4; + const int bitpair = sub%4; + const int chunk_offset = chunk*QK8_1; + + device const block_q8_1 * yb = y + block_index; + const float dy = (float) yb->d; + device const int8_t * yqs = yb->qs; + + FOR_UNROLL (short row = 0; row < NR0; ++row) { + if (r0 + row >= args.ne01) { + continue; + } + + device const block_tq2_0 * xb = ax[row] + ib; + const float dx = (float) xb->d; + device const uint8_t * xqs = xb->qs + chunk_offset; + + int32_t sumi = 0; + for (int k = 0; k < QK8_1; ++k) { + const int8_t xv = ((xqs[k] >> (bitpair*2)) & 0x3) - 1; + sumi += xv * yqs[k]; + } + + sumf[row] += (float) sumi * dx * dy; + } + } + + device float * dst_f32 = (device float *) dst + (uint64_t) im*args.ne0*args.ne1 + (uint64_t) r1*args.ne0; + + helper_mv_reduce_and_write(dst_f32, sumf, r0, args.ne01, tiisg, sgitg, shmem); +} + +[[host_name("kernel_mul_mv_tq2_0_q8_1")]] +kernel void kernel_mul_mv_tq2_0_q8_1_f32( + constant ggml_metal_kargs_mul_mv & args, + device const char * src0, + device const char * src1, + device char * dst, + threadgroup char * shmem [[threadgroup(0)]], + uint3 tgpig[[threadgroup_position_in_grid]], + ushort tiisg[[thread_index_in_simdgroup]], + ushort sgitg[[simdgroup_index_in_threadgroup]]) { + kernel_mul_mv_tq2_0_q8_1_f32_impl(args, src0, src1, dst, shmem, tgpig, tiisg, sgitg); +} + +template +void kernel_mul_mv_tq2_0_q8_0_f32_impl( + args_t args, + device const char * src0, + device const char * src1, + device char * dst, + threadgroup char * shmem, + uint3 tgpig, + ushort tiisg, + ushort sgitg) { + + const int nb = args.ne00/QK_K; + const int q8_blocks = QK_K/QK8_0; + const int total_q8_blocks = nb*q8_blocks; + + const int r0 = tgpig.x*NR0; + const int r1 = tgpig.y; + const int im = tgpig.z; + + const uint i12 = im%args.ne12; + const uint i13 = im/args.ne12; + + const uint64_t offset1 = r1*args.nb11 + (i12 )*args.nb12 + (i13 )*args.nb13; + + device const block_q8_0 * y = (device const block_q8_0 *) (src1 + offset1); + + device const block_tq2_0 * ax[NR0]; + FOR_UNROLL (short row = 0; row < NR0; ++row) { + if (r0 + row < args.ne01) { + const uint64_t offset0 = (r0 + row)*args.nb01 + (i12/args.r2)*args.nb02 + (i13/args.r3)*args.nb03; + ax[row] = (device const block_tq2_0 *) ((device char *) src0 + offset0); + } else { + ax[row] = (device const block_tq2_0 *) src0; + } + } + + float sumf[NR0] = { 0.f }; + + const int lane = sgitg*NW + tiisg; + const int lane_stride = NSG*NW; + + for (int block_index = lane; block_index < total_q8_blocks; block_index += lane_stride) { + const int ib = block_index / q8_blocks; + const int sub = block_index % q8_blocks; + const int chunk = sub/4; + const int bitpair = sub%4; + const int chunk_offset = chunk*QK8_0; + + device const block_q8_0 * yb = y + block_index; + const float dy = (float) yb->d; + device const int8_t * yqs = yb->qs; + + FOR_UNROLL (short row = 0; row < NR0; ++row) { + if (r0 + row >= args.ne01) { + continue; + } + + device const block_tq2_0 * xb = ax[row] + ib; + const float dx = (float) xb->d; + device const uint8_t * xqs = xb->qs + chunk_offset; + + int32_t sumi = 0; + for (int k = 0; k < QK8_0; ++k) { + const int8_t xv = ((xqs[k] >> (bitpair*2)) & 0x3) - 1; + sumi += xv * yqs[k]; + } + + sumf[row] += (float) sumi * dx * dy; + } + } + + device float * dst_f32 = (device float *) dst + (uint64_t) im*args.ne0*args.ne1 + (uint64_t) r1*args.ne0; + + helper_mv_reduce_and_write(dst_f32, sumf, r0, args.ne01, tiisg, sgitg, shmem); +} + +[[host_name("kernel_mul_mv_tq2_0_q8_0")]] +kernel void kernel_mul_mv_tq2_0_q8_0_f32( + constant ggml_metal_kargs_mul_mv & args, + device const char * src0, + device const char * src1, + device char * dst, + threadgroup char * shmem [[threadgroup(0)]], + uint3 tgpig[[threadgroup_position_in_grid]], + ushort tiisg[[thread_index_in_simdgroup]], + ushort sgitg[[simdgroup_index_in_threadgroup]]) { + kernel_mul_mv_tq2_0_q8_0_f32_impl(args, src0, src1, dst, shmem, tgpig, tiisg, sgitg); +} + // mat-vec kernel processing in chunks of float4 // chpb - chunks per quantization block template @@ -3907,8 +4551,10 @@ kernel void kernel_rope_norm( const float x0 = src[0]; const float x1 = src[1]; - dst_data[0] = x0*cos_theta - x1*sin_theta; - dst_data[1] = x0*sin_theta + x1*cos_theta; + const float sin_theta_mod = sin_theta * args.sin_sign; + + dst_data[0] = x0*cos_theta - x1*sin_theta_mod; + dst_data[1] = x0*sin_theta_mod + x1*cos_theta; } else { device const T * const src = (device T *)(src0 + i3*args.nb03 + i2*args.nb02 + i1*args.nb01 + i0*args.nb00); device T * dst_data = (device T *)( dst + i3*args.nb3 + i2*args.nb2 + i1*args.nb1 + i0*args.nb0); @@ -3960,8 +4606,10 @@ kernel void kernel_rope_neox( const float x0 = src[0]; const float x1 = src[args.n_dims/2]; - dst_data[0] = x0*cos_theta - x1*sin_theta; - dst_data[args.n_dims/2] = x0*sin_theta + x1*cos_theta; + const float sin_theta_mod = sin_theta * args.sin_sign; + + dst_data[0] = x0*cos_theta - x1*sin_theta_mod; + dst_data[args.n_dims/2] = x0*sin_theta_mod + x1*cos_theta; } else { device const T * const src = (device T *)(src0 + i3*args.nb03 + i2*args.nb02 + i1*args.nb01 + i0*args.nb00); device T * dst_data = (device T *)( dst + i3*args.nb3 + i2*args.nb2 + i1*args.nb1 + i0*args.nb0); @@ -4043,8 +4691,10 @@ kernel void kernel_rope_multi( const float x0 = src[0]; const float x1 = src[args.n_dims/2]; - dst_data[0] = x0*cos_theta - x1*sin_theta; - dst_data[args.n_dims/2] = x0*sin_theta + x1*cos_theta; + const float sin_theta_mod = sin_theta * args.sin_sign; + + dst_data[0] = x0*cos_theta - x1*sin_theta_mod; + dst_data[args.n_dims/2] = x0*sin_theta_mod + x1*cos_theta; } else { device const T * const src = (device T *)(src0 + i3*args.nb03 + i2*args.nb02 + i1*args.nb01 + i0*args.nb00); device T * dst_data = (device T *)( dst + i3*args.nb3 + i2*args.nb2 + i1*args.nb1 + i0*args.nb0); @@ -4110,8 +4760,10 @@ kernel void kernel_rope_vision( const float x0 = src[0]; const float x1 = src[args.n_dims]; // different from kernel_rope_multi - dst_data[0] = x0*cos_theta - x1*sin_theta; - dst_data[args.n_dims] = x0*sin_theta + x1*cos_theta; // different from kernel_rope_multi + const float sin_theta_mod = sin_theta * args.sin_sign; + + dst_data[0] = x0*cos_theta - x1*sin_theta_mod; + dst_data[args.n_dims] = x0*sin_theta_mod + x1*cos_theta; // different from kernel_rope_multi } else { device const T * const src = (device T *)(src0 + i3*args.nb03 + i2*args.nb02 + i1*args.nb01 + i0*args.nb00); device T * dst_data = (device T *)( dst + i3*args.nb3 + i2*args.nb2 + i1*args.nb1 + i0*args.nb0); @@ -9244,6 +9896,7 @@ template [[host_name("kernel_get_rows_q4_1")]] kernel get_rows_q_t kernel_get template [[host_name("kernel_get_rows_q5_0")]] kernel get_rows_q_t kernel_get_rows_q; template [[host_name("kernel_get_rows_q5_1")]] kernel get_rows_q_t kernel_get_rows_q; template [[host_name("kernel_get_rows_q8_0")]] kernel get_rows_q_t kernel_get_rows_q; +template [[host_name("kernel_get_rows_tq2_0")]] kernel get_rows_q_t kernel_get_rows_q; template [[host_name("kernel_get_rows_mxfp4")]] kernel get_rows_q_t kernel_get_rows_q; template [[host_name("kernel_get_rows_q2_K")]] kernel get_rows_q_t kernel_get_rows_q; template [[host_name("kernel_get_rows_q3_K")]] kernel get_rows_q_t kernel_get_rows_q; @@ -9306,6 +9959,7 @@ template [[host_name("kernel_mul_mm_q4_1_f32")]] kernel mul_mm_t kernel_mul_m template [[host_name("kernel_mul_mm_q5_0_f32")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_q5_1_f32")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_q8_0_f32")]] kernel mul_mm_t kernel_mul_mm; +template [[host_name("kernel_mul_mm_tq2_0_f32")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_mxfp4_f32")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_q2_K_f32")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_q3_K_f32")]] kernel mul_mm_t kernel_mul_mm; @@ -9324,14 +9978,12 @@ template [[host_name("kernel_mul_mm_iq4_xs_f32")]] kernel mul_mm_t kernel_mul_m template [[host_name("kernel_mul_mm_f32_f16")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_f16_f16")]] kernel mul_mm_t kernel_mul_mm; -#if defined(GGML_METAL_HAS_BF16) -template [[host_name("kernel_mul_mm_bf16_f16")]] kernel mul_mm_t kernel_mul_mm; -#endif template [[host_name("kernel_mul_mm_q4_0_f16")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_q4_1_f16")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_q5_0_f16")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_q5_1_f16")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_q8_0_f16")]] kernel mul_mm_t kernel_mul_mm; +template [[host_name("kernel_mul_mm_tq2_0_f16")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_mxfp4_f16")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_q2_K_f16")]] kernel mul_mm_t kernel_mul_mm; template [[host_name("kernel_mul_mm_q3_K_f16")]] kernel mul_mm_t kernel_mul_mm; @@ -9364,6 +10016,7 @@ template [[host_name("kernel_mul_mm_id_q4_1_f32")]] kernel mul_mm_id kernel_m template [[host_name("kernel_mul_mm_id_q5_0_f32")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_q5_1_f32")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_q8_0_f32")]] kernel mul_mm_id kernel_mul_mm_id; +template [[host_name("kernel_mul_mm_id_tq2_0_f32")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_mxfp4_f32")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_q2_K_f32")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_q3_K_f32")]] kernel mul_mm_id kernel_mul_mm_id; @@ -9382,14 +10035,12 @@ template [[host_name("kernel_mul_mm_id_iq4_xs_f32")]] kernel mul_mm_id kernel_m template [[host_name("kernel_mul_mm_id_f32_f16")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_f16_f16")]] kernel mul_mm_id kernel_mul_mm_id; -#if defined(GGML_METAL_HAS_BF16) -template [[host_name("kernel_mul_mm_id_bf16_f16")]] kernel mul_mm_id kernel_mul_mm_id; -#endif template [[host_name("kernel_mul_mm_id_q4_0_f16")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_q4_1_f16")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_q5_0_f16")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_q5_1_f16")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_q8_0_f16")]] kernel mul_mm_id kernel_mul_mm_id; +template [[host_name("kernel_mul_mm_id_tq2_0_f16")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_mxfp4_f16")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_q2_K_f16")]] kernel mul_mm_id kernel_mul_mm_id; template [[host_name("kernel_mul_mm_id_q3_K_f16")]] kernel mul_mm_id kernel_mul_mm_id; @@ -9551,6 +10202,7 @@ template [[host_name("kernel_mul_mv_id_q3_K_f32")]] kernel kernel_mul_mv_id_t template [[host_name("kernel_mul_mv_id_q4_K_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; template [[host_name("kernel_mul_mv_id_q5_K_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; template [[host_name("kernel_mul_mv_id_q6_K_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; +template [[host_name("kernel_mul_mv_id_tq2_0_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>; template [[host_name("kernel_mul_mv_id_iq1_s_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; template [[host_name("kernel_mul_mv_id_iq1_m_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; template [[host_name("kernel_mul_mv_id_iq2_xxs_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; diff --git a/ggml/src/ggml-opt.cpp b/ggml/src/ggml-opt.cpp index e078ad14a39c..8832382b60db 100644 --- a/ggml/src/ggml-opt.cpp +++ b/ggml/src/ggml-opt.cpp @@ -18,12 +18,13 @@ struct ggml_opt_dataset { ggml_backend_buffer_t buf = nullptr; struct ggml_tensor * data = nullptr; struct ggml_tensor * labels = nullptr; + struct ggml_tensor * masks = nullptr; int64_t ndata = -1; int64_t ndata_shard = -1; size_t nbs_data = -1; size_t nbs_labels = -1; - + size_t nbs_masks = -1; std::vector permutation; }; @@ -45,10 +46,12 @@ struct ggml_opt_context { struct ggml_tensor * inputs = nullptr; struct ggml_tensor * outputs = nullptr; struct ggml_tensor * labels = nullptr; + struct ggml_tensor * masks = nullptr; struct ggml_tensor * loss = nullptr; struct ggml_tensor * pred = nullptr; struct ggml_tensor * ncorrect = nullptr; + struct ggml_tensor * nmasked = nullptr; struct ggml_cgraph * gf = nullptr; struct ggml_cgraph * gb_grad = nullptr; @@ -76,6 +79,7 @@ struct ggml_opt_result { std::vector loss; std::vector pred; int64_t ncorrect = 0; + int64_t nmasked = 0; int64_t opt_period = -1; bool loss_per_datapoint = false; @@ -129,6 +133,63 @@ ggml_opt_dataset_t ggml_opt_dataset_init( return result; } +ggml_opt_dataset_t ggml_opt_dataset_init_with_masks( + enum ggml_type type_data, + enum ggml_type type_label, + enum ggml_type type_mask, + int64_t ne_datapoint, + int64_t ne_label, + int64_t ne_mask, + int64_t ndata, + int64_t ndata_shard) { + GGML_ASSERT(ne_datapoint > 0); + GGML_ASSERT(ne_label >= 0); + GGML_ASSERT(ne_mask >= 0); + GGML_ASSERT(ndata > 0); + GGML_ASSERT(ndata_shard > 0); + + ggml_opt_dataset_t result = new ggml_opt_dataset; + result->ndata = ndata; + result->ndata_shard = ndata_shard; + + { + struct ggml_init_params params = { + /*.mem_size =*/ 3*ggml_tensor_overhead(), + /*.mem_buffer =*/ nullptr, + /*.no_alloc =*/ true, + }; + result->ctx = ggml_init(params); + } + + result->data = ggml_new_tensor_2d(result->ctx, type_data, ne_datapoint, ndata); + result->nbs_data = ggml_nbytes(result->data) * ndata_shard/ndata; + + if (ne_label > 0) { + result->labels = ggml_new_tensor_2d(result->ctx, type_label, ne_label, ndata); + result->nbs_labels = ggml_nbytes(result->labels) * ndata_shard/ndata; + } else { + result->labels = nullptr; + result->nbs_labels = 0; + } + + if (ne_mask > 0) { + result->masks = ggml_new_tensor_2d(result->ctx, type_mask, ne_mask, ndata); + result->nbs_masks = ggml_nbytes(result->masks) * ndata_shard/ndata; + } else { + result->masks = nullptr; + result->nbs_masks = 0; + } + + result->buf = ggml_backend_alloc_ctx_tensors_from_buft(result->ctx, ggml_backend_cpu_buffer_type()); + + const int64_t nshards = ndata/ndata_shard; + result->permutation.resize(nshards); + for (int64_t i = 0; i < nshards; ++i) { + result->permutation[i] = i; + } + return result; +} + void ggml_opt_dataset_free(ggml_opt_dataset_t dataset) { ggml_backend_buffer_free(dataset->buf); ggml_free(dataset->ctx); @@ -147,6 +208,10 @@ struct ggml_tensor * ggml_opt_dataset_labels(ggml_opt_dataset_t dataset) { return dataset->labels; } +struct ggml_tensor * ggml_opt_dataset_masks(ggml_opt_dataset_t dataset) { + return dataset->masks; +} + void ggml_opt_dataset_shuffle(ggml_opt_context_t opt_ctx, ggml_opt_dataset_t dataset, int64_t idata) { GGML_ASSERT(idata <= dataset->ndata); @@ -218,6 +283,32 @@ void ggml_opt_dataset_get_batch_host(ggml_opt_dataset_t dataset, void * data_bat } } +void ggml_opt_dataset_get_batch_host_with_masks(ggml_opt_dataset_t dataset, void * data_batch, size_t nb_data_batch, void * labels_batch, void * masks_batch, int64_t ibatch) { + GGML_ASSERT((labels_batch == nullptr) == (dataset->labels == nullptr)); + GGML_ASSERT((masks_batch == nullptr) == (dataset->masks == nullptr)); + GGML_ASSERT(nb_data_batch % dataset->nbs_data == 0); + + const int64_t shards_per_batch = nb_data_batch / dataset->nbs_data; + + GGML_ASSERT((ibatch + 1)*shards_per_batch <= int64_t(dataset->permutation.size())); + + for (int64_t ishard_batch = 0; ishard_batch < shards_per_batch; ++ishard_batch) { + const int64_t ishard = dataset->permutation[ibatch*shards_per_batch + ishard_batch]; + + auto copy_shard = [ishard, ishard_batch](const struct ggml_tensor * tensor, void * batch_dst, size_t nbs) { + if (batch_dst && tensor) { + const char * ptr_tensor = (const char *) tensor->data + ishard * nbs; + char * ptr_batch = (char *) batch_dst + ishard_batch * nbs; + memcpy(ptr_batch, ptr_tensor, nbs); + } + }; + + copy_shard(dataset->data, data_batch, dataset->nbs_data); + copy_shard(dataset->labels, labels_batch, dataset->nbs_labels); + copy_shard(dataset->masks, masks_batch, dataset->nbs_masks); + } +} + // ====== Model / Context ====== struct ggml_opt_optimizer_params ggml_opt_get_default_optimizer_params(void * userdata) { @@ -412,6 +503,23 @@ static void ggml_opt_build(ggml_opt_context_t opt_ctx) { opt_ctx->loss_per_datapoint = true; break; } + case GGML_OPT_LOSS_TYPE_CROSS_ENTROPY_MASKED: { + opt_ctx->labels = ggml_dup_tensor(ctx_results, opt_ctx->outputs); + ggml_set_input(opt_ctx->labels); + ggml_set_name(opt_ctx->labels, "labels"); + opt_ctx->masks = ggml_new_tensor(ctx_results, GGML_TYPE_F32, GGML_MAX_DIMS, opt_ctx->outputs->ne); + ggml_set_input(opt_ctx->masks); + ggml_set_output(opt_ctx->masks); + ggml_set_name(opt_ctx->masks, "masks"); + opt_ctx->loss = ggml_cross_entropy_loss_masked(ctx_results, opt_ctx->outputs, opt_ctx->labels, opt_ctx->masks); + ggml_set_name(opt_ctx->loss, "loss_cross_entropy_masked"); + if (opt_ctx->opt_period > 1) { + opt_ctx->loss = ggml_scale(ctx_results, opt_ctx->loss, 1.0f / opt_ctx->opt_period); + ggml_set_name(opt_ctx->loss, "loss_cross_entropy_masked_scaled"); + } + opt_ctx->loss_per_datapoint = true; + break; + } case GGML_OPT_LOSS_TYPE_MEAN_SQUARED_ERROR: { opt_ctx->labels = ggml_dup_tensor(ctx_results, opt_ctx->outputs); ggml_set_input(opt_ctx->labels); @@ -433,16 +541,25 @@ static void ggml_opt_build(ggml_opt_context_t opt_ctx) { ggml_set_loss(opt_ctx->loss); ggml_build_forward_expand(opt_ctx->gf, opt_ctx->loss); - if (opt_ctx->loss_type == GGML_OPT_LOSS_TYPE_CROSS_ENTROPY) { + if (opt_ctx->loss_type == GGML_OPT_LOSS_TYPE_CROSS_ENTROPY || opt_ctx->loss_type == GGML_OPT_LOSS_TYPE_CROSS_ENTROPY_MASKED) { opt_ctx->pred = ggml_argmax(ctx_results, opt_ctx->outputs); ggml_set_name(opt_ctx->pred, "pred"); ggml_set_output(opt_ctx->pred); ggml_build_forward_expand(opt_ctx->gf, opt_ctx->pred); - opt_ctx->ncorrect = ggml_count_equal(ctx_results, opt_ctx->pred, ggml_argmax(ctx_results, opt_ctx->labels)); - ggml_set_name(opt_ctx->ncorrect, "ncorrect"); - ggml_set_output(opt_ctx->ncorrect); - ggml_build_forward_expand(opt_ctx->gf, opt_ctx->ncorrect); + if (opt_ctx->loss_type == GGML_OPT_LOSS_TYPE_CROSS_ENTROPY_MASKED && opt_ctx->masks != nullptr) { + // For instruction fine-tuning with masks, use masked accuracy calculation + struct ggml_tensor * labels_argmax = ggml_argmax(ctx_results, opt_ctx->labels); + opt_ctx->ncorrect = ggml_count_equal_masked(ctx_results, opt_ctx->pred, labels_argmax, opt_ctx->masks); + ggml_set_name(opt_ctx->ncorrect, "ncorrect_masked"); + ggml_set_output(opt_ctx->ncorrect); + ggml_build_forward_expand(opt_ctx->gf, opt_ctx->ncorrect); + } else { + opt_ctx->ncorrect = ggml_count_equal(ctx_results, opt_ctx->pred, ggml_argmax(ctx_results, opt_ctx->labels)); + ggml_set_name(opt_ctx->ncorrect, "ncorrect"); + ggml_set_output(opt_ctx->ncorrect); + ggml_build_forward_expand(opt_ctx->gf, opt_ctx->ncorrect); + } } if (opt_ctx->buf_static) { @@ -617,6 +734,10 @@ struct ggml_tensor * ggml_opt_labels(ggml_opt_context_t opt_ctx) { return opt_ctx->labels; } +struct ggml_tensor * ggml_opt_masks(ggml_opt_context_t opt_ctx) { + return opt_ctx->masks; +} + struct ggml_tensor * ggml_opt_loss(ggml_opt_context_t opt_ctx) { return opt_ctx->loss; } @@ -633,6 +754,35 @@ struct ggml_tensor * ggml_opt_grad_acc(ggml_opt_context_t opt_ctx, struct ggml_t return ggml_graph_get_grad_acc(opt_ctx->gb_opt, node); } +int64_t ggml_opt_get_iter(ggml_opt_context_t opt_ctx) { + return opt_ctx->iter; +} + +void ggml_opt_set_iter(ggml_opt_context_t opt_ctx, int64_t iter) { + opt_ctx->iter = iter; +} + +int32_t ggml_opt_get_nparams(ggml_opt_context_t opt_ctx) { + if (!opt_ctx) { + return 0; + } + return (int32_t)opt_ctx->grad_m.size(); +} + +struct ggml_tensor * ggml_opt_get_grad_m(ggml_opt_context_t opt_ctx, int32_t index) { + if (index < 0 || index >= (int32_t)opt_ctx->grad_m.size()) { + return nullptr; + } + return opt_ctx->grad_m[index]; +} + +struct ggml_tensor * ggml_opt_get_grad_v(ggml_opt_context_t opt_ctx, int32_t index) { + if (index < 0 || index >= (int32_t)opt_ctx->grad_v.size()) { + return nullptr; + } + return opt_ctx->grad_v[index]; +} + // ====== Optimization Result ====== ggml_opt_result_t ggml_opt_result_init() { @@ -648,6 +798,7 @@ void ggml_opt_result_reset(ggml_opt_result_t result) { result->loss.clear(); result->pred.clear(); result->ncorrect = 0; + result->nmasked = 0; } void ggml_opt_result_ndata(ggml_opt_result_t result, int64_t * ndata) { @@ -696,14 +847,15 @@ void ggml_opt_result_pred(ggml_opt_result_t result, int32_t * pred) { } void ggml_opt_result_accuracy(ggml_opt_result_t result, double * accuracy, double * unc) { - *accuracy = result->ncorrect >= 0 ? double(result->ncorrect) / double(result->ndata) : NAN; + int64_t denominator = (result->nmasked > 0) ? result->nmasked : result->ndata; + *accuracy = result->ncorrect >= 0 ? double(result->ncorrect) / double(denominator) : NAN; if (!unc) { return; } - *unc = result->ncorrect >= 0 && result->ndata >= 2 ? - sqrt((*accuracy) * (1.0 - (*accuracy)) / double(result->ndata - 1)) : NAN; + *unc = result->ncorrect >= 0 && denominator >= 2 ? + sqrt((*accuracy) * (1.0 - (*accuracy)) / double(denominator - 1)) : NAN; } // ====== Computation ====== @@ -873,6 +1025,24 @@ void ggml_opt_eval(ggml_opt_context_t opt_ctx, ggml_opt_result_t result) { int64_t ncorrect; ggml_backend_tensor_get(opt_ctx->ncorrect, &ncorrect, 0, ggml_nbytes(opt_ctx->ncorrect)); result->ncorrect += ncorrect; + + if (opt_ctx->loss_type == GGML_OPT_LOSS_TYPE_CROSS_ENTROPY_MASKED && opt_ctx->masks) { + int64_t total_valid = 0; + const int64_t nr = opt_ctx->masks->ne[1]; + + const size_t mask_size = ggml_nbytes(opt_ctx->masks); + std::vector mask_data(mask_size / sizeof(float)); + ggml_backend_tensor_get(opt_ctx->masks, mask_data.data(), 0, mask_size); + + for (int64_t i1 = 0; i1 < nr; i1++) { + const size_t idx = i1 * (opt_ctx->masks->ne[0]); + const float mask_value = mask_data[idx]; + if (mask_value > 0.5f) { + total_valid++; + } + } + result->nmasked += total_valid; + } } // ====== High-Level Functions ====== @@ -1091,3 +1261,126 @@ GGML_API const char * ggml_opt_optimizer_name(enum ggml_opt_optimizer_type o) { return "undefined"; }; } + +// ====== Optimizer State Persistence ====== + +bool ggml_opt_save_state(ggml_opt_context_t opt_ctx, const char* filename) { + if (!opt_ctx || !filename) { + return false; + } + + struct gguf_context * gguf_ctx = gguf_init_empty(); + if (!gguf_ctx) { + return false; + } + + gguf_set_val_str(gguf_ctx, "general.type", "optimizer"); + gguf_set_val_i64(gguf_ctx, "optimizer.iteration", ggml_opt_get_iter(opt_ctx)); + gguf_set_val_i32(gguf_ctx, "optimizer.n_params", ggml_opt_get_nparams(opt_ctx)); + + int32_t total_params = ggml_opt_get_nparams(opt_ctx); + + for (int32_t i = 0; i < total_params; ++i) { + struct ggml_tensor * grad_m = ggml_opt_get_grad_m(opt_ctx, i); + struct ggml_tensor * grad_v = ggml_opt_get_grad_v(opt_ctx, i); + if (grad_m) { + gguf_add_tensor(gguf_ctx, grad_m); + } + if (grad_v) { + gguf_add_tensor(gguf_ctx, grad_v); + } + } + + bool success = gguf_write_to_file(gguf_ctx, filename, false); + gguf_free(gguf_ctx); + + return success; +} + +bool ggml_opt_load_state(ggml_opt_context_t opt_ctx, const char* filename) { + if (!opt_ctx || !filename) { + return false; + } + + struct gguf_init_params gguf_params = { + /* .no_alloc = */ true, + /* .ctx = */ nullptr, + }; + + struct gguf_context * gguf_context = gguf_init_from_file(filename, gguf_params); + if (!gguf_context) { + return false; + } + + int key_idx = gguf_find_key(gguf_context, "optimizer.iteration"); + if (key_idx >= 0) { + int64_t saved_iter = gguf_get_val_i64(gguf_context, key_idx); + ggml_opt_set_iter(opt_ctx, saved_iter); + } + + gguf_free(gguf_context); + return true; +} + +bool ggml_opt_load_tensors(ggml_opt_context_t opt_ctx, const char* filename) { + if (!opt_ctx || !filename) { + return false; + } + + struct ggml_context * gguf_ctx = nullptr; + struct gguf_init_params gguf_params = { + /* .no_alloc = */ false, + /* .ctx = */ &gguf_ctx, + }; + + struct gguf_context * gguf_context = gguf_init_from_file(filename, gguf_params); + if (!gguf_context) { + return false; + } + + if (!gguf_ctx) { + gguf_free(gguf_context); + return false; + } + + int tensor_count = gguf_get_n_tensors(gguf_context); + int grad_m_loaded = 0, grad_v_loaded = 0; + const int32_t n_params = ggml_opt_get_nparams(opt_ctx); + + for (int i = 0; i < tensor_count; ++i) { + const char* tensor_name = gguf_get_tensor_name(gguf_context, i); + if (!tensor_name) continue; + + struct ggml_tensor* gguf_tensor = ggml_get_tensor(gguf_ctx, tensor_name); + if (!gguf_tensor) continue; + + for (int32_t param_idx = 0; param_idx < n_params; ++param_idx) { + struct ggml_tensor* grad_m = ggml_opt_get_grad_m(opt_ctx, param_idx); + struct ggml_tensor* grad_v = ggml_opt_get_grad_v(opt_ctx, param_idx); + + if (grad_m && strlen(grad_m->name) > 0 && strcmp(tensor_name, grad_m->name) == 0) { + if (ggml_nelements(grad_m) == ggml_nelements(gguf_tensor)) { + if (grad_m->data) { + ggml_backend_tensor_set(grad_m, gguf_tensor->data, 0, ggml_nbytes(grad_m)); + grad_m_loaded++; + } + } + break; + } + + if (grad_v && strlen(grad_v->name) > 0 && strcmp(tensor_name, grad_v->name) == 0) { + if (ggml_nelements(grad_v) == ggml_nelements(gguf_tensor)) { + if (grad_v->data) { + ggml_backend_tensor_set(grad_v, gguf_tensor->data, 0, ggml_nbytes(grad_v)); + grad_v_loaded++; + } + } + break; + } + } + } + + ggml_free(gguf_ctx); + gguf_free(gguf_context); + return (grad_m_loaded > 0 || grad_v_loaded > 0); +} diff --git a/ggml/src/ggml-quants.c b/ggml/src/ggml-quants.c index de5cbd75e868..0fe9df1954f8 100644 --- a/ggml/src/ggml-quants.c +++ b/ggml/src/ggml-quants.c @@ -2092,6 +2092,13 @@ size_t quantize_q8_0(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, return nrow * row_size; } +size_t quantize_q8_1(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrow, int64_t n_per_row, const float * quant_weights) { + (void) quant_weights; // not used + const size_t row_size = ggml_row_size(GGML_TYPE_Q8_1, n_per_row); + quantize_row_q8_1_ref(src, dst, (int64_t) nrow * n_per_row); + return nrow * row_size; +} + size_t quantize_mxfp4(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrow, int64_t n_per_row, const float * quant_weights) { GGML_UNUSED(quant_weights); quantize_row_mxfp4_ref(src, dst, (int64_t)nrow*n_per_row); diff --git a/ggml/src/ggml-quants.h b/ggml/src/ggml-quants.h index 3b688f31c214..20901d57c0d6 100644 --- a/ggml/src/ggml-quants.h +++ b/ggml/src/ggml-quants.h @@ -93,6 +93,7 @@ GGML_API size_t quantize_q4_1(const float * GGML_RESTRICT src, void * GGML_RESTR GGML_API size_t quantize_q5_0(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); GGML_API size_t quantize_q5_1(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); GGML_API size_t quantize_q8_0(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); +GGML_API size_t quantize_q8_1(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); GGML_API size_t quantize_mxfp4(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); diff --git a/ggml/src/ggml-vulkan/CMakeLists.txt b/ggml/src/ggml-vulkan/CMakeLists.txt index de01336cd3fd..1e85c2d48f27 100644 --- a/ggml/src/ggml-vulkan/CMakeLists.txt +++ b/ggml/src/ggml-vulkan/CMakeLists.txt @@ -13,6 +13,10 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") add_definitions(/MP) endif() +# Add options to disable cooperative matrix support +option(GGML_VULKAN_DISABLE_COOPMAT "Disable VK_KHR_cooperative_matrix support" OFF) +option(GGML_VULKAN_DISABLE_COOPMAT2 "Disable VK_NV_cooperative_matrix2 support" OFF) + function(detect_host_compiler) if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") find_program(HOST_C_COMPILER NAMES cl gcc clang NO_CMAKE_FIND_ROOT_PATH) @@ -62,17 +66,25 @@ if (Vulkan_FOUND) set(VULKAN_SHADER_GEN_CMAKE_ARGS "") # Test all shader extensions - test_shader_extension_support( - "GL_KHR_cooperative_matrix" - "${CMAKE_CURRENT_SOURCE_DIR}/vulkan-shaders/feature-tests/coopmat.comp" - "GGML_VULKAN_COOPMAT_GLSLC_SUPPORT" - ) + if (NOT GGML_VULKAN_DISABLE_COOPMAT) + test_shader_extension_support( + "GL_KHR_cooperative_matrix" + "${CMAKE_CURRENT_SOURCE_DIR}/vulkan-shaders/feature-tests/coopmat.comp" + "GGML_VULKAN_COOPMAT_GLSLC_SUPPORT" + ) + else() + message(STATUS "VK_KHR_cooperative_matrix support disabled by GGML_VULKAN_DISABLE_COOPMAT") + endif() - test_shader_extension_support( - "GL_NV_cooperative_matrix2" - "${CMAKE_CURRENT_SOURCE_DIR}/vulkan-shaders/feature-tests/coopmat2.comp" - "GGML_VULKAN_COOPMAT2_GLSLC_SUPPORT" - ) + if (NOT GGML_VULKAN_DISABLE_COOPMAT2) + test_shader_extension_support( + "GL_NV_cooperative_matrix2" + "${CMAKE_CURRENT_SOURCE_DIR}/vulkan-shaders/feature-tests/coopmat2.comp" + "GGML_VULKAN_COOPMAT2_GLSLC_SUPPORT" + ) + else() + message(STATUS "VK_NV_cooperative_matrix2 support disabled by GGML_VULKAN_DISABLE_COOPMAT2") + endif() test_shader_extension_support( "GL_EXT_integer_dot_product" @@ -87,8 +99,17 @@ if (Vulkan_FOUND) ) target_link_libraries(ggml-vulkan PRIVATE Vulkan::Vulkan) - target_include_directories(ggml-vulkan PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + target_include_directories(ggml-vulkan + PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} + ../../../vendor + + ) + target_include_directories(ggml-vulkan PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/vulkan_cpp_wrapper/include") + if (FORCE_GGML_VK_PERF_LOGGER) + add_compile_definitions(FORCE_GGML_VK_PERF_LOGGER) + endif() # Workaround to the "can't dereference invalidated vector iterator" bug in clang-cl debug build # Posssibly relevant: https://stackoverflow.com/questions/74748276/visual-studio-no-displays-the-correct-length-of-stdvector if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang") @@ -103,6 +124,11 @@ if (Vulkan_FOUND) add_compile_definitions(GGML_VULKAN_DEBUG) endif() + if (GGML_VULKAN_BUILD_ADRENO_SHADERS) + add_compile_definitions(GGML_VULKAN_BUILD_ADRENO_SHADERS) + list(APPEND VULKAN_SHADER_GEN_CMAKE_ARGS -DGGML_VULKAN_BUILD_ADRENO_SHADERS=ON) + endif() + if (GGML_VULKAN_MEMORY_DEBUG) add_compile_definitions(GGML_VULKAN_MEMORY_DEBUG) endif() @@ -183,16 +209,19 @@ if (Vulkan_FOUND) CONFIGURE_DEPENDS "${_ggml_vk_input_dir}/*.cpp" "${_ggml_vk_input_dir}/*.h") + set (_ggml_vk_common_cpp "${CMAKE_CURRENT_BINARY_DIR}/ggml-vulkan-shaders-common.cpp") + add_custom_command( - OUTPUT ${_ggml_vk_header} + OUTPUT ${_ggml_vk_header} ${_ggml_vk_common_cpp} COMMAND ${_ggml_vk_genshaders_cmd} --output-dir ${_ggml_vk_output_dir} --target-hpp ${_ggml_vk_header} + --target-common-cpp ${_ggml_vk_common_cpp} DEPENDS ${_ggml_vk_shaders_gen_sources} vulkan-shaders-gen - COMMENT "Generate vulkan shaders header" + COMMENT "Generate vulkan shaders header and common cpp" ) - target_sources(ggml-vulkan PRIVATE ${_ggml_vk_header}) + target_sources(ggml-vulkan PRIVATE ${_ggml_vk_header} ${_ggml_vk_common_cpp}) foreach (file_full ${_ggml_vk_shader_files}) get_filename_component(file ${file_full} NAME) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index f917a745d5a9..b718fb8cc85a 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -21,6 +21,14 @@ DispatchLoaderDynamic & ggml_vk_default_dispatcher(); #include +#define VMA_IMPLEMENTATION +#define VMA_VULKAN_VERSION 1004000 +#if defined(ANDROID) +#define VMA_STATIC_VULKAN_FUNCTIONS 0 +#define VMA_DYNAMIC_VULKAN_FUNCTIONS 1 +#endif +#include "vma/VmaUsage.h" + #include #include #include @@ -92,11 +100,14 @@ static bool is_pow2(uint32_t x) { return x > 1 && (x & (x-1)) == 0; } #define VK_VENDOR_ID_APPLE 0x106b #define VK_VENDOR_ID_INTEL 0x8086 #define VK_VENDOR_ID_NVIDIA 0x10de +#define VK_VENDOR_ID_QUALCOMM 0x5143 #define VK_DEVICE_DESCRIPTOR_POOL_SIZE 256 #define GGML_VK_MAX_NODES 8192 +#define MINIMUM_TILE_SIZE 32 + #define VK_CHECK(err, msg) \ do { \ vk::Result err_ = (err); \ @@ -107,8 +118,13 @@ static bool is_pow2(uint32_t x) { return x > 1 && (x & (x-1)) == 0; } } \ } while (0) -#ifdef GGML_VULKAN_DEBUG -#define VK_LOG_DEBUG(msg) std::cerr << msg << std::endl +#if defined(GGML_VULKAN_DEBUG) +#define VK_LOG_DEBUG(msg) \ + do { \ + std::stringstream ss; \ + ss << msg; \ + GGML_LOG_DEBUG("%s", ss.str().c_str()); \ + } while (0) #else #define VK_LOG_DEBUG(msg) ((void) 0) #endif // GGML_VULKAN_DEBUG @@ -248,6 +264,7 @@ enum vk_device_architecture { AMD_RDNA3, INTEL_XE2, NVIDIA_PRE_TURING, + QUALCOMM_ADRENO, }; static vk_device_architecture get_device_architecture(const vk::PhysicalDevice& device) { @@ -342,6 +359,8 @@ static vk_device_architecture get_device_architecture(const vk::PhysicalDevice& if (!cooperative_matrix) { return vk_device_architecture::NVIDIA_PRE_TURING; } + } else if (props.vendorID == VK_VENDOR_ID_QUALCOMM) { + return vk_device_architecture::QUALCOMM_ADRENO; } return vk_device_architecture::OTHER; } @@ -510,12 +529,15 @@ static constexpr std::initializer_list> rms_norm_mul_rope_vie struct vk_device_struct { std::recursive_mutex mutex; + VmaAllocator allocator; + vk::PhysicalDevice physical_device; vk::PhysicalDeviceProperties properties; std::string name; uint64_t max_memory_allocation_size; uint64_t max_buffer_size; uint64_t suballocation_block_size; + uint64_t tiling_threshold; bool fp16; bool bf16; bool pipeline_robustness; @@ -710,7 +732,11 @@ struct vk_device_struct { vk_pipeline pipeline_leaky_relu_f32; vk_pipeline pipeline_silu_back_f32; + vk_pipeline pipeline_geglu_back_f32; vk_pipeline pipeline_diag_mask_inf_f32; + vk_pipeline pipeline_cross_entropy_loss_back_f32; + vk_pipeline pipeline_cross_entropy_loss_masked_back_f32; + vk_pipeline pipeline_count_equal_masked_i32; vk_pipeline pipeline_soft_max_f32, pipeline_soft_max_f32_f16; vk_pipeline pipeline_soft_max_f32_wg512, pipeline_soft_max_f32_f16_wg512; vk_pipeline pipeline_soft_max_back_f32; @@ -723,6 +749,11 @@ struct vk_device_struct { vk_pipeline pipeline_topk_f32[num_topk_pipelines]; vk_pipeline pipeline_sum_rows_f32; vk_pipeline pipeline_cumsum_f32; + vk_pipeline pipeline_out_prod_f32; + vk_pipeline pipeline_out_prod_f16_f32; + vk_pipeline pipeline_out_prod_q4_0; + vk_pipeline pipeline_out_prod_q8_0; + vk_pipeline pipeline_out_prod_tq2_0; vk_pipeline pipeline_argmax_f32; vk_pipeline pipeline_count_equal_i32; std::map pipeline_solve_tri_f32; @@ -761,8 +792,6 @@ struct vk_device_struct { ggml_backend_buffer_type buffer_type; bool disable_fusion; - bool disable_host_visible_vidmem; - bool allow_sysmem_fallback; bool disable_graph_optimize; #ifdef GGML_VULKAN_MEMORY_DEBUG @@ -796,6 +825,8 @@ struct vk_device_struct { device.destroyDescriptorSetLayout(dsl); + vmaDestroyAllocator(allocator); + device.destroy(); } }; @@ -816,9 +847,9 @@ void vk_command_pool::destroy(vk::Device& device) { struct vk_buffer_struct { vk::Buffer buffer = VK_NULL_HANDLE; - vk::DeviceMemory device_memory = VK_NULL_HANDLE; vk::MemoryPropertyFlags memory_property_flags; - void * ptr; + VmaAllocation allocation; + VmaAllocationInfo info; size_t size = 0; vk::DeviceAddress bda_addr {}; @@ -830,8 +861,7 @@ struct vk_buffer_struct { } VK_LOG_DEBUG("~vk_buffer_struct(" << buffer << ", " << size << ")"); - device->device.freeMemory(device_memory); - device->device.destroyBuffer(buffer); + vmaDestroyBuffer(device->allocator, buffer, allocation); } }; @@ -1564,49 +1594,32 @@ class vk_memory_logger { #define VK_LOG_MEMORY(msg) ((void) 0) #endif // GGML_VULKAN_MEMORY_DEBUG -class vk_perf_logger { - public: - void print_timings() { - if (timings.empty()) { - return; - } - uint64_t total_all_op_times = 0; - std::cerr << "----------------\nVulkan Timings:" << std::endl; - for (const auto & t : timings) { - uint64_t total_op_times = 0; - for (const auto & time : t.second) { - total_op_times += time; - } - std::cerr << t.first << ": " << t.second.size() << " x " << (total_op_times / t.second.size() / 1000.0) - << " us"; - - // If we have as many flops entries as timing entries for the op, then compute and log the flops/S. - auto it = flops.find(t.first); - if (it != flops.end() && (it->second).size() == t.second.size()) { - uint64_t total_op_flops = 0; - for (const auto & elem : it->second) { - total_op_flops += elem; - } - std::cerr << " (" - << (double(total_op_flops) / (1000.0 * 1000.0 * 1000.0)) / - (double(total_op_times) / (1000.0 * 1000.0 * 1000.0)) - << " GFLOPS/s)"; - } - - total_all_op_times += total_op_times; +struct triplet_timing_info { + std::string operation_type; // Specific Vulkan matmul operation name + std::string triplet; + std::string size_combination; + std::vector times; + uint64_t total_time = 0; + size_t count = 0; + double avg_time_us = 0.0; +}; - std::cerr << std::endl; - } +class vk_perf_logger { +public: + // Simplified approach without context storage - if (timings.size() > 0) { - std::cerr << "Total time: " << total_all_op_times / 1000.0 << " us." << std::endl; - } + void print_timings() { + GGML_LOG_DEBUG("================\nVulkan Profiling Results:\n================\n\n"); + print_legacy_timings(); + print_triplet_timings(); timings.clear(); flops.clear(); + triplet_timings.clear(); } void log_timing(const ggml_tensor * node, uint64_t time) { + // Legacy timing tracking (for backward compatibility) if (node->op == GGML_OP_UNARY) { timings[ggml_unary_op_name(ggml_get_unary_op(node))].push_back(time); return; @@ -1629,6 +1642,13 @@ class vk_perf_logger { } timings[name].push_back(time); flops[name].push_back(m * n * (k + (k - 1)) * batch); + + // Enhanced triplet tracking for matrix operations + if (time > 0) { // Only log when we have actual timing data + // Determine operation type based on tensor characteristics + std::string op_type = determine_matmul_operation_type(node); + log_triplet_timing(node, time, op_type); + } return; } if (node->op == GGML_OP_CONV_2D || node->op == GGML_OP_CONV_TRANSPOSE_2D) { @@ -1684,11 +1704,182 @@ class vk_perf_logger { } timings[ggml_op_name(node->op)].push_back(time); } - private: + +private: std::map> timings; std::map> flops; + std::map triplet_timings; + + void log_triplet_timing(const ggml_tensor * node, uint64_t time, const std::string& operation_type = "generic_matmul") { + if (!node || !node->src[0] || !node->src[1] || time == 0) return; + + try { + // Extract tensor type information + std::string src0_type = ggml_type_name(node->src[0]->type); + std::string src1_type = ggml_type_name(node->src[1]->type); + std::string dst_type = ggml_type_name(node->type); + + // Create triplet string (using safe ASCII characters) + std::string triplet = src0_type + " x " + src1_type + " -> " + dst_type; + + // Extract dimensions + const uint64_t m = node->src[0]->ne[1]; + const uint64_t n = node->src[1]->ne[1]; + const uint64_t k = node->src[1]->ne[0]; + + // Create size combination string + std::string size_combo = "[" + std::to_string(k) + "x" + std::to_string(m) + "] x " + + "[" + std::to_string(k) + "x" + std::to_string(n) + "] -> " + + "[" + std::to_string(m) + "x" + std::to_string(n) + "]"; + + // Create combined key including operation type + std::string full_key = operation_type + " | " + triplet + " | " + size_combo; + + // Update triplet timing info + auto& info = triplet_timings[full_key]; + info.operation_type = operation_type; + info.triplet = triplet; + info.size_combination = size_combo; + info.times.push_back(time); + info.total_time += time; + info.count++; + info.avg_time_us = (info.total_time / info.count) / 1000.0; + } catch (...) { + // Silently ignore errors to prevent crashes + } + } + + // Determine operation type based on tensor characteristics + std::string determine_matmul_operation_type(const ggml_tensor * node) { + if (!node || !node->src[0] || !node->src[1]) return "unknown_matmul"; + + try { + const uint64_t n = node->src[1]->ne[1]; // Number of columns in second tensor + const bool is_permuted_src0 = ggml_is_permuted(node->src[0]); + const bool is_permuted_src1 = ggml_is_permuted(node->src[1]); + const bool is_contiguous_src0 = ggml_is_contiguous(node->src[0]); + const ggml_type src0_type = node->src[0]->type; + // const ggml_type src1_type = node->src[1]->type; // unused for now + + // Logic based on ggml_vk_mul_mat function selection + if (src0_type == GGML_TYPE_F16 && is_permuted_src0 && is_permuted_src1 && n == 1) { + return "ggml_vk_mul_mat_vec_p021_f16_f32"; + } else if (src0_type == GGML_TYPE_F16 && !is_contiguous_src0 && !ggml_is_transposed(node->src[1]) && n == 1 && + !is_permuted_src0 && !is_permuted_src1) { + return "ggml_vk_mul_mat_vec_nc_f16_f32"; + } else if (n == 1 && (src0_type == GGML_TYPE_F32 || src0_type == GGML_TYPE_F16 || + src0_type == GGML_TYPE_BF16 || ggml_is_quantized(src0_type))) { + return "ggml_vk_mul_mat_vec_q_f16"; + } else if (node->op == GGML_OP_MUL_MAT_ID) { + if (n == 1) { + return "ggml_vk_mul_mat_vec_id_q_f16"; + } else { + return "ggml_vk_mul_mat_id_q_f16"; + } + } else { + return "ggml_vk_mul_mat_q_f16"; + } + } catch (...) { + return "unknown_matmul"; + } + } + + void print_legacy_timings() { + if (timings.empty()) { + return; + } + std::stringstream ss; + uint64_t total_all_op_times = 0; + ss << "Legacy Timing Summary:\n---------------------" << std::endl; + for (const auto & t : timings) { + uint64_t total_op_times = 0; + for (const auto & time : t.second) { + total_op_times += time; + } + ss << t.first << ": " << t.second.size() << " x " << (total_op_times / t.second.size() / 1000.0) + << " us"; + + // If we have as many flops entries as timing entries for the op, then compute and log the flops/S. + auto it = flops.find(t.first); + if (it != flops.end() && (it->second).size() == t.second.size()) { + uint64_t total_op_flops = 0; + for (const auto & elem : it->second) { + total_op_flops += elem; + } + ss << " (" + << (double(total_op_flops) / (1000.0 * 1000.0 * 1000.0)) / + (double(total_op_times) / (1000.0 * 1000.0 * 1000.0)) + << " GFLOPS/s)"; + } + + total_all_op_times += total_op_times; + + ss << std::endl; + } + + if (timings.size() > 0) { + ss << "Total time: " << total_all_op_times / 1000.0 << " us." << std::endl; + } + ss << std::endl; + GGML_LOG_DEBUG("%s", ss.str().c_str()); + } + + void print_triplet_timings() { + std::stringstream ss; + try { + if (triplet_timings.empty()) { + ss << "No triplet timing data available." << std::endl; + return; + } + + ss << "Enhanced Triplet Timing Analysis by Operation Type:" << std::endl; + ss << "====================================================" << std::endl; + + // Group by operation type + std::map> operation_groups; + for (auto& [key, info] : triplet_timings) { + operation_groups[info.operation_type].push_back(&info); + } + + // Print results organized by operation type + for (const auto& [operation_type, entries] : operation_groups) { + ss << "\n" << operation_type << ":" << std::endl; + ss << std::string(operation_type.length() + 1, '-') << std::endl; + + uint64_t operation_total_time = 0; + size_t operation_total_count = 0; + + for (const auto& info : entries) { + if (info->count > 0) { + ss << " " << info->triplet << " | " << info->size_combination + << ": " << info->count << " ops, " + << std::fixed << std::setprecision(2) << info->avg_time_us << " us avg" << std::endl; + + operation_total_time += info->total_time; + operation_total_count += info->count; + } + } + + if (operation_total_count > 0) { + double operation_avg_us = (operation_total_time / operation_total_count) / 1000.0; + ss << " → " << operation_type << " Summary: " << operation_total_count + << " total ops, " << std::fixed << std::setprecision(2) << operation_avg_us << " us avg" << std::endl; + } + } + + ss << "\nOverall Statistics:" << std::endl; + ss << "Total operation types: " << operation_groups.size() << std::endl; + ss << "Total variations: " << triplet_timings.size() << std::endl; + ss << std::endl; + GGML_LOG_DEBUG("%s", ss.str().c_str()); + } catch (...) { + GGML_LOG_DEBUG("Error in triplet timing analysis - analysis skipped.\n"); + } + } }; +// Thread-local storage definition is now handled above the class + struct ggml_backend_vk_context { std::string name; @@ -1696,8 +1887,8 @@ struct ggml_backend_vk_context { size_t semaphore_idx, event_idx; ggml_vk_garbage_collector gc; - size_t prealloc_size_x, prealloc_size_y, prealloc_size_split_k, prealloc_size_add_rms_partials, prealloc_size_add_rms_partials_offset; - vk_buffer prealloc_x, prealloc_y, prealloc_split_k, prealloc_add_rms_partials, sync_staging; + size_t prealloc_size_x, prealloc_size_y, prealloc_size_split_k, prealloc_size_add_rms_partials, prealloc_size_add_rms_partials_offset, prealloc_size_tile; + vk_buffer prealloc_x, prealloc_y, prealloc_split_k, prealloc_add_rms_partials, prealloc_tile, sync_staging; vk::Fence fence, almost_ready_fence; bool submit_pending {}; bool almost_ready_fence_pending {}; @@ -2303,6 +2494,7 @@ static void ggml_vk_command_pool_cleanup(vk_device& device, vk_command_pool& p) // Requires command buffers to be done device->device.resetCommandPool(p.pool); + p.cmd_buffer_idx = 0; } @@ -2320,155 +2512,130 @@ static void ggml_vk_queue_command_pools_cleanup(vk_device& device) { } } -static std::vector ggml_vk_find_memory_properties(const vk::PhysicalDeviceMemoryProperties* mem_props, vk::MemoryRequirements* mem_req, vk::MemoryPropertyFlags flags) { - std::vector indices; - - for (uint32_t i = 0; i < mem_props->memoryTypeCount; ++i) { - vk::MemoryType memory_type = mem_props->memoryTypes[i]; - if ((mem_req->memoryTypeBits & ((uint64_t)1 << i)) && - (flags & memory_type.propertyFlags) == flags && - mem_props->memoryHeaps[memory_type.heapIndex].size >= mem_req->size) { - indices.push_back(i); - } - } - return indices; -} - -static vk_buffer ggml_vk_create_buffer(vk_device& device, size_t size, const std::initializer_list & req_flags_list) { - VK_LOG_DEBUG("ggml_vk_create_buffer(" << device->name << ", " << size << ", " << to_string(req_flags_list.begin()[0]) << ", " << to_string(req_flags_list.begin()[req_flags_list.size()-1]) << ")"); - if (size > device->max_buffer_size) { +static vk_buffer ggml_vk_create_buffer(vk_device& device, const vk::BufferCreateInfo& buffer_info, const VmaAllocationCreateInfo& alloc_info) { + VK_LOG_DEBUG("ggml_vk_create_buffer(" << device->name << ", " << buffer_info.size << ")"); + if (buffer_info.size > device->max_buffer_size) { throw vk::OutOfDeviceMemoryError("Requested buffer size exceeds device buffer size limit"); } vk_buffer buf = std::make_shared(); - if (size == 0) { + if (buffer_info.size == 0) { buf->size = 0; return buf; } - vk::BufferUsageFlags usage_flags = vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst; - vk::MemoryAllocateFlags mem_flags {}; - if (device->buffer_device_address) { - usage_flags |= vk::BufferUsageFlagBits::eShaderDeviceAddress; - mem_flags |= vk::MemoryAllocateFlagBits::eDeviceAddress; + VkResult res = vmaCreateBuffer(device->allocator, reinterpret_cast(&buffer_info), &alloc_info, reinterpret_cast(&buf->buffer), &buf->allocation, &buf->info); + if (res != VK_SUCCESS) { + throw vk::SystemError(static_cast(res), "vmaCreateBuffer failed"); } - vk::BufferCreateInfo buffer_create_info{ - vk::BufferCreateFlags(), - size, - usage_flags, - vk::SharingMode::eExclusive, - 0, - nullptr, - }; - - buf->buffer = device->device.createBuffer(buffer_create_info); + vmaGetAllocationMemoryProperties(device->allocator, buf->allocation, reinterpret_cast(&buf->memory_property_flags)); - vk::MemoryRequirements mem_req = device->device.getBufferMemoryRequirements(buf->buffer); - - vk::PhysicalDeviceMemoryProperties mem_props = device->physical_device.getMemoryProperties(); - - const vk::MemoryAllocateFlagsInfo mem_flags_info { mem_flags }; + buf->device = device; + buf->size = buffer_info.size; - for (auto it = req_flags_list.begin(); it != req_flags_list.end(); it++) { - const auto & req_flags = *it; + if (device->buffer_device_address && (buffer_info.usage & vk::BufferUsageFlagBits::eShaderDeviceAddress)) { + const vk::BufferDeviceAddressInfo addressInfo(buf->buffer); + buf->bda_addr = device->device.getBufferAddress(addressInfo); + } - const std::vector memory_type_indices = ggml_vk_find_memory_properties(&mem_props, &mem_req, req_flags); +#ifdef GGML_VULKAN_MEMORY_DEBUG + device->memory_logger->log_allocation(buf, buf->size); +#endif + VK_LOG_DEBUG("ggml_vk_create_buffer(buffer " << buf->buffer << ", usage " << to_string(buffer_info.usage) << ", memory property " << to_string(buf->memory_property_flags) << ")"); - if (memory_type_indices.empty()) { - continue; - } - buf->memory_property_flags = req_flags; + return buf; +} - bool done = false; +static vk_buffer ggml_vk_create_buffer_check(vk_device& device, size_t size) { + try { + const vk::BufferCreateInfo buffer_info{ + vk::BufferCreateFlags(), + size, + vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, + vk::SharingMode::eExclusive, + 0, + nullptr, + }; - for (auto mtype_it = memory_type_indices.begin(); mtype_it != memory_type_indices.end(); mtype_it++) { - try { - buf->device_memory = device->device.allocateMemory({ mem_req.size, *mtype_it, &mem_flags_info }); - done = true; - break; - } catch (const vk::SystemError& e) { - // loop and retry - // during last attempt throw the exception - if (it + 1 == req_flags_list.end() && mtype_it + 1 == memory_type_indices.end()) { - device->device.destroyBuffer(buf->buffer); - throw e; - } - } - } + VmaAllocationCreateInfo alloc_info = {}; + alloc_info.usage = (device->prefer_host_memory ? VMA_MEMORY_USAGE_AUTO_PREFER_HOST : VMA_MEMORY_USAGE_AUTO); + alloc_info.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT; + alloc_info.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - if (done) { - break; - } + return ggml_vk_create_buffer(device, buffer_info, alloc_info); + } catch (const vk::SystemError& e) { + std::cerr << "ggml_vulkan: Memory allocation of size " << size << " failed." << std::endl; + std::cerr << "ggml_vulkan: " << e.what() << std::endl; + throw e; } +} - if (!buf->device_memory) { - device->device.destroyBuffer(buf->buffer); - throw vk::OutOfDeviceMemoryError("No suitable memory type found"); +static vk_buffer ggml_vk_create_buffer_aligned(vk_device& device, const vk::BufferCreateInfo& buffer_info, const VmaAllocationCreateInfo& alloc_info, VkDeviceSize alignment) { + VK_LOG_DEBUG("ggml_vk_create_buffer_aligned(" << device->name << ", " << buffer_info.size << ", " << alignment << " )"); + if (buffer_info.size > device->max_buffer_size) { + throw vk::OutOfDeviceMemoryError("Requested buffer size exceeds device buffer size limit"); } - buf->ptr = nullptr; + vk_buffer buf = std::make_shared(); - if (buf->memory_property_flags & vk::MemoryPropertyFlagBits::eHostVisible) { - buf->ptr = device->device.mapMemory(buf->device_memory, 0, VK_WHOLE_SIZE); + if (buffer_info.size == 0) { + buf->size = 0; + return buf; + } + + VkResult res = vmaCreateBufferWithAlignment(device->allocator, reinterpret_cast(&buffer_info), &alloc_info, alignment, reinterpret_cast(&buf->buffer), &buf->allocation, &buf->info); + if (res != VK_SUCCESS) { + throw vk::SystemError(static_cast(res), "vmaCreateBufferWithAlignment failed"); } - device->device.bindBufferMemory(buf->buffer, buf->device_memory, 0); + vmaGetAllocationMemoryProperties(device->allocator, buf->allocation, reinterpret_cast(&buf->memory_property_flags)); buf->device = device; - buf->size = size; + buf->size = buffer_info.size; - if (device->buffer_device_address) { + if (device->buffer_device_address && (buffer_info.usage & vk::BufferUsageFlagBits::eShaderDeviceAddress)) { const vk::BufferDeviceAddressInfo addressInfo(buf->buffer); buf->bda_addr = device->device.getBufferAddress(addressInfo); } #ifdef GGML_VULKAN_MEMORY_DEBUG - device->memory_logger->log_allocation(buf, size); + device->memory_logger->log_allocation(buf, buf->size); #endif + VK_LOG_DEBUG("ggml_vk_create_buffer_aligned(buffer " << buf->buffer << ", usage " << to_string(buffer_info.usage) << ", memory property " << to_string(buf->memory_property_flags) << ")"); return buf; } -static vk_buffer ggml_vk_create_buffer_check(vk_device& device, size_t size, vk::MemoryPropertyFlags req_flags, vk::MemoryPropertyFlags fallback_flags = vk::MemoryPropertyFlags(0)) { - try { - return ggml_vk_create_buffer(device, size, {req_flags, fallback_flags}); - } catch (const vk::SystemError& e) { - std::cerr << "ggml_vulkan: Memory allocation of size " << size << " failed." << std::endl; - std::cerr << "ggml_vulkan: " << e.what() << std::endl; - throw e; - } -} - static vk_buffer ggml_vk_create_buffer_device(vk_device& device, size_t size) { + VK_LOG_MEMORY("ggml_vk_create_buffer_device(" << size << ")"); + vk_buffer buf; + + vk::BufferUsageFlags usage = vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst; + if (device->buffer_device_address) { + usage |= vk::BufferUsageFlagBits::eShaderDeviceAddress; + } + + const vk::BufferCreateInfo buffer_create_info{ + vk::BufferCreateFlags(), + size, + usage, + vk::SharingMode::eExclusive, + 0, + nullptr, + }; + + VmaAllocationCreateInfo alloc_info{}; + alloc_info.usage = (device->prefer_host_memory ? VMA_MEMORY_USAGE_AUTO_PREFER_HOST : VMA_MEMORY_USAGE_AUTO); + alloc_info.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | + VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT; + try { - if (device->prefer_host_memory) { - buf = ggml_vk_create_buffer(device, size, {vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, - vk::MemoryPropertyFlagBits::eDeviceLocal}); - } else if (device->uma) { - // Fall back to host memory type - buf = ggml_vk_create_buffer(device, size, {vk::MemoryPropertyFlagBits::eDeviceLocal, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent}); - } else if (device->disable_host_visible_vidmem) { - if (device->allow_sysmem_fallback) { - buf = ggml_vk_create_buffer(device, size, {vk::MemoryPropertyFlagBits::eDeviceLocal, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent}); - } else { - buf = ggml_vk_create_buffer(device, size, {vk::MemoryPropertyFlagBits::eDeviceLocal}); - } - } else { - // use rebar if available, otherwise fallback to device only visible memory - if (device->allow_sysmem_fallback) { - buf = ggml_vk_create_buffer(device, size, {vk::MemoryPropertyFlagBits::eDeviceLocal | vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, - vk::MemoryPropertyFlagBits::eDeviceLocal, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent}); - } else { - buf = ggml_vk_create_buffer(device, size, {vk::MemoryPropertyFlagBits::eDeviceLocal | vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, - vk::MemoryPropertyFlagBits::eDeviceLocal}); - } - } + buf = ggml_vk_create_buffer(device, buffer_create_info, alloc_info); } catch (const vk::SystemError& e) { std::cerr << "ggml_vulkan: Device memory allocation of size " << size << " failed." << std::endl; std::cerr << "ggml_vulkan: " << e.what() << std::endl; @@ -2921,6 +3088,21 @@ static void ggml_vk_load_shaders(vk_device& device) { pipeline->initialized = true; } + // if a variant shader exists for this SPIR-V symbol base, use it instead. + if (device->architecture == vk_device_architecture::QUALCOMM_ADRENO) { + uint64_t adreno_len = 0; + const void * adreno_data = nullptr; + if (ggml_vk_get_adreno_variant(spv_data, &adreno_len, &adreno_data)) { + spv_size = (size_t) adreno_len; + spv_data = adreno_data; + // update pipeline name to reflect that we are using the Adreno variant + pipeline->name = "adreno_" + std::string(name); + VK_LOG_DEBUG("ggml_vk_create_pipeline(): using Adreno variant for shader " << name); + } else { + VK_LOG_DEBUG("ggml_vk_create_pipeline(): no Adreno variant found for shader " << name); + } + } + if (!pipeline->needed || pipeline->compiled) { return; } @@ -3051,6 +3233,9 @@ static void ggml_vk_load_shaders(vk_device& device) { CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q5_0], matmul_q5_0_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q5_1], matmul_q5_1_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q8_0], matmul_q8_0_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) + CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_TQ2_0], matmul_tq2_0_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) + CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_TQ1_0], matmul_tq1_0_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) + CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q2_K], matmul_q2_k_f16, mmq_wg_denoms_k, warptile_mmq_k, vk_mat_mat_push_constants, 3) CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q3_K], matmul_q3_k_f16, mmq_wg_denoms_k, warptile_mmq_k, vk_mat_mat_push_constants, 3) CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q4_K], matmul_q4_k_f16, mmq_wg_denoms_k, warptile_mmq_k, vk_mat_mat_push_constants, 3) @@ -3080,6 +3265,9 @@ static void ggml_vk_load_shaders(vk_device& device) { CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_0], matmul_id_subgroup_q5_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4) CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_1], matmul_id_subgroup_q5_1_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4) CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0], matmul_id_subgroup_q8_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0], matmul_id_subgroup_tq2_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ1_0], matmul_id_subgroup_tq1_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K], matmul_id_subgroup_q2_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4) CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K], matmul_id_subgroup_q3_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4) CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K], matmul_id_subgroup_q4_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4) @@ -3141,6 +3329,9 @@ static void ggml_vk_load_shaders(vk_device& device) { CREATE_MM2(GGML_TYPE_Q5_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q5_0], matmul_q5_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM2(GGML_TYPE_Q5_1, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q5_1], matmul_q5_1_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM2(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q8_0], matmul_q8_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ2_0], matmul_tq2_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ1_0], matmul_tq1_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q2_K], matmul_q2_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM2(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q3_K], matmul_q3_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); @@ -3163,6 +3354,9 @@ static void ggml_vk_load_shaders(vk_device& device) { CREATE_MM(GGML_TYPE_Q5_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q5_0].f32acc, matmul_q5_0_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM(GGML_TYPE_Q5_1, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q5_1].f32acc, matmul_q5_1_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q8_0].f32acc, matmul_q8_0_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ2_0].f32acc, matmul_tq2_0_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ1_0].f32acc, matmul_tq1_0_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q2_K].f32acc, matmul_q2_k_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q3_K].f32acc, matmul_q3_k_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); @@ -3197,6 +3391,9 @@ static void ggml_vk_load_shaders(vk_device& device) { CREATE_MM2(GGML_TYPE_Q5_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_0], matmul_id_subgroup_q5_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, 4, _id); CREATE_MM2(GGML_TYPE_Q5_1, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_1], matmul_id_subgroup_q5_1_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, 4, _id); CREATE_MM2(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0], matmul_id_subgroup_q8_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, 4, _id); + CREATE_MM2(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0], matmul_id_subgroup_tq2_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, 4, _id); + CREATE_MM2(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ1_0], matmul_id_subgroup_tq1_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, 4, _id); + CREATE_MM2(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K], matmul_id_subgroup_q2_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, 4, _id); CREATE_MM2(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K], matmul_id_subgroup_q3_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, 4, _id); CREATE_MM2(GGML_TYPE_Q4_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K], matmul_id_subgroup_q4_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, 4, _id); @@ -3260,6 +3457,9 @@ static void ggml_vk_load_shaders(vk_device& device) { CREATE_MM2(GGML_TYPE_Q5_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q5_0], matmul_q5_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM2(GGML_TYPE_Q5_1, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q5_1], matmul_q5_1_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM2(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q8_0], matmul_q8_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM2(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ2_0], matmul_tq2_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM2(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ1_0], matmul_tq1_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM2(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q2_K], matmul_q2_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM2(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q3_K], matmul_q3_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); @@ -3306,6 +3506,9 @@ static void ggml_vk_load_shaders(vk_device& device) { CREATE_MM2(GGML_TYPE_Q5_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_0], matmul_id_subgroup_q5_0_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); CREATE_MM2(GGML_TYPE_Q5_1, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_1], matmul_id_subgroup_q5_1_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); CREATE_MM2(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0], matmul_id_subgroup_q8_0_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); + CREATE_MM2(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0], matmul_id_subgroup_tq2_0_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); + CREATE_MM2(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ1_0], matmul_id_subgroup_tq1_0_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); + CREATE_MM2(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K], matmul_id_subgroup_q2_k_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); CREATE_MM2(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K], matmul_id_subgroup_q3_k_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); CREATE_MM2(GGML_TYPE_Q4_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K], matmul_id_subgroup_q4_k_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); @@ -3350,6 +3553,9 @@ static void ggml_vk_load_shaders(vk_device& device) { CREATE_MM2(GGML_TYPE_Q5_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_0], matmul_id_q5_0_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); CREATE_MM2(GGML_TYPE_Q5_1, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_1], matmul_id_q5_1_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); CREATE_MM2(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0], matmul_id_q8_0_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); + CREATE_MM2(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0], matmul_id_tq2_0_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); + CREATE_MM2(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ1_0], matmul_id_tq1_0_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); + CREATE_MM2(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K], matmul_id_q2_k_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); CREATE_MM2(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K], matmul_id_q3_k_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); CREATE_MM2(GGML_TYPE_Q4_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K], matmul_id_q4_k_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); @@ -3423,6 +3629,9 @@ static void ggml_vk_load_shaders(vk_device& device) { CREATE_MM(GGML_TYPE_Q5_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q5_0].f32acc, matmul_q5_0_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM(GGML_TYPE_Q5_1, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q5_1].f32acc, matmul_q5_1_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q8_0].f32acc, matmul_q8_0_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ2_0].f32acc, matmul_tq2_0_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ1_0].f32acc, matmul_tq1_0_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q2_K].f32acc, matmul_q2_k_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q3_K].f32acc, matmul_q3_k_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); @@ -3467,6 +3676,9 @@ static void ggml_vk_load_shaders(vk_device& device) { CREATE_MM(GGML_TYPE_Q5_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_0].f32acc, matmul_id_subgroup_q5_0_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); CREATE_MM(GGML_TYPE_Q5_1, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_1].f32acc, matmul_id_subgroup_q5_1_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); CREATE_MM(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0].f32acc, matmul_id_subgroup_q8_0_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); + CREATE_MM(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0].f32acc, matmul_id_subgroup_tq2_0_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); + CREATE_MM(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ1_0].f32acc, matmul_id_subgroup_tq1_0_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); + CREATE_MM(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K].f32acc, matmul_id_subgroup_q2_k_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); CREATE_MM(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K].f32acc, matmul_id_subgroup_q3_k_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); CREATE_MM(GGML_TYPE_Q4_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K].f32acc, matmul_id_subgroup_q4_k_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, mul_mat_subgroup_size); @@ -3493,6 +3705,9 @@ static void ggml_vk_load_shaders(vk_device& device) { CREATE_MM(GGML_TYPE_Q5_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_0].f32acc, matmul_id_q5_0_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); CREATE_MM(GGML_TYPE_Q5_1, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_1].f32acc, matmul_id_q5_1_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); CREATE_MM(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0].f32acc, matmul_id_q8_0_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); + CREATE_MM(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0].f32acc, matmul_id_tq2_0_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); + CREATE_MM(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ1_0].f32acc, matmul_id_tq1_0_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); + CREATE_MM(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K].f32acc, matmul_id_q2_k_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); CREATE_MM(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K].f32acc, matmul_id_q3_k_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); CREATE_MM(GGML_TYPE_Q4_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K].f32acc, matmul_id_q4_k_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 4, _id, 0); @@ -3549,7 +3764,10 @@ static void ggml_vk_load_shaders(vk_device& device) { } uint32_t rm_iq = 2 * rm_kq; - const bool use_subgroups = device->subgroup_arithmetic && device->architecture != vk_device_architecture::AMD_GCN; + const bool use_subgroups = device->subgroup_arithmetic && + device->architecture != vk_device_architecture::AMD_GCN && + device->architecture != vk_device_architecture::QUALCOMM_ADRENO; + // Ensure a subgroup size >= 16 is available const bool use_subgroups16 = use_subgroups && subgroup_min_size_16; @@ -3582,6 +3800,9 @@ static void ggml_vk_load_shaders(vk_device& device) { ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q5_0][i], "mul_mat_vec_q5_0_f32_f32", arr_dmmv_q5_0_f32_f32_len[reduc], arr_dmmv_q5_0_f32_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {2*rm_stdq, 1, 1}, {wg_size_subgroup, 2*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q5_1][i], "mul_mat_vec_q5_1_f32_f32", arr_dmmv_q5_1_f32_f32_len[reduc], arr_dmmv_q5_1_f32_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {2*rm_stdq, 1, 1}, {wg_size_subgroup, 2*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q8_0][i], "mul_mat_vec_q8_0_f32_f32", arr_dmmv_q8_0_f32_f32_len[reduc], arr_dmmv_q8_0_f32_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {1*rm_stdq, 1, 1}, {wg_size_subgroup, 1*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_TQ2_0][i], "mul_mat_vec_tq2_0_f32_f32", arr_dmmv_tq2_0_f32_f32_len[reduc], arr_dmmv_tq2_0_f32_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {1*rm_stdq, 1, 1}, {wg_size_subgroup, 1*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_TQ1_0][i], "mul_mat_vec_tq1_0_f32_f32", arr_dmmv_tq1_0_f32_f32_len[reduc], arr_dmmv_tq1_0_f32_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {1*rm_stdq, 1, 1}, {wg_size_subgroup, 1*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q2_K][i], "mul_mat_vec_q2_k_f32_f32", arr_dmmv_q2_k_f32_f32_len[reduc16], arr_dmmv_q2_k_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q3_K][i], "mul_mat_vec_q3_k_f32_f32", arr_dmmv_q3_k_f32_f32_len[reduc16], arr_dmmv_q3_k_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q4_K][i], "mul_mat_vec_q4_k_f32_f32", arr_dmmv_q4_k_f32_f32_len[reduc16], arr_dmmv_q4_k_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); @@ -3606,6 +3827,9 @@ static void ggml_vk_load_shaders(vk_device& device) { ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q5_0][i], "mul_mat_vec_q5_0_f16_f32", arr_dmmv_q5_0_f16_f32_len[reduc], arr_dmmv_q5_0_f16_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {2*rm_stdq, 1, 1}, {wg_size_subgroup, 2*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q5_1][i], "mul_mat_vec_q5_1_f16_f32", arr_dmmv_q5_1_f16_f32_len[reduc], arr_dmmv_q5_1_f16_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {2*rm_stdq, 1, 1}, {wg_size_subgroup, 2*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q8_0][i], "mul_mat_vec_q8_0_f16_f32", arr_dmmv_q8_0_f16_f32_len[reduc], arr_dmmv_q8_0_f16_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {1*rm_stdq, 1, 1}, {wg_size_subgroup, 1*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_TQ2_0][i], "mul_mat_vec_tq2_0_f16_f32", arr_dmmv_tq2_0_f16_f32_len[reduc], arr_dmmv_tq2_0_f16_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {1*rm_stdq, 1, 1}, {wg_size_subgroup, 1*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_TQ1_0][i], "mul_mat_vec_tq1_0_f16_f32", arr_dmmv_tq1_0_f16_f32_len[reduc], arr_dmmv_tq1_0_f16_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {1*rm_stdq, 1, 1}, {wg_size_subgroup, 1*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q2_K][i], "mul_mat_vec_q2_k_f16_f32", arr_dmmv_q2_k_f16_f32_len[reduc16], arr_dmmv_q2_k_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q3_K][i], "mul_mat_vec_q3_k_f16_f32", arr_dmmv_q3_k_f16_f32_len[reduc16], arr_dmmv_q3_k_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q4_K][i], "mul_mat_vec_q4_k_f16_f32", arr_dmmv_q4_k_f16_f32_len[reduc16], arr_dmmv_q4_k_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); @@ -3627,6 +3851,9 @@ static void ggml_vk_load_shaders(vk_device& device) { const uint32_t subgroup_size_int = (device->vendor_id == VK_VENDOR_ID_INTEL && device->subgroup_size_control) ? device->subgroup_min_size : device->subgroup_size; const uint32_t wg_size_subgroup_int = (w == DMMV_WG_SIZE_SUBGROUP) ? subgroup_size_int : (subgroup_size_int * 4); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_TQ2_0][i], "mul_mat_vec_tq2_0_q8_1_f32", arr_dmmv_tq2_0_q8_1_f32_len[reduc], arr_dmmv_tq2_0_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {2*rm_stdq_int, 1, 1}, {wg_size_subgroup_int, 2*rm_stdq_int, i+1}, 1, true, use_subgroups, subgroup_size_int); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_TQ1_0][i], "mul_mat_vec_tq1_0_q8_1_f32", arr_dmmv_tq1_0_q8_1_f32_len[reduc], arr_dmmv_tq1_0_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {2*rm_stdq_int, 1, 1}, {wg_size_subgroup_int, 2*rm_stdq_int, i+1}, 1, true, use_subgroups, subgroup_size_int); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_Q4_0][i], "mul_mat_vec_q4_0_q8_1_f32", arr_dmmv_q4_0_q8_1_f32_len[reduc], arr_dmmv_q4_0_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {1*rm_stdq_int, 1, 1}, {wg_size_subgroup_int, 1*rm_stdq_int, i+1}, 1, true, use_subgroups, subgroup_size_int); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_Q4_1][i], "mul_mat_vec_q4_1_q8_1_f32", arr_dmmv_q4_1_q8_1_f32_len[reduc], arr_dmmv_q4_1_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {1*rm_stdq_int, 1, 1}, {wg_size_subgroup_int, 1*rm_stdq_int, i+1}, 1, true, use_subgroups, subgroup_size_int); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_Q5_0][i], "mul_mat_vec_q5_0_q8_1_f32", arr_dmmv_q5_0_q8_1_f32_len[reduc], arr_dmmv_q5_0_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {1*rm_stdq_int, 1, 1}, {wg_size_subgroup_int, 1*rm_stdq_int, i+1}, 1, true, use_subgroups, subgroup_size_int); @@ -3702,6 +3929,9 @@ static void ggml_vk_load_shaders(vk_device& device) { ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q5_0], "dequant_q5_0", dequant_q5_0_len, dequant_q5_0_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q5_1], "dequant_q5_1", dequant_q5_1_len, dequant_q5_1_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q8_0], "dequant_q8_0", dequant_q8_0_len, dequant_q8_0_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_TQ1_0], "dequant_tq1_0", dequant_tq1_0_len, dequant_tq1_0_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + + ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_TQ2_0], "dequant_tq2_0", dequant_tq2_0_len, dequant_tq2_0_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q2_K], "dequant_q2_k", dequant_q2_k_len, dequant_q2_k_data, "main", 2, 5 * sizeof(uint32_t), {256 * 64, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q3_K], "dequant_q3_k", dequant_q3_k_len, dequant_q3_k_data, "main", 2, 5 * sizeof(uint32_t), {256 * 64, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q4_K], "dequant_q4_k", dequant_q4_k_len, dequant_q4_k_data, "main", 2, 5 * sizeof(uint32_t), {256 * 32, 1, 1}, {}, 1); @@ -4000,8 +4230,14 @@ static void ggml_vk_load_shaders(vk_device& device) { ggml_vk_create_pipeline(device, device->pipeline_leaky_relu_f32, "leaky_relu_f32", leaky_relu_f32_len, leaky_relu_f32_data, "main", 2, sizeof(vk_op_push_constants), {512, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_silu_back_f32, "silu_back_f32", silu_back_f32_len, silu_back_f32_data, "main", 3, sizeof(vk_op_push_constants), {512, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_geglu_back_f32, "geglu_back_f32", geglu_back_f32_len, geglu_back_f32_data, "main", 3, sizeof(vk_op_push_constants), {512, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_diag_mask_inf_f32, "diag_mask_inf_f32", diag_mask_inf_f32_len, diag_mask_inf_f32_data, "main", 2, sizeof(vk_op_diag_mask_push_constants), {1, 512, 1}, {}, 1, true); + ggml_vk_create_pipeline(device, device->pipeline_cross_entropy_loss_back_f32, "cross_entropy_loss_back_f32", cross_entropy_loss_back_f32_len, cross_entropy_loss_back_f32_data, "main", 4, sizeof(vk_op_push_constants), {1, 1, 1}, { device->subgroup_size }, 1); + ggml_vk_create_pipeline(device, device->pipeline_cross_entropy_loss_masked_back_f32, "cross_entropy_loss_masked_back_f32", cross_entropy_loss_masked_back_f32_len, cross_entropy_loss_masked_back_f32_data, "main", 5, sizeof(vk_op_push_constants), {1, 1, 1}, { 32 }, 1); + ggml_vk_create_pipeline(device, device->pipeline_count_equal_masked_i32, "count_equal_masked_i32", count_equal_masked_i32_len, count_equal_masked_i32_data, "main", 4, sizeof(vk_op_push_constants), {1, 1, 1}, { 512 }, 1); + ggml_vk_create_pipeline(device, device->pipeline_soft_max_f32, "soft_max_f32", soft_max_f32_len, soft_max_f32_data, "main", 4, sizeof(vk_op_soft_max_push_constants), {1, 1, 1}, { device->subgroup_size }, 1); ggml_vk_create_pipeline(device, device->pipeline_soft_max_f32_wg512, "soft_max_f32_wg512", soft_max_f32_len, soft_max_f32_data, "main", 4, sizeof(vk_op_soft_max_push_constants), {1, 1, 1}, { 512 }, 1); ggml_vk_create_pipeline(device, device->pipeline_soft_max_f32_f16, "soft_max_f32_f16", soft_max_f32_f16_len, soft_max_f32_f16_data, "main", 4, sizeof(vk_op_soft_max_push_constants), {1, 1, 1}, { device->subgroup_size }, 1); @@ -4031,6 +4267,12 @@ static void ggml_vk_load_shaders(vk_device& device) { ggml_vk_create_pipeline(device, device->pipeline_rope_neox_f32_f16, "rope_neox_f32_f16", rope_neox_f32_f16_len, rope_neox_f32_f16_data, "main", 5, sizeof(vk_op_rope_push_constants), {1, 512, 1}, {}, 1); } + ggml_vk_create_pipeline(device, device->pipeline_out_prod_f32, "out_prod_f32", out_prod_f32_len, out_prod_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_out_prod_q4_0, "out_prod_q4_0", out_prod_q4_0_len, out_prod_q4_0_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1, true); + ggml_vk_create_pipeline(device, device->pipeline_out_prod_q8_0, "out_prod_q8_0", out_prod_q8_0_len, out_prod_q8_0_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1, true); + ggml_vk_create_pipeline(device, device->pipeline_out_prod_tq2_0, "out_prod_tq2_0", out_prod_tq2_0_len, out_prod_tq2_0_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1, true); + ggml_vk_create_pipeline(device, device->pipeline_out_prod_f16_f32, "out_prod_f16_f32", out_prod_f16_f32_len, out_prod_f16_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1); + for (uint32_t i = 0; i < num_argsort_pipelines; ++i) { uint32_t BLOCK_SIZE = 1u << std::min(i, device->max_workgroup_size_log2); if (i <= device->max_workgroup_size_log2 && @@ -4279,18 +4521,13 @@ static vk_device ggml_vk_get_device(size_t idx) { const char* GGML_VK_PREFER_HOST_MEMORY = getenv("GGML_VK_PREFER_HOST_MEMORY"); device->prefer_host_memory = GGML_VK_PREFER_HOST_MEMORY != nullptr; - const char* GGML_VK_DISABLE_HOST_VISIBLE_VIDMEM = getenv("GGML_VK_DISABLE_HOST_VISIBLE_VIDMEM"); - device->disable_host_visible_vidmem = GGML_VK_DISABLE_HOST_VISIBLE_VIDMEM != nullptr; - - const char* GGML_VK_ALLOW_SYSMEM_FALLBACK = getenv("GGML_VK_ALLOW_SYSMEM_FALLBACK"); - device->allow_sysmem_fallback = GGML_VK_ALLOW_SYSMEM_FALLBACK != nullptr; - const char* GGML_VK_DISABLE_GRAPH_OPTIMIZE = getenv("GGML_VK_DISABLE_GRAPH_OPTIMIZE"); device->disable_graph_optimize = GGML_VK_DISABLE_GRAPH_OPTIMIZE != nullptr; bool fp16_storage = false; bool fp16_compute = false; bool maintenance4_support = false; + bool descriptor_buffer_support = false; bool sm_builtins = false; bool amd_shader_core_properties2 = false; bool pipeline_robustness = false; @@ -4311,6 +4548,8 @@ static vk_device ggml_vk_get_device(size_t idx) { sm_builtins = true; } else if (strcmp("VK_AMD_shader_core_properties2", properties.extensionName) == 0) { amd_shader_core_properties2 = true; + } else if (strcmp("VK_EXT_descriptor_buffer", properties.extensionName) == 0) { + descriptor_buffer_support = true; } else if (strcmp("VK_EXT_pipeline_robustness", properties.extensionName) == 0) { pipeline_robustness = true; } else if (strcmp("VK_EXT_subgroup_size_control", properties.extensionName) == 0) { @@ -4346,6 +4585,7 @@ static vk_device ggml_vk_get_device(size_t idx) { vk::PhysicalDeviceProperties2 props2; vk::PhysicalDeviceMaintenance3Properties props3; vk::PhysicalDeviceMaintenance4Properties props4; + vk::PhysicalDeviceDescriptorBufferPropertiesEXT descriptor_buffer_props; vk::PhysicalDeviceSubgroupProperties subgroup_props; vk::PhysicalDeviceDriverProperties driver_props; vk::PhysicalDeviceShaderSMBuiltinsPropertiesNV sm_props; @@ -4367,6 +4607,10 @@ static vk_device ggml_vk_get_device(size_t idx) { last_struct->pNext = (VkBaseOutStructure *)&props4; last_struct = (VkBaseOutStructure *)&props4; } + if (descriptor_buffer_support) { + last_struct->pNext = (VkBaseOutStructure *)&descriptor_buffer_props; + last_struct = (VkBaseOutStructure *)&descriptor_buffer_props; + } if (sm_builtins) { last_struct->pNext = (VkBaseOutStructure *)&sm_props; last_struct = (VkBaseOutStructure *)&sm_props; @@ -4440,6 +4684,12 @@ static vk_device ggml_vk_get_device(size_t idx) { device->subgroup_size = subgroup_props.subgroupSize; device->subgroup_size_log2 = uint32_t(log2f(float(device->subgroup_size))); + if (descriptor_buffer_support && getenv("GGML_VK_TILING_DISABLE") == nullptr && + device->architecture == vk_device_architecture::QUALCOMM_ADRENO) { + // despite being able to allocate large buffers, using them for SSBOs cause problems + // on Adreno GPUs, so we need to tile larger operations. + device->tiling_threshold = descriptor_buffer_props.descriptorBufferAddressSpaceSize; + } device->uma = device->properties.deviceType == vk::PhysicalDeviceType::eIntegratedGpu; if (sm_builtins) { device->shader_core_count = sm_props.shaderSMCount; @@ -4857,6 +5107,29 @@ static vk_device ggml_vk_get_device(size_t idx) { device_create_info.setPNext(&device_features2); device->device = device->physical_device.createDevice(device_create_info); + { + VmaAllocatorCreateFlags vmaFlags = (device->buffer_device_address ? VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT : 0); + + VmaAllocatorCreateInfo allocatorCreateInfo = {}; + allocatorCreateInfo.flags = vmaFlags; + allocatorCreateInfo.vulkanApiVersion = vk::enumerateInstanceVersion(); + allocatorCreateInfo.physicalDevice = device->physical_device; + allocatorCreateInfo.device = device->device; + allocatorCreateInfo.instance = vk_instance.instance; + +#ifdef VMA_DYNAMIC_VULKAN_FUNCTIONS + static VmaVulkanFunctions vulkanFunctions = {}; + vulkanFunctions.vkGetInstanceProcAddr = &vkGetInstanceProcAddr; + vulkanFunctions.vkGetDeviceProcAddr = &vkGetDeviceProcAddr; + allocatorCreateInfo.pVulkanFunctions = &vulkanFunctions; +#endif + + VkResult res = vmaCreateAllocator(&allocatorCreateInfo, &device->allocator); + if (res != VK_SUCCESS) { + throw vk::SystemError(static_cast(res), "vmaCreateAllocator failed"); + } + } + // Queues ggml_vk_create_queue(device, device->compute_queue, compute_queue_family_index, 0, { vk::PipelineStageFlagBits::eComputeShader | vk::PipelineStageFlagBits::eTransfer }, false); @@ -4867,6 +5140,7 @@ static vk_device ggml_vk_get_device(size_t idx) { #ifndef GGML_VULKAN_RUN_TESTS case VK_VENDOR_ID_AMD: case VK_VENDOR_ID_INTEL: + case VK_VENDOR_ID_QUALCOMM: device->mul_mat_l[i] = false; device->mul_mat_m[i] = true; device->mul_mat_s[i] = true; @@ -4931,7 +5205,8 @@ static vk_device ggml_vk_get_device(size_t idx) { device->idx = idx; - device->disable_fusion = getenv("GGML_VK_DISABLE_FUSION") != nullptr; + device->disable_fusion = getenv("GGML_VK_DISABLE_FUSION") != nullptr || + device->vendor_id == VK_VENDOR_ID_QUALCOMM; device->add_rms_fusion = !device->disable_fusion && device->subgroup_arithmetic && @@ -5194,7 +5469,11 @@ static void ggml_vk_instance_init() { vk_instance.pfn_vkCmdInsertDebugUtilsLabelEXT = (PFN_vkCmdInsertDebugUtilsLabelEXT) vkGetInstanceProcAddr(vk_instance.instance, "vkCmdInsertDebugUtilsLabelEXT"); } +#ifndef FORCE_GGML_VK_PERF_LOGGER vk_perf_logger_enabled = getenv("GGML_VK_PERF_LOGGER") != nullptr; +#else + vk_perf_logger_enabled = true; +#endif // See https://github.com/KhronosGroup/Vulkan-Hpp?tab=readme-ov-file#extensions--per-device-function-pointers- VULKAN_HPP_DEFAULT_DISPATCHER.init(vk_instance.instance); @@ -5365,6 +5644,7 @@ static void ggml_vk_init(ggml_backend_vk_context * ctx, size_t idx) { ctx->prealloc_size_split_k = 0; // Fixed size of 1KB, for deterministic behavior ctx->prealloc_size_add_rms_partials = 1024; + ctx->prealloc_size_tile = 0; ctx->fence = ctx->device->device.createFence({}); ctx->almost_ready_fence = ctx->device->device.createFence({}); @@ -5389,6 +5669,7 @@ static vk_pipeline ggml_vk_get_to_fp16(ggml_backend_vk_context * ctx, ggml_type case GGML_TYPE_Q5_0: case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: + case GGML_TYPE_TQ2_0: case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -5460,6 +5741,9 @@ static vk_matmul_pipeline ggml_vk_get_mul_mat_mat_pipeline(ggml_backend_vk_conte case GGML_TYPE_Q5_0: case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: + case GGML_TYPE_TQ2_0: + case GGML_TYPE_TQ1_0: + case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -5497,6 +5781,8 @@ static vk_pipeline ggml_vk_get_dequantize_mul_mat_vec(ggml_backend_vk_context * if (b_type == GGML_TYPE_Q8_1) { switch (a_type) { + case GGML_TYPE_TQ2_0: + case GGML_TYPE_TQ1_0: case GGML_TYPE_Q4_0: case GGML_TYPE_Q4_1: case GGML_TYPE_Q5_0: @@ -5523,6 +5809,9 @@ static vk_pipeline ggml_vk_get_dequantize_mul_mat_vec(ggml_backend_vk_context * case GGML_TYPE_Q5_0: case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: + case GGML_TYPE_TQ2_0: + case GGML_TYPE_TQ1_0: + case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -5564,6 +5853,9 @@ static vk_pipeline ggml_vk_get_dequantize_mul_mat_vec(ggml_backend_vk_context * if (ctx->device->vendor_id == VK_VENDOR_ID_INTEL) { dmmv_wg = DMMV_WG_SIZE_SUBGROUP; } + if (ctx->device->vendor_id == VK_VENDOR_ID_QUALCOMM) { + dmmv_wg = DMMV_WG_SIZE_SUBGROUP; + } return ctx->device->pipeline_dequant_mul_mat_vec_q8_1_f32[dmmv_wg][a_type][num_cols-1]; } @@ -5613,6 +5905,9 @@ static vk_matmul_pipeline ggml_vk_get_mul_mat_mat_id_pipeline(ggml_backend_vk_co case GGML_TYPE_Q5_0: case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: + case GGML_TYPE_TQ2_0: + case GGML_TYPE_TQ1_0: + case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -5679,6 +5974,9 @@ static vk_pipeline ggml_vk_get_dequantize_mul_mat_vec_id(ggml_backend_vk_context case GGML_TYPE_Q5_0: case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: + case GGML_TYPE_TQ2_0: + case GGML_TYPE_TQ1_0: + case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -5728,22 +6026,34 @@ static vk_pipeline ggml_vk_get_dequantize_mul_mat_vec_id(ggml_backend_vk_context static void * ggml_vk_host_malloc(vk_device& device, size_t size) { VK_LOG_MEMORY("ggml_vk_host_malloc(" << size << ")"); - vk_buffer buf = ggml_vk_create_buffer(device, size, - {vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostCached, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent}); + const vk::BufferCreateInfo buffer_info{ + vk::BufferCreateFlags(), + size, + vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eTransferSrc, + vk::SharingMode::eExclusive, + 0, + nullptr, + }; + + VmaAllocationCreateInfo alloc_info = {}; + alloc_info.usage = (device->prefer_host_memory ? VMA_MEMORY_USAGE_AUTO_PREFER_HOST : VMA_MEMORY_USAGE_AUTO); + alloc_info.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT; + alloc_info.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + + vk_buffer buf = ggml_vk_create_buffer_aligned(device, buffer_info, alloc_info, TENSOR_ALIGNMENT); if(!(buf->memory_property_flags & vk::MemoryPropertyFlagBits::eHostVisible)) { fprintf(stderr, "WARNING: failed to allocate %.2f MB of pinned memory\n", size/1024.0/1024.0); - device->device.freeMemory(buf->device_memory); - device->device.destroyBuffer(buf->buffer); + buf.reset(); return nullptr; } std::lock_guard guard(device->mutex); - device->pinned_memory.push_back(std::make_tuple(buf->ptr, size, buf)); + device->pinned_memory.push_back(std::make_tuple(buf->info.pMappedData, size, buf)); - return buf->ptr; + return buf->info.pMappedData; } static void ggml_vk_host_free(vk_device& device, void* ptr) { @@ -5932,9 +6242,7 @@ static void ggml_vk_ensure_sync_staging_buffer(vk_device& device, size_t size) { if (device->sync_staging == nullptr || device->sync_staging->size < size) { VK_LOG_MEMORY("ggml_vk_ensure_sync_staging_buffer(" << size << ")"); ggml_vk_destroy_buffer(device->sync_staging); - device->sync_staging = ggml_vk_create_buffer_check(device, size, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostCached, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + device->sync_staging = ggml_vk_create_buffer_check(device, size); } } @@ -5942,9 +6250,7 @@ static void ggml_vk_ensure_sync_staging_buffer(ggml_backend_vk_context * ctx, si if (ctx->sync_staging == nullptr || ctx->sync_staging->size < size) { VK_LOG_MEMORY("ggml_vk_ensure_sync_staging_buffer(" << size << ")"); ggml_vk_destroy_buffer(ctx->sync_staging); - ctx->sync_staging = ggml_vk_create_buffer_check(ctx->device, size, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostCached, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + ctx->sync_staging = ggml_vk_create_buffer_check(ctx->device, size); } } @@ -6027,16 +6333,16 @@ static void ggml_vk_buffer_write_nc_async(ggml_backend_vk_context * ctx, vk_cont for (uint64_t i2 = 0; i2 < ne2; i2++) { // Find longest contiguous slice if (ne1*nb1 == dstnb2) { - deferred_memcpy((uint8_t *)staging->ptr + i3*dstnb3 + i2*dstnb2, (const uint8_t *) tensor->data + buf_offset + i3*nb3 + i2*nb2, dstnb2, &subctx->in_memcpys); + deferred_memcpy((uint8_t *)staging->info.pMappedData + i3*dstnb3 + i2*dstnb2, (const uint8_t *) tensor->data + buf_offset + i3*nb3 + i2*nb2, dstnb2, &subctx->in_memcpys); } else { for (uint64_t i1 = 0; i1 < ne1; i1++) { if (ne0*nb0/bs == dstnb1) { - deferred_memcpy((uint8_t *)staging->ptr + i3*dstnb3 + i2*dstnb2 + i1*dstnb1, (const uint8_t *) tensor->data + buf_offset + i3*nb3 + i2*nb2 + i1*nb1, dstnb1, &subctx->in_memcpys); + deferred_memcpy((uint8_t *)staging->info.pMappedData + i3*dstnb3 + i2*dstnb2 + i1*dstnb1, (const uint8_t *) tensor->data + buf_offset + i3*nb3 + i2*nb2 + i1*nb1, dstnb1, &subctx->in_memcpys); } else { const uint64_t s_off = buf_offset + i3*nb3 + i2*nb2 + i1*nb1; const uint64_t d_off = i3*dstnb3 + i2*dstnb2 + i1*dstnb1; for (uint64_t i0 = 0; i0 < ne0; i0++) { - deferred_memcpy((uint8_t *)staging->ptr + d_off + i0*dstnb0, (const uint8_t *) tensor->data + s_off + i0*nb0, dstnb0, &subctx->in_memcpys); + deferred_memcpy((uint8_t *)staging->info.pMappedData + d_off + i0*dstnb0, (const uint8_t *) tensor->data + s_off + i0*nb0, dstnb0, &subctx->in_memcpys); } } } @@ -6087,7 +6393,6 @@ static void ggml_vk_buffer_write_2d_async(vk_context subctx, vk_buffer& dst, siz // Staging buffer required const size_t copy_size = width*height; ggml_vk_ensure_sync_staging_buffer(dst->device, copy_size); - vk_buffer& staging_buffer = dst->device->sync_staging; VkBufferCopy buf_copy = { @@ -6099,10 +6404,10 @@ static void ggml_vk_buffer_write_2d_async(vk_context subctx, vk_buffer& dst, siz vkCmdCopyBuffer(subctx->s->buffer, (VkBuffer)staging_buffer->buffer, (VkBuffer)dst->buffer, 1, &buf_copy); if (width == spitch) { - deferred_memcpy((uint8_t *)staging_buffer->ptr, src, width * height, &subctx->in_memcpys); + deferred_memcpy((uint8_t *)staging_buffer->info.pMappedData, src, width * height, &subctx->in_memcpys); } else { for (size_t i = 0; i < height; i++) { - deferred_memcpy((uint8_t *)staging_buffer->ptr + i * width, (const uint8_t *) src + i * spitch, width, &subctx->in_memcpys); + deferred_memcpy((uint8_t *)staging_buffer->info.pMappedData + i * width, (const uint8_t *) src + i * spitch, width, &subctx->in_memcpys); } } } @@ -6119,7 +6424,7 @@ static void ggml_vk_buffer_write_2d(vk_buffer& dst, size_t offset, const void * GGML_ASSERT(dst->memory_property_flags & vk::MemoryPropertyFlagBits::eHostCoherent); for (size_t i = 0; i < height; i++) { - memcpy((uint8_t *)dst->ptr + offset + i * width, (const uint8_t *) src + i * spitch, width); + memcpy((uint8_t *)dst->info.pMappedData + offset + i * width, (const uint8_t *) src + i * spitch, width); } } else { std::lock_guard guard(dst->device->mutex); @@ -6200,7 +6505,7 @@ static bool ggml_vk_buffer_read_2d_async(vk_context subctx, vk_buffer& src, size ggml_vk_sync_buffers(nullptr, subctx); subctx->s->buffer.copyBuffer(src->buffer, staging_buffer->buffer, slices); - deferred_memcpy(dst, staging_buffer->ptr, copy_size, &subctx->out_memcpys); + deferred_memcpy(dst, staging_buffer->info.pMappedData, copy_size, &subctx->out_memcpys); return true; } @@ -6217,7 +6522,7 @@ static void ggml_vk_buffer_read(vk_buffer& src, size_t offset, void * dst, size_ if(src->memory_property_flags & vk::MemoryPropertyFlagBits::eHostVisible && src->device->uma) { GGML_ASSERT(src->memory_property_flags & vk::MemoryPropertyFlagBits::eHostCoherent); - memcpy(dst, (uint8_t *) src->ptr + offset, size); + memcpy(dst, (uint8_t *) src->info.pMappedData + offset, size); } else { std::lock_guard guard(src->device->mutex); @@ -6268,17 +6573,17 @@ static void ggml_vk_buffer_copy(vk_buffer& dst, size_t dst_offset, vk_buffer& sr // Copy to src staging buffer ggml_vk_buffer_copy(src->device->sync_staging, 0, src, src_offset, size); + // Copy to dst buffer - ggml_vk_buffer_write_2d(dst, dst_offset, src->device->sync_staging->ptr, 0, size, 1); + ggml_vk_buffer_write_2d(dst, dst_offset, src->device->sync_staging->info.pMappedData, 0, size, 1); } } static void ggml_vk_buffer_memset_async(vk_context& ctx, vk_buffer& dst, size_t offset, uint32_t c, size_t size) { VK_LOG_DEBUG("ggml_vk_buffer_memset_async(" << offset << ", " << c << ", " << size << ")"); - if (dst->memory_property_flags & vk::MemoryPropertyFlagBits::eHostVisible && - dst->device->uma) { - deferred_memset((uint8_t*)dst->ptr + offset, c, size, &ctx->memsets); + if (dst->memory_property_flags & vk::MemoryPropertyFlagBits::eHostVisible) { + deferred_memset((uint8_t*)dst->info.pMappedData + offset, c, size, &ctx->memsets); return; } @@ -6289,9 +6594,8 @@ static void ggml_vk_buffer_memset_async(vk_context& ctx, vk_buffer& dst, size_t static void ggml_vk_buffer_memset(vk_buffer& dst, size_t offset, uint32_t c, size_t size) { VK_LOG_DEBUG("ggml_vk_buffer_memset(" << offset << ", " << c << ", " << size << ")"); - if (dst->memory_property_flags & vk::MemoryPropertyFlagBits::eHostVisible && - dst->device->uma) { - memset((uint8_t*)dst->ptr + offset, c, size); + if (dst->memory_property_flags & vk::MemoryPropertyFlagBits::eHostVisible) { + memset((uint8_t*)dst->info.pMappedData + offset, c, size); return; } @@ -6314,6 +6618,12 @@ static uint32_t ggml_vk_guess_split_k(ggml_backend_vk_context * ctx, uint32_t m, return 1; } + // disable split_k for large Adreno matrices, since we usually need to do tiling + // for those cases and split_k would increase memory requirements + if (ctx->device->architecture == vk_device_architecture::QUALCOMM_ADRENO) { + return 1; + } + uint32_t split_k = 1; if (ctx->device->shader_core_count != 0 && m >= pipeline->wg_denoms[0] && n >= pipeline->wg_denoms[1]) { // If k is 'large' and the SMs will fill less than halfway, use split_k. @@ -6648,6 +6958,243 @@ static void ggml_vk_quantize_q8_1(ggml_backend_vk_context * ctx, vk_context& sub ggml_vk_sync_buffers(ctx, subctx); } +static void ggml_vk_copy_2d_to_2d_post_compute_barrier(vk_context& subctx, vk_buffer& buf, size_t offset, size_t size) { + VkBufferMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.buffer = (VkBuffer) buf->buffer; + barrier.offset = offset; + barrier.size = size; + + vkCmdPipelineBarrier(subctx->s->buffer, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 1, &barrier, 0, nullptr); +} + +static void ggml_vk_copy_2d_to_2d_pre_compute_barrier(vk_context& subctx, vk_buffer& buf, size_t offset, size_t size) { + VkBufferMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.buffer = (VkBuffer) buf->buffer; + barrier.offset = offset; + barrier.size = size; + + vkCmdPipelineBarrier(subctx->s->buffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, 0, nullptr, 1, &barrier, 0, nullptr); +} + +static void ggml_vk_copy_2d_to_2d(vk_context& subctx, vk_buffer& dst, size_t dst_offset, vk_buffer& src, size_t src_offset, size_t width, size_t height, size_t spitch, size_t dpitch) { + VK_LOG_DEBUG("ggml_vk_copy_2d_to_2d(dst=" << dst << ", dst_offset=" << dst_offset + << ", src=" << src << ", src_offset=" << src_offset + << ", width=" << width << ", height=" << height + << ", spitch=" << spitch << ", dpitch=" << dpitch + << ", multi_device=" << (src->device != dst->device) << ")"); + + size_t src_total_size = src_offset + (height - 1) * spitch + width; + GGML_ASSERT(src_total_size <= src->size && "Source buffer bounds exceeded"); + + size_t dst_total_size = dst_offset + (height - 1) * dpitch + width; + GGML_ASSERT(dst_total_size <= dst->size && "Destination buffer bounds exceeded"); + + // make sure the buffers live on the same device + if (src->device != dst->device) { + ggml_vk_ensure_sync_staging_buffer(src->device, src_total_size); + ggml_vk_ensure_sync_staging_buffer(dst->device, dst_total_size); + + ggml_vk_buffer_copy(src->device->sync_staging, 0, src, src_offset, src_total_size); + memcpy(dst->device->sync_staging->info.pMappedData, src->device->sync_staging->info.pMappedData, src_total_size); + ggml_vk_copy_2d_to_2d(subctx, dst->device->sync_staging, dst_offset, src->device->sync_staging, 0, width, height, spitch, dpitch); + return; + } + + GGML_ASSERT(src->device == dst->device); + + std::lock_guard guard(src->device->mutex); + + int group_size = dst->device->architecture == vk_device_architecture::QUALCOMM_ADRENO ? 256 : height; + int groups = CEIL_DIV(height, group_size); + + for (int i = 0; i < groups; i++) { + int group_height = std::min(group_size, (int) height - i * group_size); + std::vector copy_regions; + copy_regions.reserve(group_height); + + for (size_t row = i * group_size; row < (size_t) i * group_size + group_height; ++row) { + size_t row_src_offset = src_offset + row * spitch; + size_t row_dst_offset = dst_offset + row * dpitch; + + VkBufferCopy bc{ row_src_offset, row_dst_offset, width }; + copy_regions.push_back(bc); + } + vkCmdCopyBuffer(subctx->s->buffer, (VkBuffer)src->buffer, (VkBuffer)dst->buffer, copy_regions.size(), copy_regions.data()); + + ggml_vk_ctx_end(subctx); + ggml_vk_submit(subctx, src->device->fence); + VK_CHECK(src->device->device.waitForFences({ src->device->fence }, true, UINT64_MAX), "vk wait"); + src->device->device.resetFences({ src->device->fence }); + ggml_vk_command_pool_cleanup(src->device, *subctx->p); + ggml_vk_ctx_begin(src->device, subctx); + } +} + +static uint64_t sum_buffer_sizes(uint64_t m, uint64_t n, uint64_t k, enum ggml_type m_type, enum ggml_type n_type, enum ggml_type d_type) { + uint64_t a_size = CEIL_DIV(m*k, ggml_blck_size(m_type)) * ggml_type_size(m_type); + uint64_t b_size = CEIL_DIV(n*k, ggml_blck_size(n_type)) * ggml_type_size(n_type); + uint64_t d_size = CEIL_DIV(m*n, ggml_blck_size(d_type)) * ggml_type_size(d_type); + + return a_size + b_size + d_size; +} + +// calculates largest tile size for binary m,n,k ops such as MUL_MAT +static void calculate_tile_dims(ggml_backend_vk_context * ctx, + uint64_t m, uint64_t n, uint64_t k, + uint64_t *tile_m, uint64_t *tile_n, + uint64_t *m_tiles,uint64_t *n_tiles, + uint64_t *num_dispatches, + enum ggml_type m_type = GGML_TYPE_F32, + enum ggml_type n_type = GGML_TYPE_F32, + enum ggml_type d_type = GGML_TYPE_F32) { + VK_LOG_DEBUG("calculate_tile_dims(m=" << m << ", n=" << n << ", k=" << k + << ", m_type=" << ggml_type_name(m_type) + << ", n_type=" << ggml_type_name(n_type) + << ", d_type=" << ggml_type_name(d_type) << ")"); + + // minimum tile size + const uint64_t step = MINIMUM_TILE_SIZE; + + // starting tile size + uint64_t mt = std::min(step, m); + uint64_t nt = std::min(step, n); + + // the minimum tile size might be already too large (if it is, it's likely because of `k`) + GGML_ASSERT(sum_buffer_sizes(mt, nt, k, m_type, n_type, d_type) < ctx->device->tiling_threshold); + + while (nt < n || mt < m) { + bool nt_stopped = false; + bool mt_stopped = false; + + if (nt < n) { + uint64_t next_nt = std::min(n, nt + step); + if (sum_buffer_sizes(mt, next_nt, k, m_type, n_type, d_type) < ctx->device->tiling_threshold) { + nt = next_nt; + } else { + nt_stopped = true; + } + } + + if (mt < m) { + uint64_t next_mt = std::min(m, mt + step); + if (sum_buffer_sizes(next_mt, nt, k, m_type, n_type, d_type) < ctx->device->tiling_threshold) { + mt = next_mt; + } else { + mt_stopped = true; + } + } + + if ((nt_stopped || nt >= n) && (mt_stopped || mt >= m)) { + break; + } + } + + *tile_m = mt; + *tile_n = nt; + *m_tiles = CEIL_DIV(m, mt); + *n_tiles = CEIL_DIV(n, nt); + *num_dispatches = (*m_tiles) * (*n_tiles); +} + +static void ggml_vk_matmul_tiling(ggml_backend_vk_context *ctx, vk_context& subctx, vk_pipeline& pipeline, + const uint64_t ne00, const uint64_t ne01, const uint64_t ne10, const uint64_t ne11, const uint64_t ne20, + ggml_type a_type, ggml_type b_type, ggml_type d_type, const uint64_t tile_m, const uint64_t tile_n, + vk_buffer& d_X, size_t x_buf_offset, + vk_buffer& d_Y, size_t y_buf_offset, + vk_buffer& d_D, size_t d_buf_offset) { + VK_LOG_DEBUG("ggml_vk_matmul_tiling(ne00=" << ne00 << ", ne01=" << ne01 << ", ne10=" << ne10 << ", ne11=" << ne11 + << ", a_type=" << ggml_type_name(a_type) << ", b_type=" << ggml_type_name(b_type) << ", d_type=" << ggml_type_name(d_type) << ")"); + + uint32_t a_bytes_per_block = ggml_type_size(a_type); + uint32_t a_elems_per_block = ggml_blck_size(a_type); + + uint32_t b_bytes_per_block = ggml_type_size(b_type); + uint32_t b_elems_per_block = ggml_blck_size(b_type); + + uint32_t d_bytes_per_block = ggml_type_size(d_type); + uint32_t d_elems_per_block = ggml_blck_size(d_type); + + for (uint32_t n0 = 0; n0 < ne11; n0 += tile_n) { + const uint32_t nt = (uint32_t) std::min(tile_n, ne11 - n0); + const uint64_t b_off_bytes = y_buf_offset + CEIL_DIV(((uint64_t)n0 * (uint64_t)ne10), b_elems_per_block) * (uint64_t)b_bytes_per_block; + const uint64_t d_off_bytes_n = d_buf_offset + CEIL_DIV(((uint64_t)n0 * (uint64_t)ne20), d_elems_per_block) * (uint64_t)d_bytes_per_block; + + const uint32_t padded_n_tile = nt; + + const uint64_t b_k_bytes = CEIL_DIV(ne10, b_elems_per_block) * b_bytes_per_block; + const uint64_t orig_stride_b_bytes = b_k_bytes; + const uint64_t tile_stride_b_bytes = orig_stride_b_bytes; + const uint32_t tile_stride_b_elems = (tile_stride_b_bytes / b_bytes_per_block) * b_elems_per_block; + const uint64_t b_size_bytes = tile_stride_b_bytes * nt; + const uint64_t b_off = 0; + + // copy tile b data to buffer b + ggml_vk_copy_2d_to_2d(subctx, ctx->prealloc_tile, b_off, d_Y, b_off_bytes, b_k_bytes, nt, orig_stride_b_bytes, tile_stride_b_bytes); + + for (uint32_t m0 = 0; m0 < ne01; m0 += tile_m) { + const uint32_t mt = (uint32_t) std::min(tile_m, ne01 - m0); + const uint64_t a_off_bytes = x_buf_offset + CEIL_DIV(((uint64_t)m0 * (uint64_t)ne00), a_elems_per_block) * (uint64_t)a_bytes_per_block; + const uint64_t d_off_bytes = d_off_bytes_n + CEIL_DIV((uint64_t)m0, d_elems_per_block) * (uint64_t)d_bytes_per_block; + + const uint64_t a_k_bytes = CEIL_DIV(ne00, a_elems_per_block) * a_bytes_per_block; + const uint64_t orig_stride_a_bytes = a_k_bytes; + const uint64_t orig_stride_d_bytes = CEIL_DIV(ne20, d_elems_per_block) * d_bytes_per_block; + const uint64_t tile_stride_a_bytes = orig_stride_a_bytes; + const uint64_t tile_stride_d_bytes = CEIL_DIV(mt, d_elems_per_block) * d_bytes_per_block; + + const uint32_t tile_stride_a_elems = (tile_stride_a_bytes / a_bytes_per_block) * a_elems_per_block; + const uint32_t tile_stride_d_elems = (tile_stride_d_bytes / d_bytes_per_block) * d_elems_per_block; + const uint64_t dst_copy_row_size = CEIL_DIV(mt, d_elems_per_block) * d_bytes_per_block; + + const uint64_t a_size_bytes = tile_stride_a_bytes * mt; + const uint64_t d_size_bytes = tile_stride_d_bytes * nt; + + const uint64_t a_off = b_off + b_size_bytes; + const uint64_t d_off = a_off + a_size_bytes; + + GGML_ASSERT(a_size_bytes + b_size_bytes + d_size_bytes < ctx->device->tiling_threshold); + GGML_ASSERT(d_off + d_size_bytes < ctx->device->tiling_threshold); + + // copy tile a data to buffer a + ggml_vk_copy_2d_to_2d(subctx, ctx->prealloc_tile, a_off, d_X, a_off_bytes, a_k_bytes, mt, orig_stride_a_bytes, tile_stride_a_bytes); + ggml_vk_copy_2d_to_2d_pre_compute_barrier(subctx, ctx->prealloc_tile, 0, a_size_bytes + b_size_bytes); + + // call matmul for the tile + ggml_vk_matmul( + ctx, subctx, pipeline, + { ctx->prealloc_tile, a_off, a_size_bytes }, { ctx->prealloc_tile, b_off, b_size_bytes }, + { ctx->prealloc_tile, d_off, d_size_bytes }, { nullptr, 0, 0 }, + mt, nt, (uint32_t)ne00, + tile_stride_a_elems, tile_stride_b_elems, tile_stride_d_elems, + tile_stride_a_elems*mt, tile_stride_b_elems*nt, tile_stride_d_elems*nt, + 1u, 1u, 1u, 1u, 1u, 1u, padded_n_tile + ); + + // copy results back to dst buffer + ggml_vk_copy_2d_to_2d_post_compute_barrier(subctx, ctx->prealloc_tile, d_off, d_size_bytes); + ggml_vk_copy_2d_to_2d(subctx, d_D, d_off_bytes, ctx->prealloc_tile, d_off, dst_copy_row_size, nt, tile_stride_d_bytes, orig_stride_d_bytes); + + ggml_vk_sync_buffers(ctx, subctx); + } + } +} + static void ggml_vk_mul_mat_q_f16(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst, bool disable_split_k) { VK_LOG_DEBUG("ggml_vk_mul_mat_q_f16((" << src0 << ", name=" << src0->name << ", type=" << ggml_type_name(src0->type) << ", ne0=" << src0->ne[0] << ", ne1=" << src0->ne[1] << ", ne2=" << src0->ne[2] << ", ne3=" << src0->ne[3] << ", nb0=" << src0->nb[0] << ", nb1=" << src0->nb[1] << ", nb2=" << src0->nb[2] << ", nb3=" << src0->nb[3]; std::cerr << "), (" << src1 << ", name=" << src1->name << ", type=" << ggml_type_name(src1->type) << ", ne0=" << src1->ne[0] << ", ne1=" << src1->ne[1] << ", ne2=" << src1->ne[2] << ", ne3=" << src1->ne[3] << ", nb0=" << src1->nb[0] << ", nb1=" << src1->nb[1] << ", nb2=" << src1->nb[2] << ", nb3=" << src1->nb[3]; @@ -6666,6 +7213,7 @@ static void ggml_vk_mul_mat_q_f16(ggml_backend_vk_context * ctx, vk_context& sub const uint64_t ne12 = src1->ne[2]; const uint64_t ne13 = src1->ne[3]; + const uint64_t ne20 = dst->ne[0]; const uint64_t ne21 = dst->ne[1]; const uint32_t stride_d = dst->nb[1] / ggml_type_size(dst->type); const uint32_t stride_batch_d = stride_d*ne21; @@ -6767,7 +7315,29 @@ static void ggml_vk_mul_mat_q_f16(ggml_backend_vk_context * ctx, vk_context& sub to_q8_1 = ggml_vk_get_quantize_pipeline(ctx, GGML_TYPE_Q8_1); } + const uint64_t d_buf_offset = vk_tensor_offset(dst) + dst->view_offs; + + bool do_tiling = ctx->device->architecture == vk_device_architecture::QUALCOMM_ADRENO && + (ne02 == 1 && ne03 == 1 && ne12 == 1 && ne13 == 1) && + (x_sz + y_sz + d_sz >= ctx->device->tiling_threshold); + + uint64_t tile_m = 0, tile_n = 0, m_tiles = 0, n_tiles = 0, num_dispatches = 1; + if (do_tiling) { + GGML_ASSERT(ne02 == 1 && ne03 == 1 && ne12 == 1 && ne13 == 1); + calculate_tile_dims(ctx, ne01, ne11, ne00, &tile_m, &tile_n, &m_tiles, &n_tiles, &num_dispatches); + VK_LOG_DEBUG("[ggml_vk_mul_mat_q_f16] [tiling] tile_m=" + << tile_m << ", tile_n=" << tile_n << ", m_tiles=" << m_tiles << ", n_tiles=" << n_tiles + << ", num_dispatches=" << num_dispatches); + } + { + if (do_tiling) { + ctx->prealloc_size_tile = ctx->device->tiling_threshold; + GGML_ASSERT(split_k <= 1); + if (ctx->prealloc_tile == nullptr || ctx->prealloc_tile->size < ctx->prealloc_size_tile) { + ggml_vk_preallocate_buffers(ctx, subctx); + } + } const uint64_t split_k_size = split_k > 1 ? d_sz * split_k : 0; if ( (qx_needs_dequant && x_sz > ctx->device->properties.limits.maxStorageBufferRange) || @@ -6789,7 +7359,7 @@ static void ggml_vk_mul_mat_q_f16(ggml_backend_vk_context * ctx, vk_context& sub } // Request descriptor sets - ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); + ggml_pipeline_request_descriptor_sets(ctx, pipeline, num_dispatches); if (qx_needs_dequant) { ggml_pipeline_request_descriptor_sets(ctx, to_fp16_vk_0, 1); } @@ -6805,7 +7375,6 @@ static void ggml_vk_mul_mat_q_f16(ggml_backend_vk_context * ctx, vk_context& sub } vk_buffer d_D = dst_buf_ctx->dev_buffer; - const uint64_t d_buf_offset = vk_tensor_offset(dst) + dst->view_offs; GGML_ASSERT(d_D != nullptr); GGML_ASSERT(d_D->size >= d_buf_offset + d_sz); vk_buffer d_X; @@ -6890,14 +7459,30 @@ static void ggml_vk_mul_mat_q_f16(ggml_backend_vk_context * ctx, vk_context& sub } // compute - ggml_vk_matmul( - ctx, subctx, pipeline, - { d_X, x_buf_offset, x_sz }, { d_Y, y_buf_offset, y_sz }, - ggml_vk_subbuffer(ctx, d_D, d_buf_offset), { ctx->prealloc_split_k, 0, d_sz * split_k }, - ne01, ne11, ne10, - ne10, ne10, stride_d, stride_batch_x, stride_batch_y, stride_batch_d, - split_k, ne12*ne13, ne02, ne12, r2, r3, padded_n - ); // NOLINT + if (do_tiling) { + ggml_type a_type = qx_needs_dequant ? f16_type : src0->type; + ggml_type b_type = quantize_y ? GGML_TYPE_Q8_1 : (y_f32_kernel ? GGML_TYPE_F32 : f16_type); + ggml_type d_type = GGML_TYPE_F32; + + GGML_ASSERT(ne02 == 1 && ne03 == 1 && ne12 == 1 && ne13 == 1); + GGML_ASSERT(split_k <= 1); + + ggml_vk_matmul_tiling( + ctx, subctx, pipeline, + ne00, ne01, ne10, ne11, ne20, + a_type, b_type, d_type, tile_m, tile_n, + d_X, x_buf_offset, d_Y, y_buf_offset, d_D, d_buf_offset + ); + } else { + ggml_vk_matmul( + ctx, subctx, pipeline, + { d_X, x_buf_offset, x_sz }, { d_Y, y_buf_offset, y_sz }, + ggml_vk_subbuffer(ctx, d_D, d_buf_offset), { ctx->prealloc_split_k, 0, d_sz * split_k }, + ne01, ne11, ne10, + ne10, ne10, stride_d, stride_batch_x, stride_batch_y, stride_batch_d, + split_k, ne12*ne13, ne02, ne12, r2, r3, padded_n + ); // NOLINT + } if (x_non_contig || qx_needs_dequant) { ctx->prealloc_x_need_sync = true; @@ -8485,6 +9070,11 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const return ctx->device->pipeline_silu_back_f32; } return nullptr; + case GGML_OP_GEGLU_BACK: + if (src0->type == GGML_TYPE_F32 && src1->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32) { + return ctx->device->pipeline_geglu_back_f32; + } + return nullptr; case GGML_OP_NORM: if (src0->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32) { return ctx->device->pipeline_norm_f32; @@ -8591,6 +9181,21 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const return ctx->device->pipeline_diag_mask_inf_f32; } return nullptr; + case GGML_OP_CROSS_ENTROPY_LOSS_BACK: + if (src0->type == GGML_TYPE_F32 && src1->type == GGML_TYPE_F32 && src2->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32) { + return ctx->device->pipeline_cross_entropy_loss_back_f32; + } + return nullptr; + case GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK: + if (src0->type == GGML_TYPE_F32 && src1->type == GGML_TYPE_F32 && src2->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32) { + return ctx->device->pipeline_cross_entropy_loss_masked_back_f32; + } + return nullptr; + case GGML_OP_COUNT_EQUAL_MASKED: + if (src0->type == GGML_TYPE_I32 && src1->type == GGML_TYPE_I32 && src2->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_I64) { + return ctx->device->pipeline_count_equal_masked_i32; + } + return nullptr; case GGML_OP_SOFT_MAX: GGML_ASSERT(!src1 || src1->type == GGML_TYPE_F32 || src1->type == GGML_TYPE_F16); GGML_ASSERT(!src2 || src2->type == GGML_TYPE_F32); @@ -8660,6 +9265,17 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const } return nullptr; } + case GGML_OP_OUT_PROD: + if (dst->type == GGML_TYPE_F32 && src1->type == GGML_TYPE_F32) { + if (src0->type == GGML_TYPE_F32) return ctx->device->pipeline_out_prod_f32; + if (src0->type == GGML_TYPE_Q4_0) return ctx->device->pipeline_out_prod_q4_0; + if (src0->type == GGML_TYPE_Q8_0) return ctx->device->pipeline_out_prod_q8_0; + if (src0->type == GGML_TYPE_TQ2_0) return ctx->device->pipeline_out_prod_tq2_0; + } + if (src0->type == GGML_TYPE_F16 && src1->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32) { + return ctx->device->pipeline_out_prod_f16_f32; + } + return nullptr; case GGML_OP_SUM: case GGML_OP_SUM_ROWS: case GGML_OP_MEAN: @@ -8966,7 +9582,7 @@ static void ggml_vk_op_f32(ggml_backend_vk_context * ctx, vk_context& subctx, co } std::cerr << "), (" << dst << ", name=" << dst->name << ", type=" << dst->type << ", ne0=" << dst->ne[0] << ", ne1=" << dst->ne[1] << ", ne2=" << dst->ne[2] << ", ne3=" << dst->ne[3] << ", nb0=" << dst->nb[0] << ", nb1=" << dst->nb[1] << ", nb2=" << dst->nb[2] << ", nb3=" << dst->nb[3]; std::cerr << "), " << ggml_op_name(op) << ")"); - GGML_ASSERT(op == GGML_OP_GET_ROWS || op == GGML_OP_CPY || (!ggml_is_quantized(src0->type) && (src1 == nullptr || !ggml_is_quantized(src1->type)))); // NOLINT + GGML_ASSERT(op == GGML_OP_GET_ROWS || op == GGML_OP_CPY || op == GGML_OP_OUT_PROD || (!ggml_is_quantized(src0->type) && (src1 == nullptr || !ggml_is_quantized(src1->type)))); // NOLINT GGML_ASSERT(dst->buffer != nullptr); const uint64_t ne00 = src0->ne[0]; const uint64_t ne01 = src0->ne[1]; @@ -9040,6 +9656,32 @@ static void ggml_vk_op_f32(ggml_backend_vk_context * ctx, vk_context& subctx, co } } break; + case GGML_OP_CROSS_ENTROPY_LOSS_BACK: + { + // For cross entropy loss back, we need one workgroup per row of logits (src1) + const uint32_t nr = ggml_nrows(src1); + if (nr > 262144) { + elements = { 512, 512, CEIL_DIV(nr, 262144) }; + } else if (nr > 512) { + elements = { 512, CEIL_DIV(nr, 512), 1 }; + } else { + elements = { nr, 1, 1 }; + } + } + break; + case GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK: + { + const uint32_t nr = ggml_nrows(src1); + elements = { nr, 1, 1 }; + } + break; + case GGML_OP_COUNT_EQUAL_MASKED: + { + const uint32_t n_elements = ggml_nelements(src0); + const uint32_t chunk_size = 512; + elements = { CEIL_DIV(n_elements, chunk_size), 1, 1 }; + } + break; case GGML_OP_RMS_NORM: if (ctx->do_add_rms_partials) { // Run one element per thread, 128 threads per workgroup @@ -9157,6 +9799,7 @@ static void ggml_vk_op_f32(ggml_backend_vk_context * ctx, vk_context& subctx, co case GGML_OP_UPSCALE: case GGML_OP_UNARY: case GGML_OP_GLU: + case GGML_OP_OUT_PROD: case GGML_OP_CONV_2D_DW: { uint32_t ne = ggml_nelements(dst); @@ -9263,6 +9906,11 @@ static void ggml_vk_op_f32(ggml_backend_vk_context * ctx, vk_context& subctx, co ggml_vk_buffer_memset_async(subctx, dst_buf.buffer, dst_buf.offset, 0, dst_buf.size); ggml_vk_sync_buffers(ctx, subctx); ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { src0_buf, src1_buf, dst_buf }, pc, elements); + } else if (op == GGML_OP_COUNT_EQUAL_MASKED) { + // count_equal_masked assumes that destination buffer is initialized with zeroes + ggml_vk_buffer_memset_async(subctx, dst_buf.buffer, dst_buf.offset, 0, dst_buf.size); + ggml_vk_sync_buffers(ctx, subctx); + ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { src0_buf, src1_buf, src2_buf, dst_buf }, pc, elements); } else if (op == GGML_OP_OPT_STEP_SGD) { // OPT_STEP_SGD works on src0, it does not need dst ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { src0_buf, src1_buf, src2_buf }, pc, elements); @@ -9914,6 +10562,10 @@ static void ggml_vk_silu_back(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_vk_op_f32(ctx, subctx, src0, src1, nullptr, nullptr, dst, GGML_OP_SILU_BACK, { (uint32_t)ggml_nelements(src0), 0, 0.0f, 0.0f }); } +static void ggml_vk_geglu_back(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) { + ggml_vk_op_f32(ctx, subctx, src0, src1, nullptr, nullptr, dst, GGML_OP_GEGLU_BACK, { (uint32_t)ggml_nelements(dst), (uint32_t)dst->ne[0], 0.0f, 0.0f }); +} + static void ggml_vk_norm(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, ggml_tensor * dst) { float * op_params = (float *)dst->op_params; @@ -10145,6 +10797,92 @@ static void ggml_vk_diag_mask_inf(ggml_backend_vk_context * ctx, vk_context& sub ggml_vk_op_f32(ctx, subctx, src0, nullptr, nullptr, nullptr, dst, GGML_OP_DIAG_MASK_INF, { (uint32_t)src0->ne[0], (uint32_t)src0->ne[1], op_params[0] }); } +static void ggml_vk_cross_entropy_loss_back(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * src2, ggml_tensor * dst) { + const int64_t nclasses = src1->ne[0]; + const int64_t nrows = ggml_nrows(src1); + + ggml_vk_op_f32(ctx, subctx, src0, src1, src2, nullptr, dst, GGML_OP_CROSS_ENTROPY_LOSS_BACK, { + (uint32_t)nclasses, + (uint32_t)nrows, + 0.0f, + 0.0f + }); +} + +static void ggml_vk_op_f32_cross_entropy_loss_masked_back(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst, const vk_op_push_constants&& pc) { + const ggml_tensor * grad = dst->src[0]; // gradient of forward pass output + const ggml_tensor * logits = dst->src[1]; // logits + const ggml_tensor * labels = dst->src[2]; // targets + const ggml_tensor * mask = dst->src[3]; // mask + + GGML_ASSERT(grad->type == GGML_TYPE_F32); + GGML_ASSERT(logits->type == GGML_TYPE_F32); + GGML_ASSERT(labels->type == GGML_TYPE_F32); + GGML_ASSERT(mask->type == GGML_TYPE_F32); + GGML_ASSERT(dst->buffer != nullptr); + GGML_ASSERT(ggml_is_contiguous(grad)); + GGML_ASSERT(ggml_is_contiguous(logits)); + GGML_ASSERT(ggml_is_contiguous(labels)); + GGML_ASSERT(ggml_is_contiguous(mask)); + GGML_ASSERT(ggml_are_same_shape(logits, labels)); + GGML_ASSERT(ggml_are_same_shape(logits, dst)); + + vk_pipeline pipeline = ggml_vk_op_get_pipeline(ctx, logits, labels, mask, dst, GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK); + GGML_ASSERT(pipeline != nullptr); + + ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); + + ggml_vk_sync_buffers(ctx, subctx); + + vk_subbuffer grad_buf = ggml_vk_tensor_subbuffer(ctx, grad); + vk_subbuffer logits_buf = ggml_vk_tensor_subbuffer(ctx, logits); + vk_subbuffer labels_buf = ggml_vk_tensor_subbuffer(ctx, labels); + vk_subbuffer mask_buf = ggml_vk_tensor_subbuffer(ctx, mask); + vk_subbuffer dst_buf = ggml_vk_tensor_subbuffer(ctx, dst); + + std::array elements = { (uint32_t)ggml_nrows(logits), 1, 1 }; + + ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { + grad_buf, + logits_buf, + labels_buf, + mask_buf, + dst_buf, + }, pc, elements); +} + +static void ggml_vk_cross_entropy_loss_masked_back(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst) { + const ggml_tensor * grad = dst->src[0]; + const ggml_tensor * logits = dst->src[1]; + + const int64_t nclasses = logits->ne[0]; + const int64_t nrows = ggml_nrows(logits); + + float upstream_grad = 1.0f; + if (grad && grad->data) { + ggml_backend_tensor_get(grad, &upstream_grad, 0, sizeof(float)); + } + + ggml_vk_op_f32_cross_entropy_loss_masked_back(ctx, subctx, dst, { + (uint32_t)nclasses, + (uint32_t)nrows, + upstream_grad, + 0.0f + }); +} + +static void ggml_vk_count_equal_masked(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * predictions, const ggml_tensor * targets, const ggml_tensor * mask, ggml_tensor * dst) { + const int64_t n_elements = ggml_nelements(predictions); + const int64_t vocab_size = mask->ne[0]; + + ggml_vk_op_f32(ctx, subctx, predictions, targets, mask, nullptr, dst, GGML_OP_COUNT_EQUAL_MASKED, { + (uint32_t)n_elements, + (uint32_t)vocab_size, + 0.0f, + 0.0f + }); +} + static void ggml_vk_soft_max(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * src2, ggml_tensor * dst) { float * op_params = (float *)dst->op_params; @@ -10501,6 +11239,24 @@ static void ggml_vk_solve_tri(ggml_backend_vk_context * ctx, vk_context& subctx, }); } +static void ggml_vk_out_prod(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) { + const uint32_t src0_type_size = ggml_type_size(src0->type); + const uint32_t src1_type_size = ggml_type_size(src1->type); + const uint32_t dst_type_size = ggml_type_size(dst->type); + + const int64_t r2 = src1->ne[2] / src0->ne[2]; + const int64_t r3 = src1->ne[3] / src0->ne[3]; + + ggml_vk_op_f32(ctx, subctx, src0, src1, nullptr, nullptr, dst, GGML_OP_OUT_PROD, { + (uint32_t)ggml_nelements(dst), + (uint32_t)src0->ne[0], (uint32_t)src0->ne[1], (uint32_t)src0->ne[2],(uint32_t)src0->ne[3], (uint32_t)src0->nb[0] / src0_type_size, (uint32_t)src0->nb[1] / src0_type_size, (uint32_t)src0->nb[2] / src0_type_size, (uint32_t)src0->nb[3] / src0_type_size, + (uint32_t)src1->ne[0], (uint32_t)src1->ne[1], (uint32_t)src1->ne[2],(uint32_t)src1->ne[3], (uint32_t)src1->nb[0] / src1_type_size, (uint32_t)src1->nb[1] / src1_type_size, (uint32_t)src1->nb[2] / src1_type_size, (uint32_t)src1->nb[3] / src1_type_size, + (uint32_t) dst->ne[0], (uint32_t) dst->ne[1], (uint32_t) dst->ne[2],(uint32_t) dst->ne[3], (uint32_t) dst->nb[0] / dst_type_size, (uint32_t) dst->nb[1] / dst_type_size, (uint32_t) dst->nb[2] / dst_type_size, (uint32_t) dst->nb[3] / dst_type_size, + 0, + 0.0f, (float) r2, (int32_t) r3 + }); +} + static void ggml_vk_im2col(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) { const int32_t s0 = dst->op_params[0]; const int32_t s1 = dst->op_params[1]; @@ -10955,15 +11711,15 @@ static void ggml_vk_test_matmul(ggml_backend_vk_context * ctx, size_t m, size_t if (ctx->prealloc_split_k != nullptr) { ggml_vk_destroy_buffer(ctx->prealloc_split_k); } - ctx->prealloc_split_k = ggml_vk_create_buffer_check(ctx->device, sizeof(float) * d_ne * split_k, {vk::MemoryPropertyFlagBits::eDeviceLocal}); + ctx->prealloc_split_k = ggml_vk_create_buffer_device(ctx->device, sizeof(float) * d_ne * split_k); } } ggml_pipeline_allocate_descriptor_sets(ctx); - vk_buffer d_X = ggml_vk_create_buffer_check(ctx->device, sizeof(X_TYPE) * x_ne, {vk::MemoryPropertyFlagBits::eDeviceLocal}); - vk_buffer d_Y = ggml_vk_create_buffer_check(ctx->device, sizeof(Y_TYPE) * y_ne, {vk::MemoryPropertyFlagBits::eDeviceLocal}); - vk_buffer d_D = ggml_vk_create_buffer_check(ctx->device, sizeof(float) * d_ne, {vk::MemoryPropertyFlagBits::eDeviceLocal}); + vk_buffer d_X = ggml_vk_create_buffer_device(ctx->device, sizeof(X_TYPE) * x_ne); + vk_buffer d_Y = ggml_vk_create_buffer_device(ctx->device, sizeof(Y_TYPE) * y_ne); + vk_buffer d_D = ggml_vk_create_buffer_device(ctx->device, sizeof(float) * d_ne); X_TYPE* x = (X_TYPE *) malloc(sizeof(X_TYPE) * x_ne); Y_TYPE* y = (Y_TYPE *) malloc(sizeof(Y_TYPE) * y_ne); @@ -11189,8 +11945,8 @@ static void ggml_vk_test_dequant(ggml_backend_vk_context * ctx, size_t ne, ggml_ const size_t qx_sz = ne * ggml_type_size(quant)/ggml_blck_size(quant); float * x = (float *) malloc(x_sz); void * qx = malloc(qx_sz); - vk_buffer qx_buf = ggml_vk_create_buffer_check(ctx->device, qx_sz, {vk::MemoryPropertyFlagBits::eDeviceLocal}); - vk_buffer x_buf = ggml_vk_create_buffer_check(ctx->device, x_sz_f16, {vk::MemoryPropertyFlagBits::eDeviceLocal}); + vk_buffer qx_buf = ggml_vk_create_buffer_device(ctx->device, qx_sz); + vk_buffer x_buf = ggml_vk_create_buffer_device(ctx->device, x_sz_f16); float * x_ref = (float *) malloc(x_sz); ggml_fp16_t * x_chk = (ggml_fp16_t *) malloc(x_sz_f16); @@ -11291,8 +12047,8 @@ static void ggml_vk_test_dequant(ggml_backend_vk_context * ctx, size_t ne, ggml_ // float * x = (float *) malloc(x_sz); // block_q8_1 * qx = (block_q8_1 *)malloc(qx_sz); // block_q8_1 * qx_res = (block_q8_1 *)malloc(qx_sz); -// vk_buffer x_buf = ggml_vk_create_buffer_check(ctx->device, x_sz, {vk::MemoryPropertyFlagBits::eDeviceLocal}); -// vk_buffer qx_buf = ggml_vk_create_buffer_check(ctx->device, qx_sz, {vk::MemoryPropertyFlagBits::eDeviceLocal}); +// vk_buffer x_buf = ggml_vk_create_buffer_device(ctx->device, x_sz, {vk::MemoryPropertyFlagBits::eDeviceLocal}); +// vk_buffer qx_buf = ggml_vk_create_buffer_device(ctx->device, qx_sz, {vk::MemoryPropertyFlagBits::eDeviceLocal}); // // for (size_t i = 0; i < ne; i++) { // x[i] = rand() / (float)RAND_MAX; @@ -11435,10 +12191,10 @@ static void ggml_vk_test_dequant_matmul(ggml_backend_vk_context * ctx, size_t m, float * x = (float *) malloc(x_sz); float * y = (float *) malloc(y_sz); void * qx = malloc(qx_sz); - vk_buffer qx_buf = ggml_vk_create_buffer_check(ctx->device, qx_sz, {vk::MemoryPropertyFlagBits::eDeviceLocal}); - vk_buffer y_buf = ggml_vk_create_buffer_check(ctx->device, y_sz, {vk::MemoryPropertyFlagBits::eDeviceLocal}); - vk_buffer qy_buf = ggml_vk_create_buffer_check(ctx->device, qy_sz, {vk::MemoryPropertyFlagBits::eDeviceLocal}); - vk_buffer d_buf = ggml_vk_create_buffer_check(ctx->device, d_sz, {vk::MemoryPropertyFlagBits::eDeviceLocal}); + vk_buffer qx_buf = ggml_vk_create_buffer_device(ctx->device, qx_sz); + vk_buffer y_buf = ggml_vk_create_buffer_device(ctx->device, y_sz); + vk_buffer qy_buf = ggml_vk_create_buffer_device(ctx->device, qy_sz); + vk_buffer d_buf = ggml_vk_create_buffer_device(ctx->device, d_sz); float * d = (float *) malloc(d_sz); float * d_chk = (float *) malloc(d_sz); @@ -11465,7 +12221,7 @@ static void ggml_vk_test_dequant_matmul(ggml_backend_vk_context * ctx, size_t m, if (ctx->prealloc_split_k != nullptr) { ggml_vk_destroy_buffer(ctx->prealloc_split_k); } - ctx->prealloc_split_k = ggml_vk_create_buffer_check(ctx->device, sizeof(float) * d_ne * split_k, {vk::MemoryPropertyFlagBits::eDeviceLocal}); + ctx->prealloc_split_k = ggml_vk_create_buffer_device(ctx->device, sizeof(float) * d_ne * split_k); } } if (mmq) { @@ -11740,6 +12496,14 @@ static void ggml_vk_preallocate_buffers(ggml_backend_vk_context * ctx, vk_contex } ctx->prealloc_add_rms_partials = ggml_vk_create_buffer_device(ctx->device, ctx->prealloc_size_add_rms_partials); } + if (ctx->prealloc_tile == nullptr || (ctx->prealloc_size_tile > 0 && ctx->prealloc_tile->size < ctx->prealloc_size_tile)) { + VK_LOG_MEMORY("ggml_vk_preallocate_buffers(tile_size: " << ctx->prealloc_size_tile << ")"); + // Resize buffer + if (ctx->prealloc_tile != nullptr) { + ggml_vk_destroy_buffer(ctx->prealloc_tile); + } + ctx->prealloc_tile = ggml_vk_create_buffer_device(ctx->device, ctx->prealloc_size_tile); + } } static void ggml_vk_compute_forward(ggml_backend_vk_context* ctx, ggml_cgraph * cgraph, ggml_tensor* tensor, int tensor_idx, bool almost_ready); @@ -11900,6 +12664,10 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr case GGML_OP_GET_ROWS: ggml_vk_get_rows(ctx, compute_ctx, src0, src1, node); + break; + case GGML_OP_OUT_PROD: + ggml_vk_out_prod(ctx, compute_ctx, src0, src1, node); + break; case GGML_OP_ADD: if (ctx->num_additional_fused_ops) { @@ -11997,6 +12765,10 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr case GGML_OP_SILU_BACK: ggml_vk_silu_back(ctx, compute_ctx, src0, src1, node); + break; + case GGML_OP_GEGLU_BACK: + ggml_vk_geglu_back(ctx, compute_ctx, src0, src1, node); + break; case GGML_OP_NORM: ggml_vk_norm(ctx, compute_ctx, src0, node); @@ -12061,6 +12833,16 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr ggml_vk_diag_mask_inf(ctx, compute_ctx, src0, node); break; + + case GGML_OP_CROSS_ENTROPY_LOSS_BACK: + ggml_vk_cross_entropy_loss_back(ctx, compute_ctx, src0, src1, src2, node); + break; + case GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK: + ggml_vk_cross_entropy_loss_masked_back(ctx, compute_ctx, node); + break; + case GGML_OP_COUNT_EQUAL_MASKED: + ggml_vk_count_equal_masked(ctx, compute_ctx, src0, src1, src2, node); + break; case GGML_OP_SOFT_MAX: if (ctx->num_additional_fused_ops) { ggml_vk_topk_moe(ctx, compute_ctx, cgraph, node_idx); @@ -12328,6 +13110,7 @@ static void ggml_vk_cleanup(ggml_backend_vk_context * ctx) { ggml_vk_destroy_buffer(ctx->prealloc_split_k); ggml_vk_destroy_buffer(ctx->prealloc_add_rms_partials); ggml_vk_destroy_buffer(ctx->sync_staging); + ggml_vk_destroy_buffer(ctx->prealloc_tile); ctx->prealloc_y_last_pipeline_used = nullptr; @@ -12533,7 +13316,6 @@ static void ggml_backend_vk_host_buffer_free_buffer(ggml_backend_buffer_t buffer static ggml_backend_buffer_t ggml_backend_vk_host_buffer_type_alloc_buffer(ggml_backend_buffer_type_t buft, size_t size) { VK_LOG_MEMORY("ggml_backend_vk_host_buffer_type_alloc_buffer(" << size << ")"); - size += 32; // Behave like the CPU buffer type void * ptr = nullptr; try { ptr = ggml_vk_host_malloc(vk_instance.devices[0], size); @@ -12669,7 +13451,7 @@ static void ggml_backend_vk_get_tensor_async(ggml_backend_t backend, const ggml_ buffer_cpy.size = size; transfer_ctx->s->buffer.copyBuffer(buf->buffer, ctx->sync_staging->buffer, { buffer_cpy }); - deferred_memcpy(data, ctx->sync_staging->ptr, size, &transfer_ctx->out_memcpys); + deferred_memcpy(data, ctx->sync_staging->info.pMappedData, size, &transfer_ctx->out_memcpys); ggml_vk_synchronize(ctx); } } @@ -13351,6 +14133,7 @@ static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph * return node->op == GGML_OP_NONE || node->op == GGML_OP_RESHAPE || node->op == GGML_OP_TRANSPOSE || node->op == GGML_OP_VIEW || node->op == GGML_OP_PERMUTE; }; + auto const &is_src_of = [](const ggml_tensor *dst, const ggml_tensor *src) -> bool { for (uint32_t s = 0; s < GGML_MAX_SRC; ++s) { if (dst->src[s] == src) { @@ -13824,6 +14607,9 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm ggml_backend_vk_device_context * ctx = (ggml_backend_vk_device_context *)dev->context; const vk_device& device = ggml_vk_get_device(ctx->device); if (op->op == GGML_OP_MUL_MAT_ID) { + if (src0_type == GGML_TYPE_TQ2_0) { + return false; + } if (!device->mul_mat_id_s[src0_type] && !device->mul_mat_id_m[src0_type] && !device->mul_mat_id_l[src0_type]) { // If there's not enough shared memory for row_ids and the result tile, fallback to CPU return false; @@ -13838,6 +14624,9 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm case GGML_TYPE_Q5_0: case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: + case GGML_TYPE_TQ2_0: + case GGML_TYPE_TQ1_0: + case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -14072,6 +14861,24 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm case GGML_OP_GROUP_NORM: case GGML_OP_L2_NORM: return ggml_is_contiguous(op->src[0]); + case GGML_OP_OUT_PROD: { + const ggml_type t0 = op->src[0]->type; + const ggml_type t1 = op->src[1]->type; + const ggml_type td = op->type; + if (td != GGML_TYPE_F32 || t1 != GGML_TYPE_F32) { + return false; + } + switch (t0) { + case GGML_TYPE_F32: + case GGML_TYPE_F16: + case GGML_TYPE_Q4_0: + case GGML_TYPE_Q8_0: + case GGML_TYPE_TQ2_0: + return true; + default: + return false; + } + } case GGML_OP_ADD: case GGML_OP_SUB: case GGML_OP_MUL: @@ -14083,18 +14890,17 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm return op->src[0]->type == GGML_TYPE_F32 && op->src[1]->type == GGML_TYPE_F32 && op->src[2]->type == GGML_TYPE_I32 && op->type == GGML_TYPE_F32; case GGML_OP_SILU_BACK: + case GGML_OP_GEGLU_BACK: case GGML_OP_RMS_NORM_BACK: - return ggml_is_contiguous(op->src[0]) && op->src[0]->type == GGML_TYPE_F32; case GGML_OP_SQR: case GGML_OP_SQRT: case GGML_OP_SIN: case GGML_OP_COS: case GGML_OP_CLAMP: - return op->src[0]->type == GGML_TYPE_F32; case GGML_OP_LEAKY_RELU: case GGML_OP_OPT_STEP_ADAMW: case GGML_OP_OPT_STEP_SGD: - return ggml_is_contiguous(op->src[0]) && op->src[0]->type == GGML_TYPE_F32; + return op->src[0]->type == GGML_TYPE_F32; case GGML_OP_LOG: case GGML_OP_TRI: return (op->src[0]->type == GGML_TYPE_F32 || op->src[0]->type == GGML_TYPE_F16) && @@ -14143,18 +14949,18 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm case GGML_OP_FILL: return op->type == GGML_TYPE_F32; case GGML_OP_SCALE: - return ggml_is_contiguous(op->src[0]) && op->src[0]->type == GGML_TYPE_F32; + return op->src[0]->type == GGML_TYPE_F32; case GGML_OP_PAD: case GGML_OP_ROLL: return op->src[0]->type == GGML_TYPE_F32; case GGML_OP_DIAG_MASK_INF: - return ggml_is_contiguous(op->src[0]) && op->src[0]->type == GGML_TYPE_F32; + return op->src[0]->type == GGML_TYPE_F32; case GGML_OP_SOFT_MAX: - return ggml_is_contiguous(op->src[0]) && op->src[0]->type == GGML_TYPE_F32 + return op->src[0]->type == GGML_TYPE_F32 && (!op->src[1] || (op->src[1]->type == GGML_TYPE_F32 || op->src[1]->type == GGML_TYPE_F16)); case GGML_OP_SOFT_MAX_BACK: - return ggml_is_contiguous(op->src[0]) && op->src[0]->type == GGML_TYPE_F32 - && ggml_is_contiguous(op->src[1]) && op->src[1]->type == GGML_TYPE_F32; + return op->src[0]->type == GGML_TYPE_F32 + && op->src[1]->type == GGML_TYPE_F32; case GGML_OP_SUM: case GGML_OP_SUM_ROWS: case GGML_OP_MEAN: @@ -14252,6 +15058,8 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm return op->src[0]->type == GGML_TYPE_F32; case GGML_OP_CONV_TRANSPOSE_1D: return op->src[0]->type == GGML_TYPE_F32 && op->src[1]->type == GGML_TYPE_F32; + case GGML_OP_CROSS_ENTROPY_LOSS_BACK: + return op->src[0]->type == GGML_TYPE_F32 && op->src[1]->type == GGML_TYPE_F32 && op->src[2]->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32; case GGML_OP_CONV_2D: case GGML_OP_CONV_TRANSPOSE_2D: { @@ -14270,6 +15078,10 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm ggml_is_contiguous(op->src[1]) && ggml_is_contiguous(op)); } + case GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK: + return op->src[0]->type == GGML_TYPE_F32 && op->src[1]->type == GGML_TYPE_F32 && op->src[2]->type == GGML_TYPE_F32 && op->src[3]->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32; + case GGML_OP_COUNT_EQUAL_MASKED: + return op->src[0]->type == GGML_TYPE_I32 && op->src[1]->type == GGML_TYPE_I32 && op->src[2]->type == GGML_TYPE_F32 && op->type == GGML_TYPE_I64; default: return false; } @@ -14563,7 +15375,7 @@ size_t comp_nb[GGML_MAX_DIMS]; size_t check_counter = 0; static void ggml_vk_check_results_0(ggml_backend_vk_context * ctx, ggml_cgraph * cgraph, int tensor_idx) { ggml_tensor * tensor = cgraph->nodes[tensor_idx + ctx->num_additional_fused_ops]; - if (tensor->op == GGML_OP_TRANSPOSE || tensor->op == GGML_OP_SET_ROWS) { + if (tensor->op == GGML_OP_COUNT_EQUAL_MASKED || tensor->op == GGML_OP_TRANSPOSE || tensor->op == GGML_OP_SET_ROWS || tensor->op == GGML_OP_ARGMAX || tensor->op == GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK) { return; } @@ -14691,7 +15503,7 @@ static void ggml_vk_check_results_0(ggml_backend_vk_context * ctx, ggml_cgraph * } else if (tensor->op == GGML_OP_LOG) { tensor_clone = ggml_log(ggml_ctx, src_clone[0]); } else if (tensor->op == GGML_OP_TRI) { - tensor_clone = ggml_tri(ggml_ctx, src_clone[0], ggml_get_op_params_i32(tensor, 0)); + tensor_clone = ggml_tri(ggml_ctx, src_clone[0], (ggml_tri_type) ggml_get_op_params_i32(tensor, 0)); } else if (tensor->op == GGML_OP_CLAMP) { const float * params = (const float *)tensor->op_params; tensor_clone = ggml_clamp(ggml_ctx, src_clone[0], params[0], params[1]); @@ -14704,6 +15516,8 @@ static void ggml_vk_check_results_0(ggml_backend_vk_context * ctx, ggml_cgraph * tensor_clone = ggml_repeat_back(ggml_ctx, src_clone[0], tensor); } else if (tensor->op == GGML_OP_ADD) { tensor_clone = ggml_add(ggml_ctx, src_clone[0], src_clone[1]); + } else if (tensor->op == GGML_OP_OUT_PROD) { + tensor_clone = ggml_out_prod(ggml_ctx, src_clone[0], src_clone[1]); } else if (tensor->op == GGML_OP_ACC) { tensor_clone = ggml_acc(ggml_ctx, src_clone[0], src_clone[1], tensor->op_params[0], tensor->op_params[1], tensor->op_params[2], tensor->op_params[3]); } else if (tensor->op == GGML_OP_NORM) { @@ -14988,7 +15802,7 @@ static void ggml_vk_check_results_0(ggml_backend_vk_context * ctx, ggml_cgraph * static void ggml_vk_check_results_1(ggml_backend_vk_context * ctx, ggml_cgraph * cgraph, int tensor_idx) { ggml_tensor * tensor = cgraph->nodes[tensor_idx + ctx->num_additional_fused_ops]; - if (tensor->op == GGML_OP_TRANSPOSE || tensor->op == GGML_OP_SET_ROWS) { + if (tensor->op == GGML_OP_COUNT_EQUAL_MASKED || tensor->op == GGML_OP_TRANSPOSE || tensor->op == GGML_OP_SET_ROWS || tensor->op == GGML_OP_ARGMAX || tensor->op == GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK) { return; } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/CMakeLists.txt b/ggml/src/ggml-vulkan/vulkan-shaders/CMakeLists.txt index e1f613fb4f68..2e79138c58f4 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/CMakeLists.txt +++ b/ggml/src/ggml-vulkan/vulkan-shaders/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.19) project("vulkan-shaders-gen" C CXX) +option(GGML_VULKAN_BUILD_ADRENO_SHADERS "Build Adreno-specific shader variants" ON) + find_package (Threads REQUIRED) if (GGML_VULKAN_COOPMAT_GLSLC_SUPPORT) @@ -23,6 +25,10 @@ if (GGML_VULKAN_SHADER_DEBUG_INFO) add_compile_definitions(GGML_VULKAN_SHADER_DEBUG_INFO) message(STATUS "Enabling shader debug info") endif() +if (GGML_VULKAN_BUILD_ADRENO_SHADERS) + add_compile_definitions(GGML_VULKAN_BUILD_ADRENO_SHADERS) + message(STATUS "Building Adreno-specific shaders") +endif() set(TARGET vulkan-shaders-gen) add_executable(${TARGET} vulkan-shaders-gen.cpp) diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/count_equal_masked.comp b/ggml/src/ggml-vulkan/vulkan-shaders/count_equal_masked.comp new file mode 100644 index 000000000000..fcb55402c7d1 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/count_equal_masked.comp @@ -0,0 +1,46 @@ +#version 450 + +#extension GL_EXT_control_flow_attributes : enable +#extension GL_EXT_shader_atomic_int64 : enable + +#include "types.glsl" +#include "generic_head.glsl" + +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +layout (binding = 0) readonly buffer X {A_TYPE data_a[];}; // predictions +layout (binding = 1) readonly buffer Y {B_TYPE data_b[];}; // targets +layout (binding = 2) readonly buffer M {C_TYPE data_m[];}; // masks (float, 1.0 for positions to count, 0.0 to skip) +layout (binding = 3) buffer D {D_TYPE data_d[];}; // output count + +const uint CHUNK_SIZE = 512; + +void main() { + const uint base = gl_WorkGroupID.x * CHUNK_SIZE; + const uint col = gl_LocalInvocationID.x; + + if (gl_WorkGroupID.x == 0 && gl_LocalInvocationID.x == 0) { + data_d[0] = D_TYPE(0); + } + + barrier(); + + uint count = 0; + [[unroll]] + for (uint i = 0; i < CHUNK_SIZE; i += gl_WorkGroupSize.x) { + const uint idx = base + i + col; + if (idx >= p.KX) { + break; + } + + uint position = idx; + uint mask_offset = 0 + position * p.KY + 0; + float mask_value = data_m[mask_offset]; + + if (mask_value > 0.5 && data_a[idx] == data_b[idx]) { + count += 1; + } + } + + atomicAdd(data_d[0], D_TYPE(count)); +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/cross_entropy_loss_back.comp b/ggml/src/ggml-vulkan/vulkan-shaders/cross_entropy_loss_back.comp new file mode 100644 index 000000000000..ba913a08ac68 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/cross_entropy_loss_back.comp @@ -0,0 +1,92 @@ +#version 450 + +#extension GL_EXT_control_flow_attributes : enable + +#include "generic_head.glsl" +#include "types.glsl" + +#define FLOAT_TYPE float + +layout(constant_id = 0) const uint BLOCK_SIZE = 32; +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +layout (binding = 0) readonly buffer A {A_TYPE data_a[];}; // Grad(scalar) +layout (binding = 1) readonly buffer B {B_TYPE data_b[];}; // logits => raw model outputs(unnormalized scored) +layout (binding = 2) readonly buffer C {C_TYPE data_c[];}; // true labels(one hot encoded) +layout (binding = 3) writeonly buffer D {D_TYPE data_d[];}; // output gradients + +shared FLOAT_TYPE vals[BLOCK_SIZE]; + +void main() { + const uint nclasses = p.KX; + const uint nrows = p.KY; + + const uint row = gl_WorkGroupID.z * 262144 + gl_WorkGroupID.y * 512 + gl_WorkGroupID.x; + if (row >= nrows) { + return; + } + + const uint tid = gl_LocalInvocationID.x; + const uint warp_size = gl_WorkGroupSize.x; + + const uint logits_offset = row * nclasses; + const uint labels_offset = row * nclasses; + const uint dst_offset = row * nclasses; + + // Gradient scaling (grad / batch_size) + const FLOAT_TYPE d_by_nrows = FLOAT_TYPE(data_a[0]) / FLOAT_TYPE(nrows); + + // Get max value per thread + FLOAT_TYPE thread_max = FLOAT_TYPE(uintBitsToFloat(0xFF800000)); // -INFINITY + for (uint i = tid; i < nclasses; i += warp_size) { + FLOAT_TYPE val = FLOAT_TYPE(data_b[logits_offset + i]); + thread_max = max(thread_max, val); + } + + vals[tid] = thread_max; + barrier(); + + // Get global maximum for the row(batch) + [[unroll]] + for (uint s = warp_size / 2; s > 0; s >>= 1) { + if (tid < s) { + vals[tid] = max(vals[tid], vals[tid + s]); + } + barrier(); + } + + const FLOAT_TYPE row_max = vals[0]; + barrier(); + + // Compute sum of exp(logits - max) for softmax normalization + FLOAT_TYPE thread_sum = FLOAT_TYPE(0.0); + for (uint i = tid; i < nclasses; i += warp_size) { + FLOAT_TYPE val = FLOAT_TYPE(data_b[logits_offset + i]); + thread_sum += exp(val - row_max); + } + + vals[tid] = thread_sum; + barrier(); + + [[unroll]] + for (uint s = warp_size / 2; s > 0; s >>= 1) { + if (tid < s) { + vals[tid] += vals[tid + s]; + } + barrier(); + } + + const FLOAT_TYPE row_sum = vals[0]; + const FLOAT_TYPE sm_scale = FLOAT_TYPE(1.0) / row_sum; + barrier(); + + // Compute final gradients: (softmax - labels) * d_by_nrows + for (uint i = tid; i < nclasses; i += warp_size) { + FLOAT_TYPE logit = FLOAT_TYPE(data_b[logits_offset + i]); + FLOAT_TYPE softmax_val = exp(logit - row_max) * sm_scale; + + FLOAT_TYPE label = FLOAT_TYPE(data_c[labels_offset + i]); + + data_d[dst_offset + i] = D_TYPE((softmax_val - label) * d_by_nrows); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/cross_entropy_loss_masked_back.comp b/ggml/src/ggml-vulkan/vulkan-shaders/cross_entropy_loss_masked_back.comp new file mode 100644 index 000000000000..f1542892ea9b --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/cross_entropy_loss_masked_back.comp @@ -0,0 +1,115 @@ +#version 450 + +#extension GL_EXT_control_flow_attributes : enable + +#include "generic_head.glsl" +#include "types.glsl" + +#define FLOAT_TYPE float + +layout(constant_id = 0) const uint BLOCK_SIZE = 32; +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +layout (binding = 0) readonly buffer A {A_TYPE data_a[];}; +layout (binding = 1) readonly buffer B {B_TYPE data_b[];}; +layout (binding = 2) readonly buffer C {C_TYPE data_c[];}; +layout (binding = 3) readonly buffer D {E_TYPE data_d[];}; +layout (binding = 4) writeonly buffer E {D_TYPE data_e[];}; + +shared FLOAT_TYPE vals[BLOCK_SIZE]; + +void main() { + const uint nclasses = p.KX; + const uint nrows = p.KY; + + const uint row = gl_WorkGroupID.x; + if (row >= nrows) { + return; + } + + const uint tid = gl_LocalInvocationID.x; + const uint warp_size = gl_WorkGroupSize.x; + + const uint logits_offset = row * nclasses; + const uint labels_offset = row * nclasses; + const uint dst_offset = row * nclasses; + + const float mask_value = data_d[row * nclasses + 0]; + + if (mask_value <= 0.5) { + for (uint i = tid; i < nclasses; i += warp_size) { + data_e[dst_offset + i] = D_TYPE(0.0); + } + return; + } + + FLOAT_TYPE d_by_valid; + if (tid == 0) { + FLOAT_TYPE valid_tokens = FLOAT_TYPE(0.0); + for (uint r = 0; r < nrows; r++) { + const float mask_val = data_d[r * nclasses + 0]; + if (mask_val > 0.5) { + valid_tokens += FLOAT_TYPE(1.0); + } + } + + const FLOAT_TYPE upstream_grad = FLOAT_TYPE(p.param1); + d_by_valid = valid_tokens > 0.0 ? upstream_grad / valid_tokens : FLOAT_TYPE(0.0); + + vals[0] = d_by_valid; + } + barrier(); + + d_by_valid = vals[0]; + + FLOAT_TYPE thread_max = FLOAT_TYPE(uintBitsToFloat(0xFF800000)); // -INFINITY + for (uint i = tid; i < nclasses; i += warp_size) { + FLOAT_TYPE val = FLOAT_TYPE(data_b[logits_offset + i]); + thread_max = max(thread_max, val); + } + + vals[tid] = thread_max; + barrier(); + + [[unroll]] + for (uint s = warp_size / 2; s > 0; s >>= 1) { + if (tid < s) { + vals[tid] = max(vals[tid], vals[tid + s]); + } + barrier(); + } + + const FLOAT_TYPE row_max = vals[0]; + barrier(); + + FLOAT_TYPE thread_sum = FLOAT_TYPE(0.0); + for (uint i = tid; i < nclasses; i += warp_size) { + FLOAT_TYPE val = FLOAT_TYPE(data_b[logits_offset + i]); + thread_sum += exp(val - row_max); + } + + vals[tid] = thread_sum; + barrier(); + + [[unroll]] + for (uint s = warp_size / 2; s > 0; s >>= 1) { + if (tid < s) { + vals[tid] += vals[tid + s]; + } + barrier(); + } + + const FLOAT_TYPE row_sum = vals[0]; + const FLOAT_TYPE sm_scale = FLOAT_TYPE(1.0) / row_sum; + barrier(); + + for (uint i = tid; i < nclasses; i += warp_size) { + FLOAT_TYPE logit = FLOAT_TYPE(data_b[logits_offset + i]); + FLOAT_TYPE softmax_val = exp(logit - row_max) * sm_scale; + + FLOAT_TYPE label = FLOAT_TYPE(data_c[labels_offset + i]); + FLOAT_TYPE gradient = (softmax_val - label) * d_by_valid; + + data_e[dst_offset + i] = D_TYPE(gradient); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl index 70ee542d9695..d9aa9daa2542 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl @@ -81,8 +81,13 @@ vec2 dequantize(uint ib, uint iqs, uint a_offset) { return vec2(int(data_a[a_offset + ib].qs[iqs]), int(data_a[a_offset + ib].qs[iqs + 1])); } vec4 dequantize4(uint ib, uint iqs, uint a_offset) { +#if defined(ADRENO) + const vec2 v0 = dequantize(ib, iqs, a_offset); + const vec2 v1 = dequantize(ib, iqs + 2, a_offset); +#else const i8vec2 v0 = unpack8(int32_t(data_a_packed16[a_offset + ib].qs[iqs/2])).xy; // vec4 used due to #12147 const i8vec2 v1 = unpack8(int32_t(data_a_packed16[a_offset + ib].qs[iqs/2 + 1])).xy; +#endif return vec4(v0.x, v0.y, v1.x, v1.y); } #endif @@ -427,6 +432,49 @@ vec4 dequantize4(uint ib, uint iqs, uint a_offset) { } #endif +#if defined(DATA_A_TQ1_0) +float dequantize1(uint ib, uint iqs, uint a_offset) { + const uint pow3[6] = uint[6](1u, 3u, 9u, 27u, 81u, 243u); + + if (iqs < 160u) { // 5 trits per byte (160 elems) + const uint n = iqs / 32u; + const uint m = iqs % 32u; + const uint byte = m; + const uint q = uint(data_a[a_offset + ib].qs[byte]); + const uint xi = (((q * pow3[n]) & 255u) * 3u) >> 8; + return float(int(xi) - 1); + } else if (iqs < 240u) { // 5 trits per byte (80 elems) + const uint e = iqs - 160u; + const uint n = e / 16u; + const uint m = e % 16u; + const uint byte = 32u + m; + const uint q = uint(data_a[a_offset + ib].qs[byte]); + const uint xi = (((q * pow3[n]) & 255u) * 3u) >> 8; + return float(int(xi) - 1); + } else { // 4 trits per byte (16 elems) + const uint e = iqs - 240u; + const uint n = e / (QUANT_K / 64u); + const uint j = e % (QUANT_K / 64u); + const uint q = uint(data_a[a_offset + ib].qh[j]); + const uint xi = (((q * pow3[n]) & 255u) * 3u) >> 8; + return float(int(xi) - 1); + } +} + +vec2 dequantize(uint ib, uint iqs, uint a_offset) { + return vec2(dequantize1(ib, iqs, a_offset), dequantize1(ib, iqs + 1u, a_offset)); +} + +vec4 dequantize4(uint ib, uint iqs, uint a_offset) { + return vec4( + dequantize1(ib, iqs + 0u, a_offset), + dequantize1(ib, iqs + 1u, a_offset), + dequantize1(ib, iqs + 2u, a_offset), + dequantize1(ib, iqs + 3u, a_offset) + ); +} +#endif + #if defined(DATA_A_MXFP4) vec2 dequantize(uint ib, uint iqs, uint a_offset) { const uint vui = uint(data_a[a_offset + ib].qs[iqs]); @@ -439,6 +487,22 @@ vec4 dequantize4(uint ib, uint iqs, uint a_offset) { } #endif +#if defined(DATA_A_TQ2_0) +#include "tq_utils.comp" + +vec2 dequantize(uint ib, uint iqs, uint a_offset) { + return vec2(tq2_dequantize(ib + a_offset, iqs), tq2_dequantize(ib + a_offset, iqs + 1)); +} +vec4 dequantize4(uint ib, uint iqs, uint a_offset) { + return vec4( + tq2_dequantize(ib + a_offset, iqs + 0), + tq2_dequantize(ib + a_offset, iqs + 1), + tq2_dequantize(ib + a_offset, iqs + 2), + tq2_dequantize(ib + a_offset, iqs + 3) + ); +} +#endif + #if defined(DATA_A_F32) || defined(DATA_A_F16) || defined(DATA_A_BF16) vec2 get_dm(uint ib, uint a_offset) { return vec2(0, 0); @@ -454,7 +518,7 @@ vec2 get_dm(uint ib, uint a_offset) { } #endif -#if defined(DATA_A_Q4_0) || defined(DATA_A_Q5_0) || defined(DATA_A_Q8_0) || defined(DATA_A_IQ1_S) || defined(DATA_A_IQ2_XXS) || defined(DATA_A_IQ2_XS) || defined(DATA_A_IQ2_S) || defined(DATA_A_IQ3_XXS) || defined(DATA_A_IQ3_S) || defined(DATA_A_IQ4_XS) || defined(DATA_A_IQ4_NL) +#if defined(DATA_A_Q4_0) || defined(DATA_A_Q5_0) || defined(DATA_A_Q8_0) || defined(DATA_A_TQ2_0) || defined(DATA_A_TQ1_0) || defined(DATA_A_IQ1_S) || defined(DATA_A_IQ2_XXS) || defined(DATA_A_IQ2_XS) || defined(DATA_A_IQ2_S) || defined(DATA_A_IQ3_XXS) || defined(DATA_A_IQ3_S) || defined(DATA_A_IQ4_XS) || defined(DATA_A_IQ4_NL) vec2 get_dm(uint ib, uint a_offset) { return vec2(float(data_a[a_offset + ib].d), 0); } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs_cm2.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs_cm2.glsl index 8ac6482dc944..ad6975a2702c 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs_cm2.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs_cm2.glsl @@ -666,6 +666,62 @@ float16_t dequantFuncIQ4_NL(const in decodeBufIQ4_NL bl, const in uint blockCoor } #endif +#if defined(DATA_A_TQ2_0) +layout(buffer_reference, std430, buffer_reference_align = 2) buffer decodeBufTQ2_0 { + block_tq2_0 block; +}; + +#define TQ2_CM2 1 +#include "tq_utils.comp" +#undef TQ2_CM2 + +float16_t dequantFuncTQ2_0(const in decodeBufTQ2_0 bl, const in uint blockCoords[2], const in uint coordInBlock[2]) +{ + const float16_t d = bl.block.d; + const uint idx = coordInBlock[1]; + + const int val = tq2_dequantize(bl, idx); + + return d * float16_t(val); +} +#endif + +#if defined(DATA_A_TQ1_0) +layout(buffer_reference, std430, buffer_reference_align = 2) buffer decodeBufTQ1_0 { + block_tq1_0 block; +}; + +float16_t dequantFuncTQ1_0(const in decodeBufTQ1_0 bl, const in uint blockCoords[2], const in uint coordInBlock[2]) +{ + const float16_t d = bl.block.d; + const uint idx = coordInBlock[1]; + + const uint pow3[6] = uint[6](1u, 3u, 9u, 27u, 81u, 243u); + + uint xi; + if (idx < 160u) { + const uint n = idx / 32u; + const uint m = idx % 32u; + const uint q = uint(bl.block.qs[m]); + xi = (((q * pow3[n]) & 255u) * 3u) >> 8u; + } else if (idx < 240u) { + const uint ee = idx - 160u; + const uint n = ee / 16u; + const uint m = ee % 16u; + const uint q = uint(bl.block.qs[32u + m]); + xi = (((q * pow3[n]) & 255u) * 3u) >> 8u; + } else { + const uint ee = idx - 240u; + const uint n = ee / 4u; + const uint j = ee % 4u; + const uint q = uint(bl.block.qh[j]); + xi = (((q * pow3[n]) & 255u) * 3u) >> 8u; + } + + return d * float16_t(float(xi) - 1.0f); +} +#endif + #if defined(DATA_A_MXFP4) layout(buffer_reference, std430, buffer_reference_align = 2) buffer decodeBufMXFP4 { block_mxfp4 block; @@ -695,6 +751,8 @@ float16_t dequantFuncMXFP4(const in decodeBufMXFP4 bl, const in uint blockCoords #define dequantFuncA dequantFuncQ5_1 #elif defined(DATA_A_Q8_0) #define dequantFuncA dequantFuncQ8_0 +#elif defined(DATA_A_TQ2_0) +#define dequantFuncA dequantFuncTQ2_0 #elif defined(DATA_A_Q2_K) #define dequantFuncA dequantFuncQ2_K #elif defined(DATA_A_Q3_K) @@ -727,6 +785,10 @@ float16_t dequantFuncMXFP4(const in decodeBufMXFP4 bl, const in uint blockCoords #define dequantFuncA dequantFuncIQ4_XS #elif defined(DATA_A_IQ4_NL) #define dequantFuncA dequantFuncIQ4_NL +#elif defined(DATA_A_TQ2_0) +#define dequantFuncA dequantFuncTQ2_0 +#elif defined(DATA_A_TQ1_0) +#define dequantFuncA dequantFuncTQ1_0 #elif defined(DATA_A_MXFP4) #define dequantFuncA dequantFuncMXFP4 #elif defined(DATA_A_F32) diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_tq1_0.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_tq1_0.comp new file mode 100644 index 000000000000..fb486dc2f9b0 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_tq1_0.comp @@ -0,0 +1,52 @@ +#version 450 + +#extension GL_EXT_shader_16bit_storage : require + +#include "types.glsl" + +layout (binding = 0) readonly buffer A {A_TYPE data_a[];}; +layout (binding = 1) writeonly buffer D {D_TYPE data_b[];}; + +layout (push_constant) uniform parameter { + uint ne; +} p; + +layout (local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +void main() { + const uint i = gl_GlobalInvocationID.x * 4; + + if (i >= p.ne) { + return; + } + + const uint ib = i / QUANT_K; + const float d = float(data_a[ib].d); + const uint pow3[5] = uint[5](1u, 3u, 9u, 27u, 81u); + + for (uint j = 0; j < 4 && (i + j) < p.ne; ++j) { + const uint e = (i + j) % QUANT_K; + + uint qbyte; + uint t; + if (e < 160u) { + const uint m = e % 32u; + t = e / 32u; + qbyte = uint(data_a[ib].qs[m]); + } else if (e < 240u) { + const uint e2 = e - 160u; + const uint m = e2 % 16u; + t = e2 / 16u; + qbyte = uint(data_a[ib].qs[32u + m]); + } else { + const uint e3 = e - 240u; + const uint m = e3 % 4u; + t = e3 / 4u; + qbyte = uint(data_a[ib].qh[m]); + } + + const uint q = qbyte * pow3[t]; + const uint xi = (q * 3u) >> 8; + data_b[i + j] = D_TYPE(d * (float(xi) - 1.0f)); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_tq2_0.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_tq2_0.comp new file mode 100644 index 000000000000..15664d4f03ae --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_tq2_0.comp @@ -0,0 +1,26 @@ +#version 450 + +#include "dequant_head.glsl" + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout (binding = 0) readonly buffer A {A_TYPE data_a[];}; +layout (binding = 1) writeonly buffer D {D_TYPE data_b[];}; + +#include "tq_utils.comp" + +void main() { + [[unroll]] for (uint wgy = 0; wgy < 256; wgy += gl_WorkGroupSize.x) { + const uint ib = gl_WorkGroupID.x; + + if (ib >= p.nel / QUANT_K) { + return; + } + + const uint e = wgy + gl_LocalInvocationID.x; + + const FLOAT_TYPE d = FLOAT_TYPE(data_a[ib].d); + + data_b[ib * QUANT_K + e] = D_TYPE(d * FLOAT_TYPE(tq2_dequantize(ib, e))); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/geglu_back.comp b/ggml/src/ggml-vulkan/vulkan-shaders/geglu_back.comp new file mode 100644 index 000000000000..89b8de1e20ad --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/geglu_back.comp @@ -0,0 +1,37 @@ +#version 450 + +#include "generic_head.glsl" +#include "types.glsl" + +#extension GL_EXT_control_flow_attributes : enable + +layout(local_size_x = 512, local_size_y = 1, local_size_z = 1) in; + +layout (binding = 0) readonly buffer GRAD {A_TYPE data_grad[];}; +layout (binding = 1) readonly buffer GATE {B_TYPE data_gate[];}; +layout (binding = 2) writeonly buffer D {D_TYPE data_d[];}; + +float gelu_backward(float gate, float grad) { + const float c = 0.797884560802865; // sqrt(2/pi) + const float a = 0.044715; + const float x_squared = gate * gate; + const float x_cubed = x_squared * gate; + const float inner = c * (gate + a * x_cubed); + const float tanh_val = tanh(inner); + const float sech2_val = 1.0 - tanh_val * tanh_val; + const float dtanh_dx = c * (1.0 + 3.0 * a * x_squared) * sech2_val; + return grad * 0.5 * (1.0 + tanh_val + gate * dtanh_dx); +} + +void main() { + const uint i = gl_GlobalInvocationID.z * 262144 + gl_GlobalInvocationID.y * 512 + gl_GlobalInvocationID.x; + + if (i >= p.KX) { + return; + } + + const float grad_val = float(data_grad[i]); + const float gate_val = float(data_gate[i]); + + data_d[i] = D_TYPE(gelu_backward(gate_val, grad_val)); +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_q4_k.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_q4_k.comp index 49d91ad59101..f90e0b9787e4 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_q4_k.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_q4_k.comp @@ -22,8 +22,25 @@ void calc_superblock(const uint a_offset, const uint b_offset, const uint v_im, const uint32_t scale_0_4_l = (scale4_u32 << 16) | scale0_u32; const uint32_t scale_0_4_h = (scale_0_4_l & 0xC0C0C0C0) >> 2; + +#if defined(ADRENO) + const vec4 scale_0_4_l_f = vec4( + float((scale_0_4_l >> 0) & 0x3Fu), + float((scale_0_4_l >> 8) & 0x3Fu), + float((scale_0_4_l >> 16) & 0x3Fu), + float((scale_0_4_l >> 24) & 0x3Fu) + ); + + const vec4 scale8_f = vec4( + float(((((scale8_u32 << 12) | scale8_u32) & 0x0F0F0F0Fu) | scale_0_4_h) >> 0 & 0xFFu), + float(((((scale8_u32 << 12) | scale8_u32) & 0x0F0F0F0Fu) | scale_0_4_h) >> 8 & 0xFFu), + float(((((scale8_u32 << 12) | scale8_u32) & 0x0F0F0F0Fu) | scale_0_4_h) >> 16 & 0xFFu), + float(((((scale8_u32 << 12) | scale8_u32) & 0x0F0F0F0Fu) | scale_0_4_h) >> 24 & 0xFFu) + ); +#else const vec4 scale_0_4_l_f = vec4(unpack8(scale_0_4_l & 0x3F3F3F3F)); const vec4 scale8_f = vec4(unpack8((((scale8_u32 << 12) | scale8_u32) & 0x0F0F0F0F) | scale_0_4_h)); +#endif const FLOAT_TYPE sc0 = scale_0_4_l_f.x; const FLOAT_TYPE sc1 = scale_0_4_l_f.y; @@ -42,10 +59,17 @@ void calc_superblock(const uint a_offset, const uint b_offset, const uint v_im, const uint32_t qs64_u32_lo4 = qs64_u32 & 0x0F0F0F0F; const uint32_t qs64_u32_hi4 = (qs64_u32 >> 4) & 0x0F0F0F0F; +#if defined(ADRENO) + const vec4 qs0_lo4 = vec4(float(qs0_u32_lo4 & 0xFFu), float((qs0_u32_lo4 >> 8) & 0xFFu), float((qs0_u32_lo4 >> 16) & 0xFFu), float((qs0_u32_lo4 >> 24) & 0xFFu)); + const vec4 qs64_lo4 = vec4(float(qs64_u32_lo4 & 0xFFu), float((qs64_u32_lo4 >> 8) & 0xFFu), float((qs64_u32_lo4 >> 16) & 0xFFu), float((qs64_u32_lo4 >> 24) & 0xFFu)); + const vec4 qs0_hi4 = vec4(float(qs0_u32_hi4 & 0xFFu), float((qs0_u32_hi4 >> 8) & 0xFFu), float((qs0_u32_hi4 >> 16) & 0xFFu), float((qs0_u32_hi4 >> 24) & 0xFFu)); + const vec4 qs64_hi4 = vec4(float(qs64_u32_hi4 & 0xFFu), float((qs64_u32_hi4 >> 8) & 0xFFu), float((qs64_u32_hi4 >> 16) & 0xFFu), float((qs64_u32_hi4 >> 24) & 0xFFu)); +#else const vec4 qs0_lo4 = vec4(unpack8(qs0_u32_lo4)); const vec4 qs64_lo4 = vec4(unpack8(qs64_u32_lo4)); const vec4 qs0_hi4 = vec4(unpack8(qs0_u32_hi4)); const vec4 qs64_hi4 = vec4(unpack8(qs64_u32_hi4)); +#endif const FLOAT_TYPE q4_0 = qs0_lo4.x; const FLOAT_TYPE q4_1 = qs0_lo4.y; @@ -64,7 +88,11 @@ void calc_superblock(const uint a_offset, const uint b_offset, const uint v_im, const FLOAT_TYPE q4_14 = qs64_hi4.z; const FLOAT_TYPE q4_15 = qs64_hi4.w; +#if defined(ADRENO) + for (uint j = 0; j < NUM_COLS; ++j) { +#else [[unroll]] for (uint j = 0; j < NUM_COLS; ++j) { +#endif vec4 by10 = vec4(data_b_v4[(j*p.batch_stride_b + b_offset + y1_idx) / 4 ]); vec4 by132 = vec4(data_b_v4[(j*p.batch_stride_b + b_offset + y1_idx) / 4 + 8]); vec4 by20 = vec4(data_b_v4[(j*p.batch_stride_b + b_offset + y2_idx) / 4 ]); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_q6_k.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_q6_k.comp index d7a7f6426ee9..039480a263bf 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_q6_k.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_q6_k.comp @@ -46,10 +46,17 @@ void calc_superblock(const uint a_offset, const uint b_offset, const uint itid, const uint32_t q2_u32 = ql0_u32_hi4 | qh4_u32; const uint32_t q3_u32 = ql32_u32_hi4 | qh6_u32; +#if defined(ADRENO) + const vec4 q0 = vec4(float(q0_u32 & 0xFF), float((q0_u32 >> 8) & 0xFF), float((q0_u32 >> 16) & 0xFF), float(q0_u32 >> 24)) - 32; + const vec4 q1 = vec4(float(q1_u32 & 0xFF), float((q1_u32 >> 8) & 0xFF), float((q1_u32 >> 16) & 0xFF), float(q1_u32 >> 24)) - 32; + const vec4 q2 = vec4(float(q2_u32 & 0xFF), float((q2_u32 >> 8) & 0xFF), float((q2_u32 >> 16) & 0xFF), float(q2_u32 >> 24)) - 32; + const vec4 q3 = vec4(float(q3_u32 & 0xFF), float((q3_u32 >> 8) & 0xFF), float((q3_u32 >> 16) & 0xFF), float(q3_u32 >> 24)) - 32; +#else const vec4 q0 = vec4(unpack8(q0_u32)) - 32; const vec4 q1 = vec4(unpack8(q1_u32)) - 32; const vec4 q2 = vec4(unpack8(q2_u32)) - 32; const vec4 q3 = vec4(unpack8(q3_u32)) - 32; +#endif if (all_threads) { sccache[csel][ix][itid] = FLOAT_TYPE(data_a[ib0 + i].scales[itid]); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq1_0.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq1_0.comp new file mode 100644 index 000000000000..d4018d93b48d --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq1_0.comp @@ -0,0 +1,87 @@ +#version 450 +#extension GL_EXT_shader_explicit_arithmetic_types : require + +#include "mul_mat_vec_base.glsl" + +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +FLOAT_TYPE temp[NUM_COLS][NUM_ROWS]; + +void compute_outputs(const uint32_t first_row, const uint32_t num_rows) { + uint a_offset, b_offset, d_offset; + get_offsets(a_offset, b_offset, d_offset); + + const uint num_blocks_per_row = p.ncols / QUANT_K; + const uint tid = gl_LocalInvocationID.x; + + [[unroll]] for (uint j = 0; j < NUM_COLS; ++j) { + [[unroll]] for (uint i = 0; i < NUM_ROWS; ++i) { + temp[j][i] = FLOAT_TYPE(0); + } + } + + const uint pow3[5] = uint[5](1u, 3u, 9u, 27u, 81u); + for (uint nrow = 0; nrow < num_rows; ++nrow) { + const uint ib0 = a_offset / QUANT_K + (first_row + nrow) * num_blocks_per_row; + for (uint jcol = 0; jcol < NUM_COLS; ++jcol) { + const uint b_base = (jcol * p.batch_stride_b); + for (uint i = tid/8; i < num_blocks_per_row; i += gl_WorkGroupSize.x/8) { + const FLOAT_TYPE d = float(data_a[ib0 + i].d); + + // First qs chunk: 32 bytes (5*32 elements) + [[unroll]] for (uint m = tid%8; m < 32; m += 8) { + const uint q_byte = uint(data_a[ib0 + i].qs[m]); + [[unroll]] for (uint t = 0; t < 5; ++t) { + const uint q = (q_byte * pow3[t]) & 255u; + const uint xi = (q * 3u) >> 8; + const FLOAT_TYPE dequant_val = FLOAT_TYPE(d * (float(xi) - 1.0f)); + const uint elem = t * 32u + m; + const uint b_idx = i * QUANT_K + elem; + temp[jcol][nrow] += dequant_val * FLOAT_TYPE(data_b[b_base + b_offset + b_idx]); + } + } + + // Second qs chunk: 16 bytes (5*16 elements) + [[unroll]] for (uint m = tid%8; m < 16; m += 8) { + const uint q_byte = uint(data_a[ib0 + i].qs[32u + m]); + [[unroll]] for (uint t = 0; t < 5; ++t) { + const uint q = (q_byte * pow3[t]) & 255u; + const uint xi = (q * 3u) >> 8; + const FLOAT_TYPE dequant_val = FLOAT_TYPE(d * (float(xi) - 1.0f)); + const uint elem = 160u + t * 16u + m; + const uint b_idx = i * QUANT_K + elem; + temp[jcol][nrow] += dequant_val * FLOAT_TYPE(data_b[b_base + b_offset + b_idx]); + } + } + + // qh bytes: 4 bytes (4*4 elements) + [[unroll]] for (uint j = tid%8; j < 4; j += 8) { + const uint qh_byte = uint(data_a[ib0 + i].qh[j]); + [[unroll]] for (uint t = 0; t < 4; ++t) { + const uint q = (qh_byte * pow3[t]) & 255u; + const uint xi = (q * 3u) >> 8; + const FLOAT_TYPE dequant_val = FLOAT_TYPE(d * (float(xi) - 1.0f)); + const uint elem = 240u + t * 4u + j; + const uint b_idx = i * QUANT_K + elem; + temp[jcol][nrow] += dequant_val * FLOAT_TYPE(data_b[b_base + b_offset + b_idx]); + } + } + } + } + } + + reduce_result(temp, d_offset, first_row, num_rows, tid); +} + +void main() { + const uint first_row = NUM_ROWS * (gl_WorkGroupID.x + gl_NumWorkGroups.x * gl_WorkGroupID.z); + + if (first_row + NUM_ROWS <= p.stride_d) { + compute_outputs(first_row, NUM_ROWS); + } else { + if (first_row >= p.stride_d) { + return; + } + compute_outputs(first_row, p.stride_d - first_row); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq1_0_q.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq1_0_q.comp new file mode 100644 index 000000000000..d6744a8790e4 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq1_0_q.comp @@ -0,0 +1,108 @@ +#version 450 +#extension GL_EXT_shader_explicit_arithmetic_types : require +#extension GL_EXT_integer_dot_product : require + +#define MMQ +#define B_TYPE block_q8_1_x4 + +#include "mul_mat_vec_base.glsl" +#include "mul_mmq_funcs.comp" + +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +FLOAT_TYPE temp[NUM_COLS][NUM_ROWS]; + +int dotPacked4x8_custom(int a, int b) +{ + int8_t a0 = int8_t(a & 0xFF); + int8_t a1 = int8_t((a >> 8) & 0xFF); + int8_t a2 = int8_t((a >> 16) & 0xFF); + int8_t a3 = int8_t((a >> 24) & 0xFF); + + int8_t b0 = int8_t(b & 0xFF); + int8_t b1 = int8_t((b >> 8) & 0xFF); + int8_t b2 = int8_t((b >> 16) & 0xFF); + int8_t b3 = int8_t((b >> 24) & 0xFF); + + return int(a0) * int(b0) + + int(a1) * int(b1) + + int(a2) * int(b2) + + int(a3) * int(b3); +} + +void compute_outputs(const uint32_t first_row, const uint32_t num_rows) { + uint a_offset, b_offset, d_offset; + get_offsets(a_offset, b_offset, d_offset); + + const uint num_blocks_per_row = p.ncols / QUANT_K; + + [[unroll]] for (uint j = 0; j < NUM_COLS; ++j) { + [[unroll]] for (uint i = 0; i < NUM_ROWS; ++i) { + temp[j][i] = FLOAT_TYPE(0); + } + } + + const uint tid = gl_LocalInvocationID.x; + + for (uint jcol = 0; jcol < NUM_COLS; jcol++) { + const uint b_base = (jcol * p.batch_stride_b); + for (uint n = 0; n < num_rows; ++n) { + const uint ib0 = a_offset / QUANT_K + (first_row + n) * num_blocks_per_row; + FLOAT_TYPE acc = 0.0f; + for (uint i = tid/8; i < num_blocks_per_row; i+=gl_WorkGroupSize.x/8) { + const float d = float(data_a[ib0 + i].d); + [[unroll]] for (uint j = 0; j < 64; j += 32) { + [[unroll]] for (uint l = 0; l < 4; l+=2) { + const uint k = (tid % 8) * 4; + const uint base_elem = j * 4 + l * 32 + k; // element index within block + + // Pack four ternary elements (-1,0,1) into lanes using repack + // repack packs [base_elem..base_elem+3] in .x and [base_elem+32..+35] in .y + const i32vec2 a_packed = repack(ib0 + i, base_elem); + + const uint b0_idx = i * QUANT_K + j * 4 + l * 32; + const uint b1_idx = i * QUANT_K + j * 4 + (l+1) * 32; + + const uint b0_block_idx = b_offset + (b_base + b0_idx) / QUANT_K_Q8_1; + const uint b1_block_idx = b_offset + (b_base + b1_idx) / QUANT_K_Q8_1; + const uint b0_block_idx_outer = b0_block_idx / 4; + const uint b1_block_idx_outer = b1_block_idx / 4; + const uint b0_block_idx_inner = b0_block_idx % 4; + const uint b1_block_idx_inner = b1_block_idx % 4; + vec2 ds0 = vec2(data_b[b_offset + b0_block_idx_outer].ds[b0_block_idx_inner]); + vec2 ds1 = vec2(data_b[b_offset + b1_block_idx_outer].ds[b1_block_idx_inner]); + + const uint vec_idx = k / 4; + int32_t b0_packed = data_b[b_offset + b0_block_idx_outer].qs[b0_block_idx_inner * 8 + vec_idx]; + int32_t b1_packed = data_b[b_offset + b1_block_idx_outer].qs[b1_block_idx_inner * 8 + vec_idx]; + + int32_t a0_packed = a_packed.x; + int32_t a1_packed = a_packed.y; + + int32_t q0_sum = dotPacked4x8_custom(a0_packed, b0_packed); + acc += mul_q8_1(q0_sum, d, ds0, 4); + + int32_t q1_sum = dotPacked4x8_custom(a1_packed, b1_packed); + acc += mul_q8_1(q1_sum, d, ds1, 4); + } + } + } + temp[jcol][n] = acc; + } + } + + reduce_result(temp, d_offset, first_row, num_rows, tid); +} + +void main() { + const uint first_row = NUM_ROWS * (gl_WorkGroupID.x + gl_NumWorkGroups.x * gl_WorkGroupID.z); + + if (first_row + NUM_ROWS <= p.stride_d) { + compute_outputs(first_row, NUM_ROWS); + } else { + if (first_row >= p.stride_d) { + return; + } + compute_outputs(first_row, p.stride_d - first_row); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq2_0.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq2_0.comp new file mode 100644 index 000000000000..7da790dd36ac --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq2_0.comp @@ -0,0 +1,53 @@ +#version 450 +#extension GL_EXT_shader_explicit_arithmetic_types_int32 : require + +#include "mul_mat_vec_base.glsl" +#include "tq_utils.comp" + +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +FLOAT_TYPE temp[NUM_COLS][NUM_ROWS]; + +void compute_outputs(const uint32_t first_row, const uint32_t num_rows) { + uint a_offset, b_offset, d_offset; + get_offsets(a_offset, b_offset, d_offset); + + const uint num_blocks_per_row = p.ncols / QUANT_K; + const uint tid = gl_LocalInvocationID.x; + + [[unroll]] for (uint j = 0; j < NUM_COLS; ++j) { + [[unroll]] for (uint i = 0; i < NUM_ROWS; ++i) { + temp[j][i] = FLOAT_TYPE(0); + } + } + + [[unroll]] for (uint i = tid / (QUANT_K / 8); i < num_blocks_per_row; i += gl_WorkGroupSize.x / (QUANT_K / 8)) { + [[unroll]] for (uint n = 0; n < num_rows; ++n) { + const uint ib0 = a_offset / QUANT_K + (first_row + n) * num_blocks_per_row; + const FLOAT_TYPE d = FLOAT_TYPE(data_a[ib0 + i].d); + + [[unroll]] for (uint e = (tid % (QUANT_K / 8)) * 8; e < (tid % (QUANT_K / 8)) * 8 + 8; e++) { + const FLOAT_TYPE dequant_val = d * FLOAT_TYPE(tq2_dequantize(ib0 + i, e)); + + [[unroll]] for (uint jcol = 0; jcol < NUM_COLS; ++jcol) { + temp[jcol][n] += dequant_val * FLOAT_TYPE(data_b[jcol * p.batch_stride_b + b_offset + i * QUANT_K + e]); + } + } + } + } + + reduce_result(temp, d_offset, first_row, num_rows, tid); +} + +void main() { + const uint first_row = NUM_ROWS * (gl_WorkGroupID.x + gl_NumWorkGroups.x * gl_WorkGroupID.z); + + if (first_row + NUM_ROWS <= p.stride_d) { + compute_outputs(first_row, NUM_ROWS); + } else { + if (first_row >= p.stride_d) { + return; + } + compute_outputs(first_row, p.stride_d - first_row); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq2_0_q.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq2_0_q.comp new file mode 100644 index 000000000000..dd3b38fe7918 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq2_0_q.comp @@ -0,0 +1,95 @@ +#version 450 +#extension GL_EXT_shader_explicit_arithmetic_types : require +#extension GL_EXT_integer_dot_product : require + +#define MMQ +#define B_TYPE block_q8_1_x4 + +#include "mul_mat_vec_base.glsl" +#include "mul_mmq_funcs.comp" + +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +FLOAT_TYPE temp[NUM_COLS][NUM_ROWS]; + +void compute_outputs(const uint32_t first_row, const uint32_t num_rows) { + uint a_offset, b_offset, d_offset; + get_offsets(a_offset, b_offset, d_offset); + + const uint num_blocks_per_row = p.ncols / QUANT_K; + + [[unroll]] for (uint j = 0; j < NUM_COLS; ++j) { + [[unroll]] for (uint i = 0; i < NUM_ROWS; ++i) { + temp[j][i] = FLOAT_TYPE(0); + } + } + + const uint tid = gl_LocalInvocationID.x; + + for (uint jcol = 0; jcol < NUM_COLS; jcol++) { + const uint b_base = (jcol * p.batch_stride_b); + for (uint n = 0; n < num_rows; ++n) { + const uint ib0 = a_offset / QUANT_K + (first_row + n) * num_blocks_per_row; + FLOAT_TYPE acc = 0.0f; + for (uint i = tid/8; i < num_blocks_per_row; i+=gl_WorkGroupSize.x/8) { + const float d = float(data_a[ib0 + i].d); + [[unroll]] for (uint j = 0; j < 64; j += 32) { + [[unroll]] for (uint l = 0; l < 4; l+=2) { + uint k = (tid % 8) * 4; + + const uint shift0 = l * 2u; + const int c00 = int(((data_a[ib0 + i].qs[j + k]) >> shift0) & 3u); + const int c01 = int(((data_a[ib0 + i].qs[j + k + 1]) >> shift0) & 3u); + const int c02 = int(((data_a[ib0 + i].qs[j + k + 2]) >> shift0) & 3u); + const int c03 = int(((data_a[ib0 + i].qs[j + k + 3]) >> shift0) & 3u); + const int32_t a0_packed = c00 | (c01 << 8) | (c02 << 16) | (c03 << 24); + const uint b0_idx = i * QUANT_K + j * 4 + l * 32; + + const uint shift1 = (l+1) * 2u; + const int c10 = int(((data_a[ib0 + i].qs[j + k]) >> shift1) & 3u); + const int c11 = int(((data_a[ib0 + i].qs[j + k + 1]) >> shift1) & 3u); + const int c12 = int(((data_a[ib0 + i].qs[j + k + 2]) >> shift1) & 3u); + const int c13 = int(((data_a[ib0 + i].qs[j + k + 3]) >> shift1) & 3u); + const int32_t a1_packed = c10 | (c11 << 8) | (c12 << 16) | (c13 << 24); + const uint b1_idx = i * QUANT_K + j * 4 + (l+1) * 32; + + // Not checking for OOB since we're guaranteed to be multiple of 256 + const uint b0_block_idx = b_offset + (b_base + b0_idx) / QUANT_K_Q8_1; + const uint b1_block_idx = b_offset + (b_base + b1_idx) / QUANT_K_Q8_1; + const uint b0_block_idx_outer = b0_block_idx / 4; + const uint b1_block_idx_outer = b1_block_idx / 4; + const uint b0_block_idx_inner = b0_block_idx % 4; + const uint b1_block_idx_inner = b1_block_idx % 4; + vec2 ds0 = vec2(data_b[b_offset + b0_block_idx_outer].ds[b0_block_idx_inner]); + vec2 ds1 = vec2(data_b[b_offset + b1_block_idx_outer].ds[b1_block_idx_inner]); + + const uint vec_idx = k / 4; + int32_t b0_packed = data_b[b_offset + b0_block_idx_outer].qs[b0_block_idx_inner * 8 + vec_idx]; + int32_t b1_packed = data_b[b_offset + b1_block_idx_outer].qs[b1_block_idx_inner * 8 + vec_idx]; + + int32_t q0_sum = dotPacked4x8EXT(a0_packed, b0_packed); + int32_t q1_sum = dotPacked4x8EXT(a1_packed, b1_packed); + acc += ACC_TYPE(d * (FLOAT_TYPE(q0_sum) * ds0.x - FLOAT_TYPE(1.0f / 8) * ds0.y)); + acc += ACC_TYPE(d * (FLOAT_TYPE(q1_sum) * ds1.x - FLOAT_TYPE(1.0f / 8) * ds1.y)); + } + } + } + temp[jcol][n] = acc; + } + } + + reduce_result(temp, d_offset, first_row, num_rows, tid); +} + +void main() { + const uint first_row = NUM_ROWS * (gl_WorkGroupID.x + gl_NumWorkGroups.x * gl_WorkGroupID.z); + + if (first_row + NUM_ROWS <= p.stride_d) { + compute_outputs(first_row, NUM_ROWS); + } else { + if (first_row >= p.stride_d) { + return; + } + compute_outputs(first_row, p.stride_d - first_row); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp index 15f005be3eaf..f024d07af8f3 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp @@ -36,7 +36,7 @@ void iter(inout FLOAT_TYPE temp[NUM_COLS][NUM_ROWS], const uint first_row, const const uint b_block_idx_inner = b_block_idx % 4; cache_b_ds = vec2(data_b[b_block_idx_outer].ds[b_block_idx_inner]); -#if QUANT_R == 2 +#if QUANT_R == 2 || QUANT_R == 4 // Assumes K_PER_ITER == 8 cache_b_qs[0] = data_b[b_block_idx_outer].qs[b_block_idx_inner * 8 + b_qs_idx]; cache_b_qs[1] = data_b[b_block_idx_outer].qs[b_block_idx_inner * 8 + b_qs_idx + 4]; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl index 2389ea0b1ec4..75da3425d447 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl @@ -135,7 +135,8 @@ FLOAT_TYPE mul_q8_1(const int32_t q_sum, const float da, const vec2 dsb, const i #if defined(DATA_A_QUANT_LEGACY) || defined(DATA_A_MXFP4) FLOAT_TYPE mmvq_dot_product(const uint ib_a, const uint iqs) { int32_t q_sum = 0; -#if QUANT_R == 2 + +#if QUANT_R == 2 || QUANT_R == 4 const i32vec2 data_a_qs = repack(ib_a, iqs); q_sum += dotPacked4x8EXT(data_a_qs.x, cache_b_qs[0]); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl index ee5ded2e8d3e..179386087da9 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl @@ -1,3 +1,7 @@ +#if defined(DATA_A_TQ2_0) +#include "tq_utils.comp" +#endif + void load_a_to_shmem(const uint pos_a, const uint row, const uint col, const uint idx_m, const uint block, const uint end_k) { #if defined(DATA_A_F32) || defined(DATA_A_F16) #if LOAD_VEC_A == 8 @@ -54,8 +58,25 @@ void load_a_to_shmem(const uint pos_a, const uint row, const uint col, const uin const float d = float(data_a_packed16[ib].d); const uint vui = uint(data_a_packed16[ib].qs[2*iqs]) | (uint(data_a_packed16[ib].qs[2*iqs + 1]) << 16); + +#if defined(ADRENO) + const vec4 v0 = (vec4( + float((vui >> 0) & 0xF), + float((vui >> 8) & 0xF), + float((vui >> 16) & 0xF), + float((vui >> 24) & 0xF) + ) - 8.0) * d; + + const vec4 v1 = (vec4( + float((vui >> 4) & 0xF), + float((vui >> 12) & 0xF), + float((vui >> 20) & 0xF), + float((vui >> 28) & 0xF) + ) - 8.0) * d; +#else const vec4 v0 = (vec4(unpack8(vui & 0x0F0F0F0F)) - 8.0f) * d; const vec4 v1 = (vec4(unpack8((vui >> 4) & 0x0F0F0F0F)) - 8.0f) * d; +#endif buf_a[buf_idx ] = FLOAT_TYPE_VEC2(v0.xy); buf_a[buf_idx + 1] = FLOAT_TYPE_VEC2(v0.zw); @@ -71,8 +92,24 @@ void load_a_to_shmem(const uint pos_a, const uint row, const uint col, const uin const float d = float(data_a_packed16[ib].d); const float m = float(data_a_packed16[ib].m); const uint vui = uint(data_a_packed16[ib].qs[2*iqs]) | (uint(data_a_packed16[ib].qs[2*iqs + 1]) << 16); + +#if defined(ADRENO) + const vec4 v0 = vec4( + float((vui >> 0) & 0xF), + float((vui >> 8) & 0xF), + float((vui >> 16) & 0xF), + float((vui >> 24) & 0xF) + ) * d + m; + const vec4 v1 = vec4( + float((vui >> 4) & 0xF), + float((vui >> 12) & 0xF), + float((vui >> 20) & 0xF), + float((vui >> 28) & 0xF) + ) * d + m; +#else const vec4 v0 = vec4(unpack8(vui & 0x0F0F0F0F)) * d + m; const vec4 v1 = vec4(unpack8((vui >> 4) & 0x0F0F0F0F)) * d + m; +#endif buf_a[buf_idx ] = FLOAT_TYPE_VEC2(v0.xy); buf_a[buf_idx + 1 ] = FLOAT_TYPE_VEC2(v0.zw); @@ -120,13 +157,102 @@ void load_a_to_shmem(const uint pos_a, const uint row, const uint col, const uin const uint ib = idx / 8; const uint iqs = idx & 0x07; +#if defined(ADRENO) + const float d = float(data_a[ib].d); + const vec4 v = vec4( + int(data_a[ib].qs[4*iqs]), + int(data_a[ib].qs[4*iqs + 1]), + int(data_a[ib].qs[4*iqs + 2]), + int(data_a[ib].qs[4*iqs + 3]) + ) * d; +#else const float d = float(data_a_packed16[ib].d); const i8vec2 v0 = unpack8(int32_t(data_a_packed16[ib].qs[2*iqs])).xy; // vec4 used due to #12147 const i8vec2 v1 = unpack8(int32_t(data_a_packed16[ib].qs[2*iqs + 1])).xy; const vec4 v = vec4(v0.x, v0.y, v1.x, v1.y) * d; +#endif buf_a[buf_idx ] = FLOAT_TYPE_VEC2(v.xy); buf_a[buf_idx + 1] = FLOAT_TYPE_VEC2(v.zw); +#elif defined(DATA_A_TQ2_0) + const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row; + const uint buf_idx = col * SHMEM_STRIDE + row * LOAD_VEC_A / 2; + + const uint ib = idx / 128; // 2 values per idx + const uint iqs = idx % 128; // 0..127 + + const float d = float(data_a[ib].d); + + const uint e0 = 2 * iqs; + const uint e1 = e0 + 1; + + const float v0 = d * float(tq2_dequantize(ib, e0)); + const float v1 = d * float(tq2_dequantize(ib, e1)); + + buf_a[buf_idx] = FLOAT_TYPE_VEC2(v0, v1); +#elif defined(DATA_A_TQ1_0) + const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row; + const uint buf_idx = col * SHMEM_STRIDE + row * LOAD_VEC_A / 2; + + const uint ib = idx / 128; + const uint iqs = idx % 128; + + const float d = float(data_a[ib].d); + + const uint pow3[6] = uint[6](1u, 3u, 9u, 27u, 81u, 243u); + + const uint e0 = 2u * iqs; + const uint e1 = e0 + 1u; + + float v0; + if (e0 < 160u) { + const uint n0 = e0 / 32u; + const uint m0 = e0 % 32u; + const uint q0 = uint(data_a[ib].qs[m0]); + const uint xi0 = (((q0 * pow3[n0]) & 255u) * 3u) >> 8; + v0 = float(int(xi0) - 1); + } else if (e0 < 240u) { + const uint ee0 = e0 - 160u; + const uint n0 = ee0 / 16u; + const uint m0 = ee0 % 16u; + const uint q0 = uint(data_a[ib].qs[32u + m0]); + const uint xi0 = (((q0 * pow3[n0]) & 255u) * 3u) >> 8; + v0 = float(int(xi0) - 1); + } else { + const uint ee0 = e0 - 240u; + const uint n0 = ee0 / (QUANT_K / 64u); + const uint j0 = ee0 % (QUANT_K / 64u); + const uint q0 = uint(data_a[ib].qh[j0]); + const uint xi0 = (((q0 * pow3[n0]) & 255u) * 3u) >> 8; + v0 = float(int(xi0) - 1); + } + + float v1; + if (e1 < 160u) { + const uint n1 = e1 / 32u; + const uint m1 = e1 % 32u; + const uint q1 = uint(data_a[ib].qs[m1]); + const uint xi1 = (((q1 * pow3[n1]) & 255u) * 3u) >> 8; + v1 = float(int(xi1) - 1); + } else if (e1 < 240u) { + const uint ee1 = e1 - 160u; + const uint n1 = ee1 / 16u; + const uint m1 = ee1 % 16u; + const uint q1 = uint(data_a[ib].qs[32u + m1]); + const uint xi1 = (((q1 * pow3[n1]) & 255u) * 3u) >> 8; + v1 = float(int(xi1) - 1); + } else { + const uint ee1 = e1 - 240u; + const uint n1 = ee1 / (QUANT_K / 64u); + const uint j1 = ee1 % (QUANT_K / 64u); + const uint q1 = uint(data_a[ib].qh[j1]); + const uint xi1 = (((q1 * pow3[n1]) & 255u) * 3u) >> 8; + v1 = float(int(xi1) - 1); + } + + const vec2 v = d * vec2(v0, v1); + + buf_a[buf_idx ] = FLOAT_TYPE_VEC2(v.xy); #elif defined(DATA_A_Q2_K) const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row; const uint buf_idx = col * SHMEM_STRIDE + row * LOAD_VEC_A / 2; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp index dc8b3df47bea..abe96f3df746 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp @@ -241,7 +241,6 @@ void main() { [[unroll]] for (uint cr = 0; cr < TM; cr++) { const uint reg_ib = wsir * TM + cr; const uint buf_ib = warp_r * WM + wsir * WSUBM + tiwr * TM + cr; - block_a_to_registers(reg_ib, k_step * BM + buf_ib); } } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_funcs.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_funcs.comp new file mode 100644 index 000000000000..a2f020228934 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_funcs.comp @@ -0,0 +1,242 @@ +#extension GL_EXT_shader_explicit_arithmetic_types_int32 : require +#extension GL_EXT_shader_explicit_arithmetic_types_int16 : require +#extension GL_EXT_shader_explicit_arithmetic_types_int8 : require + +#include "types.glsl" + +// Each iqs value maps to a 32-bit integer + +#if defined(DATA_A_Q4_0) +i32vec2 repack(uint ib, uint iqs) { + // Use 2-byte loads since a q4_0 block (18 bytes) is not divisible by 4 + const u16vec2 quants = u16vec2(data_a[ib].qs[iqs * 2 ], + data_a[ib].qs[iqs * 2 + 1]); + const uint32_t vui = pack32(quants); + return i32vec2( vui & 0x0F0F0F0F, + (vui >> 4) & 0x0F0F0F0F); +} + +ACC_TYPE mul_q8_1(const int32_t q_sum, const float da, const vec2 dsb, const int32_t sum_divisor) { + return ACC_TYPE(da * (float(q_sum) * dsb.x - (8 / sum_divisor) * dsb.y)); +} +#endif + +#if defined(DATA_A_Q4_1) +i32vec2 repack(uint ib, uint iqs) { + // Use 4-byte loads since a q4_1 block (20 bytes) is divisible by 4 + const uint32_t vui = data_a_packed32[ib].qs[iqs]; + return i32vec2( vui & 0x0F0F0F0F, + (vui >> 4) & 0x0F0F0F0F); +} + +ACC_TYPE mul_q8_1(const int32_t q_sum, const vec2 dma, const vec2 dsb, const int32_t sum_divisor) { + return ACC_TYPE(float(q_sum) * dma.x * dsb.x + dma.y * dsb.y / sum_divisor); +} +#endif + +#if defined(DATA_A_Q5_0) +i32vec2 repack(uint ib, uint iqs) { + // Use 2-byte loads since a q5_0 block (22 bytes) is not divisible by 4 + const u16vec2 quants = u16vec2(data_a[ib].qs[iqs * 2 ], + data_a[ib].qs[iqs * 2 + 1]); + const uint32_t vui = pack32(quants); + const int32_t qh = int32_t((uint32_t(data_a[ib].qh[1]) << 16 | data_a[ib].qh[0]) >> (4 * iqs)); + const int32_t v0 = int32_t(vui & 0x0F0F0F0F) + | ((qh & 0xF) * 0x02040810) & 0x10101010; // (0,1,2,3) -> (4,12,20,28) + + const int32_t v1 = int32_t((vui >> 4) & 0x0F0F0F0F) + | (((qh >> 16) & 0xF) * 0x02040810) & 0x10101010; // (16,17,18,19) -> (4,12,20,28) + + return i32vec2(v0, v1); +} + +ACC_TYPE mul_q8_1(const int32_t q_sum, const float da, const vec2 dsb, const int32_t sum_divisor) { + return ACC_TYPE(da * (float(q_sum) * dsb.x - (16 / sum_divisor) * dsb.y)); +} +#endif + +#if defined(DATA_A_Q5_1) +i32vec2 repack(uint ib, uint iqs) { + // Use 4-byte loads since a q5_1 block (24 bytes) is divisible by 4 + const uint32_t vui = data_a_packed32[ib].qs[iqs]; + const int32_t qh = int32_t(data_a_packed32[ib].qh >> (4 * iqs)); + const int32_t v0 = int32_t(vui & 0x0F0F0F0F) + | ((qh & 0xF) * 0x02040810) & 0x10101010; // (0,1,2,3) -> (4,12,20,28) + + const int32_t v1 = int32_t((vui >> 4) & 0x0F0F0F0F) + | (((qh >> 16) & 0xF) * 0x02040810) & 0x10101010; // (16,17,18,19) -> (4,12,20,28) + + return i32vec2(v0, v1); +} + +ACC_TYPE mul_q8_1(const int32_t q_sum, const vec2 dma, const vec2 dsb, const int32_t sum_divisor) { + return ACC_TYPE(float(q_sum) * dma.x * dsb.x + dma.y * dsb.y / sum_divisor); +} +#endif + +#if defined(DATA_A_Q8_0) +int32_t repack(uint ib, uint iqs) { + // Use 2-byte loads since a q8_0 block (34 bytes) is not divisible by 4 + return pack32(i16vec2(data_a[ib].qs[iqs * 2 ], + data_a[ib].qs[iqs * 2 + 1])); +} + +ACC_TYPE mul_q8_1(const int32_t q_sum, const float da, const vec2 dsb, const int32_t sum_divisor) { + return ACC_TYPE(float(q_sum) * da * dsb.x); +} +#endif + +#if defined(DATA_A_TQ2_0) +i32vec2 repack(uint ib, uint iqs) { + const uint k00 = iqs + 0u; + const uint ip00 = ((k00 >> 7) & 1u) * 32u; + const uint b00 = (k00 & 31u) + ip00; + const uint s00 = ((k00 >> 5) & 3u) * 2u; + + const uint k01 = iqs + 1u; + const uint ip01 = ((k01 >> 7) & 1u) * 32u; + const uint b01 = (k01 & 31u) + ip01; + const uint s01 = ((k01 >> 5) & 3u) * 2u; + + const uint k02 = iqs + 2u; + const uint ip02 = ((k02 >> 7) & 1u) * 32u; + const uint b02 = (k02 & 31u) + ip02; + const uint s02 = ((k02 >> 5) & 3u) * 2u; + + const uint k03 = iqs + 3u; + const uint ip03 = ((k03 >> 7) & 1u) * 32u; + const uint b03 = (k03 & 31u) + ip03; + const uint s03 = ((k03 >> 5) & 3u) * 2u; + + const int q00 = int(data_a[ib].qs[b00]); + const int q01 = int(data_a[ib].qs[b01]); + const int q02 = int(data_a[ib].qs[b02]); + const int q03 = int(data_a[ib].qs[b03]); + + const int t00 = (q00 >> int(s00)) & 3; + const int t01 = (q01 >> int(s01)) & 3; + const int t02 = (q02 >> int(s02)) & 3; + const int t03 = (q03 >> int(s03)) & 3; + + const int v0 = (t00 & 0xFF) | ((t01 & 0xFF) << 8) | ((t02 & 0xFF) << 16) | ((t03 & 0xFF) << 24); + + + const uint k10 = iqs + 16u + 0u; + const uint ip10 = ((k10 >> 7) & 1u) * 32u; + const uint b10 = (k10 & 31u) + ip10; + const uint s10 = ((k10 >> 5) & 3u) * 2u; + + const uint k11 = iqs + 16u + 1u; + const uint ip11 = ((k11 >> 7) & 1u) * 32u; + const uint b11 = (k11 & 31u) + ip11; + const uint s11 = ((k11 >> 5) & 3u) * 2u; + + const uint k12 = iqs + 16u + 2u; + const uint ip12 = ((k12 >> 7) & 1u) * 32u; + const uint b12 = (k12 & 31u) + ip12; + const uint s12 = ((k12 >> 5) & 3u) * 2u; + + const uint k13 = iqs + 16u + 3u; + const uint ip13 = ((k13 >> 7) & 1u) * 32u; + const uint b13 = (k13 & 31u) + ip13; + const uint s13 = ((k13 >> 5) & 3u) * 2u; + + const int q10 = int(data_a[ib].qs[b10]); + const int q11 = int(data_a[ib].qs[b11]); + const int q12 = int(data_a[ib].qs[b12]); + const int q13 = int(data_a[ib].qs[b13]); + + const int u10 = (q10 >> int(s10)) & 3; + const int u11 = (q11 >> int(s11)) & 3; + const int u12 = (q12 >> int(s12)) & 3; + const int u13 = (q13 >> int(s13)) & 3; + + const int v1 = (u10 & 0xFF) | ((u11 & 0xFF) << 8) | ((u12 & 0xFF) << 16) | ((u13 & 0xFF) << 24); + + return i32vec2(v0, v1); +} + +ACC_TYPE mul_q8_1(const int32_t q_sum, const float da, const vec2 dsb, const int32_t sum_divisor) { + return ACC_TYPE(da * (float(q_sum) * dsb.x - float(1.0f / sum_divisor) * dsb.y)); +} +#endif + +#if defined(DATA_A_TQ1_0) +int dequant1(uint ib, uint iqs) { + const uint pow3[6] = uint[6](1u, 3u, 9u, 27u, 81u, 243u); + + if (iqs < 160u) { + const uint n = iqs / 32u; + const uint m = iqs % 32u; + const uint q = uint(data_a[ib].qs[m]); + const uint xi = (((q * pow3[n]) & 255u) * 3u) >> 8u; + return int(xi) - 1; + } else if (iqs < 240u) { + const uint e = iqs - 160u; + const uint n = e / 16u; + const uint m = e % 16u; + const uint q = uint(data_a[ib].qs[32u + m]); + const uint xi = (((q * pow3[n]) & 255u) * 3u) >> 8u; + return int(xi) - 1; + } else { + const uint e = iqs - 240u; + const uint n = e / (QUANT_K / 64u); + const uint j = e % (QUANT_K / 64u); + const uint q = uint(data_a[ib].qh[j]); + const uint xi = (((q * pow3[n]) & 255u) * 3u) >> 8u; + return int(xi) - 1; + } +} + +i32vec2 repack(uint ib, uint iqs) { + // Pack 4 consecutive elements starting at iqs and iqs+16 into two 32-bit ints + const uint k00 = iqs + 0u; + const uint k01 = iqs + 1u; + const uint k02 = iqs + 2u; + const uint k03 = iqs + 3u; + + const int t00 = dequant1(ib, k00); + const int t01 = dequant1(ib, k01); + const int t02 = dequant1(ib, k02); + const int t03 = dequant1(ib, k03); + + const int v0 = (t00 & 0xFF) | ((t01 & 0xFF) << 8) | ((t02 & 0xFF) << 16) | ((t03 & 0xFF) << 24); + + const uint k10 = iqs + 32u + 0u; + const uint k11 = iqs + 32u + 1u; + const uint k12 = iqs + 32u + 2u; + const uint k13 = iqs + 32u + 3u; + + const int u10 = dequant1(ib, k10); + const int u11 = dequant1(ib, k11); + const int u12 = dequant1(ib, k12); + const int u13 = dequant1(ib, k13); + + const int v1 = (u10 & 0xFF) | ((u11 & 0xFF) << 8) | ((u12 & 0xFF) << 16) | ((u13 & 0xFF) << 24); + + return i32vec2(v0, v1); +} + +ACC_TYPE mul_q8_1(const int32_t q_sum, const float da, const vec2 dsb, const int32_t sum_divisor) { + // q_sum already incorporates (-1,0,1) values; no offset correction needed + return ACC_TYPE(da * (float(q_sum) * dsb.x)); +} +#endif + +#if defined(DATA_A_TQ1_0) || defined(DATA_A_TQ2_0) || defined(DATA_A_Q4_0) || defined(DATA_A_Q5_0) || defined(DATA_A_Q8_0) || defined(DATA_A_IQ1_S) || defined(DATA_A_IQ2_XXS) || defined(DATA_A_IQ2_XS) || defined(DATA_A_IQ2_S) || defined(DATA_A_IQ3_XXS) || defined(DATA_A_IQ3_S) || defined(DATA_A_IQ4_XS) || defined(DATA_A_IQ4_NL) +FLOAT_TYPE get_d(uint ib) { + return FLOAT_TYPE(data_a[ib].d); +} +#endif + +#if defined(DATA_A_MXFP4) +FLOAT_TYPE get_d(uint ib) { + return FLOAT_TYPE(e8m0_to_fp32(data_a[ib].e)); +} +#endif + +#if defined(DATA_A_Q4_1) || defined(DATA_A_Q5_1) +FLOAT_TYPE_VEC2 get_dm(uint ib) { + return FLOAT_TYPE_VEC2(data_a_packed32[ib].dm); +} +#endif diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_funcs.glsl index 7f32dadf17d8..278f19db3042 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_funcs.glsl @@ -375,15 +375,44 @@ void block_a_to_shmem(const uint buf_ib, const uint ib, const uint iqs) { const uint qh_idx = (iqs_k / 32) * 8 + iqs; const uint qh_shift = ((iqs_k % 32) / 8) * 2; +#if defined(ADRENO) + uint16_t ql00 = (data_a_packed16[ib_k].ql[ql_idx * 2] >> ql_shift) & uint16_t(0x0F0F); + uint16_t qh00 = ((data_a_packed16[ib_k].qh[qh_idx * 2] >> qh_shift) & uint16_t(0x0303)) << 4; + int8_t lo00_0 = int8_t((ql00 & 0x00FF) | (qh00 & 0x00FF)); + int8_t lo00_1 = int8_t(((ql00 >> 8) & 0x00FF) | ((qh00 >> 8) & 0x00FF)); + const i8vec2 vals00 = i8vec2(lo00_0, lo00_1) - int8_t(32); + + uint16_t ql01 = (data_a_packed16[ib_k].ql[ql_idx * 2 + 1] >> ql_shift) & uint16_t(0x0F0F); + uint16_t qh01 = ((data_a_packed16[ib_k].qh[qh_idx * 2 + 1] >> qh_shift) & uint16_t(0x0303)) << 4; + int8_t lo01_0 = int8_t((ql01 & 0x00FF) | (qh01 & 0x00FF)); + int8_t lo01_1 = int8_t(((ql01 >> 8) & 0x00FF) | ((qh01 >> 8) & 0x00FF)); + const i8vec2 vals01 = i8vec2(lo01_0, lo01_1) - int8_t(32); + + uint32_t packed = + (uint32_t(uint8_t(vals00.x)) ) + | (uint32_t(uint8_t(vals00.y)) << 8) + | (uint32_t(uint8_t(vals01.x)) << 16) + | (uint32_t(uint8_t(vals01.y)) << 24); + buf_a[buf_ib].qs[iqs] = int32_t(packed); +#else const i8vec2 vals00 = (unpack8(int32_t((data_a_packed16[ib_k].ql[ql_idx * 2 ] >> ql_shift) & uint16_t(0x0F0F))).xy | unpack8(int32_t(((data_a_packed16[ib_k].qh[qh_idx * 2 ] >> qh_shift) & uint16_t(0x0303)) << 4)).xy) - int8_t(32); const i8vec2 vals01 = (unpack8(int32_t((data_a_packed16[ib_k].ql[ql_idx * 2 + 1] >> ql_shift) & uint16_t(0x0F0F))).xy | unpack8(int32_t(((data_a_packed16[ib_k].qh[qh_idx * 2 + 1] >> qh_shift) & uint16_t(0x0303)) << 4)).xy) - int8_t(32); buf_a[buf_ib].qs[iqs] = pack32(i8vec4(vals00.x, vals00.y, vals01.x, vals01.y)); +#endif if (iqs == 0) { const uint is = iqs_k / 4; + +#if defined(ADRENO) + uint32_t packed = uint32_t(data_a_packed16[ib_k].scales[is / 2]); + int8_t s0 = int8_t(packed & 0xFF); + int8_t s1 = int8_t((packed >> 8) & 0xFF); + const i8vec2 scales = i8vec2(s0, s1); +#else const i8vec2 scales = unpack8(int32_t(data_a_packed16[ib_k].scales[is / 2])).xy; +#endif buf_a[buf_ib].d_scales = FLOAT_TYPE(data_a_packed16[ib_k].d) * FLOAT_TYPE_VEC2(scales); } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/out_prod.comp b/ggml/src/ggml-vulkan/vulkan-shaders/out_prod.comp new file mode 100644 index 000000000000..f09a205243d9 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/out_prod.comp @@ -0,0 +1,56 @@ +#version 450 + +#extension GL_EXT_shader_16bit_storage : require + +#include "generic_binary_head.glsl" +#include "types.glsl" + +const uint num_threads = 256; + +layout(local_size_x = num_threads, local_size_y = 1, local_size_z = 1) in; + +void get_dst_indices(uint idx, out uint i20, out uint i21, out uint i22, out uint i23) { + i23 = fastdiv(idx, (p.ne22*p.ne21*p.ne20)); + const uint i23_offset = i23 * p.ne22*p.ne21*p.ne20; + i22 = fastdiv((idx - i23_offset), (p.ne21*p.ne20)); + const uint i22_offset = i22*p.ne21*p.ne20; + i21 = (idx - i23_offset - i22_offset) / p.ne20; + i20 = idx - i23_offset - i22_offset - i21*p.ne20; +} + +void main() { + // num_threads * num_iter must equal 512 to match the wg_denoms and get_idx + const uint num_iter = 2; + + const uint broadcast2 = uint(p.param2); + const uint broadcast3 = p.param3; + + uint idx = get_idx(); + + [[unroll]] for (uint i = 0; i < num_iter; ++i) { + if (idx >= p.ne) { + continue; + } + + uint i0, i1, i2, i3; + get_dst_indices(idx, i0, i1, i2, i3); + + FLOAT_TYPE acc = FLOAT_TYPE(0.0); + + for (uint i01 = 0; i01 < p.ne01; ++i01) { + uint a_idx = src0_idx(i0, i01, i2 / broadcast2, i3 / broadcast3); + uint b_idx = src1_idx(i1, i01, i2, i3); + + FLOAT_TYPE a_val = FLOAT_TYPE(data_a[get_aoffset() + a_idx]); + FLOAT_TYPE b_val = FLOAT_TYPE(data_b[get_boffset() + b_idx]); + + acc += a_val * b_val; + } + + uint d_idx = dst_idx(i0, i1, i2, i3); + data_d[get_doffset() + d_idx] = D_TYPE(acc); + + idx += num_threads; + } +} + diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/out_prod_q4_0.comp b/ggml/src/ggml-vulkan/vulkan-shaders/out_prod_q4_0.comp new file mode 100644 index 000000000000..e77973c15dea --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/out_prod_q4_0.comp @@ -0,0 +1,58 @@ +#version 450 + +#include "types.glsl" +#include "generic_binary_head.glsl" +#include "dequant_funcs.glsl" + +// Each thread processes two outputs: i0 and i0 + QUANT_K/2. +const uint quant_group_sz = 2; +const uint num_threads = 512 / quant_group_sz; +layout(local_size_x = num_threads, local_size_y = 1, local_size_z = 1) in; + +// Decode a "lower-half index" in [0, ne/2) to (i0, i1, i2, i3) where +// i0 is guaranteed to be in the lower half of its quantization block +// (i0 % QUANT_K < QUANT_K/2). +// Requires ne20 to be a multiple of QUANT_K (always true for Q4_0). +void get_dst_indices_lower(uint idx, out uint i0, out uint i1, out uint i2, out uint i3) { + const uint ne20h = p.ne20 >> 1; + i3 = fastdiv(idx, p.ne22 * p.ne21 * ne20h); + const uint r3 = idx - i3 * p.ne22 * p.ne21 * ne20h; + i2 = fastdiv(r3, p.ne21 * ne20h); + const uint r2 = r3 - i2 * p.ne21 * ne20h; + i1 = r2 / ne20h; + const uint i0h = r2 - i1 * ne20h; + i0 = (i0h / (QUANT_K / 2)) * QUANT_K + (i0h % (QUANT_K / 2)); +} + +void main() { + const uint broadcast2 = uint(p.param2); + const uint broadcast3 = p.param3; + + const uint idx = gl_GlobalInvocationID.z * (262144 / 2) + gl_GlobalInvocationID.y * (512 / 2) + gl_GlobalInvocationID.x; + + if (idx < p.ne / 2) { + uint i0, i1, i2, i3; + get_dst_indices_lower(idx, i0, i1, i2, i3); + + const uint i0_upper = i0 + QUANT_K / 2; + + vec2 acc = vec2(0.0); + + for (uint k = 0; k < p.ne01; k++) { + const uint a_block_base = get_aoffset() + (i3 / broadcast3) * p.nb03 + (i2 / broadcast2) * p.nb02 + k * p.nb01; + const uint ib = a_block_base + (i0 / QUANT_K) * p.nb00; + const uint iqs = i0 % (QUANT_K / QUANT_R); + + const vec2 v = dequantize(ib, iqs, 0); + const vec2 dm = get_dm(ib, 0); + const vec2 a_vals = v * dm.x + dm.y; + + const uint b_idx = src1_idx(i1, k, i2, i3); + const float b = data_b[get_boffset() + b_idx]; + acc += a_vals * b; + } + + data_d[get_doffset() + dst_idx(i0, i1, i2, i3)] = acc.x; + data_d[get_doffset() + dst_idx(i0_upper, i1, i2, i3)] = acc.y; + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/out_prod_q8_0.comp b/ggml/src/ggml-vulkan/vulkan-shaders/out_prod_q8_0.comp new file mode 100644 index 000000000000..591695915515 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/out_prod_q8_0.comp @@ -0,0 +1,64 @@ +#version 450 + +#include "types.glsl" +#include "generic_binary_head.glsl" +#include "dequant_funcs.glsl" + +const uint quant_group_sz = 2; +const uint num_threads = 512 / quant_group_sz; +layout(local_size_x = num_threads, local_size_y = 1, local_size_z = 1) in; + +void get_dst_indices(uint idx, out uint i20, out uint i21, out uint i22, out uint i23) { + i23 = fastdiv(idx, (p.ne22*p.ne21*p.ne20)); + const uint i23_offset = i23 * p.ne22*p.ne21*p.ne20; + i22 = fastdiv((idx - i23_offset), (p.ne21*p.ne20)); + const uint i22_offset = i22*p.ne21*p.ne20; + i21 = (idx - i23_offset - i22_offset) / p.ne20; + i20 = idx - i23_offset - i22_offset - i21*p.ne20; +} + +void main() { + const uint broadcast2 = uint(p.param2); + const uint broadcast3 = p.param3; + + uint idx = gl_GlobalInvocationID.z * 262144 + gl_GlobalInvocationID.y * 512 + gl_GlobalInvocationID.x * quant_group_sz; + + uint aoffset = get_aoffset(); + uint boffset = get_boffset(); + uint doffset = get_doffset(); + + if (idx < p.ne) { + uint i0, i1, i2, i3; + get_dst_indices(idx, i0, i1, i2, i3); + + vec2 acc = vec2(0.0); + + for (uint k = 0; k < p.ne01; k++) { + if (i0 + 1 >= p.ne20) { + continue; + } + + const uint a_block_base = aoffset + (i3 / broadcast3) * p.nb03 + (i2 / broadcast2) * p.nb02 + k * p.nb01; + const uint ib = a_block_base + ((i0) / QUANT_K) * p.nb00; + const uint iqs = ((i0) % QUANT_K) / QUANT_R; + + const vec2 v = dequantize(ib, iqs, 0); + const vec2 dm = get_dm(ib, 0); + const vec2 a_vals = v * dm.x + dm.y; + + const uint b_idx = src1_idx(i1, k, i2, i3); + const float b = data_b[boffset + b_idx]; + + acc += a_vals * b; + } + + uint d_idx = dst_idx(i0, i1, i2, i3); + for (uint q = 0; q < quant_group_sz; q++) { + if (d_idx + q >= p.ne) { + continue; + } + + data_d[doffset + d_idx + q] = acc[q]; + } + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/out_prod_tq2_0.comp b/ggml/src/ggml-vulkan/vulkan-shaders/out_prod_tq2_0.comp new file mode 100644 index 000000000000..d1dfd86159f9 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/out_prod_tq2_0.comp @@ -0,0 +1,54 @@ +#version 450 + +#include "types.glsl" +#include "generic_binary_head.glsl" +#include "dequant_funcs.glsl" +#include "tq_utils.comp" + +const uint num_threads = 256; +layout(local_size_x = num_threads, local_size_y = 1, local_size_z = 1) in; + +void get_dst_indices(uint idx, out uint i20, out uint i21, out uint i22, out uint i23) { + i23 = fastdiv(idx, (p.ne22*p.ne21*p.ne20)); + const uint i23_offset = i23 * p.ne22*p.ne21*p.ne20; + i22 = fastdiv((idx - i23_offset), (p.ne21*p.ne20)); + const uint i22_offset = i22*p.ne21*p.ne20; + i21 = (idx - i23_offset - i22_offset) / p.ne20; + i20 = idx - i23_offset - i22_offset - i21*p.ne20; +} + +void main() { + // num_threads * num_iter must equal 512 to match the wg_denoms and get_idx + const uint num_iter = 2; + + const uint broadcast2 = uint(p.param2); + const uint broadcast3 = p.param3; + + uint idx = get_idx(); + + [[unroll]] for (uint it = 0; it < num_iter; ++it) { + if (idx < p.ne) { + uint i0, i1, i2, i3; + get_dst_indices(idx, i0, i1, i2, i3); + + float acc = 0.0f; + + for (uint k = 0; k < p.ne01; k += 1) { + const uint a_block_base = get_aoffset() + (i3 / broadcast3) * p.nb03 + (i2 / broadcast2) * p.nb02 + k * p.nb01; + const uint ib = a_block_base + (i0 / QUANT_K); + const uint e = i0 % QUANT_K; + + const vec2 dm = get_dm(ib, 0); + const float a_val = dm.x * FLOAT_TYPE(tq2_dequantize(ib, e)); + + const uint b_idx = src1_idx(i1, k, i2, i3); + const float b = data_b[get_boffset() + b_idx]; + acc += a_val * b; + } + + uint d_idx = dst_idx(i0, i1, i2, i3); + data_d[get_doffset() + d_idx] = acc; + } + idx += num_threads; + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/quantize_q8_1.comp b/ggml/src/ggml-vulkan/vulkan-shaders/quantize_q8_1.comp index 20e45d0253e3..91d1a8a44702 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/quantize_q8_1.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/quantize_q8_1.comp @@ -86,10 +86,28 @@ void quantize() { vals = round(vals * d_inv); #ifndef QBLOCK_X4 +#if defined(ADRENO) + i8vec4 q = i8vec4(round(vals)); + data_b[ib].qs[iqs] = + int((uint(q.x) & 0xFFu) | + ((uint(q.y) & 0xFFu) << 8) | + ((uint(q.z) & 0xFFu) << 16) | + ((uint(q.w) & 0xFFu) << 24)); +#else data_b[ib].qs[iqs] = pack32(i8vec4(round(vals))); +#endif +#else +#if defined(ADRENO) + i8vec4 q = i8vec4(round(vals)); + data_b[ibx4_outer].qs[ibx4_inner * 8 + iqs] = + int((uint(q.x) & 0xFFu) | + ((uint(q.y) & 0xFFu) << 8) | + ((uint(q.z) & 0xFFu) << 16) | + ((uint(q.w) & 0xFFu) << 24)); #else data_b[ibx4_outer].qs[ibx4_inner * 8 + iqs] = pack32(i8vec4(round(vals))); #endif +#endif #ifndef USE_SUBGROUPS barrier(); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/tq_utils.comp b/ggml/src/ggml-vulkan/vulkan-shaders/tq_utils.comp new file mode 100644 index 000000000000..11ab34b0c5a9 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/tq_utils.comp @@ -0,0 +1,24 @@ +#ifndef TQ_UTILS_COMP +#define TQ_UTILS_COMP + +#if defined(DATA_A_TQ2_0) +#if defined(TQ2_CM2) +int tq2_dequantize(const in decodeBufTQ2_0 bl, uint iqs) { +#else +int tq2_dequantize(uint ib, uint iqs) { +#endif + const uint upper = iqs / 128; + + const uint byte = (upper * 32) + (iqs % 32); + const uint shift = ((iqs % 128) / 32) * 2; + + #if defined(TQ2_CM2) + const int c = (int(bl.block.qs[byte]) >> shift) & 3; + #else + const int c = (int(data_a[ib].qs[byte]) >> shift) & 3; + #endif + return c - 1; +} +#endif + +#endif diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl index 02578c77c4f3..531f8e04c035 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl @@ -1374,6 +1374,40 @@ struct block_iq4_nl_packed16 #define A_TYPE_PACKED16 block_iq4_nl_packed16 #endif +// TQ1_0 +#define QUANT_K_TQ1_0 256 +#define QUANT_R_TQ1_0 5 + +struct block_tq1_0 { + uint8_t qs[(QUANT_K_TQ1_0 - 4 * QUANT_K_TQ1_0 / 64) / QUANT_R_TQ1_0]; // 5 elements per byte (3^5 = 243 < 256) + uint8_t qh[QUANT_K_TQ1_0/64]; // 4 elements per byte + float16_t d; +}; + +#if defined(DATA_A_TQ1_0) +#define QUANT_K QUANT_K_TQ1_0 +#define QUANT_R QUANT_R_TQ1_0 +#define QUANT_AUXF 1 +#define A_TYPE block_tq1_0 +#endif + +// TQ2_0 +#define QUANT_K_TQ2_0 256 +#define QUANT_R_TQ2_0 4 + +struct block_tq2_0 +{ + uint8_t qs[QUANT_K_TQ2_0/QUANT_R_TQ2_0]; // 256/4 = 64 bytes + float16_t d; +}; + +#if defined(DATA_A_TQ2_0) +#define QUANT_K QUANT_K_TQ2_0 +#define QUANT_R QUANT_R_TQ2_0 +#define QUANT_AUXF 1 +#define A_TYPE block_tq2_0 +#endif + #define QUANT_K_MXFP4 32 #define QUANT_R_MXFP4 2 diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 92bae088b207..11fb2e201fb9 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -41,6 +41,7 @@ std::string input_filepath = ""; std::string output_dir = "/tmp"; std::string target_hpp = ""; std::string target_cpp = ""; +std::string target_common_cpp = ""; const std::vector type_names = { "f32", @@ -50,6 +51,8 @@ const std::vector type_names = { "q5_0", "q5_1", "q8_0", + "tq2_0", + "tq1_0", "q2_k", "q3_k", "q4_k", @@ -424,13 +427,19 @@ void string_to_spv(std::string name, const std::string& source, const std::map base_dict; std::string shader_name = "matmul"; + std::string device_prefix = ""; + + if (adreno) { + base_dict["ADRENO"] = "1"; + device_prefix += "adreno_"; + } if (matmul_id_type == MatMulIdType::DEFAULT) { base_dict["MUL_MAT_ID"] = "1"; @@ -520,11 +529,11 @@ void matmul_shaders(bool fp16, MatMulIdType matmul_id_type, bool coopmat, bool c }; // Shaders with f16 B_TYPE - string_to_spv(shader_name + "_f32_f16", source_name, merge_maps(merge_maps(base_dict, float_type_dict_f16), {{"DATA_A_F32", "1"}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float"}, }), fp16, coopmat, coopmat2, f16acc); - string_to_spv(shader_name + "_f32_f16_aligned", source_name, merge_maps(merge_maps(base_dict, float_type_dict_f16), {{"DATA_A_F32", "1"}, {"LOAD_VEC_A", load_vec}, {"LOAD_VEC_B", load_vec}, {"B_TYPE", aligned_b_type_f16}, {"D_TYPE", "float"}, {"ALIGNED", "1"}}), fp16, coopmat, coopmat2, f16acc); + string_to_spv(device_prefix + shader_name + "_f32_f16", source_name, merge_maps(merge_maps(base_dict, float_type_dict_f16), {{"DATA_A_F32", "1"}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float"}, }), fp16, coopmat, coopmat2, f16acc); + string_to_spv(device_prefix + shader_name + "_f32_f16_aligned", source_name, merge_maps(merge_maps(base_dict, float_type_dict_f16), {{"DATA_A_F32", "1"}, {"LOAD_VEC_A", load_vec}, {"LOAD_VEC_B", load_vec}, {"B_TYPE", aligned_b_type_f16}, {"D_TYPE", "float"}, {"ALIGNED", "1"}}), fp16, coopmat, coopmat2, f16acc); - string_to_spv(shader_name + "_f16", source_name, merge_maps(merge_maps(base_dict, float_type_dict_f16), {{"DATA_A_F16", "1"}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float"}}), fp16, coopmat, coopmat2, f16acc); - string_to_spv(shader_name + "_f16_aligned", source_name, merge_maps(merge_maps(base_dict, float_type_dict_f16), {{"DATA_A_F16", "1"}, {"LOAD_VEC_A", load_vec}, {"LOAD_VEC_B", load_vec}, {"B_TYPE", aligned_b_type_f16}, {"D_TYPE", "float"}, {"ALIGNED", "1"}}), fp16, coopmat, coopmat2, f16acc); + string_to_spv(device_prefix + shader_name + "_f16", source_name, merge_maps(merge_maps(base_dict, float_type_dict_f16), {{"DATA_A_F16", "1"}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float"}}), fp16, coopmat, coopmat2, f16acc); + string_to_spv(device_prefix + shader_name + "_f16_aligned", source_name, merge_maps(merge_maps(base_dict, float_type_dict_f16), {{"DATA_A_F16", "1"}, {"LOAD_VEC_A", load_vec}, {"LOAD_VEC_B", load_vec}, {"B_TYPE", aligned_b_type_f16}, {"D_TYPE", "float"}, {"ALIGNED", "1"}}), fp16, coopmat, coopmat2, f16acc); // bf16 { @@ -545,8 +554,8 @@ void matmul_shaders(bool fp16, MatMulIdType matmul_id_type, bool coopmat, bool c if (!(coopmat || coopmat2)) #endif { - string_to_spv(shader_name + "_bf16", source_name, merge_maps(merge_maps(base_dict, float_type_dict_bf16), {{"TO_FLOAT_TYPE", to_float_type}, {"DATA_A_BF16", "1"}, {"B_TYPE", coopmat2 ? "bfloat16_t" : "uint16_t"}, {"D_TYPE", "float"}, {"B_IS_FLOAT", "1"}, {"DATA_B_BF16", "1"}}), fp16, coopmat, coopmat2, f16acc); - string_to_spv(shader_name + "_bf16_aligned", source_name, merge_maps(merge_maps(base_dict, float_type_dict_bf16), {{"TO_FLOAT_TYPE", to_float_type}, {"DATA_A_BF16", "1"}, {"LOAD_VEC_A", load_vec_a}, {"LOAD_VEC_B", "4"}, {"B_TYPE", coopmat2 ? "bfloat16_t" : "u16vec4"}, {"D_TYPE", "float"}, {"B_IS_FLOAT", "1"}, {"DATA_B_BF16", "1"}, {"ALIGNED", "1"}}), fp16, coopmat, coopmat2, f16acc); + string_to_spv(device_prefix + shader_name + "_bf16", source_name, merge_maps(merge_maps(base_dict, float_type_dict_bf16), {{"TO_FLOAT_TYPE", to_float_type}, {"DATA_A_BF16", "1"}, {"B_TYPE", coopmat2 ? "bfloat16_t" : "uint16_t"}, {"D_TYPE", "float"}, {"B_IS_FLOAT", "1"}, {"DATA_B_BF16", "1"}}), fp16, coopmat, coopmat2, f16acc); + string_to_spv(device_prefix + shader_name + "_bf16_aligned", source_name, merge_maps(merge_maps(base_dict, float_type_dict_bf16), {{"TO_FLOAT_TYPE", to_float_type}, {"DATA_A_BF16", "1"}, {"LOAD_VEC_A", load_vec_a}, {"LOAD_VEC_B", "4"}, {"B_TYPE", coopmat2 ? "bfloat16_t" : "u16vec4"}, {"D_TYPE", "float"}, {"B_IS_FLOAT", "1"}, {"DATA_B_BF16", "1"}, {"ALIGNED", "1"}}), fp16, coopmat, coopmat2, f16acc); } } @@ -576,19 +585,19 @@ void matmul_shaders(bool fp16, MatMulIdType matmul_id_type, bool coopmat, bool c // don't generate f32 variants for coopmat2 if (!coopmat2) { - string_to_spv(shader_name + "_" + tname + "_f32", source_name, merge_maps(merge_maps(base_dict, float_type_dict), {{data_a_key, "1"}, {"LOAD_VEC_A", load_vec_a_unaligned}, {"B_TYPE", "float"}, {"D_TYPE", "float"}}), fp16, coopmat, coopmat2, f16acc); - string_to_spv(shader_name + "_" + tname + "_f32_aligned", source_name, merge_maps(merge_maps(base_dict, float_type_dict), {{data_a_key, "1"}, {"LOAD_VEC_A", load_vec_a}, {"LOAD_VEC_B", load_vec}, {"B_TYPE", aligned_b_type_f32}, {"D_TYPE", "float"}, {"ALIGNED", "1"}}), fp16, coopmat, coopmat2, f16acc); + string_to_spv(device_prefix + shader_name + "_" + tname + "_f32", source_name, merge_maps(merge_maps(base_dict, float_type_dict), {{data_a_key, "1"}, {"LOAD_VEC_A", load_vec_a_unaligned}, {"B_TYPE", "float"}, {"D_TYPE", "float"}}), fp16, coopmat, coopmat2, f16acc); + string_to_spv(device_prefix + shader_name + "_" + tname + "_f32_aligned", source_name, merge_maps(merge_maps(base_dict, float_type_dict), {{data_a_key, "1"}, {"LOAD_VEC_A", load_vec_a}, {"LOAD_VEC_B", load_vec}, {"B_TYPE", aligned_b_type_f32}, {"D_TYPE", "float"}, {"ALIGNED", "1"}}), fp16, coopmat, coopmat2, f16acc); } if (tname != "f16" && tname != "f32") { - string_to_spv(shader_name + "_" + tname + "_f16", source_name, merge_maps(merge_maps(base_dict, float_type_dict), {{data_a_key, "1"}, {"LOAD_VEC_A", load_vec_a_unaligned}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float"}}), fp16, coopmat, coopmat2, f16acc); - string_to_spv(shader_name + "_" + tname + "_f16_aligned", source_name, merge_maps(merge_maps(base_dict, float_type_dict), {{data_a_key, "1"}, {"LOAD_VEC_A", load_vec_a}, {"LOAD_VEC_B", load_vec}, {"B_TYPE", aligned_b_type_f16}, {"D_TYPE", "float"}, {"ALIGNED", "1"}}), fp16, coopmat, coopmat2, f16acc); + string_to_spv(device_prefix + shader_name + "_" + tname + "_f16", source_name, merge_maps(merge_maps(base_dict, float_type_dict), {{data_a_key, "1"}, {"LOAD_VEC_A", load_vec_a_unaligned}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float"}}), fp16, coopmat, coopmat2, f16acc); + string_to_spv(device_prefix + shader_name + "_" + tname + "_f16_aligned", source_name, merge_maps(merge_maps(base_dict, float_type_dict), {{data_a_key, "1"}, {"LOAD_VEC_A", load_vec_a}, {"LOAD_VEC_B", load_vec}, {"B_TYPE", aligned_b_type_f16}, {"D_TYPE", "float"}, {"ALIGNED", "1"}}), fp16, coopmat, coopmat2, f16acc); } #if defined(GGML_VULKAN_INTEGER_DOT_GLSLC_SUPPORT) // Integer dot mmq performs better with f32 accumulators if (!f16acc && !coopmat && !coopmat2 && (is_legacy_quant(tname) || is_k_quant(tname) || tname == "mxfp4")) { - string_to_spv(shader_name + "_" + tname + "_q8_1", "mul_mmq.comp", merge_maps(merge_maps(base_dict, float_type_dict), {{data_a_key, "1"}, {"D_TYPE", "float"},}), fp16, coopmat, coopmat2, f16acc); + string_to_spv(device_prefix + shader_name + "_" + tname + "_q8_1", "mul_mmq.comp", merge_maps(merge_maps(base_dict, float_type_dict), {{data_a_key, "1"}, {"D_TYPE", "float"},}), fp16, coopmat, coopmat2, f16acc); } #endif } @@ -669,6 +678,11 @@ void process_shaders() { // mul mat vec std::string data_a_key = "DATA_A_" + to_uppercase(tname); std::string shader = (string_ends_with(tname, "_k") || string_starts_with(tname, "iq1_") || string_starts_with(tname, "iq2_") || string_starts_with(tname, "iq3_")) ? "mul_mat_vec_" + tname + ".comp" : "mul_mat_vec.comp"; + if (tname == "tq2_0") { + shader = "mul_mat_vec_tq2_0.comp"; + } else if (tname == "tq1_0") { + shader = "mul_mat_vec_tq1_0.comp"; + } string_to_spv("mul_mat_vec_" + tname + "_f32_f32", shader, merge_maps(base_dict, {{data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}})); string_to_spv("mul_mat_vec_" + tname + "_f16_f32", shader, merge_maps(base_dict, {{data_a_key, "1"}, {"B_TYPE", "float16_t"}, {"B_TYPE_VEC2", "f16vec2"}, {"B_TYPE_VEC4", "f16vec4"}, {"D_TYPE", "float"}})); @@ -679,9 +693,11 @@ void process_shaders() { string_to_spv("mul_mat_vec_" + tname + "_f32_f32_subgroup_no_shmem", shader, merge_maps(base_dict, {{data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); string_to_spv("mul_mat_vec_" + tname + "_f16_f32_subgroup_no_shmem", shader, merge_maps(base_dict, {{data_a_key, "1"}, {"B_TYPE", "float16_t"}, {"B_TYPE_VEC2", "f16vec2"}, {"B_TYPE_VEC4", "f16vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); - string_to_spv("mul_mat_vec_id_" + tname + "_f32_f32", shader, merge_maps(base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}})); - string_to_spv("mul_mat_vec_id_" + tname + "_f32_f32_subgroup", shader, merge_maps(base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}})); - string_to_spv("mul_mat_vec_id_" + tname + "_f32_f32_subgroup_no_shmem", shader, merge_maps(base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); + if (tname != "tq2_0" && tname != "tq1_0") { + string_to_spv("mul_mat_vec_id_" + tname + "_f32_f32", shader, merge_maps(base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}})); + string_to_spv("mul_mat_vec_id_" + tname + "_f32_f32_subgroup", shader, merge_maps(base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}})); + string_to_spv("mul_mat_vec_id_" + tname + "_f32_f32_subgroup_no_shmem", shader, merge_maps(base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); + } // mul mat vec with integer dot product #if defined(GGML_VULKAN_INTEGER_DOT_GLSLC_SUPPORT) @@ -693,6 +709,14 @@ void process_shaders() { string_to_spv("mul_mat_vec_id_" + tname + "_q8_1_f32", "mul_mat_vecq.comp", merge_maps(base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}})); string_to_spv("mul_mat_vec_id_" + tname + "_q8_1_f32_subgroup", "mul_mat_vecq.comp", merge_maps(base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}})); string_to_spv("mul_mat_vec_id_" + tname + "_q8_1_f32_subgroup_no_shmem", "mul_mat_vecq.comp", merge_maps(base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); + } else if (tname == "tq2_0") { + string_to_spv("mul_mat_vec_" + tname + "_q8_1_f32", "mul_mat_vec_tq2_0_q.comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}})); + string_to_spv("mul_mat_vec_" + tname + "_q8_1_f32_subgroup", "mul_mat_vec_tq2_0_q.comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}})); + string_to_spv("mul_mat_vec_" + tname + "_q8_1_f32_subgroup_no_shmem", "mul_mat_vec_tq2_0_q.comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); + } else if (tname == "tq1_0") { + string_to_spv("mul_mat_vec_" + tname + "_q8_1_f32", "mul_mat_vec_tq1_0_q.comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}})); + string_to_spv("mul_mat_vec_" + tname + "_q8_1_f32_subgroup", "mul_mat_vec_tq1_0_q.comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}})); + string_to_spv("mul_mat_vec_" + tname + "_q8_1_f32_subgroup_no_shmem", "mul_mat_vec_tq1_0_q.comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); } #endif @@ -892,9 +916,14 @@ void process_shaders() { string_to_spv("leaky_relu_f32", "leaky_relu.comp", {{"A_TYPE", "float"}, {"D_TYPE", "float"}}); string_to_spv("silu_back_f32", "silu_back.comp", {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}}); + string_to_spv("geglu_back_f32", "geglu_back.comp", {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}}); string_to_spv("diag_mask_inf_f32", "diag_mask_inf.comp", {{"A_TYPE", "float"}, {"D_TYPE", "float"}}); + string_to_spv("cross_entropy_loss_back_f32", "cross_entropy_loss_back.comp", {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"C_TYPE", "float"}, {"D_TYPE", "float"}}); + string_to_spv("cross_entropy_loss_masked_back_f32", "cross_entropy_loss_masked_back.comp", {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"C_TYPE", "float"}, {"D_TYPE", "float"}, {"E_TYPE", "float"}}); + string_to_spv("count_equal_masked_i32", "count_equal_masked.comp", {{"A_TYPE", "int"}, {"B_TYPE", "int"}, {"C_TYPE", "float"}, {"D_TYPE", "int64_t"}}); + string_to_spv("soft_max_f32", "soft_max.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}})); string_to_spv("soft_max_f32_f16", "soft_max.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float"}})); string_to_spv("soft_max_back_f32", "soft_max_back.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}})); @@ -919,6 +948,12 @@ void process_shaders() { string_to_spv("rope_vision_f16", "rope_vision.comp", {{"A_TYPE", "float16_t"}, {"ROPE_D_TYPE", "float16_t"}}); string_to_spv("rope_vision_f16_rte", "rope_vision.comp", {{"A_TYPE", "float16_t"}, {"ROPE_D_TYPE", "float16_t"}, {"RTE16", "1"}}); + string_to_spv("out_prod_f32", "out_prod.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}})); + string_to_spv("out_prod_f16_f32", "out_prod.comp", merge_maps(base_dict, {{"A_TYPE", "float16_t"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}})); + string_to_spv("out_prod_q4_0", "out_prod_q4_0.comp", merge_maps(base_dict, {{"DATA_A_Q4_0", "1"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}})); + string_to_spv("out_prod_q8_0", "out_prod_q8_0.comp", merge_maps(base_dict, {{"DATA_A_Q8_0", "1"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}})); + string_to_spv("out_prod_tq2_0", "out_prod_tq2_0.comp", merge_maps(base_dict, {{"DATA_A_TQ2_0", "1"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}})); + string_to_spv("argsort_f32", "argsort.comp", {{"A_TYPE", "float"}}); string_to_spv("argsort_large_f32", "argsort_large.comp", {{"A_TYPE", "float"}}); @@ -995,6 +1030,69 @@ void process_shaders() { string_to_spv("topk_moe_f32", "topk_moe.comp", {}); +#ifdef GGML_VULKAN_BUILD_ADRENO_SHADERS + std::vector adreno_shader_types = {"f32", "f16", "q4_0", "q4_k", "q4_1", "q5_k", "q6_k", "q8_0"}; + std::string device_pfix = "adreno_"; + auto adreno_base_dict = merge_maps(base_dict, {{"ADRENO", "1"}}); + + for (const auto& tname : adreno_shader_types) { + // mul mat vec + std::string data_a_key = "DATA_A_" + to_uppercase(tname); + std::string shader = (string_ends_with(tname, "_k") || string_starts_with(tname, "iq1_") || string_starts_with(tname, "iq2_") || string_starts_with(tname, "iq3_")) ? "mul_mat_vec_" + tname + ".comp" : "mul_mat_vec.comp"; + + string_to_spv(device_pfix + "mul_mat_vec_" + tname + "_f32_f32", shader, merge_maps(adreno_base_dict, {{data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}})); + string_to_spv(device_pfix + "mul_mat_vec_" + tname + "_f16_f32", shader, merge_maps(adreno_base_dict, {{data_a_key, "1"}, {"B_TYPE", "float16_t"}, {"B_TYPE_VEC2", "f16vec2"}, {"B_TYPE_VEC4", "f16vec4"}, {"D_TYPE", "float"}})); + + string_to_spv(device_pfix + "mul_mat_vec_" + tname + "_f32_f32_subgroup", shader, merge_maps(adreno_base_dict, {{data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}})); + string_to_spv(device_pfix + "mul_mat_vec_" + tname + "_f16_f32_subgroup", shader, merge_maps(adreno_base_dict, {{data_a_key, "1"}, {"B_TYPE", "float16_t"}, {"B_TYPE_VEC2", "f16vec2"}, {"B_TYPE_VEC4", "f16vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}})); + + string_to_spv(device_pfix + "mul_mat_vec_" + tname + "_f32_f32_subgroup_no_shmem", shader, merge_maps(adreno_base_dict, {{data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); + string_to_spv(device_pfix + "mul_mat_vec_" + tname + "_f16_f32_subgroup_no_shmem", shader, merge_maps(adreno_base_dict, {{data_a_key, "1"}, {"B_TYPE", "float16_t"}, {"B_TYPE_VEC2", "f16vec2"}, {"B_TYPE_VEC4", "f16vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); + + string_to_spv(device_pfix + "mul_mat_vec_id_" + tname + "_f32_f32", shader, merge_maps(adreno_base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}})); + string_to_spv(device_pfix + "mul_mat_vec_id_" + tname + "_f32_f32_subgroup", shader, merge_maps(adreno_base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}})); + string_to_spv(device_pfix + "mul_mat_vec_id_" + tname + "_f32_f32_subgroup_no_shmem", shader, merge_maps(adreno_base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPE_VEC2", "vec2"}, {"B_TYPE_VEC4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); + + // mul mat vec with integer dot product +#if defined(GGML_VULKAN_INTEGER_DOT_GLSLC_SUPPORT) + if (is_legacy_quant(tname) || is_k_quant(tname)) { + string_to_spv(device_pfix + "mul_mat_vec_" + tname + "_q8_1_f32", "mul_mat_vecq.comp", merge_maps(adreno_base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}})); + string_to_spv(device_pfix + "mul_mat_vec_" + tname + "_q8_1_f32_subgroup", "mul_mat_vecq.comp", merge_maps(adreno_base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}})); + string_to_spv(device_pfix + "mul_mat_vec_" + tname + "_q8_1_f32_subgroup_no_shmem", "mul_mat_vecq.comp", merge_maps(adreno_base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); + + string_to_spv(device_pfix + "mul_mat_vec_id_" + tname + "_q8_1_f32", "mul_mat_vecq.comp", merge_maps(adreno_base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}})); + string_to_spv(device_pfix + "mul_mat_vec_id_" + tname + "_q8_1_f32_subgroup", "mul_mat_vecq.comp", merge_maps(adreno_base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}})); + string_to_spv(device_pfix + "mul_mat_vec_id_" + tname + "_q8_1_f32_subgroup_no_shmem", "mul_mat_vecq.comp", merge_maps(adreno_base_dict, {{"MUL_MAT_ID", "1"}, {data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPE_VEC2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); + } +#endif + + if (!string_ends_with(tname, "_k")) { + if (!(tname == "f32" || tname == "f16" || tname == "bf16")) { + string_to_spv(device_pfix + "get_rows_" + tname, "get_rows_quant.comp", merge_maps(adreno_base_dict, {{data_a_key, "1"}, {"B_TYPE", "int"}, {"D_TYPE", "float16_t"}})); + string_to_spv(device_pfix + "get_rows_" + tname + "_f32", "get_rows_quant.comp", merge_maps(adreno_base_dict, {{data_a_key, "1"}, {"B_TYPE", "int"}, {"D_TYPE", "float"}})); + } + } + } + + for (std::string tname : {"q4_0", "q4_1", "q8_0"}) { + string_to_spv(device_pfix + "cpy_" + tname + "_f32", "copy_from_quant.comp", {{"ADRENO", "1"}, {"DATA_A_" + to_uppercase(tname), "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}}); + } + + string_to_spv(device_pfix + "quantize_q8_1", "quantize_q8_1.comp", adreno_base_dict); + string_to_spv(device_pfix + "quantize_q8_1_x4", "quantize_q8_1.comp", merge_maps(adreno_base_dict, {{"QBLOCK_X4", "1"}})); + + // matmul + for (const MatMulIdType& matmul_id_type : {MatMulIdType::NONE, MatMulIdType::DEFAULT}) { + // No coopmats + // fp32 + matmul_shaders(false, matmul_id_type, false, false, false, true); + + // fp16, fp32acc and fp16acc + matmul_shaders(true, matmul_id_type, false, false, false, true); + matmul_shaders(true, matmul_id_type, false, false, true, true); + } +#endif + for (auto &c : compiles) { c.wait(); } @@ -1003,10 +1101,19 @@ void process_shaders() { void write_output_files() { std::stringstream hdr = make_generic_stringstream(); std::stringstream src = make_generic_stringstream(); + std::stringstream common = make_generic_stringstream(); hdr << "#include \n\n"; src << "#include \"" << basename(target_hpp) << "\"\n\n"; + hdr << R"( + #include + #include + #include + #include + #include + )"; + std::sort(shader_fnames.begin(), shader_fnames.end()); for (const auto& pair : shader_fnames) { const std::string& name = pair.first; @@ -1037,6 +1144,43 @@ void write_output_files() { } } + // generate a map of base pipeline name to pipeline_(data,len) if a device-specific variant exists + { + std::vector adreno_bases; + adreno_bases.reserve(shader_fnames.size()); + for (const auto &pair : shader_fnames) { + const std::string &nm = pair.first; + if (string_starts_with(nm, "adreno_")) { + adreno_bases.push_back(nm.substr(std::string("adreno_").size(), nm.size())); + } + } + + // Declaration only; single definition will live in one generated .cpp + hdr << "bool ggml_vk_get_adreno_variant(const void * base_data, uint64_t * len_out, const void ** data_out);\n\n"; + + // Type alias in header; map + function defined once in common .cpp + hdr << "using ggml_vk_adreno_map_t = std::unordered_map>;\n\n"; + + if (input_filepath == "") { + common << "#include \"" << basename(target_hpp) << "\"\n\n"; + common << "static const ggml_vk_adreno_map_t ggml_vk_adreno_ptr_map = {\n"; + for (size_t i = 0; i < adreno_bases.size(); ++i) { + const std::string &base = adreno_bases[i]; + common << " { " << base << "_data, { adreno_" << base << "_data, adreno_" << base << "_len } },\n"; + } + common << "};\n\n"; + + common << R"( + bool ggml_vk_get_adreno_variant(const void * base_data, uint64_t * len_out, const void ** data_out) { + if (!base_data || !len_out || !data_out) return false; + auto it = ggml_vk_adreno_ptr_map.find(base_data); + if (it == ggml_vk_adreno_ptr_map.end()) return false; + *len_out = it->second.second; *data_out = it->second.first; return true; + } + )"; + } + } + std::string suffixes[2] = {"_f32", "_f16"}; for (std::string op : {"add", "sub", "mul", "div", "add_rms"}) { hdr << "extern const void * " << op << "_data[2][2][2][2];\n"; @@ -1106,7 +1250,7 @@ void write_output_files() { for (const std::string& btype : btypes) { for (const auto& tname : type_names) { - if (btype == "q8_1" && !is_legacy_quant(tname) && tname != "mxfp4" && !is_k_quant(tname)) { + if (btype == "q8_1" && !is_legacy_quant(tname) && tname != "tq2_0" && tname != "tq1_0" && tname != "mxfp4" && !is_k_quant(tname)) { continue; } hdr << "extern const void * arr_dmmv_" << tname << "_" << btype << "_f32_data[3];\n"; @@ -1116,7 +1260,7 @@ void write_output_files() { src << "const uint64_t arr_dmmv_" << tname << "_" << btype << "_f32_len[3] = {mul_mat_vec_" << tname << "_" << btype << "_f32_len, mul_mat_vec_" << tname << "_" << btype << "_f32_subgroup_len, mul_mat_vec_" << tname << "_" << btype << "_f32_subgroup_no_shmem_len};\n"; } - if (btype == "f16") { + if (btype == "f16" || tname == "tq2_0" || tname == "tq1_0") { continue; } hdr << "extern const void * arr_dmmv_id_" << tname << "_" << btype << "_f32_data[3];\n"; @@ -1130,6 +1274,9 @@ void write_output_files() { if (input_filepath == "") { write_file_if_changed(target_hpp, hdr.str()); + if (!target_common_cpp.empty()) { + write_binary_file(target_common_cpp, common.str()); + } } if (target_cpp != "") { write_binary_file(target_cpp, src.str()); @@ -1167,6 +1314,9 @@ int main(int argc, char** argv) { if (args.find("--target-cpp") != args.end()) { target_cpp = args["--target-cpp"]; // Path to generated cpp file } + if (args.find("--target-common-cpp") != args.end()) { + target_common_cpp = args["--target-common-cpp"]; // Path to generated common cpp file + } if (!directory_exists(output_dir)) { if (!create_directory(output_dir)) { diff --git a/ggml/src/ggml.c b/ggml/src/ggml.c index 17cf4d84bb8f..a939b9afd697 100644 --- a/ggml/src/ggml.c +++ b/ggml/src/ggml.c @@ -939,10 +939,12 @@ static const char * GGML_OP_NAME[GGML_OP_COUNT] = { "MEAN", "ARGMAX", "COUNT_EQUAL", + "COUNT_EQUAL_MASKED", "REPEAT", "REPEAT_BACK", "CONCAT", "SILU_BACK", + "GEGLU_BACK", "NORM", "RMS_NORM", "RMS_NORM_BACK", @@ -1018,13 +1020,15 @@ static const char * GGML_OP_NAME[GGML_OP_COUNT] = { "CROSS_ENTROPY_LOSS", "CROSS_ENTROPY_LOSS_BACK", + "CROSS_ENTROPY_LOSS_MASKED", + "CROSS_ENTROPY_LOSS_MASKED_BACK", "OPT_STEP_ADAMW", "OPT_STEP_SGD", "GLU", }; -static_assert(GGML_OP_COUNT == 95, "GGML_OP_COUNT != 95"); +static_assert(GGML_OP_COUNT == 99, "GGML_OP_COUNT != 99"); static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = { "none", @@ -1048,10 +1052,12 @@ static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = { "Σx/n", "argmax(x)", "count_equal(x)", + "count_equal_masked(x)", "repeat(x)", "repeat_back(x)", "concat(x, y)", "silu_back(x)", + "geglu_back(x)", "norm(x)", "rms_norm(x)", "rms_norm_back(x)", @@ -1127,13 +1133,15 @@ static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = { "cross_entropy_loss(x,y)", "cross_entropy_loss_back(x,y)", + "cross_entropy_loss_masked(x,y)", + "cross_entropy_loss_masked_back(x,y)", "adamw(x)", "sgd(x)", "glu(x)", }; -static_assert(GGML_OP_COUNT == 95, "GGML_OP_COUNT != 95"); +static_assert(GGML_OP_COUNT == 99, "GGML_OP_COUNT != 99"); static_assert(GGML_OP_POOL_COUNT == 2, "GGML_OP_POOL_COUNT != 2"); @@ -2439,6 +2447,26 @@ struct ggml_tensor * ggml_count_equal( return result; } +// ggml_count_equal_masked + +struct ggml_tensor * ggml_count_equal_masked( + struct ggml_context * ctx, + struct ggml_tensor * a, + struct ggml_tensor * b, + struct ggml_tensor * c) { + GGML_ASSERT(ggml_are_same_shape(a, b)); + GGML_ASSERT(c->type == GGML_TYPE_F32); + + struct ggml_tensor * result = ggml_new_tensor_1d(ctx, GGML_TYPE_I64, 1); + + result->op = GGML_OP_COUNT_EQUAL_MASKED; + result->src[0] = a; + result->src[1] = b; + result->src[2] = c; + + return result; +} + // ggml_repeat struct ggml_tensor * ggml_repeat( @@ -2745,6 +2773,19 @@ struct ggml_tensor * ggml_silu_back( return result; } +// ggml_geglu_back +struct ggml_tensor * ggml_geglu_back( + struct ggml_context * ctx, + struct ggml_tensor * grad, + struct ggml_tensor * g) { + struct ggml_tensor * result = ggml_dup_tensor(ctx, g); + + result->op = GGML_OP_GEGLU_BACK; + result->src[0] = grad; + result->src[1] = g; + + return result; +} // ggml hardswish struct ggml_tensor * ggml_hardswish( @@ -5934,6 +5975,9 @@ struct ggml_tensor * ggml_cross_entropy_loss( result->src[0] = a; result->src[1] = b; + // Initialize op_params to 0 (no masking) + *(int32_t *)(result->op_params) = 0; + return result; } @@ -5957,6 +6001,51 @@ struct ggml_tensor * ggml_cross_entropy_loss_back( return result; } +// ggml_cross_entropy_loss_masked + +struct ggml_tensor * ggml_cross_entropy_loss_masked( + struct ggml_context * ctx, + struct ggml_tensor * a, + struct ggml_tensor * b, + struct ggml_tensor * c) { + GGML_ASSERT(ggml_are_same_shape(a, b)); + GGML_ASSERT(ggml_are_same_shape(a, c)); + GGML_ASSERT(c->type == GGML_TYPE_F32); + + struct ggml_tensor * result = ggml_new_tensor_1d(ctx, a->type, 1); + + result->op = GGML_OP_CROSS_ENTROPY_LOSS_MASKED; + result->src[0] = a; + result->src[1] = b; + result->src[2] = c; + + return result; +} + +// ggml_cross_entropy_loss_masked_back + +struct ggml_tensor * ggml_cross_entropy_loss_masked_back( + struct ggml_context * ctx, + struct ggml_tensor * a, + struct ggml_tensor * b, + struct ggml_tensor * c, + struct ggml_tensor * d) { + GGML_ASSERT(ggml_is_scalar(d)); + GGML_ASSERT(ggml_are_same_shape(a, b)); + GGML_ASSERT(ggml_are_same_shape(a, c)); + GGML_ASSERT(c->type == GGML_TYPE_F32); + + struct ggml_tensor * result = ggml_dup_tensor(ctx, a); + + result->op = GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK; + result->src[0] = d; + result->src[1] = a; + result->src[2] = b; + result->src[3] = c; + + return result; +} + // opt_step_adamw struct ggml_tensor * ggml_opt_step_adamw( @@ -6140,11 +6229,12 @@ static void ggml_acc_or_set( const size_t offset) { struct ggml_tensor * src = cgraph->visited_hash_set.keys[isrc]; GGML_ASSERT(src); + struct ggml_tensor * tensor_cont = ggml_is_contiguous(tensor) ? tensor : ggml_cont(ctx, tensor); if (cgraph->grads[isrc]) { - cgraph->grads[isrc] = ggml_acc_impl(ctx, cgraph->grads[isrc], tensor, nb1, nb2, nb3, offset, cgraph->grad_accs[isrc]); + cgraph->grads[isrc] = ggml_acc_impl(ctx, cgraph->grads[isrc], tensor_cont, nb1, nb2, nb3, offset, cgraph->grad_accs[isrc]); } else { struct ggml_tensor * a_zero = ggml_scale(ctx, src, 0.0f); // FIXME this is going to produce NaN if a contains inf/NaN - cgraph->grads[isrc] = ggml_acc_impl(ctx, a_zero, tensor, nb1, nb2, nb3, offset, false); + cgraph->grads[isrc] = ggml_acc_impl(ctx, a_zero, tensor_cont, nb1, nb2, nb3, offset, false); } ggml_format_name(cgraph->grads[isrc], "grad for %s", cgraph->visited_hash_set.keys[isrc]->name); ggml_build_forward_expand(cgraph, cgraph->grads[isrc]); @@ -6637,6 +6727,13 @@ static void ggml_compute_backward( } GGML_ASSERT(!src1_needs_grads && "backward pass for labels not implemented"); } break; + case GGML_OP_CROSS_ENTROPY_LOSS_MASKED: { + if (src0_needs_grads) { + struct ggml_tensor * mask_tensor = tensor->src[2]; + ggml_add_or_set(ctx, cgraph, isrc0, ggml_cross_entropy_loss_masked_back(ctx, src0, src1, mask_tensor, grad)); + } + GGML_ASSERT(!src1_needs_grads && "backward pass for labels not implemented"); + } break; case GGML_OP_GLU: { switch (ggml_get_glu_op(tensor)) { case GGML_GLU_OP_SWIGLU: { @@ -6648,6 +6745,16 @@ static void ggml_compute_backward( ggml_add_or_set(ctx, cgraph, isrc1, ggml_mul(ctx, ggml_silu(ctx, src0), grad)); } } break; + case GGML_GLU_OP_GEGLU: { + if (src0_needs_grads) { + GGML_ASSERT(src1 && "backward pass only implemented for split geglu"); + struct ggml_tensor * grad_mul_src1 = ggml_mul(ctx, grad, src1); + ggml_add_or_set(ctx, cgraph, isrc0, ggml_geglu_back(ctx, grad_mul_src1, src0)); + } + if (src1_needs_grads) { + ggml_add_or_set(ctx, cgraph, isrc1, ggml_mul(ctx, grad, ggml_gelu(ctx, src0))); + } + } break; default: { GGML_ABORT("unsupported glu op for backward pass: %s", ggml_glu_op_name(ggml_get_glu_op(tensor))); } //break; @@ -6799,6 +6906,10 @@ void ggml_build_backward_expand( case GGML_OP_ROPE: // positions not differentiable ignore_src[1] = true; break; + case GGML_OP_SET_ROWS: + ignore_src[0] = true; + ignore_src[1] = true; + break; default: break; @@ -7469,6 +7580,7 @@ size_t ggml_quantize_chunk( case GGML_TYPE_Q5_0: result = quantize_q5_0(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; case GGML_TYPE_Q5_1: result = quantize_q5_1(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; case GGML_TYPE_Q8_0: result = quantize_q8_0(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; + case GGML_TYPE_Q8_1: result = quantize_q8_1(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; case GGML_TYPE_MXFP4: result = quantize_mxfp4(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; case GGML_TYPE_Q2_K: result = quantize_q2_K(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; case GGML_TYPE_Q3_K: result = quantize_q3_K(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; diff --git a/ggml/src/gguf.cpp b/ggml/src/gguf.cpp index b165d8bdc62f..65c61a90369d 100644 --- a/ggml/src/gguf.cpp +++ b/ggml/src/gguf.cpp @@ -2,6 +2,7 @@ #include "ggml-backend.h" #include "ggml-impl.h" #include "gguf.h" +#include "uint8-buff-stream.h" #include #include @@ -216,14 +217,79 @@ struct gguf_context { void * data = nullptr; }; -struct gguf_reader { +struct gguf_bytes_reader { + /// @brief Reads up to `count` objects into the array `buffer`. + /// The position of the underlying stream implementation is advanced + /// by the number of characters read. + /// + /// @note If an error occurs, the resulting value of the underlying stream + /// position indicator is indeterminate. + virtual size_t read(void * buffer, size_t size, size_t count) = 0; + + /// @brief Seeks to a position aligned to the given alignment boundary. + /// @return The current position after alignment, or 0 on error. + virtual size_t align(size_t alignment) = 0; + + virtual ~gguf_bytes_reader() = 0; +}; + +gguf_bytes_reader::~gguf_bytes_reader() {} + +struct gguf_bytes_buffer_reader : public gguf_bytes_reader { + gguf_bytes_buffer_reader(std::basic_streambuf & streambuf) : streambuf(streambuf), offset(0) {} + + ~gguf_bytes_buffer_reader() {} + + size_t read(void * buffer, size_t size, size_t count) override { + size_t total_size = size * count; + auto bytes_read = streambuf.sgetn(static_cast(buffer), total_size); + offset += bytes_read; + return bytes_read; + } + + size_t align(size_t alignment) override { + size_t new_offset = GGML_PAD(offset, alignment); + size_t seek_offset = new_offset - offset; + + auto result = streambuf.pubseekoff(seek_offset, std::ios_base::cur, std::ios_base::in); + if (result == std::streampos(-1)) { + return 0; + } + offset = new_offset; + return offset; + } + + private: + std::basic_streambuf & streambuf; + size_t offset; +}; + +struct gguf_bytes_file_reader : public gguf_bytes_reader { + gguf_bytes_file_reader(FILE * file) : file(file) {} + + ~gguf_bytes_file_reader() {} + + size_t read(void * buffer, size_t size, size_t count) override { return fread(buffer, 1, size * count, file); } + + size_t align(size_t alignment) override { + if (fseek(file, GGML_PAD(ftell(file), alignment), SEEK_SET) != 0) { + return 0; + } + return ftell(file); + } + + private: FILE * file; +}; - gguf_reader(FILE * file) : file(file) {} +struct gguf_reader { + gguf_bytes_reader& bytes_reader; + + gguf_reader(gguf_bytes_reader& bytes_reader) : bytes_reader(bytes_reader) {} template bool read(T & dst) const { - return fread(&dst, 1, sizeof(dst), file) == sizeof(dst); + return bytes_reader.read(&dst, 1, sizeof(dst)) == sizeof(dst); } template @@ -278,11 +344,11 @@ struct gguf_reader { return false; } dst.resize(size); - return fread(dst.data(), 1, dst.length(), file) == dst.length(); + return bytes_reader.read(dst.data(), 1, dst.length()) == dst.length(); } bool read(void * dst, const size_t size) const { - return fread(dst, 1, size, file) == size; + return bytes_reader.read(dst, 1, size) == size; } }; @@ -316,8 +382,8 @@ bool gguf_read_emplace_helper(const struct gguf_reader & gr, std::vectorinfo.size()) == n_tensors); // we require the data section to be aligned, so take into account any padding - if (fseek(file, GGML_PAD(ftell(file), ctx->alignment), SEEK_SET) != 0) { - GGML_LOG_ERROR("%s: failed to seek to beginning of data section\n", __func__); + // store the current file offset - this is where the data section starts + ctx->offset = gr.bytes_reader.align(ctx->alignment); + if (ctx->offset == 0) { + GGML_LOG_ERROR("%s: failed to align data section\n", __func__); gguf_free(ctx); return nullptr; } - // store the current file offset - this is where the data section starts - ctx->offset = ftell(file); - // compute the total size of the data section, taking into account the alignment { ctx->size = 0; @@ -729,6 +798,13 @@ struct gguf_context * gguf_init_from_file_impl(FILE * file, struct gguf_init_par return ctx; } +} + +struct gguf_context * gguf_init_from_file_impl(FILE * file, struct gguf_init_params params) { + gguf_bytes_file_reader bytes_reader(file); + gguf_reader reader(bytes_reader); + return gguf_init_from_reader_impl(reader, params); +} struct gguf_context * gguf_init_from_file(const char * fname, struct gguf_init_params params) { FILE * file = ggml_fopen(fname, "rb"); @@ -743,6 +819,12 @@ struct gguf_context * gguf_init_from_file(const char * fname, struct gguf_init_p return result; } +struct gguf_context * gguf_init_from_buffer(std::basic_streambuf & streambuf, struct gguf_init_params params) { + gguf_bytes_buffer_reader bytes_reader(streambuf); + gguf_reader reader(bytes_reader); + return gguf_init_from_reader_impl(reader, params); +} + void gguf_free(struct gguf_context * ctx) { if (ctx == nullptr) { return; diff --git a/ggml/src/uint8-buff-stream.cpp b/ggml/src/uint8-buff-stream.cpp new file mode 100644 index 000000000000..a69419525665 --- /dev/null +++ b/ggml/src/uint8-buff-stream.cpp @@ -0,0 +1,56 @@ +#include "uint8-buff-stream.h" + +Uint8BufferStreamBuf::Uint8BufferStreamBuf(std::vector && _data) : data(std::move(_data)) { + // Cast uint8_t* to char* for basic_streambuf - this is safe since both are 1-byte types + char* start = reinterpret_cast(data.data()); + setg(start, start, start + data.size()); +} + +Uint8BufferStreamBuf::int_type Uint8BufferStreamBuf::underflow() { + if (gptr() < egptr()) { + return traits_type::to_int_type(*gptr()); + } + return traits_type::eof(); +} + +std::streamsize Uint8BufferStreamBuf::xsgetn(char_type * s, std::streamsize n) { + std::streamsize available = egptr() - gptr(); + std::streamsize to_read = std::min(n, available); + if (to_read > 0) { + std::memcpy(s, gptr(), to_read); + setg(eback(), gptr() + to_read, egptr()); + } + return to_read; +} + +Uint8BufferStreamBuf::pos_type Uint8BufferStreamBuf::seekoff(off_type off, std::ios_base::seekdir dir, + std::ios_base::openmode which) { + if (!(which & std::ios_base::in)) { + return pos_type(off_type(-1)); + } + char_type * new_pos = nullptr; + if (dir == std::ios_base::beg) { + new_pos = eback() + off; + } else if (dir == std::ios_base::cur) { + new_pos = gptr() + off; + } else if (dir == std::ios_base::end) { + new_pos = egptr() + off; + } + if (new_pos >= eback() && new_pos <= egptr()) { + setg(eback(), new_pos, egptr()); + return new_pos - eback(); + } + return pos_type(off_type(-1)); +} + +Uint8BufferStreamBuf::pos_type Uint8BufferStreamBuf::seekpos(pos_type pos, std::ios_base::openmode which) { + if (!(which & std::ios_base::in)) { + return pos_type(off_type(-1)); + } + char_type * new_pos = eback() + pos; + if (new_pos >= eback() && new_pos <= egptr()) { + setg(eback(), new_pos, egptr()); + return pos; + } + return pos_type(off_type(-1)); +} diff --git a/gguf-py/gguf/tensor_mapping.py b/gguf-py/gguf/tensor_mapping.py index a7b097397910..05ccbe48cfd8 100644 --- a/gguf-py/gguf/tensor_mapping.py +++ b/gguf-py/gguf/tensor_mapping.py @@ -956,10 +956,12 @@ class TensorNameMap: MODEL_TENSOR.ATTN_SUB_NORM: ( "model.layers.{bid}.self_attn.inner_attn_ln", # bitnet + "model.layers.{bid}.self_attn.attn_sub_norm", # microsoft-bitnet ), MODEL_TENSOR.FFN_SUB_NORM: ( "model.layers.{bid}.mlp.ffn_layernorm", # bitnet + "model.layers.{bid}.mlp.ffn_sub_norm", # microsoft-bitnet ), MODEL_TENSOR.DEC_ATTN_NORM: ( diff --git a/include/llama-cpp.h b/include/llama-cpp.h index 8f6368177de0..2357cfded7d4 100644 --- a/include/llama-cpp.h +++ b/include/llama-cpp.h @@ -5,6 +5,9 @@ #endif #include +#include +#include +#include #include "llama.h" @@ -28,3 +31,34 @@ typedef std::unique_ptr llama_model_ptr; typedef std::unique_ptr llama_context_ptr; typedef std::unique_ptr llama_sampler_ptr; typedef std::unique_ptr llama_adapter_lora_ptr; + +LLAMA_API struct llama_model * llama_model_load_from_buffer(std::vector && data, + struct llama_model_params params); +LLAMA_API bool llama_model_load_fulfill_split_future(const char * path, const char * context, + std::unique_ptr> && streambuf); + +// Read uint32 metadata directly from GGUF metadata without loading tensors. +enum class MetaResultStatus { + SUCCESS = 0, + PATH_NULL = 1, + NULL_OUT_META_HANDLE = 2, + GGUF_INIT_FAILED = 3, + META_HANDLE_NULL = 4, + KEY_NULL = 5, + VALUE_NULL = 6, + KEY_NOT_FOUND = 7, + KV_TYPE_NOT_UINT32 = 8, + STREAMBUF_SEEK_FAILED = 9, + KV_TYPE_NOT_STRING = 10, +}; + +struct llama_metadata_handle; +struct metadata_handle_deleter { + void operator()(struct llama_metadata_handle * ctx) const; +}; +typedef std::unique_ptr metadata_handle_ptr; + +LLAMA_API MetaResultStatus llama_model_meta_get_u32(metadata_handle_ptr const & meta_handle, const char * key, uint32_t * value); +LLAMA_API MetaResultStatus llama_model_meta_get_str(metadata_handle_ptr const & meta_handle, const char * key, std::string * value); +LLAMA_API MetaResultStatus llama_model_meta_from_file(const char * path_model, metadata_handle_ptr * out_meta_handle); +LLAMA_API MetaResultStatus llama_model_meta_from_streambuf(std::basic_streambuf & streambuf, metadata_handle_ptr * out_meta_handle); diff --git a/include/llama.h b/include/llama.h index b52eaacfa7e8..bb07f6d4e728 100644 --- a/include/llama.h +++ b/include/llama.h @@ -363,6 +363,7 @@ extern "C" { bool kv_unified; // use a unified buffer across the input sequences when computing the attention // try to disable when n_seq_max > 1 for improved performance when the sequences do not share a large prefix // ref: https://github.com/ggml-org/llama.cpp/pull/14363 + bool training; // if true, we're in training mode (affects LoRA K/V gradient flow) }; // model quantization parameters @@ -445,6 +446,11 @@ extern "C" { size_t n_paths, struct llama_model_params params); + LLAMA_API struct llama_model * llama_model_load_from_split_futures(const char ** paths, size_t n_paths, + const char * context, + const char * tensor_list_file, + struct llama_model_params params); + LLAMA_API void llama_model_save_to_file( const struct llama_model * model, const char * path_model); @@ -1412,8 +1418,16 @@ extern "C" { void * get_opt_pars_ud; // userdata for calculating optimizer parameters enum ggml_opt_optimizer_type optimizer_type; + + // Optional checkpoint loading + const char * checkpoint_path; // path to checkpoint file to load optimizer state from (nullptr = don't load) + bool load_optimizer_state; // whether to load optimizer state from checkpoint_path + + bool assistant_loss_only; }; + LLAMA_API struct llama_opt_params llama_opt_default_params(void); + LLAMA_API void llama_opt_init(struct llama_context * lctx, struct llama_model * model, struct llama_opt_params lopt_params); LLAMA_API void llama_opt_epoch( @@ -1425,6 +1439,80 @@ extern "C" { ggml_opt_epoch_callback callback_train, ggml_opt_epoch_callback callback_eval); + LLAMA_API void llama_opt_epoch_resume( + struct llama_context * lctx, + ggml_opt_dataset_t dataset, + ggml_opt_result_t result_train, + ggml_opt_result_t result_eval, + int64_t idata_split, + ggml_opt_epoch_callback callback_train, + ggml_opt_epoch_callback callback_eval, + int64_t resume_from_batch); + + // Optimizer state persistence + LLAMA_API bool llama_opt_save_state(struct llama_context * lctx, const char * filename); + LLAMA_API bool llama_opt_load_state(struct llama_context * lctx, const char * filename); + + // Clean up optimizer context to free memory and allow reinitialization + // Call this before calling llama_opt_init() again on the same context + LLAMA_API void llama_opt_cleanup(struct llama_context * lctx); + + // Request early exit from training epoch (thread-safe) + // Call this from a callback or another thread to stop training after the current batch + LLAMA_API void llama_opt_request_stop(struct llama_context * lctx); + + // Reset the stop flag to allow training to continue + // Call this before resuming training after a pause + LLAMA_API void llama_opt_reset_stop(struct llama_context * lctx); + + // LoRA training parameters + enum llama_lora_target_module { + LLAMA_LORA_TARGET_ATTN_Q = 1 << 0, + LLAMA_LORA_TARGET_ATTN_K = 1 << 1, + LLAMA_LORA_TARGET_ATTN_V = 1 << 2, + LLAMA_LORA_TARGET_ATTN_O = 1 << 3, + LLAMA_LORA_TARGET_FFN_GATE = 1 << 4, + LLAMA_LORA_TARGET_FFN_UP = 1 << 5, + LLAMA_LORA_TARGET_FFN_DOWN = 1 << 6, + LLAMA_LORA_TARGET_OUTPUT = 1 << 7, + LLAMA_LORA_TARGET_ALL = -1 + }; + + struct llama_lora_training_params { + uint32_t target_modules; + int32_t rank; + float alpha; + float dropout; // reserved, not yet implemented + float init_std; + uint32_t seed; // seed for reproducible weight initialization (0 = non-deterministic) + }; + + // Initialize LoRA training with the given parameters + // Creates LoRA tensors and adds them to the model context + LLAMA_API struct llama_adapter_lora * llama_lora_training_init( + struct llama_context * ctx, + struct llama_model * model, + const struct llama_lora_training_params * params + ); + + // LoRA parameter filter (returns true for LoRA tensors only) + LLAMA_API bool llama_opt_param_filter_lora(const struct ggml_tensor * tensor, void * userdata); + + LLAMA_API int64_t llama_opt_get_iter(struct llama_context * ctx); + + LLAMA_API bool llama_lora_save_adapter( + const struct llama_adapter_lora * adapter, + const char * filename, + const struct llama_model * model + ); + + LLAMA_API bool llama_lora_save_checkpoint( + const struct llama_adapter_lora * adapter, + const char * filename, + const struct llama_model * model, + struct llama_context * ctx + ); + #ifdef __cplusplus } #endif diff --git a/merging_strategy.md b/merging_strategy.md new file mode 100644 index 000000000000..3f22092f7112 --- /dev/null +++ b/merging_strategy.md @@ -0,0 +1,125 @@ +# LlamaCPP Fork Management Strategy + +## Problem Statement + +The team maintains a fork of llama.cpp with custom modifications but needs to stay synchronized with the rapidly evolving upstream repository. Traditional PR-based merging creates merge commits or squashes that modify git history, making future upstream synchronization difficult. + +## Proposed Solution + +### Branch Structure + +- **temp-upstream**: Tracks the official upstream llama.cpp repository (pure upstream) +- **temp-latest**: Contains team's custom changes rebased on top of upstream +- **Feature branches**: Built on top of temp-latest for new changes + +### Synchronization Process + +#### Initial Setup (one-time) +To make things clear, will start from a blank state. You can skip steps you've already done: + +1. **Fork the repository**: Fork `qvac-ext-lib-llama.cpp` repo in GitHub (e.g., `https://github.com/olyasir/qvac-ext-lib-llama.cpp`) + +2. **Clone locally**: + ```bash + git clone git@github.com:olyasir/qvac-ext-lib-llama.cpp.git + cd qvac-ext-lib-llama.cpp + ``` + +3. **Configure remotes**: + ```bash + # Add upstream ggml remote + git remote add ggml git@github.com:ggml-org/llama.cpp.git + git fetch ggml + + # Add tether remote + git remote add tether git@github.com:tetherto/qvac-ext-lib-llama.cpp.git + git fetch tether + ``` + +#### Regular Synchronization Process + +1. **Prepare temp-latest branch**: + ```bash + git checkout temp-latest + git pull + ``` + +2. **Rebase onto new upstream tag** (e.g., b6789): + ```bash + git rebase b6789 + ``` + +3. **Resolve conflicts**: Git will stop if it finds conflicts. Resolve them as appropriate (may need to check with original writers) + +4. **Push rebased changes**: + ```bash + git push -f + ``` + +5. **Create and push new tag**: + ```bash + git tag b6789.0.0 + # Add description like "Sync with upstream version b6789" + git push tether tag b6789.0.0 + ``` + +6. **Test and publish**: Test the new tag, and if it works properly, publish to vcpkg + +#### Testing Process + +1. **Get test project**: Download the test project from [vcpkg-test-llama-cpp](https://drive.google.com/file/d/1Fm47_QsPsjp-kjPnQpQiRTE5KIrxMh_G/view?usp=sharing) (simple project that depends on the llama-cpp port) + +2. **Update vcpkg port**: + ```bash + # Copy latest ports/llama-cpp folder from qvac-registry-vcpkg + cp -r qvac-registry-vcpkg/ports/llama-cpp vcpkg/ports/llama-cpp + ``` + +3. **Update version**: In `vcpkg/ports/llama-cpp/vcpkg.json`, update version number to new tag (without 'b' prefix) + - For tag `b6789.0.0` → version should be `6789.0.0` + +4. **Initial build attempt**: + ```bash + bare-make generate + ``` + +5. **Fix SHA512 hash**: Configuration will fail with hash mismatch error: + ``` + error: download from https://github.com/tetherto/qvac-ext-lib-llama.cpp/archive/b6435.0.0.tar.gz had an unexpected hash + note: Expected: 9baedc3c4ff681222d8fe16ac10346af9cd7fd5a4a6047448f8a3ad0712ba8e35dbd06af16b3a8c6c8b25518b43fd3b152275e90969f0c33cf851cdb44484eb0 + note: Actual : c869a45e809c367cae6122bfc26c26f16767b010f2da804eb6d20eab8fc9ee8a6fa9c35d04792d0dc1e7483a1b552441027a96ebd30cfb8ac455a3da52801f59 + ``` + Update `vcpkg/ports/llama-cpp/portfile.cmake` - replace SHA512 line with the "Actual" value + +6. **Final verification**: + ```bash + bare-make generate + bare-make build + ``` + If successful, the sync worked properly and you can publish the new vcpkg port version + +### Version Management +*(on temp-latest branch)* + +- Base versions follow upstream tags (e.g., b5932) +- Extended versions add incremental numbers: + - **b5932.0.0**: temp-upstream + mtmd changes + - **b5932.1.0**: temp-upstream + mtmd + load-from-buffer changes + - And so on... + +## Development Workflow + +1. **New PRs**: Create against temp-latest (which contains existing custom changes) +2. **After synchronization**: New PRs pointed to temp-latest should include all our changes and the new upstream version +3. **vcpkg integration**: vcpkg registry points to specific commit hashes, not branch names +4. **Testing**: Test new tags before publishing to vcpkg + +## Benefits + +This strategy ensures the team can maintain their custom modifications while staying current with upstream llama.cpp development. + +- Maintains clean git history aligned with upstream +- Enables easy future synchronization with upstream +- Protects against accidental direct merges to main +- Allows multiple teams (including external collaborators like Collabora) to build on top of stable versions +- Custom changes accumulate incrementally while staying current with upstream diff --git a/pyrightconfig.json b/pyrightconfig.json index 5320fe5864a8..d7b32ae2a938 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -19,4 +19,7 @@ "pythonVersion": "3.10", }, ], + "exclude": [ + "scripts/tune" + ] } diff --git a/scripts/tune/tune.py b/scripts/tune/tune.py new file mode 100644 index 000000000000..eff17d306369 --- /dev/null +++ b/scripts/tune/tune.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +""" +Optimize runtime parameters for llama-simple binary using eval time measurements. +Usage: python tune_tps.py --model /path/to/model.gguf +""" +import os +import time +import argparse +from functools import partial + +import numpy as np +# pip install scikit-optimize +from skopt import gp_minimize, expected_minimum +from skopt.plots import plot_objective, plot_convergence +from skopt.space import Categorical +import matplotlib.pyplot as plt +import json + +BAD_CONFIGURATIONS = [] + +# Progress tracking global variables +progress_start_time = None +progress_current_call = 0 +progress_total_calls = 0 +progress_best_score = float('inf') + +def display_progress(): + """Display current optimization progress with time estimates.""" + global progress_start_time, progress_current_call, progress_total_calls, progress_best_score + + if progress_start_time is None: + return + + elapsed_time = time.time() - progress_start_time + if progress_current_call > -1: + avg_time_per_call = elapsed_time / progress_current_call + remaining_calls = progress_total_calls - progress_current_call + estimated_remaining_time = avg_time_per_call * remaining_calls + + progress_percent = (progress_current_call / progress_total_calls) * 100 + + print(f"\n{'='*60}") + print(f"OPTIMIZATION PROGRESS") + print(f"{'='*60}") + print(f"Iteration: {progress_current_call}/{progress_total_calls} ({progress_percent:.1f}%)") + print(f"Elapsed time: {elapsed_time:.1f}s") + print(f"Est. remaining time: {estimated_remaining_time:.1f}s") + print(f"Best metric so far: {progress_best_score:.4f}") + print(f"{'='*60}\n") + +def run_iterations(get_opts_fn, run_binary_fn, run_options, model_path, binary_path="./build/bin/llama-cli", iterations=1): + """Run llama-siple with specified options and return eval time.""" + try: + run_options_str = get_opts_fn(run_options, model_path, binary_path) + print(run_options_str) + + results = [] + + # Run the test (can increase iterations for more stable results) + for _ in range(iterations): + results.append(run_binary_fn(run_options_str)) + + # Return eval time as the objective (we want to minimize this) + return np.mean(results) + + except Exception as e: + BAD_CONFIGURATIONS.append(run_options) + print("ERROR:", e, run_options) + print("BAD_CONFIGURATIONS:", BAD_CONFIGURATIONS) + return 1000 # High penalty for failed runs + + +def optimize_runtime_with_progress(x, get_opts_fn, run_binary_fn, run_options_list, model_path, llama_simple_path): + """Objective function for optimization with progress tracking.""" + global progress_current_call, progress_best_score + + progress_current_call += 1 + + run_options = { + run_options_list[i][0]: run_options_list[i][1][run_options_list[i][1].index(x[i])] + for i in range(len(run_options_list)) + } + + result = run_iterations(get_opts_fn, run_binary_fn, run_options, model_path, llama_simple_path) + + # Update best score + if result < progress_best_score: + progress_best_score = result + + # Display progress every call + display_progress() + + return result + + +def load_cache(cache_filename): + """Load cached optimization results.""" + try: + with open(cache_filename, "r") as cache_file: + cache_data = json.load(cache_file) + return cache_data["x0"], cache_data["y0"] + except: + pass + return None, None + + +def save_cache(cache_filename, x0, y0): + """Save optimization results to cache.""" + # Convert numpy int64 objects to Python int objects + x0 = [[int(item) if isinstance(item, np.int64) else item for item in sublist] for sublist in x0] + y0 = [int(item) if isinstance(item, np.int64) else item for item in y0] + + cache_data = {"x0": x0, "y0": y0} + with open(cache_filename, "w") as cache_file: + json.dump(cache_data, cache_file) + + +def plot_iterations(result): + """Plot optimization iterations.""" + search_space = result.space + x_iters = result.x_iters + func_vals = result.func_vals + search_space_names = [dim.name for dim in search_space] + opts = search_space_names + ["objective_r"] + + num_params = len(opts) + 1 + fig, axs = plt.subplots(num_params, figsize=(8, num_params * 8), sharex=True) + iterations = list(range(1, len(x_iters) + 1)) + + for i, param in enumerate(opts): + if param == "objective_r": + param_values = func_vals + else: + param_index = search_space_names.index(param) + param_values = [x[param_index] for x in x_iters] + + axs[i].scatter(iterations, param_values) + axs[i].set_xlabel("Iteration") + axs[i].set_ylabel(param) + + plot_convergence(result, true_minimum=0, ax=axs[-1]) + return axs + +def parse_args(default_bin): + parser = argparse.ArgumentParser(description='Optimize llama-simple runtime parameters') + parser.add_argument('--model', '-m', required=True, help='Path to the GGUF model file') + parser.add_argument('--ngl', type=int, required=True, help='Max number of GPU layers') + parser.add_argument('--llama-binary', default=default_bin, + help='Path to llama-simple binary (default: ./build/bin/llama-simple)') + parser.add_argument('--n-calls', type=int, default=50, + help='Number of optimization calls (default: 20)') + parser.add_argument('--cache', default='cache_simple.json', + help='Cache file name (default: cache_simple.json)') + parser.add_argument('--single-execution', type=str, + help='Run single execution with specified options (format: "--param1=value1 --param2=value2")') + + args = parser.parse_args() + return args + +def main(args, get_opts_fn, run_binary_fn, run_options_list): + + # Check if llama-simple binary exists + if not os.path.exists(args.llama_binary): + print(f"Error: llama-simple binary not found at {args.llama_binary}") + print("Please build llama.cpp first or specify correct path with --llama-binary") + return + + # Check if model exists + if not os.path.exists(args.model): + print(f"Error: Model file not found at {args.model}") + return + + # Handle single execution mode + if args.single_execution: + try: + print("Single execution") + run_options = args.single_execution + run_iterations(get_opts_fn, run_binary_fn, run_options, args.model, args.llama_binary) + return + except ValueError as e: + print(f"Error parsing single execution options: {e}") + return + + # Initialize progress tracking + global progress_start_time, progress_total_calls + progress_start_time = time.time() + progress_total_calls = args.n_calls + + # Create optimization dimensions + dimensions = [Categorical(opt[1]) for opt in run_options_list] + for i, opt in enumerate(run_options_list): + dimensions[i].name = opt[0] + + # Load cache + x0, y0 = load_cache(args.cache) + + # Create objective function + objective_function = partial(optimize_runtime_with_progress, + get_opts_fn=get_opts_fn, + run_binary_fn=run_binary_fn, + run_options_list=run_options_list, + model_path=args.model, + llama_simple_path=args.llama_binary) + + print(f"Starting optimization with {args.n_calls} calls and {args.ngl} gpu layers...") + print(f"Using model: {args.model}") + print(f"Cache file: {args.cache}") + + # Run optimization + result = gp_minimize(objective_function, dimensions, + n_calls=args.n_calls, + n_initial_points=min(10, args.n_calls), + random_state=42, + x0=x0, y0=y0, + initial_point_generator="lhs") + + # Save results + save_cache(args.cache, result.x_iters, result.func_vals) + + # Print results + print(f"\nBest options found: {result.x}") + print(f"Minimum eval time: {result.fun:.4f} seconds") + + # Convert result.x back to human-readable format - FIX: Find index of value in options list + best_options = {} + for i, (name, options) in enumerate(run_options_list): + # Find the value in result.x[i] and locate its index in the options list + value = result.x[i] + if value in options: + best_options[name] = value + else: + # Fallback: use the first option if value not found + print(f"Warning: Value '{value}' not found in options for {name}, using first option") + best_options[name] = options[0] + + print("\nBest configuration:") + for name, value in best_options.items(): + print(f" {name}: {value}") + + min_x, _ = expected_minimum(result) + print(f"Expected minimum: {min_x}") + + if BAD_CONFIGURATIONS: + print(f"\nBAD_CONFIGURATIONS: {len(BAD_CONFIGURATIONS)}") + + # Plot results + try: + plot_iterations(result) + plot_objective(result) + # Might need PyQt6 + plt.show() + except Exception as e: + print(f"Plotting failed: {e}") diff --git a/scripts/tune/tune_quality.py b/scripts/tune/tune_quality.py new file mode 100644 index 000000000000..ffae2551558d --- /dev/null +++ b/scripts/tune/tune_quality.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 +""" +BERTScore-based translation quality optimization for llama.cpp models. +Uses BERTScore to evaluate translation quality instead of HellaSwag accuracy. +""" +import subprocess +import sys +import os +import re +import json +import hashlib +import numpy as np +from typing import Dict, List, Tuple, Any, Optional +from collections import Counter + +# Import bert_score for translation quality evaluation +import bert_score + +# Import language_tool_python for grammar checking +import language_tool_python + +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, script_dir) +from tune import parse_args, main + +# Configuration +BERTSCORE_MODEL = 'microsoft/deberta-v3-base' + +# Translation benchmarks for quality evaluation +# Tiny subset of https://openslr.org/100 +TRANSLATION_BENCHMARKS = [ + { + "prompt": "Translate the following English text to French:\n\nEnglish: As you can see, it does not look like a slam lesson, it is a language lesson, a language which allows to give orders to machines and computers the language of the 21st century: the computer code.\nFrench:", + "ground_truth": "Comme vous pouvez le constater, il ne s'agit pas d'un cours de slam, il s'agit d'un cours de langue, une langue qui permet de donner des ordres à des machines et à des ordinateurs, la langue du 21e siècle : le code informatique.", + "tool": "fr-FR" + }, + { + "prompt": "Translate the following English text to Spanish:\n\nEnglish: Some years ago, when I was diving in the Lombok Strait, in Indonesia, 98 feet below the water, with that feeling of weightlessness, surrounded by a great biodiversity of reefs, corals, sea turtles, ocean sunfishes and fishes of all colors, I had an intense feeling of connection with nature.\nSpanish:", + "ground_truth": "Hace unos años, cuando me encontraba buceando en el estrecho de Lombok, en Indonesia, a 30 metros debajo del agua, con esa sensación de ingravidez, rodeado de una gran biodiversidad, de arrecifes, de corales, de tortugas, de peces mola mola y de peces de todos los colores, tuve una intensa sensación de estar conectado con la naturaleza.", + "tool": "es-ES" + }, + { + "prompt": "Translate the following English text to Portuguese:\n\nEnglish: Have you ever stopped to think about clothes for disabled people?\nPortuguese:", + "ground_truth": "Vocês já pararam pra pensar como é o vestuário das pessoas com deficiência?", + "tool": "pt-PT" + } +] + +def get_metrics(metrics_filepath: str, ground_truth: str, prediction: str, tool: str) -> Dict[str, Any]: + """ + Calculate BERTScore and other quality metrics for translation evaluation. + Caches results to avoid recomputation. + """ + print(f"Calculating metrics: {metrics_filepath}") + + metrics = { + 'bertscore_model': None, + 'bertscore_P': None, + 'bertscore_R': None, + 'bertscore_F1': None, + 'grammar_errors': None, + 'repetition_score': None, + 'objective_r': None + } + + # Load cached scores + try: + with open(metrics_filepath, 'r', encoding='utf-8') as f: + metrics.update(json.load(f)) + except FileNotFoundError: + pass + + # Calculate BERTScore if not cached or model changed + if (not metrics["bertscore_P"] or not metrics["bertscore_R"] or + not metrics["bertscore_F1"] or metrics["bertscore_model"] != BERTSCORE_MODEL): + try: + metrics["bertscore_model"] = BERTSCORE_MODEL + score = bert_score.score([prediction], [ground_truth], model_type=BERTSCORE_MODEL) + metrics["bertscore_P"], metrics["bertscore_R"], metrics["bertscore_F1"] = ( + score[0].item(), score[1].item(), score[2].item() + ) + except Exception as e: + print(f"Warning: BERTScore calculation failed: {e}") + metrics["bertscore_P"] = metrics["bertscore_R"] = metrics["bertscore_F1"] = 0.0 + + # Calculate grammar errors if not cached + if metrics["grammar_errors"] is None: + metrics["grammar_errors"] = 0.0 + + language_tool = language_tool_python.LanguageTool(tool) + try: + matches = language_tool.check(prediction) + metrics["grammar_errors"] = len(matches) / max(len(prediction.split()), 1) + except Exception as e: + print(f"Warning: Grammar checking failed: {e}") + metrics["grammar_errors"] = 0.0 + + # Calculate repetition score if not cached + if metrics["repetition_score"] is None: + try: + words = prediction.split() + if len(words) > 0: + word_counts = Counter(words) + repeated_words = sum(count - 1 for count in word_counts.values() if count > 1) + metrics["repetition_score"] = repeated_words / len(words) + else: + metrics["repetition_score"] = 0.0 + except Exception as e: + print(f"Warning: Repetition calculation failed: {e}") + metrics["repetition_score"] = 0.0 + + # Calculate objective score (we want to minimize this) + # Higher BERTScore Recall = better translation quality = lower objective value + # Add penalties for grammar errors and repetitions + if metrics["bertscore_R"] is not None: + grammar_penalty = metrics["grammar_errors"] * 0.1 # Small penalty for grammar errors + repetition_penalty = metrics["repetition_score"] * 0.05 # Small penalty for repetitions + metrics["objective_r"] = -(metrics["bertscore_R"] - grammar_penalty - repetition_penalty) + else: + metrics["objective_r"] = 1.0 # Bad score if BERTScore failed + + # Save metrics to cache + try: + with open(metrics_filepath, 'w', encoding='utf-8') as f: + json.dump(metrics, f, indent=2, ensure_ascii=False) + except Exception as e: + print(f"Warning: Failed to save metrics: {e}") + + return metrics + +def run_binary(run_options_str): + """Run the binary and evaluate translation quality using BERTScore.""" + try: + # Parse the command to extract parameters + parts = run_options_str.split() + model_path = None + binary_path = None + + # Find model path and binary path + for i, part in enumerate(parts): + if part == "-m" and i + 1 < len(parts): + model_path = parts[i + 1] + elif part.endswith("llama-cli") or part.endswith("main"): + binary_path = part + + if not model_path or not binary_path: + print("Error: Could not parse model path or binary path from command") + return 100.0 + + # Create output directory for this run + run_hash = hashlib.md5(run_options_str.encode()).hexdigest()[:8] + output_dir = f"translation_eval_{run_hash}" + os.makedirs(output_dir, exist_ok=True) + + all_scores = [] + + # Run translation benchmarks + for i, benchmark in enumerate(TRANSLATION_BENCHMARKS): + print(f"Running benchmark {i+1}/{len(TRANSLATION_BENCHMARKS)}") + + # Build command for this benchmark - use the base command and add benchmark-specific params + benchmark_cmd = run_options_str.split() + + # Add benchmark-specific parameters + benchmark_cmd.extend(["--prompt", benchmark["prompt"]]) + + # Run the command + try: + process = subprocess.run(benchmark_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=120, # 2 minute timeout per benchmark + check=False) + + if process.returncode != 0: + print(f"Warning: Benchmark {i+1} failed with return code {process.returncode}") + print(f"STDERR: {process.stderr.decode()}") + all_scores.append(1.0) # Bad score for failed runs + continue + + # Extract prediction from output + output = process.stdout.decode() + prediction = output.strip() + + # Remove the prompt from prediction if it's included + if benchmark["prompt"] in prediction: + prediction = prediction.split(benchmark["prompt"])[-1].strip() + + # Calculate metrics + metrics_filepath = os.path.join(output_dir, f"benchmark_{i}_metrics.json") + metrics = get_metrics(metrics_filepath, + benchmark["ground_truth"], prediction, benchmark["tool"]) + + objective_score = metrics.get("objective_r", 1.0) + all_scores.append(objective_score) + + print(f"Benchmark {i+1} - BERTScore R: {metrics.get('bertscore_R', 0):.4f}, " + f"Objective: {objective_score:.4f}") + + except subprocess.TimeoutExpired: + print(f"Warning: Benchmark {i+1} timed out") + all_scores.append(1.0) # Bad score for timeouts + except Exception as e: + print(f"Error running benchmark {i+1}: {e}") + all_scores.append(1.0) # Bad score for errors + + # Calculate average score across all benchmarks + if all_scores: + avg_score = np.mean(all_scores) + print(f"Average translation quality objective score: {avg_score:.4f}") + return avg_score + else: + print("Warning: No successful benchmarks") + return 100.0 # Bad score if no benchmarks succeeded + + except Exception as e: + print(f"Error in run_binary: {e}") + return 100.0 # Bad score for any other errors + +if __name__ == "__main__": + args = parse_args(default_bin='./build/bin/llama-cli') + + # Define quality-focused sampling parameters for optimization + run_options_list = [ + # Core Sampling Parameters (Most Critical for Quality) + + # 1. Temperature - Controls randomness vs determinism + ("--temp", [ + "--temp 0.1", # Very focused, deterministic + "--temp 0.3", # Focused, good for factual tasks + "--temp 0.5", # Moderate creativity + "--temp 0.7", # Balanced (recommended default) + "--temp 0.8", # Good balance + "--temp 0.9", # More creative + "--temp 1.0", # Creative but coherent + "--temp 1.2" # More creative, potentially less coherent + ]), + + # 2. Top-p (Nucleus Sampling) - Controls diversity while maintaining quality + ("--top-p", [ + "--top-p 0.5", # Very focused + "--top-p 0.7", # Focused, higher quality + "--top-p 0.8", # Good balance + "--top-p 0.85", # Balanced + "--top-p 0.9", # Good balance (recommended) + "--top-p 0.95", # Standard default + "--top-p 0.98", # More diverse + "--top-p 1.0" # No nucleus filtering + ]), + + # 3. Top-k - Limits token selection to most probable candidates + ("--top-k", [ + "--top-k 10", # Very focused + "--top-k 20", # More focused, higher quality + "--top-k 30", # Balanced + "--top-k 40", # Good balance (default) + "--top-k 50", # Balanced, more diverse + "--top-k 60", # More diverse + "--top-k 80", # Very diverse + "--top-k 100" # Most diverse + ]), + + # 4. Min-p - Filters out low-probability tokens + ("--min-p", [ + "--min-p 0.01", # Very permissive + "--min-p 0.02", # Permissive + "--min-p 0.05", # Good default + "--min-p 0.08", # More restrictive + "--min-p 0.1", # Restrictive, higher quality + "--min-p 0.15", # Very restrictive + "--min-p 0.2" # Extremely restrictive + ]), + + # Repetition Control (Critical for Coherence) + + # 5. Repeat Penalty - Prevents repetitive text + ("--repeat-penalty", [ + "--repeat-penalty 1.0", # Disabled + "--repeat-penalty 1.02", # Very light penalty + "--repeat-penalty 1.05", # Light penalty (recommended) + "--repeat-penalty 1.1", # Moderate penalty (recommended) + "--repeat-penalty 1.15", # Moderate-strong penalty + "--repeat-penalty 1.2", # Strong penalty + "--repeat-penalty 1.25", # Very strong penalty + "--repeat-penalty 1.3" # Extreme penalty + ]), + + # 6. Repeat Last N - How far back to look for repetitions + ("--repeat-last-n", [ + "--repeat-last-n 16", # Short context + "--repeat-last-n 32", # Short-medium context + "--repeat-last-n 64", # Balanced default + "--repeat-last-n 96", # Medium-large context + "--repeat-last-n 128", # Large context + "--repeat-last-n 192", # Very large context + "--repeat-last-n 256" # Maximum context + ]), + + # Advanced Quality Parameters + + # 7. Typical-p - Promotes contextually coherent tokens + ("--typical", [ + "--typical 1.0", # Disabled + "--typical 0.95", # Light filtering + "--typical 0.9", # Recommended for quality + "--typical 0.85", # Moderate filtering + "--typical 0.8", # Strong filtering + "--typical 0.75", # Very strong filtering + "--typical 0.7" # Extreme filtering + ]), + + # 8. Mirostat - Adaptive sampling for consistent quality + ("--mirostat", [ + "--mirostat 0", # Disabled (default) + "--mirostat 1", # Mirostat v1 + "--mirostat 2" # Mirostat v2 (often better quality) + ]), + + # Keep seed constant for reproducible results + ("--seed", ["-s 42"]), + ] + + def run_str(run_options, model_path, binary_path): + """Build command string for llama-cli with translation evaluation.""" + if isinstance(run_options, dict): + run_options = " ".join(run_options.values()) + # Use the main binary for translation evaluation + return f"{binary_path} -m {model_path} --threads 8 -ngl {args.ngl} {run_options}" + + main(args, run_str, run_binary, run_options_list) diff --git a/scripts/tune/tune_requirements.txt b/scripts/tune/tune_requirements.txt new file mode 100644 index 000000000000..50cb56bbe896 --- /dev/null +++ b/scripts/tune/tune_requirements.txt @@ -0,0 +1,3 @@ +language_tool_python +bert_score +scikit-optimize diff --git a/scripts/tune/tune_tps.py b/scripts/tune/tune_tps.py new file mode 100644 index 000000000000..858471348ad1 --- /dev/null +++ b/scripts/tune/tune_tps.py @@ -0,0 +1,80 @@ +import subprocess +import sys +import os +import re + +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, script_dir) +from tune import parse_args, main + +def run_str(run_options, model_path, binary_path): + run_opts = " ".join(run_options.values()) + return f"{binary_path} -m {model_path} -p 'Hello, how are you?' -n 1 {run_opts}" + +def run_binary(run_options_str): + process = subprocess.run(run_options_str, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + check=True, + ) + if process.returncode != 0: + raise Exception(f"Error running: '{run_options_str}':\n{process.stderr}") + + # Parse timing information from stderr + stderr_text = process.stderr.decode() + + # Updated regex patterns for llama-simple output + prompt_eval_time_pattern = r"prompt eval time\s*=\s*([\d.]+)\s*ms" + eval_time_pattern = r"eval time\s*=\s*([\d.]+)\s*ms" + + prompt_match = re.search(prompt_eval_time_pattern, stderr_text) + eval_match = re.search(eval_time_pattern, stderr_text) + + if prompt_match and eval_match: + prompt_eval_time = float(prompt_match.group(1)) / 1000 # Convert to seconds + eval_time = float(eval_match.group(1)) / 1000 # Convert to seconds + else: + # Fallback: look for any timing patterns + print("Warning: Could not parse timing info, using fallback") + print("STDERR:", stderr_text) + return 1000 # High penalty for failed parsing + + print("prompt eval time:", prompt_eval_time) + print("eval time:", eval_time) + + return eval_time + +if __name__ == "__main__": + args = parse_args(default_bin='./build/bin/llama-cli') + # Define runtime options to optimize - Core Performance Parameters + run_options_list = [ + # 1. Batch Processing Parameters (most critical for throughput) + ("--batch-size", ["--batch-size 31", "--batch-size 64", "--batch-size 128", "--batch-size 256", "--batch-size 512", "--batch-size 1024", "--batch-size 2048"]), + ("--ubatch-size", ["--ubatch-size 32", "--ubatch-size 64", "--ubatch-size 128", "--ubatch-size 256", "--ubatch-size 512"]), + + # 2. Context and Memory Parameters + ("--ctx-size", ["-c 512", "-c 1024", "-c 2048", "-c 4096", "-c 8192"]), + ("--defrag-thold", ["--defrag-thold -1", "--defrag-thold 0.1", "--defrag-thold 0.2", "--defrag-thold 0.5"]), + + # 3. GPU Offloading Parameters (critical for GPU performance) + # Set range to a value that makes sense for your model + ("--n-gpu-layers", [f"--n-gpu-layers {i}" for i in range(args.ngl)]), + + # 4. CPU Optimization Parameters + ("--threads", ["-t 4", "-t 8", "-t 12", "-t 16"]), + # ("--prio", ["--prio 0", "--prio 1", "--prio 2"]), + + # 5. Memory and Caching Parameters + # ("--use-mmap", ["", "--no-mmap"]), + ("--use-mlock", ["--mlock", ""]), + ("--kv-unified", ["--kv-unified", ""]), + + # 6. Advanced Performance Features + ("--flash-attn", ["--flash-attn", ""]), + # ("--no-kv-offload", ["--no-kv-offload", ""]), # Empty string means don't use the flag + + # Keep seed constant for reproducible results + ("--seed", ["-s 42"]), + ] + main(args, run_str, run_binary, run_options_list) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fbd538109ba0..861fd8914c81 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,7 +7,6 @@ llama_add_compile_flags() # llama add_library(llama - ../include/llama.h llama.cpp llama-adapter.cpp llama-arch.cpp @@ -22,10 +21,13 @@ add_library(llama llama-io.cpp llama-kv-cache.cpp llama-kv-cache-iswa.cpp + llama-lora-training.cpp llama-memory.cpp llama-memory-hybrid.cpp llama-memory-recurrent.cpp llama-mmap.cpp + llama-model-load-input.cpp + llama-model-load.cpp llama-model-loader.cpp llama-model-saver.cpp llama-model.cpp @@ -142,7 +144,12 @@ set_target_properties(llama PROPERTIES ) target_include_directories(llama PRIVATE .) -target_include_directories(llama PUBLIC ../include) +target_include_directories( + llama + PUBLIC + $ + $) + target_compile_features (llama PRIVATE cxx_std_17) # don't bump target_link_libraries(llama PUBLIC ggml) diff --git a/src/llama-adapter.cpp b/src/llama-adapter.cpp index d8eef75a7ad7..ac0427e7bf97 100644 --- a/src/llama-adapter.cpp +++ b/src/llama-adapter.cpp @@ -393,7 +393,7 @@ static void llama_adapter_lora_init_impl(llama_model & model, const char * path_ // set tensor data { - llama_file gguf_file(path_lora, "rb"); + llama_file_disk gguf_file(path_lora, "rb"); std::vector read_buf; auto set_tensor = [&](ggml_tensor * orig, ggml_tensor * dev) { size_t offs = gguf_get_data_offset(ctx_gguf.get()) + gguf_get_tensor_offset(ctx_gguf.get(), gguf_find_tensor(ctx_gguf.get(), orig->name)); diff --git a/src/llama-context.cpp b/src/llama-context.cpp index e04f0fc4f9ae..b8e0444df71e 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -46,6 +46,7 @@ llama_context::llama_context( cparams.offload_kqv = params.offload_kqv; cparams.no_perf = params.no_perf; cparams.pooling_type = params.pooling_type; + cparams.training = params.training; cparams.warmup = false; cparams.n_ctx = params.n_ctx == 0 ? hparams.n_ctx_train : params.n_ctx; @@ -116,13 +117,17 @@ llama_context::llama_context( } // ref: https://github.com/ggml-org/llama.cpp/pull/17046#discussion_r2503085732 - cparams.n_ctx = GGML_PAD(cparams.n_ctx, 256); + if (!cparams.training) { + cparams.n_ctx = GGML_PAD(cparams.n_ctx, 256); + } if (cparams.kv_unified) { cparams.n_ctx_seq = cparams.n_ctx; } else { cparams.n_ctx_seq = cparams.n_ctx / cparams.n_seq_max; - cparams.n_ctx_seq = GGML_PAD(cparams.n_ctx_seq, 256); + if (!cparams.training) { + cparams.n_ctx_seq = GGML_PAD(cparams.n_ctx_seq, 256); + } if (cparams.n_ctx_seq == 0) { throw std::runtime_error("n_ctx_seq == 0"); @@ -233,8 +238,7 @@ llama_context::llama_context( auto * buft = ggml_backend_get_default_buffer_type(backend.get()); auto backend_type = ggml_backend_dev_type(ggml_backend_get_device(backend.get())); - if (backend_type == GGML_BACKEND_DEVICE_TYPE_CPU && !model.devices.empty()) { - // use the host buffer of the first device CPU for faster transfer of the intermediate state + if (backend_type == GGML_BACKEND_DEVICE_TYPE_CPU && !model.devices.empty() && !cparams.training) { auto * dev = model.devices[0]; auto * host_buft = ggml_backend_dev_host_buffer_type(dev); if (host_buft) { @@ -1333,10 +1337,12 @@ uint32_t llama_context::output_reserve(int32_t n_outputs) { auto * buft = ggml_backend_cpu_buffer_type(); // try to use the host buffer of the device where the output tensor is allocated for faster transfer to system memory - auto * output_dev = model.dev_output(); - auto * output_dev_host_buft = output_dev ? ggml_backend_dev_host_buffer_type(output_dev) : nullptr; - if (output_dev_host_buft) { - buft = output_dev_host_buft; + if (!cparams.training) { + auto * output_dev = model.dev_output(); + auto * output_dev_host_buft = output_dev ? ggml_backend_dev_host_buffer_type(output_dev) : nullptr; + if (output_dev_host_buft) { + buft = output_dev_host_buft; + } } buf_output.reset(ggml_backend_buft_alloc_buffer(buft, new_size)); if (buf_output == nullptr) { @@ -1390,7 +1396,9 @@ uint32_t llama_context::graph_max_nodes() const { if (model.arch == LLM_ARCH_QWEN3NEXT) { return std::max(8192u, 32u*model.n_tensors()); } - return std::max(1024u, 8u*model.n_tensors()); + // Note: inference mode would only need (1024u, 8u*n_tensors), but these values + // are bumped to support LoRA finetuning which requires more graph nodes. + return std::max(2048u, 32u*model.n_tensors()); } llm_graph_result * llama_context::get_gf_res_reserve() const { @@ -1727,7 +1735,7 @@ size_t llama_context::state_seq_set_data(llama_seq_id seq_id, const uint8_t * sr } bool llama_context::state_load_file(const char * filepath, llama_token * tokens_out, size_t n_token_capacity, size_t * n_token_count_out) { - llama_file file(filepath, "rb"); + llama_file_disk file(filepath, "rb"); // sanity checks { @@ -1770,7 +1778,7 @@ bool llama_context::state_load_file(const char * filepath, llama_token * tokens_ } bool llama_context::state_save_file(const char * filepath, const llama_token * tokens, size_t n_token_count) { - llama_file file(filepath, "wb"); + llama_file_disk file(filepath, "wb"); file.write_u32(LLAMA_SESSION_MAGIC); file.write_u32(LLAMA_SESSION_VERSION); @@ -1787,7 +1795,7 @@ bool llama_context::state_save_file(const char * filepath, const llama_token * t } size_t llama_context::state_seq_load_file(llama_seq_id seq_id, const char * filepath, llama_token * tokens_out, size_t n_token_capacity, size_t * n_token_count_out) { - llama_file file(filepath, "rb"); + llama_file_disk file(filepath, "rb"); // version checks { @@ -1830,7 +1838,7 @@ size_t llama_context::state_seq_load_file(llama_seq_id seq_id, const char * file } size_t llama_context::state_seq_save_file(llama_seq_id seq_id, const char * filepath, const llama_token * tokens, size_t n_token_count) { - llama_file file(filepath, "wb"); + llama_file_disk file(filepath, "wb"); file.write_u32(LLAMA_STATE_SEQ_MAGIC); file.write_u32(LLAMA_STATE_SEQ_VERSION); @@ -2092,13 +2100,17 @@ static void llama_set_param(struct ggml_tensor * tensor, llama_opt_param_filter void llama_context::opt_init(struct llama_model * model, struct llama_opt_params lopt_params) { GGML_ASSERT(!opt_ctx); + original_n_ctx_train = model->hparams.n_ctx_train; model->hparams.n_ctx_train = lopt_params.n_ctx_train > 0 ? lopt_params.n_ctx_train : n_ctx(); const uint32_t n_batch = std::min(this->n_batch(), model->hparams.n_ctx_train); const uint32_t n_ubatch = std::min(this->n_ubatch(), n_batch); GGML_ASSERT(model->hparams.n_ctx_train % n_batch == 0); GGML_ASSERT(n_batch % n_ubatch == 0); - ggml_opt_params opt_params = ggml_opt_default_params(sched.get(), GGML_OPT_LOSS_TYPE_CROSS_ENTROPY); + opt_loss_type = lopt_params.assistant_loss_only ? + GGML_OPT_LOSS_TYPE_CROSS_ENTROPY_MASKED : GGML_OPT_LOSS_TYPE_CROSS_ENTROPY; + + ggml_opt_params opt_params = ggml_opt_default_params(sched.get(), opt_loss_type); opt_params.opt_period = n_batch / n_ubatch; opt_params.get_opt_pars = lopt_params.get_opt_pars; opt_params.get_opt_pars_ud = lopt_params.get_opt_pars_ud; @@ -2124,10 +2136,39 @@ void llama_context::opt_init(struct llama_model * model, struct llama_opt_params llama_set_param(model->cls_out_b, param_filter, param_filter_ud); for (struct llama_layer & layer : model->layers) { + static_assert(sizeof(llama_layer) % sizeof(ggml_tensor *) == 0, + "llama_layer has unexpected padding — reinterpret_cast iteration is unsafe"); for (size_t i = 0; i < sizeof(layer)/sizeof(struct ggml_tensor *); ++i) { llama_set_param(reinterpret_cast(&layer)[i], param_filter, param_filter_ud); } } + + // Set LoRA params as trainable if any? + for (const auto & adapter_pair : loras) { + llama_adapter_lora * adapter = adapter_pair.first; + if (adapter) { + // Register lora tensors as params for training + for (const auto & tensor_pair : adapter->ab_map) { + const llama_adapter_lora_weight & weight = tensor_pair.second; + if (weight.a) { + llama_set_param(weight.a, param_filter, param_filter_ud); + } + if (weight.b) { + llama_set_param(weight.b, param_filter, param_filter_ud); + } + } + } + } + + if (lopt_params.load_optimizer_state && lopt_params.checkpoint_path) { + if (opt_load_state(lopt_params.checkpoint_path)) { + pending_optimizer_checkpoint_path = lopt_params.checkpoint_path; + should_load_optimizer_tensors = true; + optimizer_tensors_loaded = false; + } else { + LLAMA_LOG_ERROR("Failed to load optimizer state from: %s\n", lopt_params.checkpoint_path); + } + } } void llama_context::opt_epoch_iter( @@ -2135,6 +2176,7 @@ void llama_context::opt_epoch_iter( ggml_opt_result_t result, const std::vector & tokens, const std::vector & labels_sparse, + const std::vector & masks_sparse, llama_batch & batch, ggml_opt_epoch_callback callback, bool train, @@ -2149,6 +2191,10 @@ void llama_context::opt_epoch_iter( memory->clear(true); for (uint32_t pos_ctx = 0; pos_ctx < n_ctx; pos_ctx += n_batch) { + // Check for early exit request before processing context batch + if (training_should_stop.load(std::memory_order_acquire)) { + return; + } batch.n_tokens = n_batch; for (uint32_t pos_batch = 0; pos_batch < n_batch; ++pos_batch) { batch.token [pos_batch] = tokens[pos_ctx + pos_batch]; @@ -2185,6 +2231,11 @@ void llama_context::opt_epoch_iter( uint32_t pos_batch = 0; do { + // Check for early exit request before processing ubatch + if (training_should_stop.load(std::memory_order_acquire)) { + break; + } + const auto & ubatch = mctx->get_ubatch(); n_outputs = ubatch.n_tokens; @@ -2216,17 +2267,56 @@ void llama_context::opt_epoch_iter( ggml_opt_prepare_alloc(opt_ctx, ctx_compute_opt, gf, res->get_tokens(), res->get_logits()); ggml_opt_alloc(opt_ctx, train); + // Load optimizer tensors on first training iteration if pending + if (train && should_load_optimizer_tensors && !optimizer_tensors_loaded) { + if (ggml_opt_load_tensors(opt_ctx, pending_optimizer_checkpoint_path.c_str())) { + LLAMA_LOG_INFO("Successfully loaded optimizer state tensor data\n"); + optimizer_tensors_loaded = true; + } else { + LLAMA_LOG_ERROR("Failed to load optimizer tensor data\n"); + } + should_load_optimizer_tensors = false; // Only try once + } + res->set_inputs(&ubatch); { struct ggml_tensor * labels = ggml_opt_labels(opt_ctx); + struct ggml_tensor * masks = (opt_loss_type == GGML_OPT_LOSS_TYPE_CROSS_ENTROPY_MASKED && ggml_opt_dataset_masks(dataset)) ? ggml_opt_masks(opt_ctx) : nullptr; // Only get masks if using masked loss GGML_ASSERT(labels->ne[1] == n_ubatch); + GGML_ASSERT(labels->type == GGML_TYPE_F32); + if (masks) { + GGML_ASSERT(masks->ne[1] == n_ubatch); + GGML_ASSERT(masks->type == GGML_TYPE_F32); + } ggml_set_zero(labels); const float onef = 1.0f; for (uint32_t pos_ubatch = 0; pos_ubatch < n_ubatch; ++pos_ubatch) { const uint32_t ilabel = pos_ctx + pos_batch + pos_ubatch; + const uint32_t imask = pos_ctx + pos_batch + pos_ubatch; + if (masks && imask < masks_sparse.size() && masks_sparse[imask] == 0) { + continue; + } + GGML_ASSERT(labels_sparse[ilabel] < labels->ne[0]); ggml_backend_tensor_set(labels, &onef, (pos_ubatch*labels->ne[0] + labels_sparse[ilabel])*sizeof(float), sizeof(float)); } + + if (masks) { + if (pos_batch == 0) { + ggml_set_zero(masks); + } + const float onef = 1.0f; + for (uint32_t pos_ubatch = 0; pos_ubatch < n_ubatch; ++pos_ubatch) { + const uint32_t imask = pos_ctx + pos_batch + pos_ubatch; + + if (imask < masks_sparse.size() && masks_sparse[imask] == 1) { + const size_t offset = (pos_ubatch * masks->ne[0] + 0) * sizeof(float); + ggml_backend_tensor_set(masks, &onef, offset, sizeof(float)); + } + } + + ggml_backend_sched_synchronize(get_sched()); + } } ggml_opt_eval(opt_ctx, result); if (callback) { @@ -2235,6 +2325,11 @@ void llama_context::opt_epoch_iter( ggml_free(ctx_compute_opt); pos_batch += ubatch.n_tokens; + + // Check for early exit request after processing ubatch + if (training_should_stop.load(std::memory_order_acquire)) { + break; + } } while (mctx->next()); } } @@ -2245,7 +2340,11 @@ void llama_context::opt_epoch( ggml_opt_result_t result_eval, int64_t idata_split, ggml_opt_epoch_callback callback_train, - ggml_opt_epoch_callback callback_eval) { + ggml_opt_epoch_callback callback_eval, + int64_t resume_from_batch) { + // Reset stop flag at the start of each epoch to ensure clean state + training_should_stop.store(false, std::memory_order_release); + const uint32_t n_ctx = this->n_ctx(); const uint32_t n_batch = std::min(cparams.n_batch, n_ctx); const uint32_t n_ubatch = std::min(cparams.n_ubatch, n_batch); @@ -2259,34 +2358,112 @@ void llama_context::opt_epoch( struct llama_batch batch = llama_batch_init(n_batch, 0, 1); std::vector tokens(n_ctx); std::vector labels_sparse(n_ctx); + std::vector masks_sparse(n_ctx); - int64_t idata = 0; + int64_t idata = (resume_from_batch >= 0) ? resume_from_batch + 1 : 0; int64_t t_loop_start = ggml_time_us(); int64_t ndata_in_loop = idata_split*ubatch_per_ctx; for (; idata < idata_split; ++idata) { + // Check for early exit request before processing batch + if (training_should_stop.load(std::memory_order_acquire)) { + break; + } + constexpr bool train = true; - const int64_t idata_in_loop = idata*ubatch_per_ctx; + const int64_t idata_in_loop = idata * ubatch_per_ctx; + + if (opt_loss_type == GGML_OPT_LOSS_TYPE_CROSS_ENTROPY_MASKED && ggml_opt_dataset_masks(dataset)) { + ggml_opt_dataset_get_batch_host_with_masks(dataset, tokens.data(), n_ctx*sizeof(llama_token), labels_sparse.data(), masks_sparse.data(), idata); - ggml_opt_dataset_get_batch_host(dataset, tokens.data(), n_ctx*sizeof(llama_token), labels_sparse.data(), idata); - opt_epoch_iter(dataset, result_train, tokens, labels_sparse, batch, + } else { + ggml_opt_dataset_get_batch_host(dataset, tokens.data(), n_ctx*sizeof(llama_token), labels_sparse.data(), idata); + } + opt_epoch_iter(dataset, result_train, tokens, labels_sparse, masks_sparse, batch, callback_train, train, idata_in_loop, ndata_in_loop, t_loop_start); + + // Check again after iteration in case it was set during processing + if (training_should_stop.load(std::memory_order_acquire)) { + break; + } } t_loop_start = ggml_time_us(); ndata_in_loop = (ndata - idata_split)*ubatch_per_ctx; for (; idata < ndata; ++idata) { + // Check for early exit request before processing batch + if (training_should_stop.load(std::memory_order_acquire)) { + break; + } + constexpr bool train = false; const int64_t idata_in_loop = (idata - idata_split)*ubatch_per_ctx; - ggml_opt_dataset_get_batch_host(dataset, tokens.data(), n_ctx*sizeof(llama_token), labels_sparse.data(), idata); - opt_epoch_iter(dataset, result_eval, tokens, labels_sparse, batch, + if (opt_loss_type == GGML_OPT_LOSS_TYPE_CROSS_ENTROPY_MASKED && ggml_opt_dataset_masks(dataset)) { + ggml_opt_dataset_get_batch_host_with_masks(dataset, tokens.data(), n_ctx*sizeof(llama_token), labels_sparse.data(), masks_sparse.data(), idata); + } else { + ggml_opt_dataset_get_batch_host(dataset, tokens.data(), n_ctx*sizeof(llama_token), labels_sparse.data(), idata); + } + opt_epoch_iter(dataset, result_eval, tokens, labels_sparse, masks_sparse, batch, callback_eval, train, idata_in_loop, ndata_in_loop, t_loop_start); + + // Check again after iteration in case it was set during processing + if (training_should_stop.load(std::memory_order_acquire)) { + break; + } } llama_batch_free(batch); } +int64_t llama_context::opt_get_iter() { + if (!opt_ctx) { + return 0; // Return 0 if optimizer not initialized + } + return ggml_opt_get_iter(opt_ctx); +} + +bool llama_context::opt_save_state(const char* filename) { + if (!opt_ctx) { + return false; + } + return ggml_opt_save_state(opt_ctx, filename); +} + +bool llama_context::opt_load_state(const char* filename) { + if (!opt_ctx) { + return false; + } + return ggml_opt_load_state(opt_ctx, filename); +} + +void llama_context::opt_cleanup() { + if (opt_ctx) { + ggml_opt_free(opt_ctx); + opt_ctx = nullptr; + should_load_optimizer_tensors = false; + optimizer_tensors_loaded = false; + pending_optimizer_checkpoint_path.clear(); + } + if (original_n_ctx_train > 0) { + const_cast(model).hparams.n_ctx_train = original_n_ctx_train; + original_n_ctx_train = 0; + } + + ggml_backend_sched_reset(sched.get()); + if (gf_res_prev) { + gf_res_prev->reset(); + } +} + +void llama_context::opt_request_stop() { + training_should_stop.store(true, std::memory_order_release); +} + +void llama_context::opt_reset_stop() { + training_should_stop.store(false, std::memory_order_release); +} + // // interface implementation // @@ -2323,6 +2500,7 @@ llama_context_params llama_context_default_params() { /*.op_offload =*/ true, /*.swa_full =*/ true, /*.kv_unified =*/ false, + /*.training =*/ false, }; return result; @@ -2965,6 +3143,20 @@ bool llama_opt_param_filter_all(const struct ggml_tensor * tensor, void * userda return true; } +struct llama_opt_params llama_opt_default_params(void) { + return { + /*n_ctx_train =*/ 0, + /*param_filter =*/ llama_opt_param_filter_all, + /*param_filter_ud =*/ nullptr, + /*get_opt_pars =*/ nullptr, + /*get_opt_pars_ud =*/ nullptr, + /*optimizer_type =*/ GGML_OPT_OPTIMIZER_TYPE_ADAMW, + /*checkpoint_path =*/ nullptr, + /*load_optimizer_state =*/ false, + /*assistant_loss_only =*/ false, + }; +} + void llama_opt_init(struct llama_context * ctx, struct llama_model * model, struct llama_opt_params lopt_params) { ctx->opt_init(model, lopt_params); } @@ -2983,5 +3175,49 @@ void llama_opt_epoch( result_eval, idata_split, callback_train, - callback_eval); + callback_eval, + /*resume_from_batch=*/ -1); +} + +void llama_opt_epoch_resume( + struct llama_context * ctx, + ggml_opt_dataset_t dataset, + ggml_opt_result_t result_train, + ggml_opt_result_t result_eval, + int64_t idata_split, + ggml_opt_epoch_callback callback_train, + ggml_opt_epoch_callback callback_eval, + int64_t resume_from_batch) { + ctx->opt_epoch( + dataset, + result_train, + result_eval, + idata_split, + callback_train, + callback_eval, + resume_from_batch); +} + +int64_t llama_opt_get_iter(struct llama_context * ctx) { + return ctx->opt_get_iter(); +} + +bool llama_opt_save_state(struct llama_context * ctx, const char * filename) { + return ctx->opt_save_state(filename); +} + +bool llama_opt_load_state(struct llama_context * ctx, const char * filename) { + return ctx->opt_load_state(filename); +} + +void llama_opt_cleanup(struct llama_context * ctx) { + ctx->opt_cleanup(); +} + +void llama_opt_request_stop(struct llama_context * ctx) { + ctx->opt_request_stop(); +} + +void llama_opt_reset_stop(struct llama_context * ctx) { + ctx->opt_reset_stop(); } diff --git a/src/llama-context.h b/src/llama-context.h index 20cbd7895541..3bb78a88436c 100644 --- a/src/llama-context.h +++ b/src/llama-context.h @@ -1,13 +1,13 @@ #pragma once -#include "llama.h" -#include "llama-cparams.h" -#include "llama-graph.h" -#include "llama-adapter.h" - #include "ggml-cpp.h" #include "ggml-opt.h" +#include "llama-adapter.h" +#include "llama-cparams.h" +#include "llama-graph.h" +#include "llama.h" +#include #include #include @@ -167,13 +167,31 @@ struct llama_context { ggml_opt_result_t result_eval, int64_t idata_split, ggml_opt_epoch_callback callback_train, - ggml_opt_epoch_callback callback_eval); + ggml_opt_epoch_callback callback_eval, + int64_t resume_from_batch = -1); + + // Optimizer state access for checkpointing (delegated to ggml_opt API) + int64_t opt_get_iter(); + + // Optimizer state persistence + bool opt_save_state(const char* filename); + bool opt_load_state(const char* filename); + + // Clean up optimizer context to free memory and allow reinitialization + void opt_cleanup(); + + // Request early exit from training epoch (thread-safe) + void opt_request_stop(); + + // Reset the stop flag to allow training to continue + void opt_reset_stop(); void opt_epoch_iter( ggml_opt_dataset_t dataset, ggml_opt_result_t result, const std::vector & tokens, const std::vector & labels_sparse, + const std::vector & masks_sparse, llama_batch & batch, ggml_opt_epoch_callback callback, bool train, @@ -272,6 +290,16 @@ struct llama_context { // training ggml_opt_context_t opt_ctx = nullptr; + uint32_t original_n_ctx_train = 0; + + // optimizer state loading (deferred until after ggml_opt_build) + std::string pending_optimizer_checkpoint_path; + bool should_load_optimizer_tensors = false; + bool optimizer_tensors_loaded = false; + ggml_opt_loss_type opt_loss_type = GGML_OPT_LOSS_TYPE_CROSS_ENTROPY; + + // early exit flag for training epochs (thread-safe) + std::atomic training_should_stop{ false }; ggml_threadpool_t threadpool = nullptr; ggml_threadpool_t threadpool_batch = nullptr; diff --git a/src/llama-cparams.h b/src/llama-cparams.h index fcef8fa97603..e1e33c2225d1 100644 --- a/src/llama-cparams.h +++ b/src/llama-cparams.h @@ -34,6 +34,7 @@ struct llama_cparams { bool warmup; bool op_offload; bool kv_unified; + bool training; enum llama_pooling_type pooling_type; diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp index 42ccb5b76aac..b34be55c7e2d 100644 --- a/src/llama-graph.cpp +++ b/src/llama-graph.cpp @@ -1605,11 +1605,26 @@ ggml_tensor * llm_graph_context::build_attn( ggml_build_forward_expand(gf, mctx_cur->cpy_v(ctx0, v_cur, v_idxs, il)); } - const auto & kq_mask = inp->get_kq_mask(); + ggml_tensor * kq_mask = inp->get_kq_mask(); ggml_tensor * q = q_cur; - ggml_tensor * k = mctx_cur->get_k(ctx0, il); - ggml_tensor * v = mctx_cur->get_v(ctx0, il); + ggml_tensor * k, * v; + + if (loras && !loras->empty() && k_cur && v_cur && cparams.training) { + k = mctx_cur->get_k_lora(ctx0, k_cur, il); + v = mctx_cur->get_v_lora(ctx0, v_cur, il); + } else { + k = mctx_cur->get_k(ctx0, il); + v = mctx_cur->get_v(ctx0, il); + } + + if (kq_mask->ne[0] != k->ne[2]) { + GGML_ASSERT(k->ne[2] <= kq_mask->ne[0]); + kq_mask = ggml_view_4d(ctx0, kq_mask, + k->ne[2], kq_mask->ne[1], kq_mask->ne[2], kq_mask->ne[3], + kq_mask->nb[1], kq_mask->nb[2], kq_mask->nb[3], 0); + kq_mask = ggml_cont(ctx0, kq_mask); + } ggml_tensor * cur = build_attn_mha(q, k, v, kq_b, kq_mask, sinks, v_mla, kq_scale, il); cb(cur, "kqv_out", il); @@ -1672,11 +1687,27 @@ ggml_tensor * llm_graph_context::build_attn( ggml_build_forward_expand(gf, mctx_cur->cpy_v(ctx0, v_cur, v_idxs, il)); } - const auto & kq_mask = is_swa ? inp->get_kq_mask_swa() : inp->get_kq_mask(); + ggml_tensor * kq_mask = is_swa ? inp->get_kq_mask_swa() : inp->get_kq_mask(); ggml_tensor * q = q_cur; - ggml_tensor * k = mctx_cur->get_k(ctx0, il); - ggml_tensor * v = mctx_cur->get_v(ctx0, il); + ggml_tensor * k, * v; + + if (loras && !loras->empty() && k_cur && v_cur && cparams.training) { + k = mctx_cur->get_k_lora(ctx0, k_cur, il); + v = mctx_cur->get_v_lora(ctx0, v_cur, il); + } else { + k = mctx_cur->get_k(ctx0, il); + v = mctx_cur->get_v(ctx0, il); + } + + // Same rationale as above for the ISWA path. + if (kq_mask->ne[0] != k->ne[2]) { + GGML_ASSERT(k->ne[2] <= kq_mask->ne[0]); + kq_mask = ggml_view_4d(ctx0, kq_mask, + k->ne[2], kq_mask->ne[1], kq_mask->ne[2], kq_mask->ne[3], + kq_mask->nb[1], kq_mask->nb[2], kq_mask->nb[3], 0); + kq_mask = ggml_cont(ctx0, kq_mask); + } ggml_tensor * cur = build_attn_mha(q, k, v, kq_b, kq_mask, sinks, v_mla, kq_scale, il); cb(cur, "kqv_out", il); diff --git a/src/llama-impl.h b/src/llama-impl.h index c3391e79f516..dd3975a84bce 100644 --- a/src/llama-impl.h +++ b/src/llama-impl.h @@ -30,6 +30,13 @@ void llama_log_callback_default(ggml_log_level level, const char * text, void * #define LLAMA_LOG_DEBUG(...) llama_log_internal(GGML_LOG_LEVEL_DEBUG, __VA_ARGS__) #define LLAMA_LOG_CONT(...) llama_log_internal(GGML_LOG_LEVEL_CONT , __VA_ARGS__) +// Debug-only logging macro that's only enabled in debug builds at compile time +#ifndef NDEBUG +#define LLAMA_LOG_CMAKE_DEBUG(...) LLAMA_LOG_DEBUG(__VA_ARGS__) +#else +#define LLAMA_LOG_CMAKE_DEBUG(...) +#endif + // // helpers // diff --git a/src/llama-kv-cache.cpp b/src/llama-kv-cache.cpp index e26385a1feaf..86eb414beae5 100644 --- a/src/llama-kv-cache.cpp +++ b/src/llama-kv-cache.cpp @@ -1131,6 +1131,33 @@ ggml_tensor * llama_kv_cache::cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggm return ggml_set_rows(ctx, v_view, v_cur, v_idxs); } +ggml_tensor * llama_kv_cache::get_k_lora(ggml_context * ctx, ggml_tensor * k_cur, int32_t il, uint32_t n_kv, const slot_info & sinfo) const { + if (sinfo.s0 == 0) { + return k_cur; + } + + slot_info past_sinfo = sinfo; + past_sinfo.s0 = 0; + past_sinfo.s1 = sinfo.s0 - 1; + + ggml_tensor * k_past = get_k(ctx, il, n_kv, past_sinfo); + + return ggml_concat(ctx, k_past, k_cur, 2); +} + +ggml_tensor * llama_kv_cache::get_v_lora(ggml_context * ctx, ggml_tensor * v_cur, int32_t il, uint32_t n_kv, const slot_info & sinfo) const { + if (sinfo.s0 == 0) { + return v_cur; + } + + slot_info past_sinfo = sinfo; + past_sinfo.s0 = 0; + past_sinfo.s1 = sinfo.s0 - 1; + ggml_tensor * v_past = get_v(ctx, il, n_kv, past_sinfo); + + return ggml_concat(ctx, v_past, v_cur, 2); +} + ggml_tensor * llama_kv_cache::build_input_k_idxs(ggml_context * ctx, const llama_ubatch & ubatch) const { const uint32_t n_tokens = ubatch.n_tokens; @@ -2023,6 +2050,14 @@ ggml_tensor * llama_kv_cache_context::cpy_v(ggml_context * ctx, ggml_tensor * v_ return kv->cpy_v(ctx, v_cur, v_idxs, il, sinfos[i_cur]); } +ggml_tensor * llama_kv_cache_context::get_k_lora(ggml_context * ctx, ggml_tensor * k_cur, int32_t il) const { + return kv->get_k_lora(ctx, k_cur, il, n_kv, sinfos[i_cur]); +} + +ggml_tensor * llama_kv_cache_context::get_v_lora(ggml_context * ctx, ggml_tensor * v_cur, int32_t il) const { + return kv->get_v_lora(ctx, v_cur, il, n_kv, sinfos[i_cur]); +} + ggml_tensor * llama_kv_cache_context::build_input_k_idxs(ggml_context * ctx, const llama_ubatch & ubatch) const { return kv->build_input_k_idxs(ctx, ubatch); } diff --git a/src/llama-kv-cache.h b/src/llama-kv-cache.h index bf7821c07ca8..902d19ad01ab 100644 --- a/src/llama-kv-cache.h +++ b/src/llama-kv-cache.h @@ -149,6 +149,10 @@ class llama_kv_cache : public llama_memory_i { ggml_tensor * cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggml_tensor * k_idxs, int32_t il, const slot_info & sinfo) const; ggml_tensor * cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggml_tensor * v_idxs, int32_t il, const slot_info & sinfo) const; + // gradient-aware retrieval for LoRA training + ggml_tensor * get_k_lora(ggml_context * ctx, ggml_tensor * k_cur, int32_t il, uint32_t n_kv, const slot_info & sinfo) const; + ggml_tensor * get_v_lora(ggml_context * ctx, ggml_tensor * v_cur, int32_t il, uint32_t n_kv, const slot_info & sinfo) const; + // // preparation API // @@ -325,6 +329,10 @@ class llama_kv_cache_context : public llama_memory_context_i { ggml_tensor * cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggml_tensor * k_idxs, int32_t il) const; ggml_tensor * cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggml_tensor * v_idxs, int32_t il) const; + // gradient-aware retrieval for LoRA training + ggml_tensor * get_k_lora(ggml_context * ctx, ggml_tensor * k_cur, int32_t il) const; + ggml_tensor * get_v_lora(ggml_context * ctx, ggml_tensor * v_cur, int32_t il) const; + // create destination indices for each head of the current batch for where it would be written in the KV cache // the indices address the global KV cache (not per stream) - this is not relevant for the user of this API, but // helps understand the implementation logic of cpy_k and cpy_v diff --git a/src/llama-lora-training.cpp b/src/llama-lora-training.cpp new file mode 100644 index 000000000000..b68138fd877d --- /dev/null +++ b/src/llama-lora-training.cpp @@ -0,0 +1,386 @@ +#include "llama-lora-training.h" + +#include +#include +#include + + +ggml_context * llama_lora_create_context(size_t mem_size) { + struct ggml_init_params init_params = { + /*.mem_size =*/ mem_size, + /*.mem_buffer =*/ nullptr, + /*.no_alloc =*/ true, + }; + return ggml_init(init_params); +} + +bool llama_lora_validate_training_params(const struct llama_lora_training_params * params) { + if (!params) { + LLAMA_LOG_ERROR("LoRA training validation: params is null\n"); + return false; + } + + if (params->rank <= 0 || params->rank > 1024) { + LLAMA_LOG_ERROR("LoRA training validation: invalid rank %d (must be 1-1024)\n", params->rank); + return false; + } + + if (params->alpha <= 0.0f) { + LLAMA_LOG_ERROR("LoRA training validation: invalid alpha %f (must be > 0)\n", params->alpha); + return false; + } + + if (params->dropout < 0.0f || params->dropout > 1.0f) { + LLAMA_LOG_ERROR("LoRA training validation: invalid dropout %f (must be [0, 1])\n", params->dropout); + return false; + } + + if (params->init_std <= 0.0f || params->init_std > 1.0f) { + LLAMA_LOG_ERROR("LoRA training validation: invalid init_std %f (must be (0, 1])\n", params->init_std); + return false; + } + + if (params->target_modules == 0) { + LLAMA_LOG_ERROR("LoRA training validation: no target modules specified\n"); + return false; + } + + return true; +} + +void llama_lora_create_tensor_pair( + struct ggml_context * lora_ctx, + const char * base_name, + const struct ggml_tensor * base_tensor, + int32_t rank, + struct ggml_tensor ** lora_a, + struct ggml_tensor ** lora_b) { + + if (!lora_ctx || !base_name || !base_tensor || !lora_a || !lora_b) { + throw std::invalid_argument("Invalid null arguments provided to llama_lora_create_tensor_pair"); + } + + // Get base tensor dim + const int64_t d0 = base_tensor->ne[0]; // input dim + const int64_t d1 = base_tensor->ne[1]; // output dim + + char lora_a_name[256], lora_b_name[256]; + int ret_a = snprintf(lora_a_name, sizeof(lora_a_name), "%s.lora_a", base_name); + int ret_b = snprintf(lora_b_name, sizeof(lora_b_name), "%s.lora_b", base_name); + if (ret_a < 0 || ret_a >= (int) sizeof(lora_a_name) || + ret_b < 0 || ret_b >= (int) sizeof(lora_b_name)) { + throw std::runtime_error(std::string("LoRA tensor name too long or formatting failed: ") + base_name); + } + + // LoRA A: [d0, rank] - projects input to low rank + *lora_a = ggml_new_tensor_2d(lora_ctx, GGML_TYPE_F32, d0, rank); + ggml_set_name(*lora_a, lora_a_name); + + // LoRA B: [rank, d1] - projects from low rank to output + *lora_b = ggml_new_tensor_2d(lora_ctx, GGML_TYPE_F32, rank, d1); + ggml_set_name(*lora_b, lora_b_name); +} + +static bool is_tensor_on_device(const struct ggml_tensor * tensor) { + return tensor->buffer && !ggml_backend_buffer_is_host(tensor->buffer); +} + +static void init_tensor_guassian(struct ggml_tensor * tensor, float std_dev, uint32_t seed) { + const size_t n_elements = ggml_nelements(tensor); + std::vector data(n_elements); + + std::mt19937 gen; + if (seed != 0) { + gen.seed(seed); + } else { + std::random_device rd; + gen.seed(rd()); + } + std::normal_distribution dist(0.0f, std_dev); + + for (size_t i = 0; i < n_elements; i++) { + data[i] = dist(gen); + } + + if (is_tensor_on_device(tensor)) { + ggml_backend_tensor_set(tensor, data.data(), 0, n_elements * sizeof(float)); + } else { + std::copy(data.begin(), data.end(), (float *)tensor->data); + } +} + +static void init_tensor_zeros(struct ggml_tensor * tensor) { + const size_t n_elements = ggml_nelements(tensor); + + if (is_tensor_on_device(tensor)) { + std::vector zeros(n_elements, 0.0f); + ggml_backend_tensor_set(tensor, zeros.data(), 0, n_elements * sizeof(float)); + } else { + std::fill_n((float *)tensor->data, n_elements, 0.0f); + } +} + +static void llama_lora_init_tensor_weights(struct ggml_tensor * lora_a, struct ggml_tensor * lora_b, float init_std, uint32_t seed) { + if (!lora_a || !lora_b || !lora_a->data || !lora_b->data) { + throw std::invalid_argument("Invalid null tensors or data pointers passed to llama_lora_init_tensor_weights"); + } + + // LoRA initialization: A ~ N(0, init_std), B = 0 + init_tensor_guassian(lora_a, init_std, seed); + init_tensor_zeros(lora_b); +} + +bool llama_lora_allocate_buffers( + struct llama_adapter_lora * adapter, + struct llama_model * model) { + + if (!adapter || !model) { + return false; + } + + std::map ctx_map; + + ggml_backend_buffer_type_t buft = ggml_backend_cpu_buffer_type(); // fallback to CPU + + // Find any layer tensor to determine the correct backend + for (const auto & tensor_pair : model->tensors_by_name) { + const std::string & name = tensor_pair.first; + struct ggml_tensor * tensor = tensor_pair.second; + + if (name.find("blk.") != std::string::npos && tensor && tensor->buffer) { + buft = ggml_backend_buffer_get_type(tensor->buffer); + break; + } + } + + if (adapter->ctxs.empty()) { + LLAMA_LOG_ERROR("No contexts found in adapter\n"); + return false; + } + ggml_context * lora_ctx = adapter->ctxs[0].get(); + + ggml_backend_buffer_ptr buf { ggml_backend_alloc_ctx_tensors_from_buft(lora_ctx, buft) }; + if (!buf) { + LLAMA_LOG_ERROR("Failed to allocate buffer for LoRA adapter\n"); + return false; + } + LLAMA_LOG_INFO("LoRA buffer size = %.2f MiB\n", ggml_backend_buffer_get_size(buf.get())/1024.0/1024.0); + adapter->bufs.emplace_back(std::move(buf)); + + return true; +} + +struct llama_adapter_lora * llama_lora_create_adapter( + struct llama_model * model, + const struct llama_lora_training_params * params) { + + llama_adapter_lora * adapter = new llama_adapter_lora(); + try { + adapter->alpha = params->alpha; + + // Create LoRA tensors and populate ab_map + // Create GGML context for LoRA tensors + // TODO (makaveli10): Remove hard-coded memory size + const size_t estimated_lora_mem = 256 * 1024 * 1024; // 256MB should be enough for most LoRA configs + ggml_context * lora_ctx = llama_lora_create_context(estimated_lora_mem); + if (!lora_ctx) { + throw std::runtime_error("Failed to create LoRA context"); + } + + adapter->ctxs.emplace_back(lora_ctx); + int created_count = 0; + + for (const auto & tensor_pair : model->tensors_by_name) { + const std::string & tensor_name = tensor_pair.first; + struct ggml_tensor * base_tensor = tensor_pair.second; + + if (!base_tensor) { + continue; + } + + const bool is_blk = tensor_name.find("blk.") != std::string::npos; + + const bool should_create_lora = + (is_blk && (params->target_modules & LLAMA_LORA_TARGET_ATTN_Q) && tensor_name.find("attn_q") != std::string::npos) || + (is_blk && (params->target_modules & LLAMA_LORA_TARGET_ATTN_K) && tensor_name.find("attn_k") != std::string::npos) || + (is_blk && (params->target_modules & LLAMA_LORA_TARGET_ATTN_V) && tensor_name.find("attn_v") != std::string::npos) || + (is_blk && (params->target_modules & LLAMA_LORA_TARGET_ATTN_O) && tensor_name.find("attn_output") != std::string::npos) || + (is_blk && (params->target_modules & LLAMA_LORA_TARGET_FFN_GATE) && tensor_name.find("ffn_gate") != std::string::npos) || + (is_blk && (params->target_modules & LLAMA_LORA_TARGET_FFN_UP) && tensor_name.find("ffn_up") != std::string::npos) || + (is_blk && (params->target_modules & LLAMA_LORA_TARGET_FFN_DOWN) && tensor_name.find("ffn_down") != std::string::npos) || + ( (params->target_modules & LLAMA_LORA_TARGET_OUTPUT) && tensor_name.find("output") != std::string::npos); + + if (should_create_lora && base_tensor->ne[1] > 0) { + struct ggml_tensor * lora_a = nullptr; + struct ggml_tensor * lora_b = nullptr; + + llama_lora_create_tensor_pair(lora_ctx, tensor_name.c_str(), base_tensor, params->rank, &lora_a, &lora_b); + created_count++; + adapter->ab_map[tensor_name] = llama_adapter_lora_weight(lora_a, lora_b); + } + } + + if (created_count == 0) { + throw std::runtime_error("No suitable tensors found for LoRA adaptation"); + } + + if (!llama_lora_allocate_buffers(adapter, model)) { + throw std::runtime_error("Failed to allocate LoRA buffers"); + } + + for (const auto & ab_pair : adapter->ab_map) { + const llama_adapter_lora_weight & weight = ab_pair.second; + + llama_lora_init_tensor_weights(weight.a, weight.b, params->init_std, params->seed); + } + return adapter; + } catch (const std::exception & err) { + LLAMA_LOG_ERROR("Failed to create LoRA adapter: %s\n", err.what()); + delete adapter; + return nullptr; + } +} + +struct llama_adapter_lora * llama_lora_training_init( + struct llama_context * ctx, + struct llama_model * model, + const struct llama_lora_training_params * params) { + + if (!ctx || !model || !params) { + LLAMA_LOG_ERROR("LoRA training init: invalid parameters\n"); + return nullptr; + } + + if (!llama_lora_validate_training_params(params)) { + return nullptr; + } + + struct llama_adapter_lora * adapter = llama_lora_create_adapter(model, params); + if (!adapter) { + return nullptr; + } + + llama_clear_adapter_lora(ctx); + + if (llama_set_adapter_lora(ctx, adapter, 1.0f) < 0) { + LLAMA_LOG_ERROR("Failed to apply LoRA adapter to context\n"); + delete adapter; + return nullptr; + } + + LLAMA_LOG_INFO("LoRA adapter contains %zu tensor pairs and is now registered with context\n", adapter->ab_map.size()); + + return adapter; +} + +bool llama_opt_param_filter_lora(const struct ggml_tensor * tensor, void * userdata) { + (void) userdata; // Unused param + + if (!tensor) { + return false; + } + + const char * name = tensor->name; + + // Check if tensor is LoRA A or B + // LoRA tensor naming convention: blk.{layer}.{module}.lora_a or .lora_b + if (strstr(name, ".lora_a") || strstr(name, ".lora_b")) { + LLAMA_LOG_DEBUG("LoRA filter: including trainable params '%s'\n", name); + return true; + } + + return false; +} + +bool llama_lora_save_adapter( + const struct llama_adapter_lora * adapter, + const char * filename, + const struct llama_model * model) { + + if (!adapter || !filename || !model) { + LLAMA_LOG_ERROR("llama_lora_save_adapter: invalid parameters\n"); + return false; + } + + struct gguf_context * gguf_ctx = gguf_init_empty(); + if (!gguf_ctx) { + LLAMA_LOG_ERROR("llama_lora_save_adapter: failed to create GGUF context\n"); + return false; + } + + std::string arch_name = model->arch_name(); + if (arch_name.empty()) { + LLAMA_LOG_ERROR("llama_lora_save_adapter: failed to get model architecture\n"); + gguf_free(gguf_ctx); + return false; + } + + gguf_set_val_str(gguf_ctx, "general.architecture", arch_name.c_str()); + gguf_set_val_str(gguf_ctx, "general.type", "adapter"); + gguf_set_val_str(gguf_ctx, "general.name", "LoRA Adapter"); + gguf_set_val_str(gguf_ctx, "adapter.type", "lora"); + gguf_set_val_f32(gguf_ctx, "adapter.lora.alpha", adapter->alpha); + + int tensor_count = 0; + for (const auto & kv : adapter->ab_map) { + const auto & lora_weight = kv.second; + + if (lora_weight.a && lora_weight.b) { + gguf_add_tensor(gguf_ctx, lora_weight.a); + gguf_add_tensor(gguf_ctx, lora_weight.b); + tensor_count += 2; + } + } + + bool success = gguf_write_to_file(gguf_ctx, filename, false); + if (success) { + LLAMA_LOG_INFO("Successfully saved LoRA adapter with %d tensors to: %s\n", + tensor_count, filename); + } else { + LLAMA_LOG_ERROR("Failed to write LoRA adapter to: %s\n", filename); + } + + gguf_free(gguf_ctx); + return success; +} + +bool llama_lora_save_checkpoint( + const struct llama_adapter_lora * adapter, + const char * checkpoint_path, + const struct llama_model * model, + struct llama_context * ctx +) { + if (!adapter || !checkpoint_path || !model || !ctx) { + LLAMA_LOG_ERROR("llama_lora_save_checkpoint: invalid parameters\n"); + return false; + } + + std::filesystem::path checkpoint_dir = std::filesystem::path(checkpoint_path); + if (!checkpoint_dir.empty()) { + if (!std::filesystem::exists(checkpoint_dir)) { + if (!std::filesystem::create_directories(checkpoint_dir)) { + LLAMA_LOG_ERROR("llama_lora_save_checkpoint: failed to create checkpoint directory: %s\n", + checkpoint_dir.string().c_str()); + return false; + } + } + } + + std::filesystem::path model_path = checkpoint_dir / "model.gguf"; + bool lora_saved = llama_lora_save_adapter(adapter, model_path.string().c_str(), model); + if (!lora_saved) { + LLAMA_LOG_ERROR("llama_lora_save_checkpoint: failed to save LoRA adapter weights to %s\n", + model_path.string().c_str()); + return false; + } + + std::filesystem::path optimizer_path = checkpoint_dir / "optimizer.gguf"; + bool optimizer_saved = ctx->opt_save_state(optimizer_path.string().c_str()); + if (!optimizer_saved) { + LLAMA_LOG_ERROR("llama_lora_save_checkpoint: failed to save optimizer state to %s\n", + optimizer_path.string().c_str()); + return false; + } + + return true; +} diff --git a/src/llama-lora-training.h b/src/llama-lora-training.h new file mode 100644 index 000000000000..9c8a0a43bba0 --- /dev/null +++ b/src/llama-lora-training.h @@ -0,0 +1,30 @@ +#pragma once + +#include "llama.h" +#include "llama-model.h" +#include "llama-adapter.h" +#include "llama-impl.h" +#include "llama-context.h" +#include "ggml.h" + + +bool llama_lora_validate_training_params(const struct llama_lora_training_params * params); + +ggml_context * llama_lora_create_context(size_t mem_size); + +void llama_lora_create_tensor_pair( + struct ggml_context * lora_ctx, + const char * base_name, + const struct ggml_tensor * base_tensor, + int32_t rank, + struct ggml_tensor ** lora_a, + struct ggml_tensor ** lora_b); + +struct llama_adapter_lora * llama_lora_create_adapter( + struct llama_model * model, + const struct llama_lora_training_params * params); + +bool llama_lora_allocate_buffers( + struct llama_adapter_lora * adapter, + struct llama_model * model); + diff --git a/src/llama-mmap.cpp b/src/llama-mmap.cpp index 0641c2d22f67..612a0fd76bb0 100644 --- a/src/llama-mmap.cpp +++ b/src/llama-mmap.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #ifdef __has_include #if __has_include() @@ -54,9 +56,7 @@ static std::string llama_format_win_err(DWORD err) { } #endif -// llama_file - -struct llama_file::impl { +struct llama_file_disk::impl { #if defined(_WIN32) HANDLE fp_win32; std::string GetErrorMessageWin32(DWORD error_code) const { @@ -241,13 +241,13 @@ struct llama_file::impl { size_t size; }; -llama_file::llama_file(const char * fname, const char * mode) : pimpl(std::make_unique(fname, mode)) {} -llama_file::~llama_file() = default; +llama_file_disk::llama_file_disk(const char * fname, const char * mode) : pimpl(std::make_unique(fname, mode)) {} +llama_file_disk::~llama_file_disk() = default; -size_t llama_file::tell() const { return pimpl->tell(); } -size_t llama_file::size() const { return pimpl->size; } +size_t llama_file_disk::tell() const { return pimpl->tell(); } +size_t llama_file_disk::size() const { return pimpl->size; } -int llama_file::file_id() const { +int llama_file_disk::file_id() const { #ifdef _WIN32 return _fileno(pimpl->fp); #else @@ -259,13 +259,193 @@ int llama_file::file_id() const { #endif } -void llama_file::seek(size_t offset, int whence) const { pimpl->seek(offset, whence); } -void llama_file::read_raw(void * ptr, size_t len) const { pimpl->read_raw(ptr, len); } +void llama_file_disk::seek(size_t offset, int whence) const { pimpl->seek(offset, whence); } +void llama_file_disk::read_raw(void * ptr, size_t len) const { pimpl->read_raw(ptr, len); } + +uint32_t llama_file_disk::read_u32() const { return pimpl->read_u32(); } + +void llama_file_disk::write_raw(const void * ptr, size_t len) const { pimpl->write_raw(ptr, len); } +void llama_file_disk::write_u32(uint32_t val) const { pimpl->write_u32(val); } + +template +llama_file_buffer::llama_file_buffer(std::unique_ptr> && streambuf) : + streambuf(std::move(streambuf)) {} + +template llama_file_buffer::~llama_file_buffer() = default; + +template size_t llama_file_buffer::tell() const { + return streambuf->pubseekoff(0, std::ios_base::cur); +} + +template size_t llama_file_buffer::size() const { + auto current_pos = streambuf->pubseekoff(0, std::ios_base::cur); + auto end_pos = streambuf->pubseekoff(0, std::ios_base::end); + streambuf->pubseekpos(current_pos); + return end_pos; +} + +template int llama_file_buffer::file_id() const { + return -1; +} + +template void llama_file_buffer::seek(size_t offset, int whence) const { + static std::map whence_to_dir = { + { SEEK_SET, std::ios_base::beg }, + { SEEK_CUR, std::ios_base::cur }, + { SEEK_END, std::ios_base::end } + }; + auto result = streambuf->pubseekoff(offset, whence_to_dir.at(whence)); + if (result == std::streampos(-1)) { + throw std::runtime_error("seek failed"); + } +} + +template void llama_file_buffer::read_raw(void * ptr, size_t len) const { + auto bytes_read = streambuf->sgetn(static_cast(ptr), len); + if (bytes_read != static_cast(len)) { + throw std::runtime_error("read beyond end of buffer"); + } +} + +template uint32_t llama_file_buffer::read_u32() const { + uint32_t val; + read_raw(&val, sizeof(val)); + return val; +} + +template <> void llama_file_buffer::write_raw([[maybe_unused]] const void * ptr, size_t len) const { + if (len > 0) { + throw std::runtime_error("buffer is not writable"); + } +} + +template <> void llama_file_buffer::write_u32(uint32_t val) const { + if (val > 0) { + // Cannot directly set [[noreturn]] for a function since it was defined without it. + throw std::runtime_error("buffer is not writable"); + } +} + +template <> void llama_file_buffer::write_raw(const void * ptr, size_t len) const { + auto bytes_written = streambuf->sputn(static_cast(ptr), len); + if (bytes_written != static_cast(len)) { + throw std::runtime_error("write beyond end of buffer"); + } +} -uint32_t llama_file::read_u32() const { return pimpl->read_u32(); } +template <> void llama_file_buffer::write_u32(uint32_t val) const { + write_raw(&val, sizeof(val)); +} + +// Explicit instantiations +template struct llama_file_buffer; +template struct llama_file_buffer; + +// llama_future_file_buffer implementation + +namespace { +std::string final_key(const std::string & promise_key, const std::string & context) { + return promise_key + ":" + context; +} + +std::mutex promise_registry_mutex; + +std::map>>> promise_registry_ro; +std::map>>> promise_registry_rw; + +template +std::map>>> & promise_registry() { + if constexpr (Writable) { + return promise_registry_rw; + } else { + return promise_registry_ro; + } +} + +/// @brief Ensures a promise exists in the registry for the given key. +/// If it doesn't exist, creates it. Returns an iterator to the promise. +/// Thread-safe. +template +typename std::map>>>::iterator +ensure_promise_registry(const std::string & key) { + std::lock_guard lock(promise_registry_mutex); + auto it = promise_registry().find(key); + if (it != promise_registry().end()) { + return it; + } + auto result = + promise_registry().emplace(key, std::promise>>()); + LLAMA_LOG_CMAKE_DEBUG("%s: created future file buffer %p for %s\n", __func__, (void *) &(*it), key.c_str()); + return result.first; +} +} // namespace + +template +llama_future_file_buffer::llama_future_file_buffer(const std::string & promise_key, + const std::string & context) : + file_buffer_future(), + file_buffer() { + std::string key = final_key(promise_key, context); + file_buffer_promise_iterator = ensure_promise_registry(key); + file_buffer_future = file_buffer_promise_iterator->second.get_future(); +} + +template +llama_future_file_buffer::llama_future_file_buffer(llama_future_file_buffer && other) noexcept : + file_buffer_promise_iterator(std::move(other.file_buffer_promise_iterator)), + file_buffer_future(std::move(other.file_buffer_future)), + file_buffer(std::move(other.file_buffer)) { + // Set the other object's iterator to end() to mark it as moved from + // to avoid early erasure at destruction of the moved other object + other.file_buffer_promise_iterator = promise_registry().end(); +} + +template +llama_future_file_buffer & llama_future_file_buffer::operator=( + llama_future_file_buffer && other) noexcept { + if (this != &other) { + file_buffer_promise_iterator = std::move(other.file_buffer_promise_iterator); + file_buffer_future = std::move(other.file_buffer_future); + file_buffer = std::move(other.file_buffer); + other.file_buffer_promise_iterator = promise_registry().end(); + } + return *this; +} + +template llama_future_file_buffer::~llama_future_file_buffer() { + std::lock_guard lock(promise_registry_mutex); + if (file_buffer_promise_iterator != promise_registry().end()) { + promise_registry().erase(file_buffer_promise_iterator); + } +} + +template +bool llama_future_file_buffer::fulfill_promise(const std::string & promise_key, const std::string & context, + std::unique_ptr> && value) { + std::string key = final_key(promise_key, context); + auto it = ensure_promise_registry(key); + if (it != promise_registry().end()) { + LLAMA_LOG_CMAKE_DEBUG("fulfilling future file buffer %p for %s\n", (void *) &(*it), key.c_str()); + it->second.set_value(std::move(value)); + return true; + } + return false; +} + +template +std::unique_ptr> llama_future_file_buffer::extract() const { + if (file_buffer) { + return std::move(file_buffer); + } + + auto future_result = file_buffer_future.get(); + file_buffer = std::move(future_result); + return std::move(file_buffer); +} -void llama_file::write_raw(const void * ptr, size_t len) const { pimpl->write_raw(ptr, len); } -void llama_file::write_u32(uint32_t val) const { pimpl->write_u32(val); } +// Explicit instantiations for llama_future_file_buffer +template struct llama_future_file_buffer; +template struct llama_future_file_buffer; // llama_mmap diff --git a/src/llama-mmap.h b/src/llama-mmap.h index 4e5aec3f440d..9a710148f14b 100644 --- a/src/llama-mmap.h +++ b/src/llama-mmap.h @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include struct llama_file; struct llama_mmap; @@ -13,27 +16,107 @@ using llama_mmaps = std::vector>; using llama_mlocks = std::vector>; struct llama_file { - llama_file(const char * fname, const char * mode); - ~llama_file(); + virtual ~llama_file() = default; - size_t tell() const; - size_t size() const; + virtual size_t tell() const = 0; + virtual size_t size() const = 0; + virtual int file_id() const = 0; + + virtual void seek(size_t offset, int whence) const = 0; - int file_id() const; // fileno overload + virtual void read_raw(void * ptr, size_t len) const = 0; + virtual uint32_t read_u32() const = 0; + + virtual void write_raw(const void * ptr, size_t len) const = 0; + virtual void write_u32(uint32_t val) const = 0; +}; - void seek(size_t offset, int whence) const; +struct llama_file_disk : public llama_file { + llama_file_disk(const char * fname, const char * mode); + ~llama_file_disk() override; - void read_raw(void * ptr, size_t len) const; - uint32_t read_u32() const; + size_t tell() const override; + size_t size() const override; + int file_id() const override; - void write_raw(const void * ptr, size_t len) const; - void write_u32(uint32_t val) const; + void seek(size_t offset, int whence) const override; + + void read_raw(void * ptr, size_t len) const override; + uint32_t read_u32() const override; + + void write_raw(const void * ptr, size_t len) const override; + void write_u32(uint32_t val) const override; private: struct impl; std::unique_ptr pimpl; }; +template struct llama_file_buffer : public llama_file { + /// @note Use char for the streambuf because not all platforms support uint8_t specialization (e.g. MacOS or newer NDKs) + /// from C++17 there are guarantees that make safe to access binary data from char + llama_file_buffer(std::unique_ptr> && streambuf); + + ~llama_file_buffer() override; + + size_t tell() const override; + size_t size() const override; + + /// @return -1 to indicate this is not a real file descriptor + int file_id() const override; + + void seek(size_t offset, int whence) const override; + + void read_raw(void * ptr, size_t len) const override; + uint32_t read_u32() const override; + + /// @throw std::runtime_error if the buffer is read-only + void write_raw(const void * ptr, size_t len) const override; + + /// @throw std::runtime_error if the buffer is read-only + void write_u32(uint32_t val) const override; + + std::unique_ptr> streambuf; +}; + +template struct llama_future_file_buffer { + /// @brief A file buffer object whose operations will block + /// until the given promise key is set with a file buffer. + /// @param promise_key The key to use for the promise (e.g. a file path). + /// @param context The context to use for the promise, used to distinguish same promise key (e.g. for a same file opened twice). + llama_future_file_buffer(const std::string & promise_key, const std::string & context); + + // Delete copy constructor and copy assignment operator + llama_future_file_buffer(const llama_future_file_buffer &) = delete; + llama_future_file_buffer & operator=(const llama_future_file_buffer &) = delete; + + llama_future_file_buffer(llama_future_file_buffer && other) noexcept; + llama_future_file_buffer & operator=(llama_future_file_buffer && other) noexcept; + + ~llama_future_file_buffer(); + + /// @brief Sets the given key and context with a file buffer so that + /// operations can resume/start. + static bool fulfill_promise(const std::string & promise_key, const std::string & context, + std::unique_ptr> && value); + + /// @brief Waits for future buffer or obtains current if already + /// fulfilled and moves the future contents outside the registry. + std::unique_ptr> extract() const; + + private: + typename std::map>>>::iterator + file_buffer_promise_iterator; + mutable std::future>> file_buffer_future; + mutable std::unique_ptr> file_buffer; +}; + +// Type aliases for convenience +using llama_file_buffer_ro = llama_file_buffer; +using llama_file_buffer_rw = llama_file_buffer; +using llama_future_file_buffer_ro = llama_future_file_buffer; +using llama_future_file_buffer_rw = llama_future_file_buffer; + struct llama_mmap { llama_mmap(const llama_mmap &) = delete; llama_mmap(struct llama_file * file, size_t prefetch = (size_t) -1, bool numa = false); diff --git a/src/llama-model-load-input.cpp b/src/llama-model-load-input.cpp new file mode 100644 index 000000000000..69c05dc86148 --- /dev/null +++ b/src/llama-model-load-input.cpp @@ -0,0 +1,59 @@ +#include "llama-model-load-input.h" + +#include + +#include "llama-mmap.h" + +namespace load_input_variant { + +const char * identifier(load_input_t & load_input) { + if (std::holds_alternative(load_input)) { + const auto & file_input = std::get(load_input); + return file_input.fname.c_str(); + } + static const char * buffer_id_str = "buffer"; + return buffer_id_str; +} + +fname_load_input split_name_from_variant(load_input_t & load_input) { + if (std::holds_alternative(load_input)) { + auto future_input = std::get(load_input); + return fname_load_input{ future_input.promise_key, future_input.splits }; + } + auto file_input = std::get(load_input); + return file_input; +} + +bool variant_supports_split_load(load_input_t & load_input) { + return std::holds_alternative(load_input) || + std::holds_alternative(load_input); +} + +bool variant_supports_split_load_from_memory(load_input_t & load_input) { + return std::holds_alternative(load_input); +} + +std::optional> parse_tensor_list_from_future(load_input_t & load_input) { + std::set tensor_names; + + if (!std::holds_alternative(load_input)) { + return std::nullopt; + } + + const auto & future_input = std::get(load_input); + + // Open and read the tensor list file + llama_future_file_buffer_ro tensor_file(future_input.tensor_list_file, future_input.context); + std::unique_ptr file_buffer = tensor_file.extract(); + + // Read directly from the stream + std::basic_istream stream(file_buffer->streambuf.get()); + std::string line; + while (std::getline(stream, line)) { + tensor_names.insert(line); + } + + return tensor_names; +} + +} // namespace load_input_variant diff --git a/src/llama-model-load-input.h b/src/llama-model-load-input.h new file mode 100644 index 000000000000..9770db83ceb4 --- /dev/null +++ b/src/llama-model-load-input.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace load_input_variant { + +struct fname_load_input { + const std::string & fname; + std::vector & splits; // optional, only need if the split does not follow naming scheme +}; + +struct buffer_load_input { + std::unique_ptr> & streambuf; +}; + +struct buffer_future_load_input { + const std::string & promise_key; + const std::string & context; + std::vector & splits; + const std::string & tensor_list_file; +}; + +} // namespace load_input_variant + +using load_input_t = std::variant; + +namespace load_input_variant { +const char * identifier(load_input_t & load_input); + +fname_load_input split_name_from_variant(load_input_t & load_input); + +bool variant_supports_split_load(load_input_t & load_input); + +bool variant_supports_split_load_from_memory(load_input_t & load_input); + +/// @brief Parse tensor list from future file or nullopt if not a future file +std::optional> parse_tensor_list_from_future(load_input_t & load_input); +} // namespace load_input_variant diff --git a/src/llama-model-load.cpp b/src/llama-model-load.cpp new file mode 100644 index 000000000000..c518b3fd4343 --- /dev/null +++ b/src/llama-model-load.cpp @@ -0,0 +1,234 @@ +#include "llama-model-load.h" + +#include +#include +#include +#include + +#include "llama-model-loader.h" + +gguf_file_load::gguf_file_load(struct ggml_context ** ctx, load_input_t load_input) : + params({ + /*.no_alloc = */ true, + /*.ctx = */ ctx, + }) { + using namespace load_input_variant; + if (std::holds_alternative(load_input)) { + const auto & file_input = std::get(load_input); + meta.reset(gguf_init_from_file(file_input.fname.c_str(), params)); + if (!meta) { + throw std::runtime_error(format("%s: failed to load model from %s", __func__, file_input.fname.c_str())); + } + file = std::make_unique(file_input.fname.c_str(), "rb"); + } else if (std::holds_alternative(load_input)) { + const auto & future_input = std::get(load_input); + auto future_file = + std::make_unique(future_input.promise_key, future_input.context); + std::unique_ptr file_buffer = future_file->extract(); + meta.reset(gguf_init_from_buffer(*file_buffer->streambuf, params)); + if (!meta) { + throw std::runtime_error(format("%s: failed to load model from buffer", __func__)); + } + file = std::move(file_buffer); + } else { + const auto & buffer_input = std::get(load_input); + meta.reset(gguf_init_from_buffer(*buffer_input.streambuf, params)); + if (!meta) { + throw std::runtime_error(format("%s: failed to load model from buffer", __func__)); + } + file = std::make_unique(std::move(buffer_input.streambuf)); + } +} + +gguf_file_load SplitLoad::load_split_gguf(struct ggml_context ** ctx, const char * fname_split, + load_input_t & load_input, std::vector & splits) { + using namespace load_input_variant; + if (std::holds_alternative(load_input)) { + return gguf_file_load(ctx, fname_load_input{ fname_split, splits }); + } + if (std::holds_alternative(load_input)) { + auto future_input = std::get(load_input); + return gguf_file_load( + ctx, buffer_future_load_input{ fname_split, future_input.context, splits, future_input.tensor_list_file }); + } + return gguf_file_load(ctx, load_input); +} + +SplitLoad::SplitLoad(load_input_t & load_input, load_input_variant::fname_load_input base_split, uint16_t idx, + std::string kv_split_no) : + load_input(load_input), + base_split(base_split), + idx(idx), + kv_split_no(std::move(kv_split_no)) {} + +IncrementalSplitsTensorLoad::IncrementalSplitsTensorLoad(struct ggml_context * ctx, struct llama_model_loader & ml, + gguf_file_load & base_split, + std::set tensor_list) : + expected_tensors(std::move(tensor_list)) { + ml.process_loaded_gguf(ctx, base_split, 0); + _process_split(ctx, ml, 0); +} + +struct ggml_context * SplitLoad::load(llama_model_loader & ml) { + if (loaded) { + return ml.contexts[idx].get(); + } + + struct ggml_context * ctx = ml.contexts.back().get(); + + const char * fname_split = base_split.splits[idx].c_str(); + LLAMA_LOG_INFO("loading split-file %s\n", fname_split); + + gguf_file_load split_gguf = gguf_file_load(load_split_gguf(&ctx, fname_split, load_input, base_split.splits)); + gguf_context_ptr & split_meta = split_gguf.meta; + + if (idx > 0) { + const int kid = gguf_find_key(split_meta.get(), kv_split_no.c_str()); + if (kid < 0) { + throw std::runtime_error(format("missing key %s in GGUF split %s", kv_split_no.c_str(), fname_split)); + } + int idx_gguf = gguf_get_val_u16(split_meta.get(), kid); + if (idx_gguf != idx) { + throw std::runtime_error( + format("invalid split file idx: %d (file: %s), expected %d", idx_gguf, fname_split, idx)); + } + } + + // Check that this split's idx matches the expected position in ml.files + if (!ml.files.empty() && idx != ml.files.size()) { + throw std::runtime_error( + format("invalid split file loading order: got idx %d but expected %zu based on ml.files size", idx, + ml.files.size())); + } + + ml.process_loaded_gguf(ctx, split_gguf, idx); + + loaded = true; + return ctx; +} + +void IncrementalSplitsTensorLoad::add_split(SplitLoad splitLoad) { + // +1 because first split is expected to have been already loaded (not delayed) + split_info[delayed_files.size() + 1] = SplitInfo(); + delayed_files.emplace_back(std::move(splitLoad)); +} + +void IncrementalSplitsTensorLoad::_load_split(struct llama_model_loader & ml, uint16_t idx) { + // -1 because first split is expected to have been already loaded (not delayed and not present in delayed_files) + const struct ggml_context * ctx = delayed_files[idx - 1].load(ml); + _process_split(ctx, ml, idx); +} + +void IncrementalSplitsTensorLoad::_process_split(const struct ggml_context * ctx, struct llama_model_loader & ml, + uint16_t idx) { + SplitInfo & split = split_info[idx]; + + for (ggml_tensor * cur = ggml_get_first_tensor(ctx); cur; cur = ggml_get_next_tensor(ctx, cur)) { + std::string tensor_name = std::string(cur->name); + split.total_tensor_count++; + + // Add tensor info with initial loaded state as false + tensor_info[tensor_name] = TensorInfo{ idx, false }; + + auto it = ml.weights_map.find(tensor_name); + if (it == ml.weights_map.end()) { + throw std::runtime_error(format("tensor '%s' not found in weights_map", tensor_name.c_str())); + } + split.data_size += ggml_nbytes(it->second.tensor); + } +} + +uint16_t IncrementalSplitsTensorLoad::load_tensor_metadata(struct llama_model_loader & ml, const char * tensor_name, + ggml_tensor ** out_tensor_metadata) { + LLAMA_LOG_CMAKE_DEBUG("%s: loading tensor %s (tensor_meta=%p, delayed_loaded=%zu, delayed_files.size=%zu)\n", + __func__, tensor_name, (void *) *out_tensor_metadata, delayed_loaded, delayed_files.size()); + if (expected_tensors.find(tensor_name) == expected_tensors.end()) { + throw std::runtime_error(format("unknown tensor not expected in split files: %s", tensor_name)); + } + while (!(*out_tensor_metadata) && delayed_loaded < delayed_files.size()) { + // +1 because first split is expected to have been already loaded (not delayed) + _load_split(ml, delayed_loaded + 1); + *out_tensor_metadata = ml.get_tensor_meta(tensor_name); + delayed_loaded++; + if (*out_tensor_metadata) { + LLAMA_LOG_CMAKE_DEBUG("%s: tensor %s found in file %zu\n", __func__, tensor_name, delayed_loaded); + } + if (delayed_loaded == delayed_files.size() && ml.weights_map.size() != expected_n_tensors()) { + throw std::runtime_error( + format("finished incrementally loading all splits but expected %zu tensors, got %zu", + expected_n_tensors(), ml.weights_map.size())); + } + } + uint16_t split_idx = get_split_idx_for_tensor(tensor_name); + + // Mark tensor as loaded and increment split's loaded count + auto tensor_it = tensor_info.find(tensor_name); + if (!tensor_it->second.is_loaded) { + tensor_it->second.is_loaded = true; + split_info[split_idx].loaded_tensor_count++; + } + + return split_idx; +} + +uint16_t IncrementalSplitsTensorLoad::get_split_idx_for_tensor(const char * tensor_name) const { + return _get_tensor_info_iterator(tensor_name)->second.split_idx; +} + +std::size_t IncrementalSplitsTensorLoad::get_split_data_size(uint16_t split_idx) const { + return _get_split_info_iterator(split_idx)->second.data_size; +} + +void IncrementalSplitsTensorLoad::print_currently_known_tensors() const { + LLAMA_LOG_INFO("Current incremental loaded tensors:\n"); + for (const auto & it : tensor_info) { + LLAMA_LOG_INFO("Tensor '%s' in split %d (loaded: %s)\n", it.first.c_str(), it.second.split_idx, + it.second.is_loaded ? "yes" : "no"); + } +} + +bool IncrementalSplitsTensorLoad::all_tensors_are_loaded(uint16_t split_idx) const { + auto it = _get_split_info_iterator(split_idx); + const SplitInfo & split = it->second; + LLAMA_LOG_CMAKE_DEBUG("Loaded tensor count for split %d: %u/%u\n", split_idx, split.loaded_tensor_count, + split.total_tensor_count); + return split.all_tensors_loaded(); +} + +std::size_t IncrementalSplitsTensorLoad::expected_n_tensors() { + return expected_tensors.size(); +} + +void IncrementalSplitsTensorLoad::release_split(struct llama_model_loader & ml, uint16_t split_idx) { + // Let destructor of the smart pointer do the release of memory + ml.files[split_idx] = nullptr; +} + +std::map::const_iterator +IncrementalSplitsTensorLoad::_get_tensor_info_iterator(const char * tensor_name) const { + auto it = tensor_info.find(tensor_name); + if (it == tensor_info.end()) { + throw std::runtime_error(format("tensor '%s' not found in tensor_info map", tensor_name)); + } + return it; +} + +std::map::const_iterator +IncrementalSplitsTensorLoad::_get_split_info_iterator(uint16_t split_idx) const { + auto it = split_info.find(split_idx); + if (it == split_info.end()) { + throw std::runtime_error(format("split index %d not found in split_info map", split_idx)); + } + return it; +} + +bool IncrementalSplitsTensorLoad::SplitInfo::all_tensors_loaded() const { + return loaded_tensor_count >= total_tensor_count; +} + +bool IncrementalSplitsTensorLoad::tensor_ignored(const std::optional & splits_tensor_load, + const char * tensor_name) { + return !splits_tensor_load.has_value() || + (splits_tensor_load.has_value() && + splits_tensor_load->expected_tensors.find(tensor_name) == splits_tensor_load->expected_tensors.end()); +} diff --git a/src/llama-model-load.h b/src/llama-model-load.h new file mode 100644 index 000000000000..98d0caeb657d --- /dev/null +++ b/src/llama-model-load.h @@ -0,0 +1,148 @@ +#pragma once + +#include +#include + +#include "ggml-cpp.h" +#include "llama-mmap.h" +#include "llama-model-load-input.h" +#include "llama-impl.h" + +struct llama_model_loader; + +/// @brief Immediately loads and stores relevant data in the struct fields. +struct gguf_file_load { + struct gguf_init_params params; + gguf_context_ptr meta; + std::unique_ptr file = nullptr; + + gguf_file_load(struct ggml_context ** ctx, load_input_t load_input); +}; + +/// @brief Stores relevant information to be able to loads a `.gguf` split file when load method is called. +struct SplitLoad { + load_input_t load_input; + load_input_variant::fname_load_input base_split; + uint16_t idx; + std::string kv_split_no; + bool loaded = false; + + SplitLoad(load_input_t & load_input, load_input_variant::fname_load_input base_split, uint16_t idx, + std::string kv_split_no); + + static gguf_file_load load_split_gguf(struct ggml_context ** ctx, const char * fname_split, + load_input_t & load_input, std::vector & splits); + + struct ggml_context * load(struct llama_model_loader & ml); +}; + +/// @brief Handles incremental load of tensor and split-files. +/// @note First split-file is expected to be already available at construction, the remainder of split-files are +/// incrementally load on-demand by calling `load_tensor_metadata` +struct IncrementalSplitsTensorLoad { + IncrementalSplitsTensorLoad(struct ggml_context * ctx, struct llama_model_loader & ml, gguf_file_load & base_split, + std::set tensor_list); + + void add_split(SplitLoad splitLoad); + + /// @brief Incrementally loads file splits until the tensor metadata is found. + /// Also increments loaded tensor count so that `all_tensors_are_loaded` returns true + /// when all tensors in a file-split have been requested. + /// @returns Split idx where the tensor was found + /// @throw runtime_error if tensor was not found + uint16_t load_tensor_metadata(struct llama_model_loader & ml, const char * tensor_name, + ggml_tensor ** out_tensor_metadata); + + /// @returns True if all tensors of a split have been loaded. + bool all_tensors_are_loaded(uint16_t split_idx) const; + + /// @returns Max number of tensors as described on the summary tensor-list file. + std::size_t expected_n_tensors(); + + /// @bried Release file memory for a split. + static void release_split(struct llama_model_loader & ml, uint16_t split_idx); + + void print_currently_known_tensors() const; + + uint16_t get_split_idx_for_tensor(const char * tensor_name) const; + + std::size_t get_split_data_size(uint16_t split_idx) const; + + static bool tensor_ignored(const std::optional & splits_tensor_load, + const char * tensor_name); + + /// @brief Lalizy get/allocate a context with enough capacity for all tensors of + /// same type of an individual split. The context can be used to instantiate the + /// final model tensors and and attach to them backend buffers. + /// @tparam impl The model implementation type where the context will be stored. + ggml_context * get_model_ctx_for_split_buft(ggml_backend_buffer_type_t buft, uint16_t split) { + auto key = std::make_pair(buft, split); + auto it = ctx_split_map.find(key); + if (it == ctx_split_map.end()) { + LLAMA_LOG_CMAKE_DEBUG("%s: creating context for split %d (buft=%s, existing=%zu)\n", __func__, split, + ggml_backend_buft_name(buft), ctx_split_map.size()); + + const size_t max_n_tensors = _get_split_info_iterator(split)->second.total_tensor_count; + const size_t ctx_size = ggml_tensor_overhead() * max_n_tensors; + + ggml_init_params params = { + /*.mem_size =*/ctx_size, + /*.mem_buffer =*/NULL, + /*.no_alloc =*/true, + }; + + ggml_context * ctx = ggml_init(params); + if (!ctx) { + throw std::runtime_error("failed to create ggml context for split-file"); + } + + ctx_split_map[key] = ggml_context_ptr(ctx); + // Contexts are cleaned up when create_split_backend_buffers is called + // Review: this will be an issue if this ctx_split_map is used after create_split_backend_buffers is called + + return ctx; + } + return it->second.get(); + } + + // public so that it can be processed by the backend storage allocator + std::map, ggml_context_ptr> ctx_split_map; + + private: + struct TensorInfo { + uint16_t split_idx = 0; + bool is_loaded = false; + }; + + struct SplitInfo { + uint32_t total_tensor_count = 0, loaded_tensor_count = 0; + + /// @brief Total ggml tensor data size of this split + std::size_t data_size = 0; + + bool all_tensors_loaded() const; + }; + + void _load_split(struct llama_model_loader & ml, uint16_t idx); + void _process_split(const struct ggml_context * ctx, struct llama_model_loader & ml, uint16_t idx); + + /// @brief Get tensor info iterator or throw if not found + /// @throw runtime_error if tensor not found + std::map::const_iterator _get_tensor_info_iterator(const char * tensor_name) const; + + /// @brief Get split info iterator or throw if not found + /// @throw runtime_error if split not found + std::map::const_iterator _get_split_info_iterator(uint16_t split_idx) const; + + std::map tensor_info; + std::map split_info; + + /// @brief Number of delayed files that have been loaded + std::size_t delayed_loaded = 0; + + /// @brief Vector of split files to be loaded on demand + std::vector delayed_files; + + /// @brief Set of expected tensor names loaded from tensor list file + std::set expected_tensors; +}; diff --git a/src/llama-model-loader.cpp b/src/llama-model-loader.cpp index aa3a65f87a54..c9632067d5f4 100644 --- a/src/llama-model-loader.cpp +++ b/src/llama-model-loader.cpp @@ -1,11 +1,15 @@ #include "llama-model-loader.h" #include "ggml.h" +#include "llama-mmap.h" +#include "llama-model-load.h" #include #include +#include #include #include +#include static const size_t kiB = 1024; static const size_t MiB = 1024*kiB; @@ -464,13 +468,37 @@ namespace GGUFMeta { // TODO: this is not very clever - figure out something better template bool llama_model_loader::get_key_or_arr>(enum llm_kv kid, std::array & result, uint32_t n, bool required); - template bool llama_model_loader::get_key_or_arr>(enum llm_kv kid, std::array & result, uint32_t n, bool required); + template bool llama_model_loader::get_key_or_arr>(enum llm_kv kid, + std::array & result, + uint32_t n, bool required); template bool llama_model_loader::get_key_or_arr>(enum llm_kv kid, std::array & result, uint32_t n, bool required); + // Save tensors data offset of the main file. + // For subsidiary files, `meta` tensor data offset must not be used, + // so we build a unified tensors index for weights. + void llama_model_loader::process_loaded_gguf(struct ggml_context * ctx, gguf_file_load & gguf_load, uint16_t idx) { + contexts.emplace_back(ctx); + files.emplace_back(std::move(gguf_load.file)); + llama_file * raw_file_ptr = files.back().get(); + + // Save tensors data offset info of the shard. + for (ggml_tensor * cur = ggml_get_first_tensor(ctx); cur; cur = ggml_get_next_tensor(ctx, cur)) { + std::string tensor_name = std::string(cur->name); + LLAMA_LOG_CMAKE_DEBUG("%s: loaded tensor %s at split %d\n", tensor_name.c_str(), __func__, idx); + // make sure there is no duplicated tensor names + if (weights_map.find(tensor_name) != weights_map.end()) { + throw std::runtime_error(format("invalid model: tensor '%s' is duplicated", ggml_get_name(cur))); + } + n_elements += ggml_nelements(cur); + n_bytes += ggml_nbytes(cur); + weights_map.emplace(tensor_name, + llama_model_loader::llama_tensor_weight(raw_file_ptr, idx, gguf_load.meta.get(), cur)); + } + } + llama_model_loader::llama_model_loader( - const std::string & fname, - std::vector & splits, + load_input_t load_input, bool use_mmap, bool check_tensors, const llama_model_kv_override * param_overrides_p, @@ -488,58 +516,46 @@ llama_model_loader::llama_model_loader( tensor_buft_overrides = param_tensor_buft_overrides_p; - // Load the main GGUF + std::optional> tensor_list = load_input_variant::parse_tensor_list_from_future(load_input); + struct ggml_context * ctx = NULL; - struct gguf_init_params params = { - /*.no_alloc = */ true, - /*.ctx = */ &ctx, - }; + gguf_file_load main_gguf(&ctx, load_input); - meta.reset(gguf_init_from_file(fname.c_str(), params)); - if (!meta) { - throw std::runtime_error(format("%s: failed to load model from %s", __func__, fname.c_str())); + if (load_input_variant::variant_supports_split_load_from_memory(load_input)) { + incremental_splits_tensor_load.emplace(ctx, *this, main_gguf, std::move(*tensor_list)); + } else { + process_loaded_gguf(ctx, main_gguf, 0); } + meta = std::move(main_gguf.meta); + get_key(llm_kv(LLM_KV_GENERAL_ARCHITECTURE), arch_name, false); llm_kv = LLM_KV(llm_arch_from_string(arch_name)); - files.emplace_back(new llama_file(fname.c_str(), "rb")); - contexts.emplace_back(ctx); - - // Save tensors data offset of the main file. - // For subsidiary files, `meta` tensor data offset must not be used, - // so we build a unified tensors index for weights. - for (ggml_tensor * cur = ggml_get_first_tensor(ctx); cur; cur = ggml_get_next_tensor(ctx, cur)) { - std::string tensor_name = std::string(cur->name); - // make sure there is no duplicated tensor names - if (weights_map.find(tensor_name) != weights_map.end()) { - throw std::runtime_error(format("invalid model: tensor '%s' is duplicated", ggml_get_name(cur))); - } - n_elements += ggml_nelements(cur); - n_bytes += ggml_nbytes(cur); - weights_map.emplace(tensor_name, llama_tensor_weight(files.back().get(), 0, meta.get(), cur)); - } uint16_t n_split = 0; get_key(llm_kv(LLM_KV_SPLIT_COUNT), n_split, false); // Load additional GGML contexts - if (n_split > 1) { + if (load_input_variant::variant_supports_split_load(load_input) && n_split > 1) { + + load_input_variant::fname_load_input base_split = load_input_variant::split_name_from_variant(load_input); + // make sure the main file is loaded first uint16_t idx = 0; const std::string kv_split_no = llm_kv(LLM_KV_SPLIT_NO); get_key(kv_split_no, idx); if (idx != 0) { - throw std::runtime_error(format("illegal split file idx: %d (file: %s), model must be loaded with the first split", idx, fname.c_str())); + throw std::runtime_error(format("illegal split file idx: %d (file: %s), model must be loaded with the first split", idx, base_split.fname.c_str())); } // generate list of splits if needed - if (splits.empty()) { - splits = llama_get_list_splits(fname, idx, n_split); + if (base_split.splits.empty()) { + base_split.splits = llama_get_list_splits(base_split.fname, idx, n_split); } // in case user give a custom list of splits, check if it matches the expected number - if (n_split != (uint16_t)splits.size()) { - throw std::runtime_error(format("invalid split count, given: %zu splits, but expected %d", splits.size(), n_split)); + if (n_split != (uint16_t)base_split.splits.size()) { + throw std::runtime_error(format("invalid split count, given: %zu splits, but expected %d", base_split.splits.size(), n_split)); } if (trace > 0) { @@ -548,49 +564,20 @@ llama_model_loader::llama_model_loader( // load other splits for (idx = 1; idx < n_split; idx++) { - const char * fname_split = splits[idx].c_str(); - - struct gguf_init_params split_params = { - /*.no_alloc = */ true, - /*.ctx = */ &ctx, - }; - gguf_context_ptr ctx_gguf { gguf_init_from_file(fname_split, split_params) }; - if (!ctx_gguf) { - throw std::runtime_error(format("%s: failed to load GGUF split from %s", __func__, fname_split)); - } + SplitLoad split_load(load_input, base_split, idx, kv_split_no); - // check idx - { - const int kid = gguf_find_key(ctx_gguf.get(), kv_split_no.c_str()); - if (kid < 0) { - throw std::runtime_error(format("missing key %s in GGUF split %s", kv_split_no.c_str(), fname_split)); - } - int idx_gguf = gguf_get_val_u16(ctx_gguf.get(), kid); - if (idx_gguf != idx) { - throw std::runtime_error(format("invalid split file idx: %d (file: %s), expected %d", idx_gguf, fname_split, idx)); - } + if(incremental_splits_tensor_load.has_value()) { + incremental_splits_tensor_load->add_split(std::move(split_load)); } - - files.emplace_back(new llama_file(fname_split, "rb")); - contexts.emplace_back(ctx); - - // Save tensors data offset info of the shard. - for (ggml_tensor * cur = ggml_get_first_tensor(ctx); cur; cur = ggml_get_next_tensor(ctx, cur)) { - std::string tensor_name = std::string(cur->name); - // make sure there is no duplicated tensor names - if (weights_map.find(tensor_name) != weights_map.end()) { - throw std::runtime_error(format("invalid model: tensor '%s' is duplicated", ggml_get_name(cur))); - } - n_elements += ggml_nelements(cur); - n_bytes += ggml_nbytes(cur); - weights_map.emplace(tensor_name, llama_tensor_weight(files.back().get(), idx, ctx_gguf.get(), cur)); + else { + split_load.load(*this); } } get_key(llm_kv(LLM_KV_SPLIT_TENSORS_COUNT), n_tensors); - // sanity check - { + // sanity check (the incremental loader does the check after loading the last split) + if(!incremental_splits_tensor_load.has_value()) { const int n_tensors_loaded = (int) weights_map.size(); if (n_tensors != n_tensors_loaded) { throw std::runtime_error(format("corrupted model: %d tensors expected but %d found", n_tensors, n_tensors_loaded)); @@ -601,16 +588,22 @@ llama_model_loader::llama_model_loader( } n_kv = gguf_get_n_kv(meta.get()); - n_tensors = weights_map.size(); + if (incremental_splits_tensor_load.has_value()) { + n_tensors = incremental_splits_tensor_load->expected_n_tensors(); + LLAMA_LOG_CMAKE_DEBUG("%s: n_tensors (expected from summary list): %d\n", __func__, n_tensors); + } else { + n_tensors = weights_map.size(); + LLAMA_LOG_CMAKE_DEBUG("%s: exact n_tensors: %d\n", __func__, n_tensors); + } fver = (enum llama_fver) gguf_get_version(meta.get()); LLAMA_LOG_INFO("%s: loaded meta data with %d key-value pairs and %d tensors from %s (version %s)\n", - __func__, n_kv, n_tensors, fname.c_str(), llama_file_version_name(fver)); + __func__, n_kv, n_tensors, load_input_variant::identifier(load_input), llama_file_version_name(fver)); // determine file type based on the number of tensors for each quantization and print meta data // TODO: make optional - { + if(!incremental_splits_tensor_load.has_value()) { std::map n_type; uint32_t n_type_max = 0; @@ -919,12 +912,9 @@ void llama_model_loader::load_data_for(struct ggml_tensor * cur) const { } } -bool llama_model_loader::load_all_data( - struct ggml_context * ctx, - llama_buf_map & bufs, - llama_mlocks * lmlocks, - llama_progress_callback progress_callback, - void * progress_callback_user_data) { +bool llama_model_loader::load_all_data(size_t size_data, struct ggml_context * ctx, llama_buf_map & bufs, + llama_mlocks * lmlocks, llama_progress_callback progress_callback, + void * progress_callback_user_data) { GGML_ASSERT(size_data != 0 && "call init_mappings() first"); std::vector> read_buf; @@ -1064,6 +1054,12 @@ bool llama_model_loader::load_all_data( } } else { const auto & file = files.at(weight->idx); + if (file == nullptr) { + throw std::runtime_error( + format("file not found for tensor '%s' at split-index %d", ggml_get_name(cur), weight->idx)); + } + LLAMA_LOG_CMAKE_DEBUG("%s: uploading tensor %s from file at split-index %d\n", __func__, ggml_get_name(cur), + weight->idx); if (ggml_backend_buffer_is_host(cur->buffer)) { file->seek(weight->offs, SEEK_SET); file->read_raw(cur->data, n_size); diff --git a/src/llama-model-loader.h b/src/llama-model-loader.h index c9189f6cb446..6348d8c95c19 100644 --- a/src/llama-model-loader.h +++ b/src/llama-model-loader.h @@ -5,6 +5,7 @@ #include "llama-impl.h" #include "llama-arch.h" #include "llama-mmap.h" +#include "llama-model-load.h" #include "ggml-cpp.h" @@ -79,6 +80,9 @@ struct llama_model_loader { llama_mmaps mappings; std::map weights_map; + + std::optional incremental_splits_tensor_load; + std::unordered_map kv_overrides; const llama_model_tensor_buft_override * tensor_buft_overrides; @@ -92,9 +96,10 @@ struct llama_model_loader { size_t size_data = 0; std::vector> mmaps_used; + void process_loaded_gguf(struct ggml_context * ctx, gguf_file_load & gguf_load, uint16_t idx); + llama_model_loader( - const std::string & fname, - std::vector & splits, // optional, only need if the split does not follow naming scheme + load_input_t load_input, bool use_mmap, bool check_tensors, const llama_model_kv_override * param_overrides_p, @@ -157,12 +162,8 @@ struct llama_model_loader { void load_data_for(struct ggml_tensor * cur) const; // Returns false if cancelled by progress_callback - bool load_all_data( - struct ggml_context * ctx, - llama_buf_map & bufs, - llama_mlocks * lmlocks, - llama_progress_callback progress_callback, - void * progress_callback_user_data); + bool load_all_data(size_t size_data, struct ggml_context * ctx, llama_buf_map & bufs, llama_mlocks * lmlocks, + llama_progress_callback progress_callback, void * progress_callback_user_data); std::string ftype_name() const; diff --git a/src/llama-model.cpp b/src/llama-model.cpp index c3675dbdc414..0779bcb831dd 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -12,15 +12,19 @@ #include "ggml-cpp.h" +#ifdef __APPLE__ +#include +#endif + #include "models/models.h" #include #include #include +#include #include #include #include -#include #include #include #include @@ -2357,7 +2361,13 @@ bool llama_model::load_tensors(llama_model_loader & ml) { throw std::runtime_error(format("%s: no CPU backend found", __func__)); } const int i_gpu_start = std::max((int) hparams.n_layer - n_gpu_layers, (int) 0); - const int act_gpu_layers = devices.empty() ? 0 : std::min(n_gpu_layers, (int)n_layer + 1); + #if TARGET_OS_IPHONE + const int max_gpu_layers = (int)n_layer; + #else + const int max_gpu_layers = (int)n_layer + 1; + #endif + const int act_gpu_layers = devices.empty() ? 0 : std::min(n_gpu_layers, max_gpu_layers); + auto get_layer_buft_list = [&](int il) -> llama_model::impl::layer_dev { const bool is_swa = il < (int) hparams.n_layer && hparams.is_swa(il); if (il < i_gpu_start || (il - i_gpu_start) >= act_gpu_layers) { @@ -2389,12 +2399,6 @@ bool llama_model::load_tensors(llama_model_loader & ml) { max_n_tensors += n_layer*2; // duplicated rope freq tensors const size_t ctx_size = ggml_tensor_overhead()*max_n_tensors; - // define a comparator for the buft -> ctx map to ensure that the order is well-defined: - struct ggml_backend_buft_comparator { - bool operator()(const ggml_backend_buffer_type_t & lhs, const ggml_backend_buffer_type_t & rhs) const { - return strcmp(ggml_backend_buft_name(lhs), ggml_backend_buft_name(rhs)) < 0; - } - }; std::map ctx_map; auto ctx_for_buft = [&](ggml_backend_buffer_type_t buft) -> ggml_context * { @@ -2451,9 +2455,35 @@ bool llama_model::load_tensors(llama_model_loader & ml) { ggml_backend_buffer_type_t first_moved_to_buft = nullptr; auto create_tensor = [&](const LLM_TN_IMPL & tn, const std::initializer_list & ne, int flags) -> ggml_tensor * { - ggml_tensor * t_meta = ml.get_tensor_meta(tn.str().c_str()); + const std::string& tensor_name = tn.str(); + // With incremental split loading, a TENSOR_DUPLICATED reference + // (e.g. rope_freqs.weight shared across every layer) may be + // requested after its owning split has already been uploaded and + // released. The original tensor still lives in tensors_by_name + // (populated by create_split_backend_buffers), so look it up there + // before touching the incremental loader — this avoids + // double-counting loaded tensors and accessing a released file. + if ((flags & TENSOR_DUPLICATED) && ml.incremental_splits_tensor_load.has_value()) { + for (auto & [name, tensor] : tensors_by_name) { + if (name == tensor_name) { + return tensor; + } + } + } + + ggml_tensor * t_meta = ml.get_tensor_meta(tensor_name.c_str()); + std::optional split_idx; + if (!t_meta && (flags & TENSOR_NOT_REQUIRED) && + IncrementalSplitsTensorLoad::tensor_ignored(ml.incremental_splits_tensor_load, tensor_name.c_str())) { + return nullptr; + } + if (ml.incremental_splits_tensor_load.has_value()) { + split_idx = ml.incremental_splits_tensor_load->load_tensor_metadata(ml, tn.str().c_str(), &t_meta); + LLAMA_LOG_CMAKE_DEBUG("split idx for tensor %s: %d\n", tn.str().c_str(), *split_idx); + } if (!t_meta) { + LLAMA_LOG_ERROR("%s: missing tensor %s\n", __func__ , tn.str().c_str()); if (flags & TENSOR_NOT_REQUIRED) { return nullptr; } @@ -2576,16 +2606,32 @@ bool llama_model::load_tensors(llama_model_loader & ml) { } } - ggml_context * ctx = ctx_for_buft(buft); + ggml_context * ctx = + split_idx.has_value() ? + ml.incremental_splits_tensor_load->get_model_ctx_for_split_buft(buft, *split_idx) : + ctx_for_buft(buft); // if duplicated, check if the original tensor was allocated in the same buffer type context and avoid creating a new one if (flags & TENSOR_DUPLICATED) { - ggml_tensor * t = ggml_get_tensor(ctx, tn.str().c_str()); + auto tn_str = tn.str(); + ggml_tensor * t = ggml_get_tensor(ctx, tn_str.c_str()); if (t) { return t; } + LLAMA_LOG_WARN("%s: duplicated tensor %s not found on existing context\n", tn_str.c_str(), __func__); + } + struct ggml_tensor * tensor = ml.create_tensor(ctx, tn, ne, flags); + + if (split_idx.has_value() && ml.incremental_splits_tensor_load->all_tensors_are_loaded(*split_idx)) { + // Upload right now. + if (!create_split_backend_buffers(*split_idx, ml.incremental_splits_tensor_load->ctx_split_map, ml, + use_mmap_buffer, use_mlock, n_gpu_layers)) { + throw std::runtime_error("Failed to create incremental backend buffers"); + } + IncrementalSplitsTensorLoad::release_split(ml, *split_idx); } - return ml.create_tensor(ctx, tn, ne, flags); + + return tensor; }; layers.resize(n_layer); @@ -6557,9 +6603,52 @@ bool llama_model::load_tensors(llama_model_loader & ml) { ml.done_getting_tensors(); + if (ml.incremental_splits_tensor_load.has_value()) { + // Already did incremental load. + print_backend_buffers_info(n_gpu_layers); + return true; + } + ml.init_mappings(true, use_mlock ? &pimpl->mlock_mmaps : nullptr); pimpl->mappings.reserve(ml.mappings.size()); + return create_backend_buffers(ml.size_data, ctx_map, ml, use_mmap_buffer, use_mlock, n_gpu_layers); +} + +bool llama_model::create_split_backend_buffers( + const uint16_t idx, std::map, ggml_context_ptr> & ctx_split_map, + llama_model_loader & ml, const bool use_mmap_buffer, const bool use_mlock, const int32_t n_gpu_layers) { + // Extract contexts for the given split index from ctx_split_map into a new map + std::map ctx_map; + for (auto it = ctx_split_map.begin(); it != ctx_split_map.end();) { + const auto & [buft_split_idx, ctx_ptr] = *it; + const auto & [buft, split_idx] = buft_split_idx; + if (split_idx == idx) { + // Move the context from ctx_split_map to ctx_map + ctx_map[buft] = std::move(it->second); + // Remove from ctx_split_map since ownership has been transferred + it = ctx_split_map.erase(it); + } else { + ++it; + } + } + + const std::size_t split_data_size = ml.incremental_splits_tensor_load->get_split_data_size(idx); + LLAMA_LOG_CMAKE_DEBUG("%s: creating backend buffers for split %d with size %zu\n", __func__, idx, split_data_size); + constexpr bool do_print_backend_buffers_info = false; + const bool creation_success = create_backend_buffers(split_data_size, ctx_map, ml, use_mmap_buffer, use_mlock, + n_gpu_layers, do_print_backend_buffers_info); + + // Note: create_backend_buffers moves the contexts into ctxs_bufs, taking ownership + // The contexts in ctx_map are now empty after the move, which is expected + + return creation_success; +} + +bool llama_model::create_backend_buffers(std::size_t size_data, + std::map & ctx_map, + llama_model_loader & ml, const bool use_mmap_buffer, const bool use_mlock, + const int32_t n_gpu_layers, bool do_print_backend_buffers_info) { // create the backend buffers std::vector> ctx_buf_maps; ctx_buf_maps.reserve(ctx_map.size()); @@ -6641,26 +6730,8 @@ bool llama_model::load_tensors(llama_model_loader & ml) { ctx_buf_maps.emplace_back(ctx, buf_map); } - if (llama_supports_gpu_offload()) { - const int n_gpu = std::min(n_gpu_layers, int(hparams.n_layer)); - - LLAMA_LOG_INFO("%s: offloading %d repeating layers to GPU\n", __func__, n_gpu); - if (n_gpu_layers > (int) hparams.n_layer) { - LLAMA_LOG_INFO("%s: offloading output layer to GPU\n", __func__); - } - - const int max_backend_supported_layers = hparams.n_layer + 1; - const int max_offloadable_layers = hparams.n_layer + 1; - - LLAMA_LOG_INFO("%s: offloaded %d/%d layers to GPU\n", __func__, std::min(n_gpu_layers, max_offloadable_layers), max_backend_supported_layers); - } - - // print memory requirements per buffer type - for (auto & [_, bufs] : pimpl->ctxs_bufs) { - for (auto & buf: bufs) { - LLAMA_LOG_INFO("%s: %12s model buffer size = %8.2f MiB\n", - __func__, ggml_backend_buffer_name(buf.get()), ggml_backend_buffer_get_size(buf.get()) / 1024.0 / 1024.0); - } + if(do_print_backend_buffers_info) { + print_backend_buffers_info(n_gpu_layers); } // populate tensors_by_name @@ -6672,7 +6743,7 @@ bool llama_model::load_tensors(llama_model_loader & ml) { // load tensor data for (auto & [ctx, buf_map] : ctx_buf_maps) { - if (!ml.load_all_data(ctx, buf_map, use_mlock ? &pimpl->mlock_mmaps : NULL, params.progress_callback, params.progress_callback_user_data)) { + if (!ml.load_all_data(size_data, ctx, buf_map, use_mlock ? &pimpl->mlock_mmaps : NULL, params.progress_callback, params.progress_callback_user_data)) { return false; } } @@ -6686,6 +6757,30 @@ bool llama_model::load_tensors(llama_model_loader & ml) { return true; } +void llama_model::print_backend_buffers_info(const int32_t n_gpu_layers) { + if (llama_supports_gpu_offload()) { + const int n_gpu = std::min(n_gpu_layers, int(hparams.n_layer)); + + LLAMA_LOG_INFO("%s: offloading %d repeating layers to GPU\n", __func__, n_gpu); + if (n_gpu_layers > (int) hparams.n_layer) { + LLAMA_LOG_INFO("%s: offloading output layer to GPU\n", __func__); + } + + const int max_backend_supported_layers = hparams.n_layer + 1; + const int max_offloadable_layers = hparams.n_layer + 1; + + LLAMA_LOG_INFO("%s: offloaded %d/%d layers to GPU\n", __func__, std::min(n_gpu_layers, max_offloadable_layers), max_backend_supported_layers); + } + + // print memory requirements per buffer type + for (auto & [_, bufs] : pimpl->ctxs_bufs) { + for (auto & buf: bufs) { + LLAMA_LOG_INFO("%s: %12s model buffer size = %8.2f MiB\n", + __func__, ggml_backend_buffer_name(buf.get()), ggml_backend_buffer_get_size(buf.get()) / 1024.0 / 1024.0); + } + } +} + std::string llama_model::arch_name() const { return llm_arch_name(arch); } diff --git a/src/llama-model.h b/src/llama-model.h index f8342cf2cb13..93964548fb37 100644 --- a/src/llama-model.h +++ b/src/llama-model.h @@ -7,11 +7,13 @@ #include "llama-memory.h" #include "llama-vocab.h" -#include +#include +#include #include #include #include #include +#include struct llama_cparams; struct llama_ubatch; @@ -413,6 +415,13 @@ struct llama_layer { struct llama_layer_nextn nextn; }; +// define a comparator for the buft -> ctx map to ensure that the order is well-defined: +struct ggml_backend_buft_comparator { + bool operator()(const ggml_backend_buffer_type_t & lhs, const ggml_backend_buffer_type_t & rhs) const { + return strcmp(ggml_backend_buft_name(lhs), ggml_backend_buft_name(rhs)) < 0; + } +}; + struct llama_model { llm_type type = LLM_TYPE_UNKNOWN; llm_arch arch = LLM_ARCH_UNKNOWN; @@ -478,6 +487,19 @@ struct llama_model { explicit llama_model(const struct llama_model_params & params); ~llama_model(); + /// @brief Create backend buffers for all tensors + bool create_backend_buffers(std::size_t size_data, + std::map & ctx_map, + llama_model_loader & ml, bool use_mmap_buffer, bool use_mlock, int32_t n_gpu_layers, + bool do_print_backend_buffers_info = true); + + /// @brief Create backend buffers for tensors on a split file idenfified by `idx`. Removes the split from the map. + bool create_split_backend_buffers( + uint16_t idx, std::map, ggml_context_ptr> & ctx_split_map, + llama_model_loader & ml, bool use_mmap_buffer, bool use_mlock, int32_t n_gpu_layers); + + void print_backend_buffers_info(int32_t n_gpu_layers); + void load_stats (llama_model_loader & ml); void load_arch (llama_model_loader & ml); void load_hparams(llama_model_loader & ml); diff --git a/src/llama-quant.cpp b/src/llama-quant.cpp index 0b23eaef3a8b..599cc8fc9c6a 100644 --- a/src/llama-quant.cpp +++ b/src/llama-quant.cpp @@ -596,7 +596,8 @@ static void llama_model_quantize_impl(const std::string & fname_inp, const std:: } std::vector splits = {}; - llama_model_loader ml(fname_inp, splits, use_mmap, /*check_tensors*/ true, kv_overrides, nullptr); + load_input_variant::fname_load_input inp{fname_inp, splits}; + llama_model_loader ml(inp, use_mmap, /*check_tensors*/ true, kv_overrides, nullptr); ml.init_mappings(false); // no prefetching llama_model model(llama_model_default_params()); diff --git a/src/llama.cpp b/src/llama.cpp index ab2e9868af46..356a65999828 100644 --- a/src/llama.cpp +++ b/src/llama.cpp @@ -9,6 +9,7 @@ #include "ggml.h" #include "ggml-backend.h" +#include "uint8-buff-stream.h" #include #include @@ -16,11 +17,17 @@ #include #include #include +#include +#include #if defined(_MSC_VER) #pragma warning(disable: 4244 4267) // possible loss of data #endif +#ifdef __cplusplus +#include "llama-cpp.h" +#endif + // // interface implementation // @@ -99,7 +106,7 @@ int64_t llama_time_us(void) { } // Returns 0 on success, -1 on error, and -2 on cancellation via llama_progress_callback -static int llama_model_load(const std::string & fname, std::vector & splits, llama_model & model, llama_model_params & params) { +static int llama_model_load(llama_model_loader & ml, llama_model & model, llama_model_params & params) { // loading time will be recalculated after the first eval, so // we take page faults deferred by mmap() into consideration model.t_load_us = 0; @@ -108,8 +115,6 @@ static int llama_model_load(const std::string & fname, std::vector model.t_start_us = tm.t_start_us; try { - llama_model_loader ml(fname, splits, params.use_mmap, params.check_tensors, params.kv_overrides, params.tensor_buft_overrides); - ml.print_info(); model.hparams.vocab_only = params.vocab_only; @@ -153,8 +158,7 @@ static int llama_model_load(const std::string & fname, std::vector } static struct llama_model * llama_model_load_from_file_impl( - const std::string & path_model, - std::vector & splits, + llama_model_loader& ml, struct llama_model_params params) { ggml_time_init(); @@ -276,7 +280,7 @@ static struct llama_model * llama_model_load_from_file_impl( props.memory_free/1024/1024); } - const int status = llama_model_load(path_model, splits, *model, params); + const int status = llama_model_load(ml, *model, params); GGML_ASSERT(status <= 0); if (status < 0) { if (status == -1) { @@ -299,27 +303,173 @@ struct llama_model * llama_load_model_from_file( return llama_model_load_from_file(path_model, params); } -struct llama_model * llama_model_load_from_file( - const char * path_model, - struct llama_model_params params) { +static llama_model_loader create_disk_fileloader(const char * path_model, std::vector & splits, + struct llama_model_params params) { + load_input_variant::fname_load_input loader_input{ path_model, splits }; + return llama_model_loader(loader_input, params.use_mmap, params.check_tensors, params.kv_overrides, + params.tensor_buft_overrides); +} + +struct llama_model * llama_model_load_from_file(const char * path_model, struct llama_model_params params) { std::vector splits = {}; - return llama_model_load_from_file_impl(path_model, splits, params); + llama_model_loader ml = create_disk_fileloader(path_model, splits, params); + return llama_model_load_from_file_impl(ml, params); } -struct llama_model * llama_model_load_from_splits( - const char ** paths, - size_t n_paths, - struct llama_model_params params) { +namespace { +void override_and_disable_mmap(struct llama_model_params & params) { + if (params.use_mmap) { + LLAMA_LOG_WARN("Overriding and disabling memory mapping when loading from memory buffer\n"); + params.use_mmap = false; + } +} +} // namespace + +struct llama_model * llama_model_load_from_buffer(std::vector && data, struct llama_model_params params) { + std::unique_ptr> streambuf = std::make_unique(std::move(data)); + override_and_disable_mmap(params); + llama_model_loader ml(load_input_variant::buffer_load_input{ streambuf }, params.use_mmap, params.check_tensors, + params.kv_overrides, params.tensor_buft_overrides); + return llama_model_load_from_file_impl(ml, params); +} + +namespace { +std::vector splits_from_c_paths(const char ** paths, size_t n_paths) { std::vector splits; if (n_paths == 0) { LLAMA_LOG_ERROR("%s: list of splits is empty\n", __func__); - return nullptr; + return splits; } splits.reserve(n_paths); for (size_t i = 0; i < n_paths; ++i) { splits.push_back(paths[i]); } - return llama_model_load_from_file_impl(splits.front(), splits, params); + return splits; +} + +template +MetaResultStatus llama_model_meta_get_impl(const metadata_handle_ptr & meta_handle, + const char * key, + T * value, + F && getter) { + if (meta_handle.get() == nullptr) { + return MetaResultStatus::META_HANDLE_NULL; + } + if (key == nullptr) { + return MetaResultStatus::KEY_NULL; + } + if (value == nullptr) { + return MetaResultStatus::VALUE_NULL; + } + const struct gguf_context * meta = reinterpret_cast(meta_handle.get()); + const int key_id = gguf_find_key(meta, key); + if (key_id < 0) { + return MetaResultStatus::KEY_NOT_FOUND; + } + if (gguf_get_kv_type(meta, key_id) != ExpectedType) { + return TypeMismatchErr; + } + *value = getter(meta, key_id); + return MetaResultStatus::SUCCESS; +} +} // namespace + +void metadata_handle_deleter::operator()(struct llama_metadata_handle * ctx) const { + gguf_free(reinterpret_cast(ctx)); +} + +MetaResultStatus llama_model_meta_get_u32(const metadata_handle_ptr & meta_handle, const char * key, uint32_t * value) { + return llama_model_meta_get_impl(meta_handle, key, value, + gguf_get_val_u32); +} + +MetaResultStatus llama_model_meta_get_str(const metadata_handle_ptr & meta_handle, + const char * key, + std::string * value) { + return llama_model_meta_get_impl(meta_handle, key, value, + gguf_get_val_str); +} + +MetaResultStatus llama_model_meta_from_file(const char * path_model, metadata_handle_ptr * out_meta_handle) { + if (path_model == nullptr) { + return MetaResultStatus::PATH_NULL; + } + if (out_meta_handle == nullptr) { + return MetaResultStatus::NULL_OUT_META_HANDLE; + } + struct gguf_init_params params = { + /*.no_alloc = */ true, + /*.ctx = */ nullptr, + /*.kv_only = */ true, + }; + + gguf_context_ptr meta(gguf_init_from_file(path_model, params)); + if (!meta) { + return MetaResultStatus::GGUF_INIT_FAILED; + } + + *out_meta_handle = metadata_handle_ptr(reinterpret_cast(meta.release())); + return MetaResultStatus::SUCCESS; +} + +MetaResultStatus llama_model_meta_from_streambuf(std::basic_streambuf & streambuf, metadata_handle_ptr * out_meta_handle) { + if (out_meta_handle == nullptr) { + return MetaResultStatus::NULL_OUT_META_HANDLE; + } + const auto current_pos = streambuf.pubseekoff(0, std::ios_base::cur, std::ios_base::in); + if (streambuf.pubseekoff(0, std::ios_base::beg, std::ios_base::in) == std::streampos(std::streamoff(-1))) { + return MetaResultStatus::STREAMBUF_SEEK_FAILED; + } + struct gguf_init_params params = { + /*.no_alloc = */ true, + /*.ctx = */ nullptr, + /*.kv_only = */ true, + }; + + gguf_context_ptr meta(gguf_init_from_buffer(streambuf, params)); + + if (current_pos != std::streampos(std::streamoff(-1))) { + streambuf.pubseekpos(current_pos, std::ios_base::in); + } + + if (!meta) { + return MetaResultStatus::GGUF_INIT_FAILED; + } + + *out_meta_handle = metadata_handle_ptr(reinterpret_cast(meta.release())); + return MetaResultStatus::SUCCESS; +} + +struct llama_model * llama_model_load_from_splits(const char ** paths, size_t n_paths, + struct llama_model_params params) { + std::vector splits = splits_from_c_paths(paths, n_paths); + if (splits.empty()) { + return nullptr; + } + llama_model_loader ml = create_disk_fileloader(splits.front().c_str(), splits, params); + return llama_model_load_from_file_impl(ml, params); +} + +struct llama_model * llama_model_load_from_split_futures(const char ** paths, size_t n_paths, const char * context, + const char * tensor_list_file, + struct llama_model_params params) { + std::vector splits = splits_from_c_paths(paths, n_paths); + if (splits.empty()) { + return nullptr; + } + std::string tensor_list_file_str(tensor_list_file); + + load_input_variant::buffer_future_load_input loader_input{ splits.front(), context, splits, tensor_list_file_str }; + override_and_disable_mmap(params); + llama_model_loader ml(loader_input, params.use_mmap, params.check_tensors, params.kv_overrides, + params.tensor_buft_overrides); + return llama_model_load_from_file_impl(ml, params); +} + +bool llama_model_load_fulfill_split_future(const char * path, const char * context, + std::unique_ptr> && streambuf) { + return llama_future_file_buffer_ro::fulfill_promise(path, context, + std::make_unique(std::move(streambuf))); } void llama_model_save_to_file(const struct llama_model * model, const char * path_model) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9361a113a19b..35f6d42d634c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,7 +8,7 @@ function(llama_build source) endif() add_executable(${TEST_TARGET} ${source}) - target_link_libraries(${TEST_TARGET} PRIVATE common) + target_link_libraries(${TEST_TARGET} PRIVATE common llama-common-test) install(TARGETS ${TEST_TARGET} RUNTIME) endfunction() @@ -97,7 +97,7 @@ function(llama_build_and_test source) add_executable(${TEST_TARGET} ${source} get-model.cpp) install(TARGETS ${TEST_TARGET} RUNTIME) - target_link_libraries(${TEST_TARGET} PRIVATE common) + target_link_libraries(${TEST_TARGET} PRIVATE common llama-common-test) add_test( NAME ${TEST_TARGET} @@ -204,6 +204,9 @@ llama_build_and_test(test-gguf.cpp) llama_build_and_test(test-backend-ops.cpp) llama_build_and_test(test-model-load-cancel.cpp LABEL "model") +llama_build_and_test(test-model-load-disk.cpp LABEL "model") +llama_build_and_test(test-model-load-memory.cpp LABEL "model") +llama_build_and_test(test-model-load-memory-split.cpp LABEL "model") llama_build_and_test(test-autorelease.cpp LABEL "model") if (NOT GGML_BACKEND_DL) @@ -215,9 +218,11 @@ if (NOT GGML_BACKEND_DL) endif() # libmtmd -set(LLAMA_TEST_NAME test-mtmd-c-api) -llama_build_and_test(test-mtmd-c-api.c) -target_link_libraries(${LLAMA_TEST_NAME} PRIVATE mtmd) +if (LLAMA_MTMD) + set(LLAMA_TEST_NAME test-mtmd-c-api) + llama_build_and_test(test-mtmd-c-api.c) + target_link_libraries(${LLAMA_TEST_NAME} PRIVATE mtmd) +endif() # dummy executable - not installed get_filename_component(TEST_TARGET test-c.c NAME_WE) diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 7ef7f2ad81e4..3e58a770c4d5 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -2108,6 +2108,39 @@ struct test_swiglu_oai : public test_case { } }; +// GGML_OP_GEGLU_BACK +struct test_geglu_back : public test_case { + const ggml_type type; + const std::array ne; + float eps; + + std::string vars() override { + return VARS_TO_STR3(type, ne, eps); + } + + test_geglu_back(ggml_type type = GGML_TYPE_F32, + std::array ne = {64, 5, 4, 3}, + float eps = 1e-6f) + : type(type), ne(ne), eps(eps) {} + + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * gate = ggml_new_tensor(ctx, type, 4, ne.data()); + ggml_set_name(gate, "gate"); + + ggml_tensor * grad = ggml_new_tensor(ctx, type, 4, ne.data()); + ggml_set_name(grad, "grad"); + + ggml_tensor * out = ggml_geglu_back(ctx, grad, gate); + ggml_set_name(out, "out"); + + return out; + } + + bool grad_precise() override { + return true; + } +}; + // GGML_OP_GET_ROWS struct test_get_rows : public test_case { const ggml_type type; @@ -5928,6 +5961,124 @@ struct test_cross_entropy_loss_back : public test_case { } }; +// GGML_OP_CROSS_ENTROPY_LOSS_MASKED +struct test_cross_entropy_loss_masked : public test_case { + const ggml_type type; + const std::array ne; + + std::string vars() override { + return VARS_TO_STR2(type, ne); + } + + test_cross_entropy_loss_masked(ggml_type type = GGML_TYPE_F32, + std::array ne = {10, 5, 4, 3}) + : type(type), ne(ne) {} + + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * logits = ggml_new_tensor(ctx, type, 4, ne.data()); + ggml_set_param(logits); + ggml_set_name(logits, "logits"); + + ggml_tensor * labels = ggml_new_tensor(ctx, type, 4, ne.data()); + ggml_set_name(labels, "labels"); + + labels = ggml_soft_max(ctx, labels); + ggml_set_name(labels, "labels_normalized"); + + ggml_tensor * mask = ggml_new_tensor(ctx, GGML_TYPE_F32, 4, ne.data()); + ggml_set_name(mask, "mask"); + + ggml_tensor * out = ggml_cross_entropy_loss_masked(ctx, logits, labels, mask); + ggml_set_name(out, "out"); + + return out; + } + + void initialize_tensors(ggml_context * ctx) override { + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != NULL; t = ggml_get_next_tensor(ctx, t)) { + if (strcmp(t->name, "mask") == 0) { + init_tensor_uniform(t, 0.0f, 1.0f); + if (t->type == GGML_TYPE_F32) { + std::vector data(ggml_nelements(t)); + ggml_backend_tensor_get(t, data.data(), 0, ggml_nbytes(t)); + for (int i = 0; i < (int)data.size(); i++) { + data[i] = data[i] > 0.5f ? 1.0f : 0.0f; + } + ggml_backend_tensor_set(t, data.data(), 0, ggml_nbytes(t)); + } + } else { + init_tensor_uniform(t, -100.0f, 100.0f); + } + } + } + + float grad_eps() override { + return 1.0f; + } + + bool grad_precise() override { + return true; + } +}; + +// GGML_OP_CROSS_ENTROPY_LOSS_MASKED_BACK +struct test_cross_entropy_loss_masked_back : public test_case { + const ggml_type type; + const std::array ne; + + std::string vars() override { + return VARS_TO_STR2(type, ne); + } + + test_cross_entropy_loss_masked_back(ggml_type type = GGML_TYPE_F32, + std::array ne = {10, 5, 4, 3}) + : type(type), ne(ne) {} + + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * grad = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1); + ggml_set_name(grad, "grad"); + + ggml_tensor * logits = ggml_new_tensor(ctx, type, 4, ne.data()); + ggml_set_name(logits, "logits"); + + ggml_tensor * labels = ggml_new_tensor(ctx, type, 4, ne.data()); + ggml_set_name(labels, "labels"); + + labels = ggml_soft_max(ctx, labels); + ggml_set_name(labels, "labels_normalized"); + + ggml_tensor * mask = ggml_new_tensor(ctx, GGML_TYPE_F32, 4, ne.data()); + ggml_set_name(mask, "mask"); + + // ggml_cross_entropy_loss_masked_back(ctx, a, b, c, d) + // a=logits, b=labels, c=mask, d=grad (scalar) + ggml_tensor * out = ggml_cross_entropy_loss_masked_back(ctx, logits, labels, mask, grad); + ggml_set_name(out, "out"); + + return out; + } + + void initialize_tensors(ggml_context * ctx) override { + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != NULL; t = ggml_get_next_tensor(ctx, t)) { + if (strcmp(t->name, "mask") == 0) { + // Initialize mask with 0.0 or 1.0 + init_tensor_uniform(t, 0.0f, 1.0f); + if (t->type == GGML_TYPE_F32) { + // Safe access for device memory (e.g. Vulkan) + std::vector data(ggml_nelements(t)); + ggml_backend_tensor_get(t, data.data(), 0, ggml_nbytes(t)); + for (int i = 0; i < (int)data.size(); i++) { + data[i] = data[i] > 0.5f ? 1.0f : 0.0f; + } + ggml_backend_tensor_set(t, data.data(), 0, ggml_nbytes(t)); + } + } else { + init_tensor_uniform(t, -1.0f, 1.0f); + } + } + } +}; + // GGML_OP_OPT_STEP_ADAMW struct test_opt_step_adamw : public test_case { const ggml_type type; @@ -6581,7 +6732,14 @@ static const ggml_type all_types[] = { GGML_TYPE_Q2_K, GGML_TYPE_Q3_K, GGML_TYPE_Q4_K, GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, - // GGML_TYPE_TQ1_0, GGML_TYPE_TQ2_0, // TODO: implement for all backends + + // TODO: implement for all backends + GGML_TYPE_TQ1_0, + // Temporarily disable TQ2_0 quantization tests to work around a bug in + // llvmpipe. Tests pass successfully on all real Vulkan hardware + // (Nvidia, ARM GPUs) but fail on llvmpipe with high error values. + // GGML_TYPE_TQ2_0, + GGML_TYPE_IQ2_XXS, GGML_TYPE_IQ2_XS, GGML_TYPE_IQ2_S, GGML_TYPE_IQ3_XXS, GGML_TYPE_IQ1_S, GGML_TYPE_IQ1_M, GGML_TYPE_IQ4_NL, GGML_TYPE_IQ3_S, GGML_TYPE_IQ4_XS, @@ -6657,6 +6815,8 @@ static std::vector> make_test_cases_eval() { } } + test_cases.emplace_back(new test_geglu_back()); + for (ggml_type type : {GGML_TYPE_F32, GGML_TYPE_Q4_0}) { test_cases.emplace_back(new test_get_rows(type, 300*256, 5, 4, 1, 2, false)); test_cases.emplace_back(new test_get_rows(type, 256, 80000, 70000, 2, 1, false)); @@ -7230,6 +7390,24 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_mul_mat(GGML_TYPE_F16, GGML_TYPE_F16, 1700000, 1, 2592, {1, 1}, {1, 1})); #endif + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ1_0, GGML_TYPE_F32, 16, 1, 256, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ1_0, GGML_TYPE_F32, 16, 2, 256, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ1_0, GGML_TYPE_F32, 16, 4, 256, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ1_0, GGML_TYPE_F32, 16, 8, 256, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ1_0, GGML_TYPE_F32, 32, 32, 256, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ1_0, GGML_TYPE_F32, 16, 1, 1024, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ1_0, GGML_TYPE_F32, 16, 2, 1024, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ1_0, GGML_TYPE_F32, 16, 4, 1024, {1, 1}, {1, 1})); + + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ2_0, GGML_TYPE_F32, 16, 1, 256, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ2_0, GGML_TYPE_F32, 16, 2, 256, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ2_0, GGML_TYPE_F32, 16, 4, 256, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ2_0, GGML_TYPE_F32, 16, 8, 256, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ2_0, GGML_TYPE_F32, 32, 32, 256, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ2_0, GGML_TYPE_F32, 16, 1, 1024, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ2_0, GGML_TYPE_F32, 16, 2, 1024, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_TQ2_0, GGML_TYPE_F32, 16, 4, 1024, {1, 1}, {1, 1})); + for (ggml_type type_a : all_types) { for (int i = 1; i < 10; ++i) { test_cases.emplace_back(new test_mul_mat(type_a, GGML_TYPE_F32, 16, i, 256, { 1, 1}, {1, 1})); @@ -7810,6 +7988,10 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_cross_entropy_loss (GGML_TYPE_F32, {30000, 1, 1, 1})); test_cases.emplace_back(new test_cross_entropy_loss_back(GGML_TYPE_F32, { 10, 5, 4, 3})); test_cases.emplace_back(new test_cross_entropy_loss_back(GGML_TYPE_F32, {30000, 1, 1, 1})); + test_cases.emplace_back(new test_cross_entropy_loss_masked (GGML_TYPE_F32, { 10, 5, 4, 3})); + test_cases.emplace_back(new test_cross_entropy_loss_masked (GGML_TYPE_F32, {30000, 1, 1, 1})); + test_cases.emplace_back(new test_cross_entropy_loss_masked_back(GGML_TYPE_F32, { 10, 5, 4, 3})); + test_cases.emplace_back(new test_cross_entropy_loss_masked_back(GGML_TYPE_F32, {30000, 1, 1, 1})); test_cases.emplace_back(new test_opt_step_adamw(GGML_TYPE_F32, {10, 5, 4, 3})); test_cases.emplace_back(new test_opt_step_sgd(GGML_TYPE_F32, {10, 5, 4, 3})); diff --git a/tests/test-gguf.cpp b/tests/test-gguf.cpp index 3f0c312e2f00..5936f0b5ddae 100644 --- a/tests/test-gguf.cpp +++ b/tests/test-gguf.cpp @@ -101,6 +101,36 @@ static bool expect_context_not_null(const enum handcrafted_file_type hft) { typedef std::pair> tensor_config_t; +// Helper function to safely cast to gguf_type, suppressing sanitizer warnings for intentional invalid values +// Portable implementation for disabling sanitizer attributes, depending on compiler +#if defined(__clang__) || defined(__GNUC__) +static inline enum gguf_type __attribute__((no_sanitize("undefined"))) +safe_cast_to_gguf_type(int value) { + return static_cast(value); +} + +// Helper to safely assign invalid enum values, suppressing sanitizer warnings at assignment point +static inline enum gguf_type __attribute__((no_sanitize("undefined"))) +safe_assign_gguf_type(enum gguf_type value) { + return value; +} +#elif defined(_MSC_VER) +// MSVC does not support __attribute__; just define without it +static inline enum gguf_type safe_cast_to_gguf_type(int value) { + return static_cast(value); +} +static inline enum gguf_type safe_assign_gguf_type(enum gguf_type value) { + return value; +} +#else +static inline enum gguf_type safe_cast_to_gguf_type(int value) { + return static_cast(value); +} +static inline enum gguf_type safe_assign_gguf_type(enum gguf_type value) { + return value; +} +#endif + static std::vector get_tensor_configs(std::mt19937 & rng) { std::vector tensor_configs; tensor_configs.reserve(100); @@ -124,15 +154,17 @@ static std::vector get_tensor_configs(std::mt19937 & rng) { return tensor_configs; } -static std::vector> get_kv_types(std::mt19937 rng) { - std::vector> kv_types; +// Store as int to avoid UBSAN errors in std::shuffle/std::swap operations +// Cast to enum only when needed +static std::vector> get_kv_types(std::mt19937 rng) { + std::vector> kv_types; kv_types.reserve(100); for (int i = 0; i < 100; ++i) { - const gguf_type type = gguf_type(rng() % GGUF_TYPE_COUNT); + const int type = rng() % GGUF_TYPE_COUNT; if (type == GGUF_TYPE_ARRAY) { - const gguf_type type_arr = gguf_type(rng() % GGUF_TYPE_COUNT); + const int type_arr = rng() % GGUF_TYPE_COUNT; if (type_arr == GGUF_TYPE_ARRAY) { continue; } @@ -140,7 +172,9 @@ static std::vector> get_kv_types(std:: continue; } - kv_types.push_back(std::make_pair(type, gguf_type(-1))); + // Intentionally create invalid enum value (-1) for testing error handling + // Stored as int to avoid UBSAN errors during std::shuffle + kv_types.push_back(std::make_pair(type, -1)); } std::shuffle(kv_types.begin(), kv_types.end(), rng); @@ -156,7 +190,12 @@ static void helper_write(FILE * file, const void * data, const size_t nbytes) { GGML_ASSERT(fwrite(data, 1, nbytes, file) == nbytes); } -static FILE * get_handcrafted_file(const unsigned int seed, const enum handcrafted_file_type hft, const int extra_bytes = 0) { +#if defined(__clang__) || defined(__GNUC__) +static FILE * __attribute__((no_sanitize("undefined"))) +#else +static FILE * +#endif +get_handcrafted_file(const unsigned int seed, const enum handcrafted_file_type hft, const int extra_bytes = 0) { FILE * file = tmpfile(); if (!file) { @@ -200,7 +239,7 @@ static FILE * get_handcrafted_file(const unsigned int seed, const enum handcraft helper_write(file, n_tensors); } - std::vector> kv_types; + std::vector> kv_types; if (hft >= offset_has_kv) { kv_types = get_kv_types(rng); } @@ -232,8 +271,10 @@ static FILE * get_handcrafted_file(const unsigned int seed, const enum handcraft } for (int i = 0; i < int(kv_types.size()); ++i) { - const enum gguf_type type = gguf_type(hft == HANDCRAFTED_KV_BAD_TYPE ? GGUF_TYPE_COUNT : kv_types[i].first); - const enum gguf_type type_arr = gguf_type(hft == HANDCRAFTED_KV_BAD_TYPE ? GGUF_TYPE_COUNT : kv_types[i].second); + // Intentionally create invalid enum values for testing error handling + // Cast from int to enum only when needed, suppressing sanitizer warnings + const enum gguf_type type = safe_assign_gguf_type(safe_cast_to_gguf_type(hft == HANDCRAFTED_KV_BAD_TYPE ? GGUF_TYPE_COUNT : kv_types[i].first)); + const enum gguf_type type_arr = safe_assign_gguf_type(safe_cast_to_gguf_type(hft == HANDCRAFTED_KV_BAD_TYPE ? GGUF_TYPE_COUNT : kv_types[i].second)); const std::string key = "my_key_" + std::to_string((hft == HANDCRAFTED_KV_DUPLICATE_KEY ? i/2 : i)); @@ -426,7 +467,7 @@ static bool handcrafted_check_header(const gguf_context * gguf_ctx, const unsign if (has_tensors) { tensor_configs = get_tensor_configs(rng); } - std::vector> kv_types; + std::vector> kv_types; if (has_kv) { kv_types = get_kv_types(rng); } @@ -446,7 +487,12 @@ static bool handcrafted_check_header(const gguf_context * gguf_ctx, const unsign return ok; } -static bool handcrafted_check_kv(const gguf_context * gguf_ctx, const unsigned int seed, const bool has_tensors, const bool alignment_defined) { +#if defined(__clang__) || defined(__GNUC__) +static bool __attribute__((no_sanitize("undefined"))) +#else +static bool +#endif +handcrafted_check_kv(const gguf_context * gguf_ctx, const unsigned int seed, const bool has_tensors, const bool alignment_defined) { if (!gguf_ctx) { return false; } @@ -458,13 +504,14 @@ static bool handcrafted_check_kv(const gguf_context * gguf_ctx, const unsigned i tensor_configs = get_tensor_configs(rng); } - std::vector> kv_types = get_kv_types(rng); + std::vector> kv_types = get_kv_types(rng); bool ok = true; for (int i = 0; i < int(kv_types.size()); ++i) { - const enum gguf_type type = gguf_type(kv_types[i].first); - const enum gguf_type type_arr = gguf_type(kv_types[i].second); + // Cast from int to enum, suppressing sanitizer warning for intentional invalid enum values in test data + const enum gguf_type type = safe_assign_gguf_type(safe_cast_to_gguf_type(kv_types[i].first)); + const enum gguf_type type_arr = safe_assign_gguf_type(safe_cast_to_gguf_type(kv_types[i].second)); const std::string key = "my_key_" + std::to_string(i); diff --git a/tests/test-model-load-cancel.cpp b/tests/test-model-load-cancel.cpp index 9095826fa988..1db479b90ddb 100644 --- a/tests/test-model-load-cancel.cpp +++ b/tests/test-model-load-cancel.cpp @@ -15,7 +15,7 @@ int main(int argc, char *argv[] ) { fclose(file); llama_backend_init(); - auto params = llama_model_params{}; + auto params = llama_model_default_params(); params.use_mmap = false; params.progress_callback = [](float progress, void * ctx){ (void) ctx; diff --git a/tests/test-model-load-disk.cpp b/tests/test-model-load-disk.cpp new file mode 100644 index 000000000000..feb072ceaeeb --- /dev/null +++ b/tests/test-model-load-disk.cpp @@ -0,0 +1,41 @@ +#include + +#include "get-model.h" +#include "llama.h" + +int main(int argc, char * argv[]) { + auto * model_path = get_model_or_exit(argc, argv); + auto * file = fopen(model_path, "r"); + if (file == nullptr) { + fprintf(stderr, "no model at '%s' found\n", model_path); + return EXIT_FAILURE; + } + + fprintf(stderr, "using '%s'\n", model_path); + fclose(file); + + llama_backend_init(); + auto params = llama_model_default_params(); + params.use_mmap = false; + params.progress_callback = [](float progress, void * ctx) { + (void) ctx; + fprintf(stderr, "%.2f%% ", progress * 100.0f); + // true means: Don't cancel the load + return true; + }; + auto * model = llama_model_load_from_file(model_path, params); + + // Add newline after progress output + fprintf(stderr, "\n"); + + if (model == nullptr) { + fprintf(stderr, "Failed to load model\n"); + llama_backend_free(); + return EXIT_FAILURE; + } + + fprintf(stderr, "Model loaded successfully\n"); + llama_model_free(model); + llama_backend_free(); + return EXIT_SUCCESS; +} diff --git a/tests/test-model-load-memory-split.cpp b/tests/test-model-load-memory-split.cpp new file mode 100644 index 000000000000..5b87bcc9c5db --- /dev/null +++ b/tests/test-model-load-memory-split.cpp @@ -0,0 +1,74 @@ +#include +#include +#include + +#include "get-model.h" +#include "llama-cpp.h" +#include "load_into_memory.h" + +int main(int argc, char * argv[]) { + auto * model_path = get_model_or_exit(argc, argv); + + if (!is_split_file(model_path)) { + printf("Skipping not-split model %s\n", model_path); + return EXIT_SUCCESS; + } + + // Manually load into a memory buffer first + file_entry tensor_list_file = load_tensor_list_file(model_path); + std::vector files = load_files_into_streambuf(model_path); + + llama_backend_init(); + auto params = llama_model_params{}; + params.use_mmap = false; + params.progress_callback = [](float progress, void * ctx) { + (void) ctx; + fprintf(stderr, "%.2f%% ", progress * 100.0f); + // true means: Don't cancel the load + return true; + }; + + printf("Loading model from %zu files\n", files.size()); + + std::vector file_paths; + for (size_t i = 0; i < files.size(); i++) { + printf("Found file %s \n", files[i].path.c_str()); + file_paths.push_back(files[i].path.c_str()); + } + + const char * async_load_context = "test-model-load"; + std::thread fulfill_thread([&files, &tensor_list_file, &async_load_context]() { + const bool success = llama_model_load_fulfill_split_future(tensor_list_file.path.c_str(), async_load_context, + std::move(tensor_list_file.streambuf)); + printf("Fulfilling tensor list file %s: %s\n", tensor_list_file.path.c_str(), success ? "success" : "failure"); + if (!success) { + exit(EXIT_FAILURE); + } + for (size_t i = 0; i < files.size(); i++) { + const bool success = llama_model_load_fulfill_split_future(files[i].path.c_str(), async_load_context, + std::move(files[i].streambuf)); + printf("Fulfilling file %s: %s\n", files[i].path.c_str(), success ? "success" : "failure"); + if (!success) { + exit(EXIT_FAILURE); + } + } + }); + fprintf(stderr, "Loading model from splits\n"); + auto * model = llama_model_load_from_split_futures(file_paths.data(), file_paths.size(), async_load_context, + tensor_list_file.path.c_str(), params); + fulfill_thread.join(); + + fprintf(stderr, "\n"); + + if (model == nullptr) { + fprintf(stderr, "Failed to load model\n"); + llama_backend_free(); + return EXIT_FAILURE; + } + + fprintf(stderr, "Model loaded successfully\n"); + llama_model_free(model); + llama_backend_free(); + + return EXIT_SUCCESS; +} diff --git a/tests/test-model-load-memory.cpp b/tests/test-model-load-memory.cpp new file mode 100644 index 000000000000..78e91505ae44 --- /dev/null +++ b/tests/test-model-load-memory.cpp @@ -0,0 +1,47 @@ +#include +#include +#include + +#include "get-model.h" +#include "llama-cpp.h" +#include "load_into_memory.h" + +int main(int argc, char * argv[]) { + auto * model_path = get_model_or_exit(argc, argv); + + if (is_split_file(model_path)) { + printf("Skipping split model %s\n", model_path); + return EXIT_SUCCESS; + } + + // Manually load into a memory buffer first + std::vector buffer = load_file_into_buffer(model_path); + + llama_backend_init(); + auto params = llama_model_default_params(); + params.use_mmap = false; + params.progress_callback = [](float progress, void * ctx) { + (void) ctx; + fprintf(stderr, "%.2f%% ", progress * 100.0f); + // true means: Don't cancel the load + return true; + }; + + // Test that it can load directly from a buffer + printf("Loading model from buffer of size %zu bytes\n", buffer.size()); + auto * model = llama_model_load_from_buffer(std::move(buffer), params); + + // Add newline after progress output + fprintf(stderr, "\n"); + + if (model == nullptr) { + fprintf(stderr, "Failed to load model\n"); + llama_backend_free(); + return EXIT_FAILURE; + } + + fprintf(stderr, "Model loaded successfully\n"); + llama_model_free(model); + llama_backend_free(); + return EXIT_SUCCESS; +} diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index d64956b84385..cdc1b0e63705 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -21,13 +21,15 @@ else() add_subdirectory(main) add_subdirectory(perplexity) add_subdirectory(quantize) - if (LLAMA_BUILD_SERVER) + if (LLAMA_BUILD_SERVER AND LLAMA_MTMD) add_subdirectory(server) endif() add_subdirectory(run) add_subdirectory(tokenize) add_subdirectory(tts) - add_subdirectory(mtmd) + if (LLAMA_MTMD) + add_subdirectory(mtmd) + endif() if (GGML_RPC) add_subdirectory(rpc) endif() diff --git a/tools/gguf-split/gguf-split.cpp b/tools/gguf-split/gguf-split.cpp index 30e771564e80..999de404277c 100644 --- a/tools/gguf-split/gguf-split.cpp +++ b/tools/gguf-split/gguf-split.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #if defined(_WIN32) #include @@ -43,6 +44,8 @@ struct split_params { std::string output; bool no_tensor_first_split = false; bool dry_run = false; + bool verbose = false; + std::set must_be_followed_layers; }; static void split_print_usage(const char * executable) { @@ -50,7 +53,8 @@ static void split_print_usage(const char * executable) { printf("\n"); printf("usage: %s [options] GGUF_IN GGUF_OUT\n", executable); printf("\n"); - printf("Apply a GGUF operation on IN to OUT."); + printf("Apply a GGUF operation on IN to OUT.\n"); + printf("When splitting, also creates GGUF_OUT.tensors.txt with all tensor names.\n"); printf("\n"); printf("options:\n"); printf(" -h, --help show this help message and exit\n"); @@ -60,7 +64,9 @@ static void split_print_usage(const char * executable) { printf(" --split-max-tensors max tensors in each split (default: %d)\n", default_params.n_split_tensors); printf(" --split-max-size N(M|G) max size per split\n"); printf(" --no-tensor-first-split do not add tensors to the first split (disabled by default)\n"); + printf(" --must-be-followed LAYER ensure LAYER is not the last tensor in a split and will not be released when loading after any tensor is created (can be used multiple times)\n"); printf(" --dry-run only print out a split plan and exit, without writing any new files\n"); + printf(" --verbose show tensor names for each split\n"); printf("\n"); } @@ -106,6 +112,9 @@ static void split_params_parse_ex(int argc, const char ** argv, split_params & p } else if (arg == "--dry-run") { arg_found = true; params.dry_run = true; + } else if (arg == "--verbose") { + arg_found = true; + params.verbose = true; } else if (arg == "--no-tensor-first-split") { arg_found = true; params.no_tensor_first_split = true; @@ -143,6 +152,13 @@ static void split_params_parse_ex(int argc, const char ** argv, split_params & p } params.mode = MODE_SIZE; params.n_bytes_split = split_str_to_n_bytes(argv[arg_idx]); + } else if (arg == "--must-be-followed") { + if (++arg_idx >= argc) { + invalid_param = true; + break; + } + arg_found = true; + params.must_be_followed_layers.insert(argv[arg_idx]); } if (!arg_found) { @@ -275,7 +291,19 @@ struct split_strategy { } } + bool must_be_followed(int i_tensor) { + if (i_tensor > 0 && i_tensor < n_tensors) { + const char* tensor_name = gguf_get_tensor_name(ctx_gguf, i_tensor); + return params.must_be_followed_layers.find(tensor_name) != params.must_be_followed_layers.end(); + } + return false; + } + bool should_split(int i_tensor, size_t next_size) { + if (must_be_followed(i_tensor) || must_be_followed(i_tensor - 1)) { + return false; + } + if (params.mode == MODE_SIZE) { // split by max size per file return next_size > params.n_bytes_split; @@ -299,10 +327,41 @@ struct split_strategy { } total_size = total_size / 1000 / 1000; // convert to megabytes printf("split %05d: n_tensors = %" PRIi64 ", total_size = %zuM\n", i_split + 1, gguf_get_n_tensors(ctx_out), total_size); + + if (params.verbose) { + for (int i = 0; i < gguf_get_n_tensors(ctx_out); ++i) { + const char * t_name = gguf_get_tensor_name(ctx_out, i); + printf(" - %s\n", t_name); + } + } i_split++; } } + void write_tensor_list() { + // Create a .txt file with all tensor names from all splits + std::string tensor_list_path = params.output + ".tensors.txt"; + std::ofstream tensor_file(tensor_list_path); + if (!tensor_file.is_open()) { + fprintf(stderr, "warning: failed to create tensor list file %s\n", tensor_list_path.c_str()); + return; + } + + printf("Writing tensor list to %s ... ", tensor_list_path.c_str()); + fflush(stdout); + + // Write all tensor names from all splits + for (auto & ctx_out : ctx_outs) { + for (int i = 0; i < gguf_get_n_tensors(ctx_out); ++i) { + const char * t_name = gguf_get_tensor_name(ctx_out, i); + tensor_file << t_name << "\n"; + } + } + + tensor_file.close(); + printf("done\n"); + } + void write() { int i_split = 0; int n_split = ctx_outs.size(); @@ -382,6 +441,9 @@ static void gguf_split(const split_params & split_params) { int n_split = strategy.ctx_outs.size(); strategy.print_info(); + // Write tensor list file + strategy.write_tensor_list(); + if (!split_params.dry_run) { // write all output splits strategy.write(); diff --git a/tools/mtmd/CMakeLists.txt b/tools/mtmd/CMakeLists.txt index f640ae2a6ea5..40497c09755f 100644 --- a/tools/mtmd/CMakeLists.txt +++ b/tools/mtmd/CMakeLists.txt @@ -20,7 +20,12 @@ set_target_properties(mtmd PROPERTIES target_link_libraries (mtmd PUBLIC ggml llama) target_link_libraries (mtmd PRIVATE Threads::Threads) -target_include_directories(mtmd PUBLIC .) +target_include_directories( + mtmd + PUBLIC + $ + $ +) target_include_directories(mtmd PRIVATE ../..) target_include_directories(mtmd PRIVATE ../../vendor) target_compile_features (mtmd PRIVATE cxx_std_17) @@ -40,8 +45,6 @@ set_target_properties(mtmd PROPERTIES PUBLIC_HEADER "${MTMD_PUBLIC_HEADERS}") -install(TARGETS mtmd LIBRARY PUBLIC_HEADER) - if (NOT MSVC) # for stb_image.h and miniaudio.h target_compile_options(mtmd PRIVATE -Wno-cast-qual) @@ -52,6 +55,7 @@ if (TARGET BUILD_INFO) add_dependencies(mtmd-helper BUILD_INFO) endif() +if (LLAMA_BUILD_TOOLS) add_executable(llama-llava-cli deprecation-warning.cpp) add_executable(llama-gemma3-cli deprecation-warning.cpp) add_executable(llama-minicpmv-cli deprecation-warning.cpp) @@ -65,3 +69,4 @@ if(LLAMA_TOOLS_INSTALL) endif() target_link_libraries (${TARGET} PRIVATE common mtmd Threads::Threads) target_compile_features(${TARGET} PRIVATE cxx_std_17) +endif() diff --git a/tools/mtmd/README.md b/tools/mtmd/README.md index ef31d1957cda..2c5d844881a7 100644 --- a/tools/mtmd/README.md +++ b/tools/mtmd/README.md @@ -37,6 +37,37 @@ Built upon `clip.cpp` (similar to `llava.cpp`), `libmtmd` offers several advanta - **Improved UX/DX:** Features a more intuitive API, inspired by the `Processor` class in the Hugging Face `transformers` library. - **Flexibility:** Designed to support multiple input types (text, audio, images) while respecting the wide variety of chat templates used by different models. +## Logging Configuration + +By default, `libmtmd` logs messages directly to stderr. To integrate `libmtmd` logging with your application's logging system, you can use the `mtmd_log_set_llama_callback()` function to redirect all mtmd/clip logs through llama's logging callback. + +**Example usage:** + +```c +#include "llama.h" +#include "mtmd.h" + +// Your custom logging callback +void my_log_callback(ggml_log_level level, const char * text, void * user_data) { + // Your logging logic here + printf("[%d] %s", level, text); +} + +int main() { + // Set up llama's logging + llama_log_set(my_log_callback, NULL); + + // Redirect mtmd/clip logging to use the same callback + mtmd_log_set_llama_callback(my_log_callback, NULL); + + // Now all mtmd and clip logs will use your custom callback + mtmd_context * ctx = mtmd_init_from_file(...); + // ... +} +``` + +This ensures that all logging from `libmtmd`, including the underlying `clip.cpp` vision encoder, is routed through your application's logging system consistently. + ## How to obtain `mmproj` Multimodal projector (`mmproj`) files are specific to each model architecture. diff --git a/tools/mtmd/clip-impl.h b/tools/mtmd/clip-impl.h index cd47865bf4a7..0a7517b655c8 100644 --- a/tools/mtmd/clip-impl.h +++ b/tools/mtmd/clip-impl.h @@ -224,12 +224,20 @@ static void clip_log_callback_default(enum ggml_log_level level, const char * te } struct clip_logger_state { + enum ggml_log_level verbosity_thold; ggml_log_callback log_callback; void * log_callback_user_data; }; extern struct clip_logger_state g_logger_state; +// Function to set logging callback (can be used to redirect to llama's logging) +// If not called, will use the default callback (logs to stderr) +static inline void clip_log_set_callback(ggml_log_callback callback, void * user_data) { + g_logger_state.log_callback = callback; + g_logger_state.log_callback_user_data = user_data; +} + static void clip_log_internal_v(enum ggml_log_level level, const char * format, va_list args) { if (format == NULL) { return; diff --git a/tools/mtmd/clip.cpp b/tools/mtmd/clip.cpp index 3ed08a0fec65..54d56cf5a506 100644 --- a/tools/mtmd/clip.cpp +++ b/tools/mtmd/clip.cpp @@ -24,7 +24,12 @@ #include #include -struct clip_logger_state g_logger_state = {clip_log_callback_default, NULL}; +// TODO: allow to pass callback from user code +struct clip_logger_state g_logger_state = { + GGML_LOG_LEVEL_CONT, // verbosity_thold + clip_log_callback_default, // log_callback + NULL // log_callback_user_data +}; enum ffn_op_type { FFN_GELU, @@ -442,7 +447,7 @@ struct clip_ctx { throw std::runtime_error("failed to initialize CPU backend"); } if (ctx_params.use_gpu) { - auto backend_name = std::getenv("MTMD_BACKEND_DEVICE"); + auto backend_name = ctx_params.backend_device ? ctx_params.backend_device : std::getenv("MTMD_BACKEND_DEVICE"); if (backend_name != nullptr) { backend = ggml_backend_init_by_name(backend_name, nullptr); if (!backend) { diff --git a/tools/mtmd/clip.h b/tools/mtmd/clip.h index e8aeb2066c6d..80f3a9477390 100644 --- a/tools/mtmd/clip.h +++ b/tools/mtmd/clip.h @@ -35,6 +35,7 @@ struct clip_context_params { int image_min_tokens; int image_max_tokens; bool warmup; + const char * backend_device; // optional, if null will use env var or default GPU backend }; struct clip_init_result { diff --git a/tools/mtmd/mtmd.cpp b/tools/mtmd/mtmd.cpp index d06fa42e616b..80eba85ab8d6 100644 --- a/tools/mtmd/mtmd.cpp +++ b/tools/mtmd/mtmd.cpp @@ -111,6 +111,7 @@ mtmd_context_params mtmd_context_params_default() { /* warmup */ true, /* image_min_tokens */ -1, /* image_max_tokens */ -1, + /* backend_device */ nullptr, }; return params; } @@ -179,6 +180,7 @@ struct mtmd_context { /* image_min_tokens */ ctx_params.image_min_tokens, /* image_max_tokens */ ctx_params.image_max_tokens, /* warmup */ ctx_params.warmup, + /* backend_device */ ctx_params.backend_device, }; auto res = clip_init(mmproj_fname, ctx_clip_params); @@ -402,6 +404,10 @@ void mtmd_free(mtmd_context * ctx) { delete ctx; } +void mtmd_log_set_llama_callback(ggml_log_callback llama_cb, void * llama_user_data) { + clip_log_set_callback(llama_cb, llama_user_data); +} + struct mtmd_tokenizer { mtmd_context * ctx; std::vector bitmaps; diff --git a/tools/mtmd/mtmd.h b/tools/mtmd/mtmd.h index b3df24c299d4..ff42eb481dca 100644 --- a/tools/mtmd/mtmd.h +++ b/tools/mtmd/mtmd.h @@ -87,6 +87,8 @@ struct mtmd_context_params { // limit number of image tokens, only for vision models with dynamic resolution int image_min_tokens; // minimum number of tokens for image input (default: read from metadata) int image_max_tokens; // maximum number of tokens for image input (default: read from metadata) + + const char * backend_device; // optional GPU backend name (e.g. "CUDA", "Metal", "Vulkan"), if null will use env var or default }; MTMD_API const char * mtmd_default_marker(void); @@ -101,6 +103,14 @@ MTMD_API mtmd_context * mtmd_init_from_file(const char * mmproj_fname, MTMD_API void mtmd_free(mtmd_context * ctx); +// Set up logging to use llama's logging callback +// This redirects all mtmd/clip logging through llama's logging system +// Call this after llama_log_set to ensure mtmd uses the same logging callback +// Example: +// llama_log_set(my_log_callback, my_user_data); +// mtmd_log_set_llama_callback(my_log_callback, my_user_data); +MTMD_API void mtmd_log_set_llama_callback(ggml_log_callback llama_cb, void * llama_user_data); + // whether we need to set non-causal mask before llama_decode MTMD_API bool mtmd_decode_use_non_causal(mtmd_context * ctx); diff --git a/tools/server/tests/unit/test_ctx_shift.py b/tools/server/tests/unit/test_ctx_shift.py index 7b047b7b3b74..ce800f7b8467 100644 --- a/tools/server/tests/unit/test_ctx_shift.py +++ b/tools/server/tests/unit/test_ctx_shift.py @@ -43,6 +43,7 @@ def test_ctx_shift_enabled(): assert res.body["truncated"] is True +@pytest.mark.skip(reason="Test disabled - n_predict=-1 case has inconsistent/flaky behavior") @pytest.mark.parametrize("n_predict,n_token_output,truncated", [ (64, 64, False), (-1, 248, True), # 8 tokens prompt + 248 tokens generated = 256 tokens total diff --git a/vendor/vma/VmaUsage.h b/vendor/vma/VmaUsage.h new file mode 100644 index 000000000000..aa630600d759 --- /dev/null +++ b/vendor/vma/VmaUsage.h @@ -0,0 +1,133 @@ +// +// Copyright (c) 2017-2025 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#ifndef VMA_USAGE_H_ +#define VMA_USAGE_H_ + +#ifdef _WIN32 + +#if !defined(NOMINMAX) + #define NOMINMAX +#endif + +#if !defined(WIN32_LEAN_AND_MEAN) + #define WIN32_LEAN_AND_MEAN +#endif + +#include +#if !defined(VK_USE_PLATFORM_WIN32_KHR) + #define VK_USE_PLATFORM_WIN32_KHR +#endif // #if !defined(VK_USE_PLATFORM_WIN32_KHR) + +#else // #ifdef _WIN32 + +#include + +#endif // #ifdef _WIN32 + +#ifdef _MSVC_LANG + +// Uncomment to test including `vulkan.h` on your own before including VMA. +//#include + +/* +In every place where you want to use Vulkan Memory Allocator, define appropriate +macros if you want to configure the library and then include its header to +include all public interface declarations. Example: +*/ + +//#define VMA_HEAVY_ASSERT(expr) assert(expr) +//#define VMA_DEDICATED_ALLOCATION 0 +//#define VMA_DEBUG_MARGIN 16 +//#define VMA_DEBUG_DETECT_CORRUPTION 1 +//#define VMA_DEBUG_MIN_BUFFER_IMAGE_GRANULARITY 256 +//#define VMA_USE_STL_SHARED_MUTEX 0 +//#define VMA_MEMORY_BUDGET 0 +//#define VMA_STATS_STRING_ENABLED 0 +//#define VMA_MAPPING_HYSTERESIS_ENABLED 0 +//#define VMA_KHR_MAINTENANCE5 0 + +//#define VMA_VULKAN_VERSION 1003000 // Vulkan 1.3 +//#define VMA_VULKAN_VERSION 1002000 // Vulkan 1.2 +//#define VMA_VULKAN_VERSION 1001000 // Vulkan 1.1 +//#define VMA_VULKAN_VERSION 1000000 // Vulkan 1.0 + +/* +#define VMA_DEBUG_LOG(format, ...) do { \ + printf(format, __VA_ARGS__); \ + printf("\n"); \ + } while(false) +*/ + +#pragma warning(push, 4) +#pragma warning(disable: 4127) // conditional expression is constant +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4189) // local variable is initialized but not referenced +#pragma warning(disable: 4324) // structure was padded due to alignment specifier +#pragma warning(disable: 4820) // 'X': 'N' bytes padding added after data member 'X' + +#endif // #ifdef _MSVC_LANG + +#ifdef __clang__ + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wtautological-compare" // comparison of unsigned expression < 0 is always false + #pragma clang diagnostic ignored "-Wunused-private-field" + #pragma clang diagnostic ignored "-Wunused-parameter" + #pragma clang diagnostic ignored "-Wmissing-field-initializers" + #pragma clang diagnostic ignored "-Wnullability-completeness" + #pragma clang diagnostic ignored "-Wnullability-extension" +#endif + +#ifdef __GNUC__ + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wextra-semi" + #pragma GCC diagnostic ignored "-Wmissing-field-initializers" + #pragma GCC diagnostic ignored "-Wunused-parameter" + #pragma GCC diagnostic ignored "-Wunused-variable" + #pragma GCC diagnostic ignored "-Wsuggest-attribute=noreturn" +#endif + +#ifdef VMA_VOLK_HEADER_PATH + #include VMA_VOLK_HEADER_PATH +#else + #include +#endif + +#ifdef _WIN32 + #include +#endif // #ifdef _WIN32 + +#include "vk_mem_alloc.h" + +#ifdef __GNUC__ + #pragma GCC diagnostic pop +#endif + +#ifdef __clang__ + #pragma clang diagnostic pop +#endif + +#ifdef _MSVC_LANG + #pragma warning(pop) +#endif + +#endif diff --git a/vendor/vma/vk_mem_alloc.h b/vendor/vma/vk_mem_alloc.h new file mode 100644 index 000000000000..8df03649b5b9 --- /dev/null +++ b/vendor/vma/vk_mem_alloc.h @@ -0,0 +1,19530 @@ +// +// Copyright (c) 2017-2025 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#ifndef AMD_VULKAN_MEMORY_ALLOCATOR_H +#define AMD_VULKAN_MEMORY_ALLOCATOR_H + +/** \mainpage Vulkan Memory Allocator + +Version 3.3.0 + +Copyright (c) 2017-2025 Advanced Micro Devices, Inc. All rights reserved. \n +License: MIT \n +See also: [product page on GPUOpen](https://gpuopen.com/gaming-product/vulkan-memory-allocator/), +[repository on GitHub](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) + + +API documentation divided into groups: [Topics](topics.html) + +General documentation chapters: + +- \subpage faq +- \subpage quick_start + - [Project setup](@ref quick_start_project_setup) + - [Initialization](@ref quick_start_initialization) + - [Resource allocation](@ref quick_start_resource_allocation) +- \subpage choosing_memory_type + - [Usage](@ref choosing_memory_type_usage) + - [Required and preferred flags](@ref choosing_memory_type_required_preferred_flags) + - [Explicit memory types](@ref choosing_memory_type_explicit_memory_types) + - [Custom memory pools](@ref choosing_memory_type_custom_memory_pools) + - [Dedicated allocations](@ref choosing_memory_type_dedicated_allocations) +- \subpage memory_mapping + - [Copy functions](@ref memory_mapping_copy_functions) + - [Mapping functions](@ref memory_mapping_mapping_functions) + - [Persistently mapped memory](@ref memory_mapping_persistently_mapped_memory) + - [Cache flush and invalidate](@ref memory_mapping_cache_control) +- \subpage staying_within_budget + - [Querying for budget](@ref staying_within_budget_querying_for_budget) + - [Controlling memory usage](@ref staying_within_budget_controlling_memory_usage) +- \subpage resource_aliasing +- \subpage custom_memory_pools + - [Choosing memory type index](@ref custom_memory_pools_MemTypeIndex) + - [When not to use custom pools](@ref custom_memory_pools_when_not_use) + - [Linear allocation algorithm](@ref linear_algorithm) + - [Free-at-once](@ref linear_algorithm_free_at_once) + - [Stack](@ref linear_algorithm_stack) + - [Double stack](@ref linear_algorithm_double_stack) + - [Ring buffer](@ref linear_algorithm_ring_buffer) +- \subpage defragmentation +- \subpage statistics + - [Numeric statistics](@ref statistics_numeric_statistics) + - [JSON dump](@ref statistics_json_dump) +- \subpage allocation_annotation + - [Allocation user data](@ref allocation_user_data) + - [Allocation names](@ref allocation_names) +- \subpage virtual_allocator +- \subpage debugging_memory_usage + - [Memory initialization](@ref debugging_memory_usage_initialization) + - [Margins](@ref debugging_memory_usage_margins) + - [Corruption detection](@ref debugging_memory_usage_corruption_detection) + - [Leak detection features](@ref debugging_memory_usage_leak_detection) +- \subpage other_api_interop +- \subpage usage_patterns + - [GPU-only resource](@ref usage_patterns_gpu_only) + - [Staging copy for upload](@ref usage_patterns_staging_copy_upload) + - [Readback](@ref usage_patterns_readback) + - [Advanced data uploading](@ref usage_patterns_advanced_data_uploading) + - [Other use cases](@ref usage_patterns_other_use_cases) +- \subpage configuration + - [Pointers to Vulkan functions](@ref config_Vulkan_functions) + - [Custom host memory allocator](@ref custom_memory_allocator) + - [Device memory allocation callbacks](@ref allocation_callbacks) + - [Device heap memory limit](@ref heap_memory_limit) +- Extension support + - \subpage vk_khr_dedicated_allocation + - \subpage enabling_buffer_device_address + - \subpage vk_ext_memory_priority + - \subpage vk_amd_device_coherent_memory + - \subpage vk_khr_external_memory_win32 +- \subpage general_considerations + - [Thread safety](@ref general_considerations_thread_safety) + - [Versioning and compatibility](@ref general_considerations_versioning_and_compatibility) + - [Validation layer warnings](@ref general_considerations_validation_layer_warnings) + - [Allocation algorithm](@ref general_considerations_allocation_algorithm) + - [Features not supported](@ref general_considerations_features_not_supported) + +\defgroup group_init Library initialization + +\brief API elements related to the initialization and management of the entire library, especially #VmaAllocator object. + +\defgroup group_alloc Memory allocation + +\brief API elements related to the allocation, deallocation, and management of Vulkan memory, buffers, images. +Most basic ones being: vmaCreateBuffer(), vmaCreateImage(). + +\defgroup group_virtual Virtual allocator + +\brief API elements related to the mechanism of \ref virtual_allocator - using the core allocation algorithm +for user-defined purpose without allocating any real GPU memory. + +\defgroup group_stats Statistics + +\brief API elements that query current status of the allocator, from memory usage, budget, to full dump of the internal state in JSON format. +See documentation chapter: \ref statistics. +*/ + + +#ifdef __cplusplus +extern "C" { +#endif + +#if !defined(VULKAN_H_) +#include +#endif + +#if !defined(VMA_VULKAN_VERSION) + #if defined(VK_VERSION_1_4) + #define VMA_VULKAN_VERSION 1004000 + #elif defined(VK_VERSION_1_3) + #define VMA_VULKAN_VERSION 1003000 + #elif defined(VK_VERSION_1_2) + #define VMA_VULKAN_VERSION 1002000 + #elif defined(VK_VERSION_1_1) + #define VMA_VULKAN_VERSION 1001000 + #else + #define VMA_VULKAN_VERSION 1000000 + #endif +#endif + +#if defined(__ANDROID__) && defined(VK_NO_PROTOTYPES) && VMA_STATIC_VULKAN_FUNCTIONS + extern PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr; + extern PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr; + extern PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties; + extern PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties; + extern PFN_vkAllocateMemory vkAllocateMemory; + extern PFN_vkFreeMemory vkFreeMemory; + extern PFN_vkMapMemory vkMapMemory; + extern PFN_vkUnmapMemory vkUnmapMemory; + extern PFN_vkFlushMappedMemoryRanges vkFlushMappedMemoryRanges; + extern PFN_vkInvalidateMappedMemoryRanges vkInvalidateMappedMemoryRanges; + extern PFN_vkBindBufferMemory vkBindBufferMemory; + extern PFN_vkBindImageMemory vkBindImageMemory; + extern PFN_vkGetBufferMemoryRequirements vkGetBufferMemoryRequirements; + extern PFN_vkGetImageMemoryRequirements vkGetImageMemoryRequirements; + extern PFN_vkCreateBuffer vkCreateBuffer; + extern PFN_vkDestroyBuffer vkDestroyBuffer; + extern PFN_vkCreateImage vkCreateImage; + extern PFN_vkDestroyImage vkDestroyImage; + extern PFN_vkCmdCopyBuffer vkCmdCopyBuffer; + #if VMA_VULKAN_VERSION >= 1001000 + extern PFN_vkGetBufferMemoryRequirements2 vkGetBufferMemoryRequirements2; + extern PFN_vkGetImageMemoryRequirements2 vkGetImageMemoryRequirements2; + extern PFN_vkBindBufferMemory2 vkBindBufferMemory2; + extern PFN_vkBindImageMemory2 vkBindImageMemory2; + extern PFN_vkGetPhysicalDeviceMemoryProperties2 vkGetPhysicalDeviceMemoryProperties2; + #endif // #if VMA_VULKAN_VERSION >= 1001000 +#endif // #if defined(__ANDROID__) && VMA_STATIC_VULKAN_FUNCTIONS && VK_NO_PROTOTYPES + +#if !defined(VMA_DEDICATED_ALLOCATION) + #if VK_KHR_get_memory_requirements2 && VK_KHR_dedicated_allocation + #define VMA_DEDICATED_ALLOCATION 1 + #else + #define VMA_DEDICATED_ALLOCATION 0 + #endif +#endif + +#if !defined(VMA_BIND_MEMORY2) + #if VK_KHR_bind_memory2 + #define VMA_BIND_MEMORY2 1 + #else + #define VMA_BIND_MEMORY2 0 + #endif +#endif + +#if !defined(VMA_MEMORY_BUDGET) + #if VK_EXT_memory_budget && (VK_KHR_get_physical_device_properties2 || VMA_VULKAN_VERSION >= 1001000) + #define VMA_MEMORY_BUDGET 1 + #else + #define VMA_MEMORY_BUDGET 0 + #endif +#endif + +// Defined to 1 when VK_KHR_buffer_device_address device extension or equivalent core Vulkan 1.2 feature is defined in its headers. +#if !defined(VMA_BUFFER_DEVICE_ADDRESS) + #if VK_KHR_buffer_device_address || VMA_VULKAN_VERSION >= 1002000 + #define VMA_BUFFER_DEVICE_ADDRESS 1 + #else + #define VMA_BUFFER_DEVICE_ADDRESS 0 + #endif +#endif + +// Defined to 1 when VK_EXT_memory_priority device extension is defined in Vulkan headers. +#if !defined(VMA_MEMORY_PRIORITY) + #if VK_EXT_memory_priority + #define VMA_MEMORY_PRIORITY 1 + #else + #define VMA_MEMORY_PRIORITY 0 + #endif +#endif + +// Defined to 1 when VK_KHR_maintenance4 device extension is defined in Vulkan headers. +#if !defined(VMA_KHR_MAINTENANCE4) + #if VK_KHR_maintenance4 + #define VMA_KHR_MAINTENANCE4 1 + #else + #define VMA_KHR_MAINTENANCE4 0 + #endif +#endif + +// Defined to 1 when VK_KHR_maintenance5 device extension is defined in Vulkan headers. +#if !defined(VMA_KHR_MAINTENANCE5) + #if VK_KHR_maintenance5 + #define VMA_KHR_MAINTENANCE5 1 + #else + #define VMA_KHR_MAINTENANCE5 0 + #endif +#endif + + +// Defined to 1 when VK_KHR_external_memory device extension is defined in Vulkan headers. +#if !defined(VMA_EXTERNAL_MEMORY) + #if VK_KHR_external_memory + #define VMA_EXTERNAL_MEMORY 1 + #else + #define VMA_EXTERNAL_MEMORY 0 + #endif +#endif + +// Defined to 1 when VK_KHR_external_memory_win32 device extension is defined in Vulkan headers. +#if !defined(VMA_EXTERNAL_MEMORY_WIN32) + #if VK_KHR_external_memory_win32 + #define VMA_EXTERNAL_MEMORY_WIN32 1 + #else + #define VMA_EXTERNAL_MEMORY_WIN32 0 + #endif +#endif + +// Define these macros to decorate all public functions with additional code, +// before and after returned type, appropriately. This may be useful for +// exporting the functions when compiling VMA as a separate library. Example: +// #define VMA_CALL_PRE __declspec(dllexport) +// #define VMA_CALL_POST __cdecl +#ifndef VMA_CALL_PRE + #define VMA_CALL_PRE +#endif +#ifndef VMA_CALL_POST + #define VMA_CALL_POST +#endif + +// Define this macro to decorate pNext pointers with an attribute specifying the Vulkan +// structure that will be extended via the pNext chain. +#ifndef VMA_EXTENDS_VK_STRUCT + #define VMA_EXTENDS_VK_STRUCT(vkStruct) +#endif + +// Define this macro to decorate pointers with an attribute specifying the +// length of the array they point to if they are not null. +// +// The length may be one of +// - The name of another parameter in the argument list where the pointer is declared +// - The name of another member in the struct where the pointer is declared +// - The name of a member of a struct type, meaning the value of that member in +// the context of the call. For example +// VMA_LEN_IF_NOT_NULL("VkPhysicalDeviceMemoryProperties::memoryHeapCount"), +// this means the number of memory heaps available in the device associated +// with the VmaAllocator being dealt with. +#ifndef VMA_LEN_IF_NOT_NULL + #define VMA_LEN_IF_NOT_NULL(len) +#endif + +// The VMA_NULLABLE macro is defined to be _Nullable when compiling with Clang. +// see: https://clang.llvm.org/docs/AttributeReference.html#nullable +#ifndef VMA_NULLABLE + #ifdef __clang__ + #define VMA_NULLABLE _Nullable + #else + #define VMA_NULLABLE + #endif +#endif + +// The VMA_NOT_NULL macro is defined to be _Nonnull when compiling with Clang. +// see: https://clang.llvm.org/docs/AttributeReference.html#nonnull +#ifndef VMA_NOT_NULL + #ifdef __clang__ + #define VMA_NOT_NULL _Nonnull + #else + #define VMA_NOT_NULL + #endif +#endif + +// If non-dispatchable handles are represented as pointers then we can give +// then nullability annotations +#ifndef VMA_NOT_NULL_NON_DISPATCHABLE + #if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__) ) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(__powerpc64__) + #define VMA_NOT_NULL_NON_DISPATCHABLE VMA_NOT_NULL + #else + #define VMA_NOT_NULL_NON_DISPATCHABLE + #endif +#endif + +#ifndef VMA_NULLABLE_NON_DISPATCHABLE + #if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__) ) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(__powerpc64__) + #define VMA_NULLABLE_NON_DISPATCHABLE VMA_NULLABLE + #else + #define VMA_NULLABLE_NON_DISPATCHABLE + #endif +#endif + +#ifndef VMA_STATS_STRING_ENABLED + #define VMA_STATS_STRING_ENABLED 1 +#endif + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// +// INTERFACE +// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// Sections for managing code placement in file, only for development purposes e.g. for convenient folding inside an IDE. +#ifndef _VMA_ENUM_DECLARATIONS + +/** +\addtogroup group_init +@{ +*/ + +/// Flags for created #VmaAllocator. +typedef enum VmaAllocatorCreateFlagBits +{ + /** \brief Allocator and all objects created from it will not be synchronized internally, so you must guarantee they are used from only one thread at a time or synchronized externally by you. + + Using this flag may increase performance because internal mutexes are not used. + */ + VMA_ALLOCATOR_CREATE_EXTERNALLY_SYNCHRONIZED_BIT = 0x00000001, + /** \brief Enables usage of VK_KHR_dedicated_allocation extension. + + The flag works only if VmaAllocatorCreateInfo::vulkanApiVersion `== VK_API_VERSION_1_0`. + When it is `VK_API_VERSION_1_1`, the flag is ignored because the extension has been promoted to Vulkan 1.1. + + Using this extension will automatically allocate dedicated blocks of memory for + some buffers and images instead of suballocating place for them out of bigger + memory blocks (as if you explicitly used #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT + flag) when it is recommended by the driver. It may improve performance on some + GPUs. + + You may set this flag only if you found out that following device extensions are + supported, you enabled them while creating Vulkan device passed as + VmaAllocatorCreateInfo::device, and you want them to be used internally by this + library: + + - VK_KHR_get_memory_requirements2 (device extension) + - VK_KHR_dedicated_allocation (device extension) + + When this flag is set, you can experience following warnings reported by Vulkan + validation layer. You can ignore them. + + > vkBindBufferMemory(): Binding memory to buffer 0x2d but vkGetBufferMemoryRequirements() has not been called on that buffer. + */ + VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT = 0x00000002, + /** + Enables usage of VK_KHR_bind_memory2 extension. + + The flag works only if VmaAllocatorCreateInfo::vulkanApiVersion `== VK_API_VERSION_1_0`. + When it is `VK_API_VERSION_1_1`, the flag is ignored because the extension has been promoted to Vulkan 1.1. + + You may set this flag only if you found out that this device extension is supported, + you enabled it while creating Vulkan device passed as VmaAllocatorCreateInfo::device, + and you want it to be used internally by this library. + + The extension provides functions `vkBindBufferMemory2KHR` and `vkBindImageMemory2KHR`, + which allow to pass a chain of `pNext` structures while binding. + This flag is required if you use `pNext` parameter in vmaBindBufferMemory2() or vmaBindImageMemory2(). + */ + VMA_ALLOCATOR_CREATE_KHR_BIND_MEMORY2_BIT = 0x00000004, + /** + Enables usage of VK_EXT_memory_budget extension. + + You may set this flag only if you found out that this device extension is supported, + you enabled it while creating Vulkan device passed as VmaAllocatorCreateInfo::device, + and you want it to be used internally by this library, along with another instance extension + VK_KHR_get_physical_device_properties2, which is required by it (or Vulkan 1.1, where this extension is promoted). + + The extension provides query for current memory usage and budget, which will probably + be more accurate than an estimation used by the library otherwise. + */ + VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT = 0x00000008, + /** + Enables usage of VK_AMD_device_coherent_memory extension. + + You may set this flag only if you: + + - found out that this device extension is supported and enabled it while creating Vulkan device passed as VmaAllocatorCreateInfo::device, + - checked that `VkPhysicalDeviceCoherentMemoryFeaturesAMD::deviceCoherentMemory` is true and set it while creating the Vulkan device, + - want it to be used internally by this library. + + The extension and accompanying device feature provide access to memory types with + `VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD` and `VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD` flags. + They are useful mostly for writing breadcrumb markers - a common method for debugging GPU crash/hang/TDR. + + When the extension is not enabled, such memory types are still enumerated, but their usage is illegal. + To protect from this error, if you don't create the allocator with this flag, it will refuse to allocate any memory or create a custom pool in such memory type, + returning `VK_ERROR_FEATURE_NOT_PRESENT`. + */ + VMA_ALLOCATOR_CREATE_AMD_DEVICE_COHERENT_MEMORY_BIT = 0x00000010, + /** + Enables usage of "buffer device address" feature, which allows you to use function + `vkGetBufferDeviceAddress*` to get raw GPU pointer to a buffer and pass it for usage inside a shader. + + You may set this flag only if you: + + 1. (For Vulkan version < 1.2) Found as available and enabled device extension + VK_KHR_buffer_device_address. + This extension is promoted to core Vulkan 1.2. + 2. Found as available and enabled device feature `VkPhysicalDeviceBufferDeviceAddressFeatures::bufferDeviceAddress`. + + When this flag is set, you can create buffers with `VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT` using VMA. + The library automatically adds `VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT` to + allocated memory blocks wherever it might be needed. + + For more information, see documentation chapter \ref enabling_buffer_device_address. + */ + VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT = 0x00000020, + /** + Enables usage of VK_EXT_memory_priority extension in the library. + + You may set this flag only if you found available and enabled this device extension, + along with `VkPhysicalDeviceMemoryPriorityFeaturesEXT::memoryPriority == VK_TRUE`, + while creating Vulkan device passed as VmaAllocatorCreateInfo::device. + + When this flag is used, VmaAllocationCreateInfo::priority and VmaPoolCreateInfo::priority + are used to set priorities of allocated Vulkan memory. Without it, these variables are ignored. + + A priority must be a floating-point value between 0 and 1, indicating the priority of the allocation relative to other memory allocations. + Larger values are higher priority. The granularity of the priorities is implementation-dependent. + It is automatically passed to every call to `vkAllocateMemory` done by the library using structure `VkMemoryPriorityAllocateInfoEXT`. + The value to be used for default priority is 0.5. + For more details, see the documentation of the VK_EXT_memory_priority extension. + */ + VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT = 0x00000040, + /** + Enables usage of VK_KHR_maintenance4 extension in the library. + + You may set this flag only if you found available and enabled this device extension, + while creating Vulkan device passed as VmaAllocatorCreateInfo::device. + */ + VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE4_BIT = 0x00000080, + /** + Enables usage of VK_KHR_maintenance5 extension in the library. + + You should set this flag if you found available and enabled this device extension, + while creating Vulkan device passed as VmaAllocatorCreateInfo::device. + */ + VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE5_BIT = 0x00000100, + + /** + Enables usage of VK_KHR_external_memory_win32 extension in the library. + + You should set this flag if you found available and enabled this device extension, + while creating Vulkan device passed as VmaAllocatorCreateInfo::device. + For more information, see \ref vk_khr_external_memory_win32. + */ + VMA_ALLOCATOR_CREATE_KHR_EXTERNAL_MEMORY_WIN32_BIT = 0x00000200, + + VMA_ALLOCATOR_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF +} VmaAllocatorCreateFlagBits; +/// See #VmaAllocatorCreateFlagBits. +typedef VkFlags VmaAllocatorCreateFlags; + +/** @} */ + +/** +\addtogroup group_alloc +@{ +*/ + +/// \brief Intended usage of the allocated memory. +typedef enum VmaMemoryUsage +{ + /** No intended memory usage specified. + Use other members of VmaAllocationCreateInfo to specify your requirements. + */ + VMA_MEMORY_USAGE_UNKNOWN = 0, + /** + \deprecated Obsolete, preserved for backward compatibility. + Prefers `VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT`. + */ + VMA_MEMORY_USAGE_GPU_ONLY = 1, + /** + \deprecated Obsolete, preserved for backward compatibility. + Guarantees `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` and `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT`. + */ + VMA_MEMORY_USAGE_CPU_ONLY = 2, + /** + \deprecated Obsolete, preserved for backward compatibility. + Guarantees `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT`, prefers `VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT`. + */ + VMA_MEMORY_USAGE_CPU_TO_GPU = 3, + /** + \deprecated Obsolete, preserved for backward compatibility. + Guarantees `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT`, prefers `VK_MEMORY_PROPERTY_HOST_CACHED_BIT`. + */ + VMA_MEMORY_USAGE_GPU_TO_CPU = 4, + /** + \deprecated Obsolete, preserved for backward compatibility. + Prefers not `VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT`. + */ + VMA_MEMORY_USAGE_CPU_COPY = 5, + /** + Lazily allocated GPU memory having `VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT`. + Exists mostly on mobile platforms. Using it on desktop PC or other GPUs with no such memory type present will fail the allocation. + + Usage: Memory for transient attachment images (color attachments, depth attachments etc.), created with `VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT`. + + Allocations with this usage are always created as dedicated - it implies #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT. + */ + VMA_MEMORY_USAGE_GPU_LAZILY_ALLOCATED = 6, + /** + Selects best memory type automatically. + This flag is recommended for most common use cases. + + When using this flag, if you want to map the allocation (using vmaMapMemory() or #VMA_ALLOCATION_CREATE_MAPPED_BIT), + you must pass one of the flags: #VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT or #VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT + in VmaAllocationCreateInfo::flags. + + It can be used only with functions that let the library know `VkBufferCreateInfo` or `VkImageCreateInfo`, e.g. + vmaCreateBuffer(), vmaCreateImage(), vmaFindMemoryTypeIndexForBufferInfo(), vmaFindMemoryTypeIndexForImageInfo() + and not with generic memory allocation functions. + */ + VMA_MEMORY_USAGE_AUTO = 7, + /** + Selects best memory type automatically with preference for GPU (device) memory. + + When using this flag, if you want to map the allocation (using vmaMapMemory() or #VMA_ALLOCATION_CREATE_MAPPED_BIT), + you must pass one of the flags: #VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT or #VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT + in VmaAllocationCreateInfo::flags. + + It can be used only with functions that let the library know `VkBufferCreateInfo` or `VkImageCreateInfo`, e.g. + vmaCreateBuffer(), vmaCreateImage(), vmaFindMemoryTypeIndexForBufferInfo(), vmaFindMemoryTypeIndexForImageInfo() + and not with generic memory allocation functions. + */ + VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE = 8, + /** + Selects best memory type automatically with preference for CPU (host) memory. + + When using this flag, if you want to map the allocation (using vmaMapMemory() or #VMA_ALLOCATION_CREATE_MAPPED_BIT), + you must pass one of the flags: #VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT or #VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT + in VmaAllocationCreateInfo::flags. + + It can be used only with functions that let the library know `VkBufferCreateInfo` or `VkImageCreateInfo`, e.g. + vmaCreateBuffer(), vmaCreateImage(), vmaFindMemoryTypeIndexForBufferInfo(), vmaFindMemoryTypeIndexForImageInfo() + and not with generic memory allocation functions. + */ + VMA_MEMORY_USAGE_AUTO_PREFER_HOST = 9, + + VMA_MEMORY_USAGE_MAX_ENUM = 0x7FFFFFFF +} VmaMemoryUsage; + +/// Flags to be passed as VmaAllocationCreateInfo::flags. +typedef enum VmaAllocationCreateFlagBits +{ + /** \brief Set this flag if the allocation should have its own memory block. + + Use it for special, big resources, like fullscreen images used as attachments. + + If you use this flag while creating a buffer or an image, `VkMemoryDedicatedAllocateInfo` + structure is applied if possible. + */ + VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT = 0x00000001, + + /** \brief Set this flag to only try to allocate from existing `VkDeviceMemory` blocks and never create new such block. + + If new allocation cannot be placed in any of the existing blocks, allocation + fails with `VK_ERROR_OUT_OF_DEVICE_MEMORY` error. + + You should not use #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT and + #VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT at the same time. It makes no sense. + */ + VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT = 0x00000002, + /** \brief Set this flag to use a memory that will be persistently mapped and retrieve pointer to it. + + Pointer to mapped memory will be returned through VmaAllocationInfo::pMappedData. + + It is valid to use this flag for allocation made from memory type that is not + `HOST_VISIBLE`. This flag is then ignored and memory is not mapped. This is + useful if you need an allocation that is efficient to use on GPU + (`DEVICE_LOCAL`) and still want to map it directly if possible on platforms that + support it (e.g. Intel GPU). + */ + VMA_ALLOCATION_CREATE_MAPPED_BIT = 0x00000004, + /** \deprecated Preserved for backward compatibility. Consider using vmaSetAllocationName() instead. + + Set this flag to treat VmaAllocationCreateInfo::pUserData as pointer to a + null-terminated string. Instead of copying pointer value, a local copy of the + string is made and stored in allocation's `pName`. The string is automatically + freed together with the allocation. It is also used in vmaBuildStatsString(). + */ + VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT = 0x00000020, + /** Allocation will be created from upper stack in a double stack pool. + + This flag is only allowed for custom pools created with #VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT flag. + */ + VMA_ALLOCATION_CREATE_UPPER_ADDRESS_BIT = 0x00000040, + /** Create both buffer/image and allocation, but don't bind them together. + It is useful when you want to bind yourself to do some more advanced binding, e.g. using some extensions. + The flag is meaningful only with functions that bind by default: vmaCreateBuffer(), vmaCreateImage(). + Otherwise it is ignored. + + If you want to make sure the new buffer/image is not tied to the new memory allocation + through `VkMemoryDedicatedAllocateInfoKHR` structure in case the allocation ends up in its own memory block, + use also flag #VMA_ALLOCATION_CREATE_CAN_ALIAS_BIT. + */ + VMA_ALLOCATION_CREATE_DONT_BIND_BIT = 0x00000080, + /** Create allocation only if additional device memory required for it, if any, won't exceed + memory budget. Otherwise return `VK_ERROR_OUT_OF_DEVICE_MEMORY`. + */ + VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT = 0x00000100, + /** \brief Set this flag if the allocated memory will have aliasing resources. + + Usage of this flag prevents supplying `VkMemoryDedicatedAllocateInfoKHR` when #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT is specified. + Otherwise created dedicated memory will not be suitable for aliasing resources, resulting in Vulkan Validation Layer errors. + */ + VMA_ALLOCATION_CREATE_CAN_ALIAS_BIT = 0x00000200, + /** + Requests possibility to map the allocation (using vmaMapMemory() or #VMA_ALLOCATION_CREATE_MAPPED_BIT). + + - If you use #VMA_MEMORY_USAGE_AUTO or other `VMA_MEMORY_USAGE_AUTO*` value, + you must use this flag to be able to map the allocation. Otherwise, mapping is incorrect. + - If you use other value of #VmaMemoryUsage, this flag is ignored and mapping is always possible in memory types that are `HOST_VISIBLE`. + This includes allocations created in \ref custom_memory_pools. + + Declares that mapped memory will only be written sequentially, e.g. using `memcpy()` or a loop writing number-by-number, + never read or accessed randomly, so a memory type can be selected that is uncached and write-combined. + + \warning Violating this declaration may work correctly, but will likely be very slow. + Watch out for implicit reads introduced by doing e.g. `pMappedData[i] += x;` + Better prepare your data in a local variable and `memcpy()` it to the mapped pointer all at once. + */ + VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT = 0x00000400, + /** + Requests possibility to map the allocation (using vmaMapMemory() or #VMA_ALLOCATION_CREATE_MAPPED_BIT). + + - If you use #VMA_MEMORY_USAGE_AUTO or other `VMA_MEMORY_USAGE_AUTO*` value, + you must use this flag to be able to map the allocation. Otherwise, mapping is incorrect. + - If you use other value of #VmaMemoryUsage, this flag is ignored and mapping is always possible in memory types that are `HOST_VISIBLE`. + This includes allocations created in \ref custom_memory_pools. + + Declares that mapped memory can be read, written, and accessed in random order, + so a `HOST_CACHED` memory type is preferred. + */ + VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT = 0x00000800, + /** + Together with #VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT or #VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT, + it says that despite request for host access, a not-`HOST_VISIBLE` memory type can be selected + if it may improve performance. + + By using this flag, you declare that you will check if the allocation ended up in a `HOST_VISIBLE` memory type + (e.g. using vmaGetAllocationMemoryProperties()) and if not, you will create some "staging" buffer and + issue an explicit transfer to write/read your data. + To prepare for this possibility, don't forget to add appropriate flags like + `VK_BUFFER_USAGE_TRANSFER_DST_BIT`, `VK_BUFFER_USAGE_TRANSFER_SRC_BIT` to the parameters of created buffer or image. + */ + VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT = 0x00001000, + /** Allocation strategy that chooses smallest possible free range for the allocation + to minimize memory usage and fragmentation, possibly at the expense of allocation time. + */ + VMA_ALLOCATION_CREATE_STRATEGY_MIN_MEMORY_BIT = 0x00010000, + /** Allocation strategy that chooses first suitable free range for the allocation - + not necessarily in terms of the smallest offset but the one that is easiest and fastest to find + to minimize allocation time, possibly at the expense of allocation quality. + */ + VMA_ALLOCATION_CREATE_STRATEGY_MIN_TIME_BIT = 0x00020000, + /** Allocation strategy that chooses always the lowest offset in available space. + This is not the most efficient strategy but achieves highly packed data. + Used internally by defragmentation, not recommended in typical usage. + */ + VMA_ALLOCATION_CREATE_STRATEGY_MIN_OFFSET_BIT = 0x00040000, + /** Alias to #VMA_ALLOCATION_CREATE_STRATEGY_MIN_MEMORY_BIT. + */ + VMA_ALLOCATION_CREATE_STRATEGY_BEST_FIT_BIT = VMA_ALLOCATION_CREATE_STRATEGY_MIN_MEMORY_BIT, + /** Alias to #VMA_ALLOCATION_CREATE_STRATEGY_MIN_TIME_BIT. + */ + VMA_ALLOCATION_CREATE_STRATEGY_FIRST_FIT_BIT = VMA_ALLOCATION_CREATE_STRATEGY_MIN_TIME_BIT, + /** A bit mask to extract only `STRATEGY` bits from entire set of flags. + */ + VMA_ALLOCATION_CREATE_STRATEGY_MASK = + VMA_ALLOCATION_CREATE_STRATEGY_MIN_MEMORY_BIT | + VMA_ALLOCATION_CREATE_STRATEGY_MIN_TIME_BIT | + VMA_ALLOCATION_CREATE_STRATEGY_MIN_OFFSET_BIT, + + VMA_ALLOCATION_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF +} VmaAllocationCreateFlagBits; +/// See #VmaAllocationCreateFlagBits. +typedef VkFlags VmaAllocationCreateFlags; + +/// Flags to be passed as VmaPoolCreateInfo::flags. +typedef enum VmaPoolCreateFlagBits +{ + /** \brief Use this flag if you always allocate only buffers and linear images or only optimal images out of this pool and so Buffer-Image Granularity can be ignored. + + This is an optional optimization flag. + + If you always allocate using vmaCreateBuffer(), vmaCreateImage(), + vmaAllocateMemoryForBuffer(), then you don't need to use it because allocator + knows exact type of your allocations so it can handle Buffer-Image Granularity + in the optimal way. + + If you also allocate using vmaAllocateMemoryForImage() or vmaAllocateMemory(), + exact type of such allocations is not known, so allocator must be conservative + in handling Buffer-Image Granularity, which can lead to suboptimal allocation + (wasted memory). In that case, if you can make sure you always allocate only + buffers and linear images or only optimal images out of this pool, use this flag + to make allocator disregard Buffer-Image Granularity and so make allocations + faster and more optimal. + */ + VMA_POOL_CREATE_IGNORE_BUFFER_IMAGE_GRANULARITY_BIT = 0x00000002, + + /** \brief Enables alternative, linear allocation algorithm in this pool. + + Specify this flag to enable linear allocation algorithm, which always creates + new allocations after last one and doesn't reuse space from allocations freed in + between. It trades memory consumption for simplified algorithm and data + structure, which has better performance and uses less memory for metadata. + + By using this flag, you can achieve behavior of free-at-once, stack, + ring buffer, and double stack. + For details, see documentation chapter \ref linear_algorithm. + */ + VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT = 0x00000004, + + /** Bit mask to extract only `ALGORITHM` bits from entire set of flags. + */ + VMA_POOL_CREATE_ALGORITHM_MASK = + VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT, + + VMA_POOL_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF +} VmaPoolCreateFlagBits; +/// Flags to be passed as VmaPoolCreateInfo::flags. See #VmaPoolCreateFlagBits. +typedef VkFlags VmaPoolCreateFlags; + +/// Flags to be passed as VmaDefragmentationInfo::flags. +typedef enum VmaDefragmentationFlagBits +{ + /* \brief Use simple but fast algorithm for defragmentation. + May not achieve best results but will require least time to compute and least allocations to copy. + */ + VMA_DEFRAGMENTATION_FLAG_ALGORITHM_FAST_BIT = 0x1, + /* \brief Default defragmentation algorithm, applied also when no `ALGORITHM` flag is specified. + Offers a balance between defragmentation quality and the amount of allocations and bytes that need to be moved. + */ + VMA_DEFRAGMENTATION_FLAG_ALGORITHM_BALANCED_BIT = 0x2, + /* \brief Perform full defragmentation of memory. + Can result in notably more time to compute and allocations to copy, but will achieve best memory packing. + */ + VMA_DEFRAGMENTATION_FLAG_ALGORITHM_FULL_BIT = 0x4, + /** \brief Use the most roboust algorithm at the cost of time to compute and number of copies to make. + Only available when bufferImageGranularity is greater than 1, since it aims to reduce + alignment issues between different types of resources. + Otherwise falls back to same behavior as #VMA_DEFRAGMENTATION_FLAG_ALGORITHM_FULL_BIT. + */ + VMA_DEFRAGMENTATION_FLAG_ALGORITHM_EXTENSIVE_BIT = 0x8, + + /// A bit mask to extract only `ALGORITHM` bits from entire set of flags. + VMA_DEFRAGMENTATION_FLAG_ALGORITHM_MASK = + VMA_DEFRAGMENTATION_FLAG_ALGORITHM_FAST_BIT | + VMA_DEFRAGMENTATION_FLAG_ALGORITHM_BALANCED_BIT | + VMA_DEFRAGMENTATION_FLAG_ALGORITHM_FULL_BIT | + VMA_DEFRAGMENTATION_FLAG_ALGORITHM_EXTENSIVE_BIT, + + VMA_DEFRAGMENTATION_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF +} VmaDefragmentationFlagBits; +/// See #VmaDefragmentationFlagBits. +typedef VkFlags VmaDefragmentationFlags; + +/// Operation performed on single defragmentation move. See structure #VmaDefragmentationMove. +typedef enum VmaDefragmentationMoveOperation +{ + /// Buffer/image has been recreated at `dstTmpAllocation`, data has been copied, old buffer/image has been destroyed. `srcAllocation` should be changed to point to the new place. This is the default value set by vmaBeginDefragmentationPass(). + VMA_DEFRAGMENTATION_MOVE_OPERATION_COPY = 0, + /// Set this value if you cannot move the allocation. New place reserved at `dstTmpAllocation` will be freed. `srcAllocation` will remain unchanged. + VMA_DEFRAGMENTATION_MOVE_OPERATION_IGNORE = 1, + /// Set this value if you decide to abandon the allocation and you destroyed the buffer/image. New place reserved at `dstTmpAllocation` will be freed, along with `srcAllocation`, which will be destroyed. + VMA_DEFRAGMENTATION_MOVE_OPERATION_DESTROY = 2, +} VmaDefragmentationMoveOperation; + +/** @} */ + +/** +\addtogroup group_virtual +@{ +*/ + +/// Flags to be passed as VmaVirtualBlockCreateInfo::flags. +typedef enum VmaVirtualBlockCreateFlagBits +{ + /** \brief Enables alternative, linear allocation algorithm in this virtual block. + + Specify this flag to enable linear allocation algorithm, which always creates + new allocations after last one and doesn't reuse space from allocations freed in + between. It trades memory consumption for simplified algorithm and data + structure, which has better performance and uses less memory for metadata. + + By using this flag, you can achieve behavior of free-at-once, stack, + ring buffer, and double stack. + For details, see documentation chapter \ref linear_algorithm. + */ + VMA_VIRTUAL_BLOCK_CREATE_LINEAR_ALGORITHM_BIT = 0x00000001, + + /** \brief Bit mask to extract only `ALGORITHM` bits from entire set of flags. + */ + VMA_VIRTUAL_BLOCK_CREATE_ALGORITHM_MASK = + VMA_VIRTUAL_BLOCK_CREATE_LINEAR_ALGORITHM_BIT, + + VMA_VIRTUAL_BLOCK_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF +} VmaVirtualBlockCreateFlagBits; +/// Flags to be passed as VmaVirtualBlockCreateInfo::flags. See #VmaVirtualBlockCreateFlagBits. +typedef VkFlags VmaVirtualBlockCreateFlags; + +/// Flags to be passed as VmaVirtualAllocationCreateInfo::flags. +typedef enum VmaVirtualAllocationCreateFlagBits +{ + /** \brief Allocation will be created from upper stack in a double stack pool. + + This flag is only allowed for virtual blocks created with #VMA_VIRTUAL_BLOCK_CREATE_LINEAR_ALGORITHM_BIT flag. + */ + VMA_VIRTUAL_ALLOCATION_CREATE_UPPER_ADDRESS_BIT = VMA_ALLOCATION_CREATE_UPPER_ADDRESS_BIT, + /** \brief Allocation strategy that tries to minimize memory usage. + */ + VMA_VIRTUAL_ALLOCATION_CREATE_STRATEGY_MIN_MEMORY_BIT = VMA_ALLOCATION_CREATE_STRATEGY_MIN_MEMORY_BIT, + /** \brief Allocation strategy that tries to minimize allocation time. + */ + VMA_VIRTUAL_ALLOCATION_CREATE_STRATEGY_MIN_TIME_BIT = VMA_ALLOCATION_CREATE_STRATEGY_MIN_TIME_BIT, + /** Allocation strategy that chooses always the lowest offset in available space. + This is not the most efficient strategy but achieves highly packed data. + */ + VMA_VIRTUAL_ALLOCATION_CREATE_STRATEGY_MIN_OFFSET_BIT = VMA_ALLOCATION_CREATE_STRATEGY_MIN_OFFSET_BIT, + /** \brief A bit mask to extract only `STRATEGY` bits from entire set of flags. + + These strategy flags are binary compatible with equivalent flags in #VmaAllocationCreateFlagBits. + */ + VMA_VIRTUAL_ALLOCATION_CREATE_STRATEGY_MASK = VMA_ALLOCATION_CREATE_STRATEGY_MASK, + + VMA_VIRTUAL_ALLOCATION_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF +} VmaVirtualAllocationCreateFlagBits; +/// Flags to be passed as VmaVirtualAllocationCreateInfo::flags. See #VmaVirtualAllocationCreateFlagBits. +typedef VkFlags VmaVirtualAllocationCreateFlags; + +/** @} */ + +#endif // _VMA_ENUM_DECLARATIONS + +#ifndef _VMA_DATA_TYPES_DECLARATIONS + +/** +\addtogroup group_init +@{ */ + +/** \struct VmaAllocator +\brief Represents main object of this library initialized. + +Fill structure #VmaAllocatorCreateInfo and call function vmaCreateAllocator() to create it. +Call function vmaDestroyAllocator() to destroy it. + +It is recommended to create just one object of this type per `VkDevice` object, +right after Vulkan is initialized and keep it alive until before Vulkan device is destroyed. +*/ +VK_DEFINE_HANDLE(VmaAllocator) + +/** @} */ + +/** +\addtogroup group_alloc +@{ +*/ + +/** \struct VmaPool +\brief Represents custom memory pool + +Fill structure VmaPoolCreateInfo and call function vmaCreatePool() to create it. +Call function vmaDestroyPool() to destroy it. + +For more information see [Custom memory pools](@ref choosing_memory_type_custom_memory_pools). +*/ +VK_DEFINE_HANDLE(VmaPool) + +/** \struct VmaAllocation +\brief Represents single memory allocation. + +It may be either dedicated block of `VkDeviceMemory` or a specific region of a bigger block of this type +plus unique offset. + +There are multiple ways to create such object. +You need to fill structure VmaAllocationCreateInfo. +For more information see [Choosing memory type](@ref choosing_memory_type). + +Although the library provides convenience functions that create Vulkan buffer or image, +allocate memory for it and bind them together, +binding of the allocation to a buffer or an image is out of scope of the allocation itself. +Allocation object can exist without buffer/image bound, +binding can be done manually by the user, and destruction of it can be done +independently of destruction of the allocation. + +The object also remembers its size and some other information. +To retrieve this information, use function vmaGetAllocationInfo() and inspect +returned structure VmaAllocationInfo. +*/ +VK_DEFINE_HANDLE(VmaAllocation) + +/** \struct VmaDefragmentationContext +\brief An opaque object that represents started defragmentation process. + +Fill structure #VmaDefragmentationInfo and call function vmaBeginDefragmentation() to create it. +Call function vmaEndDefragmentation() to destroy it. +*/ +VK_DEFINE_HANDLE(VmaDefragmentationContext) + +/** @} */ + +/** +\addtogroup group_virtual +@{ +*/ + +/** \struct VmaVirtualAllocation +\brief Represents single memory allocation done inside VmaVirtualBlock. + +Use it as a unique identifier to virtual allocation within the single block. + +Use value `VK_NULL_HANDLE` to represent a null/invalid allocation. +*/ +VK_DEFINE_NON_DISPATCHABLE_HANDLE(VmaVirtualAllocation) + +/** @} */ + +/** +\addtogroup group_virtual +@{ +*/ + +/** \struct VmaVirtualBlock +\brief Handle to a virtual block object that allows to use core allocation algorithm without allocating any real GPU memory. + +Fill in #VmaVirtualBlockCreateInfo structure and use vmaCreateVirtualBlock() to create it. Use vmaDestroyVirtualBlock() to destroy it. +For more information, see documentation chapter \ref virtual_allocator. + +This object is not thread-safe - should not be used from multiple threads simultaneously, must be synchronized externally. +*/ +VK_DEFINE_HANDLE(VmaVirtualBlock) + +/** @} */ + +/** +\addtogroup group_init +@{ +*/ + +/// Callback function called after successful vkAllocateMemory. +typedef void (VKAPI_PTR* PFN_vmaAllocateDeviceMemoryFunction)( + VmaAllocator VMA_NOT_NULL allocator, + uint32_t memoryType, + VkDeviceMemory VMA_NOT_NULL_NON_DISPATCHABLE memory, + VkDeviceSize size, + void* VMA_NULLABLE pUserData); + +/// Callback function called before vkFreeMemory. +typedef void (VKAPI_PTR* PFN_vmaFreeDeviceMemoryFunction)( + VmaAllocator VMA_NOT_NULL allocator, + uint32_t memoryType, + VkDeviceMemory VMA_NOT_NULL_NON_DISPATCHABLE memory, + VkDeviceSize size, + void* VMA_NULLABLE pUserData); + +/** \brief Set of callbacks that the library will call for `vkAllocateMemory` and `vkFreeMemory`. + +Provided for informative purpose, e.g. to gather statistics about number of +allocations or total amount of memory allocated in Vulkan. + +Used in VmaAllocatorCreateInfo::pDeviceMemoryCallbacks. +*/ +typedef struct VmaDeviceMemoryCallbacks +{ + /// Optional, can be null. + PFN_vmaAllocateDeviceMemoryFunction VMA_NULLABLE pfnAllocate; + /// Optional, can be null. + PFN_vmaFreeDeviceMemoryFunction VMA_NULLABLE pfnFree; + /// Optional, can be null. + void* VMA_NULLABLE pUserData; +} VmaDeviceMemoryCallbacks; + +/** \brief Pointers to some Vulkan functions - a subset used by the library. + +Used in VmaAllocatorCreateInfo::pVulkanFunctions. +*/ +typedef struct VmaVulkanFunctions +{ + /// Required when using VMA_DYNAMIC_VULKAN_FUNCTIONS. + PFN_vkGetInstanceProcAddr VMA_NULLABLE vkGetInstanceProcAddr; + /// Required when using VMA_DYNAMIC_VULKAN_FUNCTIONS. + PFN_vkGetDeviceProcAddr VMA_NULLABLE vkGetDeviceProcAddr; + PFN_vkGetPhysicalDeviceProperties VMA_NULLABLE vkGetPhysicalDeviceProperties; + PFN_vkGetPhysicalDeviceMemoryProperties VMA_NULLABLE vkGetPhysicalDeviceMemoryProperties; + PFN_vkAllocateMemory VMA_NULLABLE vkAllocateMemory; + PFN_vkFreeMemory VMA_NULLABLE vkFreeMemory; + PFN_vkMapMemory VMA_NULLABLE vkMapMemory; + PFN_vkUnmapMemory VMA_NULLABLE vkUnmapMemory; + PFN_vkFlushMappedMemoryRanges VMA_NULLABLE vkFlushMappedMemoryRanges; + PFN_vkInvalidateMappedMemoryRanges VMA_NULLABLE vkInvalidateMappedMemoryRanges; + PFN_vkBindBufferMemory VMA_NULLABLE vkBindBufferMemory; + PFN_vkBindImageMemory VMA_NULLABLE vkBindImageMemory; + PFN_vkGetBufferMemoryRequirements VMA_NULLABLE vkGetBufferMemoryRequirements; + PFN_vkGetImageMemoryRequirements VMA_NULLABLE vkGetImageMemoryRequirements; + PFN_vkCreateBuffer VMA_NULLABLE vkCreateBuffer; + PFN_vkDestroyBuffer VMA_NULLABLE vkDestroyBuffer; + PFN_vkCreateImage VMA_NULLABLE vkCreateImage; + PFN_vkDestroyImage VMA_NULLABLE vkDestroyImage; + PFN_vkCmdCopyBuffer VMA_NULLABLE vkCmdCopyBuffer; +#if VMA_DEDICATED_ALLOCATION || VMA_VULKAN_VERSION >= 1001000 + /// Fetch "vkGetBufferMemoryRequirements2" on Vulkan >= 1.1, fetch "vkGetBufferMemoryRequirements2KHR" when using VK_KHR_dedicated_allocation extension. + PFN_vkGetBufferMemoryRequirements2KHR VMA_NULLABLE vkGetBufferMemoryRequirements2KHR; + /// Fetch "vkGetImageMemoryRequirements2" on Vulkan >= 1.1, fetch "vkGetImageMemoryRequirements2KHR" when using VK_KHR_dedicated_allocation extension. + PFN_vkGetImageMemoryRequirements2KHR VMA_NULLABLE vkGetImageMemoryRequirements2KHR; +#endif +#if VMA_BIND_MEMORY2 || VMA_VULKAN_VERSION >= 1001000 + /// Fetch "vkBindBufferMemory2" on Vulkan >= 1.1, fetch "vkBindBufferMemory2KHR" when using VK_KHR_bind_memory2 extension. + PFN_vkBindBufferMemory2KHR VMA_NULLABLE vkBindBufferMemory2KHR; + /// Fetch "vkBindImageMemory2" on Vulkan >= 1.1, fetch "vkBindImageMemory2KHR" when using VK_KHR_bind_memory2 extension. + PFN_vkBindImageMemory2KHR VMA_NULLABLE vkBindImageMemory2KHR; +#endif +#if VMA_MEMORY_BUDGET || VMA_VULKAN_VERSION >= 1001000 + /// Fetch from "vkGetPhysicalDeviceMemoryProperties2" on Vulkan >= 1.1, but you can also fetch it from "vkGetPhysicalDeviceMemoryProperties2KHR" if you enabled extension VK_KHR_get_physical_device_properties2. + PFN_vkGetPhysicalDeviceMemoryProperties2KHR VMA_NULLABLE vkGetPhysicalDeviceMemoryProperties2KHR; +#endif +#if VMA_KHR_MAINTENANCE4 || VMA_VULKAN_VERSION >= 1003000 + /// Fetch from "vkGetDeviceBufferMemoryRequirements" on Vulkan >= 1.3, but you can also fetch it from "vkGetDeviceBufferMemoryRequirementsKHR" if you enabled extension VK_KHR_maintenance4. + PFN_vkGetDeviceBufferMemoryRequirementsKHR VMA_NULLABLE vkGetDeviceBufferMemoryRequirements; + /// Fetch from "vkGetDeviceImageMemoryRequirements" on Vulkan >= 1.3, but you can also fetch it from "vkGetDeviceImageMemoryRequirementsKHR" if you enabled extension VK_KHR_maintenance4. + PFN_vkGetDeviceImageMemoryRequirementsKHR VMA_NULLABLE vkGetDeviceImageMemoryRequirements; +#endif +#if VMA_EXTERNAL_MEMORY_WIN32 + PFN_vkGetMemoryWin32HandleKHR VMA_NULLABLE vkGetMemoryWin32HandleKHR; +#else + void* VMA_NULLABLE vkGetMemoryWin32HandleKHR; +#endif +} VmaVulkanFunctions; + +/// Description of a Allocator to be created. +typedef struct VmaAllocatorCreateInfo +{ + /// Flags for created allocator. Use #VmaAllocatorCreateFlagBits enum. + VmaAllocatorCreateFlags flags; + /// Vulkan physical device. + /** It must be valid throughout whole lifetime of created allocator. */ + VkPhysicalDevice VMA_NOT_NULL physicalDevice; + /// Vulkan device. + /** It must be valid throughout whole lifetime of created allocator. */ + VkDevice VMA_NOT_NULL device; + /// Preferred size of a single `VkDeviceMemory` block to be allocated from large heaps > 1 GiB. Optional. + /** Set to 0 to use default, which is currently 256 MiB. */ + VkDeviceSize preferredLargeHeapBlockSize; + /// Custom CPU memory allocation callbacks. Optional. + /** Optional, can be null. When specified, will also be used for all CPU-side memory allocations. */ + const VkAllocationCallbacks* VMA_NULLABLE pAllocationCallbacks; + /// Informative callbacks for `vkAllocateMemory`, `vkFreeMemory`. Optional. + /** Optional, can be null. */ + const VmaDeviceMemoryCallbacks* VMA_NULLABLE pDeviceMemoryCallbacks; + /** \brief Either null or a pointer to an array of limits on maximum number of bytes that can be allocated out of particular Vulkan memory heap. + + If not NULL, it must be a pointer to an array of + `VkPhysicalDeviceMemoryProperties::memoryHeapCount` elements, defining limit on + maximum number of bytes that can be allocated out of particular Vulkan memory + heap. + + Any of the elements may be equal to `VK_WHOLE_SIZE`, which means no limit on that + heap. This is also the default in case of `pHeapSizeLimit` = NULL. + + If there is a limit defined for a heap: + + - If user tries to allocate more memory from that heap using this allocator, + the allocation fails with `VK_ERROR_OUT_OF_DEVICE_MEMORY`. + - If the limit is smaller than heap size reported in `VkMemoryHeap::size`, the + value of this limit will be reported instead when using vmaGetMemoryProperties(). + + Warning! Using this feature may not be equivalent to installing a GPU with + smaller amount of memory, because graphics driver doesn't necessary fail new + allocations with `VK_ERROR_OUT_OF_DEVICE_MEMORY` result when memory capacity is + exceeded. It may return success and just silently migrate some device memory + blocks to system RAM. This driver behavior can also be controlled using + VK_AMD_memory_overallocation_behavior extension. + */ + const VkDeviceSize* VMA_NULLABLE VMA_LEN_IF_NOT_NULL("VkPhysicalDeviceMemoryProperties::memoryHeapCount") pHeapSizeLimit; + + /** \brief Pointers to Vulkan functions. Can be null. + + For details see [Pointers to Vulkan functions](@ref config_Vulkan_functions). + */ + const VmaVulkanFunctions* VMA_NULLABLE pVulkanFunctions; + /** \brief Handle to Vulkan instance object. + + Starting from version 3.0.0 this member is no longer optional, it must be set! + */ + VkInstance VMA_NOT_NULL instance; + /** \brief Optional. Vulkan version that the application uses. + + It must be a value in the format as created by macro `VK_MAKE_VERSION` or a constant like: `VK_API_VERSION_1_1`, `VK_API_VERSION_1_0`. + The patch version number specified is ignored. Only the major and minor versions are considered. + Only versions 1.0...1.4 are supported by the current implementation. + Leaving it initialized to zero is equivalent to `VK_API_VERSION_1_0`. + It must match the Vulkan version used by the application and supported on the selected physical device, + so it must be no higher than `VkApplicationInfo::apiVersion` passed to `vkCreateInstance` + and no higher than `VkPhysicalDeviceProperties::apiVersion` found on the physical device used. + */ + uint32_t vulkanApiVersion; +#if VMA_EXTERNAL_MEMORY + /** \brief Either null or a pointer to an array of external memory handle types for each Vulkan memory type. + + If not NULL, it must be a pointer to an array of `VkPhysicalDeviceMemoryProperties::memoryTypeCount` + elements, defining external memory handle types of particular Vulkan memory type, + to be passed using `VkExportMemoryAllocateInfoKHR`. + + Any of the elements may be equal to 0, which means not to use `VkExportMemoryAllocateInfoKHR` on this memory type. + This is also the default in case of `pTypeExternalMemoryHandleTypes` = NULL. + */ + const VkExternalMemoryHandleTypeFlagsKHR* VMA_NULLABLE VMA_LEN_IF_NOT_NULL("VkPhysicalDeviceMemoryProperties::memoryTypeCount") pTypeExternalMemoryHandleTypes; +#endif // #if VMA_EXTERNAL_MEMORY +} VmaAllocatorCreateInfo; + +/// Information about existing #VmaAllocator object. +typedef struct VmaAllocatorInfo +{ + /** \brief Handle to Vulkan instance object. + + This is the same value as has been passed through VmaAllocatorCreateInfo::instance. + */ + VkInstance VMA_NOT_NULL instance; + /** \brief Handle to Vulkan physical device object. + + This is the same value as has been passed through VmaAllocatorCreateInfo::physicalDevice. + */ + VkPhysicalDevice VMA_NOT_NULL physicalDevice; + /** \brief Handle to Vulkan device object. + + This is the same value as has been passed through VmaAllocatorCreateInfo::device. + */ + VkDevice VMA_NOT_NULL device; +} VmaAllocatorInfo; + +/** @} */ + +/** +\addtogroup group_stats +@{ +*/ + +/** \brief Calculated statistics of memory usage e.g. in a specific memory type, heap, custom pool, or total. + +These are fast to calculate. +See functions: vmaGetHeapBudgets(), vmaGetPoolStatistics(). +*/ +typedef struct VmaStatistics +{ + /** \brief Number of `VkDeviceMemory` objects - Vulkan memory blocks allocated. + */ + uint32_t blockCount; + /** \brief Number of #VmaAllocation objects allocated. + + Dedicated allocations have their own blocks, so each one adds 1 to `allocationCount` as well as `blockCount`. + */ + uint32_t allocationCount; + /** \brief Number of bytes allocated in `VkDeviceMemory` blocks. + + \note To avoid confusion, please be aware that what Vulkan calls an "allocation" - a whole `VkDeviceMemory` object + (e.g. as in `VkPhysicalDeviceLimits::maxMemoryAllocationCount`) is called a "block" in VMA, while VMA calls + "allocation" a #VmaAllocation object that represents a memory region sub-allocated from such block, usually for a single buffer or image. + */ + VkDeviceSize blockBytes; + /** \brief Total number of bytes occupied by all #VmaAllocation objects. + + Always less or equal than `blockBytes`. + Difference `(blockBytes - allocationBytes)` is the amount of memory allocated from Vulkan + but unused by any #VmaAllocation. + */ + VkDeviceSize allocationBytes; +} VmaStatistics; + +/** \brief More detailed statistics than #VmaStatistics. + +These are slower to calculate. Use for debugging purposes. +See functions: vmaCalculateStatistics(), vmaCalculatePoolStatistics(). + +Previous version of the statistics API provided averages, but they have been removed +because they can be easily calculated as: + +\code +VkDeviceSize allocationSizeAvg = detailedStats.statistics.allocationBytes / detailedStats.statistics.allocationCount; +VkDeviceSize unusedBytes = detailedStats.statistics.blockBytes - detailedStats.statistics.allocationBytes; +VkDeviceSize unusedRangeSizeAvg = unusedBytes / detailedStats.unusedRangeCount; +\endcode +*/ +typedef struct VmaDetailedStatistics +{ + /// Basic statistics. + VmaStatistics statistics; + /// Number of free ranges of memory between allocations. + uint32_t unusedRangeCount; + /// Smallest allocation size. `VK_WHOLE_SIZE` if there are 0 allocations. + VkDeviceSize allocationSizeMin; + /// Largest allocation size. 0 if there are 0 allocations. + VkDeviceSize allocationSizeMax; + /// Smallest empty range size. `VK_WHOLE_SIZE` if there are 0 empty ranges. + VkDeviceSize unusedRangeSizeMin; + /// Largest empty range size. 0 if there are 0 empty ranges. + VkDeviceSize unusedRangeSizeMax; +} VmaDetailedStatistics; + +/** \brief General statistics from current state of the Allocator - +total memory usage across all memory heaps and types. + +These are slower to calculate. Use for debugging purposes. +See function vmaCalculateStatistics(). +*/ +typedef struct VmaTotalStatistics +{ + VmaDetailedStatistics memoryType[VK_MAX_MEMORY_TYPES]; + VmaDetailedStatistics memoryHeap[VK_MAX_MEMORY_HEAPS]; + VmaDetailedStatistics total; +} VmaTotalStatistics; + +/** \brief Statistics of current memory usage and available budget for a specific memory heap. + +These are fast to calculate. +See function vmaGetHeapBudgets(). +*/ +typedef struct VmaBudget +{ + /** \brief Statistics fetched from the library. + */ + VmaStatistics statistics; + /** \brief Estimated current memory usage of the program, in bytes. + + Fetched from system using VK_EXT_memory_budget extension if enabled. + + It might be different than `statistics.blockBytes` (usually higher) due to additional implicit objects + also occupying the memory, like swapchain, pipelines, descriptor heaps, command buffers, or + `VkDeviceMemory` blocks allocated outside of this library, if any. + */ + VkDeviceSize usage; + /** \brief Estimated amount of memory available to the program, in bytes. + + Fetched from system using VK_EXT_memory_budget extension if enabled. + + It might be different (most probably smaller) than `VkMemoryHeap::size[heapIndex]` due to factors + external to the program, decided by the operating system. + Difference `budget - usage` is the amount of additional memory that can probably + be allocated without problems. Exceeding the budget may result in various problems. + */ + VkDeviceSize budget; +} VmaBudget; + +/** @} */ + +/** +\addtogroup group_alloc +@{ +*/ + +/** \brief Parameters of new #VmaAllocation. + +To be used with functions like vmaCreateBuffer(), vmaCreateImage(), and many others. +*/ +typedef struct VmaAllocationCreateInfo +{ + /// Use #VmaAllocationCreateFlagBits enum. + VmaAllocationCreateFlags flags; + /** \brief Intended usage of memory. + + You can leave #VMA_MEMORY_USAGE_UNKNOWN if you specify memory requirements in other way. \n + If `pool` is not null, this member is ignored. + */ + VmaMemoryUsage usage; + /** \brief Flags that must be set in a Memory Type chosen for an allocation. + + Leave 0 if you specify memory requirements in other way. \n + If `pool` is not null, this member is ignored.*/ + VkMemoryPropertyFlags requiredFlags; + /** \brief Flags that preferably should be set in a memory type chosen for an allocation. + + Set to 0 if no additional flags are preferred. \n + If `pool` is not null, this member is ignored. */ + VkMemoryPropertyFlags preferredFlags; + /** \brief Bitmask containing one bit set for every memory type acceptable for this allocation. + + Value 0 is equivalent to `UINT32_MAX` - it means any memory type is accepted if + it meets other requirements specified by this structure, with no further + restrictions on memory type index. \n + If `pool` is not null, this member is ignored. + */ + uint32_t memoryTypeBits; + /** \brief Pool that this allocation should be created in. + + Leave `VK_NULL_HANDLE` to allocate from default pool. If not null, members: + `usage`, `requiredFlags`, `preferredFlags`, `memoryTypeBits` are ignored. + */ + VmaPool VMA_NULLABLE pool; + /** \brief Custom general-purpose pointer that will be stored in #VmaAllocation, can be read as VmaAllocationInfo::pUserData and changed using vmaSetAllocationUserData(). + + If #VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT is used, it must be either + null or pointer to a null-terminated string. The string will be then copied to + internal buffer, so it doesn't need to be valid after allocation call. + */ + void* VMA_NULLABLE pUserData; + /** \brief A floating-point value between 0 and 1, indicating the priority of the allocation relative to other memory allocations. + + It is used only when #VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT flag was used during creation of the #VmaAllocator object + and this allocation ends up as dedicated or is explicitly forced as dedicated using #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT. + Otherwise, it has the priority of a memory block where it is placed and this variable is ignored. + */ + float priority; +} VmaAllocationCreateInfo; + +/// Describes parameter of created #VmaPool. +typedef struct VmaPoolCreateInfo +{ + /** \brief Vulkan memory type index to allocate this pool from. + */ + uint32_t memoryTypeIndex; + /** \brief Use combination of #VmaPoolCreateFlagBits. + */ + VmaPoolCreateFlags flags; + /** \brief Size of a single `VkDeviceMemory` block to be allocated as part of this pool, in bytes. Optional. + + Specify nonzero to set explicit, constant size of memory blocks used by this + pool. + + Leave 0 to use default and let the library manage block sizes automatically. + Sizes of particular blocks may vary. + In this case, the pool will also support dedicated allocations. + */ + VkDeviceSize blockSize; + /** \brief Minimum number of blocks to be always allocated in this pool, even if they stay empty. + + Set to 0 to have no preallocated blocks and allow the pool be completely empty. + */ + size_t minBlockCount; + /** \brief Maximum number of blocks that can be allocated in this pool. Optional. + + Set to 0 to use default, which is `SIZE_MAX`, which means no limit. + + Set to same value as VmaPoolCreateInfo::minBlockCount to have fixed amount of memory allocated + throughout whole lifetime of this pool. + */ + size_t maxBlockCount; + /** \brief A floating-point value between 0 and 1, indicating the priority of the allocations in this pool relative to other memory allocations. + + It is used only when #VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT flag was used during creation of the #VmaAllocator object. + Otherwise, this variable is ignored. + */ + float priority; + /** \brief Additional minimum alignment to be used for all allocations created from this pool. Can be 0. + + Leave 0 (default) not to impose any additional alignment. If not 0, it must be a power of two. + It can be useful in cases where alignment returned by Vulkan by functions like `vkGetBufferMemoryRequirements` is not enough, + e.g. when doing interop with OpenGL. + */ + VkDeviceSize minAllocationAlignment; + /** \brief Additional `pNext` chain to be attached to `VkMemoryAllocateInfo` used for every allocation made by this pool. Optional. + + Optional, can be null. If not null, it must point to a `pNext` chain of structures that can be attached to `VkMemoryAllocateInfo`. + It can be useful for special needs such as adding `VkExportMemoryAllocateInfoKHR`. + Structures pointed by this member must remain alive and unchanged for the whole lifetime of the custom pool. + + Please note that some structures, e.g. `VkMemoryPriorityAllocateInfoEXT`, `VkMemoryDedicatedAllocateInfoKHR`, + can be attached automatically by this library when using other, more convenient of its features. + */ + void* VMA_NULLABLE VMA_EXTENDS_VK_STRUCT(VkMemoryAllocateInfo) pMemoryAllocateNext; +} VmaPoolCreateInfo; + +/** @} */ + +/** +\addtogroup group_alloc +@{ +*/ + +/** +Parameters of #VmaAllocation objects, that can be retrieved using function vmaGetAllocationInfo(). + +There is also an extended version of this structure that carries additional parameters: #VmaAllocationInfo2. +*/ +typedef struct VmaAllocationInfo +{ + /** \brief Memory type index that this allocation was allocated from. + + It never changes. + */ + uint32_t memoryType; + /** \brief Handle to Vulkan memory object. + + Same memory object can be shared by multiple allocations. + + It can change after the allocation is moved during \ref defragmentation. + */ + VkDeviceMemory VMA_NULLABLE_NON_DISPATCHABLE deviceMemory; + /** \brief Offset in `VkDeviceMemory` object to the beginning of this allocation, in bytes. `(deviceMemory, offset)` pair is unique to this allocation. + + You usually don't need to use this offset. If you create a buffer or an image together with the allocation using e.g. function + vmaCreateBuffer(), vmaCreateImage(), functions that operate on these resources refer to the beginning of the buffer or image, + not entire device memory block. Functions like vmaMapMemory(), vmaBindBufferMemory() also refer to the beginning of the allocation + and apply this offset automatically. + + It can change after the allocation is moved during \ref defragmentation. + */ + VkDeviceSize offset; + /** \brief Size of this allocation, in bytes. + + It never changes. + + \note Allocation size returned in this variable may be greater than the size + requested for the resource e.g. as `VkBufferCreateInfo::size`. Whole size of the + allocation is accessible for operations on memory e.g. using a pointer after + mapping with vmaMapMemory(), but operations on the resource e.g. using + `vkCmdCopyBuffer` must be limited to the size of the resource. + */ + VkDeviceSize size; + /** \brief Pointer to the beginning of this allocation as mapped data. + + If the allocation hasn't been mapped using vmaMapMemory() and hasn't been + created with #VMA_ALLOCATION_CREATE_MAPPED_BIT flag, this value is null. + + It can change after call to vmaMapMemory(), vmaUnmapMemory(). + It can also change after the allocation is moved during \ref defragmentation. + */ + void* VMA_NULLABLE pMappedData; + /** \brief Custom general-purpose pointer that was passed as VmaAllocationCreateInfo::pUserData or set using vmaSetAllocationUserData(). + + It can change after call to vmaSetAllocationUserData() for this allocation. + */ + void* VMA_NULLABLE pUserData; + /** \brief Custom allocation name that was set with vmaSetAllocationName(). + + It can change after call to vmaSetAllocationName() for this allocation. + + Another way to set custom name is to pass it in VmaAllocationCreateInfo::pUserData with + additional flag #VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT set [DEPRECATED]. + */ + const char* VMA_NULLABLE pName; +} VmaAllocationInfo; + +/// Extended parameters of a #VmaAllocation object that can be retrieved using function vmaGetAllocationInfo2(). +typedef struct VmaAllocationInfo2 +{ + /** \brief Basic parameters of the allocation. + + If you need only these, you can use function vmaGetAllocationInfo() and structure #VmaAllocationInfo instead. + */ + VmaAllocationInfo allocationInfo; + /** \brief Size of the `VkDeviceMemory` block that the allocation belongs to. + + In case of an allocation with dedicated memory, it will be equal to `allocationInfo.size`. + */ + VkDeviceSize blockSize; + /** \brief `VK_TRUE` if the allocation has dedicated memory, `VK_FALSE` if it was placed as part of a larger memory block. + + When `VK_TRUE`, it also means `VkMemoryDedicatedAllocateInfo` was used when creating the allocation + (if VK_KHR_dedicated_allocation extension or Vulkan version >= 1.1 is enabled). + */ + VkBool32 dedicatedMemory; +} VmaAllocationInfo2; + +/** Callback function called during vmaBeginDefragmentation() to check custom criterion about ending current defragmentation pass. + +Should return true if the defragmentation needs to stop current pass. +*/ +typedef VkBool32 (VKAPI_PTR* PFN_vmaCheckDefragmentationBreakFunction)(void* VMA_NULLABLE pUserData); + +/** \brief Parameters for defragmentation. + +To be used with function vmaBeginDefragmentation(). +*/ +typedef struct VmaDefragmentationInfo +{ + /// \brief Use combination of #VmaDefragmentationFlagBits. + VmaDefragmentationFlags flags; + /** \brief Custom pool to be defragmented. + + If null then default pools will undergo defragmentation process. + */ + VmaPool VMA_NULLABLE pool; + /** \brief Maximum numbers of bytes that can be copied during single pass, while moving allocations to different places. + + `0` means no limit. + */ + VkDeviceSize maxBytesPerPass; + /** \brief Maximum number of allocations that can be moved during single pass to a different place. + + `0` means no limit. + */ + uint32_t maxAllocationsPerPass; + /** \brief Optional custom callback for stopping vmaBeginDefragmentation(). + + Have to return true for breaking current defragmentation pass. + */ + PFN_vmaCheckDefragmentationBreakFunction VMA_NULLABLE pfnBreakCallback; + /// \brief Optional data to pass to custom callback for stopping pass of defragmentation. + void* VMA_NULLABLE pBreakCallbackUserData; +} VmaDefragmentationInfo; + +/// Single move of an allocation to be done for defragmentation. +typedef struct VmaDefragmentationMove +{ + /// Operation to be performed on the allocation by vmaEndDefragmentationPass(). Default value is #VMA_DEFRAGMENTATION_MOVE_OPERATION_COPY. You can modify it. + VmaDefragmentationMoveOperation operation; + /// Allocation that should be moved. + VmaAllocation VMA_NOT_NULL srcAllocation; + /** \brief Temporary allocation pointing to destination memory that will replace `srcAllocation`. + + \warning Do not store this allocation in your data structures! It exists only temporarily, for the duration of the defragmentation pass, + to be used for binding new buffer/image to the destination memory using e.g. vmaBindBufferMemory(). + vmaEndDefragmentationPass() will destroy it and make `srcAllocation` point to this memory. + */ + VmaAllocation VMA_NOT_NULL dstTmpAllocation; +} VmaDefragmentationMove; + +/** \brief Parameters for incremental defragmentation steps. + +To be used with function vmaBeginDefragmentationPass(). +*/ +typedef struct VmaDefragmentationPassMoveInfo +{ + /// Number of elements in the `pMoves` array. + uint32_t moveCount; + /** \brief Array of moves to be performed by the user in the current defragmentation pass. + + Pointer to an array of `moveCount` elements, owned by VMA, created in vmaBeginDefragmentationPass(), destroyed in vmaEndDefragmentationPass(). + + For each element, you should: + + 1. Create a new buffer/image in the place pointed by VmaDefragmentationMove::dstMemory + VmaDefragmentationMove::dstOffset. + 2. Copy data from the VmaDefragmentationMove::srcAllocation e.g. using `vkCmdCopyBuffer`, `vkCmdCopyImage`. + 3. Make sure these commands finished executing on the GPU. + 4. Destroy the old buffer/image. + + Only then you can finish defragmentation pass by calling vmaEndDefragmentationPass(). + After this call, the allocation will point to the new place in memory. + + Alternatively, if you cannot move specific allocation, you can set VmaDefragmentationMove::operation to #VMA_DEFRAGMENTATION_MOVE_OPERATION_IGNORE. + + Alternatively, if you decide you want to completely remove the allocation: + + 1. Destroy its buffer/image. + 2. Set VmaDefragmentationMove::operation to #VMA_DEFRAGMENTATION_MOVE_OPERATION_DESTROY. + + Then, after vmaEndDefragmentationPass() the allocation will be freed. + */ + VmaDefragmentationMove* VMA_NULLABLE VMA_LEN_IF_NOT_NULL(moveCount) pMoves; +} VmaDefragmentationPassMoveInfo; + +/// Statistics returned for defragmentation process in function vmaEndDefragmentation(). +typedef struct VmaDefragmentationStats +{ + /// Total number of bytes that have been copied while moving allocations to different places. + VkDeviceSize bytesMoved; + /// Total number of bytes that have been released to the system by freeing empty `VkDeviceMemory` objects. + VkDeviceSize bytesFreed; + /// Number of allocations that have been moved to different places. + uint32_t allocationsMoved; + /// Number of empty `VkDeviceMemory` objects that have been released to the system. + uint32_t deviceMemoryBlocksFreed; +} VmaDefragmentationStats; + +/** @} */ + +/** +\addtogroup group_virtual +@{ +*/ + +/// Parameters of created #VmaVirtualBlock object to be passed to vmaCreateVirtualBlock(). +typedef struct VmaVirtualBlockCreateInfo +{ + /** \brief Total size of the virtual block. + + Sizes can be expressed in bytes or any units you want as long as you are consistent in using them. + For example, if you allocate from some array of structures, 1 can mean single instance of entire structure. + */ + VkDeviceSize size; + + /** \brief Use combination of #VmaVirtualBlockCreateFlagBits. + */ + VmaVirtualBlockCreateFlags flags; + + /** \brief Custom CPU memory allocation callbacks. Optional. + + Optional, can be null. When specified, they will be used for all CPU-side memory allocations. + */ + const VkAllocationCallbacks* VMA_NULLABLE pAllocationCallbacks; +} VmaVirtualBlockCreateInfo; + +/// Parameters of created virtual allocation to be passed to vmaVirtualAllocate(). +typedef struct VmaVirtualAllocationCreateInfo +{ + /** \brief Size of the allocation. + + Cannot be zero. + */ + VkDeviceSize size; + /** \brief Required alignment of the allocation. Optional. + + Must be power of two. Special value 0 has the same meaning as 1 - means no special alignment is required, so allocation can start at any offset. + */ + VkDeviceSize alignment; + /** \brief Use combination of #VmaVirtualAllocationCreateFlagBits. + */ + VmaVirtualAllocationCreateFlags flags; + /** \brief Custom pointer to be associated with the allocation. Optional. + + It can be any value and can be used for user-defined purposes. It can be fetched or changed later. + */ + void* VMA_NULLABLE pUserData; +} VmaVirtualAllocationCreateInfo; + +/// Parameters of an existing virtual allocation, returned by vmaGetVirtualAllocationInfo(). +typedef struct VmaVirtualAllocationInfo +{ + /** \brief Offset of the allocation. + + Offset at which the allocation was made. + */ + VkDeviceSize offset; + /** \brief Size of the allocation. + + Same value as passed in VmaVirtualAllocationCreateInfo::size. + */ + VkDeviceSize size; + /** \brief Custom pointer associated with the allocation. + + Same value as passed in VmaVirtualAllocationCreateInfo::pUserData or to vmaSetVirtualAllocationUserData(). + */ + void* VMA_NULLABLE pUserData; +} VmaVirtualAllocationInfo; + +/** @} */ + +#endif // _VMA_DATA_TYPES_DECLARATIONS + +#ifndef _VMA_FUNCTION_HEADERS + +/** +\addtogroup group_init +@{ +*/ + +#ifdef VOLK_HEADER_VERSION +/** \brief Fully initializes `pDstVulkanFunctions` structure with Vulkan functions needed by VMA +using [volk library](https://github.com/zeux/volk). + +This function is defined in VMA header only if "volk.h" was included before it. + +To use this function properly: + +-# Initialize volk and Vulkan: + -# Call `volkInitialize()` + -# Create `VkInstance` object + -# Call `volkLoadInstance()` + -# Create `VkDevice` object + -# Call `volkLoadDevice()` +-# Fill in structure #VmaAllocatorCreateInfo, especially members: + - VmaAllocatorCreateInfo::device + - VmaAllocatorCreateInfo::vulkanApiVersion + - VmaAllocatorCreateInfo::flags - set appropriate flags for the Vulkan extensions you enabled +-# Create an instance of the #VmaVulkanFunctions structure. +-# Call vmaImportVulkanFunctionsFromVolk(). + Parameter `pAllocatorCreateInfo` is read to find out which functions should be fetched for + appropriate Vulkan version and extensions. + Parameter `pDstVulkanFunctions` is filled with those function pointers, or null if not applicable. +-# Attach the #VmaVulkanFunctions structure to VmaAllocatorCreateInfo::pVulkanFunctions. +-# Call vmaCreateAllocator() to create the #VmaAllocator object. + +Example: + +\code +VmaAllocatorCreateInfo allocatorCreateInfo = {}; +allocatorCreateInfo.physicalDevice = myPhysicalDevice; +allocatorCreateInfo.device = myDevice; +allocatorCreateInfo.instance = myInstance; +allocatorCreateInfo.vulkanApiVersion = VK_API_VERSION_1_3; +allocatorCreateInfo.flags = VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT | + VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT | + VMA_ALLOCATOR_CREATE_KHR_EXTERNAL_MEMORY_WIN32_BIT; + +VmaVulkanFunctions vulkanFunctions; +VkResult res = vmaImportVulkanFunctionsFromVolk(&allocatorCreateInfo, &vulkanFunctions); +// Check res... +allocatorCreateInfo.pVulkanFunctions = &vulkanFunctions; + +VmaAllocator allocator; +res = vmaCreateAllocator(&allocatorCreateInfo, &allocator); +// Check res... +\endcode + +Internally in this function, pointers to functions related to the entire Vulkan instance are fetched using global function definitions, +while pointers to functions related to the Vulkan device are fetched using `volkLoadDeviceTable()` for given `pAllocatorCreateInfo->device`. + */ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaImportVulkanFunctionsFromVolk( + const VmaAllocatorCreateInfo* VMA_NOT_NULL pAllocatorCreateInfo, + VmaVulkanFunctions* VMA_NOT_NULL pDstVulkanFunctions); +#endif + +/// Creates #VmaAllocator object. +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateAllocator( + const VmaAllocatorCreateInfo* VMA_NOT_NULL pCreateInfo, + VmaAllocator VMA_NULLABLE* VMA_NOT_NULL pAllocator); + +/// Destroys allocator object. +VMA_CALL_PRE void VMA_CALL_POST vmaDestroyAllocator( + VmaAllocator VMA_NULLABLE allocator); + +/** \brief Returns information about existing #VmaAllocator object - handle to Vulkan device etc. + +It might be useful if you want to keep just the #VmaAllocator handle and fetch other required handles to +`VkPhysicalDevice`, `VkDevice` etc. every time using this function. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetAllocatorInfo( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocatorInfo* VMA_NOT_NULL pAllocatorInfo); + +/** +PhysicalDeviceProperties are fetched from physicalDevice by the allocator. +You can access it here, without fetching it again on your own. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetPhysicalDeviceProperties( + VmaAllocator VMA_NOT_NULL allocator, + const VkPhysicalDeviceProperties* VMA_NULLABLE* VMA_NOT_NULL ppPhysicalDeviceProperties); + +/** +PhysicalDeviceMemoryProperties are fetched from physicalDevice by the allocator. +You can access it here, without fetching it again on your own. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetMemoryProperties( + VmaAllocator VMA_NOT_NULL allocator, + const VkPhysicalDeviceMemoryProperties* VMA_NULLABLE* VMA_NOT_NULL ppPhysicalDeviceMemoryProperties); + +/** +\brief Given Memory Type Index, returns Property Flags of this memory type. + +This is just a convenience function. Same information can be obtained using +vmaGetMemoryProperties(). +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetMemoryTypeProperties( + VmaAllocator VMA_NOT_NULL allocator, + uint32_t memoryTypeIndex, + VkMemoryPropertyFlags* VMA_NOT_NULL pFlags); + +/** \brief Sets index of the current frame. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaSetCurrentFrameIndex( + VmaAllocator VMA_NOT_NULL allocator, + uint32_t frameIndex); + +/** @} */ + +/** +\addtogroup group_stats +@{ +*/ + +/** \brief Retrieves statistics from current state of the Allocator. + +This function is called "calculate" not "get" because it has to traverse all +internal data structures, so it may be quite slow. Use it for debugging purposes. +For faster but more brief statistics suitable to be called every frame or every allocation, +use vmaGetHeapBudgets(). + +Note that when using allocator from multiple threads, returned information may immediately +become outdated. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaCalculateStatistics( + VmaAllocator VMA_NOT_NULL allocator, + VmaTotalStatistics* VMA_NOT_NULL pStats); + +/** \brief Retrieves information about current memory usage and budget for all memory heaps. + +\param allocator +\param[out] pBudgets Must point to array with number of elements at least equal to number of memory heaps in physical device used. + +This function is called "get" not "calculate" because it is very fast, suitable to be called +every frame or every allocation. For more detailed statistics use vmaCalculateStatistics(). + +Note that when using allocator from multiple threads, returned information may immediately +become outdated. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetHeapBudgets( + VmaAllocator VMA_NOT_NULL allocator, + VmaBudget* VMA_NOT_NULL VMA_LEN_IF_NOT_NULL("VkPhysicalDeviceMemoryProperties::memoryHeapCount") pBudgets); + +/** @} */ + +/** +\addtogroup group_alloc +@{ +*/ + +/** +\brief Helps to find memoryTypeIndex, given memoryTypeBits and VmaAllocationCreateInfo. + +This algorithm tries to find a memory type that: + +- Is allowed by memoryTypeBits. +- Contains all the flags from pAllocationCreateInfo->requiredFlags. +- Matches intended usage. +- Has as many flags from pAllocationCreateInfo->preferredFlags as possible. + +\return Returns VK_ERROR_FEATURE_NOT_PRESENT if not found. Receiving such result +from this function or any other allocating function probably means that your +device doesn't support any memory type with requested features for the specific +type of resource you want to use it for. Please check parameters of your +resource, like image layout (OPTIMAL versus LINEAR) or mip level count. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaFindMemoryTypeIndex( + VmaAllocator VMA_NOT_NULL allocator, + uint32_t memoryTypeBits, + const VmaAllocationCreateInfo* VMA_NOT_NULL pAllocationCreateInfo, + uint32_t* VMA_NOT_NULL pMemoryTypeIndex); + +/** +\brief Helps to find memoryTypeIndex, given VkBufferCreateInfo and VmaAllocationCreateInfo. + +It can be useful e.g. to determine value to be used as VmaPoolCreateInfo::memoryTypeIndex. +It internally creates a temporary, dummy buffer that never has memory bound. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaFindMemoryTypeIndexForBufferInfo( + VmaAllocator VMA_NOT_NULL allocator, + const VkBufferCreateInfo* VMA_NOT_NULL pBufferCreateInfo, + const VmaAllocationCreateInfo* VMA_NOT_NULL pAllocationCreateInfo, + uint32_t* VMA_NOT_NULL pMemoryTypeIndex); + +/** +\brief Helps to find memoryTypeIndex, given VkImageCreateInfo and VmaAllocationCreateInfo. + +It can be useful e.g. to determine value to be used as VmaPoolCreateInfo::memoryTypeIndex. +It internally creates a temporary, dummy image that never has memory bound. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaFindMemoryTypeIndexForImageInfo( + VmaAllocator VMA_NOT_NULL allocator, + const VkImageCreateInfo* VMA_NOT_NULL pImageCreateInfo, + const VmaAllocationCreateInfo* VMA_NOT_NULL pAllocationCreateInfo, + uint32_t* VMA_NOT_NULL pMemoryTypeIndex); + +/** \brief Allocates Vulkan device memory and creates #VmaPool object. + +\param allocator Allocator object. +\param pCreateInfo Parameters of pool to create. +\param[out] pPool Handle to created pool. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreatePool( + VmaAllocator VMA_NOT_NULL allocator, + const VmaPoolCreateInfo* VMA_NOT_NULL pCreateInfo, + VmaPool VMA_NULLABLE* VMA_NOT_NULL pPool); + +/** \brief Destroys #VmaPool object and frees Vulkan device memory. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaDestroyPool( + VmaAllocator VMA_NOT_NULL allocator, + VmaPool VMA_NULLABLE pool); + +/** @} */ + +/** +\addtogroup group_stats +@{ +*/ + +/** \brief Retrieves statistics of existing #VmaPool object. + +\param allocator Allocator object. +\param pool Pool object. +\param[out] pPoolStats Statistics of specified pool. + +Note that when using the pool from multiple threads, returned information may immediately +become outdated. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetPoolStatistics( + VmaAllocator VMA_NOT_NULL allocator, + VmaPool VMA_NOT_NULL pool, + VmaStatistics* VMA_NOT_NULL pPoolStats); + +/** \brief Retrieves detailed statistics of existing #VmaPool object. + +\param allocator Allocator object. +\param pool Pool object. +\param[out] pPoolStats Statistics of specified pool. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaCalculatePoolStatistics( + VmaAllocator VMA_NOT_NULL allocator, + VmaPool VMA_NOT_NULL pool, + VmaDetailedStatistics* VMA_NOT_NULL pPoolStats); + +/** @} */ + +/** +\addtogroup group_alloc +@{ +*/ + +/** \brief Checks magic number in margins around all allocations in given memory pool in search for corruptions. + +Corruption detection is enabled only when `VMA_DEBUG_DETECT_CORRUPTION` macro is defined to nonzero, +`VMA_DEBUG_MARGIN` is defined to nonzero and the pool is created in memory type that is +`HOST_VISIBLE` and `HOST_COHERENT`. For more information, see [Corruption detection](@ref debugging_memory_usage_corruption_detection). + +Possible return values: + +- `VK_ERROR_FEATURE_NOT_PRESENT` - corruption detection is not enabled for specified pool. +- `VK_SUCCESS` - corruption detection has been performed and succeeded. +- `VK_ERROR_UNKNOWN` - corruption detection has been performed and found memory corruptions around one of the allocations. + `VMA_ASSERT` is also fired in that case. +- Other value: Error returned by Vulkan, e.g. memory mapping failure. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCheckPoolCorruption( + VmaAllocator VMA_NOT_NULL allocator, + VmaPool VMA_NOT_NULL pool); + +/** \brief Retrieves name of a custom pool. + +After the call `ppName` is either null or points to an internally-owned null-terminated string +containing name of the pool that was previously set. The pointer becomes invalid when the pool is +destroyed or its name is changed using vmaSetPoolName(). +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetPoolName( + VmaAllocator VMA_NOT_NULL allocator, + VmaPool VMA_NOT_NULL pool, + const char* VMA_NULLABLE* VMA_NOT_NULL ppName); + +/** \brief Sets name of a custom pool. + +`pName` can be either null or pointer to a null-terminated string with new name for the pool. +Function makes internal copy of the string, so it can be changed or freed immediately after this call. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaSetPoolName( + VmaAllocator VMA_NOT_NULL allocator, + VmaPool VMA_NOT_NULL pool, + const char* VMA_NULLABLE pName); + +/** \brief General purpose memory allocation. + +\param allocator +\param pVkMemoryRequirements +\param pCreateInfo +\param[out] pAllocation Handle to allocated memory. +\param[out] pAllocationInfo Optional. Information about allocated memory. It can be later fetched using function vmaGetAllocationInfo(). + +You should free the memory using vmaFreeMemory() or vmaFreeMemoryPages(). + +It is recommended to use vmaAllocateMemoryForBuffer(), vmaAllocateMemoryForImage(), +vmaCreateBuffer(), vmaCreateImage() instead whenever possible. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaAllocateMemory( + VmaAllocator VMA_NOT_NULL allocator, + const VkMemoryRequirements* VMA_NOT_NULL pVkMemoryRequirements, + const VmaAllocationCreateInfo* VMA_NOT_NULL pCreateInfo, + VmaAllocation VMA_NULLABLE* VMA_NOT_NULL pAllocation, + VmaAllocationInfo* VMA_NULLABLE pAllocationInfo); + +/** \brief General purpose memory allocation for multiple allocation objects at once. + +\param allocator Allocator object. +\param pVkMemoryRequirements Memory requirements for each allocation. +\param pCreateInfo Creation parameters for each allocation. +\param allocationCount Number of allocations to make. +\param[out] pAllocations Pointer to array that will be filled with handles to created allocations. +\param[out] pAllocationInfo Optional. Pointer to array that will be filled with parameters of created allocations. + +You should free the memory using vmaFreeMemory() or vmaFreeMemoryPages(). + +Word "pages" is just a suggestion to use this function to allocate pieces of memory needed for sparse binding. +It is just a general purpose allocation function able to make multiple allocations at once. +It may be internally optimized to be more efficient than calling vmaAllocateMemory() `allocationCount` times. + +All allocations are made using same parameters. All of them are created out of the same memory pool and type. +If any allocation fails, all allocations already made within this function call are also freed, so that when +returned result is not `VK_SUCCESS`, `pAllocation` array is always entirely filled with `VK_NULL_HANDLE`. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaAllocateMemoryPages( + VmaAllocator VMA_NOT_NULL allocator, + const VkMemoryRequirements* VMA_NOT_NULL VMA_LEN_IF_NOT_NULL(allocationCount) pVkMemoryRequirements, + const VmaAllocationCreateInfo* VMA_NOT_NULL VMA_LEN_IF_NOT_NULL(allocationCount) pCreateInfo, + size_t allocationCount, + VmaAllocation VMA_NULLABLE* VMA_NOT_NULL VMA_LEN_IF_NOT_NULL(allocationCount) pAllocations, + VmaAllocationInfo* VMA_NULLABLE VMA_LEN_IF_NOT_NULL(allocationCount) pAllocationInfo); + +/** \brief Allocates memory suitable for given `VkBuffer`. + +\param allocator +\param buffer +\param pCreateInfo +\param[out] pAllocation Handle to allocated memory. +\param[out] pAllocationInfo Optional. Information about allocated memory. It can be later fetched using function vmaGetAllocationInfo(). + +It only creates #VmaAllocation. To bind the memory to the buffer, use vmaBindBufferMemory(). + +This is a special-purpose function. In most cases you should use vmaCreateBuffer(). + +You must free the allocation using vmaFreeMemory() when no longer needed. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaAllocateMemoryForBuffer( + VmaAllocator VMA_NOT_NULL allocator, + VkBuffer VMA_NOT_NULL_NON_DISPATCHABLE buffer, + const VmaAllocationCreateInfo* VMA_NOT_NULL pCreateInfo, + VmaAllocation VMA_NULLABLE* VMA_NOT_NULL pAllocation, + VmaAllocationInfo* VMA_NULLABLE pAllocationInfo); + +/** \brief Allocates memory suitable for given `VkImage`. + +\param allocator +\param image +\param pCreateInfo +\param[out] pAllocation Handle to allocated memory. +\param[out] pAllocationInfo Optional. Information about allocated memory. It can be later fetched using function vmaGetAllocationInfo(). + +It only creates #VmaAllocation. To bind the memory to the buffer, use vmaBindImageMemory(). + +This is a special-purpose function. In most cases you should use vmaCreateImage(). + +You must free the allocation using vmaFreeMemory() when no longer needed. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaAllocateMemoryForImage( + VmaAllocator VMA_NOT_NULL allocator, + VkImage VMA_NOT_NULL_NON_DISPATCHABLE image, + const VmaAllocationCreateInfo* VMA_NOT_NULL pCreateInfo, + VmaAllocation VMA_NULLABLE* VMA_NOT_NULL pAllocation, + VmaAllocationInfo* VMA_NULLABLE pAllocationInfo); + +/** \brief Frees memory previously allocated using vmaAllocateMemory(), vmaAllocateMemoryForBuffer(), or vmaAllocateMemoryForImage(). + +Passing `VK_NULL_HANDLE` as `allocation` is valid. Such function call is just skipped. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaFreeMemory( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NULLABLE allocation); + +/** \brief Frees memory and destroys multiple allocations. + +Word "pages" is just a suggestion to use this function to free pieces of memory used for sparse binding. +It is just a general purpose function to free memory and destroy allocations made using e.g. vmaAllocateMemory(), +vmaAllocateMemoryPages() and other functions. +It may be internally optimized to be more efficient than calling vmaFreeMemory() `allocationCount` times. + +Allocations in `pAllocations` array can come from any memory pools and types. +Passing `VK_NULL_HANDLE` as elements of `pAllocations` array is valid. Such entries are just skipped. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaFreeMemoryPages( + VmaAllocator VMA_NOT_NULL allocator, + size_t allocationCount, + const VmaAllocation VMA_NULLABLE* VMA_NOT_NULL VMA_LEN_IF_NOT_NULL(allocationCount) pAllocations); + +/** \brief Returns current information about specified allocation. + +Current parameters of given allocation are returned in `pAllocationInfo`. + +Although this function doesn't lock any mutex, so it should be quite efficient, +you should avoid calling it too often. +You can retrieve same VmaAllocationInfo structure while creating your resource, from function +vmaCreateBuffer(), vmaCreateImage(). You can remember it if you are sure parameters don't change +(e.g. due to defragmentation). + +There is also a new function vmaGetAllocationInfo2() that offers extended information +about the allocation, returned using new structure #VmaAllocationInfo2. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetAllocationInfo( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VmaAllocationInfo* VMA_NOT_NULL pAllocationInfo); + +/** \brief Returns extended information about specified allocation. + +Current parameters of given allocation are returned in `pAllocationInfo`. +Extended parameters in structure #VmaAllocationInfo2 include memory block size +and a flag telling whether the allocation has dedicated memory. +It can be useful e.g. for interop with OpenGL. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetAllocationInfo2( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VmaAllocationInfo2* VMA_NOT_NULL pAllocationInfo); + +/** \brief Sets pUserData in given allocation to new value. + +The value of pointer `pUserData` is copied to allocation's `pUserData`. +It is opaque, so you can use it however you want - e.g. +as a pointer, ordinal number or some handle to you own data. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaSetAllocationUserData( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + void* VMA_NULLABLE pUserData); + +/** \brief Sets pName in given allocation to new value. + +`pName` must be either null, or pointer to a null-terminated string. The function +makes local copy of the string and sets it as allocation's `pName`. String +passed as pName doesn't need to be valid for whole lifetime of the allocation - +you can free it after this call. String previously pointed by allocation's +`pName` is freed from memory. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaSetAllocationName( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + const char* VMA_NULLABLE pName); + +/** +\brief Given an allocation, returns Property Flags of its memory type. + +This is just a convenience function. Same information can be obtained using +vmaGetAllocationInfo() + vmaGetMemoryProperties(). +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetAllocationMemoryProperties( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkMemoryPropertyFlags* VMA_NOT_NULL pFlags); + + +#if VMA_EXTERNAL_MEMORY_WIN32 +/** +\brief Given an allocation, returns Win32 handle that may be imported by other processes or APIs. + +\param hTargetProcess Must be a valid handle to target process or null. If it's null, the function returns + handle for the current process. +\param[out] pHandle Output parameter that returns the handle. + +The function fills `pHandle` with handle that can be used in target process. +The handle is fetched using function `vkGetMemoryWin32HandleKHR`. +When no longer needed, you must close it using: + +\code +CloseHandle(handle); +\endcode + +You can close it any time, before or after destroying the allocation object. +It is reference-counted internally by Windows. + +Note the handle is returned for the entire `VkDeviceMemory` block that the allocation belongs to. +If the allocation is sub-allocated from a larger block, you may need to consider the offset of the allocation +(VmaAllocationInfo::offset). + +If the function fails with `VK_ERROR_FEATURE_NOT_PRESENT` error code, please double-check +that VmaVulkanFunctions::vkGetMemoryWin32HandleKHR function pointer is set, e.g. either by using `VMA_DYNAMIC_VULKAN_FUNCTIONS` +or by manually passing it through VmaAllocatorCreateInfo::pVulkanFunctions. + +For more information, see chapter \ref vk_khr_external_memory_win32. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaGetMemoryWin32Handle(VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, HANDLE hTargetProcess, HANDLE* VMA_NOT_NULL pHandle); +#endif // VMA_EXTERNAL_MEMORY_WIN32 + +/** \brief Maps memory represented by given allocation and returns pointer to it. + +Maps memory represented by given allocation to make it accessible to CPU code. +When succeeded, `*ppData` contains pointer to first byte of this memory. + +\warning +If the allocation is part of a bigger `VkDeviceMemory` block, returned pointer is +correctly offsetted to the beginning of region assigned to this particular allocation. +Unlike the result of `vkMapMemory`, it points to the allocation, not to the beginning of the whole block. +You should not add VmaAllocationInfo::offset to it! + +Mapping is internally reference-counted and synchronized, so despite raw Vulkan +function `vkMapMemory()` cannot be used to map same block of `VkDeviceMemory` +multiple times simultaneously, it is safe to call this function on allocations +assigned to the same memory block. Actual Vulkan memory will be mapped on first +mapping and unmapped on last unmapping. + +If the function succeeded, you must call vmaUnmapMemory() to unmap the +allocation when mapping is no longer needed or before freeing the allocation, at +the latest. + +It also safe to call this function multiple times on the same allocation. You +must call vmaUnmapMemory() same number of times as you called vmaMapMemory(). + +It is also safe to call this function on allocation created with +#VMA_ALLOCATION_CREATE_MAPPED_BIT flag. Its memory stays mapped all the time. +You must still call vmaUnmapMemory() same number of times as you called +vmaMapMemory(). You must not call vmaUnmapMemory() additional time to free the +"0-th" mapping made automatically due to #VMA_ALLOCATION_CREATE_MAPPED_BIT flag. + +This function fails when used on allocation made in memory type that is not +`HOST_VISIBLE`. + +This function doesn't automatically flush or invalidate caches. +If the allocation is made from a memory types that is not `HOST_COHERENT`, +you also need to use vmaInvalidateAllocation() / vmaFlushAllocation(), as required by Vulkan specification. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaMapMemory( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + void* VMA_NULLABLE* VMA_NOT_NULL ppData); + +/** \brief Unmaps memory represented by given allocation, mapped previously using vmaMapMemory(). + +For details, see description of vmaMapMemory(). + +This function doesn't automatically flush or invalidate caches. +If the allocation is made from a memory types that is not `HOST_COHERENT`, +you also need to use vmaInvalidateAllocation() / vmaFlushAllocation(), as required by Vulkan specification. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaUnmapMemory( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation); + +/** \brief Flushes memory of given allocation. + +Calls `vkFlushMappedMemoryRanges()` for memory associated with given range of given allocation. +It needs to be called after writing to a mapped memory for memory types that are not `HOST_COHERENT`. +Unmap operation doesn't do that automatically. + +- `offset` must be relative to the beginning of allocation. +- `size` can be `VK_WHOLE_SIZE`. It means all memory from `offset` the the end of given allocation. +- `offset` and `size` don't have to be aligned. + They are internally rounded down/up to multiply of `nonCoherentAtomSize`. +- If `size` is 0, this call is ignored. +- If memory type that the `allocation` belongs to is not `HOST_VISIBLE` or it is `HOST_COHERENT`, + this call is ignored. + +Warning! `offset` and `size` are relative to the contents of given `allocation`. +If you mean whole allocation, you can pass 0 and `VK_WHOLE_SIZE`, respectively. +Do not pass allocation's offset as `offset`!!! + +This function returns the `VkResult` from `vkFlushMappedMemoryRanges` if it is +called, otherwise `VK_SUCCESS`. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaFlushAllocation( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkDeviceSize offset, + VkDeviceSize size); + +/** \brief Invalidates memory of given allocation. + +Calls `vkInvalidateMappedMemoryRanges()` for memory associated with given range of given allocation. +It needs to be called before reading from a mapped memory for memory types that are not `HOST_COHERENT`. +Map operation doesn't do that automatically. + +- `offset` must be relative to the beginning of allocation. +- `size` can be `VK_WHOLE_SIZE`. It means all memory from `offset` the the end of given allocation. +- `offset` and `size` don't have to be aligned. + They are internally rounded down/up to multiply of `nonCoherentAtomSize`. +- If `size` is 0, this call is ignored. +- If memory type that the `allocation` belongs to is not `HOST_VISIBLE` or it is `HOST_COHERENT`, + this call is ignored. + +Warning! `offset` and `size` are relative to the contents of given `allocation`. +If you mean whole allocation, you can pass 0 and `VK_WHOLE_SIZE`, respectively. +Do not pass allocation's offset as `offset`!!! + +This function returns the `VkResult` from `vkInvalidateMappedMemoryRanges` if +it is called, otherwise `VK_SUCCESS`. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaInvalidateAllocation( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkDeviceSize offset, + VkDeviceSize size); + +/** \brief Flushes memory of given set of allocations. + +Calls `vkFlushMappedMemoryRanges()` for memory associated with given ranges of given allocations. +For more information, see documentation of vmaFlushAllocation(). + +\param allocator +\param allocationCount +\param allocations +\param offsets If not null, it must point to an array of offsets of regions to flush, relative to the beginning of respective allocations. Null means all offsets are zero. +\param sizes If not null, it must point to an array of sizes of regions to flush in respective allocations. Null means `VK_WHOLE_SIZE` for all allocations. + +This function returns the `VkResult` from `vkFlushMappedMemoryRanges` if it is +called, otherwise `VK_SUCCESS`. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaFlushAllocations( + VmaAllocator VMA_NOT_NULL allocator, + uint32_t allocationCount, + const VmaAllocation VMA_NOT_NULL* VMA_NULLABLE VMA_LEN_IF_NOT_NULL(allocationCount) allocations, + const VkDeviceSize* VMA_NULLABLE VMA_LEN_IF_NOT_NULL(allocationCount) offsets, + const VkDeviceSize* VMA_NULLABLE VMA_LEN_IF_NOT_NULL(allocationCount) sizes); + +/** \brief Invalidates memory of given set of allocations. + +Calls `vkInvalidateMappedMemoryRanges()` for memory associated with given ranges of given allocations. +For more information, see documentation of vmaInvalidateAllocation(). + +\param allocator +\param allocationCount +\param allocations +\param offsets If not null, it must point to an array of offsets of regions to flush, relative to the beginning of respective allocations. Null means all offsets are zero. +\param sizes If not null, it must point to an array of sizes of regions to flush in respective allocations. Null means `VK_WHOLE_SIZE` for all allocations. + +This function returns the `VkResult` from `vkInvalidateMappedMemoryRanges` if it is +called, otherwise `VK_SUCCESS`. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaInvalidateAllocations( + VmaAllocator VMA_NOT_NULL allocator, + uint32_t allocationCount, + const VmaAllocation VMA_NOT_NULL* VMA_NULLABLE VMA_LEN_IF_NOT_NULL(allocationCount) allocations, + const VkDeviceSize* VMA_NULLABLE VMA_LEN_IF_NOT_NULL(allocationCount) offsets, + const VkDeviceSize* VMA_NULLABLE VMA_LEN_IF_NOT_NULL(allocationCount) sizes); + +/** \brief Maps the allocation temporarily if needed, copies data from specified host pointer to it, and flushes the memory from the host caches if needed. + +\param allocator +\param pSrcHostPointer Pointer to the host data that become source of the copy. +\param dstAllocation Handle to the allocation that becomes destination of the copy. +\param dstAllocationLocalOffset Offset within `dstAllocation` where to write copied data, in bytes. +\param size Number of bytes to copy. + +This is a convenience function that allows to copy data from a host pointer to an allocation easily. +Same behavior can be achieved by calling vmaMapMemory(), `memcpy()`, vmaUnmapMemory(), vmaFlushAllocation(). + +This function can be called only for allocations created in a memory type that has `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` flag. +It can be ensured e.g. by using #VMA_MEMORY_USAGE_AUTO and #VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT or +#VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT. +Otherwise, the function will fail and generate a Validation Layers error. + +`dstAllocationLocalOffset` is relative to the contents of given `dstAllocation`. +If you mean whole allocation, you should pass 0. +Do not pass allocation's offset within device memory block this parameter! +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCopyMemoryToAllocation( + VmaAllocator VMA_NOT_NULL allocator, + const void* VMA_NOT_NULL VMA_LEN_IF_NOT_NULL(size) pSrcHostPointer, + VmaAllocation VMA_NOT_NULL dstAllocation, + VkDeviceSize dstAllocationLocalOffset, + VkDeviceSize size); + +/** \brief Invalidates memory in the host caches if needed, maps the allocation temporarily if needed, and copies data from it to a specified host pointer. + +\param allocator +\param srcAllocation Handle to the allocation that becomes source of the copy. +\param srcAllocationLocalOffset Offset within `srcAllocation` where to read copied data, in bytes. +\param pDstHostPointer Pointer to the host memory that become destination of the copy. +\param size Number of bytes to copy. + +This is a convenience function that allows to copy data from an allocation to a host pointer easily. +Same behavior can be achieved by calling vmaInvalidateAllocation(), vmaMapMemory(), `memcpy()`, vmaUnmapMemory(). + +This function should be called only for allocations created in a memory type that has `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` +and `VK_MEMORY_PROPERTY_HOST_CACHED_BIT` flag. +It can be ensured e.g. by using #VMA_MEMORY_USAGE_AUTO and #VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT. +Otherwise, the function may fail and generate a Validation Layers error. +It may also work very slowly when reading from an uncached memory. + +`srcAllocationLocalOffset` is relative to the contents of given `srcAllocation`. +If you mean whole allocation, you should pass 0. +Do not pass allocation's offset within device memory block as this parameter! +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCopyAllocationToMemory( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL srcAllocation, + VkDeviceSize srcAllocationLocalOffset, + void* VMA_NOT_NULL VMA_LEN_IF_NOT_NULL(size) pDstHostPointer, + VkDeviceSize size); + +/** \brief Checks magic number in margins around all allocations in given memory types (in both default and custom pools) in search for corruptions. + +\param allocator +\param memoryTypeBits Bit mask, where each bit set means that a memory type with that index should be checked. + +Corruption detection is enabled only when `VMA_DEBUG_DETECT_CORRUPTION` macro is defined to nonzero, +`VMA_DEBUG_MARGIN` is defined to nonzero and only for memory types that are +`HOST_VISIBLE` and `HOST_COHERENT`. For more information, see [Corruption detection](@ref debugging_memory_usage_corruption_detection). + +Possible return values: + +- `VK_ERROR_FEATURE_NOT_PRESENT` - corruption detection is not enabled for any of specified memory types. +- `VK_SUCCESS` - corruption detection has been performed and succeeded. +- `VK_ERROR_UNKNOWN` - corruption detection has been performed and found memory corruptions around one of the allocations. + `VMA_ASSERT` is also fired in that case. +- Other value: Error returned by Vulkan, e.g. memory mapping failure. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCheckCorruption( + VmaAllocator VMA_NOT_NULL allocator, + uint32_t memoryTypeBits); + +/** \brief Begins defragmentation process. + +\param allocator Allocator object. +\param pInfo Structure filled with parameters of defragmentation. +\param[out] pContext Context object that must be passed to vmaEndDefragmentation() to finish defragmentation. +\returns +- `VK_SUCCESS` if defragmentation can begin. +- `VK_ERROR_FEATURE_NOT_PRESENT` if defragmentation is not supported. + +For more information about defragmentation, see documentation chapter: +[Defragmentation](@ref defragmentation). +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBeginDefragmentation( + VmaAllocator VMA_NOT_NULL allocator, + const VmaDefragmentationInfo* VMA_NOT_NULL pInfo, + VmaDefragmentationContext VMA_NULLABLE* VMA_NOT_NULL pContext); + +/** \brief Ends defragmentation process. + +\param allocator Allocator object. +\param context Context object that has been created by vmaBeginDefragmentation(). +\param[out] pStats Optional stats for the defragmentation. Can be null. + +Use this function to finish defragmentation started by vmaBeginDefragmentation(). +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaEndDefragmentation( + VmaAllocator VMA_NOT_NULL allocator, + VmaDefragmentationContext VMA_NOT_NULL context, + VmaDefragmentationStats* VMA_NULLABLE pStats); + +/** \brief Starts single defragmentation pass. + +\param allocator Allocator object. +\param context Context object that has been created by vmaBeginDefragmentation(). +\param[out] pPassInfo Computed information for current pass. +\returns +- `VK_SUCCESS` if no more moves are possible. Then you can omit call to vmaEndDefragmentationPass() and simply end whole defragmentation. +- `VK_INCOMPLETE` if there are pending moves returned in `pPassInfo`. You need to perform them, call vmaEndDefragmentationPass(), + and then preferably try another pass with vmaBeginDefragmentationPass(). +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBeginDefragmentationPass( + VmaAllocator VMA_NOT_NULL allocator, + VmaDefragmentationContext VMA_NOT_NULL context, + VmaDefragmentationPassMoveInfo* VMA_NOT_NULL pPassInfo); + +/** \brief Ends single defragmentation pass. + +\param allocator Allocator object. +\param context Context object that has been created by vmaBeginDefragmentation(). +\param pPassInfo Computed information for current pass filled by vmaBeginDefragmentationPass() and possibly modified by you. + +Returns `VK_SUCCESS` if no more moves are possible or `VK_INCOMPLETE` if more defragmentations are possible. + +Ends incremental defragmentation pass and commits all defragmentation moves from `pPassInfo`. +After this call: + +- Allocations at `pPassInfo[i].srcAllocation` that had `pPassInfo[i].operation ==` #VMA_DEFRAGMENTATION_MOVE_OPERATION_COPY + (which is the default) will be pointing to the new destination place. +- Allocation at `pPassInfo[i].srcAllocation` that had `pPassInfo[i].operation ==` #VMA_DEFRAGMENTATION_MOVE_OPERATION_DESTROY + will be freed. + +If no more moves are possible you can end whole defragmentation. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaEndDefragmentationPass( + VmaAllocator VMA_NOT_NULL allocator, + VmaDefragmentationContext VMA_NOT_NULL context, + VmaDefragmentationPassMoveInfo* VMA_NOT_NULL pPassInfo); + +/** \brief Binds buffer to allocation. + +Binds specified buffer to region of memory represented by specified allocation. +Gets `VkDeviceMemory` handle and offset from the allocation. +If you want to create a buffer, allocate memory for it and bind them together separately, +you should use this function for binding instead of standard `vkBindBufferMemory()`, +because it ensures proper synchronization so that when a `VkDeviceMemory` object is used by multiple +allocations, calls to `vkBind*Memory()` or `vkMapMemory()` won't happen from multiple threads simultaneously +(which is illegal in Vulkan). + +It is recommended to use function vmaCreateBuffer() instead of this one. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBindBufferMemory( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkBuffer VMA_NOT_NULL_NON_DISPATCHABLE buffer); + +/** \brief Binds buffer to allocation with additional parameters. + +\param allocator +\param allocation +\param allocationLocalOffset Additional offset to be added while binding, relative to the beginning of the `allocation`. Normally it should be 0. +\param buffer +\param pNext A chain of structures to be attached to `VkBindBufferMemoryInfoKHR` structure used internally. Normally it should be null. + +This function is similar to vmaBindBufferMemory(), but it provides additional parameters. + +If `pNext` is not null, #VmaAllocator object must have been created with #VMA_ALLOCATOR_CREATE_KHR_BIND_MEMORY2_BIT flag +or with VmaAllocatorCreateInfo::vulkanApiVersion `>= VK_API_VERSION_1_1`. Otherwise the call fails. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBindBufferMemory2( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkDeviceSize allocationLocalOffset, + VkBuffer VMA_NOT_NULL_NON_DISPATCHABLE buffer, + const void* VMA_NULLABLE VMA_EXTENDS_VK_STRUCT(VkBindBufferMemoryInfoKHR) pNext); + +/** \brief Binds image to allocation. + +Binds specified image to region of memory represented by specified allocation. +Gets `VkDeviceMemory` handle and offset from the allocation. +If you want to create an image, allocate memory for it and bind them together separately, +you should use this function for binding instead of standard `vkBindImageMemory()`, +because it ensures proper synchronization so that when a `VkDeviceMemory` object is used by multiple +allocations, calls to `vkBind*Memory()` or `vkMapMemory()` won't happen from multiple threads simultaneously +(which is illegal in Vulkan). + +It is recommended to use function vmaCreateImage() instead of this one. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBindImageMemory( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkImage VMA_NOT_NULL_NON_DISPATCHABLE image); + +/** \brief Binds image to allocation with additional parameters. + +\param allocator +\param allocation +\param allocationLocalOffset Additional offset to be added while binding, relative to the beginning of the `allocation`. Normally it should be 0. +\param image +\param pNext A chain of structures to be attached to `VkBindImageMemoryInfoKHR` structure used internally. Normally it should be null. + +This function is similar to vmaBindImageMemory(), but it provides additional parameters. + +If `pNext` is not null, #VmaAllocator object must have been created with #VMA_ALLOCATOR_CREATE_KHR_BIND_MEMORY2_BIT flag +or with VmaAllocatorCreateInfo::vulkanApiVersion `>= VK_API_VERSION_1_1`. Otherwise the call fails. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBindImageMemory2( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkDeviceSize allocationLocalOffset, + VkImage VMA_NOT_NULL_NON_DISPATCHABLE image, + const void* VMA_NULLABLE VMA_EXTENDS_VK_STRUCT(VkBindImageMemoryInfoKHR) pNext); + +/** \brief Creates a new `VkBuffer`, allocates and binds memory for it. + +\param allocator +\param pBufferCreateInfo +\param pAllocationCreateInfo +\param[out] pBuffer Buffer that was created. +\param[out] pAllocation Allocation that was created. +\param[out] pAllocationInfo Optional. Information about allocated memory. It can be later fetched using function vmaGetAllocationInfo(). + +This function automatically: + +-# Creates buffer. +-# Allocates appropriate memory for it. +-# Binds the buffer with the memory. + +If any of these operations fail, buffer and allocation are not created, +returned value is negative error code, `*pBuffer` and `*pAllocation` are null. + +If the function succeeded, you must destroy both buffer and allocation when you +no longer need them using either convenience function vmaDestroyBuffer() or +separately, using `vkDestroyBuffer()` and vmaFreeMemory(). + +If #VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT flag was used, +VK_KHR_dedicated_allocation extension is used internally to query driver whether +it requires or prefers the new buffer to have dedicated allocation. If yes, +and if dedicated allocation is possible +(#VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT is not used), it creates dedicated +allocation for this buffer, just like when using +#VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT. + +\note This function creates a new `VkBuffer`. Sub-allocation of parts of one large buffer, +although recommended as a good practice, is out of scope of this library and could be implemented +by the user as a higher-level logic on top of VMA. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateBuffer( + VmaAllocator VMA_NOT_NULL allocator, + const VkBufferCreateInfo* VMA_NOT_NULL pBufferCreateInfo, + const VmaAllocationCreateInfo* VMA_NOT_NULL pAllocationCreateInfo, + VkBuffer VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pBuffer, + VmaAllocation VMA_NULLABLE* VMA_NOT_NULL pAllocation, + VmaAllocationInfo* VMA_NULLABLE pAllocationInfo); + +/** \brief Creates a buffer with additional minimum alignment. + +Similar to vmaCreateBuffer() but provides additional parameter `minAlignment` which allows to specify custom, +minimum alignment to be used when placing the buffer inside a larger memory block, which may be needed e.g. +for interop with OpenGL. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateBufferWithAlignment( + VmaAllocator VMA_NOT_NULL allocator, + const VkBufferCreateInfo* VMA_NOT_NULL pBufferCreateInfo, + const VmaAllocationCreateInfo* VMA_NOT_NULL pAllocationCreateInfo, + VkDeviceSize minAlignment, + VkBuffer VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pBuffer, + VmaAllocation VMA_NULLABLE* VMA_NOT_NULL pAllocation, + VmaAllocationInfo* VMA_NULLABLE pAllocationInfo); + +/** \brief Creates a new `VkBuffer`, binds already created memory for it. + +\param allocator +\param allocation Allocation that provides memory to be used for binding new buffer to it. +\param pBufferCreateInfo +\param[out] pBuffer Buffer that was created. + +This function automatically: + +-# Creates buffer. +-# Binds the buffer with the supplied memory. + +If any of these operations fail, buffer is not created, +returned value is negative error code and `*pBuffer` is null. + +If the function succeeded, you must destroy the buffer when you +no longer need it using `vkDestroyBuffer()`. If you want to also destroy the corresponding +allocation you can use convenience function vmaDestroyBuffer(). + +\note There is a new version of this function augmented with parameter `allocationLocalOffset` - see vmaCreateAliasingBuffer2(). +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateAliasingBuffer( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + const VkBufferCreateInfo* VMA_NOT_NULL pBufferCreateInfo, + VkBuffer VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pBuffer); + +/** \brief Creates a new `VkBuffer`, binds already created memory for it. + +\param allocator +\param allocation Allocation that provides memory to be used for binding new buffer to it. +\param allocationLocalOffset Additional offset to be added while binding, relative to the beginning of the allocation. Normally it should be 0. +\param pBufferCreateInfo +\param[out] pBuffer Buffer that was created. + +This function automatically: + +-# Creates buffer. +-# Binds the buffer with the supplied memory. + +If any of these operations fail, buffer is not created, +returned value is negative error code and `*pBuffer` is null. + +If the function succeeded, you must destroy the buffer when you +no longer need it using `vkDestroyBuffer()`. If you want to also destroy the corresponding +allocation you can use convenience function vmaDestroyBuffer(). + +\note This is a new version of the function augmented with parameter `allocationLocalOffset`. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateAliasingBuffer2( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkDeviceSize allocationLocalOffset, + const VkBufferCreateInfo* VMA_NOT_NULL pBufferCreateInfo, + VkBuffer VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pBuffer); + +/** \brief Destroys Vulkan buffer and frees allocated memory. + +This is just a convenience function equivalent to: + +\code +vkDestroyBuffer(device, buffer, allocationCallbacks); +vmaFreeMemory(allocator, allocation); +\endcode + +It is safe to pass null as buffer and/or allocation. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaDestroyBuffer( + VmaAllocator VMA_NOT_NULL allocator, + VkBuffer VMA_NULLABLE_NON_DISPATCHABLE buffer, + VmaAllocation VMA_NULLABLE allocation); + +/// Function similar to vmaCreateBuffer(). +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateImage( + VmaAllocator VMA_NOT_NULL allocator, + const VkImageCreateInfo* VMA_NOT_NULL pImageCreateInfo, + const VmaAllocationCreateInfo* VMA_NOT_NULL pAllocationCreateInfo, + VkImage VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pImage, + VmaAllocation VMA_NULLABLE* VMA_NOT_NULL pAllocation, + VmaAllocationInfo* VMA_NULLABLE pAllocationInfo); + +/// Function similar to vmaCreateAliasingBuffer() but for images. +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateAliasingImage( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + const VkImageCreateInfo* VMA_NOT_NULL pImageCreateInfo, + VkImage VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pImage); + +/// Function similar to vmaCreateAliasingBuffer2() but for images. +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateAliasingImage2( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkDeviceSize allocationLocalOffset, + const VkImageCreateInfo* VMA_NOT_NULL pImageCreateInfo, + VkImage VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pImage); + +/** \brief Destroys Vulkan image and frees allocated memory. + +This is just a convenience function equivalent to: + +\code +vkDestroyImage(device, image, allocationCallbacks); +vmaFreeMemory(allocator, allocation); +\endcode + +It is safe to pass null as image and/or allocation. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaDestroyImage( + VmaAllocator VMA_NOT_NULL allocator, + VkImage VMA_NULLABLE_NON_DISPATCHABLE image, + VmaAllocation VMA_NULLABLE allocation); + +/** @} */ + +/** +\addtogroup group_virtual +@{ +*/ + +/** \brief Creates new #VmaVirtualBlock object. + +\param pCreateInfo Parameters for creation. +\param[out] pVirtualBlock Returned virtual block object or `VMA_NULL` if creation failed. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateVirtualBlock( + const VmaVirtualBlockCreateInfo* VMA_NOT_NULL pCreateInfo, + VmaVirtualBlock VMA_NULLABLE* VMA_NOT_NULL pVirtualBlock); + +/** \brief Destroys #VmaVirtualBlock object. + +Please note that you should consciously handle virtual allocations that could remain unfreed in the block. +You should either free them individually using vmaVirtualFree() or call vmaClearVirtualBlock() +if you are sure this is what you want. If you do neither, an assert is called. + +If you keep pointers to some additional metadata associated with your virtual allocations in their `pUserData`, +don't forget to free them. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaDestroyVirtualBlock( + VmaVirtualBlock VMA_NULLABLE virtualBlock); + +/** \brief Returns true of the #VmaVirtualBlock is empty - contains 0 virtual allocations and has all its space available for new allocations. +*/ +VMA_CALL_PRE VkBool32 VMA_CALL_POST vmaIsVirtualBlockEmpty( + VmaVirtualBlock VMA_NOT_NULL virtualBlock); + +/** \brief Returns information about a specific virtual allocation within a virtual block, like its size and `pUserData` pointer. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetVirtualAllocationInfo( + VmaVirtualBlock VMA_NOT_NULL virtualBlock, + VmaVirtualAllocation VMA_NOT_NULL_NON_DISPATCHABLE allocation, VmaVirtualAllocationInfo* VMA_NOT_NULL pVirtualAllocInfo); + +/** \brief Allocates new virtual allocation inside given #VmaVirtualBlock. + +If the allocation fails due to not enough free space available, `VK_ERROR_OUT_OF_DEVICE_MEMORY` is returned +(despite the function doesn't ever allocate actual GPU memory). +`pAllocation` is then set to `VK_NULL_HANDLE` and `pOffset`, if not null, it set to `UINT64_MAX`. + +\param virtualBlock Virtual block +\param pCreateInfo Parameters for the allocation +\param[out] pAllocation Returned handle of the new allocation +\param[out] pOffset Returned offset of the new allocation. Optional, can be null. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaVirtualAllocate( + VmaVirtualBlock VMA_NOT_NULL virtualBlock, + const VmaVirtualAllocationCreateInfo* VMA_NOT_NULL pCreateInfo, + VmaVirtualAllocation VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pAllocation, + VkDeviceSize* VMA_NULLABLE pOffset); + +/** \brief Frees virtual allocation inside given #VmaVirtualBlock. + +It is correct to call this function with `allocation == VK_NULL_HANDLE` - it does nothing. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaVirtualFree( + VmaVirtualBlock VMA_NOT_NULL virtualBlock, + VmaVirtualAllocation VMA_NULLABLE_NON_DISPATCHABLE allocation); + +/** \brief Frees all virtual allocations inside given #VmaVirtualBlock. + +You must either call this function or free each virtual allocation individually with vmaVirtualFree() +before destroying a virtual block. Otherwise, an assert is called. + +If you keep pointer to some additional metadata associated with your virtual allocation in its `pUserData`, +don't forget to free it as well. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaClearVirtualBlock( + VmaVirtualBlock VMA_NOT_NULL virtualBlock); + +/** \brief Changes custom pointer associated with given virtual allocation. +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaSetVirtualAllocationUserData( + VmaVirtualBlock VMA_NOT_NULL virtualBlock, + VmaVirtualAllocation VMA_NOT_NULL_NON_DISPATCHABLE allocation, + void* VMA_NULLABLE pUserData); + +/** \brief Calculates and returns statistics about virtual allocations and memory usage in given #VmaVirtualBlock. + +This function is fast to call. For more detailed statistics, see vmaCalculateVirtualBlockStatistics(). +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaGetVirtualBlockStatistics( + VmaVirtualBlock VMA_NOT_NULL virtualBlock, + VmaStatistics* VMA_NOT_NULL pStats); + +/** \brief Calculates and returns detailed statistics about virtual allocations and memory usage in given #VmaVirtualBlock. + +This function is slow to call. Use for debugging purposes. +For less detailed statistics, see vmaGetVirtualBlockStatistics(). +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaCalculateVirtualBlockStatistics( + VmaVirtualBlock VMA_NOT_NULL virtualBlock, + VmaDetailedStatistics* VMA_NOT_NULL pStats); + +/** @} */ + +#if VMA_STATS_STRING_ENABLED +/** +\addtogroup group_stats +@{ +*/ + +/** \brief Builds and returns a null-terminated string in JSON format with information about given #VmaVirtualBlock. +\param virtualBlock Virtual block. +\param[out] ppStatsString Returned string. +\param detailedMap Pass `VK_FALSE` to only obtain statistics as returned by vmaCalculateVirtualBlockStatistics(). Pass `VK_TRUE` to also obtain full list of allocations and free spaces. + +Returned string must be freed using vmaFreeVirtualBlockStatsString(). +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaBuildVirtualBlockStatsString( + VmaVirtualBlock VMA_NOT_NULL virtualBlock, + char* VMA_NULLABLE* VMA_NOT_NULL ppStatsString, + VkBool32 detailedMap); + +/// Frees a string returned by vmaBuildVirtualBlockStatsString(). +VMA_CALL_PRE void VMA_CALL_POST vmaFreeVirtualBlockStatsString( + VmaVirtualBlock VMA_NOT_NULL virtualBlock, + char* VMA_NULLABLE pStatsString); + +/** \brief Builds and returns statistics as a null-terminated string in JSON format. +\param allocator +\param[out] ppStatsString Must be freed using vmaFreeStatsString() function. +\param detailedMap +*/ +VMA_CALL_PRE void VMA_CALL_POST vmaBuildStatsString( + VmaAllocator VMA_NOT_NULL allocator, + char* VMA_NULLABLE* VMA_NOT_NULL ppStatsString, + VkBool32 detailedMap); + +VMA_CALL_PRE void VMA_CALL_POST vmaFreeStatsString( + VmaAllocator VMA_NOT_NULL allocator, + char* VMA_NULLABLE pStatsString); + +/** @} */ + +#endif // VMA_STATS_STRING_ENABLED + +#endif // _VMA_FUNCTION_HEADERS + +#ifdef __cplusplus +} +#endif + +#endif // AMD_VULKAN_MEMORY_ALLOCATOR_H + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// +// IMPLEMENTATION +// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// For Visual Studio IntelliSense. +#if defined(__cplusplus) && defined(__INTELLISENSE__) +#define VMA_IMPLEMENTATION +#endif + +#ifdef VMA_IMPLEMENTATION +#undef VMA_IMPLEMENTATION + +#include +#include +#include +#include +#include +#include + +#if !defined(VMA_CPP20) + #if __cplusplus >= 202002L || _MSVC_LANG >= 202002L // C++20 + #define VMA_CPP20 1 + #else + #define VMA_CPP20 0 + #endif +#endif + +#ifdef _MSC_VER + #include // For functions like __popcnt, _BitScanForward etc. +#endif +#if VMA_CPP20 + #include +#endif + +#if VMA_STATS_STRING_ENABLED + #include // For snprintf +#endif + +/******************************************************************************* +CONFIGURATION SECTION + +Define some of these macros before each #include of this header or change them +here if you need other then default behavior depending on your environment. +*/ +#ifndef _VMA_CONFIGURATION + +/* +Define this macro to 1 to make the library fetch pointers to Vulkan functions +internally, like: + + vulkanFunctions.vkAllocateMemory = &vkAllocateMemory; +*/ +#if !defined(VMA_STATIC_VULKAN_FUNCTIONS) && !defined(VK_NO_PROTOTYPES) + #define VMA_STATIC_VULKAN_FUNCTIONS 1 +#endif + +/* +Define this macro to 1 to make the library fetch pointers to Vulkan functions +internally, like: + + vulkanFunctions.vkAllocateMemory = (PFN_vkAllocateMemory)vkGetDeviceProcAddr(device, "vkAllocateMemory"); + +To use this feature in new versions of VMA you now have to pass +VmaVulkanFunctions::vkGetInstanceProcAddr and vkGetDeviceProcAddr as +VmaAllocatorCreateInfo::pVulkanFunctions. Other members can be null. +*/ +#if !defined(VMA_DYNAMIC_VULKAN_FUNCTIONS) + #define VMA_DYNAMIC_VULKAN_FUNCTIONS 1 +#endif + +#ifndef VMA_USE_STL_SHARED_MUTEX + #if __cplusplus >= 201703L || _MSVC_LANG >= 201703L // C++17 + #define VMA_USE_STL_SHARED_MUTEX 1 + // Visual studio defines __cplusplus properly only when passed additional parameter: /Zc:__cplusplus + // Otherwise it is always 199711L, despite shared_mutex works since Visual Studio 2015 Update 2. + #elif defined(_MSC_FULL_VER) && _MSC_FULL_VER >= 190023918 && __cplusplus == 199711L && _MSVC_LANG >= 201703L + #define VMA_USE_STL_SHARED_MUTEX 1 + #else + #define VMA_USE_STL_SHARED_MUTEX 0 + #endif +#endif + +/* +Define this macro to include custom header files without having to edit this file directly, e.g.: + + // Inside of "my_vma_configuration_user_includes.h": + + #include "my_custom_assert.h" // for MY_CUSTOM_ASSERT + #include "my_custom_min.h" // for my_custom_min + #include + #include + + // Inside a different file, which includes "vk_mem_alloc.h": + + #define VMA_CONFIGURATION_USER_INCLUDES_H "my_vma_configuration_user_includes.h" + #define VMA_ASSERT(expr) MY_CUSTOM_ASSERT(expr) + #define VMA_MIN(v1, v2) (my_custom_min(v1, v2)) + #include "vk_mem_alloc.h" + ... + +The following headers are used in this CONFIGURATION section only, so feel free to +remove them if not needed. +*/ +#if !defined(VMA_CONFIGURATION_USER_INCLUDES_H) + #include // for assert + #include // for min, max, swap + #include +#else + #include VMA_CONFIGURATION_USER_INCLUDES_H +#endif + +#ifndef VMA_NULL + // Value used as null pointer. Define it to e.g.: nullptr, NULL, 0, (void*)0. + #define VMA_NULL nullptr +#endif + +#ifndef VMA_FALLTHROUGH + #if __cplusplus >= 201703L || _MSVC_LANG >= 201703L // C++17 + #define VMA_FALLTHROUGH [[fallthrough]] + #else + #define VMA_FALLTHROUGH + #endif +#endif + +// Normal assert to check for programmer's errors, especially in Debug configuration. +#ifndef VMA_ASSERT + #ifdef NDEBUG + #define VMA_ASSERT(expr) + #else + #define VMA_ASSERT(expr) assert(expr) + #endif +#endif + +// Assert that will be called very often, like inside data structures e.g. operator[]. +// Making it non-empty can make program slow. +#ifndef VMA_HEAVY_ASSERT + #ifdef NDEBUG + #define VMA_HEAVY_ASSERT(expr) + #else + #define VMA_HEAVY_ASSERT(expr) //VMA_ASSERT(expr) + #endif +#endif + +// Assert used for reporting memory leaks - unfreed allocations. +#ifndef VMA_ASSERT_LEAK + #define VMA_ASSERT_LEAK(expr) VMA_ASSERT(expr) +#endif + +// If your compiler is not compatible with C++17 and definition of +// aligned_alloc() function is missing, uncommenting following line may help: + +//#include + +#if defined(__ANDROID_API__) && (__ANDROID_API__ < 16) +#include +static void* vma_aligned_alloc(size_t alignment, size_t size) +{ + // alignment must be >= sizeof(void*) + if(alignment < sizeof(void*)) + { + alignment = sizeof(void*); + } + + return memalign(alignment, size); +} +#elif defined(__APPLE__) || defined(__ANDROID__) || (defined(__linux__) && defined(__GLIBCXX__) && !defined(_GLIBCXX_HAVE_ALIGNED_ALLOC)) +#include + +#if defined(__APPLE__) +#include +#endif + +static void* vma_aligned_alloc(size_t alignment, size_t size) +{ + // Unfortunately, aligned_alloc causes VMA to crash due to it returning null pointers. (At least under 11.4) + // Therefore, for now disable this specific exception until a proper solution is found. + //#if defined(__APPLE__) && (defined(MAC_OS_X_VERSION_10_16) || defined(__IPHONE_14_0)) + //#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_16 || __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 + // // For C++14, usr/include/malloc/_malloc.h declares aligned_alloc()) only + // // with the MacOSX11.0 SDK in Xcode 12 (which is what adds + // // MAC_OS_X_VERSION_10_16), even though the function is marked + // // available for 10.15. That is why the preprocessor checks for 10.16 but + // // the __builtin_available checks for 10.15. + // // People who use C++17 could call aligned_alloc with the 10.15 SDK already. + // if (__builtin_available(macOS 10.15, iOS 13, *)) + // return aligned_alloc(alignment, size); + //#endif + //#endif + + // alignment must be >= sizeof(void*) + if(alignment < sizeof(void*)) + { + alignment = sizeof(void*); + } + + void *pointer; + if(posix_memalign(&pointer, alignment, size) == 0) + return pointer; + return VMA_NULL; +} +#elif defined(_WIN32) +static void* vma_aligned_alloc(size_t alignment, size_t size) +{ + return _aligned_malloc(size, alignment); +} +#elif __cplusplus >= 201703L || _MSVC_LANG >= 201703L // C++17 +static void* vma_aligned_alloc(size_t alignment, size_t size) +{ + return aligned_alloc(alignment, size); +} +#else +static void* vma_aligned_alloc(size_t alignment, size_t size) +{ + VMA_ASSERT(0 && "Could not implement aligned_alloc automatically. Please enable C++17 or later in your compiler or provide custom implementation of macro VMA_SYSTEM_ALIGNED_MALLOC (and VMA_SYSTEM_ALIGNED_FREE if needed) using the API of your system."); + return VMA_NULL; +} +#endif + +#if defined(_WIN32) +static void vma_aligned_free(void* ptr) +{ + _aligned_free(ptr); +} +#else +static void vma_aligned_free(void* VMA_NULLABLE ptr) +{ + free(ptr); +} +#endif + +#ifndef VMA_ALIGN_OF + #define VMA_ALIGN_OF(type) (alignof(type)) +#endif + +#ifndef VMA_SYSTEM_ALIGNED_MALLOC + #define VMA_SYSTEM_ALIGNED_MALLOC(size, alignment) vma_aligned_alloc((alignment), (size)) +#endif + +#ifndef VMA_SYSTEM_ALIGNED_FREE + // VMA_SYSTEM_FREE is the old name, but might have been defined by the user + #if defined(VMA_SYSTEM_FREE) + #define VMA_SYSTEM_ALIGNED_FREE(ptr) VMA_SYSTEM_FREE(ptr) + #else + #define VMA_SYSTEM_ALIGNED_FREE(ptr) vma_aligned_free(ptr) + #endif +#endif + +#ifndef VMA_COUNT_BITS_SET + // Returns number of bits set to 1 in (v) + #define VMA_COUNT_BITS_SET(v) VmaCountBitsSet(v) +#endif + +#ifndef VMA_BITSCAN_LSB + // Scans integer for index of first nonzero value from the Least Significant Bit (LSB). If mask is 0 then returns UINT8_MAX + #define VMA_BITSCAN_LSB(mask) VmaBitScanLSB(mask) +#endif + +#ifndef VMA_BITSCAN_MSB + // Scans integer for index of first nonzero value from the Most Significant Bit (MSB). If mask is 0 then returns UINT8_MAX + #define VMA_BITSCAN_MSB(mask) VmaBitScanMSB(mask) +#endif + +#ifndef VMA_MIN + #define VMA_MIN(v1, v2) ((std::min)((v1), (v2))) +#endif + +#ifndef VMA_MAX + #define VMA_MAX(v1, v2) ((std::max)((v1), (v2))) +#endif + +#ifndef VMA_SORT + #define VMA_SORT(beg, end, cmp) std::sort(beg, end, cmp) +#endif + +#ifndef VMA_DEBUG_LOG_FORMAT + #define VMA_DEBUG_LOG_FORMAT(format, ...) + /* + #define VMA_DEBUG_LOG_FORMAT(format, ...) do { \ + printf((format), __VA_ARGS__); \ + printf("\n"); \ + } while(false) + */ +#endif + +#ifndef VMA_DEBUG_LOG + #define VMA_DEBUG_LOG(str) VMA_DEBUG_LOG_FORMAT("%s", (str)) +#endif + +#ifndef VMA_LEAK_LOG_FORMAT + #define VMA_LEAK_LOG_FORMAT(format, ...) VMA_DEBUG_LOG_FORMAT(format, __VA_ARGS__) +#endif + +#ifndef VMA_CLASS_NO_COPY + #define VMA_CLASS_NO_COPY(className) \ + private: \ + className(const className&) = delete; \ + className& operator=(const className&) = delete; +#endif +#ifndef VMA_CLASS_NO_COPY_NO_MOVE + #define VMA_CLASS_NO_COPY_NO_MOVE(className) \ + private: \ + className(const className&) = delete; \ + className(className&&) = delete; \ + className& operator=(const className&) = delete; \ + className& operator=(className&&) = delete; +#endif + +// Define this macro to 1 to enable functions: vmaBuildStatsString, vmaFreeStatsString. +#if VMA_STATS_STRING_ENABLED + static inline void VmaUint32ToStr(char* VMA_NOT_NULL outStr, size_t strLen, uint32_t num) + { + snprintf(outStr, strLen, "%" PRIu32, num); + } + static inline void VmaUint64ToStr(char* VMA_NOT_NULL outStr, size_t strLen, uint64_t num) + { + snprintf(outStr, strLen, "%" PRIu64, num); + } + static inline void VmaPtrToStr(char* VMA_NOT_NULL outStr, size_t strLen, const void* ptr) + { + snprintf(outStr, strLen, "%p", ptr); + } +#endif + +#ifndef VMA_MUTEX + class VmaMutex + { + VMA_CLASS_NO_COPY_NO_MOVE(VmaMutex) + public: + VmaMutex() = default; + void Lock() { m_Mutex.lock(); } + void Unlock() { m_Mutex.unlock(); } + bool TryLock() { return m_Mutex.try_lock(); } + private: + std::mutex m_Mutex; + }; + #define VMA_MUTEX VmaMutex +#endif + +// Read-write mutex, where "read" is shared access, "write" is exclusive access. +#ifndef VMA_RW_MUTEX + #if VMA_USE_STL_SHARED_MUTEX + // Use std::shared_mutex from C++17. + #include + class VmaRWMutex + { + public: + void LockRead() { m_Mutex.lock_shared(); } + void UnlockRead() { m_Mutex.unlock_shared(); } + bool TryLockRead() { return m_Mutex.try_lock_shared(); } + void LockWrite() { m_Mutex.lock(); } + void UnlockWrite() { m_Mutex.unlock(); } + bool TryLockWrite() { return m_Mutex.try_lock(); } + private: + std::shared_mutex m_Mutex; + }; + #define VMA_RW_MUTEX VmaRWMutex + #elif defined(_WIN32) && defined(WINVER) && defined(SRWLOCK_INIT) && WINVER >= 0x0600 + // Use SRWLOCK from WinAPI. + // Minimum supported client = Windows Vista, server = Windows Server 2008. + class VmaRWMutex + { + public: + VmaRWMutex() { InitializeSRWLock(&m_Lock); } + void LockRead() { AcquireSRWLockShared(&m_Lock); } + void UnlockRead() { ReleaseSRWLockShared(&m_Lock); } + bool TryLockRead() { return TryAcquireSRWLockShared(&m_Lock) != FALSE; } + void LockWrite() { AcquireSRWLockExclusive(&m_Lock); } + void UnlockWrite() { ReleaseSRWLockExclusive(&m_Lock); } + bool TryLockWrite() { return TryAcquireSRWLockExclusive(&m_Lock) != FALSE; } + private: + SRWLOCK m_Lock; + }; + #define VMA_RW_MUTEX VmaRWMutex + #else + // Less efficient fallback: Use normal mutex. + class VmaRWMutex + { + public: + void LockRead() { m_Mutex.Lock(); } + void UnlockRead() { m_Mutex.Unlock(); } + bool TryLockRead() { return m_Mutex.TryLock(); } + void LockWrite() { m_Mutex.Lock(); } + void UnlockWrite() { m_Mutex.Unlock(); } + bool TryLockWrite() { return m_Mutex.TryLock(); } + private: + VMA_MUTEX m_Mutex; + }; + #define VMA_RW_MUTEX VmaRWMutex + #endif // #if VMA_USE_STL_SHARED_MUTEX +#endif // #ifndef VMA_RW_MUTEX + +/* +If providing your own implementation, you need to implement a subset of std::atomic. +*/ +#ifndef VMA_ATOMIC_UINT32 + #include + #define VMA_ATOMIC_UINT32 std::atomic +#endif + +#ifndef VMA_ATOMIC_UINT64 + #include + #define VMA_ATOMIC_UINT64 std::atomic +#endif + +#ifndef VMA_DEBUG_ALWAYS_DEDICATED_MEMORY + /** + Every allocation will have its own memory block. + Define to 1 for debugging purposes only. + */ + #define VMA_DEBUG_ALWAYS_DEDICATED_MEMORY (0) +#endif + +#ifndef VMA_MIN_ALIGNMENT + /** + Minimum alignment of all allocations, in bytes. + Set to more than 1 for debugging purposes. Must be power of two. + */ + #ifdef VMA_DEBUG_ALIGNMENT // Old name + #define VMA_MIN_ALIGNMENT VMA_DEBUG_ALIGNMENT + #else + #define VMA_MIN_ALIGNMENT (1) + #endif +#endif + +#ifndef VMA_DEBUG_MARGIN + /** + Minimum margin after every allocation, in bytes. + Set nonzero for debugging purposes only. + */ + #define VMA_DEBUG_MARGIN (0) +#endif + +#ifndef VMA_DEBUG_INITIALIZE_ALLOCATIONS + /** + Define this macro to 1 to automatically fill new allocations and destroyed + allocations with some bit pattern. + */ + #define VMA_DEBUG_INITIALIZE_ALLOCATIONS (0) +#endif + +#ifndef VMA_DEBUG_DETECT_CORRUPTION + /** + Define this macro to 1 together with non-zero value of VMA_DEBUG_MARGIN to + enable writing magic value to the margin after every allocation and + validating it, so that memory corruptions (out-of-bounds writes) are detected. + */ + #define VMA_DEBUG_DETECT_CORRUPTION (0) +#endif + +#ifndef VMA_DEBUG_GLOBAL_MUTEX + /** + Set this to 1 for debugging purposes only, to enable single mutex protecting all + entry calls to the library. Can be useful for debugging multithreading issues. + */ + #define VMA_DEBUG_GLOBAL_MUTEX (0) +#endif + +#ifndef VMA_DEBUG_MIN_BUFFER_IMAGE_GRANULARITY + /** + Minimum value for VkPhysicalDeviceLimits::bufferImageGranularity. + Set to more than 1 for debugging purposes only. Must be power of two. + */ + #define VMA_DEBUG_MIN_BUFFER_IMAGE_GRANULARITY (1) +#endif + +#ifndef VMA_DEBUG_DONT_EXCEED_MAX_MEMORY_ALLOCATION_COUNT + /* + Set this to 1 to make VMA never exceed VkPhysicalDeviceLimits::maxMemoryAllocationCount + and return error instead of leaving up to Vulkan implementation what to do in such cases. + */ + #define VMA_DEBUG_DONT_EXCEED_MAX_MEMORY_ALLOCATION_COUNT (1) +#endif + +#ifndef VMA_DEBUG_DONT_EXCEED_HEAP_SIZE_WITH_ALLOCATION_SIZE + /* + Set this to 1 to make VMA never exceed VkPhysicalDeviceMemoryProperties::memoryHeaps[i].size + with a single allocation size VkMemoryAllocateInfo::allocationSize + and return error instead of leaving up to Vulkan implementation what to do in such cases. + It protects agaist validation error VUID-vkAllocateMemory-pAllocateInfo-01713. + On the other hand, allowing exceeding this size may result in a successful allocation despite the validation error. + */ + #define VMA_DEBUG_DONT_EXCEED_HEAP_SIZE_WITH_ALLOCATION_SIZE (1) +#endif + +#ifndef VMA_SMALL_HEAP_MAX_SIZE + /// Maximum size of a memory heap in Vulkan to consider it "small". + #define VMA_SMALL_HEAP_MAX_SIZE (1024ULL * 1024 * 1024) +#endif + +#ifndef VMA_DEFAULT_LARGE_HEAP_BLOCK_SIZE + /// Default size of a block allocated as single VkDeviceMemory from a "large" heap. + #define VMA_DEFAULT_LARGE_HEAP_BLOCK_SIZE (256ULL * 1024 * 1024) +#endif + +/* +Mapping hysteresis is a logic that launches when vmaMapMemory/vmaUnmapMemory is called +or a persistently mapped allocation is created and destroyed several times in a row. +It keeps additional +1 mapping of a device memory block to prevent calling actual +vkMapMemory/vkUnmapMemory too many times, which may improve performance and help +tools like RenderDoc. +*/ +#ifndef VMA_MAPPING_HYSTERESIS_ENABLED + #define VMA_MAPPING_HYSTERESIS_ENABLED 1 +#endif + +#define VMA_VALIDATE(cond) do { if(!(cond)) { \ + VMA_ASSERT(0 && "Validation failed: " #cond); \ + return false; \ + } } while(false) + +/******************************************************************************* +END OF CONFIGURATION +*/ +#endif // _VMA_CONFIGURATION + + +static const uint8_t VMA_ALLOCATION_FILL_PATTERN_CREATED = 0xDC; +static const uint8_t VMA_ALLOCATION_FILL_PATTERN_DESTROYED = 0xEF; +// Decimal 2139416166, float NaN, little-endian binary 66 E6 84 7F. +static const uint32_t VMA_CORRUPTION_DETECTION_MAGIC_VALUE = 0x7F84E666; + +// Copy of some Vulkan definitions so we don't need to check their existence just to handle few constants. +static const uint32_t VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD_COPY = 0x00000040; +static const uint32_t VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD_COPY = 0x00000080; +static const uint32_t VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT_COPY = 0x00020000; +static const uint32_t VK_IMAGE_CREATE_DISJOINT_BIT_COPY = 0x00000200; +static const int32_t VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT_COPY = 1000158000; +static const uint32_t VMA_ALLOCATION_INTERNAL_STRATEGY_MIN_OFFSET = 0x10000000U; +static const uint32_t VMA_ALLOCATION_TRY_COUNT = 32; +static const uint32_t VMA_VENDOR_ID_AMD = 4098; + +// This one is tricky. Vulkan specification defines this code as available since +// Vulkan 1.0, but doesn't actually define it in Vulkan SDK earlier than 1.2.131. +// See pull request #207. +#define VK_ERROR_UNKNOWN_COPY ((VkResult)-13) + + +#if VMA_STATS_STRING_ENABLED +// Correspond to values of enum VmaSuballocationType. +static const char* const VMA_SUBALLOCATION_TYPE_NAMES[] = +{ + "FREE", + "UNKNOWN", + "BUFFER", + "IMAGE_UNKNOWN", + "IMAGE_LINEAR", + "IMAGE_OPTIMAL", +}; +#endif + +static const VkAllocationCallbacks VmaEmptyAllocationCallbacks = + { VMA_NULL, VMA_NULL, VMA_NULL, VMA_NULL, VMA_NULL, VMA_NULL }; + + +#ifndef _VMA_ENUM_DECLARATIONS + +enum VmaSuballocationType +{ + VMA_SUBALLOCATION_TYPE_FREE = 0, + VMA_SUBALLOCATION_TYPE_UNKNOWN = 1, + VMA_SUBALLOCATION_TYPE_BUFFER = 2, + VMA_SUBALLOCATION_TYPE_IMAGE_UNKNOWN = 3, + VMA_SUBALLOCATION_TYPE_IMAGE_LINEAR = 4, + VMA_SUBALLOCATION_TYPE_IMAGE_OPTIMAL = 5, + VMA_SUBALLOCATION_TYPE_MAX_ENUM = 0x7FFFFFFF +}; + +enum VMA_CACHE_OPERATION +{ + VMA_CACHE_FLUSH, + VMA_CACHE_INVALIDATE +}; + +enum class VmaAllocationRequestType +{ + Normal, + TLSF, + // Used by "Linear" algorithm. + UpperAddress, + EndOf1st, + EndOf2nd, +}; + +#endif // _VMA_ENUM_DECLARATIONS + +#ifndef _VMA_FORWARD_DECLARATIONS +// Opaque handle used by allocation algorithms to identify single allocation in any conforming way. +VK_DEFINE_NON_DISPATCHABLE_HANDLE(VmaAllocHandle); + +struct VmaMutexLock; +struct VmaMutexLockRead; +struct VmaMutexLockWrite; + +template +struct AtomicTransactionalIncrement; + +template +struct VmaStlAllocator; + +template +class VmaVector; + +template +class VmaSmallVector; + +template +class VmaPoolAllocator; + +template +struct VmaListItem; + +template +class VmaRawList; + +template +class VmaList; + +template +class VmaIntrusiveLinkedList; + +#if VMA_STATS_STRING_ENABLED +class VmaStringBuilder; +class VmaJsonWriter; +#endif + +class VmaDeviceMemoryBlock; + +struct VmaDedicatedAllocationListItemTraits; +class VmaDedicatedAllocationList; + +struct VmaSuballocation; +struct VmaSuballocationOffsetLess; +struct VmaSuballocationOffsetGreater; +struct VmaSuballocationItemSizeLess; + +typedef VmaList> VmaSuballocationList; + +struct VmaAllocationRequest; + +class VmaBlockMetadata; +class VmaBlockMetadata_Linear; +class VmaBlockMetadata_TLSF; + +class VmaBlockVector; + +struct VmaPoolListItemTraits; + +struct VmaCurrentBudgetData; + +class VmaAllocationObjectAllocator; + +#endif // _VMA_FORWARD_DECLARATIONS + + +#ifndef _VMA_FUNCTIONS + +/* +Returns number of bits set to 1 in (v). + +On specific platforms and compilers you can use intrinsics like: + +Visual Studio: + return __popcnt(v); +GCC, Clang: + return static_cast(__builtin_popcount(v)); + +Define macro VMA_COUNT_BITS_SET to provide your optimized implementation. +But you need to check in runtime whether user's CPU supports these, as some old processors don't. +*/ +static inline uint32_t VmaCountBitsSet(uint32_t v) +{ +#if VMA_CPP20 + return std::popcount(v); +#else + uint32_t c = v - ((v >> 1) & 0x55555555); + c = ((c >> 2) & 0x33333333) + (c & 0x33333333); + c = ((c >> 4) + c) & 0x0F0F0F0F; + c = ((c >> 8) + c) & 0x00FF00FF; + c = ((c >> 16) + c) & 0x0000FFFF; + return c; +#endif +} + +static inline uint8_t VmaBitScanLSB(uint64_t mask) +{ +#if defined(_MSC_VER) && defined(_WIN64) + unsigned long pos; + if (_BitScanForward64(&pos, mask)) + return static_cast(pos); + return UINT8_MAX; +#elif VMA_CPP20 + if(mask != 0) + return static_cast(std::countr_zero(mask)); + return UINT8_MAX; +#elif defined __GNUC__ || defined __clang__ + return static_cast(__builtin_ffsll(mask)) - 1U; +#else + uint8_t pos = 0; + uint64_t bit = 1; + do + { + if (mask & bit) + return pos; + bit <<= 1; + } while (pos++ < 63); + return UINT8_MAX; +#endif +} + +static inline uint8_t VmaBitScanLSB(uint32_t mask) +{ +#ifdef _MSC_VER + unsigned long pos; + if (_BitScanForward(&pos, mask)) + return static_cast(pos); + return UINT8_MAX; +#elif VMA_CPP20 + if(mask != 0) + return static_cast(std::countr_zero(mask)); + return UINT8_MAX; +#elif defined __GNUC__ || defined __clang__ + return static_cast(__builtin_ffs(mask)) - 1U; +#else + uint8_t pos = 0; + uint32_t bit = 1; + do + { + if (mask & bit) + return pos; + bit <<= 1; + } while (pos++ < 31); + return UINT8_MAX; +#endif +} + +static inline uint8_t VmaBitScanMSB(uint64_t mask) +{ +#if defined(_MSC_VER) && defined(_WIN64) + unsigned long pos; + if (_BitScanReverse64(&pos, mask)) + return static_cast(pos); +#elif VMA_CPP20 + if(mask != 0) + return 63 - static_cast(std::countl_zero(mask)); +#elif defined __GNUC__ || defined __clang__ + if (mask != 0) + return 63 - static_cast(__builtin_clzll(mask)); +#else + uint8_t pos = 63; + uint64_t bit = 1ULL << 63; + do + { + if (mask & bit) + return pos; + bit >>= 1; + } while (pos-- > 0); +#endif + return UINT8_MAX; +} + +static inline uint8_t VmaBitScanMSB(uint32_t mask) +{ +#ifdef _MSC_VER + unsigned long pos; + if (_BitScanReverse(&pos, mask)) + return static_cast(pos); +#elif VMA_CPP20 + if(mask != 0) + return 31 - static_cast(std::countl_zero(mask)); +#elif defined __GNUC__ || defined __clang__ + if (mask != 0) + return 31 - static_cast(__builtin_clz(mask)); +#else + uint8_t pos = 31; + uint32_t bit = 1UL << 31; + do + { + if (mask & bit) + return pos; + bit >>= 1; + } while (pos-- > 0); +#endif + return UINT8_MAX; +} + +/* +Returns true if given number is a power of two. +T must be unsigned integer number or signed integer but always nonnegative. +For 0 returns true. +*/ +template +inline bool VmaIsPow2(T x) +{ + return (x & (x - 1)) == 0; +} + +// Aligns given value up to nearest multiply of align value. For example: VmaAlignUp(11, 8) = 16. +// Use types like uint32_t, uint64_t as T. +template +static inline T VmaAlignUp(T val, T alignment) +{ + VMA_HEAVY_ASSERT(VmaIsPow2(alignment)); + return (val + alignment - 1) & ~(alignment - 1); +} + +// Aligns given value down to nearest multiply of align value. For example: VmaAlignDown(11, 8) = 8. +// Use types like uint32_t, uint64_t as T. +template +static inline T VmaAlignDown(T val, T alignment) +{ + VMA_HEAVY_ASSERT(VmaIsPow2(alignment)); + return val & ~(alignment - 1); +} + +// Division with mathematical rounding to nearest number. +template +static inline T VmaRoundDiv(T x, T y) +{ + return (x + (y / (T)2)) / y; +} + +// Divide by 'y' and round up to nearest integer. +template +static inline T VmaDivideRoundingUp(T x, T y) +{ + return (x + y - (T)1) / y; +} + +// Returns smallest power of 2 greater or equal to v. +static inline uint32_t VmaNextPow2(uint32_t v) +{ + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + return v; +} + +static inline uint64_t VmaNextPow2(uint64_t v) +{ + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v |= v >> 32; + v++; + return v; +} + +// Returns largest power of 2 less or equal to v. +static inline uint32_t VmaPrevPow2(uint32_t v) +{ + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v = v ^ (v >> 1); + return v; +} + +static inline uint64_t VmaPrevPow2(uint64_t v) +{ + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v |= v >> 32; + v = v ^ (v >> 1); + return v; +} + +static inline bool VmaStrIsEmpty(const char* pStr) +{ + return pStr == VMA_NULL || *pStr == '\0'; +} + +/* +Returns true if two memory blocks occupy overlapping pages. +ResourceA must be in less memory offset than ResourceB. + +Algorithm is based on "Vulkan 1.0.39 - A Specification (with all registered Vulkan extensions)" +chapter 11.6 "Resource Memory Association", paragraph "Buffer-Image Granularity". +*/ +static inline bool VmaBlocksOnSamePage( + VkDeviceSize resourceAOffset, + VkDeviceSize resourceASize, + VkDeviceSize resourceBOffset, + VkDeviceSize pageSize) +{ + VMA_ASSERT(resourceAOffset + resourceASize <= resourceBOffset && resourceASize > 0 && pageSize > 0); + VkDeviceSize resourceAEnd = resourceAOffset + resourceASize - 1; + VkDeviceSize resourceAEndPage = resourceAEnd & ~(pageSize - 1); + VkDeviceSize resourceBStart = resourceBOffset; + VkDeviceSize resourceBStartPage = resourceBStart & ~(pageSize - 1); + return resourceAEndPage == resourceBStartPage; +} + +/* +Returns true if given suballocation types could conflict and must respect +VkPhysicalDeviceLimits::bufferImageGranularity. They conflict if one is buffer +or linear image and another one is optimal image. If type is unknown, behave +conservatively. +*/ +static inline bool VmaIsBufferImageGranularityConflict( + VmaSuballocationType suballocType1, + VmaSuballocationType suballocType2) +{ + if (suballocType1 > suballocType2) + { + std::swap(suballocType1, suballocType2); + } + + switch (suballocType1) + { + case VMA_SUBALLOCATION_TYPE_FREE: + return false; + case VMA_SUBALLOCATION_TYPE_UNKNOWN: + return true; + case VMA_SUBALLOCATION_TYPE_BUFFER: + return + suballocType2 == VMA_SUBALLOCATION_TYPE_IMAGE_UNKNOWN || + suballocType2 == VMA_SUBALLOCATION_TYPE_IMAGE_OPTIMAL; + case VMA_SUBALLOCATION_TYPE_IMAGE_UNKNOWN: + return + suballocType2 == VMA_SUBALLOCATION_TYPE_IMAGE_UNKNOWN || + suballocType2 == VMA_SUBALLOCATION_TYPE_IMAGE_LINEAR || + suballocType2 == VMA_SUBALLOCATION_TYPE_IMAGE_OPTIMAL; + case VMA_SUBALLOCATION_TYPE_IMAGE_LINEAR: + return + suballocType2 == VMA_SUBALLOCATION_TYPE_IMAGE_OPTIMAL; + case VMA_SUBALLOCATION_TYPE_IMAGE_OPTIMAL: + return false; + default: + VMA_ASSERT(0); + return true; + } +} + +static void VmaWriteMagicValue(void* pData, VkDeviceSize offset) +{ +#if VMA_DEBUG_MARGIN > 0 && VMA_DEBUG_DETECT_CORRUPTION + uint32_t* pDst = (uint32_t*)((char*)pData + offset); + const size_t numberCount = VMA_DEBUG_MARGIN / sizeof(uint32_t); + for (size_t i = 0; i < numberCount; ++i, ++pDst) + { + *pDst = VMA_CORRUPTION_DETECTION_MAGIC_VALUE; + } +#else + // no-op +#endif +} + +static bool VmaValidateMagicValue(const void* pData, VkDeviceSize offset) +{ +#if VMA_DEBUG_MARGIN > 0 && VMA_DEBUG_DETECT_CORRUPTION + const uint32_t* pSrc = (const uint32_t*)((const char*)pData + offset); + const size_t numberCount = VMA_DEBUG_MARGIN / sizeof(uint32_t); + for (size_t i = 0; i < numberCount; ++i, ++pSrc) + { + if (*pSrc != VMA_CORRUPTION_DETECTION_MAGIC_VALUE) + { + return false; + } + } +#endif + return true; +} + +/* +Fills structure with parameters of an example buffer to be used for transfers +during GPU memory defragmentation. +*/ +static void VmaFillGpuDefragmentationBufferCreateInfo(VkBufferCreateInfo& outBufCreateInfo) +{ + memset(&outBufCreateInfo, 0, sizeof(outBufCreateInfo)); + outBufCreateInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + outBufCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + outBufCreateInfo.size = (VkDeviceSize)VMA_DEFAULT_LARGE_HEAP_BLOCK_SIZE; // Example size. +} + + +/* +Performs binary search and returns iterator to first element that is greater or +equal to (key), according to comparison (cmp). + +Cmp should return true if first argument is less than second argument. + +Returned value is the found element, if present in the collection or place where +new element with value (key) should be inserted. +*/ +template +static IterT VmaBinaryFindFirstNotLess(IterT beg, IterT end, const KeyT& key, const CmpLess& cmp) +{ + size_t down = 0; + size_t up = size_t(end - beg); + while (down < up) + { + const size_t mid = down + (up - down) / 2; // Overflow-safe midpoint calculation + if (cmp(*(beg + mid), key)) + { + down = mid + 1; + } + else + { + up = mid; + } + } + return beg + down; +} + +template +IterT VmaBinaryFindSorted(const IterT& beg, const IterT& end, const KeyT& value, const CmpLess& cmp) +{ + IterT it = VmaBinaryFindFirstNotLess( + beg, end, value, cmp); + if (it == end || + (!cmp(*it, value) && !cmp(value, *it))) + { + return it; + } + return end; +} + +/* +Returns true if all pointers in the array are not-null and unique. +Warning! O(n^2) complexity. Use only inside VMA_HEAVY_ASSERT. +T must be pointer type, e.g. VmaAllocation, VmaPool. +*/ +template +static bool VmaValidatePointerArray(uint32_t count, const T* arr) +{ + for (uint32_t i = 0; i < count; ++i) + { + const T iPtr = arr[i]; + if (iPtr == VMA_NULL) + { + return false; + } + for (uint32_t j = i + 1; j < count; ++j) + { + if (iPtr == arr[j]) + { + return false; + } + } + } + return true; +} + +template +static inline void VmaPnextChainPushFront(MainT* mainStruct, NewT* newStruct) +{ + newStruct->pNext = mainStruct->pNext; + mainStruct->pNext = newStruct; +} +// Finds structure with s->sType == sType in mainStruct->pNext chain. +// Returns pointer to it. If not found, returns null. +template +static inline const FindT* VmaPnextChainFind(const MainT* mainStruct, VkStructureType sType) +{ + for(const VkBaseInStructure* s = (const VkBaseInStructure*)mainStruct->pNext; + s != VMA_NULL; s = s->pNext) + { + if(s->sType == sType) + { + return (const FindT*)s; + } + } + return VMA_NULL; +} + +// An abstraction over buffer or image `usage` flags, depending on available extensions. +struct VmaBufferImageUsage +{ +#if VMA_KHR_MAINTENANCE5 + typedef uint64_t BaseType; // VkFlags64 +#else + typedef uint32_t BaseType; // VkFlags32 +#endif + + static const VmaBufferImageUsage UNKNOWN; + + BaseType Value; + + VmaBufferImageUsage() { *this = UNKNOWN; } + explicit VmaBufferImageUsage(BaseType usage) : Value(usage) { } + VmaBufferImageUsage(const VkBufferCreateInfo &createInfo, bool useKhrMaintenance5); + explicit VmaBufferImageUsage(const VkImageCreateInfo &createInfo); + + bool operator==(const VmaBufferImageUsage& rhs) const { return Value == rhs.Value; } + bool operator!=(const VmaBufferImageUsage& rhs) const { return Value != rhs.Value; } + + bool Contains(BaseType flag) const { return (Value & flag) != 0; } + bool ContainsDeviceAccess() const + { + // This relies on values of VK_IMAGE_USAGE_TRANSFER* being the same as VK_BUFFER_IMAGE_TRANSFER*. + return (Value & ~BaseType(VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT)) != 0; + } +}; + +const VmaBufferImageUsage VmaBufferImageUsage::UNKNOWN = VmaBufferImageUsage(0); + +VmaBufferImageUsage::VmaBufferImageUsage(const VkBufferCreateInfo &createInfo, + bool useKhrMaintenance5) +{ +#if VMA_KHR_MAINTENANCE5 + if(useKhrMaintenance5) + { + // If VkBufferCreateInfo::pNext chain contains VkBufferUsageFlags2CreateInfoKHR, + // take usage from it and ignore VkBufferCreateInfo::usage, per specification + // of the VK_KHR_maintenance5 extension. + const VkBufferUsageFlags2CreateInfoKHR* const usageFlags2 = + VmaPnextChainFind(&createInfo, VK_STRUCTURE_TYPE_BUFFER_USAGE_FLAGS_2_CREATE_INFO_KHR); + if(usageFlags2 != VMA_NULL) + { + this->Value = usageFlags2->usage; + return; + } + } +#endif + + this->Value = (BaseType)createInfo.usage; +} + +VmaBufferImageUsage::VmaBufferImageUsage(const VkImageCreateInfo &createInfo) + : Value((BaseType)createInfo.usage) +{ + // Maybe in the future there will be VK_KHR_maintenanceN extension with structure + // VkImageUsageFlags2CreateInfoKHR, like the one for buffers... +} + +// This is the main algorithm that guides the selection of a memory type best for an allocation - +// converts usage to required/preferred/not preferred flags. +static bool FindMemoryPreferences( + bool isIntegratedGPU, + const VmaAllocationCreateInfo& allocCreateInfo, + VmaBufferImageUsage bufImgUsage, + VkMemoryPropertyFlags& outRequiredFlags, + VkMemoryPropertyFlags& outPreferredFlags, + VkMemoryPropertyFlags& outNotPreferredFlags) +{ + outRequiredFlags = allocCreateInfo.requiredFlags; + outPreferredFlags = allocCreateInfo.preferredFlags; + outNotPreferredFlags = 0; + + switch(allocCreateInfo.usage) + { + case VMA_MEMORY_USAGE_UNKNOWN: + break; + case VMA_MEMORY_USAGE_GPU_ONLY: + if(!isIntegratedGPU || (outPreferredFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) + { + outPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + } + break; + case VMA_MEMORY_USAGE_CPU_ONLY: + outRequiredFlags |= VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + break; + case VMA_MEMORY_USAGE_CPU_TO_GPU: + outRequiredFlags |= VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; + if(!isIntegratedGPU || (outPreferredFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) + { + outPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + } + break; + case VMA_MEMORY_USAGE_GPU_TO_CPU: + outRequiredFlags |= VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; + outPreferredFlags |= VK_MEMORY_PROPERTY_HOST_CACHED_BIT; + break; + case VMA_MEMORY_USAGE_CPU_COPY: + outNotPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + break; + case VMA_MEMORY_USAGE_GPU_LAZILY_ALLOCATED: + outRequiredFlags |= VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT; + break; + case VMA_MEMORY_USAGE_AUTO: + case VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE: + case VMA_MEMORY_USAGE_AUTO_PREFER_HOST: + { + if(bufImgUsage == VmaBufferImageUsage::UNKNOWN) + { + VMA_ASSERT(0 && "VMA_MEMORY_USAGE_AUTO* values can only be used with functions like vmaCreateBuffer, vmaCreateImage so that the details of the created resource are known." + " Maybe you use VkBufferUsageFlags2CreateInfoKHR but forgot to use VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE5_BIT?" ); + return false; + } + + const bool deviceAccess = bufImgUsage.ContainsDeviceAccess(); + const bool hostAccessSequentialWrite = (allocCreateInfo.flags & VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT) != 0; + const bool hostAccessRandom = (allocCreateInfo.flags & VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT) != 0; + const bool hostAccessAllowTransferInstead = (allocCreateInfo.flags & VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT) != 0; + const bool preferDevice = allocCreateInfo.usage == VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + const bool preferHost = allocCreateInfo.usage == VMA_MEMORY_USAGE_AUTO_PREFER_HOST; + + // CPU random access - e.g. a buffer written to or transferred from GPU to read back on CPU. + if(hostAccessRandom) + { + // Prefer cached. Cannot require it, because some platforms don't have it (e.g. Raspberry Pi - see #362)! + outPreferredFlags |= VK_MEMORY_PROPERTY_HOST_CACHED_BIT; + + if (!isIntegratedGPU && deviceAccess && hostAccessAllowTransferInstead && !preferHost) + { + // Nice if it will end up in HOST_VISIBLE, but more importantly prefer DEVICE_LOCAL. + // Omitting HOST_VISIBLE here is intentional. + // In case there is DEVICE_LOCAL | HOST_VISIBLE | HOST_CACHED, it will pick that one. + // Otherwise, this will give same weight to DEVICE_LOCAL as HOST_VISIBLE | HOST_CACHED and select the former if occurs first on the list. + outPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + } + else + { + // Always CPU memory. + outRequiredFlags |= VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; + } + } + // CPU sequential write - may be CPU or host-visible GPU memory, uncached and write-combined. + else if(hostAccessSequentialWrite) + { + // Want uncached and write-combined. + outNotPreferredFlags |= VK_MEMORY_PROPERTY_HOST_CACHED_BIT; + + if(!isIntegratedGPU && deviceAccess && hostAccessAllowTransferInstead && !preferHost) + { + outPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; + } + else + { + outRequiredFlags |= VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; + // Direct GPU access, CPU sequential write (e.g. a dynamic uniform buffer updated every frame) + if(deviceAccess) + { + // Could go to CPU memory or GPU BAR/unified. Up to the user to decide. If no preference, choose GPU memory. + if(preferHost) + outNotPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + else + outPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + } + // GPU no direct access, CPU sequential write (e.g. an upload buffer to be transferred to the GPU) + else + { + // Could go to CPU memory or GPU BAR/unified. Up to the user to decide. If no preference, choose CPU memory. + if(preferDevice) + outPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + else + outNotPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + } + } + } + // No CPU access + else + { + // if(deviceAccess) + // + // GPU access, no CPU access (e.g. a color attachment image) - prefer GPU memory, + // unless there is a clear preference from the user not to do so. + // + // else: + // + // No direct GPU access, no CPU access, just transfers. + // It may be staging copy intended for e.g. preserving image for next frame (then better GPU memory) or + // a "swap file" copy to free some GPU memory (then better CPU memory). + // Up to the user to decide. If no preferece, assume the former and choose GPU memory. + + if(preferHost) + outNotPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + else + outPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + } + break; + } + default: + VMA_ASSERT(0); + } + + // Avoid DEVICE_COHERENT unless explicitly requested. + if(((allocCreateInfo.requiredFlags | allocCreateInfo.preferredFlags) & + (VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD_COPY | VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD_COPY)) == 0) + { + outNotPreferredFlags |= VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD_COPY; + } + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// Memory allocation + +static void* VmaMalloc(const VkAllocationCallbacks* pAllocationCallbacks, size_t size, size_t alignment) +{ + void* result = VMA_NULL; + if ((pAllocationCallbacks != VMA_NULL) && + (pAllocationCallbacks->pfnAllocation != VMA_NULL)) + { + result = (*pAllocationCallbacks->pfnAllocation)( + pAllocationCallbacks->pUserData, + size, + alignment, + VK_SYSTEM_ALLOCATION_SCOPE_OBJECT); + } + else + { + result = VMA_SYSTEM_ALIGNED_MALLOC(size, alignment); + } + VMA_ASSERT(result != VMA_NULL && "CPU memory allocation failed."); + return result; +} + +static void VmaFree(const VkAllocationCallbacks* pAllocationCallbacks, void* ptr) +{ + if ((pAllocationCallbacks != VMA_NULL) && + (pAllocationCallbacks->pfnFree != VMA_NULL)) + { + (*pAllocationCallbacks->pfnFree)(pAllocationCallbacks->pUserData, ptr); + } + else + { + VMA_SYSTEM_ALIGNED_FREE(ptr); + } +} + +template +static T* VmaAllocate(const VkAllocationCallbacks* pAllocationCallbacks) +{ + return (T*)VmaMalloc(pAllocationCallbacks, sizeof(T), VMA_ALIGN_OF(T)); +} + +template +static T* VmaAllocateArray(const VkAllocationCallbacks* pAllocationCallbacks, size_t count) +{ + return (T*)VmaMalloc(pAllocationCallbacks, sizeof(T) * count, VMA_ALIGN_OF(T)); +} + +#define vma_new(allocator, type) new(VmaAllocate(allocator))(type) + +#define vma_new_array(allocator, type, count) new(VmaAllocateArray((allocator), (count)))(type) + +template +static void vma_delete(const VkAllocationCallbacks* pAllocationCallbacks, T* ptr) +{ + ptr->~T(); + VmaFree(pAllocationCallbacks, ptr); +} + +template +static void vma_delete_array(const VkAllocationCallbacks* pAllocationCallbacks, T* ptr, size_t count) +{ + if (ptr != VMA_NULL) + { + for (size_t i = count; i--; ) + { + ptr[i].~T(); + } + VmaFree(pAllocationCallbacks, ptr); + } +} + +static char* VmaCreateStringCopy(const VkAllocationCallbacks* allocs, const char* srcStr) +{ + if (srcStr != VMA_NULL) + { + const size_t len = strlen(srcStr); + char* const result = vma_new_array(allocs, char, len + 1); + memcpy(result, srcStr, len + 1); + return result; + } + return VMA_NULL; +} + +#if VMA_STATS_STRING_ENABLED +static char* VmaCreateStringCopy(const VkAllocationCallbacks* allocs, const char* srcStr, size_t strLen) +{ + if (srcStr != VMA_NULL) + { + char* const result = vma_new_array(allocs, char, strLen + 1); + memcpy(result, srcStr, strLen); + result[strLen] = '\0'; + return result; + } + return VMA_NULL; +} +#endif // VMA_STATS_STRING_ENABLED + +static void VmaFreeString(const VkAllocationCallbacks* allocs, char* str) +{ + if (str != VMA_NULL) + { + const size_t len = strlen(str); + vma_delete_array(allocs, str, len + 1); + } +} + +template +size_t VmaVectorInsertSorted(VectorT& vector, const typename VectorT::value_type& value) +{ + const size_t indexToInsert = VmaBinaryFindFirstNotLess( + vector.data(), + vector.data() + vector.size(), + value, + CmpLess()) - vector.data(); + VmaVectorInsert(vector, indexToInsert, value); + return indexToInsert; +} + +template +bool VmaVectorRemoveSorted(VectorT& vector, const typename VectorT::value_type& value) +{ + CmpLess comparator; + typename VectorT::iterator it = VmaBinaryFindFirstNotLess( + vector.begin(), + vector.end(), + value, + comparator); + if ((it != vector.end()) && !comparator(*it, value) && !comparator(value, *it)) + { + size_t indexToRemove = it - vector.begin(); + VmaVectorRemove(vector, indexToRemove); + return true; + } + return false; +} +#endif // _VMA_FUNCTIONS + +#ifndef _VMA_STATISTICS_FUNCTIONS + +static void VmaClearStatistics(VmaStatistics& outStats) +{ + outStats.blockCount = 0; + outStats.allocationCount = 0; + outStats.blockBytes = 0; + outStats.allocationBytes = 0; +} + +static void VmaAddStatistics(VmaStatistics& inoutStats, const VmaStatistics& src) +{ + inoutStats.blockCount += src.blockCount; + inoutStats.allocationCount += src.allocationCount; + inoutStats.blockBytes += src.blockBytes; + inoutStats.allocationBytes += src.allocationBytes; +} + +static void VmaClearDetailedStatistics(VmaDetailedStatistics& outStats) +{ + VmaClearStatistics(outStats.statistics); + outStats.unusedRangeCount = 0; + outStats.allocationSizeMin = VK_WHOLE_SIZE; + outStats.allocationSizeMax = 0; + outStats.unusedRangeSizeMin = VK_WHOLE_SIZE; + outStats.unusedRangeSizeMax = 0; +} + +static void VmaAddDetailedStatisticsAllocation(VmaDetailedStatistics& inoutStats, VkDeviceSize size) +{ + inoutStats.statistics.allocationCount++; + inoutStats.statistics.allocationBytes += size; + inoutStats.allocationSizeMin = VMA_MIN(inoutStats.allocationSizeMin, size); + inoutStats.allocationSizeMax = VMA_MAX(inoutStats.allocationSizeMax, size); +} + +static void VmaAddDetailedStatisticsUnusedRange(VmaDetailedStatistics& inoutStats, VkDeviceSize size) +{ + inoutStats.unusedRangeCount++; + inoutStats.unusedRangeSizeMin = VMA_MIN(inoutStats.unusedRangeSizeMin, size); + inoutStats.unusedRangeSizeMax = VMA_MAX(inoutStats.unusedRangeSizeMax, size); +} + +static void VmaAddDetailedStatistics(VmaDetailedStatistics& inoutStats, const VmaDetailedStatistics& src) +{ + VmaAddStatistics(inoutStats.statistics, src.statistics); + inoutStats.unusedRangeCount += src.unusedRangeCount; + inoutStats.allocationSizeMin = VMA_MIN(inoutStats.allocationSizeMin, src.allocationSizeMin); + inoutStats.allocationSizeMax = VMA_MAX(inoutStats.allocationSizeMax, src.allocationSizeMax); + inoutStats.unusedRangeSizeMin = VMA_MIN(inoutStats.unusedRangeSizeMin, src.unusedRangeSizeMin); + inoutStats.unusedRangeSizeMax = VMA_MAX(inoutStats.unusedRangeSizeMax, src.unusedRangeSizeMax); +} + +#endif // _VMA_STATISTICS_FUNCTIONS + +#ifndef _VMA_MUTEX_LOCK +// Helper RAII class to lock a mutex in constructor and unlock it in destructor (at the end of scope). +struct VmaMutexLock +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaMutexLock) +public: + explicit VmaMutexLock(VMA_MUTEX& mutex, bool useMutex = true) : + m_pMutex(useMutex ? &mutex : VMA_NULL) + { + if (m_pMutex) { m_pMutex->Lock(); } + } + ~VmaMutexLock() { if (m_pMutex) { m_pMutex->Unlock(); } } + +private: + VMA_MUTEX* m_pMutex; +}; + +// Helper RAII class to lock a RW mutex in constructor and unlock it in destructor (at the end of scope), for reading. +struct VmaMutexLockRead +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaMutexLockRead) +public: + VmaMutexLockRead(VMA_RW_MUTEX& mutex, bool useMutex) : + m_pMutex(useMutex ? &mutex : VMA_NULL) + { + if (m_pMutex) { m_pMutex->LockRead(); } + } + ~VmaMutexLockRead() { if (m_pMutex) { m_pMutex->UnlockRead(); } } + +private: + VMA_RW_MUTEX* m_pMutex; +}; + +// Helper RAII class to lock a RW mutex in constructor and unlock it in destructor (at the end of scope), for writing. +struct VmaMutexLockWrite +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaMutexLockWrite) +public: + VmaMutexLockWrite(VMA_RW_MUTEX& mutex, bool useMutex) + : m_pMutex(useMutex ? &mutex : VMA_NULL) + { + if (m_pMutex) { m_pMutex->LockWrite(); } + } + ~VmaMutexLockWrite() { if (m_pMutex) { m_pMutex->UnlockWrite(); } } + +private: + VMA_RW_MUTEX* m_pMutex; +}; + +#if VMA_DEBUG_GLOBAL_MUTEX + static VMA_MUTEX gDebugGlobalMutex; + #define VMA_DEBUG_GLOBAL_MUTEX_LOCK VmaMutexLock debugGlobalMutexLock(gDebugGlobalMutex, true); +#else + #define VMA_DEBUG_GLOBAL_MUTEX_LOCK +#endif +#endif // _VMA_MUTEX_LOCK + +#ifndef _VMA_ATOMIC_TRANSACTIONAL_INCREMENT +// An object that increments given atomic but decrements it back in the destructor unless Commit() is called. +template +struct AtomicTransactionalIncrement +{ +public: + using T = decltype(AtomicT().load()); + + ~AtomicTransactionalIncrement() + { + if(m_Atomic) + --(*m_Atomic); + } + + void Commit() { m_Atomic = VMA_NULL; } + T Increment(AtomicT* atomic) + { + m_Atomic = atomic; + return m_Atomic->fetch_add(1); + } + +private: + AtomicT* m_Atomic = VMA_NULL; +}; +#endif // _VMA_ATOMIC_TRANSACTIONAL_INCREMENT + +#ifndef _VMA_STL_ALLOCATOR +// STL-compatible allocator. +template +struct VmaStlAllocator +{ + const VkAllocationCallbacks* const m_pCallbacks; + typedef T value_type; + + explicit VmaStlAllocator(const VkAllocationCallbacks* pCallbacks) : m_pCallbacks(pCallbacks) {} + template + explicit VmaStlAllocator(const VmaStlAllocator& src) : m_pCallbacks(src.m_pCallbacks) {} + VmaStlAllocator(const VmaStlAllocator&) = default; + VmaStlAllocator& operator=(const VmaStlAllocator&) = delete; + + T* allocate(size_t n) { return VmaAllocateArray(m_pCallbacks, n); } + void deallocate(T* p, size_t n) { VmaFree(m_pCallbacks, p); } + + template + bool operator==(const VmaStlAllocator& rhs) const + { + return m_pCallbacks == rhs.m_pCallbacks; + } + template + bool operator!=(const VmaStlAllocator& rhs) const + { + return m_pCallbacks != rhs.m_pCallbacks; + } +}; +#endif // _VMA_STL_ALLOCATOR + +#ifndef _VMA_VECTOR +/* Class with interface compatible with subset of std::vector. +T must be POD because constructors and destructors are not called and memcpy is +used for these objects. */ +template +class VmaVector +{ +public: + typedef T value_type; + typedef T* iterator; + typedef const T* const_iterator; + + explicit VmaVector(const AllocatorT& allocator); + VmaVector(size_t count, const AllocatorT& allocator); + // This version of the constructor is here for compatibility with pre-C++14 std::vector. + // value is unused. + VmaVector(size_t count, const T& value, const AllocatorT& allocator) : VmaVector(count, allocator) {} + VmaVector(const VmaVector& src); + VmaVector& operator=(const VmaVector& rhs); + ~VmaVector() { VmaFree(m_Allocator.m_pCallbacks, m_pArray); } + + bool empty() const { return m_Count == 0; } + size_t size() const { return m_Count; } + T* data() { return m_pArray; } + T& front() { VMA_HEAVY_ASSERT(m_Count > 0); return m_pArray[0]; } + T& back() { VMA_HEAVY_ASSERT(m_Count > 0); return m_pArray[m_Count - 1]; } + const T* data() const { return m_pArray; } + const T& front() const { VMA_HEAVY_ASSERT(m_Count > 0); return m_pArray[0]; } + const T& back() const { VMA_HEAVY_ASSERT(m_Count > 0); return m_pArray[m_Count - 1]; } + + iterator begin() { return m_pArray; } + iterator end() { return m_pArray + m_Count; } + const_iterator cbegin() const { return m_pArray; } + const_iterator cend() const { return m_pArray + m_Count; } + const_iterator begin() const { return cbegin(); } + const_iterator end() const { return cend(); } + + void pop_front() { VMA_HEAVY_ASSERT(m_Count > 0); remove(0); } + void pop_back() { VMA_HEAVY_ASSERT(m_Count > 0); resize(size() - 1); } + void push_front(const T& src) { insert(0, src); } + + void push_back(const T& src); + void reserve(size_t newCapacity, bool freeMemory = false); + void resize(size_t newCount); + void clear() { resize(0); } + void shrink_to_fit(); + void insert(size_t index, const T& src); + void remove(size_t index); + + T& operator[](size_t index) { VMA_HEAVY_ASSERT(index < m_Count); return m_pArray[index]; } + const T& operator[](size_t index) const { VMA_HEAVY_ASSERT(index < m_Count); return m_pArray[index]; } + +private: + AllocatorT m_Allocator; + T* m_pArray; + size_t m_Count; + size_t m_Capacity; +}; + +#ifndef _VMA_VECTOR_FUNCTIONS +template +VmaVector::VmaVector(const AllocatorT& allocator) + : m_Allocator(allocator), + m_pArray(VMA_NULL), + m_Count(0), + m_Capacity(0) {} + +template +VmaVector::VmaVector(size_t count, const AllocatorT& allocator) + : m_Allocator(allocator), + m_pArray(count ? (T*)VmaAllocateArray(allocator.m_pCallbacks, count) : VMA_NULL), + m_Count(count), + m_Capacity(count) {} + +template +VmaVector::VmaVector(const VmaVector& src) + : m_Allocator(src.m_Allocator), + m_pArray(src.m_Count ? (T*)VmaAllocateArray(src.m_Allocator.m_pCallbacks, src.m_Count) : VMA_NULL), + m_Count(src.m_Count), + m_Capacity(src.m_Count) +{ + if (m_Count != 0) + { + memcpy(m_pArray, src.m_pArray, m_Count * sizeof(T)); + } +} + +template +VmaVector& VmaVector::operator=(const VmaVector& rhs) +{ + if (&rhs != this) + { + resize(rhs.m_Count); + if (m_Count != 0) + { + memcpy(m_pArray, rhs.m_pArray, m_Count * sizeof(T)); + } + } + return *this; +} + +template +void VmaVector::push_back(const T& src) +{ + const size_t newIndex = size(); + resize(newIndex + 1); + m_pArray[newIndex] = src; +} + +template +void VmaVector::reserve(size_t newCapacity, bool freeMemory) +{ + newCapacity = VMA_MAX(newCapacity, m_Count); + + if ((newCapacity < m_Capacity) && !freeMemory) + { + newCapacity = m_Capacity; + } + + if (newCapacity != m_Capacity) + { + T* const newArray = newCapacity ? VmaAllocateArray(m_Allocator, newCapacity) : VMA_NULL; + if (m_Count != 0) + { + memcpy(newArray, m_pArray, m_Count * sizeof(T)); + } + VmaFree(m_Allocator.m_pCallbacks, m_pArray); + m_Capacity = newCapacity; + m_pArray = newArray; + } +} + +template +void VmaVector::resize(size_t newCount) +{ + size_t newCapacity = m_Capacity; + if (newCount > m_Capacity) + { + newCapacity = VMA_MAX(newCount, VMA_MAX(m_Capacity * 3 / 2, (size_t)8)); + } + + if (newCapacity != m_Capacity) + { + T* const newArray = newCapacity ? VmaAllocateArray(m_Allocator.m_pCallbacks, newCapacity) : VMA_NULL; + const size_t elementsToCopy = VMA_MIN(m_Count, newCount); + if (elementsToCopy != 0) + { + memcpy(newArray, m_pArray, elementsToCopy * sizeof(T)); + } + VmaFree(m_Allocator.m_pCallbacks, m_pArray); + m_Capacity = newCapacity; + m_pArray = newArray; + } + + m_Count = newCount; +} + +template +void VmaVector::shrink_to_fit() +{ + if (m_Capacity > m_Count) + { + T* newArray = VMA_NULL; + if (m_Count > 0) + { + newArray = VmaAllocateArray(m_Allocator.m_pCallbacks, m_Count); + memcpy(newArray, m_pArray, m_Count * sizeof(T)); + } + VmaFree(m_Allocator.m_pCallbacks, m_pArray); + m_Capacity = m_Count; + m_pArray = newArray; + } +} + +template +void VmaVector::insert(size_t index, const T& src) +{ + VMA_HEAVY_ASSERT(index <= m_Count); + const size_t oldCount = size(); + resize(oldCount + 1); + if (index < oldCount) + { + memmove(m_pArray + (index + 1), m_pArray + index, (oldCount - index) * sizeof(T)); + } + m_pArray[index] = src; +} + +template +void VmaVector::remove(size_t index) +{ + VMA_HEAVY_ASSERT(index < m_Count); + const size_t oldCount = size(); + if (index < oldCount - 1) + { + memmove(m_pArray + index, m_pArray + (index + 1), (oldCount - index - 1) * sizeof(T)); + } + resize(oldCount - 1); +} +#endif // _VMA_VECTOR_FUNCTIONS + +template +static void VmaVectorInsert(VmaVector& vec, size_t index, const T& item) +{ + vec.insert(index, item); +} + +template +static void VmaVectorRemove(VmaVector& vec, size_t index) +{ + vec.remove(index); +} +#endif // _VMA_VECTOR + +#ifndef _VMA_SMALL_VECTOR +/* +This is a vector (a variable-sized array), optimized for the case when the array is small. + +It contains some number of elements in-place, which allows it to avoid heap allocation +when the actual number of elements is below that threshold. This allows normal "small" +cases to be fast without losing generality for large inputs. +*/ +template +class VmaSmallVector +{ +public: + typedef T value_type; + typedef T* iterator; + + explicit VmaSmallVector(const AllocatorT& allocator); + VmaSmallVector(size_t count, const AllocatorT& allocator); + template + explicit VmaSmallVector(const VmaSmallVector&) = delete; + template + VmaSmallVector& operator=(const VmaSmallVector&) = delete; + ~VmaSmallVector() = default; + + bool empty() const { return m_Count == 0; } + size_t size() const { return m_Count; } + T* data() { return m_Count > N ? m_DynamicArray.data() : m_StaticArray; } + T& front() { VMA_HEAVY_ASSERT(m_Count > 0); return data()[0]; } + T& back() { VMA_HEAVY_ASSERT(m_Count > 0); return data()[m_Count - 1]; } + const T* data() const { return m_Count > N ? m_DynamicArray.data() : m_StaticArray; } + const T& front() const { VMA_HEAVY_ASSERT(m_Count > 0); return data()[0]; } + const T& back() const { VMA_HEAVY_ASSERT(m_Count > 0); return data()[m_Count - 1]; } + + iterator begin() { return data(); } + iterator end() { return data() + m_Count; } + + void pop_front() { VMA_HEAVY_ASSERT(m_Count > 0); remove(0); } + void pop_back() { VMA_HEAVY_ASSERT(m_Count > 0); resize(size() - 1); } + void push_front(const T& src) { insert(0, src); } + + void push_back(const T& src); + void resize(size_t newCount, bool freeMemory = false); + void clear(bool freeMemory = false); + void insert(size_t index, const T& src); + void remove(size_t index); + + T& operator[](size_t index) { VMA_HEAVY_ASSERT(index < m_Count); return data()[index]; } + const T& operator[](size_t index) const { VMA_HEAVY_ASSERT(index < m_Count); return data()[index]; } + +private: + size_t m_Count; + T m_StaticArray[N]; // Used when m_Size <= N + VmaVector m_DynamicArray; // Used when m_Size > N +}; + +#ifndef _VMA_SMALL_VECTOR_FUNCTIONS +template +VmaSmallVector::VmaSmallVector(const AllocatorT& allocator) + : m_Count(0), + m_DynamicArray(allocator) {} + +template +VmaSmallVector::VmaSmallVector(size_t count, const AllocatorT& allocator) + : m_Count(count), + m_DynamicArray(count > N ? count : 0, allocator) {} + +template +void VmaSmallVector::push_back(const T& src) +{ + const size_t newIndex = size(); + resize(newIndex + 1); + data()[newIndex] = src; +} + +template +void VmaSmallVector::resize(size_t newCount, bool freeMemory) +{ + if (newCount > N && m_Count > N) + { + // Any direction, staying in m_DynamicArray + m_DynamicArray.resize(newCount); + if (freeMemory) + { + m_DynamicArray.shrink_to_fit(); + } + } + else if (newCount > N && m_Count <= N) + { + // Growing, moving from m_StaticArray to m_DynamicArray + m_DynamicArray.resize(newCount); + if (m_Count > 0) + { + memcpy(m_DynamicArray.data(), m_StaticArray, m_Count * sizeof(T)); + } + } + else if (newCount <= N && m_Count > N) + { + // Shrinking, moving from m_DynamicArray to m_StaticArray + if (newCount > 0) + { + memcpy(m_StaticArray, m_DynamicArray.data(), newCount * sizeof(T)); + } + m_DynamicArray.resize(0); + if (freeMemory) + { + m_DynamicArray.shrink_to_fit(); + } + } + else + { + // Any direction, staying in m_StaticArray - nothing to do here + } + m_Count = newCount; +} + +template +void VmaSmallVector::clear(bool freeMemory) +{ + m_DynamicArray.clear(); + if (freeMemory) + { + m_DynamicArray.shrink_to_fit(); + } + m_Count = 0; +} + +template +void VmaSmallVector::insert(size_t index, const T& src) +{ + VMA_HEAVY_ASSERT(index <= m_Count); + const size_t oldCount = size(); + resize(oldCount + 1); + T* const dataPtr = data(); + if (index < oldCount) + { + // I know, this could be more optimal for case where memmove can be memcpy directly from m_StaticArray to m_DynamicArray. + memmove(dataPtr + (index + 1), dataPtr + index, (oldCount - index) * sizeof(T)); + } + dataPtr[index] = src; +} + +template +void VmaSmallVector::remove(size_t index) +{ + VMA_HEAVY_ASSERT(index < m_Count); + const size_t oldCount = size(); + if (index < oldCount - 1) + { + // I know, this could be more optimal for case where memmove can be memcpy directly from m_DynamicArray to m_StaticArray. + T* const dataPtr = data(); + memmove(dataPtr + index, dataPtr + (index + 1), (oldCount - index - 1) * sizeof(T)); + } + resize(oldCount - 1); +} +#endif // _VMA_SMALL_VECTOR_FUNCTIONS +#endif // _VMA_SMALL_VECTOR + +#ifndef _VMA_POOL_ALLOCATOR +/* +Allocator for objects of type T using a list of arrays (pools) to speed up +allocation. Number of elements that can be allocated is not bounded because +allocator can create multiple blocks. +*/ +template +class VmaPoolAllocator +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaPoolAllocator) +public: + VmaPoolAllocator(const VkAllocationCallbacks* pAllocationCallbacks, uint32_t firstBlockCapacity); + ~VmaPoolAllocator(); + template T* Alloc(Types&&... args); + void Free(T* ptr); + +private: + union Item + { + uint32_t NextFreeIndex; + alignas(T) char Value[sizeof(T)]; + }; + struct ItemBlock + { + Item* pItems; + uint32_t Capacity; + uint32_t FirstFreeIndex; + }; + + const VkAllocationCallbacks* m_pAllocationCallbacks; + const uint32_t m_FirstBlockCapacity; + VmaVector> m_ItemBlocks; + + ItemBlock& CreateNewBlock(); +}; + +#ifndef _VMA_POOL_ALLOCATOR_FUNCTIONS +template +VmaPoolAllocator::VmaPoolAllocator(const VkAllocationCallbacks* pAllocationCallbacks, uint32_t firstBlockCapacity) + : m_pAllocationCallbacks(pAllocationCallbacks), + m_FirstBlockCapacity(firstBlockCapacity), + m_ItemBlocks(VmaStlAllocator(pAllocationCallbacks)) +{ + VMA_ASSERT(m_FirstBlockCapacity > 1); +} + +template +VmaPoolAllocator::~VmaPoolAllocator() +{ + for (size_t i = m_ItemBlocks.size(); i--;) + vma_delete_array(m_pAllocationCallbacks, m_ItemBlocks[i].pItems, m_ItemBlocks[i].Capacity); + m_ItemBlocks.clear(); +} + +template +template T* VmaPoolAllocator::Alloc(Types&&... args) +{ + for (size_t i = m_ItemBlocks.size(); i--; ) + { + ItemBlock& block = m_ItemBlocks[i]; + // This block has some free items: Use first one. + if (block.FirstFreeIndex != UINT32_MAX) + { + Item* const pItem = &block.pItems[block.FirstFreeIndex]; + block.FirstFreeIndex = pItem->NextFreeIndex; + T* result = (T*)&pItem->Value; + new(result)T(std::forward(args)...); // Explicit constructor call. + return result; + } + } + + // No block has free item: Create new one and use it. + ItemBlock& newBlock = CreateNewBlock(); + Item* const pItem = &newBlock.pItems[0]; + newBlock.FirstFreeIndex = pItem->NextFreeIndex; + T* result = (T*)&pItem->Value; + new(result) T(std::forward(args)...); // Explicit constructor call. + return result; +} + +template +void VmaPoolAllocator::Free(T* ptr) +{ + // Search all memory blocks to find ptr. + for (size_t i = m_ItemBlocks.size(); i--; ) + { + ItemBlock& block = m_ItemBlocks[i]; + + // Casting to union. + Item* pItemPtr = VMA_NULL; + memcpy(&pItemPtr, &ptr, sizeof(pItemPtr)); + + // Check if pItemPtr is in address range of this block. + if ((pItemPtr >= block.pItems) && (pItemPtr < block.pItems + block.Capacity)) + { + ptr->~T(); // Explicit destructor call. + const uint32_t index = static_cast(pItemPtr - block.pItems); + pItemPtr->NextFreeIndex = block.FirstFreeIndex; + block.FirstFreeIndex = index; + return; + } + } + VMA_ASSERT(0 && "Pointer doesn't belong to this memory pool."); +} + +template +typename VmaPoolAllocator::ItemBlock& VmaPoolAllocator::CreateNewBlock() +{ + const uint32_t newBlockCapacity = m_ItemBlocks.empty() ? + m_FirstBlockCapacity : m_ItemBlocks.back().Capacity * 3 / 2; + + const ItemBlock newBlock = + { + vma_new_array(m_pAllocationCallbacks, Item, newBlockCapacity), + newBlockCapacity, + 0 + }; + + m_ItemBlocks.push_back(newBlock); + + // Setup singly-linked list of all free items in this block. + for (uint32_t i = 0; i < newBlockCapacity - 1; ++i) + newBlock.pItems[i].NextFreeIndex = i + 1; + newBlock.pItems[newBlockCapacity - 1].NextFreeIndex = UINT32_MAX; + return m_ItemBlocks.back(); +} +#endif // _VMA_POOL_ALLOCATOR_FUNCTIONS +#endif // _VMA_POOL_ALLOCATOR + +#ifndef _VMA_RAW_LIST +template +struct VmaListItem +{ + VmaListItem* pPrev; + VmaListItem* pNext; + T Value; +}; + +// Doubly linked list. +template +class VmaRawList +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaRawList) +public: + typedef VmaListItem ItemType; + + explicit VmaRawList(const VkAllocationCallbacks* pAllocationCallbacks); + // Intentionally not calling Clear, because that would be unnecessary + // computations to return all items to m_ItemAllocator as free. + ~VmaRawList() = default; + + size_t GetCount() const { return m_Count; } + bool IsEmpty() const { return m_Count == 0; } + + ItemType* Front() { return m_pFront; } + ItemType* Back() { return m_pBack; } + const ItemType* Front() const { return m_pFront; } + const ItemType* Back() const { return m_pBack; } + + ItemType* PushFront(); + ItemType* PushBack(); + ItemType* PushFront(const T& value); + ItemType* PushBack(const T& value); + void PopFront(); + void PopBack(); + + // Item can be null - it means PushBack. + ItemType* InsertBefore(ItemType* pItem); + // Item can be null - it means PushFront. + ItemType* InsertAfter(ItemType* pItem); + ItemType* InsertBefore(ItemType* pItem, const T& value); + ItemType* InsertAfter(ItemType* pItem, const T& value); + + void Clear(); + void Remove(ItemType* pItem); + +private: + const VkAllocationCallbacks* const m_pAllocationCallbacks; + VmaPoolAllocator m_ItemAllocator; + ItemType* m_pFront; + ItemType* m_pBack; + size_t m_Count; +}; + +#ifndef _VMA_RAW_LIST_FUNCTIONS +template +VmaRawList::VmaRawList(const VkAllocationCallbacks* pAllocationCallbacks) + : m_pAllocationCallbacks(pAllocationCallbacks), + m_ItemAllocator(pAllocationCallbacks, 128), + m_pFront(VMA_NULL), + m_pBack(VMA_NULL), + m_Count(0) {} + +template +VmaListItem* VmaRawList::PushFront() +{ + ItemType* const pNewItem = m_ItemAllocator.Alloc(); + pNewItem->pPrev = VMA_NULL; + if (IsEmpty()) + { + pNewItem->pNext = VMA_NULL; + m_pFront = pNewItem; + m_pBack = pNewItem; + m_Count = 1; + } + else + { + pNewItem->pNext = m_pFront; + m_pFront->pPrev = pNewItem; + m_pFront = pNewItem; + ++m_Count; + } + return pNewItem; +} + +template +VmaListItem* VmaRawList::PushBack() +{ + ItemType* const pNewItem = m_ItemAllocator.Alloc(); + pNewItem->pNext = VMA_NULL; + if(IsEmpty()) + { + pNewItem->pPrev = VMA_NULL; + m_pFront = pNewItem; + m_pBack = pNewItem; + m_Count = 1; + } + else + { + pNewItem->pPrev = m_pBack; + m_pBack->pNext = pNewItem; + m_pBack = pNewItem; + ++m_Count; + } + return pNewItem; +} + +template +VmaListItem* VmaRawList::PushFront(const T& value) +{ + ItemType* const pNewItem = PushFront(); + pNewItem->Value = value; + return pNewItem; +} + +template +VmaListItem* VmaRawList::PushBack(const T& value) +{ + ItemType* const pNewItem = PushBack(); + pNewItem->Value = value; + return pNewItem; +} + +template +void VmaRawList::PopFront() +{ + VMA_HEAVY_ASSERT(m_Count > 0); + ItemType* const pFrontItem = m_pFront; + ItemType* const pNextItem = pFrontItem->pNext; + if (pNextItem != VMA_NULL) + { + pNextItem->pPrev = VMA_NULL; + } + m_pFront = pNextItem; + m_ItemAllocator.Free(pFrontItem); + --m_Count; +} + +template +void VmaRawList::PopBack() +{ + VMA_HEAVY_ASSERT(m_Count > 0); + ItemType* const pBackItem = m_pBack; + ItemType* const pPrevItem = pBackItem->pPrev; + if(pPrevItem != VMA_NULL) + { + pPrevItem->pNext = VMA_NULL; + } + m_pBack = pPrevItem; + m_ItemAllocator.Free(pBackItem); + --m_Count; +} + +template +void VmaRawList::Clear() +{ + if (!IsEmpty()) + { + ItemType* pItem = m_pBack; + while (pItem != VMA_NULL) + { + ItemType* const pPrevItem = pItem->pPrev; + m_ItemAllocator.Free(pItem); + pItem = pPrevItem; + } + m_pFront = VMA_NULL; + m_pBack = VMA_NULL; + m_Count = 0; + } +} + +template +void VmaRawList::Remove(ItemType* pItem) +{ + VMA_HEAVY_ASSERT(pItem != VMA_NULL); + VMA_HEAVY_ASSERT(m_Count > 0); + + if(pItem->pPrev != VMA_NULL) + { + pItem->pPrev->pNext = pItem->pNext; + } + else + { + VMA_HEAVY_ASSERT(m_pFront == pItem); + m_pFront = pItem->pNext; + } + + if(pItem->pNext != VMA_NULL) + { + pItem->pNext->pPrev = pItem->pPrev; + } + else + { + VMA_HEAVY_ASSERT(m_pBack == pItem); + m_pBack = pItem->pPrev; + } + + m_ItemAllocator.Free(pItem); + --m_Count; +} + +template +VmaListItem* VmaRawList::InsertBefore(ItemType* pItem) +{ + if(pItem != VMA_NULL) + { + ItemType* const prevItem = pItem->pPrev; + ItemType* const newItem = m_ItemAllocator.Alloc(); + newItem->pPrev = prevItem; + newItem->pNext = pItem; + pItem->pPrev = newItem; + if(prevItem != VMA_NULL) + { + prevItem->pNext = newItem; + } + else + { + VMA_HEAVY_ASSERT(m_pFront == pItem); + m_pFront = newItem; + } + ++m_Count; + return newItem; + } + return PushBack(); +} + +template +VmaListItem* VmaRawList::InsertAfter(ItemType* pItem) +{ + if(pItem != VMA_NULL) + { + ItemType* const nextItem = pItem->pNext; + ItemType* const newItem = m_ItemAllocator.Alloc(); + newItem->pNext = nextItem; + newItem->pPrev = pItem; + pItem->pNext = newItem; + if(nextItem != VMA_NULL) + { + nextItem->pPrev = newItem; + } + else + { + VMA_HEAVY_ASSERT(m_pBack == pItem); + m_pBack = newItem; + } + ++m_Count; + return newItem; + } + return PushFront(); +} + +template +VmaListItem* VmaRawList::InsertBefore(ItemType* pItem, const T& value) +{ + ItemType* const newItem = InsertBefore(pItem); + newItem->Value = value; + return newItem; +} + +template +VmaListItem* VmaRawList::InsertAfter(ItemType* pItem, const T& value) +{ + ItemType* const newItem = InsertAfter(pItem); + newItem->Value = value; + return newItem; +} +#endif // _VMA_RAW_LIST_FUNCTIONS +#endif // _VMA_RAW_LIST + +#ifndef _VMA_LIST +template +class VmaList +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaList) +public: + class reverse_iterator; + class const_iterator; + class const_reverse_iterator; + + class iterator + { + friend class const_iterator; + friend class VmaList; + public: + iterator() : m_pList(VMA_NULL), m_pItem(VMA_NULL) {} + explicit iterator(const reverse_iterator& src) : m_pList(src.m_pList), m_pItem(src.m_pItem) {} + + T& operator*() const { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); return m_pItem->Value; } + T* operator->() const { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); return &m_pItem->Value; } + + bool operator==(const iterator& rhs) const { VMA_HEAVY_ASSERT(m_pList == rhs.m_pList); return m_pItem == rhs.m_pItem; } + bool operator!=(const iterator& rhs) const { VMA_HEAVY_ASSERT(m_pList == rhs.m_pList); return m_pItem != rhs.m_pItem; } + + const iterator operator++(int) { iterator result = *this; ++*this; return result; } + const iterator operator--(int) { iterator result = *this; --*this; return result; } + + iterator& operator++() { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); m_pItem = m_pItem->pNext; return *this; } + iterator& operator--(); + + private: + VmaRawList* m_pList; + VmaListItem* m_pItem; + + iterator(VmaRawList* pList, VmaListItem* pItem) : m_pList(pList), m_pItem(pItem) {} + }; + class reverse_iterator + { + friend class const_reverse_iterator; + friend class VmaList; + public: + reverse_iterator() : m_pList(VMA_NULL), m_pItem(VMA_NULL) {} + explicit reverse_iterator(const iterator& src) : m_pList(src.m_pList), m_pItem(src.m_pItem) {} + + T& operator*() const { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); return m_pItem->Value; } + T* operator->() const { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); return &m_pItem->Value; } + + bool operator==(const reverse_iterator& rhs) const { VMA_HEAVY_ASSERT(m_pList == rhs.m_pList); return m_pItem == rhs.m_pItem; } + bool operator!=(const reverse_iterator& rhs) const { VMA_HEAVY_ASSERT(m_pList == rhs.m_pList); return m_pItem != rhs.m_pItem; } + + const reverse_iterator operator++(int) { reverse_iterator result = *this; ++* this; return result; } + const reverse_iterator operator--(int) { reverse_iterator result = *this; --* this; return result; } + + reverse_iterator& operator++() { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); m_pItem = m_pItem->pPrev; return *this; } + reverse_iterator& operator--(); + + private: + VmaRawList* m_pList; + VmaListItem* m_pItem; + + reverse_iterator(VmaRawList* pList, VmaListItem* pItem) : m_pList(pList), m_pItem(pItem) {} + }; + class const_iterator + { + friend class VmaList; + public: + const_iterator() : m_pList(VMA_NULL), m_pItem(VMA_NULL) {} + explicit const_iterator(const iterator& src) : m_pList(src.m_pList), m_pItem(src.m_pItem) {} + explicit const_iterator(const reverse_iterator& src) : m_pList(src.m_pList), m_pItem(src.m_pItem) {} + + iterator drop_const() { return { const_cast*>(m_pList), const_cast*>(m_pItem) }; } + + const T& operator*() const { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); return m_pItem->Value; } + const T* operator->() const { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); return &m_pItem->Value; } + + bool operator==(const const_iterator& rhs) const { VMA_HEAVY_ASSERT(m_pList == rhs.m_pList); return m_pItem == rhs.m_pItem; } + bool operator!=(const const_iterator& rhs) const { VMA_HEAVY_ASSERT(m_pList == rhs.m_pList); return m_pItem != rhs.m_pItem; } + + const const_iterator operator++(int) { const_iterator result = *this; ++* this; return result; } + const const_iterator operator--(int) { const_iterator result = *this; --* this; return result; } + + const_iterator& operator++() { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); m_pItem = m_pItem->pNext; return *this; } + const_iterator& operator--(); + + private: + const VmaRawList* m_pList; + const VmaListItem* m_pItem; + + const_iterator(const VmaRawList* pList, const VmaListItem* pItem) : m_pList(pList), m_pItem(pItem) {} + }; + class const_reverse_iterator + { + friend class VmaList; + public: + const_reverse_iterator() : m_pList(VMA_NULL), m_pItem(VMA_NULL) {} + explicit const_reverse_iterator(const reverse_iterator& src) : m_pList(src.m_pList), m_pItem(src.m_pItem) {} + explicit const_reverse_iterator(const iterator& src) : m_pList(src.m_pList), m_pItem(src.m_pItem) {} + + reverse_iterator drop_const() { return { const_cast*>(m_pList), const_cast*>(m_pItem) }; } + + const T& operator*() const { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); return m_pItem->Value; } + const T* operator->() const { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); return &m_pItem->Value; } + + bool operator==(const const_reverse_iterator& rhs) const { VMA_HEAVY_ASSERT(m_pList == rhs.m_pList); return m_pItem == rhs.m_pItem; } + bool operator!=(const const_reverse_iterator& rhs) const { VMA_HEAVY_ASSERT(m_pList == rhs.m_pList); return m_pItem != rhs.m_pItem; } + + const const_reverse_iterator operator++(int) { const_reverse_iterator result = *this; ++* this; return result; } + const const_reverse_iterator operator--(int) { const_reverse_iterator result = *this; --* this; return result; } + + const_reverse_iterator& operator++() { VMA_HEAVY_ASSERT(m_pItem != VMA_NULL); m_pItem = m_pItem->pPrev; return *this; } + const_reverse_iterator& operator--(); + + private: + const VmaRawList* m_pList; + const VmaListItem* m_pItem; + + const_reverse_iterator(const VmaRawList* pList, const VmaListItem* pItem) : m_pList(pList), m_pItem(pItem) {} + }; + + explicit VmaList(const AllocatorT& allocator) : m_RawList(allocator.m_pCallbacks) {} + + bool empty() const { return m_RawList.IsEmpty(); } + size_t size() const { return m_RawList.GetCount(); } + + iterator begin() { return iterator(&m_RawList, m_RawList.Front()); } + iterator end() { return iterator(&m_RawList, VMA_NULL); } + + const_iterator cbegin() const { return const_iterator(&m_RawList, m_RawList.Front()); } + const_iterator cend() const { return const_iterator(&m_RawList, VMA_NULL); } + + const_iterator begin() const { return cbegin(); } + const_iterator end() const { return cend(); } + + reverse_iterator rbegin() { return reverse_iterator(&m_RawList, m_RawList.Back()); } + reverse_iterator rend() { return reverse_iterator(&m_RawList, VMA_NULL); } + + const_reverse_iterator crbegin() const { return const_reverse_iterator(&m_RawList, m_RawList.Back()); } + const_reverse_iterator crend() const { return const_reverse_iterator(&m_RawList, VMA_NULL); } + + const_reverse_iterator rbegin() const { return crbegin(); } + const_reverse_iterator rend() const { return crend(); } + + void push_back(const T& value) { m_RawList.PushBack(value); } + iterator insert(iterator it, const T& value) { return iterator(&m_RawList, m_RawList.InsertBefore(it.m_pItem, value)); } + + void clear() { m_RawList.Clear(); } + void erase(iterator it) { m_RawList.Remove(it.m_pItem); } + +private: + VmaRawList m_RawList; +}; + +#ifndef _VMA_LIST_FUNCTIONS +template +typename VmaList::iterator& VmaList::iterator::operator--() +{ + if (m_pItem != VMA_NULL) + { + m_pItem = m_pItem->pPrev; + } + else + { + VMA_HEAVY_ASSERT(!m_pList->IsEmpty()); + m_pItem = m_pList->Back(); + } + return *this; +} + +template +typename VmaList::reverse_iterator& VmaList::reverse_iterator::operator--() +{ + if (m_pItem != VMA_NULL) + { + m_pItem = m_pItem->pNext; + } + else + { + VMA_HEAVY_ASSERT(!m_pList->IsEmpty()); + m_pItem = m_pList->Front(); + } + return *this; +} + +template +typename VmaList::const_iterator& VmaList::const_iterator::operator--() +{ + if (m_pItem != VMA_NULL) + { + m_pItem = m_pItem->pPrev; + } + else + { + VMA_HEAVY_ASSERT(!m_pList->IsEmpty()); + m_pItem = m_pList->Back(); + } + return *this; +} + +template +typename VmaList::const_reverse_iterator& VmaList::const_reverse_iterator::operator--() +{ + if (m_pItem != VMA_NULL) + { + m_pItem = m_pItem->pNext; + } + else + { + VMA_HEAVY_ASSERT(!m_pList->IsEmpty()); + m_pItem = m_pList->Back(); + } + return *this; +} +#endif // _VMA_LIST_FUNCTIONS +#endif // _VMA_LIST + +#ifndef _VMA_INTRUSIVE_LINKED_LIST +/* +Expected interface of ItemTypeTraits: +struct MyItemTypeTraits +{ + typedef MyItem ItemType; + static ItemType* GetPrev(const ItemType* item) { return item->myPrevPtr; } + static ItemType* GetNext(const ItemType* item) { return item->myNextPtr; } + static ItemType*& AccessPrev(ItemType* item) { return item->myPrevPtr; } + static ItemType*& AccessNext(ItemType* item) { return item->myNextPtr; } +}; +*/ +template +class VmaIntrusiveLinkedList +{ +public: + typedef typename ItemTypeTraits::ItemType ItemType; + static ItemType* GetPrev(const ItemType* item) { return ItemTypeTraits::GetPrev(item); } + static ItemType* GetNext(const ItemType* item) { return ItemTypeTraits::GetNext(item); } + + // Movable, not copyable. + VmaIntrusiveLinkedList() = default; + VmaIntrusiveLinkedList(VmaIntrusiveLinkedList && src) noexcept; + VmaIntrusiveLinkedList(const VmaIntrusiveLinkedList&) = delete; + VmaIntrusiveLinkedList& operator=(VmaIntrusiveLinkedList&& src) noexcept; + VmaIntrusiveLinkedList& operator=(const VmaIntrusiveLinkedList&) = delete; + ~VmaIntrusiveLinkedList() { VMA_HEAVY_ASSERT(IsEmpty()); } + + size_t GetCount() const { return m_Count; } + bool IsEmpty() const { return m_Count == 0; } + ItemType* Front() { return m_Front; } + ItemType* Back() { return m_Back; } + const ItemType* Front() const { return m_Front; } + const ItemType* Back() const { return m_Back; } + + void PushBack(ItemType* item); + void PushFront(ItemType* item); + ItemType* PopBack(); + ItemType* PopFront(); + + // MyItem can be null - it means PushBack. + void InsertBefore(ItemType* existingItem, ItemType* newItem); + // MyItem can be null - it means PushFront. + void InsertAfter(ItemType* existingItem, ItemType* newItem); + void Remove(ItemType* item); + void RemoveAll(); + +private: + ItemType* m_Front = VMA_NULL; + ItemType* m_Back = VMA_NULL; + size_t m_Count = 0; +}; + +#ifndef _VMA_INTRUSIVE_LINKED_LIST_FUNCTIONS +template +VmaIntrusiveLinkedList::VmaIntrusiveLinkedList(VmaIntrusiveLinkedList&& src) noexcept + : m_Front(src.m_Front), m_Back(src.m_Back), m_Count(src.m_Count) +{ + src.m_Front = src.m_Back = VMA_NULL; + src.m_Count = 0; +} + +template +VmaIntrusiveLinkedList& VmaIntrusiveLinkedList::operator=(VmaIntrusiveLinkedList&& src) noexcept +{ + if (&src != this) + { + VMA_HEAVY_ASSERT(IsEmpty()); + m_Front = src.m_Front; + m_Back = src.m_Back; + m_Count = src.m_Count; + src.m_Front = src.m_Back = VMA_NULL; + src.m_Count = 0; + } + return *this; +} + +template +void VmaIntrusiveLinkedList::PushBack(ItemType* item) +{ + VMA_HEAVY_ASSERT(ItemTypeTraits::GetPrev(item) == VMA_NULL && ItemTypeTraits::GetNext(item) == VMA_NULL); + if (IsEmpty()) + { + m_Front = item; + m_Back = item; + m_Count = 1; + } + else + { + ItemTypeTraits::AccessPrev(item) = m_Back; + ItemTypeTraits::AccessNext(m_Back) = item; + m_Back = item; + ++m_Count; + } +} + +template +void VmaIntrusiveLinkedList::PushFront(ItemType* item) +{ + VMA_HEAVY_ASSERT(ItemTypeTraits::GetPrev(item) == VMA_NULL && ItemTypeTraits::GetNext(item) == VMA_NULL); + if (IsEmpty()) + { + m_Front = item; + m_Back = item; + m_Count = 1; + } + else + { + ItemTypeTraits::AccessNext(item) = m_Front; + ItemTypeTraits::AccessPrev(m_Front) = item; + m_Front = item; + ++m_Count; + } +} + +template +typename VmaIntrusiveLinkedList::ItemType* VmaIntrusiveLinkedList::PopBack() +{ + VMA_HEAVY_ASSERT(m_Count > 0); + ItemType* const backItem = m_Back; + ItemType* const prevItem = ItemTypeTraits::GetPrev(backItem); + if (prevItem != VMA_NULL) + { + ItemTypeTraits::AccessNext(prevItem) = VMA_NULL; + } + m_Back = prevItem; + --m_Count; + ItemTypeTraits::AccessPrev(backItem) = VMA_NULL; + ItemTypeTraits::AccessNext(backItem) = VMA_NULL; + return backItem; +} + +template +typename VmaIntrusiveLinkedList::ItemType* VmaIntrusiveLinkedList::PopFront() +{ + VMA_HEAVY_ASSERT(m_Count > 0); + ItemType* const frontItem = m_Front; + ItemType* const nextItem = ItemTypeTraits::GetNext(frontItem); + if (nextItem != VMA_NULL) + { + ItemTypeTraits::AccessPrev(nextItem) = VMA_NULL; + } + m_Front = nextItem; + --m_Count; + ItemTypeTraits::AccessPrev(frontItem) = VMA_NULL; + ItemTypeTraits::AccessNext(frontItem) = VMA_NULL; + return frontItem; +} + +template +void VmaIntrusiveLinkedList::InsertBefore(ItemType* existingItem, ItemType* newItem) +{ + VMA_HEAVY_ASSERT(newItem != VMA_NULL && ItemTypeTraits::GetPrev(newItem) == VMA_NULL && ItemTypeTraits::GetNext(newItem) == VMA_NULL); + if (existingItem != VMA_NULL) + { + ItemType* const prevItem = ItemTypeTraits::GetPrev(existingItem); + ItemTypeTraits::AccessPrev(newItem) = prevItem; + ItemTypeTraits::AccessNext(newItem) = existingItem; + ItemTypeTraits::AccessPrev(existingItem) = newItem; + if (prevItem != VMA_NULL) + { + ItemTypeTraits::AccessNext(prevItem) = newItem; + } + else + { + VMA_HEAVY_ASSERT(m_Front == existingItem); + m_Front = newItem; + } + ++m_Count; + } + else + PushBack(newItem); +} + +template +void VmaIntrusiveLinkedList::InsertAfter(ItemType* existingItem, ItemType* newItem) +{ + VMA_HEAVY_ASSERT(newItem != VMA_NULL && ItemTypeTraits::GetPrev(newItem) == VMA_NULL && ItemTypeTraits::GetNext(newItem) == VMA_NULL); + if (existingItem != VMA_NULL) + { + ItemType* const nextItem = ItemTypeTraits::GetNext(existingItem); + ItemTypeTraits::AccessNext(newItem) = nextItem; + ItemTypeTraits::AccessPrev(newItem) = existingItem; + ItemTypeTraits::AccessNext(existingItem) = newItem; + if (nextItem != VMA_NULL) + { + ItemTypeTraits::AccessPrev(nextItem) = newItem; + } + else + { + VMA_HEAVY_ASSERT(m_Back == existingItem); + m_Back = newItem; + } + ++m_Count; + } + else + return PushFront(newItem); +} + +template +void VmaIntrusiveLinkedList::Remove(ItemType* item) +{ + VMA_HEAVY_ASSERT(item != VMA_NULL && m_Count > 0); + if (ItemTypeTraits::GetPrev(item) != VMA_NULL) + { + ItemTypeTraits::AccessNext(ItemTypeTraits::AccessPrev(item)) = ItemTypeTraits::GetNext(item); + } + else + { + VMA_HEAVY_ASSERT(m_Front == item); + m_Front = ItemTypeTraits::GetNext(item); + } + + if (ItemTypeTraits::GetNext(item) != VMA_NULL) + { + ItemTypeTraits::AccessPrev(ItemTypeTraits::AccessNext(item)) = ItemTypeTraits::GetPrev(item); + } + else + { + VMA_HEAVY_ASSERT(m_Back == item); + m_Back = ItemTypeTraits::GetPrev(item); + } + ItemTypeTraits::AccessPrev(item) = VMA_NULL; + ItemTypeTraits::AccessNext(item) = VMA_NULL; + --m_Count; +} + +template +void VmaIntrusiveLinkedList::RemoveAll() +{ + if (!IsEmpty()) + { + ItemType* item = m_Back; + while (item != VMA_NULL) + { + ItemType* const prevItem = ItemTypeTraits::AccessPrev(item); + ItemTypeTraits::AccessPrev(item) = VMA_NULL; + ItemTypeTraits::AccessNext(item) = VMA_NULL; + item = prevItem; + } + m_Front = VMA_NULL; + m_Back = VMA_NULL; + m_Count = 0; + } +} +#endif // _VMA_INTRUSIVE_LINKED_LIST_FUNCTIONS +#endif // _VMA_INTRUSIVE_LINKED_LIST + +#if !defined(_VMA_STRING_BUILDER) && VMA_STATS_STRING_ENABLED +class VmaStringBuilder +{ +public: + explicit VmaStringBuilder(const VkAllocationCallbacks* allocationCallbacks) : m_Data(VmaStlAllocator(allocationCallbacks)) {} + ~VmaStringBuilder() = default; + + size_t GetLength() const { return m_Data.size(); } + // Returned string is not null-terminated! + const char* GetData() const { return m_Data.data(); } + void AddNewLine() { Add('\n'); } + void Add(char ch) { m_Data.push_back(ch); } + + void Add(const char* pStr); + void AddNumber(uint32_t num); + void AddNumber(uint64_t num); + void AddPointer(const void* ptr); + +private: + VmaVector> m_Data; +}; + +#ifndef _VMA_STRING_BUILDER_FUNCTIONS +void VmaStringBuilder::Add(const char* pStr) +{ + const size_t strLen = strlen(pStr); + if (strLen > 0) + { + const size_t oldCount = m_Data.size(); + m_Data.resize(oldCount + strLen); + memcpy(m_Data.data() + oldCount, pStr, strLen); + } +} + +void VmaStringBuilder::AddNumber(uint32_t num) +{ + char buf[11]; + buf[10] = '\0'; + char* p = &buf[10]; + do + { + *--p = '0' + (char)(num % 10); + num /= 10; + } while (num); + Add(p); +} + +void VmaStringBuilder::AddNumber(uint64_t num) +{ + char buf[21]; + buf[20] = '\0'; + char* p = &buf[20]; + do + { + *--p = '0' + (char)(num % 10); + num /= 10; + } while (num); + Add(p); +} + +void VmaStringBuilder::AddPointer(const void* ptr) +{ + char buf[21]; + VmaPtrToStr(buf, sizeof(buf), ptr); + Add(buf); +} +#endif //_VMA_STRING_BUILDER_FUNCTIONS +#endif // _VMA_STRING_BUILDER + +#if !defined(_VMA_JSON_WRITER) && VMA_STATS_STRING_ENABLED +/* +Allows to conveniently build a correct JSON document to be written to the +VmaStringBuilder passed to the constructor. +*/ +class VmaJsonWriter +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaJsonWriter) +public: + // sb - string builder to write the document to. Must remain alive for the whole lifetime of this object. + VmaJsonWriter(const VkAllocationCallbacks* pAllocationCallbacks, VmaStringBuilder& sb); + ~VmaJsonWriter(); + + // Begins object by writing "{". + // Inside an object, you must call pairs of WriteString and a value, e.g.: + // j.BeginObject(true); j.WriteString("A"); j.WriteNumber(1); j.WriteString("B"); j.WriteNumber(2); j.EndObject(); + // Will write: { "A": 1, "B": 2 } + void BeginObject(bool singleLine = false); + // Ends object by writing "}". + void EndObject(); + + // Begins array by writing "[". + // Inside an array, you can write a sequence of any values. + void BeginArray(bool singleLine = false); + // Ends array by writing "[". + void EndArray(); + + // Writes a string value inside "". + // pStr can contain any ANSI characters, including '"', new line etc. - they will be properly escaped. + void WriteString(const char* pStr); + + // Begins writing a string value. + // Call BeginString, ContinueString, ContinueString, ..., EndString instead of + // WriteString to conveniently build the string content incrementally, made of + // parts including numbers. + void BeginString(const char* pStr = VMA_NULL); + // Posts next part of an open string. + void ContinueString(const char* pStr); + // Posts next part of an open string. The number is converted to decimal characters. + void ContinueString(uint32_t n); + void ContinueString(uint64_t n); + // Posts next part of an open string. Pointer value is converted to characters + // using "%p" formatting - shown as hexadecimal number, e.g.: 000000081276Ad00 + void ContinueString_Pointer(const void* ptr); + // Ends writing a string value by writing '"'. + void EndString(const char* pStr = VMA_NULL); + + // Writes a number value. + void WriteNumber(uint32_t n); + void WriteNumber(uint64_t n); + // Writes a boolean value - false or true. + void WriteBool(bool b); + // Writes a null value. + void WriteNull(); + +private: + enum COLLECTION_TYPE + { + COLLECTION_TYPE_OBJECT, + COLLECTION_TYPE_ARRAY, + }; + struct StackItem + { + COLLECTION_TYPE type; + uint32_t valueCount; + bool singleLineMode; + }; + + static const char* const INDENT; + + VmaStringBuilder& m_SB; + VmaVector< StackItem, VmaStlAllocator > m_Stack; + bool m_InsideString; + + void BeginValue(bool isString); + void WriteIndent(bool oneLess = false); +}; +const char* const VmaJsonWriter::INDENT = " "; + +#ifndef _VMA_JSON_WRITER_FUNCTIONS +VmaJsonWriter::VmaJsonWriter(const VkAllocationCallbacks* pAllocationCallbacks, VmaStringBuilder& sb) + : m_SB(sb), + m_Stack(VmaStlAllocator(pAllocationCallbacks)), + m_InsideString(false) {} + +VmaJsonWriter::~VmaJsonWriter() +{ + VMA_ASSERT(!m_InsideString); + VMA_ASSERT(m_Stack.empty()); +} + +void VmaJsonWriter::BeginObject(bool singleLine) +{ + VMA_ASSERT(!m_InsideString); + + BeginValue(false); + m_SB.Add('{'); + + StackItem item; + item.type = COLLECTION_TYPE_OBJECT; + item.valueCount = 0; + item.singleLineMode = singleLine; + m_Stack.push_back(item); +} + +void VmaJsonWriter::EndObject() +{ + VMA_ASSERT(!m_InsideString); + + WriteIndent(true); + m_SB.Add('}'); + + VMA_ASSERT(!m_Stack.empty() && m_Stack.back().type == COLLECTION_TYPE_OBJECT); + m_Stack.pop_back(); +} + +void VmaJsonWriter::BeginArray(bool singleLine) +{ + VMA_ASSERT(!m_InsideString); + + BeginValue(false); + m_SB.Add('['); + + StackItem item; + item.type = COLLECTION_TYPE_ARRAY; + item.valueCount = 0; + item.singleLineMode = singleLine; + m_Stack.push_back(item); +} + +void VmaJsonWriter::EndArray() +{ + VMA_ASSERT(!m_InsideString); + + WriteIndent(true); + m_SB.Add(']'); + + VMA_ASSERT(!m_Stack.empty() && m_Stack.back().type == COLLECTION_TYPE_ARRAY); + m_Stack.pop_back(); +} + +void VmaJsonWriter::WriteString(const char* pStr) +{ + BeginString(pStr); + EndString(); +} + +void VmaJsonWriter::BeginString(const char* pStr) +{ + VMA_ASSERT(!m_InsideString); + + BeginValue(true); + m_SB.Add('"'); + m_InsideString = true; + if (pStr != VMA_NULL && pStr[0] != '\0') + { + ContinueString(pStr); + } +} + +void VmaJsonWriter::ContinueString(const char* pStr) +{ + VMA_ASSERT(m_InsideString); + + const size_t strLen = strlen(pStr); + for (size_t i = 0; i < strLen; ++i) + { + char ch = pStr[i]; + if (ch == '\\') + { + m_SB.Add("\\\\"); + } + else if (ch == '"') + { + m_SB.Add("\\\""); + } + else if ((uint8_t)ch >= 32) + { + m_SB.Add(ch); + } + else switch (ch) + { + case '\b': + m_SB.Add("\\b"); + break; + case '\f': + m_SB.Add("\\f"); + break; + case '\n': + m_SB.Add("\\n"); + break; + case '\r': + m_SB.Add("\\r"); + break; + case '\t': + m_SB.Add("\\t"); + break; + default: + VMA_ASSERT(0 && "Character not currently supported."); + } + } +} + +void VmaJsonWriter::ContinueString(uint32_t n) +{ + VMA_ASSERT(m_InsideString); + m_SB.AddNumber(n); +} + +void VmaJsonWriter::ContinueString(uint64_t n) +{ + VMA_ASSERT(m_InsideString); + m_SB.AddNumber(n); +} + +void VmaJsonWriter::ContinueString_Pointer(const void* ptr) +{ + VMA_ASSERT(m_InsideString); + m_SB.AddPointer(ptr); +} + +void VmaJsonWriter::EndString(const char* pStr) +{ + VMA_ASSERT(m_InsideString); + if (pStr != VMA_NULL && pStr[0] != '\0') + { + ContinueString(pStr); + } + m_SB.Add('"'); + m_InsideString = false; +} + +void VmaJsonWriter::WriteNumber(uint32_t n) +{ + VMA_ASSERT(!m_InsideString); + BeginValue(false); + m_SB.AddNumber(n); +} + +void VmaJsonWriter::WriteNumber(uint64_t n) +{ + VMA_ASSERT(!m_InsideString); + BeginValue(false); + m_SB.AddNumber(n); +} + +void VmaJsonWriter::WriteBool(bool b) +{ + VMA_ASSERT(!m_InsideString); + BeginValue(false); + m_SB.Add(b ? "true" : "false"); +} + +void VmaJsonWriter::WriteNull() +{ + VMA_ASSERT(!m_InsideString); + BeginValue(false); + m_SB.Add("null"); +} + +void VmaJsonWriter::BeginValue(bool isString) +{ + if (!m_Stack.empty()) + { + StackItem& currItem = m_Stack.back(); + if (currItem.type == COLLECTION_TYPE_OBJECT && + currItem.valueCount % 2 == 0) + { + VMA_ASSERT(isString); + } + + if (currItem.type == COLLECTION_TYPE_OBJECT && + currItem.valueCount % 2 != 0) + { + m_SB.Add(": "); + } + else if (currItem.valueCount > 0) + { + m_SB.Add(", "); + WriteIndent(); + } + else + { + WriteIndent(); + } + ++currItem.valueCount; + } +} + +void VmaJsonWriter::WriteIndent(bool oneLess) +{ + if (!m_Stack.empty() && !m_Stack.back().singleLineMode) + { + m_SB.AddNewLine(); + + size_t count = m_Stack.size(); + if (count > 0 && oneLess) + { + --count; + } + for (size_t i = 0; i < count; ++i) + { + m_SB.Add(INDENT); + } + } +} +#endif // _VMA_JSON_WRITER_FUNCTIONS + +static void VmaPrintDetailedStatistics(VmaJsonWriter& json, const VmaDetailedStatistics& stat) +{ + json.BeginObject(); + + json.WriteString("BlockCount"); + json.WriteNumber(stat.statistics.blockCount); + json.WriteString("BlockBytes"); + json.WriteNumber(stat.statistics.blockBytes); + json.WriteString("AllocationCount"); + json.WriteNumber(stat.statistics.allocationCount); + json.WriteString("AllocationBytes"); + json.WriteNumber(stat.statistics.allocationBytes); + json.WriteString("UnusedRangeCount"); + json.WriteNumber(stat.unusedRangeCount); + + if (stat.statistics.allocationCount > 1) + { + json.WriteString("AllocationSizeMin"); + json.WriteNumber(stat.allocationSizeMin); + json.WriteString("AllocationSizeMax"); + json.WriteNumber(stat.allocationSizeMax); + } + if (stat.unusedRangeCount > 1) + { + json.WriteString("UnusedRangeSizeMin"); + json.WriteNumber(stat.unusedRangeSizeMin); + json.WriteString("UnusedRangeSizeMax"); + json.WriteNumber(stat.unusedRangeSizeMax); + } + json.EndObject(); +} +#endif // _VMA_JSON_WRITER + +#ifndef _VMA_MAPPING_HYSTERESIS + +class VmaMappingHysteresis +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaMappingHysteresis) +public: + VmaMappingHysteresis() = default; + + uint32_t GetExtraMapping() const { return m_ExtraMapping; } + + // Call when Map was called. + // Returns true if switched to extra +1 mapping reference count. + bool PostMap() + { +#if VMA_MAPPING_HYSTERESIS_ENABLED + if(m_ExtraMapping == 0) + { + ++m_MajorCounter; + if(m_MajorCounter >= COUNTER_MIN_EXTRA_MAPPING) + { + m_ExtraMapping = 1; + m_MajorCounter = 0; + m_MinorCounter = 0; + return true; + } + } + else // m_ExtraMapping == 1 + PostMinorCounter(); +#endif // #if VMA_MAPPING_HYSTERESIS_ENABLED + return false; + } + + // Call when Unmap was called. + void PostUnmap() + { +#if VMA_MAPPING_HYSTERESIS_ENABLED + if(m_ExtraMapping == 0) + ++m_MajorCounter; + else // m_ExtraMapping == 1 + PostMinorCounter(); +#endif // #if VMA_MAPPING_HYSTERESIS_ENABLED + } + + // Call when allocation was made from the memory block. + void PostAlloc() + { +#if VMA_MAPPING_HYSTERESIS_ENABLED + if(m_ExtraMapping == 1) + ++m_MajorCounter; + else // m_ExtraMapping == 0 + PostMinorCounter(); +#endif // #if VMA_MAPPING_HYSTERESIS_ENABLED + } + + // Call when allocation was freed from the memory block. + // Returns true if switched to extra -1 mapping reference count. + bool PostFree() + { +#if VMA_MAPPING_HYSTERESIS_ENABLED + if(m_ExtraMapping == 1) + { + ++m_MajorCounter; + if(m_MajorCounter >= COUNTER_MIN_EXTRA_MAPPING && + m_MajorCounter > m_MinorCounter + 1) + { + m_ExtraMapping = 0; + m_MajorCounter = 0; + m_MinorCounter = 0; + return true; + } + } + else // m_ExtraMapping == 0 + PostMinorCounter(); +#endif // #if VMA_MAPPING_HYSTERESIS_ENABLED + return false; + } + +private: + static const int32_t COUNTER_MIN_EXTRA_MAPPING = 7; + + uint32_t m_MinorCounter = 0; + uint32_t m_MajorCounter = 0; + uint32_t m_ExtraMapping = 0; // 0 or 1. + + void PostMinorCounter() + { + if(m_MinorCounter < m_MajorCounter) + { + ++m_MinorCounter; + } + else if(m_MajorCounter > 0) + { + --m_MajorCounter; + --m_MinorCounter; + } + } +}; + +#endif // _VMA_MAPPING_HYSTERESIS + +#if VMA_EXTERNAL_MEMORY_WIN32 +class VmaWin32Handle +{ +public: + VmaWin32Handle() noexcept : m_hHandle(VMA_NULL) { } + explicit VmaWin32Handle(HANDLE hHandle) noexcept : m_hHandle(hHandle) { } + ~VmaWin32Handle() noexcept { if (m_hHandle != VMA_NULL) { ::CloseHandle(m_hHandle); } } + VMA_CLASS_NO_COPY_NO_MOVE(VmaWin32Handle) + +public: + // Strengthened + VkResult GetHandle(VkDevice device, VkDeviceMemory memory, PFN_vkGetMemoryWin32HandleKHR pvkGetMemoryWin32HandleKHR, HANDLE hTargetProcess, bool useMutex, HANDLE* pHandle) noexcept + { + *pHandle = VMA_NULL; + // Try to get handle first. + if (m_hHandle != VMA_NULL) + { + *pHandle = Duplicate(hTargetProcess); + return VK_SUCCESS; + } + + VkResult res = VK_SUCCESS; + // If failed, try to create it. + { + VmaMutexLockWrite lock(m_Mutex, useMutex); + if (m_hHandle == VMA_NULL) + { + res = Create(device, memory, pvkGetMemoryWin32HandleKHR, &m_hHandle); + } + } + + *pHandle = Duplicate(hTargetProcess); + return res; + } + + operator bool() const noexcept { return m_hHandle != VMA_NULL; } +private: + // Not atomic + static VkResult Create(VkDevice device, VkDeviceMemory memory, PFN_vkGetMemoryWin32HandleKHR pvkGetMemoryWin32HandleKHR, HANDLE* pHandle) noexcept + { + VkResult res = VK_ERROR_FEATURE_NOT_PRESENT; + if (pvkGetMemoryWin32HandleKHR != VMA_NULL) + { + VkMemoryGetWin32HandleInfoKHR handleInfo{ }; + handleInfo.sType = VK_STRUCTURE_TYPE_MEMORY_GET_WIN32_HANDLE_INFO_KHR; + handleInfo.memory = memory; + handleInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR; + res = pvkGetMemoryWin32HandleKHR(device, &handleInfo, pHandle); + } + return res; + } + HANDLE Duplicate(HANDLE hTargetProcess = VMA_NULL) const noexcept + { + if (!m_hHandle) + return m_hHandle; + + HANDLE hCurrentProcess = ::GetCurrentProcess(); + HANDLE hDupHandle = VMA_NULL; + if (!::DuplicateHandle(hCurrentProcess, m_hHandle, hTargetProcess ? hTargetProcess : hCurrentProcess, &hDupHandle, 0, FALSE, DUPLICATE_SAME_ACCESS)) + { + VMA_ASSERT(0 && "Failed to duplicate handle."); + } + return hDupHandle; + } +private: + HANDLE m_hHandle; + VMA_RW_MUTEX m_Mutex; // Protects access m_Handle +}; +#else +class VmaWin32Handle +{ + // ABI compatibility + void* placeholder = VMA_NULL; + VMA_RW_MUTEX placeholder2; +}; +#endif // VMA_EXTERNAL_MEMORY_WIN32 + + +#ifndef _VMA_DEVICE_MEMORY_BLOCK +/* +Represents a single block of device memory (`VkDeviceMemory`) with all the +data about its regions (aka suballocations, #VmaAllocation), assigned and free. + +Thread-safety: +- Access to m_pMetadata must be externally synchronized. +- Map, Unmap, Bind* are synchronized internally. +*/ +class VmaDeviceMemoryBlock +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaDeviceMemoryBlock) +public: + VmaBlockMetadata* m_pMetadata; + + explicit VmaDeviceMemoryBlock(VmaAllocator hAllocator); + ~VmaDeviceMemoryBlock(); + + // Always call after construction. + void Init( + VmaAllocator hAllocator, + VmaPool hParentPool, + uint32_t newMemoryTypeIndex, + VkDeviceMemory newMemory, + VkDeviceSize newSize, + uint32_t id, + uint32_t algorithm, + VkDeviceSize bufferImageGranularity); + // Always call before destruction. + void Destroy(VmaAllocator allocator); + + VmaPool GetParentPool() const { return m_hParentPool; } + VkDeviceMemory GetDeviceMemory() const { return m_hMemory; } + uint32_t GetMemoryTypeIndex() const { return m_MemoryTypeIndex; } + uint32_t GetId() const { return m_Id; } + void* GetMappedData() const { return m_pMappedData; } + uint32_t GetMapRefCount() const { return m_MapCount; } + + // Call when allocation/free was made from m_pMetadata. + // Used for m_MappingHysteresis. + void PostAlloc(VmaAllocator hAllocator); + void PostFree(VmaAllocator hAllocator); + + // Validates all data structures inside this object. If not valid, returns false. + bool Validate() const; + VkResult CheckCorruption(VmaAllocator hAllocator); + + // ppData can be null. + VkResult Map(VmaAllocator hAllocator, uint32_t count, void** ppData); + void Unmap(VmaAllocator hAllocator, uint32_t count); + + VkResult WriteMagicValueAfterAllocation(VmaAllocator hAllocator, VkDeviceSize allocOffset, VkDeviceSize allocSize); + VkResult ValidateMagicValueAfterAllocation(VmaAllocator hAllocator, VkDeviceSize allocOffset, VkDeviceSize allocSize); + + VkResult BindBufferMemory( + VmaAllocator hAllocator, + VmaAllocation hAllocation, + VkDeviceSize allocationLocalOffset, + VkBuffer hBuffer, + const void* pNext); + VkResult BindImageMemory( + VmaAllocator hAllocator, + VmaAllocation hAllocation, + VkDeviceSize allocationLocalOffset, + VkImage hImage, + const void* pNext); +#if VMA_EXTERNAL_MEMORY_WIN32 + VkResult CreateWin32Handle( + const VmaAllocator hAllocator, + PFN_vkGetMemoryWin32HandleKHR pvkGetMemoryWin32HandleKHR, + HANDLE hTargetProcess, + HANDLE* pHandle)noexcept; +#endif // VMA_EXTERNAL_MEMORY_WIN32 +private: + VmaPool m_hParentPool; // VK_NULL_HANDLE if not belongs to custom pool. + uint32_t m_MemoryTypeIndex; + uint32_t m_Id; + VkDeviceMemory m_hMemory; + + /* + Protects access to m_hMemory so it is not used by multiple threads simultaneously, e.g. vkMapMemory, vkBindBufferMemory. + Also protects m_MapCount, m_pMappedData. + Allocations, deallocations, any change in m_pMetadata is protected by parent's VmaBlockVector::m_Mutex. + */ + VMA_MUTEX m_MapAndBindMutex; + VmaMappingHysteresis m_MappingHysteresis; + uint32_t m_MapCount; + void* m_pMappedData; + + VmaWin32Handle m_Handle; +}; +#endif // _VMA_DEVICE_MEMORY_BLOCK + +#ifndef _VMA_ALLOCATION_T +struct VmaAllocationExtraData +{ + void* m_pMappedData = VMA_NULL; // Not null means memory is mapped. + VmaWin32Handle m_Handle; +}; + +struct VmaAllocation_T +{ + friend struct VmaDedicatedAllocationListItemTraits; + + enum FLAGS + { + FLAG_PERSISTENT_MAP = 0x01, + FLAG_MAPPING_ALLOWED = 0x02, + }; + +public: + enum ALLOCATION_TYPE + { + ALLOCATION_TYPE_NONE, + ALLOCATION_TYPE_BLOCK, + ALLOCATION_TYPE_DEDICATED, + }; + + // This struct is allocated using VmaPoolAllocator. + explicit VmaAllocation_T(bool mappingAllowed); + ~VmaAllocation_T(); + + void InitBlockAllocation( + VmaDeviceMemoryBlock* block, + VmaAllocHandle allocHandle, + VkDeviceSize alignment, + VkDeviceSize size, + uint32_t memoryTypeIndex, + VmaSuballocationType suballocationType, + bool mapped); + // pMappedData not null means allocation is created with MAPPED flag. + void InitDedicatedAllocation( + VmaAllocator allocator, + VmaPool hParentPool, + uint32_t memoryTypeIndex, + VkDeviceMemory hMemory, + VmaSuballocationType suballocationType, + void* pMappedData, + VkDeviceSize size); + void Destroy(VmaAllocator allocator); + + ALLOCATION_TYPE GetType() const { return (ALLOCATION_TYPE)m_Type; } + VkDeviceSize GetAlignment() const { return m_Alignment; } + VkDeviceSize GetSize() const { return m_Size; } + void* GetUserData() const { return m_pUserData; } + const char* GetName() const { return m_pName; } + VmaSuballocationType GetSuballocationType() const { return (VmaSuballocationType)m_SuballocationType; } + + VmaDeviceMemoryBlock* GetBlock() const { VMA_ASSERT(m_Type == ALLOCATION_TYPE_BLOCK); return m_BlockAllocation.m_Block; } + uint32_t GetMemoryTypeIndex() const { return m_MemoryTypeIndex; } + bool IsPersistentMap() const { return (m_Flags & FLAG_PERSISTENT_MAP) != 0; } + bool IsMappingAllowed() const { return (m_Flags & FLAG_MAPPING_ALLOWED) != 0; } + + void SetUserData(VmaAllocator hAllocator, void* pUserData) { m_pUserData = pUserData; } + void SetName(VmaAllocator hAllocator, const char* pName); + void FreeName(VmaAllocator hAllocator); + uint8_t SwapBlockAllocation(VmaAllocator hAllocator, VmaAllocation allocation); + VmaAllocHandle GetAllocHandle() const; + VkDeviceSize GetOffset() const; + VmaPool GetParentPool() const; + VkDeviceMemory GetMemory() const; + void* GetMappedData() const; + + void BlockAllocMap(); + void BlockAllocUnmap(); + VkResult DedicatedAllocMap(VmaAllocator hAllocator, void** ppData); + void DedicatedAllocUnmap(VmaAllocator hAllocator); + +#if VMA_STATS_STRING_ENABLED + VmaBufferImageUsage GetBufferImageUsage() const { return m_BufferImageUsage; } + void InitBufferUsage(const VkBufferCreateInfo &createInfo, bool useKhrMaintenance5) + { + VMA_ASSERT(m_BufferImageUsage == VmaBufferImageUsage::UNKNOWN); + m_BufferImageUsage = VmaBufferImageUsage(createInfo, useKhrMaintenance5); + } + void InitImageUsage(const VkImageCreateInfo &createInfo) + { + VMA_ASSERT(m_BufferImageUsage == VmaBufferImageUsage::UNKNOWN); + m_BufferImageUsage = VmaBufferImageUsage(createInfo); + } + void PrintParameters(class VmaJsonWriter& json) const; +#endif + +#if VMA_EXTERNAL_MEMORY_WIN32 + VkResult GetWin32Handle(VmaAllocator hAllocator, HANDLE hTargetProcess, HANDLE* hHandle) noexcept; +#endif // VMA_EXTERNAL_MEMORY_WIN32 + +private: + // Allocation out of VmaDeviceMemoryBlock. + struct BlockAllocation + { + VmaDeviceMemoryBlock* m_Block; + VmaAllocHandle m_AllocHandle; + }; + // Allocation for an object that has its own private VkDeviceMemory. + struct DedicatedAllocation + { + VmaPool m_hParentPool; // VK_NULL_HANDLE if not belongs to custom pool. + VkDeviceMemory m_hMemory; + VmaAllocationExtraData* m_ExtraData; + VmaAllocation_T* m_Prev; + VmaAllocation_T* m_Next; + }; + union + { + // Allocation out of VmaDeviceMemoryBlock. + BlockAllocation m_BlockAllocation; + // Allocation for an object that has its own private VkDeviceMemory. + DedicatedAllocation m_DedicatedAllocation; + }; + + VkDeviceSize m_Alignment; + VkDeviceSize m_Size; + void* m_pUserData; + char* m_pName; + uint32_t m_MemoryTypeIndex; + uint8_t m_Type; // ALLOCATION_TYPE + uint8_t m_SuballocationType; // VmaSuballocationType + // Reference counter for vmaMapMemory()/vmaUnmapMemory(). + uint8_t m_MapCount; + uint8_t m_Flags; // enum FLAGS +#if VMA_STATS_STRING_ENABLED + VmaBufferImageUsage m_BufferImageUsage; // 0 if unknown. +#endif + + void EnsureExtraData(VmaAllocator hAllocator); +}; +#endif // _VMA_ALLOCATION_T + +#ifndef _VMA_DEDICATED_ALLOCATION_LIST_ITEM_TRAITS +struct VmaDedicatedAllocationListItemTraits +{ + typedef VmaAllocation_T ItemType; + + static ItemType* GetPrev(const ItemType* item) + { + VMA_HEAVY_ASSERT(item->GetType() == VmaAllocation_T::ALLOCATION_TYPE_DEDICATED); + return item->m_DedicatedAllocation.m_Prev; + } + static ItemType* GetNext(const ItemType* item) + { + VMA_HEAVY_ASSERT(item->GetType() == VmaAllocation_T::ALLOCATION_TYPE_DEDICATED); + return item->m_DedicatedAllocation.m_Next; + } + static ItemType*& AccessPrev(ItemType* item) + { + VMA_HEAVY_ASSERT(item->GetType() == VmaAllocation_T::ALLOCATION_TYPE_DEDICATED); + return item->m_DedicatedAllocation.m_Prev; + } + static ItemType*& AccessNext(ItemType* item) + { + VMA_HEAVY_ASSERT(item->GetType() == VmaAllocation_T::ALLOCATION_TYPE_DEDICATED); + return item->m_DedicatedAllocation.m_Next; + } +}; +#endif // _VMA_DEDICATED_ALLOCATION_LIST_ITEM_TRAITS + +#ifndef _VMA_DEDICATED_ALLOCATION_LIST +/* +Stores linked list of VmaAllocation_T objects. +Thread-safe, synchronized internally. +*/ +class VmaDedicatedAllocationList +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaDedicatedAllocationList) +public: + VmaDedicatedAllocationList() = default; + ~VmaDedicatedAllocationList(); + + void Init(bool useMutex) { m_UseMutex = useMutex; } + bool Validate(); + + void AddDetailedStatistics(VmaDetailedStatistics& inoutStats); + void AddStatistics(VmaStatistics& inoutStats); +#if VMA_STATS_STRING_ENABLED + // Writes JSON array with the list of allocations. + void BuildStatsString(VmaJsonWriter& json); +#endif + + bool IsEmpty(); + void Register(VmaAllocation alloc); + void Unregister(VmaAllocation alloc); + +private: + typedef VmaIntrusiveLinkedList DedicatedAllocationLinkedList; + + bool m_UseMutex = true; + VMA_RW_MUTEX m_Mutex; + DedicatedAllocationLinkedList m_AllocationList; +}; + +#ifndef _VMA_DEDICATED_ALLOCATION_LIST_FUNCTIONS + +VmaDedicatedAllocationList::~VmaDedicatedAllocationList() +{ + VMA_HEAVY_ASSERT(Validate()); + + if (!m_AllocationList.IsEmpty()) + { + VMA_ASSERT_LEAK(false && "Unfreed dedicated allocations found!"); + } +} + +bool VmaDedicatedAllocationList::Validate() +{ + const size_t declaredCount = m_AllocationList.GetCount(); + size_t actualCount = 0; + VmaMutexLockRead lock(m_Mutex, m_UseMutex); + for (VmaAllocation alloc = m_AllocationList.Front(); + alloc != VMA_NULL; alloc = m_AllocationList.GetNext(alloc)) + { + ++actualCount; + } + VMA_VALIDATE(actualCount == declaredCount); + + return true; +} + +void VmaDedicatedAllocationList::AddDetailedStatistics(VmaDetailedStatistics& inoutStats) +{ + for(auto* item = m_AllocationList.Front(); item != VMA_NULL; item = DedicatedAllocationLinkedList::GetNext(item)) + { + const VkDeviceSize size = item->GetSize(); + inoutStats.statistics.blockCount++; + inoutStats.statistics.blockBytes += size; + VmaAddDetailedStatisticsAllocation(inoutStats, item->GetSize()); + } +} + +void VmaDedicatedAllocationList::AddStatistics(VmaStatistics& inoutStats) +{ + VmaMutexLockRead lock(m_Mutex, m_UseMutex); + + const uint32_t allocCount = (uint32_t)m_AllocationList.GetCount(); + inoutStats.blockCount += allocCount; + inoutStats.allocationCount += allocCount; + + for(auto* item = m_AllocationList.Front(); item != VMA_NULL; item = DedicatedAllocationLinkedList::GetNext(item)) + { + const VkDeviceSize size = item->GetSize(); + inoutStats.blockBytes += size; + inoutStats.allocationBytes += size; + } +} + +#if VMA_STATS_STRING_ENABLED +void VmaDedicatedAllocationList::BuildStatsString(VmaJsonWriter& json) +{ + VmaMutexLockRead lock(m_Mutex, m_UseMutex); + json.BeginArray(); + for (VmaAllocation alloc = m_AllocationList.Front(); + alloc != VMA_NULL; alloc = m_AllocationList.GetNext(alloc)) + { + json.BeginObject(true); + alloc->PrintParameters(json); + json.EndObject(); + } + json.EndArray(); +} +#endif // VMA_STATS_STRING_ENABLED + +bool VmaDedicatedAllocationList::IsEmpty() +{ + VmaMutexLockRead lock(m_Mutex, m_UseMutex); + return m_AllocationList.IsEmpty(); +} + +void VmaDedicatedAllocationList::Register(VmaAllocation alloc) +{ + VmaMutexLockWrite lock(m_Mutex, m_UseMutex); + m_AllocationList.PushBack(alloc); +} + +void VmaDedicatedAllocationList::Unregister(VmaAllocation alloc) +{ + VmaMutexLockWrite lock(m_Mutex, m_UseMutex); + m_AllocationList.Remove(alloc); +} +#endif // _VMA_DEDICATED_ALLOCATION_LIST_FUNCTIONS +#endif // _VMA_DEDICATED_ALLOCATION_LIST + +#ifndef _VMA_SUBALLOCATION +/* +Represents a region of VmaDeviceMemoryBlock that is either assigned and returned as +allocated memory block or free. +*/ +struct VmaSuballocation +{ + VkDeviceSize offset; + VkDeviceSize size; + void* userData; + VmaSuballocationType type; +}; + +// Comparator for offsets. +struct VmaSuballocationOffsetLess +{ + bool operator()(const VmaSuballocation& lhs, const VmaSuballocation& rhs) const + { + return lhs.offset < rhs.offset; + } +}; + +struct VmaSuballocationOffsetGreater +{ + bool operator()(const VmaSuballocation& lhs, const VmaSuballocation& rhs) const + { + return lhs.offset > rhs.offset; + } +}; + +struct VmaSuballocationItemSizeLess +{ + bool operator()(const VmaSuballocationList::iterator lhs, + const VmaSuballocationList::iterator rhs) const + { + return lhs->size < rhs->size; + } + + bool operator()(const VmaSuballocationList::iterator lhs, + VkDeviceSize rhsSize) const + { + return lhs->size < rhsSize; + } +}; +#endif // _VMA_SUBALLOCATION + +#ifndef _VMA_ALLOCATION_REQUEST +/* +Parameters of planned allocation inside a VmaDeviceMemoryBlock. +item points to a FREE suballocation. +*/ +struct VmaAllocationRequest +{ + VmaAllocHandle allocHandle; + VkDeviceSize size; + VmaSuballocationList::iterator item; + void* customData; + uint64_t algorithmData; + VmaAllocationRequestType type; +}; +#endif // _VMA_ALLOCATION_REQUEST + +#ifndef _VMA_BLOCK_METADATA +/* +Data structure used for bookkeeping of allocations and unused ranges of memory +in a single VkDeviceMemory block. +*/ +class VmaBlockMetadata +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaBlockMetadata) +public: + // pAllocationCallbacks, if not null, must be owned externally - alive and unchanged for the whole lifetime of this object. + VmaBlockMetadata(const VkAllocationCallbacks* pAllocationCallbacks, + VkDeviceSize bufferImageGranularity, bool isVirtual); + virtual ~VmaBlockMetadata() = default; + + virtual void Init(VkDeviceSize size) { m_Size = size; } + bool IsVirtual() const { return m_IsVirtual; } + VkDeviceSize GetSize() const { return m_Size; } + + // Validates all data structures inside this object. If not valid, returns false. + virtual bool Validate() const = 0; + virtual size_t GetAllocationCount() const = 0; + virtual size_t GetFreeRegionsCount() const = 0; + virtual VkDeviceSize GetSumFreeSize() const = 0; + // Returns true if this block is empty - contains only single free suballocation. + virtual bool IsEmpty() const = 0; + virtual void GetAllocationInfo(VmaAllocHandle allocHandle, VmaVirtualAllocationInfo& outInfo) = 0; + virtual VkDeviceSize GetAllocationOffset(VmaAllocHandle allocHandle) const = 0; + virtual void* GetAllocationUserData(VmaAllocHandle allocHandle) const = 0; + + virtual VmaAllocHandle GetAllocationListBegin() const = 0; + virtual VmaAllocHandle GetNextAllocation(VmaAllocHandle prevAlloc) const = 0; + virtual VkDeviceSize GetNextFreeRegionSize(VmaAllocHandle alloc) const = 0; + + // Shouldn't modify blockCount. + virtual void AddDetailedStatistics(VmaDetailedStatistics& inoutStats) const = 0; + virtual void AddStatistics(VmaStatistics& inoutStats) const = 0; + +#if VMA_STATS_STRING_ENABLED + virtual void PrintDetailedMap(class VmaJsonWriter& json) const = 0; +#endif + + // Tries to find a place for suballocation with given parameters inside this block. + // If succeeded, fills pAllocationRequest and returns true. + // If failed, returns false. + virtual bool CreateAllocationRequest( + VkDeviceSize allocSize, + VkDeviceSize allocAlignment, + bool upperAddress, + VmaSuballocationType allocType, + // Always one of VMA_ALLOCATION_CREATE_STRATEGY_* or VMA_ALLOCATION_INTERNAL_STRATEGY_* flags. + uint32_t strategy, + VmaAllocationRequest* pAllocationRequest) = 0; + + virtual VkResult CheckCorruption(const void* pBlockData) = 0; + + // Makes actual allocation based on request. Request must already be checked and valid. + virtual void Alloc( + const VmaAllocationRequest& request, + VmaSuballocationType type, + void* userData) = 0; + + // Frees suballocation assigned to given memory region. + virtual void Free(VmaAllocHandle allocHandle) = 0; + + // Frees all allocations. + // Careful! Don't call it if there are VmaAllocation objects owned by userData of cleared allocations! + virtual void Clear() = 0; + + virtual void SetAllocationUserData(VmaAllocHandle allocHandle, void* userData) = 0; + virtual void DebugLogAllAllocations() const = 0; + +protected: + const VkAllocationCallbacks* GetAllocationCallbacks() const { return m_pAllocationCallbacks; } + VkDeviceSize GetBufferImageGranularity() const { return m_BufferImageGranularity; } + VkDeviceSize GetDebugMargin() const { return VkDeviceSize(IsVirtual() ? 0 : VMA_DEBUG_MARGIN); } + + void DebugLogAllocation(VkDeviceSize offset, VkDeviceSize size, void* userData) const; +#if VMA_STATS_STRING_ENABLED + // mapRefCount == UINT32_MAX means unspecified. + void PrintDetailedMap_Begin(class VmaJsonWriter& json, + VkDeviceSize unusedBytes, + size_t allocationCount, + size_t unusedRangeCount) const; + void PrintDetailedMap_Allocation(class VmaJsonWriter& json, + VkDeviceSize offset, VkDeviceSize size, void* userData) const; + static void PrintDetailedMap_UnusedRange(class VmaJsonWriter& json, + VkDeviceSize offset, + VkDeviceSize size); + static void PrintDetailedMap_End(class VmaJsonWriter& json); +#endif + +private: + VkDeviceSize m_Size; + const VkAllocationCallbacks* m_pAllocationCallbacks; + const VkDeviceSize m_BufferImageGranularity; + const bool m_IsVirtual; +}; + +#ifndef _VMA_BLOCK_METADATA_FUNCTIONS +VmaBlockMetadata::VmaBlockMetadata(const VkAllocationCallbacks* pAllocationCallbacks, + VkDeviceSize bufferImageGranularity, bool isVirtual) + : m_Size(0), + m_pAllocationCallbacks(pAllocationCallbacks), + m_BufferImageGranularity(bufferImageGranularity), + m_IsVirtual(isVirtual) {} + +void VmaBlockMetadata::DebugLogAllocation(VkDeviceSize offset, VkDeviceSize size, void* userData) const +{ + if (IsVirtual()) + { + VMA_LEAK_LOG_FORMAT("UNFREED VIRTUAL ALLOCATION; Offset: %" PRIu64 "; Size: %" PRIu64 "; UserData: %p", offset, size, userData); + } + else + { + VMA_ASSERT(userData != VMA_NULL); + VmaAllocation allocation = reinterpret_cast(userData); + + userData = allocation->GetUserData(); + const char* name = allocation->GetName(); + +#if VMA_STATS_STRING_ENABLED + VMA_LEAK_LOG_FORMAT("UNFREED ALLOCATION; Offset: %" PRIu64 "; Size: %" PRIu64 "; UserData: %p; Name: %s; Type: %s; Usage: %" PRIu64, + offset, size, userData, name ? name : "vma_empty", + VMA_SUBALLOCATION_TYPE_NAMES[allocation->GetSuballocationType()], + (uint64_t)allocation->GetBufferImageUsage().Value); +#else + VMA_LEAK_LOG_FORMAT("UNFREED ALLOCATION; Offset: %" PRIu64 "; Size: %" PRIu64 "; UserData: %p; Name: %s; Type: %u", + offset, size, userData, name ? name : "vma_empty", + (unsigned)allocation->GetSuballocationType()); +#endif // VMA_STATS_STRING_ENABLED + } + +} + +#if VMA_STATS_STRING_ENABLED +void VmaBlockMetadata::PrintDetailedMap_Begin(class VmaJsonWriter& json, + VkDeviceSize unusedBytes, size_t allocationCount, size_t unusedRangeCount) const +{ + json.WriteString("TotalBytes"); + json.WriteNumber(GetSize()); + + json.WriteString("UnusedBytes"); + json.WriteNumber(unusedBytes); + + json.WriteString("Allocations"); + json.WriteNumber((uint64_t)allocationCount); + + json.WriteString("UnusedRanges"); + json.WriteNumber((uint64_t)unusedRangeCount); + + json.WriteString("Suballocations"); + json.BeginArray(); +} + +void VmaBlockMetadata::PrintDetailedMap_Allocation(class VmaJsonWriter& json, + VkDeviceSize offset, VkDeviceSize size, void* userData) const +{ + json.BeginObject(true); + + json.WriteString("Offset"); + json.WriteNumber(offset); + + if (IsVirtual()) + { + json.WriteString("Size"); + json.WriteNumber(size); + if (userData) + { + json.WriteString("CustomData"); + json.BeginString(); + json.ContinueString_Pointer(userData); + json.EndString(); + } + } + else + { + ((VmaAllocation)userData)->PrintParameters(json); + } + + json.EndObject(); +} + +void VmaBlockMetadata::PrintDetailedMap_UnusedRange(class VmaJsonWriter& json, + VkDeviceSize offset, VkDeviceSize size) +{ + json.BeginObject(true); + + json.WriteString("Offset"); + json.WriteNumber(offset); + + json.WriteString("Type"); + json.WriteString(VMA_SUBALLOCATION_TYPE_NAMES[VMA_SUBALLOCATION_TYPE_FREE]); + + json.WriteString("Size"); + json.WriteNumber(size); + + json.EndObject(); +} + +void VmaBlockMetadata::PrintDetailedMap_End(class VmaJsonWriter& json) +{ + json.EndArray(); +} +#endif // VMA_STATS_STRING_ENABLED +#endif // _VMA_BLOCK_METADATA_FUNCTIONS +#endif // _VMA_BLOCK_METADATA + +#ifndef _VMA_BLOCK_BUFFER_IMAGE_GRANULARITY +// Before deleting object of this class remember to call 'Destroy()' +class VmaBlockBufferImageGranularity final +{ +public: + struct ValidationContext + { + const VkAllocationCallbacks* allocCallbacks; + uint16_t* pageAllocs; + }; + + explicit VmaBlockBufferImageGranularity(VkDeviceSize bufferImageGranularity); + ~VmaBlockBufferImageGranularity(); + + bool IsEnabled() const { return m_BufferImageGranularity > MAX_LOW_BUFFER_IMAGE_GRANULARITY; } + + void Init(const VkAllocationCallbacks* pAllocationCallbacks, VkDeviceSize size); + // Before destroying object you must call free it's memory + void Destroy(const VkAllocationCallbacks* pAllocationCallbacks); + + void RoundupAllocRequest(VmaSuballocationType allocType, + VkDeviceSize& inOutAllocSize, + VkDeviceSize& inOutAllocAlignment) const; + + bool CheckConflictAndAlignUp(VkDeviceSize& inOutAllocOffset, + VkDeviceSize allocSize, + VkDeviceSize blockOffset, + VkDeviceSize blockSize, + VmaSuballocationType allocType) const; + + void AllocPages(uint8_t allocType, VkDeviceSize offset, VkDeviceSize size); + void FreePages(VkDeviceSize offset, VkDeviceSize size); + void Clear(); + + ValidationContext StartValidation(const VkAllocationCallbacks* pAllocationCallbacks, + bool isVirutal) const; + bool Validate(ValidationContext& ctx, VkDeviceSize offset, VkDeviceSize size) const; + bool FinishValidation(ValidationContext& ctx) const; + +private: + static const uint16_t MAX_LOW_BUFFER_IMAGE_GRANULARITY = 256; + + struct RegionInfo + { + uint8_t allocType; + uint16_t allocCount; + }; + + VkDeviceSize m_BufferImageGranularity; + uint32_t m_RegionCount; + RegionInfo* m_RegionInfo; + + uint32_t GetStartPage(VkDeviceSize offset) const { return OffsetToPageIndex(offset & ~(m_BufferImageGranularity - 1)); } + uint32_t GetEndPage(VkDeviceSize offset, VkDeviceSize size) const { return OffsetToPageIndex((offset + size - 1) & ~(m_BufferImageGranularity - 1)); } + + uint32_t OffsetToPageIndex(VkDeviceSize offset) const; + static void AllocPage(RegionInfo& page, uint8_t allocType); +}; + +#ifndef _VMA_BLOCK_BUFFER_IMAGE_GRANULARITY_FUNCTIONS +VmaBlockBufferImageGranularity::VmaBlockBufferImageGranularity(VkDeviceSize bufferImageGranularity) + : m_BufferImageGranularity(bufferImageGranularity), + m_RegionCount(0), + m_RegionInfo(VMA_NULL) {} + +VmaBlockBufferImageGranularity::~VmaBlockBufferImageGranularity() +{ + VMA_ASSERT(m_RegionInfo == VMA_NULL && "Free not called before destroying object!"); +} + +void VmaBlockBufferImageGranularity::Init(const VkAllocationCallbacks* pAllocationCallbacks, VkDeviceSize size) +{ + if (IsEnabled()) + { + m_RegionCount = static_cast(VmaDivideRoundingUp(size, m_BufferImageGranularity)); + m_RegionInfo = vma_new_array(pAllocationCallbacks, RegionInfo, m_RegionCount); + memset(m_RegionInfo, 0, m_RegionCount * sizeof(RegionInfo)); + } +} + +void VmaBlockBufferImageGranularity::Destroy(const VkAllocationCallbacks* pAllocationCallbacks) +{ + if (m_RegionInfo) + { + vma_delete_array(pAllocationCallbacks, m_RegionInfo, m_RegionCount); + m_RegionInfo = VMA_NULL; + } +} + +void VmaBlockBufferImageGranularity::RoundupAllocRequest(VmaSuballocationType allocType, + VkDeviceSize& inOutAllocSize, + VkDeviceSize& inOutAllocAlignment) const +{ + if (m_BufferImageGranularity > 1 && + m_BufferImageGranularity <= MAX_LOW_BUFFER_IMAGE_GRANULARITY) + { + if (allocType == VMA_SUBALLOCATION_TYPE_UNKNOWN || + allocType == VMA_SUBALLOCATION_TYPE_IMAGE_UNKNOWN || + allocType == VMA_SUBALLOCATION_TYPE_IMAGE_OPTIMAL) + { + inOutAllocAlignment = VMA_MAX(inOutAllocAlignment, m_BufferImageGranularity); + inOutAllocSize = VmaAlignUp(inOutAllocSize, m_BufferImageGranularity); + } + } +} + +bool VmaBlockBufferImageGranularity::CheckConflictAndAlignUp(VkDeviceSize& inOutAllocOffset, + VkDeviceSize allocSize, + VkDeviceSize blockOffset, + VkDeviceSize blockSize, + VmaSuballocationType allocType) const +{ + if (IsEnabled()) + { + uint32_t startPage = GetStartPage(inOutAllocOffset); + if (m_RegionInfo[startPage].allocCount > 0 && + VmaIsBufferImageGranularityConflict(static_cast(m_RegionInfo[startPage].allocType), allocType)) + { + inOutAllocOffset = VmaAlignUp(inOutAllocOffset, m_BufferImageGranularity); + if (blockSize < allocSize + inOutAllocOffset - blockOffset) + return true; + ++startPage; + } + uint32_t endPage = GetEndPage(inOutAllocOffset, allocSize); + if (endPage != startPage && + m_RegionInfo[endPage].allocCount > 0 && + VmaIsBufferImageGranularityConflict(static_cast(m_RegionInfo[endPage].allocType), allocType)) + { + return true; + } + } + return false; +} + +void VmaBlockBufferImageGranularity::AllocPages(uint8_t allocType, VkDeviceSize offset, VkDeviceSize size) +{ + if (IsEnabled()) + { + uint32_t startPage = GetStartPage(offset); + AllocPage(m_RegionInfo[startPage], allocType); + + uint32_t endPage = GetEndPage(offset, size); + if (startPage != endPage) + AllocPage(m_RegionInfo[endPage], allocType); + } +} + +void VmaBlockBufferImageGranularity::FreePages(VkDeviceSize offset, VkDeviceSize size) +{ + if (IsEnabled()) + { + uint32_t startPage = GetStartPage(offset); + --m_RegionInfo[startPage].allocCount; + if (m_RegionInfo[startPage].allocCount == 0) + m_RegionInfo[startPage].allocType = VMA_SUBALLOCATION_TYPE_FREE; + uint32_t endPage = GetEndPage(offset, size); + if (startPage != endPage) + { + --m_RegionInfo[endPage].allocCount; + if (m_RegionInfo[endPage].allocCount == 0) + m_RegionInfo[endPage].allocType = VMA_SUBALLOCATION_TYPE_FREE; + } + } +} + +void VmaBlockBufferImageGranularity::Clear() +{ + if (m_RegionInfo) + memset(m_RegionInfo, 0, m_RegionCount * sizeof(RegionInfo)); +} + +VmaBlockBufferImageGranularity::ValidationContext VmaBlockBufferImageGranularity::StartValidation( + const VkAllocationCallbacks* pAllocationCallbacks, bool isVirutal) const +{ + ValidationContext ctx{ pAllocationCallbacks, VMA_NULL }; + if (!isVirutal && IsEnabled()) + { + ctx.pageAllocs = vma_new_array(pAllocationCallbacks, uint16_t, m_RegionCount); + memset(ctx.pageAllocs, 0, m_RegionCount * sizeof(uint16_t)); + } + return ctx; +} + +bool VmaBlockBufferImageGranularity::Validate(ValidationContext& ctx, + VkDeviceSize offset, VkDeviceSize size) const +{ + if (IsEnabled()) + { + uint32_t start = GetStartPage(offset); + ++ctx.pageAllocs[start]; + VMA_VALIDATE(m_RegionInfo[start].allocCount > 0); + + uint32_t end = GetEndPage(offset, size); + if (start != end) + { + ++ctx.pageAllocs[end]; + VMA_VALIDATE(m_RegionInfo[end].allocCount > 0); + } + } + return true; +} + +bool VmaBlockBufferImageGranularity::FinishValidation(ValidationContext& ctx) const +{ + // Check proper page structure + if (IsEnabled()) + { + VMA_ASSERT(ctx.pageAllocs != VMA_NULL && "Validation context not initialized!"); + + for (uint32_t page = 0; page < m_RegionCount; ++page) + { + VMA_VALIDATE(ctx.pageAllocs[page] == m_RegionInfo[page].allocCount); + } + vma_delete_array(ctx.allocCallbacks, ctx.pageAllocs, m_RegionCount); + ctx.pageAllocs = VMA_NULL; + } + return true; +} + +uint32_t VmaBlockBufferImageGranularity::OffsetToPageIndex(VkDeviceSize offset) const +{ + return static_cast(offset >> VMA_BITSCAN_MSB(m_BufferImageGranularity)); +} + +void VmaBlockBufferImageGranularity::AllocPage(RegionInfo& page, uint8_t allocType) +{ + // When current alloc type is free then it can be overridden by new type + if (page.allocCount == 0 || (page.allocCount > 0 && page.allocType == VMA_SUBALLOCATION_TYPE_FREE)) + page.allocType = allocType; + + ++page.allocCount; +} +#endif // _VMA_BLOCK_BUFFER_IMAGE_GRANULARITY_FUNCTIONS +#endif // _VMA_BLOCK_BUFFER_IMAGE_GRANULARITY + +#ifndef _VMA_BLOCK_METADATA_LINEAR +/* +Allocations and their references in internal data structure look like this: + +if(m_2ndVectorMode == SECOND_VECTOR_EMPTY): + + 0 +-------+ + | | + | | + | | + +-------+ + | Alloc | 1st[m_1stNullItemsBeginCount] + +-------+ + | Alloc | 1st[m_1stNullItemsBeginCount + 1] + +-------+ + | ... | + +-------+ + | Alloc | 1st[1st.size() - 1] + +-------+ + | | + | | + | | +GetSize() +-------+ + +if(m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER): + + 0 +-------+ + | Alloc | 2nd[0] + +-------+ + | Alloc | 2nd[1] + +-------+ + | ... | + +-------+ + | Alloc | 2nd[2nd.size() - 1] + +-------+ + | | + | | + | | + +-------+ + | Alloc | 1st[m_1stNullItemsBeginCount] + +-------+ + | Alloc | 1st[m_1stNullItemsBeginCount + 1] + +-------+ + | ... | + +-------+ + | Alloc | 1st[1st.size() - 1] + +-------+ + | | +GetSize() +-------+ + +if(m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK): + + 0 +-------+ + | | + | | + | | + +-------+ + | Alloc | 1st[m_1stNullItemsBeginCount] + +-------+ + | Alloc | 1st[m_1stNullItemsBeginCount + 1] + +-------+ + | ... | + +-------+ + | Alloc | 1st[1st.size() - 1] + +-------+ + | | + | | + | | + +-------+ + | Alloc | 2nd[2nd.size() - 1] + +-------+ + | ... | + +-------+ + | Alloc | 2nd[1] + +-------+ + | Alloc | 2nd[0] +GetSize() +-------+ + +*/ +class VmaBlockMetadata_Linear : public VmaBlockMetadata +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaBlockMetadata_Linear) +public: + VmaBlockMetadata_Linear(const VkAllocationCallbacks* pAllocationCallbacks, + VkDeviceSize bufferImageGranularity, bool isVirtual); + ~VmaBlockMetadata_Linear() override = default; + + VkDeviceSize GetSumFreeSize() const override { return m_SumFreeSize; } + bool IsEmpty() const override { return GetAllocationCount() == 0; } + VkDeviceSize GetAllocationOffset(VmaAllocHandle allocHandle) const override { return (VkDeviceSize)allocHandle - 1; } + + void Init(VkDeviceSize size) override; + bool Validate() const override; + size_t GetAllocationCount() const override; + size_t GetFreeRegionsCount() const override; + + void AddDetailedStatistics(VmaDetailedStatistics& inoutStats) const override; + void AddStatistics(VmaStatistics& inoutStats) const override; + +#if VMA_STATS_STRING_ENABLED + void PrintDetailedMap(class VmaJsonWriter& json) const override; +#endif + + bool CreateAllocationRequest( + VkDeviceSize allocSize, + VkDeviceSize allocAlignment, + bool upperAddress, + VmaSuballocationType allocType, + uint32_t strategy, + VmaAllocationRequest* pAllocationRequest) override; + + VkResult CheckCorruption(const void* pBlockData) override; + + void Alloc( + const VmaAllocationRequest& request, + VmaSuballocationType type, + void* userData) override; + + void Free(VmaAllocHandle allocHandle) override; + void GetAllocationInfo(VmaAllocHandle allocHandle, VmaVirtualAllocationInfo& outInfo) override; + void* GetAllocationUserData(VmaAllocHandle allocHandle) const override; + VmaAllocHandle GetAllocationListBegin() const override; + VmaAllocHandle GetNextAllocation(VmaAllocHandle prevAlloc) const override; + VkDeviceSize GetNextFreeRegionSize(VmaAllocHandle alloc) const override; + void Clear() override; + void SetAllocationUserData(VmaAllocHandle allocHandle, void* userData) override; + void DebugLogAllAllocations() const override; + +private: + /* + There are two suballocation vectors, used in ping-pong way. + The one with index m_1stVectorIndex is called 1st. + The one with index (m_1stVectorIndex ^ 1) is called 2nd. + 2nd can be non-empty only when 1st is not empty. + When 2nd is not empty, m_2ndVectorMode indicates its mode of operation. + */ + typedef VmaVector> SuballocationVectorType; + + enum SECOND_VECTOR_MODE + { + SECOND_VECTOR_EMPTY, + /* + Suballocations in 2nd vector are created later than the ones in 1st, but they + all have smaller offset. + */ + SECOND_VECTOR_RING_BUFFER, + /* + Suballocations in 2nd vector are upper side of double stack. + They all have offsets higher than those in 1st vector. + Top of this stack means smaller offsets, but higher indices in this vector. + */ + SECOND_VECTOR_DOUBLE_STACK, + }; + + VkDeviceSize m_SumFreeSize; + SuballocationVectorType m_Suballocations0, m_Suballocations1; + uint32_t m_1stVectorIndex; + SECOND_VECTOR_MODE m_2ndVectorMode; + // Number of items in 1st vector with hAllocation = null at the beginning. + size_t m_1stNullItemsBeginCount; + // Number of other items in 1st vector with hAllocation = null somewhere in the middle. + size_t m_1stNullItemsMiddleCount; + // Number of items in 2nd vector with hAllocation = null. + size_t m_2ndNullItemsCount; + + SuballocationVectorType& AccessSuballocations1st() { return m_1stVectorIndex ? m_Suballocations1 : m_Suballocations0; } + SuballocationVectorType& AccessSuballocations2nd() { return m_1stVectorIndex ? m_Suballocations0 : m_Suballocations1; } + const SuballocationVectorType& AccessSuballocations1st() const { return m_1stVectorIndex ? m_Suballocations1 : m_Suballocations0; } + const SuballocationVectorType& AccessSuballocations2nd() const { return m_1stVectorIndex ? m_Suballocations0 : m_Suballocations1; } + + VmaSuballocation& FindSuballocation(VkDeviceSize offset) const; + bool ShouldCompact1st() const; + void CleanupAfterFree(); + + bool CreateAllocationRequest_LowerAddress( + VkDeviceSize allocSize, + VkDeviceSize allocAlignment, + VmaSuballocationType allocType, + uint32_t strategy, + VmaAllocationRequest* pAllocationRequest); + bool CreateAllocationRequest_UpperAddress( + VkDeviceSize allocSize, + VkDeviceSize allocAlignment, + VmaSuballocationType allocType, + uint32_t strategy, + VmaAllocationRequest* pAllocationRequest); +}; + +#ifndef _VMA_BLOCK_METADATA_LINEAR_FUNCTIONS +VmaBlockMetadata_Linear::VmaBlockMetadata_Linear(const VkAllocationCallbacks* pAllocationCallbacks, + VkDeviceSize bufferImageGranularity, bool isVirtual) + : VmaBlockMetadata(pAllocationCallbacks, bufferImageGranularity, isVirtual), + m_SumFreeSize(0), + m_Suballocations0(VmaStlAllocator(pAllocationCallbacks)), + m_Suballocations1(VmaStlAllocator(pAllocationCallbacks)), + m_1stVectorIndex(0), + m_2ndVectorMode(SECOND_VECTOR_EMPTY), + m_1stNullItemsBeginCount(0), + m_1stNullItemsMiddleCount(0), + m_2ndNullItemsCount(0) {} + +void VmaBlockMetadata_Linear::Init(VkDeviceSize size) +{ + VmaBlockMetadata::Init(size); + m_SumFreeSize = size; +} + +bool VmaBlockMetadata_Linear::Validate() const +{ + const SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + const SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + + VMA_VALIDATE(suballocations2nd.empty() == (m_2ndVectorMode == SECOND_VECTOR_EMPTY)); + VMA_VALIDATE(!suballocations1st.empty() || + suballocations2nd.empty() || + m_2ndVectorMode != SECOND_VECTOR_RING_BUFFER); + + if (!suballocations1st.empty()) + { + // Null item at the beginning should be accounted into m_1stNullItemsBeginCount. + VMA_VALIDATE(suballocations1st[m_1stNullItemsBeginCount].type != VMA_SUBALLOCATION_TYPE_FREE); + // Null item at the end should be just pop_back(). + VMA_VALIDATE(suballocations1st.back().type != VMA_SUBALLOCATION_TYPE_FREE); + } + if (!suballocations2nd.empty()) + { + // Null item at the end should be just pop_back(). + VMA_VALIDATE(suballocations2nd.back().type != VMA_SUBALLOCATION_TYPE_FREE); + } + + VMA_VALIDATE(m_1stNullItemsBeginCount + m_1stNullItemsMiddleCount <= suballocations1st.size()); + VMA_VALIDATE(m_2ndNullItemsCount <= suballocations2nd.size()); + + VkDeviceSize sumUsedSize = 0; + const size_t suballoc1stCount = suballocations1st.size(); + const VkDeviceSize debugMargin = GetDebugMargin(); + VkDeviceSize offset = 0; + + if (m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER) + { + const size_t suballoc2ndCount = suballocations2nd.size(); + size_t nullItem2ndCount = 0; + for (size_t i = 0; i < suballoc2ndCount; ++i) + { + const VmaSuballocation& suballoc = suballocations2nd[i]; + const bool currFree = (suballoc.type == VMA_SUBALLOCATION_TYPE_FREE); + + VmaAllocation const alloc = (VmaAllocation)suballoc.userData; + if (!IsVirtual()) + { + VMA_VALIDATE(currFree == (alloc == VK_NULL_HANDLE)); + } + VMA_VALIDATE(suballoc.offset >= offset); + + if (!currFree) + { + if (!IsVirtual()) + { + VMA_VALIDATE((VkDeviceSize)alloc->GetAllocHandle() == suballoc.offset + 1); + VMA_VALIDATE(alloc->GetSize() == suballoc.size); + } + sumUsedSize += suballoc.size; + } + else + { + ++nullItem2ndCount; + } + + offset = suballoc.offset + suballoc.size + debugMargin; + } + + VMA_VALIDATE(nullItem2ndCount == m_2ndNullItemsCount); + } + + for (size_t i = 0; i < m_1stNullItemsBeginCount; ++i) + { + const VmaSuballocation& suballoc = suballocations1st[i]; + VMA_VALIDATE(suballoc.type == VMA_SUBALLOCATION_TYPE_FREE && + suballoc.userData == VMA_NULL); + } + + size_t nullItem1stCount = m_1stNullItemsBeginCount; + + for (size_t i = m_1stNullItemsBeginCount; i < suballoc1stCount; ++i) + { + const VmaSuballocation& suballoc = suballocations1st[i]; + const bool currFree = (suballoc.type == VMA_SUBALLOCATION_TYPE_FREE); + + VmaAllocation const alloc = (VmaAllocation)suballoc.userData; + if (!IsVirtual()) + { + VMA_VALIDATE(currFree == (alloc == VK_NULL_HANDLE)); + } + VMA_VALIDATE(suballoc.offset >= offset); + VMA_VALIDATE(i >= m_1stNullItemsBeginCount || currFree); + + if (!currFree) + { + if (!IsVirtual()) + { + VMA_VALIDATE((VkDeviceSize)alloc->GetAllocHandle() == suballoc.offset + 1); + VMA_VALIDATE(alloc->GetSize() == suballoc.size); + } + sumUsedSize += suballoc.size; + } + else + { + ++nullItem1stCount; + } + + offset = suballoc.offset + suballoc.size + debugMargin; + } + VMA_VALIDATE(nullItem1stCount == m_1stNullItemsBeginCount + m_1stNullItemsMiddleCount); + + if (m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK) + { + const size_t suballoc2ndCount = suballocations2nd.size(); + size_t nullItem2ndCount = 0; + for (size_t i = suballoc2ndCount; i--; ) + { + const VmaSuballocation& suballoc = suballocations2nd[i]; + const bool currFree = (suballoc.type == VMA_SUBALLOCATION_TYPE_FREE); + + VmaAllocation const alloc = (VmaAllocation)suballoc.userData; + if (!IsVirtual()) + { + VMA_VALIDATE(currFree == (alloc == VK_NULL_HANDLE)); + } + VMA_VALIDATE(suballoc.offset >= offset); + + if (!currFree) + { + if (!IsVirtual()) + { + VMA_VALIDATE((VkDeviceSize)alloc->GetAllocHandle() == suballoc.offset + 1); + VMA_VALIDATE(alloc->GetSize() == suballoc.size); + } + sumUsedSize += suballoc.size; + } + else + { + ++nullItem2ndCount; + } + + offset = suballoc.offset + suballoc.size + debugMargin; + } + + VMA_VALIDATE(nullItem2ndCount == m_2ndNullItemsCount); + } + + VMA_VALIDATE(offset <= GetSize()); + VMA_VALIDATE(m_SumFreeSize == GetSize() - sumUsedSize); + + return true; +} + +size_t VmaBlockMetadata_Linear::GetAllocationCount() const +{ + return AccessSuballocations1st().size() - m_1stNullItemsBeginCount - m_1stNullItemsMiddleCount + + AccessSuballocations2nd().size() - m_2ndNullItemsCount; +} + +size_t VmaBlockMetadata_Linear::GetFreeRegionsCount() const +{ + // Function only used for defragmentation, which is disabled for this algorithm + VMA_ASSERT(0); + return SIZE_MAX; +} + +void VmaBlockMetadata_Linear::AddDetailedStatistics(VmaDetailedStatistics& inoutStats) const +{ + const VkDeviceSize size = GetSize(); + const SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + const SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + const size_t suballoc1stCount = suballocations1st.size(); + const size_t suballoc2ndCount = suballocations2nd.size(); + + inoutStats.statistics.blockCount++; + inoutStats.statistics.blockBytes += size; + + VkDeviceSize lastOffset = 0; + + if (m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER) + { + const VkDeviceSize freeSpace2ndTo1stEnd = suballocations1st[m_1stNullItemsBeginCount].offset; + size_t nextAlloc2ndIndex = 0; + while (lastOffset < freeSpace2ndTo1stEnd) + { + // Find next non-null allocation or move nextAllocIndex to the end. + while (nextAlloc2ndIndex < suballoc2ndCount && + suballocations2nd[nextAlloc2ndIndex].userData == VMA_NULL) + { + ++nextAlloc2ndIndex; + } + + // Found non-null allocation. + if (nextAlloc2ndIndex < suballoc2ndCount) + { + const VmaSuballocation& suballoc = suballocations2nd[nextAlloc2ndIndex]; + + // 1. Process free space before this allocation. + if (lastOffset < suballoc.offset) + { + // There is free space from lastOffset to suballoc.offset. + const VkDeviceSize unusedRangeSize = suballoc.offset - lastOffset; + VmaAddDetailedStatisticsUnusedRange(inoutStats, unusedRangeSize); + } + + // 2. Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + VmaAddDetailedStatisticsAllocation(inoutStats, suballoc.size); + + // 3. Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + ++nextAlloc2ndIndex; + } + // We are at the end. + else + { + // There is free space from lastOffset to freeSpace2ndTo1stEnd. + if (lastOffset < freeSpace2ndTo1stEnd) + { + const VkDeviceSize unusedRangeSize = freeSpace2ndTo1stEnd - lastOffset; + VmaAddDetailedStatisticsUnusedRange(inoutStats, unusedRangeSize); + } + + // End of loop. + lastOffset = freeSpace2ndTo1stEnd; + } + } + } + + size_t nextAlloc1stIndex = m_1stNullItemsBeginCount; + const VkDeviceSize freeSpace1stTo2ndEnd = + m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK ? suballocations2nd.back().offset : size; + while (lastOffset < freeSpace1stTo2ndEnd) + { + // Find next non-null allocation or move nextAllocIndex to the end. + while (nextAlloc1stIndex < suballoc1stCount && + suballocations1st[nextAlloc1stIndex].userData == VMA_NULL) + { + ++nextAlloc1stIndex; + } + + // Found non-null allocation. + if (nextAlloc1stIndex < suballoc1stCount) + { + const VmaSuballocation& suballoc = suballocations1st[nextAlloc1stIndex]; + + // 1. Process free space before this allocation. + if (lastOffset < suballoc.offset) + { + // There is free space from lastOffset to suballoc.offset. + const VkDeviceSize unusedRangeSize = suballoc.offset - lastOffset; + VmaAddDetailedStatisticsUnusedRange(inoutStats, unusedRangeSize); + } + + // 2. Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + VmaAddDetailedStatisticsAllocation(inoutStats, suballoc.size); + + // 3. Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + ++nextAlloc1stIndex; + } + // We are at the end. + else + { + // There is free space from lastOffset to freeSpace1stTo2ndEnd. + if (lastOffset < freeSpace1stTo2ndEnd) + { + const VkDeviceSize unusedRangeSize = freeSpace1stTo2ndEnd - lastOffset; + VmaAddDetailedStatisticsUnusedRange(inoutStats, unusedRangeSize); + } + + // End of loop. + lastOffset = freeSpace1stTo2ndEnd; + } + } + + if (m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK) + { + size_t nextAlloc2ndIndex = suballocations2nd.size() - 1; + while (lastOffset < size) + { + // Find next non-null allocation or move nextAllocIndex to the end. + while (nextAlloc2ndIndex != SIZE_MAX && + suballocations2nd[nextAlloc2ndIndex].userData == VMA_NULL) + { + --nextAlloc2ndIndex; + } + + // Found non-null allocation. + if (nextAlloc2ndIndex != SIZE_MAX) + { + const VmaSuballocation& suballoc = suballocations2nd[nextAlloc2ndIndex]; + + // 1. Process free space before this allocation. + if (lastOffset < suballoc.offset) + { + // There is free space from lastOffset to suballoc.offset. + const VkDeviceSize unusedRangeSize = suballoc.offset - lastOffset; + VmaAddDetailedStatisticsUnusedRange(inoutStats, unusedRangeSize); + } + + // 2. Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + VmaAddDetailedStatisticsAllocation(inoutStats, suballoc.size); + + // 3. Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + --nextAlloc2ndIndex; + } + // We are at the end. + else + { + // There is free space from lastOffset to size. + if (lastOffset < size) + { + const VkDeviceSize unusedRangeSize = size - lastOffset; + VmaAddDetailedStatisticsUnusedRange(inoutStats, unusedRangeSize); + } + + // End of loop. + lastOffset = size; + } + } + } +} + +void VmaBlockMetadata_Linear::AddStatistics(VmaStatistics& inoutStats) const +{ + const SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + const SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + const VkDeviceSize size = GetSize(); + const size_t suballoc1stCount = suballocations1st.size(); + const size_t suballoc2ndCount = suballocations2nd.size(); + + inoutStats.blockCount++; + inoutStats.blockBytes += size; + inoutStats.allocationBytes += size - m_SumFreeSize; + + VkDeviceSize lastOffset = 0; + + if (m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER) + { + const VkDeviceSize freeSpace2ndTo1stEnd = suballocations1st[m_1stNullItemsBeginCount].offset; + size_t nextAlloc2ndIndex = m_1stNullItemsBeginCount; + while (lastOffset < freeSpace2ndTo1stEnd) + { + // Find next non-null allocation or move nextAlloc2ndIndex to the end. + while (nextAlloc2ndIndex < suballoc2ndCount && + suballocations2nd[nextAlloc2ndIndex].userData == VMA_NULL) + { + ++nextAlloc2ndIndex; + } + + // Found non-null allocation. + if (nextAlloc2ndIndex < suballoc2ndCount) + { + const VmaSuballocation& suballoc = suballocations2nd[nextAlloc2ndIndex]; + + // Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + ++inoutStats.allocationCount; + + // Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + ++nextAlloc2ndIndex; + } + // We are at the end. + else + { + // End of loop. + lastOffset = freeSpace2ndTo1stEnd; + } + } + } + + size_t nextAlloc1stIndex = m_1stNullItemsBeginCount; + const VkDeviceSize freeSpace1stTo2ndEnd = + m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK ? suballocations2nd.back().offset : size; + while (lastOffset < freeSpace1stTo2ndEnd) + { + // Find next non-null allocation or move nextAllocIndex to the end. + while (nextAlloc1stIndex < suballoc1stCount && + suballocations1st[nextAlloc1stIndex].userData == VMA_NULL) + { + ++nextAlloc1stIndex; + } + + // Found non-null allocation. + if (nextAlloc1stIndex < suballoc1stCount) + { + const VmaSuballocation& suballoc = suballocations1st[nextAlloc1stIndex]; + + // Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + ++inoutStats.allocationCount; + + // Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + ++nextAlloc1stIndex; + } + // We are at the end. + else + { + // End of loop. + lastOffset = freeSpace1stTo2ndEnd; + } + } + + if (m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK) + { + size_t nextAlloc2ndIndex = suballocations2nd.size() - 1; + while (lastOffset < size) + { + // Find next non-null allocation or move nextAlloc2ndIndex to the end. + while (nextAlloc2ndIndex != SIZE_MAX && + suballocations2nd[nextAlloc2ndIndex].userData == VMA_NULL) + { + --nextAlloc2ndIndex; + } + + // Found non-null allocation. + if (nextAlloc2ndIndex != SIZE_MAX) + { + const VmaSuballocation& suballoc = suballocations2nd[nextAlloc2ndIndex]; + + // Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + ++inoutStats.allocationCount; + + // Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + --nextAlloc2ndIndex; + } + // We are at the end. + else + { + // End of loop. + lastOffset = size; + } + } + } +} + +#if VMA_STATS_STRING_ENABLED +void VmaBlockMetadata_Linear::PrintDetailedMap(class VmaJsonWriter& json) const +{ + const VkDeviceSize size = GetSize(); + const SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + const SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + const size_t suballoc1stCount = suballocations1st.size(); + const size_t suballoc2ndCount = suballocations2nd.size(); + + // FIRST PASS + + size_t unusedRangeCount = 0; + VkDeviceSize usedBytes = 0; + + VkDeviceSize lastOffset = 0; + + size_t alloc2ndCount = 0; + if (m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER) + { + const VkDeviceSize freeSpace2ndTo1stEnd = suballocations1st[m_1stNullItemsBeginCount].offset; + size_t nextAlloc2ndIndex = 0; + while (lastOffset < freeSpace2ndTo1stEnd) + { + // Find next non-null allocation or move nextAlloc2ndIndex to the end. + while (nextAlloc2ndIndex < suballoc2ndCount && + suballocations2nd[nextAlloc2ndIndex].userData == VMA_NULL) + { + ++nextAlloc2ndIndex; + } + + // Found non-null allocation. + if (nextAlloc2ndIndex < suballoc2ndCount) + { + const VmaSuballocation& suballoc = suballocations2nd[nextAlloc2ndIndex]; + + // 1. Process free space before this allocation. + if (lastOffset < suballoc.offset) + { + // There is free space from lastOffset to suballoc.offset. + ++unusedRangeCount; + } + + // 2. Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + ++alloc2ndCount; + usedBytes += suballoc.size; + + // 3. Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + ++nextAlloc2ndIndex; + } + // We are at the end. + else + { + if (lastOffset < freeSpace2ndTo1stEnd) + { + // There is free space from lastOffset to freeSpace2ndTo1stEnd. + ++unusedRangeCount; + } + + // End of loop. + lastOffset = freeSpace2ndTo1stEnd; + } + } + } + + size_t nextAlloc1stIndex = m_1stNullItemsBeginCount; + size_t alloc1stCount = 0; + const VkDeviceSize freeSpace1stTo2ndEnd = + m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK ? suballocations2nd.back().offset : size; + while (lastOffset < freeSpace1stTo2ndEnd) + { + // Find next non-null allocation or move nextAllocIndex to the end. + while (nextAlloc1stIndex < suballoc1stCount && + suballocations1st[nextAlloc1stIndex].userData == VMA_NULL) + { + ++nextAlloc1stIndex; + } + + // Found non-null allocation. + if (nextAlloc1stIndex < suballoc1stCount) + { + const VmaSuballocation& suballoc = suballocations1st[nextAlloc1stIndex]; + + // 1. Process free space before this allocation. + if (lastOffset < suballoc.offset) + { + // There is free space from lastOffset to suballoc.offset. + ++unusedRangeCount; + } + + // 2. Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + ++alloc1stCount; + usedBytes += suballoc.size; + + // 3. Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + ++nextAlloc1stIndex; + } + // We are at the end. + else + { + if (lastOffset < freeSpace1stTo2ndEnd) + { + // There is free space from lastOffset to freeSpace1stTo2ndEnd. + ++unusedRangeCount; + } + + // End of loop. + lastOffset = freeSpace1stTo2ndEnd; + } + } + + if (m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK) + { + size_t nextAlloc2ndIndex = suballocations2nd.size() - 1; + while (lastOffset < size) + { + // Find next non-null allocation or move nextAlloc2ndIndex to the end. + while (nextAlloc2ndIndex != SIZE_MAX && + suballocations2nd[nextAlloc2ndIndex].userData == VMA_NULL) + { + --nextAlloc2ndIndex; + } + + // Found non-null allocation. + if (nextAlloc2ndIndex != SIZE_MAX) + { + const VmaSuballocation& suballoc = suballocations2nd[nextAlloc2ndIndex]; + + // 1. Process free space before this allocation. + if (lastOffset < suballoc.offset) + { + // There is free space from lastOffset to suballoc.offset. + ++unusedRangeCount; + } + + // 2. Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + ++alloc2ndCount; + usedBytes += suballoc.size; + + // 3. Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + --nextAlloc2ndIndex; + } + // We are at the end. + else + { + if (lastOffset < size) + { + // There is free space from lastOffset to size. + ++unusedRangeCount; + } + + // End of loop. + lastOffset = size; + } + } + } + + const VkDeviceSize unusedBytes = size - usedBytes; + PrintDetailedMap_Begin(json, unusedBytes, alloc1stCount + alloc2ndCount, unusedRangeCount); + + // SECOND PASS + lastOffset = 0; + + if (m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER) + { + const VkDeviceSize freeSpace2ndTo1stEnd = suballocations1st[m_1stNullItemsBeginCount].offset; + size_t nextAlloc2ndIndex = 0; + while (lastOffset < freeSpace2ndTo1stEnd) + { + // Find next non-null allocation or move nextAlloc2ndIndex to the end. + while (nextAlloc2ndIndex < suballoc2ndCount && + suballocations2nd[nextAlloc2ndIndex].userData == VMA_NULL) + { + ++nextAlloc2ndIndex; + } + + // Found non-null allocation. + if (nextAlloc2ndIndex < suballoc2ndCount) + { + const VmaSuballocation& suballoc = suballocations2nd[nextAlloc2ndIndex]; + + // 1. Process free space before this allocation. + if (lastOffset < suballoc.offset) + { + // There is free space from lastOffset to suballoc.offset. + const VkDeviceSize unusedRangeSize = suballoc.offset - lastOffset; + PrintDetailedMap_UnusedRange(json, lastOffset, unusedRangeSize); + } + + // 2. Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + PrintDetailedMap_Allocation(json, suballoc.offset, suballoc.size, suballoc.userData); + + // 3. Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + ++nextAlloc2ndIndex; + } + // We are at the end. + else + { + if (lastOffset < freeSpace2ndTo1stEnd) + { + // There is free space from lastOffset to freeSpace2ndTo1stEnd. + const VkDeviceSize unusedRangeSize = freeSpace2ndTo1stEnd - lastOffset; + PrintDetailedMap_UnusedRange(json, lastOffset, unusedRangeSize); + } + + // End of loop. + lastOffset = freeSpace2ndTo1stEnd; + } + } + } + + nextAlloc1stIndex = m_1stNullItemsBeginCount; + while (lastOffset < freeSpace1stTo2ndEnd) + { + // Find next non-null allocation or move nextAllocIndex to the end. + while (nextAlloc1stIndex < suballoc1stCount && + suballocations1st[nextAlloc1stIndex].userData == VMA_NULL) + { + ++nextAlloc1stIndex; + } + + // Found non-null allocation. + if (nextAlloc1stIndex < suballoc1stCount) + { + const VmaSuballocation& suballoc = suballocations1st[nextAlloc1stIndex]; + + // 1. Process free space before this allocation. + if (lastOffset < suballoc.offset) + { + // There is free space from lastOffset to suballoc.offset. + const VkDeviceSize unusedRangeSize = suballoc.offset - lastOffset; + PrintDetailedMap_UnusedRange(json, lastOffset, unusedRangeSize); + } + + // 2. Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + PrintDetailedMap_Allocation(json, suballoc.offset, suballoc.size, suballoc.userData); + + // 3. Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + ++nextAlloc1stIndex; + } + // We are at the end. + else + { + if (lastOffset < freeSpace1stTo2ndEnd) + { + // There is free space from lastOffset to freeSpace1stTo2ndEnd. + const VkDeviceSize unusedRangeSize = freeSpace1stTo2ndEnd - lastOffset; + PrintDetailedMap_UnusedRange(json, lastOffset, unusedRangeSize); + } + + // End of loop. + lastOffset = freeSpace1stTo2ndEnd; + } + } + + if (m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK) + { + size_t nextAlloc2ndIndex = suballocations2nd.size() - 1; + while (lastOffset < size) + { + // Find next non-null allocation or move nextAlloc2ndIndex to the end. + while (nextAlloc2ndIndex != SIZE_MAX && + suballocations2nd[nextAlloc2ndIndex].userData == VMA_NULL) + { + --nextAlloc2ndIndex; + } + + // Found non-null allocation. + if (nextAlloc2ndIndex != SIZE_MAX) + { + const VmaSuballocation& suballoc = suballocations2nd[nextAlloc2ndIndex]; + + // 1. Process free space before this allocation. + if (lastOffset < suballoc.offset) + { + // There is free space from lastOffset to suballoc.offset. + const VkDeviceSize unusedRangeSize = suballoc.offset - lastOffset; + PrintDetailedMap_UnusedRange(json, lastOffset, unusedRangeSize); + } + + // 2. Process this allocation. + // There is allocation with suballoc.offset, suballoc.size. + PrintDetailedMap_Allocation(json, suballoc.offset, suballoc.size, suballoc.userData); + + // 3. Prepare for next iteration. + lastOffset = suballoc.offset + suballoc.size; + --nextAlloc2ndIndex; + } + // We are at the end. + else + { + if (lastOffset < size) + { + // There is free space from lastOffset to size. + const VkDeviceSize unusedRangeSize = size - lastOffset; + PrintDetailedMap_UnusedRange(json, lastOffset, unusedRangeSize); + } + + // End of loop. + lastOffset = size; + } + } + } + + PrintDetailedMap_End(json); +} +#endif // VMA_STATS_STRING_ENABLED + +bool VmaBlockMetadata_Linear::CreateAllocationRequest( + VkDeviceSize allocSize, + VkDeviceSize allocAlignment, + bool upperAddress, + VmaSuballocationType allocType, + uint32_t strategy, + VmaAllocationRequest* pAllocationRequest) +{ + VMA_ASSERT(allocSize > 0); + VMA_ASSERT(allocType != VMA_SUBALLOCATION_TYPE_FREE); + VMA_ASSERT(pAllocationRequest != VMA_NULL); + VMA_HEAVY_ASSERT(Validate()); + + if(allocSize > GetSize()) + return false; + + pAllocationRequest->size = allocSize; + return upperAddress ? + CreateAllocationRequest_UpperAddress( + allocSize, allocAlignment, allocType, strategy, pAllocationRequest) : + CreateAllocationRequest_LowerAddress( + allocSize, allocAlignment, allocType, strategy, pAllocationRequest); +} + +VkResult VmaBlockMetadata_Linear::CheckCorruption(const void* pBlockData) +{ + VMA_ASSERT(!IsVirtual()); + SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + for (size_t i = m_1stNullItemsBeginCount, count = suballocations1st.size(); i < count; ++i) + { + const VmaSuballocation& suballoc = suballocations1st[i]; + if (suballoc.type != VMA_SUBALLOCATION_TYPE_FREE) + { + if (!VmaValidateMagicValue(pBlockData, suballoc.offset + suballoc.size)) + { + VMA_ASSERT(0 && "MEMORY CORRUPTION DETECTED AFTER VALIDATED ALLOCATION!"); + return VK_ERROR_UNKNOWN_COPY; + } + } + } + + SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + for (size_t i = 0, count = suballocations2nd.size(); i < count; ++i) + { + const VmaSuballocation& suballoc = suballocations2nd[i]; + if (suballoc.type != VMA_SUBALLOCATION_TYPE_FREE) + { + if (!VmaValidateMagicValue(pBlockData, suballoc.offset + suballoc.size)) + { + VMA_ASSERT(0 && "MEMORY CORRUPTION DETECTED AFTER VALIDATED ALLOCATION!"); + return VK_ERROR_UNKNOWN_COPY; + } + } + } + + return VK_SUCCESS; +} + +void VmaBlockMetadata_Linear::Alloc( + const VmaAllocationRequest& request, + VmaSuballocationType type, + void* userData) +{ + const VkDeviceSize offset = (VkDeviceSize)request.allocHandle - 1; + const VmaSuballocation newSuballoc = { offset, request.size, userData, type }; + + switch (request.type) + { + case VmaAllocationRequestType::UpperAddress: + { + VMA_ASSERT(m_2ndVectorMode != SECOND_VECTOR_RING_BUFFER && + "CRITICAL ERROR: Trying to use linear allocator as double stack while it was already used as ring buffer."); + SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + suballocations2nd.push_back(newSuballoc); + m_2ndVectorMode = SECOND_VECTOR_DOUBLE_STACK; + } + break; + case VmaAllocationRequestType::EndOf1st: + { + SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + + VMA_ASSERT(suballocations1st.empty() || + offset >= suballocations1st.back().offset + suballocations1st.back().size); + // Check if it fits before the end of the block. + VMA_ASSERT(offset + request.size <= GetSize()); + + suballocations1st.push_back(newSuballoc); + } + break; + case VmaAllocationRequestType::EndOf2nd: + { + SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + // New allocation at the end of 2-part ring buffer, so before first allocation from 1st vector. + VMA_ASSERT(!suballocations1st.empty() && + offset + request.size <= suballocations1st[m_1stNullItemsBeginCount].offset); + SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + + switch (m_2ndVectorMode) + { + case SECOND_VECTOR_EMPTY: + // First allocation from second part ring buffer. + VMA_ASSERT(suballocations2nd.empty()); + m_2ndVectorMode = SECOND_VECTOR_RING_BUFFER; + break; + case SECOND_VECTOR_RING_BUFFER: + // 2-part ring buffer is already started. + VMA_ASSERT(!suballocations2nd.empty()); + break; + case SECOND_VECTOR_DOUBLE_STACK: + VMA_ASSERT(0 && "CRITICAL ERROR: Trying to use linear allocator as ring buffer while it was already used as double stack."); + break; + default: + VMA_ASSERT(0); + } + + suballocations2nd.push_back(newSuballoc); + } + break; + default: + VMA_ASSERT(0 && "CRITICAL INTERNAL ERROR."); + } + + m_SumFreeSize -= newSuballoc.size; +} + +void VmaBlockMetadata_Linear::Free(VmaAllocHandle allocHandle) +{ + SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + VkDeviceSize offset = (VkDeviceSize)allocHandle - 1; + + if (!suballocations1st.empty()) + { + // First allocation: Mark it as next empty at the beginning. + VmaSuballocation& firstSuballoc = suballocations1st[m_1stNullItemsBeginCount]; + if (firstSuballoc.offset == offset) + { + firstSuballoc.type = VMA_SUBALLOCATION_TYPE_FREE; + firstSuballoc.userData = VMA_NULL; + m_SumFreeSize += firstSuballoc.size; + ++m_1stNullItemsBeginCount; + CleanupAfterFree(); + return; + } + } + + // Last allocation in 2-part ring buffer or top of upper stack (same logic). + if (m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER || + m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK) + { + VmaSuballocation& lastSuballoc = suballocations2nd.back(); + if (lastSuballoc.offset == offset) + { + m_SumFreeSize += lastSuballoc.size; + suballocations2nd.pop_back(); + CleanupAfterFree(); + return; + } + } + // Last allocation in 1st vector. + else if (m_2ndVectorMode == SECOND_VECTOR_EMPTY) + { + VmaSuballocation& lastSuballoc = suballocations1st.back(); + if (lastSuballoc.offset == offset) + { + m_SumFreeSize += lastSuballoc.size; + suballocations1st.pop_back(); + CleanupAfterFree(); + return; + } + } + + VmaSuballocation refSuballoc; + refSuballoc.offset = offset; + // Rest of members stays uninitialized intentionally for better performance. + + // Item from the middle of 1st vector. + { + const SuballocationVectorType::iterator it = VmaBinaryFindSorted( + suballocations1st.begin() + m_1stNullItemsBeginCount, + suballocations1st.end(), + refSuballoc, + VmaSuballocationOffsetLess()); + if (it != suballocations1st.end()) + { + it->type = VMA_SUBALLOCATION_TYPE_FREE; + it->userData = VMA_NULL; + ++m_1stNullItemsMiddleCount; + m_SumFreeSize += it->size; + CleanupAfterFree(); + return; + } + } + + if (m_2ndVectorMode != SECOND_VECTOR_EMPTY) + { + // Item from the middle of 2nd vector. + const SuballocationVectorType::iterator it = m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER ? + VmaBinaryFindSorted(suballocations2nd.begin(), suballocations2nd.end(), refSuballoc, VmaSuballocationOffsetLess()) : + VmaBinaryFindSorted(suballocations2nd.begin(), suballocations2nd.end(), refSuballoc, VmaSuballocationOffsetGreater()); + if (it != suballocations2nd.end()) + { + it->type = VMA_SUBALLOCATION_TYPE_FREE; + it->userData = VMA_NULL; + ++m_2ndNullItemsCount; + m_SumFreeSize += it->size; + CleanupAfterFree(); + return; + } + } + + VMA_ASSERT(0 && "Allocation to free not found in linear allocator!"); +} + +void VmaBlockMetadata_Linear::GetAllocationInfo(VmaAllocHandle allocHandle, VmaVirtualAllocationInfo& outInfo) +{ + outInfo.offset = (VkDeviceSize)allocHandle - 1; + VmaSuballocation& suballoc = FindSuballocation(outInfo.offset); + outInfo.size = suballoc.size; + outInfo.pUserData = suballoc.userData; +} + +void* VmaBlockMetadata_Linear::GetAllocationUserData(VmaAllocHandle allocHandle) const +{ + return FindSuballocation((VkDeviceSize)allocHandle - 1).userData; +} + +VmaAllocHandle VmaBlockMetadata_Linear::GetAllocationListBegin() const +{ + // Function only used for defragmentation, which is disabled for this algorithm + VMA_ASSERT(0); + return VK_NULL_HANDLE; +} + +VmaAllocHandle VmaBlockMetadata_Linear::GetNextAllocation(VmaAllocHandle prevAlloc) const +{ + // Function only used for defragmentation, which is disabled for this algorithm + VMA_ASSERT(0); + return VK_NULL_HANDLE; +} + +VkDeviceSize VmaBlockMetadata_Linear::GetNextFreeRegionSize(VmaAllocHandle alloc) const +{ + // Function only used for defragmentation, which is disabled for this algorithm + VMA_ASSERT(0); + return 0; +} + +void VmaBlockMetadata_Linear::Clear() +{ + m_SumFreeSize = GetSize(); + m_Suballocations0.clear(); + m_Suballocations1.clear(); + // Leaving m_1stVectorIndex unchanged - it doesn't matter. + m_2ndVectorMode = SECOND_VECTOR_EMPTY; + m_1stNullItemsBeginCount = 0; + m_1stNullItemsMiddleCount = 0; + m_2ndNullItemsCount = 0; +} + +void VmaBlockMetadata_Linear::SetAllocationUserData(VmaAllocHandle allocHandle, void* userData) +{ + VmaSuballocation& suballoc = FindSuballocation((VkDeviceSize)allocHandle - 1); + suballoc.userData = userData; +} + +void VmaBlockMetadata_Linear::DebugLogAllAllocations() const +{ + const SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + for (auto it = suballocations1st.begin() + m_1stNullItemsBeginCount; it != suballocations1st.end(); ++it) + if (it->type != VMA_SUBALLOCATION_TYPE_FREE) + DebugLogAllocation(it->offset, it->size, it->userData); + + const SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + for (auto it = suballocations2nd.begin(); it != suballocations2nd.end(); ++it) + if (it->type != VMA_SUBALLOCATION_TYPE_FREE) + DebugLogAllocation(it->offset, it->size, it->userData); +} + +VmaSuballocation& VmaBlockMetadata_Linear::FindSuballocation(VkDeviceSize offset) const +{ + const SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + const SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + + VmaSuballocation refSuballoc; + refSuballoc.offset = offset; + // Rest of members stays uninitialized intentionally for better performance. + + // Item from the 1st vector. + { + SuballocationVectorType::const_iterator it = VmaBinaryFindSorted( + suballocations1st.begin() + m_1stNullItemsBeginCount, + suballocations1st.end(), + refSuballoc, + VmaSuballocationOffsetLess()); + if (it != suballocations1st.end()) + { + return const_cast(*it); + } + } + + if (m_2ndVectorMode != SECOND_VECTOR_EMPTY) + { + // Rest of members stays uninitialized intentionally for better performance. + SuballocationVectorType::const_iterator it = m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER ? + VmaBinaryFindSorted(suballocations2nd.begin(), suballocations2nd.end(), refSuballoc, VmaSuballocationOffsetLess()) : + VmaBinaryFindSorted(suballocations2nd.begin(), suballocations2nd.end(), refSuballoc, VmaSuballocationOffsetGreater()); + if (it != suballocations2nd.end()) + { + return const_cast(*it); + } + } + + VMA_ASSERT(0 && "Allocation not found in linear allocator!"); + return const_cast(suballocations1st.back()); // Should never occur. +} + +bool VmaBlockMetadata_Linear::ShouldCompact1st() const +{ + const size_t nullItemCount = m_1stNullItemsBeginCount + m_1stNullItemsMiddleCount; + const size_t suballocCount = AccessSuballocations1st().size(); + return suballocCount > 32 && nullItemCount * 2 >= (suballocCount - nullItemCount) * 3; +} + +void VmaBlockMetadata_Linear::CleanupAfterFree() +{ + SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + + if (IsEmpty()) + { + suballocations1st.clear(); + suballocations2nd.clear(); + m_1stNullItemsBeginCount = 0; + m_1stNullItemsMiddleCount = 0; + m_2ndNullItemsCount = 0; + m_2ndVectorMode = SECOND_VECTOR_EMPTY; + } + else + { + const size_t suballoc1stCount = suballocations1st.size(); + const size_t nullItem1stCount = m_1stNullItemsBeginCount + m_1stNullItemsMiddleCount; + VMA_ASSERT(nullItem1stCount <= suballoc1stCount); + + // Find more null items at the beginning of 1st vector. + while (m_1stNullItemsBeginCount < suballoc1stCount && + suballocations1st[m_1stNullItemsBeginCount].type == VMA_SUBALLOCATION_TYPE_FREE) + { + ++m_1stNullItemsBeginCount; + --m_1stNullItemsMiddleCount; + } + + // Find more null items at the end of 1st vector. + while (m_1stNullItemsMiddleCount > 0 && + suballocations1st.back().type == VMA_SUBALLOCATION_TYPE_FREE) + { + --m_1stNullItemsMiddleCount; + suballocations1st.pop_back(); + } + + // Find more null items at the end of 2nd vector. + while (m_2ndNullItemsCount > 0 && + suballocations2nd.back().type == VMA_SUBALLOCATION_TYPE_FREE) + { + --m_2ndNullItemsCount; + suballocations2nd.pop_back(); + } + + // Find more null items at the beginning of 2nd vector. + while (m_2ndNullItemsCount > 0 && + suballocations2nd[0].type == VMA_SUBALLOCATION_TYPE_FREE) + { + --m_2ndNullItemsCount; + VmaVectorRemove(suballocations2nd, 0); + } + + if (ShouldCompact1st()) + { + const size_t nonNullItemCount = suballoc1stCount - nullItem1stCount; + size_t srcIndex = m_1stNullItemsBeginCount; + for (size_t dstIndex = 0; dstIndex < nonNullItemCount; ++dstIndex) + { + while (suballocations1st[srcIndex].type == VMA_SUBALLOCATION_TYPE_FREE) + { + ++srcIndex; + } + if (dstIndex != srcIndex) + { + suballocations1st[dstIndex] = suballocations1st[srcIndex]; + } + ++srcIndex; + } + suballocations1st.resize(nonNullItemCount); + m_1stNullItemsBeginCount = 0; + m_1stNullItemsMiddleCount = 0; + } + + // 2nd vector became empty. + if (suballocations2nd.empty()) + { + m_2ndVectorMode = SECOND_VECTOR_EMPTY; + } + + // 1st vector became empty. + if (suballocations1st.size() - m_1stNullItemsBeginCount == 0) + { + suballocations1st.clear(); + m_1stNullItemsBeginCount = 0; + + if (!suballocations2nd.empty() && m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER) + { + // Swap 1st with 2nd. Now 2nd is empty. + m_2ndVectorMode = SECOND_VECTOR_EMPTY; + m_1stNullItemsMiddleCount = m_2ndNullItemsCount; + while (m_1stNullItemsBeginCount < suballocations2nd.size() && + suballocations2nd[m_1stNullItemsBeginCount].type == VMA_SUBALLOCATION_TYPE_FREE) + { + ++m_1stNullItemsBeginCount; + --m_1stNullItemsMiddleCount; + } + m_2ndNullItemsCount = 0; + m_1stVectorIndex ^= 1; + } + } + } + + VMA_HEAVY_ASSERT(Validate()); +} + +bool VmaBlockMetadata_Linear::CreateAllocationRequest_LowerAddress( + VkDeviceSize allocSize, + VkDeviceSize allocAlignment, + VmaSuballocationType allocType, + uint32_t strategy, + VmaAllocationRequest* pAllocationRequest) +{ + const VkDeviceSize blockSize = GetSize(); + const VkDeviceSize debugMargin = GetDebugMargin(); + const VkDeviceSize bufferImageGranularity = GetBufferImageGranularity(); + SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + + if (m_2ndVectorMode == SECOND_VECTOR_EMPTY || m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK) + { + // Try to allocate at the end of 1st vector. + + VkDeviceSize resultBaseOffset = 0; + if (!suballocations1st.empty()) + { + const VmaSuballocation& lastSuballoc = suballocations1st.back(); + resultBaseOffset = lastSuballoc.offset + lastSuballoc.size + debugMargin; + } + + // Start from offset equal to beginning of free space. + VkDeviceSize resultOffset = resultBaseOffset; + + // Apply alignment. + resultOffset = VmaAlignUp(resultOffset, allocAlignment); + + // Check previous suballocations for BufferImageGranularity conflicts. + // Make bigger alignment if necessary. + if (bufferImageGranularity > 1 && bufferImageGranularity != allocAlignment && !suballocations1st.empty()) + { + bool bufferImageGranularityConflict = false; + for (size_t prevSuballocIndex = suballocations1st.size(); prevSuballocIndex--; ) + { + const VmaSuballocation& prevSuballoc = suballocations1st[prevSuballocIndex]; + if (VmaBlocksOnSamePage(prevSuballoc.offset, prevSuballoc.size, resultOffset, bufferImageGranularity)) + { + if (VmaIsBufferImageGranularityConflict(prevSuballoc.type, allocType)) + { + bufferImageGranularityConflict = true; + break; + } + } + else + // Already on previous page. + break; + } + if (bufferImageGranularityConflict) + { + resultOffset = VmaAlignUp(resultOffset, bufferImageGranularity); + } + } + + const VkDeviceSize freeSpaceEnd = m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK ? + suballocations2nd.back().offset : blockSize; + + // There is enough free space at the end after alignment. + if (resultOffset + allocSize + debugMargin <= freeSpaceEnd) + { + // Check next suballocations for BufferImageGranularity conflicts. + // If conflict exists, allocation cannot be made here. + if ((allocSize % bufferImageGranularity || resultOffset % bufferImageGranularity) && m_2ndVectorMode == SECOND_VECTOR_DOUBLE_STACK) + { + for (size_t nextSuballocIndex = suballocations2nd.size(); nextSuballocIndex--; ) + { + const VmaSuballocation& nextSuballoc = suballocations2nd[nextSuballocIndex]; + if (VmaBlocksOnSamePage(resultOffset, allocSize, nextSuballoc.offset, bufferImageGranularity)) + { + if (VmaIsBufferImageGranularityConflict(allocType, nextSuballoc.type)) + { + return false; + } + } + else + { + // Already on previous page. + break; + } + } + } + + // All tests passed: Success. + pAllocationRequest->allocHandle = (VmaAllocHandle)(resultOffset + 1); + // pAllocationRequest->item, customData unused. + pAllocationRequest->type = VmaAllocationRequestType::EndOf1st; + return true; + } + } + + // Wrap-around to end of 2nd vector. Try to allocate there, watching for the + // beginning of 1st vector as the end of free space. + if (m_2ndVectorMode == SECOND_VECTOR_EMPTY || m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER) + { + VMA_ASSERT(!suballocations1st.empty()); + + VkDeviceSize resultBaseOffset = 0; + if (!suballocations2nd.empty()) + { + const VmaSuballocation& lastSuballoc = suballocations2nd.back(); + resultBaseOffset = lastSuballoc.offset + lastSuballoc.size + debugMargin; + } + + // Start from offset equal to beginning of free space. + VkDeviceSize resultOffset = resultBaseOffset; + + // Apply alignment. + resultOffset = VmaAlignUp(resultOffset, allocAlignment); + + // Check previous suballocations for BufferImageGranularity conflicts. + // Make bigger alignment if necessary. + if (bufferImageGranularity > 1 && bufferImageGranularity != allocAlignment && !suballocations2nd.empty()) + { + bool bufferImageGranularityConflict = false; + for (size_t prevSuballocIndex = suballocations2nd.size(); prevSuballocIndex--; ) + { + const VmaSuballocation& prevSuballoc = suballocations2nd[prevSuballocIndex]; + if (VmaBlocksOnSamePage(prevSuballoc.offset, prevSuballoc.size, resultOffset, bufferImageGranularity)) + { + if (VmaIsBufferImageGranularityConflict(prevSuballoc.type, allocType)) + { + bufferImageGranularityConflict = true; + break; + } + } + else + // Already on previous page. + break; + } + if (bufferImageGranularityConflict) + { + resultOffset = VmaAlignUp(resultOffset, bufferImageGranularity); + } + } + + size_t index1st = m_1stNullItemsBeginCount; + + // There is enough free space at the end after alignment. + if ((index1st == suballocations1st.size() && resultOffset + allocSize + debugMargin <= blockSize) || + (index1st < suballocations1st.size() && resultOffset + allocSize + debugMargin <= suballocations1st[index1st].offset)) + { + // Check next suballocations for BufferImageGranularity conflicts. + // If conflict exists, allocation cannot be made here. + if (allocSize % bufferImageGranularity || resultOffset % bufferImageGranularity) + { + for (size_t nextSuballocIndex = index1st; + nextSuballocIndex < suballocations1st.size(); + nextSuballocIndex++) + { + const VmaSuballocation& nextSuballoc = suballocations1st[nextSuballocIndex]; + if (VmaBlocksOnSamePage(resultOffset, allocSize, nextSuballoc.offset, bufferImageGranularity)) + { + if (VmaIsBufferImageGranularityConflict(allocType, nextSuballoc.type)) + { + return false; + } + } + else + { + // Already on next page. + break; + } + } + } + + // All tests passed: Success. + pAllocationRequest->allocHandle = (VmaAllocHandle)(resultOffset + 1); + pAllocationRequest->type = VmaAllocationRequestType::EndOf2nd; + // pAllocationRequest->item, customData unused. + return true; + } + } + + return false; +} + +bool VmaBlockMetadata_Linear::CreateAllocationRequest_UpperAddress( + VkDeviceSize allocSize, + VkDeviceSize allocAlignment, + VmaSuballocationType allocType, + uint32_t strategy, + VmaAllocationRequest* pAllocationRequest) +{ + const VkDeviceSize blockSize = GetSize(); + const VkDeviceSize bufferImageGranularity = GetBufferImageGranularity(); + SuballocationVectorType& suballocations1st = AccessSuballocations1st(); + SuballocationVectorType& suballocations2nd = AccessSuballocations2nd(); + + if (m_2ndVectorMode == SECOND_VECTOR_RING_BUFFER) + { + VMA_ASSERT(0 && "Trying to use pool with linear algorithm as double stack, while it is already being used as ring buffer."); + return false; + } + + // Try to allocate before 2nd.back(), or end of block if 2nd.empty(). + if (allocSize > blockSize) + { + return false; + } + VkDeviceSize resultBaseOffset = blockSize - allocSize; + if (!suballocations2nd.empty()) + { + const VmaSuballocation& lastSuballoc = suballocations2nd.back(); + resultBaseOffset = lastSuballoc.offset - allocSize; + if (allocSize > lastSuballoc.offset) + { + return false; + } + } + + // Start from offset equal to end of free space. + VkDeviceSize resultOffset = resultBaseOffset; + + const VkDeviceSize debugMargin = GetDebugMargin(); + + // Apply debugMargin at the end. + if (debugMargin > 0) + { + if (resultOffset < debugMargin) + { + return false; + } + resultOffset -= debugMargin; + } + + // Apply alignment. + resultOffset = VmaAlignDown(resultOffset, allocAlignment); + + // Check next suballocations from 2nd for BufferImageGranularity conflicts. + // Make bigger alignment if necessary. + if (bufferImageGranularity > 1 && bufferImageGranularity != allocAlignment && !suballocations2nd.empty()) + { + bool bufferImageGranularityConflict = false; + for (size_t nextSuballocIndex = suballocations2nd.size(); nextSuballocIndex--; ) + { + const VmaSuballocation& nextSuballoc = suballocations2nd[nextSuballocIndex]; + if (VmaBlocksOnSamePage(resultOffset, allocSize, nextSuballoc.offset, bufferImageGranularity)) + { + if (VmaIsBufferImageGranularityConflict(nextSuballoc.type, allocType)) + { + bufferImageGranularityConflict = true; + break; + } + } + else + // Already on previous page. + break; + } + if (bufferImageGranularityConflict) + { + resultOffset = VmaAlignDown(resultOffset, bufferImageGranularity); + } + } + + // There is enough free space. + const VkDeviceSize endOf1st = !suballocations1st.empty() ? + suballocations1st.back().offset + suballocations1st.back().size : + 0; + if (endOf1st + debugMargin <= resultOffset) + { + // Check previous suballocations for BufferImageGranularity conflicts. + // If conflict exists, allocation cannot be made here. + if (bufferImageGranularity > 1) + { + for (size_t prevSuballocIndex = suballocations1st.size(); prevSuballocIndex--; ) + { + const VmaSuballocation& prevSuballoc = suballocations1st[prevSuballocIndex]; + if (VmaBlocksOnSamePage(prevSuballoc.offset, prevSuballoc.size, resultOffset, bufferImageGranularity)) + { + if (VmaIsBufferImageGranularityConflict(allocType, prevSuballoc.type)) + { + return false; + } + } + else + { + // Already on next page. + break; + } + } + } + + // All tests passed: Success. + pAllocationRequest->allocHandle = (VmaAllocHandle)(resultOffset + 1); + // pAllocationRequest->item unused. + pAllocationRequest->type = VmaAllocationRequestType::UpperAddress; + return true; + } + + return false; +} +#endif // _VMA_BLOCK_METADATA_LINEAR_FUNCTIONS +#endif // _VMA_BLOCK_METADATA_LINEAR + +#ifndef _VMA_BLOCK_METADATA_TLSF +// To not search current larger region if first allocation won't succeed and skip to smaller range +// use with VMA_ALLOCATION_CREATE_STRATEGY_MIN_MEMORY_BIT as strategy in CreateAllocationRequest(). +// When fragmentation and reusal of previous blocks doesn't matter then use with +// VMA_ALLOCATION_CREATE_STRATEGY_MIN_TIME_BIT for fastest alloc time possible. +class VmaBlockMetadata_TLSF : public VmaBlockMetadata +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaBlockMetadata_TLSF) +public: + VmaBlockMetadata_TLSF(const VkAllocationCallbacks* pAllocationCallbacks, + VkDeviceSize bufferImageGranularity, bool isVirtual); + ~VmaBlockMetadata_TLSF() override; + + size_t GetAllocationCount() const override { return m_AllocCount; } + size_t GetFreeRegionsCount() const override { return m_BlocksFreeCount + 1; } + VkDeviceSize GetSumFreeSize() const override { return m_BlocksFreeSize + m_NullBlock->size; } + bool IsEmpty() const override { return m_NullBlock->offset == 0; } + VkDeviceSize GetAllocationOffset(VmaAllocHandle allocHandle) const override { return ((Block*)allocHandle)->offset; } + + void Init(VkDeviceSize size) override; + bool Validate() const override; + + void AddDetailedStatistics(VmaDetailedStatistics& inoutStats) const override; + void AddStatistics(VmaStatistics& inoutStats) const override; + +#if VMA_STATS_STRING_ENABLED + void PrintDetailedMap(class VmaJsonWriter& json) const override; +#endif + + bool CreateAllocationRequest( + VkDeviceSize allocSize, + VkDeviceSize allocAlignment, + bool upperAddress, + VmaSuballocationType allocType, + uint32_t strategy, + VmaAllocationRequest* pAllocationRequest) override; + + VkResult CheckCorruption(const void* pBlockData) override; + void Alloc( + const VmaAllocationRequest& request, + VmaSuballocationType type, + void* userData) override; + + void Free(VmaAllocHandle allocHandle) override; + void GetAllocationInfo(VmaAllocHandle allocHandle, VmaVirtualAllocationInfo& outInfo) override; + void* GetAllocationUserData(VmaAllocHandle allocHandle) const override; + VmaAllocHandle GetAllocationListBegin() const override; + VmaAllocHandle GetNextAllocation(VmaAllocHandle prevAlloc) const override; + VkDeviceSize GetNextFreeRegionSize(VmaAllocHandle alloc) const override; + void Clear() override; + void SetAllocationUserData(VmaAllocHandle allocHandle, void* userData) override; + void DebugLogAllAllocations() const override; + +private: + // According to original paper it should be preferable 4 or 5: + // M. Masmano, I. Ripoll, A. Crespo, and J. Real "TLSF: a New Dynamic Memory Allocator for Real-Time Systems" + // http://www.gii.upv.es/tlsf/files/ecrts04_tlsf.pdf + static const uint8_t SECOND_LEVEL_INDEX = 5; + static const uint16_t SMALL_BUFFER_SIZE = 256; + static const uint32_t INITIAL_BLOCK_ALLOC_COUNT = 16; + static const uint8_t MEMORY_CLASS_SHIFT = 7; + static const uint8_t MAX_MEMORY_CLASSES = 65 - MEMORY_CLASS_SHIFT; + + class Block + { + public: + VkDeviceSize offset; + VkDeviceSize size; + Block* prevPhysical; + Block* nextPhysical; + + void MarkFree() { prevFree = VMA_NULL; } + void MarkTaken() { prevFree = this; } + bool IsFree() const { return prevFree != this; } + void*& UserData() { VMA_HEAVY_ASSERT(!IsFree()); return userData; } + Block*& PrevFree() { return prevFree; } + Block*& NextFree() { VMA_HEAVY_ASSERT(IsFree()); return nextFree; } + + private: + Block* prevFree; // Address of the same block here indicates that block is taken + union + { + Block* nextFree; + void* userData; + }; + }; + + size_t m_AllocCount; + // Total number of free blocks besides null block + size_t m_BlocksFreeCount; + // Total size of free blocks excluding null block + VkDeviceSize m_BlocksFreeSize; + uint32_t m_IsFreeBitmap; + uint8_t m_MemoryClasses; + uint32_t m_InnerIsFreeBitmap[MAX_MEMORY_CLASSES]; + uint32_t m_ListsCount; + /* + * 0: 0-3 lists for small buffers + * 1+: 0-(2^SLI-1) lists for normal buffers + */ + Block** m_FreeList; + VmaPoolAllocator m_BlockAllocator; + Block* m_NullBlock; + VmaBlockBufferImageGranularity m_GranularityHandler; + + static uint8_t SizeToMemoryClass(VkDeviceSize size); + uint16_t SizeToSecondIndex(VkDeviceSize size, uint8_t memoryClass) const; + uint32_t GetListIndex(uint8_t memoryClass, uint16_t secondIndex) const; + uint32_t GetListIndex(VkDeviceSize size) const; + + void RemoveFreeBlock(Block* block); + void InsertFreeBlock(Block* block); + void MergeBlock(Block* block, Block* prev); + + Block* FindFreeBlock(VkDeviceSize size, uint32_t& listIndex) const; + bool CheckBlock( + Block& block, + uint32_t listIndex, + VkDeviceSize allocSize, + VkDeviceSize allocAlignment, + VmaSuballocationType allocType, + VmaAllocationRequest* pAllocationRequest); +}; + +#ifndef _VMA_BLOCK_METADATA_TLSF_FUNCTIONS +VmaBlockMetadata_TLSF::VmaBlockMetadata_TLSF(const VkAllocationCallbacks* pAllocationCallbacks, + VkDeviceSize bufferImageGranularity, bool isVirtual) + : VmaBlockMetadata(pAllocationCallbacks, bufferImageGranularity, isVirtual), + m_AllocCount(0), + m_BlocksFreeCount(0), + m_BlocksFreeSize(0), + m_IsFreeBitmap(0), + m_MemoryClasses(0), + m_ListsCount(0), + m_FreeList(VMA_NULL), + m_BlockAllocator(pAllocationCallbacks, INITIAL_BLOCK_ALLOC_COUNT), + m_NullBlock(VMA_NULL), + m_GranularityHandler(bufferImageGranularity) {} + +VmaBlockMetadata_TLSF::~VmaBlockMetadata_TLSF() +{ + if (m_FreeList) + vma_delete_array(GetAllocationCallbacks(), m_FreeList, m_ListsCount); + m_GranularityHandler.Destroy(GetAllocationCallbacks()); +} + +void VmaBlockMetadata_TLSF::Init(VkDeviceSize size) +{ + VmaBlockMetadata::Init(size); + + if (!IsVirtual()) + m_GranularityHandler.Init(GetAllocationCallbacks(), size); + + m_NullBlock = m_BlockAllocator.Alloc(); + m_NullBlock->size = size; + m_NullBlock->offset = 0; + m_NullBlock->prevPhysical = VMA_NULL; + m_NullBlock->nextPhysical = VMA_NULL; + m_NullBlock->MarkFree(); + m_NullBlock->NextFree() = VMA_NULL; + m_NullBlock->PrevFree() = VMA_NULL; + uint8_t memoryClass = SizeToMemoryClass(size); + uint16_t sli = SizeToSecondIndex(size, memoryClass); + m_ListsCount = (memoryClass == 0 ? 0 : (memoryClass - 1) * (1UL << SECOND_LEVEL_INDEX) + sli) + 1; + if (IsVirtual()) + m_ListsCount += 1UL << SECOND_LEVEL_INDEX; + else + m_ListsCount += 4; + + m_MemoryClasses = memoryClass + uint8_t(2); + memset(m_InnerIsFreeBitmap, 0, MAX_MEMORY_CLASSES * sizeof(uint32_t)); + + m_FreeList = vma_new_array(GetAllocationCallbacks(), Block*, m_ListsCount); + memset(m_FreeList, 0, m_ListsCount * sizeof(Block*)); +} + +bool VmaBlockMetadata_TLSF::Validate() const +{ + VMA_VALIDATE(GetSumFreeSize() <= GetSize()); + + VkDeviceSize calculatedSize = m_NullBlock->size; + VkDeviceSize calculatedFreeSize = m_NullBlock->size; + size_t allocCount = 0; + size_t freeCount = 0; + + // Check integrity of free lists + for (uint32_t list = 0; list < m_ListsCount; ++list) + { + Block* block = m_FreeList[list]; + if (block != VMA_NULL) + { + VMA_VALIDATE(block->IsFree()); + VMA_VALIDATE(block->PrevFree() == VMA_NULL); + while (block->NextFree()) + { + VMA_VALIDATE(block->NextFree()->IsFree()); + VMA_VALIDATE(block->NextFree()->PrevFree() == block); + block = block->NextFree(); + } + } + } + + VkDeviceSize nextOffset = m_NullBlock->offset; + auto validateCtx = m_GranularityHandler.StartValidation(GetAllocationCallbacks(), IsVirtual()); + + VMA_VALIDATE(m_NullBlock->nextPhysical == VMA_NULL); + if (m_NullBlock->prevPhysical) + { + VMA_VALIDATE(m_NullBlock->prevPhysical->nextPhysical == m_NullBlock); + } + // Check all blocks + for (Block* prev = m_NullBlock->prevPhysical; prev != VMA_NULL; prev = prev->prevPhysical) + { + VMA_VALIDATE(prev->offset + prev->size == nextOffset); + nextOffset = prev->offset; + calculatedSize += prev->size; + + uint32_t listIndex = GetListIndex(prev->size); + if (prev->IsFree()) + { + ++freeCount; + // Check if free block belongs to free list + Block* freeBlock = m_FreeList[listIndex]; + VMA_VALIDATE(freeBlock != VMA_NULL); + + bool found = false; + do + { + if (freeBlock == prev) + found = true; + + freeBlock = freeBlock->NextFree(); + } while (!found && freeBlock != VMA_NULL); + + VMA_VALIDATE(found); + calculatedFreeSize += prev->size; + } + else + { + ++allocCount; + // Check if taken block is not on a free list + Block* freeBlock = m_FreeList[listIndex]; + while (freeBlock) + { + VMA_VALIDATE(freeBlock != prev); + freeBlock = freeBlock->NextFree(); + } + + if (!IsVirtual()) + { + VMA_VALIDATE(m_GranularityHandler.Validate(validateCtx, prev->offset, prev->size)); + } + } + + if (prev->prevPhysical) + { + VMA_VALIDATE(prev->prevPhysical->nextPhysical == prev); + } + } + + if (!IsVirtual()) + { + VMA_VALIDATE(m_GranularityHandler.FinishValidation(validateCtx)); + } + + VMA_VALIDATE(nextOffset == 0); + VMA_VALIDATE(calculatedSize == GetSize()); + VMA_VALIDATE(calculatedFreeSize == GetSumFreeSize()); + VMA_VALIDATE(allocCount == m_AllocCount); + VMA_VALIDATE(freeCount == m_BlocksFreeCount); + + return true; +} + +void VmaBlockMetadata_TLSF::AddDetailedStatistics(VmaDetailedStatistics& inoutStats) const +{ + inoutStats.statistics.blockCount++; + inoutStats.statistics.blockBytes += GetSize(); + if (m_NullBlock->size > 0) + VmaAddDetailedStatisticsUnusedRange(inoutStats, m_NullBlock->size); + + for (Block* block = m_NullBlock->prevPhysical; block != VMA_NULL; block = block->prevPhysical) + { + if (block->IsFree()) + VmaAddDetailedStatisticsUnusedRange(inoutStats, block->size); + else + VmaAddDetailedStatisticsAllocation(inoutStats, block->size); + } +} + +void VmaBlockMetadata_TLSF::AddStatistics(VmaStatistics& inoutStats) const +{ + inoutStats.blockCount++; + inoutStats.allocationCount += (uint32_t)m_AllocCount; + inoutStats.blockBytes += GetSize(); + inoutStats.allocationBytes += GetSize() - GetSumFreeSize(); +} + +#if VMA_STATS_STRING_ENABLED +void VmaBlockMetadata_TLSF::PrintDetailedMap(class VmaJsonWriter& json) const +{ + size_t blockCount = m_AllocCount + m_BlocksFreeCount; + VmaStlAllocator allocator(GetAllocationCallbacks()); + VmaVector> blockList(blockCount, allocator); + + size_t i = blockCount; + for (Block* block = m_NullBlock->prevPhysical; block != VMA_NULL; block = block->prevPhysical) + { + blockList[--i] = block; + } + VMA_ASSERT(i == 0); + + VmaDetailedStatistics stats; + VmaClearDetailedStatistics(stats); + AddDetailedStatistics(stats); + + PrintDetailedMap_Begin(json, + stats.statistics.blockBytes - stats.statistics.allocationBytes, + stats.statistics.allocationCount, + stats.unusedRangeCount); + + for (; i < blockCount; ++i) + { + Block* block = blockList[i]; + if (block->IsFree()) + PrintDetailedMap_UnusedRange(json, block->offset, block->size); + else + PrintDetailedMap_Allocation(json, block->offset, block->size, block->UserData()); + } + if (m_NullBlock->size > 0) + PrintDetailedMap_UnusedRange(json, m_NullBlock->offset, m_NullBlock->size); + + PrintDetailedMap_End(json); +} +#endif + +bool VmaBlockMetadata_TLSF::CreateAllocationRequest( + VkDeviceSize allocSize, + VkDeviceSize allocAlignment, + bool upperAddress, + VmaSuballocationType allocType, + uint32_t strategy, + VmaAllocationRequest* pAllocationRequest) +{ + VMA_ASSERT(allocSize > 0 && "Cannot allocate empty block!"); + VMA_ASSERT(!upperAddress && "VMA_ALLOCATION_CREATE_UPPER_ADDRESS_BIT can be used only with linear algorithm."); + + // For small granularity round up + if (!IsVirtual()) + m_GranularityHandler.RoundupAllocRequest(allocType, allocSize, allocAlignment); + + allocSize += GetDebugMargin(); + // Quick check for too small pool + if (allocSize > GetSumFreeSize()) + return false; + + // If no free blocks in pool then check only null block + if (m_BlocksFreeCount == 0) + return CheckBlock(*m_NullBlock, m_ListsCount, allocSize, allocAlignment, allocType, pAllocationRequest); + + // Round up to the next block + VkDeviceSize sizeForNextList = allocSize; + VkDeviceSize smallSizeStep = VkDeviceSize(SMALL_BUFFER_SIZE / (IsVirtual() ? 1U << SECOND_LEVEL_INDEX : 4U)); + if (allocSize > SMALL_BUFFER_SIZE) + { + sizeForNextList += (1ULL << (VMA_BITSCAN_MSB(allocSize) - SECOND_LEVEL_INDEX)); + } + else if (allocSize > SMALL_BUFFER_SIZE - smallSizeStep) + sizeForNextList = SMALL_BUFFER_SIZE + 1; + else + sizeForNextList += smallSizeStep; + + uint32_t nextListIndex = m_ListsCount; + uint32_t prevListIndex = m_ListsCount; + Block* nextListBlock = VMA_NULL; + Block* prevListBlock = VMA_NULL; + + // Check blocks according to strategies + if (strategy & VMA_ALLOCATION_CREATE_STRATEGY_MIN_TIME_BIT) + { + // Quick check for larger block first + nextListBlock = FindFreeBlock(sizeForNextList, nextListIndex); + if (nextListBlock != VMA_NULL && CheckBlock(*nextListBlock, nextListIndex, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + + // If not fitted then null block + if (CheckBlock(*m_NullBlock, m_ListsCount, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + + // Null block failed, search larger bucket + while (nextListBlock) + { + if (CheckBlock(*nextListBlock, nextListIndex, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + nextListBlock = nextListBlock->NextFree(); + } + + // Failed again, check best fit bucket + prevListBlock = FindFreeBlock(allocSize, prevListIndex); + while (prevListBlock) + { + if (CheckBlock(*prevListBlock, prevListIndex, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + prevListBlock = prevListBlock->NextFree(); + } + } + else if (strategy & VMA_ALLOCATION_CREATE_STRATEGY_MIN_MEMORY_BIT) + { + // Check best fit bucket + prevListBlock = FindFreeBlock(allocSize, prevListIndex); + while (prevListBlock) + { + if (CheckBlock(*prevListBlock, prevListIndex, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + prevListBlock = prevListBlock->NextFree(); + } + + // If failed check null block + if (CheckBlock(*m_NullBlock, m_ListsCount, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + + // Check larger bucket + nextListBlock = FindFreeBlock(sizeForNextList, nextListIndex); + while (nextListBlock) + { + if (CheckBlock(*nextListBlock, nextListIndex, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + nextListBlock = nextListBlock->NextFree(); + } + } + else if (strategy & VMA_ALLOCATION_CREATE_STRATEGY_MIN_OFFSET_BIT ) + { + // Perform search from the start + VmaStlAllocator allocator(GetAllocationCallbacks()); + VmaVector> blockList(m_BlocksFreeCount, allocator); + + size_t i = m_BlocksFreeCount; + for (Block* block = m_NullBlock->prevPhysical; block != VMA_NULL; block = block->prevPhysical) + { + if (block->IsFree() && block->size >= allocSize) + blockList[--i] = block; + } + + for (; i < m_BlocksFreeCount; ++i) + { + Block& block = *blockList[i]; + if (CheckBlock(block, GetListIndex(block.size), allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + } + + // If failed check null block + if (CheckBlock(*m_NullBlock, m_ListsCount, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + + // Whole range searched, no more memory + return false; + } + else + { + // Check larger bucket + nextListBlock = FindFreeBlock(sizeForNextList, nextListIndex); + while (nextListBlock) + { + if (CheckBlock(*nextListBlock, nextListIndex, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + nextListBlock = nextListBlock->NextFree(); + } + + // If failed check null block + if (CheckBlock(*m_NullBlock, m_ListsCount, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + + // Check best fit bucket + prevListBlock = FindFreeBlock(allocSize, prevListIndex); + while (prevListBlock) + { + if (CheckBlock(*prevListBlock, prevListIndex, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + prevListBlock = prevListBlock->NextFree(); + } + } + + // Worst case, full search has to be done + while (++nextListIndex < m_ListsCount) + { + nextListBlock = m_FreeList[nextListIndex]; + while (nextListBlock) + { + if (CheckBlock(*nextListBlock, nextListIndex, allocSize, allocAlignment, allocType, pAllocationRequest)) + return true; + nextListBlock = nextListBlock->NextFree(); + } + } + + // No more memory sadly + return false; +} + +VkResult VmaBlockMetadata_TLSF::CheckCorruption(const void* pBlockData) +{ + for (Block* block = m_NullBlock->prevPhysical; block != VMA_NULL; block = block->prevPhysical) + { + if (!block->IsFree()) + { + if (!VmaValidateMagicValue(pBlockData, block->offset + block->size)) + { + VMA_ASSERT(0 && "MEMORY CORRUPTION DETECTED AFTER VALIDATED ALLOCATION!"); + return VK_ERROR_UNKNOWN_COPY; + } + } + } + + return VK_SUCCESS; +} + +void VmaBlockMetadata_TLSF::Alloc( + const VmaAllocationRequest& request, + VmaSuballocationType type, + void* userData) +{ + VMA_ASSERT(request.type == VmaAllocationRequestType::TLSF); + + // Get block and pop it from the free list + Block* currentBlock = (Block*)request.allocHandle; + VkDeviceSize offset = request.algorithmData; + VMA_ASSERT(currentBlock != VMA_NULL); + VMA_ASSERT(currentBlock->offset <= offset); + + if (currentBlock != m_NullBlock) + RemoveFreeBlock(currentBlock); + + VkDeviceSize debugMargin = GetDebugMargin(); + VkDeviceSize missingAlignment = offset - currentBlock->offset; + + // Append missing alignment to prev block or create new one + if (missingAlignment) + { + Block* prevBlock = currentBlock->prevPhysical; + VMA_ASSERT(prevBlock != VMA_NULL && "There should be no missing alignment at offset 0!"); + + if (prevBlock->IsFree() && prevBlock->size != debugMargin) + { + uint32_t oldList = GetListIndex(prevBlock->size); + prevBlock->size += missingAlignment; + // Check if new size crosses list bucket + if (oldList != GetListIndex(prevBlock->size)) + { + prevBlock->size -= missingAlignment; + RemoveFreeBlock(prevBlock); + prevBlock->size += missingAlignment; + InsertFreeBlock(prevBlock); + } + else + m_BlocksFreeSize += missingAlignment; + } + else + { + Block* newBlock = m_BlockAllocator.Alloc(); + currentBlock->prevPhysical = newBlock; + prevBlock->nextPhysical = newBlock; + newBlock->prevPhysical = prevBlock; + newBlock->nextPhysical = currentBlock; + newBlock->size = missingAlignment; + newBlock->offset = currentBlock->offset; + newBlock->MarkTaken(); + + InsertFreeBlock(newBlock); + } + + currentBlock->size -= missingAlignment; + currentBlock->offset += missingAlignment; + } + + VkDeviceSize size = request.size + debugMargin; + if (currentBlock->size == size) + { + if (currentBlock == m_NullBlock) + { + // Setup new null block + m_NullBlock = m_BlockAllocator.Alloc(); + m_NullBlock->size = 0; + m_NullBlock->offset = currentBlock->offset + size; + m_NullBlock->prevPhysical = currentBlock; + m_NullBlock->nextPhysical = VMA_NULL; + m_NullBlock->MarkFree(); + m_NullBlock->PrevFree() = VMA_NULL; + m_NullBlock->NextFree() = VMA_NULL; + currentBlock->nextPhysical = m_NullBlock; + currentBlock->MarkTaken(); + } + } + else + { + VMA_ASSERT(currentBlock->size > size && "Proper block already found, shouldn't find smaller one!"); + + // Create new free block + Block* newBlock = m_BlockAllocator.Alloc(); + newBlock->size = currentBlock->size - size; + newBlock->offset = currentBlock->offset + size; + newBlock->prevPhysical = currentBlock; + newBlock->nextPhysical = currentBlock->nextPhysical; + currentBlock->nextPhysical = newBlock; + currentBlock->size = size; + + if (currentBlock == m_NullBlock) + { + m_NullBlock = newBlock; + m_NullBlock->MarkFree(); + m_NullBlock->NextFree() = VMA_NULL; + m_NullBlock->PrevFree() = VMA_NULL; + currentBlock->MarkTaken(); + } + else + { + newBlock->nextPhysical->prevPhysical = newBlock; + newBlock->MarkTaken(); + InsertFreeBlock(newBlock); + } + } + currentBlock->UserData() = userData; + + if (debugMargin > 0) + { + currentBlock->size -= debugMargin; + Block* newBlock = m_BlockAllocator.Alloc(); + newBlock->size = debugMargin; + newBlock->offset = currentBlock->offset + currentBlock->size; + newBlock->prevPhysical = currentBlock; + newBlock->nextPhysical = currentBlock->nextPhysical; + newBlock->MarkTaken(); + currentBlock->nextPhysical->prevPhysical = newBlock; + currentBlock->nextPhysical = newBlock; + InsertFreeBlock(newBlock); + } + + if (!IsVirtual()) + m_GranularityHandler.AllocPages((uint8_t)(uintptr_t)request.customData, + currentBlock->offset, currentBlock->size); + ++m_AllocCount; +} + +void VmaBlockMetadata_TLSF::Free(VmaAllocHandle allocHandle) +{ + Block* block = (Block*)allocHandle; + Block* next = block->nextPhysical; + VMA_ASSERT(!block->IsFree() && "Block is already free!"); + + if (!IsVirtual()) + m_GranularityHandler.FreePages(block->offset, block->size); + --m_AllocCount; + + VkDeviceSize debugMargin = GetDebugMargin(); + if (debugMargin > 0) + { + RemoveFreeBlock(next); + MergeBlock(next, block); + block = next; + next = next->nextPhysical; + } + + // Try merging + Block* prev = block->prevPhysical; + if (prev != VMA_NULL && prev->IsFree() && prev->size != debugMargin) + { + RemoveFreeBlock(prev); + MergeBlock(block, prev); + } + + if (!next->IsFree()) + InsertFreeBlock(block); + else if (next == m_NullBlock) + MergeBlock(m_NullBlock, block); + else + { + RemoveFreeBlock(next); + MergeBlock(next, block); + InsertFreeBlock(next); + } +} + +void VmaBlockMetadata_TLSF::GetAllocationInfo(VmaAllocHandle allocHandle, VmaVirtualAllocationInfo& outInfo) +{ + Block* block = (Block*)allocHandle; + VMA_ASSERT(!block->IsFree() && "Cannot get allocation info for free block!"); + outInfo.offset = block->offset; + outInfo.size = block->size; + outInfo.pUserData = block->UserData(); +} + +void* VmaBlockMetadata_TLSF::GetAllocationUserData(VmaAllocHandle allocHandle) const +{ + Block* block = (Block*)allocHandle; + VMA_ASSERT(!block->IsFree() && "Cannot get user data for free block!"); + return block->UserData(); +} + +VmaAllocHandle VmaBlockMetadata_TLSF::GetAllocationListBegin() const +{ + if (m_AllocCount == 0) + return VK_NULL_HANDLE; + + for (Block* block = m_NullBlock->prevPhysical; block; block = block->prevPhysical) + { + if (!block->IsFree()) + return (VmaAllocHandle)block; + } + VMA_ASSERT(false && "If m_AllocCount > 0 then should find any allocation!"); + return VK_NULL_HANDLE; +} + +VmaAllocHandle VmaBlockMetadata_TLSF::GetNextAllocation(VmaAllocHandle prevAlloc) const +{ + Block* startBlock = (Block*)prevAlloc; + VMA_ASSERT(!startBlock->IsFree() && "Incorrect block!"); + + for (Block* block = startBlock->prevPhysical; block; block = block->prevPhysical) + { + if (!block->IsFree()) + return (VmaAllocHandle)block; + } + return VK_NULL_HANDLE; +} + +VkDeviceSize VmaBlockMetadata_TLSF::GetNextFreeRegionSize(VmaAllocHandle alloc) const +{ + Block* block = (Block*)alloc; + VMA_ASSERT(!block->IsFree() && "Incorrect block!"); + + if (block->prevPhysical) + return block->prevPhysical->IsFree() ? block->prevPhysical->size : 0; + return 0; +} + +void VmaBlockMetadata_TLSF::Clear() +{ + m_AllocCount = 0; + m_BlocksFreeCount = 0; + m_BlocksFreeSize = 0; + m_IsFreeBitmap = 0; + m_NullBlock->offset = 0; + m_NullBlock->size = GetSize(); + Block* block = m_NullBlock->prevPhysical; + m_NullBlock->prevPhysical = VMA_NULL; + while (block) + { + Block* prev = block->prevPhysical; + m_BlockAllocator.Free(block); + block = prev; + } + memset(m_FreeList, 0, m_ListsCount * sizeof(Block*)); + memset(m_InnerIsFreeBitmap, 0, m_MemoryClasses * sizeof(uint32_t)); + m_GranularityHandler.Clear(); +} + +void VmaBlockMetadata_TLSF::SetAllocationUserData(VmaAllocHandle allocHandle, void* userData) +{ + Block* block = (Block*)allocHandle; + VMA_ASSERT(!block->IsFree() && "Trying to set user data for not allocated block!"); + block->UserData() = userData; +} + +void VmaBlockMetadata_TLSF::DebugLogAllAllocations() const +{ + for (Block* block = m_NullBlock->prevPhysical; block != VMA_NULL; block = block->prevPhysical) + if (!block->IsFree()) + DebugLogAllocation(block->offset, block->size, block->UserData()); +} + +uint8_t VmaBlockMetadata_TLSF::SizeToMemoryClass(VkDeviceSize size) +{ + if (size > SMALL_BUFFER_SIZE) + return uint8_t(VMA_BITSCAN_MSB(size) - MEMORY_CLASS_SHIFT); + return 0; +} + +uint16_t VmaBlockMetadata_TLSF::SizeToSecondIndex(VkDeviceSize size, uint8_t memoryClass) const +{ + if (memoryClass == 0) + { + if (IsVirtual()) + return static_cast((size - 1) / 8); + return static_cast((size - 1) / 64); + } + return static_cast((size >> (memoryClass + MEMORY_CLASS_SHIFT - SECOND_LEVEL_INDEX)) ^ (1U << SECOND_LEVEL_INDEX)); +} + +uint32_t VmaBlockMetadata_TLSF::GetListIndex(uint8_t memoryClass, uint16_t secondIndex) const +{ + if (memoryClass == 0) + return secondIndex; + + const uint32_t index = static_cast(memoryClass - 1) * (1 << SECOND_LEVEL_INDEX) + secondIndex; + if (IsVirtual()) + return index + (1 << SECOND_LEVEL_INDEX); + return index + 4; +} + +uint32_t VmaBlockMetadata_TLSF::GetListIndex(VkDeviceSize size) const +{ + uint8_t memoryClass = SizeToMemoryClass(size); + return GetListIndex(memoryClass, SizeToSecondIndex(size, memoryClass)); +} + +void VmaBlockMetadata_TLSF::RemoveFreeBlock(Block* block) +{ + VMA_ASSERT(block != m_NullBlock); + VMA_ASSERT(block->IsFree()); + + if (block->NextFree() != VMA_NULL) + block->NextFree()->PrevFree() = block->PrevFree(); + if (block->PrevFree() != VMA_NULL) + block->PrevFree()->NextFree() = block->NextFree(); + else + { + uint8_t memClass = SizeToMemoryClass(block->size); + uint16_t secondIndex = SizeToSecondIndex(block->size, memClass); + uint32_t index = GetListIndex(memClass, secondIndex); + VMA_ASSERT(m_FreeList[index] == block); + m_FreeList[index] = block->NextFree(); + if (block->NextFree() == VMA_NULL) + { + m_InnerIsFreeBitmap[memClass] &= ~(1U << secondIndex); + if (m_InnerIsFreeBitmap[memClass] == 0) + m_IsFreeBitmap &= ~(1UL << memClass); + } + } + block->MarkTaken(); + block->UserData() = VMA_NULL; + --m_BlocksFreeCount; + m_BlocksFreeSize -= block->size; +} + +void VmaBlockMetadata_TLSF::InsertFreeBlock(Block* block) +{ + VMA_ASSERT(block != m_NullBlock); + VMA_ASSERT(!block->IsFree() && "Cannot insert block twice!"); + + uint8_t memClass = SizeToMemoryClass(block->size); + uint16_t secondIndex = SizeToSecondIndex(block->size, memClass); + uint32_t index = GetListIndex(memClass, secondIndex); + VMA_ASSERT(index < m_ListsCount); + block->PrevFree() = VMA_NULL; + block->NextFree() = m_FreeList[index]; + m_FreeList[index] = block; + if (block->NextFree() != VMA_NULL) + block->NextFree()->PrevFree() = block; + else + { + m_InnerIsFreeBitmap[memClass] |= 1U << secondIndex; + m_IsFreeBitmap |= 1UL << memClass; + } + ++m_BlocksFreeCount; + m_BlocksFreeSize += block->size; +} + +void VmaBlockMetadata_TLSF::MergeBlock(Block* block, Block* prev) +{ + VMA_ASSERT(block->prevPhysical == prev && "Cannot merge separate physical regions!"); + VMA_ASSERT(!prev->IsFree() && "Cannot merge block that belongs to free list!"); + + block->offset = prev->offset; + block->size += prev->size; + block->prevPhysical = prev->prevPhysical; + if (block->prevPhysical) + block->prevPhysical->nextPhysical = block; + m_BlockAllocator.Free(prev); +} + +VmaBlockMetadata_TLSF::Block* VmaBlockMetadata_TLSF::FindFreeBlock(VkDeviceSize size, uint32_t& listIndex) const +{ + uint8_t memoryClass = SizeToMemoryClass(size); + uint32_t innerFreeMap = m_InnerIsFreeBitmap[memoryClass] & (~0U << SizeToSecondIndex(size, memoryClass)); + if (!innerFreeMap) + { + // Check higher levels for available blocks + uint32_t freeMap = m_IsFreeBitmap & (~0UL << (memoryClass + 1)); + if (!freeMap) + return VMA_NULL; // No more memory available + + // Find lowest free region + memoryClass = VMA_BITSCAN_LSB(freeMap); + innerFreeMap = m_InnerIsFreeBitmap[memoryClass]; + VMA_ASSERT(innerFreeMap != 0); + } + // Find lowest free subregion + listIndex = GetListIndex(memoryClass, VMA_BITSCAN_LSB(innerFreeMap)); + VMA_ASSERT(m_FreeList[listIndex]); + return m_FreeList[listIndex]; +} + +bool VmaBlockMetadata_TLSF::CheckBlock( + Block& block, + uint32_t listIndex, + VkDeviceSize allocSize, + VkDeviceSize allocAlignment, + VmaSuballocationType allocType, + VmaAllocationRequest* pAllocationRequest) +{ + VMA_ASSERT(block.IsFree() && "Block is already taken!"); + + VkDeviceSize alignedOffset = VmaAlignUp(block.offset, allocAlignment); + if (block.size < allocSize + alignedOffset - block.offset) + return false; + + // Check for granularity conflicts + if (!IsVirtual() && + m_GranularityHandler.CheckConflictAndAlignUp(alignedOffset, allocSize, block.offset, block.size, allocType)) + return false; + + // Alloc successful + pAllocationRequest->type = VmaAllocationRequestType::TLSF; + pAllocationRequest->allocHandle = (VmaAllocHandle)█ + pAllocationRequest->size = allocSize - GetDebugMargin(); + pAllocationRequest->customData = (void*)allocType; + pAllocationRequest->algorithmData = alignedOffset; + + // Place block at the start of list if it's normal block + if (listIndex != m_ListsCount && block.PrevFree()) + { + block.PrevFree()->NextFree() = block.NextFree(); + if (block.NextFree()) + block.NextFree()->PrevFree() = block.PrevFree(); + block.PrevFree() = VMA_NULL; + block.NextFree() = m_FreeList[listIndex]; + m_FreeList[listIndex] = █ + if (block.NextFree()) + block.NextFree()->PrevFree() = █ + } + + return true; +} +#endif // _VMA_BLOCK_METADATA_TLSF_FUNCTIONS +#endif // _VMA_BLOCK_METADATA_TLSF + +#ifndef _VMA_BLOCK_VECTOR +/* +Sequence of VmaDeviceMemoryBlock. Represents memory blocks allocated for a specific +Vulkan memory type. + +Synchronized internally with a mutex. +*/ +class VmaBlockVector +{ + friend struct VmaDefragmentationContext_T; + VMA_CLASS_NO_COPY_NO_MOVE(VmaBlockVector) +public: + VmaBlockVector( + VmaAllocator hAllocator, + VmaPool hParentPool, + uint32_t memoryTypeIndex, + VkDeviceSize preferredBlockSize, + size_t minBlockCount, + size_t maxBlockCount, + VkDeviceSize bufferImageGranularity, + bool explicitBlockSize, + uint32_t algorithm, + float priority, + VkDeviceSize minAllocationAlignment, + void* pMemoryAllocateNext); + ~VmaBlockVector(); + + VmaAllocator GetAllocator() const { return m_hAllocator; } + VmaPool GetParentPool() const { return m_hParentPool; } + bool IsCustomPool() const { return m_hParentPool != VMA_NULL; } + uint32_t GetMemoryTypeIndex() const { return m_MemoryTypeIndex; } + VkDeviceSize GetPreferredBlockSize() const { return m_PreferredBlockSize; } + VkDeviceSize GetBufferImageGranularity() const { return m_BufferImageGranularity; } + uint32_t GetAlgorithm() const { return m_Algorithm; } + bool HasExplicitBlockSize() const { return m_ExplicitBlockSize; } + float GetPriority() const { return m_Priority; } + const void* GetAllocationNextPtr() const { return m_pMemoryAllocateNext; } + // To be used only while the m_Mutex is locked. Used during defragmentation. + size_t GetBlockCount() const { return m_Blocks.size(); } + // To be used only while the m_Mutex is locked. Used during defragmentation. + VmaDeviceMemoryBlock* GetBlock(size_t index) const { return m_Blocks[index]; } + VMA_RW_MUTEX &GetMutex() { return m_Mutex; } + + VkResult CreateMinBlocks(); + void AddStatistics(VmaStatistics& inoutStats); + void AddDetailedStatistics(VmaDetailedStatistics& inoutStats); + bool IsEmpty(); + bool IsCorruptionDetectionEnabled() const; + + VkResult Allocate( + VkDeviceSize size, + VkDeviceSize alignment, + const VmaAllocationCreateInfo& createInfo, + VmaSuballocationType suballocType, + size_t allocationCount, + VmaAllocation* pAllocations); + + void Free(VmaAllocation hAllocation); + +#if VMA_STATS_STRING_ENABLED + void PrintDetailedMap(class VmaJsonWriter& json); +#endif + + VkResult CheckCorruption(); + +private: + const VmaAllocator m_hAllocator; + const VmaPool m_hParentPool; + const uint32_t m_MemoryTypeIndex; + const VkDeviceSize m_PreferredBlockSize; + const size_t m_MinBlockCount; + const size_t m_MaxBlockCount; + const VkDeviceSize m_BufferImageGranularity; + const bool m_ExplicitBlockSize; + const uint32_t m_Algorithm; + const float m_Priority; + const VkDeviceSize m_MinAllocationAlignment; + + void* const m_pMemoryAllocateNext; + VMA_RW_MUTEX m_Mutex; + // Incrementally sorted by sumFreeSize, ascending. + VmaVector> m_Blocks; + uint32_t m_NextBlockId; + bool m_IncrementalSort = true; + + void SetIncrementalSort(bool val) { m_IncrementalSort = val; } + + VkDeviceSize CalcMaxBlockSize() const; + // Finds and removes given block from vector. + void Remove(VmaDeviceMemoryBlock* pBlock); + // Performs single step in sorting m_Blocks. They may not be fully sorted + // after this call. + void IncrementallySortBlocks(); + void SortByFreeSize(); + + VkResult AllocatePage( + VkDeviceSize size, + VkDeviceSize alignment, + const VmaAllocationCreateInfo& createInfo, + VmaSuballocationType suballocType, + VmaAllocation* pAllocation); + + VkResult AllocateFromBlock( + VmaDeviceMemoryBlock* pBlock, + VkDeviceSize size, + VkDeviceSize alignment, + VmaAllocationCreateFlags allocFlags, + void* pUserData, + VmaSuballocationType suballocType, + uint32_t strategy, + VmaAllocation* pAllocation); + + VkResult CommitAllocationRequest( + VmaAllocationRequest& allocRequest, + VmaDeviceMemoryBlock* pBlock, + VkDeviceSize alignment, + VmaAllocationCreateFlags allocFlags, + void* pUserData, + VmaSuballocationType suballocType, + VmaAllocation* pAllocation); + + VkResult CreateBlock(VkDeviceSize blockSize, size_t* pNewBlockIndex); + bool HasEmptyBlock(); +}; +#endif // _VMA_BLOCK_VECTOR + +#ifndef _VMA_DEFRAGMENTATION_CONTEXT +struct VmaDefragmentationContext_T +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaDefragmentationContext_T) +public: + VmaDefragmentationContext_T( + VmaAllocator hAllocator, + const VmaDefragmentationInfo& info); + ~VmaDefragmentationContext_T(); + + void GetStats(VmaDefragmentationStats& outStats) { outStats = m_GlobalStats; } + + VkResult DefragmentPassBegin(VmaDefragmentationPassMoveInfo& moveInfo); + VkResult DefragmentPassEnd(VmaDefragmentationPassMoveInfo& moveInfo); + +private: + // Max number of allocations to ignore due to size constraints before ending single pass + static const uint8_t MAX_ALLOCS_TO_IGNORE = 16; + enum class CounterStatus { Pass, Ignore, End }; + + struct FragmentedBlock + { + uint32_t data; + VmaDeviceMemoryBlock* block; + }; + struct StateBalanced + { + VkDeviceSize avgFreeSize = 0; + VkDeviceSize avgAllocSize = UINT64_MAX; + }; + struct StateExtensive + { + enum class Operation : uint8_t + { + FindFreeBlockBuffer, FindFreeBlockTexture, FindFreeBlockAll, + MoveBuffers, MoveTextures, MoveAll, + Cleanup, Done + }; + + Operation operation = Operation::FindFreeBlockTexture; + size_t firstFreeBlock = SIZE_MAX; + }; + struct MoveAllocationData + { + VkDeviceSize size; + VkDeviceSize alignment; + VmaSuballocationType type; + VmaAllocationCreateFlags flags; + VmaDefragmentationMove move = {}; + }; + + const VkDeviceSize m_MaxPassBytes; + const uint32_t m_MaxPassAllocations; + const PFN_vmaCheckDefragmentationBreakFunction m_BreakCallback; + void* m_BreakCallbackUserData; + + VmaStlAllocator m_MoveAllocator; + VmaVector> m_Moves; + + uint8_t m_IgnoredAllocs = 0; + uint32_t m_Algorithm; + uint32_t m_BlockVectorCount; + VmaBlockVector* m_PoolBlockVector; + VmaBlockVector** m_pBlockVectors; + size_t m_ImmovableBlockCount = 0; + VmaDefragmentationStats m_GlobalStats = { 0 }; + VmaDefragmentationStats m_PassStats = { 0 }; + void* m_AlgorithmState = VMA_NULL; + + static MoveAllocationData GetMoveData(VmaAllocHandle handle, VmaBlockMetadata* metadata); + CounterStatus CheckCounters(VkDeviceSize bytes); + bool IncrementCounters(VkDeviceSize bytes); + bool ReallocWithinBlock(VmaBlockVector& vector, VmaDeviceMemoryBlock* block); + bool AllocInOtherBlock(size_t start, size_t end, MoveAllocationData& data, VmaBlockVector& vector); + + bool ComputeDefragmentation(VmaBlockVector& vector, size_t index); + bool ComputeDefragmentation_Fast(VmaBlockVector& vector); + bool ComputeDefragmentation_Balanced(VmaBlockVector& vector, size_t index, bool update); + bool ComputeDefragmentation_Full(VmaBlockVector& vector); + bool ComputeDefragmentation_Extensive(VmaBlockVector& vector, size_t index); + + static void UpdateVectorStatistics(VmaBlockVector& vector, StateBalanced& state); + bool MoveDataToFreeBlocks(VmaSuballocationType currentType, + VmaBlockVector& vector, size_t firstFreeBlock, + bool& texturePresent, bool& bufferPresent, bool& otherPresent); +}; +#endif // _VMA_DEFRAGMENTATION_CONTEXT + +#ifndef _VMA_POOL_T +struct VmaPool_T +{ + friend struct VmaPoolListItemTraits; + VMA_CLASS_NO_COPY_NO_MOVE(VmaPool_T) +public: + VmaBlockVector m_BlockVector; + VmaDedicatedAllocationList m_DedicatedAllocations; + + VmaPool_T( + VmaAllocator hAllocator, + const VmaPoolCreateInfo& createInfo, + VkDeviceSize preferredBlockSize); + ~VmaPool_T(); + + uint32_t GetId() const { return m_Id; } + void SetId(uint32_t id) { VMA_ASSERT(m_Id == 0); m_Id = id; } + + const char* GetName() const { return m_Name; } + void SetName(const char* pName); + +#if VMA_STATS_STRING_ENABLED + //void PrintDetailedMap(class VmaStringBuilder& sb); +#endif + +private: + uint32_t m_Id; + char* m_Name; + VmaPool_T* m_PrevPool = VMA_NULL; + VmaPool_T* m_NextPool = VMA_NULL; +}; + +struct VmaPoolListItemTraits +{ + typedef VmaPool_T ItemType; + + static ItemType* GetPrev(const ItemType* item) { return item->m_PrevPool; } + static ItemType* GetNext(const ItemType* item) { return item->m_NextPool; } + static ItemType*& AccessPrev(ItemType* item) { return item->m_PrevPool; } + static ItemType*& AccessNext(ItemType* item) { return item->m_NextPool; } +}; +#endif // _VMA_POOL_T + +#ifndef _VMA_CURRENT_BUDGET_DATA +struct VmaCurrentBudgetData +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaCurrentBudgetData) +public: + + VMA_ATOMIC_UINT32 m_BlockCount[VK_MAX_MEMORY_HEAPS]; + VMA_ATOMIC_UINT32 m_AllocationCount[VK_MAX_MEMORY_HEAPS]; + VMA_ATOMIC_UINT64 m_BlockBytes[VK_MAX_MEMORY_HEAPS]; + VMA_ATOMIC_UINT64 m_AllocationBytes[VK_MAX_MEMORY_HEAPS]; + +#if VMA_MEMORY_BUDGET + VMA_ATOMIC_UINT32 m_OperationsSinceBudgetFetch; + VMA_RW_MUTEX m_BudgetMutex; + uint64_t m_VulkanUsage[VK_MAX_MEMORY_HEAPS]; + uint64_t m_VulkanBudget[VK_MAX_MEMORY_HEAPS]; + uint64_t m_BlockBytesAtBudgetFetch[VK_MAX_MEMORY_HEAPS]; +#endif // VMA_MEMORY_BUDGET + + VmaCurrentBudgetData(); + + void AddAllocation(uint32_t heapIndex, VkDeviceSize allocationSize); + void RemoveAllocation(uint32_t heapIndex, VkDeviceSize allocationSize); +}; + +#ifndef _VMA_CURRENT_BUDGET_DATA_FUNCTIONS +VmaCurrentBudgetData::VmaCurrentBudgetData() +{ + for (uint32_t heapIndex = 0; heapIndex < VK_MAX_MEMORY_HEAPS; ++heapIndex) + { + m_BlockCount[heapIndex] = 0; + m_AllocationCount[heapIndex] = 0; + m_BlockBytes[heapIndex] = 0; + m_AllocationBytes[heapIndex] = 0; +#if VMA_MEMORY_BUDGET + m_VulkanUsage[heapIndex] = 0; + m_VulkanBudget[heapIndex] = 0; + m_BlockBytesAtBudgetFetch[heapIndex] = 0; +#endif + } + +#if VMA_MEMORY_BUDGET + m_OperationsSinceBudgetFetch = 0; +#endif +} + +void VmaCurrentBudgetData::AddAllocation(uint32_t heapIndex, VkDeviceSize allocationSize) +{ + m_AllocationBytes[heapIndex] += allocationSize; + ++m_AllocationCount[heapIndex]; +#if VMA_MEMORY_BUDGET + ++m_OperationsSinceBudgetFetch; +#endif +} + +void VmaCurrentBudgetData::RemoveAllocation(uint32_t heapIndex, VkDeviceSize allocationSize) +{ + VMA_ASSERT(m_AllocationBytes[heapIndex] >= allocationSize); + m_AllocationBytes[heapIndex] -= allocationSize; + VMA_ASSERT(m_AllocationCount[heapIndex] > 0); + --m_AllocationCount[heapIndex]; +#if VMA_MEMORY_BUDGET + ++m_OperationsSinceBudgetFetch; +#endif +} +#endif // _VMA_CURRENT_BUDGET_DATA_FUNCTIONS +#endif // _VMA_CURRENT_BUDGET_DATA + +#ifndef _VMA_ALLOCATION_OBJECT_ALLOCATOR +/* +Thread-safe wrapper over VmaPoolAllocator free list, for allocation of VmaAllocation_T objects. +*/ +class VmaAllocationObjectAllocator +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaAllocationObjectAllocator) +public: + explicit VmaAllocationObjectAllocator(const VkAllocationCallbacks* pAllocationCallbacks) + : m_Allocator(pAllocationCallbacks, 1024) {} + + template VmaAllocation Allocate(Types&&... args); + void Free(VmaAllocation hAlloc); + +private: + VMA_MUTEX m_Mutex; + VmaPoolAllocator m_Allocator; +}; + +template +VmaAllocation VmaAllocationObjectAllocator::Allocate(Types&&... args) +{ + VmaMutexLock mutexLock(m_Mutex); + return m_Allocator.Alloc(std::forward(args)...); +} + +void VmaAllocationObjectAllocator::Free(VmaAllocation hAlloc) +{ + VmaMutexLock mutexLock(m_Mutex); + m_Allocator.Free(hAlloc); +} +#endif // _VMA_ALLOCATION_OBJECT_ALLOCATOR + +#ifndef _VMA_VIRTUAL_BLOCK_T +struct VmaVirtualBlock_T +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaVirtualBlock_T) +public: + const bool m_AllocationCallbacksSpecified; + const VkAllocationCallbacks m_AllocationCallbacks; + + explicit VmaVirtualBlock_T(const VmaVirtualBlockCreateInfo& createInfo); + ~VmaVirtualBlock_T(); + + bool IsEmpty() const { return m_Metadata->IsEmpty(); } + void Free(VmaVirtualAllocation allocation) { m_Metadata->Free((VmaAllocHandle)allocation); } + void SetAllocationUserData(VmaVirtualAllocation allocation, void* userData) { m_Metadata->SetAllocationUserData((VmaAllocHandle)allocation, userData); } + void Clear() { m_Metadata->Clear(); } + + const VkAllocationCallbacks* GetAllocationCallbacks() const; + void GetAllocationInfo(VmaVirtualAllocation allocation, VmaVirtualAllocationInfo& outInfo); + VkResult Allocate(const VmaVirtualAllocationCreateInfo& createInfo, VmaVirtualAllocation& outAllocation, + VkDeviceSize* outOffset); + void GetStatistics(VmaStatistics& outStats) const; + void CalculateDetailedStatistics(VmaDetailedStatistics& outStats) const; +#if VMA_STATS_STRING_ENABLED + void BuildStatsString(bool detailedMap, VmaStringBuilder& sb) const; +#endif + +private: + VmaBlockMetadata* m_Metadata; +}; + +#ifndef _VMA_VIRTUAL_BLOCK_T_FUNCTIONS +VmaVirtualBlock_T::VmaVirtualBlock_T(const VmaVirtualBlockCreateInfo& createInfo) + : m_AllocationCallbacksSpecified(createInfo.pAllocationCallbacks != VMA_NULL), + m_AllocationCallbacks(createInfo.pAllocationCallbacks != VMA_NULL ? *createInfo.pAllocationCallbacks : VmaEmptyAllocationCallbacks) +{ + const uint32_t algorithm = createInfo.flags & VMA_VIRTUAL_BLOCK_CREATE_ALGORITHM_MASK; + switch (algorithm) + { + case 0: + m_Metadata = vma_new(GetAllocationCallbacks(), VmaBlockMetadata_TLSF)(VK_NULL_HANDLE, 1, true); + break; + case VMA_VIRTUAL_BLOCK_CREATE_LINEAR_ALGORITHM_BIT: + m_Metadata = vma_new(GetAllocationCallbacks(), VmaBlockMetadata_Linear)(VK_NULL_HANDLE, 1, true); + break; + default: + VMA_ASSERT(0); + m_Metadata = vma_new(GetAllocationCallbacks(), VmaBlockMetadata_TLSF)(VK_NULL_HANDLE, 1, true); + } + + m_Metadata->Init(createInfo.size); +} + +VmaVirtualBlock_T::~VmaVirtualBlock_T() +{ + // Define macro VMA_DEBUG_LOG_FORMAT or more specialized VMA_LEAK_LOG_FORMAT + // to receive the list of the unfreed allocations. + if (!m_Metadata->IsEmpty()) + m_Metadata->DebugLogAllAllocations(); + // This is the most important assert in the entire library. + // Hitting it means you have some memory leak - unreleased virtual allocations. + VMA_ASSERT_LEAK(m_Metadata->IsEmpty() && "Some virtual allocations were not freed before destruction of this virtual block!"); + + vma_delete(GetAllocationCallbacks(), m_Metadata); +} + +const VkAllocationCallbacks* VmaVirtualBlock_T::GetAllocationCallbacks() const +{ + return m_AllocationCallbacksSpecified ? &m_AllocationCallbacks : VMA_NULL; +} + +void VmaVirtualBlock_T::GetAllocationInfo(VmaVirtualAllocation allocation, VmaVirtualAllocationInfo& outInfo) +{ + m_Metadata->GetAllocationInfo((VmaAllocHandle)allocation, outInfo); +} + +VkResult VmaVirtualBlock_T::Allocate(const VmaVirtualAllocationCreateInfo& createInfo, VmaVirtualAllocation& outAllocation, + VkDeviceSize* outOffset) +{ + VmaAllocationRequest request = {}; + if (m_Metadata->CreateAllocationRequest( + createInfo.size, // allocSize + VMA_MAX(createInfo.alignment, (VkDeviceSize)1), // allocAlignment + (createInfo.flags & VMA_VIRTUAL_ALLOCATION_CREATE_UPPER_ADDRESS_BIT) != 0, // upperAddress + VMA_SUBALLOCATION_TYPE_UNKNOWN, // allocType - unimportant + createInfo.flags & VMA_VIRTUAL_ALLOCATION_CREATE_STRATEGY_MASK, // strategy + &request)) + { + m_Metadata->Alloc(request, + VMA_SUBALLOCATION_TYPE_UNKNOWN, // type - unimportant + createInfo.pUserData); + outAllocation = (VmaVirtualAllocation)request.allocHandle; + if(outOffset) + *outOffset = m_Metadata->GetAllocationOffset(request.allocHandle); + return VK_SUCCESS; + } + outAllocation = (VmaVirtualAllocation)VK_NULL_HANDLE; + if (outOffset) + *outOffset = UINT64_MAX; + return VK_ERROR_OUT_OF_DEVICE_MEMORY; +} + +void VmaVirtualBlock_T::GetStatistics(VmaStatistics& outStats) const +{ + VmaClearStatistics(outStats); + m_Metadata->AddStatistics(outStats); +} + +void VmaVirtualBlock_T::CalculateDetailedStatistics(VmaDetailedStatistics& outStats) const +{ + VmaClearDetailedStatistics(outStats); + m_Metadata->AddDetailedStatistics(outStats); +} + +#if VMA_STATS_STRING_ENABLED +void VmaVirtualBlock_T::BuildStatsString(bool detailedMap, VmaStringBuilder& sb) const +{ + VmaJsonWriter json(GetAllocationCallbacks(), sb); + json.BeginObject(); + + VmaDetailedStatistics stats; + CalculateDetailedStatistics(stats); + + json.WriteString("Stats"); + VmaPrintDetailedStatistics(json, stats); + + if (detailedMap) + { + json.WriteString("Details"); + json.BeginObject(); + m_Metadata->PrintDetailedMap(json); + json.EndObject(); + } + + json.EndObject(); +} +#endif // VMA_STATS_STRING_ENABLED +#endif // _VMA_VIRTUAL_BLOCK_T_FUNCTIONS +#endif // _VMA_VIRTUAL_BLOCK_T + + +// Main allocator object. +struct VmaAllocator_T +{ + VMA_CLASS_NO_COPY_NO_MOVE(VmaAllocator_T) +public: + const bool m_UseMutex; + const uint32_t m_VulkanApiVersion; + bool m_UseKhrDedicatedAllocation; // Can be set only if m_VulkanApiVersion < VK_MAKE_VERSION(1, 1, 0). + bool m_UseKhrBindMemory2; // Can be set only if m_VulkanApiVersion < VK_MAKE_VERSION(1, 1, 0). + bool m_UseExtMemoryBudget; + bool m_UseAmdDeviceCoherentMemory; + bool m_UseKhrBufferDeviceAddress; + bool m_UseExtMemoryPriority; + bool m_UseKhrMaintenance4; + bool m_UseKhrMaintenance5; + bool m_UseKhrExternalMemoryWin32; + const VkDevice m_hDevice; + const VkInstance m_hInstance; + const bool m_AllocationCallbacksSpecified; + const VkAllocationCallbacks m_AllocationCallbacks; + VmaDeviceMemoryCallbacks m_DeviceMemoryCallbacks; + VmaAllocationObjectAllocator m_AllocationObjectAllocator; + + // Each bit (1 << i) is set if HeapSizeLimit is enabled for that heap, so cannot allocate more than the heap size. + uint32_t m_HeapSizeLimitMask; + + VkPhysicalDeviceProperties m_PhysicalDeviceProperties; + VkPhysicalDeviceMemoryProperties m_MemProps; + + // Default pools. + VmaBlockVector* m_pBlockVectors[VK_MAX_MEMORY_TYPES]; + VmaDedicatedAllocationList m_DedicatedAllocations[VK_MAX_MEMORY_TYPES]; + + VmaCurrentBudgetData m_Budget; + VMA_ATOMIC_UINT32 m_DeviceMemoryCount; // Total number of VkDeviceMemory objects. + + explicit VmaAllocator_T(const VmaAllocatorCreateInfo* pCreateInfo); + VkResult Init(const VmaAllocatorCreateInfo* pCreateInfo); + ~VmaAllocator_T(); + + const VkAllocationCallbacks* GetAllocationCallbacks() const + { + return m_AllocationCallbacksSpecified ? &m_AllocationCallbacks : VMA_NULL; + } + const VmaVulkanFunctions& GetVulkanFunctions() const + { + return m_VulkanFunctions; + } + + VkPhysicalDevice GetPhysicalDevice() const { return m_PhysicalDevice; } + + VkDeviceSize GetBufferImageGranularity() const + { + return VMA_MAX( + static_cast(VMA_DEBUG_MIN_BUFFER_IMAGE_GRANULARITY), + m_PhysicalDeviceProperties.limits.bufferImageGranularity); + } + + uint32_t GetMemoryHeapCount() const { return m_MemProps.memoryHeapCount; } + uint32_t GetMemoryTypeCount() const { return m_MemProps.memoryTypeCount; } + + uint32_t MemoryTypeIndexToHeapIndex(uint32_t memTypeIndex) const + { + VMA_ASSERT(memTypeIndex < m_MemProps.memoryTypeCount); + return m_MemProps.memoryTypes[memTypeIndex].heapIndex; + } + // True when specific memory type is HOST_VISIBLE but not HOST_COHERENT. + bool IsMemoryTypeNonCoherent(uint32_t memTypeIndex) const + { + return (m_MemProps.memoryTypes[memTypeIndex].propertyFlags & (VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) == + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; + } + // Minimum alignment for all allocations in specific memory type. + VkDeviceSize GetMemoryTypeMinAlignment(uint32_t memTypeIndex) const + { + return IsMemoryTypeNonCoherent(memTypeIndex) ? + VMA_MAX((VkDeviceSize)VMA_MIN_ALIGNMENT, m_PhysicalDeviceProperties.limits.nonCoherentAtomSize) : + (VkDeviceSize)VMA_MIN_ALIGNMENT; + } + + bool IsIntegratedGpu() const + { + return m_PhysicalDeviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU; + } + + uint32_t GetGlobalMemoryTypeBits() const { return m_GlobalMemoryTypeBits; } + + void GetBufferMemoryRequirements( + VkBuffer hBuffer, + VkMemoryRequirements& memReq, + bool& requiresDedicatedAllocation, + bool& prefersDedicatedAllocation) const; + void GetImageMemoryRequirements( + VkImage hImage, + VkMemoryRequirements& memReq, + bool& requiresDedicatedAllocation, + bool& prefersDedicatedAllocation) const; + VkResult FindMemoryTypeIndex( + uint32_t memoryTypeBits, + const VmaAllocationCreateInfo* pAllocationCreateInfo, + VmaBufferImageUsage bufImgUsage, + uint32_t* pMemoryTypeIndex) const; + + // Main allocation function. + VkResult AllocateMemory( + const VkMemoryRequirements& vkMemReq, + bool requiresDedicatedAllocation, + bool prefersDedicatedAllocation, + VkBuffer dedicatedBuffer, + VkImage dedicatedImage, + VmaBufferImageUsage dedicatedBufferImageUsage, + const VmaAllocationCreateInfo& createInfo, + VmaSuballocationType suballocType, + size_t allocationCount, + VmaAllocation* pAllocations); + + // Main deallocation function. + void FreeMemory( + size_t allocationCount, + const VmaAllocation* pAllocations); + + void CalculateStatistics(VmaTotalStatistics* pStats); + + void GetHeapBudgets( + VmaBudget* outBudgets, uint32_t firstHeap, uint32_t heapCount); + +#if VMA_STATS_STRING_ENABLED + void PrintDetailedMap(class VmaJsonWriter& json); +#endif + + static void GetAllocationInfo(VmaAllocation hAllocation, VmaAllocationInfo* pAllocationInfo); + static void GetAllocationInfo2(VmaAllocation hAllocation, VmaAllocationInfo2* pAllocationInfo); + + VkResult CreatePool(const VmaPoolCreateInfo* pCreateInfo, VmaPool* pPool); + void DestroyPool(VmaPool pool); + static void GetPoolStatistics(VmaPool pool, VmaStatistics* pPoolStats); + static void CalculatePoolStatistics(VmaPool pool, VmaDetailedStatistics* pPoolStats); + + void SetCurrentFrameIndex(uint32_t frameIndex); + uint32_t GetCurrentFrameIndex() const { return m_CurrentFrameIndex.load(); } + + static VkResult CheckPoolCorruption(VmaPool hPool); + VkResult CheckCorruption(uint32_t memoryTypeBits); + + // Call to Vulkan function vkAllocateMemory with accompanying bookkeeping. + VkResult AllocateVulkanMemory(const VkMemoryAllocateInfo* pAllocateInfo, VkDeviceMemory* pMemory); + // Call to Vulkan function vkFreeMemory with accompanying bookkeeping. + void FreeVulkanMemory(uint32_t memoryType, VkDeviceSize size, VkDeviceMemory hMemory); + // Call to Vulkan function vkBindBufferMemory or vkBindBufferMemory2KHR. + VkResult BindVulkanBuffer( + VkDeviceMemory memory, + VkDeviceSize memoryOffset, + VkBuffer buffer, + const void* pNext) const; + // Call to Vulkan function vkBindImageMemory or vkBindImageMemory2KHR. + VkResult BindVulkanImage( + VkDeviceMemory memory, + VkDeviceSize memoryOffset, + VkImage image, + const void* pNext) const; + + VkResult Map(VmaAllocation hAllocation, void** ppData); + void Unmap(VmaAllocation hAllocation); + + VkResult BindBufferMemory( + VmaAllocation hAllocation, + VkDeviceSize allocationLocalOffset, + VkBuffer hBuffer, + const void* pNext); + VkResult BindImageMemory( + VmaAllocation hAllocation, + VkDeviceSize allocationLocalOffset, + VkImage hImage, + const void* pNext); + + VkResult FlushOrInvalidateAllocation( + VmaAllocation hAllocation, + VkDeviceSize offset, VkDeviceSize size, + VMA_CACHE_OPERATION op); + VkResult FlushOrInvalidateAllocations( + uint32_t allocationCount, + const VmaAllocation* allocations, + const VkDeviceSize* offsets, const VkDeviceSize* sizes, + VMA_CACHE_OPERATION op); + + VkResult CopyMemoryToAllocation( + const void* pSrcHostPointer, + VmaAllocation dstAllocation, + VkDeviceSize dstAllocationLocalOffset, + VkDeviceSize size); + VkResult CopyAllocationToMemory( + VmaAllocation srcAllocation, + VkDeviceSize srcAllocationLocalOffset, + void* pDstHostPointer, + VkDeviceSize size); + + void FillAllocation(VmaAllocation hAllocation, uint8_t pattern); + + /* + Returns bit mask of memory types that can support defragmentation on GPU as + they support creation of required buffer for copy operations. + */ + uint32_t GetGpuDefragmentationMemoryTypeBits(); + +#if VMA_EXTERNAL_MEMORY + VkExternalMemoryHandleTypeFlagsKHR GetExternalMemoryHandleTypeFlags(uint32_t memTypeIndex) const + { + return m_TypeExternalMemoryHandleTypes[memTypeIndex]; + } +#endif // #if VMA_EXTERNAL_MEMORY + +private: + VkDeviceSize m_PreferredLargeHeapBlockSize; + + VkPhysicalDevice m_PhysicalDevice; + VMA_ATOMIC_UINT32 m_CurrentFrameIndex; + VMA_ATOMIC_UINT32 m_GpuDefragmentationMemoryTypeBits; // UINT32_MAX means uninitialized. +#if VMA_EXTERNAL_MEMORY + VkExternalMemoryHandleTypeFlagsKHR m_TypeExternalMemoryHandleTypes[VK_MAX_MEMORY_TYPES]; +#endif // #if VMA_EXTERNAL_MEMORY + + VMA_RW_MUTEX m_PoolsMutex; + typedef VmaIntrusiveLinkedList PoolList; + // Protected by m_PoolsMutex. + PoolList m_Pools; + uint32_t m_NextPoolId; + + VmaVulkanFunctions m_VulkanFunctions; + + // Global bit mask AND-ed with any memoryTypeBits to disallow certain memory types. + uint32_t m_GlobalMemoryTypeBits; + + void ImportVulkanFunctions(const VmaVulkanFunctions* pVulkanFunctions); + +#if VMA_STATIC_VULKAN_FUNCTIONS == 1 + void ImportVulkanFunctions_Static(); +#endif + + void ImportVulkanFunctions_Custom(const VmaVulkanFunctions* pVulkanFunctions); + +#if VMA_DYNAMIC_VULKAN_FUNCTIONS == 1 + void ImportVulkanFunctions_Dynamic(); +#endif + + void ValidateVulkanFunctions() const; + + VkDeviceSize CalcPreferredBlockSize(uint32_t memTypeIndex); + + VkResult AllocateMemoryOfType( + VmaPool pool, + VkDeviceSize size, + VkDeviceSize alignment, + bool dedicatedPreferred, + VkBuffer dedicatedBuffer, + VkImage dedicatedImage, + VmaBufferImageUsage dedicatedBufferImageUsage, + const VmaAllocationCreateInfo& createInfo, + uint32_t memTypeIndex, + VmaSuballocationType suballocType, + VmaDedicatedAllocationList& dedicatedAllocations, + VmaBlockVector& blockVector, + size_t allocationCount, + VmaAllocation* pAllocations); + + // Helper function only to be used inside AllocateDedicatedMemory. + VkResult AllocateDedicatedMemoryPage( + VmaPool pool, + VkDeviceSize size, + VmaSuballocationType suballocType, + uint32_t memTypeIndex, + const VkMemoryAllocateInfo& allocInfo, + bool map, + bool isUserDataString, + bool isMappingAllowed, + void* pUserData, + VmaAllocation* pAllocation); + + // Allocates and registers new VkDeviceMemory specifically for dedicated allocations. + VkResult AllocateDedicatedMemory( + VmaPool pool, + VkDeviceSize size, + VmaSuballocationType suballocType, + VmaDedicatedAllocationList& dedicatedAllocations, + uint32_t memTypeIndex, + bool map, + bool isUserDataString, + bool isMappingAllowed, + bool canAliasMemory, + void* pUserData, + float priority, + VkBuffer dedicatedBuffer, + VkImage dedicatedImage, + VmaBufferImageUsage dedicatedBufferImageUsage, + size_t allocationCount, + VmaAllocation* pAllocations, + const void* pNextChain = VMA_NULL); + + void FreeDedicatedMemory(VmaAllocation allocation); + + VkResult CalcMemTypeParams( + VmaAllocationCreateInfo& outCreateInfo, + uint32_t memTypeIndex, + VkDeviceSize size, + size_t allocationCount); + static VkResult CalcAllocationParams( + VmaAllocationCreateInfo& outCreateInfo, + bool dedicatedRequired); + + /* + Calculates and returns bit mask of memory types that can support defragmentation + on GPU as they support creation of required buffer for copy operations. + */ + uint32_t CalculateGpuDefragmentationMemoryTypeBits() const; + uint32_t CalculateGlobalMemoryTypeBits() const; + + bool GetFlushOrInvalidateRange( + VmaAllocation allocation, + VkDeviceSize offset, VkDeviceSize size, + VkMappedMemoryRange& outRange) const; + +#if VMA_MEMORY_BUDGET + void UpdateVulkanBudget(); +#endif // #if VMA_MEMORY_BUDGET +}; + + +#ifndef _VMA_MEMORY_FUNCTIONS +static void* VmaMalloc(VmaAllocator hAllocator, size_t size, size_t alignment) +{ + return VmaMalloc(&hAllocator->m_AllocationCallbacks, size, alignment); +} + +static void VmaFree(VmaAllocator hAllocator, void* ptr) +{ + VmaFree(&hAllocator->m_AllocationCallbacks, ptr); +} + +template +static T* VmaAllocate(VmaAllocator hAllocator) +{ + return (T*)VmaMalloc(hAllocator, sizeof(T), VMA_ALIGN_OF(T)); +} + +template +static T* VmaAllocateArray(VmaAllocator hAllocator, size_t count) +{ + return (T*)VmaMalloc(hAllocator, sizeof(T) * count, VMA_ALIGN_OF(T)); +} + +template +static void vma_delete(VmaAllocator hAllocator, T* ptr) +{ + if(ptr != VMA_NULL) + { + ptr->~T(); + VmaFree(hAllocator, ptr); + } +} + +template +static void vma_delete_array(VmaAllocator hAllocator, T* ptr, size_t count) +{ + if(ptr != VMA_NULL) + { + for(size_t i = count; i--; ) + ptr[i].~T(); + VmaFree(hAllocator, ptr); + } +} +#endif // _VMA_MEMORY_FUNCTIONS + +#ifndef _VMA_DEVICE_MEMORY_BLOCK_FUNCTIONS +VmaDeviceMemoryBlock::VmaDeviceMemoryBlock(VmaAllocator hAllocator) + : m_pMetadata(VMA_NULL), + m_hParentPool(nullptr), + m_MemoryTypeIndex(UINT32_MAX), + m_Id(0), + m_hMemory(VK_NULL_HANDLE), + m_MapCount(0), + m_pMappedData(VMA_NULL){} + +VmaDeviceMemoryBlock::~VmaDeviceMemoryBlock() +{ + VMA_ASSERT_LEAK(m_MapCount == 0 && "VkDeviceMemory block is being destroyed while it is still mapped."); + VMA_ASSERT_LEAK(m_hMemory == VK_NULL_HANDLE); +} + +void VmaDeviceMemoryBlock::Init( + VmaAllocator hAllocator, + VmaPool hParentPool, + uint32_t newMemoryTypeIndex, + VkDeviceMemory newMemory, + VkDeviceSize newSize, + uint32_t id, + uint32_t algorithm, + VkDeviceSize bufferImageGranularity) +{ + VMA_ASSERT(m_hMemory == VK_NULL_HANDLE); + + m_hParentPool = hParentPool; + m_MemoryTypeIndex = newMemoryTypeIndex; + m_Id = id; + m_hMemory = newMemory; + + switch (algorithm) + { + case 0: + m_pMetadata = vma_new(hAllocator, VmaBlockMetadata_TLSF)(hAllocator->GetAllocationCallbacks(), + bufferImageGranularity, false); // isVirtual + break; + case VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT: + m_pMetadata = vma_new(hAllocator, VmaBlockMetadata_Linear)(hAllocator->GetAllocationCallbacks(), + bufferImageGranularity, false); // isVirtual + break; + default: + VMA_ASSERT(0); + m_pMetadata = vma_new(hAllocator, VmaBlockMetadata_TLSF)(hAllocator->GetAllocationCallbacks(), + bufferImageGranularity, false); // isVirtual + } + m_pMetadata->Init(newSize); +} + +void VmaDeviceMemoryBlock::Destroy(VmaAllocator allocator) +{ + // Define macro VMA_DEBUG_LOG_FORMAT or more specialized VMA_LEAK_LOG_FORMAT + // to receive the list of the unfreed allocations. + if (!m_pMetadata->IsEmpty()) + m_pMetadata->DebugLogAllAllocations(); + // This is the most important assert in the entire library. + // Hitting it means you have some memory leak - unreleased VmaAllocation objects. + VMA_ASSERT_LEAK(m_pMetadata->IsEmpty() && "Some allocations were not freed before destruction of this memory block!"); + + VMA_ASSERT_LEAK(m_hMemory != VK_NULL_HANDLE); + allocator->FreeVulkanMemory(m_MemoryTypeIndex, m_pMetadata->GetSize(), m_hMemory); + m_hMemory = VK_NULL_HANDLE; + + vma_delete(allocator, m_pMetadata); + m_pMetadata = VMA_NULL; +} + +void VmaDeviceMemoryBlock::PostAlloc(VmaAllocator hAllocator) +{ + VmaMutexLock lock(m_MapAndBindMutex, hAllocator->m_UseMutex); + m_MappingHysteresis.PostAlloc(); +} + +void VmaDeviceMemoryBlock::PostFree(VmaAllocator hAllocator) +{ + VmaMutexLock lock(m_MapAndBindMutex, hAllocator->m_UseMutex); + if(m_MappingHysteresis.PostFree()) + { + VMA_ASSERT(m_MappingHysteresis.GetExtraMapping() == 0); + if (m_MapCount == 0) + { + m_pMappedData = VMA_NULL; + (*hAllocator->GetVulkanFunctions().vkUnmapMemory)(hAllocator->m_hDevice, m_hMemory); + } + } +} + +bool VmaDeviceMemoryBlock::Validate() const +{ + VMA_VALIDATE((m_hMemory != VK_NULL_HANDLE) && + (m_pMetadata->GetSize() != 0)); + + return m_pMetadata->Validate(); +} + +VkResult VmaDeviceMemoryBlock::CheckCorruption(VmaAllocator hAllocator) +{ + void* pData = VMA_NULL; + VkResult res = Map(hAllocator, 1, &pData); + if (res != VK_SUCCESS) + { + return res; + } + + res = m_pMetadata->CheckCorruption(pData); + + Unmap(hAllocator, 1); + + return res; +} + +VkResult VmaDeviceMemoryBlock::Map(VmaAllocator hAllocator, uint32_t count, void** ppData) +{ + if (count == 0) + { + return VK_SUCCESS; + } + + VmaMutexLock lock(m_MapAndBindMutex, hAllocator->m_UseMutex); + const uint32_t oldTotalMapCount = m_MapCount + m_MappingHysteresis.GetExtraMapping(); + if (oldTotalMapCount != 0) + { + VMA_ASSERT(m_pMappedData != VMA_NULL); + m_MappingHysteresis.PostMap(); + m_MapCount += count; + if (ppData != VMA_NULL) + { + *ppData = m_pMappedData; + } + return VK_SUCCESS; + } + + VkResult result = (*hAllocator->GetVulkanFunctions().vkMapMemory)( + hAllocator->m_hDevice, + m_hMemory, + 0, // offset + VK_WHOLE_SIZE, + 0, // flags + &m_pMappedData); + if (result == VK_SUCCESS) + { + VMA_ASSERT(m_pMappedData != VMA_NULL); + m_MappingHysteresis.PostMap(); + m_MapCount = count; + if (ppData != VMA_NULL) + { + *ppData = m_pMappedData; + } + } + return result; +} + +void VmaDeviceMemoryBlock::Unmap(VmaAllocator hAllocator, uint32_t count) +{ + if (count == 0) + { + return; + } + + VmaMutexLock lock(m_MapAndBindMutex, hAllocator->m_UseMutex); + if (m_MapCount >= count) + { + m_MapCount -= count; + const uint32_t totalMapCount = m_MapCount + m_MappingHysteresis.GetExtraMapping(); + if (totalMapCount == 0) + { + m_pMappedData = VMA_NULL; + (*hAllocator->GetVulkanFunctions().vkUnmapMemory)(hAllocator->m_hDevice, m_hMemory); + } + m_MappingHysteresis.PostUnmap(); + } + else + { + VMA_ASSERT(0 && "VkDeviceMemory block is being unmapped while it was not previously mapped."); + } +} + +VkResult VmaDeviceMemoryBlock::WriteMagicValueAfterAllocation(VmaAllocator hAllocator, VkDeviceSize allocOffset, VkDeviceSize allocSize) +{ + VMA_ASSERT(VMA_DEBUG_MARGIN > 0 && VMA_DEBUG_MARGIN % 4 == 0 && VMA_DEBUG_DETECT_CORRUPTION); + + void* pData = VMA_NULL; + VkResult res = Map(hAllocator, 1, &pData); + if (res != VK_SUCCESS) + { + return res; + } + + VmaWriteMagicValue(pData, allocOffset + allocSize); + + Unmap(hAllocator, 1); + return VK_SUCCESS; +} + +VkResult VmaDeviceMemoryBlock::ValidateMagicValueAfterAllocation(VmaAllocator hAllocator, VkDeviceSize allocOffset, VkDeviceSize allocSize) +{ + VMA_ASSERT(VMA_DEBUG_MARGIN > 0 && VMA_DEBUG_MARGIN % 4 == 0 && VMA_DEBUG_DETECT_CORRUPTION); + + void* pData = VMA_NULL; + VkResult res = Map(hAllocator, 1, &pData); + if (res != VK_SUCCESS) + { + return res; + } + + if (!VmaValidateMagicValue(pData, allocOffset + allocSize)) + { + VMA_ASSERT(0 && "MEMORY CORRUPTION DETECTED AFTER FREED ALLOCATION!"); + } + + Unmap(hAllocator, 1); + return VK_SUCCESS; +} + +VkResult VmaDeviceMemoryBlock::BindBufferMemory( + VmaAllocator hAllocator, + VmaAllocation hAllocation, + VkDeviceSize allocationLocalOffset, + VkBuffer hBuffer, + const void* pNext) +{ + VMA_ASSERT(hAllocation->GetType() == VmaAllocation_T::ALLOCATION_TYPE_BLOCK && + hAllocation->GetBlock() == this); + VMA_ASSERT(allocationLocalOffset < hAllocation->GetSize() && + "Invalid allocationLocalOffset. Did you forget that this offset is relative to the beginning of the allocation, not the whole memory block?"); + const VkDeviceSize memoryOffset = hAllocation->GetOffset() + allocationLocalOffset; + // This lock is important so that we don't call vkBind... and/or vkMap... simultaneously on the same VkDeviceMemory from multiple threads. + VmaMutexLock lock(m_MapAndBindMutex, hAllocator->m_UseMutex); + return hAllocator->BindVulkanBuffer(m_hMemory, memoryOffset, hBuffer, pNext); +} + +VkResult VmaDeviceMemoryBlock::BindImageMemory( + VmaAllocator hAllocator, + VmaAllocation hAllocation, + VkDeviceSize allocationLocalOffset, + VkImage hImage, + const void* pNext) +{ + VMA_ASSERT(hAllocation->GetType() == VmaAllocation_T::ALLOCATION_TYPE_BLOCK && + hAllocation->GetBlock() == this); + VMA_ASSERT(allocationLocalOffset < hAllocation->GetSize() && + "Invalid allocationLocalOffset. Did you forget that this offset is relative to the beginning of the allocation, not the whole memory block?"); + const VkDeviceSize memoryOffset = hAllocation->GetOffset() + allocationLocalOffset; + // This lock is important so that we don't call vkBind... and/or vkMap... simultaneously on the same VkDeviceMemory from multiple threads. + VmaMutexLock lock(m_MapAndBindMutex, hAllocator->m_UseMutex); + return hAllocator->BindVulkanImage(m_hMemory, memoryOffset, hImage, pNext); +} + +#if VMA_EXTERNAL_MEMORY_WIN32 +VkResult VmaDeviceMemoryBlock::CreateWin32Handle(const VmaAllocator hAllocator, PFN_vkGetMemoryWin32HandleKHR pvkGetMemoryWin32HandleKHR, HANDLE hTargetProcess, HANDLE* pHandle) noexcept +{ + VMA_ASSERT(pHandle); + return m_Handle.GetHandle(hAllocator->m_hDevice, m_hMemory, pvkGetMemoryWin32HandleKHR, hTargetProcess, hAllocator->m_UseMutex, pHandle); +} +#endif // VMA_EXTERNAL_MEMORY_WIN32 +#endif // _VMA_DEVICE_MEMORY_BLOCK_FUNCTIONS + +#ifndef _VMA_ALLOCATION_T_FUNCTIONS +VmaAllocation_T::VmaAllocation_T(bool mappingAllowed) + : m_Alignment{ 1 }, + m_Size{ 0 }, + m_pUserData{ VMA_NULL }, + m_pName{ VMA_NULL }, + m_MemoryTypeIndex{ 0 }, + m_Type{ (uint8_t)ALLOCATION_TYPE_NONE }, + m_SuballocationType{ (uint8_t)VMA_SUBALLOCATION_TYPE_UNKNOWN }, + m_MapCount{ 0 }, + m_Flags{ 0 } +{ + if(mappingAllowed) + m_Flags |= (uint8_t)FLAG_MAPPING_ALLOWED; +} + +VmaAllocation_T::~VmaAllocation_T() +{ + VMA_ASSERT_LEAK(m_MapCount == 0 && "Allocation was not unmapped before destruction."); + + // Check if owned string was freed. + VMA_ASSERT(m_pName == VMA_NULL); +} + +void VmaAllocation_T::InitBlockAllocation( + VmaDeviceMemoryBlock* block, + VmaAllocHandle allocHandle, + VkDeviceSize alignment, + VkDeviceSize size, + uint32_t memoryTypeIndex, + VmaSuballocationType suballocationType, + bool mapped) +{ + VMA_ASSERT(m_Type == ALLOCATION_TYPE_NONE); + VMA_ASSERT(block != VMA_NULL); + m_Type = (uint8_t)ALLOCATION_TYPE_BLOCK; + m_Alignment = alignment; + m_Size = size; + m_MemoryTypeIndex = memoryTypeIndex; + if(mapped) + { + VMA_ASSERT(IsMappingAllowed() && "Mapping is not allowed on this allocation! Please use one of the new VMA_ALLOCATION_CREATE_HOST_ACCESS_* flags when creating it."); + m_Flags |= (uint8_t)FLAG_PERSISTENT_MAP; + } + m_SuballocationType = (uint8_t)suballocationType; + m_BlockAllocation.m_Block = block; + m_BlockAllocation.m_AllocHandle = allocHandle; +} + +void VmaAllocation_T::InitDedicatedAllocation( + VmaAllocator allocator, + VmaPool hParentPool, + uint32_t memoryTypeIndex, + VkDeviceMemory hMemory, + VmaSuballocationType suballocationType, + void* pMappedData, + VkDeviceSize size) +{ + VMA_ASSERT(m_Type == ALLOCATION_TYPE_NONE); + VMA_ASSERT(hMemory != VK_NULL_HANDLE); + m_Type = (uint8_t)ALLOCATION_TYPE_DEDICATED; + m_Alignment = 0; + m_Size = size; + m_MemoryTypeIndex = memoryTypeIndex; + m_SuballocationType = (uint8_t)suballocationType; + m_DedicatedAllocation.m_ExtraData = VMA_NULL; + m_DedicatedAllocation.m_hParentPool = hParentPool; + m_DedicatedAllocation.m_hMemory = hMemory; + m_DedicatedAllocation.m_Prev = VMA_NULL; + m_DedicatedAllocation.m_Next = VMA_NULL; + + if (pMappedData != VMA_NULL) + { + VMA_ASSERT(IsMappingAllowed() && "Mapping is not allowed on this allocation! Please use one of the new VMA_ALLOCATION_CREATE_HOST_ACCESS_* flags when creating it."); + m_Flags |= (uint8_t)FLAG_PERSISTENT_MAP; + EnsureExtraData(allocator); + m_DedicatedAllocation.m_ExtraData->m_pMappedData = pMappedData; + } +} + +void VmaAllocation_T::Destroy(VmaAllocator allocator) +{ + FreeName(allocator); + + if (GetType() == ALLOCATION_TYPE_DEDICATED) + { + vma_delete(allocator, m_DedicatedAllocation.m_ExtraData); + } +} + +void VmaAllocation_T::SetName(VmaAllocator hAllocator, const char* pName) +{ + VMA_ASSERT(pName == VMA_NULL || pName != m_pName); + + FreeName(hAllocator); + + if (pName != VMA_NULL) + m_pName = VmaCreateStringCopy(hAllocator->GetAllocationCallbacks(), pName); +} + +uint8_t VmaAllocation_T::SwapBlockAllocation(VmaAllocator hAllocator, VmaAllocation allocation) +{ + VMA_ASSERT(allocation != VMA_NULL); + VMA_ASSERT(m_Type == ALLOCATION_TYPE_BLOCK); + VMA_ASSERT(allocation->m_Type == ALLOCATION_TYPE_BLOCK); + + if (m_MapCount != 0) + m_BlockAllocation.m_Block->Unmap(hAllocator, m_MapCount); + + m_BlockAllocation.m_Block->m_pMetadata->SetAllocationUserData(m_BlockAllocation.m_AllocHandle, allocation); + std::swap(m_BlockAllocation, allocation->m_BlockAllocation); + m_BlockAllocation.m_Block->m_pMetadata->SetAllocationUserData(m_BlockAllocation.m_AllocHandle, this); + +#if VMA_STATS_STRING_ENABLED + std::swap(m_BufferImageUsage, allocation->m_BufferImageUsage); +#endif + return m_MapCount; +} + +VmaAllocHandle VmaAllocation_T::GetAllocHandle() const +{ + switch (m_Type) + { + case ALLOCATION_TYPE_BLOCK: + return m_BlockAllocation.m_AllocHandle; + case ALLOCATION_TYPE_DEDICATED: + return VK_NULL_HANDLE; + default: + VMA_ASSERT(0); + return VK_NULL_HANDLE; + } +} + +VkDeviceSize VmaAllocation_T::GetOffset() const +{ + switch (m_Type) + { + case ALLOCATION_TYPE_BLOCK: + return m_BlockAllocation.m_Block->m_pMetadata->GetAllocationOffset(m_BlockAllocation.m_AllocHandle); + case ALLOCATION_TYPE_DEDICATED: + return 0; + default: + VMA_ASSERT(0); + return 0; + } +} + +VmaPool VmaAllocation_T::GetParentPool() const +{ + switch (m_Type) + { + case ALLOCATION_TYPE_BLOCK: + return m_BlockAllocation.m_Block->GetParentPool(); + case ALLOCATION_TYPE_DEDICATED: + return m_DedicatedAllocation.m_hParentPool; + default: + VMA_ASSERT(0); + return VK_NULL_HANDLE; + } +} + +VkDeviceMemory VmaAllocation_T::GetMemory() const +{ + switch (m_Type) + { + case ALLOCATION_TYPE_BLOCK: + return m_BlockAllocation.m_Block->GetDeviceMemory(); + case ALLOCATION_TYPE_DEDICATED: + return m_DedicatedAllocation.m_hMemory; + default: + VMA_ASSERT(0); + return VK_NULL_HANDLE; + } +} + +void* VmaAllocation_T::GetMappedData() const +{ + switch (m_Type) + { + case ALLOCATION_TYPE_BLOCK: + if (m_MapCount != 0 || IsPersistentMap()) + { + void* pBlockData = m_BlockAllocation.m_Block->GetMappedData(); + VMA_ASSERT(pBlockData != VMA_NULL); + return (char*)pBlockData + GetOffset(); + } + else + { + return VMA_NULL; + } + break; + case ALLOCATION_TYPE_DEDICATED: + VMA_ASSERT((m_DedicatedAllocation.m_ExtraData != VMA_NULL && m_DedicatedAllocation.m_ExtraData->m_pMappedData != VMA_NULL) == + (m_MapCount != 0 || IsPersistentMap())); + return m_DedicatedAllocation.m_ExtraData != VMA_NULL ? m_DedicatedAllocation.m_ExtraData->m_pMappedData : VMA_NULL; + default: + VMA_ASSERT(0); + return VMA_NULL; + } +} + +void VmaAllocation_T::BlockAllocMap() +{ + VMA_ASSERT(GetType() == ALLOCATION_TYPE_BLOCK); + VMA_ASSERT(IsMappingAllowed() && "Mapping is not allowed on this allocation! Please use one of the new VMA_ALLOCATION_CREATE_HOST_ACCESS_* flags when creating it."); + + if (m_MapCount < 0xFF) + { + ++m_MapCount; + } + else + { + VMA_ASSERT(0 && "Allocation mapped too many times simultaneously."); + } +} + +void VmaAllocation_T::BlockAllocUnmap() +{ + VMA_ASSERT(GetType() == ALLOCATION_TYPE_BLOCK); + + if (m_MapCount > 0) + { + --m_MapCount; + } + else + { + VMA_ASSERT(0 && "Unmapping allocation not previously mapped."); + } +} + +VkResult VmaAllocation_T::DedicatedAllocMap(VmaAllocator hAllocator, void** ppData) +{ + VMA_ASSERT(GetType() == ALLOCATION_TYPE_DEDICATED); + VMA_ASSERT(IsMappingAllowed() && "Mapping is not allowed on this allocation! Please use one of the new VMA_ALLOCATION_CREATE_HOST_ACCESS_* flags when creating it."); + + EnsureExtraData(hAllocator); + + if (m_MapCount != 0 || IsPersistentMap()) + { + if (m_MapCount < 0xFF) + { + VMA_ASSERT(m_DedicatedAllocation.m_ExtraData->m_pMappedData != VMA_NULL); + *ppData = m_DedicatedAllocation.m_ExtraData->m_pMappedData; + ++m_MapCount; + return VK_SUCCESS; + } + + VMA_ASSERT(0 && "Dedicated allocation mapped too many times simultaneously."); + return VK_ERROR_MEMORY_MAP_FAILED; + } + + VkResult result = (*hAllocator->GetVulkanFunctions().vkMapMemory)( + hAllocator->m_hDevice, + m_DedicatedAllocation.m_hMemory, + 0, // offset + VK_WHOLE_SIZE, + 0, // flags + ppData); + if (result == VK_SUCCESS) + { + m_DedicatedAllocation.m_ExtraData->m_pMappedData = *ppData; + m_MapCount = 1; + } + return result; +} + +void VmaAllocation_T::DedicatedAllocUnmap(VmaAllocator hAllocator) +{ + VMA_ASSERT(GetType() == ALLOCATION_TYPE_DEDICATED); + + if (m_MapCount > 0) + { + --m_MapCount; + if (m_MapCount == 0 && !IsPersistentMap()) + { + VMA_ASSERT(m_DedicatedAllocation.m_ExtraData != VMA_NULL); + m_DedicatedAllocation.m_ExtraData->m_pMappedData = VMA_NULL; + (*hAllocator->GetVulkanFunctions().vkUnmapMemory)( + hAllocator->m_hDevice, + m_DedicatedAllocation.m_hMemory); + } + } + else + { + VMA_ASSERT(0 && "Unmapping dedicated allocation not previously mapped."); + } +} + +#if VMA_STATS_STRING_ENABLED +void VmaAllocation_T::PrintParameters(class VmaJsonWriter& json) const +{ + json.WriteString("Type"); + json.WriteString(VMA_SUBALLOCATION_TYPE_NAMES[m_SuballocationType]); + + json.WriteString("Size"); + json.WriteNumber(m_Size); + json.WriteString("Usage"); + json.WriteNumber(m_BufferImageUsage.Value); // It may be uint32_t or uint64_t. + + if (m_pUserData != VMA_NULL) + { + json.WriteString("CustomData"); + json.BeginString(); + json.ContinueString_Pointer(m_pUserData); + json.EndString(); + } + if (m_pName != VMA_NULL) + { + json.WriteString("Name"); + json.WriteString(m_pName); + } +} +#if VMA_EXTERNAL_MEMORY_WIN32 +VkResult VmaAllocation_T::GetWin32Handle(VmaAllocator hAllocator, HANDLE hTargetProcess, HANDLE* pHandle) noexcept +{ + auto pvkGetMemoryWin32HandleKHR = hAllocator->GetVulkanFunctions().vkGetMemoryWin32HandleKHR; + switch (m_Type) + { + case ALLOCATION_TYPE_BLOCK: + return m_BlockAllocation.m_Block->CreateWin32Handle(hAllocator, pvkGetMemoryWin32HandleKHR, hTargetProcess, pHandle); + case ALLOCATION_TYPE_DEDICATED: + EnsureExtraData(hAllocator); + return m_DedicatedAllocation.m_ExtraData->m_Handle.GetHandle(hAllocator->m_hDevice, m_DedicatedAllocation.m_hMemory, pvkGetMemoryWin32HandleKHR, hTargetProcess, hAllocator->m_UseMutex, pHandle); + default: + VMA_ASSERT(0); + return VK_ERROR_FEATURE_NOT_PRESENT; + } +} +#endif // VMA_EXTERNAL_MEMORY_WIN32 +#endif // VMA_STATS_STRING_ENABLED + +void VmaAllocation_T::EnsureExtraData(VmaAllocator hAllocator) +{ + if (m_DedicatedAllocation.m_ExtraData == VMA_NULL) + { + m_DedicatedAllocation.m_ExtraData = vma_new(hAllocator, VmaAllocationExtraData)(); + } +} + +void VmaAllocation_T::FreeName(VmaAllocator hAllocator) +{ + if(m_pName) + { + VmaFreeString(hAllocator->GetAllocationCallbacks(), m_pName); + m_pName = VMA_NULL; + } +} +#endif // _VMA_ALLOCATION_T_FUNCTIONS + +#ifndef _VMA_BLOCK_VECTOR_FUNCTIONS +VmaBlockVector::VmaBlockVector( + VmaAllocator hAllocator, + VmaPool hParentPool, + uint32_t memoryTypeIndex, + VkDeviceSize preferredBlockSize, + size_t minBlockCount, + size_t maxBlockCount, + VkDeviceSize bufferImageGranularity, + bool explicitBlockSize, + uint32_t algorithm, + float priority, + VkDeviceSize minAllocationAlignment, + void* pMemoryAllocateNext) + : m_hAllocator(hAllocator), + m_hParentPool(hParentPool), + m_MemoryTypeIndex(memoryTypeIndex), + m_PreferredBlockSize(preferredBlockSize), + m_MinBlockCount(minBlockCount), + m_MaxBlockCount(maxBlockCount), + m_BufferImageGranularity(bufferImageGranularity), + m_ExplicitBlockSize(explicitBlockSize), + m_Algorithm(algorithm), + m_Priority(priority), + m_MinAllocationAlignment(minAllocationAlignment), + m_pMemoryAllocateNext(pMemoryAllocateNext), + m_Blocks(VmaStlAllocator(hAllocator->GetAllocationCallbacks())), + m_NextBlockId(0) {} + +VmaBlockVector::~VmaBlockVector() +{ + for (size_t i = m_Blocks.size(); i--; ) + { + m_Blocks[i]->Destroy(m_hAllocator); + vma_delete(m_hAllocator, m_Blocks[i]); + } +} + +VkResult VmaBlockVector::CreateMinBlocks() +{ + for (size_t i = 0; i < m_MinBlockCount; ++i) + { + VkResult res = CreateBlock(m_PreferredBlockSize, VMA_NULL); + if (res != VK_SUCCESS) + { + return res; + } + } + return VK_SUCCESS; +} + +void VmaBlockVector::AddStatistics(VmaStatistics& inoutStats) +{ + VmaMutexLockRead lock(m_Mutex, m_hAllocator->m_UseMutex); + + const size_t blockCount = m_Blocks.size(); + for (uint32_t blockIndex = 0; blockIndex < blockCount; ++blockIndex) + { + const VmaDeviceMemoryBlock* const pBlock = m_Blocks[blockIndex]; + VMA_ASSERT(pBlock); + VMA_HEAVY_ASSERT(pBlock->Validate()); + pBlock->m_pMetadata->AddStatistics(inoutStats); + } +} + +void VmaBlockVector::AddDetailedStatistics(VmaDetailedStatistics& inoutStats) +{ + VmaMutexLockRead lock(m_Mutex, m_hAllocator->m_UseMutex); + + const size_t blockCount = m_Blocks.size(); + for (uint32_t blockIndex = 0; blockIndex < blockCount; ++blockIndex) + { + const VmaDeviceMemoryBlock* const pBlock = m_Blocks[blockIndex]; + VMA_ASSERT(pBlock); + VMA_HEAVY_ASSERT(pBlock->Validate()); + pBlock->m_pMetadata->AddDetailedStatistics(inoutStats); + } +} + +bool VmaBlockVector::IsEmpty() +{ + VmaMutexLockRead lock(m_Mutex, m_hAllocator->m_UseMutex); + return m_Blocks.empty(); +} + +bool VmaBlockVector::IsCorruptionDetectionEnabled() const +{ + const uint32_t requiredMemFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + return (VMA_DEBUG_DETECT_CORRUPTION != 0) && + (VMA_DEBUG_MARGIN > 0) && + (m_Algorithm == 0 || m_Algorithm == VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT) && + (m_hAllocator->m_MemProps.memoryTypes[m_MemoryTypeIndex].propertyFlags & requiredMemFlags) == requiredMemFlags; +} + +VkResult VmaBlockVector::Allocate( + VkDeviceSize size, + VkDeviceSize alignment, + const VmaAllocationCreateInfo& createInfo, + VmaSuballocationType suballocType, + size_t allocationCount, + VmaAllocation* pAllocations) +{ + size_t allocIndex = 0; + VkResult res = VK_SUCCESS; + + alignment = VMA_MAX(alignment, m_MinAllocationAlignment); + + if (IsCorruptionDetectionEnabled()) + { + size = VmaAlignUp(size, sizeof(VMA_CORRUPTION_DETECTION_MAGIC_VALUE)); + alignment = VmaAlignUp(alignment, sizeof(VMA_CORRUPTION_DETECTION_MAGIC_VALUE)); + } + + { + VmaMutexLockWrite lock(m_Mutex, m_hAllocator->m_UseMutex); + for (; allocIndex < allocationCount; ++allocIndex) + { + res = AllocatePage( + size, + alignment, + createInfo, + suballocType, + pAllocations + allocIndex); + if (res != VK_SUCCESS) + { + break; + } + } + } + + if (res != VK_SUCCESS) + { + // Free all already created allocations. + while (allocIndex--) + Free(pAllocations[allocIndex]); + memset(pAllocations, 0, sizeof(VmaAllocation) * allocationCount); + } + + return res; +} + +VkResult VmaBlockVector::AllocatePage( + VkDeviceSize size, + VkDeviceSize alignment, + const VmaAllocationCreateInfo& createInfo, + VmaSuballocationType suballocType, + VmaAllocation* pAllocation) +{ + const bool isUpperAddress = (createInfo.flags & VMA_ALLOCATION_CREATE_UPPER_ADDRESS_BIT) != 0; + + VkDeviceSize freeMemory = 0; + { + const uint32_t heapIndex = m_hAllocator->MemoryTypeIndexToHeapIndex(m_MemoryTypeIndex); + VmaBudget heapBudget = {}; + m_hAllocator->GetHeapBudgets(&heapBudget, heapIndex, 1); + freeMemory = (heapBudget.usage < heapBudget.budget) ? (heapBudget.budget - heapBudget.usage) : 0; + } + + const bool canFallbackToDedicated = !HasExplicitBlockSize() && + (createInfo.flags & VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT) == 0; + const bool canCreateNewBlock = + ((createInfo.flags & VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT) == 0) && + (m_Blocks.size() < m_MaxBlockCount) && + (freeMemory >= size || !canFallbackToDedicated); + uint32_t strategy = createInfo.flags & VMA_ALLOCATION_CREATE_STRATEGY_MASK; + + // Upper address can only be used with linear allocator and within single memory block. + if (isUpperAddress && + (m_Algorithm != VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT || m_MaxBlockCount > 1)) + { + return VK_ERROR_FEATURE_NOT_PRESENT; + } + + // Early reject: requested allocation size is larger that maximum block size for this block vector. + if (size + VMA_DEBUG_MARGIN > m_PreferredBlockSize) + { + return VK_ERROR_OUT_OF_DEVICE_MEMORY; + } + + // 1. Search existing allocations. Try to allocate. + if (m_Algorithm == VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT) + { + // Use only last block. + if (!m_Blocks.empty()) + { + VmaDeviceMemoryBlock* const pCurrBlock = m_Blocks.back(); + VMA_ASSERT(pCurrBlock); + VkResult res = AllocateFromBlock( + pCurrBlock, size, alignment, createInfo.flags, createInfo.pUserData, suballocType, strategy, pAllocation); + if (res == VK_SUCCESS) + { + VMA_DEBUG_LOG_FORMAT(" Returned from last block #%" PRIu32, pCurrBlock->GetId()); + IncrementallySortBlocks(); + return VK_SUCCESS; + } + } + } + else + { + if (strategy != VMA_ALLOCATION_CREATE_STRATEGY_MIN_TIME_BIT) // MIN_MEMORY or default + { + const bool isHostVisible = + (m_hAllocator->m_MemProps.memoryTypes[m_MemoryTypeIndex].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0; + if(isHostVisible) + { + const bool isMappingAllowed = (createInfo.flags & + (VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT)) != 0; + /* + For non-mappable allocations, check blocks that are not mapped first. + For mappable allocations, check blocks that are already mapped first. + This way, having many blocks, we will separate mappable and non-mappable allocations, + hopefully limiting the number of blocks that are mapped, which will help tools like RenderDoc. + */ + for(size_t mappingI = 0; mappingI < 2; ++mappingI) + { + // Forward order in m_Blocks - prefer blocks with smallest amount of free space. + for (size_t blockIndex = 0; blockIndex < m_Blocks.size(); ++blockIndex) + { + VmaDeviceMemoryBlock* const pCurrBlock = m_Blocks[blockIndex]; + VMA_ASSERT(pCurrBlock); + const bool isBlockMapped = pCurrBlock->GetMappedData() != VMA_NULL; + if((mappingI == 0) == (isMappingAllowed == isBlockMapped)) + { + VkResult res = AllocateFromBlock( + pCurrBlock, size, alignment, createInfo.flags, createInfo.pUserData, suballocType, strategy, pAllocation); + if (res == VK_SUCCESS) + { + VMA_DEBUG_LOG_FORMAT(" Returned from existing block #%" PRIu32, pCurrBlock->GetId()); + IncrementallySortBlocks(); + return VK_SUCCESS; + } + } + } + } + } + else + { + // Forward order in m_Blocks - prefer blocks with smallest amount of free space. + for (size_t blockIndex = 0; blockIndex < m_Blocks.size(); ++blockIndex) + { + VmaDeviceMemoryBlock* const pCurrBlock = m_Blocks[blockIndex]; + VMA_ASSERT(pCurrBlock); + VkResult res = AllocateFromBlock( + pCurrBlock, size, alignment, createInfo.flags, createInfo.pUserData, suballocType, strategy, pAllocation); + if (res == VK_SUCCESS) + { + VMA_DEBUG_LOG_FORMAT(" Returned from existing block #%" PRIu32, pCurrBlock->GetId()); + IncrementallySortBlocks(); + return VK_SUCCESS; + } + } + } + } + else // VMA_ALLOCATION_CREATE_STRATEGY_MIN_TIME_BIT + { + // Backward order in m_Blocks - prefer blocks with largest amount of free space. + for (size_t blockIndex = m_Blocks.size(); blockIndex--; ) + { + VmaDeviceMemoryBlock* const pCurrBlock = m_Blocks[blockIndex]; + VMA_ASSERT(pCurrBlock); + VkResult res = AllocateFromBlock(pCurrBlock, size, alignment, createInfo.flags, createInfo.pUserData, suballocType, strategy, pAllocation); + if (res == VK_SUCCESS) + { + VMA_DEBUG_LOG_FORMAT(" Returned from existing block #%" PRIu32, pCurrBlock->GetId()); + IncrementallySortBlocks(); + return VK_SUCCESS; + } + } + } + } + + // 2. Try to create new block. + if (canCreateNewBlock) + { + // Calculate optimal size for new block. + VkDeviceSize newBlockSize = m_PreferredBlockSize; + uint32_t newBlockSizeShift = 0; + const uint32_t NEW_BLOCK_SIZE_SHIFT_MAX = 3; + + if (!m_ExplicitBlockSize) + { + // Allocate 1/8, 1/4, 1/2 as first blocks. + const VkDeviceSize maxExistingBlockSize = CalcMaxBlockSize(); + for (uint32_t i = 0; i < NEW_BLOCK_SIZE_SHIFT_MAX; ++i) + { + const VkDeviceSize smallerNewBlockSize = newBlockSize / 2; + if (smallerNewBlockSize > maxExistingBlockSize && smallerNewBlockSize >= size * 2) + { + newBlockSize = smallerNewBlockSize; + ++newBlockSizeShift; + } + else + { + break; + } + } + } + + size_t newBlockIndex = 0; + VkResult res = (newBlockSize <= freeMemory || !canFallbackToDedicated) ? + CreateBlock(newBlockSize, &newBlockIndex) : VK_ERROR_OUT_OF_DEVICE_MEMORY; + // Allocation of this size failed? Try 1/2, 1/4, 1/8 of m_PreferredBlockSize. + if (!m_ExplicitBlockSize) + { + while (res < 0 && newBlockSizeShift < NEW_BLOCK_SIZE_SHIFT_MAX) + { + const VkDeviceSize smallerNewBlockSize = newBlockSize / 2; + if (smallerNewBlockSize >= size) + { + newBlockSize = smallerNewBlockSize; + ++newBlockSizeShift; + res = (newBlockSize <= freeMemory || !canFallbackToDedicated) ? + CreateBlock(newBlockSize, &newBlockIndex) : VK_ERROR_OUT_OF_DEVICE_MEMORY; + } + else + { + break; + } + } + } + + if (res == VK_SUCCESS) + { + VmaDeviceMemoryBlock* const pBlock = m_Blocks[newBlockIndex]; + VMA_ASSERT(pBlock->m_pMetadata->GetSize() >= size); + + res = AllocateFromBlock( + pBlock, size, alignment, createInfo.flags, createInfo.pUserData, suballocType, strategy, pAllocation); + if (res == VK_SUCCESS) + { + VMA_DEBUG_LOG_FORMAT(" Created new block #%" PRIu32 " Size=%" PRIu64, pBlock->GetId(), newBlockSize); + IncrementallySortBlocks(); + return VK_SUCCESS; + } + + // Allocation from new block failed, possibly due to VMA_DEBUG_MARGIN or alignment. + return VK_ERROR_OUT_OF_DEVICE_MEMORY; + } + } + + return VK_ERROR_OUT_OF_DEVICE_MEMORY; +} + +void VmaBlockVector::Free(VmaAllocation hAllocation) +{ + VmaDeviceMemoryBlock* pBlockToDelete = VMA_NULL; + + bool budgetExceeded = false; + { + const uint32_t heapIndex = m_hAllocator->MemoryTypeIndexToHeapIndex(m_MemoryTypeIndex); + VmaBudget heapBudget = {}; + m_hAllocator->GetHeapBudgets(&heapBudget, heapIndex, 1); + budgetExceeded = heapBudget.usage >= heapBudget.budget; + } + + // Scope for lock. + { + VmaMutexLockWrite lock(m_Mutex, m_hAllocator->m_UseMutex); + + VmaDeviceMemoryBlock* pBlock = hAllocation->GetBlock(); + + if (IsCorruptionDetectionEnabled()) + { + VkResult res = pBlock->ValidateMagicValueAfterAllocation(m_hAllocator, hAllocation->GetOffset(), hAllocation->GetSize()); + VMA_ASSERT(res == VK_SUCCESS && "Couldn't map block memory to validate magic value."); + } + + if (hAllocation->IsPersistentMap()) + { + pBlock->Unmap(m_hAllocator, 1); + } + + const bool hadEmptyBlockBeforeFree = HasEmptyBlock(); + pBlock->m_pMetadata->Free(hAllocation->GetAllocHandle()); + pBlock->PostFree(m_hAllocator); + VMA_HEAVY_ASSERT(pBlock->Validate()); + + VMA_DEBUG_LOG_FORMAT(" Freed from MemoryTypeIndex=%" PRIu32, m_MemoryTypeIndex); + + const bool canDeleteBlock = m_Blocks.size() > m_MinBlockCount; + // pBlock became empty after this deallocation. + if (pBlock->m_pMetadata->IsEmpty()) + { + // Already had empty block. We don't want to have two, so delete this one. + if ((hadEmptyBlockBeforeFree || budgetExceeded) && canDeleteBlock) + { + pBlockToDelete = pBlock; + Remove(pBlock); + } + // else: We now have one empty block - leave it. A hysteresis to avoid allocating whole block back and forth. + } + // pBlock didn't become empty, but we have another empty block - find and free that one. + // (This is optional, heuristics.) + else if (hadEmptyBlockBeforeFree && canDeleteBlock) + { + VmaDeviceMemoryBlock* pLastBlock = m_Blocks.back(); + if (pLastBlock->m_pMetadata->IsEmpty()) + { + pBlockToDelete = pLastBlock; + m_Blocks.pop_back(); + } + } + + IncrementallySortBlocks(); + + m_hAllocator->m_Budget.RemoveAllocation(m_hAllocator->MemoryTypeIndexToHeapIndex(m_MemoryTypeIndex), hAllocation->GetSize()); + hAllocation->Destroy(m_hAllocator); + m_hAllocator->m_AllocationObjectAllocator.Free(hAllocation); + } + + // Destruction of a free block. Deferred until this point, outside of mutex + // lock, for performance reason. + if (pBlockToDelete != VMA_NULL) + { + VMA_DEBUG_LOG_FORMAT(" Deleted empty block #%" PRIu32, pBlockToDelete->GetId()); + pBlockToDelete->Destroy(m_hAllocator); + vma_delete(m_hAllocator, pBlockToDelete); + } +} + +VkDeviceSize VmaBlockVector::CalcMaxBlockSize() const +{ + VkDeviceSize result = 0; + for (size_t i = m_Blocks.size(); i--; ) + { + result = VMA_MAX(result, m_Blocks[i]->m_pMetadata->GetSize()); + if (result >= m_PreferredBlockSize) + { + break; + } + } + return result; +} + +void VmaBlockVector::Remove(VmaDeviceMemoryBlock* pBlock) +{ + for (uint32_t blockIndex = 0; blockIndex < m_Blocks.size(); ++blockIndex) + { + if (m_Blocks[blockIndex] == pBlock) + { + VmaVectorRemove(m_Blocks, blockIndex); + return; + } + } + VMA_ASSERT(0); +} + +void VmaBlockVector::IncrementallySortBlocks() +{ + if (!m_IncrementalSort) + return; + if (m_Algorithm != VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT) + { + // Bubble sort only until first swap. + for (size_t i = 1; i < m_Blocks.size(); ++i) + { + if (m_Blocks[i - 1]->m_pMetadata->GetSumFreeSize() > m_Blocks[i]->m_pMetadata->GetSumFreeSize()) + { + std::swap(m_Blocks[i - 1], m_Blocks[i]); + return; + } + } + } +} + +void VmaBlockVector::SortByFreeSize() +{ + VMA_SORT(m_Blocks.begin(), m_Blocks.end(), + [](VmaDeviceMemoryBlock* b1, VmaDeviceMemoryBlock* b2) -> bool + { + return b1->m_pMetadata->GetSumFreeSize() < b2->m_pMetadata->GetSumFreeSize(); + }); +} + +VkResult VmaBlockVector::AllocateFromBlock( + VmaDeviceMemoryBlock* pBlock, + VkDeviceSize size, + VkDeviceSize alignment, + VmaAllocationCreateFlags allocFlags, + void* pUserData, + VmaSuballocationType suballocType, + uint32_t strategy, + VmaAllocation* pAllocation) +{ + const bool isUpperAddress = (allocFlags & VMA_ALLOCATION_CREATE_UPPER_ADDRESS_BIT) != 0; + + VmaAllocationRequest currRequest = {}; + if (pBlock->m_pMetadata->CreateAllocationRequest( + size, + alignment, + isUpperAddress, + suballocType, + strategy, + &currRequest)) + { + return CommitAllocationRequest(currRequest, pBlock, alignment, allocFlags, pUserData, suballocType, pAllocation); + } + return VK_ERROR_OUT_OF_DEVICE_MEMORY; +} + +VkResult VmaBlockVector::CommitAllocationRequest( + VmaAllocationRequest& allocRequest, + VmaDeviceMemoryBlock* pBlock, + VkDeviceSize alignment, + VmaAllocationCreateFlags allocFlags, + void* pUserData, + VmaSuballocationType suballocType, + VmaAllocation* pAllocation) +{ + const bool mapped = (allocFlags & VMA_ALLOCATION_CREATE_MAPPED_BIT) != 0; + const bool isUserDataString = (allocFlags & VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT) != 0; + const bool isMappingAllowed = (allocFlags & + (VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT)) != 0; + + pBlock->PostAlloc(m_hAllocator); + // Allocate from pCurrBlock. + if (mapped) + { + VkResult res = pBlock->Map(m_hAllocator, 1, VMA_NULL); + if (res != VK_SUCCESS) + { + return res; + } + } + + *pAllocation = m_hAllocator->m_AllocationObjectAllocator.Allocate(isMappingAllowed); + pBlock->m_pMetadata->Alloc(allocRequest, suballocType, *pAllocation); + (*pAllocation)->InitBlockAllocation( + pBlock, + allocRequest.allocHandle, + alignment, + allocRequest.size, // Not size, as actual allocation size may be larger than requested! + m_MemoryTypeIndex, + suballocType, + mapped); + VMA_HEAVY_ASSERT(pBlock->Validate()); + if (isUserDataString) + (*pAllocation)->SetName(m_hAllocator, (const char*)pUserData); + else + (*pAllocation)->SetUserData(m_hAllocator, pUserData); + m_hAllocator->m_Budget.AddAllocation(m_hAllocator->MemoryTypeIndexToHeapIndex(m_MemoryTypeIndex), allocRequest.size); + if (VMA_DEBUG_INITIALIZE_ALLOCATIONS) + { + m_hAllocator->FillAllocation(*pAllocation, VMA_ALLOCATION_FILL_PATTERN_CREATED); + } + if (IsCorruptionDetectionEnabled()) + { + VkResult res = pBlock->WriteMagicValueAfterAllocation(m_hAllocator, (*pAllocation)->GetOffset(), allocRequest.size); + VMA_ASSERT(res == VK_SUCCESS && "Couldn't map block memory to write magic value."); + } + return VK_SUCCESS; +} + +VkResult VmaBlockVector::CreateBlock(VkDeviceSize blockSize, size_t* pNewBlockIndex) +{ + VkMemoryAllocateInfo allocInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + allocInfo.pNext = m_pMemoryAllocateNext; + allocInfo.memoryTypeIndex = m_MemoryTypeIndex; + allocInfo.allocationSize = blockSize; + +#if VMA_BUFFER_DEVICE_ADDRESS + // Every standalone block can potentially contain a buffer with VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT - always enable the feature. + VkMemoryAllocateFlagsInfoKHR allocFlagsInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO_KHR }; + if (m_hAllocator->m_UseKhrBufferDeviceAddress) + { + allocFlagsInfo.flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT_KHR; + VmaPnextChainPushFront(&allocInfo, &allocFlagsInfo); + } +#endif // VMA_BUFFER_DEVICE_ADDRESS + +#if VMA_MEMORY_PRIORITY + VkMemoryPriorityAllocateInfoEXT priorityInfo = { VK_STRUCTURE_TYPE_MEMORY_PRIORITY_ALLOCATE_INFO_EXT }; + if (m_hAllocator->m_UseExtMemoryPriority) + { + VMA_ASSERT(m_Priority >= 0.F && m_Priority <= 1.F); + priorityInfo.priority = m_Priority; + VmaPnextChainPushFront(&allocInfo, &priorityInfo); + } +#endif // VMA_MEMORY_PRIORITY + +#if VMA_EXTERNAL_MEMORY + // Attach VkExportMemoryAllocateInfoKHR if necessary. + VkExportMemoryAllocateInfoKHR exportMemoryAllocInfo = { VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO_KHR }; + exportMemoryAllocInfo.handleTypes = m_hAllocator->GetExternalMemoryHandleTypeFlags(m_MemoryTypeIndex); + if (exportMemoryAllocInfo.handleTypes != 0) + { + VmaPnextChainPushFront(&allocInfo, &exportMemoryAllocInfo); + } +#endif // VMA_EXTERNAL_MEMORY + + VkDeviceMemory mem = VK_NULL_HANDLE; + VkResult res = m_hAllocator->AllocateVulkanMemory(&allocInfo, &mem); + if (res < 0) + { + return res; + } + + // New VkDeviceMemory successfully created. + + // Create new Allocation for it. + VmaDeviceMemoryBlock* const pBlock = vma_new(m_hAllocator, VmaDeviceMemoryBlock)(m_hAllocator); + pBlock->Init( + m_hAllocator, + m_hParentPool, + m_MemoryTypeIndex, + mem, + allocInfo.allocationSize, + m_NextBlockId++, + m_Algorithm, + m_BufferImageGranularity); + + m_Blocks.push_back(pBlock); + if (pNewBlockIndex != VMA_NULL) + { + *pNewBlockIndex = m_Blocks.size() - 1; + } + + return VK_SUCCESS; +} + +bool VmaBlockVector::HasEmptyBlock() +{ + for (size_t index = 0, count = m_Blocks.size(); index < count; ++index) + { + VmaDeviceMemoryBlock* const pBlock = m_Blocks[index]; + if (pBlock->m_pMetadata->IsEmpty()) + { + return true; + } + } + return false; +} + +#if VMA_STATS_STRING_ENABLED +void VmaBlockVector::PrintDetailedMap(class VmaJsonWriter& json) +{ + VmaMutexLockRead lock(m_Mutex, m_hAllocator->m_UseMutex); + + + json.BeginObject(); + for (size_t i = 0; i < m_Blocks.size(); ++i) + { + json.BeginString(); + json.ContinueString(m_Blocks[i]->GetId()); + json.EndString(); + + json.BeginObject(); + json.WriteString("MapRefCount"); + json.WriteNumber(m_Blocks[i]->GetMapRefCount()); + + m_Blocks[i]->m_pMetadata->PrintDetailedMap(json); + json.EndObject(); + } + json.EndObject(); +} +#endif // VMA_STATS_STRING_ENABLED + +VkResult VmaBlockVector::CheckCorruption() +{ + if (!IsCorruptionDetectionEnabled()) + { + return VK_ERROR_FEATURE_NOT_PRESENT; + } + + VmaMutexLockRead lock(m_Mutex, m_hAllocator->m_UseMutex); + for (uint32_t blockIndex = 0; blockIndex < m_Blocks.size(); ++blockIndex) + { + VmaDeviceMemoryBlock* const pBlock = m_Blocks[blockIndex]; + VMA_ASSERT(pBlock); + VkResult res = pBlock->CheckCorruption(m_hAllocator); + if (res != VK_SUCCESS) + { + return res; + } + } + return VK_SUCCESS; +} + +#endif // _VMA_BLOCK_VECTOR_FUNCTIONS + +#ifndef _VMA_DEFRAGMENTATION_CONTEXT_FUNCTIONS +VmaDefragmentationContext_T::VmaDefragmentationContext_T( + VmaAllocator hAllocator, + const VmaDefragmentationInfo& info) + : m_MaxPassBytes(info.maxBytesPerPass == 0 ? VK_WHOLE_SIZE : info.maxBytesPerPass), + m_MaxPassAllocations(info.maxAllocationsPerPass == 0 ? UINT32_MAX : info.maxAllocationsPerPass), + m_BreakCallback(info.pfnBreakCallback), + m_BreakCallbackUserData(info.pBreakCallbackUserData), + m_MoveAllocator(hAllocator->GetAllocationCallbacks()), + m_Moves(m_MoveAllocator), + m_Algorithm(info.flags & VMA_DEFRAGMENTATION_FLAG_ALGORITHM_MASK) +{ + if (info.pool != VMA_NULL) + { + m_BlockVectorCount = 1; + m_PoolBlockVector = &info.pool->m_BlockVector; + m_pBlockVectors = &m_PoolBlockVector; + m_PoolBlockVector->SetIncrementalSort(false); + m_PoolBlockVector->SortByFreeSize(); + } + else + { + m_BlockVectorCount = hAllocator->GetMemoryTypeCount(); + m_PoolBlockVector = VMA_NULL; + m_pBlockVectors = hAllocator->m_pBlockVectors; + for (uint32_t i = 0; i < m_BlockVectorCount; ++i) + { + VmaBlockVector* vector = m_pBlockVectors[i]; + if (vector != VMA_NULL) + { + vector->SetIncrementalSort(false); + vector->SortByFreeSize(); + } + } + } + + switch (m_Algorithm) + { + case 0: // Default algorithm + m_Algorithm = VMA_DEFRAGMENTATION_FLAG_ALGORITHM_BALANCED_BIT; + m_AlgorithmState = vma_new_array(hAllocator, StateBalanced, m_BlockVectorCount); + break; + case VMA_DEFRAGMENTATION_FLAG_ALGORITHM_BALANCED_BIT: + m_AlgorithmState = vma_new_array(hAllocator, StateBalanced, m_BlockVectorCount); + break; + case VMA_DEFRAGMENTATION_FLAG_ALGORITHM_EXTENSIVE_BIT: + if (hAllocator->GetBufferImageGranularity() > 1) + { + m_AlgorithmState = vma_new_array(hAllocator, StateExtensive, m_BlockVectorCount); + } + break; + default: + ; // Do nothing. + } +} + +VmaDefragmentationContext_T::~VmaDefragmentationContext_T() +{ + if (m_PoolBlockVector != VMA_NULL) + { + m_PoolBlockVector->SetIncrementalSort(true); + } + else + { + for (uint32_t i = 0; i < m_BlockVectorCount; ++i) + { + VmaBlockVector* vector = m_pBlockVectors[i]; + if (vector != VMA_NULL) + vector->SetIncrementalSort(true); + } + } + + if (m_AlgorithmState) + { + switch (m_Algorithm) + { + case VMA_DEFRAGMENTATION_FLAG_ALGORITHM_BALANCED_BIT: + vma_delete_array(m_MoveAllocator.m_pCallbacks, reinterpret_cast(m_AlgorithmState), m_BlockVectorCount); + break; + case VMA_DEFRAGMENTATION_FLAG_ALGORITHM_EXTENSIVE_BIT: + vma_delete_array(m_MoveAllocator.m_pCallbacks, reinterpret_cast(m_AlgorithmState), m_BlockVectorCount); + break; + default: + VMA_ASSERT(0); + } + } +} + +VkResult VmaDefragmentationContext_T::DefragmentPassBegin(VmaDefragmentationPassMoveInfo& moveInfo) +{ + if (m_PoolBlockVector != VMA_NULL) + { + VmaMutexLockWrite lock(m_PoolBlockVector->GetMutex(), m_PoolBlockVector->GetAllocator()->m_UseMutex); + + if (m_PoolBlockVector->GetBlockCount() > 1) + ComputeDefragmentation(*m_PoolBlockVector, 0); + else if (m_PoolBlockVector->GetBlockCount() == 1) + ReallocWithinBlock(*m_PoolBlockVector, m_PoolBlockVector->GetBlock(0)); + } + else + { + for (uint32_t i = 0; i < m_BlockVectorCount; ++i) + { + if (m_pBlockVectors[i] != VMA_NULL) + { + VmaMutexLockWrite lock(m_pBlockVectors[i]->GetMutex(), m_pBlockVectors[i]->GetAllocator()->m_UseMutex); + + if (m_pBlockVectors[i]->GetBlockCount() > 1) + { + if (ComputeDefragmentation(*m_pBlockVectors[i], i)) + break; + } + else if (m_pBlockVectors[i]->GetBlockCount() == 1) + { + if (ReallocWithinBlock(*m_pBlockVectors[i], m_pBlockVectors[i]->GetBlock(0))) + break; + } + } + } + } + + moveInfo.moveCount = static_cast(m_Moves.size()); + if (moveInfo.moveCount > 0) + { + moveInfo.pMoves = m_Moves.data(); + return VK_INCOMPLETE; + } + + moveInfo.pMoves = VMA_NULL; + return VK_SUCCESS; +} + +VkResult VmaDefragmentationContext_T::DefragmentPassEnd(VmaDefragmentationPassMoveInfo& moveInfo) +{ + VMA_ASSERT(moveInfo.moveCount > 0 ? moveInfo.pMoves != VMA_NULL : true); + + VkResult result = VK_SUCCESS; + VmaStlAllocator blockAllocator(m_MoveAllocator.m_pCallbacks); + VmaVector> immovableBlocks(blockAllocator); + VmaVector> mappedBlocks(blockAllocator); + + VmaAllocator allocator = VMA_NULL; + for (uint32_t i = 0; i < moveInfo.moveCount; ++i) + { + VmaDefragmentationMove& move = moveInfo.pMoves[i]; + size_t prevCount = 0; + size_t currentCount = 0; + VkDeviceSize freedBlockSize = 0; + + uint32_t vectorIndex = 0; + VmaBlockVector* vector = VMA_NULL; + if (m_PoolBlockVector != VMA_NULL) + { + vector = m_PoolBlockVector; + } + else + { + vectorIndex = move.srcAllocation->GetMemoryTypeIndex(); + vector = m_pBlockVectors[vectorIndex]; + VMA_ASSERT(vector != VMA_NULL); + } + + switch (move.operation) + { + case VMA_DEFRAGMENTATION_MOVE_OPERATION_COPY: + { + uint8_t mapCount = move.srcAllocation->SwapBlockAllocation(vector->m_hAllocator, move.dstTmpAllocation); + if (mapCount > 0) + { + allocator = vector->m_hAllocator; + VmaDeviceMemoryBlock* newMapBlock = move.srcAllocation->GetBlock(); + bool notPresent = true; + for (FragmentedBlock& block : mappedBlocks) + { + if (block.block == newMapBlock) + { + notPresent = false; + block.data += mapCount; + break; + } + } + if (notPresent) + mappedBlocks.push_back({ mapCount, newMapBlock }); + } + + // Scope for locks, Free have it's own lock + { + VmaMutexLockRead lock(vector->GetMutex(), vector->GetAllocator()->m_UseMutex); + prevCount = vector->GetBlockCount(); + freedBlockSize = move.dstTmpAllocation->GetBlock()->m_pMetadata->GetSize(); + } + vector->Free(move.dstTmpAllocation); + { + VmaMutexLockRead lock(vector->GetMutex(), vector->GetAllocator()->m_UseMutex); + currentCount = vector->GetBlockCount(); + } + + result = VK_INCOMPLETE; + break; + } + case VMA_DEFRAGMENTATION_MOVE_OPERATION_IGNORE: + { + m_PassStats.bytesMoved -= move.srcAllocation->GetSize(); + --m_PassStats.allocationsMoved; + vector->Free(move.dstTmpAllocation); + + VmaDeviceMemoryBlock* newBlock = move.srcAllocation->GetBlock(); + bool notPresent = true; + for (const FragmentedBlock& block : immovableBlocks) + { + if (block.block == newBlock) + { + notPresent = false; + break; + } + } + if (notPresent) + immovableBlocks.push_back({ vectorIndex, newBlock }); + break; + } + case VMA_DEFRAGMENTATION_MOVE_OPERATION_DESTROY: + { + m_PassStats.bytesMoved -= move.srcAllocation->GetSize(); + --m_PassStats.allocationsMoved; + // Scope for locks, Free have it's own lock + { + VmaMutexLockRead lock(vector->GetMutex(), vector->GetAllocator()->m_UseMutex); + prevCount = vector->GetBlockCount(); + freedBlockSize = move.srcAllocation->GetBlock()->m_pMetadata->GetSize(); + } + vector->Free(move.srcAllocation); + { + VmaMutexLockRead lock(vector->GetMutex(), vector->GetAllocator()->m_UseMutex); + currentCount = vector->GetBlockCount(); + } + freedBlockSize *= prevCount - currentCount; + + VkDeviceSize dstBlockSize = SIZE_MAX; + { + VmaMutexLockRead lock(vector->GetMutex(), vector->GetAllocator()->m_UseMutex); + dstBlockSize = move.dstTmpAllocation->GetBlock()->m_pMetadata->GetSize(); + } + vector->Free(move.dstTmpAllocation); + { + VmaMutexLockRead lock(vector->GetMutex(), vector->GetAllocator()->m_UseMutex); + freedBlockSize += dstBlockSize * (currentCount - vector->GetBlockCount()); + currentCount = vector->GetBlockCount(); + } + + result = VK_INCOMPLETE; + break; + } + default: + VMA_ASSERT(0); + } + + if (prevCount > currentCount) + { + size_t freedBlocks = prevCount - currentCount; + m_PassStats.deviceMemoryBlocksFreed += static_cast(freedBlocks); + m_PassStats.bytesFreed += freedBlockSize; + } + + if(m_Algorithm == VMA_DEFRAGMENTATION_FLAG_ALGORITHM_EXTENSIVE_BIT && + m_AlgorithmState != VMA_NULL) + { + // Avoid unnecessary tries to allocate when new free block is available + StateExtensive& state = reinterpret_cast(m_AlgorithmState)[vectorIndex]; + if (state.firstFreeBlock != SIZE_MAX) + { + const size_t diff = prevCount - currentCount; + if (state.firstFreeBlock >= diff) + { + state.firstFreeBlock -= diff; + if (state.firstFreeBlock != 0) + state.firstFreeBlock -= vector->GetBlock(state.firstFreeBlock - 1)->m_pMetadata->IsEmpty(); + } + else + state.firstFreeBlock = 0; + } + } + } + moveInfo.moveCount = 0; + moveInfo.pMoves = VMA_NULL; + m_Moves.clear(); + + // Update stats + m_GlobalStats.allocationsMoved += m_PassStats.allocationsMoved; + m_GlobalStats.bytesFreed += m_PassStats.bytesFreed; + m_GlobalStats.bytesMoved += m_PassStats.bytesMoved; + m_GlobalStats.deviceMemoryBlocksFreed += m_PassStats.deviceMemoryBlocksFreed; + m_PassStats = { 0 }; + + // Move blocks with immovable allocations according to algorithm + if (!immovableBlocks.empty()) + { + do + { + if(m_Algorithm == VMA_DEFRAGMENTATION_FLAG_ALGORITHM_EXTENSIVE_BIT) + { + if (m_AlgorithmState != VMA_NULL) + { + bool swapped = false; + // Move to the start of free blocks range + for (const FragmentedBlock& block : immovableBlocks) + { + StateExtensive& state = reinterpret_cast(m_AlgorithmState)[block.data]; + if (state.operation != StateExtensive::Operation::Cleanup) + { + VmaBlockVector* vector = m_pBlockVectors[block.data]; + VmaMutexLockWrite lock(vector->GetMutex(), vector->GetAllocator()->m_UseMutex); + + for (size_t i = 0, count = vector->GetBlockCount() - m_ImmovableBlockCount; i < count; ++i) + { + if (vector->GetBlock(i) == block.block) + { + std::swap(vector->m_Blocks[i], vector->m_Blocks[vector->GetBlockCount() - ++m_ImmovableBlockCount]); + if (state.firstFreeBlock != SIZE_MAX) + { + if (i + 1 < state.firstFreeBlock) + { + if (state.firstFreeBlock > 1) + std::swap(vector->m_Blocks[i], vector->m_Blocks[--state.firstFreeBlock]); + else + --state.firstFreeBlock; + } + } + swapped = true; + break; + } + } + } + } + if (swapped) + result = VK_INCOMPLETE; + break; + } + } + + // Move to the beginning + for (const FragmentedBlock& block : immovableBlocks) + { + VmaBlockVector* vector = m_pBlockVectors[block.data]; + VmaMutexLockWrite lock(vector->GetMutex(), vector->GetAllocator()->m_UseMutex); + + for (size_t i = m_ImmovableBlockCount; i < vector->GetBlockCount(); ++i) + { + if (vector->GetBlock(i) == block.block) + { + std::swap(vector->m_Blocks[i], vector->m_Blocks[m_ImmovableBlockCount++]); + break; + } + } + } + } while (false); + } + + // Bulk-map destination blocks + for (const FragmentedBlock& block : mappedBlocks) + { + VkResult res = block.block->Map(allocator, block.data, VMA_NULL); + VMA_ASSERT(res == VK_SUCCESS); + } + return result; +} + +bool VmaDefragmentationContext_T::ComputeDefragmentation(VmaBlockVector& vector, size_t index) +{ + switch (m_Algorithm) + { + case VMA_DEFRAGMENTATION_FLAG_ALGORITHM_FAST_BIT: + return ComputeDefragmentation_Fast(vector); + case VMA_DEFRAGMENTATION_FLAG_ALGORITHM_BALANCED_BIT: + return ComputeDefragmentation_Balanced(vector, index, true); + case VMA_DEFRAGMENTATION_FLAG_ALGORITHM_FULL_BIT: + return ComputeDefragmentation_Full(vector); + case VMA_DEFRAGMENTATION_FLAG_ALGORITHM_EXTENSIVE_BIT: + return ComputeDefragmentation_Extensive(vector, index); + default: + VMA_ASSERT(0); + return ComputeDefragmentation_Balanced(vector, index, true); + } +} + +VmaDefragmentationContext_T::MoveAllocationData VmaDefragmentationContext_T::GetMoveData( + VmaAllocHandle handle, VmaBlockMetadata* metadata) +{ + MoveAllocationData moveData; + moveData.move.srcAllocation = (VmaAllocation)metadata->GetAllocationUserData(handle); + moveData.size = moveData.move.srcAllocation->GetSize(); + moveData.alignment = moveData.move.srcAllocation->GetAlignment(); + moveData.type = moveData.move.srcAllocation->GetSuballocationType(); + moveData.flags = 0; + + if (moveData.move.srcAllocation->IsPersistentMap()) + moveData.flags |= VMA_ALLOCATION_CREATE_MAPPED_BIT; + if (moveData.move.srcAllocation->IsMappingAllowed()) + moveData.flags |= VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT; + + return moveData; +} + +VmaDefragmentationContext_T::CounterStatus VmaDefragmentationContext_T::CheckCounters(VkDeviceSize bytes) +{ + // Check custom criteria if exists + if (m_BreakCallback && m_BreakCallback(m_BreakCallbackUserData)) + return CounterStatus::End; + + // Ignore allocation if will exceed max size for copy + if (m_PassStats.bytesMoved + bytes > m_MaxPassBytes) + { + if (++m_IgnoredAllocs < MAX_ALLOCS_TO_IGNORE) + return CounterStatus::Ignore; + return CounterStatus::End; + } + + m_IgnoredAllocs = 0; + return CounterStatus::Pass; +} + +bool VmaDefragmentationContext_T::IncrementCounters(VkDeviceSize bytes) +{ + m_PassStats.bytesMoved += bytes; + // Early return when max found + if (++m_PassStats.allocationsMoved >= m_MaxPassAllocations || m_PassStats.bytesMoved >= m_MaxPassBytes) + { + VMA_ASSERT((m_PassStats.allocationsMoved == m_MaxPassAllocations || + m_PassStats.bytesMoved == m_MaxPassBytes) && "Exceeded maximal pass threshold!"); + return true; + } + return false; +} + +bool VmaDefragmentationContext_T::ReallocWithinBlock(VmaBlockVector& vector, VmaDeviceMemoryBlock* block) +{ + VmaBlockMetadata* metadata = block->m_pMetadata; + + for (VmaAllocHandle handle = metadata->GetAllocationListBegin(); + handle != VK_NULL_HANDLE; + handle = metadata->GetNextAllocation(handle)) + { + MoveAllocationData moveData = GetMoveData(handle, metadata); + // Ignore newly created allocations by defragmentation algorithm + if (moveData.move.srcAllocation->GetUserData() == this) + continue; + switch (CheckCounters(moveData.move.srcAllocation->GetSize())) + { + case CounterStatus::Ignore: + continue; + case CounterStatus::End: + return true; + case CounterStatus::Pass: + break; + default: + VMA_ASSERT(0); + } + + VkDeviceSize offset = moveData.move.srcAllocation->GetOffset(); + if (offset != 0 && metadata->GetSumFreeSize() >= moveData.size) + { + VmaAllocationRequest request = {}; + if (metadata->CreateAllocationRequest( + moveData.size, + moveData.alignment, + false, + moveData.type, + VMA_ALLOCATION_CREATE_STRATEGY_MIN_OFFSET_BIT, + &request)) + { + if (metadata->GetAllocationOffset(request.allocHandle) < offset) + { + if (vector.CommitAllocationRequest( + request, + block, + moveData.alignment, + moveData.flags, + this, + moveData.type, + &moveData.move.dstTmpAllocation) == VK_SUCCESS) + { + m_Moves.push_back(moveData.move); + if (IncrementCounters(moveData.size)) + return true; + } + } + } + } + } + return false; +} + +bool VmaDefragmentationContext_T::AllocInOtherBlock(size_t start, size_t end, MoveAllocationData& data, VmaBlockVector& vector) +{ + for (; start < end; ++start) + { + VmaDeviceMemoryBlock* dstBlock = vector.GetBlock(start); + if (dstBlock->m_pMetadata->GetSumFreeSize() >= data.size) + { + if (vector.AllocateFromBlock(dstBlock, + data.size, + data.alignment, + data.flags, + this, + data.type, + 0, + &data.move.dstTmpAllocation) == VK_SUCCESS) + { + m_Moves.push_back(data.move); + if (IncrementCounters(data.size)) + return true; + break; + } + } + } + return false; +} + +bool VmaDefragmentationContext_T::ComputeDefragmentation_Fast(VmaBlockVector& vector) +{ + // Move only between blocks + + // Go through allocations in last blocks and try to fit them inside first ones + for (size_t i = vector.GetBlockCount() - 1; i > m_ImmovableBlockCount; --i) + { + VmaBlockMetadata* metadata = vector.GetBlock(i)->m_pMetadata; + + for (VmaAllocHandle handle = metadata->GetAllocationListBegin(); + handle != VK_NULL_HANDLE; + handle = metadata->GetNextAllocation(handle)) + { + MoveAllocationData moveData = GetMoveData(handle, metadata); + // Ignore newly created allocations by defragmentation algorithm + if (moveData.move.srcAllocation->GetUserData() == this) + continue; + switch (CheckCounters(moveData.move.srcAllocation->GetSize())) + { + case CounterStatus::Ignore: + continue; + case CounterStatus::End: + return true; + case CounterStatus::Pass: + break; + default: + VMA_ASSERT(0); + } + + // Check all previous blocks for free space + if (AllocInOtherBlock(0, i, moveData, vector)) + return true; + } + } + return false; +} + +bool VmaDefragmentationContext_T::ComputeDefragmentation_Balanced(VmaBlockVector& vector, size_t index, bool update) +{ + // Go over every allocation and try to fit it in previous blocks at lowest offsets, + // if not possible: realloc within single block to minimize offset (exclude offset == 0), + // but only if there are noticeable gaps between them (some heuristic, ex. average size of allocation in block) + VMA_ASSERT(m_AlgorithmState != VMA_NULL); + + StateBalanced& vectorState = reinterpret_cast(m_AlgorithmState)[index]; + if (update && vectorState.avgAllocSize == UINT64_MAX) + UpdateVectorStatistics(vector, vectorState); + + const size_t startMoveCount = m_Moves.size(); + VkDeviceSize minimalFreeRegion = vectorState.avgFreeSize / 2; + for (size_t i = vector.GetBlockCount() - 1; i > m_ImmovableBlockCount; --i) + { + VmaDeviceMemoryBlock* block = vector.GetBlock(i); + VmaBlockMetadata* metadata = block->m_pMetadata; + VkDeviceSize prevFreeRegionSize = 0; + + for (VmaAllocHandle handle = metadata->GetAllocationListBegin(); + handle != VK_NULL_HANDLE; + handle = metadata->GetNextAllocation(handle)) + { + MoveAllocationData moveData = GetMoveData(handle, metadata); + // Ignore newly created allocations by defragmentation algorithm + if (moveData.move.srcAllocation->GetUserData() == this) + continue; + switch (CheckCounters(moveData.move.srcAllocation->GetSize())) + { + case CounterStatus::Ignore: + continue; + case CounterStatus::End: + return true; + case CounterStatus::Pass: + break; + default: + VMA_ASSERT(0); + } + + // Check all previous blocks for free space + const size_t prevMoveCount = m_Moves.size(); + if (AllocInOtherBlock(0, i, moveData, vector)) + return true; + + VkDeviceSize nextFreeRegionSize = metadata->GetNextFreeRegionSize(handle); + // If no room found then realloc within block for lower offset + VkDeviceSize offset = moveData.move.srcAllocation->GetOffset(); + if (prevMoveCount == m_Moves.size() && offset != 0 && metadata->GetSumFreeSize() >= moveData.size) + { + // Check if realloc will make sense + if (prevFreeRegionSize >= minimalFreeRegion || + nextFreeRegionSize >= minimalFreeRegion || + moveData.size <= vectorState.avgFreeSize || + moveData.size <= vectorState.avgAllocSize) + { + VmaAllocationRequest request = {}; + if (metadata->CreateAllocationRequest( + moveData.size, + moveData.alignment, + false, + moveData.type, + VMA_ALLOCATION_CREATE_STRATEGY_MIN_OFFSET_BIT, + &request)) + { + if (metadata->GetAllocationOffset(request.allocHandle) < offset) + { + if (vector.CommitAllocationRequest( + request, + block, + moveData.alignment, + moveData.flags, + this, + moveData.type, + &moveData.move.dstTmpAllocation) == VK_SUCCESS) + { + m_Moves.push_back(moveData.move); + if (IncrementCounters(moveData.size)) + return true; + } + } + } + } + } + prevFreeRegionSize = nextFreeRegionSize; + } + } + + // No moves performed, update statistics to current vector state + if (startMoveCount == m_Moves.size() && !update) + { + vectorState.avgAllocSize = UINT64_MAX; + return ComputeDefragmentation_Balanced(vector, index, false); + } + return false; +} + +bool VmaDefragmentationContext_T::ComputeDefragmentation_Full(VmaBlockVector& vector) +{ + // Go over every allocation and try to fit it in previous blocks at lowest offsets, + // if not possible: realloc within single block to minimize offset (exclude offset == 0) + + for (size_t i = vector.GetBlockCount() - 1; i > m_ImmovableBlockCount; --i) + { + VmaDeviceMemoryBlock* block = vector.GetBlock(i); + VmaBlockMetadata* metadata = block->m_pMetadata; + + for (VmaAllocHandle handle = metadata->GetAllocationListBegin(); + handle != VK_NULL_HANDLE; + handle = metadata->GetNextAllocation(handle)) + { + MoveAllocationData moveData = GetMoveData(handle, metadata); + // Ignore newly created allocations by defragmentation algorithm + if (moveData.move.srcAllocation->GetUserData() == this) + continue; + switch (CheckCounters(moveData.move.srcAllocation->GetSize())) + { + case CounterStatus::Ignore: + continue; + case CounterStatus::End: + return true; + case CounterStatus::Pass: + break; + default: + VMA_ASSERT(0); + } + + // Check all previous blocks for free space + const size_t prevMoveCount = m_Moves.size(); + if (AllocInOtherBlock(0, i, moveData, vector)) + return true; + + // If no room found then realloc within block for lower offset + VkDeviceSize offset = moveData.move.srcAllocation->GetOffset(); + if (prevMoveCount == m_Moves.size() && offset != 0 && metadata->GetSumFreeSize() >= moveData.size) + { + VmaAllocationRequest request = {}; + if (metadata->CreateAllocationRequest( + moveData.size, + moveData.alignment, + false, + moveData.type, + VMA_ALLOCATION_CREATE_STRATEGY_MIN_OFFSET_BIT, + &request)) + { + if (metadata->GetAllocationOffset(request.allocHandle) < offset) + { + if (vector.CommitAllocationRequest( + request, + block, + moveData.alignment, + moveData.flags, + this, + moveData.type, + &moveData.move.dstTmpAllocation) == VK_SUCCESS) + { + m_Moves.push_back(moveData.move); + if (IncrementCounters(moveData.size)) + return true; + } + } + } + } + } + } + return false; +} + +bool VmaDefragmentationContext_T::ComputeDefragmentation_Extensive(VmaBlockVector& vector, size_t index) +{ + // First free single block, then populate it to the brim, then free another block, and so on + + // Fallback to previous algorithm since without granularity conflicts it can achieve max packing + if (vector.m_BufferImageGranularity == 1) + return ComputeDefragmentation_Full(vector); + + VMA_ASSERT(m_AlgorithmState != VMA_NULL); + + StateExtensive& vectorState = reinterpret_cast(m_AlgorithmState)[index]; + + bool texturePresent = false; + bool bufferPresent = false; + bool otherPresent = false; + switch (vectorState.operation) + { + case StateExtensive::Operation::Done: // Vector defragmented + return false; + case StateExtensive::Operation::FindFreeBlockBuffer: + case StateExtensive::Operation::FindFreeBlockTexture: + case StateExtensive::Operation::FindFreeBlockAll: + { + // No more blocks to free, just perform fast realloc and move to cleanup + if (vectorState.firstFreeBlock == 0) + { + vectorState.operation = StateExtensive::Operation::Cleanup; + return ComputeDefragmentation_Fast(vector); + } + + // No free blocks, have to clear last one + size_t last = (vectorState.firstFreeBlock == SIZE_MAX ? vector.GetBlockCount() : vectorState.firstFreeBlock) - 1; + VmaBlockMetadata* freeMetadata = vector.GetBlock(last)->m_pMetadata; + + const size_t prevMoveCount = m_Moves.size(); + for (VmaAllocHandle handle = freeMetadata->GetAllocationListBegin(); + handle != VK_NULL_HANDLE; + handle = freeMetadata->GetNextAllocation(handle)) + { + MoveAllocationData moveData = GetMoveData(handle, freeMetadata); + switch (CheckCounters(moveData.move.srcAllocation->GetSize())) + { + case CounterStatus::Ignore: + continue; + case CounterStatus::End: + return true; + case CounterStatus::Pass: + break; + default: + VMA_ASSERT(0); + } + + // Check all previous blocks for free space + if (AllocInOtherBlock(0, last, moveData, vector)) + { + // Full clear performed already + if (prevMoveCount != m_Moves.size() && freeMetadata->GetNextAllocation(handle) == VK_NULL_HANDLE) + vectorState.firstFreeBlock = last; + return true; + } + } + + if (prevMoveCount == m_Moves.size()) + { + // Cannot perform full clear, have to move data in other blocks around + if (last != 0) + { + for (size_t i = last - 1; i; --i) + { + if (ReallocWithinBlock(vector, vector.GetBlock(i))) + return true; + } + } + + if (prevMoveCount == m_Moves.size()) + { + // No possible reallocs within blocks, try to move them around fast + return ComputeDefragmentation_Fast(vector); + } + } + else + { + switch (vectorState.operation) + { + case StateExtensive::Operation::FindFreeBlockBuffer: + vectorState.operation = StateExtensive::Operation::MoveBuffers; + break; + case StateExtensive::Operation::FindFreeBlockTexture: + vectorState.operation = StateExtensive::Operation::MoveTextures; + break; + case StateExtensive::Operation::FindFreeBlockAll: + vectorState.operation = StateExtensive::Operation::MoveAll; + break; + default: + VMA_ASSERT(0); + vectorState.operation = StateExtensive::Operation::MoveTextures; + } + vectorState.firstFreeBlock = last; + // Nothing done, block found without reallocations, can perform another reallocs in same pass + return ComputeDefragmentation_Extensive(vector, index); + } + break; + } + case StateExtensive::Operation::MoveTextures: + { + if (MoveDataToFreeBlocks(VMA_SUBALLOCATION_TYPE_IMAGE_OPTIMAL, vector, + vectorState.firstFreeBlock, texturePresent, bufferPresent, otherPresent)) + { + if (texturePresent) + { + vectorState.operation = StateExtensive::Operation::FindFreeBlockTexture; + return ComputeDefragmentation_Extensive(vector, index); + } + + if (!bufferPresent && !otherPresent) + { + vectorState.operation = StateExtensive::Operation::Cleanup; + break; + } + + // No more textures to move, check buffers + vectorState.operation = StateExtensive::Operation::MoveBuffers; + bufferPresent = false; + otherPresent = false; + } + else + break; + VMA_FALLTHROUGH; // Fallthrough + } + case StateExtensive::Operation::MoveBuffers: + { + if (MoveDataToFreeBlocks(VMA_SUBALLOCATION_TYPE_BUFFER, vector, + vectorState.firstFreeBlock, texturePresent, bufferPresent, otherPresent)) + { + if (bufferPresent) + { + vectorState.operation = StateExtensive::Operation::FindFreeBlockBuffer; + return ComputeDefragmentation_Extensive(vector, index); + } + + if (!otherPresent) + { + vectorState.operation = StateExtensive::Operation::Cleanup; + break; + } + + // No more buffers to move, check all others + vectorState.operation = StateExtensive::Operation::MoveAll; + otherPresent = false; + } + else + break; + VMA_FALLTHROUGH; // Fallthrough + } + case StateExtensive::Operation::MoveAll: + { + if (MoveDataToFreeBlocks(VMA_SUBALLOCATION_TYPE_FREE, vector, + vectorState.firstFreeBlock, texturePresent, bufferPresent, otherPresent)) + { + if (otherPresent) + { + vectorState.operation = StateExtensive::Operation::FindFreeBlockBuffer; + return ComputeDefragmentation_Extensive(vector, index); + } + // Everything moved + vectorState.operation = StateExtensive::Operation::Cleanup; + } + break; + } + case StateExtensive::Operation::Cleanup: + // Cleanup is handled below so that other operations may reuse the cleanup code. This case is here to prevent the unhandled enum value warning (C4062). + break; + } + + if (vectorState.operation == StateExtensive::Operation::Cleanup) + { + // All other work done, pack data in blocks even tighter if possible + const size_t prevMoveCount = m_Moves.size(); + for (size_t i = 0; i < vector.GetBlockCount(); ++i) + { + if (ReallocWithinBlock(vector, vector.GetBlock(i))) + return true; + } + + if (prevMoveCount == m_Moves.size()) + vectorState.operation = StateExtensive::Operation::Done; + } + return false; +} + +void VmaDefragmentationContext_T::UpdateVectorStatistics(VmaBlockVector& vector, StateBalanced& state) +{ + size_t allocCount = 0; + size_t freeCount = 0; + state.avgFreeSize = 0; + state.avgAllocSize = 0; + + for (size_t i = 0; i < vector.GetBlockCount(); ++i) + { + VmaBlockMetadata* metadata = vector.GetBlock(i)->m_pMetadata; + + allocCount += metadata->GetAllocationCount(); + freeCount += metadata->GetFreeRegionsCount(); + state.avgFreeSize += metadata->GetSumFreeSize(); + state.avgAllocSize += metadata->GetSize(); + } + + state.avgAllocSize = (state.avgAllocSize - state.avgFreeSize) / allocCount; + state.avgFreeSize /= freeCount; +} + +bool VmaDefragmentationContext_T::MoveDataToFreeBlocks(VmaSuballocationType currentType, + VmaBlockVector& vector, size_t firstFreeBlock, + bool& texturePresent, bool& bufferPresent, bool& otherPresent) +{ + const size_t prevMoveCount = m_Moves.size(); + for (size_t i = firstFreeBlock ; i;) + { + VmaDeviceMemoryBlock* block = vector.GetBlock(--i); + VmaBlockMetadata* metadata = block->m_pMetadata; + + for (VmaAllocHandle handle = metadata->GetAllocationListBegin(); + handle != VK_NULL_HANDLE; + handle = metadata->GetNextAllocation(handle)) + { + MoveAllocationData moveData = GetMoveData(handle, metadata); + // Ignore newly created allocations by defragmentation algorithm + if (moveData.move.srcAllocation->GetUserData() == this) + continue; + switch (CheckCounters(moveData.move.srcAllocation->GetSize())) + { + case CounterStatus::Ignore: + continue; + case CounterStatus::End: + return true; + case CounterStatus::Pass: + break; + default: + VMA_ASSERT(0); + } + + // Move only single type of resources at once + if (!VmaIsBufferImageGranularityConflict(moveData.type, currentType)) + { + // Try to fit allocation into free blocks + if (AllocInOtherBlock(firstFreeBlock, vector.GetBlockCount(), moveData, vector)) + return false; + } + + if (!VmaIsBufferImageGranularityConflict(moveData.type, VMA_SUBALLOCATION_TYPE_IMAGE_OPTIMAL)) + texturePresent = true; + else if (!VmaIsBufferImageGranularityConflict(moveData.type, VMA_SUBALLOCATION_TYPE_BUFFER)) + bufferPresent = true; + else + otherPresent = true; + } + } + return prevMoveCount == m_Moves.size(); +} +#endif // _VMA_DEFRAGMENTATION_CONTEXT_FUNCTIONS + +#ifndef _VMA_POOL_T_FUNCTIONS +VmaPool_T::VmaPool_T( + VmaAllocator hAllocator, + const VmaPoolCreateInfo& createInfo, + VkDeviceSize preferredBlockSize) + : m_BlockVector( + hAllocator, + this, // hParentPool + createInfo.memoryTypeIndex, + createInfo.blockSize != 0 ? createInfo.blockSize : preferredBlockSize, + createInfo.minBlockCount, + createInfo.maxBlockCount, + (createInfo.flags& VMA_POOL_CREATE_IGNORE_BUFFER_IMAGE_GRANULARITY_BIT) != 0 ? 1 : hAllocator->GetBufferImageGranularity(), + createInfo.blockSize != 0, // explicitBlockSize + createInfo.flags & VMA_POOL_CREATE_ALGORITHM_MASK, // algorithm + createInfo.priority, + VMA_MAX(hAllocator->GetMemoryTypeMinAlignment(createInfo.memoryTypeIndex), createInfo.minAllocationAlignment), + createInfo.pMemoryAllocateNext), + m_Id(0), + m_Name(VMA_NULL) {} + +VmaPool_T::~VmaPool_T() +{ + VMA_ASSERT(m_PrevPool == VMA_NULL && m_NextPool == VMA_NULL); + + const VkAllocationCallbacks* allocs = m_BlockVector.GetAllocator()->GetAllocationCallbacks(); + VmaFreeString(allocs, m_Name); +} + +void VmaPool_T::SetName(const char* pName) +{ + const VkAllocationCallbacks* allocs = m_BlockVector.GetAllocator()->GetAllocationCallbacks(); + VmaFreeString(allocs, m_Name); + + if (pName != VMA_NULL) + { + m_Name = VmaCreateStringCopy(allocs, pName); + } + else + { + m_Name = VMA_NULL; + } +} +#endif // _VMA_POOL_T_FUNCTIONS + +#ifndef _VMA_ALLOCATOR_T_FUNCTIONS +VmaAllocator_T::VmaAllocator_T(const VmaAllocatorCreateInfo* pCreateInfo) : + m_UseMutex((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_EXTERNALLY_SYNCHRONIZED_BIT) == 0), + m_VulkanApiVersion(pCreateInfo->vulkanApiVersion != 0 ? pCreateInfo->vulkanApiVersion : VK_API_VERSION_1_0), + m_UseKhrDedicatedAllocation((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT) != 0), + m_UseKhrBindMemory2((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_KHR_BIND_MEMORY2_BIT) != 0), + m_UseExtMemoryBudget((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT) != 0), + m_UseAmdDeviceCoherentMemory((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_AMD_DEVICE_COHERENT_MEMORY_BIT) != 0), + m_UseKhrBufferDeviceAddress((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT) != 0), + m_UseExtMemoryPriority((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT) != 0), + m_UseKhrMaintenance4((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE4_BIT) != 0), + m_UseKhrMaintenance5((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE5_BIT) != 0), + m_UseKhrExternalMemoryWin32((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_KHR_EXTERNAL_MEMORY_WIN32_BIT) != 0), + m_hDevice(pCreateInfo->device), + m_hInstance(pCreateInfo->instance), + m_AllocationCallbacksSpecified(pCreateInfo->pAllocationCallbacks != VMA_NULL), + m_AllocationCallbacks(pCreateInfo->pAllocationCallbacks ? + *pCreateInfo->pAllocationCallbacks : VmaEmptyAllocationCallbacks), + m_AllocationObjectAllocator(&m_AllocationCallbacks), + m_HeapSizeLimitMask(0), + m_DeviceMemoryCount(0), + m_PreferredLargeHeapBlockSize(0), + m_PhysicalDevice(pCreateInfo->physicalDevice), + m_GpuDefragmentationMemoryTypeBits(UINT32_MAX), + m_NextPoolId(0), + m_GlobalMemoryTypeBits(UINT32_MAX) +{ + if(m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) + { + m_UseKhrDedicatedAllocation = false; + m_UseKhrBindMemory2 = false; + } + + if(VMA_DEBUG_DETECT_CORRUPTION) + { + // Needs to be multiply of uint32_t size because we are going to write VMA_CORRUPTION_DETECTION_MAGIC_VALUE to it. + VMA_ASSERT(VMA_DEBUG_MARGIN % sizeof(uint32_t) == 0); + } + + VMA_ASSERT(pCreateInfo->physicalDevice && pCreateInfo->device && pCreateInfo->instance); + + if(m_VulkanApiVersion < VK_MAKE_VERSION(1, 1, 0)) + { +#if !(VMA_DEDICATED_ALLOCATION) + if((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT) != 0) + { + VMA_ASSERT(0 && "VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT set but required extensions are disabled by preprocessor macros."); + } +#endif +#if !(VMA_BIND_MEMORY2) + if((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_KHR_BIND_MEMORY2_BIT) != 0) + { + VMA_ASSERT(0 && "VMA_ALLOCATOR_CREATE_KHR_BIND_MEMORY2_BIT set but required extension is disabled by preprocessor macros."); + } +#endif + } +#if !(VMA_MEMORY_BUDGET) + if((pCreateInfo->flags & VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT) != 0) + { + VMA_ASSERT(0 && "VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT set but required extension is disabled by preprocessor macros."); + } +#endif +#if !(VMA_BUFFER_DEVICE_ADDRESS) + if(m_UseKhrBufferDeviceAddress) + { + VMA_ASSERT(0 && "VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT is set but required extension or Vulkan 1.2 is not available in your Vulkan header or its support in VMA has been disabled by a preprocessor macro."); + } +#endif +#if VMA_VULKAN_VERSION < 1004000 + VMA_ASSERT(m_VulkanApiVersion < VK_MAKE_VERSION(1, 4, 0) && "vulkanApiVersion >= VK_API_VERSION_1_4 but required Vulkan version is disabled by preprocessor macros."); +#endif +#if VMA_VULKAN_VERSION < 1003000 + VMA_ASSERT(m_VulkanApiVersion < VK_MAKE_VERSION(1, 3, 0) && "vulkanApiVersion >= VK_API_VERSION_1_3 but required Vulkan version is disabled by preprocessor macros."); +#endif +#if VMA_VULKAN_VERSION < 1002000 + VMA_ASSERT(m_VulkanApiVersion < VK_MAKE_VERSION(1, 2, 0) && "vulkanApiVersion >= VK_API_VERSION_1_2 but required Vulkan version is disabled by preprocessor macros."); +#endif +#if VMA_VULKAN_VERSION < 1001000 + VMA_ASSERT(m_VulkanApiVersion < VK_MAKE_VERSION(1, 1, 0) && "vulkanApiVersion >= VK_API_VERSION_1_1 but required Vulkan version is disabled by preprocessor macros."); +#endif +#if !(VMA_MEMORY_PRIORITY) + if(m_UseExtMemoryPriority) + { + VMA_ASSERT(0 && "VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT is set but required extension is not available in your Vulkan header or its support in VMA has been disabled by a preprocessor macro."); + } +#endif +#if !(VMA_KHR_MAINTENANCE4) + if(m_UseKhrMaintenance4) + { + VMA_ASSERT(0 && "VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE4_BIT is set but required extension is not available in your Vulkan header or its support in VMA has been disabled by a preprocessor macro."); + } +#endif +#if !(VMA_KHR_MAINTENANCE5) + if(m_UseKhrMaintenance5) + { + VMA_ASSERT(0 && "VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE5_BIT is set but required extension is not available in your Vulkan header or its support in VMA has been disabled by a preprocessor macro."); + } +#endif +#if !(VMA_KHR_MAINTENANCE5) + if(m_UseKhrMaintenance5) + { + VMA_ASSERT(0 && "VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE5_BIT is set but required extension is not available in your Vulkan header or its support in VMA has been disabled by a preprocessor macro."); + } +#endif + +#if !(VMA_EXTERNAL_MEMORY_WIN32) + if(m_UseKhrExternalMemoryWin32) + { + VMA_ASSERT(0 && "VMA_ALLOCATOR_CREATE_KHR_EXTERNAL_MEMORY_WIN32_BIT is set but required extension is not available in your Vulkan header or its support in VMA has been disabled by a preprocessor macro."); + } +#endif + + memset(&m_DeviceMemoryCallbacks, 0 ,sizeof(m_DeviceMemoryCallbacks)); + memset(&m_PhysicalDeviceProperties, 0, sizeof(m_PhysicalDeviceProperties)); + memset(&m_MemProps, 0, sizeof(m_MemProps)); + + memset(&m_pBlockVectors, 0, sizeof(m_pBlockVectors)); + memset(&m_VulkanFunctions, 0, sizeof(m_VulkanFunctions)); + +#if VMA_EXTERNAL_MEMORY + memset(&m_TypeExternalMemoryHandleTypes, 0, sizeof(m_TypeExternalMemoryHandleTypes)); +#endif // #if VMA_EXTERNAL_MEMORY + + if(pCreateInfo->pDeviceMemoryCallbacks != VMA_NULL) + { + m_DeviceMemoryCallbacks.pUserData = pCreateInfo->pDeviceMemoryCallbacks->pUserData; + m_DeviceMemoryCallbacks.pfnAllocate = pCreateInfo->pDeviceMemoryCallbacks->pfnAllocate; + m_DeviceMemoryCallbacks.pfnFree = pCreateInfo->pDeviceMemoryCallbacks->pfnFree; + } + + ImportVulkanFunctions(pCreateInfo->pVulkanFunctions); + + (*m_VulkanFunctions.vkGetPhysicalDeviceProperties)(m_PhysicalDevice, &m_PhysicalDeviceProperties); + (*m_VulkanFunctions.vkGetPhysicalDeviceMemoryProperties)(m_PhysicalDevice, &m_MemProps); + + VMA_ASSERT(VmaIsPow2(VMA_MIN_ALIGNMENT)); + VMA_ASSERT(VmaIsPow2(VMA_DEBUG_MIN_BUFFER_IMAGE_GRANULARITY)); + VMA_ASSERT(VmaIsPow2(m_PhysicalDeviceProperties.limits.bufferImageGranularity)); + VMA_ASSERT(VmaIsPow2(m_PhysicalDeviceProperties.limits.nonCoherentAtomSize)); + + m_PreferredLargeHeapBlockSize = (pCreateInfo->preferredLargeHeapBlockSize != 0) ? + pCreateInfo->preferredLargeHeapBlockSize : static_cast(VMA_DEFAULT_LARGE_HEAP_BLOCK_SIZE); + + m_GlobalMemoryTypeBits = CalculateGlobalMemoryTypeBits(); + +#if VMA_EXTERNAL_MEMORY + if(pCreateInfo->pTypeExternalMemoryHandleTypes != VMA_NULL) + { + memcpy(m_TypeExternalMemoryHandleTypes, pCreateInfo->pTypeExternalMemoryHandleTypes, + sizeof(VkExternalMemoryHandleTypeFlagsKHR) * GetMemoryTypeCount()); + } +#endif // #if VMA_EXTERNAL_MEMORY + + if(pCreateInfo->pHeapSizeLimit != VMA_NULL) + { + for(uint32_t heapIndex = 0; heapIndex < GetMemoryHeapCount(); ++heapIndex) + { + const VkDeviceSize limit = pCreateInfo->pHeapSizeLimit[heapIndex]; + if(limit != VK_WHOLE_SIZE) + { + m_HeapSizeLimitMask |= 1U << heapIndex; + if(limit < m_MemProps.memoryHeaps[heapIndex].size) + { + m_MemProps.memoryHeaps[heapIndex].size = limit; + } + } + } + } + + for(uint32_t memTypeIndex = 0; memTypeIndex < GetMemoryTypeCount(); ++memTypeIndex) + { + // Create only supported types + if((m_GlobalMemoryTypeBits & (1U << memTypeIndex)) != 0) + { + const VkDeviceSize preferredBlockSize = CalcPreferredBlockSize(memTypeIndex); + m_pBlockVectors[memTypeIndex] = vma_new(this, VmaBlockVector)( + this, + VK_NULL_HANDLE, // hParentPool + memTypeIndex, + preferredBlockSize, + 0, + SIZE_MAX, + GetBufferImageGranularity(), + false, // explicitBlockSize + 0, // algorithm + 0.5F, // priority (0.5 is the default per Vulkan spec) + GetMemoryTypeMinAlignment(memTypeIndex), // minAllocationAlignment + VMA_NULL); // // pMemoryAllocateNext + // No need to call m_pBlockVectors[memTypeIndex][blockVectorTypeIndex]->CreateMinBlocks here, + // because minBlockCount is 0. + } + } +} + +VkResult VmaAllocator_T::Init(const VmaAllocatorCreateInfo* pCreateInfo) +{ + VkResult res = VK_SUCCESS; + +#if VMA_MEMORY_BUDGET + if(m_UseExtMemoryBudget) + { + UpdateVulkanBudget(); + } +#endif // #if VMA_MEMORY_BUDGET + + return res; +} + +VmaAllocator_T::~VmaAllocator_T() +{ + VMA_ASSERT(m_Pools.IsEmpty()); + + for(size_t memTypeIndex = GetMemoryTypeCount(); memTypeIndex--; ) + { + vma_delete(this, m_pBlockVectors[memTypeIndex]); + } +} + +void VmaAllocator_T::ImportVulkanFunctions(const VmaVulkanFunctions* pVulkanFunctions) +{ +#if VMA_STATIC_VULKAN_FUNCTIONS == 1 + ImportVulkanFunctions_Static(); +#endif + + if(pVulkanFunctions != VMA_NULL) + { + ImportVulkanFunctions_Custom(pVulkanFunctions); + } + +#if VMA_DYNAMIC_VULKAN_FUNCTIONS == 1 + ImportVulkanFunctions_Dynamic(); +#endif + + ValidateVulkanFunctions(); +} + +#if VMA_STATIC_VULKAN_FUNCTIONS == 1 + +void VmaAllocator_T::ImportVulkanFunctions_Static() +{ + // Vulkan 1.0 + m_VulkanFunctions.vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)vkGetInstanceProcAddr; + m_VulkanFunctions.vkGetDeviceProcAddr = (PFN_vkGetDeviceProcAddr)vkGetDeviceProcAddr; + m_VulkanFunctions.vkGetPhysicalDeviceProperties = (PFN_vkGetPhysicalDeviceProperties)vkGetPhysicalDeviceProperties; + m_VulkanFunctions.vkGetPhysicalDeviceMemoryProperties = (PFN_vkGetPhysicalDeviceMemoryProperties)vkGetPhysicalDeviceMemoryProperties; + m_VulkanFunctions.vkAllocateMemory = (PFN_vkAllocateMemory)vkAllocateMemory; + m_VulkanFunctions.vkFreeMemory = (PFN_vkFreeMemory)vkFreeMemory; + m_VulkanFunctions.vkMapMemory = (PFN_vkMapMemory)vkMapMemory; + m_VulkanFunctions.vkUnmapMemory = (PFN_vkUnmapMemory)vkUnmapMemory; + m_VulkanFunctions.vkFlushMappedMemoryRanges = (PFN_vkFlushMappedMemoryRanges)vkFlushMappedMemoryRanges; + m_VulkanFunctions.vkInvalidateMappedMemoryRanges = (PFN_vkInvalidateMappedMemoryRanges)vkInvalidateMappedMemoryRanges; + m_VulkanFunctions.vkBindBufferMemory = (PFN_vkBindBufferMemory)vkBindBufferMemory; + m_VulkanFunctions.vkBindImageMemory = (PFN_vkBindImageMemory)vkBindImageMemory; + m_VulkanFunctions.vkGetBufferMemoryRequirements = (PFN_vkGetBufferMemoryRequirements)vkGetBufferMemoryRequirements; + m_VulkanFunctions.vkGetImageMemoryRequirements = (PFN_vkGetImageMemoryRequirements)vkGetImageMemoryRequirements; + m_VulkanFunctions.vkCreateBuffer = (PFN_vkCreateBuffer)vkCreateBuffer; + m_VulkanFunctions.vkDestroyBuffer = (PFN_vkDestroyBuffer)vkDestroyBuffer; + m_VulkanFunctions.vkCreateImage = (PFN_vkCreateImage)vkCreateImage; + m_VulkanFunctions.vkDestroyImage = (PFN_vkDestroyImage)vkDestroyImage; + m_VulkanFunctions.vkCmdCopyBuffer = (PFN_vkCmdCopyBuffer)vkCmdCopyBuffer; + + // Vulkan 1.1 +#if VMA_VULKAN_VERSION >= 1001000 + if(m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) + { + m_VulkanFunctions.vkGetBufferMemoryRequirements2KHR = (PFN_vkGetBufferMemoryRequirements2)vkGetBufferMemoryRequirements2; + m_VulkanFunctions.vkGetImageMemoryRequirements2KHR = (PFN_vkGetImageMemoryRequirements2)vkGetImageMemoryRequirements2; + m_VulkanFunctions.vkBindBufferMemory2KHR = (PFN_vkBindBufferMemory2)vkBindBufferMemory2; + m_VulkanFunctions.vkBindImageMemory2KHR = (PFN_vkBindImageMemory2)vkBindImageMemory2; + } +#endif + +#if VMA_VULKAN_VERSION >= 1001000 + if(m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) + { + m_VulkanFunctions.vkGetPhysicalDeviceMemoryProperties2KHR = (PFN_vkGetPhysicalDeviceMemoryProperties2)vkGetPhysicalDeviceMemoryProperties2; + } +#endif + +#if VMA_VULKAN_VERSION >= 1003000 + if(m_VulkanApiVersion >= VK_MAKE_VERSION(1, 3, 0)) + { + m_VulkanFunctions.vkGetDeviceBufferMemoryRequirements = (PFN_vkGetDeviceBufferMemoryRequirements)vkGetDeviceBufferMemoryRequirements; + m_VulkanFunctions.vkGetDeviceImageMemoryRequirements = (PFN_vkGetDeviceImageMemoryRequirements)vkGetDeviceImageMemoryRequirements; + } +#endif +} + +#endif // VMA_STATIC_VULKAN_FUNCTIONS == 1 + +void VmaAllocator_T::ImportVulkanFunctions_Custom(const VmaVulkanFunctions* pVulkanFunctions) +{ + VMA_ASSERT(pVulkanFunctions != VMA_NULL); + +#define VMA_COPY_IF_NOT_NULL(funcName) \ + if(pVulkanFunctions->funcName != VMA_NULL) m_VulkanFunctions.funcName = pVulkanFunctions->funcName; + + VMA_COPY_IF_NOT_NULL(vkGetInstanceProcAddr); + VMA_COPY_IF_NOT_NULL(vkGetDeviceProcAddr); + VMA_COPY_IF_NOT_NULL(vkGetPhysicalDeviceProperties); + VMA_COPY_IF_NOT_NULL(vkGetPhysicalDeviceMemoryProperties); + VMA_COPY_IF_NOT_NULL(vkAllocateMemory); + VMA_COPY_IF_NOT_NULL(vkFreeMemory); + VMA_COPY_IF_NOT_NULL(vkMapMemory); + VMA_COPY_IF_NOT_NULL(vkUnmapMemory); + VMA_COPY_IF_NOT_NULL(vkFlushMappedMemoryRanges); + VMA_COPY_IF_NOT_NULL(vkInvalidateMappedMemoryRanges); + VMA_COPY_IF_NOT_NULL(vkBindBufferMemory); + VMA_COPY_IF_NOT_NULL(vkBindImageMemory); + VMA_COPY_IF_NOT_NULL(vkGetBufferMemoryRequirements); + VMA_COPY_IF_NOT_NULL(vkGetImageMemoryRequirements); + VMA_COPY_IF_NOT_NULL(vkCreateBuffer); + VMA_COPY_IF_NOT_NULL(vkDestroyBuffer); + VMA_COPY_IF_NOT_NULL(vkCreateImage); + VMA_COPY_IF_NOT_NULL(vkDestroyImage); + VMA_COPY_IF_NOT_NULL(vkCmdCopyBuffer); + +#if VMA_DEDICATED_ALLOCATION || VMA_VULKAN_VERSION >= 1001000 + VMA_COPY_IF_NOT_NULL(vkGetBufferMemoryRequirements2KHR); + VMA_COPY_IF_NOT_NULL(vkGetImageMemoryRequirements2KHR); +#endif + +#if VMA_BIND_MEMORY2 || VMA_VULKAN_VERSION >= 1001000 + VMA_COPY_IF_NOT_NULL(vkBindBufferMemory2KHR); + VMA_COPY_IF_NOT_NULL(vkBindImageMemory2KHR); +#endif + +#if VMA_MEMORY_BUDGET || VMA_VULKAN_VERSION >= 1001000 + VMA_COPY_IF_NOT_NULL(vkGetPhysicalDeviceMemoryProperties2KHR); +#endif + +#if VMA_KHR_MAINTENANCE4 || VMA_VULKAN_VERSION >= 1003000 + VMA_COPY_IF_NOT_NULL(vkGetDeviceBufferMemoryRequirements); + VMA_COPY_IF_NOT_NULL(vkGetDeviceImageMemoryRequirements); +#endif +#if VMA_EXTERNAL_MEMORY_WIN32 + VMA_COPY_IF_NOT_NULL(vkGetMemoryWin32HandleKHR); +#endif +#undef VMA_COPY_IF_NOT_NULL +} + +#if VMA_DYNAMIC_VULKAN_FUNCTIONS == 1 + +void VmaAllocator_T::ImportVulkanFunctions_Dynamic() +{ + VMA_ASSERT(m_VulkanFunctions.vkGetInstanceProcAddr && m_VulkanFunctions.vkGetDeviceProcAddr && + "To use VMA_DYNAMIC_VULKAN_FUNCTIONS in new versions of VMA you now have to pass " + "VmaVulkanFunctions::vkGetInstanceProcAddr and vkGetDeviceProcAddr as VmaAllocatorCreateInfo::pVulkanFunctions. " + "Other members can be null."); + +#define VMA_FETCH_INSTANCE_FUNC(memberName, functionPointerType, functionNameString) \ + if(m_VulkanFunctions.memberName == VMA_NULL) \ + m_VulkanFunctions.memberName = \ + (functionPointerType)m_VulkanFunctions.vkGetInstanceProcAddr(m_hInstance, functionNameString); +#define VMA_FETCH_DEVICE_FUNC(memberName, functionPointerType, functionNameString) \ + if(m_VulkanFunctions.memberName == VMA_NULL) \ + m_VulkanFunctions.memberName = \ + (functionPointerType)m_VulkanFunctions.vkGetDeviceProcAddr(m_hDevice, functionNameString); + + VMA_FETCH_INSTANCE_FUNC(vkGetPhysicalDeviceProperties, PFN_vkGetPhysicalDeviceProperties, "vkGetPhysicalDeviceProperties"); + VMA_FETCH_INSTANCE_FUNC(vkGetPhysicalDeviceMemoryProperties, PFN_vkGetPhysicalDeviceMemoryProperties, "vkGetPhysicalDeviceMemoryProperties"); + VMA_FETCH_DEVICE_FUNC(vkAllocateMemory, PFN_vkAllocateMemory, "vkAllocateMemory"); + VMA_FETCH_DEVICE_FUNC(vkFreeMemory, PFN_vkFreeMemory, "vkFreeMemory"); + VMA_FETCH_DEVICE_FUNC(vkMapMemory, PFN_vkMapMemory, "vkMapMemory"); + VMA_FETCH_DEVICE_FUNC(vkUnmapMemory, PFN_vkUnmapMemory, "vkUnmapMemory"); + VMA_FETCH_DEVICE_FUNC(vkFlushMappedMemoryRanges, PFN_vkFlushMappedMemoryRanges, "vkFlushMappedMemoryRanges"); + VMA_FETCH_DEVICE_FUNC(vkInvalidateMappedMemoryRanges, PFN_vkInvalidateMappedMemoryRanges, "vkInvalidateMappedMemoryRanges"); + VMA_FETCH_DEVICE_FUNC(vkBindBufferMemory, PFN_vkBindBufferMemory, "vkBindBufferMemory"); + VMA_FETCH_DEVICE_FUNC(vkBindImageMemory, PFN_vkBindImageMemory, "vkBindImageMemory"); + VMA_FETCH_DEVICE_FUNC(vkGetBufferMemoryRequirements, PFN_vkGetBufferMemoryRequirements, "vkGetBufferMemoryRequirements"); + VMA_FETCH_DEVICE_FUNC(vkGetImageMemoryRequirements, PFN_vkGetImageMemoryRequirements, "vkGetImageMemoryRequirements"); + VMA_FETCH_DEVICE_FUNC(vkCreateBuffer, PFN_vkCreateBuffer, "vkCreateBuffer"); + VMA_FETCH_DEVICE_FUNC(vkDestroyBuffer, PFN_vkDestroyBuffer, "vkDestroyBuffer"); + VMA_FETCH_DEVICE_FUNC(vkCreateImage, PFN_vkCreateImage, "vkCreateImage"); + VMA_FETCH_DEVICE_FUNC(vkDestroyImage, PFN_vkDestroyImage, "vkDestroyImage"); + VMA_FETCH_DEVICE_FUNC(vkCmdCopyBuffer, PFN_vkCmdCopyBuffer, "vkCmdCopyBuffer"); + +#if VMA_VULKAN_VERSION >= 1001000 + if(m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) + { + VMA_FETCH_DEVICE_FUNC(vkGetBufferMemoryRequirements2KHR, PFN_vkGetBufferMemoryRequirements2, "vkGetBufferMemoryRequirements2"); + VMA_FETCH_DEVICE_FUNC(vkGetImageMemoryRequirements2KHR, PFN_vkGetImageMemoryRequirements2, "vkGetImageMemoryRequirements2"); + VMA_FETCH_DEVICE_FUNC(vkBindBufferMemory2KHR, PFN_vkBindBufferMemory2, "vkBindBufferMemory2"); + VMA_FETCH_DEVICE_FUNC(vkBindImageMemory2KHR, PFN_vkBindImageMemory2, "vkBindImageMemory2"); + } +#endif + +#if VMA_MEMORY_BUDGET || VMA_VULKAN_VERSION >= 1001000 + if(m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) + { + VMA_FETCH_INSTANCE_FUNC(vkGetPhysicalDeviceMemoryProperties2KHR, PFN_vkGetPhysicalDeviceMemoryProperties2KHR, "vkGetPhysicalDeviceMemoryProperties2"); + // Try to fetch the pointer from the other name, based on suspected driver bug - see issue #410. + VMA_FETCH_INSTANCE_FUNC(vkGetPhysicalDeviceMemoryProperties2KHR, PFN_vkGetPhysicalDeviceMemoryProperties2KHR, "vkGetPhysicalDeviceMemoryProperties2KHR"); + } + else if(m_UseExtMemoryBudget) + { + VMA_FETCH_INSTANCE_FUNC(vkGetPhysicalDeviceMemoryProperties2KHR, PFN_vkGetPhysicalDeviceMemoryProperties2KHR, "vkGetPhysicalDeviceMemoryProperties2KHR"); + // Try to fetch the pointer from the other name, based on suspected driver bug - see issue #410. + VMA_FETCH_INSTANCE_FUNC(vkGetPhysicalDeviceMemoryProperties2KHR, PFN_vkGetPhysicalDeviceMemoryProperties2KHR, "vkGetPhysicalDeviceMemoryProperties2"); + } +#endif + +#if VMA_DEDICATED_ALLOCATION + if(m_UseKhrDedicatedAllocation) + { + VMA_FETCH_DEVICE_FUNC(vkGetBufferMemoryRequirements2KHR, PFN_vkGetBufferMemoryRequirements2KHR, "vkGetBufferMemoryRequirements2KHR"); + VMA_FETCH_DEVICE_FUNC(vkGetImageMemoryRequirements2KHR, PFN_vkGetImageMemoryRequirements2KHR, "vkGetImageMemoryRequirements2KHR"); + } +#endif + +#if VMA_BIND_MEMORY2 + if(m_UseKhrBindMemory2) + { + VMA_FETCH_DEVICE_FUNC(vkBindBufferMemory2KHR, PFN_vkBindBufferMemory2KHR, "vkBindBufferMemory2KHR"); + VMA_FETCH_DEVICE_FUNC(vkBindImageMemory2KHR, PFN_vkBindImageMemory2KHR, "vkBindImageMemory2KHR"); + } +#endif // #if VMA_BIND_MEMORY2 + +#if VMA_MEMORY_BUDGET || VMA_VULKAN_VERSION >= 1001000 + if(m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) + { + VMA_FETCH_INSTANCE_FUNC(vkGetPhysicalDeviceMemoryProperties2KHR, PFN_vkGetPhysicalDeviceMemoryProperties2KHR, "vkGetPhysicalDeviceMemoryProperties2"); + } + else if(m_UseExtMemoryBudget) + { + VMA_FETCH_INSTANCE_FUNC(vkGetPhysicalDeviceMemoryProperties2KHR, PFN_vkGetPhysicalDeviceMemoryProperties2KHR, "vkGetPhysicalDeviceMemoryProperties2KHR"); + } +#endif // #if VMA_MEMORY_BUDGET + +#if VMA_VULKAN_VERSION >= 1003000 + if(m_VulkanApiVersion >= VK_MAKE_VERSION(1, 3, 0)) + { + VMA_FETCH_DEVICE_FUNC(vkGetDeviceBufferMemoryRequirements, PFN_vkGetDeviceBufferMemoryRequirements, "vkGetDeviceBufferMemoryRequirements"); + VMA_FETCH_DEVICE_FUNC(vkGetDeviceImageMemoryRequirements, PFN_vkGetDeviceImageMemoryRequirements, "vkGetDeviceImageMemoryRequirements"); + } +#endif +#if VMA_KHR_MAINTENANCE4 + if(m_UseKhrMaintenance4) + { + VMA_FETCH_DEVICE_FUNC(vkGetDeviceBufferMemoryRequirements, PFN_vkGetDeviceBufferMemoryRequirementsKHR, "vkGetDeviceBufferMemoryRequirementsKHR"); + VMA_FETCH_DEVICE_FUNC(vkGetDeviceImageMemoryRequirements, PFN_vkGetDeviceImageMemoryRequirementsKHR, "vkGetDeviceImageMemoryRequirementsKHR"); + } +#endif +#if VMA_EXTERNAL_MEMORY_WIN32 + if (m_UseKhrExternalMemoryWin32) + { + VMA_FETCH_DEVICE_FUNC(vkGetMemoryWin32HandleKHR, PFN_vkGetMemoryWin32HandleKHR, "vkGetMemoryWin32HandleKHR"); + } +#endif +#undef VMA_FETCH_DEVICE_FUNC +#undef VMA_FETCH_INSTANCE_FUNC +} + +#endif // VMA_DYNAMIC_VULKAN_FUNCTIONS == 1 + +void VmaAllocator_T::ValidateVulkanFunctions() const +{ + VMA_ASSERT(m_VulkanFunctions.vkGetPhysicalDeviceProperties != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkGetPhysicalDeviceMemoryProperties != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkAllocateMemory != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkFreeMemory != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkMapMemory != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkUnmapMemory != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkFlushMappedMemoryRanges != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkInvalidateMappedMemoryRanges != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkBindBufferMemory != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkBindImageMemory != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkGetBufferMemoryRequirements != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkGetImageMemoryRequirements != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkCreateBuffer != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkDestroyBuffer != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkCreateImage != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkDestroyImage != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkCmdCopyBuffer != VMA_NULL); + +#if VMA_DEDICATED_ALLOCATION || VMA_VULKAN_VERSION >= 1001000 + if(m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0) || m_UseKhrDedicatedAllocation) + { + VMA_ASSERT(m_VulkanFunctions.vkGetBufferMemoryRequirements2KHR != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkGetImageMemoryRequirements2KHR != VMA_NULL); + } +#endif + +#if VMA_BIND_MEMORY2 || VMA_VULKAN_VERSION >= 1001000 + if(m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0) || m_UseKhrBindMemory2) + { + VMA_ASSERT(m_VulkanFunctions.vkBindBufferMemory2KHR != VMA_NULL); + VMA_ASSERT(m_VulkanFunctions.vkBindImageMemory2KHR != VMA_NULL); + } +#endif + +#if VMA_MEMORY_BUDGET || VMA_VULKAN_VERSION >= 1001000 + if(m_UseExtMemoryBudget || m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) + { + VMA_ASSERT(m_VulkanFunctions.vkGetPhysicalDeviceMemoryProperties2KHR != VMA_NULL); + } +#endif +#if VMA_EXTERNAL_MEMORY_WIN32 + if (m_UseKhrExternalMemoryWin32) + { + VMA_ASSERT(m_VulkanFunctions.vkGetMemoryWin32HandleKHR != VMA_NULL); + } +#endif + + // Not validating these due to suspected driver bugs with these function + // pointers being null despite correct extension or Vulkan version is enabled. + // See issue #397. Their usage in VMA is optional anyway. + // + // VMA_ASSERT(m_VulkanFunctions.vkGetDeviceBufferMemoryRequirements != VMA_NULL); + // VMA_ASSERT(m_VulkanFunctions.vkGetDeviceImageMemoryRequirements != VMA_NULL); +} + +VkDeviceSize VmaAllocator_T::CalcPreferredBlockSize(uint32_t memTypeIndex) +{ + const uint32_t heapIndex = MemoryTypeIndexToHeapIndex(memTypeIndex); + const VkDeviceSize heapSize = m_MemProps.memoryHeaps[heapIndex].size; + const bool isSmallHeap = heapSize <= VMA_SMALL_HEAP_MAX_SIZE; + return VmaAlignUp(isSmallHeap ? (heapSize / 8) : m_PreferredLargeHeapBlockSize, (VkDeviceSize)32); +} + +VkResult VmaAllocator_T::AllocateMemoryOfType( + VmaPool pool, + VkDeviceSize size, + VkDeviceSize alignment, + bool dedicatedPreferred, + VkBuffer dedicatedBuffer, + VkImage dedicatedImage, + VmaBufferImageUsage dedicatedBufferImageUsage, + const VmaAllocationCreateInfo& createInfo, + uint32_t memTypeIndex, + VmaSuballocationType suballocType, + VmaDedicatedAllocationList& dedicatedAllocations, + VmaBlockVector& blockVector, + size_t allocationCount, + VmaAllocation* pAllocations) +{ + VMA_ASSERT(pAllocations != VMA_NULL); + VMA_DEBUG_LOG_FORMAT(" AllocateMemory: MemoryTypeIndex=%" PRIu32 ", AllocationCount=%zu, Size=%" PRIu64, memTypeIndex, allocationCount, size); + + VmaAllocationCreateInfo finalCreateInfo = createInfo; + VkResult res = CalcMemTypeParams( + finalCreateInfo, + memTypeIndex, + size, + allocationCount); + if(res != VK_SUCCESS) + return res; + + if((finalCreateInfo.flags & VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT) != 0) + { + return AllocateDedicatedMemory( + pool, + size, + suballocType, + dedicatedAllocations, + memTypeIndex, + (finalCreateInfo.flags & VMA_ALLOCATION_CREATE_MAPPED_BIT) != 0, + (finalCreateInfo.flags & VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT) != 0, + (finalCreateInfo.flags & + (VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT)) != 0, + (finalCreateInfo.flags & VMA_ALLOCATION_CREATE_CAN_ALIAS_BIT) != 0, + finalCreateInfo.pUserData, + finalCreateInfo.priority, + dedicatedBuffer, + dedicatedImage, + dedicatedBufferImageUsage, + allocationCount, + pAllocations, + blockVector.GetAllocationNextPtr()); + } + + const bool canAllocateDedicated = + (finalCreateInfo.flags & VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT) == 0 && + (pool == VK_NULL_HANDLE || !blockVector.HasExplicitBlockSize()); + + if(canAllocateDedicated) + { + // Heuristics: Allocate dedicated memory if requested size if greater than half of preferred block size. + if(size > blockVector.GetPreferredBlockSize() / 2) + { + dedicatedPreferred = true; + } + // Protection against creating each allocation as dedicated when we reach or exceed heap size/budget, + // which can quickly deplete maxMemoryAllocationCount: Don't prefer dedicated allocations when above + // 3/4 of the maximum allocation count. + if(m_PhysicalDeviceProperties.limits.maxMemoryAllocationCount < UINT32_MAX / 4 && + m_DeviceMemoryCount.load() > m_PhysicalDeviceProperties.limits.maxMemoryAllocationCount * 3 / 4) + { + dedicatedPreferred = false; + } + + if(dedicatedPreferred) + { + res = AllocateDedicatedMemory( + pool, + size, + suballocType, + dedicatedAllocations, + memTypeIndex, + (finalCreateInfo.flags & VMA_ALLOCATION_CREATE_MAPPED_BIT) != 0, + (finalCreateInfo.flags & VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT) != 0, + (finalCreateInfo.flags & + (VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT)) != 0, + (finalCreateInfo.flags & VMA_ALLOCATION_CREATE_CAN_ALIAS_BIT) != 0, + finalCreateInfo.pUserData, + finalCreateInfo.priority, + dedicatedBuffer, + dedicatedImage, + dedicatedBufferImageUsage, + allocationCount, + pAllocations, + blockVector.GetAllocationNextPtr()); + if(res == VK_SUCCESS) + { + // Succeeded: AllocateDedicatedMemory function already filled pMemory, nothing more to do here. + VMA_DEBUG_LOG(" Allocated as DedicatedMemory"); + return VK_SUCCESS; + } + } + } + + res = blockVector.Allocate( + size, + alignment, + finalCreateInfo, + suballocType, + allocationCount, + pAllocations); + if(res == VK_SUCCESS) + return VK_SUCCESS; + + // Try dedicated memory. + if(canAllocateDedicated && !dedicatedPreferred) + { + res = AllocateDedicatedMemory( + pool, + size, + suballocType, + dedicatedAllocations, + memTypeIndex, + (finalCreateInfo.flags & VMA_ALLOCATION_CREATE_MAPPED_BIT) != 0, + (finalCreateInfo.flags & VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT) != 0, + (finalCreateInfo.flags & + (VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT)) != 0, + (finalCreateInfo.flags & VMA_ALLOCATION_CREATE_CAN_ALIAS_BIT) != 0, + finalCreateInfo.pUserData, + finalCreateInfo.priority, + dedicatedBuffer, + dedicatedImage, + dedicatedBufferImageUsage, + allocationCount, + pAllocations, + blockVector.GetAllocationNextPtr()); + if(res == VK_SUCCESS) + { + // Succeeded: AllocateDedicatedMemory function already filled pMemory, nothing more to do here. + VMA_DEBUG_LOG(" Allocated as DedicatedMemory"); + return VK_SUCCESS; + } + } + // Everything failed: Return error code. + VMA_DEBUG_LOG(" vkAllocateMemory FAILED"); + return res; +} + +VkResult VmaAllocator_T::AllocateDedicatedMemory( + VmaPool pool, + VkDeviceSize size, + VmaSuballocationType suballocType, + VmaDedicatedAllocationList& dedicatedAllocations, + uint32_t memTypeIndex, + bool map, + bool isUserDataString, + bool isMappingAllowed, + bool canAliasMemory, + void* pUserData, + float priority, + VkBuffer dedicatedBuffer, + VkImage dedicatedImage, + VmaBufferImageUsage dedicatedBufferImageUsage, + size_t allocationCount, + VmaAllocation* pAllocations, + const void* pNextChain) +{ + VMA_ASSERT(allocationCount > 0 && pAllocations); + + VkMemoryAllocateInfo allocInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + allocInfo.memoryTypeIndex = memTypeIndex; + allocInfo.allocationSize = size; + allocInfo.pNext = pNextChain; + +#if VMA_DEDICATED_ALLOCATION || VMA_VULKAN_VERSION >= 1001000 + VkMemoryDedicatedAllocateInfoKHR dedicatedAllocInfo = { VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO_KHR }; + if(!canAliasMemory) + { + if(m_UseKhrDedicatedAllocation || m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) + { + if(dedicatedBuffer != VK_NULL_HANDLE) + { + VMA_ASSERT(dedicatedImage == VK_NULL_HANDLE); + dedicatedAllocInfo.buffer = dedicatedBuffer; + VmaPnextChainPushFront(&allocInfo, &dedicatedAllocInfo); + } + else if(dedicatedImage != VK_NULL_HANDLE) + { + dedicatedAllocInfo.image = dedicatedImage; + VmaPnextChainPushFront(&allocInfo, &dedicatedAllocInfo); + } + } + } +#endif // #if VMA_DEDICATED_ALLOCATION || VMA_VULKAN_VERSION >= 1001000 + +#if VMA_BUFFER_DEVICE_ADDRESS + VkMemoryAllocateFlagsInfoKHR allocFlagsInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO_KHR }; + if(m_UseKhrBufferDeviceAddress) + { + bool canContainBufferWithDeviceAddress = true; + if(dedicatedBuffer != VK_NULL_HANDLE) + { + canContainBufferWithDeviceAddress = dedicatedBufferImageUsage == VmaBufferImageUsage::UNKNOWN || + dedicatedBufferImageUsage.Contains(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT_EXT); + } + else if(dedicatedImage != VK_NULL_HANDLE) + { + canContainBufferWithDeviceAddress = false; + } + if(canContainBufferWithDeviceAddress) + { + allocFlagsInfo.flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT_KHR; + VmaPnextChainPushFront(&allocInfo, &allocFlagsInfo); + } + } +#endif // #if VMA_BUFFER_DEVICE_ADDRESS + +#if VMA_MEMORY_PRIORITY + VkMemoryPriorityAllocateInfoEXT priorityInfo = { VK_STRUCTURE_TYPE_MEMORY_PRIORITY_ALLOCATE_INFO_EXT }; + if(m_UseExtMemoryPriority) + { + VMA_ASSERT(priority >= 0.F && priority <= 1.F); + priorityInfo.priority = priority; + VmaPnextChainPushFront(&allocInfo, &priorityInfo); + } +#endif // #if VMA_MEMORY_PRIORITY + +#if VMA_EXTERNAL_MEMORY + // Attach VkExportMemoryAllocateInfoKHR if necessary. + VkExportMemoryAllocateInfoKHR exportMemoryAllocInfo = { VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO_KHR }; + exportMemoryAllocInfo.handleTypes = GetExternalMemoryHandleTypeFlags(memTypeIndex); + if(exportMemoryAllocInfo.handleTypes != 0) + { + VmaPnextChainPushFront(&allocInfo, &exportMemoryAllocInfo); + } +#endif // #if VMA_EXTERNAL_MEMORY + + size_t allocIndex = 0; + VkResult res = VK_SUCCESS; + for(; allocIndex < allocationCount; ++allocIndex) + { + res = AllocateDedicatedMemoryPage( + pool, + size, + suballocType, + memTypeIndex, + allocInfo, + map, + isUserDataString, + isMappingAllowed, + pUserData, + pAllocations + allocIndex); + if(res != VK_SUCCESS) + { + break; + } + } + + if(res == VK_SUCCESS) + { + for (allocIndex = 0; allocIndex < allocationCount; ++allocIndex) + { + dedicatedAllocations.Register(pAllocations[allocIndex]); + } + VMA_DEBUG_LOG_FORMAT(" Allocated DedicatedMemory Count=%zu, MemoryTypeIndex=#%" PRIu32, allocationCount, memTypeIndex); + } + else + { + // Free all already created allocations. + while(allocIndex--) + { + VmaAllocation currAlloc = pAllocations[allocIndex]; + VkDeviceMemory hMemory = currAlloc->GetMemory(); + + /* + There is no need to call this, because Vulkan spec allows to skip vkUnmapMemory + before vkFreeMemory. + + if(currAlloc->GetMappedData() != VMA_NULL) + { + (*m_VulkanFunctions.vkUnmapMemory)(m_hDevice, hMemory); + } + */ + + FreeVulkanMemory(memTypeIndex, currAlloc->GetSize(), hMemory); + m_Budget.RemoveAllocation(MemoryTypeIndexToHeapIndex(memTypeIndex), currAlloc->GetSize()); + m_AllocationObjectAllocator.Free(currAlloc); + } + + memset(pAllocations, 0, sizeof(VmaAllocation) * allocationCount); + } + + return res; +} + +VkResult VmaAllocator_T::AllocateDedicatedMemoryPage( + VmaPool pool, + VkDeviceSize size, + VmaSuballocationType suballocType, + uint32_t memTypeIndex, + const VkMemoryAllocateInfo& allocInfo, + bool map, + bool isUserDataString, + bool isMappingAllowed, + void* pUserData, + VmaAllocation* pAllocation) +{ + VkDeviceMemory hMemory = VK_NULL_HANDLE; + VkResult res = AllocateVulkanMemory(&allocInfo, &hMemory); + if(res < 0) + { + VMA_DEBUG_LOG(" vkAllocateMemory FAILED"); + return res; + } + + void* pMappedData = VMA_NULL; + if(map) + { + res = (*m_VulkanFunctions.vkMapMemory)( + m_hDevice, + hMemory, + 0, + VK_WHOLE_SIZE, + 0, + &pMappedData); + if(res < 0) + { + VMA_DEBUG_LOG(" vkMapMemory FAILED"); + FreeVulkanMemory(memTypeIndex, size, hMemory); + return res; + } + } + + *pAllocation = m_AllocationObjectAllocator.Allocate(isMappingAllowed); + (*pAllocation)->InitDedicatedAllocation(this, pool, memTypeIndex, hMemory, suballocType, pMappedData, size); + if (isUserDataString) + (*pAllocation)->SetName(this, (const char*)pUserData); + else + (*pAllocation)->SetUserData(this, pUserData); + m_Budget.AddAllocation(MemoryTypeIndexToHeapIndex(memTypeIndex), size); + if(VMA_DEBUG_INITIALIZE_ALLOCATIONS) + { + FillAllocation(*pAllocation, VMA_ALLOCATION_FILL_PATTERN_CREATED); + } + + return VK_SUCCESS; +} + +void VmaAllocator_T::GetBufferMemoryRequirements( + VkBuffer hBuffer, + VkMemoryRequirements& memReq, + bool& requiresDedicatedAllocation, + bool& prefersDedicatedAllocation) const +{ +#if VMA_DEDICATED_ALLOCATION || VMA_VULKAN_VERSION >= 1001000 + if(m_UseKhrDedicatedAllocation || m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) + { + VkBufferMemoryRequirementsInfo2KHR memReqInfo = { VK_STRUCTURE_TYPE_BUFFER_MEMORY_REQUIREMENTS_INFO_2_KHR }; + memReqInfo.buffer = hBuffer; + + VkMemoryDedicatedRequirementsKHR memDedicatedReq = { VK_STRUCTURE_TYPE_MEMORY_DEDICATED_REQUIREMENTS_KHR }; + + VkMemoryRequirements2KHR memReq2 = { VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2_KHR }; + VmaPnextChainPushFront(&memReq2, &memDedicatedReq); + + (*m_VulkanFunctions.vkGetBufferMemoryRequirements2KHR)(m_hDevice, &memReqInfo, &memReq2); + + memReq = memReq2.memoryRequirements; + requiresDedicatedAllocation = (memDedicatedReq.requiresDedicatedAllocation != VK_FALSE); + prefersDedicatedAllocation = (memDedicatedReq.prefersDedicatedAllocation != VK_FALSE); + } + else +#endif // #if VMA_DEDICATED_ALLOCATION || VMA_VULKAN_VERSION >= 1001000 + { + (*m_VulkanFunctions.vkGetBufferMemoryRequirements)(m_hDevice, hBuffer, &memReq); + requiresDedicatedAllocation = false; + prefersDedicatedAllocation = false; + } +} + +void VmaAllocator_T::GetImageMemoryRequirements( + VkImage hImage, + VkMemoryRequirements& memReq, + bool& requiresDedicatedAllocation, + bool& prefersDedicatedAllocation) const +{ +#if VMA_DEDICATED_ALLOCATION || VMA_VULKAN_VERSION >= 1001000 + if(m_UseKhrDedicatedAllocation || m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) + { + VkImageMemoryRequirementsInfo2KHR memReqInfo = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_REQUIREMENTS_INFO_2_KHR }; + memReqInfo.image = hImage; + + VkMemoryDedicatedRequirementsKHR memDedicatedReq = { VK_STRUCTURE_TYPE_MEMORY_DEDICATED_REQUIREMENTS_KHR }; + + VkMemoryRequirements2KHR memReq2 = { VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2_KHR }; + VmaPnextChainPushFront(&memReq2, &memDedicatedReq); + + (*m_VulkanFunctions.vkGetImageMemoryRequirements2KHR)(m_hDevice, &memReqInfo, &memReq2); + + memReq = memReq2.memoryRequirements; + requiresDedicatedAllocation = (memDedicatedReq.requiresDedicatedAllocation != VK_FALSE); + prefersDedicatedAllocation = (memDedicatedReq.prefersDedicatedAllocation != VK_FALSE); + } + else +#endif // #if VMA_DEDICATED_ALLOCATION || VMA_VULKAN_VERSION >= 1001000 + { + (*m_VulkanFunctions.vkGetImageMemoryRequirements)(m_hDevice, hImage, &memReq); + requiresDedicatedAllocation = false; + prefersDedicatedAllocation = false; + } +} + +VkResult VmaAllocator_T::FindMemoryTypeIndex( + uint32_t memoryTypeBits, + const VmaAllocationCreateInfo* pAllocationCreateInfo, + VmaBufferImageUsage bufImgUsage, + uint32_t* pMemoryTypeIndex) const +{ + memoryTypeBits &= GetGlobalMemoryTypeBits(); + + if(pAllocationCreateInfo->memoryTypeBits != 0) + { + memoryTypeBits &= pAllocationCreateInfo->memoryTypeBits; + } + + VkMemoryPropertyFlags requiredFlags = 0; + VkMemoryPropertyFlags preferredFlags = 0; + VkMemoryPropertyFlags notPreferredFlags = 0; + if(!FindMemoryPreferences( + IsIntegratedGpu(), + *pAllocationCreateInfo, + bufImgUsage, + requiredFlags, preferredFlags, notPreferredFlags)) + { + return VK_ERROR_FEATURE_NOT_PRESENT; + } + + *pMemoryTypeIndex = UINT32_MAX; + uint32_t minCost = UINT32_MAX; + for(uint32_t memTypeIndex = 0, memTypeBit = 1; + memTypeIndex < GetMemoryTypeCount(); + ++memTypeIndex, memTypeBit <<= 1) + { + // This memory type is acceptable according to memoryTypeBits bitmask. + if((memTypeBit & memoryTypeBits) != 0) + { + const VkMemoryPropertyFlags currFlags = + m_MemProps.memoryTypes[memTypeIndex].propertyFlags; + // This memory type contains requiredFlags. + if((requiredFlags & ~currFlags) == 0) + { + // Calculate cost as number of bits from preferredFlags not present in this memory type. + uint32_t currCost = VMA_COUNT_BITS_SET(preferredFlags & ~currFlags) + + VMA_COUNT_BITS_SET(currFlags & notPreferredFlags); + // Remember memory type with lowest cost. + if(currCost < minCost) + { + *pMemoryTypeIndex = memTypeIndex; + if(currCost == 0) + { + return VK_SUCCESS; + } + minCost = currCost; + } + } + } + } + return (*pMemoryTypeIndex != UINT32_MAX) ? VK_SUCCESS : VK_ERROR_FEATURE_NOT_PRESENT; +} + +VkResult VmaAllocator_T::CalcMemTypeParams( + VmaAllocationCreateInfo& inoutCreateInfo, + uint32_t memTypeIndex, + VkDeviceSize size, + size_t allocationCount) +{ + // If memory type is not HOST_VISIBLE, disable MAPPED. + if((inoutCreateInfo.flags & VMA_ALLOCATION_CREATE_MAPPED_BIT) != 0 && + (m_MemProps.memoryTypes[memTypeIndex].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) + { + inoutCreateInfo.flags &= ~VMA_ALLOCATION_CREATE_MAPPED_BIT; + } + + if((inoutCreateInfo.flags & VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT) != 0 && + (inoutCreateInfo.flags & VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT) != 0) + { + const uint32_t heapIndex = MemoryTypeIndexToHeapIndex(memTypeIndex); + VmaBudget heapBudget = {}; + GetHeapBudgets(&heapBudget, heapIndex, 1); + if(heapBudget.usage + size * allocationCount > heapBudget.budget) + { + return VK_ERROR_OUT_OF_DEVICE_MEMORY; + } + } + return VK_SUCCESS; +} + +VkResult VmaAllocator_T::CalcAllocationParams( + VmaAllocationCreateInfo& inoutCreateInfo, + bool dedicatedRequired) +{ + VMA_ASSERT((inoutCreateInfo.flags & + (VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT)) != + (VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT) && + "Specifying both flags VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT and VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT is incorrect."); + VMA_ASSERT((((inoutCreateInfo.flags & VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT) == 0 || + (inoutCreateInfo.flags & (VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT)) != 0)) && + "Specifying VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT requires also VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT or VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT."); + if(inoutCreateInfo.usage == VMA_MEMORY_USAGE_AUTO || inoutCreateInfo.usage == VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE || inoutCreateInfo.usage == VMA_MEMORY_USAGE_AUTO_PREFER_HOST) + { + if((inoutCreateInfo.flags & VMA_ALLOCATION_CREATE_MAPPED_BIT) != 0) + { + VMA_ASSERT((inoutCreateInfo.flags & (VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT)) != 0 && + "When using VMA_ALLOCATION_CREATE_MAPPED_BIT and usage = VMA_MEMORY_USAGE_AUTO*, you must also specify VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT or VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT."); + } + } + + // If memory is lazily allocated, it should be always dedicated. + if(dedicatedRequired || + inoutCreateInfo.usage == VMA_MEMORY_USAGE_GPU_LAZILY_ALLOCATED) + { + inoutCreateInfo.flags |= VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT; + } + + if(inoutCreateInfo.pool != VK_NULL_HANDLE) + { + if(inoutCreateInfo.pool->m_BlockVector.HasExplicitBlockSize() && + (inoutCreateInfo.flags & VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT) != 0) + { + VMA_ASSERT(0 && "Specifying VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT while current custom pool doesn't support dedicated allocations."); + return VK_ERROR_FEATURE_NOT_PRESENT; + } + inoutCreateInfo.priority = inoutCreateInfo.pool->m_BlockVector.GetPriority(); + } + + if((inoutCreateInfo.flags & VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT) != 0 && + (inoutCreateInfo.flags & VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT) != 0) + { + VMA_ASSERT(0 && "Specifying VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT together with VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT makes no sense."); + return VK_ERROR_FEATURE_NOT_PRESENT; + } + + if(VMA_DEBUG_ALWAYS_DEDICATED_MEMORY && + (inoutCreateInfo.flags & VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT) != 0) + { + inoutCreateInfo.flags |= VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT; + } + + // Non-auto USAGE values imply HOST_ACCESS flags. + // And so does VMA_MEMORY_USAGE_UNKNOWN because it is used with custom pools. + // Which specific flag is used doesn't matter. They change things only when used with VMA_MEMORY_USAGE_AUTO*. + // Otherwise they just protect from assert on mapping. + if(inoutCreateInfo.usage != VMA_MEMORY_USAGE_AUTO && + inoutCreateInfo.usage != VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE && + inoutCreateInfo.usage != VMA_MEMORY_USAGE_AUTO_PREFER_HOST) + { + if((inoutCreateInfo.flags & (VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT)) == 0) + { + inoutCreateInfo.flags |= VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT; + } + } + + return VK_SUCCESS; +} + +VkResult VmaAllocator_T::AllocateMemory( + const VkMemoryRequirements& vkMemReq, + bool requiresDedicatedAllocation, + bool prefersDedicatedAllocation, + VkBuffer dedicatedBuffer, + VkImage dedicatedImage, + VmaBufferImageUsage dedicatedBufferImageUsage, + const VmaAllocationCreateInfo& createInfo, + VmaSuballocationType suballocType, + size_t allocationCount, + VmaAllocation* pAllocations) +{ + memset(pAllocations, 0, sizeof(VmaAllocation) * allocationCount); + + VMA_ASSERT(VmaIsPow2(vkMemReq.alignment)); + + if(vkMemReq.size == 0) + { + return VK_ERROR_INITIALIZATION_FAILED; + } + + VmaAllocationCreateInfo createInfoFinal = createInfo; + VkResult res = CalcAllocationParams(createInfoFinal, requiresDedicatedAllocation); + if(res != VK_SUCCESS) + return res; + + if(createInfoFinal.pool != VK_NULL_HANDLE) + { + VmaBlockVector& blockVector = createInfoFinal.pool->m_BlockVector; + return AllocateMemoryOfType( + createInfoFinal.pool, + vkMemReq.size, + vkMemReq.alignment, + prefersDedicatedAllocation, + dedicatedBuffer, + dedicatedImage, + dedicatedBufferImageUsage, + createInfoFinal, + blockVector.GetMemoryTypeIndex(), + suballocType, + createInfoFinal.pool->m_DedicatedAllocations, + blockVector, + allocationCount, + pAllocations); + } + + // Bit mask of memory Vulkan types acceptable for this allocation. + uint32_t memoryTypeBits = vkMemReq.memoryTypeBits; + uint32_t memTypeIndex = UINT32_MAX; + res = FindMemoryTypeIndex(memoryTypeBits, &createInfoFinal, dedicatedBufferImageUsage, &memTypeIndex); + // Can't find any single memory type matching requirements. res is VK_ERROR_FEATURE_NOT_PRESENT. + if(res != VK_SUCCESS) + return res; + + do + { + VmaBlockVector* blockVector = m_pBlockVectors[memTypeIndex]; + VMA_ASSERT(blockVector && "Trying to use unsupported memory type!"); + res = AllocateMemoryOfType( + VK_NULL_HANDLE, + vkMemReq.size, + vkMemReq.alignment, + requiresDedicatedAllocation || prefersDedicatedAllocation, + dedicatedBuffer, + dedicatedImage, + dedicatedBufferImageUsage, + createInfoFinal, + memTypeIndex, + suballocType, + m_DedicatedAllocations[memTypeIndex], + *blockVector, + allocationCount, + pAllocations); + // Allocation succeeded + if(res == VK_SUCCESS) + return VK_SUCCESS; + + // Remove old memTypeIndex from list of possibilities. + memoryTypeBits &= ~(1U << memTypeIndex); + // Find alternative memTypeIndex. + res = FindMemoryTypeIndex(memoryTypeBits, &createInfoFinal, dedicatedBufferImageUsage, &memTypeIndex); + } while(res == VK_SUCCESS); + + // No other matching memory type index could be found. + // Not returning res, which is VK_ERROR_FEATURE_NOT_PRESENT, because we already failed to allocate once. + return VK_ERROR_OUT_OF_DEVICE_MEMORY; +} + +void VmaAllocator_T::FreeMemory( + size_t allocationCount, + const VmaAllocation* pAllocations) +{ + VMA_ASSERT(pAllocations); + + for(size_t allocIndex = allocationCount; allocIndex--; ) + { + VmaAllocation allocation = pAllocations[allocIndex]; + + if(allocation != VK_NULL_HANDLE) + { + if(VMA_DEBUG_INITIALIZE_ALLOCATIONS) + { + FillAllocation(allocation, VMA_ALLOCATION_FILL_PATTERN_DESTROYED); + } + + switch(allocation->GetType()) + { + case VmaAllocation_T::ALLOCATION_TYPE_BLOCK: + { + VmaBlockVector* pBlockVector = VMA_NULL; + VmaPool hPool = allocation->GetParentPool(); + if(hPool != VK_NULL_HANDLE) + { + pBlockVector = &hPool->m_BlockVector; + } + else + { + const uint32_t memTypeIndex = allocation->GetMemoryTypeIndex(); + pBlockVector = m_pBlockVectors[memTypeIndex]; + VMA_ASSERT(pBlockVector && "Trying to free memory of unsupported type!"); + } + pBlockVector->Free(allocation); + } + break; + case VmaAllocation_T::ALLOCATION_TYPE_DEDICATED: + FreeDedicatedMemory(allocation); + break; + default: + VMA_ASSERT(0); + } + } + } +} + +void VmaAllocator_T::CalculateStatistics(VmaTotalStatistics* pStats) +{ + // Initialize. + VmaClearDetailedStatistics(pStats->total); + for(uint32_t i = 0; i < VK_MAX_MEMORY_TYPES; ++i) + VmaClearDetailedStatistics(pStats->memoryType[i]); + for(uint32_t i = 0; i < VK_MAX_MEMORY_HEAPS; ++i) + VmaClearDetailedStatistics(pStats->memoryHeap[i]); + + // Process default pools. + for(uint32_t memTypeIndex = 0; memTypeIndex < GetMemoryTypeCount(); ++memTypeIndex) + { + VmaBlockVector* const pBlockVector = m_pBlockVectors[memTypeIndex]; + if (pBlockVector != VMA_NULL) + pBlockVector->AddDetailedStatistics(pStats->memoryType[memTypeIndex]); + } + + // Process custom pools. + { + VmaMutexLockRead lock(m_PoolsMutex, m_UseMutex); + for(VmaPool pool = m_Pools.Front(); pool != VMA_NULL; pool = m_Pools.GetNext(pool)) + { + VmaBlockVector& blockVector = pool->m_BlockVector; + const uint32_t memTypeIndex = blockVector.GetMemoryTypeIndex(); + blockVector.AddDetailedStatistics(pStats->memoryType[memTypeIndex]); + pool->m_DedicatedAllocations.AddDetailedStatistics(pStats->memoryType[memTypeIndex]); + } + } + + // Process dedicated allocations. + for(uint32_t memTypeIndex = 0; memTypeIndex < GetMemoryTypeCount(); ++memTypeIndex) + { + m_DedicatedAllocations[memTypeIndex].AddDetailedStatistics(pStats->memoryType[memTypeIndex]); + } + + // Sum from memory types to memory heaps. + for(uint32_t memTypeIndex = 0; memTypeIndex < GetMemoryTypeCount(); ++memTypeIndex) + { + const uint32_t memHeapIndex = m_MemProps.memoryTypes[memTypeIndex].heapIndex; + VmaAddDetailedStatistics(pStats->memoryHeap[memHeapIndex], pStats->memoryType[memTypeIndex]); + } + + // Sum from memory heaps to total. + for(uint32_t memHeapIndex = 0; memHeapIndex < GetMemoryHeapCount(); ++memHeapIndex) + VmaAddDetailedStatistics(pStats->total, pStats->memoryHeap[memHeapIndex]); + + VMA_ASSERT(pStats->total.statistics.allocationCount == 0 || + pStats->total.allocationSizeMax >= pStats->total.allocationSizeMin); + VMA_ASSERT(pStats->total.unusedRangeCount == 0 || + pStats->total.unusedRangeSizeMax >= pStats->total.unusedRangeSizeMin); +} + +void VmaAllocator_T::GetHeapBudgets(VmaBudget* outBudgets, uint32_t firstHeap, uint32_t heapCount) +{ +#if VMA_MEMORY_BUDGET + if(m_UseExtMemoryBudget) + { + if(m_Budget.m_OperationsSinceBudgetFetch < 30) + { + VmaMutexLockRead lockRead(m_Budget.m_BudgetMutex, m_UseMutex); + for(uint32_t i = 0; i < heapCount; ++i, ++outBudgets) + { + const uint32_t heapIndex = firstHeap + i; + + outBudgets->statistics.blockCount = m_Budget.m_BlockCount[heapIndex]; + outBudgets->statistics.allocationCount = m_Budget.m_AllocationCount[heapIndex]; + outBudgets->statistics.blockBytes = m_Budget.m_BlockBytes[heapIndex]; + outBudgets->statistics.allocationBytes = m_Budget.m_AllocationBytes[heapIndex]; + + if(m_Budget.m_VulkanUsage[heapIndex] + outBudgets->statistics.blockBytes > m_Budget.m_BlockBytesAtBudgetFetch[heapIndex]) + { + outBudgets->usage = m_Budget.m_VulkanUsage[heapIndex] + + outBudgets->statistics.blockBytes - m_Budget.m_BlockBytesAtBudgetFetch[heapIndex]; + } + else + { + outBudgets->usage = 0; + } + + // Have to take MIN with heap size because explicit HeapSizeLimit is included in it. + outBudgets->budget = VMA_MIN( + m_Budget.m_VulkanBudget[heapIndex], m_MemProps.memoryHeaps[heapIndex].size); + } + } + else + { + UpdateVulkanBudget(); // Outside of mutex lock + GetHeapBudgets(outBudgets, firstHeap, heapCount); // Recursion + } + } + else +#endif + { + for(uint32_t i = 0; i < heapCount; ++i, ++outBudgets) + { + const uint32_t heapIndex = firstHeap + i; + + outBudgets->statistics.blockCount = m_Budget.m_BlockCount[heapIndex]; + outBudgets->statistics.allocationCount = m_Budget.m_AllocationCount[heapIndex]; + outBudgets->statistics.blockBytes = m_Budget.m_BlockBytes[heapIndex]; + outBudgets->statistics.allocationBytes = m_Budget.m_AllocationBytes[heapIndex]; + + outBudgets->usage = outBudgets->statistics.blockBytes; + outBudgets->budget = m_MemProps.memoryHeaps[heapIndex].size * 8 / 10; // 80% heuristics. + } + } +} + +void VmaAllocator_T::GetAllocationInfo(VmaAllocation hAllocation, VmaAllocationInfo* pAllocationInfo) +{ + pAllocationInfo->memoryType = hAllocation->GetMemoryTypeIndex(); + pAllocationInfo->deviceMemory = hAllocation->GetMemory(); + pAllocationInfo->offset = hAllocation->GetOffset(); + pAllocationInfo->size = hAllocation->GetSize(); + pAllocationInfo->pMappedData = hAllocation->GetMappedData(); + pAllocationInfo->pUserData = hAllocation->GetUserData(); + pAllocationInfo->pName = hAllocation->GetName(); +} + +void VmaAllocator_T::GetAllocationInfo2(VmaAllocation hAllocation, VmaAllocationInfo2* pAllocationInfo) +{ + GetAllocationInfo(hAllocation, &pAllocationInfo->allocationInfo); + + switch (hAllocation->GetType()) + { + case VmaAllocation_T::ALLOCATION_TYPE_BLOCK: + pAllocationInfo->blockSize = hAllocation->GetBlock()->m_pMetadata->GetSize(); + pAllocationInfo->dedicatedMemory = VK_FALSE; + break; + case VmaAllocation_T::ALLOCATION_TYPE_DEDICATED: + pAllocationInfo->blockSize = pAllocationInfo->allocationInfo.size; + pAllocationInfo->dedicatedMemory = VK_TRUE; + break; + default: + VMA_ASSERT(0); + } +} + +VkResult VmaAllocator_T::CreatePool(const VmaPoolCreateInfo* pCreateInfo, VmaPool* pPool) +{ + VMA_DEBUG_LOG_FORMAT(" CreatePool: MemoryTypeIndex=%" PRIu32 ", flags=%" PRIu32, pCreateInfo->memoryTypeIndex, pCreateInfo->flags); + + VmaPoolCreateInfo newCreateInfo = *pCreateInfo; + + // Protection against uninitialized new structure member. If garbage data are left there, this pointer dereference would crash. + if(pCreateInfo->pMemoryAllocateNext) + { + VMA_ASSERT(((const VkBaseInStructure*)pCreateInfo->pMemoryAllocateNext)->sType != 0); + } + + if(newCreateInfo.maxBlockCount == 0) + { + newCreateInfo.maxBlockCount = SIZE_MAX; + } + if(newCreateInfo.minBlockCount > newCreateInfo.maxBlockCount) + { + return VK_ERROR_INITIALIZATION_FAILED; + } + // Memory type index out of range or forbidden. + if(pCreateInfo->memoryTypeIndex >= GetMemoryTypeCount() || + ((1U << pCreateInfo->memoryTypeIndex) & m_GlobalMemoryTypeBits) == 0) + { + return VK_ERROR_FEATURE_NOT_PRESENT; + } + if(newCreateInfo.minAllocationAlignment > 0) + { + VMA_ASSERT(VmaIsPow2(newCreateInfo.minAllocationAlignment)); + } + + const VkDeviceSize preferredBlockSize = CalcPreferredBlockSize(newCreateInfo.memoryTypeIndex); + + *pPool = vma_new(this, VmaPool_T)(this, newCreateInfo, preferredBlockSize); + + VkResult res = (*pPool)->m_BlockVector.CreateMinBlocks(); + if(res != VK_SUCCESS) + { + vma_delete(this, *pPool); + *pPool = VMA_NULL; + return res; + } + + // Add to m_Pools. + { + VmaMutexLockWrite lock(m_PoolsMutex, m_UseMutex); + (*pPool)->SetId(m_NextPoolId++); + m_Pools.PushBack(*pPool); + } + + return VK_SUCCESS; +} + +void VmaAllocator_T::DestroyPool(VmaPool pool) +{ + // Remove from m_Pools. + { + VmaMutexLockWrite lock(m_PoolsMutex, m_UseMutex); + m_Pools.Remove(pool); + } + + vma_delete(this, pool); +} + +void VmaAllocator_T::GetPoolStatistics(VmaPool pool, VmaStatistics* pPoolStats) +{ + VmaClearStatistics(*pPoolStats); + pool->m_BlockVector.AddStatistics(*pPoolStats); + pool->m_DedicatedAllocations.AddStatistics(*pPoolStats); +} + +void VmaAllocator_T::CalculatePoolStatistics(VmaPool pool, VmaDetailedStatistics* pPoolStats) +{ + VmaClearDetailedStatistics(*pPoolStats); + pool->m_BlockVector.AddDetailedStatistics(*pPoolStats); + pool->m_DedicatedAllocations.AddDetailedStatistics(*pPoolStats); +} + +void VmaAllocator_T::SetCurrentFrameIndex(uint32_t frameIndex) +{ + m_CurrentFrameIndex.store(frameIndex); + +#if VMA_MEMORY_BUDGET + if(m_UseExtMemoryBudget) + { + UpdateVulkanBudget(); + } +#endif // #if VMA_MEMORY_BUDGET +} + +VkResult VmaAllocator_T::CheckPoolCorruption(VmaPool hPool) +{ + return hPool->m_BlockVector.CheckCorruption(); +} + +VkResult VmaAllocator_T::CheckCorruption(uint32_t memoryTypeBits) +{ + VkResult finalRes = VK_ERROR_FEATURE_NOT_PRESENT; + + // Process default pools. + for(uint32_t memTypeIndex = 0; memTypeIndex < GetMemoryTypeCount(); ++memTypeIndex) + { + VmaBlockVector* const pBlockVector = m_pBlockVectors[memTypeIndex]; + if(pBlockVector != VMA_NULL) + { + VkResult localRes = pBlockVector->CheckCorruption(); + switch(localRes) + { + case VK_ERROR_FEATURE_NOT_PRESENT: + break; + case VK_SUCCESS: + finalRes = VK_SUCCESS; + break; + default: + return localRes; + } + } + } + + // Process custom pools. + { + VmaMutexLockRead lock(m_PoolsMutex, m_UseMutex); + for(VmaPool pool = m_Pools.Front(); pool != VMA_NULL; pool = m_Pools.GetNext(pool)) + { + if(((1U << pool->m_BlockVector.GetMemoryTypeIndex()) & memoryTypeBits) != 0) + { + VkResult localRes = pool->m_BlockVector.CheckCorruption(); + switch(localRes) + { + case VK_ERROR_FEATURE_NOT_PRESENT: + break; + case VK_SUCCESS: + finalRes = VK_SUCCESS; + break; + default: + return localRes; + } + } + } + } + + return finalRes; +} + +VkResult VmaAllocator_T::AllocateVulkanMemory(const VkMemoryAllocateInfo* pAllocateInfo, VkDeviceMemory* pMemory) +{ + const uint32_t heapIndex = MemoryTypeIndexToHeapIndex(pAllocateInfo->memoryTypeIndex); + +#if VMA_DEBUG_DONT_EXCEED_HEAP_SIZE_WITH_ALLOCATION_SIZE + if (pAllocateInfo->allocationSize > m_MemProps.memoryHeaps[heapIndex].size) + { + return VK_ERROR_OUT_OF_DEVICE_MEMORY; + } +#endif + + AtomicTransactionalIncrement deviceMemoryCountIncrement; + const uint64_t prevDeviceMemoryCount = deviceMemoryCountIncrement.Increment(&m_DeviceMemoryCount); +#if VMA_DEBUG_DONT_EXCEED_MAX_MEMORY_ALLOCATION_COUNT + if(prevDeviceMemoryCount >= m_PhysicalDeviceProperties.limits.maxMemoryAllocationCount) + { + return VK_ERROR_TOO_MANY_OBJECTS; + } +#endif + + // HeapSizeLimit is in effect for this heap. + if((m_HeapSizeLimitMask & (1U << heapIndex)) != 0) + { + const VkDeviceSize heapSize = m_MemProps.memoryHeaps[heapIndex].size; + VkDeviceSize blockBytes = m_Budget.m_BlockBytes[heapIndex]; + for(;;) + { + const VkDeviceSize blockBytesAfterAllocation = blockBytes + pAllocateInfo->allocationSize; + if(blockBytesAfterAllocation > heapSize) + { + return VK_ERROR_OUT_OF_DEVICE_MEMORY; + } + if(m_Budget.m_BlockBytes[heapIndex].compare_exchange_strong(blockBytes, blockBytesAfterAllocation)) + { + break; + } + } + } + else + { + m_Budget.m_BlockBytes[heapIndex] += pAllocateInfo->allocationSize; + } + ++m_Budget.m_BlockCount[heapIndex]; + + // VULKAN CALL vkAllocateMemory. + VkResult res = (*m_VulkanFunctions.vkAllocateMemory)(m_hDevice, pAllocateInfo, GetAllocationCallbacks(), pMemory); + + if(res == VK_SUCCESS) + { +#if VMA_MEMORY_BUDGET + ++m_Budget.m_OperationsSinceBudgetFetch; +#endif + + // Informative callback. + if(m_DeviceMemoryCallbacks.pfnAllocate != VMA_NULL) + { + (*m_DeviceMemoryCallbacks.pfnAllocate)(this, pAllocateInfo->memoryTypeIndex, *pMemory, pAllocateInfo->allocationSize, m_DeviceMemoryCallbacks.pUserData); + } + + deviceMemoryCountIncrement.Commit(); + } + else + { + --m_Budget.m_BlockCount[heapIndex]; + m_Budget.m_BlockBytes[heapIndex] -= pAllocateInfo->allocationSize; + } + + return res; +} + +void VmaAllocator_T::FreeVulkanMemory(uint32_t memoryType, VkDeviceSize size, VkDeviceMemory hMemory) +{ + // Informative callback. + if(m_DeviceMemoryCallbacks.pfnFree != VMA_NULL) + { + (*m_DeviceMemoryCallbacks.pfnFree)(this, memoryType, hMemory, size, m_DeviceMemoryCallbacks.pUserData); + } + + // VULKAN CALL vkFreeMemory. + (*m_VulkanFunctions.vkFreeMemory)(m_hDevice, hMemory, GetAllocationCallbacks()); + + const uint32_t heapIndex = MemoryTypeIndexToHeapIndex(memoryType); + --m_Budget.m_BlockCount[heapIndex]; + m_Budget.m_BlockBytes[heapIndex] -= size; + + --m_DeviceMemoryCount; +} + +VkResult VmaAllocator_T::BindVulkanBuffer( + VkDeviceMemory memory, + VkDeviceSize memoryOffset, + VkBuffer buffer, + const void* pNext) const +{ + if(pNext != VMA_NULL) + { +#if VMA_VULKAN_VERSION >= 1001000 || VMA_BIND_MEMORY2 + if((m_UseKhrBindMemory2 || m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) && + m_VulkanFunctions.vkBindBufferMemory2KHR != VMA_NULL) + { + VkBindBufferMemoryInfoKHR bindBufferMemoryInfo = { VK_STRUCTURE_TYPE_BIND_BUFFER_MEMORY_INFO_KHR }; + bindBufferMemoryInfo.pNext = pNext; + bindBufferMemoryInfo.buffer = buffer; + bindBufferMemoryInfo.memory = memory; + bindBufferMemoryInfo.memoryOffset = memoryOffset; + return (*m_VulkanFunctions.vkBindBufferMemory2KHR)(m_hDevice, 1, &bindBufferMemoryInfo); + } +#endif // #if VMA_VULKAN_VERSION >= 1001000 || VMA_BIND_MEMORY2 + + return VK_ERROR_EXTENSION_NOT_PRESENT; + } + else + { + return (*m_VulkanFunctions.vkBindBufferMemory)(m_hDevice, buffer, memory, memoryOffset); + } +} + +VkResult VmaAllocator_T::BindVulkanImage( + VkDeviceMemory memory, + VkDeviceSize memoryOffset, + VkImage image, + const void* pNext) const +{ + if(pNext != VMA_NULL) + { +#if VMA_VULKAN_VERSION >= 1001000 || VMA_BIND_MEMORY2 + if((m_UseKhrBindMemory2 || m_VulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) && + m_VulkanFunctions.vkBindImageMemory2KHR != VMA_NULL) + { + VkBindImageMemoryInfoKHR bindBufferMemoryInfo = { VK_STRUCTURE_TYPE_BIND_IMAGE_MEMORY_INFO_KHR }; + bindBufferMemoryInfo.pNext = pNext; + bindBufferMemoryInfo.image = image; + bindBufferMemoryInfo.memory = memory; + bindBufferMemoryInfo.memoryOffset = memoryOffset; + return (*m_VulkanFunctions.vkBindImageMemory2KHR)(m_hDevice, 1, &bindBufferMemoryInfo); + } +#endif // #if VMA_BIND_MEMORY2 + + return VK_ERROR_EXTENSION_NOT_PRESENT; + } + + return (*m_VulkanFunctions.vkBindImageMemory)(m_hDevice, image, memory, memoryOffset); +} + +VkResult VmaAllocator_T::Map(VmaAllocation hAllocation, void** ppData) +{ + switch(hAllocation->GetType()) + { + case VmaAllocation_T::ALLOCATION_TYPE_BLOCK: + { + VmaDeviceMemoryBlock* const pBlock = hAllocation->GetBlock(); + char *pBytes = VMA_NULL; + VkResult res = pBlock->Map(this, 1, (void**)&pBytes); + if(res == VK_SUCCESS) + { + *ppData = pBytes + (ptrdiff_t)hAllocation->GetOffset(); + hAllocation->BlockAllocMap(); + } + return res; + } + case VmaAllocation_T::ALLOCATION_TYPE_DEDICATED: + return hAllocation->DedicatedAllocMap(this, ppData); + default: + VMA_ASSERT(0); + return VK_ERROR_MEMORY_MAP_FAILED; + } +} + +void VmaAllocator_T::Unmap(VmaAllocation hAllocation) +{ + switch(hAllocation->GetType()) + { + case VmaAllocation_T::ALLOCATION_TYPE_BLOCK: + { + VmaDeviceMemoryBlock* const pBlock = hAllocation->GetBlock(); + hAllocation->BlockAllocUnmap(); + pBlock->Unmap(this, 1); + } + break; + case VmaAllocation_T::ALLOCATION_TYPE_DEDICATED: + hAllocation->DedicatedAllocUnmap(this); + break; + default: + VMA_ASSERT(0); + } +} + +VkResult VmaAllocator_T::BindBufferMemory( + VmaAllocation hAllocation, + VkDeviceSize allocationLocalOffset, + VkBuffer hBuffer, + const void* pNext) +{ + VkResult res = VK_ERROR_UNKNOWN_COPY; + switch(hAllocation->GetType()) + { + case VmaAllocation_T::ALLOCATION_TYPE_DEDICATED: + res = BindVulkanBuffer(hAllocation->GetMemory(), allocationLocalOffset, hBuffer, pNext); + break; + case VmaAllocation_T::ALLOCATION_TYPE_BLOCK: + { + VmaDeviceMemoryBlock* const pBlock = hAllocation->GetBlock(); + VMA_ASSERT(pBlock && "Binding buffer to allocation that doesn't belong to any block."); + res = pBlock->BindBufferMemory(this, hAllocation, allocationLocalOffset, hBuffer, pNext); + break; + } + default: + VMA_ASSERT(0); + } + return res; +} + +VkResult VmaAllocator_T::BindImageMemory( + VmaAllocation hAllocation, + VkDeviceSize allocationLocalOffset, + VkImage hImage, + const void* pNext) +{ + VkResult res = VK_ERROR_UNKNOWN_COPY; + switch(hAllocation->GetType()) + { + case VmaAllocation_T::ALLOCATION_TYPE_DEDICATED: + res = BindVulkanImage(hAllocation->GetMemory(), allocationLocalOffset, hImage, pNext); + break; + case VmaAllocation_T::ALLOCATION_TYPE_BLOCK: + { + VmaDeviceMemoryBlock* pBlock = hAllocation->GetBlock(); + VMA_ASSERT(pBlock && "Binding image to allocation that doesn't belong to any block."); + res = pBlock->BindImageMemory(this, hAllocation, allocationLocalOffset, hImage, pNext); + break; + } + default: + VMA_ASSERT(0); + } + return res; +} + +VkResult VmaAllocator_T::FlushOrInvalidateAllocation( + VmaAllocation hAllocation, + VkDeviceSize offset, VkDeviceSize size, + VMA_CACHE_OPERATION op) +{ + VkResult res = VK_SUCCESS; + + VkMappedMemoryRange memRange = {}; + if(GetFlushOrInvalidateRange(hAllocation, offset, size, memRange)) + { + switch(op) + { + case VMA_CACHE_FLUSH: + res = (*GetVulkanFunctions().vkFlushMappedMemoryRanges)(m_hDevice, 1, &memRange); + break; + case VMA_CACHE_INVALIDATE: + res = (*GetVulkanFunctions().vkInvalidateMappedMemoryRanges)(m_hDevice, 1, &memRange); + break; + default: + VMA_ASSERT(0); + } + } + // else: Just ignore this call. + return res; +} + +VkResult VmaAllocator_T::FlushOrInvalidateAllocations( + uint32_t allocationCount, + const VmaAllocation* allocations, + const VkDeviceSize* offsets, const VkDeviceSize* sizes, + VMA_CACHE_OPERATION op) +{ + typedef VmaStlAllocator RangeAllocator; + typedef VmaSmallVector RangeVector; + RangeVector ranges = RangeVector(RangeAllocator(GetAllocationCallbacks())); + + for(uint32_t allocIndex = 0; allocIndex < allocationCount; ++allocIndex) + { + const VmaAllocation alloc = allocations[allocIndex]; + const VkDeviceSize offset = offsets != VMA_NULL ? offsets[allocIndex] : 0; + const VkDeviceSize size = sizes != VMA_NULL ? sizes[allocIndex] : VK_WHOLE_SIZE; + VkMappedMemoryRange newRange; + if(GetFlushOrInvalidateRange(alloc, offset, size, newRange)) + { + ranges.push_back(newRange); + } + } + + VkResult res = VK_SUCCESS; + if(!ranges.empty()) + { + switch(op) + { + case VMA_CACHE_FLUSH: + res = (*GetVulkanFunctions().vkFlushMappedMemoryRanges)(m_hDevice, (uint32_t)ranges.size(), ranges.data()); + break; + case VMA_CACHE_INVALIDATE: + res = (*GetVulkanFunctions().vkInvalidateMappedMemoryRanges)(m_hDevice, (uint32_t)ranges.size(), ranges.data()); + break; + default: + VMA_ASSERT(0); + } + } + // else: Just ignore this call. + return res; +} + +VkResult VmaAllocator_T::CopyMemoryToAllocation( + const void* pSrcHostPointer, + VmaAllocation dstAllocation, + VkDeviceSize dstAllocationLocalOffset, + VkDeviceSize size) +{ + void* dstMappedData = VMA_NULL; + VkResult res = Map(dstAllocation, &dstMappedData); + if(res == VK_SUCCESS) + { + memcpy((char*)dstMappedData + dstAllocationLocalOffset, pSrcHostPointer, (size_t)size); + Unmap(dstAllocation); + res = FlushOrInvalidateAllocation(dstAllocation, dstAllocationLocalOffset, size, VMA_CACHE_FLUSH); + } + return res; +} + +VkResult VmaAllocator_T::CopyAllocationToMemory( + VmaAllocation srcAllocation, + VkDeviceSize srcAllocationLocalOffset, + void* pDstHostPointer, + VkDeviceSize size) +{ + void* srcMappedData = VMA_NULL; + VkResult res = Map(srcAllocation, &srcMappedData); + if(res == VK_SUCCESS) + { + res = FlushOrInvalidateAllocation(srcAllocation, srcAllocationLocalOffset, size, VMA_CACHE_INVALIDATE); + if(res == VK_SUCCESS) + { + memcpy(pDstHostPointer, (const char*)srcMappedData + srcAllocationLocalOffset, (size_t)size); + Unmap(srcAllocation); + } + } + return res; +} + +void VmaAllocator_T::FreeDedicatedMemory(VmaAllocation allocation) +{ + VMA_ASSERT(allocation && allocation->GetType() == VmaAllocation_T::ALLOCATION_TYPE_DEDICATED); + + const uint32_t memTypeIndex = allocation->GetMemoryTypeIndex(); + VmaPool parentPool = allocation->GetParentPool(); + if(parentPool == VK_NULL_HANDLE) + { + // Default pool + m_DedicatedAllocations[memTypeIndex].Unregister(allocation); + } + else + { + // Custom pool + parentPool->m_DedicatedAllocations.Unregister(allocation); + } + + VkDeviceMemory hMemory = allocation->GetMemory(); + + /* + There is no need to call this, because Vulkan spec allows to skip vkUnmapMemory + before vkFreeMemory. + + if(allocation->GetMappedData() != VMA_NULL) + { + (*m_VulkanFunctions.vkUnmapMemory)(m_hDevice, hMemory); + } + */ + + FreeVulkanMemory(memTypeIndex, allocation->GetSize(), hMemory); + + m_Budget.RemoveAllocation(MemoryTypeIndexToHeapIndex(allocation->GetMemoryTypeIndex()), allocation->GetSize()); + allocation->Destroy(this); + m_AllocationObjectAllocator.Free(allocation); + + VMA_DEBUG_LOG_FORMAT(" Freed DedicatedMemory MemoryTypeIndex=%" PRIu32, memTypeIndex); +} + +uint32_t VmaAllocator_T::CalculateGpuDefragmentationMemoryTypeBits() const +{ + VkBufferCreateInfo dummyBufCreateInfo; + VmaFillGpuDefragmentationBufferCreateInfo(dummyBufCreateInfo); + + uint32_t memoryTypeBits = 0; + + // Create buffer. + VkBuffer buf = VK_NULL_HANDLE; + VkResult res = (*GetVulkanFunctions().vkCreateBuffer)( + m_hDevice, &dummyBufCreateInfo, GetAllocationCallbacks(), &buf); + if(res == VK_SUCCESS) + { + // Query for supported memory types. + VkMemoryRequirements memReq; + (*GetVulkanFunctions().vkGetBufferMemoryRequirements)(m_hDevice, buf, &memReq); + memoryTypeBits = memReq.memoryTypeBits; + + // Destroy buffer. + (*GetVulkanFunctions().vkDestroyBuffer)(m_hDevice, buf, GetAllocationCallbacks()); + } + + return memoryTypeBits; +} + +uint32_t VmaAllocator_T::CalculateGlobalMemoryTypeBits() const +{ + // Make sure memory information is already fetched. + VMA_ASSERT(GetMemoryTypeCount() > 0); + + uint32_t memoryTypeBits = UINT32_MAX; + + if(!m_UseAmdDeviceCoherentMemory) + { + // Exclude memory types that have VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD. + for(uint32_t memTypeIndex = 0; memTypeIndex < GetMemoryTypeCount(); ++memTypeIndex) + { + if((m_MemProps.memoryTypes[memTypeIndex].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD_COPY) != 0) + { + memoryTypeBits &= ~(1U << memTypeIndex); + } + } + } + + return memoryTypeBits; +} + +bool VmaAllocator_T::GetFlushOrInvalidateRange( + VmaAllocation allocation, + VkDeviceSize offset, VkDeviceSize size, + VkMappedMemoryRange& outRange) const +{ + const uint32_t memTypeIndex = allocation->GetMemoryTypeIndex(); + if(size > 0 && IsMemoryTypeNonCoherent(memTypeIndex)) + { + const VkDeviceSize nonCoherentAtomSize = m_PhysicalDeviceProperties.limits.nonCoherentAtomSize; + const VkDeviceSize allocationSize = allocation->GetSize(); + VMA_ASSERT(offset <= allocationSize); + + outRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; + outRange.pNext = VMA_NULL; + outRange.memory = allocation->GetMemory(); + + switch(allocation->GetType()) + { + case VmaAllocation_T::ALLOCATION_TYPE_DEDICATED: + outRange.offset = VmaAlignDown(offset, nonCoherentAtomSize); + if(size == VK_WHOLE_SIZE) + { + outRange.size = allocationSize - outRange.offset; + } + else + { + VMA_ASSERT(offset + size <= allocationSize); + outRange.size = VMA_MIN( + VmaAlignUp(size + (offset - outRange.offset), nonCoherentAtomSize), + allocationSize - outRange.offset); + } + break; + case VmaAllocation_T::ALLOCATION_TYPE_BLOCK: + { + // 1. Still within this allocation. + outRange.offset = VmaAlignDown(offset, nonCoherentAtomSize); + if(size == VK_WHOLE_SIZE) + { + size = allocationSize - offset; + } + else + { + VMA_ASSERT(offset + size <= allocationSize); + } + outRange.size = VmaAlignUp(size + (offset - outRange.offset), nonCoherentAtomSize); + + // 2. Adjust to whole block. + const VkDeviceSize allocationOffset = allocation->GetOffset(); + VMA_ASSERT(allocationOffset % nonCoherentAtomSize == 0); + const VkDeviceSize blockSize = allocation->GetBlock()->m_pMetadata->GetSize(); + outRange.offset += allocationOffset; + outRange.size = VMA_MIN(outRange.size, blockSize - outRange.offset); + + break; + } + default: + VMA_ASSERT(0); + } + return true; + } + return false; +} + +#if VMA_MEMORY_BUDGET +void VmaAllocator_T::UpdateVulkanBudget() +{ + VMA_ASSERT(m_UseExtMemoryBudget); + + VkPhysicalDeviceMemoryProperties2KHR memProps = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2_KHR }; + + VkPhysicalDeviceMemoryBudgetPropertiesEXT budgetProps = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT }; + VmaPnextChainPushFront(&memProps, &budgetProps); + + GetVulkanFunctions().vkGetPhysicalDeviceMemoryProperties2KHR(m_PhysicalDevice, &memProps); + + { + VmaMutexLockWrite lockWrite(m_Budget.m_BudgetMutex, m_UseMutex); + + for(uint32_t heapIndex = 0; heapIndex < GetMemoryHeapCount(); ++heapIndex) + { + m_Budget.m_VulkanUsage[heapIndex] = budgetProps.heapUsage[heapIndex]; + m_Budget.m_VulkanBudget[heapIndex] = budgetProps.heapBudget[heapIndex]; + m_Budget.m_BlockBytesAtBudgetFetch[heapIndex] = m_Budget.m_BlockBytes[heapIndex].load(); + + // Some bugged drivers return the budget incorrectly, e.g. 0 or much bigger than heap size. + if(m_Budget.m_VulkanBudget[heapIndex] == 0) + { + m_Budget.m_VulkanBudget[heapIndex] = m_MemProps.memoryHeaps[heapIndex].size * 8 / 10; // 80% heuristics. + } + else if(m_Budget.m_VulkanBudget[heapIndex] > m_MemProps.memoryHeaps[heapIndex].size) + { + m_Budget.m_VulkanBudget[heapIndex] = m_MemProps.memoryHeaps[heapIndex].size; + } + if(m_Budget.m_VulkanUsage[heapIndex] == 0 && m_Budget.m_BlockBytesAtBudgetFetch[heapIndex] > 0) + { + m_Budget.m_VulkanUsage[heapIndex] = m_Budget.m_BlockBytesAtBudgetFetch[heapIndex]; + } + } + m_Budget.m_OperationsSinceBudgetFetch = 0; + } +} +#endif // VMA_MEMORY_BUDGET + +void VmaAllocator_T::FillAllocation(VmaAllocation hAllocation, uint8_t pattern) +{ + if(VMA_DEBUG_INITIALIZE_ALLOCATIONS && + hAllocation->IsMappingAllowed() && + (m_MemProps.memoryTypes[hAllocation->GetMemoryTypeIndex()].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) + { + void* pData = VMA_NULL; + VkResult res = Map(hAllocation, &pData); + if(res == VK_SUCCESS) + { + memset(pData, (int)pattern, (size_t)hAllocation->GetSize()); + FlushOrInvalidateAllocation(hAllocation, 0, VK_WHOLE_SIZE, VMA_CACHE_FLUSH); + Unmap(hAllocation); + } + else + { + VMA_ASSERT(0 && "VMA_DEBUG_INITIALIZE_ALLOCATIONS is enabled, but couldn't map memory to fill allocation."); + } + } +} + +uint32_t VmaAllocator_T::GetGpuDefragmentationMemoryTypeBits() +{ + uint32_t memoryTypeBits = m_GpuDefragmentationMemoryTypeBits.load(); + if(memoryTypeBits == UINT32_MAX) + { + memoryTypeBits = CalculateGpuDefragmentationMemoryTypeBits(); + m_GpuDefragmentationMemoryTypeBits.store(memoryTypeBits); + } + return memoryTypeBits; +} + +#if VMA_STATS_STRING_ENABLED +void VmaAllocator_T::PrintDetailedMap(VmaJsonWriter& json) +{ + json.WriteString("DefaultPools"); + json.BeginObject(); + { + for (uint32_t memTypeIndex = 0; memTypeIndex < GetMemoryTypeCount(); ++memTypeIndex) + { + VmaBlockVector* pBlockVector = m_pBlockVectors[memTypeIndex]; + VmaDedicatedAllocationList& dedicatedAllocList = m_DedicatedAllocations[memTypeIndex]; + if (pBlockVector != VMA_NULL) + { + json.BeginString("Type "); + json.ContinueString(memTypeIndex); + json.EndString(); + json.BeginObject(); + { + json.WriteString("PreferredBlockSize"); + json.WriteNumber(pBlockVector->GetPreferredBlockSize()); + + json.WriteString("Blocks"); + pBlockVector->PrintDetailedMap(json); + + json.WriteString("DedicatedAllocations"); + dedicatedAllocList.BuildStatsString(json); + } + json.EndObject(); + } + } + } + json.EndObject(); + + json.WriteString("CustomPools"); + json.BeginObject(); + { + VmaMutexLockRead lock(m_PoolsMutex, m_UseMutex); + if (!m_Pools.IsEmpty()) + { + for (uint32_t memTypeIndex = 0; memTypeIndex < GetMemoryTypeCount(); ++memTypeIndex) + { + bool displayType = true; + size_t index = 0; + for (VmaPool pool = m_Pools.Front(); pool != VMA_NULL; pool = m_Pools.GetNext(pool)) + { + VmaBlockVector& blockVector = pool->m_BlockVector; + if (blockVector.GetMemoryTypeIndex() == memTypeIndex) + { + if (displayType) + { + json.BeginString("Type "); + json.ContinueString(memTypeIndex); + json.EndString(); + json.BeginArray(); + displayType = false; + } + + json.BeginObject(); + { + json.WriteString("Name"); + json.BeginString(); + json.ContinueString((uint64_t)index++); + if (pool->GetName()) + { + json.ContinueString(" - "); + json.ContinueString(pool->GetName()); + } + json.EndString(); + + json.WriteString("PreferredBlockSize"); + json.WriteNumber(blockVector.GetPreferredBlockSize()); + + json.WriteString("Blocks"); + blockVector.PrintDetailedMap(json); + + json.WriteString("DedicatedAllocations"); + pool->m_DedicatedAllocations.BuildStatsString(json); + } + json.EndObject(); + } + } + + if (!displayType) + json.EndArray(); + } + } + } + json.EndObject(); +} +#endif // VMA_STATS_STRING_ENABLED +#endif // _VMA_ALLOCATOR_T_FUNCTIONS + + +#ifndef _VMA_PUBLIC_INTERFACE + +#ifdef VOLK_HEADER_VERSION + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaImportVulkanFunctionsFromVolk( + const VmaAllocatorCreateInfo* VMA_NOT_NULL pAllocatorCreateInfo, + VmaVulkanFunctions* VMA_NOT_NULL pDstVulkanFunctions) +{ + VMA_ASSERT(pAllocatorCreateInfo != VMA_NULL); + VMA_ASSERT(pAllocatorCreateInfo->instance != VK_NULL_HANDLE); + VMA_ASSERT(pAllocatorCreateInfo->device != VK_NULL_HANDLE); + + memset(pDstVulkanFunctions, 0, sizeof(*pDstVulkanFunctions)); + + VolkDeviceTable src = {}; + volkLoadDeviceTable(&src, pAllocatorCreateInfo->device); + +#define COPY_GLOBAL_TO_VMA_FUNC(volkName, vmaName) if(!pDstVulkanFunctions->vmaName) pDstVulkanFunctions->vmaName = volkName; +#define COPY_DEVICE_TO_VMA_FUNC(volkName, vmaName) if(!pDstVulkanFunctions->vmaName) pDstVulkanFunctions->vmaName = src.volkName; + + COPY_GLOBAL_TO_VMA_FUNC(vkGetInstanceProcAddr, vkGetInstanceProcAddr) + COPY_GLOBAL_TO_VMA_FUNC(vkGetDeviceProcAddr, vkGetDeviceProcAddr) + COPY_GLOBAL_TO_VMA_FUNC(vkGetPhysicalDeviceProperties, vkGetPhysicalDeviceProperties) + COPY_GLOBAL_TO_VMA_FUNC(vkGetPhysicalDeviceMemoryProperties, vkGetPhysicalDeviceMemoryProperties) + COPY_DEVICE_TO_VMA_FUNC(vkAllocateMemory, vkAllocateMemory) + COPY_DEVICE_TO_VMA_FUNC(vkFreeMemory, vkFreeMemory) + COPY_DEVICE_TO_VMA_FUNC(vkMapMemory, vkMapMemory) + COPY_DEVICE_TO_VMA_FUNC(vkUnmapMemory, vkUnmapMemory) + COPY_DEVICE_TO_VMA_FUNC(vkFlushMappedMemoryRanges, vkFlushMappedMemoryRanges) + COPY_DEVICE_TO_VMA_FUNC(vkInvalidateMappedMemoryRanges, vkInvalidateMappedMemoryRanges) + COPY_DEVICE_TO_VMA_FUNC(vkBindBufferMemory, vkBindBufferMemory) + COPY_DEVICE_TO_VMA_FUNC(vkBindImageMemory, vkBindImageMemory) + COPY_DEVICE_TO_VMA_FUNC(vkGetBufferMemoryRequirements, vkGetBufferMemoryRequirements) + COPY_DEVICE_TO_VMA_FUNC(vkGetImageMemoryRequirements, vkGetImageMemoryRequirements) + COPY_DEVICE_TO_VMA_FUNC(vkCreateBuffer, vkCreateBuffer) + COPY_DEVICE_TO_VMA_FUNC(vkDestroyBuffer, vkDestroyBuffer) + COPY_DEVICE_TO_VMA_FUNC(vkCreateImage, vkCreateImage) + COPY_DEVICE_TO_VMA_FUNC(vkDestroyImage, vkDestroyImage) + COPY_DEVICE_TO_VMA_FUNC(vkCmdCopyBuffer, vkCmdCopyBuffer) +#if VMA_VULKAN_VERSION >= 1001000 + if (pAllocatorCreateInfo->vulkanApiVersion >= VK_MAKE_VERSION(1, 1, 0)) + { + COPY_GLOBAL_TO_VMA_FUNC(vkGetPhysicalDeviceMemoryProperties2, vkGetPhysicalDeviceMemoryProperties2KHR) + COPY_DEVICE_TO_VMA_FUNC(vkGetBufferMemoryRequirements2, vkGetBufferMemoryRequirements2KHR) + COPY_DEVICE_TO_VMA_FUNC(vkGetImageMemoryRequirements2, vkGetImageMemoryRequirements2KHR) + COPY_DEVICE_TO_VMA_FUNC(vkBindBufferMemory2, vkBindBufferMemory2KHR) + COPY_DEVICE_TO_VMA_FUNC(vkBindImageMemory2, vkBindImageMemory2KHR) + } +#endif +#if VMA_VULKAN_VERSION >= 1003000 + if (pAllocatorCreateInfo->vulkanApiVersion >= VK_MAKE_VERSION(1, 3, 0)) + { + COPY_DEVICE_TO_VMA_FUNC(vkGetDeviceBufferMemoryRequirements, vkGetDeviceBufferMemoryRequirements) + COPY_DEVICE_TO_VMA_FUNC(vkGetDeviceImageMemoryRequirements, vkGetDeviceImageMemoryRequirements) + } +#endif +#if VMA_KHR_MAINTENANCE4 + if((pAllocatorCreateInfo->flags & VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE4_BIT) != 0) + { + COPY_DEVICE_TO_VMA_FUNC(vkGetDeviceBufferMemoryRequirementsKHR, vkGetDeviceBufferMemoryRequirements) + COPY_DEVICE_TO_VMA_FUNC(vkGetDeviceImageMemoryRequirementsKHR, vkGetDeviceImageMemoryRequirements) + } +#endif +#if VMA_DEDICATED_ALLOCATION + if ((pAllocatorCreateInfo->flags & VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT) != 0) + { + COPY_DEVICE_TO_VMA_FUNC(vkGetBufferMemoryRequirements2KHR, vkGetBufferMemoryRequirements2KHR) + COPY_DEVICE_TO_VMA_FUNC(vkGetImageMemoryRequirements2KHR, vkGetImageMemoryRequirements2KHR) + } +#endif +#if VMA_BIND_MEMORY2 + if ((pAllocatorCreateInfo->flags & VMA_ALLOCATOR_CREATE_KHR_BIND_MEMORY2_BIT) != 0) + { + COPY_DEVICE_TO_VMA_FUNC(vkBindBufferMemory2KHR, vkBindBufferMemory2KHR) + COPY_DEVICE_TO_VMA_FUNC(vkBindImageMemory2KHR, vkBindImageMemory2KHR) + } +#endif +#if VMA_MEMORY_BUDGET + if ((pAllocatorCreateInfo->flags & VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT) != 0) + { + COPY_GLOBAL_TO_VMA_FUNC(vkGetPhysicalDeviceMemoryProperties2KHR, vkGetPhysicalDeviceMemoryProperties2KHR) + } +#endif +#if VMA_EXTERNAL_MEMORY_WIN32 + if ((pAllocatorCreateInfo->flags & VMA_ALLOCATOR_CREATE_KHR_EXTERNAL_MEMORY_WIN32_BIT) != 0) + { + COPY_DEVICE_TO_VMA_FUNC(vkGetMemoryWin32HandleKHR, vkGetMemoryWin32HandleKHR) + } +#endif + +#undef COPY_DEVICE_TO_VMA_FUNC +#undef COPY_GLOBAL_TO_VMA_FUNC + + return VK_SUCCESS; +} + +#endif // #ifdef VOLK_HEADER_VERSION + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateAllocator( + const VmaAllocatorCreateInfo* pCreateInfo, + VmaAllocator* pAllocator) +{ + VMA_ASSERT(pCreateInfo && pAllocator); + VMA_ASSERT(pCreateInfo->vulkanApiVersion == 0 || + (VK_VERSION_MAJOR(pCreateInfo->vulkanApiVersion) == 1 && VK_VERSION_MINOR(pCreateInfo->vulkanApiVersion) <= 4)); + VMA_DEBUG_LOG("vmaCreateAllocator"); + *pAllocator = vma_new(pCreateInfo->pAllocationCallbacks, VmaAllocator_T)(pCreateInfo); + VkResult result = (*pAllocator)->Init(pCreateInfo); + if(result < 0) + { + vma_delete(pCreateInfo->pAllocationCallbacks, *pAllocator); + *pAllocator = VK_NULL_HANDLE; + } + return result; +} + +VMA_CALL_PRE void VMA_CALL_POST vmaDestroyAllocator( + VmaAllocator allocator) +{ + if(allocator != VK_NULL_HANDLE) + { + VMA_DEBUG_LOG("vmaDestroyAllocator"); + VkAllocationCallbacks allocationCallbacks = allocator->m_AllocationCallbacks; // Have to copy the callbacks when destroying. + vma_delete(&allocationCallbacks, allocator); + } +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetAllocatorInfo(VmaAllocator allocator, VmaAllocatorInfo* pAllocatorInfo) +{ + VMA_ASSERT(allocator && pAllocatorInfo); + pAllocatorInfo->instance = allocator->m_hInstance; + pAllocatorInfo->physicalDevice = allocator->GetPhysicalDevice(); + pAllocatorInfo->device = allocator->m_hDevice; +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetPhysicalDeviceProperties( + VmaAllocator allocator, + const VkPhysicalDeviceProperties **ppPhysicalDeviceProperties) +{ + VMA_ASSERT(allocator && ppPhysicalDeviceProperties); + *ppPhysicalDeviceProperties = &allocator->m_PhysicalDeviceProperties; +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetMemoryProperties( + VmaAllocator allocator, + const VkPhysicalDeviceMemoryProperties** ppPhysicalDeviceMemoryProperties) +{ + VMA_ASSERT(allocator && ppPhysicalDeviceMemoryProperties); + *ppPhysicalDeviceMemoryProperties = &allocator->m_MemProps; +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetMemoryTypeProperties( + VmaAllocator allocator, + uint32_t memoryTypeIndex, + VkMemoryPropertyFlags* pFlags) +{ + VMA_ASSERT(allocator && pFlags); + VMA_ASSERT(memoryTypeIndex < allocator->GetMemoryTypeCount()); + *pFlags = allocator->m_MemProps.memoryTypes[memoryTypeIndex].propertyFlags; +} + +VMA_CALL_PRE void VMA_CALL_POST vmaSetCurrentFrameIndex( + VmaAllocator allocator, + uint32_t frameIndex) +{ + VMA_ASSERT(allocator); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + allocator->SetCurrentFrameIndex(frameIndex); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaCalculateStatistics( + VmaAllocator allocator, + VmaTotalStatistics* pStats) +{ + VMA_ASSERT(allocator && pStats); + VMA_DEBUG_GLOBAL_MUTEX_LOCK + allocator->CalculateStatistics(pStats); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetHeapBudgets( + VmaAllocator allocator, + VmaBudget* pBudgets) +{ + VMA_ASSERT(allocator && pBudgets); + VMA_DEBUG_GLOBAL_MUTEX_LOCK + allocator->GetHeapBudgets(pBudgets, 0, allocator->GetMemoryHeapCount()); +} + +#if VMA_STATS_STRING_ENABLED + +VMA_CALL_PRE void VMA_CALL_POST vmaBuildStatsString( + VmaAllocator allocator, + char** ppStatsString, + VkBool32 detailedMap) +{ + VMA_ASSERT(allocator && ppStatsString); + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + VmaStringBuilder sb(allocator->GetAllocationCallbacks()); + { + VmaBudget budgets[VK_MAX_MEMORY_HEAPS]; + allocator->GetHeapBudgets(budgets, 0, allocator->GetMemoryHeapCount()); + + VmaTotalStatistics stats; + allocator->CalculateStatistics(&stats); + + VmaJsonWriter json(allocator->GetAllocationCallbacks(), sb); + json.BeginObject(); + { + json.WriteString("General"); + json.BeginObject(); + { + const VkPhysicalDeviceProperties& deviceProperties = allocator->m_PhysicalDeviceProperties; + const VkPhysicalDeviceMemoryProperties& memoryProperties = allocator->m_MemProps; + + json.WriteString("API"); + json.WriteString("Vulkan"); + + json.WriteString("apiVersion"); + json.BeginString(); + json.ContinueString(VK_VERSION_MAJOR(deviceProperties.apiVersion)); + json.ContinueString("."); + json.ContinueString(VK_VERSION_MINOR(deviceProperties.apiVersion)); + json.ContinueString("."); + json.ContinueString(VK_VERSION_PATCH(deviceProperties.apiVersion)); + json.EndString(); + + json.WriteString("GPU"); + json.WriteString(deviceProperties.deviceName); + json.WriteString("deviceType"); + json.WriteNumber(static_cast(deviceProperties.deviceType)); + + json.WriteString("maxMemoryAllocationCount"); + json.WriteNumber(deviceProperties.limits.maxMemoryAllocationCount); + json.WriteString("bufferImageGranularity"); + json.WriteNumber(deviceProperties.limits.bufferImageGranularity); + json.WriteString("nonCoherentAtomSize"); + json.WriteNumber(deviceProperties.limits.nonCoherentAtomSize); + + json.WriteString("memoryHeapCount"); + json.WriteNumber(memoryProperties.memoryHeapCount); + json.WriteString("memoryTypeCount"); + json.WriteNumber(memoryProperties.memoryTypeCount); + } + json.EndObject(); + } + { + json.WriteString("Total"); + VmaPrintDetailedStatistics(json, stats.total); + } + { + json.WriteString("MemoryInfo"); + json.BeginObject(); + { + for (uint32_t heapIndex = 0; heapIndex < allocator->GetMemoryHeapCount(); ++heapIndex) + { + json.BeginString("Heap "); + json.ContinueString(heapIndex); + json.EndString(); + json.BeginObject(); + { + const VkMemoryHeap& heapInfo = allocator->m_MemProps.memoryHeaps[heapIndex]; + json.WriteString("Flags"); + json.BeginArray(true); + { + if (heapInfo.flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) + json.WriteString("DEVICE_LOCAL"); + #if VMA_VULKAN_VERSION >= 1001000 + if (heapInfo.flags & VK_MEMORY_HEAP_MULTI_INSTANCE_BIT) + json.WriteString("MULTI_INSTANCE"); + #endif + + VkMemoryHeapFlags flags = heapInfo.flags & + ~(VK_MEMORY_HEAP_DEVICE_LOCAL_BIT + #if VMA_VULKAN_VERSION >= 1001000 + | VK_MEMORY_HEAP_MULTI_INSTANCE_BIT + #endif + ); + if (flags != 0) + json.WriteNumber(flags); + } + json.EndArray(); + + json.WriteString("Size"); + json.WriteNumber(heapInfo.size); + + json.WriteString("Budget"); + json.BeginObject(); + { + json.WriteString("BudgetBytes"); + json.WriteNumber(budgets[heapIndex].budget); + json.WriteString("UsageBytes"); + json.WriteNumber(budgets[heapIndex].usage); + } + json.EndObject(); + + json.WriteString("Stats"); + VmaPrintDetailedStatistics(json, stats.memoryHeap[heapIndex]); + + json.WriteString("MemoryPools"); + json.BeginObject(); + { + for (uint32_t typeIndex = 0; typeIndex < allocator->GetMemoryTypeCount(); ++typeIndex) + { + if (allocator->MemoryTypeIndexToHeapIndex(typeIndex) == heapIndex) + { + json.BeginString("Type "); + json.ContinueString(typeIndex); + json.EndString(); + json.BeginObject(); + { + json.WriteString("Flags"); + json.BeginArray(true); + { + VkMemoryPropertyFlags flags = allocator->m_MemProps.memoryTypes[typeIndex].propertyFlags; + if (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) + json.WriteString("DEVICE_LOCAL"); + if (flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) + json.WriteString("HOST_VISIBLE"); + if (flags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) + json.WriteString("HOST_COHERENT"); + if (flags & VK_MEMORY_PROPERTY_HOST_CACHED_BIT) + json.WriteString("HOST_CACHED"); + if (flags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) + json.WriteString("LAZILY_ALLOCATED"); + #if VMA_VULKAN_VERSION >= 1001000 + if (flags & VK_MEMORY_PROPERTY_PROTECTED_BIT) + json.WriteString("PROTECTED"); + #endif + #if VK_AMD_device_coherent_memory + if (flags & VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD_COPY) + json.WriteString("DEVICE_COHERENT_AMD"); + if (flags & VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD_COPY) + json.WriteString("DEVICE_UNCACHED_AMD"); + #endif + + flags &= ~(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT + #if VMA_VULKAN_VERSION >= 1001000 + | VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT + #endif + #if VK_AMD_device_coherent_memory + | VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD_COPY + | VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD_COPY + #endif + | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT + | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT + | VK_MEMORY_PROPERTY_HOST_CACHED_BIT); + if (flags != 0) + json.WriteNumber(flags); + } + json.EndArray(); + + json.WriteString("Stats"); + VmaPrintDetailedStatistics(json, stats.memoryType[typeIndex]); + } + json.EndObject(); + } + } + + } + json.EndObject(); + } + json.EndObject(); + } + } + json.EndObject(); + } + + if (detailedMap == VK_TRUE) + allocator->PrintDetailedMap(json); + + json.EndObject(); + } + + *ppStatsString = VmaCreateStringCopy(allocator->GetAllocationCallbacks(), sb.GetData(), sb.GetLength()); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaFreeStatsString( + VmaAllocator allocator, + char* pStatsString) +{ + if(pStatsString != VMA_NULL) + { + VMA_ASSERT(allocator); + VmaFreeString(allocator->GetAllocationCallbacks(), pStatsString); + } +} + +#endif // VMA_STATS_STRING_ENABLED + +/* +This function is not protected by any mutex because it just reads immutable data. +*/ +VMA_CALL_PRE VkResult VMA_CALL_POST vmaFindMemoryTypeIndex( + VmaAllocator allocator, + uint32_t memoryTypeBits, + const VmaAllocationCreateInfo* pAllocationCreateInfo, + uint32_t* pMemoryTypeIndex) +{ + VMA_ASSERT(allocator != VK_NULL_HANDLE); + VMA_ASSERT(pAllocationCreateInfo != VMA_NULL); + VMA_ASSERT(pMemoryTypeIndex != VMA_NULL); + + return allocator->FindMemoryTypeIndex(memoryTypeBits, pAllocationCreateInfo, VmaBufferImageUsage::UNKNOWN, pMemoryTypeIndex); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaFindMemoryTypeIndexForBufferInfo( + VmaAllocator allocator, + const VkBufferCreateInfo* pBufferCreateInfo, + const VmaAllocationCreateInfo* pAllocationCreateInfo, + uint32_t* pMemoryTypeIndex) +{ + VMA_ASSERT(allocator != VK_NULL_HANDLE); + VMA_ASSERT(pBufferCreateInfo != VMA_NULL); + VMA_ASSERT(pAllocationCreateInfo != VMA_NULL); + VMA_ASSERT(pMemoryTypeIndex != VMA_NULL); + + const VkDevice hDev = allocator->m_hDevice; + const VmaVulkanFunctions* funcs = &allocator->GetVulkanFunctions(); + VkResult res = VK_SUCCESS; + +#if VMA_KHR_MAINTENANCE4 || VMA_VULKAN_VERSION >= 1003000 + if(funcs->vkGetDeviceBufferMemoryRequirements) + { + // Can query straight from VkBufferCreateInfo :) + VkDeviceBufferMemoryRequirementsKHR devBufMemReq = {VK_STRUCTURE_TYPE_DEVICE_BUFFER_MEMORY_REQUIREMENTS_KHR}; + devBufMemReq.pCreateInfo = pBufferCreateInfo; + + VkMemoryRequirements2 memReq = {VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2}; + (*funcs->vkGetDeviceBufferMemoryRequirements)(hDev, &devBufMemReq, &memReq); + + res = allocator->FindMemoryTypeIndex( + memReq.memoryRequirements.memoryTypeBits, pAllocationCreateInfo, + VmaBufferImageUsage(*pBufferCreateInfo, allocator->m_UseKhrMaintenance5), pMemoryTypeIndex); + } + else +#endif // VMA_KHR_MAINTENANCE4 || VMA_VULKAN_VERSION >= 1003000 + { + // Must create a dummy buffer to query :( + VkBuffer hBuffer = VK_NULL_HANDLE; + res = funcs->vkCreateBuffer( + hDev, pBufferCreateInfo, allocator->GetAllocationCallbacks(), &hBuffer); + if(res == VK_SUCCESS) + { + VkMemoryRequirements memReq = {}; + funcs->vkGetBufferMemoryRequirements(hDev, hBuffer, &memReq); + + res = allocator->FindMemoryTypeIndex( + memReq.memoryTypeBits, pAllocationCreateInfo, + VmaBufferImageUsage(*pBufferCreateInfo, allocator->m_UseKhrMaintenance5), pMemoryTypeIndex); + + funcs->vkDestroyBuffer( + hDev, hBuffer, allocator->GetAllocationCallbacks()); + } + } + return res; +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaFindMemoryTypeIndexForImageInfo( + VmaAllocator allocator, + const VkImageCreateInfo* pImageCreateInfo, + const VmaAllocationCreateInfo* pAllocationCreateInfo, + uint32_t* pMemoryTypeIndex) +{ + VMA_ASSERT(allocator != VK_NULL_HANDLE); + VMA_ASSERT(pImageCreateInfo != VMA_NULL); + VMA_ASSERT(pAllocationCreateInfo != VMA_NULL); + VMA_ASSERT(pMemoryTypeIndex != VMA_NULL); + + const VkDevice hDev = allocator->m_hDevice; + const VmaVulkanFunctions* funcs = &allocator->GetVulkanFunctions(); + VkResult res = VK_SUCCESS; + +#if VMA_KHR_MAINTENANCE4 || VMA_VULKAN_VERSION >= 1003000 + if(funcs->vkGetDeviceImageMemoryRequirements) + { + // Can query straight from VkImageCreateInfo :) + VkDeviceImageMemoryRequirementsKHR devImgMemReq = {VK_STRUCTURE_TYPE_DEVICE_IMAGE_MEMORY_REQUIREMENTS_KHR}; + devImgMemReq.pCreateInfo = pImageCreateInfo; + VMA_ASSERT(pImageCreateInfo->tiling != VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT_COPY && (pImageCreateInfo->flags & VK_IMAGE_CREATE_DISJOINT_BIT_COPY) == 0 && + "Cannot use this VkImageCreateInfo with vmaFindMemoryTypeIndexForImageInfo as I don't know what to pass as VkDeviceImageMemoryRequirements::planeAspect."); + + VkMemoryRequirements2 memReq = {VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2}; + (*funcs->vkGetDeviceImageMemoryRequirements)(hDev, &devImgMemReq, &memReq); + + res = allocator->FindMemoryTypeIndex( + memReq.memoryRequirements.memoryTypeBits, pAllocationCreateInfo, + VmaBufferImageUsage(*pImageCreateInfo), pMemoryTypeIndex); + } + else +#endif // VMA_KHR_MAINTENANCE4 || VMA_VULKAN_VERSION >= 1003000 + { + // Must create a dummy image to query :( + VkImage hImage = VK_NULL_HANDLE; + res = funcs->vkCreateImage( + hDev, pImageCreateInfo, allocator->GetAllocationCallbacks(), &hImage); + if(res == VK_SUCCESS) + { + VkMemoryRequirements memReq = {}; + funcs->vkGetImageMemoryRequirements(hDev, hImage, &memReq); + + res = allocator->FindMemoryTypeIndex( + memReq.memoryTypeBits, pAllocationCreateInfo, + VmaBufferImageUsage(*pImageCreateInfo), pMemoryTypeIndex); + + funcs->vkDestroyImage( + hDev, hImage, allocator->GetAllocationCallbacks()); + } + } + return res; +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreatePool( + VmaAllocator allocator, + const VmaPoolCreateInfo* pCreateInfo, + VmaPool* pPool) +{ + VMA_ASSERT(allocator && pCreateInfo && pPool); + + VMA_DEBUG_LOG("vmaCreatePool"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->CreatePool(pCreateInfo, pPool); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaDestroyPool( + VmaAllocator allocator, + VmaPool pool) +{ + VMA_ASSERT(allocator); + + if(pool == VK_NULL_HANDLE) + { + return; + } + + VMA_DEBUG_LOG("vmaDestroyPool"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + allocator->DestroyPool(pool); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetPoolStatistics( + VmaAllocator allocator, + VmaPool pool, + VmaStatistics* pPoolStats) +{ + VMA_ASSERT(allocator && pool && pPoolStats); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + allocator->GetPoolStatistics(pool, pPoolStats); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaCalculatePoolStatistics( + VmaAllocator allocator, + VmaPool pool, + VmaDetailedStatistics* pPoolStats) +{ + VMA_ASSERT(allocator && pool && pPoolStats); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + allocator->CalculatePoolStatistics(pool, pPoolStats); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCheckPoolCorruption(VmaAllocator allocator, VmaPool pool) +{ + VMA_ASSERT(allocator && pool); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + VMA_DEBUG_LOG("vmaCheckPoolCorruption"); + + return allocator->CheckPoolCorruption(pool); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetPoolName( + VmaAllocator allocator, + VmaPool pool, + const char** ppName) +{ + VMA_ASSERT(allocator && pool && ppName); + + VMA_DEBUG_LOG("vmaGetPoolName"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + *ppName = pool->GetName(); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaSetPoolName( + VmaAllocator allocator, + VmaPool pool, + const char* pName) +{ + VMA_ASSERT(allocator && pool); + + VMA_DEBUG_LOG("vmaSetPoolName"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + pool->SetName(pName); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaAllocateMemory( + VmaAllocator allocator, + const VkMemoryRequirements* pVkMemoryRequirements, + const VmaAllocationCreateInfo* pCreateInfo, + VmaAllocation* pAllocation, + VmaAllocationInfo* pAllocationInfo) +{ + VMA_ASSERT(allocator && pVkMemoryRequirements && pCreateInfo && pAllocation); + + VMA_DEBUG_LOG("vmaAllocateMemory"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + VkResult result = allocator->AllocateMemory( + *pVkMemoryRequirements, + false, // requiresDedicatedAllocation + false, // prefersDedicatedAllocation + VK_NULL_HANDLE, // dedicatedBuffer + VK_NULL_HANDLE, // dedicatedImage + VmaBufferImageUsage::UNKNOWN, // dedicatedBufferImageUsage + *pCreateInfo, + VMA_SUBALLOCATION_TYPE_UNKNOWN, + 1, // allocationCount + pAllocation); + + if(pAllocationInfo != VMA_NULL && result == VK_SUCCESS) + { + allocator->GetAllocationInfo(*pAllocation, pAllocationInfo); + } + + return result; +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaAllocateMemoryPages( + VmaAllocator allocator, + const VkMemoryRequirements* pVkMemoryRequirements, + const VmaAllocationCreateInfo* pCreateInfo, + size_t allocationCount, + VmaAllocation* pAllocations, + VmaAllocationInfo* pAllocationInfo) +{ + if(allocationCount == 0) + { + return VK_SUCCESS; + } + + VMA_ASSERT(allocator && pVkMemoryRequirements && pCreateInfo && pAllocations); + + VMA_DEBUG_LOG("vmaAllocateMemoryPages"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + VkResult result = allocator->AllocateMemory( + *pVkMemoryRequirements, + false, // requiresDedicatedAllocation + false, // prefersDedicatedAllocation + VK_NULL_HANDLE, // dedicatedBuffer + VK_NULL_HANDLE, // dedicatedImage + VmaBufferImageUsage::UNKNOWN, // dedicatedBufferImageUsage + *pCreateInfo, + VMA_SUBALLOCATION_TYPE_UNKNOWN, + allocationCount, + pAllocations); + + if(pAllocationInfo != VMA_NULL && result == VK_SUCCESS) + { + for(size_t i = 0; i < allocationCount; ++i) + { + allocator->GetAllocationInfo(pAllocations[i], pAllocationInfo + i); + } + } + + return result; +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaAllocateMemoryForBuffer( + VmaAllocator allocator, + VkBuffer buffer, + const VmaAllocationCreateInfo* pCreateInfo, + VmaAllocation* pAllocation, + VmaAllocationInfo* pAllocationInfo) +{ + VMA_ASSERT(allocator && buffer != VK_NULL_HANDLE && pCreateInfo && pAllocation); + + VMA_DEBUG_LOG("vmaAllocateMemoryForBuffer"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + VkMemoryRequirements vkMemReq = {}; + bool requiresDedicatedAllocation = false; + bool prefersDedicatedAllocation = false; + allocator->GetBufferMemoryRequirements(buffer, vkMemReq, + requiresDedicatedAllocation, + prefersDedicatedAllocation); + + VkResult result = allocator->AllocateMemory( + vkMemReq, + requiresDedicatedAllocation, + prefersDedicatedAllocation, + buffer, // dedicatedBuffer + VK_NULL_HANDLE, // dedicatedImage + VmaBufferImageUsage::UNKNOWN, // dedicatedBufferImageUsage + *pCreateInfo, + VMA_SUBALLOCATION_TYPE_BUFFER, + 1, // allocationCount + pAllocation); + + if(pAllocationInfo && result == VK_SUCCESS) + { + allocator->GetAllocationInfo(*pAllocation, pAllocationInfo); + } + + return result; +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaAllocateMemoryForImage( + VmaAllocator allocator, + VkImage image, + const VmaAllocationCreateInfo* pCreateInfo, + VmaAllocation* pAllocation, + VmaAllocationInfo* pAllocationInfo) +{ + VMA_ASSERT(allocator && image != VK_NULL_HANDLE && pCreateInfo && pAllocation); + + VMA_DEBUG_LOG("vmaAllocateMemoryForImage"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + VkMemoryRequirements vkMemReq = {}; + bool requiresDedicatedAllocation = false; + bool prefersDedicatedAllocation = false; + allocator->GetImageMemoryRequirements(image, vkMemReq, + requiresDedicatedAllocation, prefersDedicatedAllocation); + + VkResult result = allocator->AllocateMemory( + vkMemReq, + requiresDedicatedAllocation, + prefersDedicatedAllocation, + VK_NULL_HANDLE, // dedicatedBuffer + image, // dedicatedImage + VmaBufferImageUsage::UNKNOWN, // dedicatedBufferImageUsage + *pCreateInfo, + VMA_SUBALLOCATION_TYPE_IMAGE_UNKNOWN, + 1, // allocationCount + pAllocation); + + if(pAllocationInfo && result == VK_SUCCESS) + { + allocator->GetAllocationInfo(*pAllocation, pAllocationInfo); + } + + return result; +} + +VMA_CALL_PRE void VMA_CALL_POST vmaFreeMemory( + VmaAllocator allocator, + VmaAllocation allocation) +{ + VMA_ASSERT(allocator); + + if(allocation == VK_NULL_HANDLE) + { + return; + } + + VMA_DEBUG_LOG("vmaFreeMemory"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + allocator->FreeMemory( + 1, // allocationCount + &allocation); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaFreeMemoryPages( + VmaAllocator allocator, + size_t allocationCount, + const VmaAllocation* pAllocations) +{ + if(allocationCount == 0) + { + return; + } + + VMA_ASSERT(allocator); + + VMA_DEBUG_LOG("vmaFreeMemoryPages"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + allocator->FreeMemory(allocationCount, pAllocations); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetAllocationInfo( + VmaAllocator allocator, + VmaAllocation allocation, + VmaAllocationInfo* pAllocationInfo) +{ + VMA_ASSERT(allocator && allocation && pAllocationInfo); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + allocator->GetAllocationInfo(allocation, pAllocationInfo); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetAllocationInfo2( + VmaAllocator allocator, + VmaAllocation allocation, + VmaAllocationInfo2* pAllocationInfo) +{ + VMA_ASSERT(allocator && allocation && pAllocationInfo); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + allocator->GetAllocationInfo2(allocation, pAllocationInfo); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaSetAllocationUserData( + VmaAllocator allocator, + VmaAllocation allocation, + void* pUserData) +{ + VMA_ASSERT(allocator && allocation); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + allocation->SetUserData(allocator, pUserData); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaSetAllocationName( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + const char* VMA_NULLABLE pName) +{ + allocation->SetName(allocator, pName); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetAllocationMemoryProperties( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkMemoryPropertyFlags* VMA_NOT_NULL pFlags) +{ + VMA_ASSERT(allocator && allocation && pFlags); + const uint32_t memTypeIndex = allocation->GetMemoryTypeIndex(); + *pFlags = allocator->m_MemProps.memoryTypes[memTypeIndex].propertyFlags; +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaMapMemory( + VmaAllocator allocator, + VmaAllocation allocation, + void** ppData) +{ + VMA_ASSERT(allocator && allocation && ppData); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->Map(allocation, ppData); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaUnmapMemory( + VmaAllocator allocator, + VmaAllocation allocation) +{ + VMA_ASSERT(allocator && allocation); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + allocator->Unmap(allocation); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaFlushAllocation( + VmaAllocator allocator, + VmaAllocation allocation, + VkDeviceSize offset, + VkDeviceSize size) +{ + VMA_ASSERT(allocator && allocation); + + VMA_DEBUG_LOG("vmaFlushAllocation"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->FlushOrInvalidateAllocation(allocation, offset, size, VMA_CACHE_FLUSH); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaInvalidateAllocation( + VmaAllocator allocator, + VmaAllocation allocation, + VkDeviceSize offset, + VkDeviceSize size) +{ + VMA_ASSERT(allocator && allocation); + + VMA_DEBUG_LOG("vmaInvalidateAllocation"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->FlushOrInvalidateAllocation(allocation, offset, size, VMA_CACHE_INVALIDATE); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaFlushAllocations( + VmaAllocator allocator, + uint32_t allocationCount, + const VmaAllocation* allocations, + const VkDeviceSize* offsets, + const VkDeviceSize* sizes) +{ + VMA_ASSERT(allocator); + + if(allocationCount == 0) + { + return VK_SUCCESS; + } + + VMA_ASSERT(allocations); + + VMA_DEBUG_LOG("vmaFlushAllocations"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->FlushOrInvalidateAllocations(allocationCount, allocations, offsets, sizes, VMA_CACHE_FLUSH); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaInvalidateAllocations( + VmaAllocator allocator, + uint32_t allocationCount, + const VmaAllocation* allocations, + const VkDeviceSize* offsets, + const VkDeviceSize* sizes) +{ + VMA_ASSERT(allocator); + + if(allocationCount == 0) + { + return VK_SUCCESS; + } + + VMA_ASSERT(allocations); + + VMA_DEBUG_LOG("vmaInvalidateAllocations"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->FlushOrInvalidateAllocations(allocationCount, allocations, offsets, sizes, VMA_CACHE_INVALIDATE); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCopyMemoryToAllocation( + VmaAllocator allocator, + const void* pSrcHostPointer, + VmaAllocation dstAllocation, + VkDeviceSize dstAllocationLocalOffset, + VkDeviceSize size) +{ + VMA_ASSERT(allocator && pSrcHostPointer && dstAllocation); + + if(size == 0) + { + return VK_SUCCESS; + } + + VMA_DEBUG_LOG("vmaCopyMemoryToAllocation"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->CopyMemoryToAllocation(pSrcHostPointer, dstAllocation, dstAllocationLocalOffset, size); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCopyAllocationToMemory( + VmaAllocator allocator, + VmaAllocation srcAllocation, + VkDeviceSize srcAllocationLocalOffset, + void* pDstHostPointer, + VkDeviceSize size) +{ + VMA_ASSERT(allocator && srcAllocation && pDstHostPointer); + + if(size == 0) + { + return VK_SUCCESS; + } + + VMA_DEBUG_LOG("vmaCopyAllocationToMemory"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->CopyAllocationToMemory(srcAllocation, srcAllocationLocalOffset, pDstHostPointer, size); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCheckCorruption( + VmaAllocator allocator, + uint32_t memoryTypeBits) +{ + VMA_ASSERT(allocator); + + VMA_DEBUG_LOG("vmaCheckCorruption"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->CheckCorruption(memoryTypeBits); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBeginDefragmentation( + VmaAllocator allocator, + const VmaDefragmentationInfo* pInfo, + VmaDefragmentationContext* pContext) +{ + VMA_ASSERT(allocator && pInfo && pContext); + + VMA_DEBUG_LOG("vmaBeginDefragmentation"); + + if (pInfo->pool != VMA_NULL) + { + // Check if run on supported algorithms + if (pInfo->pool->m_BlockVector.GetAlgorithm() & VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT) + return VK_ERROR_FEATURE_NOT_PRESENT; + } + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + *pContext = vma_new(allocator, VmaDefragmentationContext_T)(allocator, *pInfo); + return VK_SUCCESS; +} + +VMA_CALL_PRE void VMA_CALL_POST vmaEndDefragmentation( + VmaAllocator allocator, + VmaDefragmentationContext context, + VmaDefragmentationStats* pStats) +{ + VMA_ASSERT(allocator && context); + + VMA_DEBUG_LOG("vmaEndDefragmentation"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + if (pStats) + context->GetStats(*pStats); + vma_delete(allocator, context); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBeginDefragmentationPass( + VmaAllocator VMA_NOT_NULL allocator, + VmaDefragmentationContext VMA_NOT_NULL context, + VmaDefragmentationPassMoveInfo* VMA_NOT_NULL pPassInfo) +{ + VMA_ASSERT(context && pPassInfo); + + VMA_DEBUG_LOG("vmaBeginDefragmentationPass"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return context->DefragmentPassBegin(*pPassInfo); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaEndDefragmentationPass( + VmaAllocator VMA_NOT_NULL allocator, + VmaDefragmentationContext VMA_NOT_NULL context, + VmaDefragmentationPassMoveInfo* VMA_NOT_NULL pPassInfo) +{ + VMA_ASSERT(context && pPassInfo); + + VMA_DEBUG_LOG("vmaEndDefragmentationPass"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return context->DefragmentPassEnd(*pPassInfo); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBindBufferMemory( + VmaAllocator allocator, + VmaAllocation allocation, + VkBuffer buffer) +{ + VMA_ASSERT(allocator && allocation && buffer); + + VMA_DEBUG_LOG("vmaBindBufferMemory"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->BindBufferMemory(allocation, 0, buffer, VMA_NULL); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBindBufferMemory2( + VmaAllocator allocator, + VmaAllocation allocation, + VkDeviceSize allocationLocalOffset, + VkBuffer buffer, + const void* pNext) +{ + VMA_ASSERT(allocator && allocation && buffer); + + VMA_DEBUG_LOG("vmaBindBufferMemory2"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->BindBufferMemory(allocation, allocationLocalOffset, buffer, pNext); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBindImageMemory( + VmaAllocator allocator, + VmaAllocation allocation, + VkImage image) +{ + VMA_ASSERT(allocator && allocation && image); + + VMA_DEBUG_LOG("vmaBindImageMemory"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->BindImageMemory(allocation, 0, image, VMA_NULL); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaBindImageMemory2( + VmaAllocator allocator, + VmaAllocation allocation, + VkDeviceSize allocationLocalOffset, + VkImage image, + const void* pNext) +{ + VMA_ASSERT(allocator && allocation && image); + + VMA_DEBUG_LOG("vmaBindImageMemory2"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + return allocator->BindImageMemory(allocation, allocationLocalOffset, image, pNext); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateBuffer( + VmaAllocator allocator, + const VkBufferCreateInfo* pBufferCreateInfo, + const VmaAllocationCreateInfo* pAllocationCreateInfo, + VkBuffer* pBuffer, + VmaAllocation* pAllocation, + VmaAllocationInfo* pAllocationInfo) +{ + VMA_ASSERT(allocator && pBufferCreateInfo && pAllocationCreateInfo && pBuffer && pAllocation); + + if(pBufferCreateInfo->size == 0) + { + return VK_ERROR_INITIALIZATION_FAILED; + } + if((pBufferCreateInfo->usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT_COPY) != 0 && + !allocator->m_UseKhrBufferDeviceAddress) + { + VMA_ASSERT(0 && "Creating a buffer with VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT is not valid if VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT was not used."); + return VK_ERROR_INITIALIZATION_FAILED; + } + + VMA_DEBUG_LOG("vmaCreateBuffer"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + *pBuffer = VK_NULL_HANDLE; + *pAllocation = VK_NULL_HANDLE; + + // 1. Create VkBuffer. + VkResult res = (*allocator->GetVulkanFunctions().vkCreateBuffer)( + allocator->m_hDevice, + pBufferCreateInfo, + allocator->GetAllocationCallbacks(), + pBuffer); + if(res >= 0) + { + // 2. vkGetBufferMemoryRequirements. + VkMemoryRequirements vkMemReq = {}; + bool requiresDedicatedAllocation = false; + bool prefersDedicatedAllocation = false; + allocator->GetBufferMemoryRequirements(*pBuffer, vkMemReq, + requiresDedicatedAllocation, prefersDedicatedAllocation); + + // 3. Allocate memory using allocator. + res = allocator->AllocateMemory( + vkMemReq, + requiresDedicatedAllocation, + prefersDedicatedAllocation, + *pBuffer, // dedicatedBuffer + VK_NULL_HANDLE, // dedicatedImage + VmaBufferImageUsage(*pBufferCreateInfo, allocator->m_UseKhrMaintenance5), // dedicatedBufferImageUsage + *pAllocationCreateInfo, + VMA_SUBALLOCATION_TYPE_BUFFER, + 1, // allocationCount + pAllocation); + + if(res >= 0) + { + // 3. Bind buffer with memory. + if((pAllocationCreateInfo->flags & VMA_ALLOCATION_CREATE_DONT_BIND_BIT) == 0) + { + res = allocator->BindBufferMemory(*pAllocation, 0, *pBuffer, VMA_NULL); + } + if(res >= 0) + { + // All steps succeeded. + #if VMA_STATS_STRING_ENABLED + (*pAllocation)->InitBufferUsage(*pBufferCreateInfo, allocator->m_UseKhrMaintenance5); + #endif + if(pAllocationInfo != VMA_NULL) + { + allocator->GetAllocationInfo(*pAllocation, pAllocationInfo); + } + + return VK_SUCCESS; + } + allocator->FreeMemory( + 1, // allocationCount + pAllocation); + *pAllocation = VK_NULL_HANDLE; + (*allocator->GetVulkanFunctions().vkDestroyBuffer)(allocator->m_hDevice, *pBuffer, allocator->GetAllocationCallbacks()); + *pBuffer = VK_NULL_HANDLE; + return res; + } + (*allocator->GetVulkanFunctions().vkDestroyBuffer)(allocator->m_hDevice, *pBuffer, allocator->GetAllocationCallbacks()); + *pBuffer = VK_NULL_HANDLE; + return res; + } + return res; +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateBufferWithAlignment( + VmaAllocator allocator, + const VkBufferCreateInfo* pBufferCreateInfo, + const VmaAllocationCreateInfo* pAllocationCreateInfo, + VkDeviceSize minAlignment, + VkBuffer* pBuffer, + VmaAllocation* pAllocation, + VmaAllocationInfo* pAllocationInfo) +{ + VMA_ASSERT(allocator && pBufferCreateInfo && pAllocationCreateInfo && VmaIsPow2(minAlignment) && pBuffer && pAllocation); + + if(pBufferCreateInfo->size == 0) + { + return VK_ERROR_INITIALIZATION_FAILED; + } + if((pBufferCreateInfo->usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT_COPY) != 0 && + !allocator->m_UseKhrBufferDeviceAddress) + { + VMA_ASSERT(0 && "Creating a buffer with VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT is not valid if VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT was not used."); + return VK_ERROR_INITIALIZATION_FAILED; + } + + VMA_DEBUG_LOG("vmaCreateBufferWithAlignment"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + *pBuffer = VK_NULL_HANDLE; + *pAllocation = VK_NULL_HANDLE; + + // 1. Create VkBuffer. + VkResult res = (*allocator->GetVulkanFunctions().vkCreateBuffer)( + allocator->m_hDevice, + pBufferCreateInfo, + allocator->GetAllocationCallbacks(), + pBuffer); + if(res >= 0) + { + // 2. vkGetBufferMemoryRequirements. + VkMemoryRequirements vkMemReq = {}; + bool requiresDedicatedAllocation = false; + bool prefersDedicatedAllocation = false; + allocator->GetBufferMemoryRequirements(*pBuffer, vkMemReq, + requiresDedicatedAllocation, prefersDedicatedAllocation); + + // 2a. Include minAlignment + vkMemReq.alignment = VMA_MAX(vkMemReq.alignment, minAlignment); + + // 3. Allocate memory using allocator. + res = allocator->AllocateMemory( + vkMemReq, + requiresDedicatedAllocation, + prefersDedicatedAllocation, + *pBuffer, // dedicatedBuffer + VK_NULL_HANDLE, // dedicatedImage + VmaBufferImageUsage(*pBufferCreateInfo, allocator->m_UseKhrMaintenance5), // dedicatedBufferImageUsage + *pAllocationCreateInfo, + VMA_SUBALLOCATION_TYPE_BUFFER, + 1, // allocationCount + pAllocation); + + if(res >= 0) + { + // 3. Bind buffer with memory. + if((pAllocationCreateInfo->flags & VMA_ALLOCATION_CREATE_DONT_BIND_BIT) == 0) + { + res = allocator->BindBufferMemory(*pAllocation, 0, *pBuffer, VMA_NULL); + } + if(res >= 0) + { + // All steps succeeded. + #if VMA_STATS_STRING_ENABLED + (*pAllocation)->InitBufferUsage(*pBufferCreateInfo, allocator->m_UseKhrMaintenance5); + #endif + if(pAllocationInfo != VMA_NULL) + { + allocator->GetAllocationInfo(*pAllocation, pAllocationInfo); + } + + return VK_SUCCESS; + } + allocator->FreeMemory( + 1, // allocationCount + pAllocation); + *pAllocation = VK_NULL_HANDLE; + (*allocator->GetVulkanFunctions().vkDestroyBuffer)(allocator->m_hDevice, *pBuffer, allocator->GetAllocationCallbacks()); + *pBuffer = VK_NULL_HANDLE; + return res; + } + (*allocator->GetVulkanFunctions().vkDestroyBuffer)(allocator->m_hDevice, *pBuffer, allocator->GetAllocationCallbacks()); + *pBuffer = VK_NULL_HANDLE; + return res; + } + return res; +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateAliasingBuffer( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + const VkBufferCreateInfo* VMA_NOT_NULL pBufferCreateInfo, + VkBuffer VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pBuffer) +{ + return vmaCreateAliasingBuffer2(allocator, allocation, 0, pBufferCreateInfo, pBuffer); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateAliasingBuffer2( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkDeviceSize allocationLocalOffset, + const VkBufferCreateInfo* VMA_NOT_NULL pBufferCreateInfo, + VkBuffer VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pBuffer) +{ + VMA_ASSERT(allocator && pBufferCreateInfo && pBuffer && allocation); + VMA_ASSERT(allocationLocalOffset + pBufferCreateInfo->size <= allocation->GetSize()); + + VMA_DEBUG_LOG("vmaCreateAliasingBuffer2"); + + *pBuffer = VK_NULL_HANDLE; + + if (pBufferCreateInfo->size == 0) + { + return VK_ERROR_INITIALIZATION_FAILED; + } + if ((pBufferCreateInfo->usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT_COPY) != 0 && + !allocator->m_UseKhrBufferDeviceAddress) + { + VMA_ASSERT(0 && "Creating a buffer with VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT is not valid if VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT was not used."); + return VK_ERROR_INITIALIZATION_FAILED; + } + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + // 1. Create VkBuffer. + VkResult res = (*allocator->GetVulkanFunctions().vkCreateBuffer)( + allocator->m_hDevice, + pBufferCreateInfo, + allocator->GetAllocationCallbacks(), + pBuffer); + if (res >= 0) + { + // 2. Bind buffer with memory. + res = allocator->BindBufferMemory(allocation, allocationLocalOffset, *pBuffer, VMA_NULL); + if (res >= 0) + { + return VK_SUCCESS; + } + (*allocator->GetVulkanFunctions().vkDestroyBuffer)(allocator->m_hDevice, *pBuffer, allocator->GetAllocationCallbacks()); + } + return res; +} + +VMA_CALL_PRE void VMA_CALL_POST vmaDestroyBuffer( + VmaAllocator allocator, + VkBuffer buffer, + VmaAllocation allocation) +{ + VMA_ASSERT(allocator); + + if(buffer == VK_NULL_HANDLE && allocation == VK_NULL_HANDLE) + { + return; + } + + VMA_DEBUG_LOG("vmaDestroyBuffer"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + if(buffer != VK_NULL_HANDLE) + { + (*allocator->GetVulkanFunctions().vkDestroyBuffer)(allocator->m_hDevice, buffer, allocator->GetAllocationCallbacks()); + } + + if(allocation != VK_NULL_HANDLE) + { + allocator->FreeMemory( + 1, // allocationCount + &allocation); + } +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateImage( + VmaAllocator allocator, + const VkImageCreateInfo* pImageCreateInfo, + const VmaAllocationCreateInfo* pAllocationCreateInfo, + VkImage* pImage, + VmaAllocation* pAllocation, + VmaAllocationInfo* pAllocationInfo) +{ + VMA_ASSERT(allocator && pImageCreateInfo && pAllocationCreateInfo && pImage && pAllocation); + + VMA_ASSERT((pImageCreateInfo->flags & VK_IMAGE_CREATE_DISJOINT_BIT_COPY) == 0 && + "vmaCreateImage() doesn't support disjoint multi-planar images. Please allocate memory for the planes using vmaAllocateMemory() and bind them using vmaBindImageMemory2()."); + + if(pImageCreateInfo->extent.width == 0 || + pImageCreateInfo->extent.height == 0 || + pImageCreateInfo->extent.depth == 0 || + pImageCreateInfo->mipLevels == 0 || + pImageCreateInfo->arrayLayers == 0) + { + return VK_ERROR_INITIALIZATION_FAILED; + } + + VMA_DEBUG_LOG("vmaCreateImage"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + *pImage = VK_NULL_HANDLE; + *pAllocation = VK_NULL_HANDLE; + + // 1. Create VkImage. + VkResult res = (*allocator->GetVulkanFunctions().vkCreateImage)( + allocator->m_hDevice, + pImageCreateInfo, + allocator->GetAllocationCallbacks(), + pImage); + if(res == VK_SUCCESS) + { + VmaSuballocationType suballocType = pImageCreateInfo->tiling == VK_IMAGE_TILING_OPTIMAL ? + VMA_SUBALLOCATION_TYPE_IMAGE_OPTIMAL : + VMA_SUBALLOCATION_TYPE_IMAGE_LINEAR; + + // 2. Allocate memory using allocator. + VkMemoryRequirements vkMemReq = {}; + bool requiresDedicatedAllocation = false; + bool prefersDedicatedAllocation = false; + allocator->GetImageMemoryRequirements(*pImage, vkMemReq, + requiresDedicatedAllocation, prefersDedicatedAllocation); + + res = allocator->AllocateMemory( + vkMemReq, + requiresDedicatedAllocation, + prefersDedicatedAllocation, + VK_NULL_HANDLE, // dedicatedBuffer + *pImage, // dedicatedImage + VmaBufferImageUsage(*pImageCreateInfo), // dedicatedBufferImageUsage + *pAllocationCreateInfo, + suballocType, + 1, // allocationCount + pAllocation); + + if(res == VK_SUCCESS) + { + // 3. Bind image with memory. + if((pAllocationCreateInfo->flags & VMA_ALLOCATION_CREATE_DONT_BIND_BIT) == 0) + { + res = allocator->BindImageMemory(*pAllocation, 0, *pImage, VMA_NULL); + } + if(res == VK_SUCCESS) + { + // All steps succeeded. + #if VMA_STATS_STRING_ENABLED + (*pAllocation)->InitImageUsage(*pImageCreateInfo); + #endif + if(pAllocationInfo != VMA_NULL) + { + allocator->GetAllocationInfo(*pAllocation, pAllocationInfo); + } + + return VK_SUCCESS; + } + allocator->FreeMemory( + 1, // allocationCount + pAllocation); + *pAllocation = VK_NULL_HANDLE; + (*allocator->GetVulkanFunctions().vkDestroyImage)(allocator->m_hDevice, *pImage, allocator->GetAllocationCallbacks()); + *pImage = VK_NULL_HANDLE; + return res; + } + (*allocator->GetVulkanFunctions().vkDestroyImage)(allocator->m_hDevice, *pImage, allocator->GetAllocationCallbacks()); + *pImage = VK_NULL_HANDLE; + return res; + } + return res; +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateAliasingImage( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + const VkImageCreateInfo* VMA_NOT_NULL pImageCreateInfo, + VkImage VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pImage) +{ + return vmaCreateAliasingImage2(allocator, allocation, 0, pImageCreateInfo, pImage); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateAliasingImage2( + VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, + VkDeviceSize allocationLocalOffset, + const VkImageCreateInfo* VMA_NOT_NULL pImageCreateInfo, + VkImage VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pImage) +{ + VMA_ASSERT(allocator && pImageCreateInfo && pImage && allocation); + + *pImage = VK_NULL_HANDLE; + + VMA_DEBUG_LOG("vmaCreateImage2"); + + if (pImageCreateInfo->extent.width == 0 || + pImageCreateInfo->extent.height == 0 || + pImageCreateInfo->extent.depth == 0 || + pImageCreateInfo->mipLevels == 0 || + pImageCreateInfo->arrayLayers == 0) + { + return VK_ERROR_INITIALIZATION_FAILED; + } + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + // 1. Create VkImage. + VkResult res = (*allocator->GetVulkanFunctions().vkCreateImage)( + allocator->m_hDevice, + pImageCreateInfo, + allocator->GetAllocationCallbacks(), + pImage); + if (res >= 0) + { + // 2. Bind image with memory. + res = allocator->BindImageMemory(allocation, allocationLocalOffset, *pImage, VMA_NULL); + if (res >= 0) + { + return VK_SUCCESS; + } + (*allocator->GetVulkanFunctions().vkDestroyImage)(allocator->m_hDevice, *pImage, allocator->GetAllocationCallbacks()); + } + return res; +} + +VMA_CALL_PRE void VMA_CALL_POST vmaDestroyImage( + VmaAllocator VMA_NOT_NULL allocator, + VkImage VMA_NULLABLE_NON_DISPATCHABLE image, + VmaAllocation VMA_NULLABLE allocation) +{ + VMA_ASSERT(allocator); + + if(image == VK_NULL_HANDLE && allocation == VK_NULL_HANDLE) + { + return; + } + + VMA_DEBUG_LOG("vmaDestroyImage"); + + VMA_DEBUG_GLOBAL_MUTEX_LOCK + + if(image != VK_NULL_HANDLE) + { + (*allocator->GetVulkanFunctions().vkDestroyImage)(allocator->m_hDevice, image, allocator->GetAllocationCallbacks()); + } + if(allocation != VK_NULL_HANDLE) + { + allocator->FreeMemory( + 1, // allocationCount + &allocation); + } +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaCreateVirtualBlock( + const VmaVirtualBlockCreateInfo* VMA_NOT_NULL pCreateInfo, + VmaVirtualBlock VMA_NULLABLE * VMA_NOT_NULL pVirtualBlock) +{ + VMA_ASSERT(pCreateInfo && pVirtualBlock); + VMA_ASSERT(pCreateInfo->size > 0); + VMA_DEBUG_LOG("vmaCreateVirtualBlock"); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + *pVirtualBlock = vma_new(pCreateInfo->pAllocationCallbacks, VmaVirtualBlock_T)(*pCreateInfo); + return VK_SUCCESS; + + /* + Code for the future if we ever need a separate Init() method that could fail: + + VkResult res = (*pVirtualBlock)->Init(); + if(res < 0) + { + vma_delete(pCreateInfo->pAllocationCallbacks, *pVirtualBlock); + *pVirtualBlock = VK_NULL_HANDLE; + } + return res; + */ +} + +VMA_CALL_PRE void VMA_CALL_POST vmaDestroyVirtualBlock(VmaVirtualBlock VMA_NULLABLE virtualBlock) +{ + if(virtualBlock != VK_NULL_HANDLE) + { + VMA_DEBUG_LOG("vmaDestroyVirtualBlock"); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + VkAllocationCallbacks allocationCallbacks = virtualBlock->m_AllocationCallbacks; // Have to copy the callbacks when destroying. + vma_delete(&allocationCallbacks, virtualBlock); + } +} + +VMA_CALL_PRE VkBool32 VMA_CALL_POST vmaIsVirtualBlockEmpty(VmaVirtualBlock VMA_NOT_NULL virtualBlock) +{ + VMA_ASSERT(virtualBlock != VK_NULL_HANDLE); + VMA_DEBUG_LOG("vmaIsVirtualBlockEmpty"); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + return virtualBlock->IsEmpty() ? VK_TRUE : VK_FALSE; +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetVirtualAllocationInfo(VmaVirtualBlock VMA_NOT_NULL virtualBlock, + VmaVirtualAllocation VMA_NOT_NULL_NON_DISPATCHABLE allocation, VmaVirtualAllocationInfo* VMA_NOT_NULL pVirtualAllocInfo) +{ + VMA_ASSERT(virtualBlock != VK_NULL_HANDLE && pVirtualAllocInfo != VMA_NULL); + VMA_DEBUG_LOG("vmaGetVirtualAllocationInfo"); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + virtualBlock->GetAllocationInfo(allocation, *pVirtualAllocInfo); +} + +VMA_CALL_PRE VkResult VMA_CALL_POST vmaVirtualAllocate(VmaVirtualBlock VMA_NOT_NULL virtualBlock, + const VmaVirtualAllocationCreateInfo* VMA_NOT_NULL pCreateInfo, VmaVirtualAllocation VMA_NULLABLE_NON_DISPATCHABLE* VMA_NOT_NULL pAllocation, + VkDeviceSize* VMA_NULLABLE pOffset) +{ + VMA_ASSERT(virtualBlock != VK_NULL_HANDLE && pCreateInfo != VMA_NULL && pAllocation != VMA_NULL); + VMA_DEBUG_LOG("vmaVirtualAllocate"); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + return virtualBlock->Allocate(*pCreateInfo, *pAllocation, pOffset); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaVirtualFree(VmaVirtualBlock VMA_NOT_NULL virtualBlock, VmaVirtualAllocation VMA_NULLABLE_NON_DISPATCHABLE allocation) +{ + if(allocation != VK_NULL_HANDLE) + { + VMA_ASSERT(virtualBlock != VK_NULL_HANDLE); + VMA_DEBUG_LOG("vmaVirtualFree"); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + virtualBlock->Free(allocation); + } +} + +VMA_CALL_PRE void VMA_CALL_POST vmaClearVirtualBlock(VmaVirtualBlock VMA_NOT_NULL virtualBlock) +{ + VMA_ASSERT(virtualBlock != VK_NULL_HANDLE); + VMA_DEBUG_LOG("vmaClearVirtualBlock"); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + virtualBlock->Clear(); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaSetVirtualAllocationUserData(VmaVirtualBlock VMA_NOT_NULL virtualBlock, + VmaVirtualAllocation VMA_NOT_NULL_NON_DISPATCHABLE allocation, void* VMA_NULLABLE pUserData) +{ + VMA_ASSERT(virtualBlock != VK_NULL_HANDLE); + VMA_DEBUG_LOG("vmaSetVirtualAllocationUserData"); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + virtualBlock->SetAllocationUserData(allocation, pUserData); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaGetVirtualBlockStatistics(VmaVirtualBlock VMA_NOT_NULL virtualBlock, + VmaStatistics* VMA_NOT_NULL pStats) +{ + VMA_ASSERT(virtualBlock != VK_NULL_HANDLE && pStats != VMA_NULL); + VMA_DEBUG_LOG("vmaGetVirtualBlockStatistics"); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + virtualBlock->GetStatistics(*pStats); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaCalculateVirtualBlockStatistics(VmaVirtualBlock VMA_NOT_NULL virtualBlock, + VmaDetailedStatistics* VMA_NOT_NULL pStats) +{ + VMA_ASSERT(virtualBlock != VK_NULL_HANDLE && pStats != VMA_NULL); + VMA_DEBUG_LOG("vmaCalculateVirtualBlockStatistics"); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + virtualBlock->CalculateDetailedStatistics(*pStats); +} + +#if VMA_STATS_STRING_ENABLED + +VMA_CALL_PRE void VMA_CALL_POST vmaBuildVirtualBlockStatsString(VmaVirtualBlock VMA_NOT_NULL virtualBlock, + char* VMA_NULLABLE * VMA_NOT_NULL ppStatsString, VkBool32 detailedMap) +{ + VMA_ASSERT(virtualBlock != VK_NULL_HANDLE && ppStatsString != VMA_NULL); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + const VkAllocationCallbacks* allocationCallbacks = virtualBlock->GetAllocationCallbacks(); + VmaStringBuilder sb(allocationCallbacks); + virtualBlock->BuildStatsString(detailedMap != VK_FALSE, sb); + *ppStatsString = VmaCreateStringCopy(allocationCallbacks, sb.GetData(), sb.GetLength()); +} + +VMA_CALL_PRE void VMA_CALL_POST vmaFreeVirtualBlockStatsString(VmaVirtualBlock VMA_NOT_NULL virtualBlock, + char* VMA_NULLABLE pStatsString) +{ + if(pStatsString != VMA_NULL) + { + VMA_ASSERT(virtualBlock != VK_NULL_HANDLE); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + VmaFreeString(virtualBlock->GetAllocationCallbacks(), pStatsString); + } +} +#if VMA_EXTERNAL_MEMORY_WIN32 +VMA_CALL_PRE VkResult VMA_CALL_POST vmaGetMemoryWin32Handle(VmaAllocator VMA_NOT_NULL allocator, + VmaAllocation VMA_NOT_NULL allocation, HANDLE hTargetProcess, HANDLE* VMA_NOT_NULL pHandle) +{ + VMA_ASSERT(allocator && allocation && pHandle); + VMA_DEBUG_GLOBAL_MUTEX_LOCK; + return allocation->GetWin32Handle(allocator, hTargetProcess, pHandle); +} +#endif // VMA_EXTERNAL_MEMORY_WIN32 +#endif // VMA_STATS_STRING_ENABLED +#endif // _VMA_PUBLIC_INTERFACE +#endif // VMA_IMPLEMENTATION + +/** +\page faq Frequenty asked questions + +What is VMA? + +Vulkan(R) Memory Allocator (VMA) is a software library for developers who use the Vulkan graphics API in their code. +It is written in C++. + +What is the license of VMA? + +VMA is licensed under MIT, which means it is open source and free software. + +What is the purpose of VMA? + +VMA helps with handling one aspect of Vulkan usage, which is device memory management - +allocation of `VkDeviceMemory` objects, and creation of `VkBuffer` and `VkImage` objects. + +Do I need to use VMA? + +You don't need to, but it may be beneficial in many cases. +Vulkan is a complex and low-level API, so libraries like this that abstract certain aspects of the API +and bring them to a higher level are useful. +When developing any non-trivial Vulkan application, you likely need to use a memory allocator. +Using VMA can save time compared to implementing your own. + +When should I not use VMA? + +While VMA is useful for most applications that use the Vulkan API, there are cases +when it may be a better choice not to use it. +For example, if the application is very simple, e.g. serving as a sample or a learning exercise +to help you understand or teach others the basics of Vulkan, +and it creates only a small number of buffers and images, then including VMA may be an overkill. +Developing your own memory allocator may also be a good learning exercise. + +What are the benefits of using VMA? + +-# VMA helps in choosing the optimal memory type for your resource (buffer or image). + In Vulkan, we have a two-level hierarchy of memory heaps and types with different flags, + and each device can expose a different set of those. + Implementing logic that would select the best memory type on each platform is a non-trivial task. + VMA does that, expecting only a high-level description of the intended usage of your resource. + For more information, see \subpage choosing_memory_type. +-# VMA allocates large blocks of `VkDeviceMemory` and sub-allocates parts of them for your resources. + Allocating a new block of device memory may be a time-consuming operation. + Some platforms also have a limit on the maximum number of those blocks (`VkPhysicalDeviceLimits::maxMemoryAllocationCount`) + as low as 4096, so allocating a separate one for each resource is not an option. + Sub-allocating parts of a memory block requires implementing an allocation algorithm, + which is a non-trivial task. + VMA does that, using an advanced and efficient algorithm that works well in various use cases. +-# VMA offers a simple API that allows creating buffers and textures within one function call. + In Vulkan, the creation of a resource is a multi-step process. + You need to create a `VkBuffer` or `VkImage`, ask it for memory requirements, + allocate a `VkDeviceMemory` object, and finally bind the resource to the memory block. + VMA does that automatically under a simple API within one function call: vmaCreateBuffer(), vmaCreateImage(). + +The library is doing much more under the hood. +For example, it respects limits like `bufferImageGranularity`, `nonCoherentAtomSize`, +and `VkMemoryDedicatedRequirements` automatically, so you don't need to think about it. + +Which version should I pick? + +You can just pick [the latest version from the "master" branch](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator). +It is kept in a good shape most of the time, compiling and working correctly, +with no compatibility-breaking changes and no unfinished code. + +If you want an even more stable version, you can pick +[the latest official release](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator/releases). +Current code from the master branch is occasionally tagged as a release, +with [CHANGELOG](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator/blob/master/CHANGELOG.md) +carefully curated to enumerate all important changes since the previous version. + +The library uses [Semantic Versioning](https://semver.org/), +which means versions that only differ in the patch number are forward and backward compatible +(e.g., only fixing some bugs), while versions that differ in the minor number are backward compatible +(e.g., only adding new functions to the API, but not removing or changing existing ones). + +How to integrate it with my code? + +VMA is an STB-style single-header C++ library. + +You can pull the entire GitHub repository, e.g. using Git submodules. +The repository contains ancillary files like the Cmake script, Doxygen config file, +sample application, test suite, and others. +You can compile it as a library and link with your project. + +However, a simpler way is taking the single file "include/vk_mem_alloc.h" and including it in your project. +This extensive file contains all you need: a copyright notice, +declarations of the public library interface (API), its internal implementation, +and even the documentation in form of Doxygen-style comments. + +The "STB style" means not everything is implemented as inline functions in the header file. +You need to extract the internal implementation using a special macro. +This means that in every .cpp file where you need to use the library you should +`#include "vk_mem_alloc.h"` to include its public interface, +but additionally in exactly one .cpp file you should `#define VMA_IMPLEMENTATION` +before this `#include` to enable its internal implementation. +For more information, see [Project setup](@ref quick_start_project_setup). + +Does the library work with C or C++? + +The internal implementation of VMA is written in C++. +It is distributed in the source format, so you need a compiler supporting at least C++14 to build it. + +However, the public interface of the library is written in C - using only enums, structs, and global functions, +in the same style as Vulkan, so you can use the library in the C code. + +I am not a fan of modern C++. Can I still use it? + +Very likely yes. +We acknowledge that many C++ developers, especially in the games industry, +do not appreciate all the latest features that the language has to offer. + +- VMA doesn't throw or catch any C++ exceptions. + It reports errors by returning a `VkResult` value instead, just like Vulkan. + If you don't use exceptions in your project, your code is not exception-safe, + or even if you disable exception handling in the compiler options, you can still use VMA. +- VMA doesn't use C++ run-time type information like `typeid` or `dynamic_cast`, + so if you disable RTTI in the compiler options, you can still use the library. +- VMA uses only a limited subset of standard C and C++ library. + It doesn't use STL containers like `std::vector`, `map`, or `string`, + either in the public interface nor in the internal implementation. + It implements its own containers instead. +- If you don't use the default heap memory allocator through `malloc/free` or `new/delete` + but implement your own allocator instead, you can pass it to VMA and + the library will use your functions for every dynamic heap allocation made internally, + as well as passing it further to Vulkan functions. For details, see [Custom host memory allocator](@ref custom_memory_allocator). + +Is it available for other programming languages? + +VMA is a C++ library with C interface in similar style as Vulkan. +An object-oriented C++ wrapper or bindings to other programming languages are out of scope of this project, +but they are welcome as external projects. +Some of them are listed in [README.md, "See also" section](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator?tab=readme-ov-file#see-also), +including binding to C++, Python, Rust, and Haskell. +Before using any of them, please check if they are still maintained and updated to use a recent version of VMA. + +What platforms does it support? + +VMA relies only on Vulkan and some parts of the standard C and C++ library, +so it supports any platform where a C++ compiler and Vulkan are available. +It is developed mostly on Microsoft(R) Windows(R), +but it has been successfully used in Linux(R), MacOS, Android, and even FreeBSD and Raspberry Pi. + +Does it only work on AMD GPUs? + +No! While VMA is published by AMD, it works on any GPU that supports Vulkan, +whether a discrete PC graphics card, a processor integrated graphics, or a mobile SoC. +It doesn't give AMD GPUs any advantage over any other GPUs. + +What Vulkan versions and extensions are supported? + +VMA is updated to support the latest versions of Vulkan. +It currently supports Vulkan up to 1.4. +The library also supports older versions down to the first release of Vulkan 1.0. +Defining a higher minimum version support would help simplify the code, +but we acknowledge that developers on some platforms like Android still use older versions, +so the support is provided for all of them. + +Among many extensions available for Vulkan, only a few interact with memory management. +VMA can automatically take advantage of them. Some of them are: +VK_EXT_memory_budget, VK_EXT_memory_priority, VK_KHR_external_memory_win32, and VK_KHR_maintenance* +extensions that are later promoted to the new versions of the core Vulkan API. + +To use them, it is your responsibility to validate if they are available on the current system and if so, +enable them while creating the Vulkan device object. +You also need to pass appropriate #VmaAllocatorCreateFlagBits to inform VMA that they are enabled. +Then, the library will automatically take advantage of them. +For more information and the full list of supported extensions, see [Enabling extensions](@ref quick_start_initialization_enabling_extensions). + +Does it support other graphics APIs, like Microsoft DirectX(R) 12? + +No, but we offer an equivalent library for DirectX 12: +[D3D12 Memory Allocator](https://github.com/GPUOpen-LibrariesAndSDKs/D3D12MemoryAllocator). +It uses the same core allocation algorithm. +It also shares many features with VMA, like the support for custom pools and virtual allocator. +However, it is not identical in terms of the features supported. +Its API also looks different, because while the interface of VMA is similar in style to Vulkan, +the interface of D3D12MA is similar to DirectX 12. + +Is the library lightweight? + +It depends on how you define it. +VMA is implemented with high-performance and real-time applications like video games in mind. +The CPU performance overhead of using this library is low. +It uses a high-quality allocation algorithm called Two-Level Segregated Fit (TLSF), +which in most cases can find a free place for a new allocation in few steps. +The library also doesn't perform too many CPU heap allocations. +In many cases, the allocation happens with 0 new CPU heap allocations performed by the library. +Even the creation of a #VmaAllocation object doesn't typically feature an CPU allocation, +because these objects are returned out of a dedicated memory pool. + +On the other hand, however, VMA needs some extra memory and extra time +to maintain the metadata about the occupied and free regions of the memory blocks, +and the algorithms and data structures used must be generic enough to work well in most cases. +If you develop your program for a very resource-constrained platform, +a custom allocator simpler than VMA may be a better choice. + +Does it have a documentation? + +Yes! VMA comes with full documentation of all elements of the API (functions, structures, enums), +as well as many generic chapters that provide an introduction, +describe core concepts of the library, good practices, etc. +The entire documentation is written in form of code comments inside "vk_mem_alloc.h", in Doxygen format. +You can access it in multiple ways: + +- Browsable online: https://gpuopen-librariesandsdks.github.io/VulkanMemoryAllocator/html/ +- Local HTML pages available after you clone the repository and open file "docs/html/index.html". +- You can rebuild the documentation in HTML or some other format from the source code using Doxygen. + Configuration file "Doxyfile" is part of the repository. +- Finally, you can just read the comments preceding declarations of any public functions of the library. + +Is it a mature project? + +Yes! The library is in development since June 2017, has over 1000 commits, over 400 issue tickets +and pull requests (most of them resolved), and over 70 contributors. +It is distributed together with Vulkan SDK. +It is used by many software projects, including some large and popular ones like Qt or Blender, +as well as some AAA games. +According to the [LunarG 2024 Ecosystem Survey](https://www.lunarg.com/2024-ecosystem-survey-progress-report-released/), +it is used by over 50% of Vulkan developers. + +How can I contribute to the project? + +If you have an idea for improvement or a feature request, +you can go to [the library repository](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) +and create an Issue ticket, describing your idea. +You can also implement it yourself by forking the repository, making changes to the code, +and creating a Pull request. + +If you want to ask a question, you can also create a ticket the same way. +Before doing this, please make sure you read the relevant part of the Vulkan specification and VMA documentation, +where you may find the answers to your question. + +If you want to report a suspected bug, you can also create a ticket the same way. +Before doing this, please put some effort into the investigation of whether the bug is really +in the library and not in your code or in the Vulkan implementation (the GPU driver) on your platform: + +- Enable Vulkan validation layer and make sure it is free from any errors. +- Make sure `VMA_ASSERT` is defined to an implementation that can report a failure and not ignore it. +- Try making your allocation using pure Vulkan functions rather than VMA and see if the bug persists. + +I found some compilation warnings. How can we fix them? + +Seeing compiler warnings may be annoying to some developers, +but it is a design decision to not fix all of them. +Due to the nature of the C++ language, certain preprocessor macros can make some variables unused, +function parameters unreferenced, or conditional expressions constant in some configurations. +The code of this library should not be bigger or more complicated just to silence these warnings. +It is recommended to disable such warnings instead. +For more information, see [Features not supported](@ref general_considerations_features_not_supported). + +However, if you observe a warning that is really dangerous, e.g., +about an implicit conversion from a larger to a smaller integer type, please report it and it will be fixed ASAP. + + +\page quick_start Quick start + +\section quick_start_project_setup Project setup + +Vulkan Memory Allocator comes in form of a "stb-style" single header file. +While you can pull the entire repository e.g. as Git module, there is also Cmake script provided, +you don't need to build it as a separate library project. +You can add file "vk_mem_alloc.h" directly to your project and submit it to code repository next to your other source files. + +"Single header" doesn't mean that everything is contained in C/C++ declarations, +like it tends to be in case of inline functions or C++ templates. +It means that implementation is bundled with interface in a single file and needs to be extracted using preprocessor macro. +If you don't do it properly, it will result in linker errors. + +To do it properly: + +-# Include "vk_mem_alloc.h" file in each CPP file where you want to use the library. + This includes declarations of all members of the library. +-# In exactly one CPP file define following macro before this include. + It enables also internal definitions. + +\code +#define VMA_IMPLEMENTATION +#include "vk_mem_alloc.h" +\endcode + +It may be a good idea to create dedicated CPP file just for this purpose, e.g. "VmaUsage.cpp". + +This library includes header ``, which in turn +includes `` on Windows. If you need some specific macros defined +before including these headers (like `WIN32_LEAN_AND_MEAN` or +`WINVER` for Windows, `VK_USE_PLATFORM_WIN32_KHR` for Vulkan), you must define +them before every `#include` of this library. +It may be a good idea to create a dedicate header file for this purpose, e.g. "VmaUsage.h", +that will be included in other source files instead of VMA header directly. + +This library is written in C++, but has C-compatible interface. +Thus, you can include and use "vk_mem_alloc.h" in C or C++ code, but full +implementation with `VMA_IMPLEMENTATION` macro must be compiled as C++, NOT as C. +Some features of C++14 are used and required. Features of C++20 are used optionally when available. +Some headers of standard C and C++ library are used, but STL containers, RTTI, or C++ exceptions are not used. + + +\section quick_start_initialization Initialization + +VMA offers library interface in a style similar to Vulkan, with object handles like #VmaAllocation, +structures describing parameters of objects to be created like #VmaAllocationCreateInfo, +and errors codes returned from functions using `VkResult` type. + +The first and the main object that needs to be created is #VmaAllocator. +It represents the initialization of the entire library. +Only one such object should be created per `VkDevice`. +You should create it at program startup, after `VkDevice` was created, and before any device memory allocator needs to be made. +It must be destroyed before `VkDevice` is destroyed. + +At program startup: + +-# Initialize Vulkan to have `VkInstance`, `VkPhysicalDevice`, `VkDevice` object. +-# Fill VmaAllocatorCreateInfo structure and call vmaCreateAllocator() to create #VmaAllocator object. + +Only members `physicalDevice`, `device`, `instance` are required. +However, you should inform the library which Vulkan version do you use by setting +VmaAllocatorCreateInfo::vulkanApiVersion and which extensions did you enable +by setting VmaAllocatorCreateInfo::flags. +Otherwise, VMA would use only features of Vulkan 1.0 core with no extensions. +See below for details. + +\subsection quick_start_initialization_selecting_vulkan_version Selecting Vulkan version + +VMA supports Vulkan version down to 1.0, for backward compatibility. +If you want to use higher version, you need to inform the library about it. +This is a two-step process. + +Step 1: Compile time. By default, VMA compiles with code supporting the highest +Vulkan version found in the included `` that is also supported by the library. +If this is OK, you don't need to do anything. +However, if you want to compile VMA as if only some lower Vulkan version was available, +define macro `VMA_VULKAN_VERSION` before every `#include "vk_mem_alloc.h"`. +It should have decimal numeric value in form of ABBBCCC, where A = major, BBB = minor, CCC = patch Vulkan version. +For example, to compile against Vulkan 1.2: + +\code +#define VMA_VULKAN_VERSION 1002000 // Vulkan 1.2 +#include "vk_mem_alloc.h" +\endcode + +Step 2: Runtime. Even when compiled with higher Vulkan version available, +VMA can use only features of a lower version, which is configurable during creation of the #VmaAllocator object. +By default, only Vulkan 1.0 is used. +To initialize the allocator with support for higher Vulkan version, you need to set member +VmaAllocatorCreateInfo::vulkanApiVersion to an appropriate value, e.g. using constants like `VK_API_VERSION_1_2`. +See code sample below. + +\subsection quick_start_initialization_importing_vulkan_functions Importing Vulkan functions + +You may need to configure importing Vulkan functions. There are 4 ways to do this: + +-# **If you link with Vulkan static library** (e.g. "vulkan-1.lib" on Windows): + - You don't need to do anything. + - VMA will use these, as macro `VMA_STATIC_VULKAN_FUNCTIONS` is defined to 1 by default. +-# **If you want VMA to fetch pointers to Vulkan functions dynamically** using `vkGetInstanceProcAddr`, + `vkGetDeviceProcAddr` (this is the option presented in the example below): + - Define `VMA_STATIC_VULKAN_FUNCTIONS` to 0, `VMA_DYNAMIC_VULKAN_FUNCTIONS` to 1. + - Provide pointers to these two functions via VmaVulkanFunctions::vkGetInstanceProcAddr, + VmaVulkanFunctions::vkGetDeviceProcAddr. + - The library will fetch pointers to all other functions it needs internally. +-# **If you fetch pointers to all Vulkan functions in a custom way**: + - Define `VMA_STATIC_VULKAN_FUNCTIONS` and `VMA_DYNAMIC_VULKAN_FUNCTIONS` to 0. + - Pass these pointers via structure #VmaVulkanFunctions. +-# **If you use [volk library](https://github.com/zeux/volk)**: + - Define `VMA_STATIC_VULKAN_FUNCTIONS` and `VMA_DYNAMIC_VULKAN_FUNCTIONS` to 0. + - Use function vmaImportVulkanFunctionsFromVolk() to fill in the structure #VmaVulkanFunctions. + For more information, see the description of this function. + +\subsection quick_start_initialization_enabling_extensions Enabling extensions + +VMA can automatically use following Vulkan extensions. +If you found them available on the selected physical device and you enabled them +while creating `VkInstance` / `VkDevice` object, inform VMA about their availability +by setting appropriate flags in VmaAllocatorCreateInfo::flags. + +Vulkan extension | VMA flag +------------------------------|----------------------------------------------------- +VK_KHR_dedicated_allocation | #VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT +VK_KHR_bind_memory2 | #VMA_ALLOCATOR_CREATE_KHR_BIND_MEMORY2_BIT +VK_KHR_maintenance4 | #VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE4_BIT +VK_KHR_maintenance5 | #VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE5_BIT +VK_EXT_memory_budget | #VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT +VK_KHR_buffer_device_address | #VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT +VK_EXT_memory_priority | #VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT +VK_AMD_device_coherent_memory | #VMA_ALLOCATOR_CREATE_AMD_DEVICE_COHERENT_MEMORY_BIT +VK_KHR_external_memory_win32 | #VMA_ALLOCATOR_CREATE_KHR_EXTERNAL_MEMORY_WIN32_BIT + +Example with fetching pointers to Vulkan functions dynamically: + +\code +#define VMA_STATIC_VULKAN_FUNCTIONS 0 +#define VMA_DYNAMIC_VULKAN_FUNCTIONS 1 +#include "vk_mem_alloc.h" + +... + +VmaVulkanFunctions vulkanFunctions = {}; +vulkanFunctions.vkGetInstanceProcAddr = &vkGetInstanceProcAddr; +vulkanFunctions.vkGetDeviceProcAddr = &vkGetDeviceProcAddr; + +VmaAllocatorCreateInfo allocatorCreateInfo = {}; +allocatorCreateInfo.flags = VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT; +allocatorCreateInfo.vulkanApiVersion = VK_API_VERSION_1_2; +allocatorCreateInfo.physicalDevice = physicalDevice; +allocatorCreateInfo.device = device; +allocatorCreateInfo.instance = instance; +allocatorCreateInfo.pVulkanFunctions = &vulkanFunctions; + +VmaAllocator allocator; +vmaCreateAllocator(&allocatorCreateInfo, &allocator); + +// Entire program... + +// At the end, don't forget to: +vmaDestroyAllocator(allocator); +\endcode + + +\subsection quick_start_initialization_other_config Other configuration options + +There are additional configuration options available through preprocessor macros that you can define +before including VMA header and through parameters passed in #VmaAllocatorCreateInfo. +They include a possibility to use your own callbacks for host memory allocations (`VkAllocationCallbacks`), +callbacks for device memory allocations (instead of `vkAllocateMemory`, `vkFreeMemory`), +or your custom `VMA_ASSERT` macro, among others. +For more information, see: @ref configuration. + + +\section quick_start_resource_allocation Resource allocation + +When you want to create a buffer or image: + +-# Fill `VkBufferCreateInfo` / `VkImageCreateInfo` structure. +-# Fill VmaAllocationCreateInfo structure. +-# Call vmaCreateBuffer() / vmaCreateImage() to get `VkBuffer`/`VkImage` with memory + already allocated and bound to it, plus #VmaAllocation objects that represents its underlying memory. + +\code +VkBufferCreateInfo bufferInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +bufferInfo.size = 65536; +bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + +VmaAllocationCreateInfo allocInfo = {}; +allocInfo.usage = VMA_MEMORY_USAGE_AUTO; + +VkBuffer buffer; +VmaAllocation allocation; +vmaCreateBuffer(allocator, &bufferInfo, &allocInfo, &buffer, &allocation, nullptr); +\endcode + +Don't forget to destroy your buffer and allocation objects when no longer needed: + +\code +vmaDestroyBuffer(allocator, buffer, allocation); +\endcode + +If you need to map the buffer, you must set flag +#VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT or #VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT +in VmaAllocationCreateInfo::flags. +There are many additional parameters that can control the choice of memory type to be used for the allocation +and other features. +For more information, see documentation chapters: @ref choosing_memory_type, @ref memory_mapping. + + +\page choosing_memory_type Choosing memory type + +Physical devices in Vulkan support various combinations of memory heaps and +types. Help with choosing correct and optimal memory type for your specific +resource is one of the key features of this library. You can use it by filling +appropriate members of VmaAllocationCreateInfo structure, as described below. +You can also combine multiple methods. + +-# If you just want to find memory type index that meets your requirements, you + can use function: vmaFindMemoryTypeIndexForBufferInfo(), + vmaFindMemoryTypeIndexForImageInfo(), vmaFindMemoryTypeIndex(). +-# If you want to allocate a region of device memory without association with any + specific image or buffer, you can use function vmaAllocateMemory(). Usage of + this function is not recommended and usually not needed. + vmaAllocateMemoryPages() function is also provided for creating multiple allocations at once, + which may be useful for sparse binding. +-# If you already have a buffer or an image created, you want to allocate memory + for it and then you will bind it yourself, you can use function + vmaAllocateMemoryForBuffer(), vmaAllocateMemoryForImage(). + For binding you should use functions: vmaBindBufferMemory(), vmaBindImageMemory() + or their extended versions: vmaBindBufferMemory2(), vmaBindImageMemory2(). +-# If you want to create a buffer or an image, allocate memory for it, and bind + them together, all in one call, you can use function vmaCreateBuffer(), + vmaCreateImage(). + This is the easiest and recommended way to use this library! + +When using 3. or 4., the library internally queries Vulkan for memory types +supported for that buffer or image (function `vkGetBufferMemoryRequirements()`) +and uses only one of these types. + +If no memory type can be found that meets all the requirements, these functions +return `VK_ERROR_FEATURE_NOT_PRESENT`. + +You can leave VmaAllocationCreateInfo structure completely filled with zeros. +It means no requirements are specified for memory type. +It is valid, although not very useful. + +\section choosing_memory_type_usage Usage + +The easiest way to specify memory requirements is to fill member +VmaAllocationCreateInfo::usage using one of the values of enum #VmaMemoryUsage. +It defines high level, common usage types. +Since version 3 of the library, it is recommended to use #VMA_MEMORY_USAGE_AUTO to let it select best memory type for your resource automatically. + +For example, if you want to create a uniform buffer that will be filled using +transfer only once or infrequently and then used for rendering every frame as a uniform buffer, you can +do it using following code. The buffer will most likely end up in a memory type with +`VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT` to be fast to access by the GPU device. + +\code +VkBufferCreateInfo bufferInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +bufferInfo.size = 65536; +bufferInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + +VmaAllocationCreateInfo allocInfo = {}; +allocInfo.usage = VMA_MEMORY_USAGE_AUTO; + +VkBuffer buffer; +VmaAllocation allocation; +vmaCreateBuffer(allocator, &bufferInfo, &allocInfo, &buffer, &allocation, nullptr); +\endcode + +If you have a preference for putting the resource in GPU (device) memory or CPU (host) memory +on systems with discrete graphics card that have the memories separate, you can use +#VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE or #VMA_MEMORY_USAGE_AUTO_PREFER_HOST. + +When using `VMA_MEMORY_USAGE_AUTO*` while you want to map the allocated memory, +you also need to specify one of the host access flags: +#VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT or #VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT. +This will help the library decide about preferred memory type to ensure it has `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` +so you can map it. + +For example, a staging buffer that will be filled via mapped pointer and then +used as a source of transfer to the buffer described previously can be created like this. +It will likely end up in a memory type that is `HOST_VISIBLE` and `HOST_COHERENT` +but not `HOST_CACHED` (meaning uncached, write-combined) and not `DEVICE_LOCAL` (meaning system RAM). + +\code +VkBufferCreateInfo stagingBufferInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +stagingBufferInfo.size = 65536; +stagingBufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + +VmaAllocationCreateInfo stagingAllocInfo = {}; +stagingAllocInfo.usage = VMA_MEMORY_USAGE_AUTO; +stagingAllocInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + +VkBuffer stagingBuffer; +VmaAllocation stagingAllocation; +vmaCreateBuffer(allocator, &stagingBufferInfo, &stagingAllocInfo, &stagingBuffer, &stagingAllocation, nullptr); +\endcode + +For more examples of creating different kinds of resources, see chapter \ref usage_patterns. +See also: @ref memory_mapping. + +Usage values `VMA_MEMORY_USAGE_AUTO*` are legal to use only when the library knows +about the resource being created by having `VkBufferCreateInfo` / `VkImageCreateInfo` passed, +so they work with functions like: vmaCreateBuffer(), vmaCreateImage(), vmaFindMemoryTypeIndexForBufferInfo() etc. +If you allocate raw memory using function vmaAllocateMemory(), you have to use other means of selecting +memory type, as described below. + +\note +Old usage values (`VMA_MEMORY_USAGE_GPU_ONLY`, `VMA_MEMORY_USAGE_CPU_ONLY`, +`VMA_MEMORY_USAGE_CPU_TO_GPU`, `VMA_MEMORY_USAGE_GPU_TO_CPU`, `VMA_MEMORY_USAGE_CPU_COPY`) +are still available and work same way as in previous versions of the library +for backward compatibility, but they are deprecated. + +\section choosing_memory_type_required_preferred_flags Required and preferred flags + +You can specify more detailed requirements by filling members +VmaAllocationCreateInfo::requiredFlags and VmaAllocationCreateInfo::preferredFlags +with a combination of bits from enum `VkMemoryPropertyFlags`. For example, +if you want to create a buffer that will be persistently mapped on host (so it +must be `HOST_VISIBLE`) and preferably will also be `HOST_COHERENT` and `HOST_CACHED`, +use following code: + +\code +VmaAllocationCreateInfo allocInfo = {}; +allocInfo.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; +allocInfo.preferredFlags = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT; +allocInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT; + +VkBuffer buffer; +VmaAllocation allocation; +vmaCreateBuffer(allocator, &bufferInfo, &allocInfo, &buffer, &allocation, nullptr); +\endcode + +A memory type is chosen that has all the required flags and as many preferred +flags set as possible. + +Value passed in VmaAllocationCreateInfo::usage is internally converted to a set of required and preferred flags, +plus some extra "magic" (heuristics). + +\section choosing_memory_type_explicit_memory_types Explicit memory types + +If you inspected memory types available on the physical device and you have +a preference for memory types that you want to use, you can fill member +VmaAllocationCreateInfo::memoryTypeBits. It is a bit mask, where each bit set +means that a memory type with that index is allowed to be used for the +allocation. Special value 0, just like `UINT32_MAX`, means there are no +restrictions to memory type index. + +Please note that this member is NOT just a memory type index. +Still you can use it to choose just one, specific memory type. +For example, if you already determined that your buffer should be created in +memory type 2, use following code: + +\code +uint32_t memoryTypeIndex = 2; + +VmaAllocationCreateInfo allocInfo = {}; +allocInfo.memoryTypeBits = 1U << memoryTypeIndex; + +VkBuffer buffer; +VmaAllocation allocation; +vmaCreateBuffer(allocator, &bufferInfo, &allocInfo, &buffer, &allocation, nullptr); +\endcode + +You can also use this parameter to exclude some memory types. +If you inspect memory heaps and types available on the current physical device and +you determine that for some reason you don't want to use a specific memory type for the allocation, +you can enable automatic memory type selection but exclude certain memory type or types +by setting all bits of `memoryTypeBits` to 1 except the ones you choose. + +\code +// ... +uint32_t excludedMemoryTypeIndex = 2; +VmaAllocationCreateInfo allocInfo = {}; +allocInfo.usage = VMA_MEMORY_USAGE_AUTO; +allocInfo.memoryTypeBits = ~(1U << excludedMemoryTypeIndex); +// ... +\endcode + + +\section choosing_memory_type_custom_memory_pools Custom memory pools + +If you allocate from custom memory pool, all the ways of specifying memory +requirements described above are not applicable and the aforementioned members +of VmaAllocationCreateInfo structure are ignored. Memory type is selected +explicitly when creating the pool and then used to make all the allocations from +that pool. For further details, see \ref custom_memory_pools. + +\section choosing_memory_type_dedicated_allocations Dedicated allocations + +Memory for allocations is reserved out of larger block of `VkDeviceMemory` +allocated from Vulkan internally. That is the main feature of this whole library. +You can still request a separate memory block to be created for an allocation, +just like you would do in a trivial solution without using any allocator. +In that case, a buffer or image is always bound to that memory at offset 0. +This is called a "dedicated allocation". +You can explicitly request it by using flag #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT. +The library can also internally decide to use dedicated allocation in some cases, e.g.: + +- When the size of the allocation is large. +- When [VK_KHR_dedicated_allocation](@ref vk_khr_dedicated_allocation) extension is enabled + and it reports that dedicated allocation is required or recommended for the resource. +- When allocation of next big memory block fails due to not enough device memory, + but allocation with the exact requested size succeeds. + + +\page memory_mapping Memory mapping + +To "map memory" in Vulkan means to obtain a CPU pointer to `VkDeviceMemory`, +to be able to read from it or write to it in CPU code. +Mapping is possible only of memory allocated from a memory type that has +`VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` flag. +Functions `vkMapMemory()`, `vkUnmapMemory()` are designed for this purpose. +You can use them directly with memory allocated by this library, +but it is not recommended because of following issue: +Mapping the same `VkDeviceMemory` block multiple times is illegal - only one mapping at a time is allowed. +This includes mapping disjoint regions. Mapping is not reference-counted internally by Vulkan. +It is also not thread-safe. +Because of this, Vulkan Memory Allocator provides following facilities: + +\note If you want to be able to map an allocation, you need to specify one of the flags +#VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT or #VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT +in VmaAllocationCreateInfo::flags. These flags are required for an allocation to be mappable +when using #VMA_MEMORY_USAGE_AUTO or other `VMA_MEMORY_USAGE_AUTO*` enum values. +For other usage values they are ignored and every such allocation made in `HOST_VISIBLE` memory type is mappable, +but these flags can still be used for consistency. + +\section memory_mapping_copy_functions Copy functions + +The easiest way to copy data from a host pointer to an allocation is to use convenience function vmaCopyMemoryToAllocation(). +It automatically maps the Vulkan memory temporarily (if not already mapped), performs `memcpy`, +and calls `vkFlushMappedMemoryRanges` (if required - if memory type is not `HOST_COHERENT`). + +It is also the safest one, because using `memcpy` avoids a risk of accidentally introducing memory reads +(e.g. by doing `pMappedVectors[i] += v`), which may be very slow on memory types that are not `HOST_CACHED`. + +\code +struct ConstantBuffer +{ + ... +}; +ConstantBuffer constantBufferData = ... + +VkBufferCreateInfo bufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +bufCreateInfo.size = sizeof(ConstantBuffer); +bufCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; +allocCreateInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + +VkBuffer buf; +VmaAllocation alloc; +vmaCreateBuffer(allocator, &bufCreateInfo, &allocCreateInfo, &buf, &alloc, nullptr); + +vmaCopyMemoryToAllocation(allocator, &constantBufferData, alloc, 0, sizeof(ConstantBuffer)); +\endcode + +Copy in the other direction - from an allocation to a host pointer can be performed the same way using function vmaCopyAllocationToMemory(). + +\section memory_mapping_mapping_functions Mapping functions + +The library provides following functions for mapping of a specific allocation: vmaMapMemory(), vmaUnmapMemory(). +They are safer and more convenient to use than standard Vulkan functions. +You can map an allocation multiple times simultaneously - mapping is reference-counted internally. +You can also map different allocations simultaneously regardless of whether they use the same `VkDeviceMemory` block. +The way it is implemented is that the library always maps entire memory block, not just region of the allocation. +For further details, see description of vmaMapMemory() function. +Example: + +\code +// Having these objects initialized: +struct ConstantBuffer +{ + ... +}; +ConstantBuffer constantBufferData = ... + +VmaAllocator allocator = ... +VkBuffer constantBuffer = ... +VmaAllocation constantBufferAllocation = ... + +// You can map and fill your buffer using following code: + +void* mappedData; +vmaMapMemory(allocator, constantBufferAllocation, &mappedData); +memcpy(mappedData, &constantBufferData, sizeof(constantBufferData)); +vmaUnmapMemory(allocator, constantBufferAllocation); +\endcode + +When mapping, you may see a warning from Vulkan validation layer similar to this one: + +Mapping an image with layout VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL can result in undefined behavior if this memory is used by the device. Only GENERAL or PREINITIALIZED should be used. + +It happens because the library maps entire `VkDeviceMemory` block, where different +types of images and buffers may end up together, especially on GPUs with unified memory like Intel. +You can safely ignore it if you are sure you access only memory of the intended +object that you wanted to map. + + +\section memory_mapping_persistently_mapped_memory Persistently mapped memory + +Keeping your memory persistently mapped is generally OK in Vulkan. +You don't need to unmap it before using its data on the GPU. +The library provides a special feature designed for that: +Allocations made with #VMA_ALLOCATION_CREATE_MAPPED_BIT flag set in +VmaAllocationCreateInfo::flags stay mapped all the time, +so you can just access CPU pointer to it any time +without a need to call any "map" or "unmap" function. +Example: + +\code +VkBufferCreateInfo bufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +bufCreateInfo.size = sizeof(ConstantBuffer); +bufCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; +allocCreateInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT; + +VkBuffer buf; +VmaAllocation alloc; +VmaAllocationInfo allocInfo; +vmaCreateBuffer(allocator, &bufCreateInfo, &allocCreateInfo, &buf, &alloc, &allocInfo); + +// Buffer is already mapped. You can access its memory. +memcpy(allocInfo.pMappedData, &constantBufferData, sizeof(constantBufferData)); +\endcode + +\note #VMA_ALLOCATION_CREATE_MAPPED_BIT by itself doesn't guarantee that the allocation will end up +in a mappable memory type. +For this, you need to also specify #VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT or +#VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT. +#VMA_ALLOCATION_CREATE_MAPPED_BIT only guarantees that if the memory is `HOST_VISIBLE`, the allocation will be mapped on creation. +For an example of how to make use of this fact, see section \ref usage_patterns_advanced_data_uploading. + +\section memory_mapping_cache_control Cache flush and invalidate + +Memory in Vulkan doesn't need to be unmapped before using it on GPU, +but unless a memory types has `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` flag set, +you need to manually **invalidate** cache before reading of mapped pointer +and **flush** cache after writing to mapped pointer. +Map/unmap operations don't do that automatically. +Vulkan provides following functions for this purpose `vkFlushMappedMemoryRanges()`, +`vkInvalidateMappedMemoryRanges()`, but this library provides more convenient +functions that refer to given allocation object: vmaFlushAllocation(), +vmaInvalidateAllocation(), +or multiple objects at once: vmaFlushAllocations(), vmaInvalidateAllocations(). + +Regions of memory specified for flush/invalidate must be aligned to +`VkPhysicalDeviceLimits::nonCoherentAtomSize`. This is automatically ensured by the library. +In any memory type that is `HOST_VISIBLE` but not `HOST_COHERENT`, all allocations +within blocks are aligned to this value, so their offsets are always multiply of +`nonCoherentAtomSize` and two different allocations never share same "line" of this size. + +Also, Windows drivers from all 3 PC GPU vendors (AMD, Intel, NVIDIA) +currently provide `HOST_COHERENT` flag on all memory types that are +`HOST_VISIBLE`, so on PC you may not need to bother. + + +\page staying_within_budget Staying within budget + +When developing a graphics-intensive game or program, it is important to avoid allocating +more GPU memory than it is physically available. When the memory is over-committed, +various bad things can happen, depending on the specific GPU, graphics driver, and +operating system: + +- It may just work without any problems. +- The application may slow down because some memory blocks are moved to system RAM + and the GPU has to access them through PCI Express bus. +- A new allocation may take very long time to complete, even few seconds, and possibly + freeze entire system. +- The new allocation may fail with `VK_ERROR_OUT_OF_DEVICE_MEMORY`. +- It may even result in GPU crash (TDR), observed as `VK_ERROR_DEVICE_LOST` + returned somewhere later. + +\section staying_within_budget_querying_for_budget Querying for budget + +To query for current memory usage and available budget, use function vmaGetHeapBudgets(). +Returned structure #VmaBudget contains quantities expressed in bytes, per Vulkan memory heap. + +Please note that this function returns different information and works faster than +vmaCalculateStatistics(). vmaGetHeapBudgets() can be called every frame or even before every +allocation, while vmaCalculateStatistics() is intended to be used rarely, +only to obtain statistical information, e.g. for debugging purposes. + +It is recommended to use VK_EXT_memory_budget device extension to obtain information +about the budget from Vulkan device. VMA is able to use this extension automatically. +When not enabled, the allocator behaves same way, but then it estimates current usage +and available budget based on its internal information and Vulkan memory heap sizes, +which may be less precise. In order to use this extension: + +1. Make sure extensions VK_EXT_memory_budget and VK_KHR_get_physical_device_properties2 + required by it are available and enable them. Please note that the first is a device + extension and the second is instance extension! +2. Use flag #VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT when creating #VmaAllocator object. +3. Make sure to call vmaSetCurrentFrameIndex() every frame. Budget is queried from + Vulkan inside of it to avoid overhead of querying it with every allocation. + +\section staying_within_budget_controlling_memory_usage Controlling memory usage + +There are many ways in which you can try to stay within the budget. + +First, when making new allocation requires allocating a new memory block, the library +tries not to exceed the budget automatically. If a block with default recommended size +(e.g. 256 MB) would go over budget, a smaller block is allocated, possibly even +dedicated memory for just this resource. + +If the size of the requested resource plus current memory usage is more than the +budget, by default the library still tries to create it, leaving it to the Vulkan +implementation whether the allocation succeeds or fails. You can change this behavior +by using #VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT flag. With it, the allocation is +not made if it would exceed the budget or if the budget is already exceeded. +VMA then tries to make the allocation from the next eligible Vulkan memory type. +If all of them fail, the call then fails with `VK_ERROR_OUT_OF_DEVICE_MEMORY`. +Example usage pattern may be to pass the #VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT flag +when creating resources that are not essential for the application (e.g. the texture +of a specific object) and not to pass it when creating critically important resources +(e.g. render targets). + +On AMD graphics cards there is a custom vendor extension available: VK_AMD_memory_overallocation_behavior +that allows to control the behavior of the Vulkan implementation in out-of-memory cases - +whether it should fail with an error code or still allow the allocation. +Usage of this extension involves only passing extra structure on Vulkan device creation, +so it is out of scope of this library. + +Finally, you can also use #VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT flag to make sure +a new allocation is created only when it fits inside one of the existing memory blocks. +If it would require to allocate a new block, if fails instead with `VK_ERROR_OUT_OF_DEVICE_MEMORY`. +This also ensures that the function call is very fast because it never goes to Vulkan +to obtain a new block. + +\note Creating \ref custom_memory_pools with VmaPoolCreateInfo::minBlockCount +set to more than 0 will currently try to allocate memory blocks without checking whether they +fit within budget. + + +\page resource_aliasing Resource aliasing (overlap) + +New explicit graphics APIs (Vulkan and Direct3D 12), thanks to manual memory +management, give an opportunity to alias (overlap) multiple resources in the +same region of memory - a feature not available in the old APIs (Direct3D 11, OpenGL). +It can be useful to save video memory, but it must be used with caution. + +For example, if you know the flow of your whole render frame in advance, you +are going to use some intermediate textures or buffers only during a small range of render passes, +and you know these ranges don't overlap in time, you can bind these resources to +the same place in memory, even if they have completely different parameters (width, height, format etc.). + +![Resource aliasing (overlap)](../gfx/Aliasing.png) + +Such scenario is possible using VMA, but you need to create your images manually. +Then you need to calculate parameters of an allocation to be made using formula: + +- allocation size = max(size of each image) +- allocation alignment = max(alignment of each image) +- allocation memoryTypeBits = bitwise AND(memoryTypeBits of each image) + +Following example shows two different images bound to the same place in memory, +allocated to fit largest of them. + +\code +// A 512x512 texture to be sampled. +VkImageCreateInfo img1CreateInfo = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; +img1CreateInfo.imageType = VK_IMAGE_TYPE_2D; +img1CreateInfo.extent.width = 512; +img1CreateInfo.extent.height = 512; +img1CreateInfo.extent.depth = 1; +img1CreateInfo.mipLevels = 10; +img1CreateInfo.arrayLayers = 1; +img1CreateInfo.format = VK_FORMAT_R8G8B8A8_SRGB; +img1CreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL; +img1CreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +img1CreateInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; +img1CreateInfo.samples = VK_SAMPLE_COUNT_1_BIT; + +// A full screen texture to be used as color attachment. +VkImageCreateInfo img2CreateInfo = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; +img2CreateInfo.imageType = VK_IMAGE_TYPE_2D; +img2CreateInfo.extent.width = 1920; +img2CreateInfo.extent.height = 1080; +img2CreateInfo.extent.depth = 1; +img2CreateInfo.mipLevels = 1; +img2CreateInfo.arrayLayers = 1; +img2CreateInfo.format = VK_FORMAT_R8G8B8A8_UNORM; +img2CreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL; +img2CreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +img2CreateInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; +img2CreateInfo.samples = VK_SAMPLE_COUNT_1_BIT; + +VkImage img1; +res = vkCreateImage(device, &img1CreateInfo, nullptr, &img1); +VkImage img2; +res = vkCreateImage(device, &img2CreateInfo, nullptr, &img2); + +VkMemoryRequirements img1MemReq; +vkGetImageMemoryRequirements(device, img1, &img1MemReq); +VkMemoryRequirements img2MemReq; +vkGetImageMemoryRequirements(device, img2, &img2MemReq); + +VkMemoryRequirements finalMemReq = {}; +finalMemReq.size = std::max(img1MemReq.size, img2MemReq.size); +finalMemReq.alignment = std::max(img1MemReq.alignment, img2MemReq.alignment); +finalMemReq.memoryTypeBits = img1MemReq.memoryTypeBits & img2MemReq.memoryTypeBits; +// Validate if(finalMemReq.memoryTypeBits != 0) + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.preferredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + +VmaAllocation alloc; +res = vmaAllocateMemory(allocator, &finalMemReq, &allocCreateInfo, &alloc, nullptr); + +res = vmaBindImageMemory(allocator, alloc, img1); +res = vmaBindImageMemory(allocator, alloc, img2); + +// You can use img1, img2 here, but not at the same time! + +vmaFreeMemory(allocator, alloc); +vkDestroyImage(allocator, img2, nullptr); +vkDestroyImage(allocator, img1, nullptr); +\endcode + +VMA also provides convenience functions that create a buffer or image and bind it to memory +represented by an existing #VmaAllocation: +vmaCreateAliasingBuffer(), vmaCreateAliasingBuffer2(), +vmaCreateAliasingImage(), vmaCreateAliasingImage2(). +Versions with "2" offer additional parameter `allocationLocalOffset`. + +Remember that using resources that alias in memory requires proper synchronization. +You need to issue a memory barrier to make sure commands that use `img1` and `img2` +don't overlap on GPU timeline. +You also need to treat a resource after aliasing as uninitialized - containing garbage data. +For example, if you use `img1` and then want to use `img2`, you need to issue +an image memory barrier for `img2` with `oldLayout` = `VK_IMAGE_LAYOUT_UNDEFINED`. + +Additional considerations: + +- Vulkan also allows to interpret contents of memory between aliasing resources consistently in some cases. +See chapter 11.8. "Memory Aliasing" of Vulkan specification or `VK_IMAGE_CREATE_ALIAS_BIT` flag. +- You can create more complex layout where different images and buffers are bound +at different offsets inside one large allocation. For example, one can imagine +a big texture used in some render passes, aliasing with a set of many small buffers +used between in some further passes. To bind a resource at non-zero offset in an allocation, +use vmaBindBufferMemory2() / vmaBindImageMemory2(). +- Before allocating memory for the resources you want to alias, check `memoryTypeBits` +returned in memory requirements of each resource to make sure the bits overlap. +Some GPUs may expose multiple memory types suitable e.g. only for buffers or +images with `COLOR_ATTACHMENT` usage, so the sets of memory types supported by your +resources may be disjoint. Aliasing them is not possible in that case. + + +\page custom_memory_pools Custom memory pools + +A memory pool contains a number of `VkDeviceMemory` blocks. +The library automatically creates and manages default pool for each memory type available on the device. +Default memory pool automatically grows in size. +Size of allocated blocks is also variable and managed automatically. +You are using default pools whenever you leave VmaAllocationCreateInfo::pool = null. + +You can create custom pool and allocate memory out of it. +It can be useful if you want to: + +- Keep certain kind of allocations separate from others. +- Enforce particular, fixed size of Vulkan memory blocks. +- Limit maximum amount of Vulkan memory allocated for that pool. +- Reserve minimum or fixed amount of Vulkan memory always preallocated for that pool. +- Use extra parameters for a set of your allocations that are available in #VmaPoolCreateInfo but not in + #VmaAllocationCreateInfo - e.g., custom minimum alignment, custom `pNext` chain. +- Perform defragmentation on a specific subset of your allocations. + +To use custom memory pools: + +-# Fill VmaPoolCreateInfo structure. +-# Call vmaCreatePool() to obtain #VmaPool handle. +-# When making an allocation, set VmaAllocationCreateInfo::pool to this handle. + You don't need to specify any other parameters of this structure, like `usage`. + +Example: + +\code +// Find memoryTypeIndex for the pool. +VkBufferCreateInfo sampleBufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +sampleBufCreateInfo.size = 0x10000; // Doesn't matter. +sampleBufCreateInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + +VmaAllocationCreateInfo sampleAllocCreateInfo = {}; +sampleAllocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; + +uint32_t memTypeIndex; +VkResult res = vmaFindMemoryTypeIndexForBufferInfo(allocator, + &sampleBufCreateInfo, &sampleAllocCreateInfo, &memTypeIndex); +// Check res... + +// Create a pool that can have at most 2 blocks, 128 MiB each. +VmaPoolCreateInfo poolCreateInfo = {}; +poolCreateInfo.memoryTypeIndex = memTypeIndex; +poolCreateInfo.blockSize = 128ULL * 1024 * 1024; +poolCreateInfo.maxBlockCount = 2; + +VmaPool pool; +res = vmaCreatePool(allocator, &poolCreateInfo, &pool); +// Check res... + +// Allocate a buffer out of it. +VkBufferCreateInfo bufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +bufCreateInfo.size = 1024; +bufCreateInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.pool = pool; + +VkBuffer buf; +VmaAllocation alloc; +res = vmaCreateBuffer(allocator, &bufCreateInfo, &allocCreateInfo, &buf, &alloc, nullptr); +// Check res... +\endcode + +You have to free all allocations made from this pool before destroying it. + +\code +vmaDestroyBuffer(allocator, buf, alloc); +vmaDestroyPool(allocator, pool); +\endcode + +New versions of this library support creating dedicated allocations in custom pools. +It is supported only when VmaPoolCreateInfo::blockSize = 0. +To use this feature, set VmaAllocationCreateInfo::pool to the pointer to your custom pool and +VmaAllocationCreateInfo::flags to #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT. + + +\section custom_memory_pools_MemTypeIndex Choosing memory type index + +When creating a pool, you must explicitly specify memory type index. +To find the one suitable for your buffers or images, you can use helper functions +vmaFindMemoryTypeIndexForBufferInfo(), vmaFindMemoryTypeIndexForImageInfo(). +You need to provide structures with example parameters of buffers or images +that you are going to create in that pool. + +\code +VkBufferCreateInfo exampleBufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +exampleBufCreateInfo.size = 1024; // Doesn't matter +exampleBufCreateInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; + +uint32_t memTypeIndex; +vmaFindMemoryTypeIndexForBufferInfo(allocator, &exampleBufCreateInfo, &allocCreateInfo, &memTypeIndex); + +VmaPoolCreateInfo poolCreateInfo = {}; +poolCreateInfo.memoryTypeIndex = memTypeIndex; +// ... +\endcode + +When creating buffers/images allocated in that pool, provide following parameters: + +- `VkBufferCreateInfo`: Prefer to pass same parameters as above. + Otherwise you risk creating resources in a memory type that is not suitable for them, which may result in undefined behavior. + Using different `VK_BUFFER_USAGE_` flags may work, but you shouldn't create images in a pool intended for buffers + or the other way around. +- VmaAllocationCreateInfo: You don't need to pass same parameters. Fill only `pool` member. + Other members are ignored anyway. + + +\section custom_memory_pools_when_not_use When not to use custom pools + +Custom pools are commonly overused by VMA users. +While it may feel natural to keep some logical groups of resources separate in memory, +in most cases it does more harm than good. +Using custom pool shouldn't be your first choice. +Instead, please make all allocations from default pools first and only use custom pools +if you can prove and measure that it is beneficial in some way, +e.g. it results in lower memory usage, better performance, etc. + +Using custom pools has disadvantages: + +- Each pool has its own collection of `VkDeviceMemory` blocks. + Some of them may be partially or even completely empty. + Spreading allocations across multiple pools increases the amount of wasted (allocated but unbound) memory. +- You must manually choose specific memory type to be used by a custom pool (set as VmaPoolCreateInfo::memoryTypeIndex). + When using default pools, best memory type for each of your allocations can be selected automatically + using a carefully design algorithm that works across all kinds of GPUs. +- If an allocation from a custom pool at specific memory type fails, entire allocation operation returns failure. + When using default pools, VMA tries another compatible memory type. +- If you set VmaPoolCreateInfo::blockSize != 0, each memory block has the same size, + while default pools start from small blocks and only allocate next blocks larger and larger + up to the preferred block size. + +Many of the common concerns can be addressed in a different way than using custom pools: + +- If you want to keep your allocations of certain size (small versus large) or certain lifetime (transient versus long lived) + separate, you likely don't need to. + VMA uses a high quality allocation algorithm that manages memory well in various cases. + Please measure and check if using custom pools provides a benefit. +- If you want to keep your images and buffers separate, you don't need to. + VMA respects `bufferImageGranularity` limit automatically. +- If you want to keep your mapped and not mapped allocations separate, you don't need to. + VMA respects `nonCoherentAtomSize` limit automatically. + It also maps only those `VkDeviceMemory` blocks that need to map any allocation. + It even tries to keep mappable and non-mappable allocations in separate blocks to minimize the amount of mapped memory. +- If you want to choose a custom size for the default memory block, you can set it globally instead + using VmaAllocatorCreateInfo::preferredLargeHeapBlockSize. +- If you want to select specific memory type for your allocation, + you can set VmaAllocationCreateInfo::memoryTypeBits to `(1U << myMemoryTypeIndex)` instead. +- If you need to create a buffer with certain minimum alignment, you can still do it + using default pools with dedicated function vmaCreateBufferWithAlignment(). + + +\section linear_algorithm Linear allocation algorithm + +Each Vulkan memory block managed by this library has accompanying metadata that +keeps track of used and unused regions. By default, the metadata structure and +algorithm tries to find best place for new allocations among free regions to +optimize memory usage. This way you can allocate and free objects in any order. + +![Default allocation algorithm](../gfx/Linear_allocator_1_algo_default.png) + +Sometimes there is a need to use simpler, linear allocation algorithm. You can +create custom pool that uses such algorithm by adding flag +#VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT to VmaPoolCreateInfo::flags while creating +#VmaPool object. Then an alternative metadata management is used. It always +creates new allocations after last one and doesn't reuse free regions after +allocations freed in the middle. It results in better allocation performance and +less memory consumed by metadata. + +![Linear allocation algorithm](../gfx/Linear_allocator_2_algo_linear.png) + +With this one flag, you can create a custom pool that can be used in many ways: +free-at-once, stack, double stack, and ring buffer. See below for details. +You don't need to specify explicitly which of these options you are going to use - it is detected automatically. + +\subsection linear_algorithm_free_at_once Free-at-once + +In a pool that uses linear algorithm, you still need to free all the allocations +individually, e.g. by using vmaFreeMemory() or vmaDestroyBuffer(). You can free +them in any order. New allocations are always made after last one - free space +in the middle is not reused. However, when you release all the allocation and +the pool becomes empty, allocation starts from the beginning again. This way you +can use linear algorithm to speed up creation of allocations that you are going +to release all at once. + +![Free-at-once](../gfx/Linear_allocator_3_free_at_once.png) + +This mode is also available for pools created with VmaPoolCreateInfo::maxBlockCount +value that allows multiple memory blocks. + +\subsection linear_algorithm_stack Stack + +When you free an allocation that was created last, its space can be reused. +Thanks to this, if you always release allocations in the order opposite to their +creation (LIFO - Last In First Out), you can achieve behavior of a stack. + +![Stack](../gfx/Linear_allocator_4_stack.png) + +This mode is also available for pools created with VmaPoolCreateInfo::maxBlockCount +value that allows multiple memory blocks. + +\subsection linear_algorithm_double_stack Double stack + +The space reserved by a custom pool with linear algorithm may be used by two +stacks: + +- First, default one, growing up from offset 0. +- Second, "upper" one, growing down from the end towards lower offsets. + +To make allocation from the upper stack, add flag #VMA_ALLOCATION_CREATE_UPPER_ADDRESS_BIT +to VmaAllocationCreateInfo::flags. + +![Double stack](../gfx/Linear_allocator_7_double_stack.png) + +Double stack is available only in pools with one memory block - +VmaPoolCreateInfo::maxBlockCount must be 1. Otherwise behavior is undefined. + +When the two stacks' ends meet so there is not enough space between them for a +new allocation, such allocation fails with usual +`VK_ERROR_OUT_OF_DEVICE_MEMORY` error. + +\subsection linear_algorithm_ring_buffer Ring buffer + +When you free some allocations from the beginning and there is not enough free space +for a new one at the end of a pool, allocator's "cursor" wraps around to the +beginning and starts allocation there. Thanks to this, if you always release +allocations in the same order as you created them (FIFO - First In First Out), +you can achieve behavior of a ring buffer / queue. + +![Ring buffer](../gfx/Linear_allocator_5_ring_buffer.png) + +Ring buffer is available only in pools with one memory block - +VmaPoolCreateInfo::maxBlockCount must be 1. Otherwise behavior is undefined. + +\note \ref defragmentation is not supported in custom pools created with #VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT. + + +\page defragmentation Defragmentation + +Interleaved allocations and deallocations of many objects of varying size can +cause fragmentation over time, which can lead to a situation where the library is unable +to find a continuous range of free memory for a new allocation despite there is +enough free space, just scattered across many small free ranges between existing +allocations. + +To mitigate this problem, you can use defragmentation feature. +It doesn't happen automatically though and needs your cooperation, +because VMA is a low level library that only allocates memory. +It cannot recreate buffers and images in a new place as it doesn't remember the contents of `VkBufferCreateInfo` / `VkImageCreateInfo` structures. +It cannot copy their contents as it doesn't record any commands to a command buffer. + +Example: + +\code +VmaDefragmentationInfo defragInfo = {}; +defragInfo.pool = myPool; +defragInfo.flags = VMA_DEFRAGMENTATION_FLAG_ALGORITHM_FAST_BIT; + +VmaDefragmentationContext defragCtx; +VkResult res = vmaBeginDefragmentation(allocator, &defragInfo, &defragCtx); +// Check res... + +for(;;) +{ + VmaDefragmentationPassMoveInfo pass; + res = vmaBeginDefragmentationPass(allocator, defragCtx, &pass); + if(res == VK_SUCCESS) + break; + else if(res != VK_INCOMPLETE) + // Handle error... + + for(uint32_t i = 0; i < pass.moveCount; ++i) + { + // Inspect pass.pMoves[i].srcAllocation, identify what buffer/image it represents. + VmaAllocationInfo allocInfo; + vmaGetAllocationInfo(allocator, pass.pMoves[i].srcAllocation, &allocInfo); + MyEngineResourceData* resData = (MyEngineResourceData*)allocInfo.pUserData; + + // Recreate and bind this buffer/image at: pass.pMoves[i].dstMemory, pass.pMoves[i].dstOffset. + VkImageCreateInfo imgCreateInfo = ... + VkImage newImg; + res = vkCreateImage(device, &imgCreateInfo, nullptr, &newImg); + // Check res... + res = vmaBindImageMemory(allocator, pass.pMoves[i].dstTmpAllocation, newImg); + // Check res... + + // Issue a vkCmdCopyBuffer/vkCmdCopyImage to copy its content to the new place. + vkCmdCopyImage(cmdBuf, resData->img, ..., newImg, ...); + } + + // Make sure the copy commands finished executing. + vkWaitForFences(...); + + // Destroy old buffers/images bound with pass.pMoves[i].srcAllocation. + for(uint32_t i = 0; i < pass.moveCount; ++i) + { + // ... + vkDestroyImage(device, resData->img, nullptr); + } + + // Update appropriate descriptors to point to the new places... + + res = vmaEndDefragmentationPass(allocator, defragCtx, &pass); + if(res == VK_SUCCESS) + break; + else if(res != VK_INCOMPLETE) + // Handle error... +} + +vmaEndDefragmentation(allocator, defragCtx, nullptr); +\endcode + +Although functions like vmaCreateBuffer(), vmaCreateImage(), vmaDestroyBuffer(), vmaDestroyImage() +create/destroy an allocation and a buffer/image at once, these are just a shortcut for +creating the resource, allocating memory, and binding them together. +Defragmentation works on memory allocations only. You must handle the rest manually. +Defragmentation is an iterative process that should repreat "passes" as long as related functions +return `VK_INCOMPLETE` not `VK_SUCCESS`. +In each pass: + +1. vmaBeginDefragmentationPass() function call: + - Calculates and returns the list of allocations to be moved in this pass. + Note this can be a time-consuming process. + - Reserves destination memory for them by creating temporary destination allocations + that you can query for their `VkDeviceMemory` + offset using vmaGetAllocationInfo(). +2. Inside the pass, **you should**: + - Inspect the returned list of allocations to be moved. + - Create new buffers/images and bind them at the returned destination temporary allocations. + - Copy data from source to destination resources if necessary. + - Destroy the source buffers/images, but NOT their allocations. +3. vmaEndDefragmentationPass() function call: + - Frees the source memory reserved for the allocations that are moved. + - Modifies source #VmaAllocation objects that are moved to point to the destination reserved memory. + - Frees `VkDeviceMemory` blocks that became empty. + +Unlike in previous iterations of the defragmentation API, there is no list of "movable" allocations passed as a parameter. +Defragmentation algorithm tries to move all suitable allocations. +You can, however, refuse to move some of them inside a defragmentation pass, by setting +`pass.pMoves[i].operation` to #VMA_DEFRAGMENTATION_MOVE_OPERATION_IGNORE. +This is not recommended and may result in suboptimal packing of the allocations after defragmentation. +If you cannot ensure any allocation can be moved, it is better to keep movable allocations separate in a custom pool. + +Inside a pass, for each allocation that should be moved: + +- You should copy its data from the source to the destination place by calling e.g. `vkCmdCopyBuffer()`, `vkCmdCopyImage()`. + - You need to make sure these commands finished executing before destroying the source buffers/images and before calling vmaEndDefragmentationPass(). +- If a resource doesn't contain any meaningful data, e.g. it is a transient color attachment image to be cleared, + filled, and used temporarily in each rendering frame, you can just recreate this image + without copying its data. +- If the resource is in `HOST_VISIBLE` and `HOST_CACHED` memory, you can copy its data on the CPU + using `memcpy()`. +- If you cannot move the allocation, you can set `pass.pMoves[i].operation` to #VMA_DEFRAGMENTATION_MOVE_OPERATION_IGNORE. + This will cancel the move. + - vmaEndDefragmentationPass() will then free the destination memory + not the source memory of the allocation, leaving it unchanged. +- If you decide the allocation is unimportant and can be destroyed instead of moved (e.g. it wasn't used for long time), + you can set `pass.pMoves[i].operation` to #VMA_DEFRAGMENTATION_MOVE_OPERATION_DESTROY. + - vmaEndDefragmentationPass() will then free both source and destination memory, and will destroy the source #VmaAllocation object. + +You can defragment a specific custom pool by setting VmaDefragmentationInfo::pool +(like in the example above) or all the default pools by setting this member to null. + +Defragmentation is always performed in each pool separately. +Allocations are never moved between different Vulkan memory types. +The size of the destination memory reserved for a moved allocation is the same as the original one. +Alignment of an allocation as it was determined using `vkGetBufferMemoryRequirements()` etc. is also respected after defragmentation. +Buffers/images should be recreated with the same `VkBufferCreateInfo` / `VkImageCreateInfo` parameters as the original ones. + +You can perform the defragmentation incrementally to limit the number of allocations and bytes to be moved +in each pass, e.g. to call it in sync with render frames and not to experience too big hitches. +See members: VmaDefragmentationInfo::maxBytesPerPass, VmaDefragmentationInfo::maxAllocationsPerPass. + +It is also safe to perform the defragmentation asynchronously to render frames and other Vulkan and VMA +usage, possibly from multiple threads, with the exception that allocations +returned in VmaDefragmentationPassMoveInfo::pMoves shouldn't be destroyed until the defragmentation pass is ended. + +Mapping is preserved on allocations that are moved during defragmentation. +Whether through #VMA_ALLOCATION_CREATE_MAPPED_BIT or vmaMapMemory(), the allocations +are mapped at their new place. Of course, pointer to the mapped data changes, so it needs to be queried +using VmaAllocationInfo::pMappedData. + +\note Defragmentation is not supported in custom pools created with #VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT. + + +\page statistics Statistics + +This library contains several functions that return information about its internal state, +especially the amount of memory allocated from Vulkan. + +\section statistics_numeric_statistics Numeric statistics + +If you need to obtain basic statistics about memory usage per heap, together with current budget, +you can call function vmaGetHeapBudgets() and inspect structure #VmaBudget. +This is useful to keep track of memory usage and stay within budget +(see also \ref staying_within_budget). +Example: + +\code +uint32_t heapIndex = ... + +VmaBudget budgets[VK_MAX_MEMORY_HEAPS]; +vmaGetHeapBudgets(allocator, budgets); + +printf("My heap currently has %u allocations taking %llu B,\n", + budgets[heapIndex].statistics.allocationCount, + budgets[heapIndex].statistics.allocationBytes); +printf("allocated out of %u Vulkan device memory blocks taking %llu B,\n", + budgets[heapIndex].statistics.blockCount, + budgets[heapIndex].statistics.blockBytes); +printf("Vulkan reports total usage %llu B with budget %llu B.\n", + budgets[heapIndex].usage, + budgets[heapIndex].budget); +\endcode + +You can query for more detailed statistics per memory heap, type, and totals, +including minimum and maximum allocation size and unused range size, +by calling function vmaCalculateStatistics() and inspecting structure #VmaTotalStatistics. +This function is slower though, as it has to traverse all the internal data structures, +so it should be used only for debugging purposes. + +You can query for statistics of a custom pool using function vmaGetPoolStatistics() +or vmaCalculatePoolStatistics(). + +You can query for information about a specific allocation using function vmaGetAllocationInfo(). +It fill structure #VmaAllocationInfo. + +\section statistics_json_dump JSON dump + +You can dump internal state of the allocator to a string in JSON format using function vmaBuildStatsString(). +The result is guaranteed to be correct JSON. +It uses ANSI encoding. +Any strings provided by user (see [Allocation names](@ref allocation_names)) +are copied as-is and properly escaped for JSON, so if they use UTF-8, ISO-8859-2 or any other encoding, +this JSON string can be treated as using this encoding. +It must be freed using function vmaFreeStatsString(). + +The format of this JSON string is not part of official documentation of the library, +but it will not change in backward-incompatible way without increasing library major version number +and appropriate mention in changelog. + +The JSON string contains all the data that can be obtained using vmaCalculateStatistics(). +It can also contain detailed map of allocated memory blocks and their regions - +free and occupied by allocations. +This allows e.g. to visualize the memory or assess fragmentation. + + +\page allocation_annotation Allocation names and user data + +\section allocation_user_data Allocation user data + +You can annotate allocations with your own information, e.g. for debugging purposes. +To do that, fill VmaAllocationCreateInfo::pUserData field when creating +an allocation. It is an opaque `void*` pointer. You can use it e.g. as a pointer, +some handle, index, key, ordinal number or any other value that would associate +the allocation with your custom metadata. +It is useful to identify appropriate data structures in your engine given #VmaAllocation, +e.g. when doing \ref defragmentation. + +\code +VkBufferCreateInfo bufCreateInfo = ... + +MyBufferMetadata* pMetadata = CreateBufferMetadata(); + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; +allocCreateInfo.pUserData = pMetadata; + +VkBuffer buffer; +VmaAllocation allocation; +vmaCreateBuffer(allocator, &bufCreateInfo, &allocCreateInfo, &buffer, &allocation, nullptr); +\endcode + +The pointer may be later retrieved as VmaAllocationInfo::pUserData: + +\code +VmaAllocationInfo allocInfo; +vmaGetAllocationInfo(allocator, allocation, &allocInfo); +MyBufferMetadata* pMetadata = (MyBufferMetadata*)allocInfo.pUserData; +\endcode + +It can also be changed using function vmaSetAllocationUserData(). + +Values of (non-zero) allocations' `pUserData` are printed in JSON report created by +vmaBuildStatsString() in hexadecimal form. + +\section allocation_names Allocation names + +An allocation can also carry a null-terminated string, giving a name to the allocation. +To set it, call vmaSetAllocationName(). +The library creates internal copy of the string, so the pointer you pass doesn't need +to be valid for whole lifetime of the allocation. You can free it after the call. + +\code +std::string imageName = "Texture: "; +imageName += fileName; +vmaSetAllocationName(allocator, allocation, imageName.c_str()); +\endcode + +The string can be later retrieved by inspecting VmaAllocationInfo::pName. +It is also printed in JSON report created by vmaBuildStatsString(). + +\note Setting string name to VMA allocation doesn't automatically set it to the Vulkan buffer or image created with it. +You must do it manually using an extension like VK_EXT_debug_utils, which is independent of this library. + + +\page virtual_allocator Virtual allocator + +As an extra feature, the core allocation algorithm of the library is exposed through a simple and convenient API of "virtual allocator". +It doesn't allocate any real GPU memory. It just keeps track of used and free regions of a "virtual block". +You can use it to allocate your own memory or other objects, even completely unrelated to Vulkan. +A common use case is sub-allocation of pieces of one large GPU buffer. + +\section virtual_allocator_creating_virtual_block Creating virtual block + +To use this functionality, there is no main "allocator" object. +You don't need to have #VmaAllocator object created. +All you need to do is to create a separate #VmaVirtualBlock object for each block of memory you want to be managed by the allocator: + +-# Fill in #VmaVirtualBlockCreateInfo structure. +-# Call vmaCreateVirtualBlock(). Get new #VmaVirtualBlock object. + +Example: + +\code +VmaVirtualBlockCreateInfo blockCreateInfo = {}; +blockCreateInfo.size = 1048576; // 1 MB + +VmaVirtualBlock block; +VkResult res = vmaCreateVirtualBlock(&blockCreateInfo, &block); +\endcode + +\section virtual_allocator_making_virtual_allocations Making virtual allocations + +#VmaVirtualBlock object contains internal data structure that keeps track of free and occupied regions +using the same code as the main Vulkan memory allocator. +Similarly to #VmaAllocation for standard GPU allocations, there is #VmaVirtualAllocation type +that represents an opaque handle to an allocation within the virtual block. + +In order to make such allocation: + +-# Fill in #VmaVirtualAllocationCreateInfo structure. +-# Call vmaVirtualAllocate(). Get new #VmaVirtualAllocation object that represents the allocation. + You can also receive `VkDeviceSize offset` that was assigned to the allocation. + +Example: + +\code +VmaVirtualAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.size = 4096; // 4 KB + +VmaVirtualAllocation alloc; +VkDeviceSize offset; +res = vmaVirtualAllocate(block, &allocCreateInfo, &alloc, &offset); +if(res == VK_SUCCESS) +{ + // Use the 4 KB of your memory starting at offset. +} +else +{ + // Allocation failed - no space for it could be found. Handle this error! +} +\endcode + +\section virtual_allocator_deallocation Deallocation + +When no longer needed, an allocation can be freed by calling vmaVirtualFree(). +You can only pass to this function an allocation that was previously returned by vmaVirtualAllocate() +called for the same #VmaVirtualBlock. + +When whole block is no longer needed, the block object can be released by calling vmaDestroyVirtualBlock(). +All allocations must be freed before the block is destroyed, which is checked internally by an assert. +However, if you don't want to call vmaVirtualFree() for each allocation, you can use vmaClearVirtualBlock() to free them all at once - +a feature not available in normal Vulkan memory allocator. Example: + +\code +vmaVirtualFree(block, alloc); +vmaDestroyVirtualBlock(block); +\endcode + +\section virtual_allocator_allocation_parameters Allocation parameters + +You can attach a custom pointer to each allocation by using vmaSetVirtualAllocationUserData(). +Its default value is null. +It can be used to store any data that needs to be associated with that allocation - e.g. an index, a handle, or a pointer to some +larger data structure containing more information. Example: + +\code +struct CustomAllocData +{ + std::string m_AllocName; +}; +CustomAllocData* allocData = new CustomAllocData(); +allocData->m_AllocName = "My allocation 1"; +vmaSetVirtualAllocationUserData(block, alloc, allocData); +\endcode + +The pointer can later be fetched, along with allocation offset and size, by passing the allocation handle to function +vmaGetVirtualAllocationInfo() and inspecting returned structure #VmaVirtualAllocationInfo. +If you allocated a new object to be used as the custom pointer, don't forget to delete that object before freeing the allocation! +Example: + +\code +VmaVirtualAllocationInfo allocInfo; +vmaGetVirtualAllocationInfo(block, alloc, &allocInfo); +delete (CustomAllocData*)allocInfo.pUserData; + +vmaVirtualFree(block, alloc); +\endcode + +\section virtual_allocator_alignment_and_units Alignment and units + +It feels natural to express sizes and offsets in bytes. +If an offset of an allocation needs to be aligned to a multiply of some number (e.g. 4 bytes), you can fill optional member +VmaVirtualAllocationCreateInfo::alignment to request it. Example: + +\code +VmaVirtualAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.size = 4096; // 4 KB +allocCreateInfo.alignment = 4; // Returned offset must be a multiply of 4 B + +VmaVirtualAllocation alloc; +res = vmaVirtualAllocate(block, &allocCreateInfo, &alloc, nullptr); +\endcode + +Alignments of different allocations made from one block may vary. +However, if all alignments and sizes are always multiply of some size e.g. 4 B or `sizeof(MyDataStruct)`, +you can express all sizes, alignments, and offsets in multiples of that size instead of individual bytes. +It might be more convenient, but you need to make sure to use this new unit consistently in all the places: + +- VmaVirtualBlockCreateInfo::size +- VmaVirtualAllocationCreateInfo::size and VmaVirtualAllocationCreateInfo::alignment +- Using offset returned by vmaVirtualAllocate() or in VmaVirtualAllocationInfo::offset + +\section virtual_allocator_statistics Statistics + +You can obtain statistics of a virtual block using vmaGetVirtualBlockStatistics() +(to get brief statistics that are fast to calculate) +or vmaCalculateVirtualBlockStatistics() (to get more detailed statistics, slower to calculate). +The functions fill structures #VmaStatistics, #VmaDetailedStatistics respectively - same as used by the normal Vulkan memory allocator. +Example: + +\code +VmaStatistics stats; +vmaGetVirtualBlockStatistics(block, &stats); +printf("My virtual block has %llu bytes used by %u virtual allocations\n", + stats.allocationBytes, stats.allocationCount); +\endcode + +You can also request a full list of allocations and free regions as a string in JSON format by calling +vmaBuildVirtualBlockStatsString(). +Returned string must be later freed using vmaFreeVirtualBlockStatsString(). +The format of this string differs from the one returned by the main Vulkan allocator, but it is similar. + +\section virtual_allocator_additional_considerations Additional considerations + +The "virtual allocator" functionality is implemented on a level of individual memory blocks. +Keeping track of a whole collection of blocks, allocating new ones when out of free space, +deleting empty ones, and deciding which one to try first for a new allocation must be implemented by the user. + +Alternative allocation algorithms are supported, just like in custom pools of the real GPU memory. +See enum #VmaVirtualBlockCreateFlagBits to learn how to specify them (e.g. #VMA_VIRTUAL_BLOCK_CREATE_LINEAR_ALGORITHM_BIT). +You can find their description in chapter \ref custom_memory_pools. +Allocation strategies are also supported. +See enum #VmaVirtualAllocationCreateFlagBits to learn how to specify them (e.g. #VMA_VIRTUAL_ALLOCATION_CREATE_STRATEGY_MIN_TIME_BIT). + +Following features are supported only by the allocator of the real GPU memory and not by virtual allocations: +buffer-image granularity, `VMA_DEBUG_MARGIN`, `VMA_MIN_ALIGNMENT`. + + +\page debugging_memory_usage Debugging incorrect memory usage + +If you suspect a bug with memory usage, like usage of uninitialized memory or +memory being overwritten out of bounds of an allocation, +you can use debug features of this library to verify this. + +\section debugging_memory_usage_initialization Memory initialization + +If you experience a bug with incorrect and nondeterministic data in your program and you suspect uninitialized memory to be used, +you can enable automatic memory initialization to verify this. +To do it, define macro `VMA_DEBUG_INITIALIZE_ALLOCATIONS` to 1. + +\code +#define VMA_DEBUG_INITIALIZE_ALLOCATIONS 1 +#include "vk_mem_alloc.h" +\endcode + +It makes memory of new allocations initialized to bit pattern `0xDCDCDCDC`. +Before an allocation is destroyed, its memory is filled with bit pattern `0xEFEFEFEF`. +Memory is automatically mapped and unmapped if necessary. + +If you find these values while debugging your program, good chances are that you incorrectly +read Vulkan memory that is allocated but not initialized, or already freed, respectively. + +Memory initialization works only with memory types that are `HOST_VISIBLE` and with allocations that can be mapped. +It works also with dedicated allocations. + +\section debugging_memory_usage_margins Margins + +By default, allocations are laid out in memory blocks next to each other if possible +(considering required alignment, `bufferImageGranularity`, and `nonCoherentAtomSize`). + +![Allocations without margin](../gfx/Margins_1.png) + +Define macro `VMA_DEBUG_MARGIN` to some non-zero value (e.g. 16) to enforce specified +number of bytes as a margin after every allocation. + +\code +#define VMA_DEBUG_MARGIN 16 +#include "vk_mem_alloc.h" +\endcode + +![Allocations with margin](../gfx/Margins_2.png) + +If your bug goes away after enabling margins, it means it may be caused by memory +being overwritten outside of allocation boundaries. It is not 100% certain though. +Change in application behavior may also be caused by different order and distribution +of allocations across memory blocks after margins are applied. + +Margins work with all types of memory. + +Margin is applied only to allocations made out of memory blocks and not to dedicated +allocations, which have their own memory block of specific size. +It is thus not applied to allocations made using #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT flag +or those automatically decided to put into dedicated allocations, e.g. due to its +large size or recommended by VK_KHR_dedicated_allocation extension. + +Margins appear in [JSON dump](@ref statistics_json_dump) as part of free space. + +Note that enabling margins increases memory usage and fragmentation. + +Margins do not apply to \ref virtual_allocator. + +\section debugging_memory_usage_corruption_detection Corruption detection + +You can additionally define macro `VMA_DEBUG_DETECT_CORRUPTION` to 1 to enable validation +of contents of the margins. + +\code +#define VMA_DEBUG_MARGIN 16 +#define VMA_DEBUG_DETECT_CORRUPTION 1 +#include "vk_mem_alloc.h" +\endcode + +When this feature is enabled, number of bytes specified as `VMA_DEBUG_MARGIN` +(it must be multiply of 4) after every allocation is filled with a magic number. +This idea is also know as "canary". +Memory is automatically mapped and unmapped if necessary. + +This number is validated automatically when the allocation is destroyed. +If it is not equal to the expected value, `VMA_ASSERT()` is executed. +It clearly means that either CPU or GPU overwritten the memory outside of boundaries of the allocation, +which indicates a serious bug. + +You can also explicitly request checking margins of all allocations in all memory blocks +that belong to specified memory types by using function vmaCheckCorruption(), +or in memory blocks that belong to specified custom pool, by using function +vmaCheckPoolCorruption(). + +Margin validation (corruption detection) works only for memory types that are +`HOST_VISIBLE` and `HOST_COHERENT`. + + +\section debugging_memory_usage_leak_detection Leak detection features + +At allocation and allocator destruction time VMA checks for unfreed and unmapped blocks using +`VMA_ASSERT_LEAK()`. This macro defaults to an assertion, triggering a typically fatal error in Debug +builds, and doing nothing in Release builds. You can provide your own definition of `VMA_ASSERT_LEAK()` +to change this behavior. + +At memory block destruction time VMA lists out all unfreed allocations using the `VMA_LEAK_LOG_FORMAT()` +macro, which defaults to `VMA_DEBUG_LOG_FORMAT`, which in turn defaults to a no-op. +If you're having trouble with leaks - for example, the aforementioned assertion triggers, but you don't +quite know \em why -, overriding this macro to print out the the leaking blocks, combined with assigning +individual names to allocations using vmaSetAllocationName(), can greatly aid in fixing them. + +\page other_api_interop Interop with other graphics APIs + +VMA provides some features that help with interoperability with other graphics APIs, e.g. OpenGL. + +\section opengl_interop_exporting_memory Exporting memory + +If you want to attach `VkExportMemoryAllocateInfoKHR` or other structure to `pNext` chain of memory allocations made by the library: + +You can create \ref custom_memory_pools for such allocations. +Define and fill in your `VkExportMemoryAllocateInfoKHR` structure and attach it to VmaPoolCreateInfo::pMemoryAllocateNext +while creating the custom pool. +Please note that the structure must remain alive and unchanged for the whole lifetime of the #VmaPool, +not only while creating it, as no copy of the structure is made, +but its original pointer is used for each allocation instead. + +If you want to export all memory allocated by VMA from certain memory types, +also dedicated allocations or other allocations made from default pools, +an alternative solution is to fill in VmaAllocatorCreateInfo::pTypeExternalMemoryHandleTypes. +It should point to an array with `VkExternalMemoryHandleTypeFlagsKHR` to be automatically passed by the library +through `VkExportMemoryAllocateInfoKHR` on each allocation made from a specific memory type. +Please note that new versions of the library also support dedicated allocations created in custom pools. + +You should not mix these two methods in a way that allows to apply both to the same memory type. +Otherwise, `VkExportMemoryAllocateInfoKHR` structure would be attached twice to the `pNext` chain of `VkMemoryAllocateInfo`. + + +\section opengl_interop_custom_alignment Custom alignment + +Buffers or images exported to a different API like OpenGL may require a different alignment, +higher than the one used by the library automatically, queried from functions like `vkGetBufferMemoryRequirements`. +To impose such alignment: + +You can create \ref custom_memory_pools for such allocations. +Set VmaPoolCreateInfo::minAllocationAlignment member to the minimum alignment required for each allocation +to be made out of this pool. +The alignment actually used will be the maximum of this member and the alignment returned for the specific buffer or image +from a function like `vkGetBufferMemoryRequirements`, which is called by VMA automatically. + +If you want to create a buffer with a specific minimum alignment out of default pools, +use special function vmaCreateBufferWithAlignment(), which takes additional parameter `minAlignment`. + +Note the problem of alignment affects only resources placed inside bigger `VkDeviceMemory` blocks and not dedicated +allocations, as these, by definition, always have alignment = 0 because the resource is bound to the beginning of its dedicated block. +You can ensure that an allocation is created as dedicated by using #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT. +Contrary to Direct3D 12, Vulkan doesn't have a concept of alignment of the entire memory block passed on its allocation. + +\section opengl_interop_extended_allocation_information Extended allocation information + +If you want to rely on VMA to allocate your buffers and images inside larger memory blocks, +but you need to know the size of the entire block and whether the allocation was made +with its own dedicated memory, use function vmaGetAllocationInfo2() to retrieve +extended allocation information in structure #VmaAllocationInfo2. + + + +\page usage_patterns Recommended usage patterns + +Vulkan gives great flexibility in memory allocation. +This chapter shows the most common patterns. + +See also slides from talk: +[Sawicki, Adam. Advanced Graphics Techniques Tutorial: Memory management in Vulkan and DX12. Game Developers Conference, 2018](https://www.gdcvault.com/play/1025458/Advanced-Graphics-Techniques-Tutorial-New) + + +\section usage_patterns_gpu_only GPU-only resource + +When: +Any resources that you frequently write and read on GPU, +e.g. images used as color attachments (aka "render targets"), depth-stencil attachments, +images/buffers used as storage image/buffer (aka "Unordered Access View (UAV)"). + +What to do: +Let the library select the optimal memory type, which will likely have `VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT`. + +\code +VkImageCreateInfo imgCreateInfo = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; +imgCreateInfo.imageType = VK_IMAGE_TYPE_2D; +imgCreateInfo.extent.width = 3840; +imgCreateInfo.extent.height = 2160; +imgCreateInfo.extent.depth = 1; +imgCreateInfo.mipLevels = 1; +imgCreateInfo.arrayLayers = 1; +imgCreateInfo.format = VK_FORMAT_R8G8B8A8_UNORM; +imgCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL; +imgCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +imgCreateInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; +imgCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT; + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; +allocCreateInfo.flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT; +allocCreateInfo.priority = 1.0f; + +VkImage img; +VmaAllocation alloc; +vmaCreateImage(allocator, &imgCreateInfo, &allocCreateInfo, &img, &alloc, nullptr); +\endcode + +Also consider: +Consider creating them as dedicated allocations using #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT, +especially if they are large or if you plan to destroy and recreate them with different sizes +e.g. when display resolution changes. +Prefer to create such resources first and all other GPU resources (like textures and vertex buffers) later. +When VK_EXT_memory_priority extension is enabled, it is also worth setting high priority to such allocation +to decrease chances to be evicted to system memory by the operating system. + +\section usage_patterns_staging_copy_upload Staging copy for upload + +When: +A "staging" buffer than you want to map and fill from CPU code, then use as a source of transfer +to some GPU resource. + +What to do: +Use flag #VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT. +Let the library select the optimal memory type, which will always have `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT`. + +\code +VkBufferCreateInfo bufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +bufCreateInfo.size = 65536; +bufCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; +allocCreateInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT; + +VkBuffer buf; +VmaAllocation alloc; +VmaAllocationInfo allocInfo; +vmaCreateBuffer(allocator, &bufCreateInfo, &allocCreateInfo, &buf, &alloc, &allocInfo); + +... + +memcpy(allocInfo.pMappedData, myData, myDataSize); +\endcode + +Also consider: +You can map the allocation using vmaMapMemory() or you can create it as persistenly mapped +using #VMA_ALLOCATION_CREATE_MAPPED_BIT, as in the example above. + + +\section usage_patterns_readback Readback + +When: +Buffers for data written by or transferred from the GPU that you want to read back on the CPU, +e.g. results of some computations. + +What to do: +Use flag #VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT. +Let the library select the optimal memory type, which will always have `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` +and `VK_MEMORY_PROPERTY_HOST_CACHED_BIT`. + +\code +VkBufferCreateInfo bufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +bufCreateInfo.size = 65536; +bufCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; +allocCreateInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT; + +VkBuffer buf; +VmaAllocation alloc; +VmaAllocationInfo allocInfo; +vmaCreateBuffer(allocator, &bufCreateInfo, &allocCreateInfo, &buf, &alloc, &allocInfo); + +... + +const float* downloadedData = (const float*)allocInfo.pMappedData; +\endcode + + +\section usage_patterns_advanced_data_uploading Advanced data uploading + +For resources that you frequently write on CPU via mapped pointer and +frequently read on GPU e.g. as a uniform buffer (also called "dynamic"), multiple options are possible: + +-# Easiest solution is to have one copy of the resource in `HOST_VISIBLE` memory, + even if it means system RAM (not `DEVICE_LOCAL`) on systems with a discrete graphics card, + and make the device reach out to that resource directly. + - Reads performed by the device will then go through PCI Express bus. + The performance of this access may be limited, but it may be fine depending on the size + of this resource (whether it is small enough to quickly end up in GPU cache) and the sparsity + of access. +-# On systems with unified memory (e.g. AMD APU or Intel integrated graphics, mobile chips), + a memory type may be available that is both `HOST_VISIBLE` (available for mapping) and `DEVICE_LOCAL` + (fast to access from the GPU). Then, it is likely the best choice for such type of resource. +-# Systems with a discrete graphics card and separate video memory may or may not expose + a memory type that is both `HOST_VISIBLE` and `DEVICE_LOCAL`, also known as Base Address Register (BAR). + If they do, it represents a piece of VRAM (or entire VRAM, if ReBAR is enabled in the motherboard BIOS) + that is available to CPU for mapping. + - Writes performed by the host to that memory go through PCI Express bus. + The performance of these writes may be limited, but it may be fine, especially on PCIe 4.0, + as long as rules of using uncached and write-combined memory are followed - only sequential writes and no reads. +-# Finally, you may need or prefer to create a separate copy of the resource in `DEVICE_LOCAL` memory, + a separate "staging" copy in `HOST_VISIBLE` memory and perform an explicit transfer command between them. + +Thankfully, VMA offers an aid to create and use such resources in the the way optimal +for the current Vulkan device. To help the library make the best choice, +use flag #VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT together with +#VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT. +It will then prefer a memory type that is both `DEVICE_LOCAL` and `HOST_VISIBLE` (integrated memory or BAR), +but if no such memory type is available or allocation from it fails +(PC graphics cards have only 256 MB of BAR by default, unless ReBAR is supported and enabled in BIOS), +it will fall back to `DEVICE_LOCAL` memory for fast GPU access. +It is then up to you to detect that the allocation ended up in a memory type that is not `HOST_VISIBLE`, +so you need to create another "staging" allocation and perform explicit transfers. + +\code +VkBufferCreateInfo bufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +bufCreateInfo.size = 65536; +bufCreateInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; +allocCreateInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | + VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT; + +VkBuffer buf; +VmaAllocation alloc; +VmaAllocationInfo allocInfo; +VkResult result = vmaCreateBuffer(allocator, &bufCreateInfo, &allocCreateInfo, &buf, &alloc, &allocInfo); +// Check result... + +VkMemoryPropertyFlags memPropFlags; +vmaGetAllocationMemoryProperties(allocator, alloc, &memPropFlags); + +if(memPropFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) +{ + // Allocation ended up in a mappable memory and is already mapped - write to it directly. + + // [Executed in runtime]: + memcpy(allocInfo.pMappedData, myData, myDataSize); + result = vmaFlushAllocation(allocator, alloc, 0, VK_WHOLE_SIZE); + // Check result... + + VkBufferMemoryBarrier bufMemBarrier = { VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER }; + bufMemBarrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; + bufMemBarrier.dstAccessMask = VK_ACCESS_UNIFORM_READ_BIT; + bufMemBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + bufMemBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + bufMemBarrier.buffer = buf; + bufMemBarrier.offset = 0; + bufMemBarrier.size = VK_WHOLE_SIZE; + + vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_VERTEX_SHADER_BIT, + 0, 0, nullptr, 1, &bufMemBarrier, 0, nullptr); +} +else +{ + // Allocation ended up in a non-mappable memory - a transfer using a staging buffer is required. + VkBufferCreateInfo stagingBufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; + stagingBufCreateInfo.size = 65536; + stagingBufCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + + VmaAllocationCreateInfo stagingAllocCreateInfo = {}; + stagingAllocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; + stagingAllocCreateInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT; + + VkBuffer stagingBuf; + VmaAllocation stagingAlloc; + VmaAllocationInfo stagingAllocInfo; + result = vmaCreateBuffer(allocator, &stagingBufCreateInfo, &stagingAllocCreateInfo, + &stagingBuf, &stagingAlloc, &stagingAllocInfo); + // Check result... + + // [Executed in runtime]: + memcpy(stagingAllocInfo.pMappedData, myData, myDataSize); + result = vmaFlushAllocation(allocator, stagingAlloc, 0, VK_WHOLE_SIZE); + // Check result... + + VkBufferMemoryBarrier bufMemBarrier = { VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER }; + bufMemBarrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; + bufMemBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + bufMemBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + bufMemBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + bufMemBarrier.buffer = stagingBuf; + bufMemBarrier.offset = 0; + bufMemBarrier.size = VK_WHOLE_SIZE; + + vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 1, &bufMemBarrier, 0, nullptr); + + VkBufferCopy bufCopy = { + 0, // srcOffset + 0, // dstOffset, + myDataSize, // size + }; + + vkCmdCopyBuffer(cmdBuf, stagingBuf, buf, 1, &bufCopy); + + VkBufferMemoryBarrier bufMemBarrier2 = { VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER }; + bufMemBarrier2.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + bufMemBarrier2.dstAccessMask = VK_ACCESS_UNIFORM_READ_BIT; // We created a uniform buffer + bufMemBarrier2.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + bufMemBarrier2.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + bufMemBarrier2.buffer = buf; + bufMemBarrier2.offset = 0; + bufMemBarrier2.size = VK_WHOLE_SIZE; + + vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_VERTEX_SHADER_BIT, + 0, 0, nullptr, 1, &bufMemBarrier2, 0, nullptr); +} +\endcode + +\section usage_patterns_other_use_cases Other use cases + +Here are some other, less obvious use cases and their recommended settings: + +- An image that is used only as transfer source and destination, but it should stay on the device, + as it is used to temporarily store a copy of some texture, e.g. from the current to the next frame, + for temporal antialiasing or other temporal effects. + - Use `VkImageCreateInfo::usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT` + - Use VmaAllocationCreateInfo::usage = #VMA_MEMORY_USAGE_AUTO +- An image that is used only as transfer source and destination, but it should be placed + in the system RAM despite it doesn't need to be mapped, because it serves as a "swap" copy to evict + least recently used textures from VRAM. + - Use `VkImageCreateInfo::usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT` + - Use VmaAllocationCreateInfo::usage = #VMA_MEMORY_USAGE_AUTO_PREFER_HOST, + as VMA needs a hint here to differentiate from the previous case. +- A buffer that you want to map and write from the CPU, directly read from the GPU + (e.g. as a uniform or vertex buffer), but you have a clear preference to place it in device or + host memory due to its large size. + - Use `VkBufferCreateInfo::usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT` + - Use VmaAllocationCreateInfo::usage = #VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE or #VMA_MEMORY_USAGE_AUTO_PREFER_HOST + - Use VmaAllocationCreateInfo::flags = #VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT + + +\page configuration Configuration + +Please check "CONFIGURATION SECTION" in the code to find macros that you can define +before each include of this file or change directly in this file to provide +your own implementation of basic facilities like assert, `min()` and `max()` functions, +mutex, atomic etc. + +For example, define `VMA_ASSERT(expr)` before including the library to provide +custom implementation of the assertion, compatible with your project. +By default it is defined to standard C `assert(expr)` in `_DEBUG` configuration +and empty otherwise. + +Similarly, you can define `VMA_LEAK_LOG_FORMAT` macro to enable printing of leaked (unfreed) allocations, +including their names and other parameters. Example: + +\code +#define VMA_LEAK_LOG_FORMAT(format, ...) do { \ + printf((format), __VA_ARGS__); \ + printf("\n"); \ + } while(false) +\endcode + +\section config_Vulkan_functions Pointers to Vulkan functions + +There are multiple ways to import pointers to Vulkan functions in the library. +In the simplest case you don't need to do anything. +If the compilation or linking of your program or the initialization of the #VmaAllocator +doesn't work for you, you can try to reconfigure it. + +First, the allocator tries to fetch pointers to Vulkan functions linked statically, +like this: + +\code +m_VulkanFunctions.vkAllocateMemory = (PFN_vkAllocateMemory)vkAllocateMemory; +\endcode + +If you want to disable this feature, set configuration macro: `#define VMA_STATIC_VULKAN_FUNCTIONS 0`. + +Second, you can provide the pointers yourself by setting member VmaAllocatorCreateInfo::pVulkanFunctions. +You can fetch them e.g. using functions `vkGetInstanceProcAddr` and `vkGetDeviceProcAddr` or +by using a helper library like [volk](https://github.com/zeux/volk). + +Third, VMA tries to fetch remaining pointers that are still null by calling +`vkGetInstanceProcAddr` and `vkGetDeviceProcAddr` on its own. +You need to only fill in VmaVulkanFunctions::vkGetInstanceProcAddr and VmaVulkanFunctions::vkGetDeviceProcAddr. +Other pointers will be fetched automatically. +If you want to disable this feature, set configuration macro: `#define VMA_DYNAMIC_VULKAN_FUNCTIONS 0`. + +Finally, all the function pointers required by the library (considering selected +Vulkan version and enabled extensions) are checked with `VMA_ASSERT` if they are not null. + + +\section custom_memory_allocator Custom host memory allocator + +If you use custom allocator for CPU memory rather than default operator `new` +and `delete` from C++, you can make this library using your allocator as well +by filling optional member VmaAllocatorCreateInfo::pAllocationCallbacks. These +functions will be passed to Vulkan, as well as used by the library itself to +make any CPU-side allocations. + +\section allocation_callbacks Device memory allocation callbacks + +The library makes calls to `vkAllocateMemory()` and `vkFreeMemory()` internally. +You can setup callbacks to be informed about these calls, e.g. for the purpose +of gathering some statistics. To do it, fill optional member +VmaAllocatorCreateInfo::pDeviceMemoryCallbacks. + +\section heap_memory_limit Device heap memory limit + +When device memory of certain heap runs out of free space, new allocations may +fail (returning error code) or they may succeed, silently pushing some existing_ +memory blocks from GPU VRAM to system RAM (which degrades performance). This +behavior is implementation-dependent - it depends on GPU vendor and graphics +driver. + +On AMD cards it can be controlled while creating Vulkan device object by using +VK_AMD_memory_overallocation_behavior extension, if available. + +Alternatively, if you want to test how your program behaves with limited amount of Vulkan device +memory available without switching your graphics card to one that really has +smaller VRAM, you can use a feature of this library intended for this purpose. +To do it, fill optional member VmaAllocatorCreateInfo::pHeapSizeLimit. + + + +\page vk_khr_dedicated_allocation VK_KHR_dedicated_allocation + +VK_KHR_dedicated_allocation is a Vulkan extension which can be used to improve +performance on some GPUs. It augments Vulkan API with possibility to query +driver whether it prefers particular buffer or image to have its own, dedicated +allocation (separate `VkDeviceMemory` block) for better efficiency - to be able +to do some internal optimizations. The extension is supported by this library. +It will be used automatically when enabled. + +It has been promoted to core Vulkan 1.1, so if you use eligible Vulkan version +and inform VMA about it by setting VmaAllocatorCreateInfo::vulkanApiVersion, +you are all set. + +Otherwise, if you want to use it as an extension: + +1 . When creating Vulkan device, check if following 2 device extensions are +supported (call `vkEnumerateDeviceExtensionProperties()`). +If yes, enable them (fill `VkDeviceCreateInfo::ppEnabledExtensionNames`). + +- VK_KHR_get_memory_requirements2 +- VK_KHR_dedicated_allocation + +If you enabled these extensions: + +2 . Use #VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT flag when creating +your #VmaAllocator to inform the library that you enabled required extensions +and you want the library to use them. + +\code +allocatorInfo.flags |= VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT; + +vmaCreateAllocator(&allocatorInfo, &allocator); +\endcode + +That is all. The extension will be automatically used whenever you create a +buffer using vmaCreateBuffer() or image using vmaCreateImage(). + +When using the extension together with Vulkan Validation Layer, you will receive +warnings like this: + +_vkBindBufferMemory(): Binding memory to buffer 0x33 but vkGetBufferMemoryRequirements() has not been called on that buffer._ + +It is OK, you should just ignore it. It happens because you use function +`vkGetBufferMemoryRequirements2KHR()` instead of standard +`vkGetBufferMemoryRequirements()`, while the validation layer seems to be +unaware of it. + +To learn more about this extension, see: + +- [VK_KHR_dedicated_allocation in Vulkan specification](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/chap50.html#VK_KHR_dedicated_allocation) +- [VK_KHR_dedicated_allocation unofficial manual](http://asawicki.info/articles/VK_KHR_dedicated_allocation.php5) + + + +\page vk_ext_memory_priority VK_EXT_memory_priority + +VK_EXT_memory_priority is a device extension that allows to pass additional "priority" +value to Vulkan memory allocations that the implementation may use prefer certain +buffers and images that are critical for performance to stay in device-local memory +in cases when the memory is over-subscribed, while some others may be moved to the system memory. + +VMA offers convenient usage of this extension. +If you enable it, you can pass "priority" parameter when creating allocations or custom pools +and the library automatically passes the value to Vulkan using this extension. + +If you want to use this extension in connection with VMA, follow these steps: + +\section vk_ext_memory_priority_initialization Initialization + +1) Call `vkEnumerateDeviceExtensionProperties` for the physical device. +Check if the extension is supported - if returned array of `VkExtensionProperties` contains "VK_EXT_memory_priority". + +2) Call `vkGetPhysicalDeviceFeatures2` for the physical device instead of old `vkGetPhysicalDeviceFeatures`. +Attach additional structure `VkPhysicalDeviceMemoryPriorityFeaturesEXT` to `VkPhysicalDeviceFeatures2::pNext` to be returned. +Check if the device feature is really supported - check if `VkPhysicalDeviceMemoryPriorityFeaturesEXT::memoryPriority` is true. + +3) While creating device with `vkCreateDevice`, enable this extension - add "VK_EXT_memory_priority" +to the list passed as `VkDeviceCreateInfo::ppEnabledExtensionNames`. + +4) While creating the device, also don't set `VkDeviceCreateInfo::pEnabledFeatures`. +Fill in `VkPhysicalDeviceFeatures2` structure instead and pass it as `VkDeviceCreateInfo::pNext`. +Enable this device feature - attach additional structure `VkPhysicalDeviceMemoryPriorityFeaturesEXT` to +`VkPhysicalDeviceFeatures2::pNext` chain and set its member `memoryPriority` to `VK_TRUE`. + +5) While creating #VmaAllocator with vmaCreateAllocator() inform VMA that you +have enabled this extension and feature - add #VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT +to VmaAllocatorCreateInfo::flags. + +\section vk_ext_memory_priority_usage Usage + +When using this extension, you should initialize following member: + +- VmaAllocationCreateInfo::priority when creating a dedicated allocation with #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT. +- VmaPoolCreateInfo::priority when creating a custom pool. + +It should be a floating-point value between `0.0f` and `1.0f`, where recommended default is `0.5F`. +Memory allocated with higher value can be treated by the Vulkan implementation as higher priority +and so it can have lower chances of being pushed out to system memory, experiencing degraded performance. + +It might be a good idea to create performance-critical resources like color-attachment or depth-stencil images +as dedicated and set high priority to them. For example: + +\code +VkImageCreateInfo imgCreateInfo = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; +imgCreateInfo.imageType = VK_IMAGE_TYPE_2D; +imgCreateInfo.extent.width = 3840; +imgCreateInfo.extent.height = 2160; +imgCreateInfo.extent.depth = 1; +imgCreateInfo.mipLevels = 1; +imgCreateInfo.arrayLayers = 1; +imgCreateInfo.format = VK_FORMAT_R8G8B8A8_UNORM; +imgCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL; +imgCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +imgCreateInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; +imgCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT; + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; +allocCreateInfo.flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT; +allocCreateInfo.priority = 1.0f; + +VkImage img; +VmaAllocation alloc; +vmaCreateImage(allocator, &imgCreateInfo, &allocCreateInfo, &img, &alloc, nullptr); +\endcode + +`priority` member is ignored in the following situations: + +- Allocations created in custom pools: They inherit the priority, along with all other allocation parameters + from the parameters passed in #VmaPoolCreateInfo when the pool was created. +- Allocations created in default pools: They inherit the priority from the parameters + VMA used when creating default pools, which means `priority == 0.5F`. + + +\page vk_amd_device_coherent_memory VK_AMD_device_coherent_memory + +VK_AMD_device_coherent_memory is a device extension that enables access to +additional memory types with `VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD` and +`VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD` flag. It is useful mostly for +allocation of buffers intended for writing "breadcrumb markers" in between passes +or draw calls, which in turn are useful for debugging GPU crash/hang/TDR cases. + +When the extension is available but has not been enabled, Vulkan physical device +still exposes those memory types, but their usage is forbidden. VMA automatically +takes care of that - it returns `VK_ERROR_FEATURE_NOT_PRESENT` when an attempt +to allocate memory of such type is made. + +If you want to use this extension in connection with VMA, follow these steps: + +\section vk_amd_device_coherent_memory_initialization Initialization + +1) Call `vkEnumerateDeviceExtensionProperties` for the physical device. +Check if the extension is supported - if returned array of `VkExtensionProperties` contains "VK_AMD_device_coherent_memory". + +2) Call `vkGetPhysicalDeviceFeatures2` for the physical device instead of old `vkGetPhysicalDeviceFeatures`. +Attach additional structure `VkPhysicalDeviceCoherentMemoryFeaturesAMD` to `VkPhysicalDeviceFeatures2::pNext` to be returned. +Check if the device feature is really supported - check if `VkPhysicalDeviceCoherentMemoryFeaturesAMD::deviceCoherentMemory` is true. + +3) While creating device with `vkCreateDevice`, enable this extension - add "VK_AMD_device_coherent_memory" +to the list passed as `VkDeviceCreateInfo::ppEnabledExtensionNames`. + +4) While creating the device, also don't set `VkDeviceCreateInfo::pEnabledFeatures`. +Fill in `VkPhysicalDeviceFeatures2` structure instead and pass it as `VkDeviceCreateInfo::pNext`. +Enable this device feature - attach additional structure `VkPhysicalDeviceCoherentMemoryFeaturesAMD` to +`VkPhysicalDeviceFeatures2::pNext` and set its member `deviceCoherentMemory` to `VK_TRUE`. + +5) While creating #VmaAllocator with vmaCreateAllocator() inform VMA that you +have enabled this extension and feature - add #VMA_ALLOCATOR_CREATE_AMD_DEVICE_COHERENT_MEMORY_BIT +to VmaAllocatorCreateInfo::flags. + +\section vk_amd_device_coherent_memory_usage Usage + +After following steps described above, you can create VMA allocations and custom pools +out of the special `DEVICE_COHERENT` and `DEVICE_UNCACHED` memory types on eligible +devices. There are multiple ways to do it, for example: + +- You can request or prefer to allocate out of such memory types by adding + `VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD` to VmaAllocationCreateInfo::requiredFlags + or VmaAllocationCreateInfo::preferredFlags. Those flags can be freely mixed with + other ways of \ref choosing_memory_type, like setting VmaAllocationCreateInfo::usage. +- If you manually found memory type index to use for this purpose, force allocation + from this specific index by setting VmaAllocationCreateInfo::memoryTypeBits `= 1U << index`. + +\section vk_amd_device_coherent_memory_more_information More information + +To learn more about this extension, see [VK_AMD_device_coherent_memory in Vulkan specification](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VK_AMD_device_coherent_memory.html) + +Example use of this extension can be found in the code of the sample and test suite +accompanying this library. + + +\page vk_khr_external_memory_win32 VK_KHR_external_memory_win32 + +On Windows, the VK_KHR_external_memory_win32 device extension allows exporting a Win32 `HANDLE` +of a `VkDeviceMemory` block, to be able to reference the memory on other Vulkan logical devices or instances, +in multiple processes, and/or in multiple APIs. +VMA offers support for it. + +\section vk_khr_external_memory_win32_initialization Initialization + +1) Make sure the extension is defined in the code by including following header before including VMA: + +\code +#include +\endcode + +2) Check if "VK_KHR_external_memory_win32" is available among device extensions. +Enable it when creating the `VkDevice` object. + +3) Enable the usage of this extension in VMA by setting flag #VMA_ALLOCATOR_CREATE_KHR_EXTERNAL_MEMORY_WIN32_BIT +when calling vmaCreateAllocator(). + +4) Make sure that VMA has access to the `vkGetMemoryWin32HandleKHR` function by either enabling `VMA_DYNAMIC_VULKAN_FUNCTIONS` macro +or setting VmaVulkanFunctions::vkGetMemoryWin32HandleKHR explicitly. +For more information, see \ref quick_start_initialization_importing_vulkan_functions. + +\section vk_khr_external_memory_win32_preparations Preparations + +You can find example usage among tests, in file "Tests.cpp", function `TestWin32Handles()`. + +To use the extenion, buffers need to be created with `VkExternalMemoryBufferCreateInfoKHR` attached to their `pNext` chain, +and memory allocations need to be made with `VkExportMemoryAllocateInfoKHR` attached to their `pNext` chain. +To make use of them, you need to use \ref custom_memory_pools. Example: + +\code +// Define an example buffer and allocation parameters. +VkExternalMemoryBufferCreateInfoKHR externalMemBufCreateInfo = { + VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO_KHR, + nullptr, + VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT +}; +VkBufferCreateInfo exampleBufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +exampleBufCreateInfo.size = 0x10000; // Doesn't matter here. +exampleBufCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; +exampleBufCreateInfo.pNext = &externalMemBufCreateInfo; + +VmaAllocationCreateInfo exampleAllocCreateInfo = {}; +exampleAllocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO; + +// Find memory type index to use for the custom pool. +uint32_t memTypeIndex; +VkResult res = vmaFindMemoryTypeIndexForBufferInfo(g_Allocator, + &exampleBufCreateInfo, &exampleAllocCreateInfo, &memTypeIndex); +// Check res... + +// Create a custom pool. +constexpr static VkExportMemoryAllocateInfoKHR exportMemAllocInfo = { + VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO_KHR, + nullptr, + VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT +}; +VmaPoolCreateInfo poolCreateInfo = {}; +poolCreateInfo.memoryTypeIndex = memTypeIndex; +poolCreateInfo.pMemoryAllocateNext = (void*)&exportMemAllocInfo; + +VmaPool pool; +res = vmaCreatePool(g_Allocator, &poolCreateInfo, &pool); +// Check res... + +// YOUR OTHER CODE COMES HERE.... + +// At the end, don't forget to destroy it! +vmaDestroyPool(g_Allocator, pool); +\endcode + +Note that the structure passed as VmaPoolCreateInfo::pMemoryAllocateNext must remain alive and unchanged +for the whole lifetime of the custom pool, because it will be used when the pool allocates a new device memory block. +No copy is made internally. This is why variable `exportMemAllocInfo` is defined as `static`. + +\section vk_khr_external_memory_win32_memory_allocation Memory allocation + +Finally, you can create a buffer with an allocation out of the custom pool. +The buffer should use same flags as the sample buffer used to find the memory type. +It should also specify `VkExternalMemoryBufferCreateInfoKHR` in its `pNext` chain. + +\code +VkExternalMemoryBufferCreateInfoKHR externalMemBufCreateInfo = { + VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO_KHR, + nullptr, + VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT +}; +VkBufferCreateInfo bufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; +bufCreateInfo.size = // Your desired buffer size. +bufCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; +bufCreateInfo.pNext = &externalMemBufCreateInfo; + +VmaAllocationCreateInfo allocCreateInfo = {}; +allocCreateInfo.pool = pool; // It is enough to set this one member. + +VkBuffer buf; +VmaAllocation alloc; +res = vmaCreateBuffer(g_Allocator, &bufCreateInfo, &allocCreateInfo, &buf, &alloc, nullptr); +// Check res... + +// YOUR OTHER CODE COMES HERE.... + +// At the end, don't forget to destroy it! +vmaDestroyBuffer(g_Allocator, buf, alloc); +\endcode + +If you need each allocation to have its own device memory block and start at offset 0, you can still do +by using #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT flag. It works also with custom pools. + +\section vk_khr_external_memory_win32_exporting_win32_handle Exporting Win32 handle + +After the allocation is created, you can acquire a Win32 `HANDLE` to the `VkDeviceMemory` block it belongs to. +VMA function vmaGetMemoryWin32Handle() is a replacement of the Vulkan function `vkGetMemoryWin32HandleKHR`. + +\code +HANDLE handle; +res = vmaGetMemoryWin32Handle(g_Allocator, alloc, nullptr, &handle); +// Check res... + +// YOUR OTHER CODE COMES HERE.... + +// At the end, you must close the handle. +CloseHandle(handle); +\endcode + +Documentation of the VK_KHR_external_memory_win32 extension states that: + +> If handleType is defined as an NT handle, vkGetMemoryWin32HandleKHR must be called no more than once for each valid unique combination of memory and handleType. + +This is ensured automatically inside VMA. +The library fetches the handle on first use, remembers it internally, and closes it when the memory block or dedicated allocation is destroyed. +Every time you call vmaGetMemoryWin32Handle(), VMA calls `DuplicateHandle` and returns a new handle that you need to close. + +For further information, please check documentation of the vmaGetMemoryWin32Handle() function. + + +\page enabling_buffer_device_address Enabling buffer device address + +Device extension VK_KHR_buffer_device_address +allow to fetch raw GPU pointer to a buffer and pass it for usage in a shader code. +It has been promoted to core Vulkan 1.2. + +If you want to use this feature in connection with VMA, follow these steps: + +\section enabling_buffer_device_address_initialization Initialization + +1) (For Vulkan version < 1.2) Call `vkEnumerateDeviceExtensionProperties` for the physical device. +Check if the extension is supported - if returned array of `VkExtensionProperties` contains +"VK_KHR_buffer_device_address". + +2) Call `vkGetPhysicalDeviceFeatures2` for the physical device instead of old `vkGetPhysicalDeviceFeatures`. +Attach additional structure `VkPhysicalDeviceBufferDeviceAddressFeatures*` to `VkPhysicalDeviceFeatures2::pNext` to be returned. +Check if the device feature is really supported - check if `VkPhysicalDeviceBufferDeviceAddressFeatures::bufferDeviceAddress` is true. + +3) (For Vulkan version < 1.2) While creating device with `vkCreateDevice`, enable this extension - add +"VK_KHR_buffer_device_address" to the list passed as `VkDeviceCreateInfo::ppEnabledExtensionNames`. + +4) While creating the device, also don't set `VkDeviceCreateInfo::pEnabledFeatures`. +Fill in `VkPhysicalDeviceFeatures2` structure instead and pass it as `VkDeviceCreateInfo::pNext`. +Enable this device feature - attach additional structure `VkPhysicalDeviceBufferDeviceAddressFeatures*` to +`VkPhysicalDeviceFeatures2::pNext` and set its member `bufferDeviceAddress` to `VK_TRUE`. + +5) While creating #VmaAllocator with vmaCreateAllocator() inform VMA that you +have enabled this feature - add #VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT +to VmaAllocatorCreateInfo::flags. + +\section enabling_buffer_device_address_usage Usage + +After following steps described above, you can create buffers with `VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT*` using VMA. +The library automatically adds `VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT*` to +allocated memory blocks wherever it might be needed. + +Please note that the library supports only `VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT*`. +The second part of this functionality related to "capture and replay" is not supported, +as it is intended for usage in debugging tools like RenderDoc, not in everyday Vulkan usage. + +\section enabling_buffer_device_address_more_information More information + +To learn more about this extension, see [VK_KHR_buffer_device_address in Vulkan specification](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/chap46.html#VK_KHR_buffer_device_address) + +Example use of this extension can be found in the code of the sample and test suite +accompanying this library. + +\page general_considerations General considerations + +\section general_considerations_thread_safety Thread safety + +- The library has no global state, so separate #VmaAllocator objects can be used + independently. + There should be no need to create multiple such objects though - one per `VkDevice` is enough. +- By default, all calls to functions that take #VmaAllocator as first parameter + are safe to call from multiple threads simultaneously because they are + synchronized internally when needed. + This includes allocation and deallocation from default memory pool, as well as custom #VmaPool. +- When the allocator is created with #VMA_ALLOCATOR_CREATE_EXTERNALLY_SYNCHRONIZED_BIT + flag, calls to functions that take such #VmaAllocator object must be + synchronized externally. +- Access to a #VmaAllocation object must be externally synchronized. For example, + you must not call vmaGetAllocationInfo() and vmaMapMemory() from different + threads at the same time if you pass the same #VmaAllocation object to these + functions. +- #VmaVirtualBlock is not safe to be used from multiple threads simultaneously. + +\section general_considerations_versioning_and_compatibility Versioning and compatibility + +The library uses [**Semantic Versioning**](https://semver.org/), +which means version numbers follow convention: Major.Minor.Patch (e.g. 2.3.0), where: + +- Incremented Patch version means a release is backward- and forward-compatible, + introducing only some internal improvements, bug fixes, optimizations etc. + or changes that are out of scope of the official API described in this documentation. +- Incremented Minor version means a release is backward-compatible, + so existing code that uses the library should continue to work, while some new + symbols could have been added: new structures, functions, new values in existing + enums and bit flags, new structure members, but not new function parameters. +- Incrementing Major version means a release could break some backward compatibility. + +All changes between official releases are documented in file "CHANGELOG.md". + +\warning Backward compatibility is considered on the level of C++ source code, not binary linkage. +Adding new members to existing structures is treated as backward compatible if initializing +the new members to binary zero results in the old behavior. +You should always fully initialize all library structures to zeros and not rely on their +exact binary size. + +\section general_considerations_validation_layer_warnings Validation layer warnings + +When using this library, you can meet following types of warnings issued by +Vulkan validation layer. They don't necessarily indicate a bug, so you may need +to just ignore them. + +- *vkBindBufferMemory(): Binding memory to buffer 0xeb8e4 but vkGetBufferMemoryRequirements() has not been called on that buffer.* + - It happens when VK_KHR_dedicated_allocation extension is enabled. + `vkGetBufferMemoryRequirements2KHR` function is used instead, while validation layer seems to be unaware of it. +- *Mapping an image with layout VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL can result in undefined behavior if this memory is used by the device. Only GENERAL or PREINITIALIZED should be used.* + - It happens when you map a buffer or image, because the library maps entire + `VkDeviceMemory` block, where different types of images and buffers may end + up together, especially on GPUs with unified memory like Intel. +- *Non-linear image 0xebc91 is aliased with linear buffer 0xeb8e4 which may indicate a bug.* + - It may happen when you use [defragmentation](@ref defragmentation). + +\section general_considerations_allocation_algorithm Allocation algorithm + +The library uses following algorithm for allocation, in order: + +-# Try to find free range of memory in existing blocks. +-# If failed, try to create a new block of `VkDeviceMemory`, with preferred block size. +-# If failed, try to create such block with size / 2, size / 4, size / 8. +-# If failed, try to allocate separate `VkDeviceMemory` for this allocation, + just like when you use #VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT. +-# If failed, choose other memory type that meets the requirements specified in + VmaAllocationCreateInfo and go to point 1. +-# If failed, return `VK_ERROR_OUT_OF_DEVICE_MEMORY`. + +\section general_considerations_features_not_supported Features not supported + +Features deliberately excluded from the scope of this library: + +-# **Data transfer.** Uploading (streaming) and downloading data of buffers and images + between CPU and GPU memory and related synchronization is responsibility of the user. + Defining some "texture" object that would automatically stream its data from a + staging copy in CPU memory to GPU memory would rather be a feature of another, + higher-level library implemented on top of VMA. + VMA doesn't record any commands to a `VkCommandBuffer`. It just allocates memory. +-# **Recreation of buffers and images.** Although the library has functions for + buffer and image creation: vmaCreateBuffer(), vmaCreateImage(), you need to + recreate these objects yourself after defragmentation. That is because the big + structures `VkBufferCreateInfo`, `VkImageCreateInfo` are not stored in + #VmaAllocation object. +-# **Handling CPU memory allocation failures.** When dynamically creating small C++ + objects in CPU memory (not Vulkan memory), allocation failures are not checked + and handled gracefully, because that would complicate code significantly and + is usually not needed in desktop PC applications anyway. + Success of an allocation is just checked with an assert. +-# **Code free of any compiler warnings.** Maintaining the library to compile and + work correctly on so many different platforms is hard enough. Being free of + any warnings, on any version of any compiler, is simply not feasible. + There are many preprocessor macros that make some variables unused, function parameters unreferenced, + or conditional expressions constant in some configurations. + The code of this library should not be bigger or more complicated just to silence these warnings. + It is recommended to disable such warnings instead. +-# This is a C++ library with C interface. **Bindings or ports to any other programming languages** are welcome as external projects but + are not going to be included into this repository. +*/ diff --git a/vulkan_profiling_analyzer.py b/vulkan_profiling_analyzer.py new file mode 100644 index 000000000000..ca571b0e98ff --- /dev/null +++ b/vulkan_profiling_analyzer.py @@ -0,0 +1,1519 @@ +#!/usr/bin/env python3 +""" +Vulkan Profiling Results Analyzer + +This script parses all Vulkan profiling result inference steps from a log file +and computes comprehensive global statistics. + +Usage: + python vulkan_profiling_analyzer.py [log_file_path] + +If no log file path is provided, it defaults to 'log.txt' in the current directory. +""" + +import sys +import re +import logging +from collections import defaultdict, Counter +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Tuple +import statistics +import argparse +import matplotlib.pyplot as plt +import os +import base64 +import io + + +@dataclass +class GeneralTimingEntry: + """Represents a single general timing entry.""" + + operation: str + count: int + avg_time_us: float + params: Optional[str] = None # For operations like MUL_MAT_VEC with parameters + + +@dataclass +class MatMulTimingEntry: + """Represents a single mat_mul timing entry.""" + + operation_type: str + data_types: str # e.g., "q4_0 x f32 -> f32" + matrix_dims: str # e.g., "[2048x1024] x [2048x1] -> [1024x1]" + count: int + avg_time_us: float + + +@dataclass +class ProfilingSection: + """Represents one complete profiling section.""" + + section_id: int + general_timings: List[GeneralTimingEntry] = field(default_factory=list) + mat_mul_timings: List[MatMulTimingEntry] = field(default_factory=list) + mat_mul_summaries: Dict[str, Tuple[int, float]] = field( + default_factory=dict + ) # operation -> (total_ops, avg_time) + total_operation_types: Optional[int] = None + total_variations: Optional[int] = None + + +@dataclass +class ModelInfo: + """Model information extracted from the log.""" + + model_file: str = "" + model_name: str = "" + architecture: str = "" + size_label: str = "" + context_length: int = 0 + embedding_length: int = 0 + feed_forward_length: int = 0 + layer_count: int = 0 + attention_heads: int = 0 + attention_heads_kv: int = 0 + quantization: str = "" + file_size: str = "" + quantized_by: str = "" + vocab_size: int = 0 + tensor_types: Dict[str, int] = field(default_factory=dict) + + +@dataclass +class DeviceAllocation: + """Device allocation information.""" + + gpu_device: str = "" + gpu_layers: int = 0 + cpu_operations: int = 0 + gpu_operations: int = 0 + cpu_operation_types: Dict[str, int] = field(default_factory=dict) + + +@dataclass +class GlobalStats: + """Global statistics across all inference steps.""" + + total_inference_steps: int + general_stats: Dict[str, Dict[str, float]] = field(default_factory=dict) + mat_mul_stats: Dict[str, Dict[str, float]] = field(default_factory=dict) + mat_mul_normalized: Dict[str, Dict[str, float]] = field( + default_factory=dict + ) # normalized by tensor size + operation_frequency: Counter = field(default_factory=Counter) + model_info: ModelInfo = field(default_factory=ModelInfo) + device_allocation: DeviceAllocation = field(default_factory=DeviceAllocation) + + +class VulkanProfilingAnalyzer: + def __init__(self, log_file_path: str): + self.log_file_path = log_file_path + self.inference_steps: List[ProfilingSection] = [] + + def parse_log_file(self) -> None: + """Parse the entire log file and extract all inference steps.""" + with open(self.log_file_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + + # Find all inference steps + pattern = r"={16}\nVulkan Profiling Results:\n={16}" + inference_step_contents = re.split(pattern, content) + + # Skip the first section (before first profiling results) + if len(inference_step_contents) > 1: + inference_step_contents = inference_step_contents[1:] + + logging.info(f"Found {len(inference_step_contents)} inference steps") + + for i, step_content in enumerate(inference_step_contents): + self._parse_section(i, step_content) + + def _parse_device_allocation(self) -> DeviceAllocation: + """Parse device allocation information from the log file.""" + device_alloc = DeviceAllocation() + + with open(self.log_file_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + + # Parse GPU device info + gpu_match = re.search(r"using device (Vulkan\d+) \(([^)]+)\)", content) + if gpu_match: + device_alloc.gpu_device = f"{gpu_match.group(1)} ({gpu_match.group(2)})" + + # Count GPU layers + device_alloc.gpu_layers = len( + re.findall(r"assigned to device Vulkan\d+", content) + ) + + # Count CPU/GPU operations by parsing each node + # Each node represents a single operation, determined by where its output tensor is allocated + cpu_ops = re.findall(r"node #[^(]*\(\s*([^)]+)\):[^[]*\[\s*CPU\s*\]", content) + gpu_ops = re.findall(r"node #[^(]*\(\s*([^)]+)\):[^[]*\[Vulka[^]]*\]", content) + + device_alloc.cpu_operations = len(cpu_ops) + device_alloc.gpu_operations = len(gpu_ops) + + # Parse CPU operation types + for op_type in cpu_ops: + op_type = op_type.strip() + device_alloc.cpu_operation_types[op_type] = ( + device_alloc.cpu_operation_types.get(op_type, 0) + 1 + ) + + return device_alloc + + def _parse_model_info(self) -> ModelInfo: + """Parse model information from the log file.""" + model_info = ModelInfo() + + with open(self.log_file_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + + # Extract model file name + model_file_match = re.search( + r"loaded meta data with [^/]+ from ([^\s]+)", content + ) + if model_file_match: + model_info.model_file = model_file_match.group(1) + + # Extract model information from key-value pairs + kv_patterns = { + "model_name": r"general\.name\s+str\s+=\s+([^\n]+)", + "architecture": r"general\.architecture\s+str\s+=\s+([^\n]+)", + "size_label": r"general\.size_label\s+str\s+=\s+([^\n]+)", + "quantized_by": r"general\.quantized_by\s+str\s+=\s+([^\n]+)", + "layer_count": r"(\w+)\.block_count\s+u32\s+=\s+(\d+)", + "context_length": r"(\w+)\.context_length\s+u32\s+=\s+(\d+)", + "embedding_length": r"(\w+)\.embedding_length\s+u32\s+=\s+(\d+)", + "feed_forward_length": r"(\w+)\.feed_forward_length\s+u32\s+=\s+(\d+)", + "attention_heads": r"(\w+)\.attention\.head_count\s+u32\s+=\s+(\d+)", + "attention_heads_kv": r"(\w+)\.attention\.head_count_kv\s+u32\s+=\s+(\d+)", + } + + for field_name, pattern in kv_patterns.items(): + match = re.search(pattern, content) + if match: + if field_name in [ + "layer_count", + "context_length", + "embedding_length", + "feed_forward_length", + "attention_heads", + "attention_heads_kv", + ]: + setattr( + model_info, + field_name, + int( + match.group(2) + if len(match.groups()) > 1 + else match.group(1) + ), + ) + else: + setattr(model_info, field_name, match.group(1).strip()) + + # Extract quantization info + quant_match = re.search(r"print_info: file type\s+=\s+(\w+)", content) + if quant_match: + model_info.quantization = quant_match.group(1) + + # Extract file size + size_match = re.search(r"print_info: file size\s+=\s+([^(]+)", content) + if size_match: + model_info.file_size = size_match.group(1).strip() + + # Extract vocab size from tokenizer + vocab_match = re.search(r"tokenizer\.ggml\.tokens\s+arr\[str,(\d+)\]", content) + if vocab_match: + model_info.vocab_size = int(vocab_match.group(1)) + + # Extract tensor types + tensor_types = re.findall( + r"llama_model_loader: - type\s+(\w+):\s+(\d+) tensors", content + ) + for tensor_type, count in tensor_types: + model_info.tensor_types[tensor_type] = int(count) + + return model_info + + def _parse_section(self, section_id: int, content: str) -> None: + """Parse a single profiling section.""" + section = ProfilingSection(section_id=section_id) + + # Parse Legacy Timing Summary + general_match = re.search( + r"Legacy Timing Summary:\n-+\n(.*?)\n(?=Enhanced Triplet|$)", + content, + re.DOTALL, + ) + if general_match: + section.general_timings = self._parse_general_timings( + general_match.group(1) + ) + + # Parse Enhanced Triplet Timing Analysis + mat_mul_match = re.search( + r"Enhanced Triplet Timing Analysis by Operation Type:\n=+\n(.*?)\nOverall Statistics:", + content, + re.DOTALL, + ) + if mat_mul_match: + section.mat_mul_timings, section.mat_mul_summaries = ( + self._parse_mat_mul_timings(mat_mul_match.group(1)) + ) + + # Parse overall statistics + stats_match = re.search( + r"Total operation types: (\d+)\nTotal variations: (\d+)", content + ) + if stats_match: + section.total_operation_types = int(stats_match.group(1)) + section.total_variations = int(stats_match.group(2)) + + self.inference_steps.append(section) + + def _parse_general_timings(self, content: str) -> List[GeneralTimingEntry]: + """Parse general timing entries.""" + timings = [] + lines = content.strip().split("\n") + + for line in lines: + line = line.strip() + if not line: + continue + + # Handle different formats: + # MUL_MAT_VEC m=1024 k=2048: 56 x 417.28 us + # ADD: 56 x 11.48 us + + if ":" in line: + operation_part, timing_part = line.split(":", 1) + operation_part = operation_part.strip() + timing_part = timing_part.strip() + + # Extract operation and parameters + if " " in operation_part and any( + c in operation_part for c in ["=", "x"] + ): + # Has parameters + parts = operation_part.split(" ", 1) + operation = parts[0] + params = parts[1] + else: + operation = operation_part + params = None + + # Parse timing: "56 x 417.28 us" + timing_match = re.match(r"(\d+)\s*x\s*([\d.]+)\s*us", timing_part) + if timing_match: + count = int(timing_match.group(1)) + avg_time = float(timing_match.group(2)) + + timings.append( + GeneralTimingEntry( + operation=operation, + count=count, + avg_time_us=avg_time, + params=params, + ) + ) + + return timings + + def _parse_mat_mul_timings( + self, content: str + ) -> Tuple[List[MatMulTimingEntry], Dict[str, Tuple[int, float]]]: + """Parse mat_mul timing entries.""" + timings = [] + summaries = {} + + # Split by operation types (sections starting with operation name followed by dashes) + operation_sections = re.split(r"\n([a-zA-Z_][a-zA-Z0-9_]*):\n-+\n", content) + + if len(operation_sections) > 1: + # First element is empty or initial content, then alternating operation names and content + for i in range(1, len(operation_sections), 2): + if i + 1 < len(operation_sections): + operation_type = operation_sections[i] + op_content = operation_sections[i + 1] + + # Parse individual timing entries + lines = op_content.strip().split("\n") + for line in lines: + line = line.strip() + if line.startswith("→"): + # Summary line: "→ ggml_vk_mul_mat_vec_q_f16 Summary: 197 total ops, 1765.49 us avg" + summary_match = re.search( + r"(\w+)\s+Summary:\s+(\d+)\s+total ops,\s+([\d.]+)\s+us avg", + line, + ) + if summary_match: + summary_op = summary_match.group(1) + total_ops = int(summary_match.group(2)) + avg_time = float(summary_match.group(3)) + summaries[summary_op] = (total_ops, avg_time) + else: + # Individual entry: "q4_0 x f32 -> f32 | [2048x1024] x [2048x1] -> [1024x1]: 56 ops, 417.28 us avg" + entry_match = re.match( + r"(.+?)\s*\|\s*(.+?):\s*(\d+)\s+ops,\s*([\d.]+)\s+us avg", + line, + ) + if entry_match: + data_types = entry_match.group(1).strip() + matrix_dims = entry_match.group(2).strip() + count = int(entry_match.group(3)) + avg_time = float(entry_match.group(4)) + + timings.append( + MatMulTimingEntry( + operation_type=operation_type, + data_types=data_types, + matrix_dims=matrix_dims, + count=count, + avg_time_us=avg_time, + ) + ) + + return timings, summaries + + def compute_global_statistics(self) -> GlobalStats: + """Compute comprehensive global statistics.""" + global_stats = GlobalStats(total_inference_steps=len(self.inference_steps)) + + # Parse model information + global_stats.model_info = self._parse_model_info() + + # Parse device allocation information + global_stats.device_allocation = self._parse_device_allocation() + + # Aggregate general timing statistics + general_data = defaultdict(list) # operation -> list of (count, avg_time_us) + + for section in self.inference_steps: + for timing in section.general_timings: + # Filter out MUL_MAT operations from general statistics + if timing.operation.startswith("MUL_MAT"): + continue + + key = ( + f"{timing.operation}({timing.params})" + if timing.params + else timing.operation + ) + general_data[key].append((timing.count, timing.avg_time_us)) + global_stats.operation_frequency[key] += timing.count + + # Compute statistics for each general operation + for operation, data_points in general_data.items(): + counts = [d[0] for d in data_points] + times = [d[1] for d in data_points] + total_ops = sum(counts) + + global_stats.general_stats[operation] = { + "total_operations": total_ops, + "total_inference_steps": len(data_points), + "avg_time_mean": statistics.mean(times), + "avg_time_median": statistics.median(times), + "avg_time_min": min(times), + "avg_time_max": max(times), + "avg_time_stdev": statistics.stdev(times) if len(times) > 1 else 0.0, + "count_mean": statistics.mean(counts), + "count_median": statistics.median(counts), + "count_min": min(counts), + "count_max": max(counts), + } + + # Aggregate mat_mul timing statistics + mat_mul_data = defaultdict( + list + ) # (operation_type, data_types, matrix_dims) -> list of (count, avg_time_us) + + for section in self.inference_steps: + for timing in section.mat_mul_timings: + key = ( + f"{timing.operation_type}|{timing.data_types}|{timing.matrix_dims}" + ) + mat_mul_data[key].append((timing.count, timing.avg_time_us)) + + # Compute statistics for each mat_mul operation + for key, data_points in mat_mul_data.items(): + operation_type, data_types, matrix_dims = key.split("|", 2) + counts = [d[0] for d in data_points] + times = [d[1] for d in data_points] + total_ops = sum(counts) + + display_key = f"{operation_type} [{data_types}] {matrix_dims}" + global_stats.mat_mul_stats[display_key] = { + "total_operations": total_ops, + "total_inference_steps": len(data_points), + "avg_time_mean": statistics.mean(times), + "avg_time_median": statistics.median(times), + "avg_time_min": min(times), + "avg_time_max": max(times), + "avg_time_stdev": statistics.stdev(times) if len(times) > 1 else 0.0, + "count_mean": statistics.mean(counts), + "count_median": statistics.median(counts), + "count_min": min(counts), + "count_max": max(counts), + } + + # Calculate normalized MatMul performance (time divided by tensor sizes) + self._calculate_normalized_mat_mul_performance(global_stats) + + return global_stats + + def _calculate_normalized_mat_mul_performance( + self, global_stats: GlobalStats + ) -> None: + """Calculate MatMul performance normalized by tensor sizes.""" + # Group operations by operation type and tensor types + grouped_operations = defaultdict( + list + ) # (operation_type, tensor_types) -> list of normalized times + + # Process ALL MatMul operations from the timing statistics + for operation_key, stats in global_stats.mat_mul_stats.items(): + # Parse operation type and tensor types from the key + # Format: "ggml_vk_mul_mat_vec_q_f16 [q4_0 x f32 -> f32] [2048x6144] x [2048x1] -> [6144x1]" + + # Extract operation type and tensor types + match = re.match(r"(ggml_vk_mul_mat[^[]*)\s*(\[[^]]+\])", operation_key) + if match: + operation_type = match.group(1).strip() + tensor_types = match.group(2) + + # Extract matrix dimensions + dims_match = re.search( + r"\[(\d+)x(\d+)\]\s*x\s*\[(\d+)x(\d+)\]\s*->\s*\[(\d+)x(\d+)\]", + operation_key, + ) + if dims_match: + # Extract input tensor dimensions + a = int(dims_match.group(1)) # rows of first matrix + b = int( + dims_match.group(2) + ) # cols of first matrix / rows of second matrix + c = int(dims_match.group(4)) # cols of second matrix + + # Calculate computational volume: a × b × c + computational_volume = a * b * c + + # Normalize average time by computational volume + normalized_time = ( + stats["avg_time_mean"] / computational_volume + if computational_volume > 0 + else 0 + ) + + # Group key: operation type + tensor types + group_key = f"{operation_type} {tensor_types}" + grouped_operations[group_key].append( + (normalized_time, stats["total_operations"]) + ) + + # Calculate weighted mean normalized time for each group + for group_key, data_points in grouped_operations.items(): + if data_points: + # Extract normalized times and weights (total operations) + normalized_times = [point[0] for point in data_points] + weights = [point[1] for point in data_points] + + # Calculate weighted mean: sum(value * weight) / sum(weights) + weighted_sum = sum( + norm_time * weight + for norm_time, weight in zip(normalized_times, weights) + ) + total_weight = sum(weights) + + weighted_mean_normalized_time = ( + weighted_sum / total_weight if total_weight > 0 else 0 + ) + + global_stats.mat_mul_normalized[group_key] = { + "mean_normalized_time": weighted_mean_normalized_time, + "sample_count": len(data_points), + "total_operations": total_weight, + } + + def _create_normalized_time_plot( + self, + global_stats: GlobalStats, + output_dir: str = ".", + return_base64: bool = False, + ) -> str: + """Create a bar plot of normalized times and return the image filename or base64 data.""" + if not global_stats.mat_mul_normalized: + return "" + + try: + # Prepare data for plotting + operations = [] + normalized_times = [] + + # Sort by normalized time (most efficient first) + sorted_data = sorted( + global_stats.mat_mul_normalized.items(), + key=lambda x: x[1]["mean_normalized_time"], + ) + + for group_key, stats in sorted_data: + # Shorten operation names for better readability + short_name = group_key.replace("ggml_vk_mul_mat_vec_", "vec_").replace( + "ggml_vk_mul_mat_", "mat_" + ) + short_name = ( + short_name.replace("_q_f16", "") + .replace("_f16_f32", "") + .replace("_nc_f16_f32", "_nc") + .replace("_p021_f16_f32", "_p021") + ) + operations.append(short_name) + normalized_times.append(stats["mean_normalized_time"]) + + # Create the plot + plt.figure(figsize=(12, 8)) + bars = plt.bar( + range(len(operations)), normalized_times, color="steelblue", alpha=0.7 + ) + + # Customize the plot + plt.title( + "MatMul Operations: Normalized Time Efficiency\n(Lower = More Efficient)", + fontsize=14, + fontweight="bold", + ) + plt.xlabel("Operation Type and Tensor Types", fontsize=12) + plt.ylabel("Normalized Time (μs per tensor element)", fontsize=12) + + # Set x-axis labels with rotation for readability + plt.xticks(range(len(operations)), operations, rotation=45, ha="right") + + # Add value labels on top of bars + for bar, value in zip(bars, normalized_times): + plt.text( + bar.get_x() + bar.get_width() / 2, + bar.get_height() + value * 0.01, + f"{value:.6f}", + ha="center", + va="bottom", + fontsize=9, + ) + + # Improve layout + plt.tight_layout() + plt.grid(axis="y", alpha=0.3) + + if return_base64: + # Save to memory buffer and convert to base64 + buffer = io.BytesIO() + plt.savefig(buffer, format="png", dpi=300, bbox_inches="tight") + buffer.seek(0) + image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") + plt.close() + buffer.close() + return f"data:image/png;base64,{image_base64}" + else: + # Save the plot to file + plot_filename = os.path.join(output_dir, "matmul_normalized_times.png") + plt.savefig(plot_filename, dpi=300, bbox_inches="tight") + plt.close() + return plot_filename + + except ImportError: + # matplotlib not available + return "" + except Exception as e: + # Any other error in plotting + logging.warning(f"Could not create plot: {e}") + return "" + + def generate_report(self, global_stats: GlobalStats) -> str: + """Generate a comprehensive text report.""" + report = [] + + report.append("=" * 80) + report.append("VULKAN PROFILING GLOBAL ANALYSIS REPORT") + report.append("=" * 80) + report.append("") + + # Model Information + report.append("MODEL INFORMATION") + report.append("=" * 30) + report.append(f"Model File: {global_stats.model_info.model_file}") + report.append(f"Model Name: {global_stats.model_info.model_name}") + report.append(f"Architecture: {global_stats.model_info.architecture}") + report.append(f"Model Size: {global_stats.model_info.size_label}") + if global_stats.model_info.layer_count > 0: + report.append(f"Layers: {global_stats.model_info.layer_count}") + if global_stats.model_info.context_length > 0: + report.append(f"Context Length: {global_stats.model_info.context_length:,}") + if global_stats.model_info.embedding_length > 0: + report.append( + f"Embedding Dimension: {global_stats.model_info.embedding_length:,}" + ) + if global_stats.model_info.attention_heads > 0: + if ( + global_stats.model_info.attention_heads_kv > 0 + and global_stats.model_info.attention_heads_kv + != global_stats.model_info.attention_heads + ): + report.append( + f"Attention Heads: {global_stats.model_info.attention_heads} ({global_stats.model_info.attention_heads_kv} KV heads)" + ) + else: + report.append( + f"Attention Heads: {global_stats.model_info.attention_heads}" + ) + if global_stats.model_info.vocab_size > 0: + report.append(f"Vocabulary Size: {global_stats.model_info.vocab_size:,}") + report.append(f"Quantization: {global_stats.model_info.quantization}") + if global_stats.model_info.file_size: + report.append(f"File Size: {global_stats.model_info.file_size}") + if global_stats.model_info.quantized_by: + report.append(f"Quantized By: {global_stats.model_info.quantized_by}") + + # Show tensor types breakdown + if global_stats.model_info.tensor_types: + report.append("Tensor Types:") + for tensor_type, count in sorted( + global_stats.model_info.tensor_types.items(), + key=lambda x: x[1], + reverse=True, + ): + report.append(f" {tensor_type}: {count:,} tensors") + report.append("") + + # Device Allocation Information + report.append("DEVICE ALLOCATION SUMMARY") + report.append("=" * 40) + report.append(f"GPU Device: {global_stats.device_allocation.gpu_device}") + report.append(f"GPU Layers: {global_stats.device_allocation.gpu_layers}") + report.append( + f"GPU Operations: {global_stats.device_allocation.gpu_operations:,}" + ) + report.append( + f"CPU Operations: {global_stats.device_allocation.cpu_operations:,}" + ) + total_ops_device = ( + global_stats.device_allocation.gpu_operations + + global_stats.device_allocation.cpu_operations + ) + gpu_percentage = ( + (global_stats.device_allocation.gpu_operations / total_ops_device * 100) + if total_ops_device > 0 + else 0 + ) + cpu_percentage = ( + (global_stats.device_allocation.cpu_operations / total_ops_device * 100) + if total_ops_device > 0 + else 0 + ) + report.append( + f"GPU Utilization: {gpu_percentage:.1f}% ({global_stats.device_allocation.gpu_operations:,}/{total_ops_device:,} operations)" + ) + report.append( + f"CPU Utilization: {cpu_percentage:.1f}% ({global_stats.device_allocation.cpu_operations:,}/{total_ops_device:,} operations)" + ) + + # Show CPU operation types if any + if global_stats.device_allocation.cpu_operation_types: + report.append("CPU Operations by Type:") + for op_type, count in sorted( + global_stats.device_allocation.cpu_operation_types.items(), + key=lambda x: x[1], + reverse=True, + ): + report.append(f" {op_type}: {count:,} operations") + + report.append("") + + # Inference Steps Summary + report.append( + f"Total inference steps analyzed: {global_stats.total_inference_steps}" + ) + report.append( + f"Note: Device allocation counts ALL executed operations ({total_ops_device:,})," + ) + report.append( + " while timing statistics below only count profiled operations." + ) + report.append("") + + # General Operations Statistics + report.append("GENERAL OPERATIONS STATISTICS") + report.append("(Ordered by average execution time - highest first)") + report.append("=" * 50) + report.append("") + + # Sort by average time (highest first) + sorted_general = sorted( + global_stats.general_stats.items(), + key=lambda x: x[1]["avg_time_mean"], + reverse=True, + ) + + for operation, stats in sorted_general: + report.append(f"Operation: {operation}") + report.append(f" Total Operations: {stats['total_operations']:,}") + report.append( + f" Appeared in {stats['total_inference_steps']}/{global_stats.total_inference_steps} inference steps" + ) + report.append( + f" Average Time (μs): {stats['avg_time_mean']:.2f} ± {stats['avg_time_stdev']:.2f}" + ) + report.append( + f" Min: {stats['avg_time_min']:.2f}, Max: {stats['avg_time_max']:.2f}, Median: {stats['avg_time_median']:.2f}" + ) + report.append( + f" Operations per Inference Step: {stats['count_mean']:.1f} ± {stats['count_stdev']:.1f}" + if stats.get("count_stdev") + else f" Operations per Inference Step: {stats['count_mean']:.1f}" + ) + report.append("") + + # MatMul Timing Statistics + report.append("MAT_MUL TIMING STATISTICS") + report.append("(Ordered by average execution time - highest first)") + report.append("=" * 50) + report.append("") + + # Sort by average time (highest first) + sorted_mat_mul = sorted( + global_stats.mat_mul_stats.items(), + key=lambda x: x[1]["avg_time_mean"], + reverse=True, + ) + + for operation, stats in sorted_mat_mul: + report.append(f"Operation: {operation}") + report.append(f" Total Operations: {stats['total_operations']:,}") + report.append( + f" Appeared in {stats['total_inference_steps']}/{global_stats.total_inference_steps} inference steps" + ) + report.append( + f" Average Time (μs): {stats['avg_time_mean']:.2f} ± {stats['avg_time_stdev']:.2f}" + ) + report.append( + f" Min: {stats['avg_time_min']:.2f}, Max: {stats['avg_time_max']:.2f}, Median: {stats['avg_time_median']:.2f}" + ) + report.append(f" Operations per Inference Step: {stats['count_mean']:.1f}") + report.append("") + + # MatMul Tensor Size Normalized Analysis + if global_stats.mat_mul_normalized: + report.append("MAT_MUL TENSOR SIZE NORMALIZED ANALYSIS") + report.append( + "(Weighted mean normalized time by operation type and tensor types)" + ) + report.append("=" * 60) + report.append("") + + # Sort by mean normalized time (most efficient first - lowest time per operation) + sorted_normalized = sorted( + global_stats.mat_mul_normalized.items(), + key=lambda x: x[1]["mean_normalized_time"], + ) + + for group_key, stats in sorted_normalized: + report.append(f"Operation: {group_key}") + report.append( + f" Weighted Mean Normalized Time: {stats['mean_normalized_time']:.9f} μs per tensor element" + ) + report.append( + f" (Based on {stats['total_operations']:,} total operations across {stats['sample_count']} matrix size variations)" + ) + report.append("") + + # Top operations by total time + report.append("TOP OPERATIONS BY TOTAL EXECUTION TIME") + report.append("=" * 50) + report.append("") + + # Calculate total time for each operation (total_ops * avg_time_mean) + total_times = [] + for operation, stats in global_stats.general_stats.items(): + total_time = stats["total_operations"] * stats["avg_time_mean"] + total_times.append( + ( + operation, + total_time, + stats["total_operations"], + stats["avg_time_mean"], + ) + ) + + for operation, stats in global_stats.mat_mul_stats.items(): + total_time = stats["total_operations"] * stats["avg_time_mean"] + total_times.append( + ( + operation, + total_time, + stats["total_operations"], + stats["avg_time_mean"], + ) + ) + + total_times.sort(key=lambda x: x[1], reverse=True) + + for i, (operation, total_time, total_ops, avg_time) in enumerate( + total_times[:20] + ): + report.append(f"Operation: {operation}") + report.append(f" Total Operations: {total_ops:,}") + report.append( + f" Total Execution Time: {total_time:,.2f} μs ({total_time /1000:.2f} ms)" + ) + report.append(f" Average Time (μs): {avg_time:.2f}") + report.append("") + + # Summary statistics + report.append("TIMING SUMMARY STATISTICS") + report.append("(Based on profiled operations only)") + report.append("=" * 30) + report.append("") + + total_general_ops = sum( + stats["total_operations"] for stats in global_stats.general_stats.values() + ) + total_mat_mul_ops = sum( + stats["total_operations"] for stats in global_stats.mat_mul_stats.values() + ) + total_all_ops = total_general_ops + total_mat_mul_ops + + # Calculate percentages + general_percentage = ( + (total_general_ops / total_all_ops * 100) if total_all_ops > 0 else 0 + ) + mat_mul_percentage = ( + (total_mat_mul_ops / total_all_ops * 100) if total_all_ops > 0 else 0 + ) + + # Calculate total execution times + total_general_time = sum( + stats["total_operations"] * stats["avg_time_mean"] + for stats in global_stats.general_stats.values() + ) + total_mat_mul_time = sum( + stats["total_operations"] * stats["avg_time_mean"] + for stats in global_stats.mat_mul_stats.values() + ) + total_execution_time = total_general_time + total_mat_mul_time + + # Calculate time per inference step + time_per_inference_step = ( + total_execution_time / global_stats.total_inference_steps + if global_stats.total_inference_steps > 0 + else 0 + ) + + report.append( + f"Total General Operations: {total_general_ops:,} ({general_percentage:.1f}%)" + ) + report.append( + f"Total MatMul Operations: {total_mat_mul_ops:,} ({mat_mul_percentage:.1f}%)" + ) + report.append(f"Total Profiled Operations: {total_all_ops:,}") + report.append( + f"Non-profiled Operations: {total_ops_device - total_all_ops:,} (setup, memory management, etc.)" + ) + report.append("") + report.append( + f"Total Execution Time: {total_execution_time:,.2f} μs ({total_execution_time /1000:.2f} ms)" + ) + report.append( + f" General Operations: {total_general_time:,.2f} μs ({total_general_time /total_execution_time *100:.1f}%)" + ) + report.append( + f" MatMul Operations: {total_mat_mul_time:,.2f} μs ({total_mat_mul_time /total_execution_time *100:.1f}%)" + ) + report.append( + f"Average Time per Inference Step: {time_per_inference_step:,.2f} μs ({time_per_inference_step /1000:.2f} ms)" + ) + report.append("") + report.append( + f"Unique General Operation Types: {len(global_stats.general_stats)}" + ) + report.append( + f"Unique MatMul Operation Types: {len(global_stats.mat_mul_stats)}" + ) + + return "\n".join(report) + + def generate_markdown_report( + self, global_stats: GlobalStats, output_dir: str = "." + ) -> str: + """Generate a comprehensive markdown report.""" + report = [] + + report.append("# Vulkan Profiling Global Analysis Report") + report.append("") + + # Model Information + report.append("## Model Information") + report.append("") + report.append(f"- **Model File:** {global_stats.model_info.model_file}") + report.append(f"- **Model Name:** {global_stats.model_info.model_name}") + report.append(f"- **Architecture:** {global_stats.model_info.architecture}") + report.append(f"- **Model Size:** {global_stats.model_info.size_label}") + if global_stats.model_info.layer_count > 0: + report.append(f"- **Layers:** {global_stats.model_info.layer_count}") + if global_stats.model_info.context_length > 0: + report.append( + f"- **Context Length:** {global_stats.model_info.context_length:,}" + ) + if global_stats.model_info.embedding_length > 0: + report.append( + f"- **Embedding Dimension:** {global_stats.model_info.embedding_length:,}" + ) + if global_stats.model_info.attention_heads > 0: + if ( + global_stats.model_info.attention_heads_kv > 0 + and global_stats.model_info.attention_heads_kv + != global_stats.model_info.attention_heads + ): + report.append( + f"- **Attention Heads:** {global_stats.model_info.attention_heads} ({global_stats.model_info.attention_heads_kv} KV heads)" + ) + else: + report.append( + f"- **Attention Heads:** {global_stats.model_info.attention_heads}" + ) + if global_stats.model_info.vocab_size > 0: + report.append( + f"- **Vocabulary Size:** {global_stats.model_info.vocab_size:,}" + ) + report.append(f"- **Quantization:** {global_stats.model_info.quantization}") + if global_stats.model_info.file_size: + report.append(f"- **File Size:** {global_stats.model_info.file_size}") + if global_stats.model_info.quantized_by: + report.append(f"- **Quantized By:** {global_stats.model_info.quantized_by}") + + # Show tensor types breakdown + if global_stats.model_info.tensor_types: + report.append("") + report.append("**Tensor Types:**") + for tensor_type, count in sorted( + global_stats.model_info.tensor_types.items(), + key=lambda x: x[1], + reverse=True, + ): + report.append(f"- {tensor_type}: {count:,} tensors") + report.append("") + + # Device Allocation Information + total_ops_device = ( + global_stats.device_allocation.gpu_operations + + global_stats.device_allocation.cpu_operations + ) + gpu_percentage = ( + (global_stats.device_allocation.gpu_operations / total_ops_device * 100) + if total_ops_device > 0 + else 0 + ) + cpu_percentage = ( + (global_stats.device_allocation.cpu_operations / total_ops_device * 100) + if total_ops_device > 0 + else 0 + ) + + report.append("## Device Allocation Summary") + report.append("") + report.append(f"- **GPU Device:** {global_stats.device_allocation.gpu_device}") + report.append(f"- **GPU Layers:** {global_stats.device_allocation.gpu_layers}") + report.append( + f"- **GPU Operations:** {global_stats.device_allocation.gpu_operations:,} ({gpu_percentage:.1f}%)" + ) + report.append( + f"- **CPU Operations:** {global_stats.device_allocation.cpu_operations:,} ({cpu_percentage:.1f}%)" + ) + + # Show CPU operation types if any + if global_stats.device_allocation.cpu_operation_types: + report.append("") + report.append("**CPU Operations by Type:**") + for op_type, count in sorted( + global_stats.device_allocation.cpu_operation_types.items(), + key=lambda x: x[1], + reverse=True, + ): + report.append(f"- {op_type}: {count:,} operations") + report.append("") + + # General Operations Statistics + report.append("## General Operations Statistics") + report.append("*(Ordered by average execution time - highest first)*") + report.append("") + + # Sort by average time (highest first) + sorted_general = sorted( + global_stats.general_stats.items(), + key=lambda x: x[1]["avg_time_mean"], + reverse=True, + ) + + for operation, stats in sorted_general: + report.append(f"### {operation}") + report.append(f"- **Total Operations:** {stats['total_operations']:,}") + report.append( + f"- **Appeared in:** {stats['total_inference_steps']}/{global_stats.total_inference_steps} inference steps" + ) + report.append( + f"- **Average Time:** {stats['avg_time_mean']:.2f} ± {stats['avg_time_stdev']:.2f} μs" + ) + report.append( + f"- **Min/Max:** {stats['avg_time_min']:.2f} / {stats['avg_time_max']:.2f} μs, Median: {stats['avg_time_median']:.2f} μs" + ) + report.append( + f"- **Operations per Inference Step:** {stats['count_mean']:.1f}" + ) + report.append("") + + # MatMul Timing Statistics + report.append("## MatMul Timing Statistics") + report.append("*(Ordered by average execution time - highest first)*") + report.append("") + + # Sort by average time (highest first) + sorted_mat_mul = sorted( + global_stats.mat_mul_stats.items(), + key=lambda x: x[1]["avg_time_mean"], + reverse=True, + ) + + for operation, stats in sorted_mat_mul: + report.append(f"### {operation}") + report.append(f"- **Total Operations:** {stats['total_operations']:,}") + report.append( + f"- **Appeared in:** {stats['total_inference_steps']}/{global_stats.total_inference_steps} inference steps" + ) + report.append( + f"- **Average Time:** {stats['avg_time_mean']:.2f} ± {stats['avg_time_stdev']:.2f} μs" + ) + report.append( + f"- **Min/Max:** {stats['avg_time_min']:.2f} / {stats['avg_time_max']:.2f} μs, Median: {stats['avg_time_median']:.2f} μs" + ) + report.append( + f"- **Operations per Inference Step:** {stats['count_mean']:.1f}" + ) + report.append("") + + # MatMul Tensor Size Normalized Analysis + if global_stats.mat_mul_normalized: + report.append("## MatMul Tensor Size Normalized Analysis") + report.append( + "*(Weighted mean normalized time by operation type and tensor types)*" + ) + report.append("") + + # Sort by mean normalized time (most efficient first - lowest time per operation) + sorted_normalized = sorted( + global_stats.mat_mul_normalized.items(), + key=lambda x: x[1]["mean_normalized_time"], + ) + + for group_key, stats in sorted_normalized: + report.append(f"### {group_key}") + report.append( + f"- **Weighted Mean Normalized Time:** {stats['mean_normalized_time']:.9f} μs per tensor element" + ) + report.append( + f"- **Based on:** {stats['total_operations']:,} total operations across {stats['sample_count']} matrix size variations" + ) + report.append("") + + # Create and include plot for markdown report + plot_filename = self._create_normalized_time_plot(global_stats, output_dir) + if plot_filename and os.path.exists(plot_filename): + plot_basename = os.path.basename(plot_filename) + report.append("### Efficiency Comparison Chart") + report.append(f"![MatMul Normalized Time Efficiency]({plot_basename})") + report.append("") + report.append( + "*Chart shows weighted mean normalized time for each operation type and tensor type combination. Lower values indicate higher efficiency.*" + ) + report.append("") + + # Top operations by total time + report.append("## Top Operations by Total Execution Time") + report.append("") + + # Calculate total time for each operation (total_ops * avg_time_mean) + total_times = [] + for operation, stats in global_stats.general_stats.items(): + total_time = stats["total_operations"] * stats["avg_time_mean"] + total_times.append( + ( + operation, + total_time, + stats["total_operations"], + stats["avg_time_mean"], + ) + ) + + for operation, stats in global_stats.mat_mul_stats.items(): + total_time = stats["total_operations"] * stats["avg_time_mean"] + total_times.append( + ( + operation, + total_time, + stats["total_operations"], + stats["avg_time_mean"], + ) + ) + + total_times.sort(key=lambda x: x[1], reverse=True) + + for i, (operation, total_time, total_ops, avg_time) in enumerate( + total_times[:20] + ): + report.append(f"### {operation}") + report.append(f"- **Total Operations:** {total_ops:,}") + report.append( + f"- **Total Execution Time:** {total_time:,.2f} μs ({total_time /1000:.2f} ms)" + ) + report.append(f"- **Average Time:** {avg_time:.2f} μs") + report.append("") + + # Summary statistics + report.append("## Timing Summary Statistics") + report.append("*(Based on profiled operations only)*") + report.append("") + + total_general_ops = sum( + stats["total_operations"] for stats in global_stats.general_stats.values() + ) + total_mat_mul_ops = sum( + stats["total_operations"] for stats in global_stats.mat_mul_stats.values() + ) + total_all_ops = total_general_ops + total_mat_mul_ops + + # Calculate percentages + general_percentage = ( + (total_general_ops / total_all_ops * 100) if total_all_ops > 0 else 0 + ) + mat_mul_percentage = ( + (total_mat_mul_ops / total_all_ops * 100) if total_all_ops > 0 else 0 + ) + + # Calculate total execution times + total_general_time = sum( + stats["total_operations"] * stats["avg_time_mean"] + for stats in global_stats.general_stats.values() + ) + total_mat_mul_time = sum( + stats["total_operations"] * stats["avg_time_mean"] + for stats in global_stats.mat_mul_stats.values() + ) + total_execution_time = total_general_time + total_mat_mul_time + + # Calculate time per inference step + time_per_inference_step = ( + total_execution_time / global_stats.total_inference_steps + if global_stats.total_inference_steps > 0 + else 0 + ) + + report.append( + f"- **Total General Operations:** {total_general_ops:,} ({general_percentage:.1f}%)" + ) + report.append( + f"- **Total MatMul Operations:** {total_mat_mul_ops:,} ({mat_mul_percentage:.1f}%)" + ) + report.append(f"- **Total Profiled Operations:** {total_all_ops:,}") + report.append( + f"- **Non-profiled Operations:** {total_ops_device - total_all_ops:,} (setup, memory management, etc.)" + ) + report.append("") + report.append( + f"- **Total Execution Time:** {total_execution_time:,.2f} μs ({total_execution_time /1000:.2f} ms)" + ) + report.append( + f" - General Operations: {total_general_time:,.2f} μs ({total_general_time /total_execution_time *100:.1f}%)" + ) + report.append( + f" - MatMul Operations: {total_mat_mul_time:,.2f} μs ({total_mat_mul_time /total_execution_time *100:.1f}%)" + ) + report.append( + f"- **Average Time per Inference Step:** {time_per_inference_step:,.2f} μs ({time_per_inference_step /1000:.2f} ms)" + ) + report.append("") + report.append( + f"- **Unique General Operation Types:** {len(global_stats.general_stats)}" + ) + report.append( + f"- **Unique MatMul Operation Types:** {len(global_stats.mat_mul_stats)}" + ) + + return "\n".join(report) + + def convert_markdown_to_html( + self, markdown_content: str, html_filename: str, global_stats: GlobalStats + ) -> bool: + """Convert markdown content to HTML with base64-embedded images.""" + try: + # Generate base64-encoded plot + plot_base64 = self._create_normalized_time_plot( + global_stats, return_base64=True + ) + + # Add basic CSS styling for better appearance + css_style = """ + + """ + + # Convert markdown-style formatting to basic HTML + html_content = markdown_content + + # Convert markdown headers + html_content = re.sub( + r"^# (.+)$", r"

\1

", html_content, flags=re.MULTILINE + ) + html_content = re.sub( + r"^## (.+)$", r"

\1

", html_content, flags=re.MULTILINE + ) + html_content = re.sub( + r"^### (.+)$", r"

\1

", html_content, flags=re.MULTILINE + ) + + # Convert bold text + html_content = re.sub( + r"\*\*([^*]+)\*\*", r"\1", html_content + ) + + # Convert italic text + html_content = re.sub(r"\*([^*]+)\*", r"\1", html_content) + + # Convert bullet points + # First handle indented sub-items (for execution time breakdown) + html_content = re.sub( + r"^ - (General Operations:.*)$", + r'
  • \1
  • ', + html_content, + flags=re.MULTILINE, + ) + html_content = re.sub( + r"^ - (MatMul Operations:.*)$", + r'
  • \1
  • ', + html_content, + flags=re.MULTILINE, + ) + # Then handle regular bullet points + html_content = re.sub( + r"^- (.+)$", r"
  • \1
  • ", html_content, flags=re.MULTILINE + ) + + # Wrap consecutive
  • elements in
      tags + html_content = re.sub( + r"(
    • .*?
    • )\s*(?=\n[^<]|\n$)", + r"
        \1
      ", + html_content, + flags=re.DOTALL, + ) + html_content = re.sub(r"\s*
    • ", r"
    • ", html_content) + + # Convert images - replace external image references with base64 data + if plot_base64: + html_content = re.sub( + r"!\[([^\]]*)\]\(matmul_normalized_times\.png\)", + f'\\1', + html_content, + ) + + # Convert any remaining images (fallback) + html_content = re.sub( + r"!\[([^\]]*)\]\(([^)]+)\)", + r'\1', + html_content, + ) + + # Clean up line breaks and spacing + # Remove empty lines within sections + html_content = re.sub(r"\n\s*\n", "\n", html_content) + + # Convert remaining line breaks, but be more selective + # Don't add breaks after HTML tags + html_content = re.sub(r"\n(?!<)", "
      \n", html_content) + + # Remove breaks before and after HTML block elements + html_content = re.sub(r"
      \s*(]*>.*?)
      ", r"\1", html_content + ) + html_content = re.sub(r"
      \s*(
        |
      )", r"\1", html_content) + html_content = re.sub(r"(
        |
      )
      ", r"\1", html_content) + + # Clean up excessive breaks + html_content = re.sub(r"(
      \s*){3,}", r"

      ", html_content) + html_content = re.sub(r"(
      \s*){2}(\2", html_content) + + full_html = f""" + + + + + + Vulkan Profiling Analysis Report + {css_style} + + + {html_content} + + + """ + + with open(html_filename, "w", encoding="utf-8") as f: + f.write(full_html) + + return True + + except Exception as e: + logging.error(f"Error converting markdown to HTML: {e}") + return False + + +def main(): + parser = argparse.ArgumentParser( + description="Analyze Vulkan profiling results from log file" + ) + parser.add_argument( + "log_file", + nargs="?", + default="log.txt", + help="Path to log file (default: log.txt)", + ) + parser.add_argument( + "--report-output", + help="Save report to specified file (default: print to stdout)", + ) + parser.add_argument( + "--markdown-output", help="Save markdown report to specified file" + ) + parser.add_argument( + "--html-output", + help="Save HTML report to specified file (converted from markdown with embedded images)", + ) + + args = parser.parse_args() + + logging.basicConfig( + level=logging.INFO, format="%(levelname)s: %(message)s" + ) + + try: + analyzer = VulkanProfilingAnalyzer(args.log_file) + logging.info(f"Analyzing log file: {args.log_file}") + + analyzer.parse_log_file() + global_stats = analyzer.compute_global_statistics() + report = analyzer.generate_report(global_stats) + + if args.report_output: + with open(args.report_output, "w") as f: + f.write(report) + logging.info(f"Report saved to: {args.report_output}") + else: + sys.stdout.write("\n" + report) + + if args.markdown_output: + markdown_dir = ( + os.path.dirname(os.path.abspath(args.markdown_output)) + if os.path.dirname(args.markdown_output) + else "." + ) + markdown_report = analyzer.generate_markdown_report( + global_stats, markdown_dir + ) + with open(args.markdown_output, "w") as f: + f.write(markdown_report) + logging.info(f"Markdown report saved to: {args.markdown_output}") + + if args.html_output: + html_dir = ( + os.path.dirname(os.path.abspath(args.html_output)) + if os.path.dirname(args.html_output) + else "." + ) + markdown_report = analyzer.generate_markdown_report(global_stats, html_dir) + success = analyzer.convert_markdown_to_html( + markdown_report, args.html_output, global_stats + ) + if success: + logging.info( + f"HTML report saved to: {args.html_output} (self-contained with embedded images)" + ) + else: + logging.error("HTML generation failed. See error messages above.") + + except FileNotFoundError: + logging.error(f"Log file '{args.log_file}' not found.") + sys.exit(1) + except Exception as e: + logging.error(f"Error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main()