| … | … | @@ -22,6 +22,11 @@ |
| 22 | 22 | |
| 23 | 23 | #include <iostream> |
| 24 | 24 | #include <fstream> |
| 25 | #include <sstream> |
| 26 | #include <iomanip> |
| 27 | #include <sys/stat.h> |
| 28 | #include <cstdlib> |
| 29 | #include <functional> |
| 25 | 30 | |
| 26 | 31 | #include <opencog/util/exceptions.h> |
| 27 | 32 | #include <opencog/util/Logger.h> |
| … | … | @@ -139,6 +144,154 @@ void OpenclNode::find_device(void) |
| 139 | 144 | get_name().c_str()); |
| 140 | 145 | } |
| 141 | 146 | |
| 147 | // ============================================================== |
| 148 | // Binary caching implementation. |
| 149 | // Caches compiled OpenCL programs to disk to avoid expensive JIT |
| 150 | // compilation on subsequent runs. Cache files are stored in |
| 151 | // ~/.cache/opencog/opencl/<device_hash>/<source_hash>.bin |
| 152 | // |
| 153 | // This follows the pattern used by hashcat, PyOpenCL, and game engines |
| 154 | // to dramatically reduce startup time (from seconds to milliseconds). |
| 155 | |
| 156 | /// Compute a simple hash of a string using std::hash. |
| 157 | /// Returns a hex string representation. |
| 158 | std::string OpenclNode::compute_hash(const std::string& data) const |
| 159 | { |
| 160 | std::hash<std::string> hasher; |
| 161 | size_t hash = hasher(data); |
| 162 | |
| 163 | std::ostringstream oss; |
| 164 | oss << std::hex << std::setfill('0') << std::setw(16) << hash; |
| 165 | return oss.str(); |
| 166 | } |
| 167 | |
| 168 | /// Get the cache directory path. |
| 169 | /// Creates ~/.cache/opencog/opencl/<device_hash>/ if it doesn't exist. |
| 170 | std::string OpenclNode::get_cache_dir(void) const |
| 171 | { |
| 172 | // Get home directory |
| 173 | const char* home = std::getenv("HOME"); |
| 174 | if (nullptr == home) home = "/tmp"; |
| 175 | |
| 176 | // Build device identifier for cache directory |
| 177 | std::string device_id = _platform.getInfo<CL_PLATFORM_NAME>() + "_" + |
| 178 | _device.getInfo<CL_DEVICE_NAME>() + "_" + |
| 179 | _device.getInfo<CL_DRIVER_VERSION>(); |
| 180 | std::string device_hash = compute_hash(device_id); |
| 181 | |
| 182 | std::string cache_dir = std::string(home) + "/.cache/opencog/opencl/" + device_hash; |
| 183 | |
| 184 | // Create directories if they don't exist (mkdir -p equivalent) |
| 185 | std::string path = std::string(home) + "/.cache"; |
| 186 | mkdir(path.c_str(), 0755); |
| 187 | path += "/opencog"; |
| 188 | mkdir(path.c_str(), 0755); |
| 189 | path += "/opencl"; |
| 190 | mkdir(path.c_str(), 0755); |
| 191 | path += "/" + device_hash; |
| 192 | mkdir(path.c_str(), 0755); |
| 193 | |
| 194 | return cache_dir; |
| 195 | } |
| 196 | |
| 197 | /// Get the full cache file path for a given source. |
| 198 | std::string OpenclNode::get_cache_path(const std::string& src) const |
| 199 | { |
| 200 | std::string source_hash = compute_hash(src); |
| 201 | return get_cache_dir() + "/" + source_hash + ".bin"; |
| 202 | } |
| 203 | |
| 204 | /// Try to load a cached binary. Returns true if successful. |
| 205 | bool OpenclNode::load_cached_binary(const std::string& cache_path) |
| 206 | { |
| 207 | std::ifstream binfile(cache_path, std::ios::binary); |
| 208 | if (!binfile.is_open()) |
| 209 | return false; |
| 210 | |
| 211 | // Read the entire binary file |
| 212 | std::vector<char> binary((std::istreambuf_iterator<char>(binfile)), |
| 213 | std::istreambuf_iterator<char>()); |
| 214 | binfile.close(); |
| 215 | |
| 216 | if (binary.empty()) |
| 217 | return false; |
| 218 | |
| 219 | try |
| 220 | { |
| 221 | // Create program from binary |
| 222 | cl::Program::Binaries binaries; |
| 223 | binaries.push_back(std::vector<unsigned char>(binary.begin(), binary.end())); |
| 224 | |
| 225 | std::vector<cl::Device> devices = {_device}; |
| 226 | std::vector<cl_int> binary_status; |
| 227 | |
| 228 | _program = cl::Program(_context, devices, binaries, &binary_status); |
| 229 | |
| 230 | // Check if binary was valid for this device |
| 231 | if (binary_status[0] != CL_SUCCESS) |
| 232 | { |
| 233 | logger().info("OpenclNode: Cached binary invalid for device, will recompile\n"); |
| 234 | return false; |
| 235 | } |
| 236 | |
| 237 | // Build the program (links the binary, much faster than JIT compile) |
| 238 | _program.build(""); |
| 239 | |
| 240 | logger().info("OpenclNode: Loaded cached binary from %s\n", cache_path.c_str()); |
| 241 | return true; |
| 242 | } |
| 243 | catch (const cl::Error& e) |
| 244 | { |
| 245 | logger().info("OpenclNode: Failed to load cached binary: %s\n", e.what()); |
| 246 | return false; |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | /// Save the compiled program binary to cache. |
| 251 | void OpenclNode::save_binary_to_cache(const std::string& cache_path) |
| 252 | { |
| 253 | try |
| 254 | { |
| 255 | // Get the binary sizes |
| 256 | std::vector<size_t> binary_sizes = _program.getInfo<CL_PROGRAM_BINARY_SIZES>(); |
| 257 | if (binary_sizes.empty() || binary_sizes[0] == 0) |
| 258 | { |
| 259 | logger().info("OpenclNode: No binary available to cache\n"); |
| 260 | return; |
| 261 | } |
| 262 | |
| 263 | // Get the binaries |
| 264 | std::vector<std::vector<unsigned char>> binaries; |
| 265 | binaries.resize(binary_sizes.size()); |
| 266 | for (size_t i = 0; i < binary_sizes.size(); i++) |
| 267 | binaries[i].resize(binary_sizes[i]); |
| 268 | |
| 269 | // Use raw pointers for the API |
| 270 | std::vector<unsigned char*> binary_ptrs; |
| 271 | for (auto& b : binaries) |
| 272 | binary_ptrs.push_back(b.data()); |
| 273 | |
| 274 | clGetProgramInfo(_program(), CL_PROGRAM_BINARIES, |
| 275 | binary_ptrs.size() * sizeof(unsigned char*), |
| 276 | binary_ptrs.data(), nullptr); |
| 277 | |
| 278 | // Write to cache file |
| 279 | std::ofstream binfile(cache_path, std::ios::binary); |
| 280 | if (binfile.is_open()) |
| 281 | { |
| 282 | binfile.write(reinterpret_cast<const char*>(binaries[0].data()), |
| 283 | binaries[0].size()); |
| 284 | binfile.close(); |
| 285 | logger().info("OpenclNode: Saved binary to cache: %s (%zu bytes)\n", |
| 286 | cache_path.c_str(), binaries[0].size()); |
| 287 | } |
| 288 | } |
| 289 | catch (const cl::Error& e) |
| 290 | { |
| 291 | logger().info("OpenclNode: Failed to save binary to cache: %s\n", e.what()); |
| 292 | } |
| 293 | } |
| 294 | |
| 142 | 295 | // ============================================================== |
| 143 | 296 | |
| 144 | 297 | void OpenclNode::build_program(void) |
| … | … | @@ -153,28 +306,44 @@ void OpenclNode::build_program(void) |
| 153 | 306 | "Unable to find source file in URL \"%s\"\n", |
| 154 | 307 | get_name().c_str()); |
| 155 | 308 | |
| 156 | | cl::Program::Sources sources; |
| 157 | | sources.push_back(src); |
| 158 | | |
| 159 | | _program = cl::Program(_context, sources); |
| 309 | // Try to load from binary cache first. |
| 310 | // This can reduce startup time from seconds to milliseconds. |
| 311 | std::string cache_path = get_cache_path(src); |
| 312 | bool loaded_from_cache = load_cached_binary(cache_path); |
| 160 | 313 | |
| 161 | | // Compile |
| 162 | | try |
| 163 | | { |
| 164 | | // Specifying flags causes exception. |
| 165 | | // program.build("-cl-std=CL1.2"); |
| 166 | | _program.build(""); |
| 167 | | } |
| 168 | | catch (const cl::Error& e) |
| 314 | if (not loaded_from_cache) |
| 169 | 315 | { |
| 170 | | logger().info("OpenclNode failed compile >>%s<<\n", |
| 171 | | _program.getBuildInfo<CL_PROGRAM_BUILD_LOG>(_device).c_str()); |
| 172 | | throw RuntimeException(TRACE_INFO, |
| 173 | | "Unable to compile source file in URL \"%s\"\n", |
| 174 | | get_name().c_str()); |
| 316 | // No cache hit - compile from source |
| 317 | logger().info("OpenclNode: Compiling kernel from source (this may take a while)...\n"); |
| 318 | |
| 319 | cl::Program::Sources sources; |
| 320 | sources.push_back(src); |
| 321 | |
| 322 | _program = cl::Program(_context, sources); |
| 323 | |
| 324 | // Compile |
| 325 | try |
| 326 | { |
| 327 | // Specifying flags causes exception. |
| 328 | // program.build("-cl-std=CL1.2"); |
| 329 | _program.build(""); |
| 330 | } |
| 331 | catch (const cl::Error& e) |
| 332 | { |
| 333 | logger().info("OpenclNode failed compile >>%s<<\n", |
| 334 | _program.getBuildInfo<CL_PROGRAM_BUILD_LOG>(_device).c_str()); |
| 335 | throw RuntimeException(TRACE_INFO, |
| 336 | "Unable to compile source file in URL \"%s\"\n", |
| 337 | get_name().c_str()); |
| 338 | } |
| 339 | |
| 340 | // Save to cache for next time |
| 341 | save_binary_to_cache(cache_path); |
| 175 | 342 | } |
| 176 | 343 | |
| 177 | 344 | // Build the interface definitions for the kernels. |
| 345 | // This must be done regardless of cache hit, as it creates Atomese |
| 346 | // for the kernel signatures. |
| 178 | 347 | GenIDL gidl; |
| 179 | 348 | HandleSeq ifcs = gidl.gen_idl(src); |
| 180 | 349 | HandleSeq asif; |