Skip to content

optimization(vulkan): accelerate Android Gaussian blur - #126

Merged
chuzcjoe merged 1 commit into
masterfrom
daily/optimization-vulkan-android-gaussian-blur-20261024
Oct 26, 2026
Merged

chuzcjoe merged 1 commit into
masterfrom
daily/optimization-vulkan-android-gaussian-blur-20261024

Conversation

@chuzcjoe

Copy link
Copy Markdown
Collaborator

Problem

The existing Vulkan ComputeGaussianBlur workload in tests/vulkan/ runs a fixed 3x3 Gaussian filter on a 4000x3000 float image. Its Slang shader reads invariant weights through a storage buffer, and every pipeline owns that buffer, its device-memory allocation and descriptor. The original Android GPU dispatch takes about 10.248 ms on the connected Adreno 740.

This PR optimizes that existing Android compute path. It does not claim to accelerate the public Vulkan library, application rendering, another graphics backend or unmeasured devices. Baseline: freshly fetched 03119ea9bb65bb8ec24d9c694e268959b5404931; reviewed merged #124/#125 and recent closed proposals, with no open issue/PR overlap or earlier Oct 24 PR.

Solution

  • On Android, use the existing GLSL implementation with compile-time weights and precise accumulation. Remove its weight buffer, device-memory allocation, initialization map/copy and storage descriptor: storage descriptors 3 -> 2, total bindings 4 -> 3. The logical removed payload is 36 bytes; driver allocation size/RSS was not measured.
  • Keep the original Slang shader and weight-buffer path on macOS and other platforms. Preserve the constructor/Run interface, dimensions, 16x16 dispatch geometry, nine-tap order and copy-through borders. No separable approximation, lower precision, extra pass or scratch image is introduced.
  • Add an executable benchmark that embeds the exact original Slang source and reproduces its original four-binding host layout. It alternates the original/final paths on the same buffers and checks complete outputs against an independent CPU stencil. Pipeline construction is outside timing.
  • Add a GPU regression case covering 12 shapes, three inputs and reused command/pipeline resources. The final output must match the original GPU output byte-for-byte, including 37 sentinel elements after the image. Dyadic/nonuniform and impulse inputs also match the CPU exactly; fractional /13 inputs validate the original GPU against the CPU within 2e-6 before comparing new/original GPU bytes. Source contents stay unchanged. Shapes include 1x1, single rows/columns, 2-pixel borders and partial 16x16 groups.

Existing ComputeGaussianBlur callers opt in automatically when built for Android. Run the existing ComputeGaussianBlur.test through the normal test script; run vulkan_gaussian_benchmark separately for measurements. This helper remains under tests; there is no new public API or independent shared-module/dependency change.

Candidate/alternative evaluation: shared-memory tiles reduced redundant loads but regressed Android by about 46% on the large input, so were rejected. Applying GLSL on macOS regressed medium inputs by about 2.5-2.8x and large inputs by about 20%, so the platform selection is deliberate. Constant Slang weights changed last-bit results on fractional inputs; the installed Slang emitted no NoContraction decoration for the attempted precise mode. A related upstream Slang precision report describes this class of limitation. The final GLSL emits NoContraction and passes original-GPU byte comparisons on Android. Timing improvements below compare the complete old Slang path with the complete new GLSL path; compiler and constant-weight effects are not isolated.

Other candidates were pitched/partial texture uploads (no concrete streaming consumer yet) and exclusive Vulkan resource ownership (requires a separate compatibility/lifetime design). Neither was expanded into this PR.

Validation

Final commit: 88dd29347ab5127017f4427281c8672432b145c8; tested source tree deee98f6223f49a6d8af30c9f37f0bad26db9e24 matches the commit. Full diff reviewed, git diff --check passed. All locked submodules initialized in this fresh Orca worktree; builds/tests stayed here. No installs, new dependencies, licenses, shell profiles, APKs or device settings were changed. An existing ignored PNG fixture was copied from the main checkout and SHA256-checked without modifying the source.

Both core builds use C++20, -O3 -DNDEBUG, -Wall -Wextra -Wpedantic -Werror; run.sh requests Debug but the top-level project supplies these actual flags. CMake 4.4.4, Vulkan SDK 1.4.350.0, glslc shaderc 2026.2, Slang 2026.8, xxd; explicit shell-local PATH resolves CMake/adb/glslc/slangc/xxd.

macOS native, Apple M6 / MoltenVK 1.4.1, macOS 27.0.1, SDK 27.0, Apple Clang 21.0.0:

source /Users/joechu/VulkanSDK/1.4.350.0/setup-env.sh
./scripts/run.sh -t macos -r tests
./build/macos/tests/vulkan/vulkan_gaussian_benchmark
  • Full script exit 0; 43 passed / 0 failed / 0 skipped, all 6 default executables: mat 3, threadpool 4, stb 4, opencl 8, vulkan 23, metal 1. All default examples built.
  • Benchmark invoked three times after the final source edits. Original and final GPU paths are intentionally the same here; no macOS speedup is claimed. Validation requested, no Vulkan validation errors observed.

Android arm64-v8a, sole authorized physical USB model 2210132C / nuwa, Adreno 740, Android 13 / API 33; existing NDK 28.2.13676358 (r28c), Android Clang 19.0.1, target API 34, JDK 17.0.20.1, adb 37.0.1:

adb devices -l
export ANDROID_NDK_ROOT=/Users/joechu/Library/Android/sdk/ndk/28.2.13676358
./scripts/run.sh -t arm64-v8a -r tests
adb push build/arm64-v8a/tests/vulkan/vulkan_gaussian_benchmark /data/local/tmp/core/vulkan_gaussian_benchmark
adb shell /data/local/tmp/core/vulkan_gaussian_benchmark
  • Vulkan SDK setup was loaded explicitly; ANDROID_SERIAL pinned all subsequent adb calls to the sole authorized physical target; emulator property was empty, hardware qcom.
  • Full script exit 0; 45 passed / 0 failed / 0 skipped, all 7 default executables actually ran on the device: mat 3, threadpool 4, stb 4, opencl 8, vulkan 23, gles 2, ahardwarebuffer 1. Verified each real adb shell invocation plus matching GoogleTest RUN/OK/start/completion/pass counts; set-e script success confirms process exits.
  • Benchmark invoked three times on that device after the final source edits. Android validation was requested; layer availability was not independently established.
  • Extracted final GLSL SPIR-V validates with spirv-val --target-env vulkan1.1 and contains NoContraction decorations. Its C-formatted words were converted to binary for validation/disassembly.

Controlled benchmark

Per invocation/case/path: 5 warmups + 31 timed samples; three complete invocations per platform, 93 samples/path/case total. Alternating order reverses each pair. Same device, toolchain/core configuration, input and buffers within comparisons; no builds ran concurrently with the final measurements. GPU timestamps bracket pipeline binding/dispatch, checked query/fence results and valid timestamp bits. Allocation, compilation/cache loading, CPU recording, submission/wait, data fill/oracle/checks and final host-read barrier are outside the GPU interval. Every output element is checked after the first warmup and last sample for both paths; input immutability is checked afterward.

Second-invocation GPU medians, microseconds:

GPU Image Original Final
Apple M6 64 x 64 6.458 6.417
Apple M6 1024 x 1024 32.208 32.083
Apple M6 1003 x 769 27.333 27.042
Apple M6 4000 x 3000 781.667 781.416
Adreno 740 64 x 64 19.427 17.552
Adreno 740 1024 x 1024 946.042 718.802
Adreno 740 1003 x 769 678.333 349.792
Adreno 740 4000 x 3000 10248.385 5133.750

For 4000x3000, Adreno p10/median/p90 in the second invocation: original [10246.979, 10248.385, 10259.427] us; final [5133.281, 5133.750, 5134.167] us. Three invocation medians: 10248.698 -> 5133.542, 10248.385 -> 5133.750, 10248.385 -> 5133.750 us: approximately 2.0x GPU dispatch speedup / 49.9% shorter interval for this workload.

M6 large-input medians: 871.042 -> 875.083, 781.667 -> 781.416, 765.292 -> 764.667 us, consistent with the unchanged GPU path and measurement noise. Medium/small-case variation is retained in the logs; Android 1024x1024 final medians range 540-719 us. Power, thermal state and frequency were not controlled. No general GPU, macOS, FPS, CPU time, upload, pipeline-compilation or RSS benefit is claimed. Benefits have only been measured on this Android GPU/configuration and these finite-input workloads.

Initial unmodified full suites passed 42 macOS / 44 Android tests. A preliminary fractional-input exact CPU comparison and the rejected constant Slang variant failed before the final design; those logs and rejected tile/macOS-GLSL measurements are retained. Final full runs above are the accepted validation. Logs, raw sample CSV rows, p10/median/p90 summaries, device/environment snapshots and verifier are in this worktree's ignored build/daily-gaussian-20261024/; macOS tmp outputs were preserved before Android recreated tmp/.

Changes

Relative to current origin/master: 9 files, +477 / -15 = 492 changed lines, below 1000.

File Added Deleted Category
tests/vulkan/CMakeLists.txt 9 0 tests/benchmark/build
tests/vulkan/ComputeGaussianBlur/ComputeGaussianBlur.comp 3 2 implementation
tests/vulkan/ComputeGaussianBlur/ComputeGaussianBlur.cpp 32 8 implementation
tests/vulkan/ComputeGaussianBlur/ComputeGaussianBlur.h 5 5 implementation
tests/vulkan/ComputeGaussianBlur/GaussianBlurReference.h 39 0 tests/benchmark/build
tests/vulkan/GaussianBlurShaderTest.cpp 112 0 tests/benchmark/build
tests/vulkan/benchmarks/GaussianBlurBaseline.h 85 0 tests/benchmark/build
tests/vulkan/benchmarks/GaussianBlurBaseline.slang 44 0 tests/benchmark/build
tests/vulkan/benchmarks/GaussianBlurBenchmark.cpp 148 0 tests/benchmark/build

Implementation/shader/helper: +40/-15; regression tests, CPU oracle, baseline and benchmark/build: +437/-0; documentation: 0. No project Markdown/README edits, generated binaries/assets or added code comments.

Risk

The measured optimization is Android-specific; other Android drivers may schedule/compiler-optimize this shader differently. Fractional finite inputs are checked against the original GPU bytes on both tested devices, not an exhaustive IEEE/NaN/subnormal proof. GLSL's precise accumulation is deliberate; the old Slang kernel remains the baseline and non-Android implementation. The private test helper's platform-specific layout requires rebuilding its callers; public Vulkan headers and function signatures are unchanged. This does not add configurable kernels, in-place blur, new dimension validation, asynchronous ownership or a rendering integration; existing positive-dimension, distinct-buffer assumptions remain. Full default suites pass on both required targets; graphical windows were built but not visually exercised.

@chuzcjoe
chuzcjoe force-pushed the daily/optimization-vulkan-android-gaussian-blur-20261024 branch from 88dd293 to 433779f Compare October 25, 2026 21:38
@chuzcjoe
chuzcjoe marked this pull request as ready for review October 26, 2026 20:47
@chuzcjoe
chuzcjoe merged commit 2335c56 into master Oct 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant