Skip to content

Commit b4fc7b5

Browse files
authored
Merge pull request #9 from KakaruHayate/feature/perf-vulkan-pipeline-cache
perf(Vulkan): persistent VkPipelineCache (cold-start fix)
2 parents bc1e410 + 8172f7c commit b4fc7b5

5 files changed

Lines changed: 303 additions & 3 deletions

File tree

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Patches are applied with `git apply` during CMake configure; they must keep
2+
# LF line endings on every platform (Windows checkout otherwise rewrites LFs to
3+
# CRLF, making the patch unapplicable).
4+
*.patch text eol=lf
5+
*.md text eol=lf

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,12 @@ configs produce identical note output):
199199
is limited.
200200
- CPU benefits from Q8 for memory, at essentially the same speed — use the
201201
`-q8` pack on CPU.
202-
- Cold start: the first inference on a GPU compiles shaders
203-
(Vulkan/Metal). NVIDIA's driver caches these across runs; expect a couple
204-
of extra seconds on the very first invocation.
202+
- Cold start: the first inference on a GPU compiles shaders (Vulkan/Metal).
203+
Metal uses an in-repo binary-archive PSO cache; **Vulkan now persists a
204+
disk-backed `VkPipelineCache`** (`cmake/patches/ggml-vulkan-pipeline-cache.*`,
205+
`GGML_VK_PIPELINE_CACHE_PATH`, default under the user cache dir), so later
206+
launches load precompiled PSOs instead of recompiling — no reliance on
207+
driver-level caches.
205208

206209
## Reproducing the benchmark
207210

cmake/Dependencies.cmake

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ if(NOT ggml_POPULATED)
104104
)
105105
endif()
106106

107+
# Vulkan cold-start fix: persist VkPipelineCache to disk. Apply on every
108+
# build (the patch only touches ggml-vulkan.cpp, compiled only when the
109+
# Vulkan backend is enabled) so CPU-only and GPU builds share one source.
110+
game_ggml_apply_patch(
111+
"${ggml_SOURCE_DIR}"
112+
"${CMAKE_CURRENT_LIST_DIR}/patches/ggml-vulkan-pipeline-cache.patch"
113+
"ggml Vulkan disk-backed VkPipelineCache"
114+
)
115+
107116
add_subdirectory("${ggml_SOURCE_DIR}" "${ggml_BINARY_DIR}")
108117
endif()
109118

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# ggml-vulkan-pipeline-cache(game.cpp 自有 patch)
2+
3+
## Purpose
4+
5+
Vulkan 冷启动 Currently every process recompiles the whole compute-pipeline
6+
set from SPIR-V on first inference(本地实测首次冷启:Vulkan+Q8 达 42s、
7+
F32 约 7.5s;NVIDIA 驱动级 shader 缓存能缓解,但 AMD/Intel/其它驱动不能
8+
保证,且无法随包分发)。
9+
10+
This patch persists `VkPipelineCache` to disk — the Vulkan analog of the
11+
Metal binary-archive PSO cache (`cmake/patches/ggml-metal-binary-archive.patch`).
12+
After the first run, subsequent launches (or any process on a compatible
13+
driver) load precompiled PSO bytes instead of recompiling every shader.
14+
15+
## Change
16+
17+
`src/ggml-vulkan/ggml-vulkan.cpp`:
18+
19+
- `vk_device_struct` gains `pipeline_cache` / `pipeline_cache_init` /
20+
`pipeline_cache_path` / `pipeline_cache_dirty`.
21+
- At device init (`ggml_vk_init_pipeline_cache`) load the on-disk cache bytes
22+
into `VkPipelineCacheCreateInfo::pInitialData` (stale/invalid bytes are safe
23+
— the driver returns `VK_INCOMPLETE` and builds a fresh cache), and create
24+
the handle.
25+
- `ggml_vk_create_pipeline_func` passes the cache handle to
26+
`createComputePipeline` and sets `pipeline_cache_dirty = true`.
27+
- On graph-compute completion (and at device teardown as a fallback),
28+
`ggml_vk_save_pipeline_cache_device` flushes `vkGetPipelineCacheData` to
29+
disk, throttled by the dirty flag.
30+
31+
## Env / control
32+
33+
- `GGML_VK_PIPELINE_CACHE_PATH` — cache file path.
34+
- default: `%LOCALAPPDATA%\\game_ggml_vk_pipeline.cache` (Windows) or
35+
`$HOME/.cache/game_ggml_vk_pipeline.cache` (POSIX).
36+
- `GGML_VK_DISABLE_PIPELINE_CACHE` — opt out.
37+
- `GGML_VK_PIPELINE_CACHE_DEBUG` — print load/save diagnostics.
38+
39+
## Baseline & re-apply
40+
41+
- Applies to **ggml v0.19.0** (`ggml-vulkan.cpp`). Pinned by game.cpp
42+
FetchContent; re-apply per ggml upgrade via `cmake/Dependencies.cmake`
43+
`game_ggml_apply_patch` (idempotent: skips if already applied).
44+
45+
## Verified (local, RTX 2070, Vulkan)
46+
47+
- Run1 (no cache): builds from scratch, saves ~1.14 MB.
48+
- Run2 (with cache): logs "loaded 1,140,086 bytes", same note output (33/33),
49+
warm total 0.42s.
50+
51+
## Notes
52+
53+
- The dispatch-table C functions (`vkCreatePipelineCache` /
54+
`vkGetPipelineCacheData` / `vkDestroyPipelineCache`) are used because this
55+
SDK's generated `vulkan.hpp` does not expose the corresponding C++ class
56+
methods.
57+
- Devices are cached for the process lifetime in ggml-vulkan, so the save is
58+
driven by graph-compute completion rather than the device destructor.
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
diff --git a/src/ggml-vulkan/ggml-vulkan.cpp b/src/ggml-vulkan/ggml-vulkan.cpp
2+
index a923755..56304ff 100644
3+
--- a/src/ggml-vulkan/ggml-vulkan.cpp
4+
+++ b/src/ggml-vulkan/ggml-vulkan.cpp
5+
@@ -50,6 +50,7 @@ typedef struct VkPhysicalDeviceCooperativeMatrixDecodeVectorFeaturesNV {
6+
7+
#include <algorithm>
8+
#include <cmath>
9+
+#include <fstream>
10+
#include <iomanip>
11+
#include <iostream>
12+
#include <tuple>
13+
@@ -67,6 +68,7 @@ typedef struct VkPhysicalDeviceCooperativeMatrixDecodeVectorFeaturesNV {
14+
#include <future>
15+
#include <condition_variable>
16+
#include <thread>
17+
+#include <string>
18+
19+
#if defined(_MSC_VER)
20+
# define NOMINMAX 1
21+
@@ -792,6 +794,13 @@ struct vk_device_struct {
22+
bool pipeline_robustness;
23+
bool memory_priority;
24+
vk::Device device;
25+
+ // game.cpp: persistent VkPipelineCache (disk-backed, like the Metal
26+
+ // binary-archive PSO cache). Loaded at device init, passed to every
27+
+ // createComputePipeline, flushed back to disk at teardown.
28+
+ vk::PipelineCache pipeline_cache;
29+
+ std::vector<uint8_t> pipeline_cache_init; // bytes read from disk (may be empty)
30+
+ std::string pipeline_cache_path; // empty = disabled/unset
31+
+ bool pipeline_cache_dirty = false; // a pipeline was created since last save
32+
uint32_t vendor_id;
33+
vk::DriverId driver_id;
34+
vk_device_architecture architecture;
35+
@@ -1122,6 +1131,48 @@ struct vk_device_struct {
36+
compute_queue.reset();
37+
transfer_queue.reset();
38+
39+
+ // game.cpp: flush the persistent pipeline cache back to disk BEFORE
40+
+ // destroying the individual pipelines (destroying a pipeline evicts its
41+
+ // cached PSOs, so vkGetPipelineCacheData would return empty after the
42+
+ // loop). This SDK's vulkan.hpp does not generate the
43+
+ // vkGetPipelineCacheData/vkDestroyPipelineCache hpp methods, so use the
44+
+ // dispatch-table C functions directly.
45+
+ if (static_cast<VkPipelineCache>(pipeline_cache) != VK_NULL_HANDLE) {
46+
+ try {
47+
+ VkDevice dev = static_cast<VkDevice>(device);
48+
+ VkPipelineCache pc = static_cast<VkPipelineCache>(pipeline_cache);
49+
+ size_t sz = 0;
50+
+ // The size query returns VK_INCOMPLETE (and fills *pDataSize);
51+
+ // accept both VK_SUCCESS and VK_INCOMPLETE here.
52+
+ const VkResult q = ::vkGetPipelineCacheData(dev, pc, &sz, nullptr);
53+
+ if (std::getenv("GGML_VK_PIPELINE_CACHE_DEBUG")) {
54+
+ std::cerr << "ggml_vulkan: pipeline cache query q=" << static_cast<int>(q)
55+
+ << " sz=" << sz << std::endl;
56+
+ }
57+
+ if ((q == VK_SUCCESS || q == VK_INCOMPLETE) && sz > 0) {
58+
+ std::vector<uint8_t> data(sz);
59+
+ const VkResult g = ::vkGetPipelineCacheData(dev, pc, &sz, data.data());
60+
+ if ((g == VK_SUCCESS || g == VK_INCOMPLETE) && !pipeline_cache_path.empty()) {
61+
+ data.resize(std::min<size_t>(sz, data.size()));
62+
+ std::ofstream f(pipeline_cache_path, std::ios::binary | std::ios::trunc);
63+
+ if (f) {
64+
+ f.write(reinterpret_cast<const char *>(data.data()),
65+
+ static_cast<std::streamsize>(data.size()));
66+
+ if (std::getenv("GGML_VK_PIPELINE_CACHE_DEBUG")) {
67+
+ std::cerr << "ggml_vulkan: pipeline cache: saved " << data.size()
68+
+ << " bytes to " << pipeline_cache_path << std::endl;
69+
+ }
70+
+ }
71+
+ }
72+
+ }
73+
+ } catch (const vk::SystemError & e) {
74+
+ std::cerr << "ggml_vulkan: pipeline cache save failed: " << e.what() << std::endl;
75+
+ }
76+
+ VkPipelineCache pc = static_cast<VkPipelineCache>(pipeline_cache);
77+
+ ::vkDestroyPipelineCache(static_cast<VkDevice>(device), pc, nullptr);
78+
+ pipeline_cache = vk::PipelineCache{};
79+
+ }
80+
+
81+
for (auto& pipeline : all_pipelines) {
82+
if (pipeline.expired()) {
83+
continue;
84+
@@ -1138,6 +1189,110 @@ struct vk_device_struct {
85+
}
86+
};
87+
88+
+// game.cpp: persistent pipeline cache helpers.
89+
+ // - Path: GGML_VK_PIPELINE_CACHE_PATH or <user-cache>/game_ggml_vk_pipeline.cache
90+
+ // - Disable: GGML_VK_DISABLE_PIPELINE_CACHE
91+
+ // Stale/invalid cache bytes are safe: the driver returns VK_INCOMPLETE and
92+
+ // creates an empty cache, so an outdated/GPU-swapped cache degrades to a
93+
+ // normal cold compile — never a crash.
94+
+ static std::string ggml_vk_pipeline_cache_path() {
95+
+ const char * env = std::getenv("GGML_VK_PIPELINE_CACHE_PATH");
96+
+ if (env && *env) return std::string(env);
97+
+#ifdef _WIN32
98+
+ if (const char * la = std::getenv("LOCALAPPDATA")) {
99+
+ return std::string(la) + "\\game_ggml_vk_pipeline.cache";
100+
+ }
101+
+#else
102+
+ if (const char * home = std::getenv("HOME")) {
103+
+ return std::string(home) + "/.cache/game_ggml_vk_pipeline.cache";
104+
+ }
105+
+#endif
106+
+ return std::string();
107+
+ }
108+
+
109+
+ static void ggml_vk_init_pipeline_cache(vk_device device) {
110+
+ if (std::getenv("GGML_VK_DISABLE_PIPELINE_CACHE")) return;
111+
+ device->pipeline_cache_path = ggml_vk_pipeline_cache_path();
112+
+ if (device->pipeline_cache_path.empty()) return;
113+
+
114+
+ std::ifstream f(device->pipeline_cache_path, std::ios::binary | std::ios::ate);
115+
+ if (f.good()) {
116+
+ const std::streamsize sz = f.tellg();
117+
+ f.seekg(0);
118+
+ device->pipeline_cache_init.resize(static_cast<size_t>(sz));
119+
+ if (sz > 0) {
120+
+ f.read(reinterpret_cast<char *>(device->pipeline_cache_init.data()), sz);
121+
+ }
122+
+ }
123+
+
124+
+ VkPipelineCacheCreateInfo ci{};
125+
+ ci.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
126+
+ if (!device->pipeline_cache_init.empty()) {
127+
+ ci.initialDataSize = device->pipeline_cache_init.size();
128+
+ ci.pInitialData = device->pipeline_cache_init.data();
129+
+ }
130+
+ try {
131+
+ VkPipelineCache raw = VK_NULL_HANDLE;
132+
+ if (::vkCreatePipelineCache(static_cast<VkDevice>(device->device), &ci, nullptr, &raw) == VK_SUCCESS) {
133+
+ device->pipeline_cache = vk::PipelineCache(raw);
134+
+ if (std::getenv("GGML_VK_PIPELINE_CACHE_DEBUG")) {
135+
+ if (device->pipeline_cache_init.empty()) {
136+
+ std::cerr << "ggml_vulkan: pipeline cache: no cached data, will build from scratch" << std::endl;
137+
+ } else {
138+
+ std::cerr << "ggml_vulkan: pipeline cache: loaded " << device->pipeline_cache_init.size()
139+
+ << " bytes from " << device->pipeline_cache_path << std::endl;
140+
+ }
141+
+ }
142+
+ } else {
143+
+ std::cerr << "ggml_vulkan: pipeline cache create failed, disabling" << std::endl;
144+
+ device->pipeline_cache_path.clear();
145+
+ }
146+
+ } catch (const vk::SystemError & e) {
147+
+ std::cerr << "ggml_vulkan: pipeline cache create failed, disabling: "
148+
+ << e.what() << std::endl;
149+
+ device->pipeline_cache = vk::PipelineCache{};
150+
+ device->pipeline_cache_path.clear();
151+
+ }
152+
+ }
153+
+
154+
+ // Persist the pipeline cache to disk if any pipeline was created since the
155+
+ // last save. Called on graph-compute completion (the devices are cached
156+
+ // for the process lifetime, so the destructor is not a usable hook in CLI
157+
+ // flows). The save is throttled by the dirty flag: it only fires after a
158+
+ // graph that actually created new pipelines.
159+
+ static void ggml_vk_save_pipeline_cache_device(vk_device device) {
160+
+ if (!device->pipeline_cache_dirty ||
161+
+ static_cast<VkPipelineCache>(device->pipeline_cache) == VK_NULL_HANDLE ||
162+
+ device->pipeline_cache_path.empty()) {
163+
+ return;
164+
+ }
165+
+ device->pipeline_cache_dirty = false;
166+
+ try {
167+
+ VkDevice dev = static_cast<VkDevice>(device->device);
168+
+ VkPipelineCache pc = static_cast<VkPipelineCache>(device->pipeline_cache);
169+
+ size_t sz = 0;
170+
+ const VkResult q = ::vkGetPipelineCacheData(dev, pc, &sz, nullptr);
171+
+ if ((q == VK_SUCCESS || q == VK_INCOMPLETE) && sz > 0) {
172+
+ std::vector<uint8_t> data(sz);
173+
+ const VkResult g = ::vkGetPipelineCacheData(dev, pc, &sz, data.data());
174+
+ if ((g == VK_SUCCESS || g == VK_INCOMPLETE) && !data.empty()) {
175+
+ data.resize(std::min<size_t>(sz, data.size()));
176+
+ std::ofstream f(device->pipeline_cache_path, std::ios::binary | std::ios::trunc);
177+
+ if (f) {
178+
+ f.write(reinterpret_cast<const char *>(data.data()),
179+
+ static_cast<std::streamsize>(data.size()));
180+
+ if (std::getenv("GGML_VK_PIPELINE_CACHE_DEBUG")) {
181+
+ std::cerr << "ggml_vulkan: pipeline cache: saved " << data.size()
182+
+ << " bytes to " << device->pipeline_cache_path << std::endl;
183+
+ }
184+
+ }
185+
+ }
186+
+ }
187+
+ } catch (const vk::SystemError & e) {
188+
+ std::cerr << "ggml_vulkan: pipeline cache save failed: " << e.what() << std::endl;
189+
+ }
190+
+ }
191+
+
192+
void vk_command_pool::init(vk_device& device, vk_queue *q_) {
193+
cmd_buffers.clear();
194+
q = q_;
195+
@@ -3025,7 +3180,8 @@ static void ggml_vk_create_pipeline_func(vk_device& device, vk_pipeline& pipelin
196+
#endif
197+
198+
try {
199+
- pipeline->pipeline = device->device.createComputePipeline(VK_NULL_HANDLE, compute_pipeline_create_info).value;
200+
+ pipeline->pipeline = device->device.createComputePipeline(device->pipeline_cache, compute_pipeline_create_info).value;
201+
+ device->pipeline_cache_dirty = true;
202+
} catch (const vk::SystemError& e) {
203+
std::cerr << "ggml_vulkan: Compute pipeline creation failed for " << pipeline->name << std::endl;
204+
std::cerr << "ggml_vulkan: " << e.what() << std::endl;
205+
@@ -6910,6 +7066,8 @@ static vk_device ggml_vk_get_device(size_t idx) {
206+
.setPEnabledExtensionNames(device_extensions);
207+
device_create_info.setPNext(&device_features2);
208+
device->device = device->physical_device.createDevice(device_create_info);
209+
+ // game.cpp: load/create a disk-backed VkPipelineCache (cold-start fix).
210+
+ ggml_vk_init_pipeline_cache(device);
211+
212+
if (device->device_fault) {
213+
device->pfn_vkGetDeviceFaultInfoEXT = (PFN_vkGetDeviceFaultInfoEXT)
214+
@@ -17372,6 +17530,11 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
215+
ggml_vk_synchronize(ctx);
216+
}
217+
218+
+ // game.cpp: persist any newly-created pipelines back to the on-disk
219+
+ // VkPipelineCache (dirty-flag throttled; no-op unless a pipeline was
220+
+ // created since the last save).
221+
+ ggml_vk_save_pipeline_cache_device(ctx->device);
222+
+
223+
return GGML_STATUS_SUCCESS;
224+
225+
UNUSED(backend);

0 commit comments

Comments
 (0)