在 Fedora 上以 CUDA 建置 llama.cpp
參考先前寫的 在 Windows 上以 CUDA 建置 llama.cpp 的文章,這次在 Fedora 43 環境嘗試相同的做法。只有工具鏈不同,流程幾乎一樣。
參考先前寫的 在 Windows 上以 CUDA 建置 llama.cpp 的文章,這次在 Fedora 43 環境嘗試相同的做法。只有工具鏈不同,流程幾乎一樣。
這是一份在 Windows 11 環境下,將本機 LLM 推理引擎的代表作 llama.cpp 從原始碼編譯、啟用 NVIDIA GPU 的 CUDA 後端,並實際執行模型的紀錄。
這篇備忘錄記錄了 CUDA Grid 和 Block。
在 CUDA 中,並行執行的程式碼稱為核函數 (Kernel)。當你啟動一個核函數時,你必須指定執行該核函數的線程數量以及它們如何組織。這就是 Grid (網格) 和 Block (塊) 的概念。
CUDA 的線程模型是一個分層結構:
這種層次結構允許你將問題分解為獨立的子任務,並將這些子任務映射到 Grid 和 Block 的維度上。
在啟動核函數時,你使用 <<<Dg, Db>>> 語法來指定 Grid 和 Block 的維度:
kernel_function<<<Dg, Db>>>(arguments);
Dg (Dimension Grid):指定 Grid 的維度。這是一個 dim3 類型的變量,可以是一維 (Dg.x)、二維 (Dg.x, Dg.y) 或三維 (Dg.x, Dg.y, Dg.z)。Db (Dimension Block):指定每個 Block 的維度。這也是一個 dim3 類型的變量,可以是一維 (Db.x)、二維 (Db.y) 或三維 (Db.z)。dim3 結構體dim3 是一個結構體,通常用於表示 Grid 或 Block 的大小。
struct dim3
{
unsigned int x, y, z;
};
如果你只指定一個值,它會被賦給 .x,其他為 1。例如 dim3(10) 等同於 dim3(10, 1, 1)。
在核函數內部,每個線程都可以通過幾個內置變量來識別自己的唯一索引:
blockIdx.x, blockIdx.y, blockIdx.z:當前線程所在的 Block 在 Grid 中的索引。blockDim.x, blockDim.y, blockDim.z:每個 Block 的維度(線程數)。threadIdx.x, threadIdx.y, threadIdx.z:當前線程在 Block 中的索引。gridDim.x, gridDim.y, gridDim.z:Grid 的維度(Block 數)。通過這些變量,可以計算出每個線程在整個 Grid 中的全局唯一索引。
對於一維 Grid 和 Block,全局索引的計算方式為:
int global_index = blockIdx.x * blockDim.x + threadIdx.x;
對於二維 Grid 和 Block,全局索引的計算方式為:
int x_index = blockIdx.x * blockDim.x + threadIdx.x;
int y_index = blockIdx.y * blockDim.y + threadIdx.y;
// 如果將二維數據存儲在一維數組中,則需要將 (x_index, y_index) 轉換為一維索引
// 假設矩陣寬度為 width
int global_index = y_index * width + x_index;
這是一個簡單的向量加法核函數,演示了 Grid 和 Block 的使用。
#include <iostream>
#include <vector>
#include <cuda_runtime.h>
// 核函數:在設備上執行向量加法
__global__ void add_vectors(int* a, int* b, int* c, int N) {
// 計算當前線程在 Grid 中的全局索引
int idx = blockIdx.x * blockDim.x + threadIdx.x;
// 確保索引在有效範圍內
if (idx < N) {
c[idx] = a[idx] + b[idx];
}
}
int main() {
int N = 100000; // 向量大小
size_t size = N * sizeof(int);
// 主機 (CPU) 數據
std::vector<int> h_a(N), h_b(N), h_c(N);
// 初始化主機數據
for (int i = 0; i < N; ++i) {
h_a[i] = i;
h_b[i] = i * 2;
}
// 設備 (GPU) 數據指針
int *d_a, *d_b, *d_c;
// 在設備上分配內存
cudaMalloc((void**)&d_a, size);
cudaMalloc((void**)&d_b, size);
cudaMalloc((void**)&d_c, size);
// 將數據從主機複製到設備
cudaMemcpy(d_a, h_a.data(), size, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b.data(), size, cudaMemcpyHostToDevice);
// 配置 Grid 和 Block 維度
int threadsPerBlock = 256;
int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock; // 確保覆蓋所有元素
// 啟動核函數
add_vectors<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, N);
// 將結果從設備複製回主機
cudaMemcpy(h_c.data(), d_c, size, cudaMemcpyDeviceToHost);
// 驗證結果 (可選)
std::cout << "h_c[0] = " << h_c[0] << std::endl; // 0 + 0 = 0
std::cout << "h_c[1] = " << h_c[1] << std::endl; // 1 + 2 = 3
std::cout << "h_c[N-1] = " << h_c[N-1] << std::endl; // (N-1) + 2*(N-1) = 3*(N-1)
// 釋放設備內存
cudaFree(d_a);
cudaFree(d_b);
cudaFree(d_c);
return 0;
}
理解 CUDA 的 Grid 和 Block 概念對於有效利用 GPU 的並行計算能力至關重要。通過合理地組織線程,你可以將複雜的計算任務分解為數百萬個小任務,並在 GPU 上高效地執行它們。
這篇備忘錄記錄了 cuBLAS。
cuBLAS 是 NVIDIA 提供的一個 GPU 加速的 BLAS(基本線性代數子程序庫)實現。它提供了高性能的標準線性代數運算,如向量-向量運算、矩陣-向量運算和矩陣-矩陣運算。
cuBLAS 庫是基於 CUDA 構建的,允許開發者利用 NVIDIA GPU 的強大並行計算能力來加速線性代數的計算。
使用 cuBLAS 通常涉及以下步驟:
矩陣乘法 (GEMM: General Matrix-Matrix Multiplication) 是 cuBLAS 最常用的功能之一。
假設我們有兩個矩陣 $A$ 和 $B$,我們想計算它們的乘積 $C = \alpha AB + \beta C$。
#include <iostream>
#include <vector>
#include <cuda_runtime.h>
#include <cublas_v2.h>
// 錯誤檢查宏
#define CUDA_CHECK(call)
do {
cudaError_t err = call;
if (err != cudaSuccess) {
fprintf(stderr, "CUDA error in %s (%s:%d): %s
", __func__, __FILE__, __LINE__, cudaGetErrorString(err));
exit(EXIT_FAILURE);
}
} while (0)
#define CUBLAS_CHECK(call)
do {
cublasStatus_t status = call;
if (status != CUBLAS_STATUS_SUCCESS) {
fprintf(stderr, "cuBLAS error in %s (%s:%d): status %d
", __func__, __FILE__, __LINE__, status);
exit(EXIT_FAILURE);
}
} while (0)
int main() {
int N = 3; // 矩陣維度 (為簡化起見,假設方陣)
// 主機數據
std::vector<float> h_A(N * N);
std::vector<float> h_B(N * N);
std::vector<float> h_C(N * N);
// 初始化 A 和 B
for (int i = 0; i < N * N; ++i) {
h_A[i] = static_cast<float>(i + 1);
h_B[i] = static_cast<float>(N * N - i);
h_C[i] = 0.0f; // 初始化 C 為零
}
// 設備數據指針
float *d_A, *d_B, *d_C;
// 1. 初始化 cuBLAS 句柄
cublasHandle_t handle;
CUBLAS_CHECK(cublasCreate(&handle));
// 2. 在 GPU 上分配內存
CUDA_CHECK(cudaMalloc((void**)&d_A, N * N * sizeof(float)));
CUDA_CHECK(cudaMalloc((void**)&d_B, N * N * sizeof(float)));
CUDA_CHECK(cudaMalloc((void**)&d_C, N * N * sizeof(float)));
// 3. 將數據從主機複製到設備
CUBLAS_CHECK(cublasSetMatrix(N, N, sizeof(float), h_A.data(), N, d_A, N));
CUBLAS_CHECK(cublasSetMatrix(N, N, sizeof(float), h_B.data(), N, d_B, N));
CUBLAS_CHECK(cublasSetMatrix(N, N, sizeof(float), h_C.data(), N, d_C, N)); // 複製初始 C (這裡都是零)
// 設置標量 alpha 和 beta
const float alpha = 1.0f;
const float beta = 0.0f;
// 4. 執行 cuBLAS 運算 (C = A * B)
// 注意:cuBLAS 使用列優先存儲,所以這裡我們調用 cublasSgemm (S 代表 float, ge 表示通用矩陣, mm 表示矩陣乘法)
// 參數順序為:handle, transa, transb, m, n, k, alpha, A, lda, B, ldb, beta, C, ldc
// 對於 C = A * B,如果 A 是 M x K,B 是 K x N,則 C 是 M x N
// 在這裡,M=N, K=N, N=N
// transa, transb 分別表示是否轉置 A 和 B
// lda, ldb, ldc 分別是 A, B, C 的 leading dimension (通常是行數或列數,取決於存儲順序)
CUBLAS_CHECK(cublasSgemm(handle,
CUBLAS_OP_N, // A 不轉置
CUBLAS_OP_N, // B 不轉置
N, // m (C 的行數)
N, // n (C 的列數)
N, // k (A 的列數 / B 的行數)
&alpha, // alpha
d_A, // A 矩陣在設備上的指針
N, // A 的 leading dimension
d_B, // B 矩陣在設備上的指針
N, // B 的 leading dimension
&beta, // beta
d_C, // C 矩陣在設備上的指針
N)); // C 的 leading dimension
// 5. 將數據從設備複製回主機
CUBLAS_CHECK(cublasGetMatrix(N, N, sizeof(float), d_C, N, h_C.data(), N));
// 打印結果 (可選)
std::cout << "Result C matrix:" << std::endl;
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
std::cout << h_C[i * N + j] << " ";
}
std::cout << std::endl;
}
// 6. 釋放 GPU 內存
CUDA_CHECK(cudaFree(d_A));
CUDA_CHECK(cudaFree(d_B));
CUDA_CHECK(cudaFree(d_C));
// 7. 銷毀 cuBLAS 句柄
CUBLAS_CHECK(cublasDestroy(handle));
return 0;
}
nvcc -lcublas your_program.cu -o your_program
./your_program
cuBLAS 提供了一個高效且易於使用的接口,用於在 NVIDIA GPU 上執行 BLAS 運算。對於需要進行大量線性代數計算的應用程式(如深度學習、科學計算),使用 cuBLAS 可以顯著提高性能。