mtmd, vulkan : Gemma 4 E2B encoder token-count + Android Vulkan build + Mali attention speedup (QVAC-21361)#172
Merged
gianni-cor merged 12 commits intoJul 3, 2026
Conversation
…uild (QVAC-21361) mtmd: clip_n_output_tokens computed Gemma3/Gemma4V/IDEFICS3/InternVL/Nemotron/ Llama4 output tokens as floor(W/p * H/p / s^2), which disagrees with the encoder's per-dimension 2D pooling floor(W/p/s) * floor(H/p/s). When a side is not divisible by n_merge the predicted count mismatched the actual graph output and aborted in clip_image_batch_encode. Round per dimension; identical for divisible inputs, correct otherwise. vulkan: the Android NDK provides only the C Vulkan headers, so the build failed on vulkan/vulkan.hpp and spirv/unified1/spirv.hpp. find_package(SPIRV-Headers) is now QUIET on Android, and the Android branch fetches pinned, header-only Vulkan-Headers + SPIRV-Headers via FetchContent so the Android Vulkan build works out of the box. Co-authored-by: Cursor <cursoragent@cursor.com>
…alysis (QVAC-21361) Device-independent analysis to guide Pixel 9 Pro (Mali) kernel optimization: encoder op coverage (no CPU fallbacks, graph splits = 1), exact matmul/attention kernel shapes, an analytical FLOP breakdown, and how the dominant kernel shifts from FFN matmul to attention as the token budget grows. Co-authored-by: Cursor <cursoragent@cursor.com>
…oid headers The qvac-fabric vcpkg port already provides the Android Vulkan C++ bindings (downloads Vulkan-Headers into ggml/src/ggml-vulkan/vulkan_cpp_wrapper) and SPIRV-Headers (vcpkg dependency). The earlier FetchContent-based change here overlapped that mechanism and, under the vcpkg port build, left ggml-vulkan.cpp unable to find <vulkan/vulkan.hpp>, breaking the arm64-android build (verified in qvac CI on the llm-llamacpp overlay PR). Revert ggml/src/ggml-vulkan/CMakeLists.txt to the temp-9341 baseline so the port keeps owning Android header provisioning. The standalone-build convenience, if wanted, can be proposed separately. Only the mtmd token-count fix remains. Co-authored-by: Cursor <cursoragent@cursor.com>
…n (QVAC-21361) Optimize the Gemma 4 E2B vision encoder on Arm Mali (Vulkan scalar flash-attn, no cooperative-matrix), measured on Pixel 9 Pro: * vulkan: bake the on-device-best scalar flash-attn tile config (D_split=4, Br=4) for Mali on the short-sequence path, plus GGML_VK_FA_BR / GGML_VK_FA_DSPLIT env overrides for tuning. Output unchanged (cosine ~0.99998 vs the generic config). * mtmd/clip: budget-aware attention selection on the AUTO path. On Mali, AUTO now picks the explicit (mul_mat) path for short sequences, where it is faster, and flash-attn for long ones, where explicit OOMs. The choice is per-image, gated by n_patches with a memory-aware cutoff, and guarded by an OOM-to-flash-attn rebuild fallback. Default-on for Mali only (other GPUs keep prior behavior); override via MTMD_CLIP_AUTO_FA_MIN_KV. * vulkan/CMake: source pinned Vulkan / SPIRV-Headers from the build tree's _deps (offline) for the Android NDK build, falling back to the upstream repos when absent. * mtmd-debug, mtmd: gate the per-op eval callback behind MTMD_DEBUG_CB_EVAL (clean encoder timing by default) and add an in-process flash-attn config sweep + embedding accuracy check used for the on-device tuning. Co-authored-by: Cursor <cursoragent@cursor.com>
Remove the device-independent op-coverage / hotspot writeup from this PR; it will be tracked separately rather than in the encoder fix/optimization PR. Co-authored-by: Cursor <cursoragent@cursor.com>
…QVAC-21361) The in-process flash-attn sweep used POSIX setenv/unsetenv, which MSVC does not provide, breaking every Windows CI leg at mtmd.cpp compile. Wrap them in mtmd_debug_setenv/unsetenv helpers that use _putenv_s on _WIN32 (empty value unsets) and setenv/unsetenv elsewhere. Debug-only path; no behavior change. Co-authored-by: Cursor <cursoragent@cursor.com>
…(QVAC-21361) - clip: use ceil for IDEFICS3/NEMOTRON_V2_VL token count (build_patch_merge_permute pads dims up), keep floor for the pool/reshape projectors - clip: cache Mali/env/device-memory inputs for the AUTO flash-attn decision so they don't run per image; force FA across the sequential OOM fallback; atoi -> strtol - vulkan: reject zero / out-of-range GGML_VK_FA_* overrides before they reach dispatch - vulkan/cmake: pin fetched Vulkan/SPIRV headers to commit SHAs; header-only FetchContent_MakeAvailable instead of deprecated FetchContent_Populate (CMake 4.x) Co-authored-by: Cursor <cursoragent@cursor.com>
…an (QVAC-21361) As a static lib, the PRIVATE link to SPIRV-Headers::SPIRV-Headers leaked into the installed ggml-targets export, breaking downstream find_package(ggml) on platforms whose Vulkan SDK ships the config target (Windows). Wrap it in BUILD_INTERFACE: since it is only needed to compile ggml-vulkan.cpp. Co-authored-by: Cursor <cursoragent@cursor.com>
iancris
approved these changes
Jul 2, 2026
olyasir
reviewed
Jul 2, 2026
olyasir
reviewed
Jul 2, 2026
olyasir
reviewed
Jul 2, 2026
…clamped FA overrides Addresses @olyasir's review on PR #172: - [blocker] CMakeLists: the Android <vulkan/vulkan.hpp> fallback checked only a build-tree _deps path the vcpkg port never populates, so a port build fell through to a live FetchContent git clone at configure time and prepended a second, differently-pinned copy BEFORE the port's staged headers (the exact conflict the earlier revert fixed). Now prefer the port's provisioned vulkan_cpp_wrapper/include first; only fall back to _deps, then FetchContent, for standalone NDK builds — no clone or shadowing under the port. - [perf] ggml-vulkan: get_fa_tuning_params_scalar runs on the FA dispatch hot path (per layer, per token). The ~9 GGML_VK_FA_* getenv() lookups (and GGML_VK_FA_DEBUG) are now parsed ONCE into a thread-safe function-local static and only cheaply applied per call. - [security] ggml-vulkan: env tile/split overrides were bounded only by UINT32_MAX, so a value like 4294967295 made Br+1 wrap to 0 in the uint32 shared-memory size math and slip past ggml_vk_flash_attn_scalar_shmem_support. Tile dims are now clamped to 1..1024 at parse time, keeping that math from wrapping and dispatch params sane. Verified: clean Android arm64 + Vulkan build (NDK r28b) of llama-mtmd-cli. Co-authored-by: Cursor <cursoragent@cursor.com>
The cached FA-override parse lambda is capture-less, so MSVC rejects using the block-scope constexpr FA_OVERRIDE_MAX inside it (error C3493: cannot be implicitly captured). GCC/Clang accept it, but the Windows Vulkan CI build failed. Make FA_OVERRIDE_MAX `static constexpr` (static storage duration → no capture required); still a compile-time constant, no behavior change. Co-authored-by: Cursor <cursoragent@cursor.com>
…d headers Per review (Gianfranco): for the addon build there is no spirv-headers issue — the qvac-fabric vcpkg port already depends on spirv-headers and stages Vulkan-Headers into vulkan_cpp_wrapper, and the temp-9341 baseline CMake already consumes both (find_package(SPIRV-Headers) + the ANDROID vulkan_cpp_wrapper include). The QUIET/FetchContent fallbacks only served a standalone (non-vcpkg) Android NDK build, which is not the addon path and is not exercised in fabric CI. Restore ggml-vulkan/CMakeLists.txt to the temp-9341 baseline. This also removes the FetchContent Android fallback olyasir flagged as a blocker (no more configure-time clone / port-header shadowing). The token-count fix and the Mali attention optimization (clip.cpp, ggml-vulkan.cpp, mtmd.cpp) are unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
…changes Per review (Gianfranco: "check if every change is really needed"): remove the on-device tuning/benchmark scaffolding that was used to *find* the config but is not needed to ship. - ggml-vulkan.cpp: drop the GGML_VK_FA_* env-override block, GGML_VK_FA_F16ACC, and the FA-CAPS/FA-TUNE debug prints (and their includes). Keep only the baked result of that sweep: the Arm Mali scalar FA tile tuning (D_split=4, Br=4). This also removes the env-override surface olyasir's perf/security comments were about. - mtmd.cpp: revert the MTMD_DEBUG_FA_SWEEP / accuracy-check / encode-bench debug harness to baseline. - mtmd-debug.cpp: revert the MTMD_DEBUG_CB_EVAL gating to baseline. Net PR is now just the correctness fix (clip.cpp token-count) + the Mali optimization (clip.cpp budget-aware attention + OOM fallback; ggml-vulkan.cpp FA tile tuning). ggml-vulkan.cpp syntax-checked with the Android/Vulkan flags. Co-authored-by: Cursor <cursoragent@cursor.com>
gianni-cor
approved these changes
Jul 3, 2026
This was referenced Jul 4, 2026
makaveli10
pushed a commit
to makaveli10/qvac-ext-lib-llama.cpp
that referenced
this pull request
Jul 13, 2026
…clamped FA overrides Addresses @olyasir's review on PR tetherto#172: - [blocker] CMakeLists: the Android <vulkan/vulkan.hpp> fallback checked only a build-tree _deps path the vcpkg port never populates, so a port build fell through to a live FetchContent git clone at configure time and prepended a second, differently-pinned copy BEFORE the port's staged headers (the exact conflict the earlier revert fixed). Now prefer the port's provisioned vulkan_cpp_wrapper/include first; only fall back to _deps, then FetchContent, for standalone NDK builds — no clone or shadowing under the port. - [perf] ggml-vulkan: get_fa_tuning_params_scalar runs on the FA dispatch hot path (per layer, per token). The ~9 GGML_VK_FA_* getenv() lookups (and GGML_VK_FA_DEBUG) are now parsed ONCE into a thread-safe function-local static and only cheaply applied per call. - [security] ggml-vulkan: env tile/split overrides were bounded only by UINT32_MAX, so a value like 4294967295 made Br+1 wrap to 0 in the uint32 shared-memory size math and slip past ggml_vk_flash_attn_scalar_shmem_support. Tile dims are now clamped to 1..1024 at parse time, keeping that math from wrapping and dispatch params sane. Verified: clean Android arm64 + Vulkan build (NDK r28b) of llama-mtmd-cli. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes and an on-device optimization for the Gemma 4 E2B vision encoder on the Vulkan backend (Android / Pixel 9 Pro, Arm Mali).
Correctness
tools/mtmd/clip.cpp).clip_n_output_tokenscomputed the post-pool token count for theGEMMA3 / GEMMA4V / IDEFICS3 / INTERNVL / NEMOTRON_V2_VL / LLAMA4projectors asfloor(W/p · H/p / s²), but the encoder graph pools per dimension:floor(W/p/s) · floor(H/p/s). When a side isn't divisible byn_mergethe twodisagree, the predicted count mismatches the actual graph output, and the run
aborts in
clip_image_batch_encode("Invalid number of output tokens").Now rounds per dimension — identical for divisible inputs, correct otherwise.
Build
ggml/src/ggml-vulkan/CMakeLists.txt).The Android NDK ships only the C Vulkan headers (
vulkan/vulkan.h); it providesneither the C++ bindings (
vulkan/vulkan.hpp) nor SPIRV-Headers(
spirv/unified1/spirv.hpp). Fix:find_package(SPIRV-Headers)isQUIET; when the target is missing the buildsources pinned, header-only SPIRV-Headers from the build tree's
_deps(offline), falling back to the upstream repo.
vulkan/vulkan.hpp) are likewise taken fromthe pinned
_depsheaders and added as an include dir only (the NDK alreadydefines the
Vulkan::Headerstarget, so it is not redefined).Optimization (Mali / Pixel 9 Pro)
The encoder's dominant kernel shifts from FFN matmul to attention as the token
budget grows, and on Mali the Vulkan flash-attn path is scalar (no
cooperative-matrix), so it is comparatively slow at short sequence lengths.
vulkan — bake the on-device-best scalar flash-attn tile config for Mali
(
ggml/src/ggml-vulkan/ggml-vulkan.cpp).Use
D_split=4, Br=4on the short-sequence path;GGML_VK_FA_BR/GGML_VK_FA_DSPLITallow overriding for tuning. Output is unchanged(cosine ~0.99998 vs the generic config).
mtmd/clip — budget-aware attention selection on the AUTO path
(
tools/mtmd/clip.cpp).On Mali, AUTO now selects the explicit (mul_mat) attention path for short
sequences — where it is faster than scalar flash-attn — and flash-attn for
long sequences, where the explicit path's
N²score matrix would OOM. Thedecision is per-image, gated by
n_patcheswith a memory-aware cutoff, andguarded by an OOM-to-flash-attn rebuild fallback (always correct). This is
default-on for Mali only; other GPUs keep the prior behavior. Override the
crossover via
MTMD_CLIP_AUTO_FA_MIN_KV.Tooling (debug-only, env-gated)
tools/mtmd/debug/mtmd-debug.cpp: the per-op eval callback now sits behindMTMD_DEBUG_CB_EVALso encoder timing is clean by default.tools/mtmd/mtmd.cpp: optional in-process flash-attn config sweep + embeddingaccuracy check (cosine / max-abs-err) used to drive the on-device tuning above.
Why
The token-count abort makes the Gemma 4 encoder unusable for any image whose
patch grid isn't a multiple of
n_merge. The build gap blocks cross-compiling theVulkan backend for Android at all. With those resolved, the attention-selection
work targets the actual hotspot identified for QVAC-21361.
Verification
-DGGML_VULKAN=ON): clean configure +build of
llama-mtmd-cli/llama-mtmd-debugandlibggml-vulkan.so, with nosystem SPIRV-Headers — the pinned
_depsheaders supply them.inputs unchanged (no regression).
explicit path at 196 tokens and flash-attn at 784 tokens (no OOM); embeddings
unchanged (cosine ~0.99998).
Encoder timing — before vs after (Pixel 9 Pro, Mali-G715)
Baseline = original generic flash-attn; after = this PR's AUTO selection + Mali tile tuning.
Embeddings match the baseline to cosine ~0.99998 in both cases.
Notes
libvulkan.so; the fetched headersare compile-time only.
Made with Cursor