|
| 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