第 8 章 · 01 backendloader 动态后端加载 本节摘要:本节深潜 ——Colibrì 让 CPU + CUDA + Metal + Vulkan 共享一个运行时的关键装置:运行时按需加载 GPU 后端的动态库( / / ),主机二进制从不静态链接 cudart/Metal/Vulkan。三个后端各自有编译开关( / / ),无 GPU 也能纯 CPU fallback。本节贴 头部注释精读,讲清为什么动态加载、统一后端接口如何构造、ABI 如何跨工具链安全。 内容来源:原项目源码 (本节聚焦前 100 行 + 设计原理) ⚠️ 注意: 在 Linux/macOS 不参与编译——Linux 上 直接链接进主机二进制;动态加载的复杂度只在 Windows。
本节摘要:本节深潜
c/backend_loader.c——Colibrì 让 CPU + CUDA + Metal + Vulkan 共享一个运行时的关键装置:运行时按需加载 GPU 后端的动态库(.dll/.so/.dylib),主机二进制从不静态链接 cudart/Metal/Vulkan。三个后端各自有编译开关(CUDA=1/METAL=1/VULKAN=1),无 GPU 也能纯 CPU fallback。本节贴backend_loader.c头部注释精读,讲清为什么动态加载、统一后端接口如何构造、ABI 如何跨工具链安全。
内容来源:原项目源码
c/backend_loader.c(本节聚焦前 100 行 + 设计原理)
⚠️ 注意:
backend_loader.c在 Linux/macOS 不参与编译——Linux 上backend_cuda.o直接链接进主机二进制;动态加载的复杂度只在 Windows。这是平台差异,不是设计缺陷。
.o。COLI_BACKEND_DLL、COLI_VENDOR_TAG、COLI_HIP_DLL 三个宏的取舍。g_cuda 的填充与使用。直接读 backend_loader.c 的开场注释,这是全章最浓缩的设计文档:
1 /* backend_loader.c — Windows runtime loader for the GPU backend DLL. 2 * 3 * Why this exists: the engine is built with MinGW-w64 (gcc), but CUDA kernels 4 * must be compiled with MSVC + nvcc. We cannot link a CUDA .o into a gcc binary 5 * reliably across the MSVC/MinGW ABI, and nvcc requires cl.exe as its host 6 * compiler. The clean cross-toolchain split is: build the CUDA backend into a 7 * standalone coli_cuda.dll with nvcc+MSVC, then load it here at runtime via 8 * LoadLibrary/GetProcAddress. The host (glm.exe) never links cudart directly. 9 * 10 * On Linux this file is not compiled (the Makefile links backend_cuda.o 11 * directly). On Windows, when COLI_CUDA is defined, glm.c calls the 12 * coli_cuda_* wrappers below, which forward through function pointers resolved 13 * from the DLL at first use. If the DLL is absent, every call safely returns 14 * the "not initialized" sentinel (0 / no-op) and the engine falls back to CPU. 15 * 16 * ABI note: ColiCudaTensor* is opaque to the host (it stores the pointer, 17 * never dereferences it), so the MSVC-allocated struct is safe to pass across 18 * the boundary as an opaque handle. All scalar types (int, size_t, pointers) 19 * agree between MSVC and MinGW-w64 on x86-64. 20 */
要点拆解:
.o 链进 gcc 二进制,跨 MSVC/MinGW ABI 不可靠;coli_cuda.dll,主机用 LoadLibrary/GetProcAddress 运行时加载。主机 glm.exe(现在叫 colibri)从不直接链 cudart。💡 深潜要点:这是一段非常工程化的取舍。Colibrì 选择"主机零 CUDA 依赖",换来两个好处:第一,主机编译只需 gcc,不要求用户机器上装 MSVC;第二,用户没装 NVIDIA driver / CUDA DLL 时,主机照常启动,只是 GPU 调用全部走 sentinel。
第 33-50 行用一组宏处理"同一个加载器既支持 CUDA 也支持 HIP(AMD ROCm)"的情况:
33 /* Which backend DLL this host looks for, and how it labels its own messages. 34 * The Makefile defines COLI_HIP_DLL for a HIP_DLL=1 host and leaves it undefined 35 * for CUDA_DLL=1; the two builds are mutually exclusive there, so exactly one 36 * arm is live. ... 43 */ 44 #ifdef COLI_HIP_DLL 45 #define COLI_BACKEND_DLL "coli_hip.dll" 46 #define COLI_BACKEND_DLL_W L"coli_hip.dll" /* the HIP host loads it wide */ 47 #define COLI_VENDOR_TAG "[HIP]" 48 #else 49 #define COLI_BACKEND_DLL "coli_cuda.dll" 50 #define COLI_VENDOR_TAG "[CUDA]" 51 #endif
要点:
COLI_HIP_DLL 和 CUDA_DLL 在 Makefile 互斥,只能定义一个;COLI_BACKEND_DLL/COLI_VENDOR_TAG 这两个常量,确保不会"换 vendor 后某处忘了改";coli_cuda_——CUDA 和 HIP 共用一份 backend_cuda.cu、一份 ABI(见 GPU_BACKENDS.md)。只有容器文件名和诊断 label 不同。这套设计让"AMD 显卡支持"几乎免费:加载器逻辑一份,只是 DLL 文件名和标签替换。
g_cuda加载器内部为每个导出符号定义一个 typedef,然后把所有指针收进一个静态结构体 g_cuda。我们看一小段代表性 typedef:
53 /* Function-pointer typedefs matching each exported symbol. */ 54 typedef int (*fn_init)(const int *devices, int count); 55 typedef void (*fn_shutdown)(void); 56 typedef int (*fn_device_count)(void); 57 typedef int (*fn_device_at)(int index); 58 typedef int (*fn_mem_info)(int device, size_t *free_bytes, size_t *total_bytes); 59 typedef int (*fn_device_integrated)(int device); 60 typedef void (*fn_stats)(int device, size_t *tensor_count, size_t *tensor_bytes);
这是一组典型的"后端生命周期 + 设备查询"接口:init / shutdown / device_count / device_at / mem_info / device_integrated / stats。再往下还有大量 expert_mlp、expert_group、attention_absorb、tensor_upload、matmul、pipe_* 等运算接口(本节不展开,见第 8 章 02 节)。
所有指针收进 g_cuda,带两个标志位:
127 /* Resolved pointers, plus a flag so we attempt the load at most once. */ 128 static struct { 129 int loaded; /* 1 = load attempted (success or fail), 0 = not yet */ 130 int available; /* 1 = DLL loaded and all symbols resolved */ 131 HMODULE dll; ... 191 } g_cuda;
loaded 保证最多加载一次(避免反复 LoadLibrary);available 表示"DLL 加载且所有符号解析成功"。两个标志分离是为了区分"试过但失败"与"还没试"——前者直接走 fallback,后者才需要触发加载。
第 16-19 行的 ABI note 是这套设计能成立的最后一块拼图:
ColiCudaTensor* 对主机是opaque——主机只存指针、绝不解引用;后果:
ColiCudaTensor 的内部布局,也就无所谓 ABI 差异;这是 Colibrì 把"两个工具链"问题降维成"一份函数指针表 + opaque handle"的关键工程。
第 12-14 行写明 fallback 行为:
If the DLL is absent, every call safely returns the "not initialized" sentinel (0 / no-op) and the engine falls back to CPU.
操作上,所有 coli_cuda_* 包装函数在 g_cuda.available=0 时直接返回 0 或 no-op。主机调用方拿到"未初始化"哨兵值,知道这次 GPU 操作没执行,转而走 CPU 路径重算。这保证:
coli_cuda.dll 改名隐藏起来,强制纯 CPU 路径,做对比实验。⚠️ 注意:
backend_gpu_compat.h是配套的兼容层头文件,让上层引擎代码可以无差别地写"if (gpu 可用) 走 GPU else 走 CPU"——细节被这层 + 加载器一起吞掉。这一层的存在是"统一后端接口"在源代码层的落地。
.o——所以 CUDA 后端编成 coli_cuda.dll,主机运行时 LoadLibrary 加载。backend_loader.c,backend_cuda.o 直接链入;动态加载只在 Windows。COLI_HIP_DLL/CUDA_DLL 互斥,CUDA 和 HIP 共用 coli_cuda_ 符号前缀和同一份 backend_cuda.cu,只是容器文件名和 label 不同。g_cuda(loaded/available 双标志 + 所有导出符号指针);opaque ColiCudaTensor* 让跨工具链 ABI 安全。下一节我们看三后端本体——
backend_cuda.cu、backend_metal.mm、backend_vulkan.c——以及它们共享的 GPU shaders(.comp文件)。